Port iBoot PDU driver from Nova

Ports a custom power driver from Nova to Ironic so that iBoot capable
devices can be used.

Co-Authored-By: Dan Prince <dprince@redhat.com>
Closes-Bug: #1226042
Change-Id: Ibec34a7e0a69bb26d3e2a21b1f2d1a7ce3514347
This commit is contained in:
Lucas Alvares Gomes 2013-10-10 17:20:02 +01:00
parent 9fe09164cb
commit 7f8fed748d
7 changed files with 488 additions and 0 deletions

View File

@ -21,6 +21,7 @@ from ironic.common import exception
from ironic.drivers import base
from ironic.drivers.modules import agent
from ironic.drivers.modules import fake
from ironic.drivers.modules import iboot
from ironic.drivers.modules import ipminative
from ironic.drivers.modules import ipmitool
from ironic.drivers.modules import pxe
@ -105,3 +106,11 @@ class FakeAgentDriver(base.BaseDriver):
self.power = fake.FakePower()
self.deploy = agent.AgentDeploy()
self.vendor = agent.AgentVendorInterface()
class FakeIBootDriver(base.BaseDriver):
"""Example implementation of a Driver."""
def __init__(self):
self.power = iboot.IBootPower()
self.deploy = fake.FakeDeploy()

View File

