Add fleetctl get host capability to get single host with labels

Getting a single host with `fleetctl get host foobar` will look up the
host with the matching hostname, uuid, osquery identifier, or node key,
and provide the full host details along with the labels the host is a
member of.
This commit is contained in:
Zachary Wasserman 2020-04-22 13:54:32 -07:00 committed by Zachary Wasserman
parent 7f757d3144
commit fcb8418b2f
13 changed files with 217 additions and 34 deletions

View File

@ -575,43 +575,57 @@ func getHostsCommand() cli.Command {
return err
}
hosts, err := fleet.GetHosts()
if err != nil {
return errors.Wrap(err, "could not list hosts")
}
identifier := c.Args().First()
if len(hosts) == 0 {
fmt.Println("no hosts found")
return nil
}
if c.Bool(jsonFlagName) || c.Bool(yamlFlagName) {
for _, host := range hosts {
err = printHost(c, &host.Host)
if err != nil {
return err
}
if identifier == "" {
hosts, err := fleet.GetHosts()
if err != nil {
return errors.Wrap(err, "could not list hosts")
}
return nil
if len(hosts) == 0 {
fmt.Println("no hosts found")
return nil
}
if c.Bool(jsonFlagName) || c.Bool(yamlFlagName) {
for _, host := range hosts {
err = printHost(c, &host.Host)
if err != nil {
return err
}
}
return nil
}
// Default to printing as table
data := [][]string{}
for _, host := range hosts {
data = append(data, []string{
host.Host.UUID,
host.DisplayText,
host.Host.Platform,
string(host.Status),
})
}
table := defaultTable()
table.SetHeader([]string{"uuid", "hostname", "platform", "status"})
table.AppendBulk(data)
table.Render()
} else {
host, err := fleet.HostByIdentifier(identifier)
if err != nil {
return errors.Wrap(err, "could not get host")
}
b, err := yaml.Marshal(host)
if err != nil {
return err
}
fmt.Print(string(b))
}
// Default to printing as table
data := [][]string{}
for _, host := range hosts {
data = append(data, []string{
host.Host.UUID,
host.DisplayText,
host.Host.Platform,
string(host.Status),
})
}
table := defaultTable()
table.SetHeader([]string{"uuid", "hostname", "platform", "status"})
table.AppendBulk(data)
table.Render()
return nil
},
}

View File

@ -500,6 +500,7 @@ func testHostIDsByName(t *testing.T, ds kolide.Datastore) {
func testHostAdditional(t *testing.T, ds kolide.Datastore) {
_, err := ds.NewHost(&kolide.Host{
DetailUpdateTime: time.Now(),
LabelUpdateTime: time.Now(),
SeenTime: time.Now(),
LabelUpdateTime: time.Now(),
OsqueryHostID: "foobar",
@ -567,3 +568,41 @@ func testHostAdditional(t *testing.T, ds kolide.Datastore) {
require.Nil(t, err)
assert.Equal(t, additional, *h.Additional)
}
func testHostByIdentifier(t *testing.T, ds kolide.Datastore) {
for i := 1; i <= 10; i++ {
_, err := ds.NewHost(&kolide.Host{
DetailUpdateTime: time.Now(),
LabelUpdateTime: time.Now(),
SeenTime: time.Now(),
OsqueryHostID: fmt.Sprintf("osquery_host_id_%d", i),
NodeKey: fmt.Sprintf("node_key_%d", i),
UUID: fmt.Sprintf("uuid_%d", i),
HostName: fmt.Sprintf("hostname_%d", i),
})
require.Nil(t, err)
}
var (
h *kolide.Host
err error
)
h, err = ds.HostByIdentifier("uuid_1")
require.NoError(t, err)
assert.Equal(t, uint(1), h.ID)
h, err = ds.HostByIdentifier("osquery_host_id_2")
require.NoError(t, err)
assert.Equal(t, uint(2), h.ID)
h, err = ds.HostByIdentifier("node_key_4")
require.NoError(t, err)
assert.Equal(t, uint(4), h.ID)
h, err = ds.HostByIdentifier("hostname_7")
require.NoError(t, err)
assert.Equal(t, uint(7), h.ID)
h, err = ds.HostByIdentifier("foobar")
require.Error(t, err)
}

View File

@ -44,6 +44,7 @@ var testFunctions = [...]func(*testing.T, kolide.Datastore){
testListHostsInPack,
testListPacksForHost,
testHostIDsByName,
testHostByIdentifier,
testListPacks,
testDistributedQueryCampaign,
testCleanupDistributedQueryCampaigns,

View File

@ -284,6 +284,7 @@ func (d *Datastore) AuthenticateHost(nodeKey string) (*kolide.Host, error) {
deleted_at,
deleted,
detail_update_time,
label_update_time,
node_key,
host_name,
uuid,
@ -480,3 +481,18 @@ func (d *Datastore) HostIDsByName(hostnames []string) ([]uint, error) {
return hostIDs, nil
}
func (d *Datastore) HostByIdentifier(identifier string) (*kolide.Host, error) {
sql := `
SELECT * FROM hosts
WHERE ? IN (host_name, osquery_host_id, node_key, uuid)
LIMIT 1
`
host := &kolide.Host{}
err := d.db.Get(host, sql, identifier)
if err != nil {
return nil, errors.Wrap(err, "get host by identifier")
}
return host, nil
}

View File

@ -67,6 +67,10 @@ type HostStore interface {
GenerateHostStatusStatistics(now time.Time) (online, offline, mia, new uint, err error)
// HostIDsByName Retrieve the IDs associated with the given hostnames
HostIDsByName(hostnames []string) ([]uint, error)
// HostByIdentifier returns one host matching the provided identifier.
// Possible matches can be on osquery_host_identifier, node_key, UUID, or
// hostname.
HostByIdentifier(identifier string) (*Host, error)
}
type HostService interface {
@ -74,6 +78,10 @@ type HostService interface {
GetHost(ctx context.Context, id uint) (host *Host, err error)
GetHostSummary(ctx context.Context) (summary *HostSummary, err error)
DeleteHost(ctx context.Context, id uint) (err error)
// HostByIdentifier returns one host matching the provided identifier.
// Possible matches can be on osquery_host_identifier, node_key, UUID, or
// hostname.
HostByIdentifier(ctx context.Context, identifier string) (*Host, error)
}
type HostListOptions struct {

View File

@ -76,6 +76,9 @@ type LabelService interface {
// given ID.
ListHostsInLabel(ctx context.Context, lid uint, opt ListOptions) ([]Host, error)
// LabelsForHost returns the labels that the given host is in.
ListLabelsForHost(ctx context.Context, hid uint) ([]Label, error)
// HostIDsForLabel returns ids of hosts that belong to the label identified
// by lid
HostIDsForLabel(lid uint) ([]uint, error)

View File

@ -18,6 +18,8 @@ type DeleteHostFunc func(hid uint) error
type HostFunc func(id uint) (*kolide.Host, error)
type HostByIdentifierFunc func(identifier string) (*kolide.Host, error)
type ListHostsFunc func(opt kolide.HostListOptions) ([]*kolide.Host, error)
type EnrollHostFunc func(osqueryHostId, nodeKey, secretName string) (*kolide.Host, error)
@ -49,6 +51,9 @@ type HostStore struct {
HostFunc HostFunc
HostFuncInvoked bool
HostByIdentifierFunc HostByIdentifierFunc
HostByIdentifierFuncInvoked bool
ListHostsFunc ListHostsFunc
ListHostsFuncInvoked bool
@ -97,6 +102,11 @@ func (s *HostStore) Host(id uint) (*kolide.Host, error) {
return s.HostFunc(id)
}
func (s *HostStore) HostByIdentifier(identifier string) (*kolide.Host, error) {
s.HostByIdentifierFuncInvoked = true
return s.HostByIdentifierFunc(identifier)
}
func (s *HostStore) ListHosts(opt kolide.HostListOptions) ([]*kolide.Host, error) {
s.ListHostsFuncInvoked = true
return s.ListHostsFunc(opt)

View File

@ -33,3 +33,31 @@ func (c *Client) GetHosts() ([]HostResponse, error) {
return responseBody.Hosts, nil
}
// HostByIdentifier retrieves a host by the uuid, osquery_host_id, hostname, or
// node_key.
func (c *Client) HostByIdentifier(identifier string) (*HostResponse, error) {
response, err := c.AuthenticatedDo("GET", "/api/v1/kolide/hosts/identifier/"+identifier, nil)
if err != nil {
return nil, errors.Wrap(err, "GET /api/v1/kolide/hosts/identifier")
}
defer response.Body.Close()
if response.StatusCode != http.StatusOK {
return nil, errors.Errorf(
"get host by identifier received status %d %s",
response.StatusCode,
extractServerErrorText(response.Body),
)
}
var responseBody getHostResponse
err = json.NewDecoder(response.Body).Decode(&responseBody)
if err != nil {
return nil, errors.Wrap(err, "decode host response")
}
if responseBody.Err != nil {
return nil, errors.Errorf("get host by identifier: %s", responseBody.Err)
}
return responseBody.Host, nil
}

View File

@ -6,6 +6,7 @@ import (
"github.com/go-kit/kit/endpoint"
"github.com/kolide/fleet/server/kolide"
"github.com/pkg/errors"
)
// HostResponse is the response struct that contains the full host information
@ -15,6 +16,7 @@ type HostResponse struct {
kolide.Host
Status kolide.HostStatus `json:"status"`
DisplayText string `json:"display_text"`
Labels []kolide.Label `json:"labels,omitempty"`
}
func hostResponseForHost(ctx context.Context, svc kolide.Service, host *kolide.Host) (*HostResponse, error) {
@ -25,6 +27,20 @@ func hostResponseForHost(ctx context.Context, svc kolide.Service, host *kolide.H
}, nil
}
func addLabelsToHost(ctx context.Context, svc kolide.Service, host *kolide.Host) (*HostResponse, error) {
labels, err := svc.ListLabelsForHost(ctx, host.ID)
if err != nil {
return nil, errors.Wrap(err, "list labels for host")
}
return &HostResponse{
Host: *host,
Status: host.Status(time.Now()),
DisplayText: host.HostName,
Labels: labels,
}, nil
}
////////////////////////////////////////////////////////////////////////////////
// Get Host
////////////////////////////////////////////////////////////////////////////////
@ -59,6 +75,33 @@ func makeGetHostEndpoint(svc kolide.Service) endpoint.Endpoint {
}
}
////////////////////////////////////////////////////////////////////////////////
// Get Host
////////////////////////////////////////////////////////////////////////////////
type hostByIdentifierRequest struct {
Identifier string `json:"identifier"`
}
func makeHostByIdentifierEndpoint(svc kolide.Service) endpoint.Endpoint {
return func(ctx context.Context, request interface{}) (interface{}, error) {
req := request.(hostByIdentifierRequest)
host, err := svc.HostByIdentifier(ctx, req.Identifier)
if err != nil {
return getHostResponse{Err: err}, nil
}
resp, err := addLabelsToHost(ctx, svc, host)
if err != nil {
return getHostResponse{Err: err}, nil
}
return getHostResponse{
Host: resp,
}, nil
}
}
////////////////////////////////////////////////////////////////////////////////
// List Hosts
////////////////////////////////////////////////////////////////////////////////

View File

@ -84,6 +84,7 @@ type KolideEndpoints struct {
GetLabelSpecs endpoint.Endpoint
GetLabelSpec endpoint.Endpoint
GetHost endpoint.Endpoint
HostByIdentifier endpoint.Endpoint
DeleteHost endpoint.Endpoint
ListHosts endpoint.Endpoint
GetHostSummary endpoint.Endpoint
@ -168,6 +169,7 @@ func MakeKolideServerEndpoints(svc kolide.Service, jwtKey, urlPrefix string) Kol
GetPackSpecs: authenticatedUser(jwtKey, svc, makeGetPackSpecsEndpoint(svc)),
GetPackSpec: authenticatedUser(jwtKey, svc, makeGetPackSpecEndpoint(svc)),
GetHost: authenticatedUser(jwtKey, svc, makeGetHostEndpoint(svc)),
HostByIdentifier: authenticatedUser(jwtKey, svc, makeHostByIdentifierEndpoint(svc)),
ListHosts: authenticatedUser(jwtKey, svc, makeListHostsEndpoint(svc)),
GetHostSummary: authenticatedUser(jwtKey, svc, makeGetHostSummaryEndpoint(svc)),
DeleteHost: authenticatedUser(jwtKey, svc, makeDeleteHostEndpoint(svc)),
@ -269,6 +271,7 @@ type kolideHandlers struct {
GetLabelSpecs http.Handler
GetLabelSpec http.Handler
GetHost http.Handler
HostByIdentifier http.Handler
DeleteHost http.Handler
ListHosts http.Handler
GetHostSummary http.Handler
@ -357,6 +360,7 @@ func makeKolideKitHandlers(e KolideEndpoints, opts []kithttp.ServerOption) *koli
GetLabelSpecs: newServer(e.GetLabelSpecs, decodeNoParamsRequest),
GetLabelSpec: newServer(e.GetLabelSpec, decodeGetGenericSpecRequest),
GetHost: newServer(e.GetHost, decodeGetHostRequest),
HostByIdentifier: newServer(e.HostByIdentifier, decodeHostByIdentifierRequest),
DeleteHost: newServer(e.DeleteHost, decodeDeleteHostRequest),
ListHosts: newServer(e.ListHosts, decodeListHostsRequest),
GetHostSummary: newServer(e.GetHostSummary, decodeNoParamsRequest),
@ -489,6 +493,7 @@ func attachKolideAPIRoutes(r *mux.Router, h *kolideHandlers) {
r.Handle("/api/v1/kolide/hosts", h.ListHosts).Methods("GET").Name("list_hosts")
r.Handle("/api/v1/kolide/host_summary", h.GetHostSummary).Methods("GET").Name("get_host_summary")
r.Handle("/api/v1/kolide/hosts/{id}", h.GetHost).Methods("GET").Name("get_host")
r.Handle("/api/v1/kolide/hosts/identifier/{identifier}", h.HostByIdentifier).Methods("GET").Name("host_by_identifier")
r.Handle("/api/v1/kolide/hosts/{id}", h.DeleteHost).Methods("DELETE").Name("delete_host")
r.Handle("/api/v1/kolide/spec/osquery_options", h.ApplyOsqueryOptionsSpec).Methods("POST").Name("apply_osquery_options_spec")

View File

@ -14,6 +14,10 @@ func (svc service) GetHost(ctx context.Context, id uint) (*kolide.Host, error) {
return svc.ds.Host(id)
}
func (svc service) HostByIdentifier(ctx context.Context, identifier string) (*kolide.Host, error) {
return svc.ds.HostByIdentifier(identifier)
}
func (svc service) GetHostSummary(ctx context.Context) (*kolide.HostSummary, error) {
online, offline, mia, new, err := svc.ds.GenerateHostStatusStatistics(svc.clock.Now())
if err != nil {

View File

@ -94,6 +94,10 @@ func (svc service) ListHostsInLabel(ctx context.Context, lid uint, opt kolide.Li
return svc.ds.ListHostsInLabel(lid, opt)
}
func (svc service) ListLabelsForHost(ctx context.Context, hid uint) ([]kolide.Label, error) {
return svc.ds.ListLabelsForHost(hid)
}
func (svc service) HostIDsForLabel(lid uint) ([]uint, error) {
hosts, err := svc.ds.ListHostsInLabel(lid, kolide.ListOptions{})
if err != nil {

View File

@ -16,6 +16,14 @@ func decodeGetHostRequest(ctx context.Context, r *http.Request) (interface{}, er
return getHostRequest{ID: id}, nil
}
func decodeHostByIdentifierRequest(ctx context.Context, r *http.Request) (interface{}, error) {
identifier, err := nameFromRequest(r, "identifier")
if err != nil {
return nil, err
}
return hostByIdentifierRequest{Identifier: identifier}, nil
}
func decodeDeleteHostRequest(ctx context.Context, r *http.Request) (interface{}, error) {
id, err := idFromRequest(r, "id")
if err != nil {