Pull Shelf info upon serial connect
This commit is contained in:
318
es24n_conf.py
318
es24n_conf.py
@@ -300,7 +300,7 @@ class SerialPort:
|
||||
|
||||
# ── Step 1: Detect serial device ──────────────────────────────────────────────
|
||||
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(" 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 ─────────────────────────
|
||||
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)...")
|
||||
ser = SerialPort(device, baudrate=115200, timeout=5.0)
|
||||
@@ -416,13 +416,127 @@ def open_serial_connection(device: str) -> Optional[SerialPort]:
|
||||
return ser
|
||||
|
||||
|
||||
# ── Step 3: Collect network configuration ─────────────────────────────────────
|
||||
def collect_network_config(cfg: ShelfConfig):
|
||||
rule("Step 3 of 6 -- IOM Network Configuration")
|
||||
# ── Redfish helpers (GET and PATCH) ──────────────────────────────────────────
|
||||
def _redfish_request(password: str, method: str, path: str,
|
||||
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?")
|
||||
print(f" {_c(C.BOLD, '1')} Static IP addresses")
|
||||
print(f" {_c(C.BOLD, '2')} DHCP")
|
||||
body = json.dumps(payload).encode("utf-8") if payload else None
|
||||
headers = {"Authorization": f"Basic {credentials}"}
|
||||
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()
|
||||
|
||||
while True:
|
||||
@@ -431,20 +545,33 @@ def collect_network_config(cfg: ShelfConfig):
|
||||
break
|
||||
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()
|
||||
|
||||
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:
|
||||
cfg.iom1 = IOMConfig("IOM1", dhcp=True)
|
||||
cfg.iom2 = IOMConfig("IOM2", dhcp=True)
|
||||
print()
|
||||
ok("Both IOMs will be set to DHCP.")
|
||||
return
|
||||
return True
|
||||
|
||||
# Static — IOM1
|
||||
print()
|
||||
info(f"Static network details for {_c(C.BOLD, 'IOM1')}:")
|
||||
iom1_ip = prompt_ip(" IOM1 IP address ")
|
||||
iom1_gw = prompt_ip(" IOM1 Gateway ")
|
||||
@@ -464,84 +591,49 @@ def collect_network_config(cfg: ShelfConfig):
|
||||
iom2_nm = prompt_ip(" IOM2 Subnet Mask")
|
||||
|
||||
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:
|
||||
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)...")
|
||||
print()
|
||||
|
||||
results = []
|
||||
all_ok = True
|
||||
results = []
|
||||
all_ok = True
|
||||
for iom_cfg in [cfg.iom1, cfg.iom2]:
|
||||
success, message = _patch_iom(cfg.password, iom_cfg)
|
||||
results.append([iom_cfg.iom,
|
||||
f"{_c(C.GRN, 'OK')}" if success else f"{_c(C.RED, 'FAIL')}",
|
||||
message])
|
||||
if not success:
|
||||
if iom_cfg.dhcp:
|
||||
payload = {"DHCPv4": {"DHCPEnabled": True}}
|
||||
else:
|
||||
payload = {
|
||||
"DHCPv4": {"DHCPEnabled": False},
|
||||
"IPv4StaticAddresses": [{
|
||||
"Address": iom_cfg.ip,
|
||||
"Gateway": iom_cfg.gateway,
|
||||
"SubnetMask": iom_cfg.netmask,
|
||||
}],
|
||||
}
|
||||
|
||||
path = f"/redfish/v1/Managers/{iom_cfg.iom}/EthernetInterfaces/1"
|
||||
success, data = _redfish_request(cfg.password, "PATCH", path, payload)
|
||||
|
||||
if success:
|
||||
mode = "DHCP" if iom_cfg.dhcp else f"Static {iom_cfg.ip}"
|
||||
results.append([iom_cfg.iom, _c(C.GRN, "OK"), f"Configured: {mode}"])
|
||||
else:
|
||||
results.append([iom_cfg.iom, _c(C.RED, "FAIL"), str(data)[:60]])
|
||||
all_ok = False
|
||||
|
||||
draw_table(["IOM", "Result", "Detail"], results, [6, 8, 44])
|
||||
draw_table(["IOM", "Result", "Detail"], results, [6, 8, 50])
|
||||
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}}
|
||||
else:
|
||||
payload = {
|
||||
"DHCPv4": {"DHCPEnabled": False},
|
||||
"IPv4StaticAddresses": [{
|
||||
"Address": iom.ip,
|
||||
"Gateway": iom.gateway,
|
||||
"SubnetMask": iom.netmask,
|
||||
}],
|
||||
}
|
||||
|
||||
data = json.dumps(payload).encode("utf-8")
|
||||
credentials = b64encode(f"Admin:{password}".encode()).decode()
|
||||
ctx = ssl.create_default_context()
|
||||
ctx.check_hostname = False
|
||||
ctx.verify_mode = ssl.CERT_NONE
|
||||
|
||||
req = urllib.request.Request(
|
||||
url,
|
||||
data=data,
|
||||
method="PATCH",
|
||||
headers={
|
||||
"Content-Type": "application/json",
|
||||
"Authorization": f"Basic {credentials}",
|
||||
},
|
||||
)
|
||||
|
||||
try:
|
||||
with urllib.request.urlopen(req, context=ctx, timeout=10) as resp:
|
||||
if resp.status in (200, 204):
|
||||
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 ───────────────────────────────────────
|
||||
def print_summary(cfg: ShelfConfig):
|
||||
rule("Step 5 of 6 -- Configuration Summary")
|
||||
# ── Step 5b: Print applied-settings summary ───────────────────────────────────
|
||||
def print_summary(cfg: ShelfConfig, changed: bool):
|
||||
rule("Summary")
|
||||
|
||||
def val(iom: IOMConfig, field: str) -> str:
|
||||
dhcp_map = {"mode": "DHCP", "ip": "-- (DHCP)", "gateway": "-- (DHCP)", "netmask": "-- (DHCP)"}
|
||||
@@ -556,39 +648,39 @@ def print_summary(cfg: ShelfConfig):
|
||||
["Gateway", val(cfg.iom1, "gateway"), val(cfg.iom2, "gateway")],
|
||||
["Subnet Mask", val(cfg.iom1, "netmask"), val(cfg.iom2, "netmask")],
|
||||
["Serial Port", cfg.device, cfg.device],
|
||||
["Changes", "Yes" if changed else "None", ""],
|
||||
],
|
||||
[12, 22, 22],
|
||||
)
|
||||
|
||||
print()
|
||||
draw_box([
|
||||
f"{_c(C.YEL, 'IMPORTANT -- Per the ES24N Service Guide:')}",
|
||||
"",
|
||||
"Remove the serial cable ONLY after verifying each",
|
||||
"expander appears in TrueNAS with matching drives.",
|
||||
"",
|
||||
"TrueNAS > System Settings > Enclosure >",
|
||||
"NVMe-oF Expansion Shelves",
|
||||
], colour=C.YEL)
|
||||
if changed:
|
||||
print()
|
||||
draw_box([
|
||||
f"{_c(C.YEL, 'IMPORTANT -- Per the ES24N Service Guide:')}",
|
||||
"",
|
||||
"Remove the serial cable ONLY after verifying each",
|
||||
"expander appears in TrueNAS with matching drives.",
|
||||
"",
|
||||
"TrueNAS > System Settings > Enclosure >",
|
||||
"NVMe-oF Expansion Shelves",
|
||||
], colour=C.YEL)
|
||||
print()
|
||||
|
||||
|
||||
# ── Step 6: Close serial connection ───────────────────────────────────────────
|
||||
# ── Disconnect ────────────────────────────────────────────────────────────────
|
||||
def close_serial_connection(ser: SerialPort, device: str):
|
||||
rule("Step 6 of 6 -- Close Serial Connection")
|
||||
|
||||
if ser and ser.is_open:
|
||||
ser.close()
|
||||
ok(f"Serial port {device} closed.")
|
||||
|
||||
print()
|
||||
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 ────────────────────────────────────────────
|
||||
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()
|
||||
cfg = ShelfConfig()
|
||||
|
||||
@@ -600,32 +692,40 @@ def configure_shelf() -> bool:
|
||||
return True
|
||||
cfg.device = device
|
||||
|
||||
# 2 — Open serial port
|
||||
# 2 — Open serial port & wake IOM console
|
||||
ser = open_serial_connection(device)
|
||||
if not ser:
|
||||
error("Could not open serial port. Returning to main menu.")
|
||||
time.sleep(2)
|
||||
return True
|
||||
|
||||
# 3 — Collect settings
|
||||
collect_network_config(cfg)
|
||||
|
||||
# 4 — Confirm & apply
|
||||
print()
|
||||
rule("Ready to Apply")
|
||||
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.")
|
||||
# Password needed before any Redfish calls
|
||||
print()
|
||||
cfg.password = prompt_password()
|
||||
|
||||
if prompt_yn("Apply configuration now?", default=True):
|
||||
apply_configuration(cfg)
|
||||
else:
|
||||
warn("Configuration skipped — no changes were made.")
|
||||
# 3 — Fetch & display current settings
|
||||
fetch_current_config(cfg)
|
||||
|
||||
# 5 — Summary & reminder
|
||||
print_summary(cfg)
|
||||
# 4 — Ask user: change or leave alone?
|
||||
apply_changes = collect_network_config(cfg)
|
||||
|
||||
# 6 — Close serial port
|
||||
# 5 — Apply if requested
|
||||
changed = False
|
||||
if apply_changes:
|
||||
print()
|
||||
rule("Ready to Apply")
|
||||
info("Redfish PATCH requests will be sent to each IOM via 127.0.0.1")
|
||||
print()
|
||||
if prompt_yn("Apply configuration now?", default=True):
|
||||
apply_configuration(cfg)
|
||||
changed = True
|
||||
else:
|
||||
warn("Configuration skipped — no changes were made.")
|
||||
|
||||
# Summary
|
||||
print_summary(cfg, changed)
|
||||
|
||||
# Disconnect
|
||||
close_serial_connection(ser, device)
|
||||
|
||||
print()
|
||||
@@ -670,4 +770,4 @@ if __name__ == "__main__":
|
||||
except KeyboardInterrupt:
|
||||
print()
|
||||
warn("Interrupted. Exiting.")
|
||||
sys.exit(0)
|
||||
sys.exit(0)
|
||||
Reference in New Issue
Block a user