get_dhcp_ha (declared on DhcpServerMixin, optional) is what a DHCP
server's own HA configuration says: on or off, this server's peer name,
each peer's name, role as the server words it, and HA URL.
ConfigSyncMixin.get_config_sync is where a host pushes its configuration
and for which services, by the names get_services reports; sync items
without a service name stay visible under `unmapped`. A driver mixes it
in, no role promises it. Both raise when they could not read.
What the facts mean for a pair -- hot standby, which side to write to --
is the caller's (NetOrk/netork#1125).
carp_virtual_ip() and its state table are CARP protocol knowledge; under the
agreed structure (PR #37, docs/STRUCTURE.md) the base package carries only
contracts, data shapes and I/O-free reconcilers. They move to the driver that
speaks CARP (napalm-opnsense). The contract stays: VirtualIpsDict.carp and
VirtualIpDict.holds.
Nothing used the helper yet; it was released in 4.10.0 an hour ago.
netOrk #1121.
A firewall pair's CARP addresses are a third virtual IP mechanism. Unlike
vip-manager and keepalived, a CARP reading says who holds the address: a
firewall's interface list need not show it, but CARP's state names the MASTER.
- VirtualIpsDict gains carp (NotRequired), VirtualIpDict gains holds
(NotRequired; None leaves the holder to the interfaces).
- carp_virtual_ip() shapes one address bound to a VHID: MASTER holds, BACKUP
stands by, INIT and DISABLED neither run nor hold, an unknown state says
nothing. The same for OPNsense and pfSense; a driver supplies the fields.
netOrk #1121.
What ties a server to its BMC: the BMC reports the server's serial
number and system UUID, the host reads the same values from its DMI
tables. Both drivers that read DMI (napalm-linux, napalm-proxmox) had a
copy of the shell for it, and neither read the board serial.
SystemIdentityMixin.get_system_identity() reads product_uuid,
product_serial and board_serial in one framed command and returns them
with vendor placeholders empty: a Supermicro board's product serial is
0123456789 on every one of them, others read "To be filled by O.E.M."
or "Default string", and many cheap boards share the UUID
03000200-0400-0500-0006-000700080009. is_placeholder_identity() is the
same filter for a BMC driver's side. A driver supplies only
_run_system_identity_command(command, privileged=); the files are
root's.
Discovery compares drivers by score divided by the most a driver could
score, and that maximum summed every positive rule. Rules that never
match together, such as one vendor's landing page or another's, made a
driver that knows several vendors lose to one that knows none:
napalm-redfish's Supermicro landing page scored 6 of 41 and lost to a
bare "linux" in the same BMC's SNMP sysDescr, 5 of 18.
FingerprintRule gets an optional group. rules_max_weight() sums the
positive rules and counts each group once, with its strongest rule.
Existing rules have no group and count as before.
The package declares "Typing :: Typed" but shipped no PEP 561 marker, so
mypy in driver repositories treated it as untyped. Add the marker, include
it as package data, and release 4.7.1.
## feat!: settle the open review questions on roles, diffs, SSH and SNMP
- roles: a driver that sets ROLE itself replaces the role it inherits,
so a Windows driver on OSDriver is "windows", not also "linux".
- os: OSDriver's account listing is get_os_users; NAPALM's get_users has
another shape and a runtime stub, so hasattr could not tell.
- README: NAPALM's own getters must come from the driver or a mixin
listed before the first role base; its stubs win the MRO otherwise.
- firewall/DHCP diffs: an empty key, a key given twice, or one more than
one live entry holds raises ValueError instead of overwriting by guess;
live entries without a key are never matched.
- ssh_server: only host key algorithms the host has a key for count as
staying; when the keys cannot be read, the host key list is not
touched. sshd_config and its copy keep their file mode (GNU and BSD
stat), 644 only for a new file.
- SNMP: 64-bit ifXTable octets and ifHighSpeed, with the 32-bit counters
as fallback for agents without ifXTable.
- pyproject: declare typing-extensions, which models.py imports.
BREAKING CHANGE: OSDriver.get_users is now get_os_users; diffs raise
ValueError on keys that cannot name one entry; plan_ssh_hardening
removes no host key algorithm without host_keys.
## fix: tell "could not read" and "does not install" apart in update and VIP readers
- package_updates: the dnf security reader records dnf's exit status and
raises when it is missing or non-zero, as the apt reader does; a failed
dnf no longer reads as "no security advisories".
- host_status: an enabled dnf-automatic.timer counts as auto updates only
with apply_updates = yes (dnf5's automatic.conf wins over dnf4's);
dnf-automatic-install.timer always does. Unattended-upgrades counts only
while the program is installed, not from a conffile left behind.
- virtual_ips: a host with systemctl but without systemd as PID 1 reads as
"did not look" (None), not as "has none". keepalived blocks whose brace
is on the next line are read.
BREAKING CHANGE: parse_dnf_security expects DNF_SECURITY_COMMAND's
__DNF_RC marker and raises ValueError without it.
A role base for baseboard management controllers (Supermicro, Dell
iDRAC, HPE iLO, Lenovo XCC), reached over Redfish or IPMI, not SSH.
It declares, under TYPE_CHECKING only: get_power_state, set_power_state
(Redfish ResetType actions), get_hardware_inventory (CPUs, DIMMs,
drives, PSUs, NICs, and the server's serial, system UUID and NIC MACs),
get_event_log, get_firmware_inventory and get_management_protocols.
reboot_host restarts the BMC itself (Manager.Reset), never the managed
server, which goes through set_power_state. No HealthMetricsMixin and no
command channel; sensors come through NAPALM's get_environment.
Squashes three changes:
## chore: support Python 3.13 and make mypy --strict pass in CI
Add 3.13 to the CI matrix and classifiers, run mypy in CI, fix the
14 strict errors it reported, and correct the README's minimum Python.
## fix: eight bugs found in a full review, each with a regression test
- channel: run_on_transport lost output sshd sent after the exit status;
it now reads stdout and stderr to EOF before returning.
- ssh_server: a failed way back (restore or reload) raised out of
harden_ssh_server after the host was touched; it is now reported in
the result, with rolled_back False.
- host_status: a newer RPM kernel (RHEL, Fedora) was never seen, because
its release, not its arch, followed the last "-".
- package_updates: an apt line wrapped at a blank lost the blank, and
with it the package.
- kernel: an empty payload (gzip or base64 missing) read as a report.
- listening: "[fe80::1]%eth0:546", as ss prints it, kept its brackets.
- ping_sweep: a reply whose "success" was not a dict aborted the sweep.
- lag: a member of unknown speed (-1) lowered the LAG's speed.
## docs: bring the README and docstrings in line with the code
The firewall worked example showed the runtime stubs the README forbids;
the class table missed PhoneDriver and MediaDriver; ConfigLifecycleMixin
is mixed in by drivers, not by a role base; services, mac_acl and
packages named a class, a key and an exception that do not exist.
A driver gains root with sudo -S and puts the password in front of stdin.
A sudo that does not prompt -- NOPASSWD, a cached timestamp -- leaves it
there, and the cat under sudo wrote it into the drop-in as its first line.
Seen on the first real run (NetOrk/netork#1027): sshd -t rejected the file
and the rollback removed it, but the password was in a 0644 file for a
moment and in the error the caller showed.
The content now goes without privilege to a mktemp file only the login user
can read; root installs that file (mode 644, root-owned) and removes it.
The fake host in the tests prepends the password to every privileged stdin,
as a non-prompting sudo does.
Bumps to 4.5.1. NetOrk/netork#1027, NetOrk/netork#1029
ssh-audit names algorithms an SSH server should stop offering. Taking them
out is the same on every OpenSSH host, so it lives here once: a driver
mixes in OpenSshServerMixin and supplies run_command.
harden_ssh_server reads the lists sshd really uses (sshd -T), removes the
requested algorithms and writes the rest to
/etc/ssh/sshd_config.d/00-netork-ssh-hardening.conf -- or, where
sshd_config has no Include of that directory, rewrites sshd_config after
a copy. It never removes the last algorithm of a list, nor, given the
managing client's lists, the last one that client speaks. sshd -t must
accept the result and sshd -T must show it; after the reload the caller's
verify opens a new session, and if that fails the still-open one puts the
previous state back and reloads again.
Bumps to 4.5.0. NetOrk/netork#1027
A Proxmox host's root filesystem stood at 94 % with about 400 MB free for a
week; a kernel upgrade failed on it with "No space left on device" and left
dpkg halfway, and nothing had said the disk was full (NetOrk/netork#862).
HOST_STATUS_COMMAND now also runs POSIX `df -Pk / /boot /var`, without root.
get_host_status() returns `filesystems`: each filesystem once (a directory
that is not a mount of its own is the filesystem it lives on), with its
mountpoint, total, used and available bytes, or None when df said nothing.
`available` is what df says an ordinary user can still write, which on ext4
is less than total minus used. How full is too full is the caller's decision.
Version 4.4.0.
On every Ubuntu host with unattended-upgrades, get_host_status() read the
package manager as busy, so an interrupted dpkg was never reported there.
Ubuntu keeps unattended-upgrade-shutdown --wait-for-signal running all the
time; its process name is cut to 15 characters, unattended-upgr, the same
as a real run's.
BUSY_CHECK now matches dpkg, apt, apt-get and aptitude by name and
unattended-upgrade by its command line. The tests run it through sh with a
stand-in ps, and check that the command does not match itself: over an exec
channel it is the command line of the shell that runs it.
Closes#26
An unattended upgrade cut off while it built an initrd left a kernel package
half-configured on an Ubuntu host. For three months apt refused every upgrade,
while apt.systemd.daily exited quietly and the timers reported success
(NetOrk/netork#738).
HOST_STATUS_COMMAND now also reads, without root:
- dpkg's state the way apt checks it: numbered files left in
/var/lib/dpkg/updates, or a package half-installed, unpacked,
half-configured, waiting for triggers or flagged for reinstallation.
While dpkg, apt, aptitude or unattended-upgrade runs, packages are
halfway on purpose, so the answer is then None.
- /var/lib/apt/periodic/upgrade-stamp, which apt.systemd.daily touches only
after unattended-upgrade succeeded, and the interval of
APT::Periodic::Unattended-Upgrade in apt's units. update-success-stamp is
not read: every apt-get update touches it, whoever runs it.
HostStatusDict gains package_manager_interrupted, interrupted_packages,
auto_updates_last_success and auto_updates_interval, all NotRequired.
A virtual IP sits on whichever member holds it right now; a standby's
address list shows no trace of it. What every member has is the
configuration that declares it, and reading that is the same on every
Linux host -- so VirtualIpsMixin.get_virtual_ips() lives here and a driver
only carries the command across (NetOrk/netork#829).
- vip-manager: every vip-manager.service / vip-manager@*.service unit; the
address from a flag (1.x's -ip=${VIP_IP} resolved through the unit's
environment), the environment, or the --config YAML (5.x).
- keepalived: the virtual_ipaddress(_excluded) entries of every
vrrp_instance, with its interface and virtual_router_id; include is
followed four levels deep, and a pattern that is no plain path glob is
never handed to the root shell.
The same files hold the etcd password and auth_pass, so an awk program
filters on the host and prints allowlisted fields only. None per
mechanism is "did not look", [] a host without it. The command is about
4 kB and goes on an exec channel.
Version 4.2.0.
HOST_STATUS_COMMAND also prints freebsd-version -k and -r where
freebsd-version exists. When the two differ, a kernel update was installed
and is not running yet, so the host needs a reboot. When they match, it does
not.
DockerInfoDict, DockerContainerDict, DockerImageDict, DockerVolumeDict,
DockerNetworkDict and the OSDriver declaration of get_docker_info() go.
They described napalm-linux' old CLI reading, which napalm-linux 0.3.0
dropped. There is no container model here: netOrk reads and handles
containers over the Engine API (NetOrk/netork#765, phase 9); this
package only gives access to the engine.
Version 4.0.0.
GUEST_AGENT_PACKAGES / GUEST_AGENT_RUNCMD described one agent per
hypervisor. A hypervisor that provisions FreeBSD and OpenBSD guests needs
a different agent per guest, and netOrk has to know which guests a driver
can provision at all (NetOrk/netork#794). Both now come from one
class-level mapping netOrk reads off the driver class:
GUEST_AGENTS = {guest_os: (packages, runcmd)}
Its keys are the guests the driver provisions, and
create_vm_from_cloud_init(guest_os=...) refuses any other. The base keeps
Linux with qemu-guest-agent.
New module `provisioning`, generic for every hypervisor:
- QEMU_GUEST_AGENTS: the QEMU agent for Linux, FreeBSD and OpenBSD, with
package names and service commands verified on FreeBSD 15.1 and
OpenBSD 7.9 cloud images (NetOrk/netork#793).
- network_config(): cloud-init's network-config v2 with MAC matching,
moved here from napalm-vmware. FreeBSD's nuageinit reads no other
version.
- split_compression(): how a packed image (.xz, .gz, .bz2, .zst) is
unpacked.
BREAKING CHANGE: GUEST_AGENT_PACKAGES and GUEST_AGENT_RUNCMD are gone;
drivers declare GUEST_AGENTS instead.
run_cli() and stream_cli() take privileged=, passed to the driver's
run_command()/open_stream(): an engine that refuses the login user can
be retried as root, the way the driver gains root for any command.
netOrk uses it to retry a refused container start/stop/restart with
sudo (NetOrk/netork#773).
netOrk reached a host's shell through napalm-linux's private `_send`: an
interactive PTY with stdout and stderr merged and no exit code. For Docker
it went further and opened its own paramiko connections around the driver.
This adds the access layer only, no container model (NetOrk/netork#765):
- `channel.py`: `CommandChannelMixin` declares `run_command()` (stdout,
stderr, exit code) and `open_stream()` (a `ByteStream` to a running
command). Declared under TYPE_CHECKING, so hasattr stays truthful.
`run_on_transport()` and `open_stream_on_transport()` implement both on
a paramiko exec channel for any SSH driver; `ParamikoExecStream` drains
stderr on every read so the shared window never stalls.
- `container_engine.py`: `ContainerEngineMixin` with `container_engines()`
and `open_container_engine()`. The returned `ContainerEngineConnection`
opens the engine API over `<binary> system dial-stdio` and runs the CLI
with caller-chosen arguments. The driver decides the binary through
`_container_engine_binary()`; callers never see the path.
- README: why the container model lives in netOrk and not here.
Additive; the Docker*Dict declarations stay until netOrk no longer reads
them. Version 2.6.0.
Refs #17
OpenWrt has no ss: the listening-socket command fell through to [no-ss]
and the reading raised ListeningSocketsUnavailable. Without ss it now runs
netstat -lntup (busybox and net-tools both), and the cgroups are read for
its PIDs alike.
- netstat names a socket's process as PID/Program; a UDP line has no
state column, "-" is a socket without a process, and net-tools prints
tcp6/udp6 and program names with a space ("sshd: /usr/sbin").
- On OpenWrt the cgroup is /services/<name>/<instance>, so the unit is
the procd service -- a jailed one too, whose PID is not the one procd
reports (dnsmasq under ujail).
- A host with neither tool still raises ListeningSocketsUnavailable.
Checked against a real OpenWrt 25.12.2 access point, and that a Linux and
a Proxmox host still read the same sockets with the longer command.
2.5.0. For netOrk#673.
Whether a service is reachable from outside its host is decided by the
address it listens on: 0.0.0.0:5432 is, 127.0.0.1:5432 is not. Reading
that is the same on every Linux host, so the command and its parse live
here once and a driver only carries the command across
(ListeningSocketsMixin, _run_listening_sockets_command).
One framed round trip: ss -lntup for every listening TCP and bound UDP
socket, then /proc/<pid>/cgroup for each process holding one, which names
the systemd service (v2, nested slices, v1's name=systemd hierarchy) or
the container (docker-<id>.scope, /docker/<id>) it runs in.
- Root: only root sees every process. The script goes as one sh -c
argument, so a sudo -n prefix covers all of it; when that brings no
report back the reading runs again unprivileged and says it is not
attributed.
- No -H: iproute2 before 4.10 fails on it, which would read as nothing
listening. The header is skipped instead.
- A host without ss raises ListeningSocketsUnavailable; a report cut short
or a failing ss raises ValueError.
- The reading is raw: docker-proxy shows up as docker.service, loopback as
loopback. What counts as reachable is the consumer's call.
2.4.0. For netOrk#658.
netOrk MVP 5 measures "security updates applied within N days" and starts
patch runs inside agreed windows. That needs three things every Linux driver
reads the same way, so they live here once:
- UpdateDict gains optional `origin` and `security` (None = unknown).
- package_updates: APT_UPGRADABLE_COMMAND and parse_apt_upgradable(). apt's
suites are the origin; a "-security" suite makes it a security update;
several architectures of one package are one entry. The command runs
through a pipe with its exit status printed inside the group: through a
pseudo-terminal apt drew progress and keypad codes, one of which (ESC >)
a screen-scraping read took for a prompt and stopped at. The parser raises
ValueError when apt failed or its status never arrived.
DNF_SECURITY_COMMAND / parse_dnf_security() / nevra_name() for dnf/yum.
- host_status: HOST_STATUS_COMMAND, parse_host_status(), HostStatusMixin
(template form, hook _run_host_status_command). reboot_required from
/var/run/reboot-required, needs-restarting -r, or a newer kernel of the
running flavour (a Raspberry Pi carries two flavours side by side);
auto_updates from APT::Periodic::Unattended-Upgrade with its timer, or
dnf-automatic. HostStatusDict in models.py.
- terminal.strip_terminal_codes(): CSI, OSC and two-character escapes, now
also used by the systemd parser.
- UpdateMixin: contract refresh_available_updates(); get_available_updates'
docstring now states the rule that a reader raises when it cannot read and
never returns [] for "don't know".
Fixtures are real output from Ubuntu 24.04, Debian 13 / OMV, Raspberry Pi OS
and Proxmox VE 9. Version 2.3.0.
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