diff --git a/dracclient/client.py b/dracclient/client.py index ff15ba1..ec802bc 100644 --- a/dracclient/client.py +++ b/dracclient/client.py @@ -383,9 +383,12 @@ class DRACClient(object): cim_name='DCIM:iDRACCardService', target=idrac_fqdd) - def list_lifecycle_settings(self): + def list_lifecycle_settings(self, by_name=False): """List the Lifecycle Controller configuration settings + :param by_name: Controls whether returned dictionary uses Lifecycle + attribute name as key. If set to False, instance_id + will be used. :returns: a dictionary with the Lifecycle Controller settings using its InstanceID as the key. The attributes are either LCEnumerableAttribute or LCStringAttribute objects. @@ -394,7 +397,49 @@ class DRACClient(object): :raises: DRACOperationFailed on error reported back by the DRAC interface """ - return self._lifecycle_cfg.list_lifecycle_settings() + return self._lifecycle_cfg.list_lifecycle_settings(by_name) + + def is_lifecycle_in_recovery(self): + """Checks if Lifecycle Controller in recovery mode or not + + This method checks the LCStatus value to determine if lifecycle + controller is in recovery mode by invoking GetRemoteServicesAPIStatus + from iDRAC. + + :returns: a boolean indicating if lifecycle controller is in recovery + :raises: WSManRequestFailure on request failures + :raises: WSManInvalidResponse when receiving invalid response + :raises: DRACOperationFailed on error reported back by the DRAC + interface + """ + + return self._lifecycle_cfg.is_lifecycle_in_recovery() + + def set_lifecycle_settings(self, settings): + """Sets lifecycle controller configuration + + It sets the pending_value parameter for each of the attributes + passed in. For the values to be applied, a config job must + be created. + + :param settings: a dictionary containing the proposed values, with + each key being the name of attribute and the value + being the proposed value. + :returns: a dictionary containing: + - The is_commit_required key with a boolean value indicating + whether a config job must be created for the values to be + applied. + - The is_reboot_required key with a RebootRequired enumerated + value indicating whether the server must be rebooted for the + values to be applied. Possible values are true and false. + :raises: WSManRequestFailure on request failures + :raises: WSManInvalidResponse when receiving invalid response + :raises: DRACOperationFailed on error reported back by the DRAC + interface + :raises: DRACUnexpectedReturnValue on return value mismatch + :raises: InvalidParameterValue on invalid Lifecycle attribute + """ + return self._lifecycle_cfg.set_lifecycle_settings(settings) def list_system_settings(self): """List the System configuration settings @@ -464,7 +509,9 @@ class DRACClient(object): cim_system_name='DCIM:ComputerSystem', reboot=False, start_time='TIME_NOW', - realtime=False): + realtime=False, + wait_for_idrac=True, + method_name='CreateTargetedConfigJob'): """Creates a configuration job. In CIM (Common Information Model), weak association is used to name an @@ -490,6 +537,10 @@ class DRACClient(object): schedule_job_execution is called :param realtime: Indicates if reatime mode should be used. Valid values are True and False. + :param wait_for_idrac: indicates whether or not to wait for the + iDRAC to be ready to accept commands before + issuing the command. + :param method_name: method of CIM object to invoke :returns: id of the created job :raises: WSManRequestFailure on request failures :raises: WSManInvalidResponse when receiving invalid response @@ -507,7 +558,9 @@ class DRACClient(object): cim_system_name=cim_system_name, reboot=reboot, start_time=start_time, - realtime=realtime) + realtime=realtime, + wait_for_idrac=wait_for_idrac, + method_name=method_name) def create_nic_config_job( self, @@ -646,6 +699,37 @@ class DRACClient(object): cim_creation_class_name='DCIM_BIOSService', cim_name='DCIM:BIOSService', target=self.BIOS_DEVICE_FQDD) + def commit_pending_lifecycle_changes( + self, + reboot=False, + start_time='TIME_NOW'): + """Applies all pending changes on Lifecycle by creating a config job + + :param reboot: indicates whether a RebootJob should also be + created or not + :param start_time: start time for job execution in format + yyyymmddhhmmss, the string 'TIME_NOW' which + means execute immediately or None which means + the job will not execute until + schedule_job_execution is called + :returns: id of the created job + :raises: WSManRequestFailure on request failures + :raises: WSManInvalidResponse when receiving invalid response + :raises: DRACOperationFailed on error reported back by the DRAC + interface, including start_time being in the past or + badly formatted start_time + :raises: DRACUnexpectedReturnValue on return value mismatch + """ + return self._job_mgmt.create_config_job( + resource_uri=uris.DCIM_LCService, + cim_creation_class_name='DCIM_LCService', + cim_name='DCIM:LCService', + target='', + reboot=reboot, + start_time=start_time, + wait_for_idrac=False, + method_name='CreateConfigJob') + def get_lifecycle_controller_version(self): """Returns the Lifecycle controller version diff --git a/dracclient/constants.py b/dracclient/constants.py index 9356060..ecaffa1 100644 --- a/dracclient/constants.py +++ b/dracclient/constants.py @@ -37,6 +37,9 @@ PRIMARY_STATUS = { # binary unit constants UNITS_KI = 2 ** 10 +# Lifecycle Controller status constant +LC_IN_RECOVERY = '4' + # Reboot required indicator # Note: When the iDRAC returns optional for this value, this indicates that diff --git a/dracclient/resources/job.py b/dracclient/resources/job.py index 32618db..5ec3443 100644 --- a/dracclient/resources/job.py +++ b/dracclient/resources/job.py @@ -102,7 +102,9 @@ class JobManagement(object): cim_system_name='DCIM:ComputerSystem', reboot=False, start_time='TIME_NOW', - realtime=False): + realtime=False, + wait_for_idrac=True, + method_name='CreateTargetedConfigJob'): """Creates a config job In CIM (Common Information Model), weak association is used to name an @@ -129,6 +131,10 @@ class JobManagement(object): job id. :param realtime: Indicates if reatime mode should be used. Valid values are True and False. Default value is False. + :param wait_for_idrac: indicates whether or not to wait for the + iDRAC to be ready to accept commands before + issuing the command. + :param method_name: method of CIM object to invoke :returns: id of the created job :raises: WSManRequestFailure on request failures :raises: WSManInvalidResponse when receiving invalid response @@ -153,10 +159,10 @@ class JobManagement(object): if start_time is not None: properties['ScheduledStartTime'] = start_time - doc = self.client.invoke(resource_uri, 'CreateTargetedConfigJob', + doc = self.client.invoke(resource_uri, method_name, selectors, properties, - expected_return_value=utils.RET_CREATED) - + expected_return_value=utils.RET_CREATED, + wait_for_idrac=wait_for_idrac) return self._get_job_id(doc) def create_reboot_job( diff --git a/dracclient/resources/lifecycle_controller.py b/dracclient/resources/lifecycle_controller.py index 9d903ef..c42bfd1 100644 --- a/dracclient/resources/lifecycle_controller.py +++ b/dracclient/resources/lifecycle_controller.py @@ -11,9 +11,9 @@ # License for the specific language governing permissions and limitations # under the License. +from dracclient import constants from dracclient.resources import uris from dracclient import utils -from dracclient import wsman class LifecycleControllerManagement(object): @@ -42,47 +42,6 @@ class LifecycleControllerManagement(object): return tuple(map(int, (lc_version_str.split('.')))) -class LCConfiguration(object): - - def __init__(self, client): - """Creates LifecycleControllerManagement object - - :param client: an instance of WSManClient - """ - self.client = client - - def list_lifecycle_settings(self): - """List the LC configuration settings - - :returns: a dictionary with the LC settings using InstanceID as the - key. The attributes are either LCEnumerableAttribute, - LCStringAttribute or LCIntegerAttribute objects. - :raises: WSManRequestFailure on request failures - :raises: WSManInvalidResponse when receiving invalid response - :raises: DRACOperationFailed on error reported back by the DRAC - interface - """ - result = {} - namespaces = [(uris.DCIM_LCEnumeration, LCEnumerableAttribute), - (uris.DCIM_LCString, LCStringAttribute)] - for (namespace, attr_cls) in namespaces: - attribs = self._get_config(namespace, attr_cls) - result.update(attribs) - return result - - def _get_config(self, resource, attr_cls): - result = {} - - doc = self.client.enumerate(resource) - - items = doc.find('.//{%s}Items' % wsman.NS_WSMAN) - for item in items: - attribute = attr_cls.parse(item) - result[attribute.instance_id] = attribute - - return result - - class LCAttribute(object): """Generic LC attribute class""" @@ -161,6 +120,17 @@ class LCEnumerableAttribute(LCAttribute): lifecycle_attr.current_value, lifecycle_attr.pending_value, lifecycle_attr.read_only, possible_values) + def validate(self, new_value): + """Validates new value""" + + if str(new_value) not in self.possible_values: + msg = ("Attribute '%(attr)s' cannot be set to value '%(val)s'." + " It must be in %(possible_values)r.") % { + 'attr': self.name, + 'val': new_value, + 'possible_values': self.possible_values} + return msg + class LCStringAttribute(LCAttribute): """String LC attribute class""" @@ -199,3 +169,96 @@ class LCStringAttribute(LCAttribute): return cls(lifecycle_attr.name, lifecycle_attr.instance_id, lifecycle_attr.current_value, lifecycle_attr.pending_value, lifecycle_attr.read_only, min_length, max_length) + + +class LCConfiguration(object): + + NAMESPACES = [(uris.DCIM_LCEnumeration, LCEnumerableAttribute), + (uris.DCIM_LCString, LCStringAttribute)] + + def __init__(self, client): + """Creates LifecycleControllerManagement object + + :param client: an instance of WSManClient + """ + self.client = client + + def list_lifecycle_settings(self, by_name=False): + """List the LC configuration settings + + :param by_name: Controls whether returned dictionary uses Lifecycle + attribute name or instance_id as key. + :returns: a dictionary with the LC settings using InstanceID as the + key. The attributes are either LCEnumerableAttribute, + LCStringAttribute or LCIntegerAttribute objects. + :raises: WSManRequestFailure on request failures + :raises: WSManInvalidResponse when receiving invalid response + :raises: DRACOperationFailed on error reported back by the DRAC + interface + """ + return utils.list_settings(self.client, self.NAMESPACES, by_name) + + def is_lifecycle_in_recovery(self): + """Check if Lifecycle Controller in recovery mode or not + + This method checks the LCStatus value to determine if lifecycle + controller is in recovery mode by invoking GetRemoteServicesAPIStatus + from iDRAC. + + :returns: a boolean indicating if lifecycle controller is in recovery + :raises: WSManRequestFailure on request failures + :raises: WSManInvalidResponse when receiving invalid response + :raises: DRACOperationFailed on error reported back by the DRAC + interface + """ + + selectors = {'SystemCreationClassName': 'DCIM_ComputerSystem', + 'SystemName': 'DCIM:ComputerSystem', + 'CreationClassName': 'DCIM_LCService', + 'Name': 'DCIM:LCService'} + + doc = self.client.invoke(uris.DCIM_LCService, + 'GetRemoteServicesAPIStatus', + selectors, + {}, + expected_return_value=utils.RET_SUCCESS, + wait_for_idrac=False) + + lc_status = utils.find_xml(doc, + 'LCStatus', + uris.DCIM_LCService).text + + return lc_status == constants.LC_IN_RECOVERY + + def set_lifecycle_settings(self, settings): + """Sets the Lifecycle Controller configuration + + It sets the pending_value parameter for each of the attributes + passed in. For the values to be applied, a config job must + be created. + + :param settings: a dictionary containing the proposed values, with + each key being the name of attribute and the value + being the proposed value. + :returns: a dictionary containing: + - The is_commit_required key with a boolean value indicating + whether a config job must be created for the values to be + applied. + - The is_reboot_required key with a RebootRequired enumerated + value indicating whether the server must be rebooted for the + values to be applied. Possible values are true and false. + :raises: WSManRequestFailure on request failures + :raises: WSManInvalidResponse when receiving invalid response + :raises: DRACOperationFailed on error reported back by the DRAC + interface + """ + + return utils.set_settings('Lifecycle', + self.client, + self.NAMESPACES, + settings, + uris.DCIM_LCService, + "DCIM_LCService", + "DCIM:LCService", + '', + wait_for_idrac=False) diff --git a/dracclient/tests/test_bios.py b/dracclient/tests/test_bios.py index 22c0ff1..139ad03 100644 --- a/dracclient/tests/test_bios.py +++ b/dracclient/tests/test_bios.py @@ -353,7 +353,8 @@ class ClientBIOSConfigurationTestCase(base.BaseTest): result) mock_invoke.assert_called_once_with( mock.ANY, uris.DCIM_BIOSService, 'SetAttributes', - expected_selectors, expected_properties) + expected_selectors, expected_properties, + wait_for_idrac=True) def test_set_bios_settings_error(self, mock_requests, mock_wait_until_idrac_is_ready): diff --git a/dracclient/tests/test_idrac_card.py b/dracclient/tests/test_idrac_card.py index 21e46d7..6228554 100644 --- a/dracclient/tests/test_idrac_card.py +++ b/dracclient/tests/test_idrac_card.py @@ -214,7 +214,8 @@ class ClientiDRACCardConfigurationTestCase(base.BaseTest): result) mock_invoke.assert_called_once_with( mock.ANY, uris.DCIM_iDRACCardService, 'SetAttributes', - expected_selectors, expected_properties) + expected_selectors, expected_properties, + wait_for_idrac=True) @mock.patch.object(dracclient.client.WSManClient, 'invoke', spec_set=True, autospec=True) @@ -245,7 +246,8 @@ class ClientiDRACCardConfigurationTestCase(base.BaseTest): result) mock_invoke.assert_called_once_with( mock.ANY, uris.DCIM_iDRACCardService, 'SetAttributes', - expected_selectors, expected_properties) + expected_selectors, expected_properties, + wait_for_idrac=True) def test_set_idrac_settings_with_too_long_string( self, mock_requests, mock_wait_until_idrac_is_ready): diff --git a/dracclient/tests/test_job.py b/dracclient/tests/test_job.py index 051b847..adb1a34 100644 --- a/dracclient/tests/test_job.py +++ b/dracclient/tests/test_job.py @@ -226,12 +226,43 @@ class ClientJobManagementTestCase(base.BaseTest): self.assertEqual(mock_requests.call_count, 2) + @mock.patch.object(dracclient.client.WSManClient, 'invoke', + spec_set=True, autospec=True) + def test_create_config_job_for_lifecycle(self, mock_invoke): + cim_creation_class_name = 'DCIM_LCService' + cim_name = 'DCIM:LCService' + target = '' + + expected_selectors = {'CreationClassName': cim_creation_class_name, + 'Name': cim_name, + 'SystemCreationClassName': 'DCIM_ComputerSystem', + 'SystemName': 'DCIM:ComputerSystem'} + expected_properties = {'Target': target, + 'ScheduledStartTime': 'TIME_NOW'} + + mock_invoke.return_value = lxml.etree.fromstring( + test_utils.JobInvocations[uris.DCIM_LCService][ + 'CreateConfigJob']['ok']) + + job_id = self.drac_client.create_config_job( + uris.DCIM_LCService, cim_creation_class_name, cim_name, target, + start_time='TIME_NOW', + wait_for_idrac=False, method_name='CreateConfigJob') + + mock_invoke.assert_called_once_with( + mock.ANY, uris.DCIM_LCService, 'CreateConfigJob', + expected_selectors, expected_properties, + expected_return_value=utils.RET_CREATED, + wait_for_idrac=False) + self.assertEqual('JID_442507917525', job_id) + @mock.patch.object(dracclient.client.WSManClient, 'invoke', spec_set=True, autospec=True) def test_create_config_job(self, mock_invoke): cim_creation_class_name = 'DCIM_BIOSService' cim_name = 'DCIM:BIOSService' target = 'BIOS.Setup.1-1' + wait_for_idrac = True expected_selectors = {'CreationClassName': cim_creation_class_name, 'Name': cim_name, 'SystemCreationClassName': 'DCIM_ComputerSystem', @@ -249,7 +280,8 @@ class ClientJobManagementTestCase(base.BaseTest): mock_invoke.assert_called_once_with( mock.ANY, uris.DCIM_BIOSService, 'CreateTargetedConfigJob', expected_selectors, expected_properties, - expected_return_value=utils.RET_CREATED) + expected_return_value=utils.RET_CREATED, + wait_for_idrac=wait_for_idrac) self.assertEqual('JID_442507917525', job_id) @mock.patch.object(dracclient.client.WSManClient, 'invoke', @@ -259,6 +291,7 @@ class ClientJobManagementTestCase(base.BaseTest): cim_name = 'DCIM:BIOSService' target = 'BIOS.Setup.1-1' start_time = "20140924120105" + wait_for_idrac = True expected_selectors = {'CreationClassName': cim_creation_class_name, 'Name': cim_name, 'SystemCreationClassName': 'DCIM_ComputerSystem', @@ -276,7 +309,8 @@ class ClientJobManagementTestCase(base.BaseTest): mock_invoke.assert_called_once_with( mock.ANY, uris.DCIM_BIOSService, 'CreateTargetedConfigJob', expected_selectors, expected_properties, - expected_return_value=utils.RET_CREATED) + expected_return_value=utils.RET_CREATED, + wait_for_idrac=wait_for_idrac) self.assertEqual('JID_442507917525', job_id) @mock.patch.object(dracclient.client.WSManClient, 'invoke', @@ -286,6 +320,7 @@ class ClientJobManagementTestCase(base.BaseTest): cim_name = 'DCIM:BIOSService' target = 'BIOS.Setup.1-1' start_time = None + wait_for_idrac = True expected_selectors = {'CreationClassName': cim_creation_class_name, 'Name': cim_name, 'SystemCreationClassName': 'DCIM_ComputerSystem', @@ -302,7 +337,8 @@ class ClientJobManagementTestCase(base.BaseTest): mock_invoke.assert_called_once_with( mock.ANY, uris.DCIM_BIOSService, 'CreateTargetedConfigJob', expected_selectors, expected_properties, - expected_return_value=utils.RET_CREATED) + expected_return_value=utils.RET_CREATED, + wait_for_idrac=wait_for_idrac) self.assertEqual('JID_442507917525', job_id) @requests_mock.Mocker() @@ -323,12 +359,32 @@ class ClientJobManagementTestCase(base.BaseTest): exceptions.DRACOperationFailed, self.drac_client.create_config_job, uris.DCIM_BIOSService, cim_creation_class_name, cim_name, target) + @requests_mock.Mocker() + @mock.patch.object(dracclient.client.WSManClient, + 'wait_until_idrac_is_ready', spec_set=True, + autospec=True) + def test_create_config_job_for_lifecycle_failed( + self, mock_requests, + mock_wait_until_idrac_is_ready): + cim_creation_class_name = 'DCIM_LCService' + cim_name = 'DCIM:LCService' + target = '' + mock_requests.post( + 'https://1.2.3.4:443/wsman', + text=test_utils.JobInvocations[uris.DCIM_LCService][ + 'CreateConfigJob']['error']) + + self.assertRaises( + exceptions.DRACOperationFailed, self.drac_client.create_config_job, + uris.DCIM_LCService, cim_creation_class_name, cim_name, target) + @mock.patch.object(dracclient.client.WSManClient, 'invoke', spec_set=True, autospec=True) def test_create_config_job_with_reboot(self, mock_invoke): cim_creation_class_name = 'DCIM_BIOSService' cim_name = 'DCIM:BIOSService' target = 'BIOS.Setup.1-1' + wait_for_idrac = True expected_selectors = {'CreationClassName': cim_creation_class_name, 'Name': cim_name, 'SystemCreationClassName': 'DCIM_ComputerSystem', @@ -347,7 +403,8 @@ class ClientJobManagementTestCase(base.BaseTest): mock_invoke.assert_called_once_with( mock.ANY, uris.DCIM_BIOSService, 'CreateTargetedConfigJob', expected_selectors, expected_properties, - expected_return_value=utils.RET_CREATED) + expected_return_value=utils.RET_CREATED, + wait_for_idrac=wait_for_idrac) self.assertEqual('JID_442507917525', job_id) @mock.patch.object(dracclient.client.WSManClient, 'invoke', spec_set=True, @@ -356,6 +413,7 @@ class ClientJobManagementTestCase(base.BaseTest): cim_creation_class_name = 'DCIM_BIOSService' cim_name = 'DCIM:BIOSService' target = 'BIOS.Setup.1-1' + wait_for_idrac = True expected_selectors = {'CreationClassName': cim_creation_class_name, 'Name': cim_name, 'SystemCreationClassName': 'DCIM_ComputerSystem', @@ -374,7 +432,8 @@ class ClientJobManagementTestCase(base.BaseTest): mock_invoke.assert_called_once_with( mock.ANY, uris.DCIM_BIOSService, 'CreateTargetedConfigJob', expected_selectors, expected_properties, - expected_return_value=utils.RET_CREATED) + expected_return_value=utils.RET_CREATED, + wait_for_idrac=wait_for_idrac) self.assertEqual('JID_442507917525', job_id) @mock.patch.object(dracclient.client.WSManClient, 'invoke', spec_set=True, diff --git a/dracclient/tests/test_lifecycle_controller.py b/dracclient/tests/test_lifecycle_controller.py index 3427cc5..58352bc 100644 --- a/dracclient/tests/test_lifecycle_controller.py +++ b/dracclient/tests/test_lifecycle_controller.py @@ -11,14 +11,20 @@ # License for the specific language governing permissions and limitations # under the License. +import lxml.etree import mock +import re import requests_mock import dracclient.client +from dracclient import constants +from dracclient import exceptions +import dracclient.resources.job from dracclient.resources import lifecycle_controller from dracclient.resources import uris from dracclient.tests import base from dracclient.tests import utils as test_utils +from dracclient import utils class ClientLifecycleControllerManagementTestCase(base.BaseTest): @@ -40,6 +46,7 @@ class ClientLifecycleControllerManagementTestCase(base.BaseTest): self.assertEqual((2, 1, 0), version) +@requests_mock.Mocker() class ClientLCConfigurationTestCase(base.BaseTest): def setUp(self): @@ -47,12 +54,12 @@ class ClientLCConfigurationTestCase(base.BaseTest): self.drac_client = dracclient.client.DRACClient( **test_utils.FAKE_ENDPOINT) - @requests_mock.Mocker() @mock.patch.object(dracclient.client.WSManClient, 'wait_until_idrac_is_ready', spec_set=True, autospec=True) - def test_list_lifecycle_settings(self, mock_requests, - mock_wait_until_idrac_is_ready): + def test_list_lifecycle_settings_by_instance_id( + self, mock_requests, + mock_wait_until_idrac_is_ready): expected_enum_attr = lifecycle_controller.LCEnumerableAttribute( name='Lifecycle Controller State', instance_id='LifecycleController.Embedded.1#LCAttributes.1#LifecycleControllerState', # noqa @@ -74,7 +81,8 @@ class ClientLCConfigurationTestCase(base.BaseTest): {'text': test_utils.LifecycleControllerEnumerations[ uris.DCIM_LCString]['ok']}]) - lifecycle_settings = self.drac_client.list_lifecycle_settings() + lifecycle_settings = self.drac_client.list_lifecycle_settings( + by_name=False) self.assertEqual(14, len(lifecycle_settings)) # enumerable attribute @@ -89,3 +97,203 @@ class ClientLCConfigurationTestCase(base.BaseTest): lifecycle_settings) self.assertEqual(expected_string_attr, lifecycle_settings['LifecycleController.Embedded.1#LCAttributes.1#SystemID']) # noqa + + @mock.patch.object(dracclient.client.WSManClient, + 'wait_until_idrac_is_ready', spec_set=True, + autospec=True) + def test_list_lifecycle_settings_by_name( + self, mock_requests, + mock_wait_until_idrac_is_ready): + expected_enum_attr = lifecycle_controller.LCEnumerableAttribute( + name='Lifecycle Controller State', + instance_id='LifecycleController.Embedded.1#LCAttributes.1#LifecycleControllerState', # noqa + read_only=False, + current_value='Enabled', + pending_value=None, + possible_values=['Disabled', 'Enabled', 'Recovery']) + expected_string_attr = lifecycle_controller.LCStringAttribute( + name='SYSID', + instance_id='LifecycleController.Embedded.1#LCAttributes.1#SystemID', # noqa + read_only=True, + current_value='639', + pending_value=None, + min_length=0, + max_length=3) + + mock_requests.post('https://1.2.3.4:443/wsman', [ + {'text': test_utils.LifecycleControllerEnumerations[ + uris.DCIM_LCEnumeration]['ok']}, + {'text': test_utils.LifecycleControllerEnumerations[ + uris.DCIM_LCString]['ok']}]) + + lifecycle_settings = self.drac_client.list_lifecycle_settings( + by_name=True) + + self.assertEqual(14, len(lifecycle_settings)) + # enumerable attribute + self.assertIn( + 'Lifecycle Controller State', + lifecycle_settings) + self.assertEqual(expected_enum_attr, lifecycle_settings[ + 'Lifecycle Controller State']) + # string attribute + self.assertIn( + 'SYSID', + lifecycle_settings) + self.assertEqual(expected_string_attr, + lifecycle_settings['SYSID']) + + @mock.patch.object(dracclient.client.WSManClient, 'invoke', + spec_set=True, autospec=True) + def test_is_lifecycle_in_recovery(self, mock_requests, + mock_invoke): + expected_selectors = {'CreationClassName': 'DCIM_LCService', + 'SystemName': 'DCIM:ComputerSystem', + 'Name': 'DCIM:LCService', + 'SystemCreationClassName': 'DCIM_ComputerSystem'} + mock_invoke.return_value = lxml.etree.fromstring( + test_utils.LifecycleControllerInvocations[uris.DCIM_LCService][ + 'GetRemoteServicesAPIStatus']['is_recovery']) + result = self.drac_client.is_lifecycle_in_recovery() + + mock_invoke.assert_called_once_with( + mock.ANY, uris.DCIM_LCService, 'GetRemoteServicesAPIStatus', + expected_selectors, {}, + expected_return_value=utils.RET_SUCCESS, + wait_for_idrac=False) + + self.assertEqual(True, result) + + @mock.patch.object(dracclient.client.WSManClient, + 'invoke', spec_set=True, + autospec=True) + def test_set_lifecycle_settings(self, mock_requests, + mock_invoke): + + mock_requests.post('https://1.2.3.4:443/wsman', [ + {'text': test_utils.LifecycleControllerEnumerations[ + uris.DCIM_LCEnumeration]['ok']}, + {'text': test_utils.LifecycleControllerEnumerations[ + uris.DCIM_LCString]['ok']}]) + + mock_invoke.return_value = lxml.etree.fromstring( + test_utils.LifecycleControllerInvocations[uris.DCIM_LCService][ + 'SetAttributes']['ok']) + + result = self.drac_client.set_lifecycle_settings( + {'Collect System Inventory on Restart': 'Disabled'}) + + self.assertEqual({'is_commit_required': True, + 'is_reboot_required': constants.RebootRequired.false + }, + result) + + @mock.patch.object(dracclient.client.WSManClient, + 'wait_until_idrac_is_ready', spec_set=True, + autospec=True) + def test_set_lifecycle_settings_with_unknown_attr( + self, mock_requests, mock_wait_until_idrac_is_ready): + mock_requests.post('https://1.2.3.4:443/wsman', [ + {'text': test_utils.LifecycleControllerEnumerations[ + uris.DCIM_LCEnumeration]['ok']}, + {'text': test_utils.LifecycleControllerEnumerations[ + uris.DCIM_LCString]['ok']}, + {'text': test_utils.LifecycleControllerInvocations[ + uris.DCIM_LCService]['SetAttributes']['error']}]) + + self.assertRaises(exceptions.InvalidParameterValue, + self.drac_client.set_lifecycle_settings, + {'foo': 'bar'}) + + @mock.patch.object(dracclient.client.WSManClient, + 'wait_until_idrac_is_ready', spec_set=True, + autospec=True) + def test_set_lifecycle_settings_with_unchanged_attr( + self, mock_requests, mock_wait_until_idrac_is_ready): + mock_requests.post('https://1.2.3.4:443/wsman', [ + {'text': test_utils.LifecycleControllerEnumerations[ + uris.DCIM_LCEnumeration]['ok']}, + {'text': test_utils.LifecycleControllerEnumerations[ + uris.DCIM_LCString]['ok']}]) + + result = self.drac_client.set_lifecycle_settings( + {'Lifecycle Controller State': 'Enabled'}) + + self.assertEqual({'is_commit_required': False, + 'is_reboot_required': + constants.RebootRequired.false}, + result) + + @mock.patch.object(dracclient.client.WSManClient, + 'wait_until_idrac_is_ready', spec_set=True, + autospec=True) + def test_set_lifecycle_settings_with_readonly_attr( + self, mock_requests, mock_wait_until_idrac_is_ready): + expected_message = ("Cannot set read-only Lifecycle attributes: " + "['Licensed'].") + mock_requests.post('https://1.2.3.4:443/wsman', [ + {'text': test_utils.LifecycleControllerEnumerations[ + uris.DCIM_LCEnumeration]['ok']}, + {'text': test_utils.LifecycleControllerEnumerations[ + uris.DCIM_LCString]['ok']}]) + + self.assertRaisesRegexp( + exceptions.DRACOperationFailed, re.escape(expected_message), + self.drac_client.set_lifecycle_settings, {'Licensed': 'yes'}) + + @mock.patch.object(dracclient.client.WSManClient, + 'wait_until_idrac_is_ready', spec_set=True, + autospec=True) + def test_set_lifecycle_settings_with_incorrect_enum_value( + self, mock_requests, mock_wait_until_idrac_is_ready): + expected_message = ("Attribute 'Lifecycle Controller State' cannot " + "be set to value 'foo'. It must be in " + "['Disabled', 'Enabled', 'Recovery'].") + + mock_requests.post('https://1.2.3.4:443/wsman', [ + {'text': test_utils.LifecycleControllerEnumerations[ + uris.DCIM_LCEnumeration]['ok']}, + {'text': test_utils.LifecycleControllerEnumerations[ + uris.DCIM_LCString]['ok']}]) + self.assertRaisesRegexp( + exceptions.DRACOperationFailed, re.escape(expected_message), + self.drac_client.set_lifecycle_settings, + {'Lifecycle Controller State': 'foo'}) + + +class ClientLCChangesTestCase(base.BaseTest): + + def setUp(self): + super(ClientLCChangesTestCase, self).setUp() + self.drac_client = dracclient.client.DRACClient( + **test_utils.FAKE_ENDPOINT) + + @mock.patch.object(dracclient.resources.job.JobManagement, + 'create_config_job', spec_set=True, autospec=True) + def test_commit_pending_lifecycle_changes(self, mock_create_config_job): + + self.drac_client.commit_pending_lifecycle_changes() + + mock_create_config_job.assert_called_once_with( + mock.ANY, resource_uri=uris.DCIM_LCService, + cim_creation_class_name='DCIM_LCService', + cim_name='DCIM:LCService', target='', + reboot=False, start_time='TIME_NOW', + wait_for_idrac=False, + method_name='CreateConfigJob') + + @mock.patch.object(dracclient.resources.job.JobManagement, + 'create_config_job', spec_set=True, autospec=True) + def test_commit_pending_lifecycle_changes_with_time( + self, mock_create_config_job): + timestamp = '20140924140201' + self.drac_client.commit_pending_lifecycle_changes( + start_time=timestamp) + + mock_create_config_job.assert_called_once_with( + mock.ANY, resource_uri=uris.DCIM_LCService, + cim_creation_class_name='DCIM_LCService', + cim_name='DCIM:LCService', target='', + reboot=False, start_time=timestamp, + wait_for_idrac=False, + method_name='CreateConfigJob') diff --git a/dracclient/tests/test_nic.py b/dracclient/tests/test_nic.py index e393d5c..7029df3 100644 --- a/dracclient/tests/test_nic.py +++ b/dracclient/tests/test_nic.py @@ -214,7 +214,8 @@ class ClientNICTestCase(base.BaseTest): mock_invoke.assert_called_once_with( mock.ANY, uris.DCIM_NICService, 'SetAttributes', - expected_selectors, expected_properties) + expected_selectors, expected_properties, + wait_for_idrac=True) @mock.patch.object(dracclient.client.WSManClient, 'invoke', spec_set=True, autospec=True) @@ -250,7 +251,8 @@ class ClientNICTestCase(base.BaseTest): mock_invoke.assert_called_once_with( mock.ANY, uris.DCIM_NICService, 'SetAttributes', - expected_selectors, expected_properties) + expected_selectors, expected_properties, + wait_for_idrac=True) @mock.patch.object(dracclient.client.WSManClient, 'invoke', spec_set=True, autospec=True) @@ -286,7 +288,8 @@ class ClientNICTestCase(base.BaseTest): mock_invoke.assert_called_once_with( mock.ANY, uris.DCIM_NICService, 'SetAttributes', - expected_selectors, expected_properties) + expected_selectors, expected_properties, + wait_for_idrac=True) def test_set_nic_settings_error(self, mock_requests, mock_wait_until_idrac_is_ready): diff --git a/dracclient/tests/utils.py b/dracclient/tests/utils.py index 49acc2f..0ac622b 100644 --- a/dracclient/tests/utils.py +++ b/dracclient/tests/utils.py @@ -133,6 +133,14 @@ JobInvocations = { 'error': load_wsman_xml( 'bios_service-invoke-delete_pending_configuration-error'), }, + }, + uris.DCIM_LCService: { + 'CreateConfigJob': { + 'ok': load_wsman_xml( + 'lc_service-invoke-create_config_job-ok'), + 'error': load_wsman_xml( + 'lc_service-invoke-create_config_job-error'), + }, } } @@ -192,7 +200,15 @@ LifecycleControllerInvocations = { 'GetRemoteServicesAPIStatus': { 'is_ready': load_wsman_xml('lc_getremoteservicesapistatus_ready'), 'is_not_ready': load_wsman_xml( - 'lc_getremoteservicesapistatus_not_ready') + 'lc_getremoteservicesapistatus_not_ready'), + 'is_recovery': load_wsman_xml( + 'lc_getremoteservicesapistatus_recovery'), + }, + 'SetAttributes': { + 'ok': load_wsman_xml( + 'lc_service-invoke-set_attributes-ok'), + 'error': load_wsman_xml( + 'lc_service-invoke-set_attributes-error'), } } } diff --git a/dracclient/tests/wsman_mocks/lc_getremoteservicesapistatus_recovery.xml b/dracclient/tests/wsman_mocks/lc_getremoteservicesapistatus_recovery.xml new file mode 100644 index 0000000..97b3a3a --- /dev/null +++ b/dracclient/tests/wsman_mocks/lc_getremoteservicesapistatus_recovery.xml @@ -0,0 +1,19 @@ + + + http://schemas.xmlsoap.org/ws/2004/08/addressing/role/anonymous + http://schemas.dell.com/wbem/wscim/1/cim-schema/2/DCIM_LCService/GetRemoteServicesAPIStatusResponse + uuid:18745811-2782-4d30-a288-8f001a895215 + uuid:9ec203ba-4fc0-1fc0-8094-98d61742a844 + + + + 4 + Lifecycle Controller Remote Services is not ready. + LC060 + 0 + 0 + 7 + 1 + + + diff --git a/dracclient/tests/wsman_mocks/lc_service-invoke-create_config_job-error.xml b/dracclient/tests/wsman_mocks/lc_service-invoke-create_config_job-error.xml new file mode 100644 index 0000000..c375bb7 --- /dev/null +++ b/dracclient/tests/wsman_mocks/lc_service-invoke-create_config_job-error.xml @@ -0,0 +1,17 @@ + + + http://schemas.xmlsoap.org/ws/2004/08/addressing/role/anonymous + http://schemas.dell.com/wbem/wscim/1/cim-schema/2/DCIM_LCService/CreateConfigJobResponse + uuid:80cf5e1b-b109-4ef5-87c8-5b03ce6ba117 + uuid:e57fa514-2189-1189-8ec1-a36fc6fe83b0 + + + + Configuration job already created, cannot create another config job on specified target until existing job is completed or is cancelled + LC007 + 2 + + + diff --git a/dracclient/tests/wsman_mocks/lc_service-invoke-create_config_job-ok.xml b/dracclient/tests/wsman_mocks/lc_service-invoke-create_config_job-ok.xml new file mode 100644 index 0000000..b7ec83c --- /dev/null +++ b/dracclient/tests/wsman_mocks/lc_service-invoke-create_config_job-ok.xml @@ -0,0 +1,28 @@ + + + http://schemas.xmlsoap.org/ws/2004/08/addressing/role/anonymous + http://schemas.dell.com/wbem/wscim/1/cim-schema/2/DCIM_LCService/CreateConfigJobResponse + uuid:fc2fdae5-6ac2-4338-9b2e-e69b813af829 + uuid:d7d89957-2189-1189-8ec0-a36fc6fe83b0 + + + + + + http://schemas.xmlsoap.org/ws/2004/08/addressing/role/anonymous + + http://schemas.dell.com/wbem/wscim/1/cim-schema/2/DCIM_LifecycleJob + + JID_442507917525 + root/dcim + + + + + 4096 + + + diff --git a/dracclient/tests/wsman_mocks/lc_service-invoke-set_attributes-error.xml b/dracclient/tests/wsman_mocks/lc_service-invoke-set_attributes-error.xml new file mode 100644 index 0000000..c2c0b75 --- /dev/null +++ b/dracclient/tests/wsman_mocks/lc_service-invoke-set_attributes-error.xml @@ -0,0 +1,21 @@ + + + http://schemas.xmlsoap.org/ws/2004/08/addressing/role/anonymous + + http://schemas.dell.com/wbem/wscim/1/cim-schema/2/DCIM_LCService/SetAttributesResponse + + uuid:bf8adefe-6fc0-456d-b97c-fd8d4aca2d6c + + uuid:84abf7b9-7176-1176-a11c-a53ffbd9bed4 + + + + + Invalid AttributeName. + LC057 + 2 + + + diff --git a/dracclient/tests/wsman_mocks/lc_service-invoke-set_attributes-ok.xml b/dracclient/tests/wsman_mocks/lc_service-invoke-set_attributes-ok.xml new file mode 100644 index 0000000..7c4ff98 --- /dev/null +++ b/dracclient/tests/wsman_mocks/lc_service-invoke-set_attributes-ok.xml @@ -0,0 +1,24 @@ + + + http://schemas.xmlsoap.org/ws/2004/08/addressing/role/anonymous + + http://schemas.dell.com/wbem/wscim/1/cim-schema/2/DCIM_LCService/SetAttributesResponse + + uuid:bf8adefe-6fc0-456d-b97c-fd8d4aca2d6c + + uuid:84abf7b9-7176-1176-a11c-a53ffbd9bed4 + + + + + LC001 + The command was successful + 0 + No + Set PendingValue + + + + diff --git a/dracclient/utils.py b/dracclient/utils.py index 86b6828..1814cda 100644 --- a/dracclient/utils.py +++ b/dracclient/utils.py @@ -233,7 +233,7 @@ def validate_integer_value(value, attr_name, error_msgs): def list_settings(client, namespaces, by_name=True, fqdd_filter=None, - name_formatter=None): + name_formatter=None, wait_for_idrac=True): """List the configuration settings :param client: an instance of WSManClient. @@ -245,6 +245,9 @@ def list_settings(client, namespaces, by_name=True, fqdd_filter=None, :param name_formatter: a method used to format the keys in the returned dictionary. By default, attribute.name will be used. + :param wait_for_idrac: indicates whether or not to wait for the + iDRAC to be ready to accept commands before + issuing the command. :returns: a dictionary with the settings using name or instance_id as the key. :raises: WSManRequestFailure on request failures @@ -256,7 +259,7 @@ def list_settings(client, namespaces, by_name=True, fqdd_filter=None, result = {} for (namespace, attr_cls) in namespaces: attribs = _get_config(client, namespace, attr_cls, by_name, - fqdd_filter, name_formatter) + fqdd_filter, name_formatter, wait_for_idrac) if not set(result).isdisjoint(set(attribs)): raise exceptions.DRACOperationFailed( drac_messages=('Colliding attributes %r' % ( @@ -266,10 +269,10 @@ def list_settings(client, namespaces, by_name=True, fqdd_filter=None, def _get_config(client, resource, attr_cls, by_name, fqdd_filter, - name_formatter): + name_formatter, wait_for_idrac): result = {} - doc = client.enumerate(resource) + doc = client.enumerate(resource, wait_for_idrac=wait_for_idrac) items = doc.find('.//{%s}Items' % wsman.NS_WSMAN) for item in items: @@ -297,7 +300,8 @@ def set_settings(settings_type, cim_creation_class_name, cim_name, target, - name_formatter=None): + name_formatter=None, + wait_for_idrac=True): """Generically handles setting various types of settings on the iDRAC This method pulls the current list of settings from the iDRAC then compares @@ -318,6 +322,9 @@ def set_settings(settings_type, :param name_formatter: a method used to format the keys in the returned dictionary. By default, attribute.name will be used. + :param wait_for_idrac: indicates whether or not to wait for the + iDRAC to be ready to accept commands before issuing + the command :returns: a dictionary containing: - The is_commit_required key with a boolean value indicating whether a config job must be created for the values to be @@ -335,12 +342,15 @@ def set_settings(settings_type, """ current_settings = list_settings(client, namespaces, by_name=True, - name_formatter=name_formatter) + name_formatter=name_formatter, + wait_for_idrac=wait_for_idrac) unknown_keys = set(new_settings) - set(current_settings) if unknown_keys: - msg = ('Unknown %(settings_type)s attributes found: %(unknown_keys)r' % - {'settings_type': settings_type, 'unknown_keys': unknown_keys}) + msg = ('Unknown %(settings_type)s attributes found: ' + '%(unknown_keys)r' % + {'settings_type': settings_type, + 'unknown_keys': unknown_keys}) raise exceptions.InvalidParameterValue(reason=msg) read_only_keys = [] @@ -393,11 +403,14 @@ def set_settings(settings_type, 'Name': cim_name, 'SystemCreationClassName': 'DCIM_ComputerSystem', 'SystemName': 'DCIM:ComputerSystem'} + properties = {'Target': target, 'AttributeName': attrib_names, 'AttributeValue': [new_settings[attr] for attr in attrib_names]} + doc = client.invoke(resource_uri, 'SetAttributes', - selectors, properties) + selectors, properties, + wait_for_idrac=wait_for_idrac) return build_return_dict(doc, resource_uri)