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.
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.
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 snapshot_create(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.