From 1226273f269be0dcc8364a4e7c527ec8cdd5e064 Mon Sep 17 00:00:00 2001 From: Debayan Ray Date: Mon, 3 Apr 2017 00:38:33 -0400 Subject: [PATCH] Separate out rest infra from ris This patch is again targeted towards support of consuming Redfish APIs. ReST infrastructure has been separated out from RIS module to be used by both RIS and Redfish portions. Partial-Bug: 1646685 Change-Id: I9b40bf732589b3604ed1e0726a0f895b49213058 --- proliantutils/ilo/ris.py | 236 +--------------- proliantutils/rest/__init__.py | 6 + proliantutils/rest/v1.py | 260 ++++++++++++++++++ proliantutils/tests/ilo/ris_sample_outputs.py | 4 - proliantutils/tests/ilo/test_ris.py | 159 ----------- proliantutils/tests/rest/__init__.py | 0 .../tests/rest/rest_sample_outputs.py | 226 +++++++++++++++ proliantutils/tests/rest/test_v1.py | 222 +++++++++++++++ 8 files changed, 721 insertions(+), 392 deletions(-) create mode 100644 proliantutils/rest/__init__.py create mode 100755 proliantutils/rest/v1.py create mode 100644 proliantutils/tests/rest/__init__.py create mode 100755 proliantutils/tests/rest/rest_sample_outputs.py create mode 100755 proliantutils/tests/rest/test_v1.py diff --git a/proliantutils/ilo/ris.py b/proliantutils/ilo/ris.py index 5a194016..101e0a0f 100755 --- a/proliantutils/ilo/ris.py +++ b/proliantutils/ilo/ris.py @@ -1,4 +1,4 @@ -# Copyright 2014 Hewlett-Packard Development Company, L.P. +# Copyright 2017 Hewlett Packard Enterprise Development Company, L.P. # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain @@ -12,25 +12,16 @@ # License for the specific language governing permissions and limitations # under the License. -__author__ = 'HP' +__author__ = 'HPE' -import base64 -import gzip import hashlib -import json - -import requests -from requests.packages import urllib3 -from requests.packages.urllib3 import exceptions as urllib3_exceptions -import retrying -import six -from six.moves.urllib import parse as urlparse from proliantutils import exception from proliantutils.ilo import common from proliantutils.ilo import firmware_controller from proliantutils.ilo import operations from proliantutils import log +from proliantutils import rest """ Currently this class supports only secure boot and firmware settings related API's . @@ -62,229 +53,16 @@ POWER_STATE = { CLASSCODE_FOR_GPU_DEVICES = [3] SUBCLASSCODE_FOR_GPU_DEVICES = [0, 1, 2, 128] -REDIRECTION_ATTEMPTS = 5 - LOG = log.get_logger(__name__) -class RISOperations(operations.IloOperations): +class RISOperations(rest.RestConnectorBase, operations.IloOperations): def __init__(self, host, login, password, bios_password=None, cacert=None): - self.host = host - self.login = login - self.password = password - self.bios_password = bios_password - # Message registry support - self.message_registries = {} - self.cacert = cacert - - # By default, requests logs following message if verify=False - # InsecureRequestWarning: Unverified HTTPS request is - # being made. Adding certificate verification is strongly advised. - # Just disable the warning if user intentionally did this. - if self.cacert is None: - urllib3.disable_warnings(urllib3_exceptions.InsecureRequestWarning) - - def _get_response_body_from_gzipped_content(self, url, response): - """Get the response body from gzipped content - - Try to decode as gzip (we should check the headers for - Content-Encoding=gzip) - - if response.headers['content-encoding'] == "gzip": - ... - - :param url: the url for which response was sent - :type url: str - :param response: response content object, probably gzipped - :type response: object - :returns: returns response body - :raises IloError: if the content is **not** gzipped - """ - try: - gzipper = gzip.GzipFile(fileobj=six.BytesIO(response.text)) - - LOG.debug(self._("Received compressed response for " - "url %(url)s."), {'url': url}) - uncompressed_string = (gzipper.read().decode('UTF-8')) - response_body = json.loads(uncompressed_string) - - except Exception as e: - LOG.debug( - self._("Error occurred while decompressing body. " - "Got invalid response '%(response)s' for " - "url %(url)s: %(error)s"), - {'url': url, 'response': response.text, 'error': e}) - raise exception.IloError(e) - - return response_body - - def _rest_op(self, operation, suburi, request_headers, request_body): - """Generic REST Operation handler.""" - - url = urlparse.urlparse('https://' + self.host + suburi) - # Used for logging on redirection error. - start_url = url.geturl() - - LOG.debug(self._("%(operation)s %(url)s"), - {'operation': operation, 'url': start_url}) - - if request_headers is None: - request_headers = {} - - # Use self.login/self.password and Basic Auth - if self.login is not None and self.password is not None: - auth_data = self.login + ":" + self.password - hr = "BASIC " + base64.b64encode( - auth_data.encode('ascii')).decode("utf-8") - request_headers['Authorization'] = hr - - """Helper methods to retry and keep retrying on redirection - START""" - - def retry_if_response_asks_for_redirection(response): - # NOTE:Do not assume every HTTP operation will return a JSON body. - # For example, ExtendedError structures are only required for - # HTTP 400 errors and are optional elsewhere as they are mostly - # redundant for many of the other HTTP status code. In particular, - # 200 OK responses should not have to return any body. - - # NOTE: this makes sure the headers names are all lower cases - # because HTTP says they are case insensitive - # Follow HTTP redirect - if response.status_code == 301 and 'location' in response.headers: - retry_if_response_asks_for_redirection.url = ( - urlparse.urlparse(response.headers['location'])) - LOG.debug(self._("Request redirected to %s."), - retry_if_response_asks_for_redirection.url.geturl()) - return True - return False - - @retrying.retry( - # Note(deray): Return True if we should retry, False otherwise. - # In our case, when the url response we receive asks for - # redirection then we retry. - retry_on_result=retry_if_response_asks_for_redirection, - # Note(deray): Return True if we should retry, False otherwise. - # In our case, when it's an IloConnectionError we don't retry. - # ``requests`` already takes care of issuing max number of - # retries if the URL service is unavailable. - retry_on_exception=( - lambda e: not isinstance(e, exception.IloConnectionError)), - stop_max_attempt_number=REDIRECTION_ATTEMPTS) - def _fetch_response(): - - url = retry_if_response_asks_for_redirection.url - - kwargs = {'headers': request_headers, - 'data': json.dumps(request_body)} - if self.cacert is not None: - kwargs['verify'] = self.cacert - else: - kwargs['verify'] = False - - LOG.debug(self._('\n\tHTTP REQUEST: %(restreq_method)s' - '\n\tPATH: %(restreq_path)s' - '\n\tBODY: %(restreq_body)s' - '\n'), - {'restreq_method': operation, - 'restreq_path': url.geturl(), - 'restreq_body': request_body}) - - request_method = getattr(requests, operation.lower()) - try: - response = request_method(url.geturl(), **kwargs) - except Exception as e: - LOG.debug(self._("Unable to connect to iLO. %s"), e) - raise exception.IloConnectionError(e) - - return response - - """Helper methods to retry and keep retrying on redirection - END""" - - try: - # Note(deray): This is a trick to use the function attributes - # to overwrite variable/s (in our case ``url``) and use the - # modified one in nested functions, i.e. :func:`_fetch_response` - # and :func:`retry_if_response_asks_for_redirection` - retry_if_response_asks_for_redirection.url = url - - response = _fetch_response() - except retrying.RetryError as e: - # Redirected for REDIRECTION_ATTEMPTS - th time. Throw error - msg = (self._("URL Redirected %(times)s times continuously. " - "URL used: %(start_url)s More info: %(error)s") % - {'start_url': start_url, 'times': REDIRECTION_ATTEMPTS, - 'error': str(e)}) - LOG.debug(msg) - raise exception.IloConnectionError(msg) - - response_body = {} - if response.text: - try: - response_body = json.loads(response.text) - except (TypeError, ValueError): - # Note(deray): If it doesn't decode as json, then - # resources may return gzipped content. - # ``json.loads`` on python3 raises TypeError when - # ``response.text`` is gzipped one. - response_body = ( - self._get_response_body_from_gzipped_content(url, - response)) - - LOG.debug(self._('\n\tHTTP RESPONSE for %(restreq_path)s:' - '\n\tCode: %(status_code)s' - '\n\tResponse Body: %(response_body)s' - '\n'), - {'restreq_path': url.geturl(), - 'status_code': response.status_code, - 'response_body': response_body}) - return response.status_code, response.headers, response_body - - def _rest_get(self, suburi, request_headers=None): - """REST GET operation. - - HTTP response codes could be 500, 404 etc. - """ - return self._rest_op('GET', suburi, request_headers, None) - - def _rest_patch(self, suburi, request_headers, request_body): - """REST PATCH operation. - - HTTP response codes could be 500, 404, 202 etc. - """ - if not isinstance(request_headers, dict): - request_headers = {} - request_headers['Content-Type'] = 'application/json' - return self._rest_op('PATCH', suburi, request_headers, request_body) - - def _rest_put(self, suburi, request_headers, request_body): - """REST PUT operation. - - HTTP response codes could be 500, 404, 202 etc. - """ - if not isinstance(request_headers, dict): - request_headers = {} - request_headers['Content-Type'] = 'application/json' - return self._rest_op('PUT', suburi, request_headers, request_body) - - def _rest_post(self, suburi, request_headers, request_body): - """REST POST operation. - - The response body after the operation could be the new resource, or - ExtendedError, or it could be empty. - """ - if not isinstance(request_headers, dict): - request_headers = {} - request_headers['Content-Type'] = 'application/json' - return self._rest_op('POST', suburi, request_headers, request_body) - - def _rest_delete(self, suburi, request_headers): - """REST DELETE operation. - - HTTP response codes could be 500, 404 etc. - """ - return self._rest_op('DELETE', suburi, request_headers, None) + super(RISOperations, self).__init__(host, login, password, + bios_password=bios_password, + cacert=cacert) def _get_collection(self, collection_uri, request_headers=None): """Generator function that returns collection members.""" diff --git a/proliantutils/rest/__init__.py b/proliantutils/rest/__init__.py new file mode 100644 index 00000000..7aaca90f --- /dev/null +++ b/proliantutils/rest/__init__.py @@ -0,0 +1,6 @@ +"""REST infrastructure to simplify communicating with REST based iLO interfaces + + Helper module to work with REST based APIs of BMCs. +""" + +from proliantutils.rest.v1 import RestConnectorBase # noqa diff --git a/proliantutils/rest/v1.py b/proliantutils/rest/v1.py new file mode 100755 index 00000000..be86445c --- /dev/null +++ b/proliantutils/rest/v1.py @@ -0,0 +1,260 @@ +# Copyright 2017 Hewlett Packard Enterprise Development Company, L.P. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +"""Helper module to work with REST APIs""" + +__author__ = 'HPE' + +import base64 +import gzip +import json + +import requests +from requests.packages import urllib3 +from requests.packages.urllib3 import exceptions as urllib3_exceptions +import retrying +import six +from six.moves.urllib import parse as urlparse + +from proliantutils import exception +from proliantutils import log + + +REDIRECTION_ATTEMPTS = 5 + +LOG = log.get_logger(__name__) + + +class RestConnectorBase(object): + + def __init__(self, host, login, password, bios_password=None, + cacert=None): + self.host = host + self.login = login + self.password = password + self.bios_password = bios_password + # Message registry support + self.message_registries = {} + self.cacert = cacert + + # By default, requests logs following message if verify=False + # InsecureRequestWarning: Unverified HTTPS request is + # being made. Adding certificate verification is strongly advised. + # Just disable the warning if user intentionally did this. + if self.cacert is None: + urllib3.disable_warnings(urllib3_exceptions.InsecureRequestWarning) + + def _(self, msg): + """Prepends host information to msg and returns it.""" + return "[iLO %s] %s" % (self.host, msg) + + def _get_response_body_from_gzipped_content(self, url, response): + """Get the response body from gzipped content + + Try to decode as gzip (we should check the headers for + Content-Encoding=gzip) + + if response.headers['content-encoding'] == "gzip": + ... + + :param url: the url for which response was sent + :type url: str + :param response: response content object, probably gzipped + :type response: object + :returns: returns response body + :raises IloError: if the content is **not** gzipped + """ + try: + gzipper = gzip.GzipFile(fileobj=six.BytesIO(response.text)) + + LOG.debug(self._("Received compressed response for " + "url %(url)s."), {'url': url}) + uncompressed_string = (gzipper.read().decode('UTF-8')) + response_body = json.loads(uncompressed_string) + + except Exception as e: + LOG.debug( + self._("Error occurred while decompressing body. " + "Got invalid response '%(response)s' for " + "url %(url)s: %(error)s"), + {'url': url, 'response': response.text, 'error': e}) + raise exception.IloError(e) + + return response_body + + def _rest_op(self, operation, suburi, request_headers, request_body): + """Generic REST Operation handler.""" + + url = urlparse.urlparse('https://' + self.host + suburi) + # Used for logging on redirection error. + start_url = url.geturl() + + LOG.debug(self._("%(operation)s %(url)s"), + {'operation': operation, 'url': start_url}) + + if request_headers is None or not isinstance(request_headers, dict): + request_headers = {} + + # Use self.login/self.password and Basic Auth + if self.login is not None and self.password is not None: + auth_data = self.login + ":" + self.password + hr = "BASIC " + base64.b64encode( + auth_data.encode('ascii')).decode("utf-8") + request_headers['Authorization'] = hr + + if request_body is not None: + if (isinstance(request_body, dict) + or isinstance(request_body, list)): + request_headers['Content-Type'] = 'application/json' + else: + request_headers['Content-Type'] = ('application/' + 'x-www-form-urlencoded') + + """Helper methods to retry and keep retrying on redirection - START""" + + def retry_if_response_asks_for_redirection(response): + # NOTE:Do not assume every HTTP operation will return a JSON + # request_body. For example, ExtendedError structures are only + # required for HTTP 400 errors and are optional elsewhere as they + # are mostly redundant for many of the other HTTP status code. + # In particular, 200 OK responses should not have to return any + # request_body. + + # NOTE: this makes sure the headers names are all lower cases + # because HTTP says they are case insensitive + # Follow HTTP redirect + if response.status_code == 301 and 'location' in response.headers: + retry_if_response_asks_for_redirection.url = ( + urlparse.urlparse(response.headers['location'])) + LOG.debug(self._("Request redirected to %s."), + retry_if_response_asks_for_redirection.url.geturl()) + return True + return False + + @retrying.retry( + # Note(deray): Return True if we should retry, False otherwise. + # In our case, when the url response we receive asks for + # redirection then we retry. + retry_on_result=retry_if_response_asks_for_redirection, + # Note(deray): Return True if we should retry, False otherwise. + # In our case, when it's an IloConnectionError we don't retry. + # ``requests`` already takes care of issuing max number of + # retries if the URL service is unavailable. + retry_on_exception=( + lambda e: not isinstance(e, exception.IloConnectionError)), + stop_max_attempt_number=REDIRECTION_ATTEMPTS) + def _fetch_response(): + + url = retry_if_response_asks_for_redirection.url + + kwargs = {'headers': request_headers, + 'data': json.dumps(request_body)} + if self.cacert is not None: + kwargs['verify'] = self.cacert + else: + kwargs['verify'] = False + + LOG.debug(self._('\n\tHTTP REQUEST: %(restreq_method)s' + '\n\tPATH: %(restreq_path)s' + '\n\tBODY: %(restreq_body)s' + '\n'), + {'restreq_method': operation, + 'restreq_path': url.geturl(), + 'restreq_body': request_body}) + + request_method = getattr(requests, operation.lower()) + try: + response = request_method(url.geturl(), **kwargs) + except Exception as e: + LOG.debug(self._("Unable to connect to iLO. %s"), e) + raise exception.IloConnectionError(e) + + return response + + """Helper methods to retry and keep retrying on redirection - END""" + + try: + # Note(deray): This is a trick to use the function attributes + # to overwrite variable/s (in our case ``url``) and use the + # modified one in nested functions, i.e. :func:`_fetch_response` + # and :func:`retry_if_response_asks_for_redirection` + retry_if_response_asks_for_redirection.url = url + + response = _fetch_response() + except retrying.RetryError as e: + # Redirected for REDIRECTION_ATTEMPTS - th time. Throw error + msg = (self._("URL Redirected %(times)s times continuously. " + "URL used: %(start_url)s More info: %(error)s") % + {'start_url': start_url, 'times': REDIRECTION_ATTEMPTS, + 'error': str(e)}) + LOG.debug(msg) + raise exception.IloConnectionError(msg) + + response_body = {} + if response.text: + try: + response_body = json.loads(response.text) + except (TypeError, ValueError): + # Note(deray): If it doesn't decode as json, then + # resources may return gzipped content. + # ``json.loads`` on python3 raises TypeError when + # ``response.text`` is gzipped one. + response_body = ( + self._get_response_body_from_gzipped_content(url, + response)) + + LOG.debug(self._('\n\tHTTP RESPONSE for %(restreq_path)s:' + '\n\tCode: %(status_code)s' + '\n\tResponse Body: %(response_body)s' + '\n'), + {'restreq_path': url.geturl(), + 'status_code': response.status_code, + 'response_body': response_body}) + return response.status_code, response.headers, response_body + + def _rest_get(self, suburi, request_headers=None): + """REST GET operation. + + HTTP response codes could be 500, 404 etc. + """ + return self._rest_op('GET', suburi, request_headers, None) + + def _rest_patch(self, suburi, request_headers, request_body): + """REST PATCH operation. + + HTTP response codes could be 500, 404, 202 etc. + """ + return self._rest_op('PATCH', suburi, request_headers, request_body) + + def _rest_put(self, suburi, request_headers, request_body): + """REST PUT operation. + + HTTP response codes could be 500, 404, 202 etc. + """ + return self._rest_op('PUT', suburi, request_headers, request_body) + + def _rest_post(self, suburi, request_headers, request_body): + """REST POST operation. + + The response body after the operation could be the new resource, or + ExtendedError, or it could be empty. + """ + return self._rest_op('POST', suburi, request_headers, request_body) + + def _rest_delete(self, suburi, request_headers): + """REST DELETE operation. + + HTTP response codes could be 500, 404 etc. + """ + return self._rest_op('DELETE', suburi, request_headers, None) diff --git a/proliantutils/tests/ilo/ris_sample_outputs.py b/proliantutils/tests/ilo/ris_sample_outputs.py index 9f04748b..dae80ee9 100755 --- a/proliantutils/tests/ilo/ris_sample_outputs.py +++ b/proliantutils/tests/ilo/ris_sample_outputs.py @@ -219,10 +219,6 @@ RESPONSE_BODY_FOR_REST_OP = """ } """ -BASE64_GZIPPED_RESPONSE = """ -H4sIAHN9ClUC/+1YW2/iOBR+51dEeZqR2pAQCoWnhUALGm5qSndXoz6YYMAaEyPHoWVH/e9r50JJ7ARajVbzsH0p8rnZ5xx/53MqPysa/9M7QQDZI1jrbU3Xr5K1PUAYLDDseAwRP+Cy75FE/P08/op1IxVh/QC5p8TFUeyAHVggjBiCWTdqd+9uMSYvYgtPAIcFpkflqZ8Lm5HeEerB6Wp1VocfgAHKyvQmW1QmnoXBZkZeIO2GjPGsKDWf1Q70GSU7SNlhArbwmM/Hww7Kbt4yK8+V7HoSQO8iIhL3nmHdCSmFPsssRoInSANeRpdR5EetMLQb2t4y6qb2xbSqtdtqzbRuvuq5SG9pJEKyTqMVl4Qi83tIKVrCvi/KuRTOeyiIf1+VGbjhbkcoi0yyxdcnxIdSpy3zK4OltDQPFtISS9szJ+ghsJYWRU5dyMJdXjB7lXY0hyvkbiDGKsEjoGt+XSqKrlDkItFuS0c/8SVbfVS/JOODntHfLgzLqOUPcw99SJFnzN0uF1t58WToGHcYvo6mYyE2hrN9/QKdhlTenvGEKAsBNmo8SiXb/Gkj9mDgUbRLIckh213IINXcQ8DgVntC8CUFuQEJmEP4fcAgUT9pXyEcd5zOcklhIKOP3vDaXq1tNdt2q72C7Vszv928wq260iJOet9PqzScFYbORypKxdBfIg8wQkf9nnD/joD6GPjhCngspJAK0WB2lMAtoYdsLh4JAzhOYCy+73IFq5GJNiZLiIUvjmIjBHymdUf1hulpvD1aqff0pLmypOIp/5mtwk5GqtLZdG6oHGfVKUgXwPHZyVUe7FOT7GQWiNpfXajY8ZcDgpd6qfpzuTdp/IhZpp4+xxlw9Z/mpMr7o8pb4peeMg/D5ZNWnrgluTjbhHH3q2jT79GEDu+paLL/0oyX0Jr/G+v5HNXL0xHAOI4KwP7+rGAqEnwmRt6PcKeUxUMUsOgICv5X0KZ3YIvwIeGNRUof5pgl7VDIZKVDvHv+fTYvOJiDQTTda5USbc5n9siDnC97hHO0gxicGEYHU9S1M3Zz+jEB5OuKY+nulj92OpSCQ0Z/6HO0AVhlse+Er4oIPNVg6Hvp3lSGY4B8haULKf8pmElpFmacJbksKWg0uuXnXLwu7r8X8bkR2iLRHjemqVQMGZm+UwHpBZku9zg9jLY6Rj7ahlul2gNch1hQLcGCoowcfN5U3nnloJhyx5TIdYjPKFGWQx0lYXivymWUe5PmQSMuiIvWPhDskG8qn70IuQVn3KUsLh5j/VdmmIZlyi+AhLZzgFwhDOMW4+QT7WGB5nw+FIzVDzHOKWDk/ygCteHULUaDDYUrEbnK6RKr7q1qEG06qFrVhcDJi67tuD+ePvz9gSDuMUjCqy8KM3OG8VUJPhXqxPzScC4m7NPBYuOLQrnQ400lfSy4NNiJ+Zkx+VbwnSK6gLnHEO9LnquA0Py3EhJG88U6eZYddU9mhs8g/vLwVfsLEl/8d2ZzrX9zXWuYLW1va39oltEy7wf/nD7vBJiFcsb1AQSYbR4IxvNdtM1vRV9c3G9zodCsNc2add2tpbdO3GCO3pNwu4hP6t4P6vXWn5ORfdSQgyeBk5C5jcLMJ5vsLqLapJAw2xwC/uRMseoIFVmg4CjRMtI5qyd3XbdNu2nX7Oa1bdm163rzxr6u39r1a7tut26a9X7dsY8HkFFAdzZ8miKZ7ymAQmqwxLZq6QU9dPpgH5G1om4lTRsZVBS3QrzCwRouu4dP7Tq2phduO4B49ZFtS21Xeav8C14gvWoyFgAA -""" - HEADERS_FOR_REST_OP = [('content-length', '2729'), ('server', 'HP-iLO-Server/1.30'), ('etag', 'W/"B61EB245"'), diff --git a/proliantutils/tests/ilo/test_ris.py b/proliantutils/tests/ilo/test_ris.py index fd62a1b2..d9a34027 100755 --- a/proliantutils/tests/ilo/test_ris.py +++ b/proliantutils/tests/ilo/test_ris.py @@ -15,13 +15,9 @@ """Test class for RIS Module.""" -import base64 import json import mock -import requests -from requests.packages import urllib3 -from requests.packages.urllib3 import exceptions as urllib3_exceptions import testtools from proliantutils import exception @@ -30,34 +26,6 @@ from proliantutils.ilo import ris from proliantutils.tests.ilo import ris_sample_outputs as ris_outputs -class IloRisTestCaseInitTestCase(testtools.TestCase): - - @mock.patch.object(urllib3, 'disable_warnings') - def test_init(self, disable_warning_mock): - ris_client = ris.RISOperations( - "x.x.x.x", "admin", "Admin", bios_password='foo', - cacert='/somepath') - - self.assertEqual(ris_client.host, "x.x.x.x") - self.assertEqual(ris_client.login, "admin") - self.assertEqual(ris_client.password, "Admin") - self.assertEqual(ris_client.bios_password, "foo") - self.assertEqual({}, ris_client.message_registries) - self.assertEqual(ris_client.cacert, '/somepath') - - @mock.patch.object(urllib3, 'disable_warnings') - def test_init_without_cacert(self, disable_warning_mock): - ris_client = ris.RISOperations( - "x.x.x.x", "admin", "Admin", bios_password='foo') - - self.assertEqual(ris_client.host, "x.x.x.x") - self.assertEqual(ris_client.login, "admin") - self.assertEqual(ris_client.password, "Admin") - self.assertIsNone(ris_client.cacert) - disable_warning_mock.assert_called_once_with( - urllib3_exceptions.InsecureRequestWarning) - - class IloRisTestCase(testtools.TestCase): def setUp(self): @@ -1042,133 +1010,6 @@ class TestRISOperationsPrivateMethods(testtools.TestCase): result = self.client._is_boot_mode_uefi() self.assertFalse(result) - @mock.patch.object(requests, 'get') - def test__rest_op_okay(self, request_mock): - sample_headers = ris_outputs.HEADERS_FOR_REST_OP - exp_headers = dict((x.lower(), y) for x, y in sample_headers) - sample_response_body = ris_outputs.RESPONSE_BODY_FOR_REST_OP - response_mock_obj = mock.MagicMock( - status_code=200, text=sample_response_body, - headers=exp_headers) - request_mock.return_value = response_mock_obj - - status, headers, response = self.client._rest_op( - 'GET', '/v1/foo', None, None) - - self.assertEqual(200, status) - self.assertEqual(exp_headers, headers) - self.assertEqual(json.loads(sample_response_body), response) - request_mock.assert_called_once_with( - 'https://1.2.3.4/v1/foo', - headers={'Authorization': 'BASIC YWRtaW46QWRtaW4='}, - data="null", verify=False) - - @mock.patch.object(requests, 'get') - def test__rest_op_request_error(self, request_mock): - request_mock.side_effect = RuntimeError("boom") - - exc = self.assertRaises(exception.IloConnectionError, - self.client._rest_op, - 'GET', '/v1/foo', {}, None) - - request_mock.assert_called_once_with( - 'https://1.2.3.4/v1/foo', - headers={'Authorization': 'BASIC YWRtaW46QWRtaW4='}, - data="null", verify=False) - self.assertIn("boom", str(exc)) - - @mock.patch.object(requests, 'get') - def test__rest_op_continous_redirection(self, request_mock): - sample_response_body = ris_outputs.RESPONSE_BODY_FOR_REST_OP - sample_headers = ris_outputs.HEADERS_FOR_REST_OP - sample_headers.append(('location', 'https://foo')) - exp_headers = dict((x.lower(), y) for x, y in sample_headers) - response_mock_obj = mock.MagicMock( - status_code=301, text=sample_response_body, - headers=exp_headers) - request_mock.side_effect = [response_mock_obj, - response_mock_obj, - response_mock_obj, - response_mock_obj, - response_mock_obj] - - exc = self.assertRaises(exception.IloConnectionError, - self.client._rest_op, - 'GET', '/v1/foo', {}, None) - - self.assertEqual(5, request_mock.call_count) - self.assertIn('https://1.2.3.4/v1/foo', str(exc)) - - @mock.patch.object(requests, 'get') - def test__rest_op_one_redirection(self, request_mock): - sample_response_body = ris_outputs.RESPONSE_BODY_FOR_REST_OP - sample_headers1 = ris_outputs.HEADERS_FOR_REST_OP - sample_headers2 = ris_outputs.HEADERS_FOR_REST_OP - sample_headers1.append(('location', 'https://5.6.7.8/v1/foo')) - exp_headers1 = dict((x.lower(), y) for x, y in sample_headers1) - exp_headers2 = dict((x.lower(), y) for x, y in sample_headers2) - response_mock_obj1 = mock.MagicMock( - status_code=301, text=sample_response_body, - headers=exp_headers1) - response_mock_obj2 = mock.MagicMock( - status_code=200, text=sample_response_body, - headers=exp_headers2) - request_mock.side_effect = [response_mock_obj1, - response_mock_obj2] - - status, headers, response = self.client._rest_op( - 'GET', '/v1/foo', {}, None) - - exp_headers = dict((x.lower(), y) for x, y in sample_headers2) - self.assertEqual(200, status) - self.assertEqual(exp_headers, headers) - self.assertEqual(json.loads(sample_response_body), response) - request_mock.assert_has_calls([ - mock.call('https://1.2.3.4/v1/foo', - headers={'Authorization': 'BASIC YWRtaW46QWRtaW4='}, - data="null", verify=False), - mock.call('https://5.6.7.8/v1/foo', - headers={'Authorization': 'BASIC YWRtaW46QWRtaW4='}, - data="null", verify=False)]) - - @mock.patch.object(requests, 'get') - def test__rest_op_response_decode_error(self, request_mock): - sample_response_body = "{[wrong json" - sample_headers = ris_outputs.HEADERS_FOR_REST_OP - exp_headers = dict((x.lower(), y) for x, y in sample_headers) - response_mock_obj = mock.MagicMock( - status_code=200, text=sample_response_body, - headers=exp_headers) - request_mock.return_value = response_mock_obj - - self.assertRaises(exception.IloError, - self.client._rest_op, - 'GET', '/v1/foo', {}, None) - - request_mock.assert_called_once_with( - 'https://1.2.3.4/v1/foo', - headers={'Authorization': 'BASIC YWRtaW46QWRtaW4='}, - data="null", verify=False) - - @mock.patch.object(requests, 'get') - def test__rest_op_response_gzipped_response(self, request_mock): - sample_response_body = ris_outputs.RESPONSE_BODY_FOR_REST_OP - gzipped_response_body = base64.b64decode( - ris_outputs.BASE64_GZIPPED_RESPONSE) - sample_headers = ris_outputs.HEADERS_FOR_REST_OP - exp_headers = dict((x.lower(), y) for x, y in sample_headers) - response_mock_obj = mock.MagicMock( - status_code=200, text=gzipped_response_body, - headers=exp_headers) - request_mock.return_value = response_mock_obj - - status, headers, response = self.client._rest_op( - 'GET', '/v1/foo', {}, None) - - self.assertEqual(200, status) - self.assertEqual(exp_headers, headers) - self.assertEqual(json.loads(sample_response_body), response) - @mock.patch.object(ris.RISOperations, '_rest_patch') @mock.patch.object(ris.RISOperations, '_check_bios_resource') def test___change_bios_setting(self, check_bios_mock, patch_mock): diff --git a/proliantutils/tests/rest/__init__.py b/proliantutils/tests/rest/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/proliantutils/tests/rest/rest_sample_outputs.py b/proliantutils/tests/rest/rest_sample_outputs.py new file mode 100755 index 00000000..b995d321 --- /dev/null +++ b/proliantutils/tests/rest/rest_sample_outputs.py @@ -0,0 +1,226 @@ +# Copyright 2017 Hewlett Packard Enterprise Development Company, L.P. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +RESPONSE_BODY_FOR_REST_OP = """ +{ + "AssetTag": "", + "AvailableActions": [ + { + "Action": "Reset", + "Capabilities": [ + { + "AllowableValues": [ + "On", + "ForceOff", + "ForceRestart", + "Nmi", + "PushPowerButton" + ], + "PropertyName": "ResetType" + } + ] + } + ], + "Bios": { + "Current": { + "VersionString": "I36 v1.40 (01/28/2015)" + } + }, + "Boot": { + "BootSourceOverrideEnabled": "Disabled", + "BootSourceOverrideSupported": [ + "None", + "Cd", + "Hdd", + "Usb", + "Utilities", + "Diags", + "BiosSetup", + "Pxe", + "UefiShell", + "UefiTarget" + ], + "BootSourceOverrideTarget": "None", + "UefiTargetBootSourceOverride": "None", + "UefiTargetBootSourceOverrideSupported": [ + "HD.Emb.1.2", + "Generic.USB.1.1", + "NIC.FlexLOM.1.1.IPv4", + "NIC.FlexLOM.1.1.IPv6", + "CD.Virtual.2.1" + ] + }, + "Description": "Computer System View", + "HostCorrelation": { + "HostMACAddress": [ + "6c:c2:17:39:fe:80", + "6c:c2:17:39:fe:88" + ], + "HostName": "", + "IPAddress": [ + "", + "" + ] + }, + "IndicatorLED": "Off", + "Manufacturer": "HP", + "Memory": { + "TotalSystemMemoryGB": 16 + }, + "Model": "ProLiant BL460c Gen9", + "Name": "Computer System", + "Oem": { + "Hp": { + "AvailableActions": [ + { + "Action": "PowerButton", + "Capabilities": [ + { + "AllowableValues": [ + "Press", + "PressAndHold" + ], + "PropertyName": "PushType" + }, + { + "AllowableValues": [ + "/Oem/Hp" + ], + "PropertyName": "Target" + } + ] + }, + { + "Action": "SystemReset", + "Capabilities": [ + { + "AllowableValues": [ + "ColdBoot" + ], + "PropertyName": "ResetType" + }, + { + "AllowableValues": [ + "/Oem/Hp" + ], + "PropertyName": "Target" + } + ] + } + ], + "Battery": [], + "Bios": { + "Backup": { + "Date": "v1.40 (01/28/2015)", + "Family": "I36", + "VersionString": "I36 v1.40 (01/28/2015)" + }, + "Current": { + "Date": "01/28/2015", + "Family": "I36", + "VersionString": "I36 v1.40 (01/28/2015)" + }, + "UefiClass": 2 + }, + "DeviceDiscoveryComplete": { + "AMSDeviceDiscovery": "NoAMS", + "SmartArrayDiscovery": "Initial", + "vAuxDeviceDiscovery": "DataIncomplete", + "vMainDeviceDiscovery": "ServerOff" + }, + "PostState": "PowerOff", + "PowerAllocationLimit": 500, + "PowerAutoOn": "PowerOn", + "PowerOnDelay": "Minimum", + "PowerRegulatorMode": "Dynamic", + "PowerRegulatorModesSupported": [ + "OSControl", + "Dynamic", + "Max", + "Min" + ], + "ServerSignature": 0, + "Type": "HpComputerSystemExt.0.10.1", + "VirtualProfile": "Inactive", + "VirtualUUID": null, + "links": { + "BIOS": { + "href": "/rest/v1/systems/1/bios" + }, + "MEMORY": { + "href": "/rest/v1/Systems/1/Memory" + }, + "PCIDevices": { + "href": "/rest/v1/Systems/1/PCIDevices" + }, + "PCISlots": { + "href": "/rest/v1/Systems/1/PCISlots" + }, + "SecureBoot": { + "href": "/rest/v1/Systems/1/SecureBoot" + } + } + } + }, + "Power": "Off", + "Processors": { + "Count": 1, + "ProcessorFamily": "Intel(R) Xeon(R) CPU E5-2609 v3 @ 1.90GHz", + "Status": { + "HealthRollUp": "OK" + } + }, + "SKU": "727021-B21", + "SerialNumber": "SGH449WNL3", + "Status": { + "Health": "OK", + "State": "Disabled" + }, + "SystemType": "Physical", + "Type": "ComputerSystem.0.9.6", + "UUID": "30373237-3132-4753-4834-3439574E4C33", + "links": { + "Chassis": [ + { + "href": "/rest/v1/Chassis/1" + } + ], + "Logs": { + "href": "/rest/v1/Systems/1/Logs" + }, + "ManagedBy": [ + { + "href": "/rest/v1/Managers/1" + } + ], + "self": { + "href": "/rest/v1/Systems/1" + } + } +} +""" + +BASE64_GZIPPED_RESPONSE = """ +H4sIAHN9ClUC/+1YW2/iOBR+51dEeZqR2pAQCoWnhUALGm5qSndXoz6YYMAaEyPHoWVH/e9r50JJ7ARajVbzsH0p8rnZ5xx/53MqPysa/9M7QQDZI1jrbU3Xr5K1PUAYLDDseAwRP+Cy75FE/P08/op1IxVh/QC5p8TFUeyAHVggjBiCWTdqd+9uMSYvYgtPAIcFpkflqZ8Lm5HeEerB6Wp1VocfgAHKyvQmW1QmnoXBZkZeIO2GjPGsKDWf1Q70GSU7SNlhArbwmM/Hww7Kbt4yK8+V7HoSQO8iIhL3nmHdCSmFPsssRoInSANeRpdR5EetMLQb2t4y6qb2xbSqtdtqzbRuvuq5SG9pJEKyTqMVl4Qi83tIKVrCvi/KuRTOeyiIf1+VGbjhbkcoi0yyxdcnxIdSpy3zK4OltDQPFtISS9szJ+ghsJYWRU5dyMJdXjB7lXY0hyvkbiDGKsEjoGt+XSqKrlDkItFuS0c/8SVbfVS/JOODntHfLgzLqOUPcw99SJFnzN0uF1t58WToGHcYvo6mYyE2hrN9/QKdhlTenvGEKAsBNmo8SiXb/Gkj9mDgUbRLIckh213IINXcQ8DgVntC8CUFuQEJmEP4fcAgUT9pXyEcd5zOcklhIKOP3vDaXq1tNdt2q72C7Vszv928wq260iJOet9PqzScFYbORypKxdBfIg8wQkf9nnD/joD6GPjhCngspJAK0WB2lMAtoYdsLh4JAzhOYCy+73IFq5GJNiZLiIUvjmIjBHymdUf1hulpvD1aqff0pLmypOIp/5mtwk5GqtLZdG6oHGfVKUgXwPHZyVUe7FOT7GQWiNpfXajY8ZcDgpd6qfpzuTdp/IhZpp4+xxlw9Z/mpMr7o8pb4peeMg/D5ZNWnrgluTjbhHH3q2jT79GEDu+paLL/0oyX0Jr/G+v5HNXL0xHAOI4KwP7+rGAqEnwmRt6PcKeUxUMUsOgICv5X0KZ3YIvwIeGNRUof5pgl7VDIZKVDvHv+fTYvOJiDQTTda5USbc5n9siDnC97hHO0gxicGEYHU9S1M3Zz+jEB5OuKY+nulj92OpSCQ0Z/6HO0AVhlse+Er4oIPNVg6Hvp3lSGY4B8haULKf8pmElpFmacJbksKWg0uuXnXLwu7r8X8bkR2iLRHjemqVQMGZm+UwHpBZku9zg9jLY6Rj7ahlul2gNch1hQLcGCoowcfN5U3nnloJhyx5TIdYjPKFGWQx0lYXivymWUe5PmQSMuiIvWPhDskG8qn70IuQVn3KUsLh5j/VdmmIZlyi+AhLZzgFwhDOMW4+QT7WGB5nw+FIzVDzHOKWDk/ygCteHULUaDDYUrEbnK6RKr7q1qEG06qFrVhcDJi67tuD+ePvz9gSDuMUjCqy8KM3OG8VUJPhXqxPzScC4m7NPBYuOLQrnQ400lfSy4NNiJ+Zkx+VbwnSK6gLnHEO9LnquA0Py3EhJG88U6eZYddU9mhs8g/vLwVfsLEl/8d2ZzrX9zXWuYLW1va39oltEy7wf/nD7vBJiFcsb1AQSYbR4IxvNdtM1vRV9c3G9zodCsNc2add2tpbdO3GCO3pNwu4hP6t4P6vXWn5ORfdSQgyeBk5C5jcLMJ5vsLqLapJAw2xwC/uRMseoIFVmg4CjRMtI5qyd3XbdNu2nX7Oa1bdm163rzxr6u39r1a7tut26a9X7dsY8HkFFAdzZ8miKZ7ymAQmqwxLZq6QU9dPpgH5G1om4lTRsZVBS3QrzCwRouu4dP7Tq2phduO4B49ZFtS21Xeav8C14gvWoyFgAA +""" + +HEADERS_FOR_REST_OP = [('content-length', '2729'), + ('server', 'HP-iLO-Server/1.30'), + ('etag', 'W/"B61EB245"'), + ('allow', 'GET, HEAD, POST, PATCH'), + ('cache-control', 'no-cache'), + ('date', 'Thu, 19 Mar 2015 06:55:59 GMT'), + ('x_hp-chrp-service-version', '1.0.3'), + ('content-type', 'application/json')] diff --git a/proliantutils/tests/rest/test_v1.py b/proliantutils/tests/rest/test_v1.py new file mode 100755 index 00000000..6000c909 --- /dev/null +++ b/proliantutils/tests/rest/test_v1.py @@ -0,0 +1,222 @@ +# Copyright 2017 Hewlett Packard Enterprise Development Company, L.P. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import base64 +import json + +import mock +import requests +from requests.packages import urllib3 +from requests.packages.urllib3 import exceptions as urllib3_exceptions +import testtools + +from proliantutils import exception +from proliantutils.rest import v1 +from proliantutils.tests.rest import rest_sample_outputs as rest_outputs + + +class RestConnectorBaseInitAndLowdashTestCase(testtools.TestCase): + + @mock.patch.object(urllib3, 'disable_warnings') + def test_init(self, disable_warning_mock): + rest_client = v1.RestConnectorBase( + "x.x.x.x", "admin", "Admin", bios_password='foo', + cacert='/somepath') + + self.assertEqual(rest_client.host, "x.x.x.x") + self.assertEqual(rest_client.login, "admin") + self.assertEqual(rest_client.password, "Admin") + self.assertEqual(rest_client.bios_password, "foo") + self.assertEqual({}, rest_client.message_registries) + self.assertEqual(rest_client.cacert, '/somepath') + + @mock.patch.object(urllib3, 'disable_warnings') + def test_init_without_cacert(self, disable_warning_mock): + rest_client = v1.RestConnectorBase( + "x.x.x.x", "admin", "Admin", bios_password='foo') + + self.assertEqual(rest_client.host, "x.x.x.x") + self.assertEqual(rest_client.login, "admin") + self.assertEqual(rest_client.password, "Admin") + self.assertIsNone(rest_client.cacert) + disable_warning_mock.assert_called_once_with( + urllib3_exceptions.InsecureRequestWarning) + + def test__okay(self): + rest_client = v1.RestConnectorBase("1.2.3.4", "admin", "Admin") + self.assertEqual('[iLO 1.2.3.4] foo', rest_client._('foo')) + + +class RestConnectorBaseTestCase(testtools.TestCase): + + def setUp(self): + super(RestConnectorBaseTestCase, self).setUp() + self.client = v1.RestConnectorBase("1.2.3.4", "admin", "Admin") + + @mock.patch.object(requests, 'get') + def test__rest_op_okay(self, request_mock): + sample_headers = rest_outputs.HEADERS_FOR_REST_OP + exp_headers = dict((x.lower(), y) for x, y in sample_headers) + sample_response_body = rest_outputs.RESPONSE_BODY_FOR_REST_OP + response_mock_obj = mock.MagicMock( + status_code=200, text=sample_response_body, + headers=exp_headers) + request_mock.return_value = response_mock_obj + + status, headers, response = self.client._rest_op( + 'GET', '/v1/foo', None, None) + + self.assertEqual(200, status) + self.assertEqual(exp_headers, headers) + self.assertEqual(json.loads(sample_response_body), response) + request_mock.assert_called_once_with( + 'https://1.2.3.4/v1/foo', + headers={'Authorization': 'BASIC YWRtaW46QWRtaW4='}, + data="null", verify=False) + + @mock.patch.object(requests, 'get') + def test__rest_op_request_error(self, request_mock): + request_mock.side_effect = RuntimeError("boom") + + exc = self.assertRaises(exception.IloConnectionError, + self.client._rest_op, + 'GET', '/v1/foo', {}, None) + + request_mock.assert_called_once_with( + 'https://1.2.3.4/v1/foo', + headers={'Authorization': 'BASIC YWRtaW46QWRtaW4='}, + data="null", verify=False) + self.assertIn("boom", str(exc)) + + @mock.patch.object(requests, 'get') + def test__rest_op_continous_redirection(self, request_mock): + sample_response_body = rest_outputs.RESPONSE_BODY_FOR_REST_OP + sample_headers = rest_outputs.HEADERS_FOR_REST_OP + sample_headers.append(('location', 'https://foo')) + exp_headers = dict((x.lower(), y) for x, y in sample_headers) + response_mock_obj = mock.MagicMock( + status_code=301, text=sample_response_body, + headers=exp_headers) + request_mock.side_effect = [response_mock_obj, + response_mock_obj, + response_mock_obj, + response_mock_obj, + response_mock_obj] + + exc = self.assertRaises(exception.IloConnectionError, + self.client._rest_op, + 'GET', '/v1/foo', {}, None) + + self.assertEqual(5, request_mock.call_count) + self.assertIn('https://1.2.3.4/v1/foo', str(exc)) + + @mock.patch.object(requests, 'get') + def test__rest_op_one_redirection(self, request_mock): + sample_response_body = rest_outputs.RESPONSE_BODY_FOR_REST_OP + sample_headers1 = rest_outputs.HEADERS_FOR_REST_OP + sample_headers2 = rest_outputs.HEADERS_FOR_REST_OP + sample_headers1.append(('location', 'https://5.6.7.8/v1/foo')) + exp_headers1 = dict((x.lower(), y) for x, y in sample_headers1) + exp_headers2 = dict((x.lower(), y) for x, y in sample_headers2) + response_mock_obj1 = mock.MagicMock( + status_code=301, text=sample_response_body, + headers=exp_headers1) + response_mock_obj2 = mock.MagicMock( + status_code=200, text=sample_response_body, + headers=exp_headers2) + request_mock.side_effect = [response_mock_obj1, + response_mock_obj2] + + status, headers, response = self.client._rest_op( + 'GET', '/v1/foo', {}, None) + + exp_headers = dict((x.lower(), y) for x, y in sample_headers2) + self.assertEqual(200, status) + self.assertEqual(exp_headers, headers) + self.assertEqual(json.loads(sample_response_body), response) + request_mock.assert_has_calls([ + mock.call('https://1.2.3.4/v1/foo', + headers={'Authorization': 'BASIC YWRtaW46QWRtaW4='}, + data="null", verify=False), + mock.call('https://5.6.7.8/v1/foo', + headers={'Authorization': 'BASIC YWRtaW46QWRtaW4='}, + data="null", verify=False)]) + + @mock.patch.object(requests, 'get') + def test__rest_op_response_decode_error(self, request_mock): + sample_response_body = "{[wrong json" + sample_headers = rest_outputs.HEADERS_FOR_REST_OP + exp_headers = dict((x.lower(), y) for x, y in sample_headers) + response_mock_obj = mock.MagicMock( + status_code=200, text=sample_response_body, + headers=exp_headers) + request_mock.return_value = response_mock_obj + + self.assertRaises(exception.IloError, + self.client._rest_op, + 'GET', '/v1/foo', {}, None) + + request_mock.assert_called_once_with( + 'https://1.2.3.4/v1/foo', + headers={'Authorization': 'BASIC YWRtaW46QWRtaW4='}, + data="null", verify=False) + + @mock.patch.object(requests, 'get') + def test__rest_op_response_gzipped_response(self, request_mock): + sample_response_body = rest_outputs.RESPONSE_BODY_FOR_REST_OP + gzipped_response_body = base64.b64decode( + rest_outputs.BASE64_GZIPPED_RESPONSE) + sample_headers = rest_outputs.HEADERS_FOR_REST_OP + exp_headers = dict((x.lower(), y) for x, y in sample_headers) + response_mock_obj = mock.MagicMock( + status_code=200, text=gzipped_response_body, + headers=exp_headers) + request_mock.return_value = response_mock_obj + + status, headers, response = self.client._rest_op( + 'GET', '/v1/foo', {}, None) + + self.assertEqual(200, status) + self.assertEqual(exp_headers, headers) + self.assertEqual(json.loads(sample_response_body), response) + + @mock.patch.object(v1.RestConnectorBase, '_rest_op') + def test__rest_get(self, _rest_op_mock): + self.client._rest_get('/v1/foo', {}) + _rest_op_mock.assert_called_once_with( + 'GET', '/v1/foo', {}, None) + + @mock.patch.object(v1.RestConnectorBase, '_rest_op') + def test__rest_patch(self, _rest_op_mock): + self.client._rest_patch('/v1/foo', {}, {'data': 'Lorem ipsum'}) + _rest_op_mock.assert_called_once_with( + 'PATCH', '/v1/foo', {}, {'data': 'Lorem ipsum'}) + + @mock.patch.object(v1.RestConnectorBase, '_rest_op') + def test__rest_put(self, _rest_op_mock): + self.client._rest_put('/v1/foo', {}, {'data': 'Lorem ipsum'}) + _rest_op_mock.assert_called_once_with( + 'PUT', '/v1/foo', {}, {'data': 'Lorem ipsum'}) + + @mock.patch.object(v1.RestConnectorBase, '_rest_op') + def test__rest_post(self, _rest_op_mock): + self.client._rest_post('/v1/foo', {}, {'data': 'Lorem ipsum'}) + _rest_op_mock.assert_called_once_with( + 'POST', '/v1/foo', {}, {'data': 'Lorem ipsum'}) + + @mock.patch.object(v1.RestConnectorBase, '_rest_op') + def test__rest_delete(self, _rest_op_mock): + self.client._rest_delete('/v1/foo', None) + _rest_op_mock.assert_called_once_with( + 'DELETE', '/v1/foo', None, None)