Part of netork#85. Reservations were the only DHCP desired state the mixin
knew about; this adds the layer above them — the ranges a device serves and
the options it publishes with them.
The identity is the CIDR, matched rather than compared, the way `mac` is for
a reservation. normalize_cidr deliberately does not rewrite the network
address: turning 10.10.20.5/24 into 10.10.20.0/24 would make a typo silently
match a real subnet and then apply that caller's pools and options to it.
option_data is compared per option, and only over the options the caller
named. An absent key means "not managed", not "should be empty" — without
that rule a caller managing only domain_search would diff against every
option the server autocollects (routers, domain_name_servers, ntp_servers)
and reconfigure the DHCP daemon on every single run.
Neither diff deletes. For subnets that is not merely conservative: removing
one takes DHCP down for a whole VLAN, and the diff cannot tell "no longer
wanted" from "was never this caller's to describe".
commit_dhcp_subnets is separate from commit_dhcp_reservations even where a
driver implements both with the same call — the two desired-state sets are
applied independently, and a caller that changed only subnets should not
have to know which reload the vendor happens to share.
19 new tests against an in-memory fake; no vendor driver needed.
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.
Sweeping a range is orchestration, not device mechanics: the only
vendor-specific part is executing a single ping, and NAPALM already
standardises that. PingSweepMixin therefore owns the loop, the reply parsing,
the target cap and the progress reporting, and is mixed into DeviceTypeDriver
so any driver implementing ping() becomes a usable sweep source without
writing sweep code of its own.
driver_supports_ping() answers "can this driver ping?" by introspection
instead of a hand-maintained list, with SUPPORTS_PING = False as the opt-out
for a driver that inherits a ping it cannot actually use.
The generic implementation is deliberately sequential — a NAPALM connection is
a single session and not safe to drive from several threads at once. A driver
whose device offers something faster overrides ping_sweep and keeps the return
shape; see napalm-opnsense's batched job API version.
FirewallRuleDict/FirewallRuleDiffDict (models.py) plus three abstract
methods (get_firewall_rules/apply_firewall_rule/commit_firewall_rules)
concrete drivers implement, and two concrete methods every driver gets
for free: diff_firewall_rules() matches desired vs. live rules by
description and reports add/update (never delete -- a firewall may carry
manually-created rules a caller's desired set was never meant to
describe); apply_firewall_ruleset() orchestrates applying the diff and
yields progress lines, meant for streaming to a caller.
This is the generic reconciliation engine NetOrk's Firewall Profile
feature needs against OPNsense -- kept here instead of in
napalm-opnsense since the matching/comparison/orchestration logic is
identical for any firewall vendor that implements the three abstract
methods.
Abstract method for sending a Wake-on-LAN magic packet through a
firewall's driver connection, following the same contract style as
get_nat_translations/get_security_zones. Raises NotImplementedError
by default; concrete drivers implement it per their own API.
Replaces template-clone semantics (template: str, existing Proxmox template
VMID) with image_url: str — the driver now downloads the cloud image itself
and imports it as the VM's root disk, rather than requiring an admin to have
pre-built a template. Adds image_checksum for optional verification and a
separate download_timeout since image downloads can take much longer than
the rest of provisioning.
Returns selectable bridge/vnet targets for a new VM's NIC, distinguishing
real bridges (Linux, OVS) from SDN vnets, and exposing whether a NIC on that
target may additionally carry a vlan_tag (Linux bridge vlan_aware flag, OVS
always, SDN vnet never — the VLAN is already fixed by the vnet's zone/tag).
Add three new methods to HypervisorDriver:
- create_vm_from_cloud_init(): provision VM from template with dual-NIC config
- destroy_vm(): stop and remove VM with optional disk cleanup
- get_vm_status(): poll runtime status, optionally wait for IP via guest-agent
New TypedDicts VMProvisionResultDict and VMStatusDict in models.py
document the provisioning API contract.
Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
The comment said AccessPointDriver sits BEFORE mixins, which was wrong after
the mixin refactor. NetworkDriver (parent) raises NotImplementedError for
standard NAPALM methods, so AccessPointDriver must come LAST in the MRO to
avoid shadowing mixin implementations.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
These three methods were defined with `raise NotImplementedError` in
AccessPointDriver. Because AccessPointDriver appears before the mixin
classes in the MRO of concrete drivers (e.g. OpenWrtDriver), this caused
the abstract body to be called instead of the mixin implementation.
Symptoms:
- get_radio_status() → NotImplementedError, silently caught in poll →
radio_snapshot never updated after the initial snap
- get_ssids() / get_wireless_clients() → same silent failure
Fix: remove the method bodies from AccessPointDriver entirely. Python then
continues the MRO search and finds the correct mixin implementation.
The comment documents the invariant so it is not accidentally re-introduced.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Neue Zwischenschicht zwischen NetworkDriver und den typ-spezifischen
Basisklassen (FirewallDriver, SwitchDriver, …). Definiert das
Fingerprinting-Interface für den Discovery-Subsystem:
- FingerprintRule (NamedTuple): pattern, weight, mandatory, negative
- PortSpec (NamedTuple): scheme, port, paths, weight, mandatory
- DeviceTypeDriver: VENDOR, DRIVER_NAME, PORT_SPECS, SNMP_OBJECT_ID_PREFIX,
SNMP_FINGERPRINT, SSH_FINGERPRINT, HTTP_FINGERPRINT
Alle *Driver-Klassen erben jetzt von DeviceTypeDriver statt NetworkDriver.
Transitiv ist NetworkDriver weiterhin in der MRO (keine Breaking Change).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Each abstract base class now carries a TYPE_LABEL: str attribute that
describes the device category in human-readable form:
AccessPointDriver → "Access Point"
FirewallDriver → "Firewall"
HypervisorDriver → "Hypervisor"
OSDriver → "OS"
ResidentialGatewayDriver → "Gateway"
StorageDriver → "Storage"
SwitchDriver → "Switch"
Concrete drivers can override TYPE_LABEL to express a more specific
category (e.g. LinuxDriver sets "Linux"). The backend reads this
attribute to expose a type_label in the DriverInfo API response,
replacing the hardcoded DRIVER_TYPE map in the frontend.
23 tests covering presence, value, inheritance, and override.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds the abstract set_lag_members() method (with docstring) implemented
by the ProCurve, NetGear and TP-Link Jetstream drivers for managing
LAG/trunk port membership.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Defines the driver-level health metrics interface across all base classes.
OSDriver/FirewallDriver/HypervisorDriver/AccessPointDriver get a default
UCD-MIB + IF-MIB implementation via shared _ucd_metrics.py; SwitchDriver
raises NotImplementedError (vendor-proprietary OIDs). Adds HealthMetricsDict
and HealthMetricsIfaceDict TypedDicts to models.py.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Introduce OSDriver abstract base class (os.py) for general-purpose OS
drivers (Linux, BSD, macOS) — registers package management, service
management, users, processes, cron job and the two new OS-specific
extension points
- Add get_docker_info() contract: returns DockerInfoDict covering
containers, images, volumes, networks and outdated image detection
- Add run_device_action() contract: generic extensibility point for
driver-specific one-off administrative actions
- Fix duplicate TypedDicts in models.py: remove early shadow definitions
of UserDict, ProcessDict, CronJobDict, ApplyUpdatesResultDict from the
Common section; keep the more complete definitions in the OS section
- Add Docker TypedDicts: DockerContainerDict, DockerImageDict,
DockerVolumeDict, DockerNetworkDict, DockerInfoDict
- Add DeviceActionResultDict
- Export OSDriver from package __init__; bump version to 0.3.0
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add ServiceDict and UpdateDict TypedDicts to models
- Add get_services() / manage_service() abstract methods for init-system interaction
- Add get_available_updates() / apply_updates() for package upgrade workflows
- Add _filter_interfaces() helper to exclude lo and phy* interfaces from interface dicts
- Extend WirelessClientDict with optional ip, hostname, and lease_end fields
- Add optional description field to VPNTunnelDict
- Bump version 0.1.0 → 0.2.0