From 478c7b434a07d4c38f5762bdd721e64e7f0f68ad Mon Sep 17 00:00:00 2001 From: Christian Manivong Date: Sun, 12 Jul 2026 11:17:26 +0200 Subject: [PATCH] feat(firewall): add send_wake_on_lan() capability to FirewallDriver Abstract method for sending a Wake-on-LAN magic packet through a firewall's driver connection, following the same contract style as get_nat_translations/get_security_zones. Raises NotImplementedError by default; concrete drivers implement it per their own API. --- napalm_device_types/firewall.py | 27 +++++++++++++++++++++++++++ tests/test_send_wake_on_lan.py | 32 ++++++++++++++++++++++++++++++++ 2 files changed, 59 insertions(+) create mode 100644 tests/test_send_wake_on_lan.py diff --git a/napalm_device_types/firewall.py b/napalm_device_types/firewall.py index e6a8552..9507e25 100644 --- a/napalm_device_types/firewall.py +++ b/napalm_device_types/firewall.py @@ -160,6 +160,33 @@ class FirewallDriver(DeviceTypeDriver): """ raise NotImplementedError + def send_wake_on_lan(self, mac_address: str, interface: str = "") -> Dict[str, Any]: + """ + Sends a Wake-on-LAN "magic packet" to wake a host on the network. + + :param mac_address: Target host's MAC address (colon-separated, + case-insensitive, e.g. ``"AA:BB:CC:DD:EE:FF"``). + :param interface: Driver-specific interface identifier to broadcast the + magic packet from. Required by drivers that scope WOL per interface + (e.g. OPNsense); an empty string means "use the driver's default/ + only broadcast domain." Consult the concrete driver's docstring for + the exact expected format. + :raises NotImplementedError: If the driver does not support Wake-on-LAN. + :raises ValueError: If ``mac_address`` is malformed, or ``interface`` is + required by this driver but was not provided. + + :returns: A dict with: + + * success (bool) - ``True`` if the magic packet was sent without error + * output (string) - human-readable status message + + Example:: + + driver.send_wake_on_lan("AA:BB:CC:DD:EE:FF", interface="lan") + # → {"success": True, "output": "Magic packet sent to AA:BB:CC:DD:EE:FF via lan"} + """ + raise NotImplementedError + def get_packages(self) -> List[PackageDict]: """ Returns all packages / plugins currently known to the firewall's diff --git a/tests/test_send_wake_on_lan.py b/tests/test_send_wake_on_lan.py new file mode 100644 index 0000000..db3f522 --- /dev/null +++ b/tests/test_send_wake_on_lan.py @@ -0,0 +1,32 @@ +"""Tests for FirewallDriver.send_wake_on_lan default contract.""" + +import pytest +from napalm_device_types import FirewallDriver + + +class _BareFirewall(FirewallDriver): + """FirewallDriver.__init__ is NetworkDriver's, which itself raises + NotImplementedError — override with a no-op so tests exercise + send_wake_on_lan itself, not construction.""" + + def __init__(self): + pass + + +def test_send_wake_on_lan_raises_not_implemented_by_default(): + with pytest.raises(NotImplementedError): + _BareFirewall().send_wake_on_lan("AA:BB:CC:DD:EE:FF") + + +def test_send_wake_on_lan_accepts_optional_interface(): + with pytest.raises(NotImplementedError): + _BareFirewall().send_wake_on_lan("AA:BB:CC:DD:EE:FF", interface="lan") + + +def test_subclass_can_implement_send_wake_on_lan(): + class MyFirewall(_BareFirewall): + def send_wake_on_lan(self, mac_address: str, interface: str = ""): + return {"success": True, "output": f"woke {mac_address} via {interface}"} + + result = MyFirewall().send_wake_on_lan("AA:BB:CC:DD:EE:FF", interface="lan") + assert result == {"success": True, "output": "woke AA:BB:CC:DD:EE:FF via lan"}