DRAC: switch to python-dracclient on power interface

DRAC specific code from Ironic is moving to its own project, to
python-dracclient project. This patch starts using the new library.

Change-Id: I68251fc2b5dd169bb22bbbe38fe9eea16c5ac806
Partial-Bug: #1454492
Depends-On: Iab9d9f7e4e25e3d3fdec9b28fe49a7226e68c9ff
This commit is contained in:
Imre Farkas 2015-10-22 15:01:17 +02:00
parent ba7ec49f88
commit 0ced09b832
10 changed files with 227 additions and 187 deletions

View File

@ -12,6 +12,7 @@ python-oneviewclient<2.1.0,>=2.0.2
python-scciclient>=0.3.0
python-seamicroclient>=0.4.0
UcsSdk==0.8.2.2
python-dracclient>=0.0.5
# The drac and amt driver import a python module called "pywsman", however,
# this does not exist on pypi.

View File

@ -484,6 +484,10 @@ class IloOperationNotSupported(IronicException):
_msg_fmt = _("%(operation)s not supported. error: %(error)s")
class DracOperationError(IronicException):
_msg_fmt = _('DRAC operation failed. Reason: %(error)s')
class DracRequestFailed(IronicException):
pass

View File

@ -10,6 +10,7 @@
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
"""
DRAC Driver for remote system management using Dell Remote Access Card.
"""
@ -37,6 +38,11 @@ class PXEDracDriver(base.BaseDriver):
driver=self.__class__.__name__,
reason=_('Unable to import pywsman library'))
if not importutils.try_import('dracclient'):
raise exception.DriverLoadError(
driver=self.__class__.__name__,
reason=_('Unable to import python-dracclient library'))
self.power = power.DracPower()
self.boot = pxe.PXEBoot()
self.deploy = iscsi_deploy.ISCSIDeploy()

View File

@ -182,6 +182,11 @@ class FakeDracDriver(base.BaseDriver):
driver=self.__class__.__name__,
reason=_('Unable to import pywsman library'))
if not importutils.try_import('dracclient'):
raise exception.DriverLoadError(
driver=self.__class__.__name__,
reason=_('Unable to import python-dracclient library'))
self.power = drac_power.DracPower()
self.deploy = fake.FakeDeploy()
self.management = drac_mgmt.DracManagement()

View File

