Aggregate munki and mdm data (#3886)

* Aggregate munki and mdm data

* Update doc

* Use reader to read

* Reader to read

* Address review comments
This commit is contained in:
Tomas Touceda
2022-01-26 17:55:07 -03:00
committed by GitHub
parent 433843ae94
commit ffabf803a3
12 changed files with 445 additions and 0 deletions
+136
View File
@@ -1256,6 +1256,142 @@ func (d *Datastore) GetMDM(ctx context.Context, hostID uint) (bool, string, bool
}
return dest.Enrolled, dest.ServerURL, dest.InstalledFromDep, nil
}
func (d *Datastore) AggregatedMunkiVersion(ctx context.Context, teamID *uint) ([]fleet.AggregatedMunkiVersion, error) {
id := uint(0)
if teamID != nil {
id = *teamID
}
var versions []fleet.AggregatedMunkiVersion
var versionsJson []byte
err := sqlx.GetContext(ctx, d.reader, &versionsJson, `select json_value from aggregated_stats where id=? and type='munki_versions'`, id)
if err != nil {
if err == sql.ErrNoRows {
// not having stats is not an error
return nil, nil
}
return nil, ctxerr.Wrap(ctx, err, "selecting munki versions")
}
if err := json.Unmarshal(versionsJson, &versions); err != nil {
return nil, ctxerr.Wrap(ctx, err, "unmarshaling munki versions")
}
return versions, nil
}
func (d *Datastore) AggregatedMDMStatus(ctx context.Context, teamID *uint) (fleet.AggregatedMDMStatus, error) {
id := uint(0)
if teamID != nil {
id = *teamID
}
var status fleet.AggregatedMDMStatus
var statusJson []byte
err := sqlx.GetContext(ctx, d.reader, &statusJson, `select json_value from aggregated_stats where id=? and type='mdm_status'`, id)
if err != nil {
if err == sql.ErrNoRows {
// not having stats is not an error
return fleet.AggregatedMDMStatus{}, nil
}
return fleet.AggregatedMDMStatus{}, ctxerr.Wrap(ctx, err, "selecting mdm status")
}
if err := json.Unmarshal(statusJson, &status); err != nil {
return fleet.AggregatedMDMStatus{}, ctxerr.Wrap(ctx, err, "unmarshaling mdm status")
}
return status, nil
}
func (d *Datastore) GenerateAggregatedMunkiAndMDM(ctx context.Context) error {
var ids []uint
if err := sqlx.SelectContext(ctx, d.reader, &ids, `SELECT id FROM teams`); err != nil {
return ctxerr.Wrap(ctx, err, "list teams")
}
for _, id := range ids {
if err := d.generateAggregatedMunkiVersion(ctx, &id); err != nil {
return ctxerr.Wrap(ctx, err, "generating aggregated munki version")
}
if err := d.generateAggregatedMDMStatus(ctx, &id); err != nil {
return ctxerr.Wrap(ctx, err, "generating aggregated mdm status")
}
}
if err := d.generateAggregatedMunkiVersion(ctx, nil); err != nil {
return ctxerr.Wrap(ctx, err, "generating aggregated munki version")
}
if err := d.generateAggregatedMDMStatus(ctx, nil); err != nil {
return ctxerr.Wrap(ctx, err, "generating aggregated mdm status")
}
return nil
}
func (d *Datastore) generateAggregatedMunkiVersion(ctx context.Context, teamID *uint) error {
id := uint(0)
var versions []fleet.AggregatedMunkiVersion
query := `SELECT count(*) as hosts_count, hm.version FROM host_munki_info hm`
args := []interface{}{}
if teamID != nil {
args = append(args, *teamID)
query += ` JOIN hosts h ON (h.id=hm.host_id) WHERE h.team_id=?`
id = *teamID
}
query += ` GROUP BY hm.version`
err := sqlx.SelectContext(ctx, d.reader, &versions, query, args...)
if err != nil {
return ctxerr.Wrapf(ctx, err, "getting aggregated data from host_munki")
}
versionsJson, err := json.Marshal(versions)
if err != nil {
return ctxerr.Wrap(ctx, err, "marshaling stats")
}
_, err = d.writer.ExecContext(ctx,
`INSERT INTO aggregated_stats(id, type, json_value) VALUES(?, ?, ?) ON DUPLICATE KEY UPDATE json_value=VALUES(json_value)`,
id, "munki_versions", versionsJson,
)
if err != nil {
return ctxerr.Wrapf(ctx, err, "inserting stats for munki_versions id %d", id)
}
return nil
}
func (d *Datastore) generateAggregatedMDMStatus(ctx context.Context, teamID *uint) error {
id := uint(0)
var status fleet.AggregatedMDMStatus
query := `SELECT
COUNT(DISTINCT host_id) as hosts_count,
COALESCE(SUM(CASE WHEN NOT enrolled THEN 1 ELSE 0 END), 0) as unenrolled_hosts_count,
COALESCE(SUM(CASE WHEN enrolled AND installed_from_dep THEN 1 ELSE 0 END), 0) as enrolled_automated_hosts_count,
COALESCE(SUM(CASE WHEN enrolled AND NOT installed_from_dep THEN 1 ELSE 0 END), 0) as enrolled_manual_hosts_count
FROM host_mdm hm
`
args := []interface{}{}
if teamID != nil {
args = append(args, *teamID)
query += ` JOIN hosts h ON (h.id=hm.host_id) WHERE h.team_id=?`
id = *teamID
}
err := sqlx.GetContext(ctx, d.reader, &status, query, args...)
if err != nil {
return ctxerr.Wrapf(ctx, err, "getting aggregated data from host_mdm")
}
statusJson, err := json.Marshal(status)
if err != nil {
return ctxerr.Wrap(ctx, err, "marshaling stats")
}
_, err = d.writer.ExecContext(ctx,
`INSERT INTO aggregated_stats(id, type, json_value) VALUES(?, ?, ?) ON DUPLICATE KEY UPDATE json_value=VALUES(json_value)`,
id, "mdm_status", statusJson,
)
if err != nil {
return ctxerr.Wrapf(ctx, err, "inserting stats for mdm_status id %d", id)
}
return nil
}
// HostLite will load the primary data of the host with the given id.
// We define "primary data" as all host information except the
+100
View File
@@ -106,6 +106,7 @@ func TestHosts(t *testing.T) {
{"ListHostDeviceMapping", testHostsListHostDeviceMapping},
{"ReplaceHostDeviceMapping", testHostsReplaceHostDeviceMapping},
{"HostMDMAndMunki", testHostMDMAndMunki},
{"AggregatedHostMDMAndMunki", testAggregatedHostMDMAndMunki},
{"HostLite", testHostsLite},
{"UpdateOsqueryIntervals", testUpdateOsqueryIntervals},
{"UpdateRefetchRequested", testUpdateRefetchRequested},
@@ -3334,6 +3335,105 @@ func testHostMDMAndMunki(t *testing.T, ds *Datastore) {
assert.True(t, installedFromDep)
}
func testAggregatedHostMDMAndMunki(t *testing.T, ds *Datastore) {
// Make sure things work before data is generated
versions, err := ds.AggregatedMunkiVersion(context.Background(), nil)
require.NoError(t, err)
require.Len(t, versions, 0)
status, err := ds.AggregatedMDMStatus(context.Background(), nil)
require.NoError(t, err)
require.Empty(t, status)
// Make sure generation works when there's no mdm or munki data
require.NoError(t, ds.GenerateAggregatedMunkiAndMDM(context.Background()))
// And after generating without any data, it all looks reasonable
versions, err = ds.AggregatedMunkiVersion(context.Background(), nil)
require.NoError(t, err)
require.Len(t, versions, 0)
status, err = ds.AggregatedMDMStatus(context.Background(), nil)
require.NoError(t, err)
require.Empty(t, status)
// So now we try with data
require.NoError(t, ds.SetOrUpdateMunkiVersion(context.Background(), 123, "1.2.3"))
require.NoError(t, ds.SetOrUpdateMunkiVersion(context.Background(), 999, "9.0"))
require.NoError(t, ds.SetOrUpdateMunkiVersion(context.Background(), 342, "1.2.3"))
require.NoError(t, ds.GenerateAggregatedMunkiAndMDM(context.Background()))
versions, err = ds.AggregatedMunkiVersion(context.Background(), nil)
require.NoError(t, err)
require.Len(t, versions, 2)
assert.ElementsMatch(t, versions, []fleet.AggregatedMunkiVersion{
{
HostMunkiInfo: fleet.HostMunkiInfo{Version: "1.2.3"},
HostsCount: 2,
},
{
HostMunkiInfo: fleet.HostMunkiInfo{Version: "9.0"},
HostsCount: 1,
},
})
require.NoError(t, ds.SetOrUpdateMDMData(context.Background(), 432, true, "url", false))
require.NoError(t, ds.SetOrUpdateMDMData(context.Background(), 123, true, "url", false))
require.NoError(t, ds.SetOrUpdateMDMData(context.Background(), 124, true, "url", false))
require.NoError(t, ds.SetOrUpdateMDMData(context.Background(), 455, true, "url2", true))
require.NoError(t, ds.SetOrUpdateMDMData(context.Background(), 999, false, "url3", true))
require.NoError(t, ds.SetOrUpdateMDMData(context.Background(), 875, false, "url3", true))
require.NoError(t, ds.GenerateAggregatedMunkiAndMDM(context.Background()))
status, err = ds.AggregatedMDMStatus(context.Background(), nil)
require.NoError(t, err)
assert.Equal(t, 6, status.HostsCount)
assert.Equal(t, 2, status.UnenrolledHostsCount)
assert.Equal(t, 3, status.EnrolledManualHostsCount)
assert.Equal(t, 1, status.EnrolledAutomatedHostsCount)
// Team filters
team1, err := ds.NewTeam(context.Background(), &fleet.Team{
Name: "team1" + t.Name(),
Description: "desc team1",
})
require.NoError(t, err)
team2, err := ds.NewTeam(context.Background(), &fleet.Team{
Name: "team2" + t.Name(),
Description: "desc team2",
})
require.NoError(t, err)
h1 := test.NewHost(t, ds, "h1"+t.Name(), "192.168.1.10", "1", "1", time.Now())
h2 := test.NewHost(t, ds, "h2"+t.Name(), "192.168.1.11", "2", "2", time.Now())
require.NoError(t, ds.AddHostsToTeam(context.Background(), &team1.ID, []uint{h1.ID}))
require.NoError(t, ds.AddHostsToTeam(context.Background(), &team2.ID, []uint{h2.ID}))
require.NoError(t, ds.SetOrUpdateMDMData(context.Background(), h1.ID, true, "url", false))
require.NoError(t, ds.SetOrUpdateMDMData(context.Background(), h2.ID, true, "url", false))
require.NoError(t, ds.SetOrUpdateMunkiVersion(context.Background(), h1.ID, "1.2.3"))
require.NoError(t, ds.SetOrUpdateMunkiVersion(context.Background(), h2.ID, "1.2.3"))
require.NoError(t, ds.GenerateAggregatedMunkiAndMDM(context.Background()))
versions, err = ds.AggregatedMunkiVersion(context.Background(), &team1.ID)
require.NoError(t, err)
require.Len(t, versions, 1)
assert.ElementsMatch(t, versions, []fleet.AggregatedMunkiVersion{
{
HostMunkiInfo: fleet.HostMunkiInfo{Version: "1.2.3"},
HostsCount: 1,
},
})
status, err = ds.AggregatedMDMStatus(context.Background(), &team1.ID)
require.NoError(t, err)
assert.Equal(t, 1, status.HostsCount)
assert.Equal(t, 0, status.UnenrolledHostsCount)
assert.Equal(t, 1, status.EnrolledManualHostsCount)
assert.Equal(t, 0, status.EnrolledAutomatedHostsCount)
}
func testHostsLite(t *testing.T, ds *Datastore) {
_, err := ds.HostLite(context.Background(), 1)
require.Error(t, err)
+4
View File
@@ -207,6 +207,10 @@ type Datastore interface {
GetMunkiVersion(ctx context.Context, hostID uint) (string, error)
GetMDM(ctx context.Context, hostID uint) (enrolled bool, serverURL string, installedFromDep bool, err error)
AggregatedMunkiVersion(ctx context.Context, teamID *uint) ([]AggregatedMunkiVersion, error)
AggregatedMDMStatus(ctx context.Context, teamID *uint) (AggregatedMDMStatus, error)
GenerateAggregatedMunkiAndMDM(ctx context.Context) error
///////////////////////////////////////////////////////////////////////////////
// TargetStore
+17
View File
@@ -269,3 +269,20 @@ type MacadminsData struct {
Munki *HostMunkiInfo `json:"munki"`
MDM *HostMDM `json:"mobile_device_management"`
}
type AggregatedMunkiVersion struct {
HostMunkiInfo
HostsCount int `json:"hosts_count" db:"hosts_count"`
}
type AggregatedMDMStatus struct {
EnrolledManualHostsCount int `json:"enrolled_manual_hosts_count" db:"enrolled_manual_hosts_count"`
EnrolledAutomatedHostsCount int `json:"enrolled_automated_hosts_count" db:"enrolled_automated_hosts_count"`
UnenrolledHostsCount int `json:"unenrolled_hosts_count" db:"unenrolled_hosts_count"`
HostsCount int `json:"hosts_count" db:"hosts_count"`
}
type AggregatedMacadminsData struct {
MunkiVersions []AggregatedMunkiVersion `json:"munki_versions"`
MDMStatus AggregatedMDMStatus `json:"mobile_device_management_enrollment_status"`
}
+1
View File
@@ -257,6 +257,7 @@ type Service interface {
ListHostDeviceMapping(ctx context.Context, id uint) ([]*HostDeviceMapping, error)
MacadminsData(ctx context.Context, id uint) (*MacadminsData, error)
AggregatedMacadminsData(ctx context.Context, teamID *uint) (*AggregatedMacadminsData, error)
///////////////////////////////////////////////////////////////////////////////
// AppConfigService provides methods for configuring the Fleet application
+30
View File
@@ -176,6 +176,12 @@ type GetMunkiVersionFunc func(ctx context.Context, hostID uint) (string, error)
type GetMDMFunc func(ctx context.Context, hostID uint) (enrolled bool, serverURL string, installedFromDep bool, err error)
type AggregatedMunkiVersionFunc func(ctx context.Context, teamID *uint) ([]fleet.AggregatedMunkiVersion, error)
type AggregatedMDMStatusFunc func(ctx context.Context, teamID *uint) (fleet.AggregatedMDMStatus, error)
type GenerateAggregatedMunkiAndMDMFunc func(ctx context.Context) error
type CountHostsInTargetsFunc func(ctx context.Context, filter fleet.TeamFilter, targets fleet.HostTargets, now time.Time) (fleet.TargetMetrics, error)
type HostIDsInTargetsFunc func(ctx context.Context, filter fleet.TeamFilter, targets fleet.HostTargets) ([]uint, error)
@@ -609,6 +615,15 @@ type DataStore struct {
GetMDMFunc GetMDMFunc
GetMDMFuncInvoked bool
AggregatedMunkiVersionFunc AggregatedMunkiVersionFunc
AggregatedMunkiVersionFuncInvoked bool
AggregatedMDMStatusFunc AggregatedMDMStatusFunc
AggregatedMDMStatusFuncInvoked bool
GenerateAggregatedMunkiAndMDMFunc GenerateAggregatedMunkiAndMDMFunc
GenerateAggregatedMunkiAndMDMFuncInvoked bool
CountHostsInTargetsFunc CountHostsInTargetsFunc
CountHostsInTargetsFuncInvoked bool
@@ -1299,6 +1314,21 @@ func (s *DataStore) GetMDM(ctx context.Context, hostID uint) (enrolled bool, ser
return s.GetMDMFunc(ctx, hostID)
}
func (s *DataStore) AggregatedMunkiVersion(ctx context.Context, teamID *uint) ([]fleet.AggregatedMunkiVersion, error) {
s.AggregatedMunkiVersionFuncInvoked = true
return s.AggregatedMunkiVersionFunc(ctx, teamID)
}
func (s *DataStore) AggregatedMDMStatus(ctx context.Context, teamID *uint) (fleet.AggregatedMDMStatus, error) {
s.AggregatedMDMStatusFuncInvoked = true
return s.AggregatedMDMStatusFunc(ctx, teamID)
}
func (s *DataStore) GenerateAggregatedMunkiAndMDM(ctx context.Context) error {
s.GenerateAggregatedMunkiAndMDMFuncInvoked = true
return s.GenerateAggregatedMunkiAndMDMFunc(ctx)
}
func (s *DataStore) CountHostsInTargets(ctx context.Context, filter fleet.TeamFilter, targets fleet.HostTargets, now time.Time) (fleet.TargetMetrics, error) {
s.CountHostsInTargetsFuncInvoked = true
return s.CountHostsInTargetsFunc(ctx, filter, targets, now)
+1
View File
@@ -537,6 +537,7 @@ func attachNewStyleFleetAPIRoutes(r *mux.Router, svc fleet.Service, opts []kitht
e.GET("/api/_version_/fleet/carves/{id:[0-9]+}/block/{block_id}", getCarveBlockEndpoint, getCarveBlockRequest{})
e.GET("/api/_version_/fleet/hosts/{id:[0-9]+}/macadmins", getMacadminsDataEndpoint, getMacadminsDataRequest{})
e.GET("/api/_version_/fleet/macadmins", getAggregatedMacadminsDataEndpoint, getAggregatedMacadminsDataRequest{})
}
// TODO: this duplicates the one in makeKitHandler
+53
View File
@@ -750,3 +750,56 @@ func (svc *Service) MacadminsData(ctx context.Context, id uint) (*fleet.Macadmin
return data, nil
}
////////////////////////////////////////////////////////////////////////////////
// Aggregated Macadmins
////////////////////////////////////////////////////////////////////////////////
type getAggregatedMacadminsDataRequest struct {
TeamID *uint `query:"team_id,optional"`
}
type getAggregatedMacadminsDataResponse struct {
Err error `json:"error,omitempty"`
Macadmins *fleet.AggregatedMacadminsData `json:"macadmins"`
}
func (r getAggregatedMacadminsDataResponse) error() error { return r.Err }
func getAggregatedMacadminsDataEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (interface{}, error) {
req := request.(*getAggregatedMacadminsDataRequest)
data, err := svc.AggregatedMacadminsData(ctx, req.TeamID)
if err != nil {
return getAggregatedMacadminsDataResponse{Err: err}, nil
}
return getAggregatedMacadminsDataResponse{Macadmins: data}, nil
}
func (svc *Service) AggregatedMacadminsData(ctx context.Context, teamID *uint) (*fleet.AggregatedMacadminsData, error) {
if err := svc.authz.Authorize(ctx, &fleet.Host{TeamID: teamID}, fleet.ActionList); err != nil {
return nil, err
}
if teamID != nil {
_, err := svc.ds.Team(ctx, *teamID)
if err != nil {
return nil, err
}
}
agg := &fleet.AggregatedMacadminsData{}
versions, err := svc.ds.AggregatedMunkiVersion(ctx, teamID)
if err != nil {
return nil, err
}
agg.MunkiVersions = versions
status, err := svc.ds.AggregatedMDMStatus(ctx, teamID)
if err != nil {
return nil, err
}
agg.MDMStatus = status
return agg, nil
}
+37
View File
@@ -1782,6 +1782,43 @@ func (s *integrationTestSuite) TestGetMacadminsData() {
require.Nil(t, macadminsData.Macadmins.Munki)
assert.Equal(t, "AAA", macadminsData.Macadmins.MDM.ServerURL)
assert.Equal(t, "Enrolled (automated)", macadminsData.Macadmins.MDM.EnrollmentStatus)
// generate aggregated data
require.NoError(t, s.ds.GenerateAggregatedMunkiAndMDM(context.Background()))
agg := getAggregatedMacadminsDataResponse{}
s.DoJSON("GET", "/api/v1/fleet/macadmins", nil, http.StatusOK, &agg)
require.NotNil(t, agg.Macadmins)
assert.Len(t, agg.Macadmins.MunkiVersions, 2)
assert.ElementsMatch(t, agg.Macadmins.MunkiVersions, []fleet.AggregatedMunkiVersion{
{
HostMunkiInfo: fleet.HostMunkiInfo{Version: "1.5.0"},
HostsCount: 1,
},
{
HostMunkiInfo: fleet.HostMunkiInfo{Version: "3.2.0"},
HostsCount: 1,
},
})
assert.Equal(t, agg.Macadmins.MDMStatus.EnrolledManualHostsCount, 0)
assert.Equal(t, agg.Macadmins.MDMStatus.EnrolledAutomatedHostsCount, 1)
assert.Equal(t, agg.Macadmins.MDMStatus.UnenrolledHostsCount, 1)
assert.Equal(t, agg.Macadmins.MDMStatus.HostsCount, 2)
team, err := s.ds.NewTeam(context.Background(), &fleet.Team{
Name: "team1" + t.Name(),
Description: "desc team1",
})
require.NoError(t, err)
agg = getAggregatedMacadminsDataResponse{}
s.DoJSON("GET", "/api/v1/fleet/macadmins", nil, http.StatusOK, &agg, "team_id", fmt.Sprint(team.ID))
require.NotNil(t, agg.Macadmins)
require.Empty(t, agg.Macadmins.MunkiVersions)
require.Empty(t, agg.Macadmins.MDMStatus)
agg = getAggregatedMacadminsDataResponse{}
s.DoJSON("GET", "/api/v1/fleet/macadmins", nil, http.StatusNotFound, &agg, "team_id", "9999999")
}
func (s *integrationTestSuite) TestLabels() {