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).