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:
+26
-1
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user