274 lines
9.4 KiB
Python
Executable File
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())
|