From 10df06f6c34f9428d6ba856704c2946040161483 Mon Sep 17 00:00:00 2001 From: Richard Pioso Date: Tue, 20 Jun 2017 15:54:34 -0400 Subject: [PATCH] Refactor iDRAC is ready functionality Web Services Management (WS-Management and WS-Man) requests/commands can fail or return invalid results when issued to an Integrated Dell Remote Access Controller (iDRAC) whose Lifecycle Controller remote service is not "ready". Specifically, that applies to the WS-Man Enumerate and Invoke operations. A Dell technical white paper [0], "Lifecycle Controller Integration -- Best Practices Guide", states that for Lifecycle Controller firmware 1.5.0 and later "The Lifecycle Controller remote service must be in a 'ready' state before running any other WSMAN commands." That applies to almost all of the workflows and use cases documented by that paper and supported by this project, openstack/python-dracclient. That document describes how to determine the readiness of the Lifecycle Controller remote service. A project commit [1] implements that. This refactors that patch in preparation for changing the internal implementation of the project's APIs so that they follow that best practice. The implementation of is_idrac_ready() and wait_until_idrac_is_ready() have been relocated further down the call stack, to the iDRAC specialization of the WS-Man client defined by class dracclient.client.WSManClient. Those methods continue to be available through the API provided by class dracclient.client.Client. No changes have been made to this project's APIs nor to any functional behavior. [0] http://en.community.dell.com/techcenter/extras/m/white_papers/20442332 [1] https://github.com/openstack/python-dracclient/commit/39253bb272a7d4cfcc161c19708b8c6949a21240 Change-Id: I87996bbca129995f6c84848ebdb0c33cfedeea53 Partial-Bug: #1697558 Related-Bug: #1691808 --- dracclient/client.py | 84 +++++++++++++++---- dracclient/resources/lifecycle_controller.py | 33 -------- dracclient/tests/test_client.py | 47 +++++++++++ dracclient/tests/test_lifecycle_controller.py | 46 ---------- 4 files changed, 113 insertions(+), 97 deletions(-) diff --git a/dracclient/client.py b/dracclient/client.py index cc0968e..23eb13a 100644 --- a/dracclient/client.py +++ b/dracclient/client.py @@ -29,6 +29,8 @@ from dracclient.resources import uris from dracclient import utils from dracclient import wsman +IDRAC_IS_READY = "LC061" + LOG = logging.getLogger(__name__) @@ -513,7 +515,7 @@ class DRACClient(object): :raises: DRACUnexpectedReturnValue on return value mismatch """ - return self._lifecycle_cfg.is_idrac_ready() + return self.client.is_idrac_ready() def wait_until_idrac_is_ready(self, retries=24, retry_delay=10): """Waits until the iDRAC is in a ready state @@ -528,23 +530,7 @@ class DRACClient(object): :raises: DRACUnexpectedReturnValue on return value mismatch """ - # Try every 10 seconds over 4 minutes for the iDRAC to become ready - while retries > 0: - LOG.debug("Checking to see if the iDRAC is ready") - - if self.is_idrac_ready(): - LOG.debug("The iDRAC is ready") - return - - LOG.debug("The iDRAC is not ready") - retries -= 1 - if retries > 0: - time.sleep(retry_delay) - - if retries == 0: - err_msg = "Timed out waiting for the iDRAC to become ready" - LOG.error(err_msg) - raise exceptions.DRACOperationFailed(drac_messages=err_msg) + return self.client.wait_until_idrac_is_ready(retries, retry_delay) class WSManClient(wsman.Client): @@ -591,3 +577,65 @@ class WSManClient(wsman.Client): actual_return_value=return_value) return resp + + def is_idrac_ready(self): + """Indicates if the iDRAC is ready to accept commands + + Returns a boolean indicating if the iDRAC is ready to accept + commands. + + :returns: Boolean indicating iDRAC readiness + :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 + """ + + selectors = {'SystemCreationClassName': 'DCIM_ComputerSystem', + 'SystemName': 'DCIM:ComputerSystem', + 'CreationClassName': 'DCIM_LCService', + 'Name': 'DCIM:LCService'} + + result = self.invoke(uris.DCIM_LCService, + 'GetRemoteServicesAPIStatus', + selectors, + {}, + expected_return_value=utils.RET_SUCCESS) + + message_id = utils.find_xml(result, + 'MessageID', + uris.DCIM_LCService).text + + return message_id == IDRAC_IS_READY + + def wait_until_idrac_is_ready(self, retries=24, retry_delay=10): + """Waits until the iDRAC is in a ready state + + :param retries: The number of times to check if the iDRAC is ready + :param retry_delay: The number of seconds to wait between retries + + :raises: WSManRequestFailure on request failures + :raises: WSManInvalidResponse when receiving invalid response + :raises: DRACOperationFailed on error reported back by the DRAC + interface or timeout + :raises: DRACUnexpectedReturnValue on return value mismatch + """ + + # Try every 10 seconds over 4 minutes for the iDRAC to become ready + while retries > 0: + LOG.debug("Checking to see if the iDRAC is ready") + + if self.is_idrac_ready(): + LOG.debug("The iDRAC is ready") + return + + LOG.debug("The iDRAC is not ready") + retries -= 1 + if retries > 0: + time.sleep(retry_delay) + + if retries == 0: + err_msg = "Timed out waiting for the iDRAC to become ready" + LOG.error(err_msg) + raise exceptions.DRACOperationFailed(drac_messages=err_msg) diff --git a/dracclient/resources/lifecycle_controller.py b/dracclient/resources/lifecycle_controller.py index 35c86b9..d0a1c54 100644 --- a/dracclient/resources/lifecycle_controller.py +++ b/dracclient/resources/lifecycle_controller.py @@ -15,8 +15,6 @@ from dracclient.resources import uris from dracclient import utils from dracclient import wsman -IDRAC_IS_READY = "LC061" - class LifecycleControllerManagement(object): @@ -87,37 +85,6 @@ class LCConfiguration(object): return result - def is_idrac_ready(self): - """Indicates if the iDRAC is ready to accept commands - - Returns a boolean indicating if the iDRAC is ready to accept - commands. - - :returns: Boolean indicating iDRAC readiness - :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 - """ - - selectors = {'SystemCreationClassName': 'DCIM_ComputerSystem', - 'SystemName': 'DCIM:ComputerSystem', - 'CreationClassName': 'DCIM_LCService', - 'Name': 'DCIM:LCService'} - - result = self.client.invoke(uris.DCIM_LCService, - 'GetRemoteServicesAPIStatus', - selectors, - {}, - expected_return_value=utils.RET_SUCCESS) - - message_id = utils.find_xml(result, - 'MessageID', - uris.DCIM_LCService).text - - return message_id == IDRAC_IS_READY - class LCAttribute(object): """Generic LC attribute class""" diff --git a/dracclient/tests/test_client.py b/dracclient/tests/test_client.py index e9395c8..b9ea44b 100644 --- a/dracclient/tests/test_client.py +++ b/dracclient/tests/test_client.py @@ -11,10 +11,12 @@ # License for the specific language governing permissions and limitations # under the License. +import mock import requests_mock import dracclient.client from dracclient import exceptions +from dracclient.resources import uris from dracclient.tests import base from dracclient.tests import utils as test_utils @@ -83,3 +85,48 @@ class WSManClientTestCase(base.BaseTest): self.assertRaises(exceptions.DRACUnexpectedReturnValue, client.invoke, 'http://resource', 'Foo', expected_return_value='4242') + + def test_is_idrac_ready_ready(self, mock_requests): + expected_text = test_utils.LifecycleControllerInvocations[ + uris.DCIM_LCService]['GetRemoteServicesAPIStatus']['is_ready'] + mock_requests.post('https://1.2.3.4:443/wsman', + text=expected_text) + + client = dracclient.client.WSManClient(**test_utils.FAKE_ENDPOINT) + self.assertTrue(client.is_idrac_ready()) + + def test_is_idrac_ready_not_ready(self, mock_requests): + expected_text = test_utils.LifecycleControllerInvocations[ + uris.DCIM_LCService]['GetRemoteServicesAPIStatus']['is_not_ready'] + mock_requests.post('https://1.2.3.4:443/wsman', + text=expected_text) + + client = dracclient.client.WSManClient(**test_utils.FAKE_ENDPOINT) + self.assertFalse(client.is_idrac_ready()) + + def test_wait_until_idrac_is_ready_ready(self, mock_requests): + expected_text = test_utils.LifecycleControllerInvocations[ + uris.DCIM_LCService]['GetRemoteServicesAPIStatus']['is_ready'] + mock_requests.post('https://1.2.3.4:443/wsman', + text=expected_text) + + client = dracclient.client.WSManClient(**test_utils.FAKE_ENDPOINT) + + try: + client.wait_until_idrac_is_ready() + except exceptions.DRACOperationFailed: + self.fail('wait_until_idrac_is_ready() timed out when it should ' + 'not have!') + + @mock.patch('time.sleep', autospec=True) + def test_wait_until_idrac_is_ready_timeout(self, + mock_requests, + mock_ts): + expected_text = test_utils.LifecycleControllerInvocations[ + uris.DCIM_LCService]['GetRemoteServicesAPIStatus']['is_not_ready'] + mock_requests.post('https://1.2.3.4:443/wsman', + text=expected_text) + + client = dracclient.client.WSManClient(**test_utils.FAKE_ENDPOINT) + self.assertRaises(exceptions.DRACOperationFailed, + client.wait_until_idrac_is_ready) diff --git a/dracclient/tests/test_lifecycle_controller.py b/dracclient/tests/test_lifecycle_controller.py index ad13431..8498e90 100644 --- a/dracclient/tests/test_lifecycle_controller.py +++ b/dracclient/tests/test_lifecycle_controller.py @@ -11,11 +11,9 @@ # License for the specific language governing permissions and limitations # under the License. -import mock import requests_mock import dracclient.client -from dracclient import exceptions from dracclient.resources import lifecycle_controller from dracclient.resources import uris from dracclient.tests import base @@ -86,47 +84,3 @@ class ClientLCConfigurationTestCase(base.BaseTest): lifecycle_settings) self.assertEqual(expected_string_attr, lifecycle_settings['LifecycleController.Embedded.1#LCAttributes.1#SystemID']) # noqa - - @requests_mock.Mocker() - def test_is_idrac_ready_ready(self, mock_requests): - expected_text = test_utils.LifecycleControllerInvocations[ - uris.DCIM_LCService]['GetRemoteServicesAPIStatus']['is_ready'] - mock_requests.post('https://1.2.3.4:443/wsman', - text=expected_text) - - self.assertTrue(self.drac_client.is_idrac_ready()) - - @requests_mock.Mocker() - def test_is_idrac_ready_not_ready(self, mock_requests): - expected_text = test_utils.LifecycleControllerInvocations[ - uris.DCIM_LCService]['GetRemoteServicesAPIStatus']['is_not_ready'] - mock_requests.post('https://1.2.3.4:443/wsman', - text=expected_text) - - self.assertFalse(self.drac_client.is_idrac_ready()) - - @requests_mock.Mocker() - def test_wait_until_idrac_is_ready_ready(self, mock_requests): - expected_text = test_utils.LifecycleControllerInvocations[ - uris.DCIM_LCService]['GetRemoteServicesAPIStatus']['is_ready'] - mock_requests.post('https://1.2.3.4:443/wsman', - text=expected_text) - - try: - self.drac_client.wait_until_idrac_is_ready() - except exceptions.DRACOperationFailed: - self.fail('wait_until_idrac_is_ready() timed out when it should ' - 'not have!') - - @requests_mock.Mocker() - @mock.patch('time.sleep', autospec=True) - def test_wait_until_idrac_is_ready_timeout(self, - mock_requests, - mock_ts): - expected_text = test_utils.LifecycleControllerInvocations[ - uris.DCIM_LCService]['GetRemoteServicesAPIStatus']['is_not_ready'] - mock_requests.post('https://1.2.3.4:443/wsman', - text=expected_text) - - self.assertRaises(exceptions.DRACOperationFailed, - self.drac_client.wait_until_idrac_is_ready)