From 667880f82a8919ca0bd7dd358c95d886658b8719 Mon Sep 17 00:00:00 2001 From: Robert McMahon Date: Thu, 23 Apr 2026 19:19:45 -0700 Subject: [PATCH] Fix RRH power scripts to handle serial-connected hubs and relay hosts. This updates keep/all-off control to toggle local and remote Acroname ports correctly, explicitly powers the keep target back on, and aligns mapping helper toggles with BrainStem connect-by-serial behavior. Made-with: Cursor --- scripts/system/map_fiber_ports.py | 38 +++++- scripts/system/rrh_power_control.py | 192 ++++++++++++++++++++++++++-- 2 files changed, 213 insertions(+), 17 deletions(-) diff --git a/scripts/system/map_fiber_ports.py b/scripts/system/map_fiber_ports.py index dbab56c..2f7fcfd 100644 --- a/scripts/system/map_fiber_ports.py +++ b/scripts/system/map_fiber_ports.py @@ -20,7 +20,6 @@ from pathlib import Path from fiwicontrol.fabric.fabric import FabricDefinition, FabricRRHBinding from fiwicontrol.lab.discovery import discover_acroname_modules -from fiwicontrol.power.acroname import AcronamePower _REPO_ROOT = Path(__file__).resolve().parents[2] @@ -68,7 +67,7 @@ async def _all_off(rrhs: list[FabricRRHBinding]) -> None: mod = by_serial.get(serial) if mod is None: raise RuntimeError("Acroname module serial {} not found on local USB".format(serial)) - await AcronamePower(mod).port_off(port) + await asyncio.to_thread(_set_port_enabled_by_serial_sync, serial=serial, port=port, enabled=False) done.add(k) print("OFF: module={} port={}".format(serial, port)) @@ -80,7 +79,6 @@ async def _step_map(rrhs: list[FabricRRHBinding], dwell: float) -> None: mod = by_serial.get(serial) if mod is None: raise RuntimeError("Acroname module serial {} not found on local USB".format(serial)) - ap = AcronamePower(mod) print( "\n[{}/{}] radio_id={} module={} port={} patch_panel_port={}".format( @@ -93,15 +91,45 @@ async def _step_map(rrhs: list[FabricRRHBinding], dwell: float) -> None: ) ) input("Press Enter to power ON this port...") - await ap.port_on(port) + await asyncio.to_thread(_set_port_enabled_by_serial_sync, serial=serial, port=port, enabled=True) print("ON: module={} port={}".format(serial, port)) input("Observe fiber/link, then press Enter to power OFF this port...") - await ap.port_off(port) + await asyncio.to_thread(_set_port_enabled_by_serial_sync, serial=serial, port=port, enabled=False) print("OFF: module={} port={}".format(serial, port)) if dwell > 0: await asyncio.sleep(dwell) +def _set_port_enabled_by_serial_sync(*, serial: int, port: int, enabled: bool) -> None: + import brainstem.discover as discover + from brainstem import stem + from brainstem.result import Result + + specs = discover.findAllModules(discover.Spec.USB, buffer_length=128) + spec = next((s for s in specs if int(s.serial_number) == int(serial)), None) + if spec is None: + raise RuntimeError("module serial {} not found on local USB".format(serial)) + model_map = {17: stem.USBHub2x4, 19: stem.USBHub3p, 24: stem.USBHub3c} + cls = model_map.get(int(spec.model)) + if cls is None: + raise RuntimeError("unsupported Acroname model {} for serial {}".format(spec.model, serial)) + hub = cls() + err = int(hub.connectFromSpec(spec)) + if err != Result.NO_ERROR: + raise RuntimeError("connectFromSpec failed for serial {}: {}".format(serial, err)) + try: + err = int(hub.hub.port[int(port)].setEnabled(bool(enabled))) + if err != Result.NO_ERROR: + raise RuntimeError( + "setEnabled failed for serial {} port {} enabled {}: {}".format(serial, port, enabled, err) + ) + finally: + try: + hub.disconnect() + except Exception: + pass + + async def _run(fabric_json: str, dwell: float, skip_all_off: bool) -> None: path = _resolve_json_path(fabric_json) fd = FabricDefinition.load(path) diff --git a/scripts/system/rrh_power_control.py b/scripts/system/rrh_power_control.py index 218f355..6ed89fc 100644 --- a/scripts/system/rrh_power_control.py +++ b/scripts/system/rrh_power_control.py @@ -15,9 +15,14 @@ import argparse import asyncio from pathlib import Path +from fiwicontrol.commands.node_control import ssh_node from fiwicontrol.fabric.fabric import FabricDefinition -from fiwicontrol.lab.discovery import discover_acroname_modules -from fiwicontrol.power.acroname import AcronamePower +from fiwicontrol.lab.discovery import ( + discover_acroname_modules, + discover_devices_remote_async, + parse_remote_discovery_payload, +) +from fiwicontrol.lab.inventory_config import default_lab_ini_path, load_inventory_ini _REPO_ROOT = Path(__file__).resolve().parents[2] @@ -25,6 +30,7 @@ _REPO_ROOT = Path(__file__).resolve().parents[2] async def _power_only_one( *, fabric_json: str, + lab_ini: str | None, keep_radio_id: str | None, all_off: bool, dry_run: bool, @@ -53,7 +59,29 @@ async def _power_only_one( raise SystemExit("radio_id {!r} not found in {}".format(keep_radio_id, json_path)) modules = discover_acroname_modules() - by_serial = {int(m.serial_number): m for m in modules} + local_serials = {int(m.serial_number) for m in modules} + remote_hosts: dict[int, tuple[str, str, str]] = {} + ini = ( + Path(lab_ini).expanduser().resolve() + if (lab_ini and str(lab_ini).strip()) + else default_lab_ini_path().expanduser().resolve() + ) + if ini.is_file(): + doc = load_inventory_ini(ini) + for host in doc.hosts: + if host.mode != "relay" or not host.ipaddr: + continue + node = ssh_node( + name=host.name, + ipaddr=host.ipaddr, + ssh_controlmaster=True, + sshtype=host.sshtype, + silent_mode=True, + ) + payload = await discover_devices_remote_async(node) + mods, _ = parse_remote_discovery_payload(payload) + for m in mods: + remote_hosts[int(m.serial_number)] = (host.name, host.ipaddr, host.sshtype) keep_key: tuple[int | None, int] | None = None if all_off: @@ -76,17 +104,150 @@ async def _power_only_one( "{}: missing acroname_module_serial in {}".format(h.radio_id, json_path) ) serial = int(h.acroname_module_serial) - mod = by_serial.get(serial) - if mod is None: - raise RuntimeError( - "{}: module serial {} not found on local USB".format(h.radio_id, serial) - ) if dry_run: - print("DRY-RUN OFF: radio_id={} module={} port={}".format(h.radio_id, serial, h.acroname_port)) + if serial in local_serials: + print("DRY-RUN OFF: radio_id={} module={} port={}".format(h.radio_id, serial, h.acroname_port)) + elif serial in remote_hosts: + _name, ip, _sshtype = remote_hosts[serial] + print( + "DRY-RUN OFF(remote): radio_id={} host={} module={} port={}".format( + h.radio_id, ip, serial, h.acroname_port + ) + ) + else: + print( + "SKIP: radio_id={} module={} port={} (serial not found locally or in relay hosts)".format( + h.radio_id, serial, h.acroname_port + ) + ) continue - ap = AcronamePower(mod) - await ap.port_off(h.acroname_port) - print("OFF: radio_id={} module={} port={}".format(h.radio_id, serial, h.acroname_port)) + if serial in local_serials: + await asyncio.to_thread( + _set_port_enabled_by_serial_sync, + serial=serial, + port=int(h.acroname_port), + enabled=False, + ) + print("OFF: radio_id={} module={} port={}".format(h.radio_id, serial, h.acroname_port)) + elif serial in remote_hosts: + name, ip, sshtype = remote_hosts[serial] + await _set_port_enabled_remote( + host_name=name, + ipaddr=ip, + sshtype=sshtype, + serial=serial, + port=int(h.acroname_port), + enabled=False, + ) + print("OFF(remote): radio_id={} host={} module={} port={}".format(h.radio_id, ip, serial, h.acroname_port)) + else: + print( + "SKIP: radio_id={} module={} port={} (serial not found locally or in relay hosts)".format( + h.radio_id, serial, h.acroname_port + ) + ) + if keep_key is not None: + keep = rrhs[keep_radio_id] + if keep.acroname_module_serial is None: + raise RuntimeError("{}: missing acroname_module_serial".format(keep.radio_id)) + serial = int(keep.acroname_module_serial) + port = int(keep.acroname_port) + if dry_run: + if serial in local_serials: + print("DRY-RUN ON: radio_id={} module={} port={}".format(keep.radio_id, serial, port)) + elif serial in remote_hosts: + _name, ip, _sshtype = remote_hosts[serial] + print("DRY-RUN ON(remote): radio_id={} host={} module={} port={}".format(keep.radio_id, ip, serial, port)) + else: + if serial in local_serials: + await asyncio.to_thread( + _set_port_enabled_by_serial_sync, + serial=serial, + port=port, + enabled=True, + ) + print("ON: radio_id={} module={} port={}".format(keep.radio_id, serial, port)) + elif serial in remote_hosts: + name, ip, sshtype = remote_hosts[serial] + await _set_port_enabled_remote( + host_name=name, + ipaddr=ip, + sshtype=sshtype, + serial=serial, + port=port, + enabled=True, + ) + print("ON(remote): radio_id={} host={} module={} port={}".format(keep.radio_id, ip, serial, port)) + + +def _set_port_enabled_by_serial_sync(*, serial: int, port: int, enabled: bool) -> None: + import brainstem.discover as discover + from brainstem import stem + from brainstem.result import Result + + specs = discover.findAllModules(discover.Spec.USB, buffer_length=128) + spec = next((s for s in specs if int(s.serial_number) == int(serial)), None) + if spec is None: + raise RuntimeError("module serial {} not found on local USB".format(serial)) + model_map = {17: stem.USBHub2x4, 19: stem.USBHub3p, 24: stem.USBHub3c} + cls = model_map.get(int(spec.model)) + if cls is None: + raise RuntimeError("unsupported Acroname model {} for serial {}".format(spec.model, serial)) + hub = cls() + err = int(hub.connectFromSpec(spec)) + if err != Result.NO_ERROR: + raise RuntimeError("connectFromSpec failed for serial {}: {}".format(serial, err)) + try: + err = int(hub.hub.port[int(port)].setEnabled(bool(enabled))) + if err != Result.NO_ERROR: + raise RuntimeError( + "setEnabled failed for serial {} port {} enabled {}: {}".format(serial, port, enabled, err) + ) + finally: + try: + hub.disconnect() + except Exception: + pass + + +async def _set_port_enabled_remote( + *, + host_name: str, + ipaddr: str, + sshtype: str, + serial: int, + port: int, + enabled: bool, +) -> None: + node = ssh_node( + name=host_name, + ipaddr=ipaddr, + ssh_controlmaster=True, + sshtype=sshtype, + silent_mode=True, + ) + py_bool = "True" if enabled else "False" + cmd = ( + "python3 -c 'import brainstem.discover as d; " + "from brainstem import stem; " + "from brainstem.result import Result; " + "serial={serial}; port={port}; enabled={enabled}; " + "specs=d.findAllModules(d.Spec.USB, buffer_length=128); " + "spec=next((s for s in specs if int(s.serial_number)==serial), None); " + "assert spec is not None, f\"serial {{serial}} not found\"; " + "m={{17: stem.USBHub2x4, 19: stem.USBHub3p, 24: stem.USBHub3c}}.get(int(spec.model)); " + "assert m is not None, f\"unsupported model {{spec.model}}\"; " + "h=m(); " + "err=int(h.connectFromSpec(spec)); " + "assert err==Result.NO_ERROR, f\"connectFromSpec err={{err}}\"; " + "err=int(h.hub.port[port].setEnabled(enabled)); " + "h.disconnect(); " + "assert err==Result.NO_ERROR, f\"setEnabled err={{err}}\"'" + ).format(serial=int(serial), port=int(port), enabled=py_bool) + session = await node.rexec(cmd=cmd, IO_TIMEOUT=30.0, CMD_TIMEOUT=90, CONNECT_TIMEOUT=30.0) + out = session.results.decode("utf-8", errors="replace") + if "Traceback" in out or "AssertionError" in out: + raise RuntimeError("remote setEnabled failed on {}: {}".format(ipaddr, out.strip())) def main() -> int: @@ -97,6 +258,12 @@ def main() -> int: default="configs/my-fabric.json", help="Path to FabricDefinition JSON (default: configs/my-fabric.json)", ) + p.add_argument( + "-c", + "--lab-ini", + default=None, + help="Lab INI path for relay host discovery (default: configs/default.ini)", + ) p.add_argument( "--keep-radio-id", default="7915", @@ -116,6 +283,7 @@ def main() -> int: asyncio.run( _power_only_one( fabric_json=args.fabric_json, + lab_ini=args.lab_ini, keep_radio_id=args.keep_radio_id if not args.all_off else None, all_off=args.all_off, dry_run=args.dry_run,