From df4e8360e21b1b31f68d7b39624e7169125f0f6a Mon Sep 17 00:00:00 2001 From: "Chaozhe.Chen" Date: Thu, 10 Dec 2015 01:08:22 +0800 Subject: [PATCH] Support getting client with keystone session This change will allow cloudkitty client to use keystoneclient/ keystoneauth session object. Change-Id: Icc4bf5da12fb24d189fc38daf1b5cfb4a43228aa --- cloudkittyclient/client.py | 110 +++++++++++++++++++++++++- cloudkittyclient/tests/test_client.py | 15 +++- cloudkittyclient/v1/client.py | 45 ++++++----- 3 files changed, 144 insertions(+), 26 deletions(-) diff --git a/cloudkittyclient/client.py b/cloudkittyclient/client.py index be930fb..9f52408 100644 --- a/cloudkittyclient/client.py +++ b/cloudkittyclient/client.py @@ -10,6 +10,10 @@ # License for the specific language governing permissions and limitations # under the License. +import contextlib +import time + +from keystoneclient import adapter from keystoneclient.auth.identity import v2 as v2_auth from keystoneclient.auth.identity import v3 as v3_auth from keystoneclient import discover @@ -21,6 +25,7 @@ import six.moves.urllib.parse as urlparse from cloudkittyclient.common import utils from cloudkittyclient import exc from cloudkittyclient.openstack.common.apiclient import auth +from cloudkittyclient.openstack.common.apiclient import client from cloudkittyclient.openstack.common.apiclient import exceptions @@ -250,9 +255,28 @@ def get_client(version, **kwargs): :param api_version: the API version to use ('1') :param kwargs: keyword args containing credentials, either: + * session: a keystoneauth/keystoneclient session object + * service_type: The default service_type for URL discovery + * service_name: The default service_name for URL discovery + * interface: The default interface for URL discovery + (Default: public) + * region_name: The default region_name for URL discovery + * endpoint_override: Always use this endpoint URL for requests + for this cloudkittyclient + * auth: An auth plugin to use instead of the session one + * user_agent: The User-Agent string to set + (Default is python-cloudkittyclient) + * connect_retries: the maximum number of retries that should be + attempted for connection errors + * logger: A logging object + + or (DEPRECATED): + * os_token: pre-existing token to re-use * os_endpoint: Cloudkitty API endpoint - or: + + or (DEPRECATED): + * os_username: name of user * os_password: user's password * os_user_id: user's id @@ -314,3 +338,87 @@ def get_auth_plugin(endpoint, **kwargs): project_domain_id=kwargs.get('project_domain_id') ) return auth_plugin + + +LEGACY_OPTS = ('auth_plugin', 'auth_url', 'token', 'insecure', 'cacert', + 'tenant_id', 'project_id', 'username', 'password', + 'project_name', 'tenant_name', + 'user_domain_name', 'user_domain_id', + 'project_domain_name', 'project_domain_id', + 'key_file', 'cert_file', 'verify', 'timeout', 'cert') + + +def construct_http_client(**kwargs): + kwargs = kwargs.copy() + if kwargs.get('session') is not None: + # Drop legacy options + for opt in LEGACY_OPTS: + kwargs.pop(opt, None) + + return SessionClient( + session=kwargs.pop('session'), + service_type=kwargs.pop('service_type', 'rating') or 'rating', + interface=kwargs.pop('interface', kwargs.pop('endpoint_type', + 'publicURL')), + region_name=kwargs.pop('region_name', None), + user_agent=kwargs.pop('user_agent', 'python-cloudkittyclient'), + auth=kwargs.get('auth', None), + timings=kwargs.pop('timings', None), + **kwargs) + else: + return client.BaseClient(client.HTTPClient( + auth_plugin=kwargs.get('auth_plugin'), + region_name=kwargs.get('region_name'), + endpoint_type=kwargs.get('endpoint_type'), + original_ip=kwargs.get('original_ip'), + verify=kwargs.get('verify'), + cert=kwargs.get('cert'), + timeout=kwargs.get('timeout'), + timings=kwargs.get('timings'), + keyring_saver=kwargs.get('keyring_saver'), + debug=kwargs.get('debug'), + user_agent=kwargs.get('user_agent'), + http=kwargs.get('http') + )) + + +@contextlib.contextmanager +def record_time(times, enabled, *args): + """Record the time of a specific action. + + :param times: A list of tuples holds time data. + :type times: list + :param enabled: Whether timing is enabled. + :type enabled: bool + :param args: Other data to be stored besides time data, these args + will be joined to a string. + """ + if not enabled: + yield + else: + start = time.time() + yield + end = time.time() + times.append((' '.join(args), start, end)) + + +class SessionClient(adapter.LegacyJsonAdapter): + def __init__(self, *args, **kwargs): + self.times = [] + self.timings = kwargs.pop('timings', False) + super(SessionClient, self).__init__(*args, **kwargs) + + def request(self, url, method, **kwargs): + kwargs.setdefault('headers', kwargs.get('headers', {})) + # NOTE(sileht): The standard call raises errors from + # keystoneauth, where we need to raise the cloudkittyclient errors. + raise_exc = kwargs.pop('raise_exc', True) + with record_time(self.times, self.timings, method, url): + resp, body = super(SessionClient, self).request(url, + method, + raise_exc=False, + **kwargs) + + if raise_exc and resp.status_code >= 400: + raise exc.from_response(resp, body) + return resp diff --git a/cloudkittyclient/tests/test_client.py b/cloudkittyclient/tests/test_client.py index a579db0..8f08683 100644 --- a/cloudkittyclient/tests/test_client.py +++ b/cloudkittyclient/tests/test_client.py @@ -45,6 +45,16 @@ class ClientTest(utils.BaseTestCase): def setUp(self): super(ClientTest, self).setUp() + def test_client_v1_with_session(self): + resp = mock.Mock(status_code=200, text=b'') + resp.json.return_value = {"modules": []} + session = mock.Mock() + session.request.return_value = resp + c = client.get_client(1, session=session) + c.modules.list() + self.assertTrue(session.request.called) + self.assertTrue(resp.json.called) + def test_client_version(self): c1 = self.create_client(env=FAKE_ENV, api_version=1) self.assertIsInstance(c1, v1client.Client) @@ -137,7 +147,8 @@ class ClientTest(utils.BaseTestCase): env = FAKE_ENV.copy() env['cacert'] = '/path/to/cacert' client = self.create_client(env) - self.assertEqual('/path/to/cacert', client.client.verify) + self.assertEqual('/path/to/cacert', + client.http_client.http_client.verify) def test_v1_client_certfile_and_keyfile(self): env = FAKE_ENV.copy() @@ -145,4 +156,4 @@ class ClientTest(utils.BaseTestCase): env['key_file'] = '/path/to/keycert' client = self.create_client(env) self.assertEqual(('/path/to/cert', '/path/to/keycert'), - client.client.cert) + client.http_client.http_client.cert) diff --git a/cloudkittyclient/v1/client.py b/cloudkittyclient/v1/client.py index c635c57..d1e4e64 100644 --- a/cloudkittyclient/v1/client.py +++ b/cloudkittyclient/v1/client.py @@ -16,7 +16,6 @@ from stevedore import extension from cloudkittyclient import client as ckclient -from cloudkittyclient.openstack.common.apiclient import client from cloudkittyclient.v1 import collector from cloudkittyclient.v1 import core from cloudkittyclient.v1 import report @@ -28,33 +27,33 @@ SUBMODULES_NAMESPACE = 'cloudkitty.client.modules' class Client(object): """Client for the Cloudkitty v1 API. - :param string endpoint: A user-supplied endpoint URL for the cloudkitty - service. - :param function token: Provides token for authentication. - :param integer timeout: Allows customization of the timeout for client - http requests. (optional) + :param session: a keystoneauth/keystoneclient session object + :type session: keystoneclient.session.Session + :param str service_type: The default service_type for URL discovery + :param str service_name: The default service_name for URL discovery + :param str interface: The default interface for URL discovery + (Default: public) + :param str region_name: The default region_name for URL discovery + :param str endpoint_override: Always use this endpoint URL for requests + for this cloudkittyclient + :param auth: An auth plugin to use instead of the session one + :type auth: keystoneclient.auth.base.BaseAuthPlugin + :param str user_agent: The User-Agent string to set + (Default is python-cloudkittyclient) + :param int connect_retries: the maximum number of retries that should be + attempted for connection errors + :param logger: A logging object + :type logger: logging.Logger """ def __init__(self, *args, **kwargs): """Initialize a new client for the Cloudkitty v1 API.""" - self.auth_plugin = (kwargs.get('auth_plugin') - or ckclient.get_auth_plugin(*args, **kwargs)) - self.client = client.HTTPClient( - auth_plugin=self.auth_plugin, - region_name=kwargs.get('region_name'), - endpoint_type=kwargs.get('endpoint_type'), - original_ip=kwargs.get('original_ip'), - verify=kwargs.get('verify'), - cert=kwargs.get('cert'), - timeout=kwargs.get('timeout'), - timings=kwargs.get('timings'), - keyring_saver=kwargs.get('keyring_saver'), - debug=kwargs.get('debug'), - user_agent=kwargs.get('user_agent'), - http=kwargs.get('http') - ) - self.http_client = client.BaseClient(self.client) + if not kwargs.get('auth_plugin'): + kwargs['auth_plugin'] = ckclient.get_auth_plugin(*args, **kwargs) + self.auth_plugin = kwargs.get('auth_plugin') + + self.http_client = ckclient.construct_http_client(**kwargs) self.modules = core.CloudkittyModuleManager(self.http_client) self.collector = collector.CollectorManager(self.http_client) self.reports = report.ReportManager(self.http_client)