# 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()))