Интегрирован wesp в сайт
CI / quality (push) Canceled after 0s

This commit is contained in:
влад
2026-07-17 12:57:18 +03:00
parent 5dfa06ddbe
commit 355c0ef9f1
883 changed files with 194576 additions and 177 deletions
@@ -0,0 +1,462 @@
"""Сбор метрик хоста для админ-дашборда (опционально через psutil)."""
from __future__ import annotations
import os
import re
import socket
import subprocess
import sys
import time
from typing import Any, Dict, List, Optional
_NET_STATE: Optional[Dict[str, Any]] = None
_ARPHRD_LOOPBACK = 772
def _ip_addresses_mode() -> str:
try:
from flask import current_app, has_app_context
if has_app_context():
raw = current_app.config.get("WESP_ADMIN_IP_ADDRESSES", "physical")
return str(raw).strip().lower()
except Exception:
pass
return os.getenv("WESP_ADMIN_IP_ADDRESSES", "physical").strip().lower()
def _interface_name_likely_virtual(name: str) -> bool:
"""Эвристика виртуальных/туннельных интерфейсов (docker, utun, tap, …)."""
n = name.lower()
if n == "lo" or re.fullmatch(r"lo\d+", n):
return True
if re.fullmatch(r"br\d+", n) or "bridge" in n:
return True
prefixes = (
"docker",
"br-",
"veth",
"virbr",
"vmnet",
"vboxnet",
"utun",
"awdl",
"dummy",
"tun",
"tap",
"wg",
"ifb",
"erspan",
"gre",
"sit",
"ip6tnl",
"ppp",
"gretap",
"mesh",
"vethernet",
)
return any(n == p or n.startswith(p) for p in prefixes)
def network_interface_is_physical(name: str) -> bool:
"""
Узкий фильтр «физический носитель»: не loopback и не типичный виртуальный интерфейс.
Linux: sysfs type/device/wireless; VLAN — по базовому имени (eth0.10 → eth0).
macOS: в основном en*; Windows: всё кроме loopback и явных виртуальных имён.
"""
if _interface_name_likely_virtual(name):
return False
if sys.platform.startswith("linux"):
type_path = f"/sys/class/net/{name}/type"
try:
with open(type_path, encoding="utf-8") as f:
if int(f.read().strip()) == _ARPHRD_LOOPBACK:
return False
except (OSError, ValueError):
pass
dev = f"/sys/class/net/{name}/device"
wl = f"/sys/class/net/{name}/wireless"
if os.path.exists(dev) or os.path.isdir(wl):
return True
if "." in name:
base = name.split(".", 1)[0]
if base != name and not _interface_name_likely_virtual(base):
return network_interface_is_physical(base)
return bool(
re.match(r"^(eth|en|wlan|wl|wwp|usb|enx|bond|team)\d?", name, re.I),
)
if sys.platform == "darwin":
return bool(re.match(r"^en\d", name))
nl = name.lower()
if "loopback" in nl:
return False
return True
def _human_uptime(seconds: int) -> str:
seconds = max(0, int(seconds))
d, r = divmod(seconds, 86400)
h, r = divmod(r, 3600)
m, s = divmod(r, 60)
parts: List[str] = []
if d:
parts.append(f"{d}д")
if h:
parts.append(f"{h}ч")
if m:
parts.append(f"{m}м")
if not parts and s:
parts.append(f"{s}с")
if not parts:
parts.append("")
return " ".join(parts)
def _network_addresses() -> List[Dict[str, str]]:
try:
import psutil
except ImportError:
return []
mode = _ip_addresses_mode()
physical_only = mode not in ("all", "any", "*", "1", "true", "yes")
rows: List[Dict[str, str]] = []
try:
for iface, snics in psutil.net_if_addrs().items():
if physical_only and not network_interface_is_physical(iface):
continue
for snic in snics:
if snic.family == socket.AF_INET:
fam = "IPv4"
elif snic.family == socket.AF_INET6:
fam = "IPv6"
else:
continue
addr = snic.address.split("%")[0]
rows.append(
{
"interface": iface,
"family": fam,
"address": addr,
}
)
except OSError:
pass
rows.sort(key=lambda x: (x["interface"], x["family"], x["address"]))
return rows
def _connection_counts() -> Optional[Dict[str, int]]:
try:
import psutil
except ImportError:
return None
try:
conns = psutil.net_connections(kind="inet")
except (psutil.AccessDenied, PermissionError, OSError):
return None
tcp = 0
udp = 0
for c in conns:
if c.type == socket.SOCK_STREAM:
tcp += 1
elif c.type == socket.SOCK_DGRAM:
udp += 1
return {"tcp": tcp, "udp": udp}
def _run_text(args: List[str], timeout: float) -> str:
try:
r = subprocess.run(
args,
capture_output=True,
text=True,
timeout=timeout,
)
if r.returncode != 0:
return ""
return (r.stdout or "").strip()
except (OSError, subprocess.TimeoutExpired, FileNotFoundError):
return ""
def _format_os_line() -> str:
import platform
system = platform.system()
release = platform.release()
machine = platform.machine() or ""
if system == "Darwin":
v = _run_text(["sw_vers", "-productVersion"], 2.0)
if v:
release = v
parts = [f"{system} {release}"]
if machine:
parts.append(machine)
return " · ".join(parts)
def _detect_cpu_model() -> str:
import platform
if sys.platform.startswith("linux"):
try:
with open("/proc/cpuinfo", encoding="utf-8", errors="replace") as f:
for line in f:
ln = line.strip()
if ln.startswith("model name") or ln.startswith("Model name") or ln.startswith("cpu model"):
if ":" in ln:
return ln.split(":", 1)[1].strip()
except OSError:
pass
proc = platform.processor()
return proc if proc else ""
if sys.platform == "darwin":
v = _run_text(["sysctl", "-n", "machdep.cpu.brand_string"], 2.0)
if v:
return v
v = _run_text(["sysctl", "-n", "hw.model"], 2.0)
if v:
return v
proc = platform.processor()
if proc:
return proc
return platform.machine() or ""
def _detect_gpu_label() -> str:
if sys.platform == "darwin":
try:
r = subprocess.run(
["system_profiler", "SPDisplaysDataType"],
capture_output=True,
text=True,
timeout=6.0,
)
if r.returncode == 0 and r.stdout:
for line in r.stdout.splitlines():
s = line.strip()
if s.startswith("Chipset Model:"):
return s.split(":", 1)[1].strip()
except (OSError, subprocess.TimeoutExpired, FileNotFoundError):
pass
return "н/д"
if sys.platform.startswith("linux"):
try:
r = subprocess.run(
["lspci"],
capture_output=True,
text=True,
timeout=3.0,
)
if r.returncode == 0 and r.stdout:
for line in r.stdout.splitlines():
low = line.lower()
if (
"vga compatible controller" in low
or "3d controller" in low
or "display controller" in low
):
parts = line.split(":", 2)
if len(parts) >= 3:
return parts[-1].strip()[:160]
except (OSError, subprocess.TimeoutExpired, FileNotFoundError):
pass
return "н/д"
def _linux_root_block_base() -> Optional[str]:
try:
with open("/proc/mounts", encoding="utf-8") as f:
for line in f:
parts = line.split()
if len(parts) < 2 or parts[1] != "/":
continue
src = parts[0]
if not src.startswith("/dev/"):
return None
name = src.rsplit("/", 1)[-1]
m = re.match(r"(nvme\d+n\d+)p\d+$", name)
if m:
return m.group(1)
m = re.match(r"(nvme\d+n\d+)$", name)
if m:
return m.group(1)
base = re.sub(r"\d+$", "", name)
return base or None
except OSError:
return None
return None
def _detect_root_disk_media() -> str:
if sys.platform == "darwin":
try:
r = subprocess.run(
["diskutil", "info", "/"],
capture_output=True,
text=True,
timeout=4.0,
)
if r.returncode == 0 and r.stdout:
for line in r.stdout.splitlines():
if "Solid State:" in line:
rest = line.split(":", 1)[-1].strip().lower()
if rest.startswith("y"):
return "SSD"
if rest.startswith("n"):
return "HDD"
except (OSError, subprocess.TimeoutExpired, FileNotFoundError):
pass
return "н/д"
if sys.platform.startswith("linux"):
base = _linux_root_block_base()
if not base:
return "н/д"
rot_path = f"/sys/block/{base}/queue/rotational"
try:
with open(rot_path, encoding="utf-8") as f:
v = f.read().strip()
if v == "0":
return "SSD / NVMe"
if v == "1":
return "HDD"
except OSError:
pass
return "н/д"
def collect_host_hardware_overview() -> Dict[str, Any]:
"""
ОС и железо для вкладки «Система».
GPU и тип носителя — по возможности; иначе «н/д».
"""
overview: Dict[str, Any] = {
"os": _format_os_line(),
"cpu_model": _detect_cpu_model(),
"cpu_logical_cores": None,
"gpu": _detect_gpu_label(),
"ram_total_bytes": None,
"root_disk_media": _detect_root_disk_media(),
"root_disk_used_bytes": None,
"root_disk_total_bytes": None,
"root_disk_percent": None,
}
try:
import psutil
except ImportError:
return overview
n = int(psutil.cpu_count(logical=True) or 0)
overview["cpu_logical_cores"] = n if n > 0 else None
overview["ram_total_bytes"] = int(psutil.virtual_memory().total)
try:
du = psutil.disk_usage("/")
overview["root_disk_used_bytes"] = int(du.used)
overview["root_disk_total_bytes"] = int(du.total)
overview["root_disk_percent"] = round(float(du.percent), 1)
except OSError:
pass
return overview
def collect_system_metrics() -> Dict[str, Any]:
try:
import psutil
except ImportError:
return {
"available": False,
"message": "Установите пакет psutil (pip install psutil) для метрик CPU/RAM/диска.",
}
now = time.time()
boot_t = float(psutil.boot_time())
host_uptime_sec = int(max(0.0, now - boot_t))
proc_uptime_sec = None
try:
proc = psutil.Process()
proc_uptime_sec = int(max(0.0, now - float(proc.create_time())))
except (psutil.Error, OSError, TypeError):
pass
cpu_percent = float(psutil.cpu_percent(interval=None))
vm = psutil.virtual_memory()
sw = psutil.swap_memory()
disk_block: Dict[str, Any] | None
try:
du = psutil.disk_usage("/")
disk_block = {
"path": "/",
"used": int(du.used),
"total": int(du.total),
"percent": round(du.percent, 2),
}
except OSError:
disk_block = None
load_avg = None
try:
load_avg = [round(x, 2) for x in os.getloadavg()]
except (OSError, AttributeError):
pass
global _NET_STATE
net = psutil.net_io_counters()
upload_bps = 0.0
download_bps = 0.0
if _NET_STATE is not None and now > float(_NET_STATE["t"]):
dt = now - float(_NET_STATE["t"])
if dt > 0:
upload_bps = max(0.0, (int(net.bytes_sent) - int(_NET_STATE["sent"])) / dt)
download_bps = max(0.0, (int(net.bytes_recv) - int(_NET_STATE["recv"])) / dt)
_NET_STATE = {"t": now, "sent": int(net.bytes_sent), "recv": int(net.bytes_recv)}
conn_counts = _connection_counts()
addrs = _network_addresses()
return {
"available": True,
"timestamp": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime(int(now))),
"host": {
"boot_time_unix": int(boot_t),
"uptime_seconds": host_uptime_sec,
"uptime_human": _human_uptime(host_uptime_sec),
},
"process": {
"pid": os.getpid(),
"uptime_seconds": proc_uptime_sec,
"uptime_human": _human_uptime(proc_uptime_sec) if proc_uptime_sec is not None else None,
},
"cpu": {
"percent": round(cpu_percent, 2),
"cores": int(psutil.cpu_count(logical=True) or 1),
},
"memory": {
"used": int(vm.used),
"total": int(vm.total),
"percent": round(vm.percent, 2),
},
"swap": {
"used": int(sw.used),
"total": int(sw.total),
"percent": round(sw.percent, 2) if sw.total else 0.0,
},
"disk": disk_block,
"network": {
"upload_bps": round(upload_bps, 2),
"download_bps": round(download_bps, 2),
"bytes_sent_total": int(net.bytes_sent),
"bytes_recv_total": int(net.bytes_recv),
},
"load_avg": load_avg,
"connections": conn_counts,
"addresses": addrs,
}