diff --git a/ironic/common/exception.py b/ironic/common/exception.py index 765ae1087c..edd0da32e4 100644 --- a/ironic/common/exception.py +++ b/ironic/common/exception.py @@ -432,6 +432,11 @@ class DracConfigJobCreationError(DracOperationError): 'Reason: %(error)s') +class DracInvalidFilterDialect(DracOperationError): + message = _('Invalid filter dialect \'%(invalid_filter)s\'. ' + 'Supported options are %(supported)s') + + class FailedToGetSensorData(IronicException): message = _("Failed to get sensor data for node %(node)s. " "Error: %(error)s") diff --git a/ironic/drivers/modules/drac/client.py b/ironic/drivers/modules/drac/client.py index 99b052db6e..73cfe13cf9 100644 --- a/ironic/drivers/modules/drac/client.py +++ b/ironic/drivers/modules/drac/client.py @@ -24,6 +24,11 @@ pywsman = importutils.try_import('pywsman') _SOAP_ENVELOPE_URI = 'http://www.w3.org/2003/05/soap-envelope' +# Filter Dialects, see (Section 2.3.1): +# http://en.community.dell.com/techcenter/extras/m/white_papers/20439105.aspx +_FILTER_DIALECT_MAP = {'cql': 'http://schemas.dmtf.org/wbem/cql/1/dsp0202.pdf', + 'wql': 'http://schemas.microsoft.com/wbem/wsman/1/WQL'} + class Client(object): @@ -37,19 +42,36 @@ class Client(object): self.client = pywsman_client - def wsman_enumerate(self, resource_uri, options, filter=None): + def wsman_enumerate(self, resource_uri, options, filter_query=None, + filter_dialect='cql'): """Enumerates a remote WS-Man class. :param resource_uri: URI of the resource. :param options: client options. - :param filter: filter for enumeration. + :param filter_query: the query string. + :param filter_dialect: the filter dialect. Valid options are: + 'cql' and 'wql'. Defaults to 'cql'. :raises: DracClientError on an error from pywsman library. + :raises: DracInvalidFilterDialect if an invalid filter dialect + was specified. :returns: an ElementTree object of the response received. """ + filter_ = None + if filter_query is not None: + try: + filter_dialect = _FILTER_DIALECT_MAP[filter_dialect] + except KeyError: + valid_opts = ', '.join(_FILTER_DIALECT_MAP) + raise exception.DracInvalidFilterDialect( + invalid_filter=filter_dialect, supported=valid_opts) + + filter_ = pywsman.Filter() + filter_.simple(filter_dialect, filter_query) + options.set_flags(pywsman.FLAG_ENUMERATION_OPTIMIZATION) options.set_max_elements(100) - doc = self.client.enumerate(options, filter, resource_uri) + doc = self.client.enumerate(options, filter_, resource_uri) root = self._get_root(doc) final_xml = root diff --git a/ironic/drivers/modules/drac/common.py b/ironic/drivers/modules/drac/common.py index 9413ed078c..dddf68bbc1 100644 --- a/ironic/drivers/modules/drac/common.py +++ b/ironic/drivers/modules/drac/common.py @@ -37,6 +37,11 @@ OPTIONAL_PROPERTIES = { COMMON_PROPERTIES = REQUIRED_PROPERTIES.copy() COMMON_PROPERTIES.update(OPTIONAL_PROPERTIES) +# ReturnValue constants +RET_SUCCESS = '0' +RET_ERROR = '2' +RET_CREATED = '4096' + def parse_driver_info(node): """Parses the driver_info of the node, reads default values @@ -109,8 +114,10 @@ def find_xml(doc, item, namespace, find_all=False): :param namespace: the namespace of the element. :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. + :returns: if find_all is False the element object will be returned + if found, None if not found. If find_all is True a list of + element objects will be returned or an empty list if no + elements were found. """ query = ('.//{%(namespace)s}%(item)s' % {'namespace': namespace, diff --git a/ironic/drivers/modules/drac/management.py b/ironic/drivers/modules/drac/management.py index ede5bf4df4..4598f078b5 100644 --- a/ironic/drivers/modules/drac/management.py +++ b/ironic/drivers/modules/drac/management.py @@ -50,13 +50,6 @@ 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. @@ -75,14 +68,11 @@ def _get_next_boot_mode(node): """ 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) + options, filter_query=filter_query) except exception.DracClientError as exc: with excutils.save_and_reraise_exception(): LOG.error(_LE('DRAC driver failed to get next boot mode for ' @@ -138,7 +128,7 @@ def _create_config_job(node): # 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: + if return_value == drac_common.RET_ERROR: error_message = drac_common.find_xml(doc, 'Message', resource_uris.DCIM_BIOSService).text raise exception.DracConfigJobCreationError(error=error_message) @@ -234,15 +224,12 @@ class DracManagement(base.ManagementInterface): 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) + options, filter_query=filter_query) except exception.DracClientError as exc: with excutils.save_and_reraise_exception(): LOG.error(_LE('DRAC driver failed to set the boot device ' @@ -274,7 +261,7 @@ class DracManagement(base.ManagementInterface): # created (but changes will be applied after # the reboot) # Boot Management Documentation: http://goo.gl/aEsvUH (Section 8.7) - if return_value == RET_ERROR: + if return_value == drac_common.RET_ERROR: error_message = drac_common.find_xml(doc, 'Message', resource_uris.DCIM_BootConfigSetting).text raise exception.DracOperationError(operation='set_boot_device', @@ -304,15 +291,12 @@ class DracManagement(base.ManagementInterface): 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) + options, filter_query=filter_query) except exception.DracClientError as exc: with excutils.save_and_reraise_exception(): LOG.error(_LE('DRAC driver failed to get the current boot ' diff --git a/ironic/drivers/modules/drac/power.py b/ironic/drivers/modules/drac/power.py index 2f48aff9c2..b532817247 100644 --- a/ironic/drivers/modules/drac/power.py +++ b/ironic/drivers/modules/drac/power.py @@ -51,15 +51,11 @@ def _get_power_state(node): client = drac_common.get_wsman_client(node) options = pywsman.ClientOptions() - filter = pywsman.Filter() - filter_dialect = 'http://schemas.dmtf.org/wbem/cql/1/dsp0202.pdf' filter_query = ('select EnabledState,ElementName from CIM_ComputerSystem ' 'where Name="srv:system"') - filter.simple(filter_dialect, filter_query) - try: doc = client.wsman_enumerate(resource_uris.DCIM_ComputerSystem, - options, filter) + options, filter_query=filter_query) except exception.DracClientError as exc: with excutils.save_and_reraise_exception(): LOG.error(_LE('DRAC driver failed to get power state for node ' @@ -100,7 +96,7 @@ def _set_power_state(node, target_state): return_value = drac_common.find_xml(root, 'ReturnValue', resource_uris.DCIM_ComputerSystem).text - if return_value != '0': + if return_value != drac_common.RET_SUCCESS: message = drac_common.find_xml(root, 'Message', resource_uris.DCIM_ComputerSystem).text LOG.error(_LE('DRAC driver failed to set power state for node ' diff --git a/ironic/tests/drivers/drac/test_client.py b/ironic/tests/drivers/drac/test_client.py index b4c1ac83bd..e6d8e165ee 100644 --- a/ironic/tests/drivers/drac/test_client.py +++ b/ironic/tests/drivers/drac/test_client.py @@ -18,6 +18,7 @@ Test class for DRAC client wrapper. import mock from xml.etree import ElementTree +from ironic.common import exception from ironic.drivers.modules.drac import client as drac_client from ironic.tests import base from ironic.tests.db import utils as db_utils @@ -30,12 +31,7 @@ INFO_DICT = db_utils.get_test_drac_info() class DracClientTestCase(base.TestCase): def test_wsman_enumerate(self, mock_client_pywsman): - mock_root = mock.Mock() - mock_root.string.return_value = '' - mock_xml = mock.Mock() - mock_xml.root.return_value = mock_root - mock_xml.context.return_value = None - + mock_xml = test_utils.mock_wsman_root('') mock_pywsman_client = mock_client_pywsman.Client.return_value mock_pywsman_client.enumerate.return_value = mock_xml @@ -81,12 +77,33 @@ class DracClientTestCase(base.TestCase): mock_pywsman_client.enumerate.assert_called_once_with(mock_options, None, resource_uri) - def test_wsman_invoke(self, mock_client_pywsman): - mock_root = mock.Mock() - mock_root.string.return_value = '' - mock_xml = mock.Mock() - mock_xml.root.return_value = mock_root + def test_wsman_enumerate_filter_query(self, mock_client_pywsman): + mock_xml = test_utils.mock_wsman_root('') + mock_pywsman_client = mock_client_pywsman.Client.return_value + mock_pywsman_client.enumerate.return_value = mock_xml + resource_uri = 'https://foo/wsman' + mock_options = mock_client_pywsman.ClientOptions.return_value + mock_filter = mock_client_pywsman.Filter.return_value + client = drac_client.Client(**INFO_DICT) + filter_query = 'SELECT * FROM foo' + client.wsman_enumerate(resource_uri, mock_options, + filter_query=filter_query) + + mock_filter.simple.assert_called_once_with(mock.ANY, filter_query) + mock_pywsman_client.enumerate.assert_called_once_with(mock_options, + mock_filter, resource_uri) + mock_xml.context.assert_called_once_with() + + def test_wsman_enumerate_invalid_filter_dialect(self, mock_client_pywsman): + client = drac_client.Client(**INFO_DICT) + self.assertRaises(exception.DracInvalidFilterDialect, + client.wsman_enumerate, 'https://foo/wsman', + mock.Mock(), filter_query='foo', + filter_dialect='invalid') + + def test_wsman_invoke(self, mock_client_pywsman): + mock_xml = test_utils.mock_wsman_root('') mock_pywsman_client = mock_client_pywsman.Client.return_value mock_pywsman_client.invoke.return_value = mock_xml diff --git a/ironic/tests/drivers/drac/test_management.py b/ironic/tests/drivers/drac/test_management.py index e5a7ef1f84..a45214465e 100644 --- a/ironic/tests/drivers/drac/test_management.py +++ b/ironic/tests/drivers/drac/test_management.py @@ -37,18 +37,6 @@ 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): @@ -67,7 +55,7 @@ class DracManagementInternalMethodsTestCase(base.TestCase): drac_mgmt.PERSISTENT}}], resource_uris.DCIM_BootConfigSetting) - mock_xml = _mock_wsman_root(result_xml) + mock_xml = test_utils.mock_wsman_root(result_xml) mock_pywsman = mock_client_pywsman.Client.return_value mock_pywsman.enumerate.return_value = mock_xml @@ -89,7 +77,7 @@ class DracManagementInternalMethodsTestCase(base.TestCase): drac_mgmt.ONE_TIME_BOOT}}], resource_uris.DCIM_BootConfigSetting) - mock_xml = _mock_wsman_root(result_xml) + mock_xml = test_utils.mock_wsman_root(result_xml) mock_pywsman = mock_client_pywsman.Client.return_value mock_pywsman.enumerate.return_value = mock_xml @@ -106,7 +94,7 @@ class DracManagementInternalMethodsTestCase(base.TestCase): {'Name': 'fake'}}], resource_uris.DCIM_LifecycleJob) - mock_xml = _mock_wsman_root(result_xml) + mock_xml = test_utils.mock_wsman_root(result_xml) mock_pywsman = mock_client_pywsman.Client.return_value mock_pywsman.enumerate.return_value = mock_xml @@ -123,7 +111,7 @@ class DracManagementInternalMethodsTestCase(base.TestCase): 'InstanceID': 'fake'}}], resource_uris.DCIM_LifecycleJob) - mock_xml = _mock_wsman_root(result_xml) + mock_xml = test_utils.mock_wsman_root(result_xml) mock_pywsman = mock_client_pywsman.Client.return_value mock_pywsman.enumerate.return_value = mock_xml @@ -134,10 +122,10 @@ class DracManagementInternalMethodsTestCase(base.TestCase): def test__create_config_job(self, mock_client_pywsman): result_xml = test_utils.build_soap_xml([{'ReturnValue': - drac_mgmt.RET_SUCCESS}], + drac_common.RET_SUCCESS}], resource_uris.DCIM_BIOSService) - mock_xml = _mock_wsman_root(result_xml) + mock_xml = test_utils.mock_wsman_root(result_xml) mock_pywsman = mock_client_pywsman.Client.return_value mock_pywsman.invoke.return_value = mock_xml @@ -149,11 +137,11 @@ class DracManagementInternalMethodsTestCase(base.TestCase): def test__create_config_job_error(self, mock_client_pywsman): result_xml = test_utils.build_soap_xml([{'ReturnValue': - drac_mgmt.RET_ERROR, + drac_common.RET_ERROR, 'Message': 'E_FAKE'}], resource_uris.DCIM_BIOSService) - mock_xml = _mock_wsman_root(result_xml) + mock_xml = test_utils.mock_wsman_root(result_xml) mock_pywsman = mock_client_pywsman.Client.return_value mock_pywsman.invoke.return_value = mock_xml @@ -194,7 +182,7 @@ class DracManagementTestCase(base.TestCase): result_xml = test_utils.build_soap_xml([{'InstanceID': 'HardDisk'}], resource_uris.DCIM_BootSourceSetting) - mock_xml = _mock_wsman_root(result_xml) + mock_xml = test_utils.mock_wsman_root(result_xml) mock_pywsman = mock_client_pywsman.Client.return_value mock_pywsman.enumerate.return_value = mock_xml @@ -213,7 +201,7 @@ class DracManagementTestCase(base.TestCase): result_xml = test_utils.build_soap_xml([{'InstanceID': 'NIC'}], resource_uris.DCIM_BootSourceSetting) - mock_xml = _mock_wsman_root(result_xml) + mock_xml = test_utils.mock_wsman_root(result_xml) mock_pywsman = mock_client_pywsman.Client.return_value mock_pywsman.enumerate.return_value = mock_xml @@ -235,7 +223,7 @@ class DracManagementTestCase(base.TestCase): 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.ANY, filter_query=mock.ANY) @mock.patch.object(drac_mgmt, '_check_for_config_job') @mock.patch.object(drac_mgmt, '_create_config_job') @@ -243,11 +231,11 @@ class DracManagementTestCase(base.TestCase): 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}], + drac_common.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_xml_enum = test_utils.mock_wsman_root(result_xml_enum) + mock_xml_invk = test_utils.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 @@ -270,12 +258,12 @@ class DracManagementTestCase(base.TestCase): 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, + drac_common.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_xml_enum = test_utils.mock_wsman_root(result_xml_enum) + mock_xml_invk = test_utils.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 @@ -302,7 +290,7 @@ class DracManagementTestCase(base.TestCase): self.driver.set_boot_device, self.task, boot_devices.PXE) mock_we.assert_called_once_with(resource_uris.DCIM_BootSourceSetting, - mock.ANY, mock.ANY) + mock.ANY, filter_query=mock.ANY) def test_get_sensors_data(self, mock_client_pywsman): self.assertRaises(NotImplementedError, diff --git a/ironic/tests/drivers/drac/test_power.py b/ironic/tests/drivers/drac/test_power.py index 9d97cf0844..7b8699a42d 100644 --- a/ironic/tests/drivers/drac/test_power.py +++ b/ironic/tests/drivers/drac/test_power.py @@ -48,13 +48,7 @@ class DracPowerInternalMethodsTestCase(base.TestCase): def test__get_power_state(self, mock_power_pywsman, mock_client_pywsman): 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 - - mock_xml = mock.Mock() - mock_xml.context.return_value = None - mock_xml.root.return_value = mock_xml_root - + mock_xml = test_utils.mock_wsman_root(result_xml) mock_pywsman_client = mock_client_pywsman.Client.return_value mock_pywsman_client.enumerate.return_value = mock_xml @@ -65,14 +59,10 @@ 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': + drac_common.RET_SUCCESS}], resource_uris.DCIM_ComputerSystem) - mock_xml_root = mock.Mock() - mock_xml_root.string.return_value = result_xml - - mock_xml = mock.Mock() - mock_xml.root.return_value = mock_xml_root - + mock_xml = test_utils.mock_wsman_root(result_xml) mock_pywsman_client = mock_client_pywsman.Client.return_value mock_pywsman_client.invoke.return_value = mock_xml @@ -92,15 +82,12 @@ 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', + result_xml = test_utils.build_soap_xml([{'ReturnValue': + drac_common.RET_ERROR, 'Message': 'error message'}], resource_uris.DCIM_ComputerSystem) - mock_xml_root = mock.Mock() - mock_xml_root.string.return_value = result_xml - - mock_xml = mock.Mock() - mock_xml.root.return_value = mock_xml_root + mock_xml = test_utils.mock_wsman_root(result_xml) mock_pywsman_client = mock_client_pywsman.Client.return_value mock_pywsman_client.invoke.return_value = mock_xml diff --git a/ironic/tests/drivers/drac/utils.py b/ironic/tests/drivers/drac/utils.py index 2d2100dd38..c31bc28071 100644 --- a/ironic/tests/drivers/drac/utils.py +++ b/ironic/tests/drivers/drac/utils.py @@ -17,6 +17,8 @@ from xml.etree import ElementTree +import mock + def build_soap_xml(items, namespace=None): """Build a SOAP XML. @@ -56,3 +58,15 @@ def build_soap_xml(items, namespace=None): envelope_element.append(body_element) return ElementTree.tostring(envelope_element) + + +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