From 599399ff508855af863617759892df1e3c7fcc1e Mon Sep 17 00:00:00 2001 From: perco Date: Tue, 30 Jun 2026 10:50:51 +0200 Subject: [PATCH] =?UTF-8?q?feat:=20card=20trafic=20par=20r=C3=A9seau=20Doc?= =?UTF-8?q?ker=20(bridges)=20avec=20conteneurs=20associ=C3=A9s?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Nouvelle route /api/dockernet : lit le trafic de chaque bridge Docker via psutil, mappe sur les noms de réseaux et conteneurs via l'API Docker socket (/networks + /containers/json). Affiche le débit DL/UL par réseau trié par activité. Monte /var/run/docker.sock en lecture. Co-Authored-By: Claude Sonnet 4.6 --- Dockerfile | 1 + app/main.py | 89 +++++++++++++++++++++++++++++++++++++++++++++- docker-compose.yml | 1 + static/index.html | 47 ++++++++++++++++++++++-- 4 files changed, 135 insertions(+), 3 deletions(-) diff --git a/Dockerfile b/Dockerfile index 0394e16..7a31e10 100644 --- a/Dockerfile +++ b/Dockerfile @@ -5,6 +5,7 @@ RUN apt-get update && apt-get install -y --no-install-recommends \ dmidecode \ lsb-release \ nmap \ + docker.io \ && rm -rf /var/lib/apt/lists/* WORKDIR /app diff --git a/app/main.py b/app/main.py index 08469cb..d9f115f 100644 --- a/app/main.py +++ b/app/main.py @@ -1,4 +1,4 @@ -import subprocess, json, time, re, os +import subprocess, json, time, re, os, http.client, socket as _socket from typing import Optional import psutil @@ -634,4 +634,91 @@ def api_netconn(): return {"hosts": result, "ts": int(now)} +# ─────────────────────────────────────────────────────── DOCKERNET ── + +_dockernet_prev: dict = {} # bridge_id → {"recv": int, "sent": int, "ts": float} + + +class _UnixHTTP(http.client.HTTPConnection): + def __init__(self, sock_path: str): + super().__init__("localhost") + self._sock_path = sock_path + + def connect(self): + s = _socket.socket(_socket.AF_UNIX, _socket.SOCK_STREAM) + s.connect(self._sock_path) + self.sock = s + + +def _docker_api(path: str) -> list | dict: + conn = _UnixHTTP("/var/run/docker.sock") + conn.request("GET", path) + r = conn.getresponse() + return json.loads(r.read()) + + +def _docker_net_map() -> dict: + """Retourne {bridge_short_id: {name, containers}} via API Docker socket.""" + try: + nets = _docker_api("/networks?filters=%7B%22driver%22%3A%7B%22bridge%22%3Atrue%7D%7D") + net_by_id = {n["Id"][:12]: {"name": n["Name"], "containers": []} for n in nets} + + # Remplir les containers via /containers/json + containers = _docker_api("/containers/json") + for c in containers: + name = c.get("Names", [""])[0].lstrip("/") + for net_name, net_cfg in c.get("NetworkSettings", {}).get("Networks", {}).items(): + net_id = net_cfg.get("NetworkID", "")[:12] + if net_id in net_by_id: + net_by_id[net_id]["containers"].append(name) + + return net_by_id + except Exception: + return {} + + +@app.get("/api/dockernet") +def api_dockernet(): + global _dockernet_prev + now = time.time() + + net_map = _docker_net_map() + per_nic = psutil.net_io_counters(pernic=True) + + # Ne garder que les bridges (br-XXXXXXXXXXXX) + bridge_re = re.compile(r"^br-([0-9a-f]{12})$") + result = [] + + dt = now - _dockernet_prev.get("_ts", now - 1) + dt = max(dt, 0.5) + + for iface, stats in per_nic.items(): + m = bridge_re.match(iface) + if not m: + continue + bid = m.group(1) + info = net_map.get(bid, {}) + if not info: + continue + + prev = _dockernet_prev.get(bid, {}) + dl_bps = max(0, (stats.bytes_recv - prev.get("recv", stats.bytes_recv)) / dt) + ul_bps = max(0, (stats.bytes_sent - prev.get("sent", stats.bytes_sent)) / dt) + + _dockernet_prev[bid] = {"recv": stats.bytes_recv, "sent": stats.bytes_sent} + + result.append({ + "bridge": iface, + "name": info["name"], + "containers": info["containers"], + "dl_bps": dl_bps, + "ul_bps": ul_bps, + }) + + _dockernet_prev["_ts"] = now + # Trier par trafic total décroissant + result.sort(key=lambda x: x["dl_bps"] + x["ul_bps"], reverse=True) + return {"networks": result, "ts": int(now)} + + app.mount("/", StaticFiles(directory="/app/static", html=True), name="static") diff --git a/docker-compose.yml b/docker-compose.yml index 66ea913..b2e39b9 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -11,6 +11,7 @@ services: - /dev:/dev - /run/udev:/run/udev:ro - /home/perco/projects/homelabdash/netmap_devices.json:/app/netmap_devices.json + - /var/run/docker.sock:/var/run/docker.sock:ro environment: - HOST_ROOT=/rootfs - TZ=Europe/Paris diff --git a/static/index.html b/static/index.html index ee61567..42b4a74 100644 --- a/static/index.html +++ b/static/index.html @@ -103,6 +103,11 @@ .s-size { font-size: 18px; font-weight: 800; color: var(--accent); } .s-info { font-size: 11px; color: var(--muted); } + /* DOCKERNET */ + .dn-net { font-weight: 700; color: var(--accent); } + .dn-containers { display: flex; flex-wrap: wrap; gap: 4px; margin-top: 2px; } + .dn-container { background: #21262d; border-radius: 4px; padding: 1px 7px; font-size: 11px; color: var(--muted); } + /* NETCONN */ .nc-table td { padding: 6px 8px; font-size: 12px; } .nc-ip { font-family: monospace; color: var(--accent); font-weight: 700; font-size: 13px; } @@ -389,8 +394,13 @@ function build_ui(os, hw, raid) { '
' )); + // DockerNet + grid.appendChild(make_card('card-dockernet','col-12','🐳','Trafic par réseau Docker — live', + '
Chargement…
' + )); + // NetConn - grid.appendChild(make_card('card-netconn','col-12','🔌','Trafic réseau par appareil — live', + grid.appendChild(make_card('card-netconn','col-12','🔌','Trafic réseau par appareil LAN — live', '
Chargement…
' )); @@ -517,6 +527,37 @@ function update_live(live) { setTimeout(() => { dot.style.background = 'var(--green)'; }, 250); } +// ──────────────────────────────── dockernet ── +async function dnRefresh() { + try { + const data = await fetch('/api/dockernet').then(r => r.json()); + dnRender(data.networks || []); + } catch { /* silencieux */ } +} + +function dnRender(nets) { + const el = document.getElementById('dockernet-body'); + if (!el) return; + const active = nets.filter(n => n.dl_bps > 100 || n.ul_bps > 100); + const rest = nets.filter(n => n.dl_bps <= 100 && n.ul_bps <= 100); + const renderRow = n => { + const busy = n.dl_bps > 1024 || n.ul_bps > 1024; + const chips = n.containers.slice(0,6).map(c => + `${c}`).join(''); + return ` + ${n.name} + ${fmt_bps(n.dl_bps)} + ${fmt_bps(n.ul_bps)} +
${chips}
+ `; + }; + const rows = [...active, ...rest].map(renderRow).join(''); + el.innerHTML = ` + + ${rows} +
Réseau Docker⬇ Download⬆ UploadConteneurs
`; +} + // ──────────────────────────────── netconn ── let _ncNames = {}; @@ -649,13 +690,15 @@ async function init() { // Charger les noms netmap pour les labels d'appareils dans netconn fetch('/api/netmap/names').then(r => r.json()).then(d => { _ncNames = d; }); - // Premier chargement netconn + // Premiers chargements + dnRefresh(); ncRefresh(); setInterval(async () => { try { const live = await fetch('/api/live').then(r=>r.json()); update_live(live); + dnRefresh(); ncRefresh(); } catch { document.getElementById('refresh-dot').style.background = 'var(--red)';