HostRebootMixin declares reboot_host(), mixed into DeviceTypeDriver so any device may be restartable. netOrk restarted hosts by sending /sbin/reboot through a driver's private _send_command; a driver talking to an API had no such method and the reboot was silently skipped. HypervisorDriver gains GUEST_AGENT_PACKAGES / GUEST_AGENT_RUNCMD, the agent cloud-init installs so the hypervisor can read a new VM's IP. The default stays qemu-guest-agent; VMware declares open-vm-tools. NetworkTargetDict.kind may be "portgroup": a VMware port group fixes its VLAN like an SDN vnet does, without being one.
napalm-device-types
Abstract intermediate device-type base classes for NAPALM drivers.
Instead of inheriting directly from napalm.base.NetworkDriver, a driver can inherit from one of the device-type classes here to gain a richer, type-specific contract:
# without napalm-device-types
class OpenWrtDriver(NetworkDriver):
...
# with napalm-device-types
from napalm_device_types import AccessPointDriver
class OpenWrtDriver(AccessPointDriver):
...
Why?
NAPALM's NetworkDriver defines a common interface for all network devices. In practice, devices fall into distinct categories with very different capabilities. A switch exposes spanning-tree and PoE data; a firewall exposes NAT tables and VPN tunnels; a NAS exposes disk pools and shares. Writing these methods directly in a concrete driver mixes concerns and makes drivers harder to discover and compare.
napalm-device-types sits in between: it adds one well-typed layer of abstract methods per device category, so every driver for the same category exposes the same interface.
Roles: what a device is
A device is often several things at once. A QNAP NAS runs VMs on a Linux userland; an OpenMediaVault box is a NAS built on Debian. So a driver inherits one role base per role its device fills, and the order it lists them in is the ranking:
class QnapQtsDriver(StorageDriver, HypervisorDriver, LinuxDriver):
... # primary_role_of(...) == "storage"
roles_of(cls), role_keys_of(cls) and primary_role_of(cls) read that back. Nothing
restates the ranking: there is no precedence table and no attribute to override.
Role bases declare; they never implement
A role base must not contain a single runtime method — not even a
NotImplementedError placeholder. Its methods are declared under if TYPE_CHECKING:
class StorageDriver(DeviceTypeDriver):
"""Contract, not code."""
ROLE: str = "storage"
TYPE_LABEL: str = "Storage"
if TYPE_CHECKING: # nothing exists at runtime
def get_disks(self) -> List[PhysicalDiskDict]: ...
This is not a style preference. A placeholder on a base class is not neutral under multiple inheritance: it wins the MRO against a sibling base's working implementation and silently replaces it. Adding one stub to a base is therefore a breaking change for every driver that mixes that base with another. It happened three times here before the rule existed, and each time the fix was hand-written forwarding methods in the driver.
Two things follow, and both are improvements:
hasattris truthful again. A method exists on a driver class exactly when that driver implemented it, which is how netOrk asks "can this driver list disks".- A method that was never implemented raises
AttributeError, notNotImplementedError. Ask before calling.
Function classes: what a device can do
Behaviour shared across roles lives in a function class, exactly once, and a role base
is a thin bundle over them — PackageManagementMixin, HealthMetricsMixin,
ServiceControlMixin, UpdateMixin, NatVpnMixin, MacAclMixin, FirewallRuleMixin,
DhcpServerMixin, PingSweepMixin, ConfigLifecycleMixin, InterfaceFilterMixin.
HostRebootMixin (reboot_host) is mixed into DeviceTypeDriver itself, since any
device may be restartable; like the others it only declares.
A function class may use the template form — public method concrete, the
device-specific part a _hook declared under if TYPE_CHECKING — when the base
genuinely does work on the result: normalising, sorting, validating, or orchestrating
several hooks. ConfigLifecycleMixin.compare_config over _get_running_config is the
model. Where the base would only pass the call through, declare the method directly;
two names for one pass-through is ceremony, not design.
NAPALM's own getters (get_facts, get_interfaces, ping, get_config) are never
wrapped in a template — they belong to NAPALM, and code outside this repo relies on
their contract.
Design principle: generic vs. device-specific logic
When adding behavior to a device-type base class, split it along one line: would this exact logic work unchanged for a different vendor's driver of the same device-type, if that driver only implemented the same abstract methods?
- If yes, it's generic — implement it once as a concrete method on the device-type base class (here, in this repo).
- If no — it talks to the device itself (a specific REST endpoint, a CLI command, a vendor-specific payload format) — it belongs in the concrete driver as the implementation of an abstract method the base class declares.
Concretely: matching/comparison/reconciliation algorithms, orchestration flows, and
generic data shapes belong here. Only the actual device communication belongs in
vendor/napalm-<name>.
Worked example — firewall rule diff/apply (FirewallDriver):
class FirewallDriver(DeviceTypeDriver):
# Abstract — every driver implements its own device communication.
def get_firewall_rules(self) -> List[FirewallRuleDict]: raise NotImplementedError
def apply_firewall_rule(self, rule: FirewallRuleDict, *, uuid: Optional[str] = None) -> Dict[str, Any]: raise NotImplementedError
def commit_firewall_rules(self) -> Dict[str, Any]: raise NotImplementedError
# Concrete — the matching/comparison/orchestration algorithm is identical
# for every firewall vendor, so it lives here once.
def diff_firewall_rules(self, desired: List[FirewallRuleDict]) -> FirewallRuleDiffDict:
... # matches self.get_firewall_rules() against `desired` by description
def apply_firewall_ruleset(self, desired: List[FirewallRuleDict]):
... # computes the diff, calls apply_firewall_rule() per change, commits
The same split applies to DhcpServerMixin: get_dhcp_reservations/
apply_dhcp_reservation/commit_dhcp_reservations are abstract (Kea REST on
OPNsense, dnsmasq/odhcpd UCI on OpenWrt), while diff_dhcp_reservations and
apply_dhcp_reservationset are concrete — matching by normalised MAC and the
apply-then-commit orchestration are identical for every DHCP server.
A new driver (FortiGate, pfSense, …) gets diff_firewall_rules/
apply_firewall_ruleset for free the moment it implements the three abstract
methods — it never needs to reimplement the reconciliation logic itself.
Second worked example — ping sweeps (PingSweepMixin, mixed into
DeviceTypeDriver, so every device-type driver has it):
class PingSweepMixin:
# Concrete — the loop, the reply parsing, the target cap and the progress
# reporting are the same for every device that can ping at all.
def ping_sweep(self, destinations, *, count=1, timeout=1, …) -> PingSweepResultDict:
... # calls NAPALM's standard ping() once per destination
A driver becomes a usable sweep source the moment it implements NAPALM's
ping() — nothing else is required, and driver_supports_ping(cls) reports
whether it did (introspection, not a hand-maintained list). A driver whose
device offers something genuinely faster overrides ping_sweep and keeps the
return shape: napalm-opnsense starts a batch of ping jobs over the
diagnostics API, waits once for all of them, and reads every result with a
single request — a per-host loop would be unusable there.
This mirrors a similar split already documented on the consumer side, in NetOrk's
docs/ARCHITECTURE.md ("Device Warnings — Trennung von Erkennung und
Präsentation"): drivers return raw signals, the higher layer gives them meaning.
Same shape of separation, different axis — device-specific vs. generic here,
detection vs. presentation there.
Installation
pip install napalm-device-types
Requires Python ≥ 3.9 and NAPALM ≥ 4.0.
Available base classes
| Class | Target devices | Example implementations |
|---|---|---|
AccessPointDriver |
Wireless access points | OpenWrt, Ubiquiti UniFi, Cisco Meraki AP |
SwitchDriver |
Ethernet switches | Cisco IOS, Arista EOS, Juniper EX |
FirewallDriver |
Firewalls & UTM appliances | pfSense, Fortinet FortiOS, Cisco ASA |
HypervisorDriver |
Hypervisors & virtualisation platforms | Proxmox VE, VMware ESXi, KVM/libvirt |
OSDriver |
General-purpose operating systems | Linux, BSD, macOS |
StorageDriver |
Storage appliances & NAS/SAN | TrueNAS, Synology DSM, QNAP QTS |
ResidentialGatewayDriver |
Router + firewall + AP in one box | OpenWrt, FritzBox |
Mixins mixed into the classes above rather than used on their own:
ConfigLifecycleMixin (config load/compare/commit/rollback), PingSweepMixin
(subnet sweeps), and DhcpServerMixin (static DHCP reservations — mixed into
FirewallDriver and ResidentialGatewayDriver, since both commonly run the
DHCP server for their networks).
Usage
Access Point
from napalm_device_types import AccessPointDriver
class OpenWrtDriver(AccessPointDriver):
def get_wireless_clients(self):
# return List[WirelessClientDict]
...
def get_ssids(self):
# return Dict[str, SSIDDict]
...
Switch
from napalm_device_types import SwitchDriver
class CiscoIOSDriver(SwitchDriver):
def get_spanning_tree(self):
# return Dict[str, SpanningTreeDict]
...
def get_poe_status(self):
# return PoESummaryDict
...
Firewall
from napalm_device_types import FirewallDriver
class PfSenseDriver(FirewallDriver):
def get_nat_translations(self):
# return List[NATTranslationDict]
...
def get_vpn_tunnels(self):
# return Dict[str, VPNTunnelDict]
...
Hypervisor
from napalm_device_types import HypervisorDriver
class ProxmoxDriver(HypervisorDriver):
def get_vms(self):
# return List[VMDict]
...
def create_vm_snapshot(self, name, snapshot, description="", include_memory=False):
...
OS / Linux
from napalm_device_types import OSDriver
class LinuxDriver(OSDriver):
def get_packages(self):
# return List[PackageDict]
...
def get_services(self):
# return List[ServiceDict]
...
def get_users(self):
# return List[UserDict]
...
def get_processes(self):
# return List[ProcessDict]
...
def get_cron_jobs(self):
# return List[CronJobDict]
...
Storage / NAS
from napalm_device_types import StorageDriver
class TrueNASDriver(StorageDriver):
def get_disks(self):
# return List[PhysicalDiskDict]
...
def get_shares(self):
# return Dict[str, NASShareDict]
...
Return types
All return types are TypedDict classes defined in napalm_device_types.models. Import them directly for type annotations in your driver:
from napalm_device_types.models import (
PhysicalDiskDict,
DiskPoolDict,
NASShareDict,
VolumeSnapshotDict,
)
Development
git clone https://github.com/chrismanivong/napalm-device-types.git
cd napalm-device-types
pip install -e ".[dev]"
Run type-checking:
mypy napalm_device_types
Contributing
- Fork the repository
- Create a feature branch (
git checkout -b feature/router-driver) - Implement your changes
- Open a Pull Request
License
Apache-2.0 – see LICENSE for details.