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):