From 20ca8186c673cd74a9ce776708e4e8222666e7a0 Mon Sep 17 00:00:00 2001 From: Feng Shengqin Date: Tue, 25 Apr 2017 16:34:21 +0800 Subject: [PATCH] Introduce API micro version The patch adds 'OpenStack-API-Version' header for each requests. Change-Id: I621a4268685a1c188a004dcfa576775b424d2610 Implements: blueprint api-microversion --- zunclient/common/httpclient.py | 21 +++++++++-- zunclient/shell.py | 49 ++++++++++++++++++++++---- zunclient/tests/unit/test_shell.py | 9 ++--- zunclient/tests/unit/v1/test_client.py | 18 ++++++---- zunclient/v1/client.py | 3 +- 5 files changed, 80 insertions(+), 20 deletions(-) diff --git a/zunclient/common/httpclient.py b/zunclient/common/httpclient.py index 4ce2981b..2f5bbe61 100644 --- a/zunclient/common/httpclient.py +++ b/zunclient/common/httpclient.py @@ -35,6 +35,7 @@ USER_AGENT = 'python-zunclient' CHUNKSIZE = 1024 * 64 # 64kB API_VERSION = '/v1' +DEFAULT_API_VERSION = 'latest' def _extract_error_json(body): @@ -65,10 +66,11 @@ def _extract_error_json(body): class HTTPClient(object): - def __init__(self, endpoint, **kwargs): + def __init__(self, endpoint, api_version=DEFAULT_API_VERSION, **kwargs): self.endpoint = endpoint self.auth_token = kwargs.get('token') self.auth_ref = kwargs.get('auth_ref') + self.api_version = api_version self.connection_params = self.get_connection_params(endpoint, **kwargs) @staticmethod @@ -155,6 +157,10 @@ class HTTPClient(object): # Copy the kwargs so we can reuse the original in case of redirects kwargs['headers'] = copy.deepcopy(kwargs.get('headers', {})) kwargs['headers'].setdefault('User-Agent', USER_AGENT) + if self.api_version: + version_string = 'container %s' % self.api_version + kwargs['headers'].setdefault( + 'OpenStack-API-Version', version_string) if self.auth_token: kwargs['headers'].setdefault('X-Auth-Token', self.auth_token) @@ -307,7 +313,10 @@ class VerifiedHTTPSConnection(six.moves.http_client.HTTPSConnection): class SessionClient(adapter.LegacyJsonAdapter): """HTTP client based on Keystone client session.""" - def __init__(self, user_agent=USER_AGENT, logger=LOG, *args, **kwargs): + def __init__(self, user_agent=USER_AGENT, logger=LOG, + api_version=DEFAULT_API_VERSION, *args, **kwargs): + self.user_agent = USER_AGENT + self.api_version = api_version super(SessionClient, self).__init__(*args, **kwargs) def _http_request(self, url, method, **kwargs): @@ -318,6 +327,14 @@ class SessionClient(adapter.LegacyJsonAdapter): kwargs.setdefault('auth', self.auth) kwargs.setdefault('endpoint_override', self.endpoint_override) + # Copy the kwargs so we can reuse the original in case of redirects + kwargs['headers'] = copy.deepcopy(kwargs.get('headers', {})) + kwargs['headers'].setdefault('User-Agent', self.user_agent) + if self.api_version: + version_string = 'container %s' % self.api_version + kwargs['headers'].setdefault( + 'OpenStack-API-Version', version_string) + # NOTE(kevinz): osprofiler_web.get_trace_id_headers does not add any # headers in case if osprofiler is not initialized. if osprofiler_web: diff --git a/zunclient/shell.py b/zunclient/shell.py index 135018e1..e37b04ea 100644 --- a/zunclient/shell.py +++ b/zunclient/shell.py @@ -55,11 +55,12 @@ except ImportError: from zunclient.common.apiclient import auth from zunclient.common import cliutils from zunclient import exceptions as exc +from zunclient.i18n import _ from zunclient.v1 import client as client_v1 from zunclient.v1 import shell as shell_v1 from zunclient import version -DEFAULT_API_VERSION = '1' +LATEST_API_VERSION = ('1', 'latest') DEFAULT_ENDPOINT_TYPE = 'publicURL' DEFAULT_SERVICE_TYPE = 'container' @@ -331,7 +332,7 @@ class OpenStackZunShell(object): metavar='', default=cliutils.env( 'ZUN_API_VERSION', - default=DEFAULT_API_VERSION), + default='latest'), help='Accepts "api", ' 'defaults to env[ZUN_API_VERSION].') parser.add_argument('--zun_api_version', @@ -386,7 +387,7 @@ class OpenStackZunShell(object): try: actions_modules = { - '1': shell_v1.COMMAND_MODULES, + '1': shell_v1.COMMAND_MODULES }[version] except KeyError: actions_modules = shell_v1.COMMAND_MODULES @@ -445,6 +446,34 @@ class OpenStackZunShell(object): logging.basicConfig(level=logging.CRITICAL, format=streamformat) + def _check_version(self, api_version): + if api_version == 'latest': + return LATEST_API_VERSION + else: + try: + versions = tuple(int(i) for i in api_version.split('.')) + except ValueError: + versions = () + if len(versions) == 1: + # Default value of zun_api_version is '1'. + # If user not specify the value of api version, not passing + # headers at all. + zun_api_version = None + elif len(versions) == 2: + zun_api_version = api_version + # In the case of '1.0' + if versions[1] == 0: + zun_api_version = None + else: + msg = _("The requested API version %(ver)s is an unexpected " + "format. Acceptable formats are 'X', 'X.Y', or the " + "literal string '%(latest)s'." + ) % {'ver': api_version, 'latest': 'latest'} + raise exc.CommandError(msg) + + api_major_version = versions[0] + return (api_major_version, zun_api_version) + def main(self, argv): # NOTE(Christoph Jansen): With Python 3.4 argv somehow becomes a Map. @@ -464,8 +493,12 @@ class OpenStackZunShell(object): spot = argv.index('--endpoint_type') argv[spot] = '--endpoint-type' + # build available subcommands based on version + (api_major_version, zun_api_version) = ( + self._check_version(options.zun_api_version)) + subcommand_parser = ( - self.get_subcommand_parser(options.zun_api_version) + self.get_subcommand_parser(api_major_version) ) self.parser = subcommand_parser @@ -488,12 +521,13 @@ class OpenStackZunShell(object): os_user_domain_id, os_user_domain_name, os_project_domain_id, os_project_domain_name, os_auth_url, os_auth_system, endpoint_type, - service_type, bypass_url, insecure) = ( + service_type, bypass_url, insecure, zun_api_version) = ( (args.os_username, args.os_project_name, args.os_project_id, args.os_user_domain_id, args.os_user_domain_name, args.os_project_domain_id, args.os_project_domain_name, args.os_auth_url, args.os_auth_system, args.endpoint_type, - args.service_type, args.bypass_url, args.insecure) + args.service_type, args.bypass_url, args.insecure, + args.zun_api_version) ) if os_auth_system and os_auth_system != "keystone": @@ -571,7 +605,7 @@ class OpenStackZunShell(object): try: client = { '1': client_v1, - }[options.zun_api_version] + }[api_major_version] except KeyError: client = client_v1 @@ -593,6 +627,7 @@ class OpenStackZunShell(object): zun_url=bypass_url, endpoint_type=endpoint_type, insecure=insecure, + api_version=zun_api_version, **kwargs) args.func(self.cs, args) diff --git a/zunclient/tests/unit/test_shell.py b/zunclient/tests/unit/test_shell.py index d16d066c..dd4d3aa7 100644 --- a/zunclient/tests/unit/test_shell.py +++ b/zunclient/tests/unit/test_shell.py @@ -248,7 +248,7 @@ class ShellTest(utils.TestCase): service_type='container', region_name=expected_region_name, project_domain_id='', project_domain_name='', user_domain_id='', user_domain_name='', profile=None, - zun_url=None, insecure=False) + zun_url=None, insecure=False, api_version='latest') def test_main_option_region(self): self.make_env() @@ -275,7 +275,7 @@ class ShellTest(utils.TestCase): service_type='container', region_name=None, project_domain_id='', project_domain_name='', user_domain_id='', user_domain_name='', profile=None, - zun_url=None, insecure=False) + zun_url=None, insecure=False, api_version='latest') @mock.patch('zunclient.v1.client.Client') def test_main_endpoint_internal(self, mock_client): @@ -288,7 +288,7 @@ class ShellTest(utils.TestCase): service_type='container', region_name=None, project_domain_id='', project_domain_name='', user_domain_id='', user_domain_name='', profile=None, - zun_url=None, insecure=False) + zun_url=None, insecure=False, api_version='latest') class ShellTestKeystoneV3(ShellTest): @@ -318,4 +318,5 @@ class ShellTestKeystoneV3(ShellTest): service_type='container', region_name=None, project_domain_id='', project_domain_name='Default', user_domain_id='', user_domain_name='Default', - zun_url=None, insecure=False, profile=None) + zun_url=None, insecure=False, profile=None, + api_version='latest') diff --git a/zunclient/tests/unit/v1/test_client.py b/zunclient/tests/unit/v1/test_client.py index fe17ad48..4b2bf116 100644 --- a/zunclient/tests/unit/v1/test_client.py +++ b/zunclient/tests/unit/v1/test_client.py @@ -31,7 +31,8 @@ class ClientTest(testtools.TestCase): region_name=None, service_name=None, service_type='container', - session=session) + session=session, + api_version=None) @mock.patch('zunclient.common.httpclient.SessionClient') @mock.patch('keystoneauth1.token_endpoint.Token') @@ -51,7 +52,8 @@ class ClientTest(testtools.TestCase): region_name=None, service_name=None, service_type='container', - session=session) + session=session, + api_version=None) @mock.patch('zunclient.common.httpclient.SessionClient') @mock.patch('keystoneauth1.loading.get_plugin_loader') @@ -76,7 +78,8 @@ class ClientTest(testtools.TestCase): region_name=None, service_name=None, service_type='container', - session=mock.ANY) + session=mock.ANY, + api_version=None) @mock.patch('zunclient.common.httpclient.SessionClient') @mock.patch('keystoneauth1.loading.get_plugin_loader') @@ -102,7 +105,8 @@ class ClientTest(testtools.TestCase): region_name=None, service_name=None, service_type='container', - session=mock.ANY) + session=mock.ANY, + api_version=None) @mock.patch('zunclient.common.httpclient.SessionClient') @mock.patch('keystoneauth1.loading.get_plugin_loader') @@ -142,7 +146,8 @@ class ClientTest(testtools.TestCase): service_name=None, service_type='container', session=session, - endpoint_override='zunurl') + endpoint_override='zunurl', + api_version=None) @mock.patch('zunclient.common.httpclient.SessionClient') @mock.patch('keystoneauth1.session.Session') @@ -158,4 +163,5 @@ class ClientTest(testtools.TestCase): service_name=None, service_type='container', session=session, - endpoint_override='zunurl') + endpoint_override='zunurl', + api_version=None) diff --git a/zunclient/v1/client.py b/zunclient/v1/client.py index 10a45e37..1ce2bd8a 100644 --- a/zunclient/v1/client.py +++ b/zunclient/v1/client.py @@ -35,7 +35,7 @@ class Client(object): interface='public', service_name=None, insecure=False, user_domain_id=None, user_domain_name=None, project_domain_id=None, project_domain_name=None, - **kwargs): + api_version=None, **kwargs): # We have to keep the api_key are for backwards compat, but let's # remove it from the rest of our code since it's not a keystone @@ -111,6 +111,7 @@ class Client(object): interface=interface, region_name=region_name, session=session, + api_version=api_version, **client_kwargs) self.containers = containers.ContainerManager(self.http_client) self.images = images.ImageManager(self.http_client)