feat(docker): the Swarm page is laid out by node: role, what its containers use, and the services placed on each (#5303)
#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>
This commit is contained in:
@@ -0,0 +1,71 @@
|
||||
# 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()))
|
||||
Reference in New Issue
Block a user