fix: keep systemd's OSC 3008 shell context out of command output (#41)
CI / test (3.10) (push) Successful in 27s
CI / test (3.11) (push) Successful in 25s
CI / test (3.12) (push) Successful in 25s

Backport of the 0.15.1 fix onto 0ba5081, the commit netOrk pins, which
cannot take 0.15.x before it moves to napalm-device-types 4.22.

systemd 258 (Ubuntu 26.04) ships /etc/profile.d/80-systemd-osc-context.sh:
an interactive bash wraps every command in OSC 3008 sequences from PS0 and
PROMPT_COMMAND, each with a fresh UUID. They ended up in every value read
through netmiko's shell, and because the prompt never looked the same twice,
the prompt line stayed in the output too. os_version grew past 255
characters and netOrk could not store the poll.

The session now unsets PS0 and PROMPT_COMMAND right after switching the
history off and reads its prompt anew; _send() strips terminal codes from
what it returns.
This commit is contained in:
Christian Manivong committed 2026-10-23 18:58:49 +02:00
1 parent 0ba5081824
commit c00deef3d7
3 files changed
+99 -4

No files matched your search

+19 -3
View File
@@ -167,6 +167,12 @@ _PRIVILEGED_STDIN = (
#: a shell without ``set -o history`` (dash) only complains, to /dev/null.
_HISTORY_OFF = " unset HISTFILE; set +o history 2>/dev/null; true"
#: Sent right after ``_HISTORY_OFF`` (#41): systemd 258's
#: ``/etc/profile.d/80-systemd-osc-context.sh`` wraps every command in OSC 3008
#: sequences from ``PS0`` and ``PROMPT_COMMAND``, each with a fresh UUID. It appends
#: to ``PROMPT_COMMAND`` as an array, so only ``unset`` removes its hook.
_PROMPT_HOOKS_OFF = " unset PS0 PROMPT_COMMAND; true"
#: First in every script ``_sudo`` runs: a sudo that does not prompt (NOPASSWD,
#: a cached timestamp) leaves the password on stdin, and nothing may read it.
_NO_STDIN = "exec </dev/null; "
@@ -398,6 +404,15 @@ class LinuxDriver(
except Exception:
logger.debug("Could not switch the shell history off on %s", self.hostname, exc_info=True)
# No systemd OSC 3008 context around every command (#41). netmiko learned
# the prompt with those sequences in front, and a prompt carrying a fresh
# UUID never matches again, so it reads the prompt anew once they are gone.
try:
self._device.send_command(_PROMPT_HOOKS_OFF, expect_string=_PROMPT_RE)
self._device.set_base_prompt()
except Exception:
logger.debug("Could not switch the prompt hooks off on %s", self.hostname, exc_info=True)
# Prevent PTY from wrapping long output lines (e.g. docker JSON).
try:
self._device.send_command("stty cols 10000 2>/dev/null || true", expect_string=r"[#$>]\s*$")
@@ -430,15 +445,16 @@ class LinuxDriver(
# ------------------------------------------------------------------
def _send(self, command: str, read_timeout: float = 100) -> str:
"""Send a command and return stripped output."""
"""Send a command and return its output without terminal codes, stripped."""
if not self._device:
raise ConnectionClosedException("Not connected")
return self._device.send_command(
output = self._device.send_command(
command,
read_timeout=read_timeout,
cmd_verify=False,
expect_string=_expect_for(command),
).strip()
)
return strip_terminal_codes(output).strip()
def _sudo(self, command: str, read_timeout: float = 100) -> str:
"""Run *command* as root and return its output, stdout and stderr together.