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 "<boot>_<power>_<deploy>" template to name the drivers consistently. Change-Id: I2b051494fdba7bf6ca30d8f7bb406511bf7d4d76
This commit is contained in:
parent
986d47569d
commit
5ad7c7c925
10
doc/source/drivers.rst
Normal file
10
doc/source/drivers.rst
Normal file
@ -0,0 +1,10 @@
|
||||
.. _drivers:
|
||||
|
||||
=================
|
||||
Available drivers
|
||||
=================
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 1
|
||||
|
||||
drivers/wol
|
128
doc/source/drivers/wol.rst
Normal file
128
doc/source/drivers/wol.rst
Normal file
@ -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=<broadcast ip> [ -i
|
||||
wol_port=<destination 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 <MAC address>
|
||||
|
||||
|
||||
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
|
@ -7,6 +7,7 @@ Contents:
|
||||
:maxdepth: 2
|
||||
|
||||
README
|
||||
drivers
|
||||
|
||||
Indices and tables
|
||||
==================
|
||||
|
0
ironic_staging_drivers/common/__init__.py
Normal file
0
ironic_staging_drivers/common/__init__.py
Normal file
@ -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
|
31
ironic_staging_drivers/common/i18n.py
Normal file
31
ironic_staging_drivers/common/i18n.py
Normal file
@ -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
|
40
ironic_staging_drivers/common/utils.py
Normal file
40
ironic_staging_drivers/common/utils.py
Normal file
@ -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
|
@ -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."""
|
0
ironic_staging_drivers/tests/unit/wol/__init__.py
Normal file
0
ironic_staging_drivers/tests/unit/wol/__init__.py
Normal file
196
ironic_staging_drivers/tests/unit/wol/test_power.py
Normal file
196
ironic_staging_drivers/tests/unit/wol/test_power.py
Normal file
@ -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)
|
67
ironic_staging_drivers/wol/__init__.py
Normal file
67
ironic_staging_drivers/wol/__init__.py
Normal file
@ -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()
|
183
ironic_staging_drivers/wol/power.py
Normal file
183
ironic_staging_drivers/wol/power.py
Normal file
@ -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)
|
3
releasenotes/notes/add-wol-driver-9d173b2ffc0dae0f.yaml
Normal file
3
releasenotes/notes/add-wol-driver-9d173b2ffc0dae0f.yaml
Normal file
@ -0,0 +1,3 @@
|
||||
---
|
||||
features:
|
||||
- Add the Wake-On-Lan (WOL) driver
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
Loading…
Reference in New Issue
Block a user