@ -0,0 +1,185 @@
# -*- coding: utf-8 -*-
#
# Copyright 2014 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 iBoot PDU power manager.
"""
from ironic.common import exception
from ironic.common import states
from ironic.conductor import task_manager
from ironic.drivers import base
from ironic.openstack.common import importutils
from ironic.openstack.common import log as logging
iboot = importutils.try_import('iboot')
LOG = logging.getLogger(__name__)
REQUIRED_PROPERTIES = {
'iboot_address': _("IP address of the node. Required."),
'iboot_username': _("username. Required."),
'iboot_password': _("password. Required."),
}
OPTIONAL_PROPERTIES = {
'iboot_relay_id': _("iBoot PDU relay id; default is 1. Optional."),
'iboot_port': _("iBoot PDU port; default is 9100. Optional."),
}
COMMON_PROPERTIES = REQUIRED_PROPERTIES.copy()
COMMON_PROPERTIES.update(OPTIONAL_PROPERTIES)
def _parse_driver_info(node):
info = node.driver_info or {}
address = info.get('iboot_address', None)
username = info.get('iboot_username', None)
password = info.get('iboot_password', None)
if not address or not username or not password:
raise exception.InvalidParameterValue(_(
"iBoot credentials not supplied to iBoot PDU driver."))
relay_id = info.get('iboot_relay_id', 1)
try:
relay_id = int(relay_id)
except ValueError:
raise exception.InvalidParameterValue(_(
"iBoot PDU relay id must be an integer."))
port = info.get('iboot_port', 9100)
try:
port = int(port)
except ValueError:
raise exception.InvalidParameterValue(_(
"iBoot PDU port must be an integer."))
return {
'address': address,
'username': username,
'password': password,
'port': port,
'relay_id': relay_id,
'uuid': node.uuid,
}
def _get_connection(driver_info):
# NOTE: python-iboot wants username and password as strings (not unicode)
return iboot.iBootInterface(driver_info['address'],
str(driver_info['username']),
str(driver_info['password']),
port=driver_info['port'],
num_relays=driver_info['relay_id'])
def _switch(driver_info, enabled):
conn = _get_connection(driver_info)
relay_id = driver_info['relay_id']
return conn.switch(relay_id, enabled)
def _power_status(driver_info):
conn = _get_connection(driver_info)
relay_id = driver_info['relay_id']
status = conn.get_relays()[relay_id - 1]
if status:
return states.POWER_ON
else:
return states.POWER_OFF
class IBootPower(base.PowerInterface):
"""iBoot PDU Power Driver for Ironic
This PowerManager class provides a mechanism for controlling power state
via an iBoot capable device.
Requires installation of python-iboot:
https://github.com/darkip/python-iboot
"""
def get_properties(self):
return COMMON_PROPERTIES
def validate(self, task):
"""Validate driver_info for iboot driver.
:param task: a TaskManager instance containing the node to act on.
:raises: InvalidParameterValue if required iboot parameters
are missing.
"""
_parse_driver_info(task.node)
def get_power_state(self, task):
"""Get the current power state of the task's node.
:param task: a TaskManager instance containing the node to act on.
:returns: one of ironic.common.states POWER_OFF, POWER_ON or ERROR.
:raises: InvalidParameterValue if required iboot parameters
are missing.
"""
driver_info = _parse_driver_info(task.node)
return _power_status(driver_info)
@task_manager.require_exclusive_lock
def set_power_state(self, task, pstate):
"""Turn the power on or off.
: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 required iboot parameters are
missing or if an invalid power state was specified.
:raises: PowerStateFailure if the power couldn't be set to pstate.
"""
driver_info = _parse_driver_info(task.node)
if pstate == states.POWER_ON:
_switch(driver_info, True)
elif pstate == states.POWER_OFF:
_switch(driver_info, False)
else:
raise exception.InvalidParameterValue(_(
"set_power_state called with invalid "
"power state %s.") % pstate)
state = _power_status(driver_info)
if state != pstate:
raise exception.PowerStateFailure(pstate=pstate)
@task_manager.require_exclusive_lock
def reboot(self, task):
"""Cycles the power to the task's node.
:param task: a TaskManager instance containing the node to act on.
:raises: InvalidParameterValue if required iboot parameters
are missing.
:raises: PowerStateFailure if the final state of the node is not
POWER_ON.
"""
driver_info = _parse_driver_info(task.node)
_switch(driver_info, False)
_switch(driver_info, True)
state = _power_status(driver_info)
if state != states.POWER_ON:
raise exception.PowerStateFailure(pstate=states.POWER_ON)

View File

@ -19,6 +19,7 @@ PXE Driver and supporting meta-classes.
from ironic.common import exception
from ironic.drivers import base
from ironic.drivers.modules import iboot
from ironic.drivers.modules import ipminative
from ironic.drivers.modules import ipmitool
from ironic.drivers.modules import pxe
@ -111,3 +112,24 @@ class PXEAndSeaMicroDriver(base.BaseDriver):
'attach_volume': self.seamicro_vendor,
'set_node_vlan_id': self.seamicro_vendor}
self.vendor = utils.MixinVendorInterface(self.mapping)
class PXEAndIBootDriver(base.BaseDriver):
"""PXE + IBoot PDU driver.
This driver implements the `core` functionality, combining
:class:ironic.drivers.modules.iboot.IBootPower for power
on/off and reboot with
:class:ironic.driver.modules.pxe.PXE for image deployment.
Implementations are in those respective classes;
this class is merely the glue between them.
"""
def __init__(self):
if not importutils.try_import('iboot'):
raise exception.DriverLoadError(
driver=self.__class__.__name__,
reason="Unable to import iboot library")
self.power = iboot.IBootPower()
self.deploy = pxe.PXEDeploy()
self.vendor = pxe.VendorPassthru()

View File

@ -94,6 +94,14 @@ def get_test_agent_driver_info():
}
def get_test_iboot_info():
return {
"iboot_address": "1.2.3.4",
"iboot_username": "admin",
"iboot_password": "fake",
}
def get_test_node(**kw):
properties = {
"cpu_arch": "x86_64",

View File

@ -0,0 +1,248 @@
# -*- coding: utf-8 -*-
#
# Copyright 2014 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 iBoot PDU driver module."""
import mock
from ironic.common import driver_factory
from ironic.common import exception
from ironic.common import states
from ironic.conductor import task_manager
from ironic.db import api as dbapi
from ironic.drivers.modules import iboot
from ironic.openstack.common import context
from ironic.tests import base
from ironic.tests.conductor import utils as mgr_utils
from ironic.tests.db import base as db_base
from ironic.tests.db import utils as db_utils
from ironic.tests.objects import utils as obj_utils
INFO_DICT = db_utils.get_test_iboot_info()
class IBootPrivateMethodTestCase(base.TestCase):
def setUp(self):
super(IBootPrivateMethodTestCase, self).setUp()
self.dbapi = dbapi.get_instance()
self.context = context.get_admin_context()
def test__parse_driver_info_good(self):
node = obj_utils.create_test_node(
self.context,
driver='fake_iboot',
driver_info=INFO_DICT)
info = iboot._parse_driver_info(node)
self.assertIsNotNone(info.get('address'))
self.assertIsNotNone(info.get('username'))
self.assertIsNotNone(info.get('password'))
self.assertIsNotNone(info.get('port'))
self.assertIsNotNone(info.get('relay_id'))
def test__parse_driver_info_good_with_explicit_port(self):
info = dict(INFO_DICT)
info['iboot_port'] = '1234'
node = obj_utils.create_test_node(
self.context,
driver='fake_iboot',
driver_info=info)
info = iboot._parse_driver_info(node)
self.assertEqual(1234, info.get('port'))
def test__parse_driver_info_good_with_explicit_relay_id(self):
info = dict(INFO_DICT)
info['iboot_relay_id'] = '2'
node = obj_utils.create_test_node(
self.context,
driver='fake_iboot',
driver_info=info)
info = iboot._parse_driver_info(node)
self.assertEqual(2, info.get('relay_id'))
def test__parse_driver_info_missing_address(self):
info = dict(INFO_DICT)
del info['iboot_address']
node = obj_utils.create_test_node(
self.context,
driver='fake_iboot',
driver_info=info)
self.assertRaises(exception.InvalidParameterValue,
iboot._parse_driver_info,
node)
def test__parse_driver_info_missing_username(self):
info = dict(INFO_DICT)
del info['iboot_username']
node = obj_utils.create_test_node(
self.context,
driver='fake_iboot',
driver_info=info)
self.assertRaises(exception.InvalidParameterValue,
iboot._parse_driver_info,
node)
def test__parse_driver_info_missing_password(self):
info = dict(INFO_DICT)
del info['iboot_password']
node = obj_utils.create_test_node(
self.context,
driver='fake_iboot',
driver_info=info)
self.assertRaises(exception.InvalidParameterValue,
iboot._parse_driver_info,
node)
def test__parse_driver_info_bad_port(self):
info = dict(INFO_DICT)
info['iboot_port'] = 'not-integer'
node = obj_utils.create_test_node(
self.context,
driver='fake_iboot',
driver_info=info)
self.assertRaises(exception.InvalidParameterValue,
iboot._parse_driver_info,
node)
def test__parse_driver_info_bad_relay_id(self):
info = dict(INFO_DICT)
info['iboot_relay_id'] = 'not-integer'
node = obj_utils.create_test_node(
self.context,
driver='fake_iboot',
driver_info=info)
self.assertRaises(exception.InvalidParameterValue,
iboot._parse_driver_info,
node)
class IBootDriverTestCase(db_base.DbTestCase):
def setUp(self):
super(IBootDriverTestCase, self).setUp()
self.dbapi = dbapi.get_instance()
mgr_utils.mock_the_extension_manager(driver='fake_iboot')
self.driver = driver_factory.get_driver('fake_iboot')
self.context = context.get_admin_context()
self.node = obj_utils.create_test_node(
self.context,
driver='fake_iboot',
driver_info=INFO_DICT)
self.info = iboot._parse_driver_info(self.node)
def test_get_properties(self):
expected = iboot.COMMON_PROPERTIES
with task_manager.acquire(self.context, self.node.uuid,
shared=True) as task:
self.assertEqual(expected, task.driver.get_properties())
@mock.patch.object(iboot, '_power_status')
@mock.patch.object(iboot, '_switch')
def test_set_power_state_good(self, mock_switch, mock_power_status):
mock_power_status.return_value = states.POWER_ON
with task_manager.acquire(self.context, self.node.uuid) as task:
task.driver.power.set_power_state(task, states.POWER_ON)
# ensure functions were called with the valid parameters
mock_switch.assert_called_once_with(self.info, True)
mock_power_status.assert_called_once_with(self.info)
@mock.patch.object(iboot, '_power_status')
@mock.patch.object(iboot, '_switch')
def test_set_power_state_bad(self, mock_switch, mock_power_status):
mock_power_status.return_value = states.POWER_OFF
with task_manager.acquire(self.context, self.node.uuid) as task:
self.assertRaises(exception.PowerStateFailure,
task.driver.power.set_power_state,
task, states.POWER_ON)
# ensure functions were called with the valid parameters
mock_switch.assert_called_once_with(self.info, True)
mock_power_status.assert_called_once_with(self.info)
@mock.patch.object(iboot, '_power_status')
@mock.patch.object(iboot, '_switch')
def test_set_power_state_invalid_parameter(self, mock_switch,
mock_power_status):
mock_power_status.return_value = states.POWER_ON
with task_manager.acquire(self.context, self.node.uuid) as task:
self.assertRaises(exception.InvalidParameterValue,
task.driver.power.set_power_state,
task, states.NOSTATE)
@mock.patch.object(iboot, '_power_status')
@mock.patch.object(iboot, '_switch')
def test_reboot_good(self, mock_switch, mock_power_status):
manager = mock.MagicMock()
mock_power_status.return_value = states.POWER_ON
manager.attach_mock(mock_switch, 'switch')
expected = [mock.call.switch(self.info, False),
mock.call.switch(self.info, True)]
with task_manager.acquire(self.context, self.node.uuid) as task:
task.driver.power.reboot(task)
self.assertEqual(manager.mock_calls, expected)
@mock.patch.object(iboot, '_power_status')
@mock.patch.object(iboot, '_switch')
def test_reboot_bad(self, mock_switch, mock_power_status):
manager = mock.MagicMock()
mock_power_status.return_value = states.POWER_OFF
manager.attach_mock(mock_switch, 'switch')
expected = [mock.call.switch(self.info, False),
mock.call.switch(self.info, True)]
with task_manager.acquire(self.context, self.node.uuid) as task:
self.assertRaises(exception.PowerStateFailure,
task.driver.power.reboot, task)
self.assertEqual(manager.mock_calls, expected)
@mock.patch.object(iboot, '_power_status')
def test_get_power_state(self, mock_power_status):
mock_power_status.return_value = states.POWER_ON
with task_manager.acquire(self.context, self.node.uuid) as task:
state = task.driver.power.get_power_state(task)
self.assertEqual(state, states.POWER_ON)
# ensure functions were called with the valid parameters
mock_power_status.assert_called_once_with(self.info)
@mock.patch.object(iboot, '_parse_driver_info')
def test_validate_good(self, parse_drv_info_mock):
with task_manager.acquire(self.context, self.node.uuid,
shared=True) as task:
task.driver.power.validate(task)
self.assertEqual(1, parse_drv_info_mock.call_count)
@mock.patch.object(iboot, '_parse_driver_info')
def test_validate_fails(self, parse_drv_info_mock):
side_effect = exception.InvalidParameterValue("Bad input")
parse_drv_info_mock.side_effect = side_effect
with task_manager.acquire(self.context, self.node.uuid,
shared=True) as task:
self.assertRaises(exception.InvalidParameterValue,
task.driver.power.validate, task)
self.assertEqual(1, parse_drv_info_mock.call_count)

