From 70841feaa130304cea182627954286ff430834ee Mon Sep 17 00:00:00 2001 From: Christian Manivong Date: Fri, 21 Aug 2026 13:07:13 +0700 Subject: [PATCH] feat: add phone and media roles, and declare transport and reboot timing MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two endpoint device types had nowhere to go and were filed under AccessPointDriver for want of anywhere better — a Yealink desk phone and a Sonos speaker. netOrk reads the access-point role to decide what appears in its wireless page, its AP profile pickers and its SSID drift view, so both showed up in all three. PhoneDriver and MediaDriver give them an honest home; each declares the surface its one existing driver actually implements, so the contract is real rather than aspirational. DeviceTypeDriver also gains two class attributes for facts netOrk kept as hardcoded driver-name sets on its own side (netork#113): USES_SSH whether netOrk reaches the device over SSH or a REST API — transport, which is why it is not a role REBOOT_SETTLE_SECONDS how long a reboot takes before polling is worth attempting again Both are driver facts and belong with the driver. A new driver is handled correctly without anyone remembering to extend a list in netOrk. --- napalm_device_types/__init__.py | 6 +++ napalm_device_types/base.py | 10 +++++ napalm_device_types/media.py | 64 +++++++++++++++++++++++++++++ napalm_device_types/phone.py | 71 +++++++++++++++++++++++++++++++++ tests/test_role_contracts.py | 6 +++ 5 files changed, 157 insertions(+) create mode 100644 napalm_device_types/media.py create mode 100644 napalm_device_types/phone.py diff --git a/napalm_device_types/__init__.py b/napalm_device_types/__init__.py index c9f27fc..37d2060 100644 --- a/napalm_device_types/__init__.py +++ b/napalm_device_types/__init__.py @@ -27,6 +27,8 @@ Role bases -- what a device *is*: * :class:`~napalm_device_types.hypervisor.HypervisorDriver` * :class:`~napalm_device_types.os.OSDriver` * :class:`~napalm_device_types.storage.StorageDriver` +* :class:`~napalm_device_types.phone.PhoneDriver` +* :class:`~napalm_device_types.media.MediaDriver` * :class:`~napalm_device_types.residential_gateway.ResidentialGatewayDriver` Function classes -- what a device *can do*. Shared behaviour lives here once @@ -60,8 +62,10 @@ from napalm_device_types.firewall_rules import FirewallRuleMixin from napalm_device_types.health_metrics import HealthMetricsMixin from napalm_device_types.interface_filter import InterfaceFilterMixin from napalm_device_types.mac_acl import MacAclMixin +from napalm_device_types.media import MediaDriver from napalm_device_types.nat_vpn import NatVpnMixin from napalm_device_types.packages import PackageManagementMixin +from napalm_device_types.phone import PhoneDriver from napalm_device_types.ping_sweep import PingSweepMixin, driver_supports_ping from napalm_device_types.roles import primary_role_of, role_keys_of, roles_of from napalm_device_types.services import ServiceControlMixin @@ -82,9 +86,11 @@ __all__ = [ "HypervisorDriver", "InterfaceFilterMixin", "MacAclMixin", + "MediaDriver", "NatVpnMixin", "OSDriver", "PackageManagementMixin", + "PhoneDriver", "PingSweepMixin", "PortSpec", "ResidentialGatewayDriver", diff --git a/napalm_device_types/base.py b/napalm_device_types/base.py index 60297f0..5f2d52d 100644 --- a/napalm_device_types/base.py +++ b/napalm_device_types/base.py @@ -80,5 +80,15 @@ class DeviceTypeDriver(PingSweepMixin, NetworkDriver): SSH_FINGERPRINT: list[FingerprintRule] = [] HTTP_FINGERPRINT: list[FingerprintRule] = [] OUI_PREFIXES: list[str] = [] + + #: Whether netOrk reaches this device over SSH. False for drivers that talk + #: to a REST API instead -- which decides whether an SSH credential is worth + #: asking for, and whether an SSH-shaped error message would even make sense. + USES_SSH: bool = True + + #: How long a reboot takes before the device is worth polling again, in + #: seconds. Hypervisors and general-purpose OS hosts run through a full + #: init sequence; a switch or an access point is back in half the time. + REBOOT_SETTLE_SECONDS: int = 45 # Format: "AA:BB:CC" — first 3 octets of MAC, uppercase, colon-separated. # A match contributes fixed weight 6.0 to the fingerprint score. diff --git a/napalm_device_types/media.py b/napalm_device_types/media.py new file mode 100644 index 0000000..20c52f9 --- /dev/null +++ b/napalm_device_types/media.py @@ -0,0 +1,64 @@ +# -*- coding: utf-8 -*- +""" +Abstract base class for networked media players. + +Usage:: + + from napalm_device_types import MediaDriver + + class SonosDriver(MediaDriver): + ... +""" + +from typing import TYPE_CHECKING, Any, Dict, List + +from napalm_device_types.base import DeviceTypeDriver + + +class MediaDriver(DeviceTypeDriver): + """ + Abstract intermediate driver for speakers, streamers and media renderers + (e.g. Sonos, Chromecast, Squeezebox, UPnP/DLNA renderers). + + Like :class:`~napalm_device_types.phone.PhoneDriver`, this exists so an + endpoint stops being filed under ``AccessPointDriver`` for want of anywhere + better. A speaker has no SSIDs, no radio configuration and no AP profile; it + has a transport state and a volume. + """ + + #: Stable key netOrk exposes as ``device_class``. The order in which a + #: driver lists its role bases is the ranking; see + #: :func:`napalm_device_types.roles.primary_role_of`. + ROLE: str = "media" + TYPE_LABEL: str = "Media" + + if TYPE_CHECKING: + + def get_playback_state(self) -> Dict[str, Any]: + """ + Returns the transport state of the renderer. + + * state (string) - ``"playing"``, ``"paused"``, ``"stopped"``, or ``"transitioning"`` + * source (string) - input or service currently selected + """ + ... + + def get_volume(self) -> int: + """Returns the current output volume, 0-100.""" + ... + + def set_volume(self, level: int) -> None: + """Sets the output volume, 0-100.""" + ... + + def get_zone_info(self) -> List[Dict[str, Any]]: + """ + Returns the zones or groups this renderer participates in. + + Each entry contains: + + * name (string) - zone/room name + * coordinator (bool) - whether this device leads the group + * members (list) - names of the other devices in the group + """ + ... diff --git a/napalm_device_types/phone.py b/napalm_device_types/phone.py new file mode 100644 index 0000000..871a7cd --- /dev/null +++ b/napalm_device_types/phone.py @@ -0,0 +1,71 @@ +# -*- coding: utf-8 -*- +""" +Abstract base class for IP telephony endpoints. + +Usage:: + + from napalm_device_types import PhoneDriver + + class YealinkDriver(PhoneDriver): + ... +""" + +from typing import TYPE_CHECKING, Any, Dict, List + +from napalm_device_types.base import DeviceTypeDriver + + +class PhoneDriver(DeviceTypeDriver): + """ + Abstract intermediate driver for desk phones, conference units and DECT + bases (e.g. Yealink T-series, Snom, Grandstream, Fanvil). + + A phone is an endpoint, not infrastructure. It was worth separating from + ``AccessPointDriver`` — which some phone drivers used to inherit for want of + anywhere better — because netOrk offers access points for AP profiles, + wireless management and SSID drift, none of which a desk phone should appear + in. A WiFi-capable phone still reports its radio through its own methods; it + just is not an access point. + """ + + #: Stable key netOrk exposes as ``device_class``. The order in which a + #: driver lists its role bases is the ranking; see + #: :func:`napalm_device_types.roles.primary_role_of`. + ROLE: str = "phone" + TYPE_LABEL: str = "Phone" + + if TYPE_CHECKING: + + def get_sip_accounts(self) -> List[Dict[str, Any]]: + """ + Returns the SIP registrations configured on the phone. + + Each entry contains: + + * account (string) - display label or line key + * user (string) - SIP user / extension + * server (string) - registrar host + * registered (bool) - whether the registration is currently active + + Example:: + + [ + { + "account": "Line 1", + "user": "201", + "server": "pbx.example.com", + "registered": True, + } + ] + """ + ... + + def get_call_status(self) -> Dict[str, Any]: + """ + Returns what the phone is doing right now. + + * state (string) - ``"idle"``, ``"ringing"``, ``"talking"``, or ``"held"`` + * remote (string) - remote party, empty string when idle + * duration (int) - seconds in the current state; ``0`` when idle + """ + ... diff --git a/tests/test_role_contracts.py b/tests/test_role_contracts.py index aa026de..f5673a5 100644 --- a/tests/test_role_contracts.py +++ b/tests/test_role_contracts.py @@ -22,7 +22,9 @@ from napalm_device_types import ( DeviceTypeDriver, FirewallDriver, HypervisorDriver, + MediaDriver, OSDriver, + PhoneDriver, ResidentialGatewayDriver, StorageDriver, SwitchDriver, @@ -33,7 +35,9 @@ ROLE_BASES = [ AccessPointDriver, FirewallDriver, HypervisorDriver, + MediaDriver, OSDriver, + PhoneDriver, ResidentialGatewayDriver, StorageDriver, SwitchDriver, @@ -78,6 +82,8 @@ class TestDeclaredMethodsDoNotExistAtRuntime: (AccessPointDriver, "get_wireless_config"), (OSDriver, "get_processes"), (ResidentialGatewayDriver, "get_wan_status"), + (PhoneDriver, "get_sip_accounts"), + (MediaDriver, "get_playback_state"), ], ) def test_absent_until_a_driver_implements_it(self, base, method):