feat: start, stop, restart, enable and disable services, and list them in one round trip
napalm-device-types' SystemdServicesMixin (2.2.0) now provides get_services() and manage_service(); this driver supplies only the transport (#7): - _run_service_command(): unprivileged reads and root logins run as they are -- the user is asked once per session with "id -u", so a root login on a box without sudo is not prefixed with one. With a sudo password the command goes through _sudo(); without one through "sudo -n", which fails at once instead of hanging the session on a password prompt until the read timeout. - get_services(): one round trip instead of an is-enabled and a show per unit (6.0 s -> 0.8 s on a 184-unit Ubuntu host). A host without systemd still falls back to "service --status-all". - manage_service(): when sudo wants a password netOrk does not have, the failure says how to fix it -- the same hint the apt and SNMP actions give, now one constant (_SUDO_PASSWORD_HINT) instead of two copies. OpenMediaVault and QNAP inherit this driver. OMV gets service control with it; QNAP opts out (napalm-qnap-qts), since QTS has no systemd. README: the sudo option is sudo_password, not secret; manage_service and the systemctl permissions are listed. Closes #7
This commit is contained in:
+52
-50
@@ -31,7 +31,13 @@ from netmiko.exceptions import (
|
||||
)
|
||||
from napalm.base.exceptions import ConnectionException, ConnectionClosedException
|
||||
from napalm.base.netmiko_helpers import netmiko_args
|
||||
from napalm_device_types import FingerprintRule, KernelFactsMixin, OSDriver
|
||||
from napalm_device_types import (
|
||||
FingerprintRule,
|
||||
KernelFactsMixin,
|
||||
OSDriver,
|
||||
SystemdServicesMixin,
|
||||
SystemdUnavailable,
|
||||
)
|
||||
from napalm_device_types.models import (
|
||||
ApplyUpdatesResultDict,
|
||||
CronJobDict,
|
||||
@@ -56,6 +62,13 @@ _PKG_MANAGERS = ["apt", "dnf", "yum", "apk", "pacman"]
|
||||
_RC_MARKER = "__NETORK_RC="
|
||||
_RC_MARKER_RE = re.compile(rf"^{_RC_MARKER}(\d+)\s*$", re.MULTILINE)
|
||||
|
||||
#: What to do when sudo wants a password netOrk does not have.
|
||||
_SUDO_PASSWORD_HINT = (
|
||||
"sudo requires a password on this device but none is configured in netOrk. "
|
||||
"Please add the sudo password to a Credential Profile assigned to this device, "
|
||||
"or configure passwordless sudo (NOPASSWD) for this user."
|
||||
)
|
||||
|
||||
# DMI field values that carry no useful information (OEM defaults, blanks)
|
||||
_BAD_DMI: frozenset[str] = frozenset({
|
||||
"", "none", "n/a", "not specified", "not applicable",
|
||||
@@ -138,7 +151,7 @@ def _short_image_id(raw: str) -> str:
|
||||
return raw.strip().removeprefix("sha256:")[:12]
|
||||
|
||||
|
||||
class LinuxDriver(KernelFactsMixin, OSDriver):
|
||||
class LinuxDriver(KernelFactsMixin, SystemdServicesMixin, OSDriver):
|
||||
"""NAPALM driver for generic Linux systems.
|
||||
|
||||
Connects via SSH (netmiko ``linux`` device type) and auto-detects the
|
||||
@@ -292,6 +305,28 @@ class LinuxDriver(KernelFactsMixin, OSDriver):
|
||||
output = (raw[: last.start()] + raw[last.end():]).strip()
|
||||
return output, int(last.group(1))
|
||||
|
||||
def _is_root(self) -> bool:
|
||||
"""Whether the SSH user is root, asked once per session.
|
||||
|
||||
A root login on a box without sudo (an LXC container, a minimal Debian)
|
||||
must not have its commands prefixed with a sudo that is not there.
|
||||
"""
|
||||
if getattr(self, "_root", None) is None:
|
||||
self._root = self._send("id -u") == "0"
|
||||
return bool(self._root)
|
||||
|
||||
def _run_service_command(self, command: str, *, privileged: bool, timeout: int) -> str:
|
||||
"""The transport for :class:`SystemdServicesMixin`.
|
||||
|
||||
Without a sudo password, ``sudo -n`` fails at once where a prompt would
|
||||
otherwise hang the session until the read timeout.
|
||||
"""
|
||||
if not privileged or self._is_root():
|
||||
return self._send(command, read_timeout=timeout)
|
||||
if self._sudo_password:
|
||||
return self._sudo(command, read_timeout=timeout)
|
||||
return self._send(f"sudo -n {command}", read_timeout=timeout)
|
||||
|
||||
def _detect_pkg_manager(self) -> str | None:
|
||||
"""Return the first package manager binary found on PATH."""
|
||||
for pm in _PKG_MANAGERS:
|
||||
@@ -1379,50 +1414,25 @@ class LinuxDriver(KernelFactsMixin, OSDriver):
|
||||
return {"success": False, "output": "", "error": str(exc)}
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# OSDriver – services (systemd)
|
||||
# OSDriver – services (systemd, through SystemdServicesMixin)
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
def get_services(self) -> list[ServiceDict]:
|
||||
"""Return systemd service units (falls back to service --status-all on SysV)."""
|
||||
out = self._send(
|
||||
"systemctl list-units --type=service --all --no-legend --no-pager "
|
||||
"--plain 2>/dev/null"
|
||||
)
|
||||
if not out:
|
||||
"""systemd's services in one round trip; ``service --status-all`` without systemd."""
|
||||
try:
|
||||
return super().get_services()
|
||||
except SystemdUnavailable:
|
||||
return self._get_services_sysv()
|
||||
|
||||
services: list[ServiceDict] = []
|
||||
for line in out.splitlines():
|
||||
# ssh.service loaded active running OpenBSD Secure Shell server
|
||||
parts = line.split(None, 4)
|
||||
if len(parts) < 4:
|
||||
continue
|
||||
unit, load, active, sub = parts[0], parts[1], parts[2], parts[3]
|
||||
name = unit.removesuffix(".service")
|
||||
running = active == "active" and sub == "running"
|
||||
enabled_out = self._send(
|
||||
f"systemctl is-enabled {unit} 2>/dev/null"
|
||||
)
|
||||
enabled = enabled_out.strip() == "enabled"
|
||||
def manage_service(self, name: str, action: str) -> dict[str, Any]:
|
||||
"""Start, stop, restart, enable or disable a systemd service.
|
||||
|
||||
# Retrieve main PID for running services
|
||||
pid = 0
|
||||
if running:
|
||||
pid_out = self._send(
|
||||
f"systemctl show -p MainPID --value {unit} 2>/dev/null"
|
||||
)
|
||||
try:
|
||||
pid = int(pid_out.strip())
|
||||
except ValueError:
|
||||
pid = 0
|
||||
|
||||
services.append({
|
||||
"name": name,
|
||||
"running": running,
|
||||
"enabled": enabled,
|
||||
"pid": pid,
|
||||
})
|
||||
return services
|
||||
:raises ValueError: for an unknown action or an invalid name.
|
||||
"""
|
||||
result = super().manage_service(name, action)
|
||||
if not result["success"] and "password is required" in result["output"]:
|
||||
result["output"] = f"{result['output']}\n{_SUDO_PASSWORD_HINT}"
|
||||
return result
|
||||
|
||||
def _get_services_sysv(self) -> list[ServiceDict]:
|
||||
out = self._send("service --status-all 2>/dev/null")
|
||||
@@ -2110,11 +2120,7 @@ class LinuxDriver(KernelFactsMixin, OSDriver):
|
||||
if not self._sudo_password:
|
||||
return {
|
||||
"success": False,
|
||||
"output": (
|
||||
"sudo requires a password on this device but none is configured in "
|
||||
"netOrk. Please add the sudo password to a Credential Profile assigned "
|
||||
"to this device, or configure passwordless sudo (NOPASSWD) for this user."
|
||||
),
|
||||
"output": _SUDO_PASSWORD_HINT,
|
||||
}
|
||||
|
||||
lines: list[str] = []
|
||||
@@ -2140,11 +2146,7 @@ class LinuxDriver(KernelFactsMixin, OSDriver):
|
||||
if not self._sudo_password:
|
||||
return {
|
||||
"success": False,
|
||||
"output": (
|
||||
"sudo requires a password on this device but none is configured in netOrk. "
|
||||
"Please add the sudo password to a Credential Profile assigned to this device, "
|
||||
"or configure passwordless sudo (NOPASSWD) for this user."
|
||||
),
|
||||
"output": _SUDO_PASSWORD_HINT,
|
||||
}
|
||||
|
||||
# 1. Install snmpd if missing
|
||||
|
||||
Reference in New Issue
Block a user