Files
napalm-device-types/README.md
T
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

266 lines
8.4 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# napalm-device-types
Abstract intermediate device-type base classes for [NAPALM](https://napalm.readthedocs.io/) 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:
```python
# 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`):
```python
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):
```python
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
```bash
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
```python
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
```python
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
```python
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
```python
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
```python
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
```python
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:
```python
from napalm_device_types.models import (
PhysicalDiskDict,
DiskPoolDict,
NASShareDict,
VolumeSnapshotDict,
)
```
## Development
```bash
git clone https://github.com/chrismanivong/napalm-device-types.git
cd napalm-device-types
pip install -e ".[dev]"
```
Run type-checking:
```bash
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](LICENSE) for details.