Listing a host's services and starting or stopping one is the same on every
host that runs systemd, so the command, its parse, the check of a unit name
and the reading of an action's exit status live here once, and a driver
supplies only the transport (napalm-linux#7, napalm-proxmox#6):
- SYSTEMD_SERVICES_COMMAND: one read-only POSIX sh line. list-unit-files, then
one systemctl show over every loaded service unit (Id, Names, LoadState,
ActiveState, SubState, UnitFileState, MainPID), and is-enabled only for
generated units, whose boot state lives in a SysV script's rc links. Framed;
[no-systemd] when /run/systemd/system is missing. Replaces an is-enabled and
a show per unit: 0.8 s instead of 6 s on a 180-unit Ubuntu host.
- parse_systemd_services(): loaded units except not-found, plus installed unit
files that are not loaded; no templates, no aliases (also not the ones older
systemd lists as "enabled"). enabled = UnitFileState enabled or
enabled-runtime, read from systemctl show and never from list-unit-files'
second column, which has had a preset column after it since systemd 245.
A report whose end is missing raises ValueError, so a list cut short never
reads as services that went away; a host without systemd raises
SystemdUnavailable, a NotImplementedError, so a driver can fall back.
- unit_name() / service_action_command() / parse_action_result(): template
instances, dots, colons and \xHH escapes accepted; a bare template, a leading
"-" and anything a shell reads refused. The action runs as
"timeout 45 systemctl --no-ask-password <action> -- <unit>.service" with its
exit status printed after it; only that status decides, 124 is not called
done, and terminal colour codes are dropped. The marker also keeps the output
from ever being empty, which a transport that retries on an empty answer
would take as a reason to run the action twice.
- SystemdServicesMixin, in the template form: get_services() and
manage_service() are concrete, _run_service_command(command, *, privileged,
timeout) is the driver's hook. Mixed in by the drivers whose host runs
systemd, not by OSDriver.
Version 2.2.0.
A kernel CVE's exploitability often hangs on code that is not there: a module
neither loaded nor shipped, an option the kernel was built without. netOrk's
KB precondition vocabulary asks exactly that (kernel_module, kernel_config).
Reading it is the same on every Linux host, so the command and its parse live
here and a driver supplies only the transport:
- KERNEL_FACTS_COMMAND: one read-only POSIX sh line, no privileges. Release,
/proc/modules, modules.builtin, modules.dep and the build configuration
(/boot/config-* or /proc/config.gz). The report is framed, gzipped and
base64-encoded, so nothing in it can look like a shell prompt to a
screen-scraping transport, and ~300 kB of configuration crosses as a fifth.
- parse_kernel_facts(): a section the command could not print comes back None,
never empty -- "could not read" and "read, and nothing there" must stay apart.
- module_name(): no path, no .ko suffix, "-" folded to "_", as the kernel does.
- KernelFactsMixin, in the template form: get_kernel_facts() is concrete,
_run_kernel_facts_command() is the driver's hook. Mixed in by the drivers
that can, not by OSDriver -- a Windows host is an OS driver too, and
hasattr(driver, "get_kernel_facts") has to stay truthful.
- KernelFactsDict in models.py. Version 2.1.0.
VMDict.vmid and VMConfigDict.vmid were int. Proxmox numbers its guests,
but VMware identifies a VM by UUID, which an int cannot hold. The
provisioning dicts already carried vmid as a string; the read side now
matches. Proxmox reports "100".
VMConfigDict gains optional hardware details -- os_name, cpu_type,
sockets, cores_per_socket, firmware, machine and passthrough (PCI/USB,
as VMPassthroughDict) -- so netOrk's VM hardware view can be filled by
any hypervisor instead of reading Proxmox's raw config through the
driver's private API.
Also fixes the README's hypervisor example, which still named the
pre-contract snapshot_create.
BREAKING CHANGE: VMDict.vmid and VMConfigDict.vmid are str.
A role base used to fill its methods with `raise NotImplementedError`. That is
not neutral under multiple inheritance: the placeholder wins the MRO against a
sibling base's working implementation and silently replaces it. Adding one stub
to a base was therefore a breaking change for every driver mixing that base with
another, and it broke three of them — OpenWrt grew seven forwarding methods,
QNAP one, and OpenMediaVault avoided inheriting StorageDriver at all.
Role bases now declare their surface under `if TYPE_CHECKING` and implement
nothing. There is no longer anything to shadow, so a device can finally say what
it is:
class QnapQtsDriver(StorageDriver, HypervisorDriver, LinuxDriver):
The order of those bases is the ranking, read back by roles_of(),
role_keys_of() and primary_role_of() in the new roles module. Nothing restates
it: no precedence table, no attribute to override.
Two consequences, both wanted. `hasattr` is a truthful capability probe again,
because a method exists exactly when a driver provided it. And a method that was
never implemented now raises AttributeError rather than NotImplementedError, so
callers should ask before calling.
Shared behaviour moves out of the roles and into function classes, each holding
it once: PackageManagementMixin (was five byte-identical copies),
HealthMetricsMixin (five), ServiceControlMixin, UpdateMixin, NatVpnMixin,
MacAclMixin, FirewallRuleMixin, InterfaceFilterMixin.
BREAKING CHANGE: methods whose contract genuinely differed were renamed apart —
StorageDriver.get_services -> get_storage_services, the storage and hypervisor
snapshot writers -> create/delete/rollback_{volume,vm}_snapshot,
HypervisorDriver.get_storage -> get_vm_storage_pools, get_snapshots ->
get_vm_snapshots, SwitchDriver.get_dot1x_config -> get_dot1x_ports. Two
duplicate names collapsed onto the one already in use: get_pending_updates ->
get_available_updates and remove_package -> uninstall_package.
Also fixes __doc__ being None on all seven role bases: TYPE_LABEL was assigned
above the triple-quoted string, which made it a bare expression rather than a
docstring.
Each abstract base class now carries a TYPE_LABEL: str attribute that
describes the device category in human-readable form:
AccessPointDriver → "Access Point"
FirewallDriver → "Firewall"
HypervisorDriver → "Hypervisor"
OSDriver → "OS"
ResidentialGatewayDriver → "Gateway"
StorageDriver → "Storage"
SwitchDriver → "Switch"
Concrete drivers can override TYPE_LABEL to express a more specific
category (e.g. LinuxDriver sets "Linux"). The backend reads this
attribute to expose a type_label in the DriverInfo API response,
replacing the hardcoded DRIVER_TYPE map in the frontend.
23 tests covering presence, value, inheritance, and override.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Introduce OSDriver abstract base class (os.py) for general-purpose OS
drivers (Linux, BSD, macOS) — registers package management, service
management, users, processes, cron job and the two new OS-specific
extension points
- Add get_docker_info() contract: returns DockerInfoDict covering
containers, images, volumes, networks and outdated image detection
- Add run_device_action() contract: generic extensibility point for
driver-specific one-off administrative actions
- Fix duplicate TypedDicts in models.py: remove early shadow definitions
of UserDict, ProcessDict, CronJobDict, ApplyUpdatesResultDict from the
Common section; keep the more complete definitions in the OS section
- Add Docker TypedDicts: DockerContainerDict, DockerImageDict,
DockerVolumeDict, DockerNetworkDict, DockerInfoDict
- Add DeviceActionResultDict
- Export OSDriver from package __init__; bump version to 0.3.0
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add ServiceDict and UpdateDict TypedDicts to models
- Add get_services() / manage_service() abstract methods for init-system interaction
- Add get_available_updates() / apply_updates() for package upgrade workflows
- Add _filter_interfaces() helper to exclude lo and phy* interfaces from interface dicts
- Extend WirelessClientDict with optional ip, hostname, and lease_end fields
- Add optional description field to VPNTunnelDict
- Bump version 0.1.0 → 0.2.0