mirror of
https://github.com/fleetdm/fleet
synced 2026-04-21 13:37:30 +00:00
More fixes.
This commit is contained in:
parent
94ef65240d
commit
33ee0e401a
2 changed files with 198 additions and 62 deletions
|
|
@ -29,57 +29,10 @@ func (ds *Datastore) NewAndroidHost(ctx context.Context, host *fleet.AndroidHost
|
|||
}
|
||||
|
||||
err = ds.withRetryTxx(ctx, func(tx sqlx.ExtContext) error {
|
||||
// We use node_key as a unique identifier for the host table row. It matches: android/{enterpriseSpecificID}.
|
||||
stmt := `
|
||||
INSERT INTO hosts (
|
||||
node_key,
|
||||
hostname,
|
||||
computer_name,
|
||||
platform,
|
||||
os_version,
|
||||
build,
|
||||
memory,
|
||||
team_id,
|
||||
hardware_serial,
|
||||
cpu_type,
|
||||
hardware_model,
|
||||
hardware_vendor,
|
||||
detail_updated_at,
|
||||
label_updated_at,
|
||||
uuid
|
||||
) VALUES (
|
||||
:node_key,
|
||||
:hostname,
|
||||
:computer_name,
|
||||
:platform,
|
||||
:os_version,
|
||||
:build,
|
||||
:memory,
|
||||
:team_id,
|
||||
:hardware_serial,
|
||||
:cpu_type,
|
||||
:hardware_model,
|
||||
:hardware_vendor,
|
||||
:detail_updated_at,
|
||||
:label_updated_at,
|
||||
:uuid
|
||||
) ON DUPLICATE KEY UPDATE
|
||||
hostname = VALUES(hostname),
|
||||
computer_name = VALUES(computer_name),
|
||||
platform = VALUES(platform),
|
||||
os_version = VALUES(os_version),
|
||||
build = VALUES(build),
|
||||
memory = VALUES(memory),
|
||||
team_id = VALUES(team_id),
|
||||
hardware_serial = VALUES(hardware_serial),
|
||||
cpu_type = VALUES(cpu_type),
|
||||
hardware_model = VALUES(hardware_model),
|
||||
hardware_vendor = VALUES(hardware_vendor),
|
||||
detail_updated_at = VALUES(detail_updated_at),
|
||||
label_updated_at = VALUES(label_updated_at),
|
||||
uuid = VALUES(uuid)
|
||||
`
|
||||
result, err := sqlx.NamedExecContext(ctx, tx, stmt, map[string]interface{}{
|
||||
// If the Fleet Android agent already orbit-enrolled this device, a hosts row exists
|
||||
// keyed by uuid = enterpriseSpecificId. Reuse it instead of inserting a duplicate.
|
||||
// (platform = '' covers agents that didn't send platform on orbit enroll.)
|
||||
params := map[string]any{
|
||||
"node_key": host.NodeKey,
|
||||
"hostname": host.Hostname,
|
||||
"computer_name": host.ComputerName,
|
||||
|
|
@ -95,21 +48,112 @@ func (ds *Datastore) NewAndroidHost(ctx context.Context, host *fleet.AndroidHost
|
|||
"detail_updated_at": host.DetailUpdatedAt,
|
||||
"label_updated_at": host.LabelUpdatedAt,
|
||||
"uuid": host.UUID,
|
||||
})
|
||||
if err != nil {
|
||||
return ctxerr.Wrap(ctx, err, "new Android host")
|
||||
}
|
||||
id, _ := result.LastInsertId()
|
||||
if id == 0 {
|
||||
// This was an UPDATE, not an INSERT, so we need to get the host ID
|
||||
var hostID uint
|
||||
err := sqlx.GetContext(ctx, tx, &hostID, `SELECT id FROM hosts WHERE node_key = ?`, host.NodeKey)
|
||||
|
||||
var existingID uint
|
||||
err := sqlx.GetContext(ctx, tx, &existingID,
|
||||
`SELECT id FROM hosts WHERE uuid = ? AND (platform = 'android' OR platform = '') ORDER BY id LIMIT 1`,
|
||||
host.UUID,
|
||||
)
|
||||
notExist := errors.Is(err, sql.ErrNoRows)
|
||||
if err != nil && !notExist {
|
||||
return ctxerr.Wrap(ctx, err, "check for existing orbit-enrolled Android host")
|
||||
}
|
||||
|
||||
if notExist {
|
||||
// No orbit-enrolled host for this uuid. Insert as usual.
|
||||
// We use node_key as a unique identifier for the host table row. It matches: android/{enterpriseSpecificID}.
|
||||
insertStmt := `
|
||||
INSERT INTO hosts (
|
||||
node_key,
|
||||
hostname,
|
||||
computer_name,
|
||||
platform,
|
||||
os_version,
|
||||
build,
|
||||
memory,
|
||||
team_id,
|
||||
hardware_serial,
|
||||
cpu_type,
|
||||
hardware_model,
|
||||
hardware_vendor,
|
||||
detail_updated_at,
|
||||
label_updated_at,
|
||||
uuid
|
||||
) VALUES (
|
||||
:node_key,
|
||||
:hostname,
|
||||
:computer_name,
|
||||
:platform,
|
||||
:os_version,
|
||||
:build,
|
||||
:memory,
|
||||
:team_id,
|
||||
:hardware_serial,
|
||||
:cpu_type,
|
||||
:hardware_model,
|
||||
:hardware_vendor,
|
||||
:detail_updated_at,
|
||||
:label_updated_at,
|
||||
:uuid
|
||||
) ON DUPLICATE KEY UPDATE
|
||||
hostname = VALUES(hostname),
|
||||
computer_name = VALUES(computer_name),
|
||||
platform = VALUES(platform),
|
||||
os_version = VALUES(os_version),
|
||||
build = VALUES(build),
|
||||
memory = VALUES(memory),
|
||||
team_id = VALUES(team_id),
|
||||
hardware_serial = VALUES(hardware_serial),
|
||||
cpu_type = VALUES(cpu_type),
|
||||
hardware_model = VALUES(hardware_model),
|
||||
hardware_vendor = VALUES(hardware_vendor),
|
||||
detail_updated_at = VALUES(detail_updated_at),
|
||||
label_updated_at = VALUES(label_updated_at),
|
||||
uuid = VALUES(uuid)
|
||||
`
|
||||
result, err := sqlx.NamedExecContext(ctx, tx, insertStmt, params)
|
||||
if err != nil {
|
||||
return ctxerr.Wrap(ctx, err, "get host ID after update")
|
||||
return ctxerr.Wrap(ctx, err, "new Android host")
|
||||
}
|
||||
id, _ := result.LastInsertId()
|
||||
if id == 0 {
|
||||
// This was an UPDATE, not an INSERT, so we need to get the host ID
|
||||
var hostID uint
|
||||
err := sqlx.GetContext(ctx, tx, &hostID, `SELECT id FROM hosts WHERE node_key = ?`, host.NodeKey)
|
||||
if err != nil {
|
||||
return ctxerr.Wrap(ctx, err, "get host ID after update")
|
||||
}
|
||||
host.Host.ID = hostID
|
||||
} else {
|
||||
host.Host.ID = uint(id) // nolint:gosec
|
||||
}
|
||||
host.Host.ID = hostID
|
||||
} else {
|
||||
host.Host.ID = uint(id) // nolint:gosec
|
||||
// Orbit-enrolled Android host already exists; update it in place so both
|
||||
// enrollment paths converge on a single hosts row.
|
||||
params["id"] = existingID
|
||||
updateStmt := `
|
||||
UPDATE hosts SET
|
||||
node_key = :node_key,
|
||||
hostname = :hostname,
|
||||
computer_name = :computer_name,
|
||||
platform = :platform,
|
||||
os_version = :os_version,
|
||||
build = :build,
|
||||
memory = :memory,
|
||||
team_id = :team_id,
|
||||
hardware_serial = :hardware_serial,
|
||||
cpu_type = :cpu_type,
|
||||
hardware_model = :hardware_model,
|
||||
hardware_vendor = :hardware_vendor,
|
||||
detail_updated_at = :detail_updated_at,
|
||||
label_updated_at = :label_updated_at,
|
||||
uuid = :uuid
|
||||
WHERE id = :id`
|
||||
if _, err := sqlx.NamedExecContext(ctx, tx, updateStmt, params); err != nil {
|
||||
return ctxerr.Wrap(ctx, err, "update existing orbit-enrolled Android host")
|
||||
}
|
||||
host.Host.ID = existingID
|
||||
}
|
||||
host.Device.HostID = host.Host.ID
|
||||
|
||||
|
|
|
|||
|
|
@ -28,6 +28,7 @@ func TestAndroid(t *testing.T) {
|
|||
fn func(t *testing.T, ds *Datastore)
|
||||
}{
|
||||
{"NewAndroidHost", testNewAndroidHost},
|
||||
{"NewAndroidHostDedupesOrbitEnrolled", testNewAndroidHostDedupesOrbitEnrolled},
|
||||
{"UpdateAndroidHost", testUpdateAndroidHost},
|
||||
{"AndroidMDMStats", testAndroidMDMStats},
|
||||
{"AndroidHostStorageData", testAndroidHostStorageData},
|
||||
|
|
@ -124,6 +125,97 @@ func testNewAndroidHost(t *testing.T, ds *Datastore) {
|
|||
require.Empty(t, lbls)
|
||||
}
|
||||
|
||||
// testNewAndroidHostDedupesOrbitEnrolled covers the duplicate-Android-hosts fix.
|
||||
// The Fleet Android agent enrolls first via /api/fleet/orbit/enroll,
|
||||
// then later the AMAPI pubsub flow delivers a STATUS_REPORT that lands in
|
||||
// NewAndroidHost. The dedupe works whether the agent also sends
|
||||
// platform="android" (newer agents) or leaves it blank (older agents).
|
||||
func testNewAndroidHostDedupesOrbitEnrolled(t *testing.T, ds *Datastore) {
|
||||
test.AddBuiltinLabels(t, ds)
|
||||
|
||||
cases := []struct {
|
||||
name string
|
||||
platform string
|
||||
}{
|
||||
{"agent sends no platform", ""},
|
||||
{"agent sends platform=android", "android"},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
ctx := testCtx()
|
||||
enterpriseSpecificID := strings.ToUpper(uuid.New().String())
|
||||
|
||||
orbitHost, err := ds.EnrollOrbit(ctx,
|
||||
fleet.WithEnrollOrbitMDMEnabled(true),
|
||||
fleet.WithEnrollOrbitHostInfo(fleet.OrbitHostInfo{
|
||||
HardwareUUID: enterpriseSpecificID,
|
||||
HardwareSerial: enterpriseSpecificID,
|
||||
Platform: tc.platform,
|
||||
Hostname: "Samsung TestDevice",
|
||||
ComputerName: "Samsung TestDevice",
|
||||
HardwareModel: "TestModel",
|
||||
}),
|
||||
fleet.WithEnrollOrbitNodeKey(uuid.New().String()),
|
||||
)
|
||||
require.NoError(t, err)
|
||||
require.NotZero(t, orbitHost.ID)
|
||||
|
||||
// Orbit enroll alone does not write an android_devices row; AndroidHostLite misses.
|
||||
_, err = ds.AndroidHostLite(ctx, enterpriseSpecificID)
|
||||
require.True(t, fleet.IsNotFound(err),
|
||||
"before AMAPI arrives there is no android_devices row, so AndroidHostLite should miss")
|
||||
|
||||
// Simulate the AMAPI pubsub path calling NewAndroidHost. The fix makes
|
||||
// NewAndroidHost find the existing orbit-enrolled hosts row by uuid and
|
||||
// reuse it instead of inserting a duplicate.
|
||||
newHost := createAndroidHost(enterpriseSpecificID)
|
||||
returned, err := ds.NewAndroidHost(ctx, newHost, false)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, returned)
|
||||
require.Equal(t, orbitHost.ID, returned.Host.ID,
|
||||
"NewAndroidHost must reuse the orbit-enrolled hosts row, not insert a duplicate")
|
||||
|
||||
// AndroidHostLite now finds the host via the newly-created android_devices row.
|
||||
androidHost, err := ds.AndroidHostLite(ctx, enterpriseSpecificID)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, androidHost)
|
||||
require.Equal(t, orbitHost.ID, androidHost.Host.ID)
|
||||
require.Equal(t, enterpriseSpecificID, androidHost.Host.UUID)
|
||||
|
||||
// Exactly one hosts row and one android_devices row for this device.
|
||||
var hostCount, deviceCount int
|
||||
require.NoError(t, sqlx.GetContext(ctx, ds.writer(ctx), &hostCount,
|
||||
`SELECT COUNT(*) FROM hosts WHERE uuid = ?`, enterpriseSpecificID))
|
||||
require.Equal(t, 1, hostCount)
|
||||
require.NoError(t, sqlx.GetContext(ctx, ds.writer(ctx), &deviceCount,
|
||||
`SELECT COUNT(*) FROM android_devices WHERE enterprise_specific_id = ?`, enterpriseSpecificID))
|
||||
require.Equal(t, 1, deviceCount)
|
||||
|
||||
// Subsequent orbit re-enroll (agent node-key wipe, reinstall) stays idempotent.
|
||||
_, err = ds.EnrollOrbit(ctx,
|
||||
fleet.WithEnrollOrbitMDMEnabled(true),
|
||||
fleet.WithEnrollOrbitHostInfo(fleet.OrbitHostInfo{
|
||||
HardwareUUID: enterpriseSpecificID,
|
||||
HardwareSerial: enterpriseSpecificID,
|
||||
Platform: tc.platform,
|
||||
Hostname: "Samsung TestDevice",
|
||||
ComputerName: "Samsung TestDevice",
|
||||
HardwareModel: "TestModel",
|
||||
}),
|
||||
fleet.WithEnrollOrbitNodeKey(uuid.New().String()),
|
||||
)
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, sqlx.GetContext(ctx, ds.writer(ctx), &hostCount,
|
||||
`SELECT COUNT(*) FROM hosts WHERE uuid = ?`, enterpriseSpecificID))
|
||||
require.Equal(t, 1, hostCount)
|
||||
require.NoError(t, sqlx.GetContext(ctx, ds.writer(ctx), &deviceCount,
|
||||
`SELECT COUNT(*) FROM android_devices WHERE enterprise_specific_id = ?`, enterpriseSpecificID))
|
||||
require.Equal(t, 1, deviceCount)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func createAndroidHost(enterpriseSpecificID string) *fleet.AndroidHost {
|
||||
// Device ID needs to be unique per device
|
||||
deviceID := md5ChecksumBytes([]byte(enterpriseSpecificID))[:16]
|
||||
|
|
|
|||
Loading…
Reference in a new issue