feat: Fingerprint-Attribute für Discovery-Scoring

Ergänzt DRIVER_NAME, HTTP_FINGERPRINT, SNMP_FINGERPRINT, SSH_FINGERPRINT,
PORT_SPECS und SNMP_OBJECT_ID_PREFIX gemäß docs/DISCOVERY_FINGERPRINTING.md.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Christian Manivong
2026-06-24 14:47:08 +02:00
co-authored by Claude Sonnet 4.6
parent aef5eb1487
commit 36a239692b
2 changed files with 86 additions and 133 deletions
+25 -86
View File
@@ -27,7 +27,6 @@ CLI notes
* Config is saved with ``write memory``.
"""
import difflib
import re
import socket
import time
@@ -37,7 +36,7 @@ import netaddr
from netmiko import ConnectHandler
from netmiko.exceptions import NetmikoAuthenticationException, NetmikoTimeoutException
from napalm_device_types import SwitchDriver
from napalm_device_types import ConfigLifecycleMixin, FingerprintRule, SwitchDriver
from napalm.base import helpers as napalm_helpers
from napalm.base.exceptions import (
CommandErrorException,
@@ -50,10 +49,23 @@ from napalm.base.netmiko_helpers import netmiko_args
import napalm.base.constants as C
class NetgearSmartDriver(SwitchDriver):
class NetgearSmartDriver(ConfigLifecycleMixin, SwitchDriver):
"""NAPALM driver for Netgear Smart Managed switches (GS-series)."""
VENDOR = "Netgear"
DRIVER_NAME = "netgear_smart"
SNMP_OBJECT_ID_PREFIX = "1.3.6.1.4.1.4526"
HTTP_FINGERPRINT = [
FingerprintRule("netgear xsm", weight=8.0),
FingerprintRule("netgear m4", weight=8.0),
FingerprintRule("netgear gs1", weight=6.0),
FingerprintRule("netgear gs2", weight=6.0),
FingerprintRule("netgear gs3", weight=6.0),
FingerprintRule("netgear", weight=3.0),
]
SNMP_FINGERPRINT = [
FingerprintRule("netgear", weight=4.0),
]
# Netmiko device type – handles prompt ``(hostname) >`` / ``(hostname) #``
NETMIKO_DEVICE_TYPE = "netgear_prosafe"
@@ -1224,10 +1236,16 @@ class NetgearSmartDriver(SwitchDriver):
vid_s, name, untagged, tagged = parts[0], parts[1], parts[2], parts[3]
if not vid_s.isdigit():
continue
if name.lower().startswith("auto-"):
# Built-in "Auto-Voice/Video/Camera/Wifi" pseudo-VLANs are
# always listed even when unused — not real user VLANs.
continue
untagged_ports = self._expand_port_list_v7(untagged)
tagged_ports = self._expand_port_list_v7(tagged)
vlans[vid_s] = {
"name": name,
"untagged": self._expand_port_list_v7(untagged),
"tagged": self._expand_port_list_v7(tagged),
"untagged": untagged_ports,
"tagged": tagged_ports,
}
return vlans
@@ -1235,78 +1253,8 @@ class NetgearSmartDriver(SwitchDriver):
# NAPALM configuration management
# ------------------------------------------------------------------
def load_merge_candidate(
self, filename: Optional[str] = None, config: Optional[str] = None
) -> None:
"""Stage CLI commands to be merged into the running configuration.
*config* is a plain-text string of CLI commands as typed in config
mode (one command per line). Sub-mode entry/exit lines are supported.
Blank lines and lines starting with ``!`` or ``#`` are ignored.
The configuration is **not** applied until :meth:`commit_config`.
:raises MergeConfigException: on invalid input.
"""
if filename is not None:
try:
with open(filename) as fh:
config = fh.read()
except OSError as exc:
raise MergeConfigException(str(exc)) from exc
if config is None:
raise MergeConfigException("Either 'filename' or 'config' must be provided.")
self._candidate_config = config
self._candidate_mode = "merge"
def load_replace_candidate(
self, filename: Optional[str] = None, config: Optional[str] = None
) -> None:
"""Stage a full running-config replacement candidate.
Netgear Smart Managed does not support atomic config replace;
commit applies the candidate lines additively (same as merge).
:meth:`compare_config` still shows a full unified diff.
:raises ReplaceConfigException: on invalid input.
"""
if filename is not None:
try:
with open(filename) as fh:
config = fh.read()
except OSError as exc:
raise ReplaceConfigException(str(exc)) from exc
if config is None:
raise ReplaceConfigException("Either 'filename' or 'config' must be provided.")
self._candidate_config = config
self._candidate_mode = "replace"
def compare_config(self) -> str:
"""Return a human-readable diff of the pending candidate vs running config.
Merge candidate → lines prefixed with ``+``.
Replace candidate → unified diff against current running config.
Returns an empty string when no candidate is staged.
"""
if self._candidate_config is None:
return ""
if self._candidate_mode == "merge":
lines = []
for line in self._candidate_config.splitlines():
if line.strip() and not line.strip().startswith(("!", "#")):
lines.append(f"+{line}")
return "\n".join(lines)
running = self._send_command("show running-config")
diff = difflib.unified_diff(
running.splitlines(),
self._candidate_config.splitlines(),
fromfile="running-config",
tofile="candidate-config",
lineterm="",
)
return "\n".join(diff)
def _get_running_config(self) -> str:
return self._send_command("show running-config")
def commit_config(self, message: str = "", revert_in: Optional[int] = None) -> None:
"""Apply the staged candidate configuration to the device and persist it.
@@ -1344,11 +1292,6 @@ class NetgearSmartDriver(SwitchDriver):
self._candidate_config = None
self._candidate_mode = None
def discard_config(self) -> None:
"""Discard the staged candidate configuration without applying it."""
self._candidate_config = None
self._candidate_mode = None
def rollback(self) -> None:
"""Revert the running config to the state before the last commit.
@@ -1462,10 +1405,6 @@ class NetgearSmartDriver(SwitchDriver):
# Additional NAPALM getters
# ------------------------------------------------------------------
def has_pending_commit(self) -> bool:
"""Return True when a candidate configuration is staged."""
return self._candidate_config is not None
def get_environment(self) -> Dict:
"""Return device environment data (CPU, memory).