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 ui import _c, C, banner, draw_box, ok, warn, prompt
|
||||||
from workflow_check import system_check_workflow
|
from workflow_check import system_check_workflow
|
||||||
from workflow_firmware import firmware_update_workflow
|
from workflow_firmware import firmware_update_workflow
|
||||||
|
from workflow_network import configure_iom_network
|
||||||
from workflow_restart import restart_iom_workflow
|
from workflow_restart import restart_iom_workflow
|
||||||
from workflow_serial import configure_shelf
|
from workflow_serial import configure_shelf
|
||||||
|
|
||||||
@@ -42,9 +43,25 @@ def main():
|
|||||||
|
|
||||||
choice = prompt("Select [1-5]")
|
choice = prompt("Select [1-5]")
|
||||||
if choice == "1":
|
if choice == "1":
|
||||||
|
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
|
another = True
|
||||||
while another:
|
while another:
|
||||||
another = configure_shelf()
|
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":
|
elif choice == "2":
|
||||||
firmware_update_workflow()
|
firmware_update_workflow()
|
||||||
elif choice == "3":
|
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