@ -22,6 +22,9 @@ from ironic.common.i18n import _
from ironic.common import utils
pywsman = importutils.try_import('pywsman')
drac_client = importutils.try_import('dracclient.client')
drac_constants = importutils.try_import('dracclient.constants')
REQUIRED_PROPERTIES = {
'drac_host': _('IP address or hostname of the DRAC card. Required.'),
@ -74,6 +77,10 @@ def parse_driver_info(node):
try:
parsed_driver_info['drac_protocol'] = str(
driver_info.get('drac_protocol', 'https'))
if parsed_driver_info['drac_protocol'] not in ['http', 'https']:
error_msgs.append(_("'drac_protocol' must be either 'http' or "
"'https'."))
except UnicodeEncodeError:
error_msgs.append(_("'drac_protocol' contains non-ASCII symbol."))
@ -89,6 +96,25 @@ def parse_driver_info(node):
return parsed_driver_info
def get_drac_client(node):
"""Returns a DRACClient object from python-dracclient library.
:param node: an ironic node object.
:returns: a DRACClient object.
:raises: InvalidParameterValue if mandatory information is missing on the
node or on invalid input.
"""
driver_info = parse_driver_info(node)
client = drac_client.DRACClient(driver_info['drac_host'],
driver_info['drac_username'],
driver_info['drac_password'],
driver_info['drac_port'],
driver_info['drac_path'],
driver_info['drac_protocol'])
return client
def find_xml(doc, item, namespace, find_all=False):
"""Find the first or all elements in an ElementTree object.

View File

@ -12,11 +12,10 @@
# under the License.
"""
DRAC Power Driver using the Base Server Profile
DRAC power interface
"""
from oslo_log import log as logging
from oslo_utils import excutils
from oslo_utils import importutils
from ironic.common import exception
@ -24,80 +23,74 @@ from ironic.common.i18n import _LE
from ironic.common import states
from ironic.conductor import task_manager
from ironic.drivers import base
from ironic.drivers.modules.drac import client as drac_client
from ironic.drivers.modules.drac import common as drac_common
from ironic.drivers.modules.drac import resource_uris
pywsman = importutils.try_import('pywsman')
drac_constants = importutils.try_import('dracclient.constants')
drac_exceptions = importutils.try_import('dracclient.exceptions')
LOG = logging.getLogger(__name__)
POWER_STATES = {
'2': states.POWER_ON,
'3': states.POWER_OFF,
'11': states.REBOOT,
}
if drac_constants:
POWER_STATES = {
drac_constants.POWER_ON: states.POWER_ON,
drac_constants.POWER_OFF: states.POWER_OFF,
drac_constants.REBOOT: states.REBOOT
}
REVERSE_POWER_STATES = dict((v, k) for (k, v) in POWER_STATES.items())
REVERSE_POWER_STATES = dict((v, k) for (k, v) in POWER_STATES.items())
def _get_power_state(node):
"""Returns the current power state of the node
"""Returns the current power state of the node.
:param node: The node.
:returns: power state, one of :mod: `ironic.common.states`.
:raises: DracClientError if the client received unexpected response.
:param node: an ironic node object.
:returns: the power state, one of :mod:`ironic.common.states`.
:raises: InvalidParameterValue if required DRAC credentials are missing.
:raises: DracOperationError on an error from python-dracclient
"""
client = drac_client.get_wsman_client(node)
filter_query = ('select EnabledState,ElementName from DCIM_ComputerSystem '
'where Name="srv:system"')
client = drac_common.get_drac_client(node)
try:
doc = client.wsman_enumerate(resource_uris.DCIM_ComputerSystem,
filter_query=filter_query)
except exception.DracClientError as exc:
with excutils.save_and_reraise_exception():
LOG.error(_LE('DRAC driver failed to get power state for node '
'%(node_uuid)s. Reason: %(error)s.'),
{'node_uuid': node.uuid, 'error': exc})
drac_power_state = client.get_power_state()
except drac_exceptions.BaseClientException as exc:
LOG.error(_LE('DRAC driver failed to get power state for node '
'%(node_uuid)s. Reason: %(error)s.'),
{'node_uuid': node.uuid, 'error': exc})
raise exception.DracOperationError(error=exc)
enabled_state = drac_common.find_xml(doc, 'EnabledState',
resource_uris.DCIM_ComputerSystem)
return POWER_STATES[enabled_state.text]
return POWER_STATES[drac_power_state]
def _set_power_state(node, target_state):
def _set_power_state(node, power_state):
"""Turns the server power on/off or do a reboot.
:param node: an ironic node object.
:param target_state: target state of the node.
:raises: DracClientError if the client received unexpected response.
:raises: InvalidParameterValue if an invalid power state was specified.
:param power_state: a power state from :mod:`ironic.common.states`.
:raises: InvalidParameterValue if required DRAC credentials are missing.
:raises: DracOperationError on an error from python-dracclient
"""
client = drac_client.get_wsman_client(node)
selectors = {'CreationClassName': 'DCIM_ComputerSystem',
'Name': 'srv:system'}
properties = {'RequestedState': REVERSE_POWER_STATES[target_state]}
client = drac_common.get_drac_client(node)
target_power_state = REVERSE_POWER_STATES[power_state]
try:
client.wsman_invoke(resource_uris.DCIM_ComputerSystem,
'RequestStateChange', selectors, properties)
except exception.DracRequestFailed as exc:
with excutils.save_and_reraise_exception():
LOG.error(_LE('DRAC driver failed to set power state for node '
'%(node_uuid)s to %(target_power_state)s. '
'Reason: %(error)s.'),
{'node_uuid': node.uuid,
'target_power_state': target_state,
'error': exc})
client.set_power_state(target_power_state)
except drac_exceptions.BaseClientException as exc:
LOG.error(_LE('DRAC driver failed to set power state for node '
'%(node_uuid)s to %(power_state)s. '
'Reason: %(error)s.'),
{'node_uuid': node.uuid,
'power_state': power_state,
'error': exc})
raise exception.DracOperationError(error=exc)
class DracPower(base.PowerInterface):
"""Interface for power-related actions."""
def get_properties(self):
"""Return the properties of the interface."""
return drac_common.COMMON_PROPERTIES
def validate(self, task):
@ -114,39 +107,36 @@ class DracPower(base.PowerInterface):
return drac_common.parse_driver_info(task.node)
def get_power_state(self, task):
"""Return the power state of the task's node.
"""Return the power state of the node.
:param task: a TaskManager instance containing the node to act on.
:returns: a power state. One of :mod:`ironic.common.states`.
:raises: DracClientError if the client received unexpected response.
:returns: the power state, one of :mod:`ironic.common.states`.
:raises: InvalidParameterValue if required DRAC credentials are
missing.
:raises: DracOperationError on an error from python-dracclient.
"""
return _get_power_state(task.node)
@task_manager.require_exclusive_lock
def set_power_state(self, task, power_state):
"""Set the power state of the task's node.
"""Set the power state of the node.
:param task: a TaskManager instance containing the node to act on.
:param power_state: Any power state from :mod:`ironic.common.states`.
:raises: DracClientError if the client received unexpected response.
:raises: DracOperationFailed if the client received response with an
error message.
:raises: DracUnexpectedReturnValue if the client received a response
with unexpected return value.
:param power_state: a power state from :mod:`ironic.common.states`.
:raises: InvalidParameterValue if required DRAC credentials are
missing.
:raises: DracOperationError on an error from python-dracclient.
"""
_set_power_state(task.node, power_state)
@task_manager.require_exclusive_lock
def reboot(self, task):
"""Perform a hard reboot of the task's node.
"""Perform a reboot of the task's node.
:param task: a TaskManager instance containing the node to act on.
:raises: DracClientError if the client received unexpected response.
:raises: DracOperationFailed if the client received response with an
error message.
:raises: DracUnexpectedReturnValue if the client received a response
with unexpected return value.
:raises: InvalidParameterValue if required DRAC credentials are
missing.
:raises: DracOperationError on an error from python-dracclient.
"""
current_power_state = _get_power_state(task.node)

View File

@ -17,6 +17,8 @@ Test class for common methods used by DRAC modules.
from xml.etree import ElementTree
import dracclient.client
import mock
from testtools.matchers import HasLength
from ironic.common import exception
@ -86,6 +88,15 @@ class DracCommonMethodsTestCase(db_base.DbTestCase):
info = drac_common.parse_driver_info(node)
self.assertEqual('https', info.get('drac_protocol'))
def test_parse_driver_info_invalid_protocol(self):
node = obj_utils.create_test_node(self.context,
driver='fake_drac',
driver_info=INFO_DICT)
node.driver_info['drac_protocol'] = 'foo'
self.assertRaises(exception.InvalidParameterValue,
drac_common.parse_driver_info, node)
def test_parse_driver_info_missing_username(self):
node = obj_utils.create_test_node(self.context,
driver='fake_drac',
@ -102,6 +113,18 @@ class DracCommonMethodsTestCase(db_base.DbTestCase):
self.assertRaises(exception.InvalidParameterValue,
drac_common.parse_driver_info, node)
@mock.patch.object(dracclient.client, 'DRACClient', autospec=True)
def test_get_drac_client(self, mock_dracclient):
expected_call = mock.call('1.2.3.4', 'admin', 'fake', 443, '/wsman',
'https')
node = obj_utils.create_test_node(self.context,
driver='fake_drac',
driver_info=INFO_DICT)
drac_common.get_drac_client(node)
self.assertEqual(mock_dracclient.mock_calls, [expected_call])
def test_find_xml(self):
namespace = 'http://fake'
value = 'fake_value'

View File

@ -12,164 +12,107 @@
# under the License.
"""
Test class for DRAC Power Driver
Test class for DRAC power interface
"""
from dracclient import constants as drac_constants
from dracclient import exceptions as drac_exceptions
import mock
from ironic.common import exception
from ironic.common import states
from ironic.conductor import task_manager
from ironic.drivers.modules.drac import client as drac_client
from ironic.drivers.modules.drac import common as drac_common
from ironic.drivers.modules.drac import power as drac_power
from ironic.drivers.modules.drac import resource_uris
from ironic.tests.unit.conductor import mgr_utils
from ironic.tests.unit.db import base
from ironic.tests.unit.db import utils as db_utils
from ironic.tests.unit.drivers.modules.drac import utils as test_utils
from ironic.tests.unit.drivers import third_party_driver_mock_specs \
as mock_specs
from ironic.tests.unit.objects import utils as obj_utils
INFO_DICT = db_utils.get_test_drac_info()
@mock.patch.object(drac_client, 'pywsman', spec_set=mock_specs.PYWSMAN_SPEC)
@mock.patch.object(drac_power, 'pywsman', spec_set=mock_specs.PYWSMAN_SPEC)
class DracPowerInternalMethodsTestCase(base.DbTestCase):
def setUp(self):
super(DracPowerInternalMethodsTestCase, self).setUp()
driver_info = INFO_DICT
self.node = db_utils.create_test_node(
driver='fake_drac',
driver_info=driver_info,
instance_uuid='instance_uuid_123')
def test__get_power_state(self, mock_power_pywsman, mock_client_pywsman):
result_xml = test_utils.build_soap_xml(
[{'EnabledState': '2'}], resource_uris.DCIM_ComputerSystem)
mock_xml = test_utils.mock_wsman_root(result_xml)
mock_pywsman_client = mock_client_pywsman.Client.return_value
mock_pywsman_client.enumerate.return_value = mock_xml
self.assertEqual(states.POWER_ON,
drac_power._get_power_state(self.node))
mock_pywsman_client.enumerate.assert_called_once_with(
mock.ANY, mock.ANY, resource_uris.DCIM_ComputerSystem)
def test__set_power_state(self, mock_power_pywsman, mock_client_pywsman):
result_xml = test_utils.build_soap_xml(
[{'ReturnValue': drac_client.RET_SUCCESS}],
resource_uris.DCIM_ComputerSystem)
mock_xml = test_utils.mock_wsman_root(result_xml)
mock_pywsman_client = mock_client_pywsman.Client.return_value
mock_pywsman_client.invoke.return_value = mock_xml
mock_pywsman_clientopts = (
mock_client_pywsman.ClientOptions.return_value)
drac_power._set_power_state(self.node, states.POWER_ON)
mock_pywsman_clientopts.add_selector.assert_has_calls([
mock.call('CreationClassName', 'DCIM_ComputerSystem'),
mock.call('Name', 'srv:system')
], any_order=True)
mock_pywsman_clientopts.add_property.assert_called_once_with(
'RequestedState', '2')
mock_pywsman_client.invoke.assert_called_once_with(
mock.ANY, resource_uris.DCIM_ComputerSystem,
'RequestStateChange', None)
def test__set_power_state_fail(self, mock_power_pywsman,
mock_client_pywsman):
result_xml = test_utils.build_soap_xml(
[{'ReturnValue': drac_client.RET_ERROR,
'Message': 'error message'}],
resource_uris.DCIM_ComputerSystem)
mock_xml = test_utils.mock_wsman_root(result_xml)
mock_pywsman_client = mock_client_pywsman.Client.return_value
mock_pywsman_client.invoke.return_value = mock_xml
mock_pywsman_clientopts = (
mock_client_pywsman.ClientOptions.return_value)
self.assertRaises(exception.DracOperationFailed,
drac_power._set_power_state, self.node,
states.POWER_ON)
mock_pywsman_clientopts.add_selector.assert_has_calls([
mock.call('CreationClassName', 'DCIM_ComputerSystem'),
mock.call('Name', 'srv:system')
], any_order=True)
mock_pywsman_clientopts.add_property.assert_called_once_with(
'RequestedState', '2')
mock_pywsman_client.invoke.assert_called_once_with(
mock.ANY, resource_uris.DCIM_ComputerSystem,
'RequestStateChange', None)
@mock.patch.object(drac_common, 'get_drac_client', spec_set=True,
autospec=True)
class DracPowerTestCase(base.DbTestCase):
def setUp(self):
super(DracPowerTestCase, self).setUp()
driver_info = INFO_DICT
mgr_utils.mock_the_extension_manager(driver="fake_drac")
self.node = db_utils.create_test_node(
driver='fake_drac',
driver_info=driver_info,
instance_uuid='instance_uuid_123')
mgr_utils.mock_the_extension_manager(driver='fake_drac')
self.node = obj_utils.create_test_node(self.context,
driver='fake_drac',
driver_info=INFO_DICT)
def test_get_properties(self):
def test_get_properties(self, mock_get_drac_client):
expected = drac_common.COMMON_PROPERTIES
driver = drac_power.DracPower()
self.assertEqual(expected, driver.get_properties())
@mock.patch.object(drac_power, '_get_power_state', spec_set=True,
autospec=True)
def test_get_power_state(self, mock_get_power_state):
mock_get_power_state.return_value = states.POWER_ON
driver = drac_power.DracPower()
task = mock.Mock()
task.node.return_value = self.node
def test_get_power_state(self, mock_get_drac_client):
mock_client = mock_get_drac_client.return_value
mock_client.get_power_state.return_value = drac_constants.POWER_ON
self.assertEqual(states.POWER_ON, driver.get_power_state(task))
mock_get_power_state.assert_called_once_with(task.node)
with task_manager.acquire(self.context, self.node.uuid,
shared=True) as task:
power_state = task.driver.power.get_power_state(task)
self.assertEqual(states.POWER_ON, power_state)
mock_client.get_power_state.assert_called_once_with()
def test_get_power_state_fail(self, mock_get_drac_client):
mock_client = mock_get_drac_client.return_value
exc = drac_exceptions.BaseClientException('boom')
mock_client.get_power_state.side_effect = exc
with task_manager.acquire(self.context, self.node.uuid,
shared=True) as task:
self.assertRaises(exception.DracOperationError,
task.driver.power.get_power_state, task)
mock_client.get_power_state.assert_called_once_with()
def test_set_power_state(self, mock_get_drac_client):
mock_client = mock_get_drac_client.return_value
@mock.patch.object(drac_power, '_set_power_state', spec_set=True,
autospec=True)
def test_set_power_state(self, mock_set_power_state):
with task_manager.acquire(self.context, self.node.uuid,
shared=False) as task:
task.driver.power.set_power_state(task, states.POWER_ON)
mock_set_power_state.assert_called_once_with(task.node,
states.POWER_ON)
task.driver.power.set_power_state(task, states.POWER_OFF)
drac_power_state = drac_power.REVERSE_POWER_STATES[states.POWER_OFF]
mock_client.set_power_state.assert_called_once_with(drac_power_state)
def test_set_power_state_fail(self, mock_get_drac_client):
mock_client = mock_get_drac_client.return_value
exc = drac_exceptions.BaseClientException('boom')
mock_client.set_power_state.side_effect = exc
with task_manager.acquire(self.context, self.node.uuid,
shared=False) as task:
self.assertRaises(exception.DracOperationError,
task.driver.power.set_power_state, task,
states.POWER_OFF)
drac_power_state = drac_power.REVERSE_POWER_STATES[states.POWER_OFF]
mock_client.set_power_state.assert_called_once_with(drac_power_state)
def test_reboot_while_powered_on(self, mock_get_drac_client):
mock_client = mock_get_drac_client.return_value
mock_client.get_power_state.return_value = drac_constants.POWER_ON
@mock.patch.object(drac_power, '_set_power_state', spec_set=True,
autospec=True)
@mock.patch.object(drac_power, '_get_power_state', spec_set=True,
autospec=True)
def test_reboot(self, mock_get_power_state, mock_set_power_state):
mock_get_power_state.return_value = states.POWER_ON
with task_manager.acquire(self.context, self.node.uuid,
shared=False) as task:
task.driver.power.reboot(task)
mock_set_power_state.assert_called_once_with(task.node,
states.REBOOT)
@mock.patch.object(drac_power, '_set_power_state', spec_set=True,
autospec=True)
@mock.patch.object(drac_power, '_get_power_state', spec_set=True,
autospec=True)
def test_reboot_in_power_off(self, mock_get_power_state,
mock_set_power_state):
mock_get_power_state.return_value = states.POWER_OFF
drac_power_state = drac_power.REVERSE_POWER_STATES[states.REBOOT]
mock_client.set_power_state.assert_called_once_with(drac_power_state)
def test_reboot_while_powered_off(self, mock_get_drac_client):
mock_client = mock_get_drac_client.return_value
mock_client.get_power_state.return_value = drac_constants.POWER_OFF
with task_manager.acquire(self.context, self.node.uuid,
shared=False) as task:
task.driver.power.reboot(task)
mock_set_power_state.assert_called_once_with(task.node,
states.POWER_ON)
drac_power_state = drac_power.REVERSE_POWER_STATES[states.POWER_ON]
mock_client.set_power_state.assert_called_once_with(drac_power_state)

View File

@ -16,6 +16,23 @@
"""This module provides mock 'specs' for third party modules that can be used
when needing to mock those third party modules"""
# python-dracclient
DRACCLIENT_SPEC = (
'client',
'constants',
'exceptions'
)
DRACCLIENT_CLIENT_MOD_SPEC = (
'DRACClient',
)
DRACCLIENT_CONSTANTS_MOD_SPEC = (
'POWER_OFF',
'POWER_ON',
'REBOOT'
)
# iboot
IBOOT_SPEC = (
'iBootInterface',

View File

@ -28,6 +28,8 @@ Current list of mocked libraries:
- pysnmp
- scciclient
- oneview_client
- pywsman
- python-dracclient
"""
import sys
@ -137,6 +139,29 @@ if not pywsman:
if 'ironic.drivers.modules.amt' in sys.modules:
six.moves.reload_module(sys.modules['ironic.drivers.modules.amt'])
# attempt to load the external 'python-dracclient' library, which is required
# by the optional drivers.modules.drac module. 'python-dracclient' is going to
# be used in the DRAC driver, once we will complete migration from 'pywsman'
dracclient = importutils.try_import('dracclient')
if not dracclient:
dracclient = mock.MagicMock(spec_set=mock_specs.DRACCLIENT_SPEC)
dracclient.client = mock.MagicMock(
spec_set=mock_specs.DRACCLIENT_CLIENT_MOD_SPEC)
dracclient.constants = mock.MagicMock(
spec_set=mock_specs.DRACCLIENT_CONSTANTS_MOD_SPEC,
POWER_OFF=mock.sentinel.POWER_OFF,
POWER_ON=mock.sentinel.POWER_ON,
REBOOT=mock.sentinel.REBOOT)
sys.modules['dracclient'] = dracclient
sys.modules['dracclient.client'] = dracclient.client
sys.modules['dracclient.constants'] = dracclient.constants
sys.modules['dracclient.exceptions'] = dracclient.exceptions
dracclient.exceptions.BaseClientException = type('BaseClientException',
(Exception,), {})
# Now that the external library has been mocked, if anything had already
# loaded any of the drivers, reload them.
if 'ironic.drivers.modules.drac' in sys.modules:
six.moves.reload_module(sys.modules['ironic.drivers.modules.drac'])
# attempt to load the external 'iboot' library, which is required by
# the optional drivers.modules.iboot module