Metadata-Version: 2.4
Name: napalm-device-types
Version: 4.21.1
Summary: Abstract device-type base classes for NAPALM drivers
Author-email: Christian Manivong <christian@manivong.de>
License-Expression: Apache-2.0
Project-URL: Repository, https://git.netork.io/NAPALM/napalm-device-types
Project-URL: Issues, https://git.netork.io/NAPALM/napalm-device-types/issues
Project-URL: Changelog, https://git.netork.io/NAPALM/napalm-device-types/src/branch/main/CHANGELOG.md
Keywords: napalm,network,automation,driver,access-point,switch,firewall,hypervisor,storage,nas
Classifier: Development Status :: 3 - Alpha
Classifier: Intended Audience :: Developers
Classifier: Intended Audience :: System Administrators
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: System :: Networking
Classifier: Topic :: System :: Systems Administration
Classifier: Typing :: Typed
Requires-Python: >=3.10
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: napalm>=4.0
Requires-Dist: typing-extensions>=4.0
Provides-Extra: dev
Requires-Dist: pytest>=8; extra == "dev"
Requires-Dist: pytest-cov>=5; extra == "dev"
Requires-Dist: mypy>=1.11; extra == "dev"
Requires-Dist: ruff>=0.6; extra == "dev"
Dynamic: license-file

# 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.

## 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**:

```python
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.

A server's baseboard management controller (Supermicro BMC, Dell iDRAC, HPE iLO,
Lenovo XCC) is a role of its own, `ManagementControllerDriver` (`management_controller`),
reached over Redfish or IPMI (`USES_SSH = False`). It is not the server it manages:
`reboot_host` restarts the BMC (Redfish `Manager.Reset`), while `set_power_state`
(`On`, `GracefulShutdown`, `ForceOff`, `GracefulRestart`, `ForceRestart`, `PowerCycle`)
acts on the server. `get_hardware_inventory` carries the server's serial number, system
UUID and NIC MACs -- what ties the BMC to the host it manages. Its sensors come through
NAPALM's `get_environment`, with `cpu` and `memory` empty, since a BMC does not know the
host OS's load; it has no `HealthMetricsMixin` (SNMP) and no command channel.

### 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`:

```python
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:

- **`hasattr` is 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`, not
  `NotImplementedError`.** Ask before calling.

The rule covers this package's methods, not NAPALM's. `NetworkDriver` still carries
a runtime `NotImplementedError` stub for each of its getters (`get_interfaces`,
`get_environment`, `get_users`, …), and every role base inherits it. So a NAPALM getter
must come from the driver class itself or from a mixin listed **before** the first role
base; a mixin listed after it loses the MRO to NAPALM's stub. For the same reason
`hasattr` says nothing about NAPALM's getters, and the OS role's account listing is
`get_os_users`, not `get_users` -- NAPALM's `get_users` has a different shape.

## 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.

**Fixes are templates** (`napalm_device_types.fixes`, since 4.13). A change the
application wants made on a device runs as fixed steps the driver implements, with values
the caller supplies: `SnmpAgentMixin.configure_snmp_agent(settings)` (install, configure,
firewall, start), `EnsurePackageMixin.ensure_package(name)`,
`PackageProxyMixin.configure_package_proxy(url)` and
`ContainerEngineMixin.grant_container_engine_access(user)`. A driver whose device takes the
change in one call overrides the template method. A driver mixes in the fixes its device
can take; none is on a role base. They replace `run_device_action`, which is deprecated.

**SNMP is the application's** (`docs/STRUCTURE.md`). `HealthMetricsMixin` is deprecated
since 4.12 and goes in 5.0. A driver does not collect over SNMP; a driver whose device
publishes its health values under OIDs of its own declares them as data:

```python
class MySwitchDriver(SwitchDriver):
    SNMP_HEALTH_PROFILE: ClassVar[SnmpHealthProfile] = {
        "uptime": {"oids": ["1.3.6.1.2.1.1.3.0"]},
        "cpu": {"oids": ["<vendor cpu oid>", "<older firmware's>"]},
        "memory": {"total": {"oids": ["<oid>"]}, "free": {"oids": ["<oid>"]}, "unit": "KiB"},
    }
```

A value is `oids` (the first that answers wins) or a `table` folded by `first`, `average`
or `max`, with an optional `pattern` for an agent that answers with text. What the profile
does not name, the application reads from the standard UCD-MIB and IF-MIB.

`KernelFactsMixin` (`get_kernel_facts`) is the exception that is mixed in by a driver
rather than by a role base: what a Linux kernel has built and loaded is read the same way
everywhere, so the command and its parse are concrete here and a driver supplies only
`_run_kernel_facts_command`. `OSDriver` does not carry it — a Windows host is an OS driver
too, and `hasattr(driver, "get_kernel_facts")` has to stay truthful.

`ServicePackagesMixin` (`get_service_packages`) is mixed in the same way: which installed
packages each systemd service is made of — the one shipping its unit file and the one
shipping the binary its `ExecStart` runs, by `dpkg -S` or `rpm -qf`, not the libraries
that binary links against. An application needs it to let a statement about a service
(the postgresql unit) reach the findings on its packages. A driver supplies
`_run_service_packages_command`.

`HostStatusMixin` (`get_host_status`) is mixed in the same way: whether a Linux host needs
a reboot to finish an update (`/var/run/reboot-required`, `needs-restarting -r`, or a newer
kernel of the running flavour installed), whether it patches itself (unattended-upgrades,
dnf-automatic) and when that last succeeded (apt's `upgrade-stamp`), and whether dpkg was
left halfway (a journal in `/var/lib/dpkg/updates` or a half-configured package; unknown
while a package manager is running), and how full `/`, `/boot` and `/var` are (`df -Pk`,
each filesystem once, in bytes; how full is too full is the caller's decision). `package_updates` holds the shared apt and dnf parsers: apt's suites become
an update's `origin`, a `-security` suite makes it a security update, and dnf's security
advisories do the same.

`ListeningSocketsMixin` (`get_listening_sockets`) is mixed in the same way: every listening
TCP and bound UDP socket from `ss -lntup`, with the systemd service or container behind it
from `/proc/<pid>/cgroup`, in one round trip. A driver supplies
`_run_listening_sockets_command(command, privileged=)`; the command arrives as one `sh -c`
argument, so a `sudo -n` prefix covers all of it. Without root `ss` names only the login
user's processes, and the reading says so (`attributed: false`) instead of failing. A host
without `ss` is read with `netstat -lntup` (OpenWrt's busybox, old net-tools); on OpenWrt the
cgroup names the procd service (`/services/<name>/<instance>`). A host with neither raises
`ListeningSocketsUnavailable`.

`HostFirewallMixin` (`get_host_firewall`) reads a Linux host's own netfilter rules the same
way: `nft -j list ruleset`, with the tables iptables-nft manages taken from `iptables-save`
instead, because nft shows their extensions (`-m conntrack`, `-m set`) only as an opaque `xt`.
A legacy iptables is a ruleset of its own and is kept beside nft's. Every chain says its family,
table, hook, priority and policy; every rule is a list of matches (`iifname`, `saddr`, `dport`,
`ct_state`, ...) and a verdict. A condition it does not model is an `unknown` match with its
text, never left out. A driver supplies `_run_host_firewall_command(command, privileged=)`; both
tools need root, so a host where neither could be read raises `HostFirewallUnavailable` rather
than reading as one without rules. Whether the rules keep a service unreachable is the
consumer's call.

`VirtualIpsMixin` (`get_virtual_ips`) reads the addresses a host's HA configuration lets
float between machines: vip-manager (5.x's `--config` YAML, 1.x's `-ip=${VIP_IP}` resolved
through the unit's environment) and keepalived (`virtual_ipaddress` of every
`vrrp_instance`, `include` followed four levels deep). It reports what is *declared*, not
what the host holds right now -- that is in its interface addresses. The same files hold
the etcd password and `auth_pass`, so an awk program filters **on the host** and prints
allowlisted fields only; a secret never reaches the caller. Root first, then without it.
A mechanism it could not read completely is `None` ("did not look"), a host without it
`[]`. The command is about 4 kB, so a driver sends it on an exec channel
(`_run_virtual_ips_command(command, privileged=)`), not typed into a shell.

`ServiceClustersMixin` (`get_service_clusters`) reads the configuration that proves two
hosts run one service together -- not merely that they answer at one address: Patroni
(`scope`, `name` and `restapi.connect_address` from its YAML, then the members and roles
its REST API's `/cluster` lists from the DCS), adguardhome-sync (`ORIGIN_URL` and
`REPLICA*_URL` of its container: the origin pushes to each replica) and BunkerWeb (a
worker's `API_WHITELIST_IP` names the manager it takes its configuration from, a manager's
`BUNKERWEB_INSTANCES` its workers). Each member carries its role as the mechanism words it
and the index of the member it takes its configuration from (`config_from`). The same
configuration holds the superuser password, the sync's logins and BunkerWeb's database URI,
so awk programs filter **on the host**: allowlisted fields only, a URL without its
`user:password@`, and the DCS endpoints stay on the host. Root first (Patroni's YAML is the
postgres user's), then without it. `None` per mechanism is "did not look", `[]` "has none".
The container engine is asked through `_container_engine_binary` where the driver has one.
A driver sends the command on an exec channel (`_run_service_clusters_command(command,
privileged=)`).

`get_dhcp_ha` (declared on `DhcpServerMixin`, optional) is what a DHCP server's own HA
configuration says: whether HA is on, which peer this server is, each peer's name, role --
as the server words it, not a shared vocabulary -- and, where the server has one, the URL
its HA listener answers on. It is configuration, not state: whether the pair is in hot
standby right now is not in it. `ConfigSyncMixin` (`get_config_sync`) is where a host
pushes its configuration (`peer`, None for no one) and for which services, by the names
`get_services` reports; sync items the driver has no service name for stay visible under
`unmapped`. A driver mixes it in -- no role promises it. Both getters raise when they could
not read; neither answers "off" for "could not ask". What the facts mean for a pair (hot
standby, which side to write to) is the caller's to work out.

A firewall's CARP addresses are a third mechanism, `carp`: a firewall driver reads them from
its own API and reports them under the reading's `carp` key (`vip_manager` and `keepalived`
then None). What a CARP state means is protocol knowledge and stays in that driver. The
contract only carries the result: unlike the other two, a CARP entry says who holds the
address (`holds`), because a firewall's interface list need not show it. `holds` None
leaves the holder to the interfaces.

`SystemIdentityMixin` (`get_system_identity`) reads a host's SMBIOS identity -- system UUID,
product serial and board serial from `/sys/class/dmi/id` -- which is what ties a server to
its BMC (`ManagementControllerDriver.get_hardware_inventory` reports the same values from
the other side). Vendor placeholders (`0123456789` on Supermicro boards, `To be filled by
O.E.M.`, `Default string`, all-zero or the common `03000200-…-000700080009` UUID) read as
empty: matching on them would join unrelated machines. `is_placeholder_identity` is the
same filter for a BMC driver's side. `product_uuid` and `product_serial` are root's, so a
driver supplies `_run_system_identity_command(command, privileged=)`.

`OpenSshServerMixin` (`harden_ssh_server`, `get_ssh_server_algorithms`) takes algorithms
out of what an OpenSSH server offers -- the ones ssh-audit flags. It reads the lists sshd
really uses (`sshd -T`), takes out what was asked and writes the rest to
`/etc/ssh/sshd_config.d/00-netork-ssh-hardening.conf`; only where sshd_config has no such
`Include` is sshd_config itself rewritten, after a copy. It never removes the last algorithm
of a class, 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 old state back.
A driver mixes it in and supplies `run_command`; a platform that regenerates sshd_config
itself (an appliance with its own database) must not, or opts out with
`harden_ssh_server = None`.

**Update readers raise when they cannot read.** `get_available_updates` returns an empty
list only when nothing is pending; netOrk keeps "pending since" per package, and an empty
list for "don't know" would reset it.

`SystemdServicesMixin` (`get_services`, `manage_service`) is mixed in the same way, by
the drivers whose host runs systemd. Listing the services, checking a unit name and
reading an action's exit status are the same on every such host, so they are concrete
here, and a driver supplies only `_run_service_command(command, *, privileged, timeout)`
— how a command reaches its host and how it gains root there. The listing is one round
trip (`list-unit-files` plus one `systemctl show` over every loaded unit) instead of an
`is-enabled` and a `show` per unit. A host without systemd raises `SystemdUnavailable`,
a `NotImplementedError`, so a driver can fall back to another init system.

### Access channels: why there is no container model here

`CommandChannelMixin` declares a driver's public channel to its host:
- `run_command(command, *, privileged=False, timeout=60, stdin=None)` returns a `CommandResult(stdout, stderr, exit_code)`;
- `open_stream(command, *, privileged=False)` returns a `ByteStream` to the command's stdin and stdout.

The mixin sits in `DeviceTypeDriver` and only declares, so `hasattr(driver, "run_command")` is true exactly where a driver implements it. For SSH drivers, `channel.run_on_transport` and `channel.open_stream_on_transport` are the implementation over a paramiko exec channel: no PTY, stderr kept apart, a real exit code. How a command gains root stays with the driver.

**`ContainerEngineMixin` is deliberately different from `SystemdServicesMixin`.** The systemd mixin owns its command and its parse; this one owns neither.
- `container_engines()` says which engines the host offers (`[{"engine": "docker", "api": "docker-engine"}]`).
- `open_container_engine(engine)` returns a `ContainerEngineConnection`:
  - `open_api()` is a stream to the engine's API (`docker system dial-stdio`);
  - `run_cli(args)` and `stream_cli(args)` run the engine's CLI with arguments the caller chooses. With `privileged=True` the driver runs it the way `run_command` gains root, for an engine that refuses the login user.
- The driver decides only *how* the engine is reached. Its hook `_container_engine_binary(engine)` returns, for QNAP, the Container Station path, and the caller never sees it.

**What runs on the engine, and what to do with it, is netOrk's** (NetOrk/netork#765): the container model, the Engine API requests, compose, updates. Above the connection everything is specific to the service, so this package abstracts the connection and nothing more.

A driver mixes `ContainerEngineMixin` in itself; `OSDriver` does not carry it. A Windows host is an OS driver too, and has no `dial-stdio` to offer over WinRM.

**Never half-close early.** `ByteStream.write` does not close anything; only `close_write` does. `dial-stdio` hands the daemon a half-close as "client gone", and the daemon then answers an unfinished request with HTTP 499.

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.

### SSH host keys: trust on first use

A driver that accepts any host key logs in with the device's credentials at whatever
answers on the device's address (NetOrk/netork#1167). The store of trusted keys is the
caller's; `ssh_host_keys.HostKeyCheck` is how every SSH driver verifies against it:

- **In:** `optional_args["ssh_host_keys"]` = `{"22": {"key_type": "ssh-ed25519", "key": "<base64>"}}`,
  by port. The driver takes the key for the port it connects to. With one, any other key is
  refused *before* authentication and the connection raises `SshHostKeyMismatch` (a
  `ConnectionException`). Without one, the first key is accepted.
- **Out:** `optional_args["ssh_host_key_observer"]`, a callable, gets one dict per
  connection: `{"host", "port", "seen": {"key_type", "key", "fingerprint"}, "expected", "accepted"}`.
  `accepted` is False only for a refused key. A failing observer is logged, never raised.
- **paramiko:** `HostKeyCheck(optional_args, host, port).connect(client, **connect_kwargs)`.
- **netmiko:** `with check.netmiko(self.netmiko_optional_args) as kwargs: ConnectHandler(..., **kwargs)`,
  then `check.observe(conn.remote_conn_pre.get_transport())`. The known key reaches netmiko as a
  `known_hosts` file that exists only inside the block, with `ssh_strict=True`.

A caller that passes neither key gets first use on every connection, as before; a driver
that ignores both still connects. Old and new versions on either side keep working.

## 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** (`FirewallRuleMixin`, in `FirewallDriver`):

```python
class FirewallRuleMixin:  # mixed into FirewallDriver
    # Declared only — every driver implements its own device communication.
    if TYPE_CHECKING:

        def get_firewall_rules(self) -> List[FirewallRuleDict]: ...
        def apply_firewall_rule(
            self, rule: FirewallRuleDict, *, uuid: Optional[str] = None
        ) -> Dict[str, Any]: ...
        def commit_firewall_rules(self) -> Dict[str, Any]: ...

    # 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.10 (tested up to 3.13) 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 |
| `PhoneDriver` | IP telephony endpoints | Yealink |
| `MediaDriver` | Speakers, streamers, media renderers | Sonos, Chromecast |
| `ManagementControllerDriver` | Server baseboard management controllers | Supermicro BMC, Dell iDRAC, HPE iLO, Lenovo XCC |

Mixins that come with the classes above: `PingSweepMixin` (subnet sweeps, in
every driver through `DeviceTypeDriver`) and `DhcpServerMixin` (static DHCP
reservations — mixed into `FirewallDriver` and `ResidentialGatewayDriver`,
since both commonly run the DHCP server for their networks).
`ConfigLifecycleMixin` (config load/compare/commit/rollback) is mixed in by a
driver itself.

### Fingerprints

`DeviceTypeDriver` carries what discovery matches a device against: `PORT_SPECS`,
`SNMP_OBJECT_ID_PREFIX`, `SNMP_FINGERPRINT`, `SSH_FINGERPRINT`, `HTTP_FINGERPRINT` and
`OUI_PREFIXES`. A `FingerprintRule` is a lowercase substring with a weight; `mandatory`
disqualifies the driver when it is missing, `negative` subtracts.

Discovery compares drivers by score divided by the most a driver could score. Rules that
can never match together, such as one vendor's landing page or another's, share a
`group`: only the strongest rule of a group counts towards that maximum
(`rules_max_weight`). Without it, a driver that recognises three vendors' pages would
score a third as well as one that recognises a single page:

```python
HTTP_FINGERPRINT = [
    FingerprintRule("aten international", 6.0, group="landing_page"),  # Supermicro
    FingerprintRule("idrac", 6.0, group="landing_page"),
    FingerprintRule("integrated lights-out", 6.0, group="landing_page"),  # HPE iLO
]
```

## 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]
        ...

    def get_port_forwards(self):
        # return List[PortForwardDict] — forwards from the WAN only, never a
        # redirect between internal networks (shared with home gateways)
        ...
```

### Hypervisor

```python
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): ...

    def get_vm_cpu_types(self):
        # optional — return List[VMCpuTypeDict]: the CPU models a new VM may get
        # on this node, each with its cpuinfo flags and whether the node can run
        # it; the name goes to create_vm_from_cloud_init(cpu_type=...)
        ...
```

A hypervisor that provisions VMs from cloud images declares, per guest OS,
the agent cloud-init installs so the hypervisor can read the new VM's IP:
`GUEST_AGENTS = {guest_os: (packages, runcmd)}`. Its keys are the guests the
driver can provision, and `create_vm_from_cloud_init(guest_os=...)` refuses
any other. A QEMU-based driver takes `provisioning.QEMU_GUEST_AGENTS`
(Linux, FreeBSD, OpenBSD). `provisioning.network_config()` writes the
network-config v2 every cloud-init flavour reads (FreeBSD's nuageinit reads
no other), and `provisioning.split_compression()` says how a packed image is
unpacked; both are the same for every hypervisor.

### 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_os_users(self):
        # return List[UserDict] -- not NAPALM's get_users, whose shape differs
        ...

    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://git.netork.io/NAPALM/napalm-device-types.git
cd napalm-device-types
pip install -e ".[dev]"
```

Run type-checking:

```bash
mypy napalm_device_types
```

## Contributing

This package follows the
[NAPALM driver standard](https://git.netork.io/NAPALM/driver-standard/src/branch/main/STANDARDS.md):
test first, ruff and strict mypy, Conventional Commits, one pull request per change
against `main`. CI checks what a tool can check:

```bash
pip install "napalm-standard @ git+https://git.netork.io/NAPALM/driver-standard.git"
napalm-standard check && napalm-standard ratchet
```

## License

Apache-2.0 – see [LICENSE](LICENSE) for details.
