2016-11-16 13:47:49 +00:00
package mysql
import (
2021-09-14 12:11:07 +00:00
"context"
2016-11-16 13:47:49 +00:00
"database/sql"
2022-01-18 01:52:09 +00:00
"encoding/json"
2021-11-15 14:11:38 +00:00
"errors"
2016-12-01 17:00:00 +00:00
"fmt"
2021-12-01 19:20:54 +00:00
"sort"
2021-05-07 04:05:09 +00:00
"strings"
2016-11-16 13:47:49 +00:00
"time"
2020-12-10 19:04:58 +00:00
"github.com/cenkalti/backoff/v4"
2021-11-12 11:18:25 +00:00
"github.com/doug-martin/goqu/v9"
2021-11-09 14:35:36 +00:00
"github.com/fleetdm/fleet/v4/server/contexts/ctxerr"
2021-06-26 04:46:51 +00:00
"github.com/fleetdm/fleet/v4/server/fleet"
2021-12-03 18:33:33 +00:00
"github.com/go-kit/kit/log/level"
2020-12-10 19:04:58 +00:00
"github.com/jmoiron/sqlx"
2016-11-16 13:47:49 +00:00
)
2021-06-24 00:32:19 +00:00
var hostSearchColumns = [ ] string { "hostname" , "uuid" , "hardware_serial" , "primary_ip" }
2021-02-18 20:52:43 +00:00
2022-01-18 01:52:09 +00:00
// NewHost creates a new host on the datastore.
//
// Currently only used for testing.
2022-02-03 17:56:22 +00:00
func ( ds * Datastore ) NewHost ( ctx context . Context , host * fleet . Host ) ( * fleet . Host , error ) {
2016-11-16 13:47:49 +00:00
sqlStatement := `
INSERT INTO hosts (
2016-12-06 19:51:11 +00:00
osquery_host_id ,
2021-06-24 00:32:19 +00:00
detail_updated_at ,
label_updated_at ,
2021-09-27 19:27:38 +00:00
policy_updated_at ,
2016-11-16 13:47:49 +00:00
node_key ,
2021-06-24 00:32:19 +00:00
hostname ,
2016-11-16 13:47:49 +00:00
uuid ,
platform ,
osquery_version ,
os_version ,
uptime ,
2021-06-24 00:32:19 +00:00
memory ,
2022-01-18 01:52:09 +00:00
team_id ,
distributed_interval ,
logger_tls_period ,
config_tls_refresh ,
refetch_requested
2016-11-16 13:47:49 +00:00
)
2022-04-15 20:09:47 +00:00
VALUES ( ? , ? , ? , ? , ? , ? , ? , ? , ? , ? , ? , ? , ? , ? , ? , ? , ? )
2016-11-16 13:47:49 +00:00
`
2022-02-03 17:56:22 +00:00
result , err := ds . writer . ExecContext (
2021-09-14 14:44:02 +00:00
ctx ,
2020-04-07 01:10:20 +00:00
sqlStatement ,
host . OsqueryHostID ,
2021-06-24 00:32:19 +00:00
host . DetailUpdatedAt ,
host . LabelUpdatedAt ,
2021-09-27 19:27:38 +00:00
host . PolicyUpdatedAt ,
2020-04-07 01:10:20 +00:00
host . NodeKey ,
2021-06-24 00:32:19 +00:00
host . Hostname ,
2020-04-07 01:10:20 +00:00
host . UUID ,
host . Platform ,
host . OsqueryVersion ,
host . OSVersion ,
host . Uptime ,
2021-06-24 00:32:19 +00:00
host . Memory ,
2021-05-27 20:18:00 +00:00
host . TeamID ,
2022-01-18 01:52:09 +00:00
host . DistributedInterval ,
host . LoggerTLSPeriod ,
host . ConfigTLSRefresh ,
host . RefetchRequested ,
2020-04-07 01:10:20 +00:00
)
2016-11-16 13:47:49 +00:00
if err != nil {
2021-11-15 14:11:38 +00:00
return nil , ctxerr . Wrap ( ctx , err , "new host" )
2016-11-16 13:47:49 +00:00
}
id , _ := result . LastInsertId ( )
host . ID = uint ( id )
2021-11-08 14:42:37 +00:00
2022-02-03 17:56:22 +00:00
_ , err = ds . writer . ExecContext ( ctx , ` INSERT INTO host_seen_times (host_id, seen_time) VALUES (?,?) ` , host . ID , host . SeenTime )
2021-11-08 14:42:37 +00:00
if err != nil {
2021-11-15 14:11:38 +00:00
return nil , ctxerr . Wrap ( ctx , err , "new host seen time" )
2021-11-08 14:42:37 +00:00
}
2016-11-16 13:47:49 +00:00
return host , nil
}
2022-02-03 17:56:22 +00:00
func ( ds * Datastore ) SerialUpdateHost ( ctx context . Context , host * fleet . Host ) error {
2021-11-08 14:42:37 +00:00
errCh := make ( chan error , 1 )
defer close ( errCh )
select {
case <- ctx . Done ( ) :
return ctx . Err ( )
2022-02-03 17:56:22 +00:00
case ds . writeCh <- itemToWrite {
2021-11-08 14:42:37 +00:00
ctx : ctx ,
errCh : errCh ,
item : host ,
} :
return <- errCh
}
}
2022-02-03 17:56:22 +00:00
func ( ds * Datastore ) SaveHostPackStats ( ctx context . Context , hostID uint , stats [ ] fleet . PackStats ) error {
return saveHostPackStatsDB ( ctx , ds . writer , hostID , stats )
2022-01-18 01:52:09 +00:00
}
func saveHostPackStatsDB ( ctx context . Context , db sqlx . ExecerContext , hostID uint , stats [ ] fleet . PackStats ) error {
2021-09-08 18:43:22 +00:00
var args [ ] interface { }
queryCount := 0
2022-01-18 01:52:09 +00:00
for _ , pack := range stats {
2021-09-08 18:43:22 +00:00
for _ , query := range pack . QueryStats {
queryCount ++
args = append ( args ,
query . PackName ,
query . ScheduledQueryName ,
2022-01-18 01:52:09 +00:00
hostID ,
2021-09-08 18:43:22 +00:00
query . AverageMemory ,
query . Denylisted ,
query . Executions ,
query . Interval ,
query . LastExecuted ,
query . OutputSize ,
query . SystemTime ,
query . UserTime ,
query . WallTime ,
)
2021-05-07 04:05:09 +00:00
}
2021-09-08 18:43:22 +00:00
}
2021-05-07 04:05:09 +00:00
2021-09-08 18:43:22 +00:00
if queryCount == 0 {
return nil
}
2021-05-07 04:05:09 +00:00
2021-09-08 18:43:22 +00:00
values := strings . TrimSuffix ( strings . Repeat ( "((SELECT sq.id FROM scheduled_queries sq JOIN packs p ON (sq.pack_id = p.id) WHERE p.name = ? AND sq.name = ?),?,?,?,?,?,?,?,?,?,?)," , queryCount ) , "," )
sql := fmt . Sprintf ( `
2021-05-07 04:05:09 +00:00
INSERT IGNORE INTO scheduled_query_stats (
scheduled_query_id ,
host_id ,
average_memory ,
denylisted ,
executions ,
schedule_interval ,
last_executed ,
output_size ,
system_time ,
user_time ,
wall_time
)
2021-09-02 20:39:08 +00:00
VALUES % s ON DUPLICATE KEY UPDATE
scheduled_query_id = VALUES ( scheduled_query_id ) ,
host_id = VALUES ( host_id ) ,
average_memory = VALUES ( average_memory ) ,
denylisted = VALUES ( denylisted ) ,
executions = VALUES ( executions ) ,
schedule_interval = VALUES ( schedule_interval ) ,
last_executed = VALUES ( last_executed ) ,
output_size = VALUES ( output_size ) ,
system_time = VALUES ( system_time ) ,
user_time = VALUES ( user_time ) ,
wall_time = VALUES ( wall_time )
2021-05-07 04:05:09 +00:00
` , values )
2021-09-14 14:44:02 +00:00
if _ , err := db . ExecContext ( ctx , sql , args ... ) ; err != nil {
2021-11-15 14:11:38 +00:00
return ctxerr . Wrap ( ctx , err , "insert pack stats" )
2021-05-07 04:05:09 +00:00
}
return nil
}
2021-12-07 15:51:53 +00:00
// MySQL is really particular about using zero values or old values for
// timestamps, so we set a default value that is plenty far in the past, but
// hopefully accepted by most MySQL configurations.
//
// NOTE: #3229 proposes a better fix that uses *time.Time for
// ScheduledQueryStats.LastExecuted.
var pastDate = "2000-01-01T00:00:00Z"
2021-11-12 11:18:25 +00:00
// loadhostPacksStatsDB will load all the pack stats for the given host. The scheduled
// queries that haven't run yet are returned with zero values.
2021-11-17 22:03:30 +00:00
func loadHostPackStatsDB ( ctx context . Context , db sqlx . QueryerContext , hid uint , hostPlatform string ) ( [ ] fleet . PackStats , error ) {
2021-11-12 11:18:25 +00:00
packs , err := listPacksForHost ( ctx , db , hid )
if err != nil {
return nil , ctxerr . Wrapf ( ctx , err , "list packs for host: %d" , hid )
}
if len ( packs ) == 0 {
return nil , nil
}
packIDs := make ( [ ] uint , len ( packs ) )
packTypes := make ( map [ uint ] * string )
for i := range packs {
packIDs [ i ] = packs [ i ] . ID
packTypes [ packs [ i ] . ID ] = packs [ i ] . Type
}
ds := dialect . From ( goqu . I ( "scheduled_queries" ) . As ( "sq" ) ) . Select (
goqu . I ( "sq.name" ) . As ( "scheduled_query_name" ) ,
goqu . I ( "sq.id" ) . As ( "scheduled_query_id" ) ,
goqu . I ( "sq.query_name" ) . As ( "query_name" ) ,
goqu . I ( "q.description" ) . As ( "description" ) ,
goqu . I ( "p.name" ) . As ( "pack_name" ) ,
goqu . I ( "p.id" ) . As ( "pack_id" ) ,
goqu . COALESCE ( goqu . I ( "sqs.average_memory" ) , 0 ) . As ( "average_memory" ) ,
goqu . COALESCE ( goqu . I ( "sqs.denylisted" ) , false ) . As ( "denylisted" ) ,
goqu . COALESCE ( goqu . I ( "sqs.executions" ) , 0 ) . As ( "executions" ) ,
goqu . I ( "sq.interval" ) . As ( "schedule_interval" ) ,
2021-12-07 15:51:53 +00:00
goqu . COALESCE ( goqu . I ( "sqs.last_executed" ) , goqu . L ( "timestamp(?)" , pastDate ) ) . As ( "last_executed" ) ,
2021-11-12 11:18:25 +00:00
goqu . COALESCE ( goqu . I ( "sqs.output_size" ) , 0 ) . As ( "output_size" ) ,
goqu . COALESCE ( goqu . I ( "sqs.system_time" ) , 0 ) . As ( "system_time" ) ,
goqu . COALESCE ( goqu . I ( "sqs.user_time" ) , 0 ) . As ( "user_time" ) ,
goqu . COALESCE ( goqu . I ( "sqs.wall_time" ) , 0 ) . As ( "wall_time" ) ,
) . Join (
dialect . From ( "packs" ) . As ( "p" ) . Select (
goqu . I ( "id" ) ,
goqu . I ( "name" ) ,
) . Where ( goqu . I ( "id" ) . In ( packIDs ) ) ,
goqu . On ( goqu . I ( "sq.pack_id" ) . Eq ( goqu . I ( "p.id" ) ) ) ,
) . Join (
goqu . I ( "queries" ) . As ( "q" ) ,
goqu . On ( goqu . I ( "sq.query_name" ) . Eq ( goqu . I ( "q.name" ) ) ) ,
) . LeftJoin (
2021-11-17 22:03:30 +00:00
dialect . From ( "scheduled_query_stats" ) . As ( "sqs" ) . Where (
goqu . I ( "host_id" ) . Eq ( hid ) ,
) ,
2021-11-12 11:18:25 +00:00
goqu . On ( goqu . I ( "sqs.scheduled_query_id" ) . Eq ( goqu . I ( "sq.id" ) ) ) ,
2021-11-17 22:03:30 +00:00
) . Where (
goqu . Or (
// sq.platform empty or NULL means the scheduled query is set to
// run on all hosts.
goqu . I ( "sq.platform" ) . Eq ( "" ) ,
goqu . I ( "sq.platform" ) . IsNull ( ) ,
// scheduled_queries.platform can be a comma-separated list of
// platforms, e.g. "darwin,windows".
2021-12-03 18:33:33 +00:00
goqu . L ( "FIND_IN_SET(?, sq.platform)" , fleet . PlatformFromHost ( hostPlatform ) ) . Neq ( 0 ) ,
2021-11-17 22:03:30 +00:00
) ,
2021-11-12 11:18:25 +00:00
)
sql , args , err := ds . ToSQL ( )
if err != nil {
return nil , ctxerr . Wrap ( ctx , err , "sql build" )
}
2021-06-06 22:07:29 +00:00
var stats [ ] fleet . ScheduledQueryStats
2021-11-12 11:18:25 +00:00
if err := sqlx . SelectContext ( ctx , db , & stats , sql , args ... ) ; err != nil {
return nil , ctxerr . Wrap ( ctx , err , "load pack stats" )
2021-05-07 04:05:09 +00:00
}
2021-11-12 11:18:25 +00:00
packStats := map [ uint ] fleet . PackStats { }
2021-05-07 04:05:09 +00:00
for _ , query := range stats {
2021-11-12 11:18:25 +00:00
pack := packStats [ query . PackID ]
2021-05-07 04:05:09 +00:00
pack . PackName = query . PackName
pack . PackID = query . PackID
2021-11-12 11:18:25 +00:00
pack . Type = getPackTypeFromDBField ( packTypes [ pack . PackID ] )
2021-05-07 04:05:09 +00:00
pack . QueryStats = append ( pack . QueryStats , query )
2021-11-12 11:18:25 +00:00
packStats [ pack . PackID ] = pack
2021-05-07 04:05:09 +00:00
}
2021-11-12 11:18:25 +00:00
var ps [ ] fleet . PackStats
for _ , pack := range packStats {
ps = append ( ps , pack )
2021-05-07 04:05:09 +00:00
}
2021-11-12 11:18:25 +00:00
return ps , nil
}
2021-05-07 04:05:09 +00:00
2021-11-12 11:18:25 +00:00
func getPackTypeFromDBField ( t * string ) string {
if t == nil {
return "pack"
}
return * t
2016-11-16 13:47:49 +00:00
}
2022-01-18 01:52:09 +00:00
func loadHostUsersDB ( ctx context . Context , db sqlx . QueryerContext , hostID uint ) ( [ ] fleet . HostUser , error ) {
2021-11-11 17:26:03 +00:00
sql := ` SELECT username, groupname, uid, user_type, shell FROM host_users WHERE host_id = ? and removed_at IS NULL `
2022-01-18 01:52:09 +00:00
var users [ ] fleet . HostUser
if err := sqlx . SelectContext ( ctx , db , & users , sql , hostID ) ; err != nil {
return nil , ctxerr . Wrap ( ctx , err , "load host users" )
2021-07-13 20:15:38 +00:00
}
2022-01-18 01:52:09 +00:00
return users , nil
2021-07-13 20:15:38 +00:00
}
2022-03-23 15:15:05 +00:00
// hostRefs are the tables referenced by hosts.
//
// Defined here for testing purposes.
var hostRefs = [ ] string {
"host_seen_times" ,
"host_software" ,
"host_users" ,
"host_emails" ,
"host_additional" ,
"scheduled_query_stats" ,
"label_membership" ,
"policy_membership" ,
"host_mdm" ,
"host_munki_info" ,
"host_device_auth" ,
}
2022-02-03 17:56:22 +00:00
func ( ds * Datastore ) DeleteHost ( ctx context . Context , hid uint ) error {
2022-01-12 17:07:51 +00:00
delHostRef := func ( tx sqlx . ExtContext , table string ) error {
_ , err := tx . ExecContext ( ctx , fmt . Sprintf ( ` DELETE FROM %s WHERE host_id=? ` , table ) , hid )
if err != nil {
return ctxerr . Wrapf ( ctx , err , "deleting %s for host %d" , table , hid )
}
return nil
2017-01-20 17:22:33 +00:00
}
2021-11-08 14:42:37 +00:00
2022-02-03 17:56:22 +00:00
return ds . withRetryTxx ( ctx , func ( tx sqlx . ExtContext ) error {
2022-01-12 17:07:51 +00:00
_ , err := tx . ExecContext ( ctx , ` DELETE FROM hosts WHERE id = ? ` , hid )
if err != nil {
return ctxerr . Wrapf ( ctx , err , "delete host" )
}
2021-11-08 14:42:37 +00:00
2022-01-12 17:07:51 +00:00
for _ , table := range hostRefs {
err := delHostRef ( tx , table )
if err != nil {
return err
}
}
2022-01-19 13:28:08 +00:00
2022-04-15 20:09:47 +00:00
_ , err = tx . ExecContext ( ctx , ` DELETE FROM pack_targets WHERE type = ? AND target_id = ? ` , fleet . TargetHost , hid )
2022-01-19 13:28:08 +00:00
if err != nil {
return ctxerr . Wrapf ( ctx , err , "deleting pack_targets for host %d" , hid )
}
2022-01-12 17:07:51 +00:00
return nil
} )
2016-11-16 13:47:49 +00:00
}
2022-02-03 17:56:22 +00:00
func ( ds * Datastore ) Host ( ctx context . Context , id uint , skipLoadingExtras bool ) ( * fleet . Host , error ) {
2021-11-18 17:36:35 +00:00
policiesColumns := ` ,
coalesce ( failing_policies . count , 0 ) as failing_policies_count ,
coalesce ( failing_policies . count , 0 ) as total_issues_count `
policiesJoin := `
JOIN (
SELECT count ( * ) as count FROM policy_membership WHERE passes = 0 AND host_id = ?
) failing_policies `
args := [ ] interface { } { id , id }
if skipLoadingExtras {
policiesColumns = ""
policiesJoin = ""
args = [ ] interface { } { id }
}
sqlStatement := fmt . Sprintf ( `
2021-11-09 14:35:36 +00:00
SELECT
2021-11-08 14:42:37 +00:00
h . * ,
2021-12-01 12:05:23 +00:00
COALESCE ( hst . seen_time , h . created_at ) AS seen_time ,
2021-11-09 14:35:36 +00:00
t . name AS team_name ,
2021-11-18 17:36:35 +00:00
( SELECT additional FROM host_additional WHERE host_id = h . id ) AS additional
% s
2021-10-15 10:34:30 +00:00
FROM hosts h
LEFT JOIN teams t ON ( h . team_id = t . id )
2021-11-08 14:42:37 +00:00
LEFT JOIN host_seen_times hst ON ( h . id = hst . host_id )
2021-11-18 17:36:35 +00:00
% s
2021-05-26 23:24:12 +00:00
WHERE h . id = ?
2021-11-18 17:36:35 +00:00
LIMIT 1 ` , policiesColumns , policiesJoin )
2021-06-06 22:07:29 +00:00
host := & fleet . Host { }
2022-02-03 17:56:22 +00:00
err := sqlx . GetContext ( ctx , ds . reader , host , sqlStatement , args ... )
2016-11-16 13:47:49 +00:00
if err != nil {
2021-12-14 21:34:11 +00:00
if err == sql . ErrNoRows {
return nil , ctxerr . Wrap ( ctx , notFound ( "Host" ) . WithID ( id ) )
}
2021-11-15 14:11:38 +00:00
return nil , ctxerr . Wrap ( ctx , err , "get host by id" )
2021-05-07 04:05:09 +00:00
}
2021-11-12 11:18:25 +00:00
2022-02-03 17:56:22 +00:00
packStats , err := loadHostPackStatsDB ( ctx , ds . reader , host . ID , host . Platform )
2021-11-12 11:18:25 +00:00
if err != nil {
2021-05-07 04:05:09 +00:00
return nil , err
2016-11-16 13:47:49 +00:00
}
2021-11-12 11:18:25 +00:00
host . PackStats = packStats
2022-02-03 17:56:22 +00:00
users , err := loadHostUsersDB ( ctx , ds . reader , host . ID )
2022-01-18 01:52:09 +00:00
if err != nil {
2021-07-13 20:15:38 +00:00
return nil , err
}
2022-01-18 01:52:09 +00:00
host . Users = users
2016-11-16 13:47:49 +00:00
return host , nil
}
2022-02-22 18:42:03 +00:00
func amountEnrolledHostsDB ( ctx context . Context , db sqlx . QueryerContext ) ( int , error ) {
2021-07-20 21:39:50 +00:00
var amount int
2022-02-22 18:42:03 +00:00
err := sqlx . GetContext ( ctx , db , & amount , ` SELECT count(*) FROM hosts ` )
2021-07-20 21:39:50 +00:00
if err != nil {
return 0 , err
}
return amount , nil
}
2022-02-03 17:56:22 +00:00
func ( ds * Datastore ) ListHosts ( ctx context . Context , filter fleet . TeamFilter , opt fleet . HostListOptions ) ( [ ] * fleet . Host , error ) {
2021-05-18 00:52:59 +00:00
sql := ` SELECT
2021-07-02 15:59:42 +00:00
h . * ,
2021-12-01 12:05:23 +00:00
COALESCE ( hst . seen_time , h . created_at ) AS seen_time ,
2021-11-29 21:04:33 +00:00
t . name AS team_name
`
failingPoliciesSelect := ` ,
2021-10-15 10:34:30 +00:00
coalesce ( failing_policies . count , 0 ) as failing_policies_count ,
coalesce ( failing_policies . count , 0 ) as total_issues_count
2021-11-29 21:04:33 +00:00
`
if opt . DisableFailingPolicies {
failingPoliciesSelect = ""
}
sql += failingPoliciesSelect
Add host additional info filters (#28)
This change adds the ability to filter additional host info via the list hosts endpoint; a continuation from [here](https://github.com/kolide/fleet/pull/2330), but now filtering is accomplished via SQL.
Additional object without filter:
```
curl 'https://localhost:8080/api/v1/kolide/hosts'
...
"additional": {
"macs": [
{
"mac": "00:00:00:00:00:00"
},
{
"mac": "02:42:c0:a8:10:05"
}
],
"time": [
{
"day": "13",
"hour": "3",
"year": "2020",
"month": "10",
"minutes": "43",
"seconds": "11",
"weekday": "Tuesday",
"datetime": "2020-10-13T03:43:11Z",
"iso_8601": "2020-10-13T03:43:11Z",
"timezone": "GMT",
"timestamp": "Tue Oct 13 03:43:11 2020 UTC",
"unix_time": "1602560591",
"local_time": "1602560591",
"local_timezone": "UTC"
}
},
...
```
Additional object with filter:
```
curl 'https://localhost:8080/api/v1/kolide/hosts?additional_info_filters=macs,notreal'
...
"additional": {
"macs": [
{
"mac": "00:00:00:00:00:00"
},
{
"mac": "02:42:c0:a8:10:05"
}
],
"notreal": null
},
...
```
2020-11-14 00:33:25 +00:00
2020-03-30 02:19:54 +00:00
var params [ ] interface { }
Add host additional info filters (#28)
This change adds the ability to filter additional host info via the list hosts endpoint; a continuation from [here](https://github.com/kolide/fleet/pull/2330), but now filtering is accomplished via SQL.
Additional object without filter:
```
curl 'https://localhost:8080/api/v1/kolide/hosts'
...
"additional": {
"macs": [
{
"mac": "00:00:00:00:00:00"
},
{
"mac": "02:42:c0:a8:10:05"
}
],
"time": [
{
"day": "13",
"hour": "3",
"year": "2020",
"month": "10",
"minutes": "43",
"seconds": "11",
"weekday": "Tuesday",
"datetime": "2020-10-13T03:43:11Z",
"iso_8601": "2020-10-13T03:43:11Z",
"timezone": "GMT",
"timestamp": "Tue Oct 13 03:43:11 2020 UTC",
"unix_time": "1602560591",
"local_time": "1602560591",
"local_timezone": "UTC"
}
},
...
```
Additional object with filter:
```
curl 'https://localhost:8080/api/v1/kolide/hosts?additional_info_filters=macs,notreal'
...
"additional": {
"macs": [
{
"mac": "00:00:00:00:00:00"
},
{
"mac": "02:42:c0:a8:10:05"
}
],
"notreal": null
},
...
```
2020-11-14 00:33:25 +00:00
2021-05-26 23:24:12 +00:00
// Only include "additional" if filter provided.
if len ( opt . AdditionalFilters ) == 1 && opt . AdditionalFilters [ 0 ] == "*" {
// All info requested.
sql += `
, ( SELECT additional FROM host_additional WHERE host_id = h . id ) AS additional
`
} else if len ( opt . AdditionalFilters ) > 0 {
// Filter specific columns.
sql += ` , ( SELECT JSON_OBJECT (
Add host additional info filters (#28)
This change adds the ability to filter additional host info via the list hosts endpoint; a continuation from [here](https://github.com/kolide/fleet/pull/2330), but now filtering is accomplished via SQL.
Additional object without filter:
```
curl 'https://localhost:8080/api/v1/kolide/hosts'
...
"additional": {
"macs": [
{
"mac": "00:00:00:00:00:00"
},
{
"mac": "02:42:c0:a8:10:05"
}
],
"time": [
{
"day": "13",
"hour": "3",
"year": "2020",
"month": "10",
"minutes": "43",
"seconds": "11",
"weekday": "Tuesday",
"datetime": "2020-10-13T03:43:11Z",
"iso_8601": "2020-10-13T03:43:11Z",
"timezone": "GMT",
"timestamp": "Tue Oct 13 03:43:11 2020 UTC",
"unix_time": "1602560591",
"local_time": "1602560591",
"local_timezone": "UTC"
}
},
...
```
Additional object with filter:
```
curl 'https://localhost:8080/api/v1/kolide/hosts?additional_info_filters=macs,notreal'
...
"additional": {
"macs": [
{
"mac": "00:00:00:00:00:00"
},
{
"mac": "02:42:c0:a8:10:05"
}
],
"notreal": null
},
...
```
2020-11-14 00:33:25 +00:00
`
for _ , field := range opt . AdditionalFilters {
2021-05-17 17:29:50 +00:00
sql += ` ?, JSON_EXTRACT(additional, ?), `
Add host additional info filters (#28)
This change adds the ability to filter additional host info via the list hosts endpoint; a continuation from [here](https://github.com/kolide/fleet/pull/2330), but now filtering is accomplished via SQL.
Additional object without filter:
```
curl 'https://localhost:8080/api/v1/kolide/hosts'
...
"additional": {
"macs": [
{
"mac": "00:00:00:00:00:00"
},
{
"mac": "02:42:c0:a8:10:05"
}
],
"time": [
{
"day": "13",
"hour": "3",
"year": "2020",
"month": "10",
"minutes": "43",
"seconds": "11",
"weekday": "Tuesday",
"datetime": "2020-10-13T03:43:11Z",
"iso_8601": "2020-10-13T03:43:11Z",
"timezone": "GMT",
"timestamp": "Tue Oct 13 03:43:11 2020 UTC",
"unix_time": "1602560591",
"local_time": "1602560591",
"local_timezone": "UTC"
}
},
...
```
Additional object with filter:
```
curl 'https://localhost:8080/api/v1/kolide/hosts?additional_info_filters=macs,notreal'
...
"additional": {
"macs": [
{
"mac": "00:00:00:00:00:00"
},
{
"mac": "02:42:c0:a8:10:05"
}
],
"notreal": null
},
...
```
2020-11-14 00:33:25 +00:00
params = append ( params , field , fmt . Sprintf ( ` $."%s" ` , field ) )
}
sql = sql [ : len ( sql ) - 2 ]
sql += `
2021-05-26 23:24:12 +00:00
) FROM host_additional WHERE host_id = h . id ) AS additional
Add host additional info filters (#28)
This change adds the ability to filter additional host info via the list hosts endpoint; a continuation from [here](https://github.com/kolide/fleet/pull/2330), but now filtering is accomplished via SQL.
Additional object without filter:
```
curl 'https://localhost:8080/api/v1/kolide/hosts'
...
"additional": {
"macs": [
{
"mac": "00:00:00:00:00:00"
},
{
"mac": "02:42:c0:a8:10:05"
}
],
"time": [
{
"day": "13",
"hour": "3",
"year": "2020",
"month": "10",
"minutes": "43",
"seconds": "11",
"weekday": "Tuesday",
"datetime": "2020-10-13T03:43:11Z",
"iso_8601": "2020-10-13T03:43:11Z",
"timezone": "GMT",
"timestamp": "Tue Oct 13 03:43:11 2020 UTC",
"unix_time": "1602560591",
"local_time": "1602560591",
"local_timezone": "UTC"
}
},
...
```
Additional object with filter:
```
curl 'https://localhost:8080/api/v1/kolide/hosts?additional_info_filters=macs,notreal'
...
"additional": {
"macs": [
{
"mac": "00:00:00:00:00:00"
},
{
"mac": "02:42:c0:a8:10:05"
}
],
"notreal": null
},
...
```
2020-11-14 00:33:25 +00:00
`
}
2022-02-03 17:56:22 +00:00
sql , params = ds . applyHostFilters ( opt , sql , filter , params )
2021-10-07 11:25:35 +00:00
hosts := [ ] * fleet . Host { }
2022-02-03 17:56:22 +00:00
if err := sqlx . SelectContext ( ctx , ds . reader , & hosts , sql , params ... ) ; err != nil {
2021-11-15 14:11:38 +00:00
return nil , ctxerr . Wrap ( ctx , err , "list hosts" )
2021-10-07 11:25:35 +00:00
}
return hosts , nil
}
2022-02-03 17:56:22 +00:00
func ( ds * Datastore ) applyHostFilters ( opt fleet . HostListOptions , sql string , filter fleet . TeamFilter , params [ ] interface { } ) ( string , [ ] interface { } ) {
2022-04-15 20:09:47 +00:00
policyMembershipJoin := "JOIN policy_membership pm ON (h.id = pm.host_id)"
2021-08-24 20:24:52 +00:00
if opt . PolicyIDFilter == nil {
policyMembershipJoin = ""
} else if opt . PolicyResponseFilter == nil {
policyMembershipJoin = "LEFT " + policyMembershipJoin
}
2021-10-12 14:38:12 +00:00
softwareFilter := "TRUE"
if opt . SoftwareIDFilter != nil {
2022-04-15 20:09:47 +00:00
softwareFilter = "EXISTS (SELECT 1 FROM host_software hs WHERE hs.host_id = h.id AND hs.software_id = ?)"
2021-10-12 14:38:12 +00:00
params = append ( params , opt . SoftwareIDFilter )
}
2021-11-29 21:04:33 +00:00
failingPoliciesJoin := ` LEFT JOIN (
2022-04-15 20:09:47 +00:00
SELECT host_id , count ( * ) as count FROM policy_membership WHERE passes = 0
2021-11-29 21:04:33 +00:00
GROUP BY host_id
) as failing_policies ON ( h . id = failing_policies . host_id ) `
if opt . DisableFailingPolicies {
failingPoliciesJoin = ""
}
2021-11-08 14:42:37 +00:00
sql += fmt . Sprintf ( ` FROM hosts h
2022-04-15 20:09:47 +00:00
LEFT JOIN host_seen_times hst ON ( h . id = hst . host_id )
2021-11-08 14:42:37 +00:00
LEFT JOIN teams t ON ( h . team_id = t . id )
2021-11-29 21:04:33 +00:00
% s
2021-08-24 20:24:52 +00:00
% s
2021-10-12 14:38:12 +00:00
WHERE TRUE AND % s AND % s
2022-02-03 17:56:22 +00:00
` , policyMembershipJoin , failingPoliciesJoin , ds . whereFilterHostsByTeams ( filter , "h" ) , softwareFilter ,
2021-06-04 01:53:43 +00:00
)
2021-08-05 17:56:29 +00:00
sql , params = filterHostsByStatus ( sql , opt , params )
2021-08-11 14:40:56 +00:00
sql , params = filterHostsByTeam ( sql , opt , params )
2021-08-24 20:24:52 +00:00
sql , params = filterHostsByPolicy ( sql , opt , params )
2021-12-21 20:36:19 +00:00
sql , params = hostSearchLike ( sql , params , opt . MatchQuery , hostSearchColumns ... )
2021-11-29 18:06:00 +00:00
sql , params = appendListOptionsWithCursorToSQL ( sql , params , opt . ListOptions )
2021-08-05 17:56:29 +00:00
2021-10-07 11:25:35 +00:00
return sql , params
2021-08-05 17:56:29 +00:00
}
2021-08-11 14:40:56 +00:00
func filterHostsByTeam ( sql string , opt fleet . HostListOptions , params [ ] interface { } ) ( string , [ ] interface { } ) {
if opt . TeamFilter != nil {
sql += ` AND h.team_id = ? `
params = append ( params , * opt . TeamFilter )
}
return sql , params
}
2021-08-24 20:24:52 +00:00
func filterHostsByPolicy ( sql string , opt fleet . HostListOptions , params [ ] interface { } ) ( string , [ ] interface { } ) {
if opt . PolicyIDFilter != nil && opt . PolicyResponseFilter != nil {
sql += ` AND pm.policy_id = ? AND pm.passes = ? `
params = append ( params , * opt . PolicyIDFilter , * opt . PolicyResponseFilter )
} else if opt . PolicyIDFilter != nil && opt . PolicyResponseFilter == nil {
sql += ` AND (pm.policy_id = ? OR pm.policy_id IS NULL) AND pm.passes IS NULL `
params = append ( params , * opt . PolicyIDFilter )
}
return sql , params
}
2021-08-05 17:56:29 +00:00
func filterHostsByStatus ( sql string , opt fleet . HostListOptions , params [ ] interface { } ) ( string , [ ] interface { } ) {
2020-03-30 02:19:54 +00:00
switch opt . StatusFilter {
case "new" :
2021-05-18 00:52:59 +00:00
sql += "AND DATE_ADD(h.created_at, INTERVAL 1 DAY) >= ?"
2020-03-30 02:19:54 +00:00
params = append ( params , time . Now ( ) )
case "online" :
2021-12-01 12:05:23 +00:00
sql += fmt . Sprintf ( "AND DATE_ADD(COALESCE(hst.seen_time, h.created_at), INTERVAL LEAST(h.distributed_interval, h.config_tls_refresh) + %d SECOND) > ?" , fleet . OnlineIntervalBuffer )
2020-03-30 02:19:54 +00:00
params = append ( params , time . Now ( ) )
case "offline" :
2021-12-01 12:05:23 +00:00
sql += fmt . Sprintf ( "AND DATE_ADD(COALESCE(hst.seen_time, h.created_at), INTERVAL LEAST(h.distributed_interval, h.config_tls_refresh) + %d SECOND) <= ? AND DATE_ADD(COALESCE(hst.seen_time, h.created_at), INTERVAL 30 DAY) >= ?" , fleet . OnlineIntervalBuffer )
2020-03-30 02:19:54 +00:00
params = append ( params , time . Now ( ) , time . Now ( ) )
case "mia" :
2021-12-01 12:05:23 +00:00
sql += "AND DATE_ADD(COALESCE(hst.seen_time, h.created_at), INTERVAL 30 DAY) <= ?"
2020-03-30 02:19:54 +00:00
params = append ( params , time . Now ( ) )
}
2021-08-05 17:56:29 +00:00
return sql , params
2016-12-06 19:51:11 +00:00
}
2022-02-03 17:56:22 +00:00
func ( ds * Datastore ) CountHosts ( ctx context . Context , filter fleet . TeamFilter , opt fleet . HostListOptions ) ( int , error ) {
2021-10-07 11:25:35 +00:00
sql := ` SELECT count(*) `
2021-10-12 14:38:12 +00:00
// ignore pagination in count
opt . Page = 0
opt . PerPage = 0
2021-10-07 11:25:35 +00:00
var params [ ] interface { }
2022-02-03 17:56:22 +00:00
sql , params = ds . applyHostFilters ( opt , sql , filter , params )
2021-10-07 11:25:35 +00:00
var count int
2022-02-03 17:56:22 +00:00
if err := sqlx . GetContext ( ctx , ds . reader , & count , sql , params ... ) ; err != nil {
2021-11-15 14:11:38 +00:00
return 0 , ctxerr . Wrap ( ctx , err , "count hosts" )
2021-10-07 11:25:35 +00:00
}
return count , nil
}
2022-02-03 17:56:22 +00:00
func ( ds * Datastore ) CleanupIncomingHosts ( ctx context . Context , now time . Time ) error {
2019-04-09 18:11:11 +00:00
sqlStatement := `
DELETE FROM hosts
2021-06-24 00:32:19 +00:00
WHERE hostname = ' ' AND osquery_version = ' '
2019-04-09 18:11:11 +00:00
AND created_at < ( ? - INTERVAL 5 MINUTE )
`
2022-02-03 17:56:22 +00:00
if _ , err := ds . writer . ExecContext ( ctx , sqlStatement , now ) ; err != nil {
2021-11-15 14:11:38 +00:00
return ctxerr . Wrap ( ctx , err , "cleanup incoming hosts" )
2019-04-09 18:11:11 +00:00
}
return nil
}
2022-02-03 17:56:22 +00:00
func ( ds * Datastore ) GenerateHostStatusStatistics ( ctx context . Context , filter fleet . TeamFilter , now time . Time , platform * string ) ( * fleet . HostSummary , error ) {
2017-04-18 17:39:50 +00:00
// The logic in this function should remain synchronized with
2021-11-09 14:35:36 +00:00
// host.Status and CountHostsInTargets - that is, the intervals associated
// with each status must be the same.
2017-04-18 17:39:50 +00:00
2022-01-24 17:49:21 +00:00
args := [ ] interface { } { now , now , now , now , now }
2022-02-03 17:56:22 +00:00
whereClause := ds . whereFilterHostsByTeams ( filter , "h" )
2022-01-24 17:49:21 +00:00
if platform != nil {
2022-02-07 16:50:36 +00:00
whereClause += " AND h.platform IN (?) "
args = append ( args , fleet . ExpandPlatform ( * platform ) )
2022-01-24 17:49:21 +00:00
}
2017-04-18 17:39:50 +00:00
sqlStatement := fmt . Sprintf ( `
2021-06-04 01:53:43 +00:00
SELECT
2021-11-09 14:35:36 +00:00
COUNT ( * ) total ,
2021-12-01 12:05:23 +00:00
COALESCE ( SUM ( CASE WHEN DATE_ADD ( COALESCE ( hst . seen_time , h . created_at ) , INTERVAL 30 DAY ) <= ? THEN 1 ELSE 0 END ) , 0 ) mia ,
COALESCE ( SUM ( CASE WHEN DATE_ADD ( COALESCE ( hst . seen_time , h . created_at ) , INTERVAL LEAST ( distributed_interval , config_tls_refresh ) + % d SECOND ) <= ? AND DATE_ADD ( COALESCE ( hst . seen_time , h . created_at ) , INTERVAL 30 DAY ) >= ? THEN 1 ELSE 0 END ) , 0 ) offline ,
COALESCE ( SUM ( CASE WHEN DATE_ADD ( COALESCE ( hst . seen_time , h . created_at ) , INTERVAL LEAST ( distributed_interval , config_tls_refresh ) + % d SECOND ) > ? THEN 1 ELSE 0 END ) , 0 ) online ,
2021-06-04 01:53:43 +00:00
COALESCE ( SUM ( CASE WHEN DATE_ADD ( created_at , INTERVAL 1 DAY ) >= ? THEN 1 ELSE 0 END ) , 0 ) new
2022-04-15 20:09:47 +00:00
FROM hosts h LEFT JOIN host_seen_times hst ON ( h . id = hst . host_id ) WHERE % s
2021-06-04 01:53:43 +00:00
LIMIT 1 ;
2021-11-09 14:35:36 +00:00
` , fleet . OnlineIntervalBuffer , fleet . OnlineIntervalBuffer , whereClause )
2017-01-04 21:16:17 +00:00
2022-02-07 16:50:36 +00:00
stmt , args , err := sqlx . In ( sqlStatement , args ... )
if err != nil {
return nil , ctxerr . Wrap ( ctx , err , "generating host statistics statement" )
}
2021-11-15 14:56:13 +00:00
summary := fleet . HostSummary { TeamID : filter . TeamID }
2022-02-07 16:50:36 +00:00
err = sqlx . GetContext ( ctx , ds . reader , & summary , stmt , args ... )
2017-01-16 19:52:03 +00:00
if err != nil && err != sql . ErrNoRows {
2021-11-09 14:35:36 +00:00
return nil , ctxerr . Wrap ( ctx , err , "generating host statistics" )
}
// get the counts per platform, the `h` alias for hosts is required so that
// reusing the whereClause is ok.
2022-01-24 17:49:21 +00:00
args = [ ] interface { } { }
if platform != nil {
2022-02-07 16:50:36 +00:00
args = append ( args , fleet . ExpandPlatform ( * platform ) )
2022-01-24 17:49:21 +00:00
}
2021-11-09 14:35:36 +00:00
sqlStatement = fmt . Sprintf ( `
SELECT
COUNT ( * ) total ,
h . platform
FROM hosts h
WHERE % s
GROUP BY h . platform
` , whereClause )
var platforms [ ] * fleet . HostSummaryPlatform
2022-02-07 16:50:36 +00:00
stmt , args , err = sqlx . In ( sqlStatement , args ... )
if err != nil {
return nil , ctxerr . Wrap ( ctx , err , "generating host platforms statement" )
}
err = sqlx . SelectContext ( ctx , ds . reader , & platforms , stmt , args ... )
2021-11-09 14:35:36 +00:00
if err != nil {
return nil , ctxerr . Wrap ( ctx , err , "generating host platforms statistics" )
2017-01-04 21:16:17 +00:00
}
2021-11-09 14:35:36 +00:00
summary . Platforms = platforms
2017-01-04 21:16:17 +00:00
2021-11-09 14:35:36 +00:00
return & summary , nil
2017-01-04 21:16:17 +00:00
}
2016-11-16 13:47:49 +00:00
// EnrollHost enrolls a host
2022-02-03 17:56:22 +00:00
func ( ds * Datastore ) EnrollHost ( ctx context . Context , osqueryHostID , nodeKey string , teamID * uint , cooldown time . Duration ) ( * fleet . Host , error ) {
2016-12-06 19:51:11 +00:00
if osqueryHostID == "" {
2021-11-15 14:11:38 +00:00
return nil , ctxerr . New ( ctx , "missing osquery host identifier" )
2016-11-16 13:47:49 +00:00
}
2016-12-06 19:51:11 +00:00
2021-06-06 22:07:29 +00:00
var host fleet . Host
2022-02-03 17:56:22 +00:00
err := ds . withRetryTxx ( ctx , func ( tx sqlx . ExtContext ) error {
2020-12-10 19:04:58 +00:00
zeroTime := time . Unix ( 0 , 0 ) . Add ( 24 * time . Hour )
2016-11-16 13:47:49 +00:00
2021-12-01 12:05:23 +00:00
var hostID int64
2021-09-14 14:44:02 +00:00
err := sqlx . GetContext ( ctx , tx , & host , ` SELECT id, last_enrolled_at FROM hosts WHERE osquery_host_id = ? ` , osqueryHostID )
2021-01-19 22:45:58 +00:00
switch {
case err != nil && ! errors . Is ( err , sql . ErrNoRows ) :
2021-11-15 14:11:38 +00:00
return ctxerr . Wrap ( ctx , err , "check existing" )
2021-01-19 22:45:58 +00:00
case errors . Is ( err , sql . ErrNoRows ) :
2020-12-10 19:04:58 +00:00
// Create new host record
sqlInsert := `
INSERT INTO hosts (
2021-06-24 00:32:19 +00:00
detail_updated_at ,
label_updated_at ,
2021-09-27 19:27:38 +00:00
policy_updated_at ,
2020-12-10 19:04:58 +00:00
osquery_host_id ,
node_key ,
2021-05-31 16:02:05 +00:00
team_id
2021-11-08 14:42:37 +00:00
) VALUES ( ? , ? , ? , ? , ? , ? )
2020-12-10 19:04:58 +00:00
`
2021-11-08 14:42:37 +00:00
result , err := tx . ExecContext ( ctx , sqlInsert , zeroTime , zeroTime , zeroTime , osqueryHostID , nodeKey , teamID )
2020-12-10 19:04:58 +00:00
if err != nil {
2021-11-15 14:11:38 +00:00
return ctxerr . Wrap ( ctx , err , "insert host" )
2020-12-10 19:04:58 +00:00
}
2021-12-01 12:05:23 +00:00
hostID , _ = result . LastInsertId ( )
2021-01-19 22:45:58 +00:00
default :
2020-12-10 19:04:58 +00:00
// Prevent hosts from enrolling too often with the same identifier.
// Prior to adding this we saw many hosts (probably VMs) with the
// same identifier competing for enrollment and causing perf issues.
2021-06-24 00:32:19 +00:00
if cooldown > 0 && time . Since ( host . LastEnrolledAt ) < cooldown {
2021-11-15 14:11:38 +00:00
return backoff . Permanent ( ctxerr . Errorf ( ctx , "host identified by %s enrolling too often" , osqueryHostID ) )
2020-12-10 19:04:58 +00:00
}
2021-12-01 12:05:23 +00:00
hostID = int64 ( host . ID )
2020-12-10 19:04:58 +00:00
// Update existing host record
sqlUpdate := `
UPDATE hosts
SET node_key = ? ,
2021-05-31 16:02:05 +00:00
team_id = ? ,
2021-06-24 00:32:19 +00:00
last_enrolled_at = NOW ( )
2020-12-10 19:04:58 +00:00
WHERE osquery_host_id = ?
`
2021-09-14 14:44:02 +00:00
_ , err := tx . ExecContext ( ctx , sqlUpdate , nodeKey , teamID , osqueryHostID )
2020-12-10 19:04:58 +00:00
if err != nil {
2021-11-15 14:11:38 +00:00
return ctxerr . Wrap ( ctx , err , "update host" )
2020-12-10 19:04:58 +00:00
}
}
2021-12-01 12:05:23 +00:00
_ , err = tx . ExecContext ( ctx , `
2022-04-15 20:09:47 +00:00
INSERT INTO host_seen_times ( host_id , seen_time ) VALUES ( ? , ? )
2021-12-01 12:05:23 +00:00
ON DUPLICATE KEY UPDATE seen_time = VALUES ( seen_time ) ` ,
hostID , time . Now ( ) . UTC ( ) )
if err != nil {
return ctxerr . Wrap ( ctx , err , "new host seen time" )
}
2020-12-10 19:04:58 +00:00
sqlSelect := `
SELECT * FROM hosts WHERE id = ? LIMIT 1
`
2021-12-01 12:05:23 +00:00
err = sqlx . GetContext ( ctx , tx , & host , sqlSelect , hostID )
2020-12-10 19:04:58 +00:00
if err != nil {
2021-11-15 14:11:38 +00:00
return ctxerr . Wrap ( ctx , err , "getting the host to return" )
2020-12-10 19:04:58 +00:00
}
2021-12-01 12:05:23 +00:00
_ , err = tx . ExecContext ( ctx , ` INSERT IGNORE INTO label_membership (host_id, label_id) VALUES (?, (SELECT id FROM labels WHERE name = 'All Hosts' AND label_type = 1)) ` , hostID )
2020-12-10 19:04:58 +00:00
if err != nil {
2021-11-15 14:11:38 +00:00
return ctxerr . Wrap ( ctx , err , "insert new host into all hosts label" )
2020-12-10 19:04:58 +00:00
}
return nil
} )
if err != nil {
return nil , err
}
return & host , nil
2016-11-16 13:47:49 +00:00
}
2022-03-09 21:13:56 +00:00
// getContextTryStmt will attempt to run sqlx.GetContext on a cached statement if available, resorting to ds.reader.
func ( ds * Datastore ) getContextTryStmt ( ctx context . Context , dest interface { } , query string , args ... interface { } ) error {
2022-02-14 15:13:38 +00:00
var err error
//nolint the statements are closed in Datastore.Close.
if stmt := ds . loadOrPrepareStmt ( ctx , query ) ; stmt != nil {
err = stmt . GetContext ( ctx , dest , args ... )
} else {
err = sqlx . GetContext ( ctx , ds . reader , dest , query , args ... )
}
return err
}
2022-01-18 01:52:09 +00:00
// LoadHostByNodeKey loads the whole host identified by the node key.
// If the node key is invalid it returns a NotFoundError.
2022-02-03 17:56:22 +00:00
func ( ds * Datastore ) LoadHostByNodeKey ( ctx context . Context , nodeKey string ) ( * fleet . Host , error ) {
2022-02-14 15:13:38 +00:00
query := ` SELECT * FROM hosts WHERE node_key = ? `
2022-01-18 01:52:09 +00:00
var host fleet . Host
2022-03-09 21:13:56 +00:00
switch err := ds . getContextTryStmt ( ctx , & host , query , nodeKey ) ; {
case err == nil :
return & host , nil
case errors . Is ( err , sql . ErrNoRows ) :
return nil , ctxerr . Wrap ( ctx , notFound ( "Host" ) )
default :
return nil , ctxerr . Wrap ( ctx , err , "find host" )
}
}
// LoadHostByDeviceAuthToken loads the whole host identified by the device auth token.
// If the token is invalid it returns a NotFoundError.
func ( ds * Datastore ) LoadHostByDeviceAuthToken ( ctx context . Context , authToken string ) ( * fleet . Host , error ) {
const query = `
SELECT
h . *
FROM
host_device_auth hda
INNER JOIN
hosts h
ON
hda . host_id = h . id
WHERE hda . token = ? `
var host fleet . Host
switch err := sqlx . GetContext ( ctx , ds . reader , & host , query , authToken ) ; {
2022-01-18 01:52:09 +00:00
case err == nil :
return & host , nil
case errors . Is ( err , sql . ErrNoRows ) :
return nil , ctxerr . Wrap ( ctx , notFound ( "Host" ) )
default :
return nil , ctxerr . Wrap ( ctx , err , "find host" )
2016-11-16 13:47:49 +00:00
}
}
2022-03-15 19:51:00 +00:00
// SetOrUpdateDeviceAuthToken inserts or updates the auth token for a host.
func ( ds * Datastore ) SetOrUpdateDeviceAuthToken ( ctx context . Context , hostID uint , authToken string ) error {
return ds . updateOrInsert (
ctx ,
2022-04-15 20:09:47 +00:00
` UPDATE host_device_auth SET token = ? WHERE host_id = ? ` ,
` INSERT INTO host_device_auth (token, host_id) VALUES (?, ?) ` ,
2022-03-15 19:51:00 +00:00
authToken , hostID ,
)
}
2022-02-03 17:56:22 +00:00
func ( ds * Datastore ) MarkHostsSeen ( ctx context . Context , hostIDs [ ] uint , t time . Time ) error {
2021-04-12 23:22:22 +00:00
if len ( hostIDs ) == 0 {
return nil
}
2021-12-01 19:20:54 +00:00
// Sort by host id to prevent deadlocks:
// https://percona.community/blog/2018/09/24/minimize-mysql-deadlocks-3-steps/
// https://dev.mysql.com/doc/refman/5.7/en/innodb-deadlocks-handling.html
sort . Slice ( hostIDs , func ( i , j int ) bool { return hostIDs [ i ] < hostIDs [ j ] } )
2022-02-03 17:56:22 +00:00
if err := ds . withRetryTxx ( ctx , func ( tx sqlx . ExtContext ) error {
2021-12-01 12:05:23 +00:00
var insertArgs [ ] interface { }
for _ , hostID := range hostIDs {
insertArgs = append ( insertArgs , hostID , t )
2021-04-12 23:22:22 +00:00
}
2021-12-01 12:05:23 +00:00
insertValues := strings . TrimSuffix ( strings . Repeat ( "(?, ?)," , len ( hostIDs ) ) , "," )
query := fmt . Sprintf ( `
INSERT INTO host_seen_times ( host_id , seen_time ) VALUES % s
ON DUPLICATE KEY UPDATE seen_time = VALUES ( seen_time ) ` ,
insertValues ,
)
if _ , err := tx . ExecContext ( ctx , query , insertArgs ... ) ; err != nil {
2021-11-15 14:11:38 +00:00
return ctxerr . Wrap ( ctx , err , "exec update" )
2021-04-12 23:22:22 +00:00
}
return nil
} ) ; err != nil {
2021-11-15 14:11:38 +00:00
return ctxerr . Wrap ( ctx , err , "MarkHostsSeen transaction" )
2021-04-12 23:22:22 +00:00
}
return nil
}
2021-10-21 20:46:21 +00:00
// SearchHosts performs a search on the hosts table using the following criteria:
// - Use the provided team filter.
2022-02-22 13:19:51 +00:00
// - Search hostname, uuid, hardware_serial, and primary_ip using LIKE (mimics ListHosts behavior)
2021-10-21 20:46:21 +00:00
// - An optional list of IDs to omit from the search.
2022-02-22 13:19:51 +00:00
func ( ds * Datastore ) SearchHosts ( ctx context . Context , filter fleet . TeamFilter , matchQuery string , omit ... uint ) ( [ ] * fleet . Host , error ) {
query := ` SELECT
2021-12-01 12:05:23 +00:00
h . * ,
COALESCE ( hst . seen_time , h . created_at ) AS seen_time
FROM hosts h
LEFT JOIN host_seen_times hst
2022-04-15 20:09:47 +00:00
ON ( h . id = hst . host_id ) WHERE TRUE `
2016-12-01 17:00:00 +00:00
2021-10-21 20:46:21 +00:00
var args [ ] interface { }
2022-02-22 13:19:51 +00:00
if len ( matchQuery ) > 0 {
query , args = hostSearchLike ( query , args , matchQuery , hostSearchColumns ... )
2016-11-16 13:47:49 +00:00
}
2017-01-17 14:51:04 +00:00
var in interface { }
2021-10-21 20:46:21 +00:00
// use -1 if there are no values to omit.
// Avoids empty args error for `sqlx.In`
in = omit
if len ( omit ) == 0 {
in = - 1
}
args = append ( args , in )
2022-02-22 13:19:51 +00:00
query += " AND id NOT IN (?) AND "
query += ds . whereFilterHostsByTeams ( filter , "h" )
query += ` ORDER BY h.id DESC LIMIT 10 `
2021-10-21 20:46:21 +00:00
2022-02-22 13:19:51 +00:00
query , args , err := sqlx . In ( query , args ... )
2017-01-17 14:51:04 +00:00
if err != nil {
2021-11-15 14:11:38 +00:00
return nil , ctxerr . Wrap ( ctx , err , "searching default hosts" )
2017-01-17 14:51:04 +00:00
}
2022-02-22 13:19:51 +00:00
query = ds . reader . Rebind ( query )
2021-06-06 22:07:29 +00:00
hosts := [ ] * fleet . Host { }
2022-02-22 13:19:51 +00:00
if err := sqlx . SelectContext ( ctx , ds . reader , & hosts , query , args ... ) ; err != nil {
2021-11-15 14:11:38 +00:00
return nil , ctxerr . Wrap ( ctx , err , "searching hosts" )
2016-11-16 13:47:49 +00:00
}
return hosts , nil
}
2022-02-03 17:56:22 +00:00
func ( ds * Datastore ) HostIDsByName ( ctx context . Context , filter fleet . TeamFilter , hostnames [ ] string ) ( [ ] uint , error ) {
2020-01-24 05:27:20 +00:00
if len ( hostnames ) == 0 {
return [ ] uint { } , nil
}
2021-06-04 01:53:43 +00:00
sqlStatement := fmt . Sprintf ( `
SELECT id FROM hosts
2021-06-24 00:32:19 +00:00
WHERE hostname IN ( ? ) AND % s
2022-02-03 17:56:22 +00:00
` , ds . whereFilterHostsByTeams ( filter , "hosts" ) ,
2021-06-04 01:53:43 +00:00
)
2018-05-17 22:54:34 +00:00
sql , args , err := sqlx . In ( sqlStatement , hostnames )
if err != nil {
2021-11-15 14:11:38 +00:00
return nil , ctxerr . Wrap ( ctx , err , "building query to get host IDs" )
2018-05-17 22:54:34 +00:00
}
var hostIDs [ ] uint
2022-02-03 17:56:22 +00:00
if err := sqlx . SelectContext ( ctx , ds . reader , & hostIDs , sql , args ... ) ; err != nil {
2021-11-15 14:11:38 +00:00
return nil , ctxerr . Wrap ( ctx , err , "get host IDs" )
2018-05-17 22:54:34 +00:00
}
return hostIDs , nil
}
2020-04-22 20:54:32 +00:00
2022-02-03 17:56:22 +00:00
func ( ds * Datastore ) HostByIdentifier ( ctx context . Context , identifier string ) ( * fleet . Host , error ) {
2021-12-14 21:34:11 +00:00
stmt := `
2020-04-22 20:54:32 +00:00
SELECT * FROM hosts
2021-06-24 00:32:19 +00:00
WHERE ? IN ( hostname , osquery_host_id , node_key , uuid )
2020-04-22 20:54:32 +00:00
LIMIT 1
`
2021-06-06 22:07:29 +00:00
host := & fleet . Host { }
2022-02-03 17:56:22 +00:00
err := sqlx . GetContext ( ctx , ds . reader , host , stmt , identifier )
2020-04-22 20:54:32 +00:00
if err != nil {
2021-12-14 21:34:11 +00:00
if err == sql . ErrNoRows {
return nil , ctxerr . Wrap ( ctx , notFound ( "Host" ) . WithName ( identifier ) )
}
2021-11-15 14:11:38 +00:00
return nil , ctxerr . Wrap ( ctx , err , "get host by identifier" )
2020-04-22 20:54:32 +00:00
}
2022-02-03 17:56:22 +00:00
packStats , err := loadHostPackStatsDB ( ctx , ds . reader , host . ID , host . Platform )
2021-11-12 11:18:25 +00:00
if err != nil {
2021-05-07 04:05:09 +00:00
return nil , err
}
2021-11-12 11:18:25 +00:00
host . PackStats = packStats
2021-05-07 04:05:09 +00:00
2020-04-22 20:54:32 +00:00
return host , nil
}
2021-05-17 19:23:21 +00:00
2022-02-03 17:56:22 +00:00
func ( ds * Datastore ) AddHostsToTeam ( ctx context . Context , teamID * uint , hostIDs [ ] uint ) error {
2021-05-17 19:23:21 +00:00
if len ( hostIDs ) == 0 {
return nil
}
2022-02-03 17:56:22 +00:00
return ds . withRetryTxx ( ctx , func ( tx sqlx . ExtContext ) error {
2021-10-05 18:48:26 +00:00
// hosts can only be in one team, so if there's a policy that has a team id and a result from one of our hosts
// it can only be from the previous team they are being transferred from
2021-12-03 16:10:11 +00:00
query , args , err := sqlx . In ( ` DELETE FROM policy_membership
2021-10-05 18:48:26 +00:00
WHERE policy_id IN ( SELECT id FROM policies WHERE team_id IS NOT NULL ) AND host_id IN ( ? ) ` , hostIDs )
if err != nil {
2021-11-15 14:11:38 +00:00
return ctxerr . Wrap ( ctx , err , "add host to team sqlx in" )
2021-10-05 18:48:26 +00:00
}
if _ , err := tx . ExecContext ( ctx , query , args ... ) ; err != nil {
2021-12-03 16:10:11 +00:00
return ctxerr . Wrap ( ctx , err , "exec AddHostsToTeam delete policy membership" )
2021-10-05 18:48:26 +00:00
}
2021-05-17 19:23:21 +00:00
2021-10-05 18:48:26 +00:00
query , args , err = sqlx . In ( ` UPDATE hosts SET team_id = ? WHERE id IN (?) ` , teamID , hostIDs )
if err != nil {
2021-11-15 14:11:38 +00:00
return ctxerr . Wrap ( ctx , err , "sqlx.In AddHostsToTeam" )
2021-10-05 18:48:26 +00:00
}
2021-05-17 19:23:21 +00:00
2021-10-05 18:48:26 +00:00
if _ , err := tx . ExecContext ( ctx , query , args ... ) ; err != nil {
2021-11-15 14:11:38 +00:00
return ctxerr . Wrap ( ctx , err , "exec AddHostsToTeam" )
2021-10-05 18:48:26 +00:00
}
return nil
} )
2021-05-17 19:23:21 +00:00
}
2021-05-31 17:56:50 +00:00
2022-02-03 17:56:22 +00:00
func ( ds * Datastore ) SaveHostAdditional ( ctx context . Context , hostID uint , additional * json . RawMessage ) error {
return saveHostAdditionalDB ( ctx , ds . writer , hostID , additional )
2022-01-18 01:52:09 +00:00
}
func saveHostAdditionalDB ( ctx context . Context , exec sqlx . ExecerContext , hostID uint , additional * json . RawMessage ) error {
2021-05-26 23:24:12 +00:00
sql := `
INSERT INTO host_additional ( host_id , additional )
VALUES ( ? , ? )
ON DUPLICATE KEY UPDATE additional = VALUES ( additional )
`
2022-01-18 01:52:09 +00:00
if _ , err := exec . ExecContext ( ctx , sql , hostID , additional ) ; err != nil {
2021-11-15 14:11:38 +00:00
return ctxerr . Wrap ( ctx , err , "insert additional" )
2021-05-26 23:24:12 +00:00
}
return nil
}
2021-07-13 20:15:38 +00:00
2022-02-03 17:56:22 +00:00
func ( ds * Datastore ) SaveHostUsers ( ctx context . Context , hostID uint , users [ ] fleet . HostUser ) error {
return ds . withRetryTxx ( ctx , func ( tx sqlx . ExtContext ) error {
2022-01-18 01:52:09 +00:00
return saveHostUsersDB ( ctx , tx , hostID , users )
} )
}
func saveHostUsersDB ( ctx context . Context , tx sqlx . ExtContext , hostID uint , users [ ] fleet . HostUser ) error {
currentHostUsers , err := loadHostUsersDB ( ctx , tx , hostID )
if err != nil {
2021-07-13 20:15:38 +00:00
return err
}
2021-09-07 14:02:35 +00:00
keyForUser := func ( u * fleet . HostUser ) string { return fmt . Sprintf ( "%d\x00%s" , u . Uid , u . Username ) }
incomingUsers := make ( map [ string ] bool )
var insertArgs [ ] interface { }
2022-01-18 01:52:09 +00:00
for _ , u := range users {
insertArgs = append ( insertArgs , hostID , u . Uid , u . Username , u . Type , u . GroupName , u . Shell )
2021-09-07 14:02:35 +00:00
incomingUsers [ keyForUser ( & u ) ] = true
2021-07-13 20:15:38 +00:00
}
var removedArgs [ ] interface { }
2022-01-18 01:52:09 +00:00
for _ , u := range currentHostUsers {
2021-09-07 14:02:35 +00:00
if _ , ok := incomingUsers [ keyForUser ( & u ) ] ; ! ok {
removedArgs = append ( removedArgs , u . Username )
2021-07-13 20:15:38 +00:00
}
}
2022-01-18 01:52:09 +00:00
insertValues := strings . TrimSuffix ( strings . Repeat ( "(?, ?, ?, ?, ?, ?)," , len ( users ) ) , "," )
2021-09-07 14:02:35 +00:00
insertSql := fmt . Sprintf (
2021-12-14 21:34:11 +00:00
` INSERT INTO host_users ( host_id , uid , username , user_type , groupname , shell )
VALUES % s
2021-11-23 13:23:12 +00:00
ON DUPLICATE KEY UPDATE
user_type = VALUES ( user_type ) ,
groupname = VALUES ( groupname ) ,
shell = VALUES ( shell ) ,
2022-04-15 20:09:47 +00:00
removed_at = NULL ` ,
2021-09-07 14:02:35 +00:00
insertValues ,
)
2021-09-14 14:44:02 +00:00
if _ , err := tx . ExecContext ( ctx , insertSql , insertArgs ... ) ; err != nil {
2021-11-15 14:11:38 +00:00
return ctxerr . Wrap ( ctx , err , "insert users" )
2021-09-07 14:02:35 +00:00
}
2021-07-13 20:15:38 +00:00
if len ( removedArgs ) == 0 {
return nil
}
removedValues := strings . TrimSuffix ( strings . Repeat ( "?," , len ( removedArgs ) ) , "," )
removedSql := fmt . Sprintf (
2021-09-07 14:02:35 +00:00
` UPDATE host_users SET removed_at = CURRENT_TIMESTAMP WHERE host_id = ? and username IN (%s) ` ,
2021-07-13 20:15:38 +00:00
removedValues ,
)
2022-01-18 01:52:09 +00:00
if _ , err := tx . ExecContext ( ctx , removedSql , append ( [ ] interface { } { hostID } , removedArgs ... ) ... ) ; err != nil {
2021-11-15 14:11:38 +00:00
return ctxerr . Wrap ( ctx , err , "mark users as removed" )
2021-07-13 20:15:38 +00:00
}
return nil
}
2021-08-27 14:15:36 +00:00
2022-02-03 17:56:22 +00:00
func ( ds * Datastore ) TotalAndUnseenHostsSince ( ctx context . Context , daysCount int ) ( total int , unseen int , err error ) {
2021-12-01 12:05:23 +00:00
var counts struct {
Total int ` db:"total" `
Unseen int ` db:"unseen" `
2021-08-27 14:15:36 +00:00
}
2022-02-03 17:56:22 +00:00
err = sqlx . GetContext ( ctx , ds . reader , & counts ,
2021-12-01 12:05:23 +00:00
` SELECT
COUNT ( * ) as total ,
SUM ( IF ( DATEDIFF ( CURRENT_DATE , COALESCE ( hst . seen_time , h . created_at ) ) >= ? , 1 , 0 ) ) as unseen
FROM hosts h
LEFT JOIN host_seen_times hst
ON h . id = hst . host_id ` ,
2021-08-27 14:15:36 +00:00
daysCount ,
2021-09-01 19:50:52 +00:00
)
2021-08-27 14:15:36 +00:00
if err != nil {
2021-12-01 12:05:23 +00:00
return 0 , 0 , ctxerr . Wrap ( ctx , err , "getting total and unseen host counts" )
2021-08-27 14:15:36 +00:00
}
2021-12-01 12:05:23 +00:00
return counts . Total , counts . Unseen , nil
2021-08-27 14:15:36 +00:00
}
2021-09-29 16:13:23 +00:00
2022-02-03 17:56:22 +00:00
func ( ds * Datastore ) DeleteHosts ( ctx context . Context , ids [ ] uint ) error {
2022-03-23 15:15:05 +00:00
for _ , id := range ids {
if err := ds . DeleteHost ( ctx , id ) ; err != nil {
return ctxerr . Wrapf ( ctx , err , "delete host %d" , id )
}
2022-01-19 13:28:08 +00:00
}
2021-09-29 16:13:23 +00:00
return nil
}
2021-10-07 11:11:10 +00:00
2022-02-03 17:56:22 +00:00
func ( ds * Datastore ) ListPoliciesForHost ( ctx context . Context , host * fleet . Host ) ( [ ] * fleet . HostPolicy , error ) {
2021-12-03 18:33:33 +00:00
if host . FleetPlatform ( ) == "" {
// We log to help troubleshooting in case this happens.
2022-02-03 17:56:22 +00:00
level . Error ( ds . logger ) . Log ( "err" , fmt . Sprintf ( "host %d with empty platform" , host . ID ) )
2021-12-03 18:33:33 +00:00
}
2021-11-24 17:16:42 +00:00
query := ` SELECT p . * ,
COALESCE ( u . name , ' < deleted > ' ) AS author_name ,
COALESCE ( u . email , ' ' ) AS author_email ,
2021-10-07 11:11:10 +00:00
CASE
2021-11-09 14:35:36 +00:00
WHEN pm . passes = 1 THEN ' pass '
WHEN pm . passes = 0 THEN ' fail '
ELSE ' '
2021-10-20 15:07:16 +00:00
END AS response ,
2021-10-21 18:53:23 +00:00
coalesce ( p . resolution , ' ' ) as resolution
2021-11-11 11:40:32 +00:00
FROM policies p
2021-12-03 16:10:11 +00:00
LEFT JOIN policy_membership pm ON ( p . id = pm . policy_id AND host_id = ? )
2021-11-24 21:17:44 +00:00
LEFT JOIN users u ON p . author_id = u . id
2021-12-03 18:33:33 +00:00
WHERE ( p . team_id IS NULL OR p . team_id = ( select team_id from hosts WHERE id = ? ) )
2022-04-11 18:47:50 +00:00
AND ( p . platforms IS NULL OR p . platforms = ' ' OR FIND_IN_SET ( ? , p . platforms ) != 0 ) `
2021-10-07 11:11:10 +00:00
var policies [ ] * fleet . HostPolicy
2022-02-03 17:56:22 +00:00
if err := sqlx . SelectContext ( ctx , ds . reader , & policies , query , host . ID , host . ID , host . FleetPlatform ( ) ) ; err != nil {
2021-11-15 14:11:38 +00:00
return nil , ctxerr . Wrap ( ctx , err , "get host policies" )
2021-10-07 11:11:10 +00:00
}
return policies , nil
}
2021-10-19 20:47:37 +00:00
2022-02-03 17:56:22 +00:00
func ( ds * Datastore ) CleanupExpiredHosts ( ctx context . Context ) error {
ac , err := appConfigDB ( ctx , ds . reader )
2021-10-19 20:47:37 +00:00
if err != nil {
2021-11-15 14:11:38 +00:00
return ctxerr . Wrap ( ctx , err , "getting app config" )
2021-10-19 20:47:37 +00:00
}
if ! ac . HostExpirySettings . HostExpiryEnabled {
return nil
}
2021-11-08 14:42:37 +00:00
// Usual clean up queries used to be like this:
// DELETE FROM hosts WHERE id in (SELECT host_id FROM host_seen_times WHERE seen_time < DATE_SUB(NOW(), INTERVAL ? DAY))
// This means a full table scan for hosts, and for big deployments, that's not ideal
// so instead, we get the ids one by one and delete things one by one
// it might take longer, but it should lock only the row we need
2022-02-03 17:56:22 +00:00
rows , err := ds . writer . QueryContext (
2021-11-08 14:42:37 +00:00
ctx ,
2021-12-01 12:05:23 +00:00
` SELECT h . id FROM hosts h
LEFT JOIN host_seen_times hst
ON h . id = hst . host_id
WHERE COALESCE ( hst . seen_time , h . created_at ) < DATE_SUB ( NOW ( ) , INTERVAL ? DAY ) ` ,
2021-11-08 14:42:37 +00:00
ac . HostExpirySettings . HostExpiryWindow ,
)
if err != nil {
2021-11-15 14:11:38 +00:00
return ctxerr . Wrap ( ctx , err , "getting expired host ids" )
2021-11-08 14:42:37 +00:00
}
defer rows . Close ( )
for rows . Next ( ) {
var id uint
err := rows . Scan ( & id )
if err != nil {
2021-11-15 14:11:38 +00:00
return ctxerr . Wrap ( ctx , err , "scanning expired host id" )
2021-11-08 14:42:37 +00:00
}
2022-02-03 17:56:22 +00:00
err = ds . DeleteHost ( ctx , id )
2021-11-08 14:42:37 +00:00
if err != nil {
2022-01-12 17:07:51 +00:00
return err
2021-12-21 20:36:19 +00:00
}
2021-11-08 14:42:37 +00:00
}
if err := rows . Err ( ) ; err != nil {
2021-11-15 14:11:38 +00:00
return ctxerr . Wrap ( ctx , err , "expired hosts, row err" )
2021-11-08 14:42:37 +00:00
}
2022-02-03 17:56:22 +00:00
_ , err = ds . writer . ExecContext ( ctx , ` DELETE FROM host_seen_times WHERE seen_time < DATE_SUB(NOW(), INTERVAL ? DAY) ` , ac . HostExpirySettings . HostExpiryWindow )
2021-10-19 20:47:37 +00:00
if err != nil {
2021-11-15 14:11:38 +00:00
return ctxerr . Wrap ( ctx , err , "deleting expired host seen times" )
2021-10-19 20:47:37 +00:00
}
return nil
}
2021-12-21 12:37:58 +00:00
2022-02-03 17:56:22 +00:00
func ( ds * Datastore ) ListHostDeviceMapping ( ctx context . Context , id uint ) ( [ ] * fleet . HostDeviceMapping , error ) {
2021-12-21 20:36:19 +00:00
stmt := `
SELECT
id ,
host_id ,
email ,
source
FROM
host_emails
WHERE
host_id = ?
ORDER BY
email , source `
var mappings [ ] * fleet . HostDeviceMapping
2022-02-03 17:56:22 +00:00
err := sqlx . SelectContext ( ctx , ds . reader , & mappings , stmt , id )
2021-12-21 20:36:19 +00:00
if err != nil {
return nil , ctxerr . Wrap ( ctx , err , "select host emails by host id" )
}
return mappings , nil
}
2022-02-03 17:56:22 +00:00
func ( ds * Datastore ) ReplaceHostDeviceMapping ( ctx context . Context , hid uint , mappings [ ] * fleet . HostDeviceMapping ) error {
2021-12-21 20:36:19 +00:00
for _ , m := range mappings {
if hid != m . HostID {
return ctxerr . Errorf ( ctx , "host device mapping are not all for the provided host id %d, found %d" , hid , m . HostID )
}
}
// the following SQL statements assume a small number of emails reported
// per host.
const (
selStmt = `
SELECT
id ,
email ,
source
FROM
host_emails
WHERE
host_id = ? `
delStmt = `
DELETE FROM
host_emails
WHERE
id IN ( ? ) `
insStmt = `
INSERT INTO
host_emails ( host_id , email , source )
VALUES `
insPart = ` (?, ?, ?), `
)
// index the mappings by email and source, to quickly check which ones
// need to be deleted and inserted
toIns := make ( map [ string ] * fleet . HostDeviceMapping )
for _ , m := range mappings {
toIns [ m . Email + "\n" + m . Source ] = m
}
2022-02-03 17:56:22 +00:00
return ds . withRetryTxx ( ctx , func ( tx sqlx . ExtContext ) error {
2021-12-21 20:36:19 +00:00
var prevMappings [ ] * fleet . HostDeviceMapping
if err := sqlx . SelectContext ( ctx , tx , & prevMappings , selStmt , hid ) ; err != nil {
return ctxerr . Wrap ( ctx , err , "select previous host emails" )
}
var delIDs [ ] uint
for _ , pm := range prevMappings {
key := pm . Email + "\n" + pm . Source
if _ , ok := toIns [ key ] ; ok {
// already exists, no need to insert
delete ( toIns , key )
} else {
// does not exist anymore, must be deleted
delIDs = append ( delIDs , pm . ID )
}
}
if len ( delIDs ) > 0 {
stmt , args , err := sqlx . In ( delStmt , delIDs )
if err != nil {
return ctxerr . Wrap ( ctx , err , "prepare delete statement" )
}
if _ , err := tx . ExecContext ( ctx , stmt , args ... ) ; err != nil {
return ctxerr . Wrap ( ctx , err , "delete host emails" )
}
}
if len ( toIns ) > 0 {
var args [ ] interface { }
for _ , m := range toIns {
args = append ( args , hid , m . Email , m . Source )
}
stmt := insStmt + strings . TrimSuffix ( strings . Repeat ( insPart , len ( toIns ) ) , "," )
if _ , err := tx . ExecContext ( ctx , stmt , args ... ) ; err != nil {
return ctxerr . Wrap ( ctx , err , "insert host emails" )
}
}
return nil
} )
}
2022-02-03 17:56:22 +00:00
func ( ds * Datastore ) updateOrInsert ( ctx context . Context , updateQuery string , insertQuery string , args ... interface { } ) error {
res , err := ds . writer . ExecContext ( ctx , updateQuery , args ... )
2021-12-21 12:37:58 +00:00
if err != nil {
return ctxerr . Wrap ( ctx , err )
}
affected , err := res . RowsAffected ( )
if err != nil {
return ctxerr . Wrap ( ctx , err )
}
if affected == 0 {
2022-02-03 17:56:22 +00:00
_ , err = ds . writer . ExecContext ( ctx , insertQuery , args ... )
2021-12-21 12:37:58 +00:00
}
return ctxerr . Wrap ( ctx , err )
}
2022-02-03 17:56:22 +00:00
func ( ds * Datastore ) SetOrUpdateMunkiVersion ( ctx context . Context , hostID uint , version string ) error {
2022-02-15 19:29:14 +00:00
if version == "" {
// Only update deleted_at if there wasn't any deleted at for this host
2022-04-15 20:09:47 +00:00
updateQuery := ` UPDATE host_munki_info SET deleted_at = NOW() WHERE host_id = ? AND deleted_at is NULL `
2022-02-15 19:29:14 +00:00
_ , err := ds . writer . ExecContext ( ctx , updateQuery , hostID )
if err != nil {
return ctxerr . Wrap ( ctx , err )
}
return nil
}
2022-02-03 17:56:22 +00:00
return ds . updateOrInsert (
2021-12-21 12:37:58 +00:00
ctx ,
2022-04-15 20:09:47 +00:00
` UPDATE host_munki_info SET version = ? WHERE host_id = ? ` ,
` INSERT INTO host_munki_info (version, host_id) VALUES (?, ?) ` ,
2021-12-21 12:37:58 +00:00
version , hostID ,
)
}
2022-02-03 17:56:22 +00:00
func ( ds * Datastore ) SetOrUpdateMDMData ( ctx context . Context , hostID uint , enrolled bool , serverURL string , installedFromDep bool ) error {
return ds . updateOrInsert (
2021-12-21 12:37:58 +00:00
ctx ,
2022-04-15 20:09:47 +00:00
` UPDATE host_mdm SET enrolled = ?, server_url = ?, installed_from_dep = ? WHERE host_id = ? ` ,
` INSERT INTO host_mdm (enrolled, server_url, installed_from_dep, host_id) VALUES (?, ?, ?, ?) ` ,
2021-12-21 12:37:58 +00:00
enrolled , serverURL , installedFromDep , hostID ,
)
}
2022-02-03 17:56:22 +00:00
func ( ds * Datastore ) GetMunkiVersion ( ctx context . Context , hostID uint ) ( string , error ) {
2021-12-21 12:37:58 +00:00
var version string
2022-04-15 20:09:47 +00:00
err := sqlx . GetContext ( ctx , ds . reader , & version , ` SELECT version FROM host_munki_info WHERE deleted_at is NULL AND host_id = ? ` , hostID )
2021-12-21 12:37:58 +00:00
if err != nil {
if err == sql . ErrNoRows {
return "" , ctxerr . Wrap ( ctx , notFound ( "MunkiInfo" ) . WithID ( hostID ) )
}
return "" , ctxerr . Wrapf ( ctx , err , "getting data from host_munki_info for host_id %d" , hostID )
}
return version , nil
}
2022-02-03 17:56:22 +00:00
func ( ds * Datastore ) GetMDM ( ctx context . Context , hostID uint ) ( bool , string , bool , error ) {
2021-12-21 12:37:58 +00:00
dest := struct {
Enrolled bool ` db:"enrolled" `
ServerURL string ` db:"server_url" `
InstalledFromDep bool ` db:"installed_from_dep" `
} { }
2022-04-15 20:09:47 +00:00
err := sqlx . GetContext ( ctx , ds . reader , & dest , ` SELECT enrolled, server_url, installed_from_dep FROM host_mdm WHERE host_id = ? ` , hostID )
2021-12-21 12:37:58 +00:00
if err != nil {
if err == sql . ErrNoRows {
return false , "" , false , ctxerr . Wrap ( ctx , notFound ( "MDM" ) . WithID ( hostID ) )
}
return false , "" , false , ctxerr . Wrapf ( ctx , err , "getting data from host_mdm for host_id %d" , hostID )
}
return dest . Enrolled , dest . ServerURL , dest . InstalledFromDep , nil
}
2022-02-14 15:13:38 +00:00
2022-02-07 17:53:33 +00:00
func ( ds * Datastore ) AggregatedMunkiVersion ( ctx context . Context , teamID * uint ) ( [ ] fleet . AggregatedMunkiVersion , time . Time , error ) {
2022-01-26 20:55:07 +00:00
id := uint ( 0 )
if teamID != nil {
id = * teamID
}
var versions [ ] fleet . AggregatedMunkiVersion
2022-02-07 17:53:33 +00:00
var versionsJson struct {
JsonValue [ ] byte ` db:"json_value" `
UpdatedAt time . Time ` db:"updated_at" `
}
err := sqlx . GetContext (
ctx , ds . reader , & versionsJson ,
2022-04-15 20:09:47 +00:00
` SELECT json_value, updated_at FROM aggregated_stats WHERE id = ? AND type = 'munki_versions' ` ,
2022-02-07 17:53:33 +00:00
id ,
)
2022-01-26 20:55:07 +00:00
if err != nil {
if err == sql . ErrNoRows {
// not having stats is not an error
2022-02-07 17:53:33 +00:00
return nil , time . Time { } , nil
2022-01-26 20:55:07 +00:00
}
2022-02-07 17:53:33 +00:00
return nil , time . Time { } , ctxerr . Wrap ( ctx , err , "selecting munki versions" )
2022-01-26 20:55:07 +00:00
}
2022-02-07 17:53:33 +00:00
if err := json . Unmarshal ( versionsJson . JsonValue , & versions ) ; err != nil {
return nil , time . Time { } , ctxerr . Wrap ( ctx , err , "unmarshaling munki versions" )
2022-01-26 20:55:07 +00:00
}
2022-02-07 17:53:33 +00:00
return versions , versionsJson . UpdatedAt , nil
2022-01-26 20:55:07 +00:00
}
2022-02-07 17:53:33 +00:00
func ( ds * Datastore ) AggregatedMDMStatus ( ctx context . Context , teamID * uint ) ( fleet . AggregatedMDMStatus , time . Time , error ) {
2022-01-26 20:55:07 +00:00
id := uint ( 0 )
if teamID != nil {
id = * teamID
}
var status fleet . AggregatedMDMStatus
2022-02-07 17:53:33 +00:00
var statusJson struct {
JsonValue [ ] byte ` db:"json_value" `
UpdatedAt time . Time ` db:"updated_at" `
}
err := sqlx . GetContext (
ctx , ds . reader , & statusJson ,
2022-04-15 20:09:47 +00:00
` select json_value, updated_at from aggregated_stats where id = ? and type = 'mdm_status' ` ,
2022-02-07 17:53:33 +00:00
id ,
)
2022-01-26 20:55:07 +00:00
if err != nil {
if err == sql . ErrNoRows {
// not having stats is not an error
2022-02-07 17:53:33 +00:00
return fleet . AggregatedMDMStatus { } , time . Time { } , nil
2022-01-26 20:55:07 +00:00
}
2022-02-07 17:53:33 +00:00
return fleet . AggregatedMDMStatus { } , time . Time { } , ctxerr . Wrap ( ctx , err , "selecting mdm status" )
2022-01-26 20:55:07 +00:00
}
2022-02-07 17:53:33 +00:00
if err := json . Unmarshal ( statusJson . JsonValue , & status ) ; err != nil {
return fleet . AggregatedMDMStatus { } , time . Time { } , ctxerr . Wrap ( ctx , err , "unmarshaling mdm status" )
2022-01-26 20:55:07 +00:00
}
2022-02-07 17:53:33 +00:00
return status , statusJson . UpdatedAt , nil
2022-01-26 20:55:07 +00:00
}
2022-02-03 17:56:22 +00:00
func ( ds * Datastore ) GenerateAggregatedMunkiAndMDM ( ctx context . Context ) error {
2022-01-26 20:55:07 +00:00
var ids [ ] uint
2022-02-03 17:56:22 +00:00
if err := sqlx . SelectContext ( ctx , ds . reader , & ids , ` SELECT id FROM teams ` ) ; err != nil {
2022-01-26 20:55:07 +00:00
return ctxerr . Wrap ( ctx , err , "list teams" )
}
for _ , id := range ids {
2022-02-03 17:56:22 +00:00
if err := ds . generateAggregatedMunkiVersion ( ctx , & id ) ; err != nil {
2022-01-26 20:55:07 +00:00
return ctxerr . Wrap ( ctx , err , "generating aggregated munki version" )
}
2022-02-03 17:56:22 +00:00
if err := ds . generateAggregatedMDMStatus ( ctx , & id ) ; err != nil {
2022-01-26 20:55:07 +00:00
return ctxerr . Wrap ( ctx , err , "generating aggregated mdm status" )
}
}
2022-02-03 17:56:22 +00:00
if err := ds . generateAggregatedMunkiVersion ( ctx , nil ) ; err != nil {
2022-01-26 20:55:07 +00:00
return ctxerr . Wrap ( ctx , err , "generating aggregated munki version" )
}
2022-02-03 17:56:22 +00:00
if err := ds . generateAggregatedMDMStatus ( ctx , nil ) ; err != nil {
2022-01-26 20:55:07 +00:00
return ctxerr . Wrap ( ctx , err , "generating aggregated mdm status" )
}
return nil
}
2022-02-03 17:56:22 +00:00
func ( ds * Datastore ) generateAggregatedMunkiVersion ( ctx context . Context , teamID * uint ) error {
2022-01-26 20:55:07 +00:00
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 )
2022-04-15 20:09:47 +00:00
query += ` JOIN hosts h ON (h.id = hm.host_id) WHERE h.team_id = ? AND `
2022-01-26 20:55:07 +00:00
id = * teamID
2022-02-15 19:29:14 +00:00
} else {
query += ` WHERE `
2022-01-26 20:55:07 +00:00
}
2022-04-15 20:09:47 +00:00
query += ` hm.deleted_at IS NULL GROUP BY hm.version `
2022-02-03 17:56:22 +00:00
err := sqlx . SelectContext ( ctx , ds . reader , & versions , query , args ... )
2022-01-26 20:55:07 +00:00
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" )
}
2022-02-03 17:56:22 +00:00
_ , err = ds . writer . ExecContext ( ctx ,
2022-04-15 20:09:47 +00:00
`
INSERT INTO aggregated_stats ( id , type , json_value )
VALUES ( ? , ? , ? )
ON DUPLICATE KEY UPDATE
json_value = VALUES ( json_value ) ,
updated_at = CURRENT_TIMESTAMP
` ,
2022-01-26 20:55:07 +00:00
id , "munki_versions" , versionsJson ,
)
if err != nil {
return ctxerr . Wrapf ( ctx , err , "inserting stats for munki_versions id %d" , id )
}
return nil
}
2022-02-03 17:56:22 +00:00
func ( ds * Datastore ) generateAggregatedMDMStatus ( ctx context . Context , teamID * uint ) error {
2022-01-26 20:55:07 +00:00
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 )
2022-04-15 20:09:47 +00:00
query += ` JOIN hosts h ON (h.id = hm.host_id) WHERE h.team_id = ? `
2022-01-26 20:55:07 +00:00
id = * teamID
}
2022-02-03 17:56:22 +00:00
err := sqlx . GetContext ( ctx , ds . reader , & status , query , args ... )
2022-01-26 20:55:07 +00:00
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" )
}
2022-02-03 17:56:22 +00:00
_ , err = ds . writer . ExecContext ( ctx ,
2022-04-15 20:09:47 +00:00
`
INSERT INTO aggregated_stats ( id , type , json_value )
VALUES ( ? , ? , ? )
ON DUPLICATE KEY UPDATE
json_value = VALUES ( json_value ) ,
updated_at = CURRENT_TIMESTAMP
` ,
2022-01-26 20:55:07 +00:00
id , "mdm_status" , statusJson ,
)
if err != nil {
return ctxerr . Wrapf ( ctx , err , "inserting stats for mdm_status id %d" , id )
}
return nil
}
2022-01-18 01:52:09 +00:00
// HostLite will load the primary data of the host with the given id.
// We define "primary data" as all host information except the
// details (like cpu, memory, gigs_disk_space_available, etc.).
//
// If the host doesn't exist, a NotFoundError is returned.
2022-02-03 17:56:22 +00:00
func ( ds * Datastore ) HostLite ( ctx context . Context , id uint ) ( * fleet . Host , error ) {
query , args , err := dialect . From ( goqu . I ( "hosts" ) ) . Select (
2022-01-18 01:52:09 +00:00
"id" ,
"created_at" ,
"updated_at" ,
"osquery_host_id" ,
"node_key" ,
"hostname" ,
"uuid" ,
"platform" ,
"team_id" ,
"distributed_interval" ,
"logger_tls_period" ,
"config_tls_refresh" ,
"detail_updated_at" ,
"label_updated_at" ,
"last_enrolled_at" ,
"policy_updated_at" ,
"refetch_requested" ,
2022-02-03 17:56:22 +00:00
) . Where ( goqu . I ( "id" ) . Eq ( id ) ) . ToSQL ( )
2022-01-18 01:52:09 +00:00
if err != nil {
return nil , ctxerr . Wrap ( ctx , err , "sql build" )
}
var host fleet . Host
2022-02-03 17:56:22 +00:00
if err := sqlx . GetContext ( ctx , ds . reader , & host , query , args ... ) ; err != nil {
2022-01-18 01:52:09 +00:00
if err == sql . ErrNoRows {
return nil , ctxerr . Wrap ( ctx , notFound ( "Host" ) . WithID ( id ) )
}
return nil , ctxerr . Wrapf ( ctx , err , "load host %d" , id )
}
return & host , nil
}
// UpdateHostOsqueryIntervals updates the osquery intervals of a host.
2022-02-03 17:56:22 +00:00
func ( ds * Datastore ) UpdateHostOsqueryIntervals ( ctx context . Context , id uint , intervals fleet . HostOsqueryIntervals ) error {
2022-01-18 01:52:09 +00:00
sqlStatement := `
UPDATE hosts SET
distributed_interval = ? ,
config_tls_refresh = ? ,
logger_tls_period = ?
WHERE id = ?
`
2022-02-03 17:56:22 +00:00
_ , err := ds . writer . ExecContext ( ctx , sqlStatement ,
2022-01-18 01:52:09 +00:00
intervals . DistributedInterval ,
intervals . ConfigTLSRefresh ,
intervals . LoggerTLSPeriod ,
id ,
)
if err != nil {
return ctxerr . Wrapf ( ctx , err , "update host %d osquery intervals" , id )
}
return nil
}
// UpdateHostRefetchRequested updates a host's refetch requested field.
2022-02-03 17:56:22 +00:00
func ( ds * Datastore ) UpdateHostRefetchRequested ( ctx context . Context , id uint , value bool ) error {
2022-01-18 01:52:09 +00:00
sqlStatement := ` UPDATE hosts SET refetch_requested = ? WHERE id = ? `
2022-02-03 17:56:22 +00:00
_ , err := ds . writer . ExecContext ( ctx , sqlStatement , value , id )
2022-01-18 01:52:09 +00:00
if err != nil {
return ctxerr . Wrapf ( ctx , err , "update host %d refetch_requested" , id )
}
return nil
}
// UpdateHost updates a host.
//
// UpdateHost updates all columns of the `hosts` table.
// It only updates `hosts` table, other additional host information is ignored.
2022-02-03 17:56:22 +00:00
func ( ds * Datastore ) UpdateHost ( ctx context . Context , host * fleet . Host ) error {
2022-01-18 01:52:09 +00:00
sqlStatement := `
UPDATE hosts SET
detail_updated_at = ? ,
label_updated_at = ? ,
policy_updated_at = ? ,
node_key = ? ,
hostname = ? ,
uuid = ? ,
platform = ? ,
osquery_version = ? ,
os_version = ? ,
uptime = ? ,
memory = ? ,
cpu_type = ? ,
cpu_subtype = ? ,
cpu_brand = ? ,
cpu_physical_cores = ? ,
hardware_vendor = ? ,
hardware_model = ? ,
hardware_version = ? ,
hardware_serial = ? ,
computer_name = ? ,
build = ? ,
platform_like = ? ,
code_name = ? ,
cpu_logical_cores = ? ,
distributed_interval = ? ,
config_tls_refresh = ? ,
logger_tls_period = ? ,
team_id = ? ,
primary_ip = ? ,
primary_mac = ? ,
2022-03-21 16:29:52 +00:00
public_ip = ? ,
2022-01-18 01:52:09 +00:00
refetch_requested = ? ,
gigs_disk_space_available = ? ,
percent_disk_space_available = ?
WHERE id = ?
`
2022-02-03 17:56:22 +00:00
_ , err := ds . writer . ExecContext ( ctx , sqlStatement ,
2022-01-18 01:52:09 +00:00
host . DetailUpdatedAt ,
host . LabelUpdatedAt ,
host . PolicyUpdatedAt ,
host . NodeKey ,
host . Hostname ,
host . UUID ,
host . Platform ,
host . OsqueryVersion ,
host . OSVersion ,
host . Uptime ,
host . Memory ,
host . CPUType ,
host . CPUSubtype ,
host . CPUBrand ,
host . CPUPhysicalCores ,
host . HardwareVendor ,
host . HardwareModel ,
host . HardwareVersion ,
host . HardwareSerial ,
host . ComputerName ,
host . Build ,
host . PlatformLike ,
host . CodeName ,
host . CPULogicalCores ,
host . DistributedInterval ,
host . ConfigTLSRefresh ,
host . LoggerTLSPeriod ,
host . TeamID ,
host . PrimaryIP ,
host . PrimaryMac ,
2022-03-21 16:29:52 +00:00
host . PublicIP ,
2022-01-18 01:52:09 +00:00
host . RefetchRequested ,
host . GigsDiskSpaceAvailable ,
host . PercentDiskSpaceAvailable ,
host . ID ,
)
if err != nil {
return ctxerr . Wrapf ( ctx , err , "save host with id %d" , host . ID )
}
return nil
}
2022-03-28 15:15:45 +00:00
// OSVersions gets the aggregated os version host counts. If a non-nil teamID is passed, it will filter hosts by team.
func ( ds * Datastore ) OSVersions ( ctx context . Context , teamID * uint , platform * string ) ( * fleet . OSVersions , error ) {
query := `
SELECT
json_value ,
updated_at
FROM aggregated_stats
WHERE
id = ? AND
type = ' os_versions '
`
var row struct {
JSONValue * json . RawMessage ` db:"json_value" `
UpdatedAt time . Time ` db:"updated_at" `
}
var args [ ] interface { }
if teamID == nil { // all hosts
args = append ( args , 0 )
} else {
args = append ( args , * teamID )
}
2022-04-18 21:19:58 +00:00
err := sqlx . GetContext ( ctx , ds . reader , & row , query , args ... )
if err != nil {
if err == sql . ErrNoRows {
return nil , ctxerr . Wrap ( ctx , notFound ( "OSVersions" ) )
}
2022-03-28 15:15:45 +00:00
return nil , err
}
osVersions := & fleet . OSVersions {
CountsUpdatedAt : row . UpdatedAt ,
}
if row . JSONValue != nil {
err := json . Unmarshal ( * row . JSONValue , & osVersions . OSVersions )
if err != nil {
return nil , err
}
}
// filter by os versions by platform
if platform != nil {
var filtered [ ] fleet . OSVersion
for _ , osVersion := range osVersions . OSVersions {
if * platform == osVersion . Platform {
filtered = append ( filtered , osVersion )
}
}
osVersions . OSVersions = filtered
}
// Sort by os versions. We can't control the order when using json_arrayagg
// See https://dev.mysql.com/doc/refman/5.7/en/aggregate-functions.html#function_json-arrayagg.
sort . Slice ( osVersions . OSVersions , func ( i , j int ) bool { return osVersions . OSVersions [ i ] . Name < osVersions . OSVersions [ j ] . Name } )
return osVersions , nil
}
2022-04-18 21:19:58 +00:00
// Aggregated stats for os versions are stored by team id with 0 representing the global case
// If existing team has no hosts, we explicity set the json value as an empty array
2022-03-28 15:15:45 +00:00
func ( ds * Datastore ) UpdateOSVersions ( ctx context . Context ) error {
2022-04-18 21:19:58 +00:00
sql := `
2022-03-28 15:15:45 +00:00
INSERT INTO aggregated_stats ( id , type , json_value )
SELECT
team_id id ,
' os_versions ' type ,
COALESCE (
2022-04-25 20:43:08 +00:00
CONCAT ( '[' , GROUP_CONCAT (
2022-03-28 15:15:45 +00:00
JSON_OBJECT (
' hosts_count ' , hosts_count ,
' name ' , name ,
' platform ' , platform
)
2022-04-25 20:43:08 +00:00
) , ']' ) ,
2022-03-28 15:15:45 +00:00
JSON_ARRAY ( )
) json_value
FROM
(
SELECT
COUNT ( * ) hosts_count ,
h . os_version name ,
h . platform ,
0 team_id
FROM
hosts h
GROUP BY
h . os_version ,
h . platform
UNION
SELECT
COUNT ( * ) hosts_count ,
h . os_version name ,
h . platform ,
h . team_id
FROM
hosts h
JOIN teams t ON t . id = h . team_id
GROUP BY
h . os_version ,
h . platform ,
h . team_id
) as team_os_versions
GROUP BY
team_id
2022-04-18 21:19:58 +00:00
UNION
SELECT
t . id ,
' os_versions ' type ,
JSON_ARRAY ( ) json_value
FROM
teams t
WHERE NOT EXISTS (
SELECT
id
FROM
hosts h
WHERE
t . id = h . team_id
)
2022-03-28 15:15:45 +00:00
ON DUPLICATE KEY UPDATE
json_value = VALUES ( json_value ) ,
updated_at = CURRENT_TIMESTAMP
`
2022-04-18 21:19:58 +00:00
_ , err := ds . writer . ExecContext ( ctx , sql )
2022-03-28 15:15:45 +00:00
if err != nil {
return ctxerr . Wrapf ( ctx , err , "update aggregated stats for os versions" )
}
return nil
}