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>
48 lines
2.4 KiB
Python
48 lines
2.4 KiB
Python
"""The host_agent plugin's MCP tools."""
|
|
from datetime import datetime, timedelta, timezone
|
|
from types import SimpleNamespace as NS
|
|
from unittest.mock import MagicMock
|
|
|
|
import plugins.host_agent as agent_plugin
|
|
from plugins.host_agent.mcp_tools import agent_state
|
|
|
|
NOW = datetime(2026, 10, 6, 12, 0, tzinfo=timezone.utc)
|
|
|
|
|
|
def test_agent_state():
|
|
assert agent_state(NS(last_seen_at=None), NOW, 180) == "pending"
|
|
assert agent_state(NS(last_seen_at=NOW - timedelta(seconds=60)), NOW, 180) == "reporting"
|
|
assert agent_state(NS(last_seen_at=NOW - timedelta(minutes=10)), NOW, 180) == "stale"
|
|
|
|
|
|
def test_plugin_exposes_the_mcp_hooks():
|
|
reg = MagicMock()
|
|
agent_plugin.register_mcp_tools(reg)
|
|
assert [c.args[0].__name__ for c in reg.call_args_list] == [
|
|
"list_agents", "list_maintenance_plans", "get_maintenance_job", "run_maintenance"]
|
|
roles = {c.args[0].__name__: c.kwargs["role"] for c in reg.call_args_list}
|
|
assert roles["run_maintenance"].value == "admin" # as Update now on the page
|
|
assert roles["get_maintenance_job"].value == "viewer"
|
|
assert callable(agent_plugin.mcp_overview)
|
|
|
|
|
|
def test_a_job_row_says_what_it_did_and_when_it_gives_up():
|
|
from plugins.host_agent.mcp_tools import _job_row
|
|
job = NS(id="j1", host_id="h1", state="rebooting", state_at=NOW - timedelta(minutes=5),
|
|
reboot_policy="if_required", packages=12, reboot_needed=True, back_at=None,
|
|
node_id="n1", drain_host_id="h2", detail=None, created_by=None, run_id="r1",
|
|
created_at=NOW - timedelta(minutes=20), finished_at=None,
|
|
drained_at=NOW - timedelta(minutes=11), docker_update=False,
|
|
checked={"pending": 12, "docker_packages": [], "containers": []})
|
|
row = _job_row(job, {"h1": "web-1", "h2": "mgr-1"}, NOW)
|
|
assert (row["host"], row["drained_by"], row["label"], row["active"]) == (
|
|
"web-1", "mgr-1", "Rebooting", True)
|
|
assert row["started_by"] == "maintenance plan" and row["state_age_s"] == 300
|
|
assert row["gives_up_at"] == (NOW + timedelta(minutes=25)).isoformat().replace("+00:00", "Z")
|
|
|
|
done = NS(**{**job.__dict__, "state": "done", "back_at": NOW, "finished_at": NOW,
|
|
"created_by": "u1", "run_id": None})
|
|
row = _job_row(done, {"h1": "web-1", "u1": "alice"}, NOW)
|
|
assert row["summary"] == "Done: 12 packages updated, rebooted"
|
|
assert row["started_by"] == "alice" and row["gives_up_at"] is None and not row["active"]
|