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 <noreply@anthropic.com>
This commit is contained in:
2026-06-18 11:10:32 -04:00
parent 38eef32d48
commit 62395a7dfe
2 changed files with 249 additions and 3 deletions

View File

@@ -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":

229
modules/workflow_network.py Normal file
View File

@@ -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)