Add System Check workflow with serial and network connection options
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 <noreply@anthropic.com>
This commit is contained in:
@@ -30,6 +30,7 @@ modules/
|
|||||||
redfish.py ← Redfish API client (shared by all workflows)
|
redfish.py ← Redfish API client (shared by all workflows)
|
||||||
workflow_serial.py ← Serial-based IOM network configuration workflow
|
workflow_serial.py ← Serial-based IOM network configuration workflow
|
||||||
workflow_firmware.py ← IOM and Fabric Card firmware update 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.
|
`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()`)
|
- 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
|
- 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
|
## Key Design Notes
|
||||||
|
|
||||||
**Static IP firmware bug workaround:** Setting a static IP requires two sequential PATCH requests:
|
**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
|
## 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
|
- 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
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ import time
|
|||||||
sys.path.insert(0, os.path.join(os.path.dirname(os.path.abspath(__file__)), "modules"))
|
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 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_firmware import firmware_update_workflow
|
||||||
from workflow_serial import configure_shelf
|
from workflow_serial import configure_shelf
|
||||||
|
|
||||||
@@ -32,11 +33,12 @@ def main():
|
|||||||
draw_box([
|
draw_box([
|
||||||
f" {_c(C.BOLD, '1')} Configure a new ES24N shelf (serial)",
|
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, '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()
|
print()
|
||||||
|
|
||||||
choice = prompt("Select [1/2/3]")
|
choice = prompt("Select [1/2/3/4]")
|
||||||
if choice == "1":
|
if choice == "1":
|
||||||
another = configure_shelf()
|
another = configure_shelf()
|
||||||
if not another:
|
if not another:
|
||||||
@@ -44,9 +46,11 @@ def main():
|
|||||||
elif choice == "2":
|
elif choice == "2":
|
||||||
firmware_update_workflow()
|
firmware_update_workflow()
|
||||||
elif choice == "3":
|
elif choice == "3":
|
||||||
|
system_check_workflow()
|
||||||
|
elif choice == "4":
|
||||||
break
|
break
|
||||||
else:
|
else:
|
||||||
warn("Please enter 1, 2, or 3.")
|
warn("Please enter 1, 2, 3, or 4.")
|
||||||
time.sleep(1)
|
time.sleep(1)
|
||||||
|
|
||||||
print()
|
print()
|
||||||
|
|||||||
235
modules/workflow_check.py
Normal file
235
modules/workflow_check.py
Normal file
@@ -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
|
||||||
Reference in New Issue
Block a user