From bd605e330cccd3e855e6f943aca25ed3d2a6ad5a Mon Sep 17 00:00:00 2001 From: James Phillips Date: Tue, 17 Jan 2017 22:20:11 -0800 Subject: [PATCH] Adds basic support for node IDs. --- api/agent_test.go | 2 +- command/agent/agent.go | 71 ++++++++++++++++++- command/agent/agent_test.go | 58 +++++++++++++++ command/agent/command.go | 2 + command/agent/config.go | 8 +++ command/agent/config_test.go | 8 ++- consul/client.go | 4 +- consul/config.go | 4 ++ consul/server.go | 6 +- consul/util.go | 10 --- lib/path.go | 14 ++++ types/node_id.go | 4 ++ .../docs/agent/http/agent.html.markdown | 2 + .../source/docs/agent/options.html.markdown | 12 ++++ 14 files changed, 189 insertions(+), 16 deletions(-) create mode 100644 lib/path.go create mode 100644 types/node_id.go diff --git a/api/agent_test.go b/api/agent_test.go index cd48d57087..984d927387 100644 --- a/api/agent_test.go +++ b/api/agent_test.go @@ -657,7 +657,7 @@ func TestAgent_Monitor(t *testing.T) { // Wait for the first log message and validate it select { case log := <-logCh: - if !strings.Contains(log, "[INFO] raft: Initial configuration") { + if !strings.Contains(log, "[INFO]") { t.Fatalf("bad: %q", log) } case <-time.After(10 * time.Second): diff --git a/command/agent/agent.go b/command/agent/agent.go index 850733a772..b29d05a90b 100644 --- a/command/agent/agent.go +++ b/command/agent/agent.go @@ -224,7 +224,6 @@ func Create(config *Config, logOutput io.Writer, logWriter *logger.LogWriter, shutdownCh: make(chan struct{}), endpoints: make(map[string]string), } - if err := agent.resolveTmplAddrs(); err != nil { return nil, err } @@ -236,6 +235,12 @@ func Create(config *Config, logOutput io.Writer, logWriter *logger.LogWriter, } agent.acls = acls + // Retrieve or generate the node ID before setting up the rest of the + // agent, which depends on it. + if err := agent.setupNodeID(config); err != nil { + return nil, fmt.Errorf("Failed to setup node ID: %v", err) + } + // Initialize the local state. agent.state.Init(config, agent.logger) @@ -303,6 +308,9 @@ func (a *Agent) consulConfig() *consul.Config { base = consul.DefaultConfig() } + // This is set when the agent starts up + base.NodeID = a.config.NodeID + // Apply dev mode base.DevMode = a.config.DevMode @@ -600,6 +608,67 @@ func (a *Agent) setupClient() error { return nil } +// setupNodeID will pull the persisted node ID, if any, or create a random one +// and persist it. +func (a *Agent) setupNodeID(config *Config) error { + // If they've configured a node ID manually then just use that, as + // long as it's valid. + if config.NodeID != "" { + if _, err := uuid.ParseUUID(string(config.NodeID)); err != nil { + return err + } + + return nil + } + + // For dev mode we have no filesystem access so just make a GUID. + if a.config.DevMode { + id, err := uuid.GenerateUUID() + if err != nil { + return err + } + + config.NodeID = types.NodeID(id) + a.logger.Printf("[INFO] agent: Generated unique node ID %q for this agent (will not be persisted in dev mode)", config.NodeID) + return nil + } + + // Load saved state, if any. Since a user could edit this, we also + // validate it. + fileID := filepath.Join(config.DataDir, "node-id") + if _, err := os.Stat(fileID); err == nil { + rawID, err := ioutil.ReadFile(fileID) + if err != nil { + return err + } + + nodeID := strings.TrimSpace(string(rawID)) + if _, err := uuid.ParseUUID(nodeID); err != nil { + return err + } + + config.NodeID = types.NodeID(nodeID) + } + + // If we still don't have a valid node ID, make one. + if config.NodeID == "" { + id, err := uuid.GenerateUUID() + if err != nil { + return err + } + if err := lib.EnsurePath(fileID, false); err != nil { + return err + } + if err := ioutil.WriteFile(fileID, []byte(id), 0600); err != nil { + return err + } + + config.NodeID = types.NodeID(id) + a.logger.Printf("[INFO] agent: Generated unique node ID %q for this agent (persisted)", config.NodeID) + } + return nil +} + // setupKeyrings is used to initialize and load keyrings during agent startup func (a *Agent) setupKeyrings(config *consul.Config) error { fileLAN := filepath.Join(a.config.DataDir, serfLANKeyring) diff --git a/command/agent/agent_test.go b/command/agent/agent_test.go index 4eedaba0b5..2e2b38bdf8 100644 --- a/command/agent/agent_test.go +++ b/command/agent/agent_test.go @@ -18,6 +18,8 @@ import ( "github.com/hashicorp/consul/consul/structs" "github.com/hashicorp/consul/logger" "github.com/hashicorp/consul/testutil" + "github.com/hashicorp/consul/types" + "github.com/hashicorp/go-uuid" "github.com/hashicorp/raft" "strings" ) @@ -308,6 +310,62 @@ func TestAgent_ReconnectConfigSettings(t *testing.T) { }() } +func TestAgent_NodeID(t *testing.T) { + c := nextConfig() + dir, agent := makeAgent(t, c) + defer os.RemoveAll(dir) + defer agent.Shutdown() + + // The auto-assigned ID should be valid. + id := agent.consulConfig().NodeID + if _, err := uuid.ParseUUID(string(id)); err != nil { + t.Fatalf("err: %v", err) + } + + // Set an invalid ID via config. + c.NodeID = types.NodeID("nope") + err := agent.setupNodeID(c) + if err == nil || !strings.Contains(err.Error(), "uuid string is wrong length") { + t.Fatalf("err: %v", err) + } + + // Set a valid ID via config. + newID, err := uuid.GenerateUUID() + if err != nil { + t.Fatalf("err: %v", err) + } + c.NodeID = types.NodeID(newID) + if err := agent.setupNodeID(c); err != nil { + t.Fatalf("err: %v", err) + } + if id := agent.consulConfig().NodeID; string(id) != newID { + t.Fatalf("bad: %q vs. %q", id, newID) + } + + // Set an invalid ID via the file. + fileID := filepath.Join(c.DataDir, "node-id") + if err := ioutil.WriteFile(fileID, []byte("adf4238a!882b!9ddc!4a9d!5b6758e4159e"), 0600); err != nil { + t.Fatalf("err: %v", err) + } + c.NodeID = "" + err = agent.setupNodeID(c) + if err == nil || !strings.Contains(err.Error(), "uuid is improperly formatted") { + t.Fatalf("err: %v", err) + } + + // Set a valid ID via the file. + if err := ioutil.WriteFile(fileID, []byte("adf4238a-882b-9ddc-4a9d-5b6758e4159e"), 0600); err != nil { + t.Fatalf("err: %v", err) + } + c.NodeID = "" + if err := agent.setupNodeID(c); err != nil { + t.Fatalf("err: %v", err) + } + if id := agent.consulConfig().NodeID; string(id) != "adf4238a-882b-9ddc-4a9d-5b6758e4159e" { + t.Fatalf("bad: %q vs. %q", id, newID) + } +} + func TestAgent_AddService(t *testing.T) { dir, agent := makeAgent(t, nextConfig()) defer os.RemoveAll(dir) diff --git a/command/agent/command.go b/command/agent/command.go index 3e909d6540..19ca68be46 100644 --- a/command/agent/command.go +++ b/command/agent/command.go @@ -92,6 +92,7 @@ func (c *Command) readConfig() *Config { cmdFlags.StringVar(&cmdConfig.LogLevel, "log-level", "", "log level") cmdFlags.StringVar(&cmdConfig.NodeName, "node", "", "node name") + cmdFlags.StringVar((*string)(&cmdConfig.NodeID), "node-id", "", "node ID") cmdFlags.StringVar(&dcDeprecated, "dc", "", "node datacenter (deprecated: use 'datacenter' instead)") cmdFlags.StringVar(&cmdConfig.Datacenter, "datacenter", "", "node datacenter") cmdFlags.StringVar(&cmdConfig.DataDir, "data-dir", "", "path to the data directory") @@ -1115,6 +1116,7 @@ func (c *Command) Run(args []string) int { c.Ui.Output("Consul agent running!") c.Ui.Info(fmt.Sprintf(" Version: '%s'", c.HumanVersion)) + c.Ui.Info(fmt.Sprintf(" Node ID: '%s'", config.NodeID)) c.Ui.Info(fmt.Sprintf(" Node name: '%s'", config.NodeName)) c.Ui.Info(fmt.Sprintf(" Datacenter: '%s'", config.Datacenter)) c.Ui.Info(fmt.Sprintf(" Server: %v (bootstrap: %v)", config.Server, config.Bootstrap)) diff --git a/command/agent/config.go b/command/agent/config.go index 61302291a6..f1e7f492fd 100644 --- a/command/agent/config.go +++ b/command/agent/config.go @@ -14,6 +14,7 @@ import ( "github.com/hashicorp/consul/consul" "github.com/hashicorp/consul/lib" + "github.com/hashicorp/consul/types" "github.com/hashicorp/consul/watch" "github.com/mitchellh/mapstructure" ) @@ -312,6 +313,10 @@ type Config struct { // LogLevel is the level of the logs to putout LogLevel string `mapstructure:"log_level"` + // Node ID is a unique ID for this node across space and time. Defaults + // to a randomly-generated ID that persists in the data-dir. + NodeID types.NodeID `mapstructure:"node_id"` + // Node name is the name we use to advertise. Defaults to hostname. NodeName string `mapstructure:"node_name"` @@ -1273,6 +1278,9 @@ func MergeConfig(a, b *Config) *Config { if b.Protocol > 0 { result.Protocol = b.Protocol } + if b.NodeID != "" { + result.NodeID = b.NodeID + } if b.NodeName != "" { result.NodeName = b.NodeName } diff --git a/command/agent/config_test.go b/command/agent/config_test.go index 1f21d76c31..efa9165db5 100644 --- a/command/agent/config_test.go +++ b/command/agent/config_test.go @@ -60,7 +60,7 @@ func TestDecodeConfig(t *testing.T) { } // Without a protocol - input = `{"node_name": "foo", "datacenter": "dc2"}` + input = `{"node_id": "bar", "node_name": "foo", "datacenter": "dc2"}` config, err = DecodeConfig(bytes.NewReader([]byte(input))) if err != nil { t.Fatalf("err: %s", err) @@ -70,6 +70,10 @@ func TestDecodeConfig(t *testing.T) { t.Fatalf("bad: %#v", config) } + if config.NodeID != "bar" { + t.Fatalf("bad: %#v", config) + } + if config.Datacenter != "dc2" { t.Fatalf("bad: %#v", config) } @@ -1532,6 +1536,7 @@ func TestMergeConfig(t *testing.T) { DataDir: "/tmp/foo", Domain: "basic", LogLevel: "debug", + NodeID: "bar", NodeName: "foo", ClientAddr: "127.0.0.1", BindAddr: "127.0.0.1", @@ -1586,6 +1591,7 @@ func TestMergeConfig(t *testing.T) { }, Domain: "other", LogLevel: "info", + NodeID: "bar", NodeName: "baz", ClientAddr: "127.0.0.2", BindAddr: "127.0.0.2", diff --git a/consul/client.go b/consul/client.go index 92c4919a2a..6ef697e7c3 100644 --- a/consul/client.go +++ b/consul/client.go @@ -14,6 +14,7 @@ import ( "github.com/hashicorp/consul/consul/agent" "github.com/hashicorp/consul/consul/servers" "github.com/hashicorp/consul/consul/structs" + "github.com/hashicorp/consul/lib" "github.com/hashicorp/serf/coordinate" "github.com/hashicorp/serf/serf" ) @@ -144,6 +145,7 @@ func (c *Client) setupSerf(conf *serf.Config, ch chan serf.Event, path string) ( conf.NodeName = c.config.NodeName conf.Tags["role"] = "node" conf.Tags["dc"] = c.config.Datacenter + conf.Tags["id"] = string(c.config.NodeID) conf.Tags["vsn"] = fmt.Sprintf("%d", c.config.ProtocolVersion) conf.Tags["vsn_min"] = fmt.Sprintf("%d", ProtocolVersionMin) conf.Tags["vsn_max"] = fmt.Sprintf("%d", ProtocolVersionMax) @@ -156,7 +158,7 @@ func (c *Client) setupSerf(conf *serf.Config, ch chan serf.Event, path string) ( conf.RejoinAfterLeave = c.config.RejoinAfterLeave conf.Merge = &lanMergeDelegate{dc: c.config.Datacenter} conf.DisableCoordinates = c.config.DisableCoordinates - if err := ensurePath(conf.SnapshotPath, false); err != nil { + if err := lib.EnsurePath(conf.SnapshotPath, false); err != nil { return nil, err } return serf.Create(conf) diff --git a/consul/config.go b/consul/config.go index abef7a0daf..ae8e1e3155 100644 --- a/consul/config.go +++ b/consul/config.go @@ -8,6 +8,7 @@ import ( "time" "github.com/hashicorp/consul/tlsutil" + "github.com/hashicorp/consul/types" "github.com/hashicorp/memberlist" "github.com/hashicorp/raft" "github.com/hashicorp/serf/serf" @@ -66,6 +67,9 @@ type Config struct { // DevMode is used to enable a development server mode. DevMode bool + // NodeID is a unique identifier for this node across space and time. + NodeID types.NodeID + // Node name is the name we use to advertise. Defaults to hostname. NodeName string diff --git a/consul/server.go b/consul/server.go index 43e9c582d7..18568dfdb5 100644 --- a/consul/server.go +++ b/consul/server.go @@ -20,6 +20,7 @@ import ( "github.com/hashicorp/consul/consul/agent" "github.com/hashicorp/consul/consul/state" "github.com/hashicorp/consul/consul/structs" + "github.com/hashicorp/consul/lib" "github.com/hashicorp/consul/tlsutil" "github.com/hashicorp/raft" "github.com/hashicorp/raft-boltdb" @@ -308,6 +309,7 @@ func (s *Server) setupSerf(conf *serf.Config, ch chan serf.Event, path string, w } conf.Tags["role"] = "consul" conf.Tags["dc"] = s.config.Datacenter + conf.Tags["id"] = string(s.config.NodeID) conf.Tags["vsn"] = fmt.Sprintf("%d", s.config.ProtocolVersion) conf.Tags["vsn_min"] = fmt.Sprintf("%d", ProtocolVersionMin) conf.Tags["vsn_max"] = fmt.Sprintf("%d", ProtocolVersionMax) @@ -337,7 +339,7 @@ func (s *Server) setupSerf(conf *serf.Config, ch chan serf.Event, path string, w // When enabled, the Serf gossip may just turn off if we are the minority // node which is rather unexpected. conf.EnableNameConflictResolution = false - if err := ensurePath(conf.SnapshotPath, false); err != nil { + if err := lib.EnsurePath(conf.SnapshotPath, false); err != nil { return nil, err } @@ -390,7 +392,7 @@ func (s *Server) setupRaft() error { } else { // Create the base raft path. path := filepath.Join(s.config.DataDir, raftState) - if err := ensurePath(path, true); err != nil { + if err := lib.EnsurePath(path, true); err != nil { return err } diff --git a/consul/util.go b/consul/util.go index 02dda3c116..b0000a1998 100644 --- a/consul/util.go +++ b/consul/util.go @@ -4,8 +4,6 @@ import ( "encoding/binary" "fmt" "net" - "os" - "path/filepath" "runtime" "strconv" @@ -64,14 +62,6 @@ func init() { privateBlocks[5] = block } -// ensurePath is used to make sure a path exists -func ensurePath(path string, dir bool) error { - if !dir { - path = filepath.Dir(path) - } - return os.MkdirAll(path, 0755) -} - // CanServersUnderstandProtocol checks to see if all the servers in the given // list understand the given protocol version. If there are no servers in the // list then this will return false. diff --git a/lib/path.go b/lib/path.go new file mode 100644 index 0000000000..8c959a72fa --- /dev/null +++ b/lib/path.go @@ -0,0 +1,14 @@ +package lib + +import ( + "os" + "path/filepath" +) + +// EnsurePath is used to make sure a path exists +func EnsurePath(path string, dir bool) error { + if !dir { + path = filepath.Dir(path) + } + return os.MkdirAll(path, 0755) +} diff --git a/types/node_id.go b/types/node_id.go new file mode 100644 index 0000000000..c0588ed421 --- /dev/null +++ b/types/node_id.go @@ -0,0 +1,4 @@ +package types + +// NodeID is a unique identifier for a node across space and time. +type NodeID string diff --git a/website/source/docs/agent/http/agent.html.markdown b/website/source/docs/agent/http/agent.html.markdown index 5b5dbd14b9..1f065cba72 100644 --- a/website/source/docs/agent/http/agent.html.markdown +++ b/website/source/docs/agent/http/agent.html.markdown @@ -143,6 +143,7 @@ It returns a JSON body like this: "DNSRecursors": [], "Domain": "consul.", "LogLevel": "INFO", + "NodeID": "40e4a748-2192-161a-0510-9bf59fe950b5", "NodeName": "foobar", "ClientAddr": "127.0.0.1", "BindAddr": "0.0.0.0", @@ -183,6 +184,7 @@ It returns a JSON body like this: "Tags": { "bootstrap": "1", "dc": "dc1", + "id": "40e4a748-2192-161a-0510-9bf59fe950b5", "port": "8300", "role": "consul", "vsn": "1", diff --git a/website/source/docs/agent/options.html.markdown b/website/source/docs/agent/options.html.markdown index 96aecabd6b..55bc0dbeb3 100644 --- a/website/source/docs/agent/options.html.markdown +++ b/website/source/docs/agent/options.html.markdown @@ -282,6 +282,15 @@ will exit with an error at startup. * `-node` - The name of this node in the cluster. This must be unique within the cluster. By default this is the hostname of the machine. +* `-node-id` - Available in Consul 0.7.3 and later, this + is a unique identifier for this node across all time, even if the name of the node or address + changes. This must be in the form of a hex string, 36 characters long, such as + `adf4238a-882b-9ddc-4a9d-5b6758e4159e`. If this isn't supplied, which is the most common case, then + the agent will generate an identifier at startup and persist it in the data directory + so that it will remain the same across agent restarts. This is currently only exposed via the agent's + /v1/agent/self endpoint, but future versions of + Consul will use this to better manage cluster changes, especially for Consul servers. + * `-node-meta` - Available in Consul 0.7.3 and later, this specifies an arbitrary metadata key/value pair to associate with the node, of the form `key:value`. This can be specified multiple times. Node metadata pairs have the following restrictions: @@ -695,6 +704,9 @@ Consul will not enable TLS for the HTTP API unless the `https` port has been ass * `log_level` Equivalent to the [`-log-level` command-line flag](#_log_level). +* `node_id` Equivalent to the + [`-node-id` command-line flag](#_node_id). + * `node_name` Equivalent to the [`-node` command-line flag](#_node).