Add DracDriver and its DracPower module
Implements: blueprint drac-power-driver Change-Id: If93231c39ce901224f3a920f5342c44ba1b26851
This commit is contained in:
parent
2d59074349
commit
a2d3e4c493
14
doc/source/deploy/drivers.rst
Normal file
14
doc/source/deploy/drivers.rst
Normal file
@ -0,0 +1,14 @@
|
||||
.. _drivers:
|
||||
|
||||
=================
|
||||
Enabling Drivers
|
||||
=================
|
||||
|
||||
DRAC
|
||||
----
|
||||
|
||||
DRAC with PXE deploy
|
||||
^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
- Add ``pxe_drac`` to the list of ``enabled_drivers in`` ``/etc/ironic/ironic.conf``
|
||||
- Install openwsman-python package
|
@ -62,6 +62,7 @@ Overview
|
||||
|
||||
deploy/user-guide
|
||||
deploy/install-guide
|
||||
deploy/drivers
|
||||
|
||||
Indices and tables
|
||||
==================
|
||||
|
@ -408,6 +408,17 @@ class IloOperationError(IronicException):
|
||||
message = _("%(operation)s failed, error: %(error)s")
|
||||
|
||||
|
||||
class DracClientError(IronicException):
|
||||
message = _('DRAC client failed. '
|
||||
'Last error (cURL error code): %(last_error)s, '
|
||||
'fault string: "%(fault_string)s" '
|
||||
'response_code: %(response_code)s')
|
||||
|
||||
|
||||
class DracOperationError(IronicException):
|
||||
message = _('DRAC %(operation)s failed. Reason: %(error)s')
|
||||
|
||||
|
||||
class FailedToGetSensorData(IronicException):
|
||||
message = _("Failed to get sensor data for node %(node)s. "
|
||||
"Error: %(error)s")
|
||||
|
39
ironic/drivers/drac.py
Normal file
39
ironic/drivers/drac.py
Normal file
@ -0,0 +1,39 @@
|
||||
#
|
||||
# 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.
|
||||
"""
|
||||
DRAC Driver for remote system management using Dell Remote Access Card.
|
||||
"""
|
||||
|
||||
from ironic.common import exception
|
||||
from ironic.drivers import base
|
||||
from ironic.drivers.modules.drac import power
|
||||
from ironic.drivers.modules import ipmitool
|
||||
from ironic.drivers.modules import pxe
|
||||
from ironic.openstack.common import importutils
|
||||
|
||||
|
||||
class PXEDracDriver(base.BaseDriver):
|
||||
|
||||
"""Drac driver using PXE for deploy and ipmitool for management."""
|
||||
|
||||
def __init__(self):
|
||||
if not importutils.try_import('pywsman'):
|
||||
raise exception.DriverLoadError(
|
||||
driver=self.__class__.__name__,
|
||||
reason=_('Unable to import pywsman library'))
|
||||
|
||||
self.power = power.DracPower()
|
||||
self.deploy = pxe.PXEDeploy()
|
||||
# NOTE(ifarkas): using ipmitool is a temporary solution. It will be
|
||||
# replaced by the DracManagement interface.
|
||||
self.management = ipmitool.IPMIManagement()
|
@ -22,6 +22,7 @@ from oslo.utils import importutils
|
||||
from ironic.common import exception
|
||||
from ironic.drivers import base
|
||||
from ironic.drivers.modules import agent
|
||||
from ironic.drivers.modules.drac import power as drac_power
|
||||
from ironic.drivers.modules import fake
|
||||
from ironic.drivers.modules import iboot
|
||||
from ironic.drivers.modules.ilo import power as ilo_power
|
||||
@ -128,3 +129,16 @@ class FakeIloDriver(base.BaseDriver):
|
||||
reason=_("Unable to import proliantutils library"))
|
||||
self.power = ilo_power.IloPower()
|
||||
self.deploy = fake.FakeDeploy()
|
||||
|
||||
|
||||
class FakeDracDriver(base.BaseDriver):
|
||||
"""Fake Drac driver."""
|
||||
|
||||
def __init__(self):
|
||||
if not importutils.try_import('pywsman'):
|
||||
raise exception.DriverLoadError(
|
||||
driver=self.__class__.__name__,
|
||||
reason=_('Unable to import pywsman library'))
|
||||
|
||||
self.power = drac_power.DracPower()
|
||||
self.deploy = fake.FakeDeploy()
|
||||
|
0
ironic/drivers/modules/drac/__init__.py
Normal file
0
ironic/drivers/modules/drac/__init__.py
Normal file
80
ironic/drivers/modules/drac/client.py
Normal file
80
ironic/drivers/modules/drac/client.py
Normal file
@ -0,0 +1,80 @@
|
||||
#
|
||||
# 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.
|
||||
|
||||
"""
|
||||
Wrapper for pywsman.Client
|
||||
"""
|
||||
|
||||
from ironic.common import exception
|
||||
from ironic.openstack.common import importutils
|
||||
|
||||
pywsman = importutils.try_import('pywsman')
|
||||
|
||||
|
||||
class Client(object):
|
||||
|
||||
def __init__(self, drac_host, drac_port, drac_path, drac_protocol,
|
||||
drac_username, drac_password):
|
||||
pywsman_client = pywsman.Client(drac_host, drac_port, drac_path,
|
||||
drac_protocol, drac_username,
|
||||
drac_password)
|
||||
# TODO(ifarkas): Add support for CACerts
|
||||
pywsman.wsman_transport_set_verify_peer(pywsman_client, False)
|
||||
|
||||
self.client = pywsman_client
|
||||
|
||||
def wsman_enumerate(self, resource_uri, options, filter=None):
|
||||
"""Enumerates a remote WS-Man class.
|
||||
|
||||
:param resource_uri: URI of the resource.
|
||||
:param options: client options.
|
||||
:param filter: filter for enumeration.
|
||||
:returns: array of xml responses received.
|
||||
"""
|
||||
options.set_flags(pywsman.FLAG_ENUMERATION_OPTIMIZATION)
|
||||
options.set_max_elements(100)
|
||||
|
||||
partial_responses = []
|
||||
doc = self.client.enumerate(options, filter, resource_uri)
|
||||
root = self._get_root(doc)
|
||||
partial_responses.append(root)
|
||||
|
||||
while doc.context() is not None:
|
||||
doc = self.client.pull(options, None, resource_uri,
|
||||
str(doc.context()))
|
||||
root = self._get_root(doc)
|
||||
partial_responses.append(root)
|
||||
|
||||
return partial_responses
|
||||
|
||||
def wsman_invoke(self, resource_uri, options, method):
|
||||
"""Invokes a remote WS-Man method.
|
||||
|
||||
:param resource_uri: URI of the resource.
|
||||
:param options: client options.
|
||||
:param method: name of the method to invoke.
|
||||
:returns: xml response received.
|
||||
"""
|
||||
doc = self.client.invoke(options, resource_uri, method)
|
||||
root = self._get_root(doc)
|
||||
|
||||
return root
|
||||
|
||||
def _get_root(self, doc):
|
||||
if doc is None or doc.root() is None:
|
||||
raise exception.DracClientError(
|
||||
last_error=self.client.last_error(),
|
||||
fault_string=self.client.fault_string(),
|
||||
response_code=self.client.response_code())
|
||||
|
||||
return doc.root()
|
100
ironic/drivers/modules/drac/common.py
Normal file
100
ironic/drivers/modules/drac/common.py
Normal file
@ -0,0 +1,100 @@
|
||||
#
|
||||
# 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.
|
||||
|
||||
"""
|
||||
Common functionalities shared between different DRAC modules.
|
||||
"""
|
||||
|
||||
from ironic.common import exception
|
||||
from ironic.drivers.modules.drac import client as drac_client
|
||||
from ironic.openstack.common import importutils
|
||||
|
||||
pywsman = importutils.try_import('pywsman')
|
||||
|
||||
REQUIRED_PROPERTIES = {
|
||||
'drac_host': _('IP address or hostname of the DRAC card. Required.'),
|
||||
'drac_username': _('username used for authentication. Required.'),
|
||||
'drac_password': _('password used for authentication. Required.')
|
||||
}
|
||||
OPTIONAL_PROPERTIES = {
|
||||
'drac_port': _('port used for WS-Man endpoint; default is 443. Optional.'),
|
||||
'drac_path': _('path used for WS-Man endpoint; default is "/wsman". '
|
||||
'Optional.'),
|
||||
'drac_protocol': _('protocol used for WS-Man endpoint; one of http, https;'
|
||||
' default is "https". Optional.'),
|
||||
}
|
||||
COMMON_PROPERTIES = REQUIRED_PROPERTIES.copy()
|
||||
COMMON_PROPERTIES.update(OPTIONAL_PROPERTIES)
|
||||
|
||||
|
||||
def parse_driver_info(node):
|
||||
"""Parses the driver_info of the node, reads default values
|
||||
and returns a dict containing the combination of both.
|
||||
|
||||
:param node: an ironic node object.
|
||||
:returns: a dict containing information from driver_info
|
||||
and default values.
|
||||
:raises: InvalidParameterValue if some mandatory information
|
||||
is missing on the node or on invalid inputs.
|
||||
"""
|
||||
driver_info = node.driver_info
|
||||
parsed_driver_info = {}
|
||||
|
||||
error_msgs = []
|
||||
for param in REQUIRED_PROPERTIES:
|
||||
try:
|
||||
parsed_driver_info[param] = str(driver_info[param])
|
||||
except KeyError:
|
||||
error_msgs.append(_("'%s' not supplied to DracDriver.") % param)
|
||||
except UnicodeEncodeError:
|
||||
error_msgs.append(_("'%s' contains non-ASCII symbol.") % param)
|
||||
|
||||
parsed_driver_info['drac_port'] = driver_info.get('drac_port', 443)
|
||||
|
||||
try:
|
||||
parsed_driver_info['drac_path'] = str(driver_info.get('drac_path',
|
||||
'/wsman'))
|
||||
except UnicodeEncodeError:
|
||||
error_msgs.append(_("'drac_path' contains non-ASCII symbol."))
|
||||
|
||||
try:
|
||||
parsed_driver_info['drac_protocol'] = str(
|
||||
driver_info.get('drac_protocol', 'https'))
|
||||
except UnicodeEncodeError:
|
||||
error_msgs.append(_("'drac_protocol' contains non-ASCII symbol."))
|
||||
|
||||
try:
|
||||
parsed_driver_info['drac_port'] = int(parsed_driver_info['drac_port'])
|
||||
except ValueError:
|
||||
error_msgs.append(_("'drac_port' is not an integer value."))
|
||||
|
||||
if error_msgs:
|
||||
msg = (_('The following errors were encountered while parsing '
|
||||
'driver_info:\n%s') % '\n'.join(error_msgs))
|
||||
raise exception.InvalidParameterValue(msg)
|
||||
|
||||
return parsed_driver_info
|
||||
|
||||
|
||||
def get_wsman_client(node):
|
||||
"""Given an ironic node object, this method gives back a
|
||||
Client object which is a wrapper for pywsman.Client.
|
||||
|
||||
:param node: an ironic node object.
|
||||
:returns: a Client object.
|
||||
:raises: InvalidParameterValue if some mandatory information
|
||||
is missing on the node or on invalid inputs.
|
||||
"""
|
||||
driver_info = parse_driver_info(node)
|
||||
client = drac_client.Client(**driver_info)
|
||||
return client
|
163
ironic/drivers/modules/drac/power.py
Normal file
163
ironic/drivers/modules/drac/power.py
Normal file
@ -0,0 +1,163 @@
|
||||
#
|
||||
# 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.
|
||||
|
||||
"""
|
||||
DRAC Power Driver using the Base Server Profile
|
||||
"""
|
||||
|
||||
from ironic.common import exception
|
||||
from ironic.common import i18n
|
||||
from ironic.common import states
|
||||
from ironic.drivers import base
|
||||
from ironic.drivers.modules.drac import common as drac_common
|
||||
from ironic.drivers.modules.drac import resource_uris
|
||||
from ironic.openstack.common import excutils
|
||||
from ironic.openstack.common import importutils
|
||||
from ironic.openstack.common import log as logging
|
||||
|
||||
pywsman = importutils.try_import('pywsman')
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
_LE = i18n._LE
|
||||
|
||||
POWER_STATES = {
|
||||
'2': states.POWER_ON,
|
||||
'3': states.POWER_OFF,
|
||||
'11': states.REBOOT,
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
:param node: The node.
|
||||
:returns: power state, one of :mod: `ironic.common.states`.
|
||||
:raises: DracClientError if the client received unexpected response.
|
||||
:raises: InvalidParameterValue if required DRAC credentials are missing.
|
||||
"""
|
||||
|
||||
client = drac_common.get_wsman_client(node)
|
||||
options = pywsman.ClientOptions()
|
||||
filter = pywsman.Filter()
|
||||
filter_dialect = 'http://schemas.dmtf.org/wbem/cql/1/dsp0202.pdf'
|
||||
filter_query = ('select EnabledState,ElementName from CIM_ComputerSystem '
|
||||
'where Name="srv:system"')
|
||||
filter.simple(filter_dialect, filter_query)
|
||||
|
||||
try:
|
||||
docs = client.wsman_enumerate(resource_uris.DCIM_ComputerSystem,
|
||||
options, filter)
|
||||
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})
|
||||
|
||||
doc = docs[0]
|
||||
enabled_state = str(doc.find(resource_uris.DCIM_ComputerSystem,
|
||||
'EnabledState'))
|
||||
|
||||
return POWER_STATES[enabled_state]
|
||||
|
||||
|
||||
def _set_power_state(node, target_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.
|
||||
"""
|
||||
|
||||
client = drac_common.get_wsman_client(node)
|
||||
options = pywsman.ClientOptions()
|
||||
options.add_selector('CreationClassName', 'DCIM_ComputerSystem')
|
||||
options.add_selector('Name', 'srv:system')
|
||||
options.add_property('RequestedState', REVERSE_POWER_STATES[target_state])
|
||||
|
||||
try:
|
||||
root = client.wsman_invoke(resource_uris.DCIM_ComputerSystem, options,
|
||||
'RequestStateChange')
|
||||
except exception.DracClientError 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})
|
||||
|
||||
return_value = str(root.find(resource_uris.DCIM_ComputerSystem,
|
||||
'ReturnValue'))
|
||||
|
||||
if return_value != '0':
|
||||
message = str(root.find(resource_uris.DCIM_ComputerSystem, 'Message'))
|
||||
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': message})
|
||||
raise exception.DracOperationError(operation='set_power_state',
|
||||
error=message)
|
||||
|
||||
|
||||
class DracPower(base.PowerInterface):
|
||||
"""Interface for power-related actions."""
|
||||
|
||||
def get_properties(self):
|
||||
return drac_common.COMMON_PROPERTIES
|
||||
|
||||
def validate(self, task):
|
||||
"""Validate the driver-specific Node power info.
|
||||
|
||||
This method validates whether the 'driver_info' property of the
|
||||
supplied node contains the required information for this driver to
|
||||
manage the power state of the node.
|
||||
|
||||
:param task: a TaskManager instance containing the node to act on.
|
||||
:raises: InvalidParameterValue if required driver_info attribute
|
||||
is missing or invalid on the node.
|
||||
"""
|
||||
return drac_common.parse_driver_info(task.node)
|
||||
|
||||
def get_power_state(self, task):
|
||||
"""Return the power state of the task's 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.
|
||||
"""
|
||||
return _get_power_state(task.node)
|
||||
|
||||
def set_power_state(self, task, power_state):
|
||||
"""Set the power state of the task's 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: DracOperationError if failed to set the power state.
|
||||
"""
|
||||
return _set_power_state(task.node, power_state)
|
||||
|
||||
def reboot(self, task):
|
||||
"""Perform a hard 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: DracOperationError if failed to set the power state.
|
||||
"""
|
||||
return _set_power_state(task.node, states.REBOOT)
|
20
ironic/drivers/modules/drac/resource_uris.py
Normal file
20
ironic/drivers/modules/drac/resource_uris.py
Normal file
@ -0,0 +1,20 @@
|
||||
#
|
||||
# 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.
|
||||
|
||||
"""
|
||||
Resource URIs and helper functions for the classes implemented by the DRAC
|
||||
WS-Man API.
|
||||
"""
|
||||
|
||||
DCIM_ComputerSystem = ('http://schemas.dell.com/wbem/wscim/1/cim-schema/2'
|
||||
'/DCIM_ComputerSystem')
|
@ -78,6 +78,17 @@ def get_test_ilo_info():
|
||||
}
|
||||
|
||||
|
||||
def get_test_drac_info():
|
||||
return {
|
||||
"drac_host": "1.2.3.4",
|
||||
"drac_port": "443",
|
||||
"drac_path": "/wsman",
|
||||
"drac_protocol": "https",
|
||||
"drac_username": "admin",
|
||||
"drac_password": "fake",
|
||||
}
|
||||
|
||||
|
||||
def get_test_agent_instance_info():
|
||||
return {
|
||||
'image_source': 'fake-image',
|
||||
|
0
ironic/tests/drivers/drac/__init__.py
Normal file
0
ironic/tests/drivers/drac/__init__.py
Normal file
78
ironic/tests/drivers/drac/test_client.py
Normal file
78
ironic/tests/drivers/drac/test_client.py
Normal file
@ -0,0 +1,78 @@
|
||||
#
|
||||
# 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 DRAC client wrapper.
|
||||
"""
|
||||
|
||||
import mock
|
||||
|
||||
from ironic.drivers.modules.drac import client as drac_client
|
||||
from ironic.tests import base
|
||||
from ironic.tests.db import utils as db_utils
|
||||
|
||||
INFO_DICT = db_utils.get_test_drac_info()
|
||||
|
||||
|
||||
@mock.patch.object(drac_client, 'pywsman')
|
||||
class DracClientTestCase(base.TestCase):
|
||||
|
||||
def test_wsman_enumerate(self, mock_client_pywsman):
|
||||
mock_xml = mock.Mock()
|
||||
mock_xml.context.return_value = None
|
||||
|
||||
mock_pywsman_client = mock_client_pywsman.Client.return_value
|
||||
mock_pywsman_client.enumerate.return_value = mock_xml
|
||||
|
||||
resource_uri = 'https://foo/wsman'
|
||||
mock_options = mock_client_pywsman.ClientOptions.return_value
|
||||
client = drac_client.Client(**INFO_DICT)
|
||||
client.wsman_enumerate(resource_uri, mock_options)
|
||||
|
||||
mock_options.set_flags.assert_called_once_with(
|
||||
mock_client_pywsman.FLAG_ENUMERATION_OPTIMIZATION)
|
||||
mock_options.set_max_elements.assert_called_once_with(100)
|
||||
mock_pywsman_client.enumerate.assert_called_once_with(mock_options,
|
||||
None, resource_uri)
|
||||
mock_xml.context.assert_called_once_with()
|
||||
|
||||
def test_wsman_enumerate_with_additional_pull(self, mock_client_pywsman):
|
||||
mock_xml = mock.Mock()
|
||||
mock_xml.context.side_effect = [42, 42, None]
|
||||
|
||||
mock_pywsman_client = mock_client_pywsman.Client.return_value
|
||||
mock_pywsman_client.enumerate.return_value = mock_xml
|
||||
mock_pywsman_client.pull.return_value = mock_xml
|
||||
|
||||
resource_uri = 'https://foo/wsman'
|
||||
mock_options = mock_client_pywsman.ClientOptions.return_value
|
||||
client = drac_client.Client(**INFO_DICT)
|
||||
client.wsman_enumerate(resource_uri, mock_options)
|
||||
|
||||
mock_options.set_flags.assert_called_once_with(
|
||||
mock_client_pywsman.FLAG_ENUMERATION_OPTIMIZATION)
|
||||
mock_options.set_max_elements.assert_called_once_with(100)
|
||||
mock_pywsman_client.enumerate.assert_called_once_with(mock_options,
|
||||
None, resource_uri)
|
||||
|
||||
def test_wsman_invoke(self, mock_client_pywsman):
|
||||
mock_pywsman_client = mock_client_pywsman.Client.return_value
|
||||
|
||||
resource_uri = 'https://foo/wsman'
|
||||
mock_options = mock_client_pywsman.ClientOptions.return_value
|
||||
method_name = 'method'
|
||||
client = drac_client.Client(**INFO_DICT)
|
||||
client.wsman_invoke(resource_uri, mock_options, method_name)
|
||||
|
||||
mock_pywsman_client.invoke.assert_called_once_with(mock_options,
|
||||
resource_uri, method_name)
|
104
ironic/tests/drivers/drac/test_common.py
Normal file
104
ironic/tests/drivers/drac/test_common.py
Normal file
@ -0,0 +1,104 @@
|
||||
#
|
||||
# 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 common methods used by DRAC modules.
|
||||
"""
|
||||
|
||||
from ironic.common import exception
|
||||
from ironic.drivers.modules.drac import common as drac_common
|
||||
from ironic.openstack.common import context
|
||||
from ironic.tests import 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_drac_info()
|
||||
|
||||
|
||||
class DracCommonMethodsTestCase(base.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
super(DracCommonMethodsTestCase, self).setUp()
|
||||
self.context = context.get_admin_context()
|
||||
|
||||
def test_parse_driver_info(self):
|
||||
node = obj_utils.create_test_node(self.context,
|
||||
driver='fake_drac',
|
||||
driver_info=INFO_DICT)
|
||||
info = drac_common.parse_driver_info(node)
|
||||
|
||||
self.assertIsNotNone(info.get('drac_host'))
|
||||
self.assertIsNotNone(info.get('drac_port'))
|
||||
self.assertIsNotNone(info.get('drac_path'))
|
||||
self.assertIsNotNone(info.get('drac_protocol'))
|
||||
self.assertIsNotNone(info.get('drac_username'))
|
||||
self.assertIsNotNone(info.get('drac_password'))
|
||||
|
||||
def test_parse_driver_info_missing_host(self):
|
||||
node = obj_utils.create_test_node(self.context,
|
||||
driver='fake_drac',
|
||||
driver_info=INFO_DICT)
|
||||
del node.driver_info['drac_host']
|
||||
self.assertRaises(exception.InvalidParameterValue,
|
||||
drac_common.parse_driver_info, node)
|
||||
|
||||
def test_parse_driver_info_missing_port(self):
|
||||
node = obj_utils.create_test_node(self.context,
|
||||
driver='fake_drac',
|
||||
driver_info=INFO_DICT)
|
||||
del node.driver_info['drac_port']
|
||||
|
||||
info = drac_common.parse_driver_info(node)
|
||||
self.assertEqual(443, info.get('drac_port'))
|
||||
|
||||
def test_parse_driver_info_invalid_port(self):
|
||||
node = obj_utils.create_test_node(self.context,
|
||||
driver='fake_drac',
|
||||
driver_info=INFO_DICT)
|
||||
node.driver_info['drac_port'] = 'foo'
|
||||
self.assertRaises(exception.InvalidParameterValue,
|
||||
drac_common.parse_driver_info, node)
|
||||
|
||||
def test_parse_driver_info_missing_path(self):
|
||||
node = obj_utils.create_test_node(self.context,
|
||||
driver='fake_drac',
|
||||
driver_info=INFO_DICT)
|
||||
del node.driver_info['drac_path']
|
||||
|
||||
info = drac_common.parse_driver_info(node)
|
||||
self.assertEqual('/wsman', info.get('drac_path'))
|
||||
|
||||
def test_parse_driver_info_missing_protocol(self):
|
||||
node = obj_utils.create_test_node(self.context,
|
||||
driver='fake_drac',
|
||||
driver_info=INFO_DICT)
|
||||
del node.driver_info['drac_protocol']
|
||||
|
||||
info = drac_common.parse_driver_info(node)
|
||||
self.assertEqual('https', info.get('drac_protocol'))
|
||||
|
||||
def test_parse_driver_info_missing_username(self):
|
||||
node = obj_utils.create_test_node(self.context,
|
||||
driver='fake_drac',
|
||||
driver_info=INFO_DICT)
|
||||
del node.driver_info['drac_username']
|
||||
self.assertRaises(exception.InvalidParameterValue,
|
||||
drac_common.parse_driver_info, node)
|
||||
|
||||
def test_parse_driver_info_missing_password(self):
|
||||
node = obj_utils.create_test_node(self.context,
|
||||
driver='fake_drac',
|
||||
driver_info=INFO_DICT)
|
||||
del node.driver_info['drac_password']
|
||||
self.assertRaises(exception.InvalidParameterValue,
|
||||
drac_common.parse_driver_info, node)
|
170
ironic/tests/drivers/drac/test_power.py
Normal file
170
ironic/tests/drivers/drac/test_power.py
Normal file
@ -0,0 +1,170 @@
|
||||
#
|
||||
# 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 DRAC Power Driver
|
||||
"""
|
||||
|
||||
import mock
|
||||
|
||||
from ironic.common import exception
|
||||
from ironic.common import states
|
||||
from ironic.db import api as dbapi
|
||||
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.openstack.common import context
|
||||
from ironic.tests import base
|
||||
from ironic.tests.db import utils as db_utils
|
||||
|
||||
INFO_DICT = db_utils.get_test_drac_info()
|
||||
|
||||
|
||||
@mock.patch.object(drac_client, 'pywsman')
|
||||
@mock.patch.object(drac_power, 'pywsman')
|
||||
class DracPowerInternalMethodsTestCase(base.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
super(DracPowerInternalMethodsTestCase, self).setUp()
|
||||
driver_info = INFO_DICT
|
||||
db_node = db_utils.get_test_node(driver='fake_drac',
|
||||
driver_info=driver_info,
|
||||
instance_uuid='instance_uuid_123')
|
||||
self.dbapi = dbapi.get_instance()
|
||||
self.node = self.dbapi.create_node(db_node)
|
||||
|
||||
def test__get_power_state(self, mock_power_pywsman, mock_client_pywsman):
|
||||
mock_xml_root = mock.Mock()
|
||||
mock_xml_root.find.return_value = '2'
|
||||
|
||||
mock_xml = mock.Mock()
|
||||
mock_xml.context.return_value = None
|
||||
mock_xml.root.return_value = mock_xml_root
|
||||
|
||||
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)
|
||||
mock_xml_root.find.assert_called_once_with(
|
||||
resource_uris.DCIM_ComputerSystem, 'EnabledState')
|
||||
|
||||
def test__set_power_state(self, mock_power_pywsman, mock_client_pywsman):
|
||||
mock_xml_root = mock.Mock()
|
||||
mock_xml_root.find.return_value = '0'
|
||||
|
||||
mock_xml = mock.Mock()
|
||||
mock_xml.root.return_value = mock_xml_root
|
||||
|
||||
mock_pywsman_client = mock_client_pywsman.Client.return_value
|
||||
mock_pywsman_client.invoke.return_value = mock_xml
|
||||
|
||||
mock_pywsman_clientopts = mock_power_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')
|
||||
])
|
||||
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')
|
||||
mock_xml_root.find.assert_called_once_with(
|
||||
resource_uris.DCIM_ComputerSystem, 'ReturnValue')
|
||||
|
||||
def test__set_power_state_fail(self, mock_power_pywsman,
|
||||
mock_client_pywsman):
|
||||
mock_xml_root = mock.Mock()
|
||||
mock_xml_root.find.side_effect = ['1', 'error message']
|
||||
|
||||
mock_xml = mock.Mock()
|
||||
mock_xml.root.return_value = mock_xml_root
|
||||
|
||||
mock_pywsman_client = mock_client_pywsman.Client.return_value
|
||||
mock_pywsman_client.invoke.return_value = mock_xml
|
||||
|
||||
mock_pywsman_clientopts = mock_power_pywsman.ClientOptions.return_value
|
||||
|
||||
self.assertRaises(exception.DracOperationError,
|
||||
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')
|
||||
])
|
||||
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')
|
||||
|
||||
mock_xml_root.find.assert_has_calls([
|
||||
mock.call(resource_uris.DCIM_ComputerSystem, 'ReturnValue'),
|
||||
mock.call(resource_uris.DCIM_ComputerSystem, 'Message')
|
||||
])
|
||||
|
||||
|
||||
class DracPowerTestCase(base.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
super(DracPowerTestCase, self).setUp()
|
||||
driver_info = INFO_DICT
|
||||
db_node = db_utils.get_test_node(driver='fake_drac',
|
||||
driver_info=driver_info,
|
||||
instance_uuid='instance_uuid_123')
|
||||
self.dbapi = dbapi.get_instance()
|
||||
self.node = self.dbapi.create_node(db_node)
|
||||
self.context = context.get_admin_context()
|
||||
|
||||
def test_get_properties(self):
|
||||
expected = drac_common.COMMON_PROPERTIES
|
||||
driver = drac_power.DracPower()
|
||||
self.assertEqual(expected, driver.get_properties())
|
||||
|
||||
@mock.patch.object(drac_power, '_get_power_state')
|
||||
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
|
||||
|
||||
self.assertEqual(states.POWER_ON, driver.get_power_state(task))
|
||||
mock_get_power_state.assert_called_once_with(task.node)
|
||||
|
||||
@mock.patch.object(drac_power, '_set_power_state')
|
||||
def test_set_power_state(self, mock_set_power_state):
|
||||
driver = drac_power.DracPower()
|
||||
task = mock.Mock()
|
||||
task.node.return_value = self.node
|
||||
|
||||
driver.set_power_state(task, states.POWER_ON)
|
||||
mock_set_power_state.assert_called_once_with(task.node,
|
||||
states.POWER_ON)
|
||||
|
||||
@mock.patch.object(drac_power, '_set_power_state')
|
||||
def test_reboot(self, mock_set_power_state):
|
||||
driver = drac_power.DracPower()
|
||||
task = mock.Mock()
|
||||
task.node.return_value = self.node
|
||||
|
||||
driver.reboot(task)
|
||||
mock_set_power_state.assert_called_once_with(task.node,
|
||||
states.REBOOT)
|
@ -85,6 +85,19 @@ if 'ironic.drivers.ilo' in sys.modules:
|
||||
reload(sys.modules['ironic.drivers.ilo'])
|
||||
|
||||
|
||||
# attempt to load the external 'pywsman' library, which is required by
|
||||
# the optional drivers.modules.drac module
|
||||
pywsman = importutils.try_import('pywsman')
|
||||
if not pywsman:
|
||||
pywsman = mock.Mock()
|
||||
sys.modules['pywsman'] = pywsman
|
||||
|
||||
# if anything has loaded the drac driver yet, reload it now that the
|
||||
# external library has been mocked
|
||||
if 'ironic.drivers.modules.drac' in sys.modules:
|
||||
reload(sys.modules['ironic.drivers.modules.drac'])
|
||||
|
||||
|
||||
# attempt to load the external 'iboot' library, which is required by
|
||||
# the optional drivers.modules.iboot module
|
||||
iboot = importutils.try_import("iboot")
|
||||
|
@ -42,12 +42,14 @@ ironic.drivers =
|
||||
fake_seamicro = ironic.drivers.fake:FakeSeaMicroDriver
|
||||
fake_iboot = ironic.drivers.fake:FakeIBootDriver
|
||||
fake_ilo = ironic.drivers.fake:FakeIloDriver
|
||||
fake_drac = ironic.drivers.fake:FakeDracDriver
|
||||
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
|
||||
pxe_ilo = ironic.drivers.pxe:PXEAndIloDriver
|
||||
pxe_drac = ironic.drivers.drac:PXEDracDriver
|
||||
|
||||
[pbr]
|
||||
autodoc_index_modules = True
|
||||
|
Loading…
Reference in New Issue
Block a user