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:
parent
9fe09164cb
commit
7f8fed748d
@ -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()
|
||||
|
185
ironic/drivers/modules/iboot.py
Normal file
185
ironic/drivers/modules/iboot.py
Normal 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)
|
@ -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()
|
||||
|
@ -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",
|
||||
|
248
ironic/tests/drivers/test_iboot.py
Normal file
248
ironic/tests/drivers/test_iboot.py
Normal 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)
|
@ -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'])
|
||||
|
@ -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]
|
||||
|
Loading…
x
Reference in New Issue
Block a user