#5303 "Swarm page laid out by node: role, resources and the tasks placed on each" A verdict line first, then the nodes that are down or drained and the services short of replicas, then one card per node. A node with a Steward agent shows what its swarm tasks use against the machine's cores and memory, service by service. A node without one lists Swarm's placement, by count. The services table moved to the Docker overview, where it can be filtered. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
72 lines
3.6 KiB
Python
72 lines
3.6 KiB
Python
# plugins/docker/swarm_nodes.py
|
|
"""The Swarm page laid out by node (#5303): one card per node with its role,
|
|
whether it takes work, what its containers use against what the machine has,
|
|
and the services Swarm placed on it. Model-free, so it is unit-tested without
|
|
a database; routes.swarm() feeds it.
|
|
|
|
A node with a Steward agent reports its own containers, so its services carry
|
|
CPU and memory. A node without one is known only from the managers' placement:
|
|
its services are listed by count, with no figures.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
from .swarm_view import _placement
|
|
|
|
|
|
def node_ok(n) -> bool:
|
|
"""A node that is up and taking work."""
|
|
return n.status == "ready" and n.availability == "active"
|
|
|
|
|
|
def node_cards(nodes, services, containers, *, node_host: dict, host_names: dict,
|
|
capacity: dict, service_state: dict) -> list[dict]:
|
|
"""One card per node, a node in trouble first, then managers, then by name.
|
|
|
|
`nodes` and `services` are one swarm's deduplicated rows; `containers`
|
|
every container row (only running swarm tasks on these nodes count);
|
|
`node_host` maps a node id to the Steward host on it, `capacity` a host id
|
|
to (cores, memory bytes), `service_state` a service name to healthy,
|
|
degraded or down."""
|
|
running_on: dict[str, dict[str, dict]] = {}
|
|
for c in containers:
|
|
if not (c.node_id and c.service_name and c.status == "running"):
|
|
continue
|
|
row = running_on.setdefault(c.node_id, {}).setdefault(
|
|
c.service_name, {"name": c.service_name, "tasks": 0, "cpu_pct": 0.0, "mem_bytes": 0,
|
|
"measured": True})
|
|
row["tasks"] += 1
|
|
row["cpu_pct"] += c.cpu_pct or 0.0
|
|
row["mem_bytes"] += c.mem_usage_bytes or 0
|
|
placed: dict[str, dict[str, int]] = {}
|
|
for s in services:
|
|
for p in _placement(s):
|
|
count = int(p.get("running") or 0)
|
|
if count > 0 and p.get("node_id"):
|
|
placed.setdefault(p["node_id"], {})[s.service_name] = count
|
|
|
|
cards = []
|
|
for n in nodes:
|
|
rows = dict(running_on.get(n.node_id, {}))
|
|
for name, count in placed.get(n.node_id, {}).items():
|
|
rows.setdefault(name, {"name": name, "tasks": count, "cpu_pct": None, "mem_bytes": None,
|
|
"measured": False})
|
|
for r in rows.values():
|
|
r["state"] = service_state.get(r["name"], "healthy")
|
|
r["cores"] = r["cpu_pct"] / 100 if r["cpu_pct"] is not None else None
|
|
tasks = sorted(rows.values(), key=lambda r: (r["state"] == "healthy", -(r["cores"] or 0.0),
|
|
r["name"].lower()))
|
|
host_id = node_host.get(n.node_id)
|
|
cores, mem = capacity.get(host_id, (None, None)) if host_id else (None, None)
|
|
measured = [r for r in tasks if r["measured"]]
|
|
cards.append({
|
|
"node_id": n.node_id, "hostname": n.hostname or n.node_id[:12], "role": n.role,
|
|
"leader": n.leader, "status": n.status, "availability": n.availability, "ok": node_ok(n),
|
|
"host_id": host_id, "host_name": host_names.get(host_id) if host_id else None,
|
|
"agent": bool(running_on.get(n.node_id)) or host_id is not None,
|
|
"tasks": tasks, "task_count": sum(r["tasks"] for r in tasks),
|
|
"cores_used": sum(r["cores"] for r in measured) if measured else None,
|
|
"mem_used": sum(r["mem_bytes"] for r in measured) if measured else None,
|
|
"cores": cores, "mem": mem,
|
|
})
|
|
return sorted(cards, key=lambda c: (c["ok"], c["role"] != "manager", c["hostname"].lower()))
|