diff --git a/CLAUDE.md b/CLAUDE.md index 22fceaf..7b2678b 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -76,15 +76,16 @@ The `docs/` directory contains reference documentation for this project. Check a A single PATCH combining both fields fails due to a known ES24N firmware bug. Documented in `_apply_iom()` in `workflow_serial.py`. **Authentication:** -- Serial loopback (`127.0.0.1`): username `root` -- Network IP: username `Admin` +- Serial loopback (`127.0.0.1`): username `root`, password auto-detected from hostname +- Network IP: username `Admin`, password prompted from user **Redfish API paths:** - Network config: `/redfish/v1/Managers/{IOM1|IOM2}/EthernetInterfaces/1` - Firmware update: `/redfish/v1/UpdateService` - Fabric card: `/redfish/v1/Chassis/{IOM1|IOM2}/NetworkAdapters/1` +**Serial password auto-detection:** The IOM BMC serial number is embedded in the hostname shown on the serial login prompt (e.g. `ves-ves-vds2249r-MXE3000048LHA03C-mgr1 login:`). The `MXE...` segment is the root password. `_login_serial_console()` extracts it automatically — from the login prompt hostname if a login is needed, or by running `hostname` on the shell if already logged in. Falls back to `prompt_password()` if extraction fails. + ## Planned Features - Network-based IOM network configuration (`workflow_network.py`) — same config workflow as serial but connecting via the IOM's existing IP address using `Admin` credentials, for cases where the IOM is already reachable on the network -- Auto-detect password from serial login prompt — the IOM BMC serial number (e.g. `MXE3000043CHA007`) appears to be embedded in the login prompt hostname; if confirmed on a live system, this could allow `_login_serial_console()` to skip the manual password prompt diff --git a/modules/workflow_check.py b/modules/workflow_check.py index 67ce2e3..5477643 100644 --- a/modules/workflow_check.py +++ b/modules/workflow_check.py @@ -90,9 +90,8 @@ def _check_via_serial(): return print() - password = prompt_password() - - if not _login_serial_console(ser, password): + logged_in, password = _login_serial_console(ser) + if not logged_in: error("Could not log in to IOM console. Returning to main menu.") close_serial_connection(ser, device) time.sleep(2) diff --git a/modules/workflow_serial.py b/modules/workflow_serial.py index 86a6991..6143e4c 100644 --- a/modules/workflow_serial.py +++ b/modules/workflow_serial.py @@ -46,14 +46,48 @@ def _at_shell_prompt(text: str) -> bool: return False +def _extract_serial_from_hostname(hostname: str) -> Optional[str]: + """ + Extract the BMC serial number from an IOM hostname. + The serial number (which doubles as the root password) is a + dash-separated segment matching MXE followed by alphanumerics. + e.g. 'ves-ves-vds2249r-MXE3000048LHA03C-mgr1' → 'MXE3000048LHA03C' + Returns None if no matching segment is found. + """ + for segment in hostname.split("-"): + if re.match(r"^MXE[A-Z0-9]+$", segment, re.IGNORECASE): + return segment + return None + + +def _detect_password_from_shell(ser: SerialPort) -> Optional[str]: + """ + When already logged in, run 'hostname' on the IOM shell and extract + the BMC serial number from the output. + """ + ser.send_line("hostname", delay=0.3) + out = _ANSI_RE.sub("", ser.read_until_quiet(quiet_period=0.5)) + for line in out.splitlines(): + pw = _extract_serial_from_hostname(line.strip()) + if pw: + return pw + return None + + # ── Serial Redfish transport ─────────────────────────────────────────────────── -def _login_serial_console(ser: SerialPort, password: str) -> bool: +def _login_serial_console(ser: SerialPort) -> tuple: """ Perform the root login sequence on the IOM serial console. - Handles both the case where a login prompt is showing and where - a shell session is already active (i.e. was never logged out after - a prior run — the IOM stays logged in until power-cycled). - Returns True if a shell prompt is reached, False on failure. + + Auto-detects the root password from the IOM hostname — the BMC serial + number (e.g. MXE3000048LHA03C) is embedded in both the login prompt + hostname and the output of 'hostname', so no manual entry is needed. + + - Login prompt present: extracts serial from 'hostname login:' line. + - Already logged in: runs 'hostname' on the shell to extract the serial. + - Falls back to prompting the user if auto-detection fails either way. + + Returns (success: bool, password: str). """ info("Logging in to IOM serial console...") @@ -61,20 +95,37 @@ def _login_serial_console(ser: SerialPort, password: str) -> bool: ser.send_line("", delay=0.5) response = _ANSI_RE.sub("", ser.read_until_quiet(quiet_period=0.5)) - # Already at a shell prompt — no login needed. - # Use _at_shell_prompt() rather than a bare '#' check so that - # 'Last login: ...' text in the response does not cause a false negative. + # Already at a shell prompt — extract password via 'hostname' command if _at_shell_prompt(response): ok("Already at shell prompt.") - return True + password = _detect_password_from_shell(ser) + if password: + ok(f"Password auto-detected: {_c(C.BOLD, password)}") + return True, password + warn("Could not auto-detect password from hostname.") + return True, prompt_password() low = response.lower() + # Try to extract the password from the login prompt hostname before + # sending credentials — format: 'ves-ves-model-SERIAL-mgr1 login:' + password = None + login_match = re.search(r"(\S+)\s+login:", response, re.IGNORECASE) + if login_match: + password = _extract_serial_from_hostname(login_match.group(1)) + if password: + ok(f"Password auto-detected from login prompt: {_c(C.BOLD, password)}") + # Send username if we see a login prompt (or an empty/unknown response) if "login" in low or not response.strip(): ser.send_line("root", delay=0.5) response = _ANSI_RE.sub("", ser.read_until_quiet(quiet_period=0.5)) + # Fall back to asking the user if auto-detection failed + if not password: + warn("Could not auto-detect password from login prompt.") + password = prompt_password() + # Send password if "password" in response.lower(): ser.send_line(password, delay=0.5) @@ -82,10 +133,10 @@ def _login_serial_console(ser: SerialPort, password: str) -> bool: if _at_shell_prompt(response): ok("Logged in to IOM console.") - return True + return True, password error(f"Login failed. Console response: {response.strip()[:120]}") - return False + return False, "" def _serial_redfish_request(ser: SerialPort, password: str, method: str, @@ -549,12 +600,10 @@ def configure_shelf() -> bool: time.sleep(2) return True - # Password needed before login + # Log in to the IOM console (auto-detects password from hostname) print() - cfg.password = prompt_password() - - # Log in to the IOM console (required before any Redfish curl calls) - if not _login_serial_console(ser, cfg.password): + logged_in, cfg.password = _login_serial_console(ser) + if not logged_in: error("Could not log in to IOM console. Returning to main menu.") close_serial_connection(ser, device) time.sleep(2)