From 2afa2903021caa567098b83f3f8604e82c2bdd74 Mon Sep 17 00:00:00 2001 From: Hiroki Ito Date: Sat, 4 Nov 2017 23:42:30 +0900 Subject: [PATCH] Restore backward compatibility for init client After merged the patch I08c8b753972c27b4e6bbe07a8aa51e0e72fbc56d, blazarclient can't be used with blazar_url and auth_token since this patch only allows sessions for the initiation of client. For backward compatibility, blazarclient should be initiated with blazar_url and auth_token as before. This patch enables using blazarclient with a set of blazar_url and auth_token or session by reviving BaseClientManager class with adding a logic to chose an auth method based on given params. Change-Id: I25a665145b0503cc04e49bc85c39e2f6dca36925 Closes-Bug: #1724757 --- blazarclient/base.py | 135 ++++++++++++++++++++++++++ blazarclient/exception.py | 8 ++ blazarclient/tests/test_base.py | 162 ++++++++++++++++++++++++++++++++ blazarclient/v1/client.py | 26 +++-- blazarclient/v1/hosts.py | 22 ++--- blazarclient/v1/leases.py | 19 ++-- 6 files changed, 341 insertions(+), 31 deletions(-) create mode 100644 blazarclient/base.py create mode 100644 blazarclient/tests/test_base.py diff --git a/blazarclient/base.py b/blazarclient/base.py new file mode 100644 index 0000000..fc18ef3 --- /dev/null +++ b/blazarclient/base.py @@ -0,0 +1,135 @@ +# Copyright (c) 2013 Mirantis 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 json + +from keystoneauth1 import adapter +import requests + +from blazarclient import exception +from blazarclient.i18n import _ + + +class RequestManager(object): + """Manager to create request from given Blazar URL and auth token.""" + + def __init__(self, blazar_url, auth_token, user_agent): + self.blazar_url = blazar_url + self.auth_token = auth_token + self.user_agent = user_agent + + def get(self, url): + """Sends get request to Blazar. + + :param url: URL to the wanted Blazar resource. + :type url: str + """ + return self.request(url, 'GET') + + def post(self, url, body): + """Sends post request to Blazar. + + :param url: URL to the wanted Blazar resource. + :type url: str + + :param body: Values resource to be created from. + :type body: dict + """ + return self.request(url, 'POST', body=body) + + def delete(self, url): + """Sends delete request to Blazar. + + :param url: URL to the wanted Blazar resource. + :type url: str + """ + return self.request(url, 'DELETE') + + def put(self, url, body): + """Sends update request to Blazar. + + :param url: URL to the wanted Blazar resource. + :type url: str + + :param body: Values resource to be updated from. + :type body: dict + """ + return self.request(url, 'PUT', body=body) + + def request(self, url, method, **kwargs): + """Base request method. + + Adds specific headers and URL prefix to the request. + + :param url: Resource URL. + :type url: str + + :param method: Method to be called (GET, POST, PUT, DELETE). + :type method: str + + :returns: Response and body. + :rtype: tuple + """ + kwargs.setdefault('headers', kwargs.get('headers', {})) + kwargs['headers']['User-Agent'] = self.user_agent + kwargs['headers']['Accept'] = 'application/json' + kwargs['headers']['x-auth-token'] = self.auth_token + + if 'body' in kwargs: + kwargs['headers']['Content-Type'] = 'application/json' + kwargs['data'] = json.dumps(kwargs['body']) + del kwargs['body'] + + resp = requests.request(method, self.blazar_url + url, **kwargs) + + try: + body = json.loads(resp.text) + except ValueError: + body = None + + if resp.status_code >= 400: + if body is not None: + error_message = body.get('error_message', body) + else: + error_message = resp.text + + body = _("ERROR: {0}").format(error_message) + raise exception.BlazarClientException(body, code=resp.status_code) + + return resp, body + + +class BaseClientManager(object): + """Base class for managing resources of Blazar.""" + + user_agent = 'python-blazarclient' + + def __init__(self, blazar_url, auth_token, session, **kwargs): + self.blazar_url = blazar_url + self.auth_token = auth_token + self.session = session + + if self.session: + self.request_manager = adapter.LegacyJsonAdapter( + session=self.session, + user_agent=self.user_agent, + **kwargs + ) + elif self.blazar_url and self.auth_token: + self.request_manager = RequestManager(blazar_url=self.blazar_url, + auth_token=self.auth_token, + user_agent=self.user_agent) + else: + raise exception.InsufficientAuthInfomation diff --git a/blazarclient/exception.py b/blazarclient/exception.py index 8aa8791..89568f9 100644 --- a/blazarclient/exception.py +++ b/blazarclient/exception.py @@ -82,3 +82,11 @@ class DuplicatedLeaseParameters(BlazarClientException): """Occurs if lease parameters are duplicated.""" message = _("The lease parameters are duplicated.") code = 400 + + +class InsufficientAuthInfomation(BlazarClientException): + """Occurs if the auth info passed to blazar client is insufficient.""" + message = _("The passed arguments are insufficient " + "for the authentication. The instance of " + "keystoneauth1.session.Session class is required.") + code = 400 diff --git a/blazarclient/tests/test_base.py b/blazarclient/tests/test_base.py new file mode 100644 index 0000000..4d561f4 --- /dev/null +++ b/blazarclient/tests/test_base.py @@ -0,0 +1,162 @@ +# Copyright (c) 2014 Mirantis 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 mock + +from keystoneauth1 import adapter + +from blazarclient import base +from blazarclient import exception +from blazarclient import tests + + +class RequestManagerTestCase(tests.TestCase): + + def setUp(self): + super(RequestManagerTestCase, self).setUp() + + self.blazar_url = "www.fake.com/reservation" + self.auth_token = "aaa-bbb-ccc" + self.user_agent = "python-blazarclient" + self.manager = base.RequestManager(blazar_url=self.blazar_url, + auth_token=self.auth_token, + user_agent=self.user_agent) + + @mock.patch('blazarclient.base.RequestManager.request', + return_value=(200, {"fake": "FAKE"})) + def test_get(self, m): + url = '/leases' + resp, body = self.manager.get(url) + self.assertEqual(resp, 200) + self.assertDictEqual(body, {"fake": "FAKE"}) + m.assert_called_once_with(url, "GET") + + @mock.patch('blazarclient.base.RequestManager.request', + return_value=(200, {"fake": "FAKE"})) + def test_post(self, m): + url = '/leases' + req_body = { + 'start': '2020-07-24 20:00', + 'end': '2020-08-09 22:30', + 'before_end': '2020-08-09 21:30', + 'events': [], + 'name': 'lease-test', + 'reservations': [ + { + 'min': '1', + 'max': '2', + 'hypervisor_properties': + '[">=", "$vcpus", "2"]', + 'resource_properties': + '["==", "$extra_key", "extra_value"]', + 'resource_type': 'physical:host', + 'before_end': 'default' + } + ] + } + resp, body = self.manager.post(url, req_body) + self.assertEqual(resp, 200) + self.assertDictEqual(body, {"fake": "FAKE"}) + m.assert_called_once_with(url, "POST", body=req_body) + + @mock.patch('blazarclient.base.RequestManager.request', + return_value=(200, {"fake": "FAKE"})) + def test_delete(self, m): + url = '/leases/aaa-bbb-ccc' + resp, body = self.manager.delete(url) + self.assertEqual(resp, 200) + self.assertDictEqual(body, {"fake": "FAKE"}) + m.assert_called_once_with(url, "DELETE") + + @mock.patch('blazarclient.base.RequestManager.request', + return_value=(200, {"fake": "FAKE"})) + def test_put(self, m): + url = '/leases/aaa-bbb-ccc' + req_body = { + 'name': 'lease-test', + } + resp, body = self.manager.put(url, req_body) + self.assertEqual(resp, 200) + self.assertDictEqual(body, {"fake": "FAKE"}) + m.assert_called_once_with(url, "PUT", body=req_body) + + @mock.patch('requests.request') + def test_request_ok_with_body(self, m): + m.return_value.status_code = 200 + m.return_value.text = '{"resp_key": "resp_value"}' + url = '/leases' + kwargs = {"body": {"req_key": "req_value"}} + self.assertEqual(self.manager.request(url, "POST", **kwargs), + (m(), {"resp_key": "resp_value"})) + + @mock.patch('requests.request') + def test_request_ok_without_body(self, m): + m.return_value.status_code = 200 + m.return_value.text = "resp" + url = '/leases' + kwargs = {"body": {"req_key": "req_value"}} + self.assertEqual(self.manager.request(url, "POST", **kwargs), + (m(), None)) + + @mock.patch('requests.request') + def test_request_fail_with_body(self, m): + m.return_value.status_code = 400 + m.return_value.text = '{"resp_key": "resp_value"}' + url = '/leases' + kwargs = {"body": {"req_key": "req_value"}} + self.assertRaises(exception.BlazarClientException, + self.manager.request, url, "POST", **kwargs) + + @mock.patch('requests.request') + def test_request_fail_without_body(self, m): + m.return_value.status_code = 400 + m.return_value.text = "resp" + url = '/leases' + kwargs = {"body": {"req_key": "req_value"}} + self.assertRaises(exception.BlazarClientException, + self.manager.request, url, "POST", **kwargs) + + +class BaseClientManagerTestCase(tests.TestCase): + + def setUp(self): + super(BaseClientManagerTestCase, self).setUp() + + self.blazar_url = "www.fake.com/reservation" + self.auth_token = "aaa-bbb-ccc" + self.session = mock.MagicMock() + self.user_agent = "python-blazarclient" + + def test_init_with_session(self): + manager = base.BaseClientManager(blazar_url=None, + auth_token=None, + session=self.session) + self.assertIsInstance(manager.request_manager, + adapter.LegacyJsonAdapter) + + def test_init_with_url_and_token(self): + manager = base.BaseClientManager(blazar_url=self.blazar_url, + auth_token=self.auth_token, + session=None) + self.assertIsInstance(manager.request_manager, + base.RequestManager) + + def test_init_with_insufficient_info(self): + self.assertRaises(exception.InsufficientAuthInfomation, + base.BaseClientManager, + blazar_url=None, + auth_token=self.auth_token, + session=None) diff --git a/blazarclient/v1/client.py b/blazarclient/v1/client.py index 7c0d423..d9ffe3d 100644 --- a/blazarclient/v1/client.py +++ b/blazarclient/v1/client.py @@ -13,6 +13,7 @@ # See the License for the specific language governing permissions and # limitations under the License. +import logging from blazarclient.v1 import hosts from blazarclient.v1 import leases @@ -31,15 +32,26 @@ class Client(object): ... """ - def __init__(self, session, *args, **kwargs): - self.session = session - self.version = '1' + version = '1' - self.lease = leases.LeaseClientManager(session=self.session, + def __init__(self, blazar_url=None, auth_token=None, session=None, + **kwargs): + self.blazar_url = blazar_url + self.auth_token = auth_token + self.session = session + + if not self.session: + logging.warning('Use a keystoneauth session object for the ' + 'authentication. The authentication with ' + 'blazar_url and auth_token is deprecated.') + + self.lease = leases.LeaseClientManager(blazar_url=self.blazar_url, + auth_token=self.auth_token, + session=self.session, version=self.version, - *args, **kwargs) - self.host = hosts.ComputeHostClientManager(session=self.session, + self.host = hosts.ComputeHostClientManager(blazar_url=self.blazar_url, + auth_token=self.auth_token, + session=self.session, version=self.version, - *args, **kwargs) diff --git a/blazarclient/v1/hosts.py b/blazarclient/v1/hosts.py index 2303f44..d66864b 100644 --- a/blazarclient/v1/hosts.py +++ b/blazarclient/v1/hosts.py @@ -13,45 +13,41 @@ # See the License for the specific language governing permissions and # limitations under the License. -from keystoneauth1 import adapter - +from blazarclient import base from blazarclient.i18n import _ -class ComputeHostClientManager(adapter.LegacyJsonAdapter): +class ComputeHostClientManager(base.BaseClientManager): """Manager for the ComputeHost connected requests.""" - client_name = 'python-blazarclient' - def create(self, name, **kwargs): """Creates host from values passed.""" values = {'name': name} values.update(**kwargs) - resp, body = self.post('/os-hosts', body=values) + resp, body = self.request_manager.post('/os-hosts', body=values) return body['host'] def get(self, host_id): """Describes host specifications such as name and details.""" - resp, body = super(ComputeHostClientManager, - self).get('/os-hosts/%s' % host_id) + resp, body = self.request_manager.get('/os-hosts/%s' % host_id) return body['host'] def update(self, host_id, values): """Update attributes of the host.""" if not values: return _('No values to update passed.') - resp, body = self.put('/os-hosts/%s' % host_id, body=values) + resp, body = self.request_manager.put( + '/os-hosts/%s' % host_id, body=values + ) return body['host'] def delete(self, host_id): """Deletes host with specified ID.""" - resp, body = super(ComputeHostClientManager, - self).delete('/os-hosts/%s' % host_id) + resp, body = self.request_manager.delete('/os-hosts/%s' % host_id) def list(self, sort_by=None): """List all hosts.""" - resp, body = super(ComputeHostClientManager, - self).get('/os-hosts') + resp, body = self.request_manager.get('/os-hosts') hosts = body['hosts'] if sort_by: hosts = sorted(hosts, key=lambda l: l[sort_by]) diff --git a/blazarclient/v1/leases.py b/blazarclient/v1/leases.py index 45207e3..2fb59cf 100644 --- a/blazarclient/v1/leases.py +++ b/blazarclient/v1/leases.py @@ -13,33 +13,30 @@ # See the License for the specific language governing permissions and # limitations under the License. -from keystoneauth1 import adapter from oslo_utils import timeutils +from blazarclient import base from blazarclient.i18n import _ from blazarclient import utils -class LeaseClientManager(adapter.LegacyJsonAdapter): +class LeaseClientManager(base.BaseClientManager): """Manager for the lease connected requests.""" - client_name = 'python-blazarclient' - def create(self, name, start, end, reservations, events, before_end=None): """Creates lease from values passed.""" values = {'name': name, 'start_date': start, 'end_date': end, 'reservations': reservations, 'events': events, 'before_end_date': before_end} - resp, body = self.post('/leases', body=values) + resp, body = self.request_manager.post('/leases', body=values) return body['lease'] def get(self, lease_id): """Describes lease specifications such as name, status and locked condition. """ - resp, body = super(LeaseClientManager, - self).get('/leases/%s' % lease_id) + resp, body = self.request_manager.get('/leases/%s' % lease_id) return body['lease'] def update(self, lease_id, name=None, prolong_for=None, reduce_by=None, @@ -81,17 +78,17 @@ class LeaseClientManager(adapter.LegacyJsonAdapter): if not values: return _('No values to update passed.') - resp, body = self.put('/leases/%s' % lease_id, body=values) + resp, body = self.request_manager.put('/leases/%s' % lease_id, + body=values) return body['lease'] def delete(self, lease_id): """Deletes lease with specified ID.""" - resp, body = super(LeaseClientManager, - self).delete('/leases/%s' % lease_id) + resp, body = self.request_manager.delete('/leases/%s' % lease_id) def list(self, sort_by=None): """List all leases.""" - resp, body = super(LeaseClientManager, self).get('/leases') + resp, body = self.request_manager.get('/leases') leases = body['leases'] if sort_by: leases = sorted(leases, key=lambda l: l[sort_by])