From e6b1ed9ade8dde47a2b68d3684d8a12cdb29d517 Mon Sep 17 00:00:00 2001 From: John Murphy Date: Wed, 7 Dec 2016 01:37:22 +0800 Subject: [PATCH] Added MIA status for hosts that haven't been updated for 30 days (#570) --- server/kolide/targets.go | 19 ++++++- server/service/endpoint_hosts.go | 17 ++++++ server/service/endpoint_labels.go | 32 +++++++---- server/service/endpoint_targets.go | 32 ++++++----- server/service/service_campaigns.go | 18 ++++-- server/service/service_hosts.go | 13 +++-- server/service/service_targets.go | 23 +++++--- server/service/service_targets_test.go | 77 +++++++++++++++++--------- 8 files changed, 159 insertions(+), 72 deletions(-) diff --git a/server/kolide/targets.go b/server/kolide/targets.go index 9362d75adc..2594c647fc 100644 --- a/server/kolide/targets.go +++ b/server/kolide/targets.go @@ -9,6 +9,19 @@ type TargetSearchResults struct { Labels []Label } +// TargetMetrics contains information about the state +// of hosts that are tracked by the app +type TargetMetrics struct { + TotalHosts uint + // OnlineHosts have updated within the last 30 minutes + OnlineHosts uint + // OfflineHosts are hosts that haven't updated in 30 minutes + OfflineHosts uint + // MissingInActionHosts are hosts that haven't had an update for more + // than thirty days + MissingInActionHosts uint +} + type TargetService interface { // SearchTargets will accept a search query, a slice of IDs of hosts to omit, // and a slice of IDs of labels to omit, and it will return a set of targets @@ -17,8 +30,10 @@ type TargetService interface { // CountHostsInTargets returns the count of hosts in the selected // targets. The first return uint is the total number of hosts in the - // targets. The second return uint is the total online hosts. - CountHostsInTargets(ctx context.Context, hostIDs []uint, labelIDs []uint) (total uint, online uint, err error) + // targets. The second return uint is the total online hosts. The third + // returned uint is the total number of hosts that have been offline for more + // than 30 days. (Missing in action) + CountHostsInTargets(ctx context.Context, hostIDs []uint, labelIDs []uint) (*TargetMetrics, error) } type TargetType int diff --git a/server/service/endpoint_hosts.go b/server/service/endpoint_hosts.go index 85c82d9657..412201e554 100644 --- a/server/service/endpoint_hosts.go +++ b/server/service/endpoint_hosts.go @@ -1,11 +1,28 @@ package service import ( + "time" + "github.com/go-kit/kit/endpoint" "github.com/kolide/kolide-ose/server/kolide" "golang.org/x/net/context" ) +const ( + // StatusOnline host is active + StatusOnline string = "online" + // StatusOffline no communication with host for OfflineDuration + StatusOffline string = "offline" + // StatusMIA no communition with host for MIADuration + StatusMIA string = "mia" + // OfflineDuration if a host hasn't been in communition for this + // period it is considered offline + OfflineDuration time.Duration = 30 * time.Minute + // OfflineDuration if a host hasn't been in communition for this + // period it is considered MIA + MIADuration time.Duration = 30 * 24 * time.Hour +) + type hostResponse struct { kolide.Host Status string `json:"status"` diff --git a/server/service/endpoint_labels.go b/server/service/endpoint_labels.go index d42cb53adf..a6d4574e86 100644 --- a/server/service/endpoint_labels.go +++ b/server/service/endpoint_labels.go @@ -16,9 +16,11 @@ type getLabelRequest struct { type labelResponse struct { kolide.Label - DisplayText string `json:"display_text"` - Count uint `json:"count"` - Online uint `json:"online"` + DisplayText string `json:"display_text"` + Count uint `json:"count"` + Online uint `json:"online"` + Offline uint `json:"offline"` + MissingInAction uint `json:"missing_in_action"` } type getLabelResponse struct { @@ -35,7 +37,7 @@ func makeGetLabelEndpoint(svc kolide.Service) endpoint.Endpoint { if err != nil { return getLabelResponse{Err: err}, nil } - total, online, err := svc.CountHostsInTargets(ctx, nil, []uint{label.ID}) + metrics, err := svc.CountHostsInTargets(ctx, nil, []uint{label.ID}) if err != nil { return getLabelResponse{Err: err}, nil } @@ -43,8 +45,10 @@ func makeGetLabelEndpoint(svc kolide.Service) endpoint.Endpoint { Label: labelResponse{ *label, label.Name, - total, - online, + metrics.TotalHosts, + metrics.OnlineHosts, + metrics.OfflineHosts, + metrics.MissingInActionHosts, }, }, nil } @@ -75,7 +79,7 @@ func makeListLabelsEndpoint(svc kolide.Service) endpoint.Endpoint { resp := listLabelsResponse{} for _, label := range labels { - total, online, err := svc.CountHostsInTargets(ctx, nil, []uint{label.ID}) + metrics, err := svc.CountHostsInTargets(ctx, nil, []uint{label.ID}) if err != nil { return listLabelsResponse{Err: err}, nil } @@ -83,8 +87,10 @@ func makeListLabelsEndpoint(svc kolide.Service) endpoint.Endpoint { labelResponse{ *label, label.Name, - total, - online, + metrics.TotalHosts, + metrics.OnlineHosts, + metrics.OfflineHosts, + metrics.MissingInActionHosts, }, ) } @@ -114,7 +120,7 @@ func makeCreateLabelEndpoint(svc kolide.Service) endpoint.Endpoint { if err != nil { return createLabelResponse{Err: err}, nil } - total, online, err := svc.CountHostsInTargets(ctx, nil, []uint{label.ID}) + metrics, err := svc.CountHostsInTargets(ctx, nil, []uint{label.ID}) if err != nil { return createLabelResponse{Err: err}, nil } @@ -122,8 +128,10 @@ func makeCreateLabelEndpoint(svc kolide.Service) endpoint.Endpoint { Label: labelResponse{ *label, label.Name, - total, - online, + metrics.TotalHosts, + metrics.OnlineHosts, + metrics.OfflineHosts, + metrics.MissingInActionHosts, }, }, nil } diff --git a/server/service/endpoint_targets.go b/server/service/endpoint_targets.go index 097d89f5ab..8f71db6178 100644 --- a/server/service/endpoint_targets.go +++ b/server/service/endpoint_targets.go @@ -25,9 +25,10 @@ type hostSearchResult struct { type labelSearchResult struct { kolide.Label - DisplayText string `json:"display_text"` - Count uint `json:"count"` - Online uint `json:"online"` + DisplayText string `json:"display_text"` + Count uint `json:"count"` + Online uint `json:"online"` + MissingInAction uint `json:"missing_in_action"` } type targetsData struct { @@ -36,10 +37,11 @@ type targetsData struct { } type searchTargetsResponse struct { - Targets *targetsData `json:"targets,omitempty"` - SelectedTargetsCount uint `json:"selected_targets_count"` - SelectedTargetsOnline uint `json:"selected_targets_online"` - Err error `json:"error,omitempty"` + Targets *targetsData `json:"targets,omitempty"` + SelectedTargetsCount uint `json:"selected_targets_count"` + SelectedTargetsOnline uint `json:"selected_targets_online"` + SelectedTargetsMissingInAction uint `json:"selected_targets_missing_in_action"` + Err error `json:"error,omitempty"` } func (r searchTargetsResponse) error() error { return r.Err } @@ -68,7 +70,7 @@ func makeSearchTargetsEndpoint(svc kolide.Service) endpoint.Endpoint { } for _, label := range results.Labels { - total, online, err := svc.CountHostsInTargets(ctx, nil, []uint{label.ID}) + metrics, err := svc.CountHostsInTargets(ctx, nil, []uint{label.ID}) if err != nil { return searchTargetsResponse{Err: err}, nil } @@ -76,21 +78,23 @@ func makeSearchTargetsEndpoint(svc kolide.Service) endpoint.Endpoint { labelSearchResult{ label, label.Name, - total, - online, + metrics.TotalHosts, + metrics.OnlineHosts, + metrics.MissingInActionHosts, }, ) } - total, online, err := svc.CountHostsInTargets(ctx, req.Selected.Hosts, req.Selected.Labels) + metrics, err := svc.CountHostsInTargets(ctx, req.Selected.Hosts, req.Selected.Labels) if err != nil { return searchTargetsResponse{Err: err}, nil } return searchTargetsResponse{ - Targets: targets, - SelectedTargetsCount: total, - SelectedTargetsOnline: online, + Targets: targets, + SelectedTargetsCount: metrics.TotalHosts, + SelectedTargetsOnline: metrics.OnlineHosts, + SelectedTargetsMissingInAction: metrics.MissingInActionHosts, }, nil } } diff --git a/server/service/service_campaigns.go b/server/service/service_campaigns.go index 0bf5ce9749..f6f8a9eb3a 100644 --- a/server/service/service_campaigns.go +++ b/server/service/service_campaigns.go @@ -63,8 +63,10 @@ func (svc service) NewDistributedQueryCampaign(ctx context.Context, queryString } type targetTotals struct { - Total uint `json:"count"` - Online uint `json:"online"` + Total uint `json:"count"` + Online uint `json:"online"` + Offline uint `json:"offline"` + MissingInAction uint `json:"missing_in_action"` } func (svc service) StreamCampaignResults(ctx context.Context, conn *websocket.Conn, campaignID uint) { @@ -126,16 +128,20 @@ func (svc service) StreamCampaignResults(ctx context.Context, conn *websocket.Co } } - var totals targetTotals - totals.Total, totals.Online, err = svc.CountHostsInTargets( - context.Background(), hostIDs, labelIDs, - ) + metrics, err := svc.CountHostsInTargets(context.Background(), hostIDs, labelIDs) if err != nil { if err = conn.WriteJSONError("error retrieving target counts"); err != nil { return } } + totals := targetTotals{ + Total: metrics.TotalHosts, + Online: metrics.OnlineHosts, + Offline: metrics.OfflineHosts, + MissingInAction: metrics.MissingInActionHosts, + } + if err = conn.WriteJSONMessage("totals", totals); err != nil { return } diff --git a/server/service/service_hosts.go b/server/service/service_hosts.go index f6d47d54ef..798d231e85 100644 --- a/server/service/service_hosts.go +++ b/server/service/service_hosts.go @@ -1,8 +1,6 @@ package service import ( - "time" - "github.com/kolide/kolide-ose/server/kolide" "golang.org/x/net/context" ) @@ -16,11 +14,14 @@ func (svc service) GetHost(ctx context.Context, id uint) (*kolide.Host, error) { } func (svc service) HostStatus(ctx context.Context, host kolide.Host) string { - if host.UpdatedAt.Add(30 * time.Minute).Before(svc.clock.Now()) { - return "offline" - } else { - return "online" + if host.UpdatedAt.Add(OfflineDuration).Before(svc.clock.Now()) { + if host.UpdatedAt.Add(MIADuration).Before(svc.clock.Now()) { + return StatusMIA + } + return StatusOffline } + + return StatusOnline } func (svc service) DeleteHost(ctx context.Context, id uint) error { diff --git a/server/service/service_targets.go b/server/service/service_targets.go index 951da368b2..7cb5acd6f2 100644 --- a/server/service/service_targets.go +++ b/server/service/service_targets.go @@ -23,30 +23,39 @@ func (svc service) SearchTargets(ctx context.Context, query string, selectedHost return results, nil } -func (svc service) CountHostsInTargets(ctx context.Context, hostIDs []uint, labelIDs []uint) (total uint, online uint, err error) { +func (svc service) CountHostsInTargets(ctx context.Context, hostIDs []uint, labelIDs []uint) (*kolide.TargetMetrics, error) { hosts, err := svc.ds.ListUniqueHostsInLabels(labelIDs) if err != nil { - return 0, 0, err + return nil, err } for _, id := range hostIDs { h, err := svc.ds.Host(id) if err != nil { - return 0, 0, err + return nil, err } hosts = append(hosts, *h) } hostLookup := map[uint]bool{} - online = uint(0) + + result := &kolide.TargetMetrics{} + for _, host := range hosts { if !hostLookup[host.ID] { hostLookup[host.ID] = true - if svc.HostStatus(ctx, host) == "online" { - online++ + switch svc.HostStatus(ctx, host) { + case StatusOnline: + result.OnlineHosts++ + case StatusOffline: + result.OfflineHosts++ + case StatusMIA: + result.MissingInActionHosts++ } } } - return uint(len(hostLookup)), online, nil + result.TotalHosts = uint(len(hostLookup)) + + return result, nil } diff --git a/server/service/service_targets_test.go b/server/service/service_targets_test.go index 435e6c3d39..e205fe64c3 100644 --- a/server/service/service_targets_test.go +++ b/server/service/service_targets_test.go @@ -96,6 +96,15 @@ func TestCountHostsInTargets(t *testing.T) { require.Nil(t, err) require.Nil(t, ds.MarkHostSeen(h5, mockClock.Now())) + h6, err := ds.NewHost(&kolide.Host{ + HostName: "zzz.local", + NodeKey: "6", + UUID: "6", + }) + require.Nil(t, err) + const thirtyDaysAndAMinuteAgo = -1 * (30*24*60 + 1) + require.Nil(t, ds.MarkHostSeen(h6, mockClock.Now().Add(thirtyDaysAndAMinuteAgo*time.Minute))) + l1, err := ds.NewLabel(&kolide.Label{ Name: "label foo", Query: "query foo", @@ -112,7 +121,7 @@ func TestCountHostsInTargets(t *testing.T) { require.NotZero(t, l2.ID) l2ID := fmt.Sprintf("%d", l2.ID) - for _, h := range []*kolide.Host{h1, h2, h3} { + for _, h := range []*kolide.Host{h1, h2, h3, h6} { err = ds.RecordLabelQueryExecutions(h, map[string]bool{l1ID: true}, time.Now()) assert.Nil(t, err) } @@ -122,37 +131,55 @@ func TestCountHostsInTargets(t *testing.T) { assert.Nil(t, err) } - total, online, err := svc.CountHostsInTargets(ctx, nil, []uint{l1.ID, l2.ID}) - assert.Nil(t, err) - assert.Equal(t, uint(5), total) - assert.Equal(t, uint(4), online) + metrics, err := svc.CountHostsInTargets(ctx, nil, []uint{l1.ID, l2.ID}) + require.Nil(t, err) + require.NotNil(t, metrics) + assert.Equal(t, uint(6), metrics.TotalHosts) + assert.Equal(t, uint(1), metrics.OfflineHosts) + assert.Equal(t, uint(4), metrics.OnlineHosts) + assert.Equal(t, uint(1), metrics.MissingInActionHosts) - total, online, err = svc.CountHostsInTargets(ctx, []uint{h1.ID, h2.ID}, []uint{l1.ID, l2.ID}) - assert.Nil(t, err) - assert.Equal(t, uint(5), total) - assert.Equal(t, uint(4), online) + metrics, err = svc.CountHostsInTargets(ctx, []uint{h1.ID, h2.ID}, []uint{l1.ID, l2.ID}) + require.Nil(t, err) + require.NotNil(t, metrics) + assert.Equal(t, uint(6), metrics.TotalHosts) + assert.Equal(t, uint(1), metrics.OfflineHosts) + assert.Equal(t, uint(4), metrics.OnlineHosts) + assert.Equal(t, uint(1), metrics.MissingInActionHosts) - total, online, err = svc.CountHostsInTargets(ctx, []uint{h1.ID, h2.ID}, nil) - assert.Nil(t, err) - assert.Equal(t, uint(2), total) - assert.Equal(t, uint(1), online) + metrics, err = svc.CountHostsInTargets(ctx, []uint{h1.ID, h2.ID}, nil) + require.Nil(t, err) + require.NotNil(t, metrics) + assert.Equal(t, uint(2), metrics.TotalHosts) + assert.Equal(t, uint(1), metrics.OnlineHosts) + assert.Equal(t, uint(1), metrics.OfflineHosts) + assert.Equal(t, uint(0), metrics.MissingInActionHosts) - total, online, err = svc.CountHostsInTargets(ctx, []uint{h1.ID}, []uint{l2.ID}) - assert.Nil(t, err) - assert.Equal(t, uint(4), total) - assert.Equal(t, uint(4), online) + metrics, err = svc.CountHostsInTargets(ctx, []uint{h1.ID}, []uint{l2.ID}) + require.Nil(t, err) + require.NotNil(t, metrics) + assert.Equal(t, uint(4), metrics.TotalHosts) + assert.Equal(t, uint(4), metrics.OnlineHosts) + assert.Equal(t, uint(0), metrics.OfflineHosts) + assert.Equal(t, uint(0), metrics.MissingInActionHosts) - total, online, err = svc.CountHostsInTargets(ctx, nil, nil) - assert.Nil(t, err) - assert.Equal(t, uint(0), total) - assert.Equal(t, uint(0), online) + metrics, err = svc.CountHostsInTargets(ctx, nil, nil) + require.Nil(t, err) + require.NotNil(t, metrics) + assert.Equal(t, uint(0), metrics.TotalHosts) + assert.Equal(t, uint(0), metrics.OnlineHosts) + assert.Equal(t, uint(0), metrics.OfflineHosts) + assert.Equal(t, uint(0), metrics.MissingInActionHosts) // Advance clock so all hosts are offline mockClock.AddTime(1 * time.Hour) - total, online, err = svc.CountHostsInTargets(ctx, nil, []uint{l1.ID, l2.ID}) - assert.Nil(t, err) - assert.Equal(t, uint(5), total) - assert.Equal(t, uint(0), online) + metrics, err = svc.CountHostsInTargets(ctx, nil, []uint{l1.ID, l2.ID}) + require.Nil(t, err) + require.NotNil(t, metrics) + assert.Equal(t, uint(6), metrics.TotalHosts) + assert.Equal(t, uint(0), metrics.OnlineHosts) + assert.Equal(t, uint(5), metrics.OfflineHosts) + assert.Equal(t, uint(1), metrics.MissingInActionHosts) }