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:
@@ -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
229
modules/workflow_network.py
Normal 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)
|
||||
Reference in New Issue
Block a user