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