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 <noreply@anthropic.com>
This commit is contained in:
2026-04-16 10:23:13 -04:00
parent 67cbee7897
commit 4c820c5086
3 changed files with 71 additions and 22 deletions

View File

@@ -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)