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/user-guide
|
||||||
deploy/install-guide
|
deploy/install-guide
|
||||||
|
deploy/drivers
|
||||||
|
|
||||||
Indices and tables
|
Indices and tables
|
||||||
==================
|
==================
|
||||||
|
@ -408,6 +408,17 @@ class IloOperationError(IronicException):
|
|||||||
message = _("%(operation)s failed, error: %(error)s")
|
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):
|
class FailedToGetSensorData(IronicException):
|
||||||
message = _("Failed to get sensor data for node %(node)s. "
|
message = _("Failed to get sensor data for node %(node)s. "
|
||||||
"Error: %(error)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.common import exception
|
||||||
from ironic.drivers import base
|
from ironic.drivers import base
|
||||||
from ironic.drivers.modules import agent
|
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 fake
|
||||||
from ironic.drivers.modules import iboot
|
from ironic.drivers.modules import iboot
|
||||||
from ironic.drivers.modules.ilo import power as ilo_power
|
from ironic.drivers.modules.ilo import power as ilo_power
|
||||||
@ -128,3 +129,16 @@ class FakeIloDriver(base.BaseDriver):
|
|||||||
reason=_("Unable to import proliantutils library"))
|
reason=_("Unable to import proliantutils library"))
|
||||||
self.power = ilo_power.IloPower()
|
self.power = ilo_power.IloPower()
|
||||||
self.deploy = fake.FakeDeploy()
|
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():
|
def get_test_agent_instance_info():
|
||||||
return {
|
return {
|
||||||
'image_source': 'fake-image',
|
'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'])
|
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
|
# attempt to load the external 'iboot' library, which is required by
|
||||||
# the optional drivers.modules.iboot module
|
# the optional drivers.modules.iboot module
|
||||||
iboot = importutils.try_import("iboot")
|
iboot = importutils.try_import("iboot")
|
||||||
|
@ -42,12 +42,14 @@ ironic.drivers =
|
|||||||
fake_seamicro = ironic.drivers.fake:FakeSeaMicroDriver
|
fake_seamicro = ironic.drivers.fake:FakeSeaMicroDriver
|
||||||
fake_iboot = ironic.drivers.fake:FakeIBootDriver
|
fake_iboot = ironic.drivers.fake:FakeIBootDriver
|
||||||
fake_ilo = ironic.drivers.fake:FakeIloDriver
|
fake_ilo = ironic.drivers.fake:FakeIloDriver
|
||||||
|
fake_drac = ironic.drivers.fake:FakeDracDriver
|
||||||
pxe_ipmitool = ironic.drivers.pxe:PXEAndIPMIToolDriver
|
pxe_ipmitool = ironic.drivers.pxe:PXEAndIPMIToolDriver
|
||||||
pxe_ipminative = ironic.drivers.pxe:PXEAndIPMINativeDriver
|
pxe_ipminative = ironic.drivers.pxe:PXEAndIPMINativeDriver
|
||||||
pxe_ssh = ironic.drivers.pxe:PXEAndSSHDriver
|
pxe_ssh = ironic.drivers.pxe:PXEAndSSHDriver
|
||||||
pxe_seamicro = ironic.drivers.pxe:PXEAndSeaMicroDriver
|
pxe_seamicro = ironic.drivers.pxe:PXEAndSeaMicroDriver
|
||||||
pxe_iboot = ironic.drivers.pxe:PXEAndIBootDriver
|
pxe_iboot = ironic.drivers.pxe:PXEAndIBootDriver
|
||||||
pxe_ilo = ironic.drivers.pxe:PXEAndIloDriver
|
pxe_ilo = ironic.drivers.pxe:PXEAndIloDriver
|
||||||
|
pxe_drac = ironic.drivers.drac:PXEDracDriver
|
||||||
|
|
||||||
[pbr]
|
[pbr]
|
||||||
autodoc_index_modules = True
|
autodoc_index_modules = True
|
||||||
|
Loading…
Reference in New Issue
Block a user