From 5ad7c7c92597175751df770f1f30dec5ab69c67d Mon Sep 17 00:00:00 2001 From: Lucas Alvares Gomes Date: Thu, 28 Jan 2016 14:10:23 +0000 Subject: [PATCH] Add Wake-On-Lan driver This patch is importing the Wake-On-Lan (WOL) driver and its documentation from ironic to ironic-staging-driver. Since we can't have duplicated entry points in setuptools we had to rename the driver names as following: pxe_wol -> pxe_wol_iscsi agent_wol -> pxe_wol_agent fake_wol -> fake_wol_fake This patch is using the "__" template to name the drivers consistently. Change-Id: I2b051494fdba7bf6ca30d8f7bb406511bf7d4d76 --- doc/source/drivers.rst | 10 + doc/source/drivers/wol.rst | 128 ++++++++++++ doc/source/index.rst | 1 + ironic_staging_drivers/common/__init__.py | 0 .../unit/test_foo.py => common/exception.py} | 11 +- ironic_staging_drivers/common/i18n.py | 31 +++ ironic_staging_drivers/common/utils.py | 40 ++++ ironic_staging_drivers/tests/base.py | 23 -- .../tests/unit/wol/__init__.py | 0 .../tests/unit/wol/test_power.py | 196 ++++++++++++++++++ ironic_staging_drivers/wol/__init__.py | 67 ++++++ ironic_staging_drivers/wol/power.py | 183 ++++++++++++++++ .../add-wol-driver-9d173b2ffc0dae0f.yaml | 3 + requirements.txt | 2 + setup.cfg | 3 + test-requirements.txt | 1 + 16 files changed, 671 insertions(+), 28 deletions(-) create mode 100644 doc/source/drivers.rst create mode 100644 doc/source/drivers/wol.rst create mode 100644 ironic_staging_drivers/common/__init__.py rename ironic_staging_drivers/{tests/unit/test_foo.py => common/exception.py} (78%) create mode 100644 ironic_staging_drivers/common/i18n.py create mode 100644 ironic_staging_drivers/common/utils.py delete mode 100644 ironic_staging_drivers/tests/base.py create mode 100644 ironic_staging_drivers/tests/unit/wol/__init__.py create mode 100644 ironic_staging_drivers/tests/unit/wol/test_power.py create mode 100644 ironic_staging_drivers/wol/__init__.py create mode 100644 ironic_staging_drivers/wol/power.py create mode 100644 releasenotes/notes/add-wol-driver-9d173b2ffc0dae0f.yaml diff --git a/doc/source/drivers.rst b/doc/source/drivers.rst new file mode 100644 index 0000000..4beeaff --- /dev/null +++ b/doc/source/drivers.rst @@ -0,0 +1,10 @@ +.. _drivers: + +================= +Available drivers +================= + +.. toctree:: + :maxdepth: 1 + + drivers/wol diff --git a/doc/source/drivers/wol.rst b/doc/source/drivers/wol.rst new file mode 100644 index 0000000..aa1ad8e --- /dev/null +++ b/doc/source/drivers/wol.rst @@ -0,0 +1,128 @@ +.. _WOL: + +================== +Wake-On-Lan driver +================== + +Overview +======== + +Wake-On-Lan is a standard that allows a computer to be powered on by a +network message. This is widely available and doesn't require any fancy +hardware to work with [1]_. + +The Wake-On-Lan driver is a **testing** driver not meant for +production. And useful for users that wants to try Ironic with real +bare metal instead of virtual machines. + +It's important to note that Wake-On-Lan is only capable of powering on +the machine. When power off is called the driver won't take any action +and will just log a message, the power off require manual intervention +to be performed. + +Also, since Wake-On-Lan does not offer any means to determine the current +power state of the machine, the driver relies on the power state set in +the Ironic database. Any calls to the API to get the power state of the +node will return the value from the Ironic's database. + + +Drivers +======= + +pxe_wol_iscsi +^^^^^^^^^^^^^ + +Overview +~~~~~~~~ + +The ``pxe_wol_iscsi`` driver uses the Wake-On-Lan technology to control the +power state, PXE/iPXE technology for booting and the iSCSI methodology +for deploying the node. + +Requirements +~~~~~~~~~~~~ + +* Wake-On-Lan should be enabled in the BIOS + +Configuring and Enabling the driver +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +1. Add ``pxe_wol_iscsi`` to the list of ``enabled_drivers`` in + */etc/ironic/ironic.conf*. For example:: + + [DEFAULT] + ... + enabled_drivers = pxe_ipmitool,pxe_wol_iscsi + +2. Restart the Ironic conductor service:: + + service ironic-conductor restart + +Registering a node with the Wake-On-Lan driver +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Nodes configured for Wake-On-Lan driver should have the ``driver`` +property set to ``pxe_wol_iscsi``. + +The node should have at least one port registered with it because the +Wake-On-Lan driver will use the MAC address of the ports to create the +magic packet [2]_. + +The following configuration values are optional and can be added to the +node's ``driver_info`` as needed to match the network configuration: + +- ``wol_host``: The broadcast IP address; defaults to + **255.255.255.255**. +- ``wol_port``: The destination port; defaults to **9**. + +.. note:: + Say the ``ironic-conductor`` is connected to more than one network and + the node you are trying to wake up is in the ``192.0.2.0/24`` range. The + ``wol_host`` configuration should be set to **192.0.2.255** (the + broadcast IP) so the packets will get routed correctly. + +The following sequence of commands can be used to enroll a node with +the Wake-On-Lan driver. + +1. Create node:: + + ironic node-create -d pxe_wol_iscsi [-i wol_host= [ -i + wol_port=]] + +The above command ``ironic node-create`` will return UUID of the node, +which is the value of *$NODE* in the following command. + +2. Associate port with the node created:: + + ironic port-create -n $NODE -a + + +pxe_wol_agent +^^^^^^^^^^^^^ + +Overview +~~~~~~~~ + +The ``pxe_wol_agent`` driver uses the Wake-On-Lan technology to control +the power state, PXE/iPXE technology for booting and the Ironic Python +Agent for deploying the node. + +Additional requirements +~~~~~~~~~~~~~~~~~~~~~~~ + +* Boot device order should be set to "PXE, DISK" in the BIOS setup + +* BIOS must try next boot device if PXE boot failed + +* Cleaning should be disabled, see :ref:`cleaning` + +* Node should be powered off before start of deploy + +Configuration steps are the same as for ``pxe_wol_iscsi`` driver, replace +"pxe_wol_iscsi" with "pxe_wol_agent". + + +References +========== +.. [1] Wake-On-Lan - https://en.wikipedia.org/wiki/Wake-on-LAN +.. [2] Magic packet - https://en.wikipedia.org/wiki/Wake-on-LAN#Sending_the_magic_packet diff --git a/doc/source/index.rst b/doc/source/index.rst index 83437d0..fc443f1 100644 --- a/doc/source/index.rst +++ b/doc/source/index.rst @@ -7,6 +7,7 @@ Contents: :maxdepth: 2 README + drivers Indices and tables ================== diff --git a/ironic_staging_drivers/common/__init__.py b/ironic_staging_drivers/common/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/ironic_staging_drivers/tests/unit/test_foo.py b/ironic_staging_drivers/common/exception.py similarity index 78% rename from ironic_staging_drivers/tests/unit/test_foo.py rename to ironic_staging_drivers/common/exception.py index 8c38c7e..a5b7f28 100644 --- a/ironic_staging_drivers/tests/unit/test_foo.py +++ b/ironic_staging_drivers/common/exception.py @@ -1,3 +1,6 @@ +# Copyright 2016 Red Hat, Inc. +# All Rights Reserved. +# # 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 @@ -10,10 +13,8 @@ # License for the specific language governing permissions and limitations # under the License. -from ironic_staging_drivers.tests import base +from ironic.common import exception -class FooTestCase(base.TestCase): - - def test_foo(self): - pass +class WOLOperationError(exception.IronicException): + pass diff --git a/ironic_staging_drivers/common/i18n.py b/ironic_staging_drivers/common/i18n.py new file mode 100644 index 0000000..64f7017 --- /dev/null +++ b/ironic_staging_drivers/common/i18n.py @@ -0,0 +1,31 @@ +# Copyright 2016 Red Hat, Inc. +# All Rights Reserved. +# +# 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. + +import oslo_i18n as i18n + +_translators = i18n.TranslatorFactory(domain='ironic-staging-drivers') + +# The primary translation function using the well-known name "_" +_ = _translators.primary + +# Translators for log levels. +# +# The abbreviated names are meant to reflect the usual use of a short +# name like '_'. The "L" is for "log" and the other letter comes from +# the level. +_LI = _translators.log_info +_LW = _translators.log_warning +_LE = _translators.log_error +_LC = _translators.log_critical diff --git a/ironic_staging_drivers/common/utils.py b/ironic_staging_drivers/common/utils.py new file mode 100644 index 0000000..118ee29 --- /dev/null +++ b/ironic_staging_drivers/common/utils.py @@ -0,0 +1,40 @@ +# Copyright 2016 Red Hat, Inc. +# All Rights Reserved. +# +# 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. + +from ironic.common import exception as ironic_exception + +from ironic_staging_drivers.common.i18n import _ + + +def validate_network_port(port, port_name="Port"): + """Validates the given port. + + :param port: TCP/UDP port. + :param port_name: Name of the port. + :returns: An integer port number. + :raises: InvalidParameterValue, if the port is invalid. + """ + try: + port = int(port) + except ValueError: + raise ironic_exception.InvalidParameterValue(_( + '%(port_name)s "%(port)s" is not a valid integer.') % + {'port_name': port_name, 'port': port}) + if port < 1 or port > 65535: + raise ironic_exception.InvalidParameterValue(_( + '%(port_name)s "%(port)s" is out of range. Valid port ' + 'numbers must be between 1 and 65535.') % + {'port_name': port_name, 'port': port}) + return port diff --git a/ironic_staging_drivers/tests/base.py b/ironic_staging_drivers/tests/base.py deleted file mode 100644 index 1c30cdb..0000000 --- a/ironic_staging_drivers/tests/base.py +++ /dev/null @@ -1,23 +0,0 @@ -# -*- coding: utf-8 -*- - -# Copyright 2010-2011 OpenStack Foundation -# Copyright (c) 2013 Hewlett-Packard Development Company, L.P. -# -# 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. - -from oslotest import base - - -class TestCase(base.BaseTestCase): - - """Test case base class for all unit tests.""" diff --git a/ironic_staging_drivers/tests/unit/wol/__init__.py b/ironic_staging_drivers/tests/unit/wol/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/ironic_staging_drivers/tests/unit/wol/test_power.py b/ironic_staging_drivers/tests/unit/wol/test_power.py new file mode 100644 index 0000000..d226160 --- /dev/null +++ b/ironic_staging_drivers/tests/unit/wol/test_power.py @@ -0,0 +1,196 @@ +# Copyright 2016 Red Hat, Inc. +# All Rights Reserved. +# +# 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. + +"""Test class for Wake-On-Lan driver module.""" + +import socket +import time + +from ironic.common import driver_factory +from ironic.common import exception as ironic_exception +from ironic.common import states +from ironic.conductor import task_manager +from ironic.tests.unit.conductor import mgr_utils +from ironic.tests.unit.db import base as db_base +from ironic.tests.unit.objects import utils as obj_utils +import mock +from oslo_utils import uuidutils + +from ironic_staging_drivers.common import exception +from ironic_staging_drivers.wol import power as wol_power + + +@mock.patch.object(time, 'sleep', lambda *_: None) +class WakeOnLanPrivateMethodTestCase(db_base.DbTestCase): + + def setUp(self): + super(WakeOnLanPrivateMethodTestCase, self).setUp() + mgr_utils.mock_the_extension_manager(driver='fake_wol_fake') + self.driver = driver_factory.get_driver('fake_wol_fake') + self.node = obj_utils.create_test_node(self.context, + driver='fake_wol_fake') + self.port = obj_utils.create_test_port(self.context, + node_id=self.node.id) + + def test__parse_parameters(self): + with task_manager.acquire( + self.context, self.node.uuid, shared=True) as task: + params = wol_power._parse_parameters(task) + self.assertEqual('255.255.255.255', params['host']) + self.assertEqual(9, params['port']) + + def test__parse_parameters_non_default_params(self): + with task_manager.acquire( + self.context, self.node.uuid, shared=True) as task: + task.node.driver_info = {'wol_host': '1.2.3.4', + 'wol_port': 7} + params = wol_power._parse_parameters(task) + self.assertEqual('1.2.3.4', params['host']) + self.assertEqual(7, params['port']) + + def test__parse_parameters_no_ports_fail(self): + node = obj_utils.create_test_node( + self.context, + uuid=uuidutils.generate_uuid(), + driver='fake_wol_fake') + with task_manager.acquire( + self.context, node.uuid, shared=True) as task: + self.assertRaises(ironic_exception.InvalidParameterValue, + wol_power._parse_parameters, task) + + @mock.patch.object(socket, 'socket', autospec=True, spec_set=True) + def test_send_magic_packets(self, mock_socket): + fake_socket = mock.Mock(spec=socket, spec_set=True) + mock_socket.return_value = fake_socket() + obj_utils.create_test_port(self.context, + uuid=uuidutils.generate_uuid(), + address='aa:bb:cc:dd:ee:ff', + node_id=self.node.id) + with task_manager.acquire( + self.context, self.node.uuid, shared=True) as task: + wol_power._send_magic_packets(task, '255.255.255.255', 9) + + expected_calls = [ + mock.call(), + mock.call().setsockopt(socket.SOL_SOCKET, + socket.SO_BROADCAST, 1), + mock.call().sendto(mock.ANY, ('255.255.255.255', 9)), + mock.call().sendto(mock.ANY, ('255.255.255.255', 9)), + mock.call().close()] + + fake_socket.assert_has_calls(expected_calls) + self.assertEqual(1, mock_socket.call_count) + + @mock.patch.object(socket, 'socket', autospec=True, spec_set=True) + def test_send_magic_packets_network_sendto_error(self, mock_socket): + fake_socket = mock.Mock(spec=socket, spec_set=True) + fake_socket.return_value.sendto.side_effect = socket.error('boom') + mock_socket.return_value = fake_socket() + with task_manager.acquire( + self.context, self.node.uuid, shared=True) as task: + self.assertRaises(exception.WOLOperationError, + wol_power._send_magic_packets, + task, '255.255.255.255', 9) + self.assertEqual(1, mock_socket.call_count) + # assert sendt0() was invoked + fake_socket.return_value.sendto.assert_called_once_with( + mock.ANY, ('255.255.255.255', 9)) + + @mock.patch.object(socket, 'socket', autospec=True, spec_set=True) + def test_magic_packet_format(self, mock_socket): + fake_socket = mock.Mock(spec=socket, spec_set=True) + mock_socket.return_value = fake_socket() + with task_manager.acquire( + self.context, self.node.uuid, shared=True) as task: + wol_power._send_magic_packets(task, '255.255.255.255', 9) + + expected_packet = (b'\xff\xff\xff\xff\xff\xffRT\x00\xcf-1RT\x00' + b'\xcf-1RT\x00\xcf-1RT\x00\xcf-1RT\x00\xcf-1RT' + b'\x00\xcf-1RT\x00\xcf-1RT\x00\xcf-1RT\x00' + b'\xcf-1RT\x00\xcf-1RT\x00\xcf-1RT\x00\xcf-1RT' + b'\x00\xcf-1RT\x00\xcf-1RT\x00\xcf-1RT\x00' + b'\xcf-1') + mock_socket.return_value.sendto.assert_called_once_with( + expected_packet, ('255.255.255.255', 9)) + + +@mock.patch.object(time, 'sleep', lambda *_: None) +class WakeOnLanDriverTestCase(db_base.DbTestCase): + + def setUp(self): + super(WakeOnLanDriverTestCase, self).setUp() + mgr_utils.mock_the_extension_manager(driver='fake_wol_fake') + self.driver = driver_factory.get_driver('fake_wol_fake') + self.node = obj_utils.create_test_node(self.context, + driver='fake_wol_fake') + self.port = obj_utils.create_test_port(self.context, + node_id=self.node.id) + + def test_get_properties(self): + expected = wol_power.COMMON_PROPERTIES + with task_manager.acquire( + self.context, self.node.uuid, shared=True) as task: + self.assertEqual(expected, task.driver.get_properties()) + + def test_get_power_state(self): + with task_manager.acquire( + self.context, self.node.uuid, shared=True) as task: + task.node.power_state = states.POWER_ON + pstate = task.driver.power.get_power_state(task) + self.assertEqual(states.POWER_ON, pstate) + + def test_get_power_state_nostate(self): + with task_manager.acquire( + self.context, self.node.uuid, shared=True) as task: + task.node.power_state = states.NOSTATE + pstate = task.driver.power.get_power_state(task) + self.assertEqual(states.POWER_OFF, pstate) + + @mock.patch.object(wol_power, '_send_magic_packets', autospec=True, + spec_set=True) + def test_set_power_state_power_on(self, mock_magic): + with task_manager.acquire(self.context, self.node.uuid) as task: + task.driver.power.set_power_state(task, states.POWER_ON) + mock_magic.assert_called_once_with(task, '255.255.255.255', 9) + + @mock.patch.object(wol_power.LOG, 'info', autospec=True, spec_set=True) + @mock.patch.object(wol_power, '_send_magic_packets', autospec=True, + spec_set=True) + def test_set_power_state_power_off(self, mock_magic, mock_log): + with task_manager.acquire(self.context, self.node.uuid) as task: + task.driver.power.set_power_state(task, states.POWER_OFF) + mock_log.assert_called_once_with(mock.ANY, self.node.uuid) + # assert magic packets weren't sent + self.assertFalse(mock_magic.called) + + @mock.patch.object(wol_power, '_send_magic_packets', autospec=True, + spec_set=True) + def test_set_power_state_power_fail(self, mock_magic): + with task_manager.acquire(self.context, self.node.uuid) as task: + self.assertRaises(ironic_exception.InvalidParameterValue, + task.driver.power.set_power_state, + task, 'wrong-state') + # assert magic packets weren't sent + self.assertFalse(mock_magic.called) + + @mock.patch.object(wol_power.LOG, 'info', autospec=True, spec_set=True) + @mock.patch.object(wol_power.WakeOnLanPower, 'set_power_state', + autospec=True, spec_set=True) + def test_reboot(self, mock_power, mock_log): + with task_manager.acquire(self.context, self.node.uuid) as task: + task.driver.power.reboot(task) + mock_log.assert_called_once_with(mock.ANY, self.node.uuid) + mock_power.assert_called_once_with(task.driver.power, task, + states.POWER_ON) diff --git a/ironic_staging_drivers/wol/__init__.py b/ironic_staging_drivers/wol/__init__.py new file mode 100644 index 0000000..202338d --- /dev/null +++ b/ironic_staging_drivers/wol/__init__.py @@ -0,0 +1,67 @@ +# Copyright 2016 Red Hat, Inc. +# All Rights Reserved. +# +# 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. + +from ironic.drivers import base +from ironic.drivers.modules import agent +from ironic.drivers.modules import fake +from ironic.drivers.modules import iscsi_deploy +from ironic.drivers.modules import pxe + +from ironic_staging_drivers.wol import power as wol_power + + +class FakeWakeOnLanFakeDriver(base.BaseDriver): + """Fake Wake-On-Lan driver.""" + + def __init__(self): + self.boot = fake.FakeBoot() + self.power = wol_power.WakeOnLanPower() + self.deploy = fake.FakeDeploy() + + +class PXEWakeOnLanISCSIDriver(base.BaseDriver): + """PXE + WakeOnLan + iSCSI driver. + + This driver implements the `core` functionality, combining + :class:`ironic.drivers.modules.pxe.PXEBoot` for boot and + :class:`ironic_staging_drivers.wol.power.WakeOnLanPower` for power + and :class:`ironic.drivers.modules.iscsi_deploy.ISCSIDeploy` for + image deployment. Implementations are in those respective classes; + this class is merely the glue between them. + + """ + def __init__(self): + self.boot = pxe.PXEBoot() + self.power = wol_power.WakeOnLanPower() + self.deploy = iscsi_deploy.ISCSIDeploy() + self.vendor = iscsi_deploy.VendorPassthru() + + +class PXEWakeOnLanAgentDriver(base.BaseDriver): + """PXE + WakeOnLan + Agent driver. + + This driver implements the `core` functionality, combining + :class:`ironic.drivers.modules.pxe.PXEBoot` for boot and + :class:`ironic_staging_drivers.wol.power.WakeOnLanPower` for power + and :class:`ironic.drivers.modules.agent.AgentDeploy` for + image deployment. Implementations are in those respective classes; + this class is merely the glue between them. + + """ + def __init__(self): + self.boot = pxe.PXEBoot() + self.power = wol_power.WakeOnLanPower() + self.deploy = agent.AgentDeploy() + self.vendor = agent.AgentVendorInterface() diff --git a/ironic_staging_drivers/wol/power.py b/ironic_staging_drivers/wol/power.py new file mode 100644 index 0000000..95941f1 --- /dev/null +++ b/ironic_staging_drivers/wol/power.py @@ -0,0 +1,183 @@ +# Copyright 2016 Red Hat, Inc. +# All Rights Reserved. +# +# 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. + +""" +Ironic Wake-On-Lan power manager. +""" + +import contextlib +import socket +import time + +from ironic.common import exception as ironic_exception +from ironic.common import states +from ironic.conductor import task_manager +from ironic.drivers import base +from oslo_log import log + +from ironic_staging_drivers.common import exception +from ironic_staging_drivers.common.i18n import _ +from ironic_staging_drivers.common.i18n import _LI +from ironic_staging_drivers.common import utils + + +LOG = log.getLogger(__name__) + +REQUIRED_PROPERTIES = {} +OPTIONAL_PROPERTIES = { + 'wol_host': _('Broadcast IP address; defaults to ' + '255.255.255.255. Optional.'), + 'wol_port': _("Destination port; defaults to 9. Optional."), +} +COMMON_PROPERTIES = REQUIRED_PROPERTIES.copy() +COMMON_PROPERTIES.update(OPTIONAL_PROPERTIES) + + +def _send_magic_packets(task, dest_host, dest_port): + """Create and send magic packets. + + Creates and sends a magic packet for each MAC address registered in + the Node. + + :param task: a TaskManager instance containing the node to act on. + :param dest_host: The broadcast to this IP address. + :param dest_port: The destination port. + :raises: WOLOperationError if an error occur when connecting to the + host or sending the magic packets + + """ + s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + s.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1) + with contextlib.closing(s) as sock: + for port in task.ports: + address = port.address.replace(':', '') + + # TODO(lucasagomes): Implement sending the magic packets with + # SecureON password feature. If your NIC is capable of, you can + # set the password of your SecureON using the ethtool utility. + data = 'FFFFFFFFFFFF' + (address * 16) + packet = bytearray.fromhex(data) + + try: + sock.sendto(packet, (dest_host, dest_port)) + except socket.error as e: + msg = (_("Failed to send Wake-On-Lan magic packets to " + "node %(node)s port %(port)s. Error: %(error)s") % + {'node': task.node.uuid, 'port': port.address, + 'error': e}) + LOG.exception(msg) + raise exception.WOLOperationError(msg) + + # let's not flood the network with broadcast packets + time.sleep(0.5) + + +def _parse_parameters(task): + driver_info = task.node.driver_info + host = driver_info.get('wol_host', '255.255.255.255') + port = driver_info.get('wol_port', 9) + port = utils.validate_network_port(port, 'wol_port') + + if len(task.ports) < 1: + raise ironic_exception.MissingParameterValue(_( + 'Wake-On-Lan needs at least one port resource to be ' + 'registered in the node')) + + return {'host': host, 'port': port} + + +class WakeOnLanPower(base.PowerInterface): + """Wake-On-Lan Driver for Ironic + + This PowerManager class provides a mechanism for controlling power + state via Wake-On-Lan. + + """ + + def get_properties(self): + return COMMON_PROPERTIES + + def validate(self, task): + """Validate driver. + + :param task: a TaskManager instance containing the node to act on. + :raises: InvalidParameterValue if parameters are invalid. + :raises: MissingParameterValue if required parameters are missing. + + """ + _parse_parameters(task) + + def get_power_state(self, task): + """Not supported. Get the current power state of the task's node. + + This operation is not supported by the Wake-On-Lan driver. So + value returned will be from the database and may not reflect + the actual state of the system. + + :returns: POWER_OFF if power state is not set otherwise return + the node's power_state value from the database. + + """ + pstate = task.node.power_state + return states.POWER_OFF if pstate is states.NOSTATE else pstate + + @task_manager.require_exclusive_lock + def set_power_state(self, task, pstate): + """Wakes the task's node on power on. Powering off is not supported. + + Wakes the task's node on. Wake-On-Lan does not support powering + the task's node off so, just log it. + + :param task: a TaskManager instance containing the node to act on. + :param pstate: The desired power state, one of ironic.common.states + POWER_ON, POWER_OFF. + :raises: InvalidParameterValue if parameters are invalid. + :raises: MissingParameterValue if required parameters are missing. + :raises: WOLOperationError if an error occur when sending the + magic packets + + """ + node = task.node + params = _parse_parameters(task) + if pstate == states.POWER_ON: + _send_magic_packets(task, params['host'], params['port']) + elif pstate == states.POWER_OFF: + LOG.info(_LI('Power off called for node %s. Wake-On-Lan does not ' + 'support this operation. Manual intervention ' + 'required to perform this action.'), node.uuid) + else: + raise ironic_exception.InvalidParameterValue(_( + "set_power_state called for Node %(node)s with invalid " + "power state %(pstate)s.") % {'node': node.uuid, + 'pstate': pstate}) + + @task_manager.require_exclusive_lock + def reboot(self, task): + """Not supported. Cycles the power to the task's node. + + This operation is not fully supported by the Wake-On-Lan + driver. So this method will just try to power the task's node on. + + :param task: a TaskManager instance containing the node to act on. + :raises: InvalidParameterValue if parameters are invalid. + :raises: MissingParameterValue if required parameters are missing. + :raises: WOLOperationError if an error occur when sending the + magic packets + + """ + LOG.info(_LI('Reboot called for node %s. Wake-On-Lan does ' + 'not fully support this operation. Trying to ' + 'power on the node.'), task.node.uuid) + self.set_power_state(task, states.POWER_ON) diff --git a/releasenotes/notes/add-wol-driver-9d173b2ffc0dae0f.yaml b/releasenotes/notes/add-wol-driver-9d173b2ffc0dae0f.yaml new file mode 100644 index 0000000..18f4484 --- /dev/null +++ b/releasenotes/notes/add-wol-driver-9d173b2ffc0dae0f.yaml @@ -0,0 +1,3 @@ +--- +features: + - Add the Wake-On-Lan (WOL) driver diff --git a/requirements.txt b/requirements.txt index 95d0fe8..0cd7961 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,3 +3,5 @@ # process, which may cause wedges in the gate later. pbr>=1.6 # Apache-2.0 +oslo.i18n>=2.1.0 # Apache-2.0 +oslo.utils>=3.5.0 # Apache-2.0 diff --git a/setup.cfg b/setup.cfg index ef51729..f718dae 100644 --- a/setup.cfg +++ b/setup.cfg @@ -24,6 +24,9 @@ packages = [entry_points] ironic.drivers = + fake_wol_fake = ironic_staging_drivers.wol:FakeWakeOnLanFakeDriver + pxe_wol_iscsi = ironic_staging_drivers.wol:PXEWakeOnLanISCSIDriver + pxe_wol_agent = ironic_staging_drivers.wol:PXEWakeOnLanAgentDriver [build_sphinx] source-dir = doc/source diff --git a/test-requirements.txt b/test-requirements.txt index 6a6a4ee..5974613 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -15,3 +15,4 @@ testscenarios>=0.4 # Apache-2.0/BSD testtools>=1.4.0 # MIT os-testr>=0.4.1 # Apache-2.0 reno>=0.1.1 # Apache2 +mock>=1.2 # BSD