Pull Shelf info upon serial connect

This commit is contained in:
2026-03-04 07:59:44 -05:00
parent 4b3c7abfda
commit a6ba8ebe4e

View File

@@ -300,7 +300,7 @@ class SerialPort:
# ── Step 1: Detect serial device ────────────────────────────────────────────── # ── Step 1: Detect serial device ──────────────────────────────────────────────
def detect_serial_device() -> Optional[str]: def detect_serial_device() -> Optional[str]:
rule("Step 1 of 6 -- Serial Cable & Device Detection") rule("Step 1 of 5 -- Serial Cable & Device Detection")
print(" Connect the serial cable from the ES24N IOM1 port") print(" Connect the serial cable from the ES24N IOM1 port")
print(" to the active F-Series controller USB port.") print(" to the active F-Series controller USB port.")
@@ -376,7 +376,7 @@ def _fix_permissions(device: str):
# ── Step 2: Open serial connection & wake IOM console ───────────────────────── # ── Step 2: Open serial connection & wake IOM console ─────────────────────────
def open_serial_connection(device: str) -> Optional[SerialPort]: def open_serial_connection(device: str) -> Optional[SerialPort]:
rule("Step 2 of 6 -- Opening Serial Connection") rule("Step 2 of 5 -- Opening Serial Connection")
info(f"Opening {device} at 115200 baud (8N1)...") info(f"Opening {device} at 115200 baud (8N1)...")
ser = SerialPort(device, baudrate=115200, timeout=5.0) ser = SerialPort(device, baudrate=115200, timeout=5.0)
@@ -416,13 +416,127 @@ def open_serial_connection(device: str) -> Optional[SerialPort]:
return ser return ser
# ── Step 3: Collect network configuration ───────────────────────────────────── # ── Redfish helpers (GET and PATCH) ──────────────────────────────────────────
def collect_network_config(cfg: ShelfConfig): def _redfish_request(password: str, method: str, path: str,
rule("Step 3 of 6 -- IOM Network Configuration") payload: Optional[dict] = None) -> tuple:
"""
Issue a Redfish HTTP request over the loopback (127.0.0.1).
Returns (success: bool, data: dict|str).
"""
url = f"https://127.0.0.1{path}"
credentials = b64encode(f"Admin:{password}".encode()).decode()
ctx = ssl.create_default_context()
ctx.check_hostname = False
ctx.verify_mode = ssl.CERT_NONE
print(" How should the IOMs be configured?") body = json.dumps(payload).encode("utf-8") if payload else None
print(f" {_c(C.BOLD, '1')} Static IP addresses") headers = {"Authorization": f"Basic {credentials}"}
print(f" {_c(C.BOLD, '2')} DHCP") if body:
headers["Content-Type"] = "application/json"
req = urllib.request.Request(url, data=body, method=method, headers=headers)
try:
with urllib.request.urlopen(req, context=ctx, timeout=10) as resp:
raw = resp.read().decode("utf-8", errors="replace")
try:
return True, json.loads(raw) if raw.strip() else {}
except json.JSONDecodeError:
return True, {}
except urllib.error.HTTPError as e:
raw = e.read().decode("utf-8", errors="replace")
try:
msg = json.loads(raw).get("error", {}).get("message", raw)
except json.JSONDecodeError:
msg = raw
return False, f"HTTP {e.code}: {msg[:120]}"
except OSError as e:
return False, f"Connection error: {e}"
# ── Step 3: Fetch & display current IOM network settings ─────────────────────
def fetch_current_config(cfg: ShelfConfig) -> bool:
"""
Query Redfish for the current network config of both IOMs.
Populates cfg.iom1 / cfg.iom2 with live data.
Returns True if at least one IOM responded.
"""
rule("Step 3 of 5 -- Current IOM Network Settings")
info("Querying Redfish API for current network configuration...")
print()
any_ok = False
rows = []
for iom in ("IOM1", "IOM2"):
path = f"/redfish/v1/Managers/{iom}/EthernetInterfaces/1"
ok_flag, data = _redfish_request(cfg.password, "GET", path)
if ok_flag and isinstance(data, dict):
any_ok = True
# Determine mode
dhcp_enabled = (
data.get("DHCPv4", {}).get("DHCPEnabled", False) or
data.get("DHCPv6", {}).get("DHCPEnabled", False)
)
# Pull address info — prefer StaticAddresses, fall back to IPv4Addresses
addrs = data.get("IPv4StaticAddresses") or data.get("IPv4Addresses", [])
if addrs:
addr_rec = addrs[0]
ip = addr_rec.get("Address", "--")
gateway = addr_rec.get("Gateway", "--")
netmask = addr_rec.get("SubnetMask", "--")
else:
ip = gateway = netmask = "--"
origin = data.get("IPv4Addresses", [{}])[0].get("AddressOrigin", "Unknown") \
if data.get("IPv4Addresses") else ("DHCP" if dhcp_enabled else "Static")
iom_cfg = IOMConfig(
iom = iom,
dhcp = dhcp_enabled,
ip = ip if ip != "--" else "",
gateway = gateway if gateway != "--" else "",
netmask = netmask if netmask != "--" else "",
)
if iom == "IOM1":
cfg.iom1 = iom_cfg
else:
cfg.iom2 = iom_cfg
mode_str = f"{_c(C.CYN, 'DHCP')}" if dhcp_enabled else f"{_c(C.GRN, 'Static')}"
rows.append([iom, mode_str, origin, ip, gateway, netmask])
else:
rows.append([iom, _c(C.RED, "No response"), "--", "--", "--", "--"])
draw_table(
["IOM", "Mode", "Origin", "IP Address", "Gateway", "Subnet Mask"],
rows,
[5, 10, 8, 16, 16, 16],
)
print()
if not any_ok:
error("Neither IOM responded to the Redfish query.")
error("Check that the serial cable is connected and the IOM is booted.")
return any_ok
# ── Step 4: Prompt user — change config or exit ───────────────────────────────
def collect_network_config(cfg: ShelfConfig) -> bool:
"""
Show current settings, ask user what to do.
Returns True to proceed with applying changes, False to skip.
"""
rule("Step 4 of 5 -- Change Configuration?")
print(f" {_c(C.BOLD, '1')} Change network configuration")
print(f" {_c(C.BOLD, '2')} Leave settings as-is and disconnect")
print() print()
while True: while True:
@@ -431,20 +545,33 @@ def collect_network_config(cfg: ShelfConfig):
break break
warn("Please enter 1 or 2.") warn("Please enter 1 or 2.")
use_dhcp = (choice == "2") if choice == "2":
info("No changes requested.")
return False
# ── User wants to change settings ─────────────────────────────────────────
print()
print(" How should the IOMs be configured?")
print(f" {_c(C.BOLD, '1')} Static IP addresses")
print(f" {_c(C.BOLD, '2')} DHCP")
print() print()
cfg.password = prompt_password() while True:
mode = prompt("Select mode [1/2]")
if mode in ("1", "2"):
break
warn("Please enter 1 or 2.")
use_dhcp = (mode == "2")
print()
if use_dhcp: if use_dhcp:
cfg.iom1 = IOMConfig("IOM1", dhcp=True) cfg.iom1 = IOMConfig("IOM1", dhcp=True)
cfg.iom2 = IOMConfig("IOM2", dhcp=True) cfg.iom2 = IOMConfig("IOM2", dhcp=True)
print()
ok("Both IOMs will be set to DHCP.") ok("Both IOMs will be set to DHCP.")
return return True
# Static — IOM1 # Static — IOM1
print()
info(f"Static network details for {_c(C.BOLD, 'IOM1')}:") info(f"Static network details for {_c(C.BOLD, 'IOM1')}:")
iom1_ip = prompt_ip(" IOM1 IP address ") iom1_ip = prompt_ip(" IOM1 IP address ")
iom1_gw = prompt_ip(" IOM1 Gateway ") iom1_gw = prompt_ip(" IOM1 Gateway ")
@@ -464,11 +591,12 @@ def collect_network_config(cfg: ShelfConfig):
iom2_nm = prompt_ip(" IOM2 Subnet Mask") iom2_nm = prompt_ip(" IOM2 Subnet Mask")
cfg.iom2 = IOMConfig("IOM2", dhcp=False, ip=iom2_ip, gateway=iom2_gw, netmask=iom2_nm) cfg.iom2 = IOMConfig("IOM2", dhcp=False, ip=iom2_ip, gateway=iom2_gw, netmask=iom2_nm)
return True
# ── Step 4: Apply configuration via Redfish ────────────────────────────────── # ── Step 5a: Apply configuration via Redfish ──────────────────────────────────
def apply_configuration(cfg: ShelfConfig) -> bool: def apply_configuration(cfg: ShelfConfig) -> bool:
rule("Step 4 of 6 -- Applying Configuration via Redfish API") rule("Step 5 of 5 -- Applying Configuration via Redfish API")
info("Sending Redfish PATCH requests over serial loopback (127.0.0.1)...") info("Sending Redfish PATCH requests over serial loopback (127.0.0.1)...")
print() print()
@@ -476,72 +604,36 @@ def apply_configuration(cfg: ShelfConfig) -> bool:
results = [] results = []
all_ok = True all_ok = True
for iom_cfg in [cfg.iom1, cfg.iom2]: for iom_cfg in [cfg.iom1, cfg.iom2]:
success, message = _patch_iom(cfg.password, iom_cfg) if iom_cfg.dhcp:
results.append([iom_cfg.iom,
f"{_c(C.GRN, 'OK')}" if success else f"{_c(C.RED, 'FAIL')}",
message])
if not success:
all_ok = False
draw_table(["IOM", "Result", "Detail"], results, [6, 8, 44])
print()
return all_ok
def _patch_iom(password: str, iom: IOMConfig) -> tuple:
url = f"https://127.0.0.1/redfish/v1/Managers/{iom.iom}/EthernetInterfaces/1"
if iom.dhcp:
payload = {"DHCPv4": {"DHCPEnabled": True}} payload = {"DHCPv4": {"DHCPEnabled": True}}
else: else:
payload = { payload = {
"DHCPv4": {"DHCPEnabled": False}, "DHCPv4": {"DHCPEnabled": False},
"IPv4StaticAddresses": [{ "IPv4StaticAddresses": [{
"Address": iom.ip, "Address": iom_cfg.ip,
"Gateway": iom.gateway, "Gateway": iom_cfg.gateway,
"SubnetMask": iom.netmask, "SubnetMask": iom_cfg.netmask,
}], }],
} }
data = json.dumps(payload).encode("utf-8") path = f"/redfish/v1/Managers/{iom_cfg.iom}/EthernetInterfaces/1"
credentials = b64encode(f"Admin:{password}".encode()).decode() success, data = _redfish_request(cfg.password, "PATCH", path, payload)
ctx = ssl.create_default_context()
ctx.check_hostname = False
ctx.verify_mode = ssl.CERT_NONE
req = urllib.request.Request( if success:
url, mode = "DHCP" if iom_cfg.dhcp else f"Static {iom_cfg.ip}"
data=data, results.append([iom_cfg.iom, _c(C.GRN, "OK"), f"Configured: {mode}"])
method="PATCH", else:
headers={ results.append([iom_cfg.iom, _c(C.RED, "FAIL"), str(data)[:60]])
"Content-Type": "application/json", all_ok = False
"Authorization": f"Basic {credentials}",
},
)
try: draw_table(["IOM", "Result", "Detail"], results, [6, 8, 50])
with urllib.request.urlopen(req, context=ctx, timeout=10) as resp: print()
if resp.status in (200, 204): return all_ok
mode = "DHCP" if iom.dhcp else f"static {iom.ip}"
return True, f"Configured: {mode}"
body = resp.read().decode("utf-8", errors="replace")
return False, f"HTTP {resp.status}: {body[:80]}"
except urllib.error.HTTPError as e:
body = e.read().decode("utf-8", errors="replace")
try:
msg = json.loads(body).get("error", {}).get("message", body)
except json.JSONDecodeError:
msg = body
return False, f"HTTP {e.code}: {msg[:80]}"
except OSError as e:
return False, f"Connection error: {e}"
# ── Step 5: Print configuration summary ─────────────────────────────────────── # ── Step 5b: Print applied-settings summary ───────────────────────────────────
def print_summary(cfg: ShelfConfig): def print_summary(cfg: ShelfConfig, changed: bool):
rule("Step 5 of 6 -- Configuration Summary") rule("Summary")
def val(iom: IOMConfig, field: str) -> str: def val(iom: IOMConfig, field: str) -> str:
dhcp_map = {"mode": "DHCP", "ip": "-- (DHCP)", "gateway": "-- (DHCP)", "netmask": "-- (DHCP)"} dhcp_map = {"mode": "DHCP", "ip": "-- (DHCP)", "gateway": "-- (DHCP)", "netmask": "-- (DHCP)"}
@@ -556,10 +648,12 @@ def print_summary(cfg: ShelfConfig):
["Gateway", val(cfg.iom1, "gateway"), val(cfg.iom2, "gateway")], ["Gateway", val(cfg.iom1, "gateway"), val(cfg.iom2, "gateway")],
["Subnet Mask", val(cfg.iom1, "netmask"), val(cfg.iom2, "netmask")], ["Subnet Mask", val(cfg.iom1, "netmask"), val(cfg.iom2, "netmask")],
["Serial Port", cfg.device, cfg.device], ["Serial Port", cfg.device, cfg.device],
["Changes", "Yes" if changed else "None", ""],
], ],
[12, 22, 22], [12, 22, 22],
) )
if changed:
print() print()
draw_box([ draw_box([
f"{_c(C.YEL, 'IMPORTANT -- Per the ES24N Service Guide:')}", f"{_c(C.YEL, 'IMPORTANT -- Per the ES24N Service Guide:')}",
@@ -573,22 +667,20 @@ def print_summary(cfg: ShelfConfig):
print() print()
# ── Step 6: Close serial connection ─────────────────────────────────────────── # ── Disconnect ────────────────────────────────────────────────────────────────
def close_serial_connection(ser: SerialPort, device: str): def close_serial_connection(ser: SerialPort, device: str):
rule("Step 6 of 6 -- Close Serial Connection")
if ser and ser.is_open: if ser and ser.is_open:
ser.close() ser.close()
ok(f"Serial port {device} closed.") ok(f"Serial port {device} closed.")
print() print()
prompt("Disconnect the serial cable, then press Enter to continue") prompt("Disconnect the serial cable, then press Enter to continue")
ok("Serial cable disconnected. Shelf configuration complete.") ok("Serial cable disconnected. Shelf complete.")
# ── Full shelf configuration cycle ──────────────────────────────────────────── # ── Full shelf configuration cycle ────────────────────────────────────────────
def configure_shelf() -> bool: def configure_shelf() -> bool:
"""Run one complete shelf cycle. Returns True if user wants another.""" """Run one complete shelf cycle. Returns True if user wants another shelf."""
banner() banner()
cfg = ShelfConfig() cfg = ShelfConfig()
@@ -600,32 +692,40 @@ def configure_shelf() -> bool:
return True return True
cfg.device = device cfg.device = device
# 2 — Open serial port # 2 — Open serial port & wake IOM console
ser = open_serial_connection(device) ser = open_serial_connection(device)
if not ser: if not ser:
error("Could not open serial port. Returning to main menu.") error("Could not open serial port. Returning to main menu.")
time.sleep(2) time.sleep(2)
return True return True
# 3 — Collect settings # Password needed before any Redfish calls
collect_network_config(cfg) print()
cfg.password = prompt_password()
# 4Confirm & apply # 3Fetch & display current settings
fetch_current_config(cfg)
# 4 — Ask user: change or leave alone?
apply_changes = collect_network_config(cfg)
# 5 — Apply if requested
changed = False
if apply_changes:
print() print()
rule("Ready to Apply") rule("Ready to Apply")
info("Redfish PATCH requests will be sent to each IOM via 127.0.0.1") info("Redfish PATCH requests will be sent to each IOM via 127.0.0.1")
info("using the active serial session as the communication path.")
print() print()
if prompt_yn("Apply configuration now?", default=True): if prompt_yn("Apply configuration now?", default=True):
apply_configuration(cfg) apply_configuration(cfg)
changed = True
else: else:
warn("Configuration skipped — no changes were made.") warn("Configuration skipped — no changes were made.")
# 5 — Summary & reminder # Summary
print_summary(cfg) print_summary(cfg, changed)
# 6 — Close serial port # Disconnect
close_serial_connection(ser, device) close_serial_connection(ser, device)
print() print()