From a0c8bbf5b47a0a97811a29799ce4f80c14dace78 Mon Sep 17 00:00:00 2001 From: Calvin Leung Huang Date: Fri, 16 Aug 2019 12:07:44 -0700 Subject: [PATCH 01/17] sys: add host-info endpoint, add client API method --- api/go.mod | 1 + api/go.sum | 7 +++++ api/sys_hostinfo.go | 35 ++++++++++++++++++++++++ go.mod | 1 + go.sum | 7 +++++ http/handler.go | 2 ++ http/logical.go | 7 +++++ vault/hostinfo.go | 50 +++++++++++++++++++++++++++++++++++ vault/hostinfo_test.go | 29 ++++++++++++++++++++ vault/logical_system.go | 26 ++++++++++++++++++ vault/logical_system_paths.go | 15 +++++++++++ 11 files changed, 180 insertions(+) create mode 100644 api/sys_hostinfo.go create mode 100644 vault/hostinfo.go create mode 100644 vault/hostinfo_test.go diff --git a/api/go.mod b/api/go.mod index 76485044adea..e460a03dffed 100644 --- a/api/go.mod +++ b/api/go.mod @@ -13,6 +13,7 @@ require ( github.com/hashicorp/hcl v1.0.0 github.com/hashicorp/vault/sdk v0.1.14-0.20190805214312-16112a336457 github.com/mitchellh/mapstructure v1.1.2 + github.com/shirou/gopsutil v0.0.0-20190731134726-d80c43f9c984 // indirect golang.org/x/net v0.0.0-20190620200207-3b0461eec859 golang.org/x/time v0.0.0-20190308202827-9d24e82272b4 gopkg.in/square/go-jose.v2 v2.3.1 diff --git a/api/go.sum b/api/go.sum index 0f4df0aedefe..d4f8f3c5f7c5 100644 --- a/api/go.sum +++ b/api/go.sum @@ -1,5 +1,6 @@ cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/StackExchange/wmi v0.0.0-20180116203802-5d049714c4a6/go.mod h1:3eOhrUMpNV+6aFIbp5/iudMxNCF27Vw2OZgy4xEx0Fg= github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY= github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= @@ -10,6 +11,7 @@ github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M= github.com/go-ldap/ldap v3.0.2+incompatible/go.mod h1:qfd9rJvER9Q0/D/Sqn1DfHRoBp40uXYvFoEVrNEPqRc= +github.com/go-ole/go-ole v1.2.1/go.mod h1:7FAglXiTm7HKlQRDeOQ6ZNUHidzCWXuZWq/1dTyBNF8= github.com/go-test/deep v1.0.2-0.20181118220953-042da051cf31/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= @@ -66,6 +68,9 @@ github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndr github.com/ryanuber/columnize v2.1.0+incompatible/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= github.com/ryanuber/go-glob v1.0.0 h1:iQh3xXAumdQ+4Ufa5b25cRpC5TYKlno6hsv6Cb3pkBk= github.com/ryanuber/go-glob v1.0.0/go.mod h1:807d1WSdnB0XRJzKNil9Om6lcp/3a0v4qIHxIXzX/Yc= +github.com/shirou/gopsutil v0.0.0-20190731134726-d80c43f9c984 h1:wsZAb4P8F7uQSwsnxE1gk9AHCcc5U0wvyDzcLwFY0Eo= +github.com/shirou/gopsutil v0.0.0-20190731134726-d80c43f9c984/go.mod h1:WWnYX4lzhCH5h/3YBfyVA3VbLYjlMZZAQcW9ojMexNc= +github.com/shirou/w32 v0.0.0-20160930032740-bb4de0191aa4/go.mod h1:qsXQc7+bwAM3Q1u/4XEfrquwF8Lw7D7y5cD8CuHnfIc= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q= @@ -91,8 +96,10 @@ golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190129075346-302c3dd5f1cc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190403152447-81d4e9dc473e h1:nFYrTHrdrAOpShe27kaFHjsqYSEQ0KWqdWLu3xuZJts= golang.org/x/sys v0.0.0-20190403152447-81d4e9dc473e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20181227161524-e6919f6577db h1:6/JqlYfC1CCaLnGceQTI+sDGhC9UBSPAsBqI0Gun6kU= diff --git a/api/sys_hostinfo.go b/api/sys_hostinfo.go new file mode 100644 index 000000000000..448c3546ee49 --- /dev/null +++ b/api/sys_hostinfo.go @@ -0,0 +1,35 @@ +package api + +import ( + "context" + "time" + + "github.com/shirou/gopsutil/cpu" + "github.com/shirou/gopsutil/disk" + "github.com/shirou/gopsutil/host" + "github.com/shirou/gopsutil/mem" +) + +func (c *Sys) HostInfo() (*HostInfoResponse, error) { + r := c.c.NewRequest("GET", "/v1/sys/host-info") + + ctx, cancelFunc := context.WithCancel(context.Background()) + defer cancelFunc() + resp, err := c.c.RawRequestWithContext(ctx, r) + if err != nil { + return nil, err + } + defer resp.Body.Close() + + var result HostInfoResponse + err = resp.DecodeJSON(&result) + return &result, err +} + +type HostInfoResponse struct { + CollectionTime time.Time `json:"collection_time"` + CPU []cpu.InfoStat `json:"cpu"` + Disk *disk.UsageStat `json:"disk"` + Host *host.InfoStat `json:"host"` + Memory *mem.VirtualMemoryStat `json:"memory"` +} diff --git a/go.mod b/go.mod index c48b998a577f..60035a3f3783 100644 --- a/go.mod +++ b/go.mod @@ -109,6 +109,7 @@ require ( github.com/ryanuber/columnize v2.1.0+incompatible github.com/ryanuber/go-glob v1.0.0 github.com/samuel/go-zookeeper v0.0.0-20180130194729-c4fab1ac1bec + github.com/shirou/gopsutil v0.0.0-20190731134726-d80c43f9c984 github.com/shopspring/decimal v0.0.0-20180709203117-cd690d0c9e24 // indirect github.com/streadway/amqp v0.0.0-20190404075320-75d898a42a94 // indirect github.com/stretchr/testify v1.3.0 diff --git a/go.sum b/go.sum index 92784e0f7c39..b34ce7a193bd 100644 --- a/go.sum +++ b/go.sum @@ -32,6 +32,7 @@ github.com/SAP/go-hdb v0.14.1 h1:hkw4ozGZ/i4eak7ZuGkY5e0hxiXFdNUBNhr4AvZVNFE= github.com/SAP/go-hdb v0.14.1/go.mod h1:7fdQLVC2lER3urZLjZCm0AuMQfApof92n3aylBPEkMo= github.com/Shopify/sarama v1.19.0/go.mod h1:FVkBWblsNy7DGZRfXLU0O9RCGt5g3g3yEuWXgklEdEo= github.com/Shopify/toxiproxy v2.1.4+incompatible/go.mod h1:OXgGpZ6Cli1/URJOF1DMxUHB2q5Ap20/P/eIdh4G0pI= +github.com/StackExchange/wmi v0.0.0-20180116203802-5d049714c4a6/go.mod h1:3eOhrUMpNV+6aFIbp5/iudMxNCF27Vw2OZgy4xEx0Fg= github.com/abdullin/seq v0.0.0-20160510034733-d5467c17e7af h1:DBNMBMuMiWYu0b+8KMJuWmfCkcxl09JwdlqwDZZ6U14= github.com/abdullin/seq v0.0.0-20160510034733-d5467c17e7af/go.mod h1:5Jv4cbFiHJMsVxt52+i0Ha45fjshj6wxYr1r19tB9bw= github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= @@ -157,6 +158,7 @@ github.com/go-ldap/ldap v3.0.2+incompatible/go.mod h1:qfd9rJvER9Q0/D/Sqn1DfHRoBp github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= github.com/go-martini/martini v0.0.0-20170121215854-22fa46961aab h1:xveKWz2iaueeTaUgdetzel+U7exyigDYBryyVfV/rZk= github.com/go-martini/martini v0.0.0-20170121215854-22fa46961aab/go.mod h1:/P9AEU963A2AYjv4d1V5eVL1CQbEJq6aCNHDDjibzu8= +github.com/go-ole/go-ole v1.2.1/go.mod h1:7FAglXiTm7HKlQRDeOQ6ZNUHidzCWXuZWq/1dTyBNF8= github.com/go-sql-driver/mysql v1.4.1 h1:g24URVg0OFbNUTx9qqY1IRZ9D9z3iPyi5zKhQZpNwpA= github.com/go-sql-driver/mysql v1.4.1/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= @@ -508,6 +510,11 @@ github.com/satori/go.uuid v1.2.0 h1:0uYX9dsZ2yD7q2RtLRtPSdGDWzjeM3TbMJP9utgA0ww= github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0= github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529 h1:nn5Wsu0esKSJiIVhscUtVbo7ada43DJhG55ua/hjS5I= github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= +github.com/shirou/gopsutil v0.0.0-20190731134726-d80c43f9c984 h1:wsZAb4P8F7uQSwsnxE1gk9AHCcc5U0wvyDzcLwFY0Eo= +github.com/shirou/gopsutil v0.0.0-20190731134726-d80c43f9c984/go.mod h1:WWnYX4lzhCH5h/3YBfyVA3VbLYjlMZZAQcW9ojMexNc= +github.com/shirou/gopsutil v2.18.12+incompatible h1:1eaJvGomDnH74/5cF4CTmTbLHAriGFsTZppLXDX93OM= +github.com/shirou/gopsutil v2.18.12+incompatible/go.mod h1:5b4v6he4MtMOwMlS0TUMTu2PcXUg8+E1lC7eC3UO/RA= +github.com/shirou/w32 v0.0.0-20160930032740-bb4de0191aa4/go.mod h1:qsXQc7+bwAM3Q1u/4XEfrquwF8Lw7D7y5cD8CuHnfIc= github.com/shopspring/decimal v0.0.0-20180709203117-cd690d0c9e24 h1:pntxY8Ary0t43dCZ5dqY4YTJCObLY1kIXl0uzMv+7DE= github.com/shopspring/decimal v0.0.0-20180709203117-cd690d0c9e24/go.mod h1:M+9NzErvs504Cn4c5DxATwIqPbtswREoFCre64PpcG4= github.com/sirupsen/logrus v1.0.5 h1:8c8b5uO0zS4X6RPl/sd1ENwSkIc0/H2PaHxE3udaE8I= diff --git a/http/handler.go b/http/handler.go index 2562c6a052c2..da6f41790549 100644 --- a/http/handler.go +++ b/http/handler.go @@ -110,6 +110,8 @@ func Handler(props *vault.HandlerProperties) http.Handler { // Create the muxer to handle the actual endpoints mux := http.NewServeMux() + // mux.Handle("/v1/sys/host-info", handleSysHostInfo(core)) + mux.Handle("/v1/sys/host-info", handleLogical(core)) mux.Handle("/v1/sys/init", handleSysInit(core)) mux.Handle("/v1/sys/seal-status", handleSysSealStatus(core)) mux.Handle("/v1/sys/seal", handleSysSeal(core)) diff --git a/http/logical.go b/http/logical.go index 4147693ea81c..dc78600e837d 100644 --- a/http/logical.go +++ b/http/logical.go @@ -277,6 +277,13 @@ func handleLogicalInternal(core *vault.Core, injectDataIntoTopLevel bool) http.H // success. resp, ok, needsForward := request(core, w, r, req) if needsForward { + // Do not forward these specific requests since they are only + // applicable to the local node. + switch req.Path { + case "sys/host-info": + return + } + if origBody != nil { r.Body = origBody } diff --git a/vault/hostinfo.go b/vault/hostinfo.go new file mode 100644 index 000000000000..73989650e85b --- /dev/null +++ b/vault/hostinfo.go @@ -0,0 +1,50 @@ +package vault + +import ( + "time" + + "github.com/hashicorp/go-multierror" + "github.com/shirou/gopsutil/cpu" + "github.com/shirou/gopsutil/disk" + "github.com/shirou/gopsutil/host" + "github.com/shirou/gopsutil/mem" +) + +type HostInfo struct { + CollectionTime time.Time `json:"collection_time"` + CPU []cpu.InfoStat `json:"cpu"` + Disk *disk.UsageStat `json:"disk"` + Host *host.InfoStat `json:"host"` + Memory *mem.VirtualMemoryStat `json:"memory"` +} + +func (c *Core) CollectHostInfo() (*HostInfo, error) { + var retErr error + info := &HostInfo{CollectionTime: time.Now()} + + if h, err := host.Info(); err != nil { + retErr = multierror.Append(retErr, err) + } else { + info.Host = h + } + + if v, err := mem.VirtualMemory(); err != nil { + retErr = multierror.Append(retErr, err) + } else { + info.Memory = v + } + + if d, err := disk.Usage("/"); err != nil { + retErr = multierror.Append(retErr, err) + } else { + info.Disk = d + } + + if c, err := cpu.Info(); err != nil { + retErr = multierror.Append(retErr, err) + } else { + info.CPU = c + } + + return info, retErr +} diff --git a/vault/hostinfo_test.go b/vault/hostinfo_test.go new file mode 100644 index 000000000000..37d3637fa859 --- /dev/null +++ b/vault/hostinfo_test.go @@ -0,0 +1,29 @@ +package vault + +import ( + "testing" +) + +func TestCollectHostInfo(t *testing.T) { + c, _, _ := TestCoreUnsealed(t) + + info, err := c.CollectHostInfo() + if err != nil { + t.Fatal(err) + } + if info.CollectionTime.IsZero() { + t.Fatal("expected non-zero collection_time") + } + if info.CPU == nil { + t.Fatal("expected non-nil CPU value") + } + if info.Disk == nil { + t.Fatal("expected non-nil Disk value") + } + if info.Host == nil { + t.Fatal("expected non-nil Host value") + } + if info.Memory == nil { + t.Fatal("expected non-nil Memory value") + } +} diff --git a/vault/logical_system.go b/vault/logical_system.go index d331c253c4ae..cc717ad3d014 100644 --- a/vault/logical_system.go +++ b/vault/logical_system.go @@ -166,6 +166,7 @@ func NewSystemBackend(core *Core, logger log.Logger) *SystemBackend { b.Backend.Paths = append(b.Backend.Paths, b.internalPaths()...) b.Backend.Paths = append(b.Backend.Paths, b.remountPath()) b.Backend.Paths = append(b.Backend.Paths, b.metricsPath()) + b.Backend.Paths = append(b.Backend.Paths, b.hostInfoPath()) if core.rawEnabled { b.Backend.Paths = append(b.Backend.Paths, &framework.Path{ @@ -2609,6 +2610,25 @@ func (b *SystemBackend) handleMetrics(ctx context.Context, req *logical.Request, return b.Core.metricsHelper.ResponseForFormat(format) } +func (b *SystemBackend) handleHostInfo(ctx context.Context, req *logical.Request, data *framework.FieldData) (*logical.Response, error) { + info, err := b.Core.CollectHostInfo() + if err != nil { + return nil, err + } + + respData := map[string]interface{}{ + "collection_time": info.CollectionTime, + "cpu": info.CPU, + "disk": info.Disk, + "host": info.Host, + "memory": info.Memory, + } + + return &logical.Response{ + Data: respData, + }, nil +} + func (b *SystemBackend) handleWrappingLookup(ctx context.Context, req *logical.Request, data *framework.FieldData) (*logical.Response, error) { // This ordering of lookups has been validated already in the wrapping // validation func, we're just doing this for a safety check @@ -4042,4 +4062,10 @@ This path responds to the following HTTP methods. "Count of requests seen by this Vault cluster over time.", "Count of requests seen by this Vault cluster over time. Not included in count: health checks, UI asset requests, requests forwarded from another cluster.", }, + "host-info": { + "Information about the the host instance that this Vault server is running on.", + `Information about the the host instance that this Vault server is running on. + The information that gets collected includes host hardware information, and CPU, + disk, and memory utilization`, + }, } diff --git a/vault/logical_system_paths.go b/vault/logical_system_paths.go index c52e3a3bf87d..5588c1d0eebe 100644 --- a/vault/logical_system_paths.go +++ b/vault/logical_system_paths.go @@ -1130,6 +1130,21 @@ func (b *SystemBackend) metricsPath() *framework.Path { } +func (b *SystemBackend) hostInfoPath() *framework.Path { + return &framework.Path{ + Pattern: "host-info/?", + Operations: map[logical.Operation]framework.OperationHandler{ + logical.ReadOperation: &framework.PathOperation{ + Callback: b.handleHostInfo, + Summary: strings.TrimSpace(sysHelp["host-info"][0]), + Description: strings.TrimSpace(sysHelp["host-info"][1]), + }, + }, + HelpSynopsis: strings.TrimSpace(sysHelp["host-info"][0]), + HelpDescription: strings.TrimSpace(sysHelp["host-info"][1]), + } +} + func (b *SystemBackend) authPaths() []*framework.Path { return []*framework.Path{ { From 8a9885413a789811d487140c7be859e2f35f6551 Mon Sep 17 00:00:00 2001 From: Calvin Leung Huang Date: Fri, 16 Aug 2019 12:10:00 -0700 Subject: [PATCH 02/17] remove old commented handler --- http/handler.go | 1 - 1 file changed, 1 deletion(-) diff --git a/http/handler.go b/http/handler.go index da6f41790549..462698b994f8 100644 --- a/http/handler.go +++ b/http/handler.go @@ -110,7 +110,6 @@ func Handler(props *vault.HandlerProperties) http.Handler { // Create the muxer to handle the actual endpoints mux := http.NewServeMux() - // mux.Handle("/v1/sys/host-info", handleSysHostInfo(core)) mux.Handle("/v1/sys/host-info", handleLogical(core)) mux.Handle("/v1/sys/init", handleSysInit(core)) mux.Handle("/v1/sys/seal-status", handleSysSealStatus(core)) From 66deab846865ba43ab5ae3793b1d5b6309f543ea Mon Sep 17 00:00:00 2001 From: Calvin Leung Huang Date: Fri, 16 Aug 2019 16:06:40 -0700 Subject: [PATCH 03/17] add http tests, fix bugs --- api/sys_hostinfo.go | 21 ++++++++++++++++- http/sys_hostinfo_test.go | 49 +++++++++++++++++++++++++++++++++++++++ vault/hostinfo.go | 32 +++++++++++++++++++------ vault/hostinfo_test.go | 2 +- vault/logical_system.go | 40 +++++++++++++++++++++++++------- 5 files changed, 127 insertions(+), 17 deletions(-) create mode 100644 http/sys_hostinfo_test.go diff --git a/api/sys_hostinfo.go b/api/sys_hostinfo.go index 448c3546ee49..a514ba9e8a9a 100644 --- a/api/sys_hostinfo.go +++ b/api/sys_hostinfo.go @@ -2,8 +2,10 @@ package api import ( "context" + "errors" "time" + "github.com/mitchellh/mapstructure" "github.com/shirou/gopsutil/cpu" "github.com/shirou/gopsutil/disk" "github.com/shirou/gopsutil/host" @@ -21,8 +23,25 @@ func (c *Sys) HostInfo() (*HostInfoResponse, error) { } defer resp.Body.Close() + secret, err := ParseSecret(resp.Body) + if err != nil { + return nil, err + } + if secret == nil || secret.Data == nil { + return nil, errors.New("data from server response is empty") + } + var result HostInfoResponse - err = resp.DecodeJSON(&result) + err = mapstructure.WeakDecode(secret.Data, &result) + if err != nil { + return nil, err + } + + result.CollectionTime, err = time.Parse(time.RFC3339, secret.Data["collection_time"].(string)) + if err != nil { + return nil, err + } + return &result, err } diff --git a/http/sys_hostinfo_test.go b/http/sys_hostinfo_test.go new file mode 100644 index 000000000000..512ca0b9adda --- /dev/null +++ b/http/sys_hostinfo_test.go @@ -0,0 +1,49 @@ +package http + +import ( + "testing" + + "github.com/hashicorp/vault/vault" +) + +func TestSysHostInfo(t *testing.T) { + cluster := vault.NewTestCluster(t, &vault.CoreConfig{}, &vault.TestClusterOptions{ + HandlerFunc: Handler, + }) + cluster.Start() + defer cluster.Cleanup() + cores := cluster.Cores + + vault.TestWaitActive(t, cores[0].Core) + + // Query against the active node, should get host information back + info, err := cores[0].Client.Sys().HostInfo() + if err != nil { + t.Fatal(err) + } + if info == nil { + t.Fatal("expected non-nil HostInfo") + } + + if info.CollectionTime.IsZero() { + t.Fatalf("expected a valid timestamp") + } + if info.CPU == nil { + t.Fatal("expected CPU info") + } + if info.Disk == nil { + t.Fatal("expected disk info") + } + if info.Host == nil { + t.Fatal("expected host info") + } + if info.Memory == nil { + t.Fatal("expected memory info") + } + + // Query against the standby, should error + info, err = cores[1].Client.Sys().HostInfo() + if err == nil || info != nil { + t.Fatalf("expected error on standby node, HostInfo: %v", info) + } +} diff --git a/vault/hostinfo.go b/vault/hostinfo.go index 73989650e85b..5a5a2f46caa0 100644 --- a/vault/hostinfo.go +++ b/vault/hostinfo.go @@ -1,6 +1,7 @@ package vault import ( + "runtime" "time" "github.com/hashicorp/go-multierror" @@ -18,33 +19,50 @@ type HostInfo struct { Memory *mem.VirtualMemoryStat `json:"memory"` } +type HostInfoError struct { + Err error +} + +func (e *HostInfoError) WrappedErrors() []error { + return []error{e.Err} +} + +func (e *HostInfoError) Error() string { + return e.Err.Error() +} + func (c *Core) CollectHostInfo() (*HostInfo, error) { - var retErr error + var retErr *multierror.Error info := &HostInfo{CollectionTime: time.Now()} if h, err := host.Info(); err != nil { - retErr = multierror.Append(retErr, err) + retErr = multierror.Append(retErr, &HostInfoError{err}) } else { info.Host = h } if v, err := mem.VirtualMemory(); err != nil { - retErr = multierror.Append(retErr, err) + retErr = multierror.Append(retErr, &HostInfoError{err}) } else { info.Memory = v } - if d, err := disk.Usage("/"); err != nil { - retErr = multierror.Append(retErr, err) + diskPath := "/" + if runtime.GOOS == "windows" { + diskPath = "C:" + } + + if d, err := disk.Usage(diskPath); err != nil { + retErr = multierror.Append(retErr, &HostInfoError{err}) } else { info.Disk = d } if c, err := cpu.Info(); err != nil { - retErr = multierror.Append(retErr, err) + retErr = multierror.Append(retErr, &HostInfoError{err}) } else { info.CPU = c } - return info, retErr + return info, retErr.ErrorOrNil() } diff --git a/vault/hostinfo_test.go b/vault/hostinfo_test.go index 37d3637fa859..8b535b410d5a 100644 --- a/vault/hostinfo_test.go +++ b/vault/hostinfo_test.go @@ -12,7 +12,7 @@ func TestCollectHostInfo(t *testing.T) { t.Fatal(err) } if info.CollectionTime.IsZero() { - t.Fatal("expected non-zero collection_time") + t.Fatal("expected non-zero CollectionTime") } if info.CPU == nil { t.Fatal("expected non-nil CPU value") diff --git a/vault/logical_system.go b/vault/logical_system.go index cc717ad3d014..afa5204ad218 100644 --- a/vault/logical_system.go +++ b/vault/logical_system.go @@ -18,6 +18,7 @@ import ( "sync" "time" + "github.com/hashicorp/go-multierror" "github.com/hashicorp/vault/physical/raft" "github.com/hashicorp/errwrap" @@ -2611,22 +2612,45 @@ func (b *SystemBackend) handleMetrics(ctx context.Context, req *logical.Request, } func (b *SystemBackend) handleHostInfo(ctx context.Context, req *logical.Request, data *framework.FieldData) (*logical.Response, error) { + resp := &logical.Response{} info, err := b.Core.CollectHostInfo() if err != nil { - return nil, err + // If the error is a HostInfoError, we return them as response warnings + if errs, ok := err.(*multierror.Error); ok { + var warnings []string + for _, mErr := range errs.Errors { + if errwrap.ContainsType(mErr, new(HostInfoError)) { + warnings = append(warnings, mErr.Error()) + } + } + resp.Warnings = warnings + } else { + return nil, err + } + } + + if info == nil { + return nil, errors.New("unable to collect host information: nil HostInfo") } respData := map[string]interface{}{ "collection_time": info.CollectionTime, - "cpu": info.CPU, - "disk": info.Disk, - "host": info.Host, - "memory": info.Memory, } + if info.CPU != nil { + respData["cpu"] = info.CPU + } + if info.Disk != nil { + respData["disk"] = info.Disk + } + if info.Host != nil { + respData["host"] = info.Host + } + if info.Memory != nil { + respData["memory"] = info.Memory + } + resp.Data = respData - return &logical.Response{ - Data: respData, - }, nil + return resp, nil } func (b *SystemBackend) handleWrappingLookup(ctx context.Context, req *logical.Request, data *framework.FieldData) (*logical.Response, error) { From 6639190d6246670be210c3b0c955e86ceea1c53e Mon Sep 17 00:00:00 2001 From: Calvin Leung Huang Date: Wed, 28 Aug 2019 12:31:52 -0700 Subject: [PATCH 04/17] query all partitions for disk usage --- api/sys_hostinfo.go | 16 +++++----------- http/sys_hostinfo_test.go | 2 +- vault/hostinfo.go | 32 ++++++++++++++++++-------------- vault/hostinfo_test.go | 4 ++-- vault/logical_system.go | 2 +- 5 files changed, 27 insertions(+), 29 deletions(-) diff --git a/api/sys_hostinfo.go b/api/sys_hostinfo.go index a514ba9e8a9a..fccf15a43c36 100644 --- a/api/sys_hostinfo.go +++ b/api/sys_hostinfo.go @@ -3,7 +3,6 @@ package api import ( "context" "errors" - "time" "github.com/mitchellh/mapstructure" "github.com/shirou/gopsutil/cpu" @@ -37,18 +36,13 @@ func (c *Sys) HostInfo() (*HostInfoResponse, error) { return nil, err } - result.CollectionTime, err = time.Parse(time.RFC3339, secret.Data["collection_time"].(string)) - if err != nil { - return nil, err - } - return &result, err } type HostInfoResponse struct { - CollectionTime time.Time `json:"collection_time"` - CPU []cpu.InfoStat `json:"cpu"` - Disk *disk.UsageStat `json:"disk"` - Host *host.InfoStat `json:"host"` - Memory *mem.VirtualMemoryStat `json:"memory"` + Timestamp string `json:"timestamp" mapstructure:"-"` + CPU []cpu.InfoStat `json:"cpu"` + Disk []*disk.UsageStat `json:"disk"` + Host *host.InfoStat `json:"host"` + Memory *mem.VirtualMemoryStat `json:"memory"` } diff --git a/http/sys_hostinfo_test.go b/http/sys_hostinfo_test.go index 512ca0b9adda..78bd40ab356e 100644 --- a/http/sys_hostinfo_test.go +++ b/http/sys_hostinfo_test.go @@ -25,7 +25,7 @@ func TestSysHostInfo(t *testing.T) { t.Fatal("expected non-nil HostInfo") } - if info.CollectionTime.IsZero() { + if info.Timestamp == "" { t.Fatalf("expected a valid timestamp") } if info.CPU == nil { diff --git a/vault/hostinfo.go b/vault/hostinfo.go index 5a5a2f46caa0..b40a7084739a 100644 --- a/vault/hostinfo.go +++ b/vault/hostinfo.go @@ -1,7 +1,6 @@ package vault import ( - "runtime" "time" "github.com/hashicorp/go-multierror" @@ -12,11 +11,11 @@ import ( ) type HostInfo struct { - CollectionTime time.Time `json:"collection_time"` - CPU []cpu.InfoStat `json:"cpu"` - Disk *disk.UsageStat `json:"disk"` - Host *host.InfoStat `json:"host"` - Memory *mem.VirtualMemoryStat `json:"memory"` + Timestamp time.Time `json:"timestamp"` + CPU []cpu.InfoStat `json:"cpu"` + Disk []*disk.UsageStat `json:"disk"` + Host *host.InfoStat `json:"host"` + Memory *mem.VirtualMemoryStat `json:"memory"` } type HostInfoError struct { @@ -33,7 +32,7 @@ func (e *HostInfoError) Error() string { func (c *Core) CollectHostInfo() (*HostInfo, error) { var retErr *multierror.Error - info := &HostInfo{CollectionTime: time.Now()} + info := &HostInfo{Timestamp: time.Now().UTC()} if h, err := host.Info(); err != nil { retErr = multierror.Append(retErr, &HostInfoError{err}) @@ -47,15 +46,20 @@ func (c *Core) CollectHostInfo() (*HostInfo, error) { info.Memory = v } - diskPath := "/" - if runtime.GOOS == "windows" { - diskPath = "C:" - } - - if d, err := disk.Usage(diskPath); err != nil { + parts, err := disk.Partitions(false) + if err != nil { retErr = multierror.Append(retErr, &HostInfoError{err}) } else { - info.Disk = d + var usage []*disk.UsageStat + for _, part := range parts { + u, err := disk.Usage(part.Mountpoint) + if err != nil { + retErr = multierror.Append(retErr, &HostInfoError{err}) + } + usage = append(usage, u) + + } + info.Disk = usage } if c, err := cpu.Info(); err != nil { diff --git a/vault/hostinfo_test.go b/vault/hostinfo_test.go index 8b535b410d5a..396e519a35dd 100644 --- a/vault/hostinfo_test.go +++ b/vault/hostinfo_test.go @@ -11,8 +11,8 @@ func TestCollectHostInfo(t *testing.T) { if err != nil { t.Fatal(err) } - if info.CollectionTime.IsZero() { - t.Fatal("expected non-zero CollectionTime") + if info.Timestamp.IsZero() { + t.Fatal("expected non-zero Timestamp") } if info.CPU == nil { t.Fatal("expected non-nil CPU value") diff --git a/vault/logical_system.go b/vault/logical_system.go index afa5204ad218..e4857729ea1f 100644 --- a/vault/logical_system.go +++ b/vault/logical_system.go @@ -2634,7 +2634,7 @@ func (b *SystemBackend) handleHostInfo(ctx context.Context, req *logical.Request } respData := map[string]interface{}{ - "collection_time": info.CollectionTime, + "timestamp": info.Timestamp, } if info.CPU != nil { respData["cpu"] = info.CPU From 48af9003f1190bc9bac746d830c8e46f48cf3422 Mon Sep 17 00:00:00 2001 From: Calvin Leung Huang Date: Wed, 28 Aug 2019 14:08:33 -0700 Subject: [PATCH 05/17] fix Timestamp decoding --- api/sys_hostinfo.go | 12 +++++++++++- http/sys_hostinfo_test.go | 2 +- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/api/sys_hostinfo.go b/api/sys_hostinfo.go index fccf15a43c36..3d438f111f04 100644 --- a/api/sys_hostinfo.go +++ b/api/sys_hostinfo.go @@ -3,6 +3,7 @@ package api import ( "context" "errors" + "time" "github.com/mitchellh/mapstructure" "github.com/shirou/gopsutil/cpu" @@ -30,17 +31,26 @@ func (c *Sys) HostInfo() (*HostInfoResponse, error) { return nil, errors.New("data from server response is empty") } + // Parse timestamp separately since WeakDecode can't handle this field. + timestampRaw := secret.Data["timestamp"].(string) + timestamp, err := time.Parse(time.RFC3339, timestampRaw) + if err != nil { + return nil, err + } + delete(secret.Data, "timestamp") + var result HostInfoResponse err = mapstructure.WeakDecode(secret.Data, &result) if err != nil { return nil, err } + result.Timestamp = timestamp return &result, err } type HostInfoResponse struct { - Timestamp string `json:"timestamp" mapstructure:"-"` + Timestamp time.Time `json:"timestamp"` CPU []cpu.InfoStat `json:"cpu"` Disk []*disk.UsageStat `json:"disk"` Host *host.InfoStat `json:"host"` diff --git a/http/sys_hostinfo_test.go b/http/sys_hostinfo_test.go index 78bd40ab356e..37b7816d5138 100644 --- a/http/sys_hostinfo_test.go +++ b/http/sys_hostinfo_test.go @@ -25,7 +25,7 @@ func TestSysHostInfo(t *testing.T) { t.Fatal("expected non-nil HostInfo") } - if info.Timestamp == "" { + if info.Timestamp.IsZero() { t.Fatalf("expected a valid timestamp") } if info.CPU == nil { From 8beceab8ebb05b6e82c54a47c4144706afd3e5ee Mon Sep 17 00:00:00 2001 From: Calvin Leung Huang Date: Wed, 28 Aug 2019 17:01:52 -0700 Subject: [PATCH 06/17] add comments for clarification --- vault/hostinfo.go | 8 ++++++++ vault/logical_system.go | 8 ++++++++ 2 files changed, 16 insertions(+) diff --git a/vault/hostinfo.go b/vault/hostinfo.go index b40a7084739a..c41dbc9ccce6 100644 --- a/vault/hostinfo.go +++ b/vault/hostinfo.go @@ -10,6 +10,7 @@ import ( "github.com/shirou/gopsutil/mem" ) +// HostInfo holds all the information that gets captured on the host. type HostInfo struct { Timestamp time.Time `json:"timestamp"` CPU []cpu.InfoStat `json:"cpu"` @@ -18,6 +19,7 @@ type HostInfo struct { Memory *mem.VirtualMemoryStat `json:"memory"` } +// HostInfoError is a typed error for more convenient error checking. type HostInfoError struct { Err error } @@ -30,6 +32,12 @@ func (e *HostInfoError) Error() string { return e.Err.Error() } +// CollectHostInfo returns information on the host, which includes general +// host status, CPU, memory, and disk utilization. +// +// The function does a best-effort capture on the most information possible, +// continuing on capture errors encountered and appending them to a resulting +// multierror.Error that gets returned at the end. func (c *Core) CollectHostInfo() (*HostInfo, error) { var retErr *multierror.Error info := &HostInfo{Timestamp: time.Now().UTC()} diff --git a/vault/logical_system.go b/vault/logical_system.go index e4857729ea1f..2cb78b611dc3 100644 --- a/vault/logical_system.go +++ b/vault/logical_system.go @@ -2611,6 +2611,9 @@ func (b *SystemBackend) handleMetrics(ctx context.Context, req *logical.Request, return b.Core.metricsHelper.ResponseForFormat(format) } +// handleHostInfo collects and returns host-related information, which includes +// system information, cpu, disk, and memory usage. Any capture-related errors +// returned by the collection method will be returned as response warnings. func (b *SystemBackend) handleHostInfo(ctx context.Context, req *logical.Request, data *framework.FieldData) (*logical.Response, error) { resp := &logical.Response{} info, err := b.Core.CollectHostInfo() @@ -2621,6 +2624,11 @@ func (b *SystemBackend) handleHostInfo(ctx context.Context, req *logical.Request for _, mErr := range errs.Errors { if errwrap.ContainsType(mErr, new(HostInfoError)) { warnings = append(warnings, mErr.Error()) + } else { + // If the error is a multierror, it should only be for + // HostInfoError, but if it's not for any reason, we return + // it as an error to avoid it being swallowed. + return nil, err } } resp.Warnings = warnings From 88d08bf229ab08ba1fa424b80cc3af7b7cedb494 Mon Sep 17 00:00:00 2001 From: Calvin Leung Huang Date: Thu, 29 Aug 2019 12:02:02 -0700 Subject: [PATCH 07/17] dont append a nil entry on disk usage query error --- vault/hostinfo.go | 1 + 1 file changed, 1 insertion(+) diff --git a/vault/hostinfo.go b/vault/hostinfo.go index c41dbc9ccce6..b992b14968b6 100644 --- a/vault/hostinfo.go +++ b/vault/hostinfo.go @@ -63,6 +63,7 @@ func (c *Core) CollectHostInfo() (*HostInfo, error) { u, err := disk.Usage(part.Mountpoint) if err != nil { retErr = multierror.Append(retErr, &HostInfoError{err}) + continue } usage = append(usage, u) From fd3f11617e1f2c12e988417b1d261a1f43b9c717 Mon Sep 17 00:00:00 2001 From: Calvin Leung Huang Date: Thu, 29 Aug 2019 14:35:04 -0700 Subject: [PATCH 08/17] remove HostInfo from the sdk api We can use Logical().Read(...) to query this endpoint since the payload is contained with the data object. All warnings are preserved under Secret.Warnings. --- api/sys_hostinfo.go | 58 --------------------------------------- http/sys_hostinfo_test.go | 27 ++++++++++++------ 2 files changed, 19 insertions(+), 66 deletions(-) delete mode 100644 api/sys_hostinfo.go diff --git a/api/sys_hostinfo.go b/api/sys_hostinfo.go deleted file mode 100644 index 3d438f111f04..000000000000 --- a/api/sys_hostinfo.go +++ /dev/null @@ -1,58 +0,0 @@ -package api - -import ( - "context" - "errors" - "time" - - "github.com/mitchellh/mapstructure" - "github.com/shirou/gopsutil/cpu" - "github.com/shirou/gopsutil/disk" - "github.com/shirou/gopsutil/host" - "github.com/shirou/gopsutil/mem" -) - -func (c *Sys) HostInfo() (*HostInfoResponse, error) { - r := c.c.NewRequest("GET", "/v1/sys/host-info") - - ctx, cancelFunc := context.WithCancel(context.Background()) - defer cancelFunc() - resp, err := c.c.RawRequestWithContext(ctx, r) - if err != nil { - return nil, err - } - defer resp.Body.Close() - - secret, err := ParseSecret(resp.Body) - if err != nil { - return nil, err - } - if secret == nil || secret.Data == nil { - return nil, errors.New("data from server response is empty") - } - - // Parse timestamp separately since WeakDecode can't handle this field. - timestampRaw := secret.Data["timestamp"].(string) - timestamp, err := time.Parse(time.RFC3339, timestampRaw) - if err != nil { - return nil, err - } - delete(secret.Data, "timestamp") - - var result HostInfoResponse - err = mapstructure.WeakDecode(secret.Data, &result) - if err != nil { - return nil, err - } - result.Timestamp = timestamp - - return &result, err -} - -type HostInfoResponse struct { - Timestamp time.Time `json:"timestamp"` - CPU []cpu.InfoStat `json:"cpu"` - Disk []*disk.UsageStat `json:"disk"` - Host *host.InfoStat `json:"host"` - Memory *mem.VirtualMemoryStat `json:"memory"` -} diff --git a/http/sys_hostinfo_test.go b/http/sys_hostinfo_test.go index 37b7816d5138..a45031b42b3b 100644 --- a/http/sys_hostinfo_test.go +++ b/http/sys_hostinfo_test.go @@ -1,6 +1,7 @@ package http import ( + "encoding/json" "testing" "github.com/hashicorp/vault/vault" @@ -17,19 +18,29 @@ func TestSysHostInfo(t *testing.T) { vault.TestWaitActive(t, cores[0].Core) // Query against the active node, should get host information back - info, err := cores[0].Client.Sys().HostInfo() + secret, err := cores[0].Client.Logical().Read("sys/host-info") if err != nil { t.Fatal(err) } - if info == nil { - t.Fatal("expected non-nil HostInfo") + if secret == nil || secret.Data == nil { + t.Fatal("expected data in the response") + } + + dataBytes, err := json.Marshal(secret.Data) + if err != nil { + t.Fatal(err) + } + + var info vault.HostInfo + if err := json.Unmarshal(dataBytes, &info); err != nil { + t.Fatal(err) } if info.Timestamp.IsZero() { - t.Fatalf("expected a valid timestamp") + t.Fatal("expected non-zero Timestamp") } if info.CPU == nil { - t.Fatal("expected CPU info") + t.Fatal("expected non-nil CPU value") } if info.Disk == nil { t.Fatal("expected disk info") @@ -42,8 +53,8 @@ func TestSysHostInfo(t *testing.T) { } // Query against the standby, should error - info, err = cores[1].Client.Sys().HostInfo() - if err == nil || info != nil { - t.Fatalf("expected error on standby node, HostInfo: %v", info) + secret, err = cores[1].Client.Logical().Read("sys/host-info") + if err == nil || secret != nil { + t.Fatalf("expected error on standby node, HostInfo: %v", secret) } } From e858dd4dd21b7b14f7947977609e1b4fc3e63dd5 Mon Sep 17 00:00:00 2001 From: Calvin Leung Huang Date: Thu, 5 Sep 2019 12:28:07 -0700 Subject: [PATCH 09/17] ensure that we're testing failure case against a standby node --- http/sys_hostinfo_test.go | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/http/sys_hostinfo_test.go b/http/sys_hostinfo_test.go index a45031b42b3b..1fe0497bf1d5 100644 --- a/http/sys_hostinfo_test.go +++ b/http/sys_hostinfo_test.go @@ -52,7 +52,16 @@ func TestSysHostInfo(t *testing.T) { t.Fatal("expected memory info") } - // Query against the standby, should error + // Check we're standby + healthResp, err := cores[1].Client.Sys().Health() + if err != nil { + t.Fatal(err) + } + if !healthResp.Standby { + t.Fatal("expected node to be standby") + } + + // Query against a standby, should error secret, err = cores[1].Client.Logical().Read("sys/host-info") if err == nil || secret != nil { t.Fatalf("expected error on standby node, HostInfo: %v", secret) From 70bada6ec44047f5733a56bb9893f437c32a9215 Mon Sep 17 00:00:00 2001 From: Calvin Leung Huang Date: Thu, 5 Sep 2019 13:26:46 -0700 Subject: [PATCH 10/17] add and use TestWaitStandby to ensure core is on standby --- http/sys_hostinfo_test.go | 8 +------- vault/testing.go | 19 +++++++++++++++++++ 2 files changed, 20 insertions(+), 7 deletions(-) diff --git a/http/sys_hostinfo_test.go b/http/sys_hostinfo_test.go index 1fe0497bf1d5..0a37c7c8f0c0 100644 --- a/http/sys_hostinfo_test.go +++ b/http/sys_hostinfo_test.go @@ -53,13 +53,7 @@ func TestSysHostInfo(t *testing.T) { } // Check we're standby - healthResp, err := cores[1].Client.Sys().Health() - if err != nil { - t.Fatal(err) - } - if !healthResp.Standby { - t.Fatal("expected node to be standby") - } + vault.TestWaitStandby(t, cores[1].Core) // Query against a standby, should error secret, err = cores[1].Client.Logical().Read("sys/host-info") diff --git a/vault/testing.go b/vault/testing.go index 4a6c061f385b..97063b2b4a5a 100644 --- a/vault/testing.go +++ b/vault/testing.go @@ -783,6 +783,25 @@ func TestWaitActiveWithError(core *Core) error { return nil } +func TestWaitStandby(t testing.T, core *Core) { + t.Helper() + start := time.Now() + var standby bool + var err error + for time.Now().Sub(start) < 30*time.Second { + standby, err = core.Standby() + if standby { + break + } + } + if err != nil { + t.Fatal(err) + } + if !standby { + t.Fatal("should be in standby mode") + } +} + type TestCluster struct { BarrierKeys [][]byte RecoveryKeys [][]byte From 282898dfbc7ea5223f54fd4f4c4498970c0f9c23 Mon Sep 17 00:00:00 2001 From: Calvin Leung Huang Date: Thu, 5 Sep 2019 14:24:37 -0700 Subject: [PATCH 11/17] remove TestWaitStandby --- http/sys_hostinfo_test.go | 3 --- vault/testing.go | 19 ------------------- 2 files changed, 22 deletions(-) diff --git a/http/sys_hostinfo_test.go b/http/sys_hostinfo_test.go index 0a37c7c8f0c0..6b91c0f2e902 100644 --- a/http/sys_hostinfo_test.go +++ b/http/sys_hostinfo_test.go @@ -52,9 +52,6 @@ func TestSysHostInfo(t *testing.T) { t.Fatal("expected memory info") } - // Check we're standby - vault.TestWaitStandby(t, cores[1].Core) - // Query against a standby, should error secret, err = cores[1].Client.Logical().Read("sys/host-info") if err == nil || secret != nil { diff --git a/vault/testing.go b/vault/testing.go index 97063b2b4a5a..4a6c061f385b 100644 --- a/vault/testing.go +++ b/vault/testing.go @@ -783,25 +783,6 @@ func TestWaitActiveWithError(core *Core) error { return nil } -func TestWaitStandby(t testing.T, core *Core) { - t.Helper() - start := time.Now() - var standby bool - var err error - for time.Now().Sub(start) < 30*time.Second { - standby, err = core.Standby() - if standby { - break - } - } - if err != nil { - t.Fatal(err) - } - if !standby { - t.Fatal("should be in standby mode") - } -} - type TestCluster struct { BarrierKeys [][]byte RecoveryKeys [][]byte From 40a63400e5ff38f875110a772fcf0ab227a50bca Mon Sep 17 00:00:00 2001 From: Calvin Leung Huang Date: Mon, 16 Sep 2019 15:45:22 -0700 Subject: [PATCH 12/17] respond with local-only error --- http/logical.go | 1 + vault/cluster.go | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/http/logical.go b/http/logical.go index 8e4dac1d2f8e..8bea71718fb4 100644 --- a/http/logical.go +++ b/http/logical.go @@ -281,6 +281,7 @@ func handleLogicalInternal(core *vault.Core, injectDataIntoTopLevel bool) http.H // applicable to the local node. switch req.Path { case "sys/host-info": + respondError(w, http.StatusBadRequest, vault.ErrCannotForwardLocalOnly) return } diff --git a/vault/cluster.go b/vault/cluster.go index 983afdbb8b1c..364cd56d3c7e 100644 --- a/vault/cluster.go +++ b/vault/cluster.go @@ -37,7 +37,8 @@ const ( ) var ( - ErrCannotForward = errors.New("cannot forward request; no connection or address not known") + ErrCannotForward = errors.New("cannot forward request; no connection or address not known") + ErrCannotForwardLocalOnly = errors.New("cannot forward local-only request") ) type ClusterLeaderParams struct { From a85bf3dbb9ea5742743a49ebd6220c86ba81336a Mon Sep 17 00:00:00 2001 From: Calvin Leung Huang Date: Tue, 17 Sep 2019 13:14:39 -0700 Subject: [PATCH 13/17] move HostInfo into its own helper package --- {vault => helper/hostutil}/hostinfo.go | 4 ++-- {vault => helper/hostutil}/hostinfo_test.go | 6 ++---- vault/logical_system.go | 3 ++- 3 files changed, 6 insertions(+), 7 deletions(-) rename {vault => helper/hostutil}/hostinfo.go (96%) rename {vault => helper/hostutil}/hostinfo_test.go (84%) diff --git a/vault/hostinfo.go b/helper/hostutil/hostinfo.go similarity index 96% rename from vault/hostinfo.go rename to helper/hostutil/hostinfo.go index b992b14968b6..113274f17c75 100644 --- a/vault/hostinfo.go +++ b/helper/hostutil/hostinfo.go @@ -1,4 +1,4 @@ -package vault +package hostutil import ( "time" @@ -38,7 +38,7 @@ func (e *HostInfoError) Error() string { // The function does a best-effort capture on the most information possible, // continuing on capture errors encountered and appending them to a resulting // multierror.Error that gets returned at the end. -func (c *Core) CollectHostInfo() (*HostInfo, error) { +func CollectHostInfo() (*HostInfo, error) { var retErr *multierror.Error info := &HostInfo{Timestamp: time.Now().UTC()} diff --git a/vault/hostinfo_test.go b/helper/hostutil/hostinfo_test.go similarity index 84% rename from vault/hostinfo_test.go rename to helper/hostutil/hostinfo_test.go index 396e519a35dd..4d4676e31618 100644 --- a/vault/hostinfo_test.go +++ b/helper/hostutil/hostinfo_test.go @@ -1,13 +1,11 @@ -package vault +package hostutil import ( "testing" ) func TestCollectHostInfo(t *testing.T) { - c, _, _ := TestCoreUnsealed(t) - - info, err := c.CollectHostInfo() + info, err := CollectHostInfo() if err != nil { t.Fatal(err) } diff --git a/vault/logical_system.go b/vault/logical_system.go index 2cb78b611dc3..01ff50abcf22 100644 --- a/vault/logical_system.go +++ b/vault/logical_system.go @@ -25,6 +25,7 @@ import ( log "github.com/hashicorp/go-hclog" memdb "github.com/hashicorp/go-memdb" uuid "github.com/hashicorp/go-uuid" + "github.com/hashicorp/vault/helper/hostutil" "github.com/hashicorp/vault/helper/identity" "github.com/hashicorp/vault/helper/metricsutil" "github.com/hashicorp/vault/helper/namespace" @@ -2616,7 +2617,7 @@ func (b *SystemBackend) handleMetrics(ctx context.Context, req *logical.Request, // returned by the collection method will be returned as response warnings. func (b *SystemBackend) handleHostInfo(ctx context.Context, req *logical.Request, data *framework.FieldData) (*logical.Response, error) { resp := &logical.Response{} - info, err := b.Core.CollectHostInfo() + info, err := hostutil.CollectHostInfo() if err != nil { // If the error is a HostInfoError, we return them as response warnings if errs, ok := err.(*multierror.Error); ok { From eacb69e1ac50a687c9ca392446a755db16c1f2a0 Mon Sep 17 00:00:00 2001 From: Calvin Leung Huang Date: Tue, 17 Sep 2019 13:27:01 -0700 Subject: [PATCH 14/17] fix imports; use new no-forward handler --- http/handler.go | 5 ++++- http/sys_hostinfo_test.go | 3 ++- vault/logical_system.go | 2 +- 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/http/handler.go b/http/handler.go index 462698b994f8..a51227c4eef3 100644 --- a/http/handler.go +++ b/http/handler.go @@ -110,7 +110,10 @@ func Handler(props *vault.HandlerProperties) http.Handler { // Create the muxer to handle the actual endpoints mux := http.NewServeMux() - mux.Handle("/v1/sys/host-info", handleLogical(core)) + + // Handle non-forwarded paths + mux.Handle("/v1/sys/host-info", handleLogicalNoForward(core)) + mux.Handle("/v1/sys/init", handleSysInit(core)) mux.Handle("/v1/sys/seal-status", handleSysSealStatus(core)) mux.Handle("/v1/sys/seal", handleSysSeal(core)) diff --git a/http/sys_hostinfo_test.go b/http/sys_hostinfo_test.go index 6b91c0f2e902..af313a382b2b 100644 --- a/http/sys_hostinfo_test.go +++ b/http/sys_hostinfo_test.go @@ -4,6 +4,7 @@ import ( "encoding/json" "testing" + "github.com/hashicorp/vault/helper/hostutil" "github.com/hashicorp/vault/vault" ) @@ -31,7 +32,7 @@ func TestSysHostInfo(t *testing.T) { t.Fatal(err) } - var info vault.HostInfo + var info hostutil.HostInfo if err := json.Unmarshal(dataBytes, &info); err != nil { t.Fatal(err) } diff --git a/vault/logical_system.go b/vault/logical_system.go index 01ff50abcf22..8cdf7077bd1c 100644 --- a/vault/logical_system.go +++ b/vault/logical_system.go @@ -2623,7 +2623,7 @@ func (b *SystemBackend) handleHostInfo(ctx context.Context, req *logical.Request if errs, ok := err.(*multierror.Error); ok { var warnings []string for _, mErr := range errs.Errors { - if errwrap.ContainsType(mErr, new(HostInfoError)) { + if errwrap.ContainsType(mErr, new(hostutil.HostInfoError)) { warnings = append(warnings, mErr.Error()) } else { // If the error is a multierror, it should only be for From b717d9702e7dbaa37cec9b325efeb4f9c6cd84d9 Mon Sep 17 00:00:00 2001 From: Calvin Leung Huang Date: Wed, 18 Sep 2019 14:28:57 -0700 Subject: [PATCH 15/17] add cpu times to collection --- helper/hostutil/hostinfo.go | 13 ++++++++++++- helper/hostutil/hostinfo_test.go | 3 +++ vault/logical_system.go | 3 +++ 3 files changed, 18 insertions(+), 1 deletion(-) diff --git a/helper/hostutil/hostinfo.go b/helper/hostutil/hostinfo.go index 113274f17c75..7e9e3b3d6e38 100644 --- a/helper/hostutil/hostinfo.go +++ b/helper/hostutil/hostinfo.go @@ -10,10 +10,14 @@ import ( "github.com/shirou/gopsutil/mem" ) -// HostInfo holds all the information that gets captured on the host. +// HostInfo holds all the information that gets captured on the host. The +// set of information captured depends on the host operating system. For more +// information, refer to: https://github.com/shirou/gopsutil#current-status type HostInfo struct { + // Timestamp returns the timestamp in UTC on the collection time. Timestamp time.Time `json:"timestamp"` CPU []cpu.InfoStat `json:"cpu"` + CPUTimes []cpu.TimesStat `json:"cpu_times"` Disk []*disk.UsageStat `json:"disk"` Host *host.InfoStat `json:"host"` Memory *mem.VirtualMemoryStat `json:"memory"` @@ -77,5 +81,12 @@ func CollectHostInfo() (*HostInfo, error) { info.CPU = c } + t, err := cpu.Times(true) + if err != nil { + retErr = multierror.Append(retErr, &HostInfoError{err}) + } else { + info.CPUTimes = t + } + return info, retErr.ErrorOrNil() } diff --git a/helper/hostutil/hostinfo_test.go b/helper/hostutil/hostinfo_test.go index 4d4676e31618..a96307b4b255 100644 --- a/helper/hostutil/hostinfo_test.go +++ b/helper/hostutil/hostinfo_test.go @@ -15,6 +15,9 @@ func TestCollectHostInfo(t *testing.T) { if info.CPU == nil { t.Fatal("expected non-nil CPU value") } + if info.CPUTimes == nil { + t.Fatal("expected non-nil CPUTimes value") + } if info.Disk == nil { t.Fatal("expected non-nil Disk value") } diff --git a/vault/logical_system.go b/vault/logical_system.go index 8cdf7077bd1c..4c9c615ea6ea 100644 --- a/vault/logical_system.go +++ b/vault/logical_system.go @@ -2648,6 +2648,9 @@ func (b *SystemBackend) handleHostInfo(ctx context.Context, req *logical.Request if info.CPU != nil { respData["cpu"] = info.CPU } + if info.CPUTimes != nil { + respData["cpu_times"] = info.CPUTimes + } if info.Disk != nil { respData["disk"] = info.Disk } From ad5dd01f0f594044c7eea3e86f0f6312566c85a1 Mon Sep 17 00:00:00 2001 From: Calvin Leung Huang Date: Wed, 18 Sep 2019 15:04:09 -0700 Subject: [PATCH 16/17] emit clearer multierrors/warnings by collection type --- helper/hostutil/hostinfo.go | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/helper/hostutil/hostinfo.go b/helper/hostutil/hostinfo.go index 7e9e3b3d6e38..62d335220a6f 100644 --- a/helper/hostutil/hostinfo.go +++ b/helper/hostutil/hostinfo.go @@ -1,6 +1,7 @@ package hostutil import ( + "fmt" "time" "github.com/hashicorp/go-multierror" @@ -25,7 +26,8 @@ type HostInfo struct { // HostInfoError is a typed error for more convenient error checking. type HostInfoError struct { - Err error + Type string + Err error } func (e *HostInfoError) WrappedErrors() []error { @@ -33,7 +35,7 @@ func (e *HostInfoError) WrappedErrors() []error { } func (e *HostInfoError) Error() string { - return e.Err.Error() + return fmt.Sprintf("%s: %s", e.Type, e.Err.Error()) } // CollectHostInfo returns information on the host, which includes general @@ -47,26 +49,26 @@ func CollectHostInfo() (*HostInfo, error) { info := &HostInfo{Timestamp: time.Now().UTC()} if h, err := host.Info(); err != nil { - retErr = multierror.Append(retErr, &HostInfoError{err}) + retErr = multierror.Append(retErr, &HostInfoError{"host", err}) } else { info.Host = h } if v, err := mem.VirtualMemory(); err != nil { - retErr = multierror.Append(retErr, &HostInfoError{err}) + retErr = multierror.Append(retErr, &HostInfoError{"memory", err}) } else { info.Memory = v } parts, err := disk.Partitions(false) if err != nil { - retErr = multierror.Append(retErr, &HostInfoError{err}) + retErr = multierror.Append(retErr, &HostInfoError{"disk", err}) } else { var usage []*disk.UsageStat - for _, part := range parts { + for i, part := range parts { u, err := disk.Usage(part.Mountpoint) if err != nil { - retErr = multierror.Append(retErr, &HostInfoError{err}) + retErr = multierror.Append(retErr, &HostInfoError{fmt.Sprintf("disk.%d", i), err}) continue } usage = append(usage, u) @@ -76,14 +78,14 @@ func CollectHostInfo() (*HostInfo, error) { } if c, err := cpu.Info(); err != nil { - retErr = multierror.Append(retErr, &HostInfoError{err}) + retErr = multierror.Append(retErr, &HostInfoError{"cpu", err}) } else { info.CPU = c } t, err := cpu.Times(true) if err != nil { - retErr = multierror.Append(retErr, &HostInfoError{err}) + retErr = multierror.Append(retErr, &HostInfoError{"cpu_times", err}) } else { info.CPUTimes = t } From ad7788b30ddd62f3330302d4c6a6344559c4d910 Mon Sep 17 00:00:00 2001 From: Calvin Leung Huang Date: Wed, 18 Sep 2019 15:15:29 -0700 Subject: [PATCH 17/17] add comments on HostInfo fields --- helper/hostutil/hostinfo.go | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/helper/hostutil/hostinfo.go b/helper/hostutil/hostinfo.go index 62d335220a6f..e67c788dc7d6 100644 --- a/helper/hostutil/hostinfo.go +++ b/helper/hostutil/hostinfo.go @@ -16,12 +16,19 @@ import ( // information, refer to: https://github.com/shirou/gopsutil#current-status type HostInfo struct { // Timestamp returns the timestamp in UTC on the collection time. - Timestamp time.Time `json:"timestamp"` - CPU []cpu.InfoStat `json:"cpu"` - CPUTimes []cpu.TimesStat `json:"cpu_times"` - Disk []*disk.UsageStat `json:"disk"` - Host *host.InfoStat `json:"host"` - Memory *mem.VirtualMemoryStat `json:"memory"` + Timestamp time.Time `json:"timestamp"` + // CPU returns information about the CPU such as family, model, cores, etc. + CPU []cpu.InfoStat `json:"cpu"` + // CPUTimes returns statistics on CPU usage represented in Jiffies. + CPUTimes []cpu.TimesStat `json:"cpu_times"` + // Disk returns statitics on disk usage for all accessible partitions. + Disk []*disk.UsageStat `json:"disk"` + // Host returns general host information such as hostname, platform, uptime, + // kernel version, etc. + Host *host.InfoStat `json:"host"` + // Memory contains statistics about the memory such as total, available, and + // used memory in number of bytes. + Memory *mem.VirtualMemoryStat `json:"memory"` } // HostInfoError is a typed error for more convenient error checking.