Implement the SeaMicro Power driver

* Implement ironic's PowerInterface for SeaMicro power driver
* Use python-seamicroclient to access SeaMicro hardware
* Adds unit tests for SeaMicro power driver
* Adds python-seamicroclient to requirments.txt

Change-Id: I4b7263a28d479faebe1969f3d622bbb1f9957897
Implements: blueprint seamicro-power-driver
This commit is contained in:
Rohan Kanade 2014-01-17 14:25:02 -08:00
parent 76e6305386
commit ba207b4aa0
8 changed files with 636 additions and 0 deletions

View File

@ -868,6 +868,20 @@
#topics=notifications
[seamicro]
#
# Options defined in ironic.drivers.modules.seamicro
#
# Maximum retries for SeaMicro operations (integer value)
#max_retry=3
# Seconds to wait for power action to be completed (integer
# value)
#action_timeout=10
[ssh]
#

View File

@ -22,6 +22,7 @@ from ironic.drivers.modules import fake
from ironic.drivers.modules import ipminative
from ironic.drivers.modules import ipmitool
from ironic.drivers.modules import pxe
from ironic.drivers.modules import seamicro
from ironic.drivers.modules import ssh
@ -72,3 +73,15 @@ class FakeIPMINativeDriver(base.BaseDriver):
self.power = ipminative.NativeIPMIPower()
self.deploy = fake.FakeDeploy()
self.vendor = ipminative.VendorPassthru()
class FakeSeaMicroDriver(base.BaseDriver):
"""Fake SeaMicro driver."""
def __init__(self):
self.power = seamicro.Power()
self.deploy = fake.FakeDeploy()
self.rescue = self.deploy
a = fake.FakeVendorA()
b = fake.FakeVendorB()
self.vendor = fake.MultipleVendorInterface(a, b)

View File

