mirror of
https://github.com/allaunthefox/Research-Stack.git
synced 2026-07-31 03:05:21 +00:00
144 lines
4.5 KiB
Python
144 lines
4.5 KiB
Python
#!/usr/bin/env python3
|
|
"""SSH-based remote node probe runner. Connects via Tailscale, executes probe script, returns JSON."""
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
import subprocess
|
|
import time
|
|
from dataclasses import dataclass
|
|
from pathlib import Path
|
|
from typing import Any
|
|
|
|
|
|
PROBE_DIR = Path(__file__).resolve().parent.parent / "nodes"
|
|
|
|
|
|
@dataclass
|
|
class ProbeResult:
|
|
node: str
|
|
ip: str
|
|
optional: bool
|
|
reachable: bool
|
|
error: str | None
|
|
data: dict[str, Any]
|
|
elapsed_ms: float
|
|
|
|
def to_dict(self) -> dict[str, Any]:
|
|
return {
|
|
"node": self.node,
|
|
"ip": self.ip,
|
|
"optional": self.optional,
|
|
"reachable": self.reachable,
|
|
"error": self.error,
|
|
"data": self.data,
|
|
"elapsed_ms": self.elapsed_ms,
|
|
}
|
|
|
|
|
|
def run_probe(
|
|
node_name: str,
|
|
ip: str,
|
|
ssh_target: str,
|
|
probe_name: str,
|
|
optional: bool = False,
|
|
timeout: int = 30,
|
|
) -> ProbeResult:
|
|
"""
|
|
SSH into a node and execute a probe script, returning parsed JSON.
|
|
Uses ssh_target (e.g. 'root@100.102.173.61') directly. 'local' runs locally.
|
|
"""
|
|
script_path = PROBE_DIR / f"{probe_name}.sh"
|
|
if not script_path.exists():
|
|
return ProbeResult(
|
|
node=node_name, ip=ip, optional=optional, reachable=False,
|
|
error=f"probe script not found: {script_path}", data={}, elapsed_ms=0,
|
|
)
|
|
|
|
t0 = time.monotonic()
|
|
script_text = script_path.read_text()
|
|
|
|
if ssh_target == "local":
|
|
try:
|
|
proc = subprocess.run(
|
|
["bash", "-s"],
|
|
input=script_text,
|
|
capture_output=True, text=True, timeout=timeout,
|
|
)
|
|
except subprocess.TimeoutExpired:
|
|
return ProbeResult(
|
|
node=node_name, ip=ip, optional=optional, reachable=False,
|
|
error="local timeout", data={},
|
|
elapsed_ms=(time.monotonic() - t0) * 1000,
|
|
)
|
|
except Exception as exc:
|
|
return ProbeResult(
|
|
node=node_name, ip=ip, optional=optional, reachable=False,
|
|
error=str(exc), data={},
|
|
elapsed_ms=(time.monotonic() - t0) * 1000,
|
|
)
|
|
return _parse_output(node_name, ip, optional, proc, (time.monotonic() - t0) * 1000)
|
|
|
|
try:
|
|
proc = subprocess.run(
|
|
[
|
|
"ssh",
|
|
"-o", "ConnectTimeout=5",
|
|
"-o", "StrictHostKeyChecking=accept-new",
|
|
"-o", "BatchMode=yes",
|
|
ssh_target,
|
|
"bash -s",
|
|
],
|
|
input=script_text,
|
|
capture_output=True, text=True, timeout=timeout,
|
|
)
|
|
except subprocess.TimeoutExpired:
|
|
return ProbeResult(
|
|
node=node_name, ip=ip, optional=optional, reachable=False,
|
|
error="SSH timeout", data={},
|
|
elapsed_ms=(time.monotonic() - t0) * 1000,
|
|
)
|
|
except Exception as exc:
|
|
return ProbeResult(
|
|
node=node_name, ip=ip, optional=optional, reachable=False,
|
|
error=str(exc), data={},
|
|
elapsed_ms=(time.monotonic() - t0) * 1000,
|
|
)
|
|
|
|
return _parse_output(node_name, ip, optional, proc, (time.monotonic() - t0) * 1000)
|
|
|
|
|
|
def _parse_output(node_name, ip, optional, proc, elapsed_ms):
|
|
if proc.returncode != 0:
|
|
return ProbeResult(
|
|
node=node_name, ip=ip, optional=optional, reachable=False,
|
|
error=f"exit {proc.returncode}: {proc.stderr.strip()[:300]}",
|
|
data={}, elapsed_ms=elapsed_ms,
|
|
)
|
|
|
|
raw = proc.stdout.strip()
|
|
if not raw:
|
|
return ProbeResult(
|
|
node=node_name, ip=ip, optional=optional, reachable=True,
|
|
error="probe returned empty output", data={}, elapsed_ms=elapsed_ms,
|
|
)
|
|
|
|
start = raw.find("{")
|
|
end = raw.rfind("}")
|
|
if start == -1 or end == -1:
|
|
return ProbeResult(
|
|
node=node_name, ip=ip, optional=optional, reachable=True,
|
|
error=f"no JSON in output: {raw[:200]}", data={}, elapsed_ms=elapsed_ms,
|
|
)
|
|
|
|
try:
|
|
data = json.loads(raw[start:end + 1])
|
|
except json.JSONDecodeError as exc:
|
|
return ProbeResult(
|
|
node=node_name, ip=ip, optional=optional, reachable=True,
|
|
error=f"JSON parse error: {exc}", data={}, elapsed_ms=elapsed_ms,
|
|
)
|
|
|
|
return ProbeResult(
|
|
node=node_name, ip=ip, optional=optional, reachable=True,
|
|
error=None, data=data, elapsed_ms=elapsed_ms,
|
|
)
|