From 62395a7dfe81ea635c6c825268dc6d7e2dd7c18e Mon Sep 17 00:00:00 2001 From: scott Date: Thu, 18 Jun 2026 11:10:32 -0400 Subject: [PATCH] Add network-based IOM network configuration workflow (option 1 sub-menu) Adds workflow_network.py implementing direct Redfish PATCH over the network for cases where the IOM is already reachable (e.g. on DHCP) and needs reconfiguration. Uses the same two-step PATCH workaround as the serial path: pass 1 sets IPv4StaticAddresses, pass 2 disables DHCPv4. Option 1 in the main menu now presents a Serial / Network / Back sub-menu before dispatching. Co-Authored-By: Claude Sonnet 4.6 --- es24n_conf.py | 23 +++- modules/workflow_network.py | 229 ++++++++++++++++++++++++++++++++++++ 2 files changed, 249 insertions(+), 3 deletions(-) create mode 100644 modules/workflow_network.py diff --git a/es24n_conf.py b/es24n_conf.py index bb0b09b..3aff218 100755 --- a/es24n_conf.py +++ b/es24n_conf.py @@ -24,6 +24,7 @@ sys.path.insert(0, os.path.join(os.path.dirname(os.path.abspath(__file__)), "mod 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_network import configure_iom_network from workflow_restart import restart_iom_workflow from workflow_serial import configure_shelf @@ -42,9 +43,25 @@ def main(): choice = prompt("Select [1-5]") if choice == "1": - another = True - while another: - another = configure_shelf() + print() + draw_box([ + f" {_c(C.BOLD, '1')} Serial Connection (IOM not yet on the network)", + f" {_c(C.BOLD, '2')} Network Connection (IOM reachable via management IP)", + f" {_c(C.BOLD, '3')} Back", + ]) + print() + sub = prompt("Select [1-3]") + if sub == "1": + another = True + while another: + another = configure_shelf() + elif sub == "2": + another = True + while another: + another = configure_iom_network() + elif sub != "3": + warn("Please enter 1, 2, or 3.") + time.sleep(1) elif choice == "2": firmware_update_workflow() elif choice == "3": diff --git a/modules/workflow_network.py b/modules/workflow_network.py new file mode 100644 index 0000000..94abd68 --- /dev/null +++ b/modules/workflow_network.py @@ -0,0 +1,229 @@ +""" +workflow_network.py — Network-based ES24N IOM network configuration workflow. +Connects to the IOM via its existing management IP address using Admin credentials +and reconfigures network settings via the Redfish API. +Used when the IOM is already reachable on the network (e.g. currently using DHCP) +and needs its IP configuration changed. +""" + +import time +from typing import Optional + +from models import IOMConfig +from redfish import _redfish_request +from ui import ( + _c, C, + banner, rule, draw_table, draw_box, + info, ok, warn, error, + prompt, prompt_ip, prompt_yn, prompt_password, +) + + +def _sanitize(value: str) -> str: + return "".join(c for c in value if 32 <= ord(c) < 128) + + +def _fetch_and_display(password: str, ip: str, iom: str) -> Optional[IOMConfig]: + """ + Query and display the current network settings for the given IOM. + Returns an IOMConfig populated with current values, or None on failure. + """ + info(f"Querying {iom} at {ip}...") + ok_flag, data = _redfish_request( + password, "GET", + f"/redfish/v1/Managers/{iom}/EthernetInterfaces/1", + host=ip, + ) + + if not (ok_flag and isinstance(data, dict)): + error(f"{iom} query failed: {data}") + draw_table( + ["IOM", "Mode", "IP Address", "Gateway", "Subnet Mask"], + [[iom, _c(C.RED, "No response"), "--", "--", "--"]], + [5, 8, 15, 15, 15], + ) + print() + return None + + 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 = _sanitize(addr.get("Address", "--")) + gateway = _sanitize(addr.get("Gateway", "--")) + netmask = _sanitize(addr.get("SubnetMask", "--")) + else: + ip_addr = gateway = netmask = "--" + + mode_str = _c(C.CYN, "DHCP") if dhcp_enabled else _c(C.GRN, "Static") + draw_table( + ["IOM", "Mode", "IP Address", "Gateway", "Subnet Mask"], + [[iom, mode_str, ip_addr, gateway, netmask]], + [5, 8, 15, 15, 15], + ) + print() + + return IOMConfig( + iom = iom, + dhcp = dhcp_enabled, + ip = ip_addr if ip_addr != "--" else "", + gateway = gateway if gateway != "--" else "", + netmask = netmask if netmask != "--" else "", + ) + + +def _collect_new_config(iom: str) -> Optional[IOMConfig]: + """ + Prompt the user for the desired new network configuration. + Returns a populated IOMConfig, or None if the user opts out. + """ + print(f" How should {iom} be configured?") + print(f" {_c(C.BOLD, '1')} Static IP address") + print(f" {_c(C.BOLD, '2')} DHCP") + print(f" {_c(C.BOLD, '3')} Leave settings as-is") + 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 == "3": + info("No changes requested.") + return None + + if choice == "2": + ok(f"{iom} will be set to DHCP.") + return IOMConfig(iom, dhcp=True) + + # Static IP + print() + info(f"Static network details for {_c(C.BOLD, iom)}:") + new_ip = prompt_ip(f" {iom} IP address ") + gateway = prompt_ip(f" {iom} Gateway ") + netmask = prompt_ip(f" {iom} Subnet Mask") + return IOMConfig(iom, dhcp=False, ip=new_ip, gateway=gateway, netmask=netmask) + + +def _apply_config(password: str, host: str, iom_cfg: IOMConfig) -> tuple: + """ + Apply network configuration via direct Redfish PATCH. + + DHCP: single PATCH enabling DHCPv4. + + Static: two sequential PATCHes to work around the ES24N firmware bug + that rejects a single request combining IPv4StaticAddresses and DHCPv4 disable. + Pass 1 — set IPv4StaticAddresses (DHCP still enabled) + Pass 2 — disable DHCPv4 + """ + path = f"/redfish/v1/Managers/{iom_cfg.iom}/EthernetInterfaces/1" + + if iom_cfg.dhcp: + ok_flag, data = _redfish_request( + password, "PATCH", path, + {"DHCPv4": {"DHCPEnabled": True}}, + host=host, + ) + return (True, "Configured: DHCP") if ok_flag else (False, str(data)[:80]) + + # Static — Pass 1: set address while DHCP still enabled + info(f" {iom_cfg.iom} pass 1/2 — setting static address {iom_cfg.ip}...") + ok_flag, data = _redfish_request( + password, "PATCH", path, + { + "IPv4StaticAddresses": [{ + "Address": iom_cfg.ip, + "Gateway": iom_cfg.gateway, + "SubnetMask": iom_cfg.netmask, + }] + }, + host=host, + ) + if not ok_flag: + return False, f"Pass 1 failed: {str(data)[:70]}" + + time.sleep(1) + + # Static — Pass 2: disable DHCP + info(f" {iom_cfg.iom} pass 2/2 — disabling DHCP...") + ok_flag, data = _redfish_request( + password, "PATCH", path, + {"DHCPv4": {"DHCPEnabled": False}}, + host=host, + ) + if not ok_flag: + return False, f"Pass 2 failed: {str(data)[:70]}" + + return True, f"Configured: Static {iom_cfg.ip}" + + +def configure_iom_network() -> bool: + """ + Run one complete network-based IOM configuration cycle. + Returns True if the user wants to configure another IOM. + """ + banner() + rule("Configure Network Settings — Network Connection") + + print() + password = prompt_password() + print() + ip = prompt_ip(" Current IOM IP address (IOM1 or IOM2)") + + print() + print(" Which IOM would you like to configure?") + 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"Current {iom} Network Settings") + current = _fetch_and_display(password, ip, iom) + if current is None: + warn("Check that the IP is correct and this system can reach the IOM.") + print() + return prompt_yn("Try another IOM?", default=False) + + rule("New Configuration") + new_cfg = _collect_new_config(iom) + if new_cfg is None: + print() + return prompt_yn("Configure another IOM?", default=False) + + print() + rule("Ready to Apply") + info("Redfish PATCH requests will be sent directly to the IOM.") + if not new_cfg.dhcp: + warn(f"The IOM will move to {new_cfg.ip} — this connection will stop working after the change.") + print() + if not prompt_yn("Apply configuration now?", default=True): + warn("Configuration skipped — no changes were made.") + print() + return prompt_yn("Configure another IOM?", default=False) + + rule("Applying Configuration") + success, detail = _apply_config(password, ip, new_cfg) + status = _c(C.GRN, "OK") if success else _c(C.RED, "FAIL") + draw_table(["IOM", "Result", "Detail"], [[iom, status, detail]], [6, 8, 50]) + print() + + if success: + draw_box([ + f"{_c(C.YEL, 'IMPORTANT — Per the ES24N Service Guide:')}", + "", + "Verify the expander appears in TrueNAS with matching drives:", + "TrueNAS > System Settings > Enclosure >", + "NVMe-oF Expansion Shelves", + ], colour=C.YEL) + print() + + return prompt_yn("Configure another IOM?", default=False)