From 4c820c5086835e6561cb0e26cf034aa99d65e8c3 Mon Sep 17 00:00:00 2001 From: scott Date: Thu, 16 Apr 2026 10:23:13 -0400 Subject: [PATCH] Auto-detect root password from IOM hostname on serial login The BMC serial number (e.g. MXE3000048LHA03C) is embedded in the IOM hostname shown in the serial login prompt. It also doubles as the root password, so no manual entry is needed. - If at login prompt: extract serial from 'hostname login:' line - If already logged in: run 'hostname' on the shell and extract serial - Falls back to prompt_password() if extraction fails either way _login_serial_console() now returns (success, password) instead of bool. Callers in workflow_serial.py and workflow_check.py updated accordingly. CLAUDE.md updated to document the behaviour and remove it from planned features. Co-Authored-By: Claude Sonnet 4.6 --- CLAUDE.md | 7 ++-- modules/workflow_check.py | 5 +-- modules/workflow_serial.py | 81 ++++++++++++++++++++++++++++++-------- 3 files changed, 71 insertions(+), 22 deletions(-) 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)