diff --git a/driver-requirements.txt b/driver-requirements.txt index ea09434315..255a36e529 100644 --- a/driver-requirements.txt +++ b/driver-requirements.txt @@ -13,6 +13,7 @@ python-ilorest-library>=2.1.0 hpOneView>=4.4.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 eb3d1341c3..03eb8b5308 100644 --- a/etc/ironic/ironic.conf.sample +++ b/etc/ironic/ironic.conf.sample @@ -4233,3 +4233,24 @@ # for endpoint URL discovery. Mutually exclusive with # min_version and max_version (string value) #version = + + +[xclarity] + +# +# From ironic +# + +# IP address of XClarity controller. (string value) +#manager_ip = + +# Username to access the XClarity controller. (string value) +#username = + +# Password for XClarity controller username. (string value) +#password = + +# 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 e68bfaf5ed..da095e2ba7 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 34ba57bf10..4dd446b88c 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 0000000000..a595126ba5 --- /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 0000000000..e69de29bb2 diff --git a/ironic/drivers/modules/xclarity/common.py b/ironic/drivers/modules/xclarity/common.py new file mode 100644 index 0000000000..ee35a843ca --- /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 0000000000..c892687d16 --- /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 0000000000..b4ed8a8c37 --- /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 0000000000..87b3569951 --- /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 71aa3d0987..3741d090d7 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 0000000000..e69de29bb2 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 0000000000..563e48f06c --- /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 0000000000..1d4fc92095 --- /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 0000000000..f695c4c29f --- /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 0000000000..fdd94852f5 --- /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 d3c006b140..2574743c93 100644 --- a/ironic/tests/unit/drivers/third_party_driver_mock_specs.py +++ b/ironic/tests/unit/drivers/third_party_driver_mock_specs.py @@ -180,3 +180,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 430293bb6e..28e3741987 100644 --- a/ironic/tests/unit/drivers/third_party_driver_mocks.py +++ b/ironic/tests/unit/drivers/third_party_driver_mocks.py @@ -270,3 +270,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 0000000000..e9a83acf84 --- /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 3dbdd1589b..85097253bb 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