View File

@ -83,3 +83,17 @@ if not proliantutils:
if 'ironic.drivers.ilo' in sys.modules:
reload(sys.modules['ironic.drivers.ilo'])
# attempt to load the external 'iboot' library, which is required by
# the optional drivers.modules.iboot module
iboot = importutils.try_import("iboot")
if not iboot:
ib = mock.Mock()
ib.iBootInterface = mock.Mock()
sys.modules['iboot'] = ib
# if anything has loaded the iboot driver yet, reload it now that the
# external library has been mocked
if 'ironic.drivers.modules.iboot' in sys.modules:
reload(sys.modules['ironic.drivers.modules.iboot'])

View File

@ -40,10 +40,12 @@ ironic.drivers =
fake_ssh = ironic.drivers.fake:FakeSSHDriver
fake_pxe = ironic.drivers.fake:FakePXEDriver
fake_seamicro = ironic.drivers.fake:FakeSeaMicroDriver
fake_iboot = ironic.drivers.fake:FakeIBootDriver
pxe_ipmitool = ironic.drivers.pxe:PXEAndIPMIToolDriver
pxe_ipminative = ironic.drivers.pxe:PXEAndIPMINativeDriver
pxe_ssh = ironic.drivers.pxe:PXEAndSSHDriver
pxe_seamicro = ironic.drivers.pxe:PXEAndSeaMicroDriver
pxe_iboot = ironic.drivers.pxe:PXEAndIBootDriver
ilo = ironic.drivers.ilo:IloDriver
[pbr]