fix: read a command to its exit status, not to the first line that looks like a prompt

_send waited for any output ending in "#", "$" or ">". netmiko matches that
against everything read so far, so a chunk that happened to end in such a
line ended the read while the command still ran. apt's bad-signature line,
"... <ftpmaster@ubuntu.com>", did exactly that: refresh_available_updates
returned half its output, and the rest -- exit status and prompt included --
arrived as the next command's output, so the update read after it failed
with "no exit status" (netOrk #615, seen on nine hosts on 2026-10-06).

A command that ends in "echo __NAME=$?" (__NETORK_RC=, and device-types'
__APT_RC= and __SVC_RC=) is now read until that marker, with a number, and
the prompt line after it. The echoed command line carries a literal $? and
cannot match. Every other command keeps the prompt pattern.
This commit is contained in:
Christian Manivong
2026-10-06 07:18:37 +02:00
parent b49acb8ed7
commit 2fed2f73e2
2 changed files with 128 additions and 1 deletions
+26 -1
View File
@@ -69,6 +69,31 @@ _RC_MARKER = "__NETORK_RC="
_RC_MARKER_RE = re.compile(rf"^{_RC_MARKER}(\d+)\s*$", re.MULTILINE)
#: The end of a command's output when nothing better is known: a line that looks
#: like a shell prompt.
_PROMPT_RE = r"[#$\>]\s*$"
#: A command ending in ``echo __NAME=$?`` reports its exit status on a line of its
#: own: ``_RC_MARKER`` here, ``__APT_RC=`` and ``__SVC_RC=`` in napalm-device-types.
_STATUS_ECHO_RE = re.compile(r"echo\s+(__[A-Z_]+=)\$\?")
def _expect_for(command: str) -> str:
"""The pattern that ends *command*'s output.
netmiko stops reading as soon as the pattern matches what it has read so far.
A line the command prints can end in ``#``, ``$`` or ``>`` -- apt's
``<ftpmaster@ubuntu.com>`` after a bad signature -- and was taken for the
prompt: half the output came back, and the rest started the next command's
(#615). A command that echoes its exit status is read until that marker, with
a number, and the prompt line after it. The echoed command line carries a
literal ``$?`` and cannot match.
"""
markers = _STATUS_ECHO_RE.findall(command)
if not markers:
return _PROMPT_RE
return re.escape(markers[-1]) + r"\d+\s*\n.*" + _PROMPT_RE
def _split_status(raw: str) -> tuple[str, int | None]:
"""``(output, exit_status)`` of a command followed by ``echo {_RC_MARKER}$?``.
@@ -306,7 +331,7 @@ class LinuxDriver(KernelFactsMixin, SystemdServicesMixin, HostStatusMixin, OSDri
command,
read_timeout=read_timeout,
cmd_verify=False,
expect_string=r'[#$\>]\s*$',
expect_string=_expect_for(command),
).strip()
def _sudo(self, command: str, read_timeout: float = 100) -> str: