-
Notifications
You must be signed in to change notification settings - Fork 6
feat(network): add Wake-on-LAN wake module (type: wol) #217
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: develop
Are you sure you want to change the base?
Changes from all commits
c26f24a
1a27ddd
1fc2e62
39d0dd0
f8131e3
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,101 @@ | ||
| #!/usr/bin/env python3 | ||
| #** ***************************************************************************** | ||
| # * | ||
| # * If not stated otherwise in this file or this component's LICENSE file the | ||
| # * following copyright and licenses apply: | ||
| # * | ||
| # * Copyright 2026 RDK Management | ||
| # * | ||
| # * Licensed under the Apache License, Version 2.0 (the "License"); | ||
| # * you may not use this file except in compliance with the License. | ||
| # * You may obtain a copy of the License at | ||
| # * | ||
| # * | ||
| # http://www.apache.org/licenses/LICENSE-2.0 | ||
| # * | ||
| # * Unless required by applicable law or agreed to in writing, software | ||
| # * distributed under the License is distributed on an "AS IS" BASIS, | ||
| # * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
| # * See the License for the specific language governing permissions and | ||
| # * limitations under the License. | ||
| # * | ||
| #* ****************************************************************************** | ||
| #* | ||
| #* ** Project : RAFT | ||
| #* ** @addtogroup : core | ||
| #* ** @file : networkControl.py | ||
| #* ** | ||
| #* ** @brief : Network-level device control (e.g. Wake-on-LAN wake) for a DUT. | ||
| #* ** | ||
| #* ****************************************************************************** | ||
|
|
||
| from framework.core.networkModules.wol import networkWol | ||
| from framework.core.utilities import utilities | ||
|
|
||
| from framework.core.logModule import logModule | ||
|
|
||
| class networkControlClass(): | ||
|
|
||
| def __init__(self, log:logModule, config:dict): | ||
| """Initialise the network controller. | ||
|
|
||
| Args: | ||
| log (logModule): log module class | ||
| config (dict): configuration from the .yml file (a device ``network:`` block) | ||
| """ | ||
| self.log = log | ||
| self.utils = utilities(log) | ||
| # Fall back to the type so logs read e.g. "wake (wol)" rather than "wake (None)". | ||
| self.name = config.get("name") or config.get("type") | ||
|
|
||
| # 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) | ||
|
Comment on lines
+51
to
+53
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should retries really come from the config. Not the test?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. thank you read that wrong. This is coming from the config, the retry count is a system control not a test control, that's what I required, that would then mean that the hardware we're talking too has a specification, and if it doesn't meet that then it fails... And that's why it's setup this way. It's not ok for a test to think it knows the specification of the hardware.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. In this case the hardware is the DUT, right? If that's the case I would expect the retry count to be in the device_config.yml and the test to read it and pass it in at run time. I could be missing something though? |
||
|
|
||
| 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 | ||
|
|
||
| def wake(self): | ||
| """Wake the device over the network (e.g. Wake-on-LAN magic packet). | ||
|
|
||
| Returns: | ||
| bool: True if the wake was successfully sent. | ||
| """ | ||
| if self.networkModule is None: | ||
| self.log.error("wake ({}): no network module configured (check 'type')".format(self.name)) | ||
| return False | ||
| self.log.info("wake ({})".format(self.name)) | ||
| return self.networkRetry(self.networkModule.wake) | ||
|
|
||
| def networkRetry(self, networkMethod): | ||
|
TB-1993 marked this conversation as resolved.
|
||
| """Perform the passed networkMethod and retry it if it fails. | ||
|
|
||
| Retries on both a raised exception and a falsy return, up to | ||
| ``retryCount`` times with a fixed ``retryDelay`` between attempts | ||
| (Wake-on-LAN either lands or it does not -- no exponential backoff). | ||
|
|
||
| Args: | ||
| networkMethod (Method): The networkMethod to perform. | ||
|
|
||
| Returns: | ||
| boolean: True if any attempt succeeded, otherwise False. | ||
| """ | ||
| result = False | ||
| for attempt in range(self.retryCount + 1): | ||
| try: | ||
| result = networkMethod() | ||
| except Exception as error: | ||
| self.log.error("{} raised: {}".format(networkMethod.__name__, error)) | ||
| result = False | ||
| if result: | ||
| break | ||
| if attempt < self.retryCount: | ||
| self.log.info("{} failed, retry [{}] of [{}] after [{}]s".format( | ||
| networkMethod.__name__, attempt + 1, self.retryCount, self.retryDelay)) | ||
| self.utils.wait(self.retryDelay) | ||
| return result | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,9 @@ | ||
| #!/usr/bin/env python3 | ||
|
|
||
| import sys | ||
| import os | ||
|
|
||
| dir_path = os.path.dirname(os.path.realpath(__file__)) | ||
| sys.path.append( dir_path ) | ||
|
|
||
| #print("framework-> networkModules -> __init__.py") |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,108 @@ | ||
| #!/usr/bin/env python3 | ||
| #** ***************************************************************************** | ||
| # * | ||
| # * If not stated otherwise in this file or this component's LICENSE file the | ||
| # * following copyright and licenses apply: | ||
| # * | ||
| # * Copyright 2026 RDK Management | ||
| # * | ||
| # * Licensed under the Apache License, Version 2.0 (the "License"); | ||
| # * you may not use this file except in compliance with the License. | ||
| # * You may obtain a copy of the License at | ||
| # * | ||
| # * | ||
| # http://www.apache.org/licenses/LICENSE-2.0 | ||
| # * | ||
| # * Unless required by applicable law or agreed to in writing, software | ||
| # * distributed under the License is distributed on an "AS IS" BASIS, | ||
| # * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
| # * See the License for the specific language governing permissions and | ||
| # * limitations under the License. | ||
| # * | ||
| #* ****************************************************************************** | ||
| #* | ||
| #* ** Project : RAFT | ||
| #* ** @addtogroup : core.networkModules | ||
| #* ** @file : wol.py | ||
| #* ** @brief : Wake-on-LAN network module. wake() sends a magic packet to | ||
| #* ** wake a sleeping / soft-off device on the same broadcast domain. | ||
| #* ** | ||
| #* ****************************************************************************** | ||
|
|
||
| import socket | ||
|
|
||
|
|
||
| class networkWol(): | ||
|
TB-1993 marked this conversation as resolved.
|
||
| """Wake-on-LAN network module. | ||
|
|
||
| A magic packet wakes a device that is asleep or in a Wake-on-LAN soft-off | ||
| state, provided the host sending it shares the target's layer-2 broadcast | ||
| domain (or a directed subnet broadcast reaches it). Wake-on-LAN is a | ||
| network (layer-2) action, not a power action: it can only wake a device, so | ||
| the module exposes a single ``wake()`` verb. | ||
|
|
||
| Rack-config fields (under a device's ``network:``):: | ||
|
|
||
| network: | ||
| type: "wol" | ||
| mac: "b0:3e:51:ff:f6:bc" # required - target NIC MAC | ||
| broadcast: "255.255.255.255" # optional - subnet broadcast for directed WoL | ||
| port: 9 # optional - 9 (default) or 7 | ||
| """ | ||
|
|
||
| def __init__(self, log, mac, broadcast="255.255.255.255", port=9): | ||
| """ | ||
| Args: | ||
| log: The log module. | ||
| mac (str): Target NIC MAC address (":" / "-" separated or bare hex). | ||
| broadcast (str): Broadcast address the magic packet is sent to. | ||
| port (int): UDP port for the magic packet (typically 7 or 9). | ||
| """ | ||
| self.log = log | ||
| if not mac: | ||
| raise ValueError("networkWol requires a 'mac' address") | ||
| self.mac = mac | ||
| # Validate/normalise the MAC up front so a misconfiguration fails at | ||
| # construction with a clear error, rather than silently at wake() time. | ||
| self._hex_mac = self._normalise_mac(mac) | ||
| self.broadcast = broadcast or "255.255.255.255" | ||
| self.port = int(port) if port else 9 | ||
| self.log.info("networkWol(mac={}, broadcast={}, port={})".format( | ||
| self.mac, self.broadcast, self.port)) | ||
|
Comment on lines
+68
to
+71
|
||
|
|
||
| @staticmethod | ||
| def _normalise_mac(mac): | ||
| """Strip separators/whitespace and validate the MAC as 12 hex digits. | ||
|
|
||
| Raises: | ||
| ValueError: with a clear, field-named message if the MAC is not | ||
| twelve hexadecimal digits. | ||
| """ | ||
| hex_mac = mac.strip().replace(":", "").replace("-", "").replace(".", "") | ||
| if len(hex_mac) != 12 or any(c not in "0123456789abcdefABCDEF" for c in hex_mac): | ||
| raise ValueError( | ||
| "Invalid Wake-on-LAN 'mac' address: {!r} " | ||
| "(expected 12 hex digits, e.g. aa:bb:cc:dd:ee:ff)".format(mac)) | ||
| return hex_mac | ||
|
|
||
| def _magic_packet(self): | ||
| """Build the Wake-on-LAN magic packet: 6x 0xFF followed by the MAC x16.""" | ||
| return b"\xff" * 6 + bytes.fromhex(self._hex_mac) * 16 | ||
|
|
||
| def wake(self): | ||
| """Send the Wake-on-LAN magic packet. | ||
|
|
||
| Returns: | ||
| bool: True if the packet was sent, False on error. | ||
| """ | ||
| try: | ||
| packet = self._magic_packet() | ||
| 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 for {} sent to {}:{}".format( | ||
| self.mac, self.broadcast, self.port)) | ||
| return True | ||
| except Exception as error: | ||
| self.log.error("networkWol().wake failed: {}".format(error)) | ||
| return False | ||
Uh oh!
There was an error while loading. Please reload this page.