Initial commit — bootstrapped from es24n-conf (TrueNAS/Linux edition)
Starting point for Windows packaging. Serial backend will be replaced with pyserial; PyInstaller used to produce a standalone .exe. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
253
modules/workflow_check.py
Normal file
253
modules/workflow_check.py
Normal file
@@ -0,0 +1,253 @@
|
||||
"""
|
||||
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 _sanitize(value: str) -> str:
|
||||
"""Strip non-printable and non-ASCII characters from a string field.
|
||||
IOM Redfish responses occasionally contain binary artifacts in text fields."""
|
||||
return "".join(c for c in value if 32 <= ord(c) < 128)
|
||||
|
||||
|
||||
def _parse_network_data(data: dict) -> tuple:
|
||||
"""
|
||||
Extract network details from a Redfish EthernetInterfaces/1 response dict.
|
||||
Returns (dhcp_enabled, ip, gateway, netmask, mac, hostname, link_status).
|
||||
"""
|
||||
dhcp_enabled = (
|
||||
data.get("DHCPv4", {}).get("DHCPEnabled", False) or
|
||||
data.get("DHCPv6", {}).get("DHCPEnabled", False)
|
||||
)
|
||||
|
||||
# Prefer IPv4StaticAddresses; fall back to IPv4Addresses
|
||||
addrs = data.get("IPv4StaticAddresses") or data.get("IPv4Addresses", [])
|
||||
if addrs:
|
||||
addr = addrs[0]
|
||||
ip = _sanitize(addr.get("Address", "--"))
|
||||
gateway = _sanitize(addr.get("Gateway", "--"))
|
||||
netmask = _sanitize(addr.get("SubnetMask", "--"))
|
||||
else:
|
||||
ip = gateway = netmask = "--"
|
||||
|
||||
mac = _sanitize(data.get("MACAddress", "--"))
|
||||
hostname = _sanitize(data.get("HostName", "--"))
|
||||
link_status = _sanitize(data.get("LinkStatus", "--"))
|
||||
|
||||
return dhcp_enabled, ip, gateway, netmask, mac, hostname, link_status
|
||||
|
||||
|
||||
def _print_results(net_rows: list, iface_rows: list, fw_rows: list):
|
||||
"""
|
||||
Display network settings, interface identity, and firmware versions.
|
||||
|
||||
net_rows: [IOM, Mode, IP Address, Gateway, Subnet Mask, Link Status]
|
||||
iface_rows: [IOM, MAC Address, Hostname]
|
||||
fw_rows: [IOM, IOM Firmware, Fabric Firmware]
|
||||
"""
|
||||
rule("Network Settings")
|
||||
draw_table(
|
||||
["IOM", "Mode", "IP Address", "Gateway", "Subnet Mask", "Link"],
|
||||
net_rows,
|
||||
[5, 8, 15, 15, 15, 8],
|
||||
)
|
||||
print()
|
||||
|
||||
rule("Interface Details")
|
||||
draw_table(
|
||||
["IOM", "MAC Address", "Hostname"],
|
||||
iface_rows,
|
||||
[5, 19, 42],
|
||||
)
|
||||
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()
|
||||
logged_in, password = _login_serial_console(ser)
|
||||
if not logged_in:
|
||||
error("Could not log in to IOM console. Returning to main menu.")
|
||||
close_serial_connection(ser, device)
|
||||
time.sleep(2)
|
||||
return
|
||||
|
||||
# Prompt for which IOM the serial cable is connected to
|
||||
print()
|
||||
print(" Which IOM is the serial cable connected to?")
|
||||
print(f" {_c(C.BOLD, '1')} IOM1")
|
||||
print(f" {_c(C.BOLD, '2')} IOM2")
|
||||
print()
|
||||
while True:
|
||||
iom_choice = prompt("Select [1/2]")
|
||||
if iom_choice in ("1", "2"):
|
||||
break
|
||||
warn("Please enter 1 or 2.")
|
||||
iom = "IOM1" if iom_choice == "1" else "IOM2"
|
||||
|
||||
rule(f"Querying {iom} Status")
|
||||
info(f"Querying {iom} network settings and firmware versions via serial console...")
|
||||
print()
|
||||
|
||||
# ── 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, mac, hostname, link = _parse_network_data(net_data)
|
||||
mode = _c(C.CYN, "DHCP") if dhcp else _c(C.GRN, "Static")
|
||||
net_rows = [[iom, mode, ip, gw, nm, link]]
|
||||
iface_rows = [[iom, mac, hostname]]
|
||||
else:
|
||||
net_rows = [[iom, _c(C.RED, "No response"), "--", "--", "--", "--"]]
|
||||
iface_rows = [[iom, "--", "--"]]
|
||||
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("VikingEnterpriseSolutions", {})
|
||||
.get("Version", {})
|
||||
.get("ActiveFirmwareVersion", "Unknown"))
|
||||
if (fab_ok and isinstance(fab_data, dict))
|
||||
else _c(C.RED, "Unreachable")
|
||||
)
|
||||
|
||||
_print_results(net_rows, iface_rows, [[iom, iom_ver, fab_ver]])
|
||||
close_serial_connection(ser, device)
|
||||
|
||||
|
||||
# ── Network check ──────────────────────────────────────────────────────────────
|
||||
|
||||
def _check_via_network():
|
||||
banner()
|
||||
rule("System Check -- Network Connection")
|
||||
|
||||
print()
|
||||
password = prompt_password()
|
||||
print()
|
||||
ip = prompt_ip(" IOM IP address (either IOM1 or IOM2)")
|
||||
|
||||
iom_list = [("IOM1", ip), ("IOM2", ip)]
|
||||
|
||||
rule("Querying IOM Status")
|
||||
info("Querying network settings and firmware versions over the network...")
|
||||
print()
|
||||
|
||||
net_rows = []
|
||||
iface_rows = []
|
||||
fw_rows = []
|
||||
|
||||
for iom, host in iom_list:
|
||||
# ── Network settings ───────────────────────────────────────────────────
|
||||
net_ok, net_data = _redfish_request(
|
||||
password, "GET",
|
||||
f"/redfish/v1/Managers/{iom}/EthernetInterfaces/1",
|
||||
host=host,
|
||||
)
|
||||
if net_ok and isinstance(net_data, dict):
|
||||
dhcp, ip_addr, gw, nm, mac, hostname, link = _parse_network_data(net_data)
|
||||
mode = _c(C.CYN, "DHCP") if dhcp else _c(C.GRN, "Static")
|
||||
net_rows.append([iom, mode, ip_addr, gw, nm, link])
|
||||
iface_rows.append([iom, mac, hostname])
|
||||
else:
|
||||
net_rows.append([iom, _c(C.RED, "No response"), "--", "--", "--", "--"])
|
||||
iface_rows.append([iom, "--", "--"])
|
||||
error(f"{iom} network query failed: {net_data}")
|
||||
|
||||
# ── Firmware versions (reuse shared redfish helpers) ───────────────────
|
||||
iom_ver = _get_iom_fw_version(password, host, iom)
|
||||
fab_ver = _get_fabric_fw_version(password, host, iom)
|
||||
fw_rows.append([iom, iom_ver, fab_ver])
|
||||
|
||||
_print_results(net_rows, iface_rows, fw_rows)
|
||||
prompt("Press Enter to return to main menu")
|
||||
|
||||
|
||||
# ── 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