feat: add phone and media roles, and declare transport and reboot timing

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.
This commit is contained in:
2026-08-21 13:07:13 +07:00
parent d8dbc7a442
commit 70841feaa1
5 changed files with 157 additions and 0 deletions
+6
View File
@@ -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",
+10
View File
@@ -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.
+64
View File
@@ -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
"""
...
+71
View File
@@ -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
"""
...
+6
View File
@@ -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):