diff --git a/setup.py b/setup.py index 6a66f012f9..5d0dc0ea35 100644 --- a/setup.py +++ b/setup.py @@ -88,7 +88,6 @@ 'utilities_common', 'watchdogutil', 'sonic_cli_gen', - 'wol', ], package_data={ 'generic_config_updater': ['gcu_services_validator.conf.json', 'gcu_field_operation_validators.conf.json'], @@ -223,7 +222,6 @@ 'undebug = undebug.main:cli', 'watchdogutil = watchdogutil.main:watchdogutil', 'sonic-cli-gen = sonic_cli_gen.main:cli', - 'wol = wol.main:wol', ] }, install_requires=[ diff --git a/tests/wol_test.py b/tests/wol_test.py deleted file mode 100644 index 011676eeac..0000000000 --- a/tests/wol_test.py +++ /dev/null @@ -1,229 +0,0 @@ -import click -import io -import pytest -import wol.main as wol -from click.testing import CliRunner -from unittest.mock import patch, MagicMock - -ETHER_TYPE_WOL = b'\x08\x42' -BROADCAST_MAC = wol.MacAddress('ff:ff:ff:ff:ff:ff') - -SAMPLE_INTERFACE_ETH0 = "Ethernet0" -SAMPLE_INTERFACE_VLAN1000 = "Vlan1000" -SAMPLE_INTERFACE_PO100 = "PortChannel100" - -SAMPLE_ETH0_MAC = wol.MacAddress('11:33:55:77:99:bb') -SAMPLE_VLAN1000_MAC = wol.MacAddress('22:44:66:88:aa:cc') -SAMPLE_PO100_MAC = wol.MacAddress('33:55:77:99:bb:dd') -SAMPLE_TARGET_MAC = wol.MacAddress('44:66:88:aa:cc:ee') -SAMPLE_TARGET_MAC_LIST = [wol.MacAddress('44:66:88:aa:cc:ee'), wol.MacAddress('55:77:99:bb:dd:ff')] - -SAMPLE_MAGIC_PACKET_UNICAST = SAMPLE_TARGET_MAC.to_bytes() + SAMPLE_ETH0_MAC.to_bytes() + ETHER_TYPE_WOL + b'\xff' * 6 + SAMPLE_TARGET_MAC.to_bytes() * 16 -SAMPLE_MAGIC_PACKET_BROADCAST = BROADCAST_MAC.to_bytes() + SAMPLE_ETH0_MAC.to_bytes() + ETHER_TYPE_WOL + b'\xff' * 6 + SAMPLE_TARGET_MAC.to_bytes() * 16 - - -class TestMacAddress(): - def test_init(self): - # Test Case 1: Test with a valid MAC address - assert wol.MacAddress('00:11:22:33:44:55').address == b'\x00\x11\x22\x33\x44\x55' - # Test Case 2: Test with an invalid MAC address - with pytest.raises(ValueError) as exc_info: - wol.MacAddress('INVALID_MAC_ADDRESS') - assert exc_info.value.message == "invalid MAC address" - with pytest.raises(ValueError) as exc_info: - wol.MacAddress('00:11:22:33:44') - assert exc_info.value.message == "invalid MAC address" - - def test_str(self): - assert str(wol.MacAddress('00:01:0a:a0:aa:ee')) == '00:01:0a:a0:aa:ee' - assert str(wol.MacAddress('ff:ff:ff:ff:ff:ff')) == 'ff:ff:ff:ff:ff:ff' - - def test_eq(self): - # Test Case 1: Test with two equal MAC addresses - assert wol.MacAddress('00:11:22:33:44:55') == wol.MacAddress('00:11:22:33:44:55') - # Test Case 2: Test with two unequal MAC addresses - assert wol.MacAddress('00:11:22:33:44:55') != wol.MacAddress('55:44:33:22:11:00') - - def test_to_bytes(self): - assert wol.MacAddress('00:11:22:33:44:55').to_bytes() == b'\x00\x11\x22\x33\x44\x55' - - -@patch('wol.main.get_interface_mac', MagicMock(return_value=SAMPLE_ETH0_MAC)) -def test_build_magic_packet(): - # Test Case 1: Test build magic packet basic - expected_output = SAMPLE_TARGET_MAC.to_bytes() + SAMPLE_ETH0_MAC.to_bytes() + ETHER_TYPE_WOL \ - + b'\xff' * 6 + SAMPLE_TARGET_MAC.to_bytes() * 16 - assert wol.build_magic_packet(SAMPLE_INTERFACE_ETH0, SAMPLE_TARGET_MAC, broadcast=False, password=b'') == expected_output - # Test Case 2: Test build magic packet with broadcast flag - expected_output = BROADCAST_MAC.to_bytes() + SAMPLE_ETH0_MAC.to_bytes() + ETHER_TYPE_WOL \ - + b'\xff' * 6 + SAMPLE_TARGET_MAC.to_bytes() * 16 - assert wol.build_magic_packet(SAMPLE_INTERFACE_ETH0, SAMPLE_TARGET_MAC, broadcast=True, password=b'') == expected_output - # Test Case 3: Test build magic packet with 4-byte password - password = b'\x12\x34' - expected_output = SAMPLE_TARGET_MAC.to_bytes() + SAMPLE_ETH0_MAC.to_bytes() + ETHER_TYPE_WOL \ - + b'\xff' * 6 + SAMPLE_TARGET_MAC.to_bytes() * 16 + password - assert wol.build_magic_packet(SAMPLE_INTERFACE_ETH0, SAMPLE_TARGET_MAC, broadcast=False, password=password) == expected_output - # Test Case 4: Test build magic packet with 6-byte password - password = b'\x12\x34\x56\x78\x9a\xbc' - expected_output = SAMPLE_TARGET_MAC.to_bytes() + SAMPLE_ETH0_MAC.to_bytes() + ETHER_TYPE_WOL \ - + b'\xff' * 6 + SAMPLE_TARGET_MAC.to_bytes() * 16 + password - assert wol.build_magic_packet(SAMPLE_INTERFACE_ETH0, SAMPLE_TARGET_MAC, broadcast=False, password=password) == expected_output - - -def test_send_magic_packet(): - # Test Case 1: Test send magic packet with count is 1 - with patch('socket.socket') as mock_socket: - wol.send_magic_packet(SAMPLE_INTERFACE_ETH0, SAMPLE_TARGET_MAC, SAMPLE_MAGIC_PACKET_UNICAST, count=1, interval=0, verbose=False) - mock_socket.return_value.bind.assert_called_once_with((SAMPLE_INTERFACE_ETH0, 0)) - mock_socket.return_value.send.assert_called_once_with(SAMPLE_MAGIC_PACKET_UNICAST) - # Test Case 2: Test send magic packet with count is 3 - with patch('socket.socket') as mock_socket: - wol.send_magic_packet(SAMPLE_INTERFACE_ETH0, SAMPLE_TARGET_MAC, SAMPLE_MAGIC_PACKET_UNICAST, count=3, interval=0, verbose=False) - assert mock_socket.return_value.bind.call_count == 1 - assert mock_socket.return_value.send.call_count == 3 - # Test Case 3: Test send magic packet with interval is 1000 - with patch('socket.socket') as mock_socket, \ - patch('time.sleep') as mock_sleep: - wol.send_magic_packet(SAMPLE_INTERFACE_ETH0, SAMPLE_TARGET_MAC, SAMPLE_MAGIC_PACKET_UNICAST, count=3, interval=1000, verbose=False) - assert mock_socket.return_value.bind.call_count == 1 - assert mock_socket.return_value.send.call_count == 3 - assert mock_sleep.call_count == 2 # sleep twice between 3 packets - mock_sleep.assert_called_with(1) - # Test Case 4: Test send magic packet with verbose is True - expected_verbose_output = f"Sending 5 magic packet to {SAMPLE_TARGET_MAC} via interface {SAMPLE_INTERFACE_ETH0}\n" + \ - f"1st magic packet sent to {SAMPLE_TARGET_MAC}\n" + \ - f"2nd magic packet sent to {SAMPLE_TARGET_MAC}\n" + \ - f"3rd magic packet sent to {SAMPLE_TARGET_MAC}\n" + \ - f"4th magic packet sent to {SAMPLE_TARGET_MAC}\n" + \ - f"5th magic packet sent to {SAMPLE_TARGET_MAC}\n" - with patch('socket.socket') as mock_socket, patch('time.sleep'), patch('sys.stdout', new_callable=io.StringIO) as mock_stdout: - wol.send_magic_packet(SAMPLE_INTERFACE_ETH0, SAMPLE_TARGET_MAC, SAMPLE_MAGIC_PACKET_UNICAST, count=5, interval=1000, verbose=True) - assert mock_socket.return_value.bind.call_count == 1 - assert mock_socket.return_value.send.call_count == 5 - assert mock_stdout.getvalue() == expected_verbose_output - - -@patch('netifaces.interfaces', MagicMock(return_value=[SAMPLE_INTERFACE_ETH0])) -@patch('wol.main.get_interface_operstate', MagicMock(return_value="up")) -def test_validate_interface(): - # Test Case 1: Test with a valid SONiC interface name - assert wol.validate_interface(None, None, SAMPLE_INTERFACE_ETH0) == SAMPLE_INTERFACE_ETH0 - # Test Case 2: Test with an invalid SONiC interface name - with pytest.raises(click.BadParameter) as exc_info: - wol.validate_interface(None, None, "INVALID_SONIC_INTERFACE") - assert exc_info.value.message == "invalid SONiC interface name INVALID_SONIC_INTERFACE" - # Test Case 3: Test with an valid SONiC interface name, but the interface operstat is down - with patch('wol.main.get_interface_operstate', MagicMock(return_value="down")): - with pytest.raises(click.BadParameter) as exc_info: - wol.validate_interface(None, None, SAMPLE_INTERFACE_ETH0) - assert exc_info.value.message == f"interface {SAMPLE_INTERFACE_ETH0} is not up" - - -def test_parse_target_mac(): - # Test Case 1: Test with a single valid target MAC address - wol.parse_target_mac(None, None, str(SAMPLE_TARGET_MAC)) == [SAMPLE_TARGET_MAC] - # Test Case 2: Test with a list of valid target MAC addresses - mac_list = [SAMPLE_ETH0_MAC, SAMPLE_VLAN1000_MAC, SAMPLE_PO100_MAC] - assert wol.parse_target_mac(None, None, ",".join([str(x) for x in mac_list])) == mac_list - # Test Case 3: Test with a single invalid target MAC address - with pytest.raises(click.BadParameter) as exc_info: - wol.parse_target_mac(None, None, "INVALID_MAC_ADDRESS") - assert exc_info.value.message == "invalid MAC address INVALID_MAC_ADDRESS" - # Test Case 4: Test with a list of target MAC addresses, one of them is invalid - with pytest.raises(click.BadParameter) as exc_info: - wol.parse_target_mac(None, None, ",".join([str(SAMPLE_ETH0_MAC), "INVALID_MAC_ADDRESS"])) - assert exc_info.value.message == "invalid MAC address INVALID_MAC_ADDRESS" - - -def test_parse_password(): - # Test Case 1: Test with an empty password - assert wol.parse_password(None, None, "") == b'' - # Test Case 2: Test with a valid 4-byte password - assert wol.parse_password(None, None, "1.2.3.4") == b'\x01\x02\x03\x04' - # Test Case 3: Test with an invalid 4-byte password - with pytest.raises(click.BadParameter) as exc_info: - wol.parse_password(None, None, "1.2.3.999") - assert exc_info.value.message == "invalid password 1.2.3.999" - # Test Case 4: Test with a valid 6-byte password - assert wol.parse_password(None, None, str(SAMPLE_TARGET_MAC)) == SAMPLE_TARGET_MAC.to_bytes() - # Test Case 5: Test with an invalid 6-byte password - with pytest.raises(click.BadParameter) as exc_info: - wol.parse_password(None, None, "11:22:33:44:55:999") - assert exc_info.value.message == "invalid password 11:22:33:44:55:999" - # Test Case 6: Test with an invalid password string - with pytest.raises(click.BadParameter) as exc_info: - wol.parse_password(None, None, "INVALID_PASSWORD") - assert exc_info.value.message == "invalid password INVALID_PASSWORD" - - -def test_validate_count_interval(): - # Test Case 1: input valid count and interval - assert wol.validate_count_interval(1, 1000) == (1, 1000) - # Test Case 2: Test with both count and interval are not provided - assert wol.validate_count_interval(None, None) == (1, 0) - # Test Case 3: Test count and interval not provided together - with pytest.raises(click.BadParameter) as exc_info: - wol.validate_count_interval(3, None) - assert exc_info.value.message == "count and interval must be used together" - with pytest.raises(click.BadParameter) as exc_info: - wol.validate_count_interval(None, 1000) - assert exc_info.value.message == "count and interval must be used together" - # Test Case 4: Test with count or interval not in valid range - # This restriction is validated by click.IntRange(), so no need to call the command line function - runner = CliRunner() - result = runner.invoke(wol.wol, [SAMPLE_INTERFACE_ETH0, str(SAMPLE_TARGET_MAC), '-c', '100', '-i', '1000']) - assert 'Invalid value for "-c": 100 is not in the valid range of 1 to 5.' in result.stdout - result = runner.invoke(wol.wol, [SAMPLE_INTERFACE_ETH0, str(SAMPLE_TARGET_MAC), '-c', '3', '-i', '100000']) - assert 'Invalid value for "-i": 100000 is not in the valid range of 0 to 2000.' in result.stdout - - -@patch('netifaces.interfaces', MagicMock(return_value=[SAMPLE_INTERFACE_ETH0])) -@patch('wol.main.is_root', MagicMock(return_value=True)) -@patch('wol.main.get_interface_operstate', MagicMock(return_value="up")) -@patch('wol.main.get_interface_mac', MagicMock(return_value=SAMPLE_ETH0_MAC)) -def test_wol_send_magic_packet_call_count(): - """ - Test the count of send_magic_packet() function call in wol is correct. - """ - runner = CliRunner() - # Test Case 1: Test with only required arguments - # 1.1 Single Target Mac - with patch('wol.main.send_magic_packet') as mock_send_magic_packet: - result = runner.invoke(wol.wol, [SAMPLE_INTERFACE_ETH0, str(SAMPLE_TARGET_MAC)]) - assert result.exit_code == 0 - mock_send_magic_packet.assert_called_once_with(SAMPLE_INTERFACE_ETH0, SAMPLE_TARGET_MAC, SAMPLE_MAGIC_PACKET_UNICAST, 1, 0, False) - # 1.2 Multiple Target Mac - with patch('wol.main.send_magic_packet') as mock_send_magic_packet: - result = runner.invoke(wol.wol, [SAMPLE_INTERFACE_ETH0, ','.join([str(v) for v in SAMPLE_TARGET_MAC_LIST])]) - assert result.exit_code == 0 - assert mock_send_magic_packet.call_count == 2 - # Test Case 2: Test with specified count and interval - # 2.1 Single Target Mac - with patch('wol.main.send_magic_packet') as mock_send_magic_packet: - result = runner.invoke(wol.wol, [SAMPLE_INTERFACE_ETH0, str(SAMPLE_TARGET_MAC), '-c', '5', '-i', '1000']) - assert result.exit_code == 0 - mock_send_magic_packet.assert_called_once_with(SAMPLE_INTERFACE_ETH0, SAMPLE_TARGET_MAC, SAMPLE_MAGIC_PACKET_UNICAST, 5, 1000, False) - # 2.2 Multiple Target Mac - with patch('wol.main.send_magic_packet') as mock_send_magic_packet: - result = runner.invoke(wol.wol, [SAMPLE_INTERFACE_ETH0, ','.join([str(v) for v in SAMPLE_TARGET_MAC_LIST]), '-c', '5', '-i', '1000']) - assert result.exit_code == 0 - assert mock_send_magic_packet.call_count == 2 - - -@patch('netifaces.interfaces', MagicMock(return_value=[SAMPLE_INTERFACE_ETH0])) -@patch('wol.main.is_root', MagicMock(return_value=True)) -@patch('wol.main.get_interface_operstate', MagicMock(return_value="up")) -@patch('wol.main.get_interface_mac', MagicMock(return_value=SAMPLE_ETH0_MAC)) -def test_wol_send_magic_packet_throw_exception(): - """ - Test the exception handling of send_magic_packet() function in wol. - """ - runner = CliRunner() - # Test Case 1: Test with OSError exception (interface flap) - with patch('wol.main.send_magic_packet', MagicMock(side_effect=OSError("[Errno 100] Network is down"))): - result = runner.invoke(wol.wol, [SAMPLE_INTERFACE_ETH0, str(SAMPLE_TARGET_MAC)]) - assert "Exception: [Errno 100] Network is down" in result.stdout - # Test Case 2: Test with other exception - with patch('wol.main.send_magic_packet', MagicMock(side_effect=Exception("Exception message"))): - result = runner.invoke(wol.wol, [SAMPLE_INTERFACE_ETH0, str(SAMPLE_TARGET_MAC)]) - assert "Exception: Exception message" in result.stdout diff --git a/wol/__init__.py b/wol/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/wol/main.py b/wol/main.py deleted file mode 100644 index 3b569a3a4f..0000000000 --- a/wol/main.py +++ /dev/null @@ -1,202 +0,0 @@ -#!/usr/bin/env python3 - -""" -use wol to generate and send Wake-On-LAN (WOL) "Magic Packet" to specific interface - -Usage: wol_click [OPTIONS] INTERFACE TARGET_MAC - - Generate and send Wake-On-LAN (WOL) "Magic Packet" to specific interface - -Options: - -b Use broadcast MAC address instead of target device's MAC - address as Destination MAC Address in Ethernet Frame Header. - [default: False] - -p password An optional 4 or 6 byte password, in ethernet hex format or - quad-dotted decimal [default: ] - -c count For each target MAC address, the count of magic packets to - send. count must between 1 and 5. This param must use with -i. - [default: 1] - -i interval Wait interval milliseconds between sending each magic packet. - interval must between 0 and 2000. This param must use with -c. - [default: 0] - -v Verbose output [default: False] - -h, --help Show this message and exit. - -Examples: - wol Ethernet10 00:11:22:33:44:55 - wol Ethernet10 00:11:22:33:44:55 -b - wol Vlan1000 00:11:22:33:44:55,11:33:55:77:99:bb -p 00:22:44:66:88:aa - wol Vlan1000 00:11:22:33:44:55,11:33:55:77:99:bb -p 192.168.1.1 -c 3 -i 2000 -""" - -import binascii -import click -import copy -import netifaces -import os -import socket -import time - -CONTEXT_SETTINGS = dict(help_option_names=['-h', '--help']) -EPILOG = """\b -Examples: - wol Ethernet10 00:11:22:33:44:55 - wol Ethernet10 00:11:22:33:44:55 -b - wol Vlan1000 00:11:22:33:44:55,11:33:55:77:99:bb -p 00:22:44:66:88:aa - wol Vlan1000 00:11:22:33:44:55,11:33:55:77:99:bb -p 192.168.1.1 -c 3 -i 2000 -""" -ORDINAL_NUMBER = ["0", "1st", "2nd", "3rd", "4th", "5th"] -ETHER_TYPE_WOL = b'\x08\x42' - - -class MacAddress(object): - """ - Class to handle MAC addresses and perform operations on them. - - Attributes: - - address: bytes - """ - - def __init__(self, address: str): - """ - Constructor to instantiate the MacAddress class. - - Parameters: - - address: str - The MAC address in the format '01:23:45:67:89:AB' or '01-23-45-67-89-AB'. - - Raises: - - ValueError: - Throws an error if the provided address is not in the correct format. - """ - try: - self.address = binascii.unhexlify(address.replace(':', '').replace('-', '')) - except binascii.Error: - raise ValueError("invalid MAC address") - if len(self.address) != 6: - raise ValueError("invalid MAC address") - - def __str__(self): - return ":".join(["%02x" % v for v in self.address]) - - def __eq__(self, other): - return self.address == other.address - - def to_bytes(self): - return copy.copy(self.address) - - -BROADCAST_MAC = MacAddress('ff:ff:ff:ff:ff:ff') - - -def is_root(): - return os.geteuid() == 0 - - -def get_interface_operstate(interface): - with open('/sys/class/net/{}/operstate'.format(interface), 'r') as f: - return f.read().strip().lower() - - -def get_interface_mac(interface): - return MacAddress(netifaces.ifaddresses(interface)[netifaces.AF_LINK][0].get('addr')) - - -def build_magic_packet(interface, target_mac, broadcast, password): - dst_mac = BROADCAST_MAC if broadcast else target_mac - src_mac = get_interface_mac(interface) - return dst_mac.to_bytes() + src_mac.to_bytes() + ETHER_TYPE_WOL \ - + b'\xff' * 6 + target_mac.to_bytes() * 16 + password - - -def send_magic_packet(interface, target_mac, pkt, count, interval, verbose): - if verbose: - print("Sending {} magic packet to {} via interface {}".format(count, target_mac, interface)) - sock = socket.socket(socket.AF_PACKET, socket.SOCK_RAW) - sock.bind((interface, 0)) - for i in range(count): - sock.send(pkt) - if verbose: - print("{} magic packet sent to {}".format(ORDINAL_NUMBER[i + 1], target_mac)) - if i + 1 != count: - time.sleep(interval / 1000) - sock.close() - - -def validate_interface(ctx, param, value): - if value not in netifaces.interfaces(): - raise click.BadParameter("invalid SONiC interface name {}".format(value)) - if get_interface_operstate(value) != 'up': - raise click.BadParameter("interface {} is not up".format(value)) - return value - - -def parse_target_mac(ctx, param, value): - mac_list = [] - for mac in value.split(','): - try: - mac_list.append(MacAddress(mac)) - except ValueError: - raise click.BadParameter("invalid MAC address {}".format(mac)) - return mac_list - - -def parse_password(ctx, param, value): - if len(value) == 0: - return b'' # Empty password is valid. - elif len(value) <= 15: # The length of a valid IPv4 address is less or equal to 15. - try: - password = socket.inet_aton(value) - except OSError: - raise click.BadParameter("invalid password format") - else: # The length of a valid MAC address is 17. - try: - password = MacAddress(value).to_bytes() - except ValueError: - raise click.BadParameter("invalid password format") - if len(password) not in [4, 6]: - raise click.BadParameter("password must be 4 or 6 bytes or empty") - return password - - -def validate_count_interval(count, interval): - if count is None and interval is None: - return 1, 0 # By default, count=1 and interval=0. - if count is None or interval is None: - raise click.BadParameter("count and interval must be used together") - # The values are confirmed in valid range by click.IntRange(). - return count, interval - - -@click.command(context_settings=CONTEXT_SETTINGS, epilog=EPILOG) -@click.argument('interface', type=click.STRING, callback=validate_interface) -@click.argument('target_mac', type=click.STRING, callback=parse_target_mac) -@click.option('-b', 'broadcast', is_flag=True, show_default=True, default=False, - help="Use broadcast MAC address instead of target device's MAC address as Destination MAC Address in Ethernet Frame Header.") -@click.option('-p', 'password', type=click.STRING, show_default=True, default='', callback=parse_password, metavar='password', - help='An optional 4 or 6 byte password, in ethernet hex format or quad-dotted decimal') -@click.option('-c', 'count', type=click.IntRange(1, 5), metavar='count', show_default=True, # default=1, - help='For each target MAC address, the count of magic packets to send. count must between 1 and 5. This param must use with -i.') -@click.option('-i', 'interval', type=click.IntRange(0, 2000), metavar='interval', # show_default=True, default=0, - help="Wait interval milliseconds between sending each magic packet. interval must between 0 and 2000. This param must use with -c.") -@click.option('-v', 'verbose', is_flag=True, show_default=True, default=False, - help='Verbose output') -def wol(interface, target_mac, broadcast, password, count, interval, verbose): - """ - Generate and send Wake-On-LAN (WOL) "Magic Packet" to specific interface - """ - count, interval = validate_count_interval(count, interval) - - if not is_root(): - raise click.ClickException("root priviledge is required to run this script") - - for mac in target_mac: - pkt = build_magic_packet(interface, mac, broadcast, password) - try: - send_magic_packet(interface, mac, pkt, count, interval, verbose) - except Exception as e: - raise click.ClickException(f'Exception: {e}') - - -if __name__ == '__main__': - wol()