Limit serial workflows to IOM1 only — IOM2 unreachable over serial
The serial cable connects to IOM1's console port. IOM2 cannot be queried or configured via the serial connection. Updated all serial-path code to reflect this: workflow_serial.py: - fetch_current_config: query IOM1 only, single-row table - collect_network_config: prompt for IOM1 settings only, drop IOM2 section - apply_configuration: apply to IOM1 only, single-row result table - print_summary: IOM1-only table, updated warning text workflow_check.py (_check_via_serial): - Query IOM1 only for network settings, IOM firmware, and Fabric Card firmware CLAUDE.md updated to document the IOM1-only serial limitation. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
10
CLAUDE.md
10
CLAUDE.md
@@ -39,12 +39,12 @@ modules/
|
|||||||
## Workflows
|
## Workflows
|
||||||
|
|
||||||
### 1 — Serial Network Configuration (`workflow_serial.py`)
|
### 1 — Serial Network Configuration (`workflow_serial.py`)
|
||||||
5-step workflow using the USB serial cable:
|
5-step workflow using the USB serial cable. **Serial access is limited to IOM1 only** — the cable connects to IOM1's console port; IOM2 cannot be queried or configured this way.
|
||||||
1. Detect USB serial device (`/dev/ttyUSB*`, `/dev/ttyACM*`, `/dev/ttyU*`)
|
1. Detect USB serial device (`/dev/ttyUSB*`, `/dev/ttyACM*`, `/dev/ttyU*`)
|
||||||
2. Open 115200-baud 8N1 connection and wake the IOM console
|
2. Open 115200-baud 8N1 connection and wake the IOM console
|
||||||
3. Query current network settings via Redfish (`GET` over `127.0.0.1`)
|
3. Query IOM1 current network settings via Redfish (`GET` over `127.0.0.1`)
|
||||||
4. Collect new settings from user (Static IP or DHCP)
|
4. Collect new settings from user (Static IP or DHCP) for IOM1
|
||||||
5. Apply via Redfish `PATCH` over `127.0.0.1`
|
5. Apply via Redfish `PATCH` over `127.0.0.1` to IOM1
|
||||||
|
|
||||||
### 2 — Firmware Update (`workflow_firmware.py`)
|
### 2 — Firmware Update (`workflow_firmware.py`)
|
||||||
Updates IOM firmware and/or Fabric Card firmware over the network:
|
Updates IOM firmware and/or Fabric Card firmware over the network:
|
||||||
@@ -58,7 +58,7 @@ Updates IOM firmware and/or Fabric Card firmware over the network:
|
|||||||
|
|
||||||
### 3 — System Check (`workflow_check.py`)
|
### 3 — System Check (`workflow_check.py`)
|
||||||
Read-only diagnostic workflow — queries current network settings and firmware versions, makes no changes:
|
Read-only diagnostic workflow — queries current network settings and firmware versions, makes no changes:
|
||||||
- **Serial:** logs in via serial console, queries both IOMs using `_serial_redfish_request()` (curl over the serial session); covers network settings, IOM firmware, and Fabric Card firmware
|
- **Serial:** logs in via serial console, queries IOM1 only using `_serial_redfish_request()` (curl over the serial session); covers network settings, IOM firmware, and Fabric Card firmware. IOM2 is not reachable over serial.
|
||||||
- **Network:** prompts for management IP(s), queries via direct HTTPS using `Admin` credentials; reuses `_redfish_request()`, `_get_iom_fw_version()`, `_get_fabric_fw_version()` from `redfish.py`
|
- **Network:** prompts for management IP(s), queries via direct HTTPS using `Admin` credentials; reuses `_redfish_request()`, `_get_iom_fw_version()`, `_get_fabric_fw_version()` from `redfish.py`
|
||||||
- Displays results in two tables: network configuration and firmware versions
|
- Displays results in two tables: network configuration and firmware versions
|
||||||
- User selects Serial, Network, or Cancel at the sub-menu prompt
|
- User selects Serial, Network, or Cancel at the sub-menu prompt
|
||||||
|
|||||||
@@ -97,31 +97,28 @@ def _check_via_serial():
|
|||||||
time.sleep(2)
|
time.sleep(2)
|
||||||
return
|
return
|
||||||
|
|
||||||
rule("Querying IOM Status")
|
rule("Querying IOM1 Status")
|
||||||
info("Querying network settings and firmware versions via serial console...")
|
info("Querying IOM1 network settings and firmware versions via serial console...")
|
||||||
|
info("Note: only IOM1 is reachable over the serial connection.")
|
||||||
print()
|
print()
|
||||||
|
|
||||||
net_rows = []
|
# ── Network settings ───────────────────────────────────────────────────────
|
||||||
fw_rows = []
|
|
||||||
|
|
||||||
for iom in ("IOM1", "IOM2"):
|
|
||||||
# ── Network settings ───────────────────────────────────────────────────
|
|
||||||
net_ok, net_data = _serial_redfish_request(
|
net_ok, net_data = _serial_redfish_request(
|
||||||
ser, password, "GET",
|
ser, password, "GET",
|
||||||
f"/redfish/v1/Managers/{iom}/EthernetInterfaces/1",
|
"/redfish/v1/Managers/IOM1/EthernetInterfaces/1",
|
||||||
)
|
)
|
||||||
if net_ok and isinstance(net_data, dict):
|
if net_ok and isinstance(net_data, dict):
|
||||||
dhcp, ip, gw, nm, origin = _parse_network_data(net_data)
|
dhcp, ip, gw, nm, origin = _parse_network_data(net_data)
|
||||||
mode = _c(C.CYN, "DHCP") if dhcp else _c(C.GRN, "Static")
|
mode = _c(C.CYN, "DHCP") if dhcp else _c(C.GRN, "Static")
|
||||||
net_rows.append([iom, mode, origin, ip, gw, nm])
|
net_rows = [["IOM1", mode, origin, ip, gw, nm]]
|
||||||
else:
|
else:
|
||||||
net_rows.append([iom, _c(C.RED, "No response"), "--", "--", "--", "--"])
|
net_rows = [["IOM1", _c(C.RED, "No response"), "--", "--", "--", "--"]]
|
||||||
error(f"{iom} network query failed: {net_data}")
|
error(f"IOM1 network query failed: {net_data}")
|
||||||
|
|
||||||
# ── IOM firmware version ───────────────────────────────────────────────
|
# ── IOM firmware version ───────────────────────────────────────────────────
|
||||||
iom_ok, iom_data = _serial_redfish_request(
|
iom_ok, iom_data = _serial_redfish_request(
|
||||||
ser, password, "GET",
|
ser, password, "GET",
|
||||||
f"/redfish/v1/Managers/{iom}",
|
"/redfish/v1/Managers/IOM1",
|
||||||
)
|
)
|
||||||
iom_ver = (
|
iom_ver = (
|
||||||
iom_data.get("FirmwareVersion", "Unknown")
|
iom_data.get("FirmwareVersion", "Unknown")
|
||||||
@@ -129,10 +126,10 @@ def _check_via_serial():
|
|||||||
else _c(C.RED, "Unreachable")
|
else _c(C.RED, "Unreachable")
|
||||||
)
|
)
|
||||||
|
|
||||||
# ── Fabric card firmware version ───────────────────────────────────────
|
# ── Fabric card firmware version ───────────────────────────────────────────
|
||||||
fab_ok, fab_data = _serial_redfish_request(
|
fab_ok, fab_data = _serial_redfish_request(
|
||||||
ser, password, "GET",
|
ser, password, "GET",
|
||||||
f"/redfish/v1/Chassis/{iom}/NetworkAdapters/1",
|
"/redfish/v1/Chassis/IOM1/NetworkAdapters/1",
|
||||||
)
|
)
|
||||||
fab_ver = (
|
fab_ver = (
|
||||||
(fab_data.get("Oem", {})
|
(fab_data.get("Oem", {})
|
||||||
@@ -142,9 +139,7 @@ def _check_via_serial():
|
|||||||
else _c(C.RED, "Unreachable")
|
else _c(C.RED, "Unreachable")
|
||||||
)
|
)
|
||||||
|
|
||||||
fw_rows.append([iom, iom_ver, fab_ver])
|
_print_results(net_rows, [["IOM1", iom_ver, fab_ver]])
|
||||||
|
|
||||||
_print_results(net_rows, fw_rows)
|
|
||||||
close_serial_connection(ser, device)
|
close_serial_connection(ser, device)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -339,26 +339,21 @@ def open_serial_connection(device: str) -> Optional[SerialPort]:
|
|||||||
# ── Step 3: Fetch & display current IOM network settings ─────────────────────
|
# ── Step 3: Fetch & display current IOM network settings ─────────────────────
|
||||||
def fetch_current_config(cfg: ShelfConfig, ser: SerialPort) -> bool:
|
def fetch_current_config(cfg: ShelfConfig, ser: SerialPort) -> bool:
|
||||||
"""
|
"""
|
||||||
Query Redfish for the current network config of both IOMs via curl
|
Query Redfish for the current network config of IOM1 via curl over the
|
||||||
over the serial console session.
|
serial console session. Only IOM1 is reachable — the serial cable
|
||||||
Populates cfg.iom1 / cfg.iom2 with live data.
|
connects to IOM1's console port; IOM2 cannot be queried this way.
|
||||||
Returns True if at least one IOM responded.
|
Populates cfg.iom1 with live data.
|
||||||
|
Returns True if IOM1 responded successfully.
|
||||||
"""
|
"""
|
||||||
rule("Step 3 of 5 -- Current IOM Network Settings")
|
rule("Step 3 of 5 -- Current IOM1 Network Settings")
|
||||||
info("Querying Redfish API via serial console...")
|
info("Querying IOM1 Redfish API via serial console...")
|
||||||
|
info("Note: only IOM1 is reachable over the serial connection.")
|
||||||
print()
|
print()
|
||||||
|
|
||||||
any_ok = False
|
path = "/redfish/v1/Managers/IOM1/EthernetInterfaces/1"
|
||||||
rows = []
|
|
||||||
errors = []
|
|
||||||
|
|
||||||
for iom in ("IOM1", "IOM2"):
|
|
||||||
path = f"/redfish/v1/Managers/{iom}/EthernetInterfaces/1"
|
|
||||||
ok_flag, data = _serial_redfish_request(ser, cfg.password, "GET", path)
|
ok_flag, data = _serial_redfish_request(ser, cfg.password, "GET", path)
|
||||||
|
|
||||||
if ok_flag and isinstance(data, dict):
|
if ok_flag and isinstance(data, dict):
|
||||||
any_ok = True
|
|
||||||
|
|
||||||
# Determine mode
|
# Determine mode
|
||||||
dhcp_enabled = (
|
dhcp_enabled = (
|
||||||
data.get("DHCPv4", {}).get("DHCPEnabled", False) or
|
data.get("DHCPv4", {}).get("DHCPEnabled", False) or
|
||||||
@@ -378,41 +373,33 @@ def fetch_current_config(cfg: ShelfConfig, ser: SerialPort) -> bool:
|
|||||||
origin = data.get("IPv4Addresses", [{}])[0].get("AddressOrigin", "Unknown") \
|
origin = data.get("IPv4Addresses", [{}])[0].get("AddressOrigin", "Unknown") \
|
||||||
if data.get("IPv4Addresses") else ("DHCP" if dhcp_enabled else "Static")
|
if data.get("IPv4Addresses") else ("DHCP" if dhcp_enabled else "Static")
|
||||||
|
|
||||||
iom_cfg = IOMConfig(
|
cfg.iom1 = IOMConfig(
|
||||||
iom = iom,
|
iom = "IOM1",
|
||||||
dhcp = dhcp_enabled,
|
dhcp = dhcp_enabled,
|
||||||
ip = ip if ip != "--" else "",
|
ip = ip if ip != "--" else "",
|
||||||
gateway = gateway if gateway != "--" else "",
|
gateway = gateway if gateway != "--" else "",
|
||||||
netmask = netmask if netmask != "--" 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')}"
|
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"), "--", "--", "--", "--"])
|
|
||||||
errors.append((iom, str(data)))
|
|
||||||
|
|
||||||
draw_table(
|
draw_table(
|
||||||
["IOM", "Mode", "Origin", "IP Address", "Gateway", "Subnet Mask"],
|
["IOM", "Mode", "Origin", "IP Address", "Gateway", "Subnet Mask"],
|
||||||
rows,
|
[["IOM1", mode_str, origin, ip, gateway, netmask]],
|
||||||
[5, 10, 8, 16, 16, 16],
|
[5, 10, 8, 16, 16, 16],
|
||||||
)
|
)
|
||||||
print()
|
print()
|
||||||
|
return True
|
||||||
if errors:
|
else:
|
||||||
for iom, err in errors:
|
draw_table(
|
||||||
error(f"{iom} query failed: {err}")
|
["IOM", "Mode", "Origin", "IP Address", "Gateway", "Subnet Mask"],
|
||||||
|
[["IOM1", _c(C.RED, "No response"), "--", "--", "--", "--"]],
|
||||||
|
[5, 10, 8, 16, 16, 16],
|
||||||
|
)
|
||||||
print()
|
print()
|
||||||
|
error(f"IOM1 query failed: {data}")
|
||||||
if not any_ok:
|
error("Check that the serial cable is connected and IOM1 is booted.")
|
||||||
error("Neither IOM responded to the Redfish query.")
|
print()
|
||||||
error("Check that the serial cable is connected and the IOM is booted.")
|
return False
|
||||||
|
|
||||||
return any_ok
|
|
||||||
|
|
||||||
|
|
||||||
# ── Step 4: Prompt user — change config or exit ───────────────────────────────
|
# ── Step 4: Prompt user — change config or exit ───────────────────────────────
|
||||||
@@ -439,8 +426,8 @@ def collect_network_config(cfg: ShelfConfig) -> bool:
|
|||||||
|
|
||||||
# ── User wants to change settings ─────────────────────────────────────────
|
# ── User wants to change settings ─────────────────────────────────────────
|
||||||
print()
|
print()
|
||||||
print(" How should the IOMs be configured?")
|
print(" How should IOM1 be configured?")
|
||||||
print(f" {_c(C.BOLD, '1')} Static IP addresses")
|
print(f" {_c(C.BOLD, '1')} Static IP address")
|
||||||
print(f" {_c(C.BOLD, '2')} DHCP")
|
print(f" {_c(C.BOLD, '2')} DHCP")
|
||||||
print()
|
print()
|
||||||
|
|
||||||
@@ -455,8 +442,7 @@ def collect_network_config(cfg: ShelfConfig) -> bool:
|
|||||||
|
|
||||||
if use_dhcp:
|
if use_dhcp:
|
||||||
cfg.iom1 = IOMConfig("IOM1", dhcp=True)
|
cfg.iom1 = IOMConfig("IOM1", dhcp=True)
|
||||||
cfg.iom2 = IOMConfig("IOM2", dhcp=True)
|
ok("IOM1 will be set to DHCP.")
|
||||||
ok("Both IOMs will be set to DHCP.")
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
# Static — IOM1
|
# Static — IOM1
|
||||||
@@ -465,20 +451,6 @@ def collect_network_config(cfg: ShelfConfig) -> bool:
|
|||||||
iom1_gw = prompt_ip(" IOM1 Gateway ")
|
iom1_gw = prompt_ip(" IOM1 Gateway ")
|
||||||
iom1_nm = prompt_ip(" IOM1 Subnet Mask")
|
iom1_nm = prompt_ip(" IOM1 Subnet Mask")
|
||||||
cfg.iom1 = IOMConfig("IOM1", dhcp=False, ip=iom1_ip, gateway=iom1_gw, netmask=iom1_nm)
|
cfg.iom1 = IOMConfig("IOM1", dhcp=False, ip=iom1_ip, gateway=iom1_gw, netmask=iom1_nm)
|
||||||
|
|
||||||
# Static — IOM2
|
|
||||||
print()
|
|
||||||
info(f"Static network details for {_c(C.BOLD, 'IOM2')}:")
|
|
||||||
iom2_ip = prompt_ip(" IOM2 IP address ")
|
|
||||||
|
|
||||||
same = prompt_yn(" Same gateway and subnet mask as IOM1?", default=True)
|
|
||||||
if same:
|
|
||||||
iom2_gw, iom2_nm = iom1_gw, iom1_nm
|
|
||||||
else:
|
|
||||||
iom2_gw = prompt_ip(" IOM2 Gateway ")
|
|
||||||
iom2_nm = prompt_ip(" IOM2 Subnet Mask")
|
|
||||||
|
|
||||||
cfg.iom2 = IOMConfig("IOM2", dhcp=False, ip=iom2_ip, gateway=iom2_gw, netmask=iom2_nm)
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
@@ -486,21 +458,14 @@ def collect_network_config(cfg: ShelfConfig) -> bool:
|
|||||||
def apply_configuration(cfg: ShelfConfig, ser: SerialPort) -> bool:
|
def apply_configuration(cfg: ShelfConfig, ser: SerialPort) -> bool:
|
||||||
rule("Step 5 of 5 -- Applying Configuration via Redfish API")
|
rule("Step 5 of 5 -- Applying Configuration via Redfish API")
|
||||||
|
|
||||||
info("Sending Redfish PATCH requests via serial console curl...")
|
info("Sending Redfish PATCH request to IOM1 via serial console curl...")
|
||||||
print()
|
print()
|
||||||
|
|
||||||
results = []
|
success, detail = _apply_iom(cfg.password, cfg.iom1, ser)
|
||||||
all_ok = True
|
|
||||||
for iom_cfg in [cfg.iom1, cfg.iom2]:
|
|
||||||
success, detail = _apply_iom(cfg.password, iom_cfg, ser)
|
|
||||||
status = _c(C.GRN, "OK") if success else _c(C.RED, "FAIL")
|
status = _c(C.GRN, "OK") if success else _c(C.RED, "FAIL")
|
||||||
results.append([iom_cfg.iom, status, detail])
|
draw_table(["IOM", "Result", "Detail"], [["IOM1", status, detail]], [6, 8, 50])
|
||||||
if not success:
|
|
||||||
all_ok = False
|
|
||||||
|
|
||||||
draw_table(["IOM", "Result", "Detail"], results, [6, 8, 50])
|
|
||||||
print()
|
print()
|
||||||
return all_ok
|
return success
|
||||||
|
|
||||||
|
|
||||||
def _apply_iom(password: str, iom_cfg: IOMConfig, ser: SerialPort) -> tuple:
|
def _apply_iom(password: str, iom_cfg: IOMConfig, ser: SerialPort) -> tuple:
|
||||||
@@ -560,22 +525,23 @@ def _apply_iom(password: str, iom_cfg: IOMConfig, ser: SerialPort) -> tuple:
|
|||||||
def print_summary(cfg: ShelfConfig, changed: bool):
|
def print_summary(cfg: ShelfConfig, changed: bool):
|
||||||
rule("Summary")
|
rule("Summary")
|
||||||
|
|
||||||
def val(iom: IOMConfig, field: str) -> str:
|
iom = cfg.iom1
|
||||||
dhcp_map = {"mode": "DHCP", "ip": "-- (DHCP)", "gateway": "-- (DHCP)", "netmask": "-- (DHCP)"}
|
if iom.dhcp:
|
||||||
stat_map = {"mode": "Static", "ip": iom.ip, "gateway": iom.gateway, "netmask": iom.netmask}
|
mode, ip, gateway, netmask = "DHCP", "-- (DHCP)", "-- (DHCP)", "-- (DHCP)"
|
||||||
return (dhcp_map if iom.dhcp else stat_map).get(field, "")
|
else:
|
||||||
|
mode, ip, gateway, netmask = "Static", iom.ip, iom.gateway, iom.netmask
|
||||||
|
|
||||||
draw_table(
|
draw_table(
|
||||||
["Setting", "IOM1", "IOM2"],
|
["Setting", "IOM1"],
|
||||||
[
|
[
|
||||||
["Mode", val(cfg.iom1, "mode"), val(cfg.iom2, "mode")],
|
["Mode", mode],
|
||||||
["IP Address", val(cfg.iom1, "ip"), val(cfg.iom2, "ip")],
|
["IP Address", ip],
|
||||||
["Gateway", val(cfg.iom1, "gateway"), val(cfg.iom2, "gateway")],
|
["Gateway", gateway],
|
||||||
["Subnet Mask", val(cfg.iom1, "netmask"), val(cfg.iom2, "netmask")],
|
["Subnet Mask", netmask],
|
||||||
["Serial Port", cfg.device, cfg.device],
|
["Serial Port", cfg.device],
|
||||||
["Changes", "Yes" if changed else "None", ""],
|
["Changes", "Yes" if changed else "None"],
|
||||||
],
|
],
|
||||||
[12, 22, 22],
|
[12, 28],
|
||||||
)
|
)
|
||||||
|
|
||||||
if changed:
|
if changed:
|
||||||
@@ -583,7 +549,7 @@ def print_summary(cfg: ShelfConfig, changed: bool):
|
|||||||
draw_box([
|
draw_box([
|
||||||
f"{_c(C.YEL, 'IMPORTANT -- Per the ES24N Service Guide:')}",
|
f"{_c(C.YEL, 'IMPORTANT -- Per the ES24N Service Guide:')}",
|
||||||
"",
|
"",
|
||||||
"Remove the serial cable ONLY after verifying each",
|
"Remove the serial cable ONLY after verifying the",
|
||||||
"expander appears in TrueNAS with matching drives.",
|
"expander appears in TrueNAS with matching drives.",
|
||||||
"",
|
"",
|
||||||
"TrueNAS > System Settings > Enclosure >",
|
"TrueNAS > System Settings > Enclosure >",
|
||||||
|
|||||||
Reference in New Issue
Block a user