sys: add host-info endpoint (#7330)

* sys: add host-info endpoint, add client API method

* remove old commented handler

* add http tests, fix bugs

* query all partitions for disk usage

* fix Timestamp decoding

* add comments for clarification

* dont append a nil entry on disk usage query error

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

* ensure that we're testing failure case against a standby node

* add and use TestWaitStandby to ensure core is on standby

* remove TestWaitStandby

* respond with local-only error

* move HostInfo into its own helper package

* fix imports; use new no-forward handler

* add cpu times to collection

* emit clearer multierrors/warnings by collection type

* add comments on HostInfo fields
This commit is contained in:
Calvin Leung Huang 2019-10-03 09:43:52 -07:00 committed by GitHub
parent a726e71e20
commit 3f1c7c86a0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 288 additions and 1 deletions

View File

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

View File

@ -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=

2
go.mod
View File

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

7
go.sum
View File

@ -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=

101
helper/hostutil/hostinfo.go Normal file
View File

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

View File

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

View File

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

61
http/sys_hostinfo_test.go Normal file
View File

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

View File

@ -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`,
},
}

View File

@ -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{
{