diff --git a/command/server.go b/command/server.go index 39a1f1753..19f97b9a7 100644 --- a/command/server.go +++ b/command/server.go @@ -187,6 +187,7 @@ func (c *ServerCommand) Run(args []string) int { DisableMlock: config.DisableMlock, MaxLeaseTTL: config.MaxLeaseTTL, DefaultLeaseTTL: config.DefaultLeaseTTL, + ClusterName: config.ClusterName, } // Initialize the separate HA physical backend, if it exists diff --git a/command/server/config.go b/command/server/config.go index 5eef8abb6..571d4fc59 100644 --- a/command/server/config.go +++ b/command/server/config.go @@ -33,6 +33,8 @@ type Config struct { MaxLeaseTTLRaw string `hcl:"max_lease_ttl"` DefaultLeaseTTL time.Duration `hcl:"-"` DefaultLeaseTTLRaw string `hcl:"default_lease_ttl"` + + ClusterName string `hcl:"cluster_name"` } // DevConfig is a Config that is used for dev mode of Vault. @@ -210,6 +212,11 @@ func (c *Config) Merge(c2 *Config) *Config { result.DefaultLeaseTTL = c2.DefaultLeaseTTL } + result.ClusterName = c.ClusterName + if c2.ClusterName != "" { + result.ClusterName = c2.ClusterName + } + return result } @@ -277,6 +284,7 @@ func ParseConfig(d string) (*Config, error) { "telemetry", "default_lease_ttl", "max_lease_ttl", + "cluster_name", // TODO: Remove in 0.6.0 // Deprecated keys diff --git a/command/server/config_test.go b/command/server/config_test.go index 708bf337c..0d963f9a8 100644 --- a/command/server/config_test.go +++ b/command/server/config_test.go @@ -61,6 +61,7 @@ func TestLoadConfigFile(t *testing.T) { MaxLeaseTTLRaw: "10h", DefaultLeaseTTL: 10 * time.Hour, DefaultLeaseTTLRaw: "10h", + ClusterName: "testcluster", } if !reflect.DeepEqual(config, expected) { t.Fatalf("expected \n\n%#v\n\n to be \n\n%#v\n\n", config, expected) @@ -120,6 +121,7 @@ func TestLoadConfigFile_json(t *testing.T) { MaxLeaseTTLRaw: "10h", DefaultLeaseTTL: 10 * time.Hour, DefaultLeaseTTLRaw: "10h", + ClusterName: "testcluster", } if !reflect.DeepEqual(config, expected) { t.Fatalf("expected \n\n%#v\n\n to be \n\n%#v\n\n", config, expected) @@ -218,6 +220,7 @@ func TestLoadConfigDir(t *testing.T) { MaxLeaseTTL: 10 * time.Hour, DefaultLeaseTTL: 10 * time.Hour, + ClusterName: "testcluster", } if !reflect.DeepEqual(config, expected) { t.Fatalf("bad: %#v", config) diff --git a/command/server/test-fixtures/config-dir/baz.hcl b/command/server/test-fixtures/config-dir/baz.hcl index e57d76581..9b650104b 100644 --- a/command/server/test-fixtures/config-dir/baz.hcl +++ b/command/server/test-fixtures/config-dir/baz.hcl @@ -5,3 +5,4 @@ telemetry { } default_lease_ttl = "10h" +cluster_name = "testcluster" diff --git a/command/server/test-fixtures/config.hcl b/command/server/test-fixtures/config.hcl index 122710bf4..426f10923 100644 --- a/command/server/test-fixtures/config.hcl +++ b/command/server/test-fixtures/config.hcl @@ -26,3 +26,4 @@ ha_backend "consul" { max_lease_ttl = "10h" default_lease_ttl = "10h" +cluster_name = "testcluster" diff --git a/command/server/test-fixtures/config.hcl.json b/command/server/test-fixtures/config.hcl.json index 094dc154a..eb0970d31 100644 --- a/command/server/test-fixtures/config.hcl.json +++ b/command/server/test-fixtures/config.hcl.json @@ -20,5 +20,6 @@ "statsite_address": "baz" }, "max_lease_ttl": "10h", - "default_lease_ttl": "10h" + "default_lease_ttl": "10h", + "cluster_name":"testcluster" } diff --git a/http/sys_health.go b/http/sys_health.go index 440985305..4f3182bb2 100644 --- a/http/sys_health.go +++ b/http/sys_health.go @@ -114,6 +114,21 @@ func getSysHealth(core *vault.Core, r *http.Request) (int, *HealthResponse, erro code = standbyCode } + // Fetch the local cluster name and identifier + var clusterName, clusterID string + if !sealed { + cluster, err := core.Cluster() + + // Don't set the cluster details in the health status when Vault is sealed + if err != nil { + return http.StatusInternalServerError, nil, err + } + if cluster != nil { + clusterName = cluster.Name + clusterID = cluster.ID + } + } + // Format the body body := &HealthResponse{ Initialized: init, @@ -121,6 +136,8 @@ func getSysHealth(core *vault.Core, r *http.Request) (int, *HealthResponse, erro Standby: standby, ServerTimeUTC: time.Now().UTC().Unix(), Version: version.GetVersion().String(), + ClusterName: clusterName, + ClusterID: clusterID, } return code, body, nil } @@ -131,4 +148,6 @@ type HealthResponse struct { Standby bool `json:"standby"` ServerTimeUTC int64 `json:"server_time_utc"` Version string `json:"version"` + ClusterName string `json:"cluster_name,omitempty"` + ClusterID string `json:"cluster_id,omitempty"` } diff --git a/http/sys_health_test.go b/http/sys_health_test.go index 711acdf02..93b519b6b 100644 --- a/http/sys_health_test.go +++ b/http/sys_health_test.go @@ -31,6 +31,8 @@ func TestSysHealth_get(t *testing.T) { testResponseBody(t, resp, &actual) expected["server_time_utc"] = actual["server_time_utc"] expected["version"] = actual["version"] + expected["cluster_name"] = actual["cluster_name"] + expected["cluster_id"] = actual["cluster_id"] if !reflect.DeepEqual(actual, expected) { t.Fatalf("bad: expected:%#v\nactual:%#v", expected, actual) } @@ -52,6 +54,8 @@ func TestSysHealth_get(t *testing.T) { testResponseBody(t, resp, &actual) expected["server_time_utc"] = actual["server_time_utc"] expected["version"] = actual["version"] + expected["cluster_name"] = actual["cluster_name"] + expected["cluster_id"] = actual["cluster_id"] if !reflect.DeepEqual(actual, expected) { t.Fatalf("bad: expected:%#v\nactual:%#v", expected, actual) } @@ -82,6 +86,8 @@ func TestSysHealth_customcodes(t *testing.T) { expected["server_time_utc"] = actual["server_time_utc"] expected["version"] = actual["version"] + expected["cluster_name"] = actual["cluster_name"] + expected["cluster_id"] = actual["cluster_id"] if !reflect.DeepEqual(actual, expected) { t.Fatalf("bad: expected:%#v\nactual:%#v", expected, actual) } @@ -107,6 +113,8 @@ func TestSysHealth_customcodes(t *testing.T) { testResponseBody(t, resp, &actual) expected["server_time_utc"] = actual["server_time_utc"] expected["version"] = actual["version"] + expected["cluster_name"] = actual["cluster_name"] + expected["cluster_id"] = actual["cluster_id"] if !reflect.DeepEqual(actual, expected) { t.Fatalf("bad: expected:%#v\nactual:%#v", expected, actual) } diff --git a/http/sys_seal_test.go b/http/sys_seal_test.go index 6c702a3ec..bc044c918 100644 --- a/http/sys_seal_test.go +++ b/http/sys_seal_test.go @@ -3,7 +3,6 @@ package http import ( "encoding/hex" "encoding/json" - "log" "net/http" "reflect" "strconv" @@ -173,7 +172,6 @@ func TestSysUnseal_Reset(t *testing.T) { if !reflect.DeepEqual(actual, expected) { t.Fatalf("\nexpected:\n%#v\nactual:\n%#v\n", expected, actual) } - log.Printf("reached here\n") } resp = testHttpPut(t, "", addr+"/v1/sys/unseal", map[string]interface{}{ diff --git a/vault/cluster.go b/vault/cluster.go new file mode 100644 index 000000000..53c68da81 --- /dev/null +++ b/vault/cluster.go @@ -0,0 +1,94 @@ +package vault + +import ( + "encoding/json" + "fmt" + + "github.com/hashicorp/go-uuid" + "github.com/hashicorp/vault/helper/jsonutil" +) + +const ( + // Storage path where the local cluster name and identifier are stored + coreClusterPath = "core/cluster/local" +) + +// Structure representing the storage entry that holds cluster information +type Cluster struct { + // Name of the cluster + Name string `json:"name" structs:"name" mapstructure:"name"` + + // Identifier of the cluster + ID string `json:"id" structs:"id" mapstructure:"id"` +} + +// Cluster fetches the details of either local or global cluster based on the +// input. This method errors out when Vault is sealed. +func (c *Core) Cluster() (*Cluster, error) { + // Fetch the storage entry. This call fails when Vault is sealed. + entry, err := c.barrier.Get(coreClusterPath) + if err != nil { + return nil, err + } + if entry == nil { + return nil, nil + } + + // Decode the cluster information + var cluster Cluster + if err = jsonutil.DecodeJSON(entry.Value, &cluster); err != nil { + return nil, fmt.Errorf("failed to decode cluster details: %v", err) + } + + return &cluster, nil +} + +// setupCluster creates storage entries for holding Vault cluster information. +// Entries will be created only if they are not already present. If clusterName +// is not supplied, this method will auto-generate it. +func (c *Core) setupCluster() error { + // Check if storage index is already present or not + cluster, err := c.Cluster() + if err != nil { + c.logger.Printf("[ERR] core: failed to get cluster details: %v", err) + return err + } + if cluster != nil { + // If index is already present, don't update it + return nil + } + + // If cluster name is not supplied, generate one + if c.clusterName == "" { + clusterNameBytes, err := uuid.GenerateRandomBytes(4) + if err != nil { + c.logger.Printf("[ERR] core: failed to generate cluster name: %v", err) + return err + } + c.clusterName = fmt.Sprintf("vault-cluster-%08x", clusterNameBytes) + } + + // Generate a clusterID + clusterID, err := uuid.GenerateUUID() + if err != nil { + c.logger.Printf("[ERR] core: failed to generate cluster identifier: %v", err) + return err + } + + // Encode the cluster information into as a JSON string + rawCluster, err := json.Marshal(&Cluster{ + Name: c.clusterName, + ID: clusterID, + }) + if err != nil { + c.logger.Printf("[ERR] core: failed to encode cluster details: %v", err) + return err + } + + // Store it + return c.barrier.Put(&Entry{ + Key: coreClusterPath, + Value: rawCluster, + }) + +} diff --git a/vault/cluster_test.go b/vault/cluster_test.go new file mode 100644 index 000000000..e4c6596ca --- /dev/null +++ b/vault/cluster_test.go @@ -0,0 +1,14 @@ +package vault + +import "testing" + +func TestCluster(t *testing.T) { + c, _, _ := TestCoreUnsealed(t) + cluster, err := c.Cluster() + if err != nil { + t.Fatal(err) + } + if cluster == nil || cluster.Name == "" || cluster.ID == "" { + t.Fatalf("cluster information missing: cluster:%#v", cluster) + } +} diff --git a/vault/core.go b/vault/core.go index 0dc6d6edb..30a63095b 100644 --- a/vault/core.go +++ b/vault/core.go @@ -218,23 +218,44 @@ type Core struct { // cachingDisabled indicates whether caches are disabled cachingDisabled bool + + clusterName string } // CoreConfig is used to parameterize a core type CoreConfig struct { - LogicalBackends map[string]logical.Factory - CredentialBackends map[string]logical.Factory - AuditBackends map[string]audit.Factory - Physical physical.Backend - HAPhysical physical.HABackend // May be nil, which disables HA operations - Seal Seal - Logger *log.Logger - DisableCache bool // Disables the LRU cache on the physical backend - DisableMlock bool // Disables mlock syscall - CacheSize int // Custom cache size of zero for default - AdvertiseAddr string // Set as the leader address for HA - DefaultLeaseTTL time.Duration - MaxLeaseTTL time.Duration + LogicalBackends map[string]logical.Factory `json:"logical_backends" structs:"logical_backends" mapstructure:"logical_backends"` + + CredentialBackends map[string]logical.Factory `json:"credential_backends" structs:"credential_backends" mapstructure:"credential_backends"` + + AuditBackends map[string]audit.Factory `json:"audit_backends" structs:"audit_backends" mapstructure:"audit_backends"` + + Physical physical.Backend `json:"physical" structs:"physical" mapstructure:"physical"` + + // May be nil, which disables HA operations + HAPhysical physical.HABackend `json:"ha_physical" structs:"ha_physical" mapstructure:"ha_physical"` + + Seal Seal `json:"seal" structs:"seal" mapstructure:"seal"` + + Logger *log.Logger `json:"logger" structs:"logger" mapstructure:"logger"` + + // Disables the LRU cache on the physical backend + DisableCache bool `json:"disable_cache" structs:"disable_cache" mapstructure:"disable_cache"` + + // Disables mlock syscall + DisableMlock bool `json:"disable_mlock" structs:"disable_mlock" mapstructure:"disable_mlock"` + + // Custom cache size of zero for default + CacheSize int `json:"cache_size" structs:"cache_size" mapstructure:"cache_size"` + + // Set as the leader address for HA + AdvertiseAddr string `json:"advertise_addr" structs:"advertise_addr" mapstructure:"advertise_addr"` + + DefaultLeaseTTL time.Duration `json:"default_lease_ttl" structs:"default_lease_ttl" mapstructure:"default_lease_ttl"` + + MaxLeaseTTL time.Duration `json:"max_lease_ttl" structs:"max_lease_ttl" mapstructure:"max_lease_ttl"` + + ClusterName string `json:"cluster_name" structs:"cluster_name" mapstructure:"cluster_name"` } // NewCore is used to construct a new core @@ -315,6 +336,7 @@ func NewCore(conf *CoreConfig) (*Core, error) { defaultLeaseTTL: conf.DefaultLeaseTTL, maxLeaseTTL: conf.MaxLeaseTTL, cachingDisabled: conf.DisableCache, + clusterName: conf.ClusterName, } if conf.HAPhysical != nil && conf.HAPhysical.HAEnabled() { @@ -970,6 +992,9 @@ func (c *Core) postUnseal() (retErr error) { if err := c.setupAudits(); err != nil { return err } + if err := c.setupCluster(); err != nil { + return err + } c.metricsCh = make(chan struct{}) go c.emitMetrics(c.metricsCh) c.logger.Printf("[INFO] core: post-unseal setup complete") diff --git a/website/source/docs/http/sys-health.html.md b/website/source/docs/http/sys-health.html.md index 454f0e28a..cd770c59e 100644 --- a/website/source/docs/http/sys-health.html.md +++ b/website/source/docs/http/sys-health.html.md @@ -54,8 +54,10 @@ description: |- ```javascript { - "version": "Vault v0.6.1-dev ('418257d27c67fabc4fdd831a6a750d54d8bed76f+CHANGES')", - "server_time_utc": 1469226751, + "cluster_id": "c9abceea-4f46-4dab-a688-5ce55f89e228", + "cluster_name": "vault-cluster-5515c810", + "version": "Vault v0.6.1-dev ('f76c926b0a36e55e71190eb3e2da312f29aca54e+CHANGES')", + "server_time_utc": 1469555798, "standby": false, "sealed": false, "initialized": true