diff --git a/.gitignore b/.gitignore index 20d3475d..56682576 100644 --- a/.gitignore +++ b/.gitignore @@ -14,6 +14,7 @@ storops-*/ # IDE related .idea/ +.swp # tests junit-result.xml @@ -23,4 +24,4 @@ htmlcov/ .coverage .coverage.* logs/ -*_names.json \ No newline at end of file +*_names.json diff --git a/README.rst b/README.rst index 7f7b7f23..ae85cb59 100644 --- a/README.rst +++ b/README.rst @@ -10,7 +10,7 @@ StorOps: The Python Library for VNX & Unity .. image:: https://img.shields.io/pypi/v/storops.svg :target: https://pypi.python.org/pypi/storops -VERSION: 0.2.23 +VERSION: 0.2.24 A minimalist Python library to manage VNX/Unity systems. This document lies in the source code and go with the release. diff --git a/storops/connection/client.py b/storops/connection/client.py index 04915919..cabfeb6d 100644 --- a/storops/connection/client.py +++ b/storops/connection/client.py @@ -26,7 +26,7 @@ from requests.exceptions import RequestException from retryz import retry -from storops.connection.exceptions import from_response +from storops.connection import exceptions log = logging.getLogger(__name__) @@ -41,13 +41,13 @@ def _wait_callback(tried): class HTTPClient(object): def __init__(self, base_url, headers, insecure=False, auth=None, - timeout=None, retries=None): + timeout=None, retries=None, ca_cert_path=None): self.base_url = base_url if retries is None: retries = 2 self.retries = retries self.request_options = self._set_request_options( - insecure, auth, timeout) + insecure, auth, timeout, ca_cert_path) self.headers = headers self.session = requests.session() @@ -55,9 +55,13 @@ def __del__(self): self.session.close() @staticmethod - def _set_request_options(insecure=None, auth=None, timeout=None): + def _set_request_options(insecure=None, auth=None, timeout=None, + ca_cert_path=None): options = {'verify': True} + if ca_cert_path is not None: + options['verify'] = ca_cert_path + if insecure: options['verify'] = False @@ -74,8 +78,9 @@ def request(self, full_url, method, **kwargs): options = copy.deepcopy(self.request_options) + content_type = headers.get('Content-Type', None) if 'body' in kwargs: - if headers['Content-Type'] == 'application/json': + if content_type == 'application/json': options['data'] = json.dumps(kwargs['body']) else: options['data'] = kwargs['body'] @@ -90,7 +95,7 @@ def request(self, full_url, method, **kwargs): body = None if resp.text: try: - if headers['Content-Type'] == 'application/json': + if content_type == 'application/json': body = json.loads(resp.text) else: body = resp.text @@ -99,7 +104,7 @@ def request(self, full_url, method, **kwargs): pass if resp.status_code == 401: - raise from_response(resp, method, full_url) + raise exceptions.from_response(resp, method, full_url) return resp, body diff --git a/storops/connection/connector.py b/storops/connection/connector.py index 9389043c..6925ce5a 100644 --- a/storops/connection/connector.py +++ b/storops/connection/connector.py @@ -51,13 +51,21 @@ class UnityRESTConnector(object): 'User-agent': 'EMC-OpenStack', } - def __init__(self, host, port=443, user='admin', password=''): + def __init__(self, host, port=443, user='admin', password='', + verify=True): base_url = 'https://{host}:{port}'.format(host=host, port=port) + insecure = False + ca_cert_path = None + if isinstance(verify, bool): + insecure = not verify + else: + ca_cert_path = verify self.http_client = client.HTTPClient(base_url=base_url, headers=self.HEADERS, auth=(user, password), - insecure=True) + insecure=insecure, + ca_cert_path=ca_cert_path) def get(self, url, **kwargs): return self.http_client.get(url, **kwargs) diff --git a/storops/unity/client.py b/storops/unity/client.py index 336a8443..50b94f96 100644 --- a/storops/unity/client.py +++ b/storops/unity/client.py @@ -32,9 +32,10 @@ class UnityClient(object): - def __init__(self, ip, username, password, port=443): + def __init__(self, ip, username, password, port=443, verify=True): self._rest = UnityRESTConnector(ip, port=port, user=username, - password=password) + password=password, + verify=verify) def get_all(self, type_name, base_fields=None, the_filter=None, nested_fields=None): diff --git a/storops/unity/resource/system.py b/storops/unity/resource/system.py index b1d8ab01..4abde2d3 100644 --- a/storops/unity/resource/system.py +++ b/storops/unity/resource/system.py @@ -50,10 +50,11 @@ class UnitySystem(UnitySingletonResource): def __init__(self, host=None, username=None, password=None, - port=443, cli=None): + port=443, cli=None, verify=True): super(UnitySystem, self).__init__(cli=cli) if cli is None: - self._cli = UnityClient(host, username, password, port) + self._cli = UnityClient(host, username, password, port, + verify=verify) else: self._cli = cli diff --git a/test/connection/__init__.py b/test/connection/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/test/connection/test_client.py b/test/connection/test_client.py new file mode 100644 index 00000000..3259ea0b --- /dev/null +++ b/test/connection/test_client.py @@ -0,0 +1,186 @@ +# coding=utf-8 +# Copyright (c) 2015 EMC Corporation. +# 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. +from __future__ import unicode_literals + +import unittest + +from hamcrest import assert_that, calling, equal_to, raises +import mock +from requests import exceptions + +from storops.connection import client +from storops.connection import exceptions as storops_ex + + +class MockResponse(object): + def __init__(self, text, status_code): + self.text = text + self.status_code = status_code + + +def _request_side_effect(method, url, **kwargs): + response_map = {'right_url': MockResponse('OK', 200), + 'right_url_json': MockResponse('{"id": "id_123"}', 200), + 'bad_url': MockResponse('Failed', 404), + 'bad_url_raise': MockResponse('Failed', 401)} + return response_map[url] + + +class ClientModuleTest(unittest.TestCase): + def test_on_error_callback_true(self): + result = client._on_error_callback(exceptions.RequestException()) + assert_that(True, equal_to(result)) + + def test_on_error_callback_false(self): + result = client._on_error_callback(ValueError()) + assert_that(False, equal_to(result)) + + def test_wait_callback(self): + assert_that(8, equal_to(client._wait_callback(4))) + + +class HTTPClientTest(unittest.TestCase): + + def setUp(self): + self.client = client.HTTPClient('https://10.10.10.10', {}) + + def test_set_request_options_insecure_true(self): + options = client.HTTPClient._set_request_options(insecure=False, + auth=None, + timeout=None, + ca_cert_path=None) + assert_that(options, equal_to({'verify': True, + 'auth': None})) + + def test_set_request_options_insecure_true_path(self): + options = client.HTTPClient._set_request_options( + insecure=False, auth=None, timeout=None, ca_cert_path='/tmp.crt') + assert_that(options, equal_to({'verify': '/tmp.crt', + 'auth': None})) + + def test_set_request_options_insecure_false(self): + options = client.HTTPClient._set_request_options( + insecure=True, auth=None, timeout=None, ca_cert_path='/tmp.crt') + assert_that(options, equal_to({'verify': False, + 'auth': None})) + + def test_request_content_json(self): + self.client.session.request = mock.MagicMock( + side_effect=_request_side_effect) + self.client.headers['Content-Type'] = 'application/json' + + resp, body = self.client.request('right_url_json', 'GET', + body={"k_abc": "v_abc"}) + + self.client.session.request.assert_called_with( + 'GET', 'right_url_json', auth=None, verify=True, + headers={'Content-Type': 'application/json'}, + data='{"k_abc": "v_abc"}') + assert_that(resp.status_code, equal_to(200)) + assert_that(body, equal_to({'id': 'id_123'})) + + def test_request_content_plain(self): + self.client.session.request = mock.MagicMock( + side_effect=_request_side_effect) + + resp, body = self.client.request('right_url', 'GET', + body='{"k_abc": "v_abc"}') + + self.client.session.request.assert_called_with( + 'GET', 'right_url', auth=None, verify=True, headers={}, + data='{"k_abc": "v_abc"}') + assert_that(resp.status_code, equal_to(200)) + assert_that(body, equal_to('OK')) + + def test_request_content_404(self): + self.client.session.request = mock.MagicMock( + side_effect=_request_side_effect) + + resp, body = self.client.request('bad_url', 'GET', + body='{"k_abc": "v_abc"}') + + self.client.session.request.assert_called_with( + 'GET', 'bad_url', auth=None, verify=True, headers={}, + data='{"k_abc": "v_abc"}') + assert_that(resp.status_code, equal_to(404)) + assert_that(body, equal_to('Failed')) + + @mock.patch('storops.connection.exceptions.from_response') + def test_request_content_raise(self, mocked_from_response): + self.client.session.request = mock.MagicMock( + side_effect=_request_side_effect) + mocked_from_response.return_value = storops_ex.HttpError() + + def _tmp_func(): + self.client.request('bad_url_raise', 'GET', + body='{"k_abc": "v_abc"}') + assert_that(calling(_tmp_func), + raises(storops_ex.HttpError)) + + self.client.session.request.assert_called_with( + 'GET', 'bad_url_raise', auth=None, verify=True, + headers={}, + data='{"k_abc": "v_abc"}') + + @mock.patch( + 'storops.connection.client.HTTPClient._cs_request_with_retries') + def test_cs_request(self, mocked_cs_request_with_retries): + self.client.base_url = 'https://10.10.10.10' + self.client._cs_request('/api/types/instance', 'GET') + + mocked_cs_request_with_retries.assert_called_with( + 'https://10.10.10.10/api/types/instance', + 'GET') + + def test_get_limit(self): + self.client.retries = 99 + assert_that(99, equal_to(self.client._get_limit())) + + @mock.patch( + 'storops.connection.client.HTTPClient._cs_request') + def test_get(self, mocked_cs_request): + self.client.get('/api/types/instance') + + mocked_cs_request.assert_called_with( + '/api/types/instance', + 'GET') + + @mock.patch( + 'storops.connection.client.HTTPClient._cs_request') + def test_post(self, mocked_cs_request): + self.client.post('/api/types/instance') + + mocked_cs_request.assert_called_with( + '/api/types/instance', + 'POST') + + @mock.patch( + 'storops.connection.client.HTTPClient._cs_request') + def test_put(self, mocked_cs_request): + self.client.put('/api/types/instance') + + mocked_cs_request.assert_called_with( + '/api/types/instance', + 'PUT') + + @mock.patch( + 'storops.connection.client.HTTPClient._cs_request') + def test_delete(self, mocked_cs_request): + self.client.delete('/api/types/instance') + + mocked_cs_request.assert_called_with( + '/api/types/instance', + 'DELETE') diff --git a/test/connection/test_connector.py b/test/connection/test_connector.py new file mode 100644 index 00000000..aba4ecf4 --- /dev/null +++ b/test/connection/test_connector.py @@ -0,0 +1,65 @@ +# coding=utf-8 +# Copyright (c) 2015 EMC Corporation. +# 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. + +from __future__ import unicode_literals + +import unittest + +import mock + +from storops.connection import connector + + +class UnityRESTConnectorTest(unittest.TestCase): + + @mock.patch('storops.connection.client.HTTPClient') + def test_new_connector_verify_false(self, mocked_httpclient): + + connector.UnityRESTConnector('10.10.10.10', + verify=False) + + mocked_httpclient.assert_called_with( + base_url='https://10.10.10.10:443', + headers=connector.UnityRESTConnector.HEADERS, + auth=('admin', ''), + insecure=True, + ca_cert_path=None) + + @mock.patch('storops.connection.client.HTTPClient') + def test_new_connector_verify_true(self, mocked_httpclient): + + connector.UnityRESTConnector('10.10.10.10', + verify=True) + + mocked_httpclient.assert_called_with( + base_url='https://10.10.10.10:443', + headers=connector.UnityRESTConnector.HEADERS, + auth=('admin', ''), + insecure=False, + ca_cert_path=None) + + @mock.patch('storops.connection.client.HTTPClient') + def test_new_connector_verify_path(self, mocked_httpclient): + + connector.UnityRESTConnector('10.10.10.10', + verify='/tmp/ca_cert.crt') + + mocked_httpclient.assert_called_with( + base_url='https://10.10.10.10:443', + headers=connector.UnityRESTConnector.HEADERS, + auth=('admin', ''), + insecure=False, + ca_cert_path='/tmp/ca_cert.crt')