diff --git a/api/go.mod b/api/go.mod index 8f92a43bc..bd9429894 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.20190919081434-645ac174deeb 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 43c170e62..34c284e2d 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= @@ -67,6 +69,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= @@ -92,8 +97,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/go.mod b/go.mod index befb49285..015fb8b90 100644 --- a/go.mod +++ b/go.mod @@ -91,6 +91,7 @@ require ( github.com/joyent/triton-go v0.0.0-20190112182421-51ffac552869 github.com/keybase/go-crypto v0.0.0-20190403132359-d65b6b94177f github.com/kr/pretty v0.1.0 + github.com/kr/pty v1.1.3 // indirect github.com/kr/text v0.1.0 github.com/lib/pq v1.2.0 github.com/mattn/go-colorable v0.1.2 @@ -115,6 +116,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 62b14cd48..1f6a6a688 100644 --- a/go.sum +++ b/go.sum @@ -33,6 +33,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= @@ -167,6 +168,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= @@ -551,6 +553,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/helper/hostutil/hostinfo.go b/helper/hostutil/hostinfo.go new file mode 100644 index 000000000..e67c788dc --- /dev/null +++ b/helper/hostutil/hostinfo.go @@ -0,0 +1,101 @@ +package hostutil + +import ( + "fmt" + "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" +) + +// 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 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. +type HostInfoError struct { + Type string + Err error +} + +func (e *HostInfoError) WrappedErrors() []error { + return []error{e.Err} +} + +func (e *HostInfoError) Error() string { + return fmt.Sprintf("%s: %s", e.Type, 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 CollectHostInfo() (*HostInfo, error) { + var retErr *multierror.Error + info := &HostInfo{Timestamp: time.Now().UTC()} + + if h, err := host.Info(); err != nil { + retErr = multierror.Append(retErr, &HostInfoError{"host", err}) + } else { + info.Host = h + } + + if v, err := mem.VirtualMemory(); err != nil { + retErr = multierror.Append(retErr, &HostInfoError{"memory", err}) + } else { + info.Memory = v + } + + parts, err := disk.Partitions(false) + if err != nil { + retErr = multierror.Append(retErr, &HostInfoError{"disk", err}) + } else { + var usage []*disk.UsageStat + for i, part := range parts { + u, err := disk.Usage(part.Mountpoint) + if err != nil { + retErr = multierror.Append(retErr, &HostInfoError{fmt.Sprintf("disk.%d", i), err}) + continue + } + usage = append(usage, u) + + } + info.Disk = usage + } + + if c, err := cpu.Info(); err != nil { + retErr = multierror.Append(retErr, &HostInfoError{"cpu", err}) + } else { + info.CPU = c + } + + t, err := cpu.Times(true) + if err != nil { + retErr = multierror.Append(retErr, &HostInfoError{"cpu_times", err}) + } else { + info.CPUTimes = t + } + + return info, retErr.ErrorOrNil() +} diff --git a/helper/hostutil/hostinfo_test.go b/helper/hostutil/hostinfo_test.go new file mode 100644 index 000000000..a96307b4b --- /dev/null +++ b/helper/hostutil/hostinfo_test.go @@ -0,0 +1,30 @@ +package hostutil + +import ( + "testing" +) + +func TestCollectHostInfo(t *testing.T) { + info, err := CollectHostInfo() + if err != nil { + t.Fatal(err) + } + if info.Timestamp.IsZero() { + t.Fatal("expected non-zero Timestamp") + } + 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") + } + 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/http/handler.go b/http/handler.go index ef568065b..83cc087df 100644 --- a/http/handler.go +++ b/http/handler.go @@ -111,8 +111,9 @@ func Handler(props *vault.HandlerProperties) http.Handler { // Create the muxer to handle the actual endpoints mux := http.NewServeMux() - // Handle pprof paths + // Handle non-forwarded paths mux.Handle("/v1/sys/pprof/", handleLogicalNoForward(core)) + mux.Handle("/v1/sys/host-info", handleLogicalNoForward(core)) mux.Handle("/v1/sys/init", handleSysInit(core)) mux.Handle("/v1/sys/seal-status", handleSysSealStatus(core)) diff --git a/http/sys_hostinfo_test.go b/http/sys_hostinfo_test.go new file mode 100644 index 000000000..af313a382 --- /dev/null +++ b/http/sys_hostinfo_test.go @@ -0,0 +1,61 @@ +package http + +import ( + "encoding/json" + "testing" + + "github.com/hashicorp/vault/helper/hostutil" + "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 + secret, err := cores[0].Client.Logical().Read("sys/host-info") + if err != nil { + t.Fatal(err) + } + 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 hostutil.HostInfo + if err := json.Unmarshal(dataBytes, &info); err != nil { + t.Fatal(err) + } + + if info.Timestamp.IsZero() { + t.Fatal("expected non-zero Timestamp") + } + if info.CPU == nil { + t.Fatal("expected non-nil CPU value") + } + 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 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) + } +} diff --git a/vault/logical_system.go b/vault/logical_system.go index d92a96009..78fa3fd67 100644 --- a/vault/logical_system.go +++ b/vault/logical_system.go @@ -18,12 +18,14 @@ import ( "sync" "time" + "github.com/hashicorp/go-multierror" "github.com/hashicorp/vault/physical/raft" "github.com/hashicorp/errwrap" 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" @@ -167,6 +169,7 @@ func NewSystemBackend(core *Core, logger log.Logger) *SystemBackend { b.Backend.Paths = append(b.Backend.Paths, b.pprofPaths()...) 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{ @@ -2610,6 +2613,59 @@ 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 := hostutil.CollectHostInfo() + if err != nil { + // 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(hostutil.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 + } else { + return nil, err + } + } + + if info == nil { + return nil, errors.New("unable to collect host information: nil HostInfo") + } + + respData := map[string]interface{}{ + "timestamp": info.Timestamp, + } + 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 + } + if info.Host != nil { + respData["host"] = info.Host + } + if info.Memory != nil { + respData["memory"] = info.Memory + } + resp.Data = respData + + return resp, 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 @@ -4052,4 +4108,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 4a7e56f4c..1fc010785 100644 --- a/vault/logical_system_paths.go +++ b/vault/logical_system_paths.go @@ -1166,6 +1166,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{ {