feat(network): add Wake-on-LAN wake module (type: wol)#217
Conversation
There was a problem hiding this comment.
Pull request overview
Adds a new Wake-on-LAN (WoL) “power switch” implementation and wires it into the existing powerControlClass dispatch so RAFT can wake devices that are asleep/soft-off via a WoL magic packet.
Changes:
- Added
powerWolmodule that constructs and sends a WoL magic packet to a configurable broadcast address/UDP port. - Added
type == "wol"dispatch branch inpowerControlClassto instantiate the new module.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 4 comments.
| File | Description |
|---|---|
| framework/core/powerModules/wol.py | Implements WoL magic-packet creation and UDP broadcast send; includes wake-only semantics for powerOff()/reboot(). |
| framework/core/powerControl.py | Adds import and type: "wol" dispatch branch to construct powerWol. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| raise ValueError("powerWol requires a 'mac' address") | ||
| self.mac = mac | ||
| self.broadcast = broadcast or "255.255.255.255" | ||
| self.port = int(port) if port else 9 |
| self.log.info("powerWol(mac={}, broadcast={}, port={})".format( | ||
| self.mac, self.broadcast, self.port)) | ||
|
|
||
| def _magicPacket(self): |
| hexMac = self.mac.replace(":", "").replace("-", "").replace(".", "") | ||
| if len(hexMac) != 12: | ||
| raise ValueError("Invalid MAC address: {}".format(self.mac)) | ||
| return b"\xff" * 6 + bytes.fromhex(hexMac) * 16 |
| bool: True if the packet was sent, False on error. | ||
| """ | ||
| try: | ||
| packet = self._magicPacket() |
c5d9760 to
0a32b41
Compare
powerControlClass had no way to wake a device that is asleep or in a Wake-on-LAN soft-off state. Add framework/core/powerModules/wol.py (powerWol) and a `type: "wol"` dispatch branch: - powerOn() sends the WoL magic packet (6x 0xFF + MAC x16) to broadcast:port. - Wake-on-LAN is wake-only: powerOff() is a logged no-op, reboot() a best-effort wake. - Config: mac (required), broadcast (default 255.255.255.255), port (default 9). - Standard-library only (socket); no new dependencies. Fixes rdkcentral#216
| self.networkModule = None | ||
| type = config.get("type") | ||
| if type == "wol": | ||
| self.networkModule = networkWol(log, config.get("mac"), config.get("broadcast", "255.255.255.255"), config.get("port", 9)) | ||
| else: | ||
| self.log.error("Network controller type [{}] unknown".format(type)) | ||
| return |
| self.log.info("wake ({})".format(self.name)) | ||
| return self.networkRetry(self.networkModule.wake) | ||
|
|
| self.log.info(f"Could not perform {[networkMethod.__name__]}. Retry count: [{x}] of [{self.retryCount}], delay is [{self.retryDelay}]") | ||
| if x == self.retryCount: | ||
| raise e | ||
| self.utils.wait(self.retryDelay) |
| """Build the Wake-on-LAN magic packet: 6x 0xFF followed by the MAC x16.""" | ||
| hexMac = self.mac.replace(":", "").replace("-", "").replace(".", "") | ||
| if len(hexMac) != 12: | ||
| raise ValueError("Invalid MAC address: {}".format(self.mac)) | ||
| return b"\xff" * 6 + bytes.fromhex(hexMac) * 16 |
| # [ network: optional ] - Network/wake controller for the slot | ||
| # Network-level device control (distinct from powerSwitch); wake-only. | ||
| # supported types: | ||
| # [type: "wol", mac: "aa:bb:cc:dd:ee:ff", broadcast:"optional (default 255.255.255.255)", port:"optional (default 9)"] | ||
| # Wake-on-LAN magic packet - wakes a sleeping / soft-off device on the | ||
| # same broadcast domain. Call dut.networkController.wake(). |
| def _magicPacket(self): | ||
| """Build the Wake-on-LAN magic packet: 6x 0xFF followed by the MAC x16.""" | ||
| hexMac = self.mac.replace(":", "").replace("-", "").replace(".", "") | ||
| if len(hexMac) != 12: | ||
| raise ValueError("Invalid MAC address: {}".format(self.mac)) | ||
| return b"\xff" * 6 + bytes.fromhex(hexMac) * 16 | ||
|
|
||
| def wake(self): |
…ake seam
Wake-on-LAN is a network (layer-2) action, not a power action: it sends a
UDP magic packet, cannot power a device off, and supports none of the power
metering contract. Move it out of powerModules onto a dedicated network
controller that mirrors the existing device-controller pattern
(remoteController / hdmiCECController / avSyncController).
- Add framework/core/networkModules/wol.py (networkWol) with a single wake()
verb (renamed from powerOn; drop the power-only powerOff/reboot shims).
- Add framework/core/networkControl.py (networkControlClass) dispatching
type: "wol", with retry, mirroring powerControl.
- Wire a device `network:` config block in deviceManager -> dut.networkController;
expose it in testControl.
- Revert the power-seam coupling: remove the wol import + type:"wol" branch
from powerControl.py and delete powerModules/wol.py.
- Document the `network:` block in the example rack config.
- Add tests/networkControl_tests.py (packet correctness, loopback capture,
MAC validation, unknown-type handling).
Config now:
network:
type: "wol"
mac: "aa:bb:cc:dd:ee:ff"
broadcast: "255.255.255.255" # optional
port: 9 # optional
Waking is now an explicit dut.networkController.wake() rather than riding on
powerControl.powerOn().
Relates to rdkcentral#216
64dff49 to
1a27ddd
Compare
|
Updated and ready for review. This revision re-homes Wake-on-LAN from the power seam onto a dedicated network/wake controller ( dut.networkController.wake()@TB-1993 could you take another look and approve when happy? Thanks 🙏 |
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 9 out of 9 changed files in this pull request and generated 3 comments.
Comments suppressed due to low confidence (1)
framework/core/networkControl.py:59
- If
config['type']is unknown, this constructor only logs an error and leavesself.networkModuleasNone. A subsequentwake()call will then raiseAttributeErrorwhen it tries to accessself.networkModule.wake. Also, assigning to a local namedtypeshadows the Python built-in.
self.networkModule = None
type = config.get("type")
if type == "wol":
self.networkModule = networkWol(log, config.get("mac"), config.get("broadcast", "255.255.255.255"), config.get("port", 9))
else:
self.log.error("Network controller type [{}] unknown".format(type))
| for x in range(self.retryCount+1): | ||
| try: | ||
| result = networkMethod() | ||
| break | ||
| except Exception as e: | ||
| self.log.info(f"Could not perform {[networkMethod.__name__]}. Retry count: [{x}] of [{self.retryCount}], delay is [{self.retryDelay}]") | ||
| if x == self.retryCount: | ||
| raise e | ||
| self.utils.wait(self.retryDelay) | ||
| return result |
| try: | ||
| packet = self._magicPacket() | ||
| with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as sock: | ||
| sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1) | ||
| sock.sendto(packet, (self.broadcast, self.port)) | ||
| self.log.info("networkWol().wake: magic packet sent to {}".format(self.mac)) | ||
| return True | ||
| except Exception as error: | ||
| self.log.error("networkWol().wake failed: {}".format(error)) | ||
| return False |
| (default 30s). `powerControlClass` (config `powerSwitch:`) owns device power; the | ||
| separate `networkControlClass` (config `network:`) owns network-level actions such as | ||
| Wake-on-LAN. They are intentionally distinct: Wake-on-LAN is a network (layer-2) | ||
| action, not a power action, so it lives in `networkModules/`, not `powerModules/`. |
| self.log.info("wake ({})".format(self.name)) | ||
| return self.networkRetry(self.networkModule.wake) | ||
|
|
||
| def networkRetry(self, networkMethod): |
There was a problem hiding this comment.
We should probably get people to use the backoff library for this https://pypi.org/project/backoff/. I think this method becomes a nightmare if we ever add any methods that require arguments.
| import socket | ||
|
|
||
|
|
||
| class networkWol(): |
There was a problem hiding this comment.
I was thinking the networkController class would be where we would implement interacting with different network switches. So users could look up MAC addresses, IPs, enable/disable ports firewalls etc. So I was thinking this could just be a generic Host based network controller. With methods implemented to run on the host, providing some of that functionality.
What do you think?
| # If variables are not passed in the config they will be defaulted to retryCount: [1], retryDelay: [30] | ||
| self.retryCount = config.get("retryCount", 1) | ||
| self.retryDelay = config.get("retryDelay", 30) |
There was a problem hiding this comment.
Should retries really come from the config. Not the test?
Closes #216.
What
Adds a network/wake controller for a device — Wake-on-LAN lives here, not in the power seam.
framework/core/networkModules/wol.py(networkWol) — a singlewake()verb that sends the WoL magic packet (6×0xFF+ target MAC ×16) tobroadcast:port.framework/core/networkControl.py(networkControlClass) — dispatchestype: "wol", with the same retry behaviour (retryCount/retryDelay) aspowerControl.deviceManagervia a devicenetwork:block → exposed asdut.networkController; also surfaced ontestControlasself.networkController.socket) — no new dependencies.Usage in a suite is deliberately dead-simple:
Why network, not power
Wake-on-LAN is a network (layer-2) action, not a power action: it sends a UDP magic packet, cannot power a device off, and supports none of the power-metering contract (
getPowerLevel/getVoltageLevel/getCurrentLevel). Filing it underpowerModulesmislabelled it and left half the "power switch" contract inapplicable.So it is re-homed onto a dedicated network controller that mirrors the existing device-controller pattern (
remoteController/hdmiCECController/avSyncController). The problem it solves is unchanged: targets that aggressively deep-sleep drop their console when idle and can only be brought back by a magic packet; without this, runs against them need an out-of-band wake step before the session opens.Behavioural note: waking is now an explicit
dut.networkController.wake()rather than riding onpowerControl.powerOn(). The seam is designed to grow (e.g.isReachable()/waitForOnline()as a natural companion towake();deviceClass.pingTest()already offers ICMP reachability).Testing
tests/networkControl_tests.py— buildsnetworkControlClassfrom config and callswake().6×0xFF+ MAC×16),wake()sends and was captured on loopback, missing / malformed MAC raiseValueError, unknown type is handled gracefully.network:block producesdut.networkController;powerControl(type:"wol")now correctly reports the type as unknown.Docs
AGENTS.md— directory tree, controller shortcut attributes, and a new §5.2 Network / wake modules (broadened §5 to "Power & Network Control").docs/modules/device_manager.md—networkControlleradded to thedeviceClassmanaged objects.Note: the branch name (
…power-module) predates the re-home; the code is a network module. First commit adds the initial (power-seam) cut; the second re-homes it to the network seam and reverts the power coupling.