Skip to content

Commit 7c9b463

Browse files
committed
Add host name template enforcement pipeline and verification
Relates to #48623 Adds the mdm_apple_profile_manager cron step that resolves each queued host's name template and enqueues the Settings/DeviceName command, the DEVNAME- command-result handler, and independent verification with drift detection at both name-ingestion sites (iOS/iPadOS DeviceInformation refetch and macOS osquery detail ingest). Mismatching reports arriving shortly after an acknowledgment are treated as stale pre-rename reports rather than drift.
1 parent adb1cc7 commit 7c9b463

15 files changed

Lines changed: 1048 additions & 120 deletions

cmd/fleet/cron.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1902,6 +1902,9 @@ func newAppleMDMProfileManagerSchedule(
19021902
schedule.WithJob("manage_apple_declarations", func(ctx context.Context) error {
19031903
return service.ReconcileAppleDeclarationsBatched(ctx, ds, commander, logger)
19041904
}),
1905+
schedule.WithJob("manage_apple_device_names", func(ctx context.Context) error {
1906+
return service.ReconcileHostDeviceNames(ctx, ds, commander, logger)
1907+
}),
19051908
)
19061909

19071910
return s, nil

server/datastore/mysql/apple_device_names.go

Lines changed: 82 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"context"
55
"database/sql"
66
"errors"
7+
"time"
78

89
"github.com/fleetdm/fleet/v4/server/contexts/ctxerr"
910
"github.com/fleetdm/fleet/v4/server/fleet"
@@ -76,27 +77,36 @@ func (ds *Datastore) ListHostsPendingDeviceNameCommand(ctx context.Context, limi
7677
return pending, nil
7778
}
7879

79-
func (ds *Datastore) SetHostDeviceNameCommandSent(ctx context.Context, hostUUID, commandUUID, expectedName string) error {
80+
func (ds *Datastore) SetHostDeviceNameStatus(ctx context.Context, hostUUID string, status fleet.MDMDeliveryStatus, commandUUID *string, expectedName, detail string) error {
81+
// commandUUID is bound directly: a non-nil value records the enqueued
82+
// command; nil clears it so a result from a previously sent command can't
83+
// match this row and overwrite the outcome recorded here.
8084
const stmt = `
8185
UPDATE host_mdm_apple_device_names
82-
SET status = ?, command_uuid = ?, expected_device_name = ?, detail = ''
86+
SET status = ?, command_uuid = ?, expected_device_name = ?, detail = ?
8387
WHERE host_uuid = ?`
8488

85-
res, err := ds.writer(ctx).ExecContext(ctx, stmt, fleet.MDMDeliveryPending, commandUUID, expectedName, hostUUID)
89+
res, err := ds.writer(ctx).ExecContext(ctx, stmt, status, commandUUID, expectedName, detail, hostUUID)
8690
if err != nil {
87-
return ctxerr.Wrap(ctx, err, "set host device name command sent")
91+
return ctxerr.Wrap(ctx, err, "set host device name status")
8892
}
8993
if rows, _ := res.RowsAffected(); rows == 0 {
90-
// The row went away between the cron listing it and sending the command
91-
// (e.g. the template was cleared). The command's later result will simply
92-
// not match any row and be dropped.
93-
ds.logger.DebugContext(ctx, "device name command sent but no enforcement row updated", "host_uuid", hostUUID, "command_uuid", commandUUID)
94+
// The row went away between the cron listing it and this write (e.g. the
95+
// template was cleared); nothing to record. Any command already sent will
96+
// simply not match a row when its result arrives and be dropped.
97+
ds.logger.DebugContext(ctx, "host device name status set but no enforcement row updated", "host_uuid", hostUUID)
9498
}
9599
return nil
96100
}
97101

98-
func (ds *Datastore) UpdateHostDeviceNameStatusFromCommand(ctx context.Context, commandUUID string, status fleet.MDMDeliveryStatus, detail string) (hostUUID string, expectedName string, err error) {
99-
err = ds.withRetryTxx(ctx, func(tx sqlx.ExtContext) error {
102+
func (ds *Datastore) UpdateHostDeviceNameStatusFromCommand(ctx context.Context, commandUUID string, acknowledged bool, detail string) error {
103+
// The command result is one of exactly two outcomes: acknowledged (the
104+
// device applied the rename → verifying) or an error (→ failed).
105+
status := fleet.MDMDeliveryFailed
106+
if acknowledged {
107+
status = fleet.MDMDeliveryVerifying
108+
}
109+
return ds.withRetryTxx(ctx, func(tx sqlx.ExtContext) error {
100110
// The UPDATE is authoritative. A 0-row result means no current row holds
101111
// this command UUID: it was superseded by a newer command for the same
102112
// host (the row keeps only the latest) or the row was deleted. Either way
@@ -113,51 +123,93 @@ func (ds *Datastore) UpdateHostDeviceNameStatusFromCommand(ctx context.Context,
113123
return ctxerr.Wrap(ctx, notFound("HostDeviceNameEnforcement").WithName(commandUUID))
114124
}
115125

116-
// Read back the host and the name we told the device to apply so the
117-
// caller can rename the host in Fleet; a plain UPDATE can't return these.
118-
// The row is locked by the UPDATE above, so this can't miss.
119-
var row struct {
120-
HostUUID string `db:"host_uuid"`
126+
if !acknowledged {
127+
// Only an acknowledgment renames the host; error results just record
128+
// the failure on the row.
129+
return nil
130+
}
131+
132+
// Acknowledged: rename the host in Fleet in this same transaction so the
133+
// row transition and the Fleet-side rename are atomic. Join the row to
134+
// its host to read the expected name and the fields needed to derive the
135+
// display name. The row is locked by the UPDATE above, so this can't miss.
136+
var host struct {
137+
ID uint `db:"id"`
138+
HardwareModel string `db:"hardware_model"`
139+
HardwareSerial string `db:"hardware_serial"`
121140
ExpectedDeviceName *string `db:"expected_device_name"`
122141
}
123142
const selectStmt = `
124-
SELECT host_uuid, expected_device_name
125-
FROM host_mdm_apple_device_names
126-
WHERE command_uuid = ?`
127-
if err := sqlx.GetContext(ctx, tx, &row, selectStmt, commandUUID); err != nil {
128-
return ctxerr.Wrapf(ctx, err, "get host device name enforcement for command %s", commandUUID)
143+
SELECT h.id, h.hardware_model, h.hardware_serial, n.expected_device_name
144+
FROM host_mdm_apple_device_names n
145+
JOIN hosts h ON h.uuid = n.host_uuid
146+
WHERE n.command_uuid = ?`
147+
if err := sqlx.GetContext(ctx, tx, &host, selectStmt, commandUUID); err != nil {
148+
return ctxerr.Wrapf(ctx, err, "get host to rename for command %s", commandUUID)
129149
}
150+
// A row only carries a command_uuid when SetHostDeviceNameStatus set it
151+
// together with the resolved expected_device_name, so a row matched here by
152+
// command_uuid always has a name.
153+
name := *host.ExpectedDeviceName
130154

131-
hostUUID = row.HostUUID
132-
if row.ExpectedDeviceName != nil {
133-
expectedName = *row.ExpectedDeviceName
155+
if _, err := tx.ExecContext(ctx,
156+
`UPDATE hosts SET computer_name = ?, hostname = ? WHERE id = ?`, name, name, host.ID); err != nil {
157+
return ctxerr.Wrap(ctx, err, "rename host from device name")
158+
}
159+
displayName := fleet.HostDisplayName(name, name, host.HardwareModel, host.HardwareSerial)
160+
if _, err := tx.ExecContext(ctx, `
161+
INSERT INTO host_display_names (host_id, display_name) VALUES (?, ?)
162+
ON DUPLICATE KEY UPDATE display_name = VALUES(display_name)`, host.ID, displayName); err != nil {
163+
return ctxerr.Wrap(ctx, err, "update host display name from device name")
134164
}
135165
return nil
136166
})
137-
return hostUUID, expectedName, err
138167
}
139168

140-
func (ds *Datastore) VerifyHostDeviceName(ctx context.Context, hostUUID, reportedName string) error {
169+
// deviceNameVerifyGracePeriod is how long after an acknowledgment (the row
170+
// entered verifying) a mismatching reported name is ignored rather than
171+
// recorded as drift. A report the agent collected before the device applied the
172+
// rename can arrive after the acknowledgment carrying the old name; the only
173+
// staleness is the agent's collect-to-submit latency (seconds), so a small
174+
// fixed window comfortably covers it. The comparison is entirely against the DB
175+
// clock (updated_at vs NOW()), so there's no cross-machine skew to pad for.
176+
const deviceNameVerifyGracePeriod = 60 * time.Second
177+
178+
func (ds *Datastore) UpdateHostDeviceNameStatusFromReport(ctx context.Context, hostUUID, reportedName string) error {
141179
// Only rows already awaiting or past verification are reconciled against the
142180
// device-reported name: a match confirms the rename (verified), a mismatch
143181
// records drift (failed). Rows in any other state and hosts with no row are
144182
// left untouched.
183+
//
184+
// A mismatch on a row acknowledged within the last deviceNameVerifyGracePeriod
185+
// is left untouched (false drift; failed rows only recover via an explicit
186+
// resend). Rows already verified reached that state through a fresh
187+
// post-rename report, so a mismatch there is genuine drift regardless of age.
188+
// When the CASEs resolve to the current values, MySQL skips the row write,
189+
// preserving updated_at (the grace anchor).
145190
const stmt = `
146191
UPDATE host_mdm_apple_device_names
147192
SET
148-
status = CASE WHEN expected_device_name = ? THEN ? ELSE ? END,
149-
detail = CASE WHEN expected_device_name = ? THEN '' ELSE ? END
193+
status = CASE
194+
WHEN expected_device_name = ? THEN ?
195+
WHEN status = ? AND updated_at > DATE_SUB(NOW(6), INTERVAL ? SECOND) THEN status
196+
ELSE ? END,
197+
detail = CASE
198+
WHEN expected_device_name = ? THEN ''
199+
WHEN status = ? AND updated_at > DATE_SUB(NOW(6), INTERVAL ? SECOND) THEN detail
200+
ELSE ? END
150201
WHERE host_uuid = ?
151202
AND status IN (?, ?)`
152203

153204
const driftDetail = "Host was renamed on the device and no longer matches the fleet's naming template."
205+
graceSeconds := int(deviceNameVerifyGracePeriod.Seconds())
154206
if _, err := ds.writer(ctx).ExecContext(ctx, stmt,
155-
reportedName, fleet.MDMDeliveryVerified, fleet.MDMDeliveryFailed,
156-
reportedName, driftDetail,
207+
reportedName, fleet.MDMDeliveryVerified, fleet.MDMDeliveryVerifying, graceSeconds, fleet.MDMDeliveryFailed,
208+
reportedName, fleet.MDMDeliveryVerifying, graceSeconds, driftDetail,
157209
hostUUID,
158210
fleet.MDMDeliveryVerifying, fleet.MDMDeliveryVerified,
159211
); err != nil {
160-
return ctxerr.Wrap(ctx, err, "verify host device name")
212+
return ctxerr.Wrap(ctx, err, "update host device name status from report")
161213
}
162214
return nil
163215
}

0 commit comments

Comments
 (0)