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]
39253bb272

Change-Id: I87996bbca129995f6c84848ebdb0c33cfedeea53
Partial-Bug: #1697558
Related-Bug: #1691808
This commit is contained in:
Richard Pioso 2017-06-20 15:54:34 -04:00
parent 00c9fe4fde
commit 10df06f6c3
4 changed files with 113 additions and 97 deletions

View File

@ -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)

View File

@ -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"""

View File

@ -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)

View File

@ -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)