Initial commit: Metro Warden TUI network operations center

This commit is contained in:
2026-03-22 21:33:40 -04:00
commit 98a17d9b7e
45 changed files with 4215 additions and 0 deletions
+129
View File
@@ -0,0 +1,129 @@
"""
Network Plugin — monitors network interfaces and traffic using psutil.
Publishes to:
network.interfaces — dict of {iface: {status, ip4, ip6, rx_bytes, tx_bytes, ...}}
network.stats — aggregate stats snapshot
"""
from __future__ import annotations
import asyncio
import logging
import socket
from typing import Any, Dict
try:
import psutil
_PSUTIL_AVAILABLE = True
except ImportError:
_PSUTIL_AVAILABLE = False
from plugins.base import BasePlugin
log = logging.getLogger(__name__)
# Polling interval in seconds
DEFAULT_POLL_INTERVAL = 5.0
def _get_interfaces() -> Dict[str, Dict]:
"""Collect interface statistics from psutil."""
if not _PSUTIL_AVAILABLE:
return {}
stats: Dict[str, Dict] = {}
io_counters = psutil.net_io_counters(pernic=True)
if_stats = psutil.net_if_stats()
if_addrs = psutil.net_if_addrs()
for iface, io in io_counters.items():
nic_stat = if_stats.get(iface)
addrs = if_addrs.get(iface, [])
ip4 = ""
ip6 = ""
mac = ""
for addr in addrs:
if addr.family == socket.AF_INET:
ip4 = addr.address
elif addr.family == socket.AF_INET6:
ip6 = addr.address
elif addr.family == psutil.AF_LINK:
mac = addr.address
stats[iface] = {
"status": "UP" if (nic_stat and nic_stat.isup) else "DOWN",
"speed": nic_stat.speed if nic_stat else 0,
"mtu": nic_stat.mtu if nic_stat else 0,
"ip4": ip4,
"ip6": ip6,
"mac": mac,
"rx_bytes": io.bytes_recv,
"tx_bytes": io.bytes_sent,
"rx_packets": io.packets_recv,
"tx_packets": io.packets_sent,
"rx_errors": io.errin,
"tx_errors": io.errout,
"rx_drop": io.dropin,
"tx_drop": io.dropout,
}
return stats
class NetworkPlugin(BasePlugin):
"""Monitors network interfaces and publishes stats to the event bus."""
name = "network"
version = "1.0.0"
description = "Monitors network interfaces and traffic statistics via psutil"
tags = ["network", "monitoring"]
def __init__(self, bus=None, state=None, poll_interval: float = DEFAULT_POLL_INTERVAL) -> None:
super().__init__(bus=bus, state=state)
self._poll_interval = poll_interval
self._task: asyncio.Task | None = None
self._running = False
def on_load(self) -> None:
super().on_load()
if not _PSUTIL_AVAILABLE:
self._log.warning("psutil not available — network monitoring degraded")
self._running = True
# Schedule the polling loop
try:
loop = asyncio.get_running_loop()
self._task = loop.create_task(self._poll_loop())
except RuntimeError:
self._log.debug("no running event loop at load time; task deferred")
def on_unload(self) -> None:
self._running = False
if self._task and not self._task.done():
self._task.cancel()
super().on_unload()
async def _poll_loop(self) -> None:
"""Periodically collect interface data and publish to bus."""
self._log.debug("network poll loop started (interval=%.1fs)", self._poll_interval)
while self._running:
try:
data = await asyncio.to_thread(_get_interfaces)
self.state_set("network.interfaces", data)
await self._bus.publish("network.interfaces", data) if self._bus else None
await self._bus.publish("network.stats", {
"interface_count": len(data),
"active_count": sum(1 for v in data.values() if v["status"] == "UP"),
}) if self._bus else None
except asyncio.CancelledError:
break
except Exception as exc:
self._log.error("poll error: %s", exc)
await asyncio.sleep(self._poll_interval)
self._log.debug("network poll loop stopped")
def on_event(self, topic: str, data: Any) -> None:
"""Handle requests to refresh immediately."""
if topic == "network.refresh":
self._log.debug("manual refresh requested")