Files
Nexus/server/infrastructure/ssh/remote_probe.py
T
Nexus Agent 32a1885f0d feat(watch): 监测槽开关、8h/24h、到期暂停与探针修复
支持随时暂停/恢复实时监测并保留槽位;TTL 到期自动暂停不占删槽;修复到期竞态导致空槽与唯一约束冲突;探针 parse_error 与中文错误;移除失败 snackbar。
2026-06-12 03:47:56 +08:00

277 lines
9.0 KiB
Python

"""SSH-based remote health / metrics probe (no Agent HTTP port required)."""
import json
from typing import Any
from server.domain.models import Server
from server.infrastructure.ssh.asyncssh_pool import exec_ssh_command
from server.utils.watch_probe_errors import watch_probe_error_zh
# Emits one JSON line: {"status":"healthy","system_info":{...}}
SSH_HEALTH_METRICS_CMD = r"""python3 -c "
import json, platform
from datetime import datetime, timezone
info = {
'hostname': platform.node(),
'platform': platform.platform(),
'agent_time': datetime.now(timezone.utc).isoformat(),
'probe': 'ssh',
}
try:
import psutil
info['cpu_usage'] = psutil.cpu_percent(interval=0.3)
info['mem_usage'] = psutil.virtual_memory().percent
info['disk_usage'] = round(psutil.disk_usage('/').percent, 1)
except Exception as exc:
info['metrics_error'] = str(exc)[:200]
print(json.dumps({'status': 'healthy', 'system_info': info}))
" 2>/dev/null || python3.12 -c "
import json, platform
from datetime import datetime, timezone
info = {
'hostname': platform.node(),
'platform': platform.platform(),
'agent_time': datetime.now(timezone.utc).isoformat(),
'probe': 'ssh',
}
try:
import psutil
info['cpu_usage'] = psutil.cpu_percent(interval=0.3)
info['mem_usage'] = psutil.virtual_memory().percent
info['disk_usage'] = round(psutil.disk_usage('/').percent, 1)
except Exception as exc:
info['metrics_error'] = str(exc)[:200]
print(json.dumps({'status': 'healthy', 'system_info': info}))
" 2>/dev/null || echo '{"status":"healthy","system_info":{"hostname":"'"$(hostname)"'","probe":"ssh","ssh_only":true}}'
"""
SSH_WATCH_METRICS_CMD = r"""python3 -c "
import json
try:
import psutil
except ImportError:
print(json.dumps({'status':'error','error':'psutil missing'}))
raise SystemExit(0)
try:
load = list(psutil.getloadavg())
except (AttributeError, OSError, NotImplementedError):
load = [0.0, 0.0, 0.0]
try:
net = psutil.net_io_counters()
except Exception:
net = None
try:
disk_io = psutil.disk_io_counters()
except Exception:
disk_io = None
try:
cpu = psutil.cpu_percent(interval=0.3)
mem = psutil.virtual_memory().percent
disk = round(psutil.disk_usage('/').percent, 1)
except Exception as exc:
print(json.dumps({'status':'error','error': str(exc)[:200]}))
raise SystemExit(0)
out = {
'status': 'ok',
'cpu_usage': cpu,
'mem_usage': mem,
'disk_usage': disk,
'disk_mount': '/',
'load_avg': load,
'net_io': {
'bytes_sent': int(net.bytes_sent) if net else 0,
'bytes_recv': int(net.bytes_recv) if net else 0,
},
'disk_io': {
'read_bytes': int(disk_io.read_bytes) if disk_io else 0,
'write_bytes': int(disk_io.write_bytes) if disk_io else 0,
},
}
print(json.dumps(out))
" 2>/dev/null || python3.12 -c "
import json
try:
import psutil
except ImportError:
print(json.dumps({'status':'error','error':'psutil missing'}))
raise SystemExit(0)
try:
load = list(psutil.getloadavg())
except (AttributeError, OSError, NotImplementedError):
load = [0.0, 0.0, 0.0]
try:
net = psutil.net_io_counters()
except Exception:
net = None
try:
disk_io = psutil.disk_io_counters()
except Exception:
disk_io = None
try:
cpu = psutil.cpu_percent(interval=0.3)
mem = psutil.virtual_memory().percent
disk = round(psutil.disk_usage('/').percent, 1)
except Exception as exc:
print(json.dumps({'status':'error','error': str(exc)[:200]}))
raise SystemExit(0)
out = {
'status': 'ok',
'cpu_usage': cpu,
'mem_usage': mem,
'disk_usage': disk,
'disk_mount': '/',
'load_avg': load,
'net_io': {
'bytes_sent': int(net.bytes_sent) if net else 0,
'bytes_recv': int(net.bytes_recv) if net else 0,
},
'disk_io': {
'read_bytes': int(disk_io.read_bytes) if disk_io else 0,
'write_bytes': int(disk_io.write_bytes) if disk_io else 0,
},
}
print(json.dumps(out))
" 2>/dev/null || echo '{"status":"error","error":"ssh_probe_failed"}'
"""
SSH_WATCH_PROCESSES_CMD = r"""python3 -c "
import json
try:
import psutil
except ImportError:
print(json.dumps({'status':'error','error':'psutil missing'}))
raise SystemExit(0)
procs = []
for p in psutil.process_iter(['pid','name','cpu_percent','memory_percent']):
try:
i = p.info
procs.append({
'pid': i.get('pid'),
'name': (i.get('name') or '')[:80],
'cpu_pct': round(float(i.get('cpu_percent') or 0), 1),
'mem_pct': round(float(i.get('memory_percent') or 0), 1),
})
except Exception:
pass
procs.sort(key=lambda x: x.get('cpu_pct') or 0, reverse=True)
print(json.dumps({'status':'ok','top_processes': procs[:5]}))
" 2>/dev/null || echo '{"status":"error","error":"ssh_probe_failed"}'
"""
def _parse_json_stdout(stdout: str) -> dict[str, Any] | None:
text = (stdout or "").strip()
if not text:
return None
candidates: list[str] = []
for line in reversed(text.splitlines()):
line = line.strip()
if not line:
continue
start = line.find("{")
if start >= 0:
candidates.append(line[start:])
elif line.startswith("{"):
candidates.append(line)
if text.startswith("{"):
candidates.append(text)
seen: set[str] = set()
for chunk in candidates:
if chunk in seen:
continue
seen.add(chunk)
try:
data = json.loads(chunk)
except json.JSONDecodeError:
continue
if isinstance(data, dict) and data.get("status"):
return data
return None
async def ssh_health_probe(server: Server, timeout: int = 15) -> dict[str, Any]:
"""Run metrics probe over SSH; returns {status, system_info?, error?, channel}."""
result = await exec_ssh_command(server, SSH_HEALTH_METRICS_CMD, timeout=timeout)
if result.get("exit_code", -1) != 0:
return {
"status": "offline",
"error": (result.get("stderr") or result.get("stdout") or "SSH probe failed")[:300],
"channel": "ssh",
}
parsed = _parse_json_stdout(result.get("stdout", ""))
if not parsed:
return {
"status": "online",
"system_info": {"probe": "ssh", "raw": result.get("stdout", "")[:500]},
"channel": "ssh",
}
return {
"status": "online" if parsed.get("status") == "healthy" else "offline",
"system_info": parsed,
"channel": "ssh",
}
async def ssh_watch_probe(server: Server, timeout: int = 8) -> dict[str, Any]:
"""Full watch metrics over SSH (CPU/mem/disk/net/disk-io)."""
import time
started = time.monotonic()
result = await exec_ssh_command(server, SSH_WATCH_METRICS_CMD, timeout=timeout)
duration_ms = int((time.monotonic() - started) * 1000)
if result.get("exit_code", -1) != 0:
err_raw = (result.get("stderr") or result.get("stdout") or "SSH watch probe failed")[:300]
if "timed out" in err_raw.lower() or duration_ms >= timeout * 1000 - 50:
return {
"ok": False,
"probe_status": "ssh_timeout",
"error": watch_probe_error_zh(err_raw, status="ssh_timeout"),
"duration_ms": duration_ms,
}
if "auth" in err_raw.lower() or "permission denied" in err_raw.lower():
return {
"ok": False,
"probe_status": "ssh_auth",
"error": watch_probe_error_zh(err_raw, status="ssh_auth"),
"duration_ms": duration_ms,
}
return {
"ok": False,
"probe_status": "offline",
"error": watch_probe_error_zh(err_raw, status="offline"),
"duration_ms": duration_ms,
}
parsed = _parse_json_stdout(result.get("stdout", ""))
if not parsed or parsed.get("status") != "ok":
raw = (result.get("stdout") or "").strip().replace("\n", " ")[:200]
err_key = (parsed or {}).get("error") or "parse_error"
err_combined = f"parse_error: {raw}" if err_key == "parse_error" and raw else str(err_key)
return {
"ok": False,
"probe_status": "parse_error",
"error": watch_probe_error_zh(err_combined, status="parse_error"),
"duration_ms": duration_ms,
}
parsed["duration_ms"] = duration_ms
return {"ok": True, "payload": parsed, "duration_ms": duration_ms}
async def ssh_watch_processes(server: Server, timeout: int = 8) -> list[dict[str, Any]] | None:
"""Fetch top-5 processes over SSH (10s cadence during watch)."""
result = await exec_ssh_command(server, SSH_WATCH_PROCESSES_CMD, timeout=timeout)
if result.get("exit_code", -1) != 0:
return None
parsed = _parse_json_stdout(result.get("stdout", ""))
if not parsed or parsed.get("status") != "ok":
return None
from server.utils.watch_metrics import sanitize_processes
return sanitize_processes(parsed.get("top_processes"))
def _parse_health_stdout(stdout: str) -> dict[str, Any] | None:
return _parse_json_stdout(stdout)