"""Model-free builder for the swarm-aware container view. Merges two sources into one service-grouped, cluster-complete picture: • docker_containers — real container rows, collected LOCAL-per-node, so each carries the host (= node) it runs on plus its swarm service/node labels. • DockerSwarmService.placement_json — the managers' cluster-wide task→node counts, which include tasks on nodes that run no Steward agent (and so have no container row of their own). For every swarm service we list the real replicas we have detail for, then add "ghost" replicas for the remaining placement count on each node (agent-less nodes). Non-swarm containers pass through grouped by host, unchanged. Kept model-free (no ORM imports) so it's unit-testable and safe to import in tests without the plugin-loader "table already defined" gotcha. """ from __future__ import annotations import json def _service_state(running: int, desired: int) -> str: if desired > 0 and running >= desired: return "healthy" if running > 0: return "degraded" return "down" def _placement(service) -> list[dict]: raw = getattr(service, "placement_json", None) or "[]" try: data = json.loads(raw) except (ValueError, TypeError): return [] return [p for p in data if isinstance(p, dict)] def build_swarm_services(containers, services, node_hostname: dict, host_name: dict) -> dict: """Return {"services": [...], "standalone": [...]}. services[i] = {name, mode, desired, running, state, replicas:[...]} real replica = {ghost:False, host, host_id, name, status, cpu_pct, mem_pct, health, restart_count} ghost replica = {ghost:True, host, count} # placement with no local row standalone[i] = {host_id, host, containers:[...]} # non-swarm, by host """ containers = list(containers) swarm_cs = [c for c in containers if getattr(c, "service_name", None)] standalone_cs = [c for c in containers if not getattr(c, "service_name", None)] # Dedup service rows by name (every manager reports the same cluster-global # set); keep the first — placement is identical across managers. svc_by_name: dict[str, object] = {} for s in services: name = getattr(s, "service_name", None) if name and name not in svc_by_name: svc_by_name[name] = s # Real containers grouped by service name. cs_by_service: dict[str, list] = {} for c in swarm_cs: cs_by_service.setdefault(c.service_name, []).append(c) def _host_of(c): return (host_name.get(getattr(c, "host_id", None)) or node_hostname.get(getattr(c, "node_id", None)) or getattr(c, "host_id", None) or "?") out_services = [] for name in sorted(set(svc_by_name) | set(cs_by_service)): svc = svc_by_name.get(name) reals = cs_by_service.get(name, []) replicas = [] for c in sorted(reals, key=lambda c: (_host_of(c).lower(), c.name)): replicas.append({ "ghost": False, "host": _host_of(c), "host_id": getattr(c, "host_id", None), "name": c.name, "status": getattr(c, "status", "unknown"), "cpu_pct": getattr(c, "cpu_pct", None), "mem_pct": getattr(c, "mem_pct", None), "health": getattr(c, "health", None), "restart_count": getattr(c, "restart_count", 0) or 0, }) # Real running replicas per node, to subtract from placement. real_running_by_node: dict[str, int] = {} for c in reals: if (getattr(c, "status", "") or "").lower() == "running": nid = getattr(c, "node_id", None) if nid: real_running_by_node[nid] = real_running_by_node.get(nid, 0) + 1 ghosts = [] for p in _placement(svc) if svc is not None else []: nid = p.get("node_id", "") placed = int(p.get("running", 0) or 0) ghost = placed - real_running_by_node.get(nid, 0) if ghost > 0: ghosts.append({ "ghost": True, "host": node_hostname.get(nid) or (nid[:12] if nid else "?"), "count": ghost, }) ghosts.sort(key=lambda g: g["host"].lower()) replicas.extend(ghosts) running = getattr(svc, "running", None) if svc is not None else None desired = getattr(svc, "desired", None) if svc is not None else None # No service row (manager didn't report it): infer from what we can see. if running is None: running = sum(1 for r in replicas if (not r["ghost"] and (r["status"] or "").lower() == "running")) if desired is None: desired = len(replicas) out_services.append({ "name": name, "mode": getattr(svc, "mode", "replicated") if svc is not None else "replicated", "running": running, "desired": desired, "state": _service_state(int(running or 0), int(desired or 0)), "replicas": replicas, }) # Non-swarm containers grouped by host, running-first order preserved. groups: dict[str, dict] = {} for c in standalone_cs: hid = getattr(c, "host_id", None) g = groups.get(hid) if g is None: g = groups[hid] = {"host_id": hid, "host": host_name.get(hid) or hid or "?", "containers": []} g["containers"].append(c) standalone = sorted(groups.values(), key=lambda g: g["host"].lower()) return {"services": out_services, "standalone": standalone}