From 3361fdd1a6543f1e0d1300c9f3a396825edcc2dc Mon Sep 17 00:00:00 2001 From: scott Date: Tue, 17 Mar 2026 22:25:16 -0400 Subject: [PATCH] Add System Check workflow with serial and network connection options MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds workflow_check.py: a read-only diagnostic that queries current network settings and firmware versions (IOM + Fabric Card) from both IOMs. Accessible via a new main menu option (3 — System Check); Exit moves to option 4. Supports both serial console (curl over the serial session) and direct network (HTTPS to management IP) connection methods. Co-Authored-By: Claude Sonnet 4.6 --- CLAUDE.md | 9 ++ es24n_conf.py | 10 +- modules/workflow_check.py | 235 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 251 insertions(+), 3 deletions(-) create mode 100644 modules/workflow_check.py diff --git a/CLAUDE.md b/CLAUDE.md index 3384c07..9a041bc 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -30,6 +30,7 @@ modules/ redfish.py ← Redfish API client (shared by all workflows) workflow_serial.py ← Serial-based IOM network configuration workflow workflow_firmware.py ← IOM and Fabric Card firmware update workflow + workflow_check.py ← Read-only system check (network settings + firmware versions) ``` `es24n_conf.py` adds `modules/` to `sys.path` at startup so all inter-module imports work without a package structure. @@ -54,6 +55,13 @@ Updates IOM firmware and/or Fabric Card firmware over the network: - The firmware file must be re-uploaded between the IOM and Fabric Card steps — it does not persist after the first update (firmware quirk, documented in `_update_fabric_fw()`) - Scans the current working directory for firmware files (`.bin`, `.img`, `.fw`, `.hex`, `.zip`, `.tar`, `.tgz`, `.gz`) and presents them as a numbered list before falling back to manual path entry +### 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 +- **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 + ## Key Design Notes **Static IP firmware bug workaround:** Setting a static IP requires two sequential PATCH requests: @@ -74,3 +82,4 @@ A single PATCH combining both fields fails due to a known ES24N firmware bug. Do ## Planned Features - Network-based IOM network configuration (`workflow_network.py`) — same config workflow as serial but connecting via the IOM's existing IP address using `Admin` credentials, for cases where the IOM is already reachable on the network +- Auto-detect password from serial login prompt — the IOM BMC serial number (e.g. `MXE3000043CHA007`) appears to be embedded in the login prompt hostname; if confirmed on a live system, this could allow `_login_serial_console()` to skip the manual password prompt diff --git a/es24n_conf.py b/es24n_conf.py index 1be27e3..e19d698 100755 --- a/es24n_conf.py +++ b/es24n_conf.py @@ -22,6 +22,7 @@ import time sys.path.insert(0, os.path.join(os.path.dirname(os.path.abspath(__file__)), "modules")) from ui import _c, C, banner, draw_box, ok, warn, prompt +from workflow_check import system_check_workflow from workflow_firmware import firmware_update_workflow from workflow_serial import configure_shelf @@ -32,11 +33,12 @@ def main(): draw_box([ f" {_c(C.BOLD, '1')} Configure a new ES24N shelf (serial)", f" {_c(C.BOLD, '2')} Update IOM / Fabric Card Firmware", - f" {_c(C.BOLD, '3')} Exit", + f" {_c(C.BOLD, '3')} System Check", + f" {_c(C.BOLD, '4')} Exit", ]) print() - choice = prompt("Select [1/2/3]") + choice = prompt("Select [1/2/3/4]") if choice == "1": another = configure_shelf() if not another: @@ -44,9 +46,11 @@ def main(): elif choice == "2": firmware_update_workflow() elif choice == "3": + system_check_workflow() + elif choice == "4": break else: - warn("Please enter 1, 2, or 3.") + warn("Please enter 1, 2, 3, or 4.") time.sleep(1) print() diff --git a/modules/workflow_check.py b/modules/workflow_check.py new file mode 100644 index 0000000..de3e2a0 --- /dev/null +++ b/modules/workflow_check.py @@ -0,0 +1,235 @@ +""" +workflow_check.py — System check workflow for ES24N IOM controllers. + +Queries current network settings and firmware versions from one or both IOMs +via either a serial connection (for IOMs not yet on the network) or a direct +network connection (for IOMs already reachable via their management IP). +No changes are made — this is a read-only diagnostic workflow. +""" + +import time + +from redfish import _redfish_request, _get_iom_fw_version, _get_fabric_fw_version +from workflow_serial import ( + detect_serial_device, + open_serial_connection, + close_serial_connection, + _login_serial_console, + _serial_redfish_request, +) +from ui import ( + _c, C, + banner, rule, draw_table, + info, ok, warn, error, + prompt, prompt_ip, prompt_password, +) + + +# ── Shared helpers ───────────────────────────────────────────────────────────── + +def _parse_network_data(data: dict) -> tuple: + """ + Extract (dhcp_enabled, ip, gateway, netmask, origin) from a + Redfish EthernetInterfaces/1 response dict. + """ + dhcp_enabled = ( + data.get("DHCPv4", {}).get("DHCPEnabled", False) or + data.get("DHCPv6", {}).get("DHCPEnabled", False) + ) + addrs = data.get("IPv4StaticAddresses") or data.get("IPv4Addresses", []) + if addrs: + addr = addrs[0] + ip = addr.get("Address", "--") + gateway = addr.get("Gateway", "--") + netmask = addr.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") + ) + return dhcp_enabled, ip, gateway, netmask, origin + + +def _print_results(net_rows: list, fw_rows: list): + rule("Network Settings") + draw_table( + ["IOM", "Mode", "Origin", "IP Address", "Gateway", "Subnet Mask"], + net_rows, + [5, 10, 8, 16, 16, 16], + ) + print() + + rule("Firmware Versions") + draw_table( + ["IOM", "IOM Firmware", "Fabric Firmware"], + fw_rows, + [5, 32, 24], + ) + print() + + +# ── Serial check ─────────────────────────────────────────────────────────────── + +def _check_via_serial(): + banner() + rule("System Check -- Serial Connection") + + device = detect_serial_device() + if not device: + error("Could not detect a serial device. Returning to main menu.") + time.sleep(2) + return + + ser = open_serial_connection(device) + if not ser: + error("Could not open serial port. Returning to main menu.") + time.sleep(2) + return + + print() + password = prompt_password() + + if not _login_serial_console(ser, password): + error("Could not log in to IOM console. Returning to main menu.") + close_serial_connection(ser, device) + time.sleep(2) + return + + rule("Querying IOM Status") + info("Querying network settings and firmware versions via serial console...") + print() + + net_rows = [] + fw_rows = [] + + 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", + 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", + 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) + close_serial_connection(ser, device) + + +# ── Network check ────────────────────────────────────────────────────────────── + +def _check_via_network(): + banner() + rule("System Check -- Network Connection") + + print() + password = prompt_password() + print() + + print(" Which IOM(s) do you want to check?") + print(f" {_c(C.BOLD, '1')} IOM1 only") + print(f" {_c(C.BOLD, '2')} IOM2 only") + print(f" {_c(C.BOLD, '3')} Both IOM1 and IOM2") + print() + + while True: + choice = prompt("Select [1/2/3]") + if choice in ("1", "2", "3"): + break + warn("Please enter 1, 2, or 3.") + + iom_list = [] + if choice in ("1", "3"): + ip1 = prompt_ip(" IOM1 IP address") + iom_list.append(("IOM1", ip1)) + if choice in ("2", "3"): + ip2 = prompt_ip(" IOM2 IP address") + iom_list.append(("IOM2", ip2)) + + rule("Querying IOM Status") + info("Querying network settings and firmware versions over the network...") + print() + + net_rows = [] + fw_rows = [] + + for iom, ip in iom_list: + # ── Network settings ─────────────────────────────────────────────────── + net_ok, net_data = _redfish_request( + password, "GET", + f"/redfish/v1/Managers/{iom}/EthernetInterfaces/1", + host=ip, + ) + if net_ok and isinstance(net_data, dict): + dhcp, ip_addr, 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_addr, gw, nm]) + else: + net_rows.append([iom, _c(C.RED, "No response"), "--", "--", "--", "--"]) + error(f"{iom} network query failed: {net_data}") + + # ── Firmware versions (reuse shared redfish helpers) ─────────────────── + iom_ver = _get_iom_fw_version(password, ip, iom) + fab_ver = _get_fabric_fw_version(password, ip, iom) + fw_rows.append([iom, iom_ver, fab_ver]) + + _print_results(net_rows, fw_rows) + + +# ── Top-level entry point ────────────────────────────────────────────────────── + +def system_check_workflow(): + banner() + rule("System Check") + + print(" How do you want to connect to the IOM(s)?") + print() + print(f" {_c(C.BOLD, '1')} Serial connection (IOM not yet on the network)") + print(f" {_c(C.BOLD, '2')} Network connection (IOM reachable via management IP)") + print(f" {_c(C.BOLD, '3')} Cancel") + print() + + while True: + choice = prompt("Select [1/2/3]") + if choice in ("1", "2", "3"): + break + warn("Please enter 1, 2, or 3.") + + if choice == "1": + _check_via_serial() + elif choice == "2": + _check_via_network() + # choice == "3" returns to main menu