Merge "Fix cacert and insecure options on HTTP requests"
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
# Copyright 2013 - Mirantis, Inc.
|
||||
# Copyright 2016 - StackStorm, Inc.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
@@ -12,6 +13,9 @@
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
import copy
|
||||
import os
|
||||
|
||||
import requests
|
||||
|
||||
import logging
|
||||
@@ -30,39 +34,61 @@ def log_request(func):
|
||||
|
||||
|
||||
class HTTPClient(object):
|
||||
def __init__(self, base_url, token=None, project_id=None, user_id=None):
|
||||
def __init__(self, base_url, token=None, project_id=None, user_id=None,
|
||||
cacert=None, insecure=False):
|
||||
self.base_url = base_url
|
||||
self.token = token
|
||||
self.project_id = project_id
|
||||
self.user_id = user_id
|
||||
self.ssl_options = {}
|
||||
|
||||
if self.base_url.startswith('https'):
|
||||
if cacert and not os.path.exists(cacert):
|
||||
raise ValueError('Unable to locate cacert file '
|
||||
'at %s.' % cacert)
|
||||
|
||||
if cacert and insecure:
|
||||
LOG.warning('Client is set to not verify even though '
|
||||
'cacert is provided.')
|
||||
|
||||
self.ssl_options['verify'] = not insecure
|
||||
self.ssl_options['cert'] = cacert
|
||||
|
||||
@log_request
|
||||
def get(self, url, headers=None):
|
||||
headers = self._update_headers(headers)
|
||||
options = self._get_request_options('get', headers)
|
||||
|
||||
return requests.get(self.base_url + url, headers=headers)
|
||||
return requests.get(self.base_url + url, **options)
|
||||
|
||||
@log_request
|
||||
def post(self, url, body, headers=None):
|
||||
headers = self._update_headers(headers)
|
||||
content_type = headers.get('content-type', 'application/json')
|
||||
headers['content-type'] = content_type
|
||||
options = self._get_request_options('post', headers)
|
||||
|
||||
return requests.post(self.base_url + url, body, headers=headers)
|
||||
return requests.post(self.base_url + url, body, **options)
|
||||
|
||||
@log_request
|
||||
def put(self, url, body, headers=None):
|
||||
headers = self._update_headers(headers)
|
||||
content_type = headers.get('content-type', 'application/json')
|
||||
headers['content-type'] = content_type
|
||||
options = self._get_request_options('put', headers)
|
||||
|
||||
return requests.put(self.base_url + url, body, headers=headers)
|
||||
return requests.put(self.base_url + url, body, **options)
|
||||
|
||||
@log_request
|
||||
def delete(self, url, headers=None):
|
||||
options = self._get_request_options('delete', headers)
|
||||
|
||||
return requests.delete(self.base_url + url, **options)
|
||||
|
||||
def _get_request_options(self, method, headers):
|
||||
headers = self._update_headers(headers)
|
||||
|
||||
return requests.delete(self.base_url + url, headers=headers)
|
||||
if method in ['post', 'put']:
|
||||
content_type = headers.get('content-type', 'application/json')
|
||||
headers['content-type'] = content_type
|
||||
|
||||
options = copy.deepcopy(self.ssl_options)
|
||||
options['headers'] = headers
|
||||
|
||||
return options
|
||||
|
||||
def _update_headers(self, headers):
|
||||
if not headers:
|
||||
|
@@ -62,7 +62,9 @@ class Client(object):
|
||||
mistral_url,
|
||||
auth_token,
|
||||
project_id,
|
||||
user_id
|
||||
user_id,
|
||||
cacert=cacert,
|
||||
insecure=insecure
|
||||
)
|
||||
|
||||
# Create all resource managers.
|
||||
|
@@ -1,4 +1,5 @@
|
||||
# Copyright 2015 Huawei Technologies Co., Ltd.
|
||||
# Copyright 2015 - Huawei Technologies Co., Ltd.
|
||||
# Copyright 2016 - StackStorm, Inc.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
@@ -12,21 +13,165 @@
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
import os
|
||||
import tempfile
|
||||
import uuid
|
||||
|
||||
import mock
|
||||
import testtools
|
||||
|
||||
from mistralclient.api import client
|
||||
|
||||
AUTH_HTTP_URL = 'http://localhost:35357/v3'
|
||||
AUTH_HTTPS_URL = AUTH_HTTP_URL.replace('http', 'https')
|
||||
MISTRAL_HTTP_URL = 'http://localhost:8989/v2'
|
||||
MISTRAL_HTTPS_URL = MISTRAL_HTTP_URL.replace('http', 'https')
|
||||
|
||||
|
||||
class BaseClientTests(testtools.TestCase):
|
||||
|
||||
@mock.patch('keystoneclient.v3.client.Client')
|
||||
@mock.patch('mistralclient.api.httpclient.HTTPClient')
|
||||
def test_mistral_url_defult(self, mock, keystone_client_mock):
|
||||
client.client(username='mistral',
|
||||
project_name='misteal',
|
||||
auth_url="http://localhost:35357/v3")
|
||||
def test_mistral_url_default(self, mock, keystone_client_mock):
|
||||
keystone_client_instance = keystone_client_mock.return_value
|
||||
keystone_client_instance.auth_token = str(uuid.uuid4())
|
||||
keystone_client_instance.project_id = str(uuid.uuid4())
|
||||
keystone_client_instance.user_id = str(uuid.uuid4())
|
||||
|
||||
expected_args = (
|
||||
MISTRAL_HTTP_URL,
|
||||
keystone_client_instance.auth_token,
|
||||
keystone_client_instance.project_id,
|
||||
keystone_client_instance.user_id
|
||||
)
|
||||
|
||||
expected_kwargs = {
|
||||
'cacert': None,
|
||||
'insecure': False
|
||||
}
|
||||
|
||||
client.client(
|
||||
username='mistral',
|
||||
project_name='mistral',
|
||||
auth_url=AUTH_HTTP_URL
|
||||
)
|
||||
|
||||
self.assertTrue(mock.called)
|
||||
params = mock.call_args
|
||||
self.assertEqual('http://localhost:8989/v2',
|
||||
params[0][0])
|
||||
self.assertEqual(mock.call_args[0], expected_args)
|
||||
self.assertDictEqual(mock.call_args[1], expected_kwargs)
|
||||
|
||||
@mock.patch('keystoneclient.v3.client.Client')
|
||||
@mock.patch('mistralclient.api.httpclient.HTTPClient')
|
||||
def test_mistral_url_https_insecure(self, mock, keystone_client_mock):
|
||||
keystone_client_instance = keystone_client_mock.return_value
|
||||
keystone_client_instance.auth_token = str(uuid.uuid4())
|
||||
keystone_client_instance.project_id = str(uuid.uuid4())
|
||||
keystone_client_instance.user_id = str(uuid.uuid4())
|
||||
|
||||
expected_args = (
|
||||
MISTRAL_HTTPS_URL,
|
||||
keystone_client_instance.auth_token,
|
||||
keystone_client_instance.project_id,
|
||||
keystone_client_instance.user_id
|
||||
)
|
||||
|
||||
expected_kwargs = {
|
||||
'cacert': None,
|
||||
'insecure': True
|
||||
}
|
||||
|
||||
client.client(
|
||||
mistral_url=MISTRAL_HTTPS_URL,
|
||||
username='mistral',
|
||||
project_name='mistral',
|
||||
auth_url=AUTH_HTTP_URL,
|
||||
cacert=None,
|
||||
insecure=True
|
||||
)
|
||||
|
||||
self.assertTrue(mock.called)
|
||||
self.assertEqual(mock.call_args[0], expected_args)
|
||||
self.assertDictEqual(mock.call_args[1], expected_kwargs)
|
||||
|
||||
@mock.patch('keystoneclient.v3.client.Client')
|
||||
@mock.patch('mistralclient.api.httpclient.HTTPClient')
|
||||
def test_mistral_url_https_secure(self, mock, keystone_client_mock):
|
||||
fd, path = tempfile.mkstemp(suffix='.pem')
|
||||
|
||||
keystone_client_instance = keystone_client_mock.return_value
|
||||
keystone_client_instance.auth_token = str(uuid.uuid4())
|
||||
keystone_client_instance.project_id = str(uuid.uuid4())
|
||||
keystone_client_instance.user_id = str(uuid.uuid4())
|
||||
|
||||
expected_args = (
|
||||
MISTRAL_HTTPS_URL,
|
||||
keystone_client_instance.auth_token,
|
||||
keystone_client_instance.project_id,
|
||||
keystone_client_instance.user_id
|
||||
)
|
||||
|
||||
expected_kwargs = {
|
||||
'cacert': path,
|
||||
'insecure': False
|
||||
}
|
||||
|
||||
try:
|
||||
client.client(
|
||||
mistral_url=MISTRAL_HTTPS_URL,
|
||||
username='mistral',
|
||||
project_name='mistral',
|
||||
auth_url=AUTH_HTTP_URL,
|
||||
cacert=path,
|
||||
insecure=False
|
||||
)
|
||||
finally:
|
||||
os.close(fd)
|
||||
os.unlink(path)
|
||||
|
||||
self.assertTrue(mock.called)
|
||||
self.assertEqual(mock.call_args[0], expected_args)
|
||||
self.assertDictEqual(mock.call_args[1], expected_kwargs)
|
||||
|
||||
@mock.patch('keystoneclient.v3.client.Client')
|
||||
def test_mistral_url_https_bad_cacert(self, keystone_client_mock):
|
||||
keystone_client_instance = keystone_client_mock.return_value
|
||||
keystone_client_instance.auth_token = str(uuid.uuid4())
|
||||
keystone_client_instance.project_id = str(uuid.uuid4())
|
||||
keystone_client_instance.user_id = str(uuid.uuid4())
|
||||
|
||||
self.assertRaises(
|
||||
ValueError,
|
||||
client.client,
|
||||
mistral_url=MISTRAL_HTTPS_URL,
|
||||
username='mistral',
|
||||
project_name='mistral',
|
||||
auth_url=AUTH_HTTP_URL,
|
||||
cacert='/path/to/foobar',
|
||||
insecure=False
|
||||
)
|
||||
|
||||
@mock.patch('logging.Logger.warning')
|
||||
@mock.patch('keystoneclient.v3.client.Client')
|
||||
def test_mistral_url_https_bad_insecure(self, keystone_client_mock,
|
||||
log_warning_mock):
|
||||
fd, path = tempfile.mkstemp(suffix='.pem')
|
||||
|
||||
keystone_client_instance = keystone_client_mock.return_value
|
||||
keystone_client_instance.auth_token = str(uuid.uuid4())
|
||||
keystone_client_instance.project_id = str(uuid.uuid4())
|
||||
keystone_client_instance.user_id = str(uuid.uuid4())
|
||||
|
||||
try:
|
||||
client.client(
|
||||
mistral_url=MISTRAL_HTTPS_URL,
|
||||
username='mistral',
|
||||
project_name='mistral',
|
||||
auth_url=AUTH_HTTP_URL,
|
||||
cacert=path,
|
||||
insecure=True
|
||||
)
|
||||
finally:
|
||||
os.close(fd)
|
||||
os.unlink(path)
|
||||
|
||||
self.assertTrue(log_warning_mock.called)
|
||||
|
256
mistralclient/tests/unit/test_httpclient.py
Normal file
256
mistralclient/tests/unit/test_httpclient.py
Normal file
@@ -0,0 +1,256 @@
|
||||
# Copyright 2016 - StackStorm, Inc.
|
||||
#
|
||||
# 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 copy
|
||||
import uuid
|
||||
|
||||
import mock
|
||||
import requests
|
||||
import testtools
|
||||
|
||||
from mistralclient.api import httpclient
|
||||
|
||||
API_BASE_URL = 'http://localhost:8989/v2'
|
||||
API_URL = '/executions'
|
||||
|
||||
EXPECTED_URL = API_BASE_URL + API_URL
|
||||
|
||||
AUTH_TOKEN = str(uuid.uuid4())
|
||||
PROJECT_ID = str(uuid.uuid4())
|
||||
USER_ID = str(uuid.uuid4())
|
||||
|
||||
EXPECTED_AUTH_HEADERS = {
|
||||
'x-auth-token': AUTH_TOKEN,
|
||||
'X-Project-Id': PROJECT_ID,
|
||||
'X-User-Id': USER_ID
|
||||
}
|
||||
|
||||
EXPECTED_REQ_OPTIONS = {
|
||||
'headers': EXPECTED_AUTH_HEADERS
|
||||
}
|
||||
|
||||
EXPECTED_BODY = {
|
||||
'k1': 'abc',
|
||||
'k2': 123,
|
||||
'k3': True
|
||||
}
|
||||
|
||||
|
||||
class FakeRequest(object):
|
||||
|
||||
def __init__(self, method):
|
||||
self.method = method
|
||||
|
||||
|
||||
class FakeResponse(object):
|
||||
|
||||
def __init__(self, method, url, status_code):
|
||||
self.request = FakeRequest(method)
|
||||
self.url = url
|
||||
self.status_code = status_code
|
||||
|
||||
|
||||
class HTTPClientTest(testtools.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
super(HTTPClientTest, self).setUp()
|
||||
self.client = httpclient.HTTPClient(
|
||||
API_BASE_URL,
|
||||
AUTH_TOKEN,
|
||||
PROJECT_ID,
|
||||
USER_ID
|
||||
)
|
||||
|
||||
@mock.patch.object(
|
||||
requests,
|
||||
'get',
|
||||
mock.MagicMock(return_value=FakeResponse('get', EXPECTED_URL, 200))
|
||||
)
|
||||
def test_get_request_options(self):
|
||||
self.client.get(API_URL)
|
||||
|
||||
requests.get.assert_called_with(
|
||||
EXPECTED_URL,
|
||||
**EXPECTED_REQ_OPTIONS
|
||||
)
|
||||
|
||||
@mock.patch.object(
|
||||
requests,
|
||||
'get',
|
||||
mock.MagicMock(return_value=FakeResponse('get', EXPECTED_URL, 200))
|
||||
)
|
||||
def test_get_request_options_with_headers_for_get(self):
|
||||
headers = {'foo': 'bar'}
|
||||
|
||||
self.client.get(API_URL, headers=headers)
|
||||
|
||||
expected_options = copy.deepcopy(EXPECTED_REQ_OPTIONS)
|
||||
expected_options['headers'].update(headers)
|
||||
|
||||
requests.get.assert_called_with(
|
||||
EXPECTED_URL,
|
||||
**expected_options
|
||||
)
|
||||
|
||||
@mock.patch.object(
|
||||
requests,
|
||||
'post',
|
||||
mock.MagicMock(return_value=FakeResponse('post', EXPECTED_URL, 201))
|
||||
)
|
||||
def test_get_request_options_with_headers_for_post(self):
|
||||
headers = {'foo': 'bar'}
|
||||
|
||||
self.client.post(API_URL, EXPECTED_BODY, headers=headers)
|
||||
|
||||
expected_options = copy.deepcopy(EXPECTED_REQ_OPTIONS)
|
||||
expected_options['headers'].update(headers)
|
||||
expected_options['headers']['content-type'] = 'application/json'
|
||||
|
||||
requests.post.assert_called_with(
|
||||
EXPECTED_URL,
|
||||
EXPECTED_BODY,
|
||||
**expected_options
|
||||
)
|
||||
|
||||
@mock.patch.object(
|
||||
requests,
|
||||
'put',
|
||||
mock.MagicMock(return_value=FakeResponse('put', EXPECTED_URL, 200))
|
||||
)
|
||||
def test_get_request_options_with_headers_for_put(self):
|
||||
headers = {'foo': 'bar'}
|
||||
|
||||
self.client.put(API_URL, EXPECTED_BODY, headers=headers)
|
||||
|
||||
expected_options = copy.deepcopy(EXPECTED_REQ_OPTIONS)
|
||||
expected_options['headers'].update(headers)
|
||||
expected_options['headers']['content-type'] = 'application/json'
|
||||
|
||||
requests.put.assert_called_with(
|
||||
EXPECTED_URL,
|
||||
EXPECTED_BODY,
|
||||
**expected_options
|
||||
)
|
||||
|
||||
@mock.patch.object(
|
||||
requests,
|
||||
'delete',
|
||||
mock.MagicMock(return_value=FakeResponse('delete', EXPECTED_URL, 200))
|
||||
)
|
||||
def test_get_request_options_with_headers_for_delete(self):
|
||||
headers = {'foo': 'bar'}
|
||||
|
||||
self.client.delete(API_URL, headers=headers)
|
||||
|
||||
expected_options = copy.deepcopy(EXPECTED_REQ_OPTIONS)
|
||||
expected_options['headers'].update(headers)
|
||||
|
||||
requests.delete.assert_called_with(
|
||||
EXPECTED_URL,
|
||||
**expected_options
|
||||
)
|
||||
|
||||
@mock.patch.object(
|
||||
httpclient.HTTPClient,
|
||||
'_get_request_options',
|
||||
mock.MagicMock(return_value=copy.deepcopy(EXPECTED_REQ_OPTIONS))
|
||||
)
|
||||
@mock.patch.object(
|
||||
requests,
|
||||
'get',
|
||||
mock.MagicMock(return_value=FakeResponse('get', EXPECTED_URL, 200))
|
||||
)
|
||||
def test_http_get(self):
|
||||
self.client.get(API_URL)
|
||||
|
||||
httpclient.HTTPClient._get_request_options.assert_called_with(
|
||||
'get',
|
||||
None
|
||||
)
|
||||
|
||||
requests.get.assert_called_with(
|
||||
EXPECTED_URL,
|
||||
**EXPECTED_REQ_OPTIONS
|
||||
)
|
||||
|
||||
@mock.patch.object(
|
||||
httpclient.HTTPClient,
|
||||
'_get_request_options',
|
||||
mock.MagicMock(return_value=copy.deepcopy(EXPECTED_REQ_OPTIONS))
|
||||
)
|
||||
@mock.patch.object(
|
||||
requests,
|
||||
'post',
|
||||
mock.MagicMock(return_value=FakeResponse('post', EXPECTED_URL, 201))
|
||||
)
|
||||
def test_http_post(self):
|
||||
self.client.post(API_URL, EXPECTED_BODY)
|
||||
|
||||
httpclient.HTTPClient._get_request_options.assert_called_with(
|
||||
'post',
|
||||
None
|
||||
)
|
||||
|
||||
requests.post.assert_called_with(
|
||||
EXPECTED_URL,
|
||||
EXPECTED_BODY,
|
||||
**EXPECTED_REQ_OPTIONS
|
||||
)
|
||||
|
||||
@mock.patch.object(
|
||||
httpclient.HTTPClient,
|
||||
'_get_request_options',
|
||||
mock.MagicMock(return_value=copy.deepcopy(EXPECTED_REQ_OPTIONS))
|
||||
)
|
||||
@mock.patch.object(
|
||||
requests,
|
||||
'put',
|
||||
mock.MagicMock(return_value=FakeResponse('put', EXPECTED_URL, 200))
|
||||
)
|
||||
def test_http_put(self):
|
||||
self.client.put(API_URL, EXPECTED_BODY)
|
||||
|
||||
httpclient.HTTPClient._get_request_options.assert_called_with(
|
||||
'put',
|
||||
None
|
||||
)
|
||||
|
||||
requests.put.assert_called_with(
|
||||
EXPECTED_URL,
|
||||
EXPECTED_BODY,
|
||||
**EXPECTED_REQ_OPTIONS
|
||||
)
|
||||
|
||||
@mock.patch.object(
|
||||
httpclient.HTTPClient,
|
||||
'_get_request_options',
|
||||
mock.MagicMock(return_value=copy.deepcopy(EXPECTED_REQ_OPTIONS))
|
||||
)
|
||||
@mock.patch.object(
|
||||
requests,
|
||||
'delete',
|
||||
mock.MagicMock(return_value=FakeResponse('delete', EXPECTED_URL, 200))
|
||||
)
|
||||
def test_http_delete(self):
|
||||
self.client.delete(API_URL)
|
||||
|
||||
httpclient.HTTPClient._get_request_options.assert_called_with(
|
||||
'delete',
|
||||
None
|
||||
)
|
||||
|
||||
requests.delete.assert_called_with(
|
||||
EXPECTED_URL,
|
||||
**EXPECTED_REQ_OPTIONS
|
||||
)
|
Reference in New Issue
Block a user