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
This commit is contained in:
Robert McMahon 2026-04-23 19:19:45 -07:00
parent 2034da7185
commit 667880f82a
2 changed files with 213 additions and 17 deletions

View File

@ -20,7 +20,6 @@ from pathlib import Path
from fiwicontrol.fabric.fabric import FabricDefinition, FabricRRHBinding from fiwicontrol.fabric.fabric import FabricDefinition, FabricRRHBinding
from fiwicontrol.lab.discovery import discover_acroname_modules from fiwicontrol.lab.discovery import discover_acroname_modules
from fiwicontrol.power.acroname import AcronamePower
_REPO_ROOT = Path(__file__).resolve().parents[2] _REPO_ROOT = Path(__file__).resolve().parents[2]
@ -68,7 +67,7 @@ async def _all_off(rrhs: list[FabricRRHBinding]) -> None:
mod = by_serial.get(serial) mod = by_serial.get(serial)
if mod is None: if mod is None:
raise RuntimeError("Acroname module serial {} not found on local USB".format(serial)) 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) done.add(k)
print("OFF: module={} port={}".format(serial, port)) 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) mod = by_serial.get(serial)
if mod is None: if mod is None:
raise RuntimeError("Acroname module serial {} not found on local USB".format(serial)) raise RuntimeError("Acroname module serial {} not found on local USB".format(serial))
ap = AcronamePower(mod)
print( print(
"\n[{}/{}] radio_id={} module={} port={} patch_panel_port={}".format( "\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...") 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)) print("ON: module={} port={}".format(serial, port))
input("Observe fiber/link, then press Enter to power OFF this 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)) print("OFF: module={} port={}".format(serial, port))
if dwell > 0: if dwell > 0:
await asyncio.sleep(dwell) 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: async def _run(fabric_json: str, dwell: float, skip_all_off: bool) -> None:
path = _resolve_json_path(fabric_json) path = _resolve_json_path(fabric_json)
fd = FabricDefinition.load(path) fd = FabricDefinition.load(path)

View File

@ -15,9 +15,14 @@ import argparse
import asyncio import asyncio
from pathlib import Path from pathlib import Path
from fiwicontrol.commands.node_control import ssh_node
from fiwicontrol.fabric.fabric import FabricDefinition from fiwicontrol.fabric.fabric import FabricDefinition
from fiwicontrol.lab.discovery import discover_acroname_modules from fiwicontrol.lab.discovery import (
from fiwicontrol.power.acroname import AcronamePower 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] _REPO_ROOT = Path(__file__).resolve().parents[2]
@ -25,6 +30,7 @@ _REPO_ROOT = Path(__file__).resolve().parents[2]
async def _power_only_one( async def _power_only_one(
*, *,
fabric_json: str, fabric_json: str,
lab_ini: str | None,
keep_radio_id: str | None, keep_radio_id: str | None,
all_off: bool, all_off: bool,
dry_run: 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)) raise SystemExit("radio_id {!r} not found in {}".format(keep_radio_id, json_path))
modules = discover_acroname_modules() 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 keep_key: tuple[int | None, int] | None = None
if all_off: if all_off:
@ -76,17 +104,150 @@ async def _power_only_one(
"{}: missing acroname_module_serial in {}".format(h.radio_id, json_path) "{}: missing acroname_module_serial in {}".format(h.radio_id, json_path)
) )
serial = int(h.acroname_module_serial) 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: 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 continue
ap = AcronamePower(mod) if serial in local_serials:
await ap.port_off(h.acroname_port) await asyncio.to_thread(
print("OFF: radio_id={} module={} port={}".format(h.radio_id, serial, h.acroname_port)) _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: def main() -> int:
@ -97,6 +258,12 @@ def main() -> int:
default="configs/my-fabric.json", default="configs/my-fabric.json",
help="Path to FabricDefinition JSON (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( p.add_argument(
"--keep-radio-id", "--keep-radio-id",
default="7915", default="7915",
@ -116,6 +283,7 @@ def main() -> int:
asyncio.run( asyncio.run(
_power_only_one( _power_only_one(
fabric_json=args.fabric_json, fabric_json=args.fabric_json,
lab_ini=args.lab_ini,
keep_radio_id=args.keep_radio_id if not args.all_off else None, keep_radio_id=args.keep_radio_id if not args.all_off else None,
all_off=args.all_off, all_off=args.all_off,
dry_run=args.dry_run, dry_run=args.dry_run,