Skip to content

[BUG] Retry re-marks host for build and invalidates in-flight PXE token #758

Open

Description

@kambiz-aghaiepour

Description

A retried or overlapping move/rebuild can invalidate the provisioning token of a host that is already PXE booting. The host then loops on 401 Unauthorized against /unattended/provision until it is powered off and boots PXE again.

Root cause

Foreman.prepare_host_provisioning() calls mark_for_build() before writing the OS/medium/ptable:

  • src/quads/tools/external/foreman.py:615
  • mark_for_build() unconditionally PUTs {"host": {"build": 1}} (foreman.py:444-446).

Foreman generates a new provisioning token whenever the host is marked for build. The running installer does not re-read the Foreman host token; it retries the kickstart URL embedded in the PXE template from its boot. A second mark after that boot rotates the token and makes the installer's URL stale.

There is also an ordering window on Dell/HPE:

  • src/quads/plugins/builtin/release/standard.py:192 powers the host on before prepare_host_provisioning() at standard.py:207.
  • If a previous attempt left a one-time PXE boot target set, the host can boot and fetch its PXE template before the new attempt marks it for build.
  • mark_for_build() then rotates the token the host just received.

There is no guard that skips build=1 when the host is already marked for build, and the failure path clears QUADS' build flag but not Foreman's build state/token.

Observed sequence

From Foreman production logs (hostname, IPs, and tokens omitted):

12:20:13  PUT /api/v2/hosts/<id> {"host":{"build":1}}
12:20:24  GET /unattended/provision?token=<prior-generation> -> 401
12:21:42  PUT /api/v2/hosts/<id> {"host":{"build":1}}

The host was already using a token from an earlier generation when a later move attempt marked it for build again. Retrying Anaconda cannot recover because it keeps requesting the same URL.

Expected behavior

A retry must not invalidate the token of a host that is already provisioning. Marking an already-building host should be a no-op, or QUADS must guarantee the host is not booting before rotating the token.

Suggested fix

  • Make mark_for_build() idempotent: read the host's current build state and return without a PUT when build is already true.
  • Avoid powering on a host with a pending one-time PXE target before Foreman build state and parameters are finalized.
  • Keep token generation as close as possible to the reboot that will consume it, and serialize per-host rebuilds.
  • Add a regression test that asserts a second prepare_host_provisioning()/mark_for_build() call does not PUT build=1 again when the host is already building.

Impact

A failed or delayed boot followed by a retry leaves the host stuck in the installer with an invalid token. The current recovery is manual: mark the host for build again and power cycle it so it fetches a fresh PXE template.

Activity

  1. kambiz-aghaiepour commented on Oct 30, 2026

    @kambiz-aghaiepour
    ContributorAuthor

    Additional recovery data: the cached TFTP grub config for the affected host was generated with the then-current token and was not refreshed by later build=1 updates. A fresh PXE boot still embedded the stale token and returned 401. hammer host rebuild-config --only TFTP regenerated the cached grub config with the current Foreman token; the next PXE cycle returned 200.

    So there are two coupled failure modes: rotating the token while the host is already building, and serving stale cached PXE config after a token change. The code change under review avoids the unnecessary rotation on retries; refreshing TFTP when the token changes may also be needed.

  2. sadsfae commented on Oct 30, 2026

    @sadsfae
    Contributor

    I think this is less pressing because we as a matter of policy don't expire Foreman provisioning tokens, nonetheless we'll include it in a future release.

  3. kambiz-aghaiepour commented on Oct 30, 2026

    @kambiz-aghaiepour
    ContributorAuthor

    Agreed. Until then, a workaround is to mark the host as build=false, and then back to build=true. After 57 quads deployments in a test environment, I came across this 1 time.

  4. ttlogan commented on Oct 31, 2026

    @ttlogan
    Contributor

    Verified against the source: mark_for_build() at src/quads/tools/external/foreman.py:444 unconditionally PUTs {"host":{"build":1}}, and prepare_host_provisioning() fires it at line 615 before it sets OS/medium/ptable. So a retry or overlapping rebuild on a host that is already booting rotates the token the running installer holds, which is exactly the 401 loop.

    The idempotent guard is the right minimal fix. Read the host's current build state first and skip the PUT when build is already true. That also defuses the Dell/HPE ordering window, since you avoid touching a host that is already provisioning.

    The stale TFTP grub config is a separate trigger but the same root (rotation after an earlier boot), so stopping the unnecessary rotation should cover both. No commitment from me here, happy to look at a change if one lands.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions