FiWiManager/tests/check_remote.py

274 lines
9.4 KiB
Python
Executable File

#!/usr/bin/env python3
"""
Probe configured remote hub hosts and list **USB power-control hubs** (per-port switching and
monitoring) on each via remote ``fiwi.py discover`` — serial, model, hub index / port count.
Uses the generic ``discover`` view, not the ``show_hostcards`` concentrator label (same scan).
Does **not** load BrainStem locally. Default: one SSH **readiness** probe (repo ``cd``, ``fiwi.py``,
``env/``, ``import brainstem`` via ``FIWI_REMOTE_PYTHON``), then remote ``discover``. Use
``--probe-only`` to skip discovery; ``--invoke`` to run another Fi-Wi subcommand instead.
``--list-only`` prints resolved config and hub list (no SSH).
**Standalone**::
python tests/check_remote.py -c uax24
python tests/check_remote.py -c uax24 --probe-only
python tests/check_remote.py -c uax24 --list-only
python tests/check_remote.py -c uax24 --invoke show_hostcards
``--config`` matches ``FIWI_CONFIG`` (profile name or absolute ``*.ini`` path).
"""
from __future__ import annotations
import argparse
import os
import shlex
import sys
_ROOT = os.path.abspath(os.path.join(os.path.dirname(os.path.abspath(__file__)), ".."))
if _ROOT not in sys.path:
sys.path.insert(0, _ROOT)
def _configure_paths() -> None:
import fiwi.paths as paths_mod
paths_mod.configure(_ROOT)
def _print_config_summary() -> None:
from fiwi.ssh import SshNodeConfig
cfg = SshNodeConfig.load()
raw = (cfg.calibrate_remotes or "").strip()
print("Resolved remote SSH (after paths + INI)", flush=True)
print("-" * 60, flush=True)
print(f" FIWI_REMOTE_PYTHON → {cfg.python}", flush=True)
print(f" FIWI_REMOTE_SCRIPT → {cfg.script}", flush=True)
print(f" FIWI_SSH_BIN {cfg.ssh_bin}", flush=True)
if cfg.ssh_extra_argv:
print(f" FIWI_SSH_OPTS {' '.join(cfg.ssh_extra_argv)}", flush=True)
print(f" merged hub hosts {raw or '(none)'}", flush=True)
print("-" * 60, flush=True)
print(flush=True)
def _print_ssh_client_hints(stderr: str) -> None:
err = stderr or ""
if "Bad owner or permissions" in err and "ssh_config" in err:
print(
" Hint: OpenSSH refused to read a config snippet (ownership/permissions). "
"On Fedora: ``sudo chmod 644 /etc/ssh/ssh_config.d/*.conf`` (or remove the bad include).",
flush=True,
)
def _print_readiness(_host: str, r) -> None:
from fiwi.fiwi_relay.host import RemoteReadiness
assert isinstance(r, RemoteReadiness)
print("== remote readiness ==", flush=True)
if r.timed_out:
print(" SSH: timed out", flush=True)
return
if not r.ssh_connected:
print(f" SSH: failed (exit {r.ssh_exit_code})", flush=True)
if r.raw_stderr.strip():
print(r.raw_stderr.rstrip(), file=sys.stderr, flush=True)
_print_ssh_client_hints(r.raw_stderr)
return
hn = (r.hostname or "").strip() or "?"
un = (r.uname or "").strip() or "?"
print(f" hostname: {hn}", flush=True)
print(f" uname: {un}", flush=True)
print(f" repo cd: {'ok' if r.cd_ok else 'FAIL'}", flush=True)
print(f" fiwi.py: {'present' if r.script_present else 'MISSING'}", flush=True)
print(f" env/: {'yes' if r.venv_present else 'no'}", flush=True)
pyx = (r.remote_python_executable or "").strip()
if pyx:
print(f" {pyx}", flush=True)
bs = "ok" if r.brainstem_import_ok else "FAIL (ModuleNotFound or error)"
print(f" brainstem (FIWI_REMOTE_PYTHON): {bs}", flush=True)
bvenv = "ok" if r.brainstem_ok_in_venv else ("n/a" if not r.venv_present else "FAIL")
print(f" brainstem (env/bin/python3): {bvenv}", flush=True)
if r.suggests_remote_python_venv:
from fiwi.ssh import SshNodeConfig
from fiwi.fiwi_relay.host import suggested_remote_venv_python
hint = suggested_remote_venv_python(SshNodeConfig.load().script)
print(
f" → Set remote_python in [remote_ssh] to {hint!r} "
f"(deps are in env/, not system python3).",
flush=True,
)
elif r.suggests_fiwi_relay_setup:
from fiwi.fiwi_relay.host import ssh_target_points_here
cfg = (os.environ.get("FIWI_CONFIG") or "").strip() or "default"
cmd = f"{shlex.quote(sys.executable)} -m fiwi.fiwi_relay -c {shlex.quote(cfg)}"
where = "this checkout" if ssh_target_points_here(_host) else "the hub over SSH"
print(f" → Run: {cmd} (installs deps on {where})", flush=True)
if r.raw_stderr.strip() and not r.suggests_fiwi_relay_setup and not r.suggests_remote_python_venv:
print(r.raw_stderr.rstrip(), file=sys.stderr, flush=True)
def _probe_host(target: str, *, timeout: float) -> tuple[int, object]:
"""SSH readiness probe; returns (exit_code, RemoteReadiness)."""
from fiwi.fiwi_relay.host import probe_remote_hub_readiness
r = probe_remote_hub_readiness(target, timeout=timeout)
_print_readiness(target, r)
if r.timed_out:
return 124, r
if not r.ssh_connected:
return r.ssh_exit_code if r.ssh_exit_code >= 0 else 255, r
return 0, r
def _merged_hosts() -> list[str]:
from fiwi.ssh import SshNodeConfig
raw = (SshNodeConfig.load().calibrate_remotes or "").strip()
seen: list[str] = []
for part in raw.split(","):
h = part.strip()
if h and h not in seen:
seen.append(h)
return seen
def main() -> int:
p = argparse.ArgumentParser(
description="SSH probe for remote USB power-control hub hosts from config.",
formatter_class=argparse.RawDescriptionHelpFormatter,
)
p.add_argument(
"-c",
"--config",
metavar="PROFILE_OR_INI",
help="INI profile or path (sets FIWI_CONFIG for this run)",
)
p.add_argument(
"--timeout",
type=float,
default=45.0,
metavar="SEC",
help="SSH timeout for the light probe (default: 45)",
)
p.add_argument(
"--discover-timeout",
"--hostcards-timeout",
type=float,
default=90.0,
dest="discover_timeout",
metavar="SEC",
help="Timeout for remote discover (default: 90; only after successful probe)",
)
p.add_argument(
"--list-only",
action="store_true",
help="Print resolved SSH config and hub list only (no SSH)",
)
p.add_argument(
"--probe-only",
action="store_true",
help="Only run SSH readiness probe; do not run remote discover",
)
p.add_argument(
"--invoke",
nargs=argparse.REMAINDER,
metavar="ARGS",
help="Run only this Fi-Wi subcommand on each host (skips probe and default discover)",
)
args = p.parse_args()
if args.config:
os.environ["FIWI_CONFIG"] = args.config.strip()
try:
os.chdir(_ROOT)
except OSError as exc:
print(f"FAIL: cannot chdir to repo root {_ROOT!r}: {exc}", file=sys.stderr)
return 1
_configure_paths()
_print_config_summary()
hosts = _merged_hosts()
if not hosts:
print(
"No remote hosts configured. Set [remote_hubs] hosts or FIWI_REMOTE_HUBS / "
"FIWI_CALIBRATE_REMOTES.",
flush=True,
)
return 2
if args.list_only:
print(f"Hubs ({len(hosts)}): {', '.join(hosts)}", flush=True)
label = os.environ.get("FIWI_CONFIG", "default")
print(f"\nDone [config: {label}]")
return 0
invoke_args = args.invoke
if invoke_args is not None and len(invoke_args) >= 1 and invoke_args[0] == "--":
invoke_args = invoke_args[1:]
from fiwi.ssh import SshNode
rc = 0
for host in hosts:
print(f"\n>>> {host}\n", flush=True)
if invoke_args:
try:
node = SshNode.parse(host)
code, out, err = node.invoke_capture(list(invoke_args), timeout=args.timeout, defer=False)
if out.strip():
print(out.rstrip(), flush=True)
if err.strip():
print(err.rstrip(), file=sys.stderr, flush=True)
if code != 0:
print(f" (fiwi invoke exit {code})", flush=True)
rc = 1
except Exception as exc:
print(f" invoke_capture failed: {exc}", flush=True)
rc = 1
continue
prc, readiness = _probe_host(host, timeout=args.timeout)
if prc != 0:
rc = 1
continue
if readiness.suggests_fiwi_relay_setup or readiness.suggests_remote_python_venv:
rc = 1
if args.probe_only:
continue
print("\n== remote USB power-control hubs (fiwi discover) ==\n", flush=True)
try:
node = SshNode.parse(host)
code, out, err = node.invoke_capture(
["discover"],
timeout=args.discover_timeout,
defer=False,
)
if out.strip():
print(out.rstrip(), flush=True)
if err.strip():
print(err.rstrip(), file=sys.stderr, flush=True)
if code != 0:
print(f" (discover exit {code})", flush=True)
rc = 1
except Exception as exc:
print(f" discover failed: {exc}", flush=True)
rc = 1
label = os.environ.get("FIWI_CONFIG", "default")
print(f"\nDone [config: {label}]")
return rc
if __name__ == "__main__":
raise SystemExit(main())