feat: card trafic par réseau Docker (bridges) avec conteneurs associés
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 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 4.6
parent
2c3ab22565
commit
599399ff50
@@ -5,6 +5,7 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
|
|||||||
dmidecode \
|
dmidecode \
|
||||||
lsb-release \
|
lsb-release \
|
||||||
nmap \
|
nmap \
|
||||||
|
docker.io \
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|||||||
+88
-1
@@ -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
|
from typing import Optional
|
||||||
|
|
||||||
import psutil
|
import psutil
|
||||||
@@ -634,4 +634,91 @@ def api_netconn():
|
|||||||
return {"hosts": result, "ts": int(now)}
|
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")
|
app.mount("/", StaticFiles(directory="/app/static", html=True), name="static")
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ services:
|
|||||||
- /dev:/dev
|
- /dev:/dev
|
||||||
- /run/udev:/run/udev:ro
|
- /run/udev:/run/udev:ro
|
||||||
- /home/perco/projects/homelabdash/netmap_devices.json:/app/netmap_devices.json
|
- /home/perco/projects/homelabdash/netmap_devices.json:/app/netmap_devices.json
|
||||||
|
- /var/run/docker.sock:/var/run/docker.sock:ro
|
||||||
environment:
|
environment:
|
||||||
- HOST_ROOT=/rootfs
|
- HOST_ROOT=/rootfs
|
||||||
- TZ=Europe/Paris
|
- TZ=Europe/Paris
|
||||||
|
|||||||
+45
-2
@@ -103,6 +103,11 @@
|
|||||||
.s-size { font-size: 18px; font-weight: 800; color: var(--accent); }
|
.s-size { font-size: 18px; font-weight: 800; color: var(--accent); }
|
||||||
.s-info { font-size: 11px; color: var(--muted); }
|
.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 */
|
/* NETCONN */
|
||||||
.nc-table td { padding: 6px 8px; font-size: 12px; }
|
.nc-table td { padding: 6px 8px; font-size: 12px; }
|
||||||
.nc-ip { font-family: monospace; color: var(--accent); font-weight: 700; font-size: 13px; }
|
.nc-ip { font-family: monospace; color: var(--accent); font-weight: 700; font-size: 13px; }
|
||||||
@@ -389,8 +394,13 @@ function build_ui(os, hw, raid) {
|
|||||||
'<div id="temp-table"></div>'
|
'<div id="temp-table"></div>'
|
||||||
));
|
));
|
||||||
|
|
||||||
|
// DockerNet
|
||||||
|
grid.appendChild(make_card('card-dockernet','col-12','🐳','Trafic par réseau Docker — live',
|
||||||
|
'<div id="dockernet-body"><span style="color:var(--muted)">Chargement…</span></div>'
|
||||||
|
));
|
||||||
|
|
||||||
// NetConn
|
// 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',
|
||||||
'<div id="netconn-body"><span style="color:var(--muted)">Chargement…</span></div>'
|
'<div id="netconn-body"><span style="color:var(--muted)">Chargement…</span></div>'
|
||||||
));
|
));
|
||||||
|
|
||||||
@@ -517,6 +527,37 @@ function update_live(live) {
|
|||||||
setTimeout(() => { dot.style.background = 'var(--green)'; }, 250);
|
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 =>
|
||||||
|
`<span class="dn-container">${c}</span>`).join('');
|
||||||
|
return `<tr${busy ? ' style="background:#1a1e2a"' : ''}>
|
||||||
|
<td class="dn-net">${n.name}</td>
|
||||||
|
<td class="nc-bps nc-dl">${fmt_bps(n.dl_bps)}</td>
|
||||||
|
<td class="nc-bps nc-ul">${fmt_bps(n.ul_bps)}</td>
|
||||||
|
<td><div class="dn-containers">${chips}</div></td>
|
||||||
|
</tr>`;
|
||||||
|
};
|
||||||
|
const rows = [...active, ...rest].map(renderRow).join('');
|
||||||
|
el.innerHTML = `<table class="nc-table">
|
||||||
|
<thead><tr><th>Réseau Docker</th><th>⬇ Download</th><th>⬆ Upload</th><th>Conteneurs</th></tr></thead>
|
||||||
|
<tbody>${rows}</tbody>
|
||||||
|
</table>`;
|
||||||
|
}
|
||||||
|
|
||||||
// ──────────────────────────────── netconn ──
|
// ──────────────────────────────── netconn ──
|
||||||
let _ncNames = {};
|
let _ncNames = {};
|
||||||
|
|
||||||
@@ -649,13 +690,15 @@ async function init() {
|
|||||||
// Charger les noms netmap pour les labels d'appareils dans netconn
|
// Charger les noms netmap pour les labels d'appareils dans netconn
|
||||||
fetch('/api/netmap/names').then(r => r.json()).then(d => { _ncNames = d; });
|
fetch('/api/netmap/names').then(r => r.json()).then(d => { _ncNames = d; });
|
||||||
|
|
||||||
// Premier chargement netconn
|
// Premiers chargements
|
||||||
|
dnRefresh();
|
||||||
ncRefresh();
|
ncRefresh();
|
||||||
|
|
||||||
setInterval(async () => {
|
setInterval(async () => {
|
||||||
try {
|
try {
|
||||||
const live = await fetch('/api/live').then(r=>r.json());
|
const live = await fetch('/api/live').then(r=>r.json());
|
||||||
update_live(live);
|
update_live(live);
|
||||||
|
dnRefresh();
|
||||||
ncRefresh();
|
ncRefresh();
|
||||||
} catch {
|
} catch {
|
||||||
document.getElementById('refresh-dot').style.background = 'var(--red)';
|
document.getElementById('refresh-dot').style.background = 'var(--red)';
|
||||||
|
|||||||
Reference in New Issue
Block a user