feat: get_service_packages, which packages a systemd service is (NetOrk/netork#1236)
CI / test (3.10) (pull_request) Successful in 41s
CI / test (3.11) (pull_request) Successful in 40s
CI / test (3.12) (pull_request) Successful in 32s
CI / test (3.10) (push) Successful in 29s
CI / test (3.11) (push) Successful in 27s
CI / test (3.12) (push) Successful in 34s

Mixes in napalm-device-types' ServicePackagesMixin. The transport runs the
shared read-only command on an exec channel under sh, without sudo: it is
about 1 kB, past what the PTY takes reliably, and written for sh whatever
the login shell. Requires napalm-device-types 4.20.0; version 0.12.0.
This commit is contained in:
Christian Manivong committed 2026-10-19 17:40:06 +02:00
1 parent c725f4882c
commit a2bfa709f9
4 files changed
+50 -2

No files matched your search

+3
View File
@@ -7,6 +7,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased] ## [Unreleased]
### Added
- `get_service_packages()` (napalm-device-types' `ServicePackagesMixin`, NetOrk/netork#1236): which installed packages each systemd service is made of — the one shipping its unit file and the one shipping its `ExecStart` binary. Read-only, on an exec channel under `sh`, without sudo. Requires napalm-device-types 4.20.0; version 0.12.0.
### Security ### Security
- The SSH host key is verified (NetOrk/netork#1167). `open()` hands netmiko the key the caller expects for the port in `optional_args["ssh_host_keys"]` (as a `known_hosts` file that exists only while it connects, with `ssh_strict`), so any other key is refused before the login (`SshHostKeyMismatch`); without one netmiko accepts the first key. Every key seen goes to `optional_args["ssh_host_key_observer"]`. OpenMediaVault and QNAP get this through `LinuxDriver.open()`. Requires napalm-device-types 4.19.0; version 0.11.0. - The SSH host key is verified (NetOrk/netork#1167). `open()` hands netmiko the key the caller expects for the port in `optional_args["ssh_host_keys"]` (as a `known_hosts` file that exists only while it connects, with `ssh_strict`), so any other key is refused before the login (`SshHostKeyMismatch`); without one netmiko accepts the first key. Every key seen goes to `optional_args["ssh_host_key_observer"]`. OpenMediaVault and QNAP get this through `LinuxDriver.open()`. Requires napalm-device-types 4.19.0; version 0.11.0.
+11
View File
@@ -47,6 +47,7 @@ from napalm_device_types import (
OSDriver, OSDriver,
PackageProxyMixin, PackageProxyMixin,
ServiceClustersMixin, ServiceClustersMixin,
ServicePackagesMixin,
SnmpAgentMixin, SnmpAgentMixin,
SystemIdentityMixin, SystemIdentityMixin,
SystemdServicesMixin, SystemdServicesMixin,
@@ -294,6 +295,7 @@ class LinuxDriver(
EnsurePackageMixin, EnsurePackageMixin,
PackageProxyMixin, PackageProxyMixin,
KernelFactsMixin, KernelFactsMixin,
ServicePackagesMixin,
ListeningSocketsMixin, ListeningSocketsMixin,
VirtualIpsMixin, VirtualIpsMixin,
ServiceClustersMixin, ServiceClustersMixin,
@@ -1164,6 +1166,15 @@ class LinuxDriver(
"""The transport for ``KernelFactsMixin.get_kernel_facts``: read-only, no sudo.""" """The transport for ``KernelFactsMixin.get_kernel_facts``: read-only, no sudo."""
return self._send(command, read_timeout=60) return self._send(command, read_timeout=60)
def _run_service_packages_command(self, command: str) -> str:
"""The transport for ``ServicePackagesMixin.get_service_packages``.
An exec channel rather than the PTY -- the command is about 1 kB -- under
``sh``, which it is written for, whatever the login shell. Read-only, so
never through sudo.
"""
return self.run_command(f"sh -c {_shlex_quote(command)}", timeout=120).stdout
# ------------------------------------------------------------------ # ------------------------------------------------------------------
# ListeningSocketsMixin – the transport for get_listening_sockets # ListeningSocketsMixin – the transport for get_listening_sockets
# ------------------------------------------------------------------ # ------------------------------------------------------------------
+2 -2
View File
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project] [project]
name = "napalm-linux" name = "napalm-linux"
version = "0.11.0" version = "0.12.0"
description = "NAPALM driver for generic Linux systems via SSH" description = "NAPALM driver for generic Linux systems via SSH"
readme = "README.md" readme = "README.md"
requires-python = ">=3.9" requires-python = ">=3.9"
@@ -37,7 +37,7 @@ classifiers = [
] ]
dependencies = [ dependencies = [
"napalm>=4.0", "napalm>=4.0",
"napalm-device-types>=4.19.0", "napalm-device-types>=4.20.0",
"netmiko>=4.0.0", "netmiko>=4.0.0",
"paramiko>=5.0.0", # CVE-2026-44405 "paramiko>=5.0.0", # CVE-2026-44405
] ]
+34
View File
@@ -1193,6 +1193,40 @@ def test_get_kernel_facts_raises_on_output_without_a_report(driver):
driver.get_kernel_facts() driver.get_kernel_facts()
# ---------------------------------------------------------------------------
# get_service_packages -- the command and its parse live in napalm-device-types
# ---------------------------------------------------------------------------
_SVCPKG = (
"SVCPKG_BEGIN\n[units]\nId=nginx.service\n"
"FragmentPath=/lib/systemd/system/nginx.service\n"
"ExecStart={ path=/usr/sbin/nginx ; argv[]=/usr/sbin/nginx }\n\n"
"[dpkg]\nnginx-common: /lib/systemd/system/nginx.service\n"
"nginx-core: /usr/sbin/nginx\nSVCPKG_END\n"
)
def test_get_service_packages_runs_the_shared_command_under_sh_on_an_exec_channel(driver):
"""Read-only, so never through sudo; ``sh`` because the login shell may be another."""
from shlex import quote
from napalm_device_types import CommandResult, ServicePackagesMixin
from napalm_device_types.service_packages import SERVICE_PACKAGES_COMMAND
assert isinstance(driver, ServicePackagesMixin)
calls = []
def run_command(command, *, privileged=False, timeout=60, stdin=None):
calls.append((command, privileged))
return CommandResult(_SVCPKG, "", 0)
driver.run_command = run_command
assert driver.get_service_packages() == {"nginx": ["nginx-common", "nginx-core"]}
assert calls == [(f"sh -c {quote(SERVICE_PACKAGES_COMMAND)}", False)]
driver._device.send_command.assert_not_called()
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# Services: listed in one round trip, controlled through systemctl (#7) # Services: listed in one round trip, controlled through systemctl (#7)
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------