refactor: move lab discovery to fiwicontrol.lab; remote setup to commands
- Add fiwicontrol.lab package (discovery, inventory_config, inventory_verify) with default lab INI configs/default.ini (FIWI_LAB_INI override). - Move remote_setup to fiwicontrol.commands; add python -m fiwicontrol.commands. - Power CLI keeps --discovery-json, --list-power-devices, --verify-inventory; add -c/--config for inventory paths; drop --setup-remote (use commands module). - Re-export lab symbols from fiwicontrol.power for compatibility. - Update docs, scripts, and tests. Made-with: Cursor
This commit is contained in:
parent
72048e8a62
commit
4ad0b4b249
25
README.md
25
README.md
|
|
@ -6,8 +6,9 @@ Tools and libraries for managing and testing Umber Fi‑Wi networks.
|
|||
|
||||
This repository ships that distribution (**`fiwicontrol`** on PyPI / `pip`) with import root **`fiwicontrol`**:
|
||||
|
||||
1. **`fiwicontrol.commands`** — run commands on remote rigs via OpenSSH or **`ush`**, with asyncio streaming, timeouts, repeats (`Command`), and a small registry (`CommandManager`). Implementation: `src/fiwicontrol/commands/node_control.py`.
|
||||
2. **`fiwicontrol.power`** — discovery plus **`Power`** (``.on()`` / ``.off()`` / ``.voltage()`` / ``.current()`` / ``.watts()``) delegating to **`AcronamePower`** and **`MonsoonPower`**. May import **`fiwicontrol.commands`**; the reverse is forbidden.
|
||||
1. **`fiwicontrol.commands`** — run commands on remote rigs via OpenSSH or **`ush`** (`Command`, `CommandManager`, `ssh_node`). Remote Pi bootstrap: **`python -m fiwicontrol.commands`** (`--setup-remote`). Must not import **`fiwicontrol.power`**.
|
||||
2. **`fiwicontrol.lab`** — USB discovery (Acroname / Monsoon) and **`configs/*.ini`** inventory load + verify. Imports **`fiwicontrol.commands`** for SSH discovery only.
|
||||
3. **`fiwicontrol.power`** — **`Power`** (``.on()`` / ``.off()`` / ``.voltage()`` / …) via **`AcronamePower`** and **`MonsoonPower`**, plus a small CLI (**`--discovery-json`**, **`--list-power-devices`**, **`--verify-inventory`** with **`-c`**). Re-exports **`fiwicontrol.lab`** discovery/inventory for compatibility. May import **`fiwicontrol.commands`** and **`fiwicontrol.lab`**; the reverse is forbidden.
|
||||
|
||||
**Layout**
|
||||
|
||||
|
|
@ -24,9 +25,20 @@ FiWiControl/
|
|||
│ └── fiwicontrol/
|
||||
│ ├── commands/
|
||||
│ │ ├── __init__.py
|
||||
│ │ └── node_control.py
|
||||
│ │ ├── __main__.py
|
||||
│ │ ├── node_control.py
|
||||
│ │ └── remote_setup.py
|
||||
│ ├── lab/
|
||||
│ │ ├── __init__.py
|
||||
│ │ ├── discovery.py
|
||||
│ │ ├── inventory_config.py
|
||||
│ │ └── inventory_verify.py
|
||||
│ └── power/
|
||||
│ └── __init__.py
|
||||
│ ├── __init__.py
|
||||
│ ├── __main__.py
|
||||
│ ├── acroname.py
|
||||
│ ├── control.py
|
||||
│ └── monsoon.py
|
||||
└── tests/
|
||||
```
|
||||
|
||||
|
|
@ -76,12 +88,13 @@ cd ~/Code/FiWiControl
|
|||
FIWI_REMOTE_IP=192.168.1.39 pytest -q tests/test_node_control.py
|
||||
```
|
||||
|
||||
Power / USB inventory (needs **`brainstem`** and **`pyserial`**, e.g. **`pip install -e ".[power]"`**): see **`docs/power-control-and-inventory.md`** for **what must be installed on the remote** (e.g. Pi) and how discovery over SSH works. **`configs/clubhouse.ini`** lists expected Acroname hubs as **`Stem:ports`** (downstream USB count) and **`monsoon_count`** per **`[inventory.host.*]`**. Compare live discovery to the INI (local host + SSH to the Pi for remote rows):
|
||||
Power / USB inventory (needs **`brainstem`** and **`pyserial`**, e.g. **`pip install -e ".[power]"`**): see **`docs/power-control-and-inventory.md`**. Lab layout is **`configs/default.ini`** by default (override with **`-c`**). **`configs/clubhouse.ini`** is an alternate example file.
|
||||
|
||||
```bash
|
||||
cd ~/Code/FiWiControl
|
||||
python -m pip install -e ".[power]"
|
||||
python -m fiwicontrol.power --verify-inventory configs/clubhouse.ini
|
||||
python -m fiwicontrol.power --verify-inventory
|
||||
# or: python -m fiwicontrol.power -c configs/clubhouse.ini --verify-inventory
|
||||
```
|
||||
|
||||
The same check from **pytest** (skipped unless enabled):
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
# [node.*] — SSH targets (passwordless root for automation).
|
||||
# [inventory.host.*] — expected Acroname hubs (stem_class:downstream_usb_ports)
|
||||
# and Monsoon serial devices (count). Used by:
|
||||
# python -m fiwicontrol.power --verify-inventory configs/clubhouse.ini
|
||||
# python -m fiwicontrol.power -c configs/clubhouse.ini --verify-inventory
|
||||
|
||||
[site]
|
||||
name = ClubHouse
|
||||
|
|
|
|||
|
|
@ -0,0 +1,37 @@
|
|||
# FiWiControl — ClubHouse lab nodes + power / USB inventory
|
||||
#
|
||||
# [node.*] — SSH targets (passwordless root for automation).
|
||||
# [inventory.host.*] — expected Acroname hubs (stem_class:downstream_usb_ports)
|
||||
# and Monsoon serial devices (count). Used by:
|
||||
# python -m fiwicontrol.power --verify-inventory
|
||||
# (default INI: configs/default.ini; override: -c configs/clubhouse.ini)
|
||||
|
||||
[site]
|
||||
name = ClubHouse
|
||||
|
||||
[node.localhost]
|
||||
label = local
|
||||
ipaddr = 127.0.0.1
|
||||
sshtype = ssh
|
||||
description = Fedora workstation (local BrainStem over USB).
|
||||
|
||||
[node.raspberry_pi5]
|
||||
label = pi5
|
||||
ipaddr = 192.168.1.39
|
||||
sshtype = ssh
|
||||
description = Raspberry Pi 5 — two USBHub3p (8 downstream USB each), one USBHub2x4, Monsoon HVPM on USB.
|
||||
|
||||
# --- Expected USB lab devices (multiset match vs discovery) ---
|
||||
|
||||
[inventory.host.localhost]
|
||||
mode = local
|
||||
# One programmable 4-port USB hub (USBHub2x4) on this machine.
|
||||
acroname = USBHub2x4:4
|
||||
monsoon_count = 0
|
||||
|
||||
[inventory.host.pi5]
|
||||
mode = remote
|
||||
node = raspberry_pi5
|
||||
# Match ``python3 -m fiwicontrol.power --discovery-json`` on the Pi (stem_class:downstream_ports); one Monsoon.
|
||||
acroname = USBHub3p:8, USBHub3p:8, USBHub2x4:4
|
||||
monsoon_count = 1
|
||||
|
|
@ -17,7 +17,7 @@ Use this order the first time you (or someone else) joins the project or replace
|
|||
| 2 | Workstation | **Python 3.11+** and an **editable install**: **`python3 -m pip install -e ".[dev]"`**, then **`python3 -m pip install -e ".[power]"`** if you use Acroname/Monsoon or remote power tests (see **Workstation** below). |
|
||||
| 3 | Workstation → rig | **Passwordless SSH as root:** **`ssh -o BatchMode=yes root@192.168.1.39 true`** must exit **0** with no password prompt. On the rig, install your workstation’s **public key** in **`/root/.ssh/authorized_keys`** and ensure **`sshd`** allows **`root`** with pubkey auth. |
|
||||
| 4 | Rig (e.g. Pi) | **`git clone`** the same repo where you will run power/discovery (often **`/root/Code/FiWiControl`**). Your **`--remote-repo`** path must match. |
|
||||
| 5 | Workstation | From your clone, run **`--setup-remote`** (PEP 668 / Raspberry Pi OS needs **`--break-system-packages`** or **`FIWI_REMOTE_PIP_FLAGS`**). See **Remote host setup** §3–§4 below. |
|
||||
| 5 | Workstation | From your clone, run **`python3 -m fiwicontrol.commands`** with the Pi IP and **`--remote-repo`** (PEP 668 / Raspberry Pi OS needs **`--break-system-packages`** or **`FIWI_REMOTE_PIP_FLAGS`**). See **Remote host setup** §3–§4 below. |
|
||||
| 6 | Workstation | **Verify** (minimal): **`pytest -q tests/test_package_layout.py`**, then **`FIWI_REMOTE_IP=… pytest -q tests/test_node_control.py`**, then **`pytest -q tests/test_remote_power_dependencies.py`**. Default **`FIWI_REMOTE_IP`** is **`192.168.1.39`** if unset. For a full post-install checklist (INI, **`--verify-inventory`**, …), see **`docs/power-control-and-inventory.md`** → **“Verification checklist (after install)”**. |
|
||||
|
||||
**SSH user:** **`ssh_node`** ( **`sshtype="ssh"`** ) always uses an explicit **`root@<ipaddr>`** (or whatever **`ssh_session`** **`user`** is set to). It does **not** open **`ssh 192.168.1.39`** with your local login name, so your manual tests should use **`root@…`** as well.
|
||||
|
|
@ -99,7 +99,7 @@ Then:
|
|||
|
||||
```bash
|
||||
cd ~/Code/FiWiControl
|
||||
python3 -m fiwicontrol.power --setup-remote 192.168.1.39 --remote-repo /root/Code/FiWiControl --break-system-packages
|
||||
python3 -m fiwicontrol.commands 192.168.1.39 --remote-repo /root/Code/FiWiControl --break-system-packages
|
||||
```
|
||||
|
||||
**Raspberry Pi OS / Debian (PEP 668):** system Python blocks **`pip install`** without **`--break-system-packages`**. Prefer the **`--break-system-packages`** flag above.
|
||||
|
|
@ -110,7 +110,7 @@ python3 -m fiwicontrol.power --setup-remote 192.168.1.39 --remote-repo /root/Cod
|
|||
- **`--pip-flags=--break-system-packages`** (equals form), or
|
||||
- **`export FIWI_REMOTE_PIP_FLAGS='--break-system-packages'`** and omit **`--pip-flags`**.
|
||||
|
||||
**Wrapper:** **`scripts/setup_pi_power.sh`** sets **`PYTHONPATH=src`** on Fedora if needed, then runs **`--setup-remote`** using **`FIWI_REMOTE_IP`** and **`FIWI_REMOTE_REPO`**:
|
||||
**Wrapper:** **`scripts/setup_pi_power.sh`** sets **`PYTHONPATH=src`** on Fedora if needed, then runs **`python3 -m fiwicontrol.commands`** (**`--setup-remote`**-style args) using **`FIWI_REMOTE_IP`** and **`FIWI_REMOTE_REPO`**:
|
||||
|
||||
```bash
|
||||
cd ~/Code/FiWiControl
|
||||
|
|
@ -131,7 +131,7 @@ If **`pip show fiwicontrol`** reports **`Location: …/python3.11/…`** but pla
|
|||
export FIWI_REMOTE_PYTHON=python3.11 # or full path, e.g. /usr/bin/python3.11
|
||||
```
|
||||
|
||||
That variable is read by **`discover_devices_remote_async`**, **`tests/test_remote_power_dependencies.py`**, and **`python3 -m fiwicontrol.power --setup-remote --remote-python …`**.
|
||||
That variable is read by **`discover_devices_remote_async`**, **`tests/test_remote_power_dependencies.py`**, and **`python3 -m fiwicontrol.commands … --remote-python …`** (remote setup).
|
||||
|
||||
### 5. Verify after install
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
# Power control, discovery, and INI inventory (FiWiControl)
|
||||
|
||||
**Package:** **`fiwicontrol.power`**
|
||||
**Code:** `src/fiwicontrol/power/` (discovery, **`AcronamePower`**, **`MonsoonPower`**, **`Power`** facade, INI inventory helpers).
|
||||
**Packages:** **`fiwicontrol.lab`** (discovery + INI inventory), **`fiwicontrol.power`** (**`Power`**, **`AcronamePower`**, **`MonsoonPower`**, CLI wrappers).
|
||||
**Code:** `src/fiwicontrol/lab/`, `src/fiwicontrol/power/`.
|
||||
|
||||
This document describes **what must be installed on remote rigs** (e.g. a Raspberry Pi 5) when you use **remote discovery** or **`--verify-inventory`** with `mode = remote` in your INI. It also explains **how to edit the lab INI** and **how to verify** a new install end-to-end.
|
||||
|
||||
|
|
@ -42,7 +42,7 @@ Every remote **`[inventory.host.*]`** must set **`node = <id>`** where **`<id>`*
|
|||
| **`acroname`** | no (default empty) | Comma-separated list of **`StemClass:downstream_usb_ports`**. Each entry is one expected Acroname / BrainStem module (by **stem class** and **downstream USB port count**). Order does not matter; duplicates are allowed (**multiset** match vs discovery). Example: **`USBHub2x4:4, USBHub3p:8`**. |
|
||||
| **`monsoon_count`** | no (default **`0`**) | Exact number of Monsoon-class USB serial devices discovery must find on that host. |
|
||||
|
||||
**Finding stem names and port counts:** run **`python3 -m fiwicontrol.power --discovery-json`** on the host (or **`--list-power-devices your.ini`** once you have a draft INI) and read each Acroname row’s **`stem_class`** and **`downstream_usb_ports`**. Those strings and integers must match your **`acroname`** entries.
|
||||
**Finding stem names and port counts:** run **`python3 -m fiwicontrol.power --discovery-json`** on the host (or **`python3 -m fiwicontrol.power --list-power-devices`** with **`-c`** pointing at your draft INI) and read each Acroname row’s **`stem_class`** and **`downstream_usb_ports`**. Those strings and integers must match your **`acroname`** entries.
|
||||
|
||||
**Empty hardware:** **`acroname`** may be left blank and **`monsoon_count = 0`** if that host has no power/USB inventory to track.
|
||||
|
||||
|
|
@ -99,7 +99,8 @@ If **`fiwicontrol`** is only on `PYTHONPATH` (no install), ensure the remote she
|
|||
**List attached power/USB devices on every inventory host** (this machine + each `mode=remote` SSH target; addresses in each section header):
|
||||
|
||||
```bash
|
||||
python3 -m fiwicontrol.power --list-power-devices configs/clubhouse.ini
|
||||
python3 -m fiwicontrol.power --list-power-devices
|
||||
# or: python3 -m fiwicontrol.power -c configs/clubhouse.ini --list-power-devices
|
||||
```
|
||||
|
||||
**Sanity on the Pi** (after install):
|
||||
|
|
@ -198,7 +199,8 @@ From your **workstation** (repo root, passwordless **`ssh root@<pi-ip>`** workin
|
|||
|
||||
```bash
|
||||
cd ~/Code/FiWiControl
|
||||
python3 -m fiwicontrol.power --verify-inventory configs/clubhouse.ini
|
||||
python3 -m fiwicontrol.power --verify-inventory
|
||||
# or: python3 -m fiwicontrol.power -c configs/clubhouse.ini --verify-inventory
|
||||
echo "exit code: $?"
|
||||
```
|
||||
|
||||
|
|
@ -250,8 +252,8 @@ Run these **in order** on your **workstation** (repo root, **`pip install -e ".[
|
|||
|
||||
8. **INI matches your lab (workstation)**
|
||||
Edit **`configs/*.ini`** (see **Lab INI file reference** above). Then:
|
||||
**`python3 -m fiwicontrol.power --list-power-devices configs/your.ini`** — confirms discovery per host with headers showing addresses.
|
||||
**`python3 -m fiwicontrol.power --verify-inventory configs/your.ini`** — must print **`OK: discovery matches INI…`** (exit **0**). If you see **`MISMATCH`**, adjust **`acroname`** / **`monsoon_count`** or fix hardware/cabling.
|
||||
**`python3 -m fiwicontrol.power --list-power-devices`** (optional **`-c path/to.ini`**; default **`configs/default.ini`**) — confirms discovery per host with headers showing addresses.
|
||||
**`python3 -m fiwicontrol.power --verify-inventory`** — same **`-c`** rules; must print **`OK: discovery matches INI…`** (exit **0**). If you see **`MISMATCH`**, adjust **`acroname`** / **`monsoon_count`** or fix hardware/cabling.
|
||||
|
||||
9. **Optional strict pytest (workstation)**
|
||||
**`FIWI_VERIFY_POWER_INI=1 pytest -q tests/test_inventory_verify_live.py`** — opt-in live test that compares discovery to **`configs/clubhouse.ini`** (edit the test or your INI if your lab uses a different file).
|
||||
|
|
@ -276,7 +278,7 @@ See **`configs/clubhouse.ini`** for a full example and inline comments.
|
|||
```bash
|
||||
cd ~/Code/FiWiControl
|
||||
python3 -m pip install -e ".[power]"
|
||||
python3 -m fiwicontrol.power --verify-inventory configs/clubhouse.ini
|
||||
python3 -m fiwicontrol.power --verify-inventory
|
||||
```
|
||||
|
||||
**Full inventory match including hardware** (pytest, opt-in):
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ build-backend = "setuptools.build_meta"
|
|||
[project]
|
||||
name = "fiwicontrol"
|
||||
version = "0.1.0"
|
||||
description = "FiWiControl repo: fiwicontrol.commands (SSH/ush) and fiwicontrol.power in one installable package."
|
||||
description = "FiWiControl repo: commands (SSH/ush), lab (USB discovery + INI inventory), power (Acroname/Monsoon)."
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.11"
|
||||
license = { file = "LICENSE" }
|
||||
|
|
|
|||
|
|
@ -9,4 +9,4 @@ export FIWI_REMOTE_REPO="${FIWI_REMOTE_REPO:-/root/Code/FiWiControl}"
|
|||
if ! python3 -c "import fiwicontrol.power" 2>/dev/null; then
|
||||
export PYTHONPATH="${ROOT}/src${PYTHONPATH:+:${PYTHONPATH}}"
|
||||
fi
|
||||
exec python3 -m fiwicontrol.power --setup-remote "$FIWI_REMOTE_IP" --remote-repo "$FIWI_REMOTE_REPO" "$@"
|
||||
exec python3 -m fiwicontrol.commands "$FIWI_REMOTE_IP" --remote-repo "$FIWI_REMOTE_REPO" "$@"
|
||||
|
|
|
|||
|
|
@ -0,0 +1,19 @@
|
|||
# Copyright (c) 2026 Umber
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0; see LICENSE.
|
||||
|
||||
"""``python -m fiwicontrol.commands`` — remote lab host bootstrap (``--setup-remote``)."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
|
||||
from fiwicontrol.commands.remote_setup import main_setup
|
||||
|
||||
|
||||
def main() -> int:
|
||||
return main_setup(sys.argv[1:])
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
|
|
@ -0,0 +1,65 @@
|
|||
# Copyright (c) 2026 Umber
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0; see LICENSE.
|
||||
|
||||
"""Lab USB topology: BrainStem / Monsoon discovery and INI inventory."""
|
||||
|
||||
from fiwicontrol.lab.discovery import (
|
||||
MONSOON_USB_PIDS,
|
||||
MONSOON_USB_VID,
|
||||
AcronameModuleInfo,
|
||||
MonsoonPortInfo,
|
||||
acroname_module_info_from_spec,
|
||||
discover_acroname_modules,
|
||||
discover_acroname_modules_async,
|
||||
discover_devices_remote_async,
|
||||
discover_monsoon_serial_ports,
|
||||
discover_monsoon_serial_ports_async,
|
||||
discovery_json,
|
||||
discovery_payload_dict,
|
||||
parse_remote_discovery_payload,
|
||||
)
|
||||
from fiwicontrol.lab.inventory_config import (
|
||||
HostInventorySpec,
|
||||
InventoryDocument,
|
||||
NodeSection,
|
||||
default_lab_ini_path,
|
||||
load_inventory_ini,
|
||||
)
|
||||
from fiwicontrol.lab.inventory_verify import (
|
||||
compare_host_inventory,
|
||||
dump_inventory_discovery_ini_sync,
|
||||
main_dump_discovery,
|
||||
main_verify,
|
||||
verify_inventory_document,
|
||||
verify_inventory_ini,
|
||||
verify_inventory_ini_sync,
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
"MONSOON_USB_PIDS",
|
||||
"MONSOON_USB_VID",
|
||||
"AcronameModuleInfo",
|
||||
"MonsoonPortInfo",
|
||||
"HostInventorySpec",
|
||||
"InventoryDocument",
|
||||
"NodeSection",
|
||||
"acroname_module_info_from_spec",
|
||||
"compare_host_inventory",
|
||||
"default_lab_ini_path",
|
||||
"discover_acroname_modules",
|
||||
"discover_acroname_modules_async",
|
||||
"discover_devices_remote_async",
|
||||
"discover_monsoon_serial_ports",
|
||||
"discover_monsoon_serial_ports_async",
|
||||
"discovery_json",
|
||||
"discovery_payload_dict",
|
||||
"dump_inventory_discovery_ini_sync",
|
||||
"load_inventory_ini",
|
||||
"main_dump_discovery",
|
||||
"main_verify",
|
||||
"parse_remote_discovery_payload",
|
||||
"verify_inventory_document",
|
||||
"verify_inventory_ini",
|
||||
"verify_inventory_ini_sync",
|
||||
]
|
||||
|
|
@ -7,10 +7,16 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import configparser
|
||||
import os
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def default_lab_ini_path() -> Path:
|
||||
"""Default lab inventory INI (``configs/default.ini``, or ``FIWI_LAB_INI``)."""
|
||||
return Path(os.environ.get("FIWI_LAB_INI", "configs/default.ini")).expanduser()
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class NodeSection:
|
||||
"""Resolved ``[node.ID]`` entry."""
|
||||
|
|
@ -13,13 +13,13 @@ import sys
|
|||
from pathlib import Path
|
||||
from typing import Any, TextIO
|
||||
|
||||
from fiwicontrol.power.discovery import (
|
||||
from fiwicontrol.lab.discovery import (
|
||||
AcronameModuleInfo,
|
||||
discovery_payload_dict,
|
||||
discover_devices_remote_async,
|
||||
parse_remote_discovery_payload,
|
||||
)
|
||||
from fiwicontrol.power.inventory_config import HostInventorySpec, InventoryDocument, load_inventory_ini
|
||||
from fiwicontrol.lab.inventory_config import HostInventorySpec, InventoryDocument, default_lab_ini_path, load_inventory_ini
|
||||
|
||||
|
||||
def _workstation_ipv4_addrs() -> str:
|
||||
|
|
@ -80,15 +80,22 @@ def main_dump_discovery(argv: list[str] | None) -> int:
|
|||
import argparse
|
||||
|
||||
p = argparse.ArgumentParser(description="Print BrainStem/Monsoon discovery per [inventory.host.*] entry.")
|
||||
p.add_argument("ini", type=Path, help="INI path (e.g. configs/clubhouse.ini)")
|
||||
p.add_argument(
|
||||
"-c",
|
||||
"--config",
|
||||
type=Path,
|
||||
default=None,
|
||||
help="Lab inventory INI (default: configs/default.ini or $FIWI_LAB_INI).",
|
||||
)
|
||||
p.add_argument(
|
||||
"--ssh-controlmaster",
|
||||
action="store_true",
|
||||
help="Use ssh_node ControlMaster mode for remote hosts.",
|
||||
)
|
||||
args = p.parse_args(argv)
|
||||
ini = args.config if args.config is not None else default_lab_ini_path()
|
||||
try:
|
||||
dump_inventory_discovery_ini_sync(args.ini, ssh_controlmaster=args.ssh_controlmaster)
|
||||
dump_inventory_discovery_ini_sync(ini, ssh_controlmaster=args.ssh_controlmaster)
|
||||
except Exception as exc:
|
||||
print("Failed: {}".format(exc), file=sys.stderr)
|
||||
return 1
|
||||
|
|
@ -184,15 +191,22 @@ def main_verify(argv: list[str] | None) -> int:
|
|||
import argparse
|
||||
|
||||
parser = argparse.ArgumentParser(description="Verify power/USB discovery vs INI inventory.")
|
||||
parser.add_argument("ini", type=Path, help="Path to INI (e.g. configs/clubhouse.ini)")
|
||||
parser.add_argument(
|
||||
"-c",
|
||||
"--config",
|
||||
type=Path,
|
||||
default=None,
|
||||
help="Lab inventory INI (default: configs/default.ini or $FIWI_LAB_INI).",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--ssh-controlmaster",
|
||||
action="store_true",
|
||||
help="Use ssh_node ControlMaster mode for remote hosts (matches some lab setups).",
|
||||
)
|
||||
args = parser.parse_args(argv)
|
||||
ini = args.config if args.config is not None else default_lab_ini_path()
|
||||
try:
|
||||
doc = load_inventory_ini(args.ini)
|
||||
doc = load_inventory_ini(ini)
|
||||
except Exception as exc:
|
||||
print("Failed to load INI: {}".format(exc), file=sys.stderr)
|
||||
return 2
|
||||
|
|
@ -207,7 +221,7 @@ def main_verify(argv: list[str] | None) -> int:
|
|||
)
|
||||
)
|
||||
|
||||
errs = verify_inventory_ini_sync(args.ini, ssh_controlmaster=args.ssh_controlmaster)
|
||||
errs = verify_inventory_ini_sync(ini, ssh_controlmaster=args.ssh_controlmaster)
|
||||
if errs:
|
||||
print("MISMATCH:", file=sys.stderr)
|
||||
for line in errs:
|
||||
|
|
@ -1,16 +1,15 @@
|
|||
"""
|
||||
Power switching, monitoring, and discovery (Acroname, Monsoon, …).
|
||||
Power switching, monitoring (Acroname, Monsoon, …).
|
||||
|
||||
May import ``fiwicontrol.commands``; the reverse is forbidden.
|
||||
Lab USB discovery and INI inventory live in ``fiwicontrol.lab``; this package re-exports
|
||||
them for compatibility. May import ``fiwicontrol.commands``; the reverse is forbidden.
|
||||
"""
|
||||
|
||||
from fiwicontrol.power.acroname import AcronamePower
|
||||
from fiwicontrol.power.control import Power
|
||||
from fiwicontrol.power.discovery import (
|
||||
AcronameModuleInfo,
|
||||
MonsoonPortInfo,
|
||||
from fiwicontrol.lab.discovery import (
|
||||
MONSOON_USB_PIDS,
|
||||
MONSOON_USB_VID,
|
||||
AcronameModuleInfo,
|
||||
MonsoonPortInfo,
|
||||
acroname_module_info_from_spec,
|
||||
discover_acroname_modules,
|
||||
discover_acroname_modules_async,
|
||||
|
|
@ -21,13 +20,21 @@ from fiwicontrol.power.discovery import (
|
|||
discovery_payload_dict,
|
||||
parse_remote_discovery_payload,
|
||||
)
|
||||
from fiwicontrol.power.inventory_config import InventoryDocument, HostInventorySpec, NodeSection, load_inventory_ini
|
||||
from fiwicontrol.power.inventory_verify import (
|
||||
from fiwicontrol.lab.inventory_config import (
|
||||
HostInventorySpec,
|
||||
InventoryDocument,
|
||||
NodeSection,
|
||||
default_lab_ini_path,
|
||||
load_inventory_ini,
|
||||
)
|
||||
from fiwicontrol.lab.inventory_verify import (
|
||||
compare_host_inventory,
|
||||
verify_inventory_document,
|
||||
verify_inventory_ini,
|
||||
verify_inventory_ini_sync,
|
||||
)
|
||||
from fiwicontrol.power.acroname import AcronamePower
|
||||
from fiwicontrol.power.control import Power
|
||||
from fiwicontrol.power.monsoon import MonsoonPower, MonsoonReading
|
||||
|
||||
__all__ = [
|
||||
|
|
@ -39,6 +46,7 @@ __all__ = [
|
|||
"NodeSection",
|
||||
"Power",
|
||||
"compare_host_inventory",
|
||||
"default_lab_ini_path",
|
||||
"discovery_payload_dict",
|
||||
"load_inventory_ini",
|
||||
"verify_inventory_document",
|
||||
|
|
|
|||
|
|
@ -2,26 +2,34 @@
|
|||
#
|
||||
# Licensed under the Apache License, Version 2.0; see LICENSE.
|
||||
#
|
||||
"""CLI: ``--discovery-json``, ``--list-power-devices``, ``--verify-inventory``, ``--setup-remote``."""
|
||||
"""CLI: ``--discovery-json``, ``--list-power-devices``, ``--verify-inventory`` (lab INI via ``-c``)."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
from fiwicontrol.power.discovery import discovery_json
|
||||
from fiwicontrol.power.inventory_verify import main_dump_discovery, main_verify
|
||||
from fiwicontrol.power.remote_setup import main_setup
|
||||
from fiwicontrol.lab.discovery import discovery_json
|
||||
from fiwicontrol.lab.inventory_config import default_lab_ini_path
|
||||
from fiwicontrol.lab.inventory_verify import main_dump_discovery, main_verify
|
||||
|
||||
|
||||
def main(argv: list[str] | None = None) -> int:
|
||||
parser = argparse.ArgumentParser(
|
||||
prog="python -m fiwicontrol.power",
|
||||
epilog=(
|
||||
"Remote Pi (PEP 668): use --break-system-packages or --pip-flags=--break-system-packages "
|
||||
"(do not put a space before --break-system-packages after --pip-flags)."
|
||||
"Remote Pi install: use ``python -m fiwicontrol.commands --setup-remote …`` "
|
||||
"(PEP 668: ``--break-system-packages`` or ``--pip-flags=--break-system-packages``)."
|
||||
),
|
||||
)
|
||||
parser.add_argument(
|
||||
"-c",
|
||||
"--config",
|
||||
type=Path,
|
||||
default=None,
|
||||
help="Lab inventory INI for --list-power-devices / --verify-inventory (default: configs/default.ini or $FIWI_LAB_INI).",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--discovery-json",
|
||||
action="store_true",
|
||||
|
|
@ -29,100 +37,32 @@ def main(argv: list[str] | None = None) -> int:
|
|||
)
|
||||
parser.add_argument(
|
||||
"--list-power-devices",
|
||||
dest="list_power_devices_ini",
|
||||
metavar="INI",
|
||||
type=str,
|
||||
default=None,
|
||||
help=(
|
||||
"Load INI and print BrainStem/Monsoon discovery for each [inventory.host.*] "
|
||||
"(local: workstation addresses; remote: SSH target IP from [node.*])."
|
||||
),
|
||||
action="store_true",
|
||||
help="Print discovery JSON for each [inventory.host.*] in the lab INI (-c).",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--verify-inventory",
|
||||
dest="verify_ini",
|
||||
metavar="INI",
|
||||
type=str,
|
||||
default=None,
|
||||
help="Compare discovery (local + remote per INI) to [inventory.host.*] expectations.",
|
||||
action="store_true",
|
||||
help="Compare live discovery to the lab INI (-c).",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--ssh-controlmaster",
|
||||
action="store_true",
|
||||
help="With --verify-inventory: use ControlMaster on ssh_node for remote hosts.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--setup-remote",
|
||||
dest="setup_remote",
|
||||
action="store_true",
|
||||
help="SSH to the Pi (or other host) and pip install -e '.[power]' in --remote-repo.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--remote-repo",
|
||||
default=None,
|
||||
help="With --setup-remote: FiWiControl path on the remote (default: $FIWI_REMOTE_REPO or /root/Code/FiWiControl).",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--pip-flags",
|
||||
default=None,
|
||||
metavar="ARGS",
|
||||
help=(
|
||||
"With --setup-remote: extra args for remote pip (default: $FIWI_REMOTE_PIP_FLAGS). "
|
||||
"If the value starts with --, use equals form, e.g. --pip-flags=--break-system-packages."
|
||||
),
|
||||
)
|
||||
parser.add_argument(
|
||||
"--break-system-packages",
|
||||
dest="pip_break_system",
|
||||
action="store_true",
|
||||
help="With --setup-remote: same as --pip-flags=--break-system-packages (PEP 668 / Raspberry Pi OS).",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--remote-python",
|
||||
default=None,
|
||||
metavar="EXE",
|
||||
help="With --setup-remote: remote interpreter for pip and checks (default: $FIWI_REMOTE_PYTHON or python3).",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--dry-run",
|
||||
action="store_true",
|
||||
help="With --setup-remote: print ssh remote script only.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"setup_host",
|
||||
nargs="?",
|
||||
default=None,
|
||||
help="With --setup-remote: host or user@host (default: $FIWI_REMOTE_IP or 192.168.1.39).",
|
||||
help="With --list-power-devices / --verify-inventory: use ControlMaster on ssh_node.",
|
||||
)
|
||||
args = parser.parse_args(argv)
|
||||
if args.list_power_devices_ini:
|
||||
sub = [args.list_power_devices_ini]
|
||||
ini = args.config if args.config is not None else default_lab_ini_path()
|
||||
|
||||
if args.list_power_devices:
|
||||
sub: list[str] = ["-c", str(ini)]
|
||||
if args.ssh_controlmaster:
|
||||
sub.append("--ssh-controlmaster")
|
||||
return main_dump_discovery(sub)
|
||||
if args.setup_remote:
|
||||
sargv: list[str] = []
|
||||
if args.setup_host:
|
||||
sargv.append(args.setup_host)
|
||||
if args.remote_repo:
|
||||
sargv.extend(["--remote-repo", args.remote_repo])
|
||||
pip_merged: list[str] = []
|
||||
if args.pip_flags:
|
||||
pip_merged.append(args.pip_flags.strip())
|
||||
if getattr(args, "pip_break_system", False):
|
||||
pip_merged.append("--break-system-packages")
|
||||
if pip_merged:
|
||||
sargv.append("--pip-flags=" + " ".join(pip_merged))
|
||||
if args.remote_python:
|
||||
sargv.extend(["--remote-python", args.remote_python])
|
||||
if args.dry_run:
|
||||
sargv.append("--dry-run")
|
||||
return main_setup(sargv)
|
||||
if args.verify_ini:
|
||||
vargv = [args.verify_ini]
|
||||
if args.verify_inventory:
|
||||
sub = ["-c", str(ini)]
|
||||
if args.ssh_controlmaster:
|
||||
vargv.append("--ssh-controlmaster")
|
||||
return main_verify(vargv)
|
||||
sub.append("--ssh-controlmaster")
|
||||
return main_verify(sub)
|
||||
if args.discovery_json:
|
||||
sys.stdout.write(discovery_json())
|
||||
sys.stdout.write("\n")
|
||||
|
|
|
|||
|
|
@ -10,12 +10,12 @@ from pathlib import Path
|
|||
|
||||
import pytest
|
||||
|
||||
from fiwicontrol.power.inventory_config import load_inventory_ini
|
||||
from fiwicontrol.power.inventory_verify import compare_host_inventory, dump_inventory_discovery_ini_sync
|
||||
from fiwicontrol.lab.inventory_config import HostInventorySpec, load_inventory_ini
|
||||
from fiwicontrol.lab.inventory_verify import compare_host_inventory, dump_inventory_discovery_ini_sync
|
||||
|
||||
|
||||
def test_load_clubhouse_ini(tmp_path: Path) -> None:
|
||||
src = Path(__file__).resolve().parents[1] / "configs" / "clubhouse.ini"
|
||||
def test_load_default_lab_ini(tmp_path: Path) -> None:
|
||||
src = Path(__file__).resolve().parents[1] / "configs" / "default.ini"
|
||||
doc = load_inventory_ini(src)
|
||||
assert doc.site_name == "ClubHouse"
|
||||
assert "raspberry_pi5" in doc.nodes
|
||||
|
|
@ -33,8 +33,6 @@ def test_load_clubhouse_ini(tmp_path: Path) -> None:
|
|||
|
||||
|
||||
def test_compare_host_inventory_match() -> None:
|
||||
from fiwicontrol.power.inventory_config import HostInventorySpec
|
||||
|
||||
spec = HostInventorySpec(
|
||||
name="t",
|
||||
mode="local",
|
||||
|
|
@ -75,8 +73,6 @@ def test_compare_host_inventory_match() -> None:
|
|||
|
||||
|
||||
def test_compare_host_inventory_mismatch() -> None:
|
||||
from fiwicontrol.power.inventory_config import HostInventorySpec
|
||||
|
||||
spec = HostInventorySpec(
|
||||
name="t",
|
||||
mode="local",
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
#
|
||||
# Licensed under the Apache License, Version 2.0; see LICENSE.
|
||||
|
||||
"""Live inventory check vs ``configs/clubhouse.ini`` (opt-in via env)."""
|
||||
"""Live inventory check vs ``configs/default.ini`` (opt-in via env)."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
|
|
@ -11,17 +11,17 @@ from pathlib import Path
|
|||
|
||||
import pytest
|
||||
|
||||
INI = Path(__file__).resolve().parents[1] / "configs" / "clubhouse.ini"
|
||||
INI = Path(__file__).resolve().parents[1] / "configs" / "default.ini"
|
||||
|
||||
|
||||
@pytest.mark.skipif(
|
||||
os.environ.get("FIWI_VERIFY_POWER_INI", "").lower() not in ("1", "true", "yes"),
|
||||
reason="Set FIWI_VERIFY_POWER_INI=1 to run local+remote discovery vs configs/clubhouse.ini (needs hardware + SSH).",
|
||||
reason="Set FIWI_VERIFY_POWER_INI=1 to run local+remote discovery vs configs/default.ini (needs hardware + SSH).",
|
||||
)
|
||||
def test_inventory_matches_ini_live() -> None:
|
||||
pytest.importorskip("brainstem")
|
||||
pytest.importorskip("serial")
|
||||
from fiwicontrol.power.inventory_verify import verify_inventory_ini_sync
|
||||
from fiwicontrol.lab.inventory_verify import verify_inventory_ini_sync
|
||||
|
||||
errs = verify_inventory_ini_sync(INI, ssh_controlmaster=False)
|
||||
assert errs == [], ";\n".join(errs)
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
def test_import_subpackages() -> None:
|
||||
import fiwicontrol
|
||||
import fiwicontrol.commands
|
||||
import fiwicontrol.lab
|
||||
import fiwicontrol.power
|
||||
|
||||
assert fiwicontrol.__version__
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ from unittest import mock
|
|||
|
||||
import pytest
|
||||
|
||||
from fiwicontrol.power.discovery import (
|
||||
from fiwicontrol.lab.discovery import (
|
||||
MONSOON_USB_VID,
|
||||
MONSOON_USB_PIDS,
|
||||
_discover_monsoon_sysfs_usb,
|
||||
|
|
@ -94,8 +94,8 @@ def test_discover_monsoon_serial_ports_filters_vid_pid():
|
|||
def test_discovery_json_is_parseable():
|
||||
pytest.importorskip("brainstem")
|
||||
pytest.importorskip("serial")
|
||||
with mock.patch("fiwicontrol.power.discovery.discover_acroname_modules", return_value=[]):
|
||||
with mock.patch("fiwicontrol.power.discovery.discover_monsoon_serial_ports", return_value=[]):
|
||||
with mock.patch("fiwicontrol.lab.discovery.discover_acroname_modules", return_value=[]):
|
||||
with mock.patch("fiwicontrol.lab.discovery.discover_monsoon_serial_ports", return_value=[]):
|
||||
data = json.loads(discovery_json())
|
||||
assert data["acroname"] == []
|
||||
assert data["monsoon"] == []
|
||||
|
|
|
|||
Loading…
Reference in New Issue