Adds basic support for node IDs.

This commit is contained in:
James Phillips 2017-01-17 22:20:11 -08:00
parent 17c4754eac
commit 96bff003b7
No known key found for this signature in database
GPG Key ID: 77183E682AC5FC11
14 changed files with 189 additions and 16 deletions

View File

@ -657,7 +657,7 @@ func TestAgent_Monitor(t *testing.T) {
// Wait for the first log message and validate it // Wait for the first log message and validate it
select { select {
case log := <-logCh: case log := <-logCh:
if !strings.Contains(log, "[INFO] raft: Initial configuration") { if !strings.Contains(log, "[INFO]") {
t.Fatalf("bad: %q", log) t.Fatalf("bad: %q", log)
} }
case <-time.After(10 * time.Second): case <-time.After(10 * time.Second):

View File

@ -224,7 +224,6 @@ func Create(config *Config, logOutput io.Writer, logWriter *logger.LogWriter,
shutdownCh: make(chan struct{}), shutdownCh: make(chan struct{}),
endpoints: make(map[string]string), endpoints: make(map[string]string),
} }
if err := agent.resolveTmplAddrs(); err != nil { if err := agent.resolveTmplAddrs(); err != nil {
return nil, err return nil, err
} }
@ -236,6 +235,12 @@ func Create(config *Config, logOutput io.Writer, logWriter *logger.LogWriter,
} }
agent.acls = acls 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. // Initialize the local state.
agent.state.Init(config, agent.logger) agent.state.Init(config, agent.logger)
@ -303,6 +308,9 @@ func (a *Agent) consulConfig() *consul.Config {
base = consul.DefaultConfig() base = consul.DefaultConfig()
} }
// This is set when the agent starts up
base.NodeID = a.config.NodeID
// Apply dev mode // Apply dev mode
base.DevMode = a.config.DevMode base.DevMode = a.config.DevMode
@ -600,6 +608,67 @@ func (a *Agent) setupClient() error {
return nil 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 // setupKeyrings is used to initialize and load keyrings during agent startup
func (a *Agent) setupKeyrings(config *consul.Config) error { func (a *Agent) setupKeyrings(config *consul.Config) error {
fileLAN := filepath.Join(a.config.DataDir, serfLANKeyring) fileLAN := filepath.Join(a.config.DataDir, serfLANKeyring)

View File

@ -18,6 +18,8 @@ import (
"github.com/hashicorp/consul/consul/structs" "github.com/hashicorp/consul/consul/structs"
"github.com/hashicorp/consul/logger" "github.com/hashicorp/consul/logger"
"github.com/hashicorp/consul/testutil" "github.com/hashicorp/consul/testutil"
"github.com/hashicorp/consul/types"
"github.com/hashicorp/go-uuid"
"github.com/hashicorp/raft" "github.com/hashicorp/raft"
"strings" "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) { func TestAgent_AddService(t *testing.T) {
dir, agent := makeAgent(t, nextConfig()) dir, agent := makeAgent(t, nextConfig())
defer os.RemoveAll(dir) defer os.RemoveAll(dir)

View File

@ -92,6 +92,7 @@ func (c *Command) readConfig() *Config {
cmdFlags.StringVar(&cmdConfig.LogLevel, "log-level", "", "log level") cmdFlags.StringVar(&cmdConfig.LogLevel, "log-level", "", "log level")
cmdFlags.StringVar(&cmdConfig.NodeName, "node", "", "node name") 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(&dcDeprecated, "dc", "", "node datacenter (deprecated: use 'datacenter' instead)")
cmdFlags.StringVar(&cmdConfig.Datacenter, "datacenter", "", "node datacenter") cmdFlags.StringVar(&cmdConfig.Datacenter, "datacenter", "", "node datacenter")
cmdFlags.StringVar(&cmdConfig.DataDir, "data-dir", "", "path to the data directory") 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.Output("Consul agent running!")
c.Ui.Info(fmt.Sprintf(" Version: '%s'", c.HumanVersion)) 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(" Node name: '%s'", config.NodeName))
c.Ui.Info(fmt.Sprintf(" Datacenter: '%s'", config.Datacenter)) c.Ui.Info(fmt.Sprintf(" Datacenter: '%s'", config.Datacenter))
c.Ui.Info(fmt.Sprintf(" Server: %v (bootstrap: %v)", config.Server, config.Bootstrap)) c.Ui.Info(fmt.Sprintf(" Server: %v (bootstrap: %v)", config.Server, config.Bootstrap))

View File

@ -14,6 +14,7 @@ import (
"github.com/hashicorp/consul/consul" "github.com/hashicorp/consul/consul"
"github.com/hashicorp/consul/lib" "github.com/hashicorp/consul/lib"
"github.com/hashicorp/consul/types"
"github.com/hashicorp/consul/watch" "github.com/hashicorp/consul/watch"
"github.com/mitchellh/mapstructure" "github.com/mitchellh/mapstructure"
) )
@ -312,6 +313,10 @@ type Config struct {
// LogLevel is the level of the logs to putout // LogLevel is the level of the logs to putout
LogLevel string `mapstructure:"log_level"` 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. // Node name is the name we use to advertise. Defaults to hostname.
NodeName string `mapstructure:"node_name"` NodeName string `mapstructure:"node_name"`
@ -1273,6 +1278,9 @@ func MergeConfig(a, b *Config) *Config {
if b.Protocol > 0 { if b.Protocol > 0 {
result.Protocol = b.Protocol result.Protocol = b.Protocol
} }
if b.NodeID != "" {
result.NodeID = b.NodeID
}
if b.NodeName != "" { if b.NodeName != "" {
result.NodeName = b.NodeName result.NodeName = b.NodeName
} }

View File

@ -60,7 +60,7 @@ func TestDecodeConfig(t *testing.T) {
} }
// Without a protocol // 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))) config, err = DecodeConfig(bytes.NewReader([]byte(input)))
if err != nil { if err != nil {
t.Fatalf("err: %s", err) t.Fatalf("err: %s", err)
@ -70,6 +70,10 @@ func TestDecodeConfig(t *testing.T) {
t.Fatalf("bad: %#v", config) t.Fatalf("bad: %#v", config)
} }
if config.NodeID != "bar" {
t.Fatalf("bad: %#v", config)
}
if config.Datacenter != "dc2" { if config.Datacenter != "dc2" {
t.Fatalf("bad: %#v", config) t.Fatalf("bad: %#v", config)
} }
@ -1532,6 +1536,7 @@ func TestMergeConfig(t *testing.T) {
DataDir: "/tmp/foo", DataDir: "/tmp/foo",
Domain: "basic", Domain: "basic",
LogLevel: "debug", LogLevel: "debug",
NodeID: "bar",
NodeName: "foo", NodeName: "foo",
ClientAddr: "127.0.0.1", ClientAddr: "127.0.0.1",
BindAddr: "127.0.0.1", BindAddr: "127.0.0.1",
@ -1586,6 +1591,7 @@ func TestMergeConfig(t *testing.T) {
}, },
Domain: "other", Domain: "other",
LogLevel: "info", LogLevel: "info",
NodeID: "bar",
NodeName: "baz", NodeName: "baz",
ClientAddr: "127.0.0.2", ClientAddr: "127.0.0.2",
BindAddr: "127.0.0.2", BindAddr: "127.0.0.2",

View File

@ -14,6 +14,7 @@ import (
"github.com/hashicorp/consul/consul/agent" "github.com/hashicorp/consul/consul/agent"
"github.com/hashicorp/consul/consul/servers" "github.com/hashicorp/consul/consul/servers"
"github.com/hashicorp/consul/consul/structs" "github.com/hashicorp/consul/consul/structs"
"github.com/hashicorp/consul/lib"
"github.com/hashicorp/serf/coordinate" "github.com/hashicorp/serf/coordinate"
"github.com/hashicorp/serf/serf" "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.NodeName = c.config.NodeName
conf.Tags["role"] = "node" conf.Tags["role"] = "node"
conf.Tags["dc"] = c.config.Datacenter 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"] = fmt.Sprintf("%d", c.config.ProtocolVersion)
conf.Tags["vsn_min"] = fmt.Sprintf("%d", ProtocolVersionMin) conf.Tags["vsn_min"] = fmt.Sprintf("%d", ProtocolVersionMin)
conf.Tags["vsn_max"] = fmt.Sprintf("%d", ProtocolVersionMax) 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.RejoinAfterLeave = c.config.RejoinAfterLeave
conf.Merge = &lanMergeDelegate{dc: c.config.Datacenter} conf.Merge = &lanMergeDelegate{dc: c.config.Datacenter}
conf.DisableCoordinates = c.config.DisableCoordinates 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 nil, err
} }
return serf.Create(conf) return serf.Create(conf)

View File

@ -8,6 +8,7 @@ import (
"time" "time"
"github.com/hashicorp/consul/tlsutil" "github.com/hashicorp/consul/tlsutil"
"github.com/hashicorp/consul/types"
"github.com/hashicorp/memberlist" "github.com/hashicorp/memberlist"
"github.com/hashicorp/raft" "github.com/hashicorp/raft"
"github.com/hashicorp/serf/serf" "github.com/hashicorp/serf/serf"
@ -66,6 +67,9 @@ type Config struct {
// DevMode is used to enable a development server mode. // DevMode is used to enable a development server mode.
DevMode bool 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. // Node name is the name we use to advertise. Defaults to hostname.
NodeName string NodeName string

View File

@ -20,6 +20,7 @@ import (
"github.com/hashicorp/consul/consul/agent" "github.com/hashicorp/consul/consul/agent"
"github.com/hashicorp/consul/consul/state" "github.com/hashicorp/consul/consul/state"
"github.com/hashicorp/consul/consul/structs" "github.com/hashicorp/consul/consul/structs"
"github.com/hashicorp/consul/lib"
"github.com/hashicorp/consul/tlsutil" "github.com/hashicorp/consul/tlsutil"
"github.com/hashicorp/raft" "github.com/hashicorp/raft"
"github.com/hashicorp/raft-boltdb" "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["role"] = "consul"
conf.Tags["dc"] = s.config.Datacenter 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"] = fmt.Sprintf("%d", s.config.ProtocolVersion)
conf.Tags["vsn_min"] = fmt.Sprintf("%d", ProtocolVersionMin) conf.Tags["vsn_min"] = fmt.Sprintf("%d", ProtocolVersionMin)
conf.Tags["vsn_max"] = fmt.Sprintf("%d", ProtocolVersionMax) 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 // When enabled, the Serf gossip may just turn off if we are the minority
// node which is rather unexpected. // node which is rather unexpected.
conf.EnableNameConflictResolution = false conf.EnableNameConflictResolution = false
if err := ensurePath(conf.SnapshotPath, false); err != nil { if err := lib.EnsurePath(conf.SnapshotPath, false); err != nil {
return nil, err return nil, err
} }
@ -390,7 +392,7 @@ func (s *Server) setupRaft() error {
} else { } else {
// Create the base raft path. // Create the base raft path.
path := filepath.Join(s.config.DataDir, raftState) path := filepath.Join(s.config.DataDir, raftState)
if err := ensurePath(path, true); err != nil { if err := lib.EnsurePath(path, true); err != nil {
return err return err
} }

View File

@ -4,8 +4,6 @@ import (
"encoding/binary" "encoding/binary"
"fmt" "fmt"
"net" "net"
"os"
"path/filepath"
"runtime" "runtime"
"strconv" "strconv"
@ -64,14 +62,6 @@ func init() {
privateBlocks[5] = block 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 // 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 understand the given protocol version. If there are no servers in the
// list then this will return false. // list then this will return false.

14
lib/path.go Normal file
View File

@ -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)
}

4
types/node_id.go Normal file
View File

@ -0,0 +1,4 @@
package types
// NodeID is a unique identifier for a node across space and time.
type NodeID string

View File

@ -143,6 +143,7 @@ It returns a JSON body like this:
"DNSRecursors": [], "DNSRecursors": [],
"Domain": "consul.", "Domain": "consul.",
"LogLevel": "INFO", "LogLevel": "INFO",
"NodeID": "40e4a748-2192-161a-0510-9bf59fe950b5",
"NodeName": "foobar", "NodeName": "foobar",
"ClientAddr": "127.0.0.1", "ClientAddr": "127.0.0.1",
"BindAddr": "0.0.0.0", "BindAddr": "0.0.0.0",
@ -183,6 +184,7 @@ It returns a JSON body like this:
"Tags": { "Tags": {
"bootstrap": "1", "bootstrap": "1",
"dc": "dc1", "dc": "dc1",
"id": "40e4a748-2192-161a-0510-9bf59fe950b5",
"port": "8300", "port": "8300",
"role": "consul", "role": "consul",
"vsn": "1", "vsn": "1",

View File

@ -282,6 +282,15 @@ will exit with an error at startup.
* <a name="_node"></a><a href="#_node">`-node`</a> - The name of this node in the cluster. * <a name="_node"></a><a href="#_node">`-node`</a> - The name of this node in the cluster.
This must be unique within the cluster. By default this is the hostname of the machine. This must be unique within the cluster. By default this is the hostname of the machine.
* <a name="_node_id"></a><a href="#_node_id">`-node-id`</a> - 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 <a href="#_data_dir">data directory</a>
so that it will remain the same across agent restarts. This is currently only exposed via the agent's
<a href="/docs/agent/http/agent.html#agent_self">/v1/agent/self</a> endpoint, but future versions of
Consul will use this to better manage cluster changes, especially for Consul servers.
* <a name="_node_meta"></a><a href="#_node_meta">`-node-meta`</a> - Available in Consul 0.7.3 and later, * <a name="_node_meta"></a><a href="#_node_meta">`-node-meta`</a> - 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 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: 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
* <a name="log_level"></a><a href="#log_level">`log_level`</a> Equivalent to the * <a name="log_level"></a><a href="#log_level">`log_level`</a> Equivalent to the
[`-log-level` command-line flag](#_log_level). [`-log-level` command-line flag](#_log_level).
* <a name="node_id"></a><a href="#node_id">`node_id`</a> Equivalent to the
[`-node-id` command-line flag](#_node_id).
* <a name="node_name"></a><a href="#node_name">`node_name`</a> Equivalent to the * <a name="node_name"></a><a href="#node_name">`node_name`</a> Equivalent to the
[`-node` command-line flag](#_node). [`-node` command-line flag](#_node).