@ -0,0 +1,309 @@
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# 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 SeaMicro interfaces.
Provides basic power control of servers in SeaMicro chassis via
python-seamicroclient.
"""
from oslo.config import cfg
from seamicroclient import client as seamicro_client
from seamicroclient import exceptions as seamicro_client_exception
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 log as logging
from ironic.openstack.common import loopingcall
opts = [
cfg.IntOpt('max_retry',
default=3,
help='Maximum retries for SeaMicro operations'),
cfg.IntOpt('action_timeout',
default=10,
help='Seconds to wait for power action to be completed')
]
CONF = cfg.CONF
opt_group = cfg.OptGroup(name='seamicro',
title='Options for the seamicro power driver')
CONF.register_group(opt_group)
CONF.register_opts(opts, opt_group)
LOG = logging.getLogger(__name__)
def _get_client(*args, **kwargs):
"""Creates the python-seamicro_client
:param kwargs: A dict of keyword arguments to be passed to the method,
which should contain: 'username', 'password',
'auth_url', 'api_version' parameters.
:returns: SeaMicro API client.
"""
cl_kwargs = {'username': kwargs['username'],
'password': kwargs['password'],
'auth_url': kwargs['api_endpoint']}
return seamicro_client.Client(kwargs['api_version'], **cl_kwargs)
def _parse_driver_info(node):
"""Parses and creates seamicro driver info
:param node: An Ironic node object.
:returns: SeaMicro driver info.
"""
info = node.get('driver_info', {})
api_endpoint = info.get('seamicro_api_endpoint', None)
username = info.get('seamicro_username', None)
password = info.get('seamicro_password', None)
server_id = info.get('seamicro_server_id', None)
api_version = info.get('seamicro_api_version', "2")
if not api_endpoint:
raise exception.InvalidParameterValue(_(
"SeaMicro driver requires api_endpoint be set"))
if not username or not password:
raise exception.InvalidParameterValue(_(
"SeaMicro driver requires both username and password be set"))
if not server_id:
raise exception.InvalidParameterValue(_(
"SeaMicro driver requires server_id be set"))
res = {'username': username,
'password': password,
'api_endpoint': api_endpoint,
'server_id': server_id,
'api_version': api_version,
'uuid': node.get('uuid')}
return res
def _get_server(driver_info):
"""Get server from server_id."""
s_client = _get_client(**driver_info)
return s_client.servers.get(driver_info['server_id'])
def _get_power_status(node):
"""Get current power state of this node
:param node: Ironic node one of :class:`ironic.db.models.Node`
:returns: Power state of the given node
"""
seamicro_info = _parse_driver_info(node)
try:
server = _get_server(seamicro_info)
if not hasattr(server, 'active') or server.active is None:
return states.ERROR
if not server.active:
return states.POWER_OFF
elif server.active:
return states.POWER_ON
except seamicro_client_exception.NotFound:
raise exception.NodeNotFound(node=seamicro_info['uuid'])
except seamicro_client_exception.ClientException as ex:
LOG.error(_("SeaMicro client exception %(msg)s for node %(uuid)s"),
{'msg': ex.message, 'uuid': seamicro_info['uuid']})
raise exception.ServiceUnavailable(message=ex.message)
def _power_on(node, timeout=None):
"""Power ON this node
:param node: An Ironic node object.
:param timeout: Time in seconds to wait till power on is complete.
:returns: Power state of the given node.
"""
if timeout is None:
timeout = CONF.seamicro.action_timeout
state = [None]
retries = [0]
seamicro_info = _parse_driver_info(node)
server = _get_server(seamicro_info)
def _wait_for_power_on(state, retries):
"""Called at an interval until the node is powered on."""
state[0] = _get_power_status(node)
if state[0] == states.POWER_ON:
raise loopingcall.LoopingCallDone()
if retries[0] > CONF.seamicro.max_retry:
state[0] = states.ERROR
raise loopingcall.LoopingCallDone()
try:
retries[0] += 1
server.power_on()
except seamicro_client_exception.ClientException:
LOG.warning(_("Power-on failed for node %s."),
seamicro_info['uuid'])
timer = loopingcall.FixedIntervalLoopingCall(_wait_for_power_on,
state, retries)
timer.start(interval=timeout).wait()
return state[0]
def _power_off(node, timeout=None):
"""Power OFF this node
:param node: Ironic node one of :class:`ironic.db.models.Node`
:param timeout: Time in seconds to wait till power off is compelete
:returns: Power state of the given node
"""
if timeout is None:
timeout = CONF.seamicro.action_timeout
state = [None]
retries = [0]
seamicro_info = _parse_driver_info(node)
server = _get_server(seamicro_info)
def _wait_for_power_off(state, retries):
"""Called at an interval until the node is powered off."""
state[0] = _get_power_status(node)
if state[0] == states.POWER_OFF:
raise loopingcall.LoopingCallDone()
if retries[0] > CONF.seamicro.max_retry:
state[0] = states.ERROR
raise loopingcall.LoopingCallDone()
try:
retries[0] += 1
server.power_off()
except seamicro_client_exception.ClientException:
LOG.warning(_("Power-off failed for node %s."),
seamicro_info['uuid'])
timer = loopingcall.FixedIntervalLoopingCall(_wait_for_power_off,
state, retries)
timer.start(interval=timeout).wait()
return state[0]
def _reboot(node, timeout=None):
"""Reboot this node
:param node: Ironic node one of :class:`ironic.db.models.Node`
:param timeout: Time in seconds to wait till reboot is compelete
:returns: Power state of the given node
"""
if timeout is None:
timeout = CONF.seamicro.action_timeout
state = [None]
retries = [0]
seamicro_info = _parse_driver_info(node)
server = _get_server(seamicro_info)
def _wait_for_reboot(state, retries):
"""Called at an interval until the node is rebooted successfully."""
state[0] = _get_power_status(node)
if state[0] == states.POWER_ON:
raise loopingcall.LoopingCallDone()
if retries[0] > CONF.seamicro.max_retry:
state[0] = states.ERROR
raise loopingcall.LoopingCallDone()
try:
retries[0] += 1
server.reset()
except seamicro_client_exception.ClientException:
LOG.warning(_("Reboot failed for node %s."),
seamicro_info['uuid'])
timer = loopingcall.FixedIntervalLoopingCall(_wait_for_reboot,
state, retries)
server.reset()
timer.start(interval=timeout).wait()
return state[0]
class Power(base.PowerInterface):
"""SeaMicro Power Interface.
This PowerInterface class provides a mechanism for controlling the power
state of servers in a seamicro chassis.
"""
def validate(self, node):
"""Check that node 'driver_info' is valid.
Check that node 'driver_info' contains the required fields.
:param node: Single node object.
:raises: InvalidParameterValue
"""
_parse_driver_info(node)
def get_power_state(self, task, node):
"""Get the current power state.
Poll the host for the current power state of the node.
:param task: A instance of `ironic.manager.task_manager.TaskManager`.
:param node: A single node.
:returns: power state. One of :class:`ironic.common.states`.
"""
return _get_power_status(node)
@task_manager.require_exclusive_lock
def set_power_state(self, task, node, pstate):
"""Turn the power on or off.
Set the power state of a node.
:param task: A instance of `ironic.manager.task_manager.TaskManager`.
:param node: A single node.
:param pstate: Either POWER_ON or POWER_OFF from :class:
`ironic.common.states`.
"""
if pstate == states.POWER_ON:
state = _power_on(node)
elif pstate == states.POWER_OFF:
state = _power_off(node)
else:
raise exception.IronicException(_(
"set_power_state called with invalid power state."))
if state != pstate:
raise exception.PowerStateFailure(pstate=pstate)
@task_manager.require_exclusive_lock
def reboot(self, task, node):
"""Cycles the power to a node.
:param task: a TaskManager instance.
:param node: An Ironic node object.
"""
state = _reboot(node)
if state != states.POWER_ON:
raise exception.PowerStateFailure(pstate=states.POWER_ON)

View File

@ -21,6 +21,7 @@ from ironic.drivers import base
from ironic.drivers.modules import ipminative
from ironic.drivers.modules import ipmitool
from ironic.drivers.modules import pxe
from ironic.drivers.modules import seamicro
from ironic.drivers.modules import ssh
@ -75,3 +76,21 @@ class PXEAndIPMINativeDriver(base.BaseDriver):
self.deploy = pxe.PXEDeploy()
self.rescue = self.deploy
self.vendor = pxe.VendorPassthru()
class PXEAndSeaMicroDriver(base.BaseDriver):
"""PXE + SeaMicro driver.
This driver implements the `core` functionality, combining
:class:ironic.drivers.modules.seamicro.Power 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):
self.power = seamicro.Power()
self.deploy = pxe.PXEDeploy()
self.rescue = self.deploy
self.vendor = pxe.VendorPassthru()

View File

@ -45,6 +45,15 @@ def get_test_pxe_info():
}
def get_test_seamicro_info():
return {
"seamicro_api_endpoint": "http://1.2.3.4",
"seamicro_username": "admin",
"seamicro_password": "fake",
"seamicro_server_id": "0/0",
}
def get_test_node(**kw):
properties = {
"cpu_arch": "x86_64",

View File

@ -0,0 +1,269 @@
# 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 Ironic SeaMicro driver."""
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 seamicro
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
INFO_DICT = db_utils.get_test_seamicro_info()
class Fake_Server():
def __init__(self, active=False, *args, **kwargs):
self.active = active
def power_on(self):
self.active = True
def power_off(self, force=False):
self.active = False
def reset(self):
self.active = True
class SeaMicroValidateParametersTestCase(base.TestCase):
def test__parse_driver_info_good(self):
# make sure we get back the expected things
node = db_utils.get_test_node(driver='fake_seamicro',
driver_info=INFO_DICT)
info = seamicro._parse_driver_info(node)
self.assertIsNotNone(info.get('api_endpoint'))
self.assertIsNotNone(info.get('username'))
self.assertIsNotNone(info.get('password'))
self.assertIsNotNone(info.get('server_id'))
self.assertIsNotNone(info.get('uuid'))
def test__parse_driver_info_missing_api_endpoint(self):
# make sure error is raised when info is missing
info = dict(INFO_DICT)
del info['seamicro_api_endpoint']
node = db_utils.get_test_node(driver_info=info)
self.assertRaises(exception.InvalidParameterValue,
seamicro._parse_driver_info,
node)
def test__parse_driver_info_missing_username(self):
# make sure error is raised when info is missing
info = dict(INFO_DICT)
del info['seamicro_username']
node = db_utils.get_test_node(driver_info=info)
self.assertRaises(exception.InvalidParameterValue,
seamicro._parse_driver_info,
node)
def test__parse_driver_info_missing_password(self):
# make sure error is raised when info is missing
info = dict(INFO_DICT)
del info['seamicro_password']
node = db_utils.get_test_node(driver_info=info)
self.assertRaises(exception.InvalidParameterValue,
seamicro._parse_driver_info,
node)
def test__parse_driver_info_missing_server_id(self):
# make sure error is raised when info is missing
info = dict(INFO_DICT)
del info['seamicro_server_id']
node = db_utils.get_test_node(driver_info=info)
self.assertRaises(exception.InvalidParameterValue,
seamicro._parse_driver_info,
node)
class SeaMicroPrivateMethodsTestCase(base.TestCase):
def setUp(self):
super(SeaMicroPrivateMethodsTestCase, self).setUp()
self.node = db_utils.get_test_node(driver='fake_seamicro',
driver_info=INFO_DICT)
self.Server = Fake_Server
@mock.patch.object(seamicro, "_get_server")
def test__get_power_status_on(self, mock_get_server):
mock_get_server.return_value = self.Server(active=True)
pstate = seamicro._get_power_status(self.node)
self.assertEqual(states.POWER_ON, pstate)
@mock.patch.object(seamicro, "_get_server")
def test__get_power_status_off(self, mock_get_server):
mock_get_server.return_value = self.Server(active=False)
pstate = seamicro._get_power_status(self.node)
self.assertEqual(states.POWER_OFF, pstate)
@mock.patch.object(seamicro, "_get_server")
def test__get_power_status_error(self, mock_get_server):
mock_get_server.return_value = self.Server(active=None)
pstate = seamicro._get_power_status(self.node)
self.assertEqual(states.ERROR, pstate)
@mock.patch.object(seamicro, "_get_server")
def test__power_on_good(self, mock_get_server):
mock_get_server.return_value = self.Server(active=False)
pstate = seamicro._power_on(self.node, timeout=2)
self.assertEqual(states.POWER_ON, pstate)
@mock.patch.object(seamicro, "_get_server")
def test__power_on_fail(self, mock_get_server):
def fake_power_on():
return
server = self.Server(active=False)
server.power_on = fake_power_on
mock_get_server.return_value = server
pstate = seamicro._power_on(self.node, timeout=2)
self.assertEqual(states.ERROR, pstate)
@mock.patch.object(seamicro, "_get_server")
def test__power_off_good(self, mock_get_server):
mock_get_server.return_value = self.Server(active=True)
pstate = seamicro._power_off(self.node, timeout=2)
self.assertEqual(states.POWER_OFF, pstate)
@mock.patch.object(seamicro, "_get_server")
def test__power_off_fail(self, mock_get_server):
def fake_power_off():
return
server = self.Server(active=True)
server.power_off = fake_power_off
mock_get_server.return_value = server
pstate = seamicro._power_off(self.node, timeout=2)
self.assertEqual(states.ERROR, pstate)
@mock.patch.object(seamicro, "_get_server")
def test__reboot_good(self, mock_get_server):
mock_get_server.return_value = self.Server(active=True)
pstate = seamicro._reboot(self.node, timeout=2)
self.assertEqual(states.POWER_ON, pstate)
@mock.patch.object(seamicro, "_get_server")
def test__reboot_fail(self, mock_get_server):
def fake_reboot():
return
server = self.Server(active=False)
server.reset = fake_reboot
mock_get_server.return_value = server
pstate = seamicro._reboot(self.node, timeout=2)
self.assertEqual(states.ERROR, pstate)
class SeaMicroPowerDriverTestCase(db_base.DbTestCase):
def setUp(self):
super(SeaMicroPowerDriverTestCase, self).setUp()
mgr_utils.mock_the_extension_manager(driver='fake_seamicro')
self.driver = driver_factory.get_driver('fake_seamicro')
self.node = db_utils.get_test_node(driver='fake_seamicro',
driver_info=INFO_DICT)
self.dbapi = dbapi.get_instance()
self.dbapi.create_node(self.node)
self.parse_drv_info_patcher = mock.patch.object(seamicro,
'_parse_driver_info')
self.parse_drv_info_mock = None
self.get_server_patcher = mock.patch.object(seamicro, '_get_server')
self.get_server_mock = None
self.Server = Fake_Server
@mock.patch.object(seamicro, '_reboot')
def test_reboot(self, mock_reboot):
info = seamicro._parse_driver_info(self.node)
mock_reboot.return_value = states.POWER_ON
with task_manager.acquire(self.context, [info['uuid']],
shared=False) as task:
task.resources[0].driver.power.reboot(task, self.node)
mock_reboot.assert_called_once_with(self.node)
def test_set_power_state_bad_state(self):
info = seamicro ._parse_driver_info(self.node)
self.get_server_mock = self.get_server_patcher.start()
self.get_server_mock.return_value = self.Server()
with task_manager.acquire(self.context, [info['uuid']],
shared=False) as task:
self.assertRaises(exception.IronicException,
task.resources[0].driver.power.set_power_state,
task, self.node, "BAD_PSTATE")
self.get_server_patcher.stop()
@mock.patch.object(seamicro, '_power_on')
def test_set_power_state_on_good(self, mock_power_on):
info = seamicro._parse_driver_info(self.node)
mock_power_on.return_value = states.POWER_ON
with task_manager.acquire(self.context, [info['uuid']],
shared=False) as task:
task.resources[0].driver.power.set_power_state(task,
self.node,
states.POWER_ON)
mock_power_on.assert_called_once_with(self.node)
@mock.patch.object(seamicro, '_power_on')
def test_set_power_state_on_fail(self, mock_power_on):
info = seamicro._parse_driver_info(self.node)
mock_power_on.return_value = states.POWER_OFF
with task_manager.acquire(self.context, [info['uuid']],
shared=False) as task:
self.assertRaises(exception.PowerStateFailure,
task.resources[0]
.driver.power.set_power_state,
task, self.node, states.POWER_ON)
mock_power_on.assert_called_once_with(self.node)
@mock.patch.object(seamicro, '_power_off')
def test_set_power_state_off_good(self, mock_power_off):
info = seamicro._parse_driver_info(self.node)
mock_power_off.return_value = states.POWER_OFF
with task_manager.acquire(self.context, [info['uuid']],
shared=False) as task:
task.resources[0].driver.power.\
set_power_state(task, self.node, states.POWER_OFF)
mock_power_off.assert_called_once_with(self.node)
@mock.patch.object(seamicro, '_power_off')
def test_set_power_state_off_fail(self, mock_power_off):
info = seamicro._parse_driver_info(self.node)
mock_power_off.return_value = states.POWER_ON
with task_manager.acquire(self.context, [info['uuid']],
shared=False) as task:
self.assertRaises(exception.PowerStateFailure,
task.resources[0]
.driver.power.set_power_state,
task, self.node, states.POWER_OFF)
mock_power_off.assert_called_once_with(self.node)

View File

@ -28,3 +28,4 @@ jsonpatch>=1.1
WSME>=0.6
Jinja2
pyghmi>=0.5.8
python-seamicroclient>=0.1.0,<2.0

View File

@ -35,9 +35,11 @@ ironic.drivers =
fake_ipminative = ironic.drivers.fake:FakeIPMINativeDriver
fake_ssh = ironic.drivers.fake:FakeSSHDriver
fake_pxe = ironic.drivers.fake:FakePXEDriver
fake_seamicro = ironic.drivers.fake:FakeSeaMicroDriver
pxe_ipmitool = ironic.drivers.pxe:PXEAndIPMIToolDriver
pxe_ipminative = ironic.drivers.pxe:PXEAndIPMINativeDriver
pxe_ssh = ironic.drivers.pxe:PXEAndSSHDriver
pxe_seamicro = ironic.drivers.pxe:PXEAndSeaMicroDriver
[pbr]
autodoc_index_modules = True