Repository navigation
Start nspawn workers after reboot on RHEL and recover stopped workers - #437
hbc (bcho) with Copilot wants to merge 13 commits into
Conversation
Co-authored-by: bcho <1975118+bcho@users.noreply.github.com>
Co-authored-by: bcho <1975118+bcho@users.noreply.github.com>
Co-authored-by: bcho <1975118+bcho@users.noreply.github.com>
There was a problem hiding this comment.
🟡 Changes recommended
Worker failures can go undetected and unrecovered, while the new preflight check blocks online bootstrap before required packages can be installed.
2 open issues
What changed in this PR
This PR addresses the reboot-startup portion of #436 by making the Flex Node daemon request machines.target, allowing enabled nspawn workers to start even when RHEL presets disable the target.
Changes:
- Add
machines.targetdependencies to host and container daemon units. - Register a preflight check for target availability and enablement.
- Add checker tests, update service snapshots, and document preflight behavior.
| File | Description |
|---|---|
pkg/daemon/testdata/aks-flex-node-agent.service.golden |
Update the expected host-unit dependency. |
pkg/daemon/testdata/aks-flex-node-agent-arc.service.golden |
Update the expected Arc-enabled unit dependency. |
pkg/daemon/preflight.go |
Add target-state validation. |
pkg/daemon/preflight_test.go |
Test target states and daemon-unit dependencies. |
pkg/daemon/assets/aks-flex-node-agent.service |
Request worker startup through machines.target. |
pkg/daemon/assets/aks-flex-node-agent-container.service |
Add the same dependency for container deployments. |
pkg/cmd/preflight/preflight.go |
Register the new preflight check. |
docs/usage/operations.md |
Explain target-check errors and warnings. |
Review effort: Balanced
Philip Lombardi (plombardi89)
left a comment
There was a problem hiding this comment.
Thanks for this. The boot fix itself works. Requesting changes for the preflight check and some housekeeping.
Blocking
- The
machines-targetcheck fails preflight on a fresh host wheresystemd-containerisn't installed yet, although bootstrap installs it and unbounded's own checks only warn about it. Inline onpkg/daemon/preflight.go:37, with a reproducer on RHEL 9.8. - Housekeeping before merge:
- The description is out of date. It still lists the "Worker health" changes that 695b06b removed.
Fixes #436will close the issue, but only half of it is fixed. The issue's second request (the agent noticing a stopped worker instead of reporting success for three days) was dropped. Please useRefs #436and open a follow-up issue, or note on #436 that this part won't be done.- No
CHANGELOG.mdentry. There's an[Unreleased]section, and this change is user-facing: a fix (workers now start after reboot on RHEL) and a new preflight check.
Verified: Wants=machines.target. I tested it on systemd 252 (Rocky Linux 9.8, the same systemd as the RHEL 9.8 hosts in #436):
- The worker was the real
systemd-nspawn@.service, with only its process replaced bysleep. - The agent unit was the actual golden unit from
mainor from this PR, again with onlyExecStartreplaced. machines.targetwasdisabledandsystemd-nspawn@kube1.servicewas enabled, as on the reporter's hosts.- Each scenario was a reboot.
| Scenario | machines.target |
Worker | Agent |
|---|---|---|---|
Agent unit from main |
inactive | never started (reproduces #436) | active |
| Agent unit from this PR | active | active | active, without waiting for the worker |
PR's container-variant unit, containerd start fails |
active | active | not started (dependency failed) |
| Worker disabled and stopped on purpose, as unbounded does when it stops a machine | active | stays stopped | active |
The upgrade path also heals hosts that are already stuck. Writing the new unit and running daemon-reload alone starts nothing, but the agent restart that follows an upgrade starts machines.target and the stopped worker. Stopping the agent doesn't stop either of them.
After=machines.target: no change needed. Unbounded's own daemon unit has After= as well as Wants=. Adding it here would hold the agent until the worker's start finishes; I tested that, and a worker whose start fails doesn't block the agent. A start that hangs would hold it for up to the worker's start timeout, 90 seconds by default for the Type=notify nspawn unit. I found nothing in pkg/daemon that assumes the worker is up at startup, and the worker's pre-start/post-start hooks don't depend on the agent unit. So Wants= alone is the better choice here.
The remaining gap is a real reboot of a real RHEL host with a real nspawn worker, which the PR description already lists as outstanding.
| case "disabled", "enabled-runtime": | ||
| return preflight.ResultsWarning(c.Name(), target, "machines.target is not enabled persistently; bootstrap installs a daemon Wants=machines.target dependency to start workers at boot") | ||
| default: | ||
| return preflight.ResultsError(c.Name(), target, "machines.target is missing, masked, or unavailable; install systemd-container and unmask machines.target before bootstrap") |
There was a problem hiding this comment.
Blocking: preflight fails on a fresh host that bootstrap would set up fine
machines.target ships in systemd-container, on RHEL 9 and on Ubuntu 24.04 alike. On a host where that package isn't installed yet, systemctl show returns an empty UnitFileState, and this default branch reports an error. But systemd-container is one of the packages bootstrap installs. Unbounded's own host checks report it as missing with a warning online ("may be installed by bootstrap"), and as an error only when offlineArtifacts is configured (pkg/agent/phases/host/preflight_host.go:156-172 in unbounded v0.21.0). So on the same host, host-packages says bootstrap will install the package, while machines-target fails preflight for the package being missing. docs/usage/operations.md tells users to "Run preflight before mutating the host", which is exactly when the package may not be there yet.
Reproducer. I built aks-flex-node from this PR (69f1cc6) and ran it in a Rocky Linux 9.8 container with systemd as PID 1. RHEL's own UBI 9.8 image gives the same machines-target error, but UBI's repos don't carry systemd-container, so I used Rocky for the before/after.
- Start the host and confirm the package is missing:
docker run -d --name rhel9 --privileged --cgroupns=private --tmpfs /run --tmpfs /tmp \ -v "$PWD/aks-flex-node:/usr/local/bin/aks-flex-node:ro" -v "$PWD/config.json:/repro/config.json:ro" \ docker.io/rockylinux/rockylinux:9-ubi-init docker exec rhel9 rpm -q systemd-container # package systemd-container is not installed docker exec rhel9 systemctl show --property=UnitFileState --value machines.target # empty, exit 0
config.jsonis the bootstrap-token example fromdocs/usage/configuration.md, with placeholder values. - Run preflight. The two ignored checks fail only because of the test environment: a placeholder API server, and no
/etc/sysctl.din the image.Result: exit 1,docker exec rhel9 aks-flex-node preflight --config /repro/config.json --output json \ --ignore-preflight-errors=api-server-reachable,host-os-configuration"status": "failed", andmachines-targetis the only error:error machines-target machines.target is missing, masked, or unavailable; install systemd-container and unmask machines.target before bootstrap warning host-packages required host packages are missing and may be installed by bootstrap: systemd-container, curl, nftables - Install the package, as bootstrap would, and run step 2 again:
Result: exit 0,
docker exec rhel9 dnf -y install systemd-container docker exec rhel9 systemctl is-enabled machines.target # disabled (the RHEL preset from #436)
"status": "ok", andmachines-targetis now a warning ("not enabled persistently; ... Wants=machines.target").
With machines-target added to --ignore-preflight-errors in step 2, preflight exits 0 (checked on the UBI 9.8 image), so this check alone fails preflight.
Suggested fix:
- Handle a missing target (empty
UnitFileState, i.e.LoadState=not-found) the way unbounded handles a missing package:- a warning when bootstrap can install
systemd-container - an error when
agentCfg.OfflineArtifactsConfigured()is set;pkg/cmd/preflight/preflight.goalready hasagentCfgto pass in
- a warning when bootstrap can install
- Keep
maskedandmasked-runtimeas errors, since bootstrap doesn't unmask the target. - Include the actual state in the default message. States like
linked,indirectorgeneratedcurrently get "missing, masked, or unavailable", which doesn't say which one. - Update the docs sentence in
operations.md("fails ifmachines.targetis missing or masked") to match. - Add a test for the offline/online split. Matching unbounded's behavior also makes the planned move to unbounded straightforward.
There was a problem hiding this comment.
Addressed in 85ea2de: empty/not-found UnitFileState warns during native online bootstrap with guidance that bootstrap may install systemd-container, but fails offline. Masked/masked-runtime remain fatal; unexpected-state diagnostics include the actual state. That commit added online/offline tests and updated operations guidance. dc6cf2e additionally tests missing/not-found and both masked states across all four bootstrap modes. Ready for reviewer resolution.
|
Copilot address pr comments |
Co-authored-by: bcho <1975118+bcho@users.noreply.github.com>
Co-authored-by: bcho <1975118+bcho@users.noreply.github.com>
Co-authored-by: bcho <1975118+bcho@users.noreply.github.com>
|
Copilot resolve conflicts |
…target-issue # Conflicts: # CHANGELOG.md Co-authored-by: bcho <1975118+bcho@users.noreply.github.com>
Resolved the changelog conflict by merging |


Summary
RHEL presets leave
machines.targetdisabled, preventing workers from starting after reboot. The daemon still reported success because it checked persisted settings, not worker liveness.Wants=machines.targetto host and container daemon units.Validation
Full test suite, affected-package race tests, vet, documentation checks, build, and CLI smoke test passed. CodeQL and secret scans were clean.
golangci-lintwas unavailable; live RHEL reboot testing remains outstanding.Changelog
UnreleasedinCHANGELOG.md, or this change does not require a changelog entry.