feat: DeviceTypeDriver base class mit FingerprintRule und PortSpec
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>
This commit is contained in:
@@ -30,6 +30,7 @@ Also provided:
|
||||
drivers.
|
||||
"""
|
||||
|
||||
from napalm_device_types.base import DeviceTypeDriver, FingerprintRule, PortSpec
|
||||
from napalm_device_types.access_point import AccessPointDriver
|
||||
from napalm_device_types.config_lifecycle import ConfigLifecycleMixin
|
||||
from napalm_device_types.firewall import FirewallDriver
|
||||
@@ -42,9 +43,12 @@ from napalm_device_types.switch import SwitchDriver
|
||||
__all__ = [
|
||||
"AccessPointDriver",
|
||||
"ConfigLifecycleMixin",
|
||||
"DeviceTypeDriver",
|
||||
"FingerprintRule",
|
||||
"FirewallDriver",
|
||||
"HypervisorDriver",
|
||||
"OSDriver",
|
||||
"PortSpec",
|
||||
"ResidentialGatewayDriver",
|
||||
"StorageDriver",
|
||||
"SwitchDriver",
|
||||
|
||||
@@ -11,7 +11,7 @@ Usage::
|
||||
"""
|
||||
|
||||
from typing import Any, Dict, List
|
||||
from napalm.base import NetworkDriver
|
||||
from napalm_device_types.base import DeviceTypeDriver
|
||||
from napalm_device_types._ucd_metrics import IF_SKIP_DEFAULT, collect_ucd_metrics
|
||||
from napalm_device_types.models import (
|
||||
Dot1XConfigDict,
|
||||
@@ -31,7 +31,7 @@ from napalm_device_types.models import (
|
||||
)
|
||||
|
||||
|
||||
class AccessPointDriver(NetworkDriver):
|
||||
class AccessPointDriver(DeviceTypeDriver):
|
||||
TYPE_LABEL: str = "Access Point"
|
||||
"""
|
||||
Abstract intermediate driver for wireless access points.
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
"""
|
||||
DeviceTypeDriver — common base for all netOrk device-type drivers.
|
||||
|
||||
Provides the fingerprinting contract used by the discovery subsystem to
|
||||
identify devices from SNMP / SSH / HTTP probe data without requiring a
|
||||
live connection.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import NamedTuple
|
||||
|
||||
from napalm.base import NetworkDriver
|
||||
|
||||
|
||||
class FingerprintRule(NamedTuple):
|
||||
"""Single pattern-matching rule for device fingerprinting.
|
||||
|
||||
pattern — substring matched against lowercased probe data
|
||||
weight — evidence added to the driver's score on match
|
||||
mandatory — pattern absent → driver immediately disqualified
|
||||
negative — pattern present → weight subtracted instead of added
|
||||
"""
|
||||
|
||||
pattern: str
|
||||
weight: float = 1.0
|
||||
mandatory: bool = False
|
||||
negative: bool = False
|
||||
|
||||
|
||||
class PortSpec(NamedTuple):
|
||||
"""Port to probe during discovery and its fingerprinting contribution.
|
||||
|
||||
scheme — "http", "https", or "ssh"
|
||||
port — TCP port number
|
||||
paths — URL paths to try (http/https only; ignored for ssh)
|
||||
weight — evidence added to score when this port responds
|
||||
mandatory — port silent → driver immediately disqualified
|
||||
"""
|
||||
|
||||
scheme: str
|
||||
port: int
|
||||
paths: tuple[str, ...] = ("/",)
|
||||
weight: float = 5.0
|
||||
mandatory: bool = False
|
||||
|
||||
|
||||
class DeviceTypeDriver(NetworkDriver):
|
||||
"""Common base for all netOrk device-type drivers.
|
||||
|
||||
Sits between napalm.base.NetworkDriver and the type-specific abstract
|
||||
classes (FirewallDriver, SwitchDriver, …). Adds the fingerprinting
|
||||
interface consumed by the discovery subsystem; does not implement any
|
||||
NAPALM abstract methods.
|
||||
|
||||
Override these class attributes in each concrete driver:
|
||||
|
||||
VENDOR Human-readable vendor name ("AVM", "OPNsense", …).
|
||||
DRIVER_NAME NAPALM entry-point key ("fritzbox", "opnsense", …).
|
||||
PORT_SPECS Non-standard ports to probe beyond 80/443/22.
|
||||
None = no driver-specific ports expected.
|
||||
SNMP_OBJECT_ID_PREFIX OID prefix for sysObjectID (1.3.6.1.2.1.1.2.0).
|
||||
Match → +15.0 score; set but no match → disqualified.
|
||||
SNMP_FINGERPRINT Rules matched against sysDescr (lowercased).
|
||||
SSH_FINGERPRINT Rules matched against SSH version string +
|
||||
pre-auth banner (lowercased, space-joined).
|
||||
HTTP_FINGERPRINT Rules matched against page title + Server header +
|
||||
body snippet (lowercased, whitespace-normalised).
|
||||
"""
|
||||
|
||||
VENDOR: str = ""
|
||||
DRIVER_NAME: str = ""
|
||||
PORT_SPECS: list[PortSpec] | None = None
|
||||
SNMP_OBJECT_ID_PREFIX: str | None = None
|
||||
SNMP_FINGERPRINT: list[FingerprintRule] = []
|
||||
SSH_FINGERPRINT: list[FingerprintRule] = []
|
||||
HTTP_FINGERPRINT: list[FingerprintRule] = []
|
||||
@@ -11,7 +11,7 @@ Usage::
|
||||
"""
|
||||
|
||||
from typing import Any, Dict, List
|
||||
from napalm.base import NetworkDriver
|
||||
from napalm_device_types.base import DeviceTypeDriver
|
||||
from napalm_device_types._ucd_metrics import IF_SKIP_DEFAULT, collect_ucd_metrics
|
||||
from napalm_device_types.models import (
|
||||
HealthMetricsDict,
|
||||
@@ -23,7 +23,7 @@ from napalm_device_types.models import (
|
||||
)
|
||||
|
||||
|
||||
class FirewallDriver(NetworkDriver):
|
||||
class FirewallDriver(DeviceTypeDriver):
|
||||
TYPE_LABEL: str = "Firewall"
|
||||
"""
|
||||
Abstract intermediate driver for firewall/security devices.
|
||||
|
||||
@@ -11,7 +11,7 @@ Usage::
|
||||
"""
|
||||
|
||||
from typing import Any, Dict, List
|
||||
from napalm.base import NetworkDriver
|
||||
from napalm_device_types.base import DeviceTypeDriver
|
||||
from napalm_device_types._ucd_metrics import IF_SKIP_DEFAULT, collect_ucd_metrics
|
||||
from napalm_device_types.models import (
|
||||
HealthMetricsDict,
|
||||
@@ -24,7 +24,7 @@ from napalm_device_types.models import (
|
||||
)
|
||||
|
||||
|
||||
class HypervisorDriver(NetworkDriver):
|
||||
class HypervisorDriver(DeviceTypeDriver):
|
||||
TYPE_LABEL: str = "Hypervisor"
|
||||
"""
|
||||
Abstract intermediate driver for hypervisors and virtualisation platforms
|
||||
|
||||
@@ -12,7 +12,7 @@ Usage::
|
||||
|
||||
import re
|
||||
from typing import List, Optional
|
||||
from napalm.base import NetworkDriver
|
||||
from napalm_device_types.base import DeviceTypeDriver
|
||||
from napalm_device_types._ucd_metrics import IF_SKIP_DEFAULT, collect_ucd_metrics
|
||||
from napalm_device_types.models import (
|
||||
ApplyUpdatesResultDict,
|
||||
@@ -29,7 +29,7 @@ from napalm_device_types.models import (
|
||||
)
|
||||
|
||||
|
||||
class OSDriver(NetworkDriver):
|
||||
class OSDriver(DeviceTypeDriver):
|
||||
TYPE_LABEL: str = "OS"
|
||||
"""
|
||||
Abstract intermediate driver for general-purpose operating systems
|
||||
|
||||
@@ -19,7 +19,7 @@ Usage::
|
||||
"""
|
||||
|
||||
from typing import Dict, List
|
||||
from napalm.base import NetworkDriver
|
||||
from napalm_device_types.base import DeviceTypeDriver
|
||||
from napalm_device_types._ucd_metrics import IF_SKIP_DEFAULT, collect_ucd_metrics
|
||||
from napalm_device_types.models import (
|
||||
HealthMetricsDict,
|
||||
@@ -34,7 +34,7 @@ from napalm_device_types.models import (
|
||||
)
|
||||
|
||||
|
||||
class ResidentialGatewayDriver(NetworkDriver):
|
||||
class ResidentialGatewayDriver(DeviceTypeDriver):
|
||||
TYPE_LABEL: str = "Gateway"
|
||||
"""
|
||||
Abstract intermediate driver for residential gateways (router + firewall + AP).
|
||||
|
||||
@@ -11,7 +11,7 @@ Usage::
|
||||
"""
|
||||
|
||||
from typing import Any, Dict, List
|
||||
from napalm.base import NetworkDriver
|
||||
from napalm_device_types.base import DeviceTypeDriver
|
||||
from napalm_device_types.models import (
|
||||
DiskPoolDict,
|
||||
LogicalVolumeDict,
|
||||
@@ -25,7 +25,7 @@ from napalm_device_types.models import (
|
||||
)
|
||||
|
||||
|
||||
class StorageDriver(NetworkDriver):
|
||||
class StorageDriver(DeviceTypeDriver):
|
||||
TYPE_LABEL: str = "Storage"
|
||||
"""
|
||||
Abstract intermediate driver for storage appliances and NAS/SAN devices
|
||||
|
||||
@@ -11,7 +11,7 @@ Usage::
|
||||
"""
|
||||
|
||||
from typing import Dict, List
|
||||
from napalm.base import NetworkDriver
|
||||
from napalm_device_types.base import DeviceTypeDriver
|
||||
from napalm_device_types.models import (
|
||||
Dot1XPortDict,
|
||||
HealthMetricsDict,
|
||||
@@ -24,7 +24,7 @@ from napalm_device_types.models import (
|
||||
)
|
||||
|
||||
|
||||
class SwitchDriver(NetworkDriver):
|
||||
class SwitchDriver(DeviceTypeDriver):
|
||||
TYPE_LABEL: str = "Switch"
|
||||
"""
|
||||
Abstract intermediate driver for Ethernet switches.
|
||||
|
||||
@@ -0,0 +1,145 @@
|
||||
"""Tests for DeviceTypeDriver, FingerprintRule, and PortSpec."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import pytest
|
||||
from napalm_device_types import (
|
||||
AccessPointDriver,
|
||||
DeviceTypeDriver,
|
||||
FingerprintRule,
|
||||
FirewallDriver,
|
||||
HypervisorDriver,
|
||||
OSDriver,
|
||||
PortSpec,
|
||||
ResidentialGatewayDriver,
|
||||
StorageDriver,
|
||||
SwitchDriver,
|
||||
)
|
||||
from napalm.base import NetworkDriver
|
||||
|
||||
|
||||
# ── Hierarchy ─────────────────────────────────────────────────────────────────
|
||||
|
||||
TYPE_DRIVERS = [
|
||||
AccessPointDriver,
|
||||
FirewallDriver,
|
||||
HypervisorDriver,
|
||||
OSDriver,
|
||||
ResidentialGatewayDriver,
|
||||
StorageDriver,
|
||||
SwitchDriver,
|
||||
]
|
||||
|
||||
|
||||
@pytest.mark.parametrize("cls", TYPE_DRIVERS)
|
||||
def test_type_driver_inherits_device_type_driver(cls):
|
||||
assert issubclass(cls, DeviceTypeDriver)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("cls", TYPE_DRIVERS)
|
||||
def test_type_driver_inherits_network_driver(cls):
|
||||
"""Transitiv: DeviceTypeDriver → NetworkDriver muss erhalten bleiben."""
|
||||
assert issubclass(cls, NetworkDriver)
|
||||
|
||||
|
||||
def test_device_type_driver_inherits_network_driver():
|
||||
assert issubclass(DeviceTypeDriver, NetworkDriver)
|
||||
|
||||
|
||||
# ── Default attributes ────────────────────────────────────────────────────────
|
||||
|
||||
def test_defaults_on_device_type_driver():
|
||||
assert DeviceTypeDriver.VENDOR == ""
|
||||
assert DeviceTypeDriver.DRIVER_NAME == ""
|
||||
assert DeviceTypeDriver.PORT_SPECS is None
|
||||
assert DeviceTypeDriver.SNMP_OBJECT_ID_PREFIX is None
|
||||
assert DeviceTypeDriver.SNMP_FINGERPRINT == []
|
||||
assert DeviceTypeDriver.SSH_FINGERPRINT == []
|
||||
assert DeviceTypeDriver.HTTP_FINGERPRINT == []
|
||||
|
||||
|
||||
@pytest.mark.parametrize("cls", TYPE_DRIVERS)
|
||||
def test_fingerprint_attributes_inherited(cls):
|
||||
assert hasattr(cls, "HTTP_FINGERPRINT")
|
||||
assert hasattr(cls, "SNMP_FINGERPRINT")
|
||||
assert hasattr(cls, "SSH_FINGERPRINT")
|
||||
assert hasattr(cls, "PORT_SPECS")
|
||||
assert hasattr(cls, "SNMP_OBJECT_ID_PREFIX")
|
||||
|
||||
|
||||
# ── FingerprintRule ───────────────────────────────────────────────────────────
|
||||
|
||||
def test_fingerprint_rule_defaults():
|
||||
r = FingerprintRule("fritz!box")
|
||||
assert r.pattern == "fritz!box"
|
||||
assert r.weight == 1.0
|
||||
assert r.mandatory is False
|
||||
assert r.negative is False
|
||||
|
||||
|
||||
def test_fingerprint_rule_mandatory():
|
||||
r = FingerprintRule("opnsense", weight=8.0, mandatory=True)
|
||||
assert r.mandatory is True
|
||||
assert r.weight == 8.0
|
||||
|
||||
|
||||
def test_fingerprint_rule_negative():
|
||||
r = FingerprintRule("pfsense", weight=5.0, negative=True)
|
||||
assert r.negative is True
|
||||
|
||||
|
||||
def test_fingerprint_rule_is_immutable():
|
||||
r = FingerprintRule("test", weight=3.0)
|
||||
with pytest.raises(AttributeError):
|
||||
r.weight = 99.0 # type: ignore[misc]
|
||||
|
||||
|
||||
# ── PortSpec ──────────────────────────────────────────────────────────────────
|
||||
|
||||
def test_port_spec_defaults():
|
||||
p = PortSpec("https", 8006)
|
||||
assert p.scheme == "https"
|
||||
assert p.port == 8006
|
||||
assert p.paths == ("/",)
|
||||
assert p.weight == 5.0
|
||||
assert p.mandatory is False
|
||||
|
||||
|
||||
def test_port_spec_mandatory():
|
||||
p = PortSpec("http", 1400, ("/xml/device_description.xml",), weight=9.0, mandatory=True)
|
||||
assert p.mandatory is True
|
||||
assert p.paths == ("/xml/device_description.xml",)
|
||||
|
||||
|
||||
def test_port_spec_is_immutable():
|
||||
p = PortSpec("http", 80)
|
||||
with pytest.raises(AttributeError):
|
||||
p.port = 8080 # type: ignore[misc]
|
||||
|
||||
|
||||
# ── Concrete driver subclass ──────────────────────────────────────────────────
|
||||
|
||||
def test_concrete_driver_overrides_fingerprint():
|
||||
class MyDriver(FirewallDriver):
|
||||
VENDOR = "Acme"
|
||||
DRIVER_NAME = "acme"
|
||||
HTTP_FINGERPRINT = [FingerprintRule("acme portal", weight=9.0, mandatory=True)]
|
||||
PORT_SPECS = [PortSpec("https", 9443, weight=7.0)]
|
||||
|
||||
assert MyDriver.VENDOR == "Acme"
|
||||
assert MyDriver.HTTP_FINGERPRINT[0].pattern == "acme portal"
|
||||
assert MyDriver.PORT_SPECS[0].port == 9443
|
||||
# base class unaffected
|
||||
assert FirewallDriver.HTTP_FINGERPRINT == []
|
||||
assert FirewallDriver.PORT_SPECS is None
|
||||
|
||||
|
||||
def test_type_label_unaffected_by_refactor():
|
||||
"""TYPE_LABEL muss nach dem Refactoring noch korrekt sein."""
|
||||
assert FirewallDriver.TYPE_LABEL == "Firewall"
|
||||
assert SwitchDriver.TYPE_LABEL == "Switch"
|
||||
assert ResidentialGatewayDriver.TYPE_LABEL == "Gateway"
|
||||
assert HypervisorDriver.TYPE_LABEL == "Hypervisor"
|
||||
assert OSDriver.TYPE_LABEL == "OS"
|
||||
assert StorageDriver.TYPE_LABEL == "Storage"
|
||||
assert AccessPointDriver.TYPE_LABEL == "Access Point"
|
||||
Reference in New Issue
Block a user