summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorRushil Chugh <rushil.chugh@gmail.com>2017-11-13 11:38:17 -0500
committerRushil Chugh <rushil.chugh@gmail.com>2018-01-22 08:39:09 -0500
commit346a9a3bfc5312deb78bda8a82ae238e031413bd (patch)
tree6738c77c9d02115b2e19d0e8769f62be1a342165
parent4a74c3406115902078586d9440573d1c6fe19380 (diff)
downloadironic-346a9a3bfc5312deb78bda8a82ae238e031413bd.tar.gz
Add XClarity Driver
This patch proposes to add new interfaces for management and power for the Lenovo XClarity Driver. Change-Id: Ic2743f9a4959a6165a7ec40f4772afb231205f36 Closes-Bug: #1702508
-rw-r--r--driver-requirements.txt1
-rw-r--r--etc/ironic/ironic.conf.sample21
-rw-r--r--ironic/conf/__init__.py2
-rw-r--r--ironic/conf/opts.py1
-rw-r--r--ironic/conf/xclarity.py33
-rw-r--r--ironic/drivers/modules/xclarity/__init__.py0
-rw-r--r--ironic/drivers/modules/xclarity/common.py138
-rw-r--r--ironic/drivers/modules/xclarity/management.py219
-rw-r--r--ironic/drivers/modules/xclarity/power.py112
-rw-r--r--ironic/drivers/xclarity.py35
-rw-r--r--ironic/tests/unit/db/utils.py15
-rw-r--r--ironic/tests/unit/drivers/modules/xclarity/__init__.py0
-rw-r--r--ironic/tests/unit/drivers/modules/xclarity/test_common.py65
-rw-r--r--ironic/tests/unit/drivers/modules/xclarity/test_management.py125
-rw-r--r--ironic/tests/unit/drivers/modules/xclarity/test_power.py113
-rw-r--r--ironic/tests/unit/drivers/test_xclarity.py49
-rw-r--r--ironic/tests/unit/drivers/third_party_driver_mock_specs.py18
-rw-r--r--ironic/tests/unit/drivers/third_party_driver_mocks.py18
-rw-r--r--releasenotes/notes/xclarity-driver-622800d17459e3f9.yaml9
-rw-r--r--setup.cfg3
20 files changed, 977 insertions, 0 deletions
diff --git a/driver-requirements.txt b/driver-requirements.txt
index 952f3a4e7..809093288 100644
--- a/driver-requirements.txt
+++ b/driver-requirements.txt
@@ -11,6 +11,7 @@ python-oneviewclient<3.0.0,>=2.5.2
python-scciclient>=0.6.0
UcsSdk==0.8.2.2
python-dracclient>=1.3.0
+python-xclarityclient>=0.1.6
# The CIMC drivers use the Cisco IMC SDK version 0.7.2 or greater
ImcSdk>=0.7.2
diff --git a/etc/ironic/ironic.conf.sample b/etc/ironic/ironic.conf.sample
index 1ef285d50..c31db8e79 100644
--- a/etc/ironic/ironic.conf.sample
+++ b/etc/ironic/ironic.conf.sample
@@ -4224,3 +4224,24 @@
# for endpoint URL discovery. Mutually exclusive with
# min_version and max_version (string value)
#version = <None>
+
+
+[xclarity]
+
+#
+# From ironic
+#
+
+# IP address of XClarity controller. (string value)
+#manager_ip = <None>
+
+# Username to access the XClarity controller. (string value)
+#username = <None>
+
+# Password for XClarity controller username. (string value)
+#password = <None>
+
+# Port to be used for XClarity operations. (port value)
+# Minimum value: 0
+# Maximum value: 65535
+#port = 443
diff --git a/ironic/conf/__init__.py b/ironic/conf/__init__.py
index e68bfaf5e..da095e2ba 100644
--- a/ironic/conf/__init__.py
+++ b/ironic/conf/__init__.py
@@ -44,6 +44,7 @@ from ironic.conf import redfish
from ironic.conf import service_catalog
from ironic.conf import snmp
from ironic.conf import swift
+from ironic.conf import xclarity
CONF = cfg.CONF
@@ -76,3 +77,4 @@ redfish.register_opts(CONF)
service_catalog.register_opts(CONF)
snmp.register_opts(CONF)
swift.register_opts(CONF)
+xclarity.register_opts(CONF)
diff --git a/ironic/conf/opts.py b/ironic/conf/opts.py
index 34ba57bf1..4dd446b88 100644
--- a/ironic/conf/opts.py
+++ b/ironic/conf/opts.py
@@ -61,6 +61,7 @@ _opts = [
('service_catalog', ironic.conf.service_catalog.list_opts()),
('snmp', ironic.conf.snmp.opts),
('swift', ironic.conf.swift.list_opts()),
+ ('xclarity', ironic.conf.xclarity.opts),
]
diff --git a/ironic/conf/xclarity.py b/ironic/conf/xclarity.py
new file mode 100644
index 000000000..a595126ba
--- /dev/null
+++ b/ironic/conf/xclarity.py
@@ -0,0 +1,33 @@
+# Copyright 2017 LENOVO Development Company, LP
+#
+# 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.
+
+from oslo_config import cfg
+
+from ironic.common.i18n import _
+
+opts = [
+ cfg.StrOpt('manager_ip',
+ help=_('IP address of XClarity controller.')),
+ cfg.StrOpt('username',
+ help=_('Username to access the XClarity controller.')),
+ cfg.StrOpt('password',
+ help=_('Password for XClarity controller username.')),
+ cfg.PortOpt('port',
+ default=443,
+ help=_('Port to be used for XClarity operations.')),
+]
+
+
+def register_opts(conf):
+ conf.register_opts(opts, group='xclarity')
diff --git a/ironic/drivers/modules/xclarity/__init__.py b/ironic/drivers/modules/xclarity/__init__.py
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/ironic/drivers/modules/xclarity/__init__.py
diff --git a/ironic/drivers/modules/xclarity/common.py b/ironic/drivers/modules/xclarity/common.py
new file mode 100644
index 000000000..ee35a843c
--- /dev/null
+++ b/ironic/drivers/modules/xclarity/common.py
@@ -0,0 +1,138 @@
+# Copyright 2017 Lenovo, Inc.
+#
+# 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.
+
+from oslo_log import log as logging
+from oslo_utils import importutils
+
+from ironic.common import exception
+from ironic.common.i18n import _
+from ironic.common import states
+from ironic.conf import CONF
+
+LOG = logging.getLogger(__name__)
+
+client = importutils.try_import('xclarity_client.client')
+xclarity_client_constants = importutils.try_import('xclarity_client.constants')
+xclarity_client_exceptions = importutils.try_import(
+ 'xclarity_client.exceptions')
+
+REQUIRED_ON_DRIVER_INFO = {
+ 'xclarity_hardware_id': _("XClarity Server Hardware ID. "
+ "Required in driver_info."),
+}
+
+COMMON_PROPERTIES = {
+ 'xclarity_address': _("IP address of the XClarity node."),
+ 'xclarity_username': _("Username for the XClarity with administrator "
+ "privileges."),
+ 'xclarity_password': _("Password for xclarity_username."),
+ 'xclarity_port': _("Port to be used for xclarity_username."),
+}
+
+COMMON_PROPERTIES.update(REQUIRED_ON_DRIVER_INFO)
+
+
+def get_properties():
+ return COMMON_PROPERTIES
+
+
+def get_xclarity_client():
+ """Generates an instance of the XClarity client.
+
+ Generates an instance of the XClarity client using the imported
+ xclarity_client library.
+
+ :returns: an instance of the XClarity client
+ :raises: XClarityError if can't get to the XClarity client
+ """
+ try:
+ xclarity_client = client.Client(
+ ip=CONF.xclarity.manager_ip,
+ username=CONF.xclarity.username,
+ password=CONF.xclarity.password,
+ port=CONF.xclarity.port
+ )
+ except xclarity_client_exceptions.XClarityError as exc:
+ msg = (_("Error getting connection to XClarity manager IP: %(ip)s. "
+ "Error: %(exc)s"), {'ip': CONF.xclarity.manager_ip,
+ 'exc': exc})
+ raise XClarityError(error=msg)
+ return xclarity_client
+
+
+def get_server_hardware_id(node):
+ """Validates node configuration and returns xclarity hardware id.
+
+ Validates whether node configutation is consistent with XClarity and
+ returns the XClarity Hardware ID for a specific node.
+ :param: node: node object to get information from
+ :returns: the XClarity Hardware ID for a specific node
+ :raises: MissingParameterValue if unable to validate XClarity Hardware ID
+
+ """
+ xclarity_hardware_id = node.driver_info.get('xclarity_hardware_id')
+ if not xclarity_hardware_id:
+ msg = (_("Error validating node driver info, "
+ "server uuid: %s missing xclarity_hardware_id") %
+ node.uuid)
+ raise exception.MissingParameterValue(error=msg)
+ return xclarity_hardware_id
+
+
+def translate_xclarity_power_state(power_state):
+ """Translates XClarity's power state strings to be consistent with Ironic.
+
+ :param: power_state: power state string to be translated
+ :returns: the translated power state
+ """
+ power_states_map = {
+ xclarity_client_constants.STATE_POWER_ON: states.POWER_ON,
+ xclarity_client_constants.STATE_POWER_OFF: states.POWER_OFF,
+ }
+
+ return power_states_map.get(power_state, states.ERROR)
+
+
+def translate_xclarity_power_action(power_action):
+ """Translates ironic's power action strings to XClarity's format.
+
+ :param: power_action: power action string to be translated
+ :returns: the power action translated
+ """
+
+ power_action_map = {
+ states.POWER_ON: xclarity_client_constants.ACTION_POWER_ON,
+ states.POWER_OFF: xclarity_client_constants.ACTION_POWER_OFF,
+ states.REBOOT: xclarity_client_constants.ACTION_REBOOT
+ }
+
+ return power_action_map[power_action]
+
+
+def is_node_managed_by_xclarity(xclarity_client, node):
+ """Determines whether dynamic allocation is enabled for a specifc node.
+
+ :param: xclarity_client: an instance of the XClarity client
+ :param: node: node object to get information from
+ :returns: Boolean depending on whether node is managed by XClarity
+ """
+ try:
+ hardware_id = get_server_hardware_id(node)
+ return xclarity_client.is_node_managed(hardware_id)
+ except exception.MissingParameterValue:
+ return False
+
+
+class XClarityError(exception.IronicException):
+ _msg_fmt = _("XClarity exception occurred. Error: %(error)s")
diff --git a/ironic/drivers/modules/xclarity/management.py b/ironic/drivers/modules/xclarity/management.py
new file mode 100644
index 000000000..c892687d1
--- /dev/null
+++ b/ironic/drivers/modules/xclarity/management.py
@@ -0,0 +1,219 @@
+# Copyright 2017 Lenovo, Inc.
+#
+# 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.
+
+
+from ironic_lib import metrics_utils
+from oslo_log import log as logging
+from oslo_utils import importutils
+
+from ironic.common import boot_devices
+from ironic.common import exception
+from ironic.common.i18n import _
+from ironic.conductor import task_manager
+from ironic.drivers import base
+from ironic.drivers.modules.xclarity import common
+
+LOG = logging.getLogger(__name__)
+
+METRICS = metrics_utils.get_metrics_logger(__name__)
+
+xclarity_client_exceptions = importutils.try_import(
+ 'xclarity_client.exceptions')
+
+BOOT_DEVICE_MAPPING_TO_XCLARITY = {
+ boot_devices.PXE: 'PXE Network',
+ boot_devices.DISK: 'Hard Disk 0',
+ boot_devices.CDROM: 'CD/DVD Rom',
+ boot_devices.BIOS: 'Boot To F1'
+}
+
+SUPPORTED_BOOT_DEVICES = [
+ boot_devices.PXE,
+ boot_devices.DISK,
+ boot_devices.CDROM,
+ boot_devices.BIOS,
+]
+
+
+class XClarityManagement(base.ManagementInterface):
+ def __init__(self):
+ super(XClarityManagement, self).__init__()
+ self.xclarity_client = common.get_xclarity_client()
+
+ def get_properties(self):
+ return common.COMMON_PROPERTIES
+
+ @METRICS.timer('XClarityManagement.validate')
+ def validate(self, task):
+ """It validates if the node is being used by XClarity.
+
+ :param task: a task from TaskManager.
+ """
+ common.is_node_managed_by_xclarity(self.xclarity_client, task.node)
+
+ @METRICS.timer('XClarityManagement.get_supported_boot_devices')
+ def get_supported_boot_devices(self, task):
+ """Gets a list of the supported boot devices.
+
+ :param task: a task from TaskManager.
+ :returns: A list with the supported boot devices defined
+ in :mod:`ironic.common.boot_devices`.
+ """
+
+ return SUPPORTED_BOOT_DEVICES
+
+ def _validate_supported_boot_device(self, task, boot_device):
+ """It validates if the boot device is supported by XClarity.
+
+ :param task: a task from TaskManager.
+ :param boot_device: the boot device, one of [PXE, DISK, CDROM, BIOS]
+ :raises: InvalidParameterValue if the boot device is not supported.
+ """
+ if boot_device not in SUPPORTED_BOOT_DEVICES:
+ raise exception.InvalidParameterValue(
+ _("Unsupported boot device %(device)s for node: %(node)s ")
+ % {"device": boot_device, "node": task.node.uuid}
+ )
+
+ @METRICS.timer('XClarityManagement.get_boot_device')
+ def get_boot_device(self, task):
+ """Get the current boot device for the task's node.
+
+ :param task: a task from TaskManager.
+ :returns: a dictionary containing:
+ :boot_device: the boot device, one of [PXE, DISK, CDROM, BIOS]
+ :persistent: Whether the boot device will persist or not
+ :raises: InvalidParameterValue if the boot device is unknown
+ :raises: XClarityError if the communication with XClarity fails
+ """
+ server_hardware_id = common.get_server_hardware_id(task.node)
+ try:
+ boot_info = (
+ self.xclarity_client.get_node_all_boot_info(
+ server_hardware_id)
+ )
+ except xclarity_client_exceptions.XClarityError as xclarity_exc:
+ LOG.error(
+ "Error getting boot device from XClarity for node %(node)s. "
+ "Error: %(error)s", {'node': task.node.uuid,
+ 'error': xclarity_exc})
+ raise common.XClarityError(error=xclarity_exc)
+
+ persistent = False
+ primary = None
+ boot_order = boot_info['bootOrder']['bootOrderList']
+ for item in boot_order:
+ current = item.get('currentBootOrderDevices', None)
+ boot_type = item.get('bootType', None)
+ if boot_type == "SingleUse":
+ persistent = False
+ primary = current[0]
+ if primary != 'None':
+ boot_device = {'boot_device': primary,
+ 'persistent': persistent}
+ self._validate_whether_supported_boot_device(primary)
+ return boot_device
+ elif boot_type == "Permanent":
+ persistent = True
+ boot_device = {'boot_device': current[0],
+ 'persistent': persistent}
+ self._validate_supported_boot_device(task, primary)
+ return boot_device
+
+ @METRICS.timer('XClarityManagement.set_boot_device')
+ @task_manager.require_exclusive_lock
+ def set_boot_device(self, task, device, persistent=False):
+ """Sets the boot device for a node.
+
+ :param task: a task from TaskManager.
+ :param device: the boot device, one of the supported devices
+ listed in :mod:`ironic.common.boot_devices`.
+ :param persistent: Boolean value. True if the boot device will
+ persist to all future boots, False if not.
+ Default: False.
+ :raises: InvalidParameterValue if an invalid boot device is
+ specified.
+ :raises: XClarityError if the communication with XClarity fails
+ """
+ self._validate_supported_boot_device(task=task, boot_device=device)
+
+ server_hardware_id = task.node.driver_info.get('server_hardware_id')
+ LOG.debug("Setting boot device to %(device)s for node %(node)s",
+ {"device": device, "node": task.node.uuid})
+ self._set_boot_device(task, server_hardware_id, device,
+ singleuse=not persistent)
+
+ @METRICS.timer('XClarityManagement.get_sensors_data')
+ def get_sensors_data(self, task):
+ """Get sensors data.
+
+ :param task: a TaskManager instance.
+ :raises: NotImplementedError
+
+ """
+ raise NotImplementedError()
+
+ def _translate_ironic_to_xclarity(self, boot_device):
+ """Translates Ironic boot options to Xclarity boot options.
+
+ :param boot_device: Ironic boot_device
+ :returns: Translated XClarity boot_device.
+
+ """
+ return BOOT_DEVICE_MAPPING_TO_XCLARITY.get(boot_device)
+
+ def _set_boot_device(self, task, server_hardware_id,
+ new_primary_boot_device, singleuse=False):
+ """Set the current boot device for xclarity
+
+ :param server_hardware_id: the uri of the server hardware in XClarity
+ :param new_primary_boot_device: boot device to be set
+ :param task: a TaskManager instance.
+ :param singleuse: if this device will be used only once at next boot
+ """
+ boot_info = self.xclarity_client.get_node_all_boot_info(
+ server_hardware_id)
+ xclarity_boot_device = self._translate_ironic_to_xclarity(
+ new_primary_boot_device)
+ current = []
+ LOG.debug(
+ ("Setting boot device to %(device)s for XClarity "
+ "node %(node)s"),
+ {'device': xclarity_boot_device, 'node': task.node.uuid}
+ )
+ for item in boot_info['bootOrder']['bootOrderList']:
+ if singleuse and item['bootType'] == 'SingleUse':
+ item['currentBootOrderDevices'][0] = xclarity_boot_device
+ elif not singleuse and item['bootType'] == 'Permanent':
+ current = item['currentBootOrderDevices']
+ if xclarity_boot_device == current[0]:
+ return
+ if xclarity_boot_device in current:
+ current.remove(xclarity_boot_device)
+ current.insert(0, xclarity_boot_device)
+ item['currentBootOrderDevices'] = current
+
+ try:
+ self.xclarity_client.set_node_boot_info(server_hardware_id,
+ boot_info,
+ xclarity_boot_device,
+ singleuse)
+ except xclarity_client_exceptions.XClarityError as xclarity_exc:
+ LOG.error(
+ ('Error setting boot device %(boot_device)s for the XClarity '
+ 'node %(node)s. Error: %(error)s'),
+ {'boot_device': xclarity_boot_device, 'node': task.node.uuid,
+ 'error': xclarity_exc}
+ )
+ raise common.XClarityError(error=xclarity_exc)
diff --git a/ironic/drivers/modules/xclarity/power.py b/ironic/drivers/modules/xclarity/power.py
new file mode 100644
index 000000000..b4ed8a8c3
--- /dev/null
+++ b/ironic/drivers/modules/xclarity/power.py
@@ -0,0 +1,112 @@
+# Copyright 2017 Lenovo, Inc.
+#
+# 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.
+
+from ironic_lib import metrics_utils
+from oslo_log import log as logging
+from oslo_utils import importutils
+
+from ironic.common import states
+from ironic.conductor import task_manager
+from ironic.drivers import base
+from ironic.drivers.modules.xclarity import common
+
+LOG = logging.getLogger(__name__)
+
+METRICS = metrics_utils.get_metrics_logger(__name__)
+
+xclarity_client_exceptions = importutils.try_import(
+ 'xclarity_client.exceptions')
+
+
+class XClarityPower(base.PowerInterface):
+ def __init__(self):
+ super(XClarityPower, self).__init__()
+ self.xclarity_client = common.get_xclarity_client()
+
+ def get_properties(self):
+ return common.get_properties()
+
+ @METRICS.timer('XClarityPower.validate')
+ def validate(self, task):
+ """It validates if the node is being used by XClarity.
+
+ :param task: a task from TaskManager.
+ """
+
+ common.is_node_managed_by_xclarity(self.xclarity_client, task.node)
+
+ @METRICS.timer('XClarityPower.get_power_state')
+ def get_power_state(self, task):
+ """Gets the current power state.
+
+ :param task: a TaskManager instance.
+ :returns: one of :mod:`ironic.common.states` POWER_OFF,
+ POWER_ON or ERROR.
+ :raises: XClarityError if fails to retrieve power state of XClarity
+ resource
+ """
+ server_hardware_id = common.get_server_hardware_id(task.node)
+ try:
+ power_state = self.xclarity_client.get_node_power_status(
+ server_hardware_id)
+ except xclarity_client_exceptions.XClarityException as xclarity_exc:
+ LOG.error(
+ ("Error getting power state for node %(node)s. Error: "
+ "%(error)s"),
+ {'node': task.node.uuid, 'error': xclarity_exc}
+ )
+ raise common.XClarityError(error=xclarity_exc)
+ return common.translate_xclarity_power_state(power_state)
+
+ @METRICS.timer('XClarityPower.set_power_state')
+ @task_manager.require_exclusive_lock
+ def set_power_state(self, task, power_state):
+ """Turn the current power state on or off.
+
+ :param task: a TaskManager instance.
+ :param power_state: The desired power state POWER_ON, POWER_OFF or
+ REBOOT from :mod:`ironic.common.states`.
+ :raises: InvalidParameterValue if an invalid power state was specified.
+ :raises: XClarityError if XClarity fails setting the power state.
+ """
+
+ if power_state == states.REBOOT:
+ target_power_state = self.get_power_state(task)
+ if target_power_state == states.POWER_OFF:
+ power_state = states.POWER_ON
+
+ server_hardware_id = common.get_server_hardware_id(task.node)
+ LOG.debug("Setting power state of node %(node_uuid)s to "
+ "%(power_state)s",
+ {'node_uuid': task.node.uuid, 'power_state': power_state})
+
+ try:
+ self.xclarity_client.set_node_power_status(server_hardware_id,
+ power_state)
+ except xclarity_client_exceptions.XClarityError as xclarity_exc:
+ LOG.error(
+ "Error setting power state of node %(node_uuid)s to "
+ "%(power_state)s",
+ {'node_uuid': task.node.uuid, 'power_state': power_state})
+ raise common.XClarityError(error=xclarity_exc)
+
+ @METRICS.timer('XClarityPower.reboot')
+ @task_manager.require_exclusive_lock
+ def reboot(self, task):
+ """Reboot the node
+
+ :param task: a TaskManager instance.
+ """
+
+ self.set_power_state(task, states.REBOOT)
diff --git a/ironic/drivers/xclarity.py b/ironic/drivers/xclarity.py
new file mode 100644
index 000000000..87b356995
--- /dev/null
+++ b/ironic/drivers/xclarity.py
@@ -0,0 +1,35 @@
+# Copyright 2017 Lenovo, Inc.
+#
+# 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.
+
+"""
+XClarity Driver and supporting meta-classes.
+"""
+
+from ironic.drivers import generic
+from ironic.drivers.modules.xclarity import management
+from ironic.drivers.modules.xclarity import power
+
+
+class XClarityHardware(generic.GenericHardware):
+ """XClarity hardware type. """
+
+ @property
+ def supported_management_interfaces(self):
+ """List of supported management interfaces."""
+ return [management.XClarityManagement]
+
+ @property
+ def supported_power_interfaces(self):
+ """List of supported power interfaces."""
+ return [power.XClarityPower]
diff --git a/ironic/tests/unit/db/utils.py b/ironic/tests/unit/db/utils.py
index 946a4e3d6..92bd835bd 100644
--- a/ironic/tests/unit/db/utils.py
+++ b/ironic/tests/unit/db/utils.py
@@ -491,6 +491,21 @@ def create_test_node_tag(**kw):
return dbapi.add_node_tag(tag['node_id'], tag['tag'])
+def get_test_xclarity_properties():
+ return {
+ "cpu_arch": "x86_64",
+ "cpus": "8",
+ "local_gb": "10",
+ "memory_mb": "4096",
+ }
+
+
+def get_test_xclarity_driver_info():
+ return {
+ 'xclarity_hardware_id': 'fake_sh_id',
+ }
+
+
def get_test_node_trait(**kw):
return {
# TODO(mgoddard): Replace None below with the NodeTrait RPC object
diff --git a/ironic/tests/unit/drivers/modules/xclarity/__init__.py b/ironic/tests/unit/drivers/modules/xclarity/__init__.py
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/ironic/tests/unit/drivers/modules/xclarity/__init__.py
diff --git a/ironic/tests/unit/drivers/modules/xclarity/test_common.py b/ironic/tests/unit/drivers/modules/xclarity/test_common.py
new file mode 100644
index 000000000..563e48f06
--- /dev/null
+++ b/ironic/tests/unit/drivers/modules/xclarity/test_common.py
@@ -0,0 +1,65 @@
+# Copyright 2017 Lenovo, Inc.
+# All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+import mock
+
+from oslo_utils import importutils
+
+from ironic.drivers.modules.xclarity import common
+from ironic.tests.unit.db import base as db_base
+from ironic.tests.unit.db import utils as db_utils
+from ironic.tests.unit.objects import utils as obj_utils
+
+xclarity_exceptions = importutils.try_import('xclarity_client.exceptions')
+xclarity_constants = importutils.try_import('xclarity_client.constants')
+
+
+class XClarityCommonTestCase(db_base.DbTestCase):
+
+ def setUp(self):
+ super(XClarityCommonTestCase, self).setUp()
+
+ self.config(manager_ip='1.2.3.4', group='xclarity')
+ self.config(username='user', group='xclarity')
+ self.config(password='password', group='xclarity')
+
+ self.node = obj_utils.create_test_node(
+ self.context, driver='fake-xclarity',
+ properties=db_utils.get_test_xclarity_properties(),
+ driver_info=db_utils.get_test_xclarity_driver_info(),
+ )
+
+ def test_get_server_hardware_id(self):
+ driver_info = self.node.driver_info
+ driver_info['xclarity_hardware_id'] = 'test'
+ self.node.driver_info = driver_info
+ result = common.get_server_hardware_id(self.node)
+ self.assertEqual(result, 'test')
+
+ @mock.patch.object(common, 'get_server_hardware_id',
+ spec_set=True, autospec=True)
+ @mock.patch.object(common, 'get_xclarity_client',
+ spec_set=True, autospec=True)
+ def test_check_node_managed_by_xclarity(self, mock_xc_client,
+ mock_validate_driver_info):
+ driver_info = self.node.driver_info
+ driver_info['xclarity_hardware_id'] = 'abcd'
+ self.node.driver_info = driver_info
+
+ xclarity_client = mock_xc_client()
+ mock_validate_driver_info.return_value = '12345'
+ common.is_node_managed_by_xclarity(xclarity_client,
+ self.node)
+ xclarity_client.is_node_managed.assert_called_once_with('12345')
diff --git a/ironic/tests/unit/drivers/modules/xclarity/test_management.py b/ironic/tests/unit/drivers/modules/xclarity/test_management.py
new file mode 100644
index 000000000..1d4fc9209
--- /dev/null
+++ b/ironic/tests/unit/drivers/modules/xclarity/test_management.py
@@ -0,0 +1,125 @@
+# Copyright 2017 Lenovo, Inc.
+# All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+import sys
+
+import six
+
+import mock
+
+from oslo_utils import importutils
+
+from ironic.common import boot_devices
+from ironic.conductor import task_manager
+from ironic.drivers.modules.xclarity import common
+from ironic.drivers.modules.xclarity import management
+from ironic.tests.unit.conductor import mgr_utils
+from ironic.tests.unit.db import base as db_base
+from ironic.tests.unit.db import utils as db_utils
+from ironic.tests.unit.objects import utils as obj_utils
+
+
+xclarity_client_exceptions = importutils.try_import(
+ 'xclarity_client.exceptions')
+
+
+@mock.patch.object(common, 'get_xclarity_client', spect_set=True,
+ autospec=True)
+class XClarityManagementDriverTestCase(db_base.DbTestCase):
+
+ def setUp(self):
+ super(XClarityManagementDriverTestCase, self).setUp()
+ self.config(enabled_hardware_types=['xclarity'],
+ enabled_power_interfaces=['xclarity'],
+ enabled_management_interfaces=['xclarity'])
+ mgr_utils.mock_the_extension_manager(
+ driver='xclarity', namespace='ironic.hardware.types')
+ self.node = obj_utils.create_test_node(
+ self.context,
+ driver='xclarity',
+ driver_info=db_utils.get_test_xclarity_driver_info())
+
+ @mock.patch.object(common, 'get_server_hardware_id',
+ spect_set=True, autospec=True)
+ def test_validate(self, mock_validate, mock_get_xc_client):
+ with task_manager.acquire(self.context, self.node.uuid) as task:
+ task.driver.management.validate(task)
+ common.get_server_hardware_id(task.node)
+ mock_validate.assert_called_with(task.node)
+
+ def test_get_properties(self, mock_get_xc_client):
+
+ expected = common.REQUIRED_ON_DRIVER_INFO
+ self.assertItemsEqual(expected,
+ self.node.driver_info)
+
+ @mock.patch.object(management.XClarityManagement, 'get_boot_device',
+ return_value='pxe')
+ def test_set_boot_device(self, mock_get_boot_device,
+ mock_get_xc_client):
+ with task_manager.acquire(self.context, self.node.uuid) as task:
+ task.driver.management.set_boot_device(task, 'pxe')
+ result = task.driver.management.get_boot_device(task)
+ self.assertEqual(result, 'pxe')
+
+ def test_set_boot_device_fail(self, mock_get_xc_client):
+ with task_manager.acquire(self.context, self.node.uuid) as task:
+ xclarity_client_exceptions.XClarityError = Exception
+ sys.modules['xclarity_client.exceptions'] = (
+ xclarity_client_exceptions)
+ if 'ironic.drivers.modules.xclarity' in sys.modules:
+ six.moves.reload_module(
+ sys.modules['ironic.drivers.modules.xclarity'])
+ ex = common.XClarityError('E')
+ mock_get_xc_client.return_value.set_node_boot_info.side_effect = ex
+ self.assertRaises(common.XClarityError,
+ task.driver.management.set_boot_device,
+ task,
+ "pxe")
+
+ def test_get_supported_boot_devices(self, mock_get_xc_client):
+ with task_manager.acquire(self.context, self.node.uuid) as task:
+ expected = [boot_devices.PXE, boot_devices.BIOS,
+ boot_devices.DISK, boot_devices.CDROM]
+ self.assertItemsEqual(
+ expected,
+ task.driver.management.get_supported_boot_devices(task))
+
+ @mock.patch.object(
+ management.XClarityManagement,
+ 'get_boot_device',
+ return_value={'boot_device': 'pxe', 'persistent': False})
+ def test_get_boot_device(self, mock_get_boot_device, mock_get_xc_client):
+ reference = {'boot_device': 'pxe', 'persistent': False}
+ with task_manager.acquire(self.context, self.node.uuid) as task:
+ expected_boot_device = task.driver.management.get_boot_device(
+ task=task)
+
+ self.assertEqual(reference, expected_boot_device)
+
+ def test_get_boot_device_fail(self, mock_xc_client):
+ with task_manager.acquire(self.context, self.node.uuid) as task:
+ xclarity_client_exceptions.XClarityError = Exception
+ sys.modules['xclarity_client.exceptions'] = (
+ xclarity_client_exceptions)
+ if 'ironic.drivers.modules.xclarity' in sys.modules:
+ six.moves.reload_module(
+ sys.modules['ironic.drivers.modules.xclarity'])
+ ex = common.XClarityError('E')
+ mock_xc_client.return_value.get_node_all_boot_info.side_effect = ex
+ self.assertRaises(
+ common.XClarityError,
+ task.driver.management.get_boot_device,
+ task)
diff --git a/ironic/tests/unit/drivers/modules/xclarity/test_power.py b/ironic/tests/unit/drivers/modules/xclarity/test_power.py
new file mode 100644
index 000000000..f695c4c29
--- /dev/null
+++ b/ironic/tests/unit/drivers/modules/xclarity/test_power.py
@@ -0,0 +1,113 @@
+# Copyright 2017 Lenovo, Inc.
+# All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+STATE_POWER_ON = "power on"
+STATE_POWER_OFF = "power off"
+STATE_POWERING_ON = "power on"
+STATE_POWERING_OFF = "power on"
+
+import sys
+
+import six
+
+import mock
+
+from oslo_utils import importutils
+
+from ironic.common import states
+from ironic.conductor import task_manager
+from ironic.drivers.modules.xclarity import common
+from ironic.drivers.modules.xclarity import power
+from ironic.tests.unit.conductor import mgr_utils
+from ironic.tests.unit.db import base as db_base
+from ironic.tests.unit.db import utils as db_utils
+from ironic.tests.unit.objects import utils as obj_utils
+
+xclarity_constants = importutils.try_import('xclarity_client.constants')
+xclarity_client_exceptions = importutils.try_import(
+ 'xclarity_client.exceptions')
+
+
+@mock.patch.object(common, 'get_xclarity_client',
+ spect_set=True, autospec=True)
+class XClarityPowerDriverTestCase(db_base.DbTestCase):
+
+ def setUp(self):
+ super(XClarityPowerDriverTestCase, self).setUp()
+ self.config(enabled_hardware_types=['xclarity'],
+ enabled_power_interfaces=['xclarity'],
+ enabled_management_interfaces=['xclarity'])
+ mgr_utils.mock_the_extension_manager(
+ driver='xclarity', namespace='ironic.hardware.types')
+ self.node = obj_utils.create_test_node(
+ self.context,
+ driver='xclarity',
+ driver_info=db_utils.get_test_xclarity_driver_info())
+
+ def test_get_properties(self, mock_get_xc_client):
+ expected = common.REQUIRED_ON_DRIVER_INFO
+ self.assertItemsEqual(expected,
+ self.node.driver_info)
+
+ @mock.patch.object(common, 'get_server_hardware_id',
+ spect_set=True, autospec=True)
+ def test_validate(self, mock_validate_driver_info, mock_get_xc_client):
+ with task_manager.acquire(self.context, self.node.uuid) as task:
+ task.driver.power.validate(task)
+ common.get_server_hardware_id(task.node)
+ mock_validate_driver_info.assert_called_with(task.node)
+
+ @mock.patch.object(power.XClarityPower, 'get_power_state',
+ return_value=STATE_POWER_ON)
+ def test_get_power_state(self, mock_get_power_state, mock_get_xc_client):
+ with task_manager.acquire(self.context, self.node.uuid) as task:
+ result = power.XClarityPower.get_power_state(task)
+ self.assertEqual(STATE_POWER_ON, result)
+
+ def test_get_power_state_fail(self, mock_xc_client):
+ with task_manager.acquire(self.context, self.node.uuid) as task:
+ xclarity_client_exceptions.XClarityError = Exception
+ sys.modules['xclarity_client.exceptions'] = (
+ xclarity_client_exceptions)
+ if 'ironic.drivers.modules.xclarity' in sys.modules:
+ six.moves.reload_module(
+ sys.modules['ironic.drivers.modules.xclarity'])
+ ex = common.XClarityError('E')
+ mock_xc_client.return_value.get_node_power_status.side_effect = ex
+ self.assertRaises(common.XClarityError,
+ task.driver.power.get_power_state,
+ task)
+
+ @mock.patch.object(power.XClarityPower, 'get_power_state',
+ return_value=states.POWER_ON)
+ def test_set_power(self, mock_set_power_state, mock_get_xc_client):
+ with task_manager.acquire(self.context, self.node.uuid) as task:
+ task.driver.power.set_power_state(task, states.POWER_ON)
+ expected = task.driver.power.get_power_state(task)
+ self.assertEqual(expected, states.POWER_ON)
+
+ def test_set_power_fail(self, mock_xc_client):
+ with task_manager.acquire(self.context, self.node.uuid) as task:
+ xclarity_client_exceptions.XClarityError = Exception
+ sys.modules['xclarity_client.exceptions'] = (
+ xclarity_client_exceptions)
+ if 'ironic.drivers.modules.xclarity' in sys.modules:
+ six.moves.reload_module(
+ sys.modules['ironic.drivers.modules.xclarity'])
+ ex = common.XClarityError('E')
+ mock_xc_client.return_value.set_node_power_status.side_effect = ex
+ self.assertRaises(common.XClarityError,
+ task.driver.power.set_power_state,
+ task, states.POWER_OFF)
diff --git a/ironic/tests/unit/drivers/test_xclarity.py b/ironic/tests/unit/drivers/test_xclarity.py
new file mode 100644
index 000000000..fdd94852f
--- /dev/null
+++ b/ironic/tests/unit/drivers/test_xclarity.py
@@ -0,0 +1,49 @@
+# Copyright 2017 Lenovo, Inc.
+#
+# 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 XClarity Driver
+"""
+
+from ironic.conductor import task_manager
+from ironic.drivers.modules import agent
+from ironic.drivers.modules import iscsi_deploy
+from ironic.drivers.modules import pxe
+from ironic.drivers.xclarity import management as xc_management
+from ironic.drivers.xclarity import power as xc_power
+
+from ironic.tests.unit.db import base as db_base
+from ironic.tests.unit.objects import utils as obj_utils
+
+
+class XClarityHardwareTestCase(db_base.DbTestCase):
+
+ def setUp(self):
+ super(XClarityHardwareTestCase, self).setUp()
+ self.config(enabled_hardware_types=['xclarity'],
+ enabled_power_interfaces=['xclarity'],
+ enabled_management_interfaces=['xclarity'])
+
+ def test_default_interfaces(self):
+ node = obj_utils.create_test_node(self.context, driver='xclarity')
+ with task_manager.acquire(self.context, node.id) as task:
+ self.assertIsInstance(task.driver.boot,
+ pxe.PXEBoot)
+ self.assertIsInstance(task.driver.deploy,
+ iscsi_deploy.ISCSIDeploy,
+ agent.AgentDeploy)
+ self.assertIsInstance(task.driver.management,
+ xc_management.XClarityManagement)
+ self.assertIsInstance(task.driver.power,
+ xc_power.XClarityPower)
diff --git a/ironic/tests/unit/drivers/third_party_driver_mock_specs.py b/ironic/tests/unit/drivers/third_party_driver_mock_specs.py
index f21adafd4..9f1535ed9 100644
--- a/ironic/tests/unit/drivers/third_party_driver_mock_specs.py
+++ b/ironic/tests/unit/drivers/third_party_driver_mock_specs.py
@@ -162,3 +162,21 @@ SUSHY_CONSTANTS_SPEC = (
'BOOT_SOURCE_ENABLED_CONTINUOUS',
'BOOT_SOURCE_ENABLED_ONCE',
)
+
+XCLARITY_SPEC = (
+ 'client',
+ 'states',
+ 'exceptions',
+ 'models',
+ 'utils',
+)
+
+XCLARITY_CLIENT_CLS_SPEC = (
+)
+
+XCLARITY_STATES_SPEC = (
+ 'STATE_POWERING_OFF',
+ 'STATE_POWERING_ON',
+ 'STATE_POWER_OFF',
+ 'STATE_POWER_ON',
+)
diff --git a/ironic/tests/unit/drivers/third_party_driver_mocks.py b/ironic/tests/unit/drivers/third_party_driver_mocks.py
index 31fec78b7..e0ad499e0 100644
--- a/ironic/tests/unit/drivers/third_party_driver_mocks.py
+++ b/ironic/tests/unit/drivers/third_party_driver_mocks.py
@@ -253,3 +253,21 @@ if not sushy:
if 'ironic.drivers.modules.redfish' in sys.modules:
six.moves.reload_module(
sys.modules['ironic.drivers.modules.redfish'])
+
+xclarity_client = importutils.try_import('xclarity_client')
+if not xclarity_client:
+ xclarity_client = mock.MagicMock(spec_set=mock_specs.XCLARITY_SPEC)
+ sys.modules['xclarity_client'] = xclarity_client
+ sys.modules['xclarity_client.client'] = xclarity_client.client
+ states = mock.MagicMock(
+ spec_set=mock_specs.XCLARITY_STATES_SPEC,
+ STATE_POWER_ON="power on",
+ STATE_POWER_OFF="power off",
+ STATE_POWERING_ON="powering_on",
+ STATE_POWERING_OFF="powering_off")
+ sys.modules['xclarity_client.states'] = states
+ sys.modules['xclarity_client.exceptions'] = xclarity_client.exceptions
+ sys.modules['xclarity_client.utils'] = xclarity_client.utils
+ xclarity_client.exceptions.XClarityException = type('XClarityException',
+ (Exception,), {})
+ sys.modules['xclarity_client.models'] = xclarity_client.models
diff --git a/releasenotes/notes/xclarity-driver-622800d17459e3f9.yaml b/releasenotes/notes/xclarity-driver-622800d17459e3f9.yaml
new file mode 100644
index 000000000..e9a83acf8
--- /dev/null
+++ b/releasenotes/notes/xclarity-driver-622800d17459e3f9.yaml
@@ -0,0 +1,9 @@
+---
+
+features:
+ - |
+ Adds the new ``xclarity`` hardware type for managing Lenovo server
+ hardware with the following interfaces:
+
+ * management: ``xclarity``
+ * power: ``xclarity``
diff --git a/setup.cfg b/setup.cfg
index 3dbdd1589..85097253b 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -127,6 +127,7 @@ ironic.hardware.interfaces.management =
oneview = ironic.drivers.modules.oneview.management:OneViewManagement
redfish = ironic.drivers.modules.redfish.management:RedfishManagement
ucsm = ironic.drivers.modules.ucs.management:UcsManagement
+ xclarity = ironic.drivers.modules.xclarity.management:XClarityManagement
ironic.hardware.interfaces.network =
flat = ironic.drivers.modules.network.flat:FlatNetwork
@@ -144,6 +145,7 @@ ironic.hardware.interfaces.power =
redfish = ironic.drivers.modules.redfish.power:RedfishPower
snmp = ironic.drivers.modules.snmp:SNMPPower
ucsm = ironic.drivers.modules.ucs.power:Power
+ xclarity = ironic.drivers.modules.xclarity.power:XClarityPower
ironic.hardware.interfaces.raid =
agent = ironic.drivers.modules.agent:AgentRAID
@@ -178,6 +180,7 @@ ironic.hardware.types =
oneview = ironic.drivers.oneview:OneViewHardware
redfish = ironic.drivers.redfish:RedfishHardware
snmp = ironic.drivers.snmp:SNMPHardware
+ xclarity = ironic.drivers.xclarity:XClarityHardware
ironic.database.migration_backend =
sqlalchemy = ironic.db.sqlalchemy.migration