fleet/server/datastore/mysql/hosts.go
John Murphy f4bee00b01 Fix Issue where saving same option value errs. (#1433)
Closes issue #1390

There were quite a few places where UPDATES could fail silently because we weren't checking target rows where actually found where we expect them to be. In order to address this problem clientFoundRows was set in the sql driver configuration and checks for UPDATES were added to determine if matched rows were found where we expect them to be.
2017-03-30 17:03:48 -05:00

693 lines
15 KiB
Go

package mysql
import (
"database/sql"
"fmt"
"time"
"github.com/jmoiron/sqlx"
"github.com/kolide/kolide/server/kolide"
"github.com/patrickmn/sortutil"
"github.com/pkg/errors"
)
func (d *Datastore) NewHost(host *kolide.Host) (*kolide.Host, error) {
sqlStatement := `
INSERT INTO hosts (
osquery_host_id,
detail_update_time,
node_key,
host_name,
uuid,
platform,
osquery_version,
os_version,
uptime,
physical_memory,
seen_time
)
VALUES( ?,?,?,?,?,?,?,?,?,?,? )
`
result, err := d.db.Exec(sqlStatement, host.OsqueryHostID, host.DetailUpdateTime,
host.NodeKey, host.HostName, host.UUID, host.Platform, host.OsqueryVersion,
host.OSVersion, host.Uptime, host.PhysicalMemory, host.SeenTime)
if err != nil {
return nil, errors.Wrap(err, "new host")
}
id, _ := result.LastInsertId()
host.ID = uint(id)
return host, nil
}
func removedUnusedNics(tx *sqlx.Tx, host *kolide.Host) error {
if len(host.NetworkInterfaces) == 0 {
_, err := tx.Exec(`DELETE FROM network_interfaces WHERE host_id = ?`, host.ID)
return err
}
// Remove nics not associated with host
sqlStatement := fmt.Sprintf(`
DELETE FROM network_interfaces
WHERE host_id = %d AND id NOT IN (?)
`, host.ID)
list := []uint{}
for _, nic := range host.NetworkInterfaces {
list = append(list, nic.ID)
}
sql, args, err := sqlx.In(sqlStatement, list)
if err != nil {
return err
}
sql = tx.Rebind(sql)
_, err = tx.Exec(sql, args...)
if err != nil {
return err
}
return nil
}
func updateNicsForHost(tx *sqlx.Tx, host *kolide.Host) ([]*kolide.NetworkInterface, error) {
updatedNics := []*kolide.NetworkInterface{}
// id = LAST_INSERT_ID(id) is a fix for the lastinsertid not being set
// properly. See comments in https://goo.gl/cwWRXd.
sqlStatement := `
INSERT INTO network_interfaces (
host_id,
mac,
ip_address,
broadcast,
ibytes,
interface,
ipackets,
last_change,
mask,
metric,
mtu,
obytes,
ierrors,
oerrors,
opackets,
point_to_point,
type
) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)
ON DUPLICATE KEY UPDATE
id = LAST_INSERT_ID(id),
mac = VALUES(mac),
broadcast = VALUES(broadcast),
ibytes = VALUES(ibytes),
ipackets = VALUES(ipackets),
last_change = VALUES(last_change),
mask = VALUES(mask),
metric = VALUES(metric),
mtu = VALUES(mtu),
obytes = VALUES(obytes),
ierrors = VALUES(ierrors),
oerrors = VALUES(oerrors),
opackets = VALUES(opackets),
point_to_point = VALUES(point_to_point),
type = VALUES(type)
`
for _, nic := range host.NetworkInterfaces {
nic.HostID = host.ID
result, err := tx.Exec(sqlStatement,
nic.HostID,
nic.MAC,
nic.IPAddress,
nic.Broadcast,
nic.IBytes,
nic.Interface,
nic.IPackets,
nic.LastChange,
nic.Mask,
nic.Metric,
nic.MTU,
nic.OBytes,
nic.IErrors,
nic.OErrors,
nic.OPackets,
nic.PointToPoint,
nic.Type,
)
if err != nil {
return nil, err
}
nicID, _ := result.LastInsertId()
// if row was updated there is no LastInsertID
if nicID != 0 {
nic.ID = uint(nicID)
}
updatedNics = append(updatedNics, nic)
}
return updatedNics, nil
}
// TODO needs test
func (d *Datastore) SaveHost(host *kolide.Host) error {
sqlStatement := `
UPDATE hosts SET
detail_update_time = ?,
node_key = ?,
host_name = ?,
uuid = ?,
platform = ?,
osquery_version = ?,
os_version = ?,
uptime = ?,
physical_memory = ?,
cpu_type = ?,
cpu_subtype = ?,
cpu_brand = ?,
cpu_physical_cores = ?,
hardware_vendor = ?,
hardware_model = ?,
hardware_version = ?,
hardware_serial = ?,
computer_name = ?,
primary_ip_id = ?,
build = ?,
platform_like = ?,
code_name = ?,
cpu_logical_cores = ?,
seen_time = ?
WHERE id = ?
`
tx, err := d.db.Beginx()
if err != nil {
return errors.Wrap(err, "creating transaction")
}
results, err := tx.Exec(sqlStatement,
host.DetailUpdateTime,
host.NodeKey,
host.HostName,
host.UUID,
host.Platform,
host.OsqueryVersion,
host.OSVersion,
host.Uptime,
host.PhysicalMemory,
host.CPUType,
host.CPUSubtype,
host.CPUBrand,
host.CPUPhysicalCores,
host.HardwareVendor,
host.HardwareModel,
host.HardwareVersion,
host.HardwareSerial,
host.ComputerName,
host.PrimaryNetworkInterfaceID,
host.Build,
host.PlatformLike,
host.CodeName,
host.CPULogicalCores,
host.SeenTime,
host.ID)
if err != nil {
tx.Rollback()
return errors.Wrap(err, "executing main SQL statement")
}
rowsAffected, err := results.RowsAffected()
if err != nil {
tx.Rollback()
return errors.Wrap(err, "rows affected updating host")
}
if rowsAffected == 0 {
tx.Rollback()
return notFound("Host").WithID(host.ID)
}
host.NetworkInterfaces, err = updateNicsForHost(tx, host)
if err != nil {
tx.Rollback()
return errors.Wrap(err, "updating nics")
}
if err = removedUnusedNics(tx, host); err != nil {
tx.Rollback()
return errors.Wrap(err, "removing unused nics")
}
if needsUpdate := host.ResetPrimaryNetwork(); needsUpdate {
results, err = tx.Exec(
"UPDATE hosts SET primary_ip_id = ? WHERE id = ?",
host.PrimaryNetworkInterfaceID,
host.ID,
)
if err != nil {
tx.Rollback()
return errors.Wrap(err, "resetting primary network")
}
rowsAffected, err = results.RowsAffected()
if err != nil {
tx.Rollback()
return errors.Wrap(err, "rows affected resetting primary network")
}
if rowsAffected == 0 {
tx.Rollback()
return notFound("Host").WithID(host.ID)
}
}
if err = tx.Commit(); err != nil {
tx.Rollback()
return errors.Wrap(err, "committing transaction")
}
return nil
}
func (d *Datastore) DeleteHost(hid uint) error {
_, err := d.db.Exec("DELETE FROM hosts WHERE id = ?", hid)
if err != nil {
return errors.Wrapf(err, "deleting host with id %d", hid)
}
return nil
}
// TODO needs test
func (d *Datastore) Host(id uint) (*kolide.Host, error) {
sqlStatement := `
SELECT * FROM hosts
WHERE id = ? AND NOT deleted LIMIT 1
`
host := &kolide.Host{}
err := d.db.Get(host, sqlStatement, id)
if err != nil {
return nil, errors.Wrap(err, "getting host by id")
}
if err := d.getNetInterfacesForHost(host); err != nil {
return nil, err
}
return host, nil
}
func (d *Datastore) ListHosts(opt kolide.ListOptions) ([]*kolide.Host, error) {
sqlStatement := `
SELECT * FROM hosts
WHERE NOT deleted
`
sqlStatement = appendListOptionsToSQL(sqlStatement, opt)
hosts := []*kolide.Host{}
if err := d.db.Select(&hosts, sqlStatement); err != nil {
return nil, errors.Wrap(err, "list hosts")
}
if err := d.getNetInterfacesForHosts(hosts); err != nil {
return nil, err
}
return hosts, nil
}
func (d *Datastore) GenerateHostStatusStatistics(now time.Time, onlineInterval time.Duration) (online, offline, mia, new uint, e error) {
sqlStatement := `
SELECT (
SELECT count(id)
FROM hosts
WHERE DATE_ADD(seen_time, INTERVAL 30 DAY) <= ?
) AS mia,
(
SELECT count(id)
FROM hosts
WHERE DATE_ADD(seen_time, INTERVAL ? SECOND) <= ?
AND DATE_ADD(seen_time, INTERVAL 30 DAY) >= ?
) AS offline,
(
SELECT count(id)
FROM hosts
WHERE DATE_ADD(seen_time, INTERVAL ? SECOND) > ?
) AS online,
(
SELECT count(id)
FROM hosts
WHERE DATE_ADD(created_at, INTERVAL 1 DAY) >= ?
) AS new
FROM hosts
LIMIT 1;
`
counts := struct {
MIA uint `db:"mia"`
Offline uint `db:"offline"`
Online uint `db:"online"`
New uint `db:"new"`
}{}
err := d.db.Get(&counts, sqlStatement, now, onlineInterval.Seconds(), now, now, onlineInterval.Seconds(), now, now)
if err != nil && err != sql.ErrNoRows {
e = errors.Wrap(err, "generating host statistics")
return
}
mia = counts.MIA
offline = counts.Offline
online = counts.Online
new = counts.New
return online, offline, mia, new, nil
}
// Optimized network interface fetch for sets of hosts. Instead of looping
// through hosts and doing a select for each host to get nics, we get all
// nics at once, so 2 db calls, and then assign nics to hosts here.
func (d *Datastore) getNetInterfacesForHosts(hosts []*kolide.Host) error {
if len(hosts) == 0 {
return nil
}
sqlStatement := `
SELECT *
FROM network_interfaces
WHERE host_id IN (:hosts)
ORDER BY host_id ASC
`
hostIDs := make([]uint, len(hosts))
for _, host := range hosts {
hostIDs = append(hostIDs, host.ID)
}
arg := map[string]interface{}{
"hosts": hostIDs,
}
query, args, err := sqlx.Named(sqlStatement, arg)
if err != nil {
return errors.Wrap(err, "select nics for hosts, named query")
}
query, args, err = sqlx.In(query, args...)
if err != nil {
return errors.Wrap(err, "select nics for hosts, in query")
}
query = d.db.Rebind(query)
nics := []*kolide.NetworkInterface{}
err = d.db.Select(&nics, query, args...)
if err != nil {
return errors.Wrap(err, "select nics for hosts, rebound query")
}
sortutil.AscByField(hosts, "ID")
for _, host := range hosts {
for i := 0; i < len(nics); i++ {
if host.ID == nics[i].HostID {
host.NetworkInterfaces = append(host.NetworkInterfaces, nics[i])
}
}
}
return nil
}
func (d *Datastore) getNetInterfacesForHost(host *kolide.Host) error {
sqlStatement := `
SELECT * FROM network_interfaces
WHERE host_id = ?
`
if err := d.db.Select(&host.NetworkInterfaces, sqlStatement, host.ID); err != nil {
return err
}
return nil
}
// EnrollHost enrolls a host
func (d *Datastore) EnrollHost(osqueryHostID string, nodeKeySize int) (*kolide.Host, error) {
if osqueryHostID == "" {
return nil, fmt.Errorf("missing osquery host identifier")
}
detailUpdateTime := time.Unix(0, 0).Add(24 * time.Hour)
nodeKey, err := kolide.RandomText(nodeKeySize)
if err != nil {
return nil, errors.Wrap(err, "generating random text")
}
sqlInsert := `
INSERT INTO hosts (
detail_update_time,
osquery_host_id,
seen_time,
node_key
) VALUES (?, ?, ?, ?)
ON DUPLICATE KEY UPDATE
node_key = VALUES(node_key),
deleted = FALSE
`
var result sql.Result
result, err = d.db.Exec(sqlInsert, detailUpdateTime, osqueryHostID, time.Now().UTC(), nodeKey)
if err != nil {
return nil, errors.Wrap(err, "inserting")
}
id, _ := result.LastInsertId()
sqlSelect := `
SELECT * FROM hosts WHERE id = ? LIMIT 1
`
host := &kolide.Host{}
err = d.db.Get(host, sqlSelect, id)
if err != nil {
return nil, errors.Wrap(err, "getting the host to return")
}
return host, nil
}
func (d *Datastore) AuthenticateHost(nodeKey string) (*kolide.Host, error) {
sqlStatement := `
SELECT *
FROM hosts
WHERE node_key = ? AND NOT deleted
LIMIT 1
`
host := &kolide.Host{}
if err := d.db.Get(host, sqlStatement, nodeKey); err != nil {
switch err {
case sql.ErrNoRows:
return nil, errors.Wrap(err, "host not found")
default:
return nil, errors.New("finding host")
}
}
if err := d.getNetInterfacesForHost(host); err != nil {
return nil, errors.Wrap(err, "getting interfaces")
}
return host, nil
}
func (d *Datastore) MarkHostSeen(host *kolide.Host, t time.Time) error {
sqlStatement := `
UPDATE hosts SET
seen_time = ?
WHERE node_key=?
`
_, err := d.db.Exec(sqlStatement, t, host.NodeKey)
if err != nil {
return errors.Wrap(err, "marking host seen")
}
host.UpdatedAt = t
return nil
}
func (d *Datastore) searchHostsWithOmits(query string, omit ...uint) ([]*kolide.Host, error) {
hostnameQuery := query
if len(hostnameQuery) > 0 {
hostnameQuery += "*"
}
ipQuery := `"` + query + `"`
sqlStatement :=
`
SELECT DISTINCT *
FROM hosts
WHERE
(
id IN (
SELECT id
FROM hosts
WHERE
MATCH(host_name) AGAINST(? IN BOOLEAN MODE)
)
OR
id IN (
SELECT host_id
FROM network_interfaces
WHERE
MATCH(ip_address) AGAINST(? IN BOOLEAN MODE)
)
)
AND NOT deleted
AND id NOT IN (?)
LIMIT 10
`
sql, args, err := sqlx.In(sqlStatement, hostnameQuery, ipQuery, omit)
if err != nil {
return nil, errors.Wrap(err, "searching hosts")
}
sql = d.db.Rebind(sql)
hosts := []*kolide.Host{}
err = d.db.Select(&hosts, sql, args...)
if err != nil {
return nil, errors.Wrap(err, "searching hosts rebound")
}
if err := d.getNetInterfacesForHosts(hosts); err != nil {
return nil, errors.Wrap(err, "getting network interfaces for hosts")
}
return hosts, nil
}
func (d *Datastore) searchHostsDefault(omit ...uint) ([]*kolide.Host, error) {
sqlStatement := `
SELECT * FROM hosts
WHERE NOT deleted
AND id NOT IN (?)
ORDER BY seen_time DESC
LIMIT 5
`
var in interface{}
{
// use -1 if there are no values to omit.
//Avoids empty args error for `sqlx.In`
in = omit
if len(omit) == 0 {
in = -1
}
}
var hosts []*kolide.Host
sql, args, err := sqlx.In(sqlStatement, in)
if err != nil {
return nil, errors.Wrap(err, "searching default hosts")
}
sql = d.db.Rebind(sql)
err = d.db.Select(&hosts, sql, args...)
if err != nil {
return nil, errors.Wrap(err, "searching default hosts rebound")
}
if err := d.getNetInterfacesForHosts(hosts); err != nil {
return nil, errors.Wrap(err, "getting network interfaces for default search hosts")
}
return hosts, nil
}
// SearchHosts find hosts by query containing an IP address or a host name. Optionally
// pass a list of IDs to omit from the search
func (d *Datastore) SearchHosts(query string, omit ...uint) ([]*kolide.Host, error) {
if query == "" {
return d.searchHostsDefault(omit...)
}
if len(omit) > 0 {
return d.searchHostsWithOmits(query, omit...)
}
hostnameQuery := query
hostnameQuery += "*"
// Needs quotes to avoid each . marking a word boundary
ipQuery := `"` + query + `"`
sqlStatement :=
`
SELECT DISTINCT *
FROM hosts
WHERE
(
id IN (
SELECT id
FROM hosts
WHERE
MATCH(host_name) AGAINST(? IN BOOLEAN MODE)
)
OR
id IN (
SELECT host_id
FROM network_interfaces
WHERE
MATCH(ip_address) AGAINST(? IN BOOLEAN MODE)
)
)
AND NOT deleted
LIMIT 10
`
hosts := []*kolide.Host{}
if err := d.db.Select(&hosts, sqlStatement, hostnameQuery, ipQuery); err != nil {
return nil, errors.Wrap(err, "searching hosts")
}
if err := d.getNetInterfacesForHosts(hosts); err != nil {
return nil, errors.Wrap(err, "getting interfaces")
}
return hosts, nil
}
func (d *Datastore) DistributedQueriesForHost(host *kolide.Host) (map[uint]string, error) {
sqlStatement := `
SELECT DISTINCT dqc.id, q.query
FROM distributed_query_campaigns dqc
JOIN distributed_query_campaign_targets dqct
ON (dqc.id = dqct.distributed_query_campaign_id)
LEFT JOIN label_query_executions lqe
ON (dqct.type = ? AND dqct.target_id = lqe.label_id AND lqe.matches)
LEFT JOIN hosts h
ON ((dqct.type = ? AND lqe.host_id = h.id) OR (dqct.type = ? AND dqct.target_id = h.id))
LEFT JOIN distributed_query_executions dqe
ON (h.id = dqe.host_id AND dqc.id = dqe.distributed_query_campaign_id)
JOIN queries q
ON (dqc.query_id = q.id)
WHERE dqe.status IS NULL AND dqc.status = ? AND h.id = ?
AND NOT q.deleted
AND NOT dqc.deleted
`
rows, err := d.db.Query(sqlStatement, kolide.TargetLabel, kolide.TargetLabel,
kolide.TargetHost, kolide.QueryRunning, host.ID)
if err != nil {
return nil, errors.Wrap(err, "finding distributed queries for host")
}
defer rows.Close()
results := map[uint]string{}
for rows.Next() {
var (
id uint
query string
)
err = rows.Scan(&id, &query)
if err != nil {
return nil, errors.Wrap(err, "scanning query results")
}
results[id] = query
}
return results, nil
}