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:
2026-06-24 14:29:41 +02:00
co-authored by Claude Sonnet 4.6
parent 95f8771824
commit 0f6734e6da
10 changed files with 240 additions and 14 deletions
+145
View File
@@ -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"