Pull Shelf info upon serial connect
This commit is contained in:
316
es24n_conf.py
316
es24n_conf.py
@@ -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,84 +591,49 @@ 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()
|
||||||
|
|
||||||
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,
|
payload = {"DHCPv4": {"DHCPEnabled": True}}
|
||||||
f"{_c(C.GRN, 'OK')}" if success else f"{_c(C.RED, 'FAIL')}",
|
else:
|
||||||
message])
|
payload = {
|
||||||
if not success:
|
"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
|
all_ok = False
|
||||||
|
|
||||||
draw_table(["IOM", "Result", "Detail"], results, [6, 8, 44])
|
draw_table(["IOM", "Result", "Detail"], results, [6, 8, 50])
|
||||||
print()
|
print()
|
||||||
return all_ok
|
return all_ok
|
||||||
|
|
||||||
|
|
||||||
def _patch_iom(password: str, iom: IOMConfig) -> tuple:
|
# ── Step 5b: Print applied-settings summary ───────────────────────────────────
|
||||||
url = f"https://127.0.0.1/redfish/v1/Managers/{iom.iom}/EthernetInterfaces/1"
|
def print_summary(cfg: ShelfConfig, changed: bool):
|
||||||
|
rule("Summary")
|
||||||
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")
|
|
||||||
|
|
||||||
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,39 +648,39 @@ 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],
|
||||||
)
|
)
|
||||||
|
|
||||||
print()
|
if changed:
|
||||||
draw_box([
|
print()
|
||||||
f"{_c(C.YEL, 'IMPORTANT -- Per the ES24N Service Guide:')}",
|
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.",
|
"Remove the serial cable ONLY after verifying each",
|
||||||
"",
|
"expander appears in TrueNAS with matching drives.",
|
||||||
"TrueNAS > System Settings > Enclosure >",
|
"",
|
||||||
"NVMe-oF Expansion Shelves",
|
"TrueNAS > System Settings > Enclosure >",
|
||||||
], colour=C.YEL)
|
"NVMe-oF Expansion Shelves",
|
||||||
|
], colour=C.YEL)
|
||||||
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)
|
|
||||||
|
|
||||||
# 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.")
|
|
||||||
print()
|
print()
|
||||||
|
cfg.password = prompt_password()
|
||||||
|
|
||||||
if prompt_yn("Apply configuration now?", default=True):
|
# 3 — Fetch & display current settings
|
||||||
apply_configuration(cfg)
|
fetch_current_config(cfg)
|
||||||
else:
|
|
||||||
warn("Configuration skipped — no changes were made.")
|
|
||||||
|
|
||||||
# 5 — Summary & reminder
|
# 4 — Ask user: change or leave alone?
|
||||||
print_summary(cfg)
|
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)
|
close_serial_connection(ser, device)
|
||||||
|
|
||||||
print()
|
print()
|
||||||
|
|||||||
Reference in New Issue
Block a user