feat(host_agent): a group's updates and reboots on a weekly cadence, one host at a time (#5768)
#5768 "A maintenance cadence: a group's updates and reboots, one host at a time, on a schedule". A plan names an inventory group, a weekday and time in a time zone, a window and a reboot policy (only when required by default). At its slot a run takes the group's hosts in order and gives each a #5767 maintenance job. The next host starts only when the last is back in service: its agent is posting and, for a swarm node, the node is active and every replicated service is at its replicas. A failure stops the run, and no host is started once the window has passed. A host's latest failure shows as host_agent/<host>/maintenance_failed, for an ordinary alert rule. The page is Hosts → Maintenance. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -86,8 +86,19 @@ def handoff(job) -> dict | None:
|
||||
return out
|
||||
|
||||
|
||||
REBOOT_POLICIES = ("if_required", "always", "never")
|
||||
|
||||
|
||||
def wants_reboot(job) -> bool:
|
||||
"""Reboot only when the update says one is needed (the operator's
|
||||
default), always (a plan may ask), or never."""
|
||||
if job.reboot_policy == "always":
|
||||
return True
|
||||
return bool(job.reboot_needed) and job.reboot_policy == "if_required"
|
||||
|
||||
|
||||
def _after_update(job, now: datetime) -> None:
|
||||
if not (job.reboot_needed and job.reboot_policy != "never"):
|
||||
if not wants_reboot(job):
|
||||
_enter(job, "done", now)
|
||||
elif job.node_id and job.drain_host_id:
|
||||
_enter(job, "draining", now)
|
||||
@@ -296,7 +307,7 @@ async def plan_job(session, host, reg, reboot_policy: str = "if_required") -> tu
|
||||
from .models import HostAgentRegistration, HostMaintenanceJob
|
||||
if reg is None or not reg.maint_helper:
|
||||
raise CannotStart("its agent has no maintenance helper: run Update agent first")
|
||||
if reboot_policy not in ("if_required", "never"):
|
||||
if reboot_policy not in REBOOT_POLICIES:
|
||||
raise CannotStart(f"unknown reboot policy {reboot_policy!r}")
|
||||
busy = (await session.execute(select(HostMaintenanceJob.id).where(
|
||||
HostMaintenanceJob.host_id == host.id,
|
||||
@@ -312,21 +323,23 @@ async def plan_job(session, host, reg, reboot_policy: str = "if_required") -> tu
|
||||
|
||||
|
||||
async def create_job(session, host, reg, user_id: str | None,
|
||||
reboot_policy: str = "if_required", now: datetime | None = None):
|
||||
reboot_policy: str = "if_required", now: datetime | None = None,
|
||||
run_id: str | None = None):
|
||||
"""Queue a maintenance job for `host`; it starts with the agent's next post."""
|
||||
from .models import HostMaintenanceJob
|
||||
now = now or utcnow()
|
||||
node_id, drain_host_id = await plan_job(session, host, reg, reboot_policy)
|
||||
job = HostMaintenanceJob(host_id=host.id, state="queued", state_at=now,
|
||||
reboot_policy=reboot_policy, node_id=node_id,
|
||||
drain_host_id=drain_host_id, created_by=user_id, created_at=now)
|
||||
drain_host_id=drain_host_id, created_by=user_id, created_at=now,
|
||||
run_id=run_id)
|
||||
session.add(job)
|
||||
await session.flush()
|
||||
return job
|
||||
|
||||
|
||||
async def latest_job(session, host_id: str, now: datetime, days: int = 7):
|
||||
"""The host's running job, else its last one in `days`, for the panel."""
|
||||
async def latest_job(session, host_id: str, now: datetime, days: int = 366):
|
||||
"""The host's running job, else its last one (within a year), for the panel."""
|
||||
from sqlalchemy import select
|
||||
from .models import HostMaintenanceJob
|
||||
return (await session.execute(select(HostMaintenanceJob).where(
|
||||
|
||||
Reference in New Issue
Block a user