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:
co-authored by
Claude Sonnet 4.6
parent
99e9431666
commit
9b65132949
+33
-27
@@ -34,7 +34,7 @@ management methods (``load_merge_candidate``, ``commit_config``, ...).
|
||||
from __future__ import annotations
|
||||
|
||||
import socket
|
||||
from typing import Any, Dict, List, Optional
|
||||
from typing import Any
|
||||
|
||||
from fritzconnection import FritzConnection
|
||||
from fritzconnection.core.exceptions import (
|
||||
@@ -45,7 +45,7 @@ from fritzconnection.core.exceptions import (
|
||||
)
|
||||
from napalm.base.exceptions import ConnectionClosedException, ConnectionException
|
||||
|
||||
from napalm_device_types import ResidentialGatewayDriver
|
||||
from napalm_device_types import FingerprintRule, ResidentialGatewayDriver
|
||||
from napalm_device_types.models import (
|
||||
HostDict,
|
||||
NATTranslationDict,
|
||||
@@ -66,6 +66,12 @@ class FritzBoxDriver(ResidentialGatewayDriver):
|
||||
"""NAPALM driver for AVM FritzBox (read-only, TR-064)."""
|
||||
|
||||
VENDOR = "AVM"
|
||||
DRIVER_NAME = "fritzbox"
|
||||
HTTP_FINGERPRINT = [
|
||||
FingerprintRule("fritz!box", weight=10.0, mandatory=True),
|
||||
FingerprintRule("fritzbox", weight=4.0),
|
||||
FingerprintRule("avm fritz", weight=4.0),
|
||||
]
|
||||
|
||||
_WAN_IP_SERVICES = ("WANIPConnection1", "WANPPPConnection1")
|
||||
_WLAN_SERVICES = ("WLANConfiguration1", "WLANConfiguration2", "WLANConfiguration3")
|
||||
@@ -76,7 +82,7 @@ class FritzBoxDriver(ResidentialGatewayDriver):
|
||||
username: str,
|
||||
password: str,
|
||||
timeout: int = 60,
|
||||
optional_args: Optional[Dict[str, Any]] = None,
|
||||
optional_args: dict[str, Any] | None = None,
|
||||
) -> None:
|
||||
self.hostname = hostname
|
||||
self.username = username
|
||||
@@ -91,7 +97,7 @@ class FritzBoxDriver(ResidentialGatewayDriver):
|
||||
self.use_tls = bool(self.optional_args.get("use_tls", True))
|
||||
self.port = int(self.optional_args.get("port") or (49443 if self.use_tls else 49000))
|
||||
|
||||
self.fc: Optional[FritzConnection] = None
|
||||
self.fc: FritzConnection | None = None
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Connection management
|
||||
@@ -121,7 +127,7 @@ class FritzBoxDriver(ResidentialGatewayDriver):
|
||||
"""Drop the TR-064 connection."""
|
||||
self.fc = None
|
||||
|
||||
def is_alive(self) -> Dict[str, bool]:
|
||||
def is_alive(self) -> dict[str, bool]:
|
||||
"""Return whether the connection is usable.
|
||||
|
||||
Performs a lightweight socket-level check without sending a full
|
||||
@@ -140,7 +146,7 @@ class FritzBoxDriver(ResidentialGatewayDriver):
|
||||
# Internal helpers
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
def _call(self, service: str, action: str, **kwargs: Any) -> Dict[str, Any]:
|
||||
def _call(self, service: str, action: str, **kwargs: Any) -> dict[str, Any]:
|
||||
"""Invoke a TR-064 SOAP action and return its output arguments.
|
||||
|
||||
:raises ConnectionClosedException: if called before :meth:`open`.
|
||||
@@ -149,7 +155,7 @@ class FritzBoxDriver(ResidentialGatewayDriver):
|
||||
raise ConnectionClosedException("Not connected – call open() first.")
|
||||
return self.fc.call_action(service, action, **kwargs)
|
||||
|
||||
def _wlan_services(self) -> List[str]:
|
||||
def _wlan_services(self) -> list[str]:
|
||||
"""Return the WLAN service names actually present on this device."""
|
||||
if self.fc is None:
|
||||
return []
|
||||
@@ -182,7 +188,7 @@ class FritzBoxDriver(ResidentialGatewayDriver):
|
||||
# NAPALM standard getters
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
def get_facts(self) -> Dict[str, Any]:
|
||||
def get_facts(self) -> dict[str, Any]:
|
||||
"""Return a dictionary of general device facts.
|
||||
|
||||
Calls ``DeviceInfo1.GetInfo``.
|
||||
@@ -205,14 +211,14 @@ class FritzBoxDriver(ResidentialGatewayDriver):
|
||||
"interface_list": interface_list,
|
||||
}
|
||||
|
||||
def get_interfaces(self) -> Dict[str, Dict[str, Any]]:
|
||||
def get_interfaces(self) -> dict[str, dict[str, Any]]:
|
||||
"""Return interface details keyed by interface name.
|
||||
|
||||
Combines ``LANEthernetInterfaceConfig1.GetInfo``,
|
||||
``WANCommonInterfaceConfig1.GetCommonLinkProperties`` and
|
||||
``WLANConfiguration{1,2,3}.GetInfo``.
|
||||
"""
|
||||
interfaces: Dict[str, Dict[str, Any]] = {}
|
||||
interfaces: dict[str, dict[str, Any]] = {}
|
||||
|
||||
try:
|
||||
lan = self._call("LANEthernetInterfaceConfig1", "GetInfo")
|
||||
@@ -259,7 +265,7 @@ class FritzBoxDriver(ResidentialGatewayDriver):
|
||||
|
||||
return interfaces
|
||||
|
||||
def get_interfaces_ip(self) -> Dict[str, Dict[str, Any]]:
|
||||
def get_interfaces_ip(self) -> dict[str, dict[str, Any]]:
|
||||
"""Return IP addresses keyed by interface name.
|
||||
|
||||
WAN address comes from ``GetExternalIPAddress`` (and, if available,
|
||||
@@ -267,7 +273,7 @@ class FritzBoxDriver(ResidentialGatewayDriver):
|
||||
service. The LAN address is the address used to reach the device
|
||||
(TR-064 does not expose the router's own LAN IP generically).
|
||||
"""
|
||||
result: Dict[str, Dict[str, Any]] = {}
|
||||
result: dict[str, dict[str, Any]] = {}
|
||||
|
||||
try:
|
||||
service = self._wan_ip_service()
|
||||
@@ -283,12 +289,12 @@ class FritzBoxDriver(ResidentialGatewayDriver):
|
||||
result["lan"] = {"ipv4": {self.hostname: {"prefix_length": 24}}}
|
||||
return result
|
||||
|
||||
def get_arp_table(self, vrf: str = "") -> List[Dict[str, Any]]:
|
||||
def get_arp_table(self, vrf: str = "") -> list[dict[str, Any]]:
|
||||
"""Return the ARP table, derived from :meth:`get_hosts`.
|
||||
|
||||
Each entry contains: ``interface``, ``mac``, ``ip``, ``age``.
|
||||
"""
|
||||
arp_table: List[Dict[str, Any]] = []
|
||||
arp_table: list[dict[str, Any]] = []
|
||||
for host in self.get_hosts():
|
||||
if not host["is_active"] or not host["mac"] or not host["ip"]:
|
||||
continue
|
||||
@@ -342,14 +348,14 @@ class FritzBoxDriver(ResidentialGatewayDriver):
|
||||
|
||||
return result
|
||||
|
||||
def get_port_forwards(self) -> List[PortForwardDict]:
|
||||
def get_port_forwards(self) -> list[PortForwardDict]:
|
||||
"""Return configured port forwarding rules.
|
||||
|
||||
Iterates ``GetGenericPortMappingEntry`` on the active WAN connection
|
||||
service until the device reports an out-of-range index.
|
||||
"""
|
||||
service = self._wan_ip_service()
|
||||
forwards: List[PortForwardDict] = []
|
||||
forwards: list[PortForwardDict] = []
|
||||
index = 0
|
||||
while True:
|
||||
try:
|
||||
@@ -373,7 +379,7 @@ class FritzBoxDriver(ResidentialGatewayDriver):
|
||||
|
||||
return forwards
|
||||
|
||||
def get_hosts(self) -> List[HostDict]:
|
||||
def get_hosts(self) -> list[HostDict]:
|
||||
"""Return hosts known to the FritzBox.
|
||||
|
||||
Iterates ``Hosts1.GetGenericHostEntry`` for
|
||||
@@ -386,7 +392,7 @@ class FritzBoxDriver(ResidentialGatewayDriver):
|
||||
except _OPTIONAL_SERVICE_ERRORS:
|
||||
return []
|
||||
|
||||
hosts: List[HostDict] = []
|
||||
hosts: list[HostDict] = []
|
||||
for index in range(count):
|
||||
try:
|
||||
entry = self._call("Hosts1", "GetGenericHostEntry", NewIndex=index)
|
||||
@@ -407,7 +413,7 @@ class FritzBoxDriver(ResidentialGatewayDriver):
|
||||
|
||||
return hosts
|
||||
|
||||
def get_nat_translations(self) -> List[NATTranslationDict]:
|
||||
def get_nat_translations(self) -> list[NATTranslationDict]:
|
||||
"""Return NAT translation entries derived from active port forwards.
|
||||
|
||||
TR-064 does not expose a live connection-tracking table; this
|
||||
@@ -415,7 +421,7 @@ class FritzBoxDriver(ResidentialGatewayDriver):
|
||||
rules, which is the closest equivalent FritzBox provides.
|
||||
"""
|
||||
ext_ip = self._wan_external_ip()
|
||||
translations: List[NATTranslationDict] = []
|
||||
translations: list[NATTranslationDict] = []
|
||||
for forward in self.get_port_forwards():
|
||||
if not forward["enabled"]:
|
||||
continue
|
||||
@@ -432,7 +438,7 @@ class FritzBoxDriver(ResidentialGatewayDriver):
|
||||
)
|
||||
return translations
|
||||
|
||||
def get_vpn_tunnels(self) -> Dict[str, VPNTunnelDict]:
|
||||
def get_vpn_tunnels(self) -> dict[str, VPNTunnelDict]:
|
||||
"""Return VPN tunnel status.
|
||||
|
||||
FritzOS does not expose VPN (IPsec/WireGuard) tunnel state through
|
||||
@@ -440,13 +446,13 @@ class FritzBoxDriver(ResidentialGatewayDriver):
|
||||
"""
|
||||
return {}
|
||||
|
||||
def get_wireless_clients(self) -> List[WirelessClientDict]:
|
||||
def get_wireless_clients(self) -> list[WirelessClientDict]:
|
||||
"""Return wireless clients associated with the built-in access point(s).
|
||||
|
||||
Iterates ``GetGenericAssociatedDeviceInfo`` for each
|
||||
``WLANConfiguration{1,2,3}`` service present on the device.
|
||||
"""
|
||||
clients: List[WirelessClientDict] = []
|
||||
clients: list[WirelessClientDict] = []
|
||||
for service in self._wlan_services():
|
||||
try:
|
||||
ssid = self._call(service, "GetInfo").get("NewSSID", "")
|
||||
@@ -481,13 +487,13 @@ class FritzBoxDriver(ResidentialGatewayDriver):
|
||||
|
||||
return clients
|
||||
|
||||
def get_ssids(self) -> Dict[str, SSIDDict]:
|
||||
def get_ssids(self) -> dict[str, SSIDDict]:
|
||||
"""Return configured wireless networks (SSIDs).
|
||||
|
||||
Calls ``GetInfo`` and ``GetTotalAssociations`` on each
|
||||
``WLANConfiguration{1,2,3}`` service present on the device.
|
||||
"""
|
||||
ssids: Dict[str, SSIDDict] = {}
|
||||
ssids: dict[str, SSIDDict] = {}
|
||||
for service in self._wlan_services():
|
||||
try:
|
||||
info = self._call(service, "GetInfo")
|
||||
@@ -516,14 +522,14 @@ class FritzBoxDriver(ResidentialGatewayDriver):
|
||||
|
||||
return ssids
|
||||
|
||||
def get_radio_status(self) -> Dict[str, RadioStatusDict]:
|
||||
def get_radio_status(self) -> dict[str, RadioStatusDict]:
|
||||
"""Return wireless radio status.
|
||||
|
||||
Calls ``GetInfo`` on each ``WLANConfiguration{1,2,3}`` service
|
||||
present on the device. ``channel_width``, ``tx_power`` and
|
||||
``frequency`` are not exposed via TR-064 and are reported as ``0``.
|
||||
"""
|
||||
radios: Dict[str, RadioStatusDict] = {}
|
||||
radios: dict[str, RadioStatusDict] = {}
|
||||
for service in self._wlan_services():
|
||||
try:
|
||||
info = self._call(service, "GetInfo")
|
||||
|
||||
Reference in New Issue
Block a user