From 2eb3c12ce3fb2b2eb4408367f2c950724a098d93 Mon Sep 17 00:00:00 2001 From: Lucas Alvares Gomes Date: Mon, 25 Aug 2014 11:11:53 +0100 Subject: [PATCH] Implements the DRAC ManagementInterface for get/set boot device What it says. The get_sensors_data() won't be implemented as part of this patch because it's outside the scope of the blueprint. Co-Authored-By: Imre Farkas Implements blueprint drac-management-driver Change-Id: I9cae3156b6af016bfbf64cb3c117fdec8536f714 --- ironic/common/exception.py | 5 + ironic/drivers/drac.py | 9 +- ironic/drivers/fake.py | 2 + ironic/drivers/modules/drac/common.py | 9 +- ironic/drivers/modules/drac/management.py | 339 +++++++++++++++++++ ironic/drivers/modules/drac/resource_uris.py | 12 + ironic/tests/drivers/drac/test_client.py | 4 +- ironic/tests/drivers/drac/test_common.py | 22 ++ ironic/tests/drivers/drac/test_management.py | 309 +++++++++++++++++ ironic/tests/drivers/drac/test_power.py | 8 +- ironic/tests/drivers/drac/utils.py | 30 +- 11 files changed, 726 insertions(+), 23 deletions(-) create mode 100644 ironic/drivers/modules/drac/management.py create mode 100644 ironic/tests/drivers/drac/test_management.py diff --git a/ironic/common/exception.py b/ironic/common/exception.py index aeec784dc4..765ae1087c 100644 --- a/ironic/common/exception.py +++ b/ironic/common/exception.py @@ -427,6 +427,11 @@ class DracOperationError(IronicException): message = _('DRAC %(operation)s failed. Reason: %(error)s') +class DracConfigJobCreationError(DracOperationError): + message = _('DRAC failed to create a configuration job. ' + 'Reason: %(error)s') + + class FailedToGetSensorData(IronicException): message = _("Failed to get sensor data for node %(node)s. " "Error: %(error)s") diff --git a/ironic/drivers/drac.py b/ironic/drivers/drac.py index 869230dd4f..e61681b57c 100644 --- a/ironic/drivers/drac.py +++ b/ironic/drivers/drac.py @@ -16,15 +16,14 @@ 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 management 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.""" + """Drac driver using PXE for deploy.""" def __init__(self): if not importutils.try_import('pywsman'): @@ -34,6 +33,4 @@ class PXEDracDriver(base.BaseDriver): 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() + self.management = management.DracManagement() diff --git a/ironic/drivers/fake.py b/ironic/drivers/fake.py index 7d4e7cfd20..efd2a7d56a 100644 --- a/ironic/drivers/fake.py +++ b/ironic/drivers/fake.py @@ -22,6 +22,7 @@ from oslo.utils import importutils from ironic.common import exception from ironic.drivers import base from ironic.drivers.modules import agent +from ironic.drivers.modules.drac import management as drac_mgmt from ironic.drivers.modules.drac import power as drac_power from ironic.drivers.modules import fake from ironic.drivers.modules import iboot @@ -142,3 +143,4 @@ class FakeDracDriver(base.BaseDriver): self.power = drac_power.DracPower() self.deploy = fake.FakeDeploy() + self.management = drac_mgmt.DracManagement() diff --git a/ironic/drivers/modules/drac/common.py b/ironic/drivers/modules/drac/common.py index a9ae83765d..dedf89ea00 100644 --- a/ironic/drivers/modules/drac/common.py +++ b/ironic/drivers/modules/drac/common.py @@ -100,15 +100,20 @@ def get_wsman_client(node): return client -def find_xml(doc, item, namespace): +def find_xml(doc, item, namespace, find_all=False): """Find the first or all elements in a ElementTree object. :param doc: the element tree object. :param item: the element name. :param namespace: the namespace of the element. - :returns: The element object or None if the element is not found. + :param find_all: Boolean value, if True find all elements, if False + find only the first one. Defaults to False. + :returns: The element object if find_all is False or a list of + element objects if find_all is True. """ query = ('.//{%(namespace)s}%(item)s' % {'namespace': namespace, 'item': item}) + if find_all: + return doc.findall(query) return doc.find(query) diff --git a/ironic/drivers/modules/drac/management.py b/ironic/drivers/modules/drac/management.py new file mode 100644 index 0000000000..ede5bf4df4 --- /dev/null +++ b/ironic/drivers/modules/drac/management.py @@ -0,0 +1,339 @@ +# -*- coding: utf-8 -*- +# +# Copyright 2014 Red Hat, 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. + +""" +DRAC Management Driver +""" + +from ironic.common import boot_devices +from ironic.common import exception +from ironic.common import i18n +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__) + +_ = i18n._ +_LE = i18n._LE + +_BOOT_DEVICES_MAP = { + boot_devices.DISK: 'HardDisk', + boot_devices.PXE: 'NIC', + boot_devices.CDROM: 'Optical', +} + +# IsNext constants +PERSISTENT = '1' # is the next boot config the system will use + +NOT_NEXT = '2' # is not the next boot config the system will use + +ONE_TIME_BOOT = '3' # is the next boot config the system will use, + # one time boot only + +# ReturnValue constants +RET_SUCCESS = '0' +RET_ERROR = '2' +RET_CREATED = '4096' + +FILTER_DIALECT = 'http://schemas.dmtf.org/wbem/cql/1/dsp0202.pdf' + + +def _get_next_boot_mode(node): + """Get the next boot mode. + + To see a list of supported boot modes see: http://goo.gl/aEsvUH + (Section 7.2) + + :param node: an ironic node object. + :raises: DracClientError on an error from pywsman library. + :returns: a dictionary containing: + + :instance_id: the instance id of the boot device. + :is_next: whether it's the next device to boot or not. One of + PERSISTENT, NOT_NEXT, ONE_TIME_BOOT constants. + + """ + client = drac_common.get_wsman_client(node) + options = pywsman.ClientOptions() + filter = pywsman.Filter() + filter_query = ('select * from DCIM_BootConfigSetting where IsNext=%s ' + 'or IsNext=%s' % (PERSISTENT, ONE_TIME_BOOT)) + filter.simple(FILTER_DIALECT, filter_query) + + try: + doc = client.wsman_enumerate(resource_uris.DCIM_BootConfigSetting, + options, filter) + except exception.DracClientError as exc: + with excutils.save_and_reraise_exception(): + LOG.error(_LE('DRAC driver failed to get next boot mode for ' + 'node %(node_uuid)s. Reason: %(error)s.'), + {'node_uuid': node.uuid, 'error': exc}) + + items = drac_common.find_xml(doc, 'DCIM_BootConfigSetting', + resource_uris.DCIM_BootConfigSetting, + find_all=True) + + # This list will have 2 items maximum, one for the persistent element + # and another one for the OneTime if set + boot_mode = None + for i in items: + instance_id = drac_common.find_xml(i, 'InstanceID', + resource_uris.DCIM_BootConfigSetting).text + is_next = drac_common.find_xml(i, 'IsNext', + resource_uris.DCIM_BootConfigSetting).text + + boot_mode = {'instance_id': instance_id, 'is_next': is_next} + # If OneTime is set we should return it, because that's + # where the next boot device is + if is_next == ONE_TIME_BOOT: + break + + return boot_mode + + +def _create_config_job(node): + """Create a configuration job. + + This method is used to apply the pending values created by + set_boot_device(). + + :param node: an ironic node object. + :raises: DracClientError on an error from pywsman library. + :raises: DracConfigJobCreationError on an error when creating the job. + + """ + client = drac_common.get_wsman_client(node) + options = pywsman.ClientOptions() + options.add_selector('CreationClassName', 'DCIM_BIOSService') + options.add_selector('Name', 'DCIM:BIOSService') + options.add_selector('SystemCreationClassName', 'DCIM_ComputerSystem') + options.add_selector('SystemName', 'DCIM:ComputerSystem') + options.add_property('Target', 'BIOS.Setup.1-1') + options.add_property('ScheduledStartTime', 'TIME_NOW') + doc = client.wsman_invoke(resource_uris.DCIM_BIOSService, + options, 'CreateTargetedConfigJob') + return_value = drac_common.find_xml(doc, 'ReturnValue', + resource_uris.DCIM_BIOSService).text + # NOTE(lucasagomes): Possible return values are: RET_ERROR for error + # or RET_CREATED job created (but changes will be + # applied after the reboot) + # Boot Management Documentation: http://goo.gl/aEsvUH (Section 8.4) + if return_value == RET_ERROR: + error_message = drac_common.find_xml(doc, 'Message', + resource_uris.DCIM_BIOSService).text + raise exception.DracConfigJobCreationError(error=error_message) + + +def _check_for_config_job(node): + """Check if a configuration job is already created. + + :param node: an ironic node object. + :raises: DracClientError on an error from pywsman library. + :raises: DracConfigJobCreationError if the job is already created. + + """ + client = drac_common.get_wsman_client(node) + options = pywsman.ClientOptions() + try: + doc = client.wsman_enumerate(resource_uris.DCIM_LifecycleJob, options) + except exception.DracClientError as exc: + with excutils.save_and_reraise_exception(): + LOG.error(_LE('DRAC driver failed to list the configuration jobs ' + 'for node %(node_uuid)s. Reason: %(error)s.'), + {'node_uuid': node.uuid, 'error': exc}) + + items = drac_common.find_xml(doc, 'DCIM_LifecycleJob', + resource_uris.DCIM_LifecycleJob, + find_all=True) + for i in items: + name = drac_common.find_xml(i, 'Name', resource_uris.DCIM_LifecycleJob) + if 'BIOS.Setup.1-1' not in name.text: + continue + + job_status = drac_common.find_xml(i, 'JobStatus', + resource_uris.DCIM_LifecycleJob).text + # If job is already completed or failed we can + # create another one. + # Job Control Documentation: http://goo.gl/o1dDD3 (Section 7.2.3.2) + if job_status.lower() not in ('completed', 'failed'): + job_id = drac_common.find_xml(i, 'InstanceID', + resource_uris.DCIM_LifecycleJob).text + reason = (_('Another job with ID "%s" is already created ' + 'to configure the BIOS. Wait until existing job ' + 'is completed or is cancelled') % job_id) + raise exception.DracConfigJobCreationError(error=reason) + + +class DracManagement(base.ManagementInterface): + + def get_properties(self): + return drac_common.COMMON_PROPERTIES + + def validate(self, task): + """Validate the driver-specific info supplied. + + This method validates whether the 'driver_info' property of the + supplied node contains the required information for this driver to + manage 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_supported_boot_devices(self): + """Get a list of the supported boot devices. + + :returns: A list with the supported boot devices defined + in :mod:`ironic.common.boot_devices`. + + """ + return list(_BOOT_DEVICES_MAP.keys()) + + def set_boot_device(self, task, device, persistent=False): + """Set the boot device for a node. + + Set the boot device to use on next reboot of the node. + + :param task: a task from TaskManager. + :param device: the boot device, one of + :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: DracClientError on an error from pywsman library. + :raises: InvalidParameterValue if an invalid boot device is + specified. + :raises: DracConfigJobCreationError on an error when creating the job. + + """ + # Check for an existing configuration job + _check_for_config_job(task.node) + + client = drac_common.get_wsman_client(task.node) + options = pywsman.ClientOptions() + filter = pywsman.Filter() + filter_query = ("select * from DCIM_BootSourceSetting where " + "InstanceID like '%%#%s%%'" % + _BOOT_DEVICES_MAP[device]) + filter.simple(FILTER_DIALECT, filter_query) + + try: + doc = client.wsman_enumerate(resource_uris.DCIM_BootSourceSetting, + options, filter) + except exception.DracClientError as exc: + with excutils.save_and_reraise_exception(): + LOG.error(_LE('DRAC driver failed to set the boot device ' + 'for node %(node_uuid)s. Can\'t find the ID ' + 'for the %(device)s type. Reason: %(error)s.'), + {'node_uuid': task.node.uuid, 'error': exc, + 'device': device}) + + instance_id = drac_common.find_xml(doc, 'InstanceID', + resource_uris.DCIM_BootSourceSetting).text + + source = 'OneTime' + if persistent: + source = drac_common.find_xml(doc, 'BootSourceType', + resource_uris.DCIM_BootSourceSetting).text + + # NOTE(lucasagomes): Don't ask me why 'BootSourceType' is set + # for 'InstanceID' and 'InstanceID' is set for 'source'! You + # know enterprisey... + options = pywsman.ClientOptions() + options.add_selector('InstanceID', source) + options.add_property('source', instance_id) + doc = client.wsman_invoke(resource_uris.DCIM_BootConfigSetting, + options, 'ChangeBootOrderByInstanceID') + return_value = drac_common.find_xml(doc, 'ReturnValue', + resource_uris.DCIM_BootConfigSetting).text + # NOTE(lucasagomes): Possible return values are: RET_ERROR for error, + # RET_SUCCESS for success or RET_CREATED job + # created (but changes will be applied after + # the reboot) + # Boot Management Documentation: http://goo.gl/aEsvUH (Section 8.7) + if return_value == RET_ERROR: + error_message = drac_common.find_xml(doc, 'Message', + resource_uris.DCIM_BootConfigSetting).text + raise exception.DracOperationError(operation='set_boot_device', + error=error_message) + # Create a configuration job + _create_config_job(task.node) + + def get_boot_device(self, task): + """Get the current boot device for a node. + + Returns the current boot device of the node. + + :param task: a task from TaskManager. + :raises: DracClientError on an error from pywsman library. + :returns: a dictionary containing: + + :boot_device: the boot device, one of + :mod:`ironic.common.boot_devices` or None if it is unknown. + :persistent: Whether the boot device will persist to all + future boots or not, None if it is unknown. + + """ + client = drac_common.get_wsman_client(task.node) + boot_mode = _get_next_boot_mode(task.node) + + persistent = boot_mode['is_next'] == PERSISTENT + instance_id = boot_mode['instance_id'] + + options = pywsman.ClientOptions() + filter = pywsman.Filter() + filter_query = ('select * from DCIM_BootSourceSetting where ' + 'PendingAssignedSequence=0 and ' + 'BootSourceType="%s"' % instance_id) + filter.simple(FILTER_DIALECT, filter_query) + + try: + doc = client.wsman_enumerate(resource_uris.DCIM_BootSourceSetting, + options, filter) + except exception.DracClientError as exc: + with excutils.save_and_reraise_exception(): + LOG.error(_LE('DRAC driver failed to get the current boot ' + 'device for node %(node_uuid)s. ' + 'Reason: %(error)s.'), + {'node_uuid': task.node.uuid, 'error': exc}) + + instance_id = drac_common.find_xml(doc, 'InstanceID', + resource_uris.DCIM_BootSourceSetting).text + boot_device = next((key for (key, value) in _BOOT_DEVICES_MAP.items() + if value in instance_id), None) + return {'boot_device': boot_device, 'persistent': persistent} + + def get_sensors_data(self, task): + """Get sensors data. + + :param task: a TaskManager instance. + :raises: FailedToGetSensorData when getting the sensor data fails. + :raises: FailedToParseSensorData when parsing sensor data fails. + :returns: returns a consistent format dict of sensor data grouped by + sensor type, which can be processed by Ceilometer. + + """ + raise NotImplementedError() diff --git a/ironic/drivers/modules/drac/resource_uris.py b/ironic/drivers/modules/drac/resource_uris.py index 69b1549c57..ea7445774a 100644 --- a/ironic/drivers/modules/drac/resource_uris.py +++ b/ironic/drivers/modules/drac/resource_uris.py @@ -18,3 +18,15 @@ WS-Man API. DCIM_ComputerSystem = ('http://schemas.dell.com/wbem/wscim/1/cim-schema/2' '/DCIM_ComputerSystem') + +DCIM_BootSourceSetting = ('http://schemas.dell.com/wbem/wscim/1/cim-schema/2/' + 'DCIM_BootSourceSetting') + +DCIM_BootConfigSetting = ('http://schemas.dell.com/wbem/wscim/1/cim-schema/2/' + 'DCIM_BootConfigSetting') + +DCIM_BIOSService = ('http://schemas.dell.com/wbem/wscim/1/cim-schema/2/' + 'DCIM_BIOSService') + +DCIM_LifecycleJob = ('http://schemas.dell.com/wbem/wscim/1/cim-schema/2/' + 'DCIM_LifecycleJob') diff --git a/ironic/tests/drivers/drac/test_client.py b/ironic/tests/drivers/drac/test_client.py index f9ef15e365..b4c1ac83bd 100644 --- a/ironic/tests/drivers/drac/test_client.py +++ b/ironic/tests/drivers/drac/test_client.py @@ -54,9 +54,9 @@ class DracClientTestCase(base.TestCase): def test_wsman_enumerate_with_additional_pull(self, mock_client_pywsman): mock_root = mock.Mock() mock_root.string.side_effect = [test_utils.build_soap_xml( - {'item1': 'test1'}), + [{'item1': 'test1'}]), test_utils.build_soap_xml( - {'item2': 'test2'})] + [{'item2': 'test2'}])] mock_xml = mock.Mock() mock_xml.root.return_value = mock_root mock_xml.context.side_effect = [42, 42, None] diff --git a/ironic/tests/drivers/drac/test_common.py b/ironic/tests/drivers/drac/test_common.py index c2faaa3765..3f0f730775 100644 --- a/ironic/tests/drivers/drac/test_common.py +++ b/ironic/tests/drivers/drac/test_common.py @@ -17,6 +17,8 @@ Test class for common methods used by DRAC modules. from xml.etree import ElementTree +from testtools.matchers import HasLength + from ironic.common import exception from ironic.drivers.modules.drac import common as drac_common from ironic.openstack.common import context @@ -116,3 +118,23 @@ class DracCommonMethodsTestCase(base.TestCase): result = drac_common.find_xml(test_doc, 'test_element', namespace) self.assertEqual(value, result.text) + + def test_find_xml_find_all(self): + namespace = 'http://fake' + value1 = 'fake_value1' + value2 = 'fake_value2' + test_doc = ElementTree.fromstring(""" + + %(value1)s + meow + %(value2)s + bark + + """ % {'ns': namespace, 'value1': value1, + 'value2': value2}) + + result = drac_common.find_xml(test_doc, 'test_element', + namespace, find_all=True) + self.assertThat(result, HasLength(2)) + result_text = [v.text for v in result] + self.assertEqual(sorted([value1, value2]), sorted(result_text)) diff --git a/ironic/tests/drivers/drac/test_management.py b/ironic/tests/drivers/drac/test_management.py new file mode 100644 index 0000000000..e5a7ef1f84 --- /dev/null +++ b/ironic/tests/drivers/drac/test_management.py @@ -0,0 +1,309 @@ +# -*- coding: utf-8 -*- +# +# Copyright 2014 Red Hat, 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. + +""" +Test class for DRAC ManagementInterface +""" + +import mock + +from ironic.common import boot_devices +from ironic.common import exception +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 management as drac_mgmt +from ironic.drivers.modules.drac import resource_uris +from ironic.openstack.common import context +from ironic.tests import base +from ironic.tests.conductor import utils as mgr_utils +from ironic.tests.db import utils as db_utils +from ironic.tests.drivers.drac import utils as test_utils +from ironic.tests.objects import utils as obj_utils + +INFO_DICT = db_utils.get_test_drac_info() + + +def _mock_wsman_root(return_value): + """Helper function to mock the root() from wsman client.""" + mock_xml_root = mock.Mock() + mock_xml_root.string.return_value = return_value + + mock_xml = mock.Mock() + mock_xml.context.return_value = None + mock_xml.root.return_value = mock_xml_root + + return mock_xml + + +@mock.patch.object(drac_client, 'pywsman') +class DracManagementInternalMethodsTestCase(base.TestCase): + + def setUp(self): + super(DracManagementInternalMethodsTestCase, self).setUp() + mgr_utils.mock_the_extension_manager(driver='fake_drac') + self.context = context.get_admin_context() + self.node = obj_utils.create_test_node(self.context, + driver='fake_drac', + driver_info=INFO_DICT) + + def test__get_next_boot_mode(self, mock_client_pywsman): + result_xml = test_utils.build_soap_xml([{'DCIM_BootConfigSetting': + {'InstanceID': 'IPL', + 'IsNext': + drac_mgmt.PERSISTENT}}], + resource_uris.DCIM_BootConfigSetting) + + mock_xml = _mock_wsman_root(result_xml) + mock_pywsman = mock_client_pywsman.Client.return_value + mock_pywsman.enumerate.return_value = mock_xml + + expected = {'instance_id': 'IPL', 'is_next': drac_mgmt.PERSISTENT} + result = drac_mgmt._get_next_boot_mode(self.node) + + self.assertEqual(expected, result) + mock_pywsman.enumerate.assert_called_once_with(mock.ANY, mock.ANY, + resource_uris.DCIM_BootConfigSetting) + + def test__get_next_boot_mode_onetime(self, mock_client_pywsman): + result_xml = test_utils.build_soap_xml([{'DCIM_BootConfigSetting': + {'InstanceID': 'IPL', + 'IsNext': + drac_mgmt.PERSISTENT}}, + {'DCIM_BootConfigSetting': + {'InstanceID': 'OneTime', + 'IsNext': + drac_mgmt.ONE_TIME_BOOT}}], + resource_uris.DCIM_BootConfigSetting) + + mock_xml = _mock_wsman_root(result_xml) + mock_pywsman = mock_client_pywsman.Client.return_value + mock_pywsman.enumerate.return_value = mock_xml + + expected = {'instance_id': 'OneTime', + 'is_next': drac_mgmt.ONE_TIME_BOOT} + result = drac_mgmt._get_next_boot_mode(self.node) + + self.assertEqual(expected, result) + mock_pywsman.enumerate.assert_called_once_with(mock.ANY, mock.ANY, + resource_uris.DCIM_BootConfigSetting) + + def test__check_for_config_job(self, mock_client_pywsman): + result_xml = test_utils.build_soap_xml([{'DCIM_LifecycleJob': + {'Name': 'fake'}}], + resource_uris.DCIM_LifecycleJob) + + mock_xml = _mock_wsman_root(result_xml) + mock_pywsman = mock_client_pywsman.Client.return_value + mock_pywsman.enumerate.return_value = mock_xml + + result = drac_mgmt._check_for_config_job(self.node) + + self.assertIsNone(result) + mock_pywsman.enumerate.assert_called_once_with(mock.ANY, mock.ANY, + resource_uris.DCIM_LifecycleJob) + + def test__check_for_config_job_already_exist(self, mock_client_pywsman): + result_xml = test_utils.build_soap_xml([{'DCIM_LifecycleJob': + {'Name': 'BIOS.Setup.1-1', + 'JobStatus': 'scheduled', + 'InstanceID': 'fake'}}], + resource_uris.DCIM_LifecycleJob) + + mock_xml = _mock_wsman_root(result_xml) + mock_pywsman = mock_client_pywsman.Client.return_value + mock_pywsman.enumerate.return_value = mock_xml + + self.assertRaises(exception.DracConfigJobCreationError, + drac_mgmt._check_for_config_job, self.node) + mock_pywsman.enumerate.assert_called_once_with(mock.ANY, mock.ANY, + resource_uris.DCIM_LifecycleJob) + + def test__create_config_job(self, mock_client_pywsman): + result_xml = test_utils.build_soap_xml([{'ReturnValue': + drac_mgmt.RET_SUCCESS}], + resource_uris.DCIM_BIOSService) + + mock_xml = _mock_wsman_root(result_xml) + mock_pywsman = mock_client_pywsman.Client.return_value + mock_pywsman.invoke.return_value = mock_xml + + result = drac_mgmt._create_config_job(self.node) + + self.assertIsNone(result) + mock_pywsman.invoke.assert_called_once_with(mock.ANY, + resource_uris.DCIM_BIOSService, 'CreateTargetedConfigJob') + + def test__create_config_job_error(self, mock_client_pywsman): + result_xml = test_utils.build_soap_xml([{'ReturnValue': + drac_mgmt.RET_ERROR, + 'Message': 'E_FAKE'}], + resource_uris.DCIM_BIOSService) + + mock_xml = _mock_wsman_root(result_xml) + mock_pywsman = mock_client_pywsman.Client.return_value + mock_pywsman.invoke.return_value = mock_xml + + self.assertRaises(exception.DracConfigJobCreationError, + drac_mgmt._create_config_job, self.node) + mock_pywsman.invoke.assert_called_once_with(mock.ANY, + resource_uris.DCIM_BIOSService, 'CreateTargetedConfigJob') + + +@mock.patch.object(drac_client, 'pywsman') +class DracManagementTestCase(base.TestCase): + + def setUp(self): + super(DracManagementTestCase, self).setUp() + mgr_utils.mock_the_extension_manager(driver='fake_drac') + self.context = context.get_admin_context() + self.node = obj_utils.create_test_node(self.context, + driver='fake_drac', + driver_info=INFO_DICT) + self.driver = drac_mgmt.DracManagement() + self.task = mock.Mock() + self.task.node = self.node + + def test_get_properties(self, mock_client_pywsman): + expected = drac_common.COMMON_PROPERTIES + self.assertEqual(expected, self.driver.get_properties()) + + def test_get_supported_boot_devices(self, mock_client_pywsman): + expected = [boot_devices.PXE, boot_devices.DISK, boot_devices.CDROM] + self.assertEqual(sorted(expected), + sorted(self.driver.get_supported_boot_devices())) + + @mock.patch.object(drac_mgmt, '_get_next_boot_mode') + def test_get_boot_devices(self, mock_gnbm, mock_client_pywsman): + mock_gnbm.return_value = {'instance_id': 'OneTime', + 'is_next': drac_mgmt.ONE_TIME_BOOT} + + result_xml = test_utils.build_soap_xml([{'InstanceID': 'HardDisk'}], + resource_uris.DCIM_BootSourceSetting) + + mock_xml = _mock_wsman_root(result_xml) + mock_pywsman = mock_client_pywsman.Client.return_value + mock_pywsman.enumerate.return_value = mock_xml + + result = self.driver.get_boot_device(self.task) + expected = {'boot_device': boot_devices.DISK, 'persistent': False} + + self.assertEqual(expected, result) + mock_pywsman.enumerate.assert_called_once_with(mock.ANY, mock.ANY, + resource_uris.DCIM_BootSourceSetting) + + @mock.patch.object(drac_mgmt, '_get_next_boot_mode') + def test_get_boot_devices_persistent(self, mock_gnbm, mock_client_pywsman): + mock_gnbm.return_value = {'instance_id': 'IPL', + 'is_next': drac_mgmt.PERSISTENT} + + result_xml = test_utils.build_soap_xml([{'InstanceID': 'NIC'}], + resource_uris.DCIM_BootSourceSetting) + + mock_xml = _mock_wsman_root(result_xml) + mock_pywsman = mock_client_pywsman.Client.return_value + mock_pywsman.enumerate.return_value = mock_xml + + result = self.driver.get_boot_device(self.task) + expected = {'boot_device': boot_devices.PXE, 'persistent': True} + + self.assertEqual(expected, result) + mock_pywsman.enumerate.assert_called_once_with(mock.ANY, mock.ANY, + resource_uris.DCIM_BootSourceSetting) + + @mock.patch.object(drac_client.Client, 'wsman_enumerate') + @mock.patch.object(drac_mgmt, '_get_next_boot_mode') + def test_get_boot_devices_client_error(self, mock_gnbm, mock_we, + mock_client_pywsman): + mock_gnbm.return_value = {'instance_id': 'OneTime', + 'is_next': drac_mgmt.ONE_TIME_BOOT} + mock_we.side_effect = exception.DracClientError('E_FAKE') + + self.assertRaises(exception.DracClientError, + self.driver.get_boot_device, self.task) + mock_we.assert_called_once_with(resource_uris.DCIM_BootSourceSetting, + mock.ANY, mock.ANY) + + @mock.patch.object(drac_mgmt, '_check_for_config_job') + @mock.patch.object(drac_mgmt, '_create_config_job') + def test_set_boot_device(self, mock_ccj, mock_cfcj, mock_client_pywsman): + result_xml_enum = test_utils.build_soap_xml([{'InstanceID': 'NIC'}], + resource_uris.DCIM_BootSourceSetting) + result_xml_invk = test_utils.build_soap_xml([{'ReturnValue': + drac_mgmt.RET_SUCCESS}], + resource_uris.DCIM_BootConfigSetting) + + mock_xml_enum = _mock_wsman_root(result_xml_enum) + mock_xml_invk = _mock_wsman_root(result_xml_invk) + mock_pywsman = mock_client_pywsman.Client.return_value + mock_pywsman.enumerate.return_value = mock_xml_enum + mock_pywsman.invoke.return_value = mock_xml_invk + + result = self.driver.set_boot_device(self.task, boot_devices.PXE) + + self.assertIsNone(result) + mock_pywsman.enumerate.assert_called_once_with(mock.ANY, mock.ANY, + resource_uris.DCIM_BootSourceSetting) + mock_pywsman.invoke.assert_called_once_with(mock.ANY, + resource_uris.DCIM_BootConfigSetting, + 'ChangeBootOrderByInstanceID') + mock_cfcj.assert_called_once_with(self.node) + mock_ccj.assert_called_once_with(self.node) + + @mock.patch.object(drac_mgmt, '_check_for_config_job') + @mock.patch.object(drac_mgmt, '_create_config_job') + def test_set_boot_device_fail(self, mock_ccj, mock_cfcj, + mock_client_pywsman): + result_xml_enum = test_utils.build_soap_xml([{'InstanceID': 'NIC'}], + resource_uris.DCIM_BootSourceSetting) + result_xml_invk = test_utils.build_soap_xml([{'ReturnValue': + drac_mgmt.RET_ERROR, + 'Message': 'E_FAKE'}], + resource_uris.DCIM_BootConfigSetting) + + mock_xml_enum = _mock_wsman_root(result_xml_enum) + mock_xml_invk = _mock_wsman_root(result_xml_invk) + mock_pywsman = mock_client_pywsman.Client.return_value + mock_pywsman.enumerate.return_value = mock_xml_enum + mock_pywsman.invoke.return_value = mock_xml_invk + + self.assertRaises(exception.DracOperationError, + self.driver.set_boot_device, self.task, + boot_devices.PXE) + + mock_pywsman.enumerate.assert_called_once_with(mock.ANY, mock.ANY, + resource_uris.DCIM_BootSourceSetting) + mock_pywsman.invoke.assert_called_once_with(mock.ANY, + resource_uris.DCIM_BootConfigSetting, + 'ChangeBootOrderByInstanceID') + mock_cfcj.assert_called_once_with(self.node) + self.assertFalse(mock_ccj.called) + + @mock.patch.object(drac_client.Client, 'wsman_enumerate') + @mock.patch.object(drac_mgmt, '_check_for_config_job') + def test_set_boot_device_client_error(self, mock_cfcj, mock_we, + mock_client_pywsman): + mock_we.side_effect = exception.DracClientError('E_FAKE') + + self.assertRaises(exception.DracClientError, + self.driver.set_boot_device, self.task, + boot_devices.PXE) + mock_we.assert_called_once_with(resource_uris.DCIM_BootSourceSetting, + mock.ANY, mock.ANY) + + def test_get_sensors_data(self, mock_client_pywsman): + self.assertRaises(NotImplementedError, + self.driver.get_sensors_data, self.task) diff --git a/ironic/tests/drivers/drac/test_power.py b/ironic/tests/drivers/drac/test_power.py index 5955e0bed8..9d97cf0844 100644 --- a/ironic/tests/drivers/drac/test_power.py +++ b/ironic/tests/drivers/drac/test_power.py @@ -46,7 +46,7 @@ class DracPowerInternalMethodsTestCase(base.TestCase): self.node = self.dbapi.create_node(db_node) def test__get_power_state(self, mock_power_pywsman, mock_client_pywsman): - result_xml = test_utils.build_soap_xml({'EnabledState': '2'}, + result_xml = test_utils.build_soap_xml([{'EnabledState': '2'}], resource_uris.DCIM_ComputerSystem) mock_xml_root = mock.Mock() mock_xml_root.string.return_value = result_xml @@ -65,7 +65,7 @@ class DracPowerInternalMethodsTestCase(base.TestCase): mock.ANY, resource_uris.DCIM_ComputerSystem) def test__set_power_state(self, mock_power_pywsman, mock_client_pywsman): - result_xml = test_utils.build_soap_xml({'ReturnValue': '0'}, + result_xml = test_utils.build_soap_xml([{'ReturnValue': '0'}], resource_uris.DCIM_ComputerSystem) mock_xml_root = mock.Mock() mock_xml_root.string.return_value = result_xml @@ -92,8 +92,8 @@ class DracPowerInternalMethodsTestCase(base.TestCase): def test__set_power_state_fail(self, mock_power_pywsman, mock_client_pywsman): - result_xml = test_utils.build_soap_xml({'ReturnValue': '1', - 'Message': 'error message'}, + result_xml = test_utils.build_soap_xml([{'ReturnValue': '1', + 'Message': 'error message'}], resource_uris.DCIM_ComputerSystem) mock_xml_root = mock.Mock() mock_xml_root.string.return_value = result_xml diff --git a/ironic/tests/drivers/drac/utils.py b/ironic/tests/drivers/drac/utils.py index 90e4027835..2d2100dd38 100644 --- a/ironic/tests/drivers/drac/utils.py +++ b/ironic/tests/drivers/drac/utils.py @@ -21,26 +21,38 @@ from xml.etree import ElementTree def build_soap_xml(items, namespace=None): """Build a SOAP XML. - :param items: a dictionary where key is the element name and the - value is the element text. + :param items: a list of dictionaries where key is the element name + and the value is the element text. :param namespace: the namespace for the elements, None for no namespace. Defaults to None :returns: a XML string. """ - soap_namespace = "http://www.w3.org/2003/05/soap-envelope" - envelope_element = ElementTree.Element("{%s}Envelope" % soap_namespace) - body_element = ElementTree.Element("{%s}Body" % soap_namespace) - for i in items: - xml_string = i + def _create_element(name, value=None): + xml_string = name if namespace: xml_string = "{%(namespace)s}%(item)s" % {'namespace': namespace, 'item': xml_string} element = ElementTree.Element(xml_string) - element.text = items[i] - body_element.append(element) + element.text = value + return element + + soap_namespace = "http://www.w3.org/2003/05/soap-envelope" + envelope_element = ElementTree.Element("{%s}Envelope" % soap_namespace) + body_element = ElementTree.Element("{%s}Body" % soap_namespace) + + for item in items: + for i in item: + insertion_point = _create_element(i) + if isinstance(item[i], dict): + for j, value in item[i].items(): + insertion_point.append(_create_element(j, value)) + else: + insertion_point.text = item[i] + + body_element.append(insertion_point) envelope_element.append(body_element) return ElementTree.tostring(envelope_element)