feat(mcp): swarm nodes link to their host and maintenance; the overview names a node that is down (#5835)
#5835 "MCP: swarm nodes", step 2 of milestone 520. list_swarm_services already listed every node (role, leader, availability, status); the audit missed it. What was missing: - Each node row carries `host_id` (the Steward host of that name) and `maintenance` (the state of a maintenance job running on it), so an agent can go node -> get_host / get_maintenance_job. - get_overview names a node whose status isn't ready, unless its host has a maintenance job running (rebooting on purpose). - Drained alone is not a finding: VDNT-Util01 is a manager kept drained on purpose (#5777), and a drain a failed job left behind is already that job's maintenance_failed finding. unique_nodes() keeps the newest report of each node, as the list did. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -196,3 +196,30 @@ def test_services_that_failed_together_are_one_finding_and_recovered_ones_none()
|
||||
assert "auth (1 of 2 replicas running), wiki (0 of 1 replicas running)" in together["summary"]
|
||||
assert together["since"] == "2026-10-06T11:30:00Z"
|
||||
assert alone["subject"] == "later" and alone["kind"] == "swarm_service_degraded"
|
||||
|
||||
|
||||
def _node(node_id, hostname, status="ready", availability="active", role="worker", minutes=0):
|
||||
return NS(node_id=node_id, hostname=hostname, status=status, availability=availability,
|
||||
role=role, leader=False, host_id="mgr",
|
||||
updated_at=datetime(2026, 10, 15, 12, 0, tzinfo=timezone.utc) - timedelta(minutes=minutes))
|
||||
|
||||
|
||||
def test_unique_nodes_keep_the_newest_report_of_each():
|
||||
old, new = _node("n1", "a", status="down", minutes=5), _node("n1", "a")
|
||||
assert t.unique_nodes([old, new, _node("n2", "b")]) == [new, _node("n2", "b")]
|
||||
|
||||
|
||||
def test_node_findings_name_a_down_node_unless_maintenance_is_rebooting_it():
|
||||
nodes = [_node("n1", "Web-1", status="down", role="manager"),
|
||||
_node("n2", "web-2", availability="drain"), # kept drained on purpose (#5777)
|
||||
_node("n3", "web-3", status="unknown", availability="pause"),
|
||||
_node("n4", "web-4", availability="drain", status="down"), # being rebooted
|
||||
_node("n5", "web-5")]
|
||||
out = {f["subject"]: f for f in t.node_findings(nodes, {"web-4": "rebooting"})}
|
||||
assert set(out) == {"Web-1", "web-3"}
|
||||
assert out["Web-1"]["severity"] == "critical" and out["Web-1"]["kind"] == "swarm_node_down"
|
||||
assert out["Web-1"]["summary"] == "swarm manager Web-1 is down"
|
||||
assert out["web-3"]["summary"] == "swarm worker web-3 is unknown and paused"
|
||||
# Once its job is over, the same node drained and down is named.
|
||||
[f] = t.node_findings([_node("n4", "web-4", availability="drain", status="down")], {})
|
||||
assert f["summary"] == "swarm worker web-4 is down and drained"
|
||||
|
||||
Reference in New Issue
Block a user