diff --git a/cmd/fleetctl/get.go b/cmd/fleetctl/get.go index d4c0bc136..c101f1ac0 100644 --- a/cmd/fleetctl/get.go +++ b/cmd/fleetctl/get.go @@ -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 }, } diff --git a/server/datastore/datastore_hosts_test.go b/server/datastore/datastore_hosts_test.go index 7785ccf9e..72c885513 100644 --- a/server/datastore/datastore_hosts_test.go +++ b/server/datastore/datastore_hosts_test.go @@ -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) +} diff --git a/server/datastore/datastore_test.go b/server/datastore/datastore_test.go index b622a79e3..8e020393a 100644 --- a/server/datastore/datastore_test.go +++ b/server/datastore/datastore_test.go @@ -44,6 +44,7 @@ var testFunctions = [...]func(*testing.T, kolide.Datastore){ testListHostsInPack, testListPacksForHost, testHostIDsByName, + testHostByIdentifier, testListPacks, testDistributedQueryCampaign, testCleanupDistributedQueryCampaigns, diff --git a/server/datastore/mysql/hosts.go b/server/datastore/mysql/hosts.go index 30e3286df..7ef9b2b00 100644 --- a/server/datastore/mysql/hosts.go +++ b/server/datastore/mysql/hosts.go @@ -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 +} diff --git a/server/kolide/hosts.go b/server/kolide/hosts.go index 4db4db655..a920eef11 100644 --- a/server/kolide/hosts.go +++ b/server/kolide/hosts.go @@ -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 { diff --git a/server/kolide/labels.go b/server/kolide/labels.go index 3298950a2..d34e7c9e4 100644 --- a/server/kolide/labels.go +++ b/server/kolide/labels.go @@ -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) diff --git a/server/mock/datastore_hosts.go b/server/mock/datastore_hosts.go index 79bf18fc3..fe7c7fe02 100644 --- a/server/mock/datastore_hosts.go +++ b/server/mock/datastore_hosts.go @@ -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) diff --git a/server/service/client_hosts.go b/server/service/client_hosts.go index e41a86ce9..7823bd6f3 100644 --- a/server/service/client_hosts.go +++ b/server/service/client_hosts.go @@ -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 +} diff --git a/server/service/endpoint_hosts.go b/server/service/endpoint_hosts.go index d8b111fd9..3e52286e0 100644 --- a/server/service/endpoint_hosts.go +++ b/server/service/endpoint_hosts.go @@ -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 //////////////////////////////////////////////////////////////////////////////// diff --git a/server/service/handler.go b/server/service/handler.go index 4fbd74b2a..ed8f71d7b 100644 --- a/server/service/handler.go +++ b/server/service/handler.go @@ -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") diff --git a/server/service/service_hosts.go b/server/service/service_hosts.go index e051b74ce..c2a8f7cd7 100644 --- a/server/service/service_hosts.go +++ b/server/service/service_hosts.go @@ -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 { diff --git a/server/service/service_labels.go b/server/service/service_labels.go index 8182a0644..980054112 100644 --- a/server/service/service_labels.go +++ b/server/service/service_labels.go @@ -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 { diff --git a/server/service/transport_hosts.go b/server/service/transport_hosts.go index 42c9cf7e6..1be2efee8 100644 --- a/server/service/transport_hosts.go +++ b/server/service/transport_hosts.go @@ -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 {