Fix serial login detection when IOM is already logged in

Adds _at_shell_prompt() helper that detects a shell prompt by checking
whether any line ends with '#' or '# ', rather than using a bare '#' in
response check guarded by 'login' not in output. The old guard caused a
false negative when the IOM echoed 'Last login: ...' text alongside the
prompt, sending an unnecessary login attempt to an active session.

Also broadens _ANSI_RE to catch all CSI escape sequences and two-character
ESC sequences, not just the original five terminal codes.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-16 10:10:24 -04:00
parent 4b31c8eb8a
commit 67cbee7897

View File

@@ -26,8 +26,24 @@ from ui import (
prompt, prompt_ip, prompt_yn, prompt_password, prompt, prompt_ip, prompt_yn, prompt_password,
) )
# Strip ANSI escape sequences from serial terminal output # Strip ANSI escape sequences from serial terminal output.
_ANSI_RE = re.compile(r'\x1b\[[0-9;]*[mGKHF]') # Catches CSI sequences (ESC[...X) and other two-character ESC sequences.
_ANSI_RE = re.compile(r'\x1b(?:\[[0-9;]*[A-Za-z]|[^[])')
def _at_shell_prompt(text: str) -> bool:
"""
Return True if any line in text looks like a root shell prompt.
Checks whether a non-empty line ends with '#' or '# ' — the pattern
produced by prompts like 'root@hostname:~#' — without being tripped
up by the word 'login' appearing elsewhere in the output (e.g. in
'Last login: ...' messages shown after a session is resumed).
"""
for line in text.splitlines():
stripped = line.rstrip()
if stripped.endswith("#") or stripped.endswith("# "):
return True
return False
# ── Serial Redfish transport ─────────────────────────────────────────────────── # ── Serial Redfish transport ───────────────────────────────────────────────────
@@ -35,7 +51,8 @@ def _login_serial_console(ser: SerialPort, password: str) -> bool:
""" """
Perform the root login sequence on the IOM serial console. Perform the root login sequence on the IOM serial console.
Handles both the case where a login prompt is showing and where Handles both the case where a login prompt is showing and where
a shell session is already active. 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. Returns True if a shell prompt is reached, False on failure.
""" """
info("Logging in to IOM serial console...") info("Logging in to IOM serial console...")
@@ -43,13 +60,16 @@ def _login_serial_console(ser: SerialPort, password: str) -> bool:
# Send a newline to get the current state of the console # Send a newline to get the current state of the console
ser.send_line("", delay=0.5) ser.send_line("", delay=0.5)
response = _ANSI_RE.sub("", ser.read_until_quiet(quiet_period=0.5)) response = _ANSI_RE.sub("", ser.read_until_quiet(quiet_period=0.5))
low = response.lower()
# Already at a shell prompt — no login needed # Already at a shell prompt — no login needed.
if ("#" in response or "$" in response) and "login" not in low: # Use _at_shell_prompt() rather than a bare '#' check so that
# 'Last login: ...' text in the response does not cause a false negative.
if _at_shell_prompt(response):
ok("Already at shell prompt.") ok("Already at shell prompt.")
return True return True
low = response.lower()
# Send username if we see a login prompt (or an empty/unknown response) # Send username if we see a login prompt (or an empty/unknown response)
if "login" in low or not response.strip(): if "login" in low or not response.strip():
ser.send_line("root", delay=0.5) ser.send_line("root", delay=0.5)
@@ -60,7 +80,7 @@ def _login_serial_console(ser: SerialPort, password: str) -> bool:
ser.send_line(password, delay=0.5) ser.send_line(password, delay=0.5)
response = _ANSI_RE.sub("", ser.read_until_quiet(quiet_period=2.0)) response = _ANSI_RE.sub("", ser.read_until_quiet(quiet_period=2.0))
if "#" in response or "$" in response: if _at_shell_prompt(response):
ok("Logged in to IOM console.") ok("Logged in to IOM console.")
return True return True