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 6d8f01fe27
commit f6bd38a9fe
+48 -39
View File
@@ -2,11 +2,11 @@ from __future__ import annotations
import re import re
import socket import socket
from typing import Any, Dict, List, Optional from typing import Any
from napalm.base import NetworkDriver
from napalm.base.exceptions import ConnectionException from napalm.base.exceptions import ConnectionException
from napalm_device_types import AccessPointDriver, FingerprintRule, PortSpec
from napalm_device_types.models import DeviceActionResultDict from napalm_device_types.models import DeviceActionResultDict
"""NAPALM driver for Sonos speakers. """NAPALM driver for Sonos speakers.
@@ -19,7 +19,7 @@ media-playback controls.
_VOLUME_RE = re.compile(r"^\d+$") _VOLUME_RE = re.compile(r"^\d+$")
class SonosDriver(NetworkDriver): class SonosDriver(AccessPointDriver):
"""NAPALM driver for controlling Sonos speakers. """NAPALM driver for controlling Sonos speakers.
Implements the standard NAPALM interface plus media-playback methods Implements the standard NAPALM interface plus media-playback methods
@@ -27,6 +27,15 @@ class SonosDriver(NetworkDriver):
""" """
VENDOR = "Sonos" VENDOR = "Sonos"
DRIVER_NAME = "sonos"
PORT_SPECS = [
PortSpec("http", 1400, ("/xml/device_description.xml",), weight=9.0, mandatory=True),
PortSpec("https", 1443, weight=4.0),
]
HTTP_FINGERPRINT = [
FingerprintRule("sonos", weight=8.0, mandatory=True),
FingerprintRule("rincon", weight=5.0),
]
def __init__( def __init__(
self, self,
@@ -34,7 +43,7 @@ class SonosDriver(NetworkDriver):
username: str = "", username: str = "",
password: str = "", password: str = "",
timeout: int = 60, timeout: int = 60,
optional_args: Optional[Dict[str, Any]] = None, optional_args: dict[str, Any] | None = None,
) -> None: ) -> None:
"""Initialise the driver with connection details.""" """Initialise the driver with connection details."""
self.hostname = hostname self.hostname = hostname
@@ -44,7 +53,7 @@ class SonosDriver(NetworkDriver):
self.optional_args = optional_args or {} self.optional_args = optional_args or {}
self._device: Any = None self._device: Any = None
self._speaker_info: Optional[Dict[str, Any]] = None self._speaker_info: dict[str, Any] | None = None
def open(self) -> None: def open(self) -> None:
"""Open a connection to the Sonos speaker.""" """Open a connection to the Sonos speaker."""
@@ -62,7 +71,7 @@ class SonosDriver(NetworkDriver):
self._device = None self._device = None
self._speaker_info = None self._speaker_info = None
def is_alive(self) -> Dict[str, bool]: def is_alive(self) -> dict[str, bool]:
"""Check whether the speaker is reachable on the network.""" """Check whether the speaker is reachable on the network."""
if self._device is None: if self._device is None:
return {"is_alive": False} return {"is_alive": False}
@@ -75,7 +84,7 @@ class SonosDriver(NetworkDriver):
except OSError: except OSError:
return {"is_alive": False} return {"is_alive": False}
def get_facts(self) -> Dict[str, Any]: def get_facts(self) -> dict[str, Any]:
"""Return device facts gathered during connection.""" """Return device facts gathered during connection."""
if not self._speaker_info: if not self._speaker_info:
return {} return {}
@@ -91,8 +100,8 @@ class SonosDriver(NetworkDriver):
"interface_list": self._detect_interfaces(info), "interface_list": self._detect_interfaces(info),
} }
def _detect_interfaces(self, info: Dict[str, Any]) -> List[str]: def _detect_interfaces(self, info: dict[str, Any]) -> list[str]:
ifaces: List[str] = [] ifaces: list[str] = []
has_eth = self._check_wired_connection() has_eth = self._check_wired_connection()
if has_eth: if has_eth:
ifaces.append("eth0") ifaces.append("eth0")
@@ -112,7 +121,7 @@ class SonosDriver(NetworkDriver):
pass pass
return False return False
def get_interfaces(self) -> Dict[str, Dict[str, Any]]: def get_interfaces(self) -> dict[str, dict[str, Any]]:
"""Return interface information for this speaker.""" """Return interface information for this speaker."""
if not self._speaker_info: if not self._speaker_info:
return {} return {}
@@ -121,7 +130,7 @@ class SonosDriver(NetworkDriver):
is_up = self.is_alive().get("is_alive", False) is_up = self.is_alive().get("is_alive", False)
has_eth = self._check_wired_connection() has_eth = self._check_wired_connection()
interfaces: Dict[str, Dict[str, Any]] = {} interfaces: dict[str, dict[str, Any]] = {}
if has_eth: if has_eth:
interfaces["eth0"] = { interfaces["eth0"] = {
"is_up": is_up, "is_up": is_up,
@@ -143,7 +152,7 @@ class SonosDriver(NetworkDriver):
} }
return interfaces return interfaces
def get_interfaces_ip(self) -> Dict[str, Dict[str, Any]]: def get_interfaces_ip(self) -> dict[str, dict[str, Any]]:
"""Return IP address information for the speaker interfaces.""" """Return IP address information for the speaker interfaces."""
if not self._speaker_info: if not self._speaker_info:
return {} return {}
@@ -156,45 +165,45 @@ class SonosDriver(NetworkDriver):
"prefix_length": 24, "prefix_length": 24,
} }
} }
interfaces: Dict[str, Any] = {} interfaces: dict[str, Any] = {}
if has_eth: if has_eth:
interfaces["eth0"] = {"ipv4": ipv4_entry, "ipv6": {}} interfaces["eth0"] = {"ipv4": ipv4_entry, "ipv6": {}}
interfaces["wlan0"] = {"ipv4": ipv4_entry, "ipv6": {}} interfaces["wlan0"] = {"ipv4": ipv4_entry, "ipv6": {}}
return interfaces return interfaces
def get_arp_table(self) -> List[Dict[str, Any]]: def get_arp_table(self) -> list[dict[str, Any]]:
"""Return the ARP table (not supported on Sonos).""" """Return the ARP table (not supported on Sonos)."""
return [] return []
def get_lldp_neighbors(self) -> Dict[str, Any]: def get_lldp_neighbors(self) -> dict[str, Any]:
"""Return LLDP neighbors (not supported on Sonos).""" """Return LLDP neighbors (not supported on Sonos)."""
return {} return {}
def get_lldp_neighbors_detail(self, interface: str = "") -> Dict[str, Any]: def get_lldp_neighbors_detail(self, interface: str = "") -> dict[str, Any]:
"""Return detailed LLDP neighbors (not supported on Sonos).""" """Return detailed LLDP neighbors (not supported on Sonos)."""
return {} return {}
def get_config(self) -> Dict[str, Any]: def get_config(self) -> dict[str, Any]:
"""Return the device config (not supported on Sonos).""" """Return the device config (not supported on Sonos)."""
return {} return {}
def get_environment(self) -> Dict[str, Any]: def get_environment(self) -> dict[str, Any]:
"""Return environment/power info (not supported on Sonos).""" """Return environment/power info (not supported on Sonos)."""
return {} return {}
def get_optics(self) -> Dict[str, Any]: def get_optics(self) -> dict[str, Any]:
"""Return optics info (not supported on Sonos).""" """Return optics info (not supported on Sonos)."""
return {} return {}
def get_ntp_peers(self) -> Dict[str, Optional[str]]: def get_ntp_peers(self) -> dict[str, str | None]:
"""Return NTP peers (not supported on Sonos).""" """Return NTP peers (not supported on Sonos)."""
return {} return {}
def get_ntp_servers(self) -> Dict[str, Optional[str]]: def get_ntp_servers(self) -> dict[str, str | None]:
"""Return NTP servers (not supported on Sonos).""" """Return NTP servers (not supported on Sonos)."""
return {} return {}
def get_ntp_stats(self) -> List[Dict[str, Any]]: def get_ntp_stats(self) -> list[dict[str, Any]]:
"""Return NTP stats (not supported on Sonos).""" """Return NTP stats (not supported on Sonos)."""
return [] return []
@@ -207,7 +216,7 @@ class SonosDriver(NetworkDriver):
size: int = 64, size: int = 64,
count: int = 5, count: int = 5,
vrf: str = "", vrf: str = "",
) -> Dict[str, Any]: ) -> dict[str, Any]:
"""Ping a destination (not supported on Sonos).""" """Ping a destination (not supported on Sonos)."""
return { return {
"success": {}, "success": {},
@@ -221,58 +230,58 @@ class SonosDriver(NetworkDriver):
ttl: int = 255, ttl: int = 255,
timeout: int = 2, timeout: int = 2,
vrf: str = "", vrf: str = "",
) -> Dict[str, Any]: ) -> dict[str, Any]:
"""Run traceroute (not supported on Sonos).""" """Run traceroute (not supported on Sonos)."""
return { return {
"success": {}, "success": {},
"error": "Not supported on Sonos", "error": "Not supported on Sonos",
} }
def get_route_to(self, destination: str) -> List[Dict[str, Any]]: def get_route_to(self, destination: str) -> list[dict[str, Any]]:
"""Return routing table (not supported on Sonos).""" """Return routing table (not supported on Sonos)."""
return [] return []
def get_snmp_information(self) -> Dict[str, Any]: def get_snmp_information(self) -> dict[str, Any]:
"""Return SNMP info (not supported on Sonos).""" """Return SNMP info (not supported on Sonos)."""
return {} return {}
def get_users(self) -> Dict[str, Any]: def get_users(self) -> dict[str, Any]:
"""Return user accounts (not supported on Sonos).""" """Return user accounts (not supported on Sonos)."""
return {} return {}
def get_network_instances(self, name: str = "") -> Dict[str, Any]: def get_network_instances(self, name: str = "") -> dict[str, Any]:
"""Return network instances / VRFs (not supported on Sonos).""" """Return network instances / VRFs (not supported on Sonos)."""
return {} return {}
def get_bgp_config(self, group: str = "") -> Dict[str, Any]: def get_bgp_config(self, group: str = "") -> dict[str, Any]:
"""Return BGP config (not supported on Sonos).""" """Return BGP config (not supported on Sonos)."""
return {} return {}
def get_bgp_neighbors(self) -> Dict[str, Any]: def get_bgp_neighbors(self) -> dict[str, Any]:
"""Return BGP neighbors (not supported on Sonos).""" """Return BGP neighbors (not supported on Sonos)."""
return {} return {}
def get_bgp_neighbors_detail(self, neighbor_address: str = "") -> Dict[str, Any]: def get_bgp_neighbors_detail(self, neighbor_address: str = "") -> dict[str, Any]:
"""Return detailed BGP neighbor info (not supported on Sonos).""" """Return detailed BGP neighbor info (not supported on Sonos)."""
return {} return {}
def get_probes_config(self) -> Dict[str, Any]: def get_probes_config(self) -> dict[str, Any]:
"""Return probe config (not supported on Sonos).""" """Return probe config (not supported on Sonos)."""
return {} return {}
def get_probes_results(self) -> Dict[str, Any]: def get_probes_results(self) -> dict[str, Any]:
"""Return probe results (not supported on Sonos).""" """Return probe results (not supported on Sonos)."""
return {} return {}
def get_mac_address_table(self) -> List[Dict[str, Any]]: def get_mac_address_table(self) -> list[dict[str, Any]]:
"""Return MAC address table (not supported on Sonos).""" """Return MAC address table (not supported on Sonos)."""
return [] return []
def get_firewall_policies(self) -> Dict[str, Any]: def get_firewall_policies(self) -> dict[str, Any]:
"""Return firewall policies (not supported on Sonos).""" """Return firewall policies (not supported on Sonos)."""
return {} return {}
def cli(self, commands: List[str]) -> List[Dict[str, Any]]: def cli(self, commands: list[str]) -> list[dict[str, Any]]:
"""Run CLI commands (not supported on Sonos).""" """Run CLI commands (not supported on Sonos)."""
return [] return []
@@ -310,7 +319,7 @@ class SonosDriver(NetworkDriver):
# Broad catch: SoCo transport errors are unpredictable # Broad catch: SoCo transport errors are unpredictable
return "STOPPED" return "STOPPED"
def get_current_track(self) -> Dict[str, Any]: def get_current_track(self) -> dict[str, Any]:
"""Return information about the currently playing track.""" """Return information about the currently playing track."""
if self._device is None: if self._device is None:
return {} return {}
@@ -383,7 +392,7 @@ class SonosDriver(NetworkDriver):
return return
self._device.previous() self._device.previous()
def get_group_info(self) -> Dict[str, Any]: def get_group_info(self) -> dict[str, Any]:
"""Return information about the Sonos group this speaker belongs to.""" """Return information about the Sonos group this speaker belongs to."""
if self._device is None: if self._device is None:
return {} return {}
@@ -407,7 +416,7 @@ class SonosDriver(NetworkDriver):
# Broad catch: SoCo group/network operations can fail in many ways # Broad catch: SoCo group/network operations can fail in many ways
return {} return {}
def get_zone_info(self) -> Dict[str, Any]: def get_zone_info(self) -> dict[str, Any]:
"""Return detailed zone/speaker information.""" """Return detailed zone/speaker information."""
if self._device is None: if self._device is None:
return {} return {}