feat(host_agent): Steward hands a host a maintenance job and follows it through the reboot (#5767)
#5767 "Steward hands a host a maintenance job and follows it to the end, through its own restarts", milestone 514. A host_maintenance_jobs row per job: queued → updating → [draining →] rebooting → [undraining →] done, or failed / timed_out. Steward's answer to an ingest post carries the step that host's agent should carry out now; the agent (1.18.0) leaves each step id for the root helper once, and the helper refuses an id it already ran, so repeats are harmless. Results come back as the #5766 maintenance events, matched by step id; the reboot completes when the host's `started` event has a boot_at after the reboot was handed out. The job reboots only when the update says one is needed (or never, if asked). A swarm node is drained first by itself if it's a manager or by a manager with the helper, and made active after; with no manager to drain it the job is refused. A minute tick times out a job past its state's deadline. A maintenance window over host_agent/<host> lasts the rest of the job. Everything is on the row, so a Steward restart, or the reboot of the host Steward runs on, resumes from the next post. Ingest runs it in its own savepoint so a job going wrong never costs the samples. The Agent panel leads with a running job and says what a finished one did; Manage agent has Update now (with "never reboot"), or says why a job can't start. Integration test: hand off, Steward restarts, the post-boot events close the job. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,46 @@
|
||||
"""host_agent: a host's maintenance job, followed through its reboot (#5767)
|
||||
|
||||
Revision ID: host_agent_007_maint_jobs
|
||||
"""
|
||||
from typing import Sequence, Union
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
revision: str = "host_agent_007_maint_jobs"
|
||||
down_revision: Union[str, None] = "host_agent_006_maint_helper"
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
op.create_table(
|
||||
"host_maintenance_jobs",
|
||||
sa.Column("id", sa.String(36), primary_key=True),
|
||||
sa.Column("host_id", sa.String(36), sa.ForeignKey("hosts.id", ondelete="CASCADE"),
|
||||
nullable=False),
|
||||
sa.Column("state", sa.String(16), nullable=False),
|
||||
sa.Column("state_at", sa.DateTime(timezone=True), nullable=True),
|
||||
sa.Column("reboot_policy", sa.String(16), nullable=False),
|
||||
sa.Column("step_id", sa.String(64), nullable=True),
|
||||
sa.Column("step_seen", sa.Boolean(), nullable=False, server_default=sa.false()),
|
||||
sa.Column("node_id", sa.String(64), nullable=True),
|
||||
sa.Column("drain_host_id", sa.String(36), sa.ForeignKey("hosts.id", ondelete="SET NULL"),
|
||||
nullable=True),
|
||||
sa.Column("packages", sa.Integer(), nullable=True),
|
||||
sa.Column("reboot_needed", sa.Boolean(), nullable=True),
|
||||
sa.Column("back_at", sa.DateTime(timezone=True), nullable=True),
|
||||
sa.Column("detail", sa.Text(), nullable=True),
|
||||
sa.Column("window_id", sa.String(36),
|
||||
sa.ForeignKey("maintenance_windows.id", ondelete="SET NULL"), nullable=True),
|
||||
sa.Column("created_by", sa.String(36), sa.ForeignKey("users.id", ondelete="SET NULL"),
|
||||
nullable=True),
|
||||
sa.Column("created_at", sa.DateTime(timezone=True), nullable=False),
|
||||
sa.Column("finished_at", sa.DateTime(timezone=True), nullable=True),
|
||||
)
|
||||
op.create_index("ix_host_maintenance_jobs_host_id", "host_maintenance_jobs", ["host_id"])
|
||||
op.create_index("ix_host_maintenance_jobs_state", "host_maintenance_jobs", ["state"])
|
||||
op.create_index("ix_host_maintenance_jobs_step_id", "host_maintenance_jobs", ["step_id"])
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
op.drop_table("host_maintenance_jobs")
|
||||
Reference in New Issue
Block a user