Skip to content

Rename hosts: update-template endpoint, enforcement pipeline, and verification #48623

Description

@juan-fdz-hawa

Related user story

#38806

Task

Make templates real: the POST /api/v1/fleet/name_template endpoint (license + authz + validation + activity), team-payload/spec plumbing (which GitOps rides on), bulk-marking hosts on save, the cron step that resolves and enqueues Settings commands, the command-result handler (ACK → rename host in Fleet + verifying; Error → failed), and independent verification/drift detection at both name-ingestion sites. After this merges, saving a template renames a fleet end to end.

Endpoint (server/service/mdm.go, register in server/service/handler.go next to line 846)

Follow updateDiskEncryptionEndpoint (mdm.go:2986-3034) exactly:

// handler.go, next to ue.POST("/api/_version_/fleet/disk_encryption", ...)
ue.POST("/api/_version_/fleet/name_template", updateNameTemplateEndpoint, updateNameTemplateRequest{})

type updateNameTemplateRequest struct {
	TeamID       *uint  `json:"team_id" renameto:"fleet_id"`
	NameTemplate string `json:"name_template"`
}
// response: 204 like updateMDMDiskEncryptionResponse

Core Service.UpdateMDMNameTemplate(ctx, teamID *uint, nameTemplate string) error:

  • license check first (license.FromContextsvc.authz.SkipAuthorization + fleet.ErrMissingLicense), then svc.authz.Authorize(ctx, fleet.MDMAppleSettingsPayload{TeamID: teamID}, fleet.ActionWrite) — same authz object as disk encryption (story: "Same permissions as recovery lock, disk encryption").
  • teamID == nil → 422 "name_template requires fleet_id; this setting is only available for fleets" (fleets-only decision).
  • non-empty template → fleet.ValidateHostNameTemplate (sub-issue Rename hosts: config types, template validation, and activity type #48621) → 422 with the validator's message on failure.
  • dispatch svc.EnterpriseOverrides.UpdateTeamMDMNameTemplate(ctx, tm, nameTemplate); wire the override in ee/server/service/service.go (like line 96).

EE implementation (ee/server/service/teams.go, follow updateTeamMDMDiskEncryption at teams.go:2218-2275)

updateTeamMDMNameTemplate(ctx, tm *fleet.Team, nameTemplate string):

  • no-op if tm.Config.MDM.NameTemplate == nameTemplate (idempotent; avoids re-enqueue storms on repeated saves — mirrors "exclude current entity" upsert guidance);
  • set + svc.ds.SaveTeam;
  • emit ActivityTypeEditedHostNameTemplate{TeamID, TeamName, NameTemplate: nilIfEmpty};
  • non-empty → svc.ds.BulkUpsertHostDeviceNameEnforcement(ctx, tm.ID); empty → svc.ds.DeleteHostDeviceNameEnforcementForTeam(ctx, tm.ID) (clearing never renames — rows are deleted, no command sent).

Also plumb TeamPayloadMDM.NameTemplate through ModifyTeam (set near ee/server/service/teams.go:313-319, activity near 604-614) and TeamSpecMDM.NameTemplate through team-spec apply (teams.go:1580, 1765, 2028 regions), all funneling into the same helper so PATCH /teams/{id}, spec apply, GitOps, and the dedicated endpoint behave identically (validation + activity + row reconcile).

Cron enqueue step

Add a step to the mdm_apple_profile_manager cron (registered at cmd/fleet/cron_registration.go:224; symbol fleet.CronMDMAppleProfileManager, cmd/fleet/cron.go:1888; its main loop calls ReconcileAppleProfilesBatched at cron.go:1900 — add the device-name step alongside):

  • ListHostsPendingDeviceNameCommand(ctx, batchLimit);
  • per host: resolved := fleet.ResolveHostNameTemplate(team.Config.MDM.NameTemplate, host); enforce a 63-byte UTF-8 cap on the resolved name — if exceeded, set row failed with detail Resolved name exceeds 63 bytes. (Apple truncates/rejects longer names — SME confirm exact limit);
  • skip if the host's current computer_name already equals resolved → mark row verified directly (no needless command);
  • cmdUUID := fleet.DeviceNameCommandUUIDPrefix + uuid.New().String(); commander.DeviceNameSetting(...); SetHostDeviceNameCommandSent(...).

Command results handler (server/service/apple_mdm.go)

Route by UUID prefix before the requestType switch, like the refetch prefix routing at apple_mdm.go:4167-4171 (why prefix, not a "Settings" case: future features may send other Settings items; the prefix scopes handling to renames):

  • Acknowledged: fetch row by command_uuid; update hosts.computer_name + hostname to expected_device_name and upsert host_display_names (see writes at server/datastore/mysql/hosts.go:2427, display-name derivation server/fleet/hosts.go:1008); set row verifying. Parse per-item status from the Settings ack payload if present (each Settings item can individually error — SME confirm shape) → treat item-level error as Error.
  • Error/CommandFormatError: row failed, detail = Apple error chain (format like the InstallProfile case at apple_mdm.go:4182).
  • Map via mdmAppleDeliveryStatusFromCommandStatus (apple_mdm.go:5504) where convenient.

Verification + drift detection (both name-ingestion sites)

  • iOS/iPadOS: in handleRefetchDeviceResults (apple_mdm.go:5275), after DeviceName is extracted (apple_mdm.go:5296, 5337-5340), call ds.VerifyHostDeviceName(ctx, host.UUID, deviceName).
  • macOS: osquery system_info ingestion sets host.ComputerName in-memory (server/service/osquery_utils/queries.go:381-420, computer_name at :418) without DB access; hook the comparison where detail results are persisted in the osquery service flow (after the ingest funcs run and the host is saved) — call ds.VerifyHostDeviceName(ctx, host.UUID, host.ComputerName) only when a rename row exists (cheap indexed lookup; consider piggybacking on an existing per-detail-ingest DB call to avoid a hot-path query for hosts with no enforcement).
  • VerifyHostDeviceName semantics (from sub-issue Rename hosts: enforcement-state migrations and datastore methods #48620): rows in verifying/verified only; match → verified; mismatch → failed with detail Host was renamed on the device and no longer matches the fleet's naming template. (drift → Failed; this is the piece the Figma dev note marks as cuttable under capacity pressure).

Condition of satisfaction

  • Endpoint: MYSQL_TEST=1 REDIS_TEST=1 go test ./server/service/... — 402/ErrMissingLicense on free tier; 401/403 for observer & team-scoped users of other teams (same matrix as disk encryption); 422 for missing fleet_id, bad variable, $FLEET_SECRET_*, control chars; 204 + edited_host_name_template activity (with name_template: null when cleared) on success; saving the identical template emits no duplicate activity and re-enqueues nothing.
  • Rows on save: saving a template creates NULL-status rows for eligible hosts only; clearing deletes all rows for the team without touching hosts.computer_name.
  • Cron: enterprise integration test — pending row + eligible host → command enqueued with DEVNAME- prefix and resolved name (serial expanded); already-matching host goes straight to verified; BYOD host has no row and gets nothing; >63-byte resolution goes failed; a simulated unsupervised-device command Error lands failed with Apple's error in detail and is not re-sent by subsequent cron runs.
  • Ack/Error: simulated ACK renames the host in Fleet (computer_name, hostname, display name) and sets verifying; simulated Error sets failed with the Apple error chain in detail.
  • Verification: reported name == expected flips verifyingverified; a later mismatched report flips verifiedfailed (drift), for both the iOS refetch path and the macOS detail-ingest path; hosts with no row are untouched (no hot-path regression — add a benchmark-ish assertion or at minimum verify no extra query when no enforcement exists).
  • End-to-end (integration-mdm bundle): set template → cron → ack → refetch-verify → verified, on a fake MDM device.

Metadata

Metadata

Assignees

Labels

#g-orchestrationOrchestration product group:releaseReady to write code. Scheduled in a release. See "Making changes" in handbook.~backendBackend-related issue.~sub-taskA technical sub-task that is part of a story. (Not QA'd. Not estimated.)

Type

Fields

No fields configured for Task.

Projects

Status
✔️Awaiting QA

Milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions