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
This commit is contained in:
Debayan Ray 2017-04-03 00:38:33 -04:00
parent 0a3a490d34
commit 1226273f26
8 changed files with 721 additions and 392 deletions

View File

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

View File

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

260
proliantutils/rest/v1.py Executable file
View File

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

View File

@ -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"'),

View File

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

View File

View File

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

View File

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