diff --git a/dracclient/client.py b/dracclient/client.py index 7358c86..cc0968e 100644 --- a/dracclient/client.py +++ b/dracclient/client.py @@ -38,7 +38,7 @@ class DRACClient(object): BIOS_DEVICE_FQDD = 'BIOS.Setup.1-1' def __init__(self, host, username, password, port=443, path='/wsman', - protocol='https'): + protocol='https', retries=3, retry_delay=0): """Creates client object :param host: hostname or IP of the DRAC interface @@ -47,9 +47,11 @@ class DRACClient(object): :param port: port for accessing the DRAC interface :param path: path for accessing the DRAC interface :param protocol: protocol for accessing the DRAC interface + :param retries: number of resends to attempt on failure + :param retry_delay: number of seconds to wait between retries """ self.client = WSManClient(host, username, password, port, path, - protocol) + protocol, retries, retry_delay) self._job_mgmt = job.JobManagement(self.client) self._power_mgmt = bios.PowerManagement(self.client) self._boot_mgmt = bios.BootManagement(self.client) diff --git a/dracclient/tests/test_wsman.py b/dracclient/tests/test_wsman.py index 5f008f9..a76b582 100644 --- a/dracclient/tests/test_wsman.py +++ b/dracclient/tests/test_wsman.py @@ -17,6 +17,7 @@ import uuid import lxml.etree import lxml.objectify import mock +import requests.exceptions import requests_mock from dracclient import exceptions @@ -108,6 +109,74 @@ class ClientTestCase(base.BaseTest): self.assertEqual('yay!', resp.text) + @requests_mock.Mocker() + def test_invoke_with_ssl_errors(self, mock_requests): + mock_requests.post('https://1.2.3.4:443/wsman', + exc=requests.exceptions.SSLError) + + self.assertRaises(exceptions.WSManRequestFailure, + self.client.invoke, 'http://resource', 'method', + {'selector': 'foo'}, {'property': 'bar'}) + + @requests_mock.Mocker() + def test_invoke_with_ssl_error_success(self, mock_requests): + expected_resp = 'yay!' + mock_requests.post('https://1.2.3.4:443/wsman', + [{'exc': requests.exceptions.SSLError}, + {'text': expected_resp}]) + + resp = self.client.invoke('http://resource', 'method', + {'selector': 'foo'}, {'property': 'bar'}) + + self.assertEqual('yay!', resp.text) + + @requests_mock.Mocker() + def test_invoke_with_connection_errors(self, mock_requests): + mock_requests.post('https://1.2.3.4:443/wsman', + exc=requests.exceptions.ConnectionError) + + self.assertRaises(exceptions.WSManRequestFailure, + self.client.invoke, 'http://resource', 'method', + {'selector': 'foo'}, {'property': 'bar'}) + + @requests_mock.Mocker() + def test_invoke_with_connection_error_success(self, mock_requests): + expected_resp = 'yay!' + mock_requests.post('https://1.2.3.4:443/wsman', + [{'exc': requests.exceptions.ConnectionError}, + {'text': expected_resp}]) + + resp = self.client.invoke('http://resource', 'method', + {'selector': 'foo'}, {'property': 'bar'}) + + self.assertEqual('yay!', resp.text) + + @requests_mock.Mocker() + def test_invoke_with_unknown_error(self, mock_requests): + mock_requests.post('https://1.2.3.4:443/wsman', + exc=requests.exceptions.HTTPError) + self.assertRaises(exceptions.WSManRequestFailure, + self.client.invoke, 'http://resource', 'method', + {'selector': 'foo'}, {'property': 'bar'}) + + @requests_mock.Mocker() + @mock.patch('time.sleep', autospec=True) + def test_client_retry_delay(self, mock_requests, mock_ts): + retry_delay = 5 + fake_endpoint = test_utils.FAKE_ENDPOINT.copy() + fake_endpoint['retry_delay'] = retry_delay + client = dracclient.wsman.Client(**fake_endpoint) + expected_resp = 'yay!' + mock_requests.post('https://1.2.3.4:443/wsman', + [{'exc': requests.exceptions.SSLError}, + {'text': expected_resp}]) + + resp = client.invoke('http://resource', 'method', + {'selector': 'foo'}, {'property': 'bar'}) + + self.assertEqual('yay!', resp.text) + mock_ts.assert_called_once_with(retry_delay) + class PayloadTestCase(base.BaseTest): diff --git a/dracclient/wsman.py b/dracclient/wsman.py index cd94f02..0799a8d 100644 --- a/dracclient/wsman.py +++ b/dracclient/wsman.py @@ -12,10 +12,10 @@ # under the License. import logging +import time import uuid from lxml import etree as ElementTree -import requests import requests.exceptions from dracclient import exceptions @@ -41,13 +41,27 @@ class Client(object): """Simple client for talking over WSMan protocol.""" def __init__(self, host, username, password, port=443, path='/wsman', - protocol='https'): + protocol='https', retries=3, retry_delay=0): + """Creates client object + + :param host: hostname or IP of the DRAC interface + :param username: username for accessing the DRAC interface + :param password: password for accessing the DRAC interface + :param port: port for accessing the DRAC interface + :param path: path for accessing the DRAC interface + :param protocol: protocol for accessing the DRAC interface + :param retries: number of resends to attempt on failure + :param retry_delay: number of seconds to wait between retries + """ + self.host = host self.username = username self.password = password self.port = port self.path = path self.protocol = protocol + self.retries = retries + self.retry_delay = retry_delay self.endpoint = ('%(protocol)s://%(host)s:%(port)s%(path)s' % { 'protocol': self.protocol, 'host': self.host, @@ -58,16 +72,52 @@ class Client(object): payload = payload.build() LOG.debug('Sending request to %(endpoint)s: %(payload)s', {'endpoint': self.endpoint, 'payload': payload}) - try: - resp = requests.post( - self.endpoint, - auth=requests.auth.HTTPBasicAuth(self.username, self.password), - data=payload, - # TODO(ifarkas): enable cert verification - verify=False) - except requests.exceptions.RequestException: - LOG.exception('Request failed') - raise exceptions.WSManRequestFailure() + + num_tries = 1 + while num_tries <= self.retries: + try: + resp = requests.post( + self.endpoint, + auth=requests.auth.HTTPBasicAuth(self.username, + self.password), + data=payload, + # TODO(ifarkas): enable cert verification + verify=False) + break + except (requests.exceptions.ConnectionError, + requests.exceptions.SSLError) as ex: + + error_msg = "A {error_type} error occurred while " \ + " communicating with {host}, attempt {num_tries} of " \ + "{retries}".format( + error_type=type(ex).__name__, + host=self.host, + num_tries=num_tries, + retries=self.retries) + + if num_tries == self.retries: + LOG.error(error_msg) + raise exceptions.WSManRequestFailure( + "A {error_type} error occurred while communicating " + "with {host}: {error}".format( + error_type=type(ex).__name__, + host=self.host, + error=ex)) + else: + LOG.warning(error_msg) + + num_tries += 1 + if self.retry_delay > 0 and num_tries <= self.retries: + time.sleep(self.retry_delay) + + except requests.exceptions.RequestException as ex: + error_msg = "A {error_type} error occurred while " \ + "communicating with {host}: {error}".format( + error_type=type(ex).__name__, + host=self.host, + error=ex) + LOG.error(error_msg) + raise exceptions.WSManRequestFailure(error_msg) LOG.debug('Received response from %(endpoint)s: %(payload)s', {'endpoint': self.endpoint, 'payload': resp.content})