christianmanivong b8977cdaa5 feat(dhcp): add DhcpServerMixin for static DHCP reservations
Adds the generic half of DHCP reservation management: diff_dhcp_reservations
matches desired against live reservations by normalised MAC, and
apply_dhcp_reservationset walks the diff and commits once at the end.

Both are concrete here because neither is vendor-specific — only
get_dhcp_reservations/apply_dhcp_reservation/commit_dhcp_reservations touch
the device (Kea REST on OPNsense, dnsmasq/odhcpd UCI on OpenWrt).

Two deliberate choices:

- The MAC is the matching key, not a description as with firewall rules. A
  reservation has a natural identity and this is it. That also means a host
  moving to another VLAN is an update of the existing entry rather than a
  second one for the same MAC.
- An empty diff skips the commit. Committing reloads the DHCP daemon and
  drops in-flight requests, which is too high a price for a no-op run. This
  differs from apply_firewall_ruleset, which always commits.

Live reservations with no desired counterpart are never reported for
deletion — a DHCP server routinely carries hand-created entries the caller's
desired set was never meant to describe.

Mixed into FirewallDriver and ResidentialGatewayDriver: both device types
commonly run the DHCP server for their networks.
2026-08-19 07:24:39 +07:00
2026-05-11 21:31:52 +02:00
2026-05-11 21:31:52 +02:00

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.

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

  1. Fork the repository
  2. Create a feature branch (git checkout -b feature/router-driver)
  3. Implement your changes
  4. Open a Pull Request

License

Apache-2.0 – see LICENSE for details.

S
Description
No description provided
Readme
206 KiB
Languages
Python 100%