fix(maintenance): check first, drain before a Docker upgrade, wait 5 min after a drain, see containers back (#6098)
CI / lint (push) Successful in 2s
CI / unit (push) Failing after 1m29s
CI / integration (push) Successful in 2m7s
CI / publish (push) Skipped

On 2026-10-18 an update on VDNT-Docker02 restarted the Docker daemon six
seconds in, before the drain that came only with the reboot, and every
container on the node stopped. Docker's own packages restart the daemon from
their install scripts; needrestart's list-only mode doesn't stop that.

- The helper gains a `check` action (agent 1.24.0): a dry run of the update
  for each package manager, whether Docker is among it, and the running
  containers outside the swarm with their restart policies.
- A swarm node is drained before an update that upgrades Docker, else only
  before a reboot, and at most once a job.
- After a drain the job settles 5 minutes before the update or reboot.
- Once the work is over, the containers that ran outside the swarm must be
  seen running again within 10 minutes, or the job fails naming them.
- An older agent gets no check and its job runs as before.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-19 10:44:45 -04:00
co-authored by Claude Opus 5.5
parent da81010a75
commit 1ba4784fa6
15 changed files with 671 additions and 67 deletions
+36 -8
View File
@@ -160,14 +160,24 @@ systemd when the agent leaves it a request:
- `steward-maint.path` (`PathExistsGlob=…/requests/*.json`) starts
`steward-maint.service`, a oneshot running `maint.py` as root.
- The helper opens each request with `O_NOFOLLOW`, wants a regular file under
4 KiB, and accepts only `update`, `reboot`, `drain` or `undrain`, an id of
4 KiB, and accepts only `check`, `update`, `reboot`, `drain` or `undrain`, an id of
letters, digits and dashes, and a node id in the swarm's own 25-character
form. It deletes every request, valid or not, and writes the outcome to
`/var/lib/steward-maint/results/<id>.json` (root's; kept a week). A request
whose id already has a result is not run again.
- **check** (1.24.0, #6098): what the update would upgrade, changing nothing
but the package lists: `apt-get update` then `apt-get -s dist-upgrade`;
`dnf`/`yum check-update`; `zypper list-updates` (`dup --dry-run` on
Tumbleweed); `pacman -Sy` then `-Qu`. It says whether Docker is among it
(`docker`, `docker-ce`, `docker.io`, `docker-engine`, `moby-engine`,
`containerd`, `containerd.io`: the packages whose install scripts restart
the daemon), and lists the running containers that aren't swarm tasks,
each with its restart policy (at most 40).
- **update**: `apt-get update` then `dist-upgrade` keeping existing config
files, with `NEEDRESTART_MODE=l` so needrestart only lists services rather
than restarting the Docker daemon under its containers;
than restarting them. A package's own install script still restarts its
service: Docker's does, stopping every container on the host, which is
what the check is for;
`dnf -y upgrade --refresh` (or yum's); `zypper refresh` then
`zypper update` (`zypper dup` on Tumbleweed), counting zypper's 102/103
"reboot / restart needed" exits as success; or `pacman -Syu`
@@ -199,11 +209,27 @@ runs on, loses nothing. `plugins/host_agent/maintenance.py` has the states and
transitions (model-free, unit-tested) and the database side.
```
queued → updating → [draining →] rebooting → [undraining →] done
↘ done (no reboot needed, or the job may never reboot)
queued → [checking →] [draining → settling →] updating
→ [draining → settling →] [rebooting →] [undraining →] [recovering →] done
any state → failed (a step's result said so) | timed_out (its deadline passed)
```
**Docker upgraded, drained first (1.24.0, #6098).** On 2026-10-18 an update
on VDNT-Docker02 restarted the Docker daemon six seconds in, before the drain
that came only with the reboot: every container on the node stopped, and
portal_app was down until it was redeployed by hand. A job for an agent of
1.24.0 or later now starts with a **check**. When the update upgrades Docker,
a swarm node is drained before it; otherwise it is drained only before a
reboot, and a node is drained at most once a job. After a drain, **settling**
gives the node's tasks 5 minutes (`DRAIN_SETTLE`) to move before the update
or reboot goes ahead. The containers the check found running outside the
swarm are kept on the job (`checked`); once the work is over, **recovering**
waits up to 10 minutes (`RECOVER_LIMIT`) for each to be seen running again
in the Docker plugin's stored containers, and fails the job naming any that
aren't, marking those with no restart policy. settling and recovering are
Steward's own waits, moved on by the minute tick. An older agent gets no
check, and its job runs as before.
- **Hand-off.** Steward's answer to an ingest post carries
`"maintenance": [{"id", "action", ["node"]}]`, the step that host's agent
should carry out now. It is repeated on every post until a result for that
@@ -218,13 +244,15 @@ any state → failed (a step's result said so) | timed_out (its deadline passed)
restart on a host that stayed up has an old `boot_at` and changes nothing.
- **Reboot only when required**: the update's result says whether a reboot is
needed; a job made with "never reboot" stops after the update.
- **Swarm nodes** are drained first and made active after. The node is found
- **Swarm nodes** are drained first and made active after (see above for
when). The node is found
by hostname in the Docker plugin's swarm nodes; a manager drains itself, a
worker is drained by a manager whose agent has the helper. With no such
manager the job is refused rather than rebooting an undrained node.
- **Deadlines** (`DEADLINES`): queued 1 h, updating 90 min, draining 10 min,
rebooting 30 min, undraining 10 min. A minute tick (`host_agent_maintenance`)
times out a job past its deadline.
- **Deadlines** (`DEADLINES`): queued 1 h, checking 30 min, draining 10 min,
settling 15 min, updating 90 min, rebooting 30 min, undraining 10 min,
recovering 15 min. A minute tick (`host_agent_maintenance`) times out a job
past its deadline.
- **A maintenance window** scoped to the host's agent metrics
(`host_agent/<host>`) opens when the job starts, lasts as long as the rest of
the job could take, and ends when it does.