From 76ee347d914eed2fd923c374afb112b659e8b91a Mon Sep 17 00:00:00 2001 From: scott Date: Thu, 16 Apr 2026 10:47:12 -0400 Subject: [PATCH] =?UTF-8?q?Limit=20serial=20workflows=20to=20IOM1=20only?= =?UTF-8?q?=20=E2=80=94=20IOM2=20unreachable=20over=20serial?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The serial cable connects to IOM1's console port. IOM2 cannot be queried or configured via the serial connection. Updated all serial-path code to reflect this: workflow_serial.py: - fetch_current_config: query IOM1 only, single-row table - collect_network_config: prompt for IOM1 settings only, drop IOM2 section - apply_configuration: apply to IOM1 only, single-row result table - print_summary: IOM1-only table, updated warning text workflow_check.py (_check_via_serial): - Query IOM1 only for network settings, IOM firmware, and Fabric Card firmware CLAUDE.md updated to document the IOM1-only serial limitation. Co-Authored-By: Claude Sonnet 4.6 --- CLAUDE.md | 10 +- modules/workflow_check.py | 81 ++++++++--------- modules/workflow_serial.py | 182 +++++++++++++++---------------------- 3 files changed, 117 insertions(+), 156 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 7b2678b..ae592c1 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -39,12 +39,12 @@ modules/ ## Workflows ### 1 — Serial Network Configuration (`workflow_serial.py`) -5-step workflow using the USB serial cable: +5-step workflow using the USB serial cable. **Serial access is limited to IOM1 only** — the cable connects to IOM1's console port; IOM2 cannot be queried or configured this way. 1. Detect USB serial device (`/dev/ttyUSB*`, `/dev/ttyACM*`, `/dev/ttyU*`) 2. Open 115200-baud 8N1 connection and wake the IOM console -3. Query current network settings via Redfish (`GET` over `127.0.0.1`) -4. Collect new settings from user (Static IP or DHCP) -5. Apply via Redfish `PATCH` over `127.0.0.1` +3. Query IOM1 current network settings via Redfish (`GET` over `127.0.0.1`) +4. Collect new settings from user (Static IP or DHCP) for IOM1 +5. Apply via Redfish `PATCH` over `127.0.0.1` to IOM1 ### 2 — Firmware Update (`workflow_firmware.py`) Updates IOM firmware and/or Fabric Card firmware over the network: @@ -58,7 +58,7 @@ Updates IOM firmware and/or Fabric Card firmware over the network: ### 3 — System Check (`workflow_check.py`) Read-only diagnostic workflow — queries current network settings and firmware versions, makes no changes: -- **Serial:** logs in via serial console, queries both IOMs using `_serial_redfish_request()` (curl over the serial session); covers network settings, IOM firmware, and Fabric Card firmware +- **Serial:** logs in via serial console, queries IOM1 only using `_serial_redfish_request()` (curl over the serial session); covers network settings, IOM firmware, and Fabric Card firmware. IOM2 is not reachable over serial. - **Network:** prompts for management IP(s), queries via direct HTTPS using `Admin` credentials; reuses `_redfish_request()`, `_get_iom_fw_version()`, `_get_fabric_fw_version()` from `redfish.py` - Displays results in two tables: network configuration and firmware versions - User selects Serial, Network, or Cancel at the sub-menu prompt diff --git a/modules/workflow_check.py b/modules/workflow_check.py index 5477643..5a8fe5a 100644 --- a/modules/workflow_check.py +++ b/modules/workflow_check.py @@ -97,54 +97,49 @@ def _check_via_serial(): time.sleep(2) return - rule("Querying IOM Status") - info("Querying network settings and firmware versions via serial console...") + rule("Querying IOM1 Status") + info("Querying IOM1 network settings and firmware versions via serial console...") + info("Note: only IOM1 is reachable over the serial connection.") print() - net_rows = [] - fw_rows = [] + # ── Network settings ─────────────────────────────────────────────────────── + net_ok, net_data = _serial_redfish_request( + ser, password, "GET", + "/redfish/v1/Managers/IOM1/EthernetInterfaces/1", + ) + if net_ok and isinstance(net_data, dict): + dhcp, ip, gw, nm, origin = _parse_network_data(net_data) + mode = _c(C.CYN, "DHCP") if dhcp else _c(C.GRN, "Static") + net_rows = [["IOM1", mode, origin, ip, gw, nm]] + else: + net_rows = [["IOM1", _c(C.RED, "No response"), "--", "--", "--", "--"]] + error(f"IOM1 network query failed: {net_data}") - for iom in ("IOM1", "IOM2"): - # ── Network settings ─────────────────────────────────────────────────── - net_ok, net_data = _serial_redfish_request( - ser, password, "GET", - f"/redfish/v1/Managers/{iom}/EthernetInterfaces/1", - ) - if net_ok and isinstance(net_data, dict): - dhcp, ip, gw, nm, origin = _parse_network_data(net_data) - mode = _c(C.CYN, "DHCP") if dhcp else _c(C.GRN, "Static") - net_rows.append([iom, mode, origin, ip, gw, nm]) - else: - net_rows.append([iom, _c(C.RED, "No response"), "--", "--", "--", "--"]) - error(f"{iom} network query failed: {net_data}") + # ── IOM firmware version ─────────────────────────────────────────────────── + iom_ok, iom_data = _serial_redfish_request( + ser, password, "GET", + "/redfish/v1/Managers/IOM1", + ) + iom_ver = ( + iom_data.get("FirmwareVersion", "Unknown") + if (iom_ok and isinstance(iom_data, dict)) + else _c(C.RED, "Unreachable") + ) - # ── IOM firmware version ─────────────────────────────────────────────── - iom_ok, iom_data = _serial_redfish_request( - ser, password, "GET", - f"/redfish/v1/Managers/{iom}", - ) - iom_ver = ( - iom_data.get("FirmwareVersion", "Unknown") - if (iom_ok and isinstance(iom_data, dict)) - else _c(C.RED, "Unreachable") - ) + # ── Fabric card firmware version ─────────────────────────────────────────── + fab_ok, fab_data = _serial_redfish_request( + ser, password, "GET", + "/redfish/v1/Chassis/IOM1/NetworkAdapters/1", + ) + fab_ver = ( + (fab_data.get("Oem", {}) + .get("Version", {}) + .get("ActiveFirmwareVersion", "Unknown")) + if (fab_ok and isinstance(fab_data, dict)) + else _c(C.RED, "Unreachable") + ) - # ── Fabric card firmware version ─────────────────────────────────────── - fab_ok, fab_data = _serial_redfish_request( - ser, password, "GET", - f"/redfish/v1/Chassis/{iom}/NetworkAdapters/1", - ) - fab_ver = ( - (fab_data.get("Oem", {}) - .get("Version", {}) - .get("ActiveFirmwareVersion", "Unknown")) - if (fab_ok and isinstance(fab_data, dict)) - else _c(C.RED, "Unreachable") - ) - - fw_rows.append([iom, iom_ver, fab_ver]) - - _print_results(net_rows, fw_rows) + _print_results(net_rows, [["IOM1", iom_ver, fab_ver]]) close_serial_connection(ser, device) diff --git a/modules/workflow_serial.py b/modules/workflow_serial.py index 59bacd1..ec85663 100644 --- a/modules/workflow_serial.py +++ b/modules/workflow_serial.py @@ -339,80 +339,67 @@ def open_serial_connection(device: str) -> Optional[SerialPort]: # ── Step 3: Fetch & display current IOM network settings ───────────────────── def fetch_current_config(cfg: ShelfConfig, ser: SerialPort) -> bool: """ - Query Redfish for the current network config of both IOMs via curl - over the serial console session. - Populates cfg.iom1 / cfg.iom2 with live data. - Returns True if at least one IOM responded. + Query Redfish for the current network config of IOM1 via curl over the + serial console session. Only IOM1 is reachable — the serial cable + connects to IOM1's console port; IOM2 cannot be queried this way. + Populates cfg.iom1 with live data. + Returns True if IOM1 responded successfully. """ - rule("Step 3 of 5 -- Current IOM Network Settings") - info("Querying Redfish API via serial console...") + rule("Step 3 of 5 -- Current IOM1 Network Settings") + info("Querying IOM1 Redfish API via serial console...") + info("Note: only IOM1 is reachable over the serial connection.") print() - any_ok = False - rows = [] - errors = [] + path = "/redfish/v1/Managers/IOM1/EthernetInterfaces/1" + ok_flag, data = _serial_redfish_request(ser, cfg.password, "GET", path) - for iom in ("IOM1", "IOM2"): - path = f"/redfish/v1/Managers/{iom}/EthernetInterfaces/1" - ok_flag, data = _serial_redfish_request(ser, cfg.password, "GET", path) + if ok_flag and isinstance(data, dict): + # Determine mode + dhcp_enabled = ( + data.get("DHCPv4", {}).get("DHCPEnabled", False) or + data.get("DHCPv6", {}).get("DHCPEnabled", False) + ) - if ok_flag and isinstance(data, dict): - any_ok = True - - # Determine mode - dhcp_enabled = ( - data.get("DHCPv4", {}).get("DHCPEnabled", False) or - data.get("DHCPv6", {}).get("DHCPEnabled", False) - ) - - # Pull address info — prefer StaticAddresses, fall back to IPv4Addresses - addrs = data.get("IPv4StaticAddresses") or data.get("IPv4Addresses", []) - if addrs: - addr_rec = addrs[0] - ip = addr_rec.get("Address", "--") - gateway = addr_rec.get("Gateway", "--") - netmask = addr_rec.get("SubnetMask", "--") - else: - ip = gateway = netmask = "--" - - origin = data.get("IPv4Addresses", [{}])[0].get("AddressOrigin", "Unknown") \ - if data.get("IPv4Addresses") else ("DHCP" if dhcp_enabled else "Static") - - iom_cfg = IOMConfig( - iom = iom, - dhcp = dhcp_enabled, - ip = ip if ip != "--" else "", - gateway = gateway if gateway != "--" else "", - netmask = netmask if netmask != "--" else "", - ) - if iom == "IOM1": - cfg.iom1 = iom_cfg - else: - cfg.iom2 = iom_cfg - - mode_str = f"{_c(C.CYN, 'DHCP')}" if dhcp_enabled else f"{_c(C.GRN, 'Static')}" - rows.append([iom, mode_str, origin, ip, gateway, netmask]) + # Pull address info — prefer StaticAddresses, fall back to IPv4Addresses + addrs = data.get("IPv4StaticAddresses") or data.get("IPv4Addresses", []) + if addrs: + addr_rec = addrs[0] + ip = addr_rec.get("Address", "--") + gateway = addr_rec.get("Gateway", "--") + netmask = addr_rec.get("SubnetMask", "--") else: - rows.append([iom, _c(C.RED, "No response"), "--", "--", "--", "--"]) - errors.append((iom, str(data))) + ip = gateway = netmask = "--" - draw_table( - ["IOM", "Mode", "Origin", "IP Address", "Gateway", "Subnet Mask"], - rows, - [5, 10, 8, 16, 16, 16], - ) - print() + origin = data.get("IPv4Addresses", [{}])[0].get("AddressOrigin", "Unknown") \ + if data.get("IPv4Addresses") else ("DHCP" if dhcp_enabled else "Static") - if errors: - for iom, err in errors: - error(f"{iom} query failed: {err}") + cfg.iom1 = IOMConfig( + iom = "IOM1", + dhcp = dhcp_enabled, + ip = ip if ip != "--" else "", + gateway = gateway if gateway != "--" else "", + netmask = netmask if netmask != "--" else "", + ) + + mode_str = f"{_c(C.CYN, 'DHCP')}" if dhcp_enabled else f"{_c(C.GRN, 'Static')}" + draw_table( + ["IOM", "Mode", "Origin", "IP Address", "Gateway", "Subnet Mask"], + [["IOM1", mode_str, origin, ip, gateway, netmask]], + [5, 10, 8, 16, 16, 16], + ) print() - - if not any_ok: - error("Neither IOM responded to the Redfish query.") - error("Check that the serial cable is connected and the IOM is booted.") - - return any_ok + return True + else: + draw_table( + ["IOM", "Mode", "Origin", "IP Address", "Gateway", "Subnet Mask"], + [["IOM1", _c(C.RED, "No response"), "--", "--", "--", "--"]], + [5, 10, 8, 16, 16, 16], + ) + print() + error(f"IOM1 query failed: {data}") + error("Check that the serial cable is connected and IOM1 is booted.") + print() + return False # ── Step 4: Prompt user — change config or exit ─────────────────────────────── @@ -439,8 +426,8 @@ def collect_network_config(cfg: ShelfConfig) -> bool: # ── User wants to change settings ───────────────────────────────────────── print() - print(" How should the IOMs be configured?") - print(f" {_c(C.BOLD, '1')} Static IP addresses") + print(" How should IOM1 be configured?") + print(f" {_c(C.BOLD, '1')} Static IP address") print(f" {_c(C.BOLD, '2')} DHCP") print() @@ -455,8 +442,7 @@ def collect_network_config(cfg: ShelfConfig) -> bool: if use_dhcp: cfg.iom1 = IOMConfig("IOM1", dhcp=True) - cfg.iom2 = IOMConfig("IOM2", dhcp=True) - ok("Both IOMs will be set to DHCP.") + ok("IOM1 will be set to DHCP.") return True # Static — IOM1 @@ -465,20 +451,6 @@ def collect_network_config(cfg: ShelfConfig) -> bool: iom1_gw = prompt_ip(" IOM1 Gateway ") iom1_nm = prompt_ip(" IOM1 Subnet Mask") cfg.iom1 = IOMConfig("IOM1", dhcp=False, ip=iom1_ip, gateway=iom1_gw, netmask=iom1_nm) - - # Static — IOM2 - print() - info(f"Static network details for {_c(C.BOLD, 'IOM2')}:") - iom2_ip = prompt_ip(" IOM2 IP address ") - - same = prompt_yn(" Same gateway and subnet mask as IOM1?", default=True) - if same: - iom2_gw, iom2_nm = iom1_gw, iom1_nm - else: - iom2_gw = prompt_ip(" IOM2 Gateway ") - iom2_nm = prompt_ip(" IOM2 Subnet Mask") - - cfg.iom2 = IOMConfig("IOM2", dhcp=False, ip=iom2_ip, gateway=iom2_gw, netmask=iom2_nm) return True @@ -486,21 +458,14 @@ def collect_network_config(cfg: ShelfConfig) -> bool: def apply_configuration(cfg: ShelfConfig, ser: SerialPort) -> bool: rule("Step 5 of 5 -- Applying Configuration via Redfish API") - info("Sending Redfish PATCH requests via serial console curl...") + info("Sending Redfish PATCH request to IOM1 via serial console curl...") print() - results = [] - all_ok = True - for iom_cfg in [cfg.iom1, cfg.iom2]: - success, detail = _apply_iom(cfg.password, iom_cfg, ser) - status = _c(C.GRN, "OK") if success else _c(C.RED, "FAIL") - results.append([iom_cfg.iom, status, detail]) - if not success: - all_ok = False - - draw_table(["IOM", "Result", "Detail"], results, [6, 8, 50]) + success, detail = _apply_iom(cfg.password, cfg.iom1, ser) + status = _c(C.GRN, "OK") if success else _c(C.RED, "FAIL") + draw_table(["IOM", "Result", "Detail"], [["IOM1", status, detail]], [6, 8, 50]) print() - return all_ok + return success def _apply_iom(password: str, iom_cfg: IOMConfig, ser: SerialPort) -> tuple: @@ -560,22 +525,23 @@ def _apply_iom(password: str, iom_cfg: IOMConfig, ser: SerialPort) -> tuple: def print_summary(cfg: ShelfConfig, changed: bool): rule("Summary") - def val(iom: IOMConfig, field: str) -> str: - dhcp_map = {"mode": "DHCP", "ip": "-- (DHCP)", "gateway": "-- (DHCP)", "netmask": "-- (DHCP)"} - stat_map = {"mode": "Static", "ip": iom.ip, "gateway": iom.gateway, "netmask": iom.netmask} - return (dhcp_map if iom.dhcp else stat_map).get(field, "") + iom = cfg.iom1 + if iom.dhcp: + mode, ip, gateway, netmask = "DHCP", "-- (DHCP)", "-- (DHCP)", "-- (DHCP)" + else: + mode, ip, gateway, netmask = "Static", iom.ip, iom.gateway, iom.netmask draw_table( - ["Setting", "IOM1", "IOM2"], + ["Setting", "IOM1"], [ - ["Mode", val(cfg.iom1, "mode"), val(cfg.iom2, "mode")], - ["IP Address", val(cfg.iom1, "ip"), val(cfg.iom2, "ip")], - ["Gateway", val(cfg.iom1, "gateway"), val(cfg.iom2, "gateway")], - ["Subnet Mask", val(cfg.iom1, "netmask"), val(cfg.iom2, "netmask")], - ["Serial Port", cfg.device, cfg.device], - ["Changes", "Yes" if changed else "None", ""], + ["Mode", mode], + ["IP Address", ip], + ["Gateway", gateway], + ["Subnet Mask", netmask], + ["Serial Port", cfg.device], + ["Changes", "Yes" if changed else "None"], ], - [12, 22, 22], + [12, 28], ) if changed: @@ -583,7 +549,7 @@ def print_summary(cfg: ShelfConfig, changed: bool): draw_box([ f"{_c(C.YEL, 'IMPORTANT -- Per the ES24N Service Guide:')}", "", - "Remove the serial cable ONLY after verifying each", + "Remove the serial cable ONLY after verifying the", "expander appears in TrueNAS with matching drives.", "", "TrueNAS > System Settings > Enclosure >",