Introduce API micro version

The patch adds 'OpenStack-API-Version' header for each requests.

Change-Id: I621a4268685a1c188a004dcfa576775b424d2610
Implements: blueprint api-microversion
This commit is contained in:
Feng Shengqin 2017-04-25 16:34:21 +08:00
parent fa61ba016e
commit 20ca8186c6
5 changed files with 80 additions and 20 deletions

View File

@ -35,6 +35,7 @@ USER_AGENT = 'python-zunclient'
CHUNKSIZE = 1024 * 64 # 64kB CHUNKSIZE = 1024 * 64 # 64kB
API_VERSION = '/v1' API_VERSION = '/v1'
DEFAULT_API_VERSION = 'latest'
def _extract_error_json(body): def _extract_error_json(body):
@ -65,10 +66,11 @@ def _extract_error_json(body):
class HTTPClient(object): class HTTPClient(object):
def __init__(self, endpoint, **kwargs): def __init__(self, endpoint, api_version=DEFAULT_API_VERSION, **kwargs):
self.endpoint = endpoint self.endpoint = endpoint
self.auth_token = kwargs.get('token') self.auth_token = kwargs.get('token')
self.auth_ref = kwargs.get('auth_ref') self.auth_ref = kwargs.get('auth_ref')
self.api_version = api_version
self.connection_params = self.get_connection_params(endpoint, **kwargs) self.connection_params = self.get_connection_params(endpoint, **kwargs)
@staticmethod @staticmethod
@ -155,6 +157,10 @@ class HTTPClient(object):
# Copy the kwargs so we can reuse the original in case of redirects # Copy the kwargs so we can reuse the original in case of redirects
kwargs['headers'] = copy.deepcopy(kwargs.get('headers', {})) kwargs['headers'] = copy.deepcopy(kwargs.get('headers', {}))
kwargs['headers'].setdefault('User-Agent', USER_AGENT) 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: if self.auth_token:
kwargs['headers'].setdefault('X-Auth-Token', 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): class SessionClient(adapter.LegacyJsonAdapter):
"""HTTP client based on Keystone client session.""" """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) super(SessionClient, self).__init__(*args, **kwargs)
def _http_request(self, url, method, **kwargs): def _http_request(self, url, method, **kwargs):
@ -318,6 +327,14 @@ class SessionClient(adapter.LegacyJsonAdapter):
kwargs.setdefault('auth', self.auth) kwargs.setdefault('auth', self.auth)
kwargs.setdefault('endpoint_override', self.endpoint_override) 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 # NOTE(kevinz): osprofiler_web.get_trace_id_headers does not add any
# headers in case if osprofiler is not initialized. # headers in case if osprofiler is not initialized.
if osprofiler_web: if osprofiler_web:

View File

@ -55,11 +55,12 @@ except ImportError:
from zunclient.common.apiclient import auth from zunclient.common.apiclient import auth
from zunclient.common import cliutils from zunclient.common import cliutils
from zunclient import exceptions as exc from zunclient import exceptions as exc
from zunclient.i18n import _
from zunclient.v1 import client as client_v1 from zunclient.v1 import client as client_v1
from zunclient.v1 import shell as shell_v1 from zunclient.v1 import shell as shell_v1
from zunclient import version from zunclient import version
DEFAULT_API_VERSION = '1' LATEST_API_VERSION = ('1', 'latest')
DEFAULT_ENDPOINT_TYPE = 'publicURL' DEFAULT_ENDPOINT_TYPE = 'publicURL'
DEFAULT_SERVICE_TYPE = 'container' DEFAULT_SERVICE_TYPE = 'container'
@ -331,7 +332,7 @@ class OpenStackZunShell(object):
metavar='<zun-api-ver>', metavar='<zun-api-ver>',
default=cliutils.env( default=cliutils.env(
'ZUN_API_VERSION', 'ZUN_API_VERSION',
default=DEFAULT_API_VERSION), default='latest'),
help='Accepts "api", ' help='Accepts "api", '
'defaults to env[ZUN_API_VERSION].') 'defaults to env[ZUN_API_VERSION].')
parser.add_argument('--zun_api_version', parser.add_argument('--zun_api_version',
@ -386,7 +387,7 @@ class OpenStackZunShell(object):
try: try:
actions_modules = { actions_modules = {
'1': shell_v1.COMMAND_MODULES, '1': shell_v1.COMMAND_MODULES
}[version] }[version]
except KeyError: except KeyError:
actions_modules = shell_v1.COMMAND_MODULES actions_modules = shell_v1.COMMAND_MODULES
@ -445,6 +446,34 @@ class OpenStackZunShell(object):
logging.basicConfig(level=logging.CRITICAL, logging.basicConfig(level=logging.CRITICAL,
format=streamformat) 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): def main(self, argv):
# NOTE(Christoph Jansen): With Python 3.4 argv somehow becomes a Map. # NOTE(Christoph Jansen): With Python 3.4 argv somehow becomes a Map.
@ -464,8 +493,12 @@ class OpenStackZunShell(object):
spot = argv.index('--endpoint_type') spot = argv.index('--endpoint_type')
argv[spot] = '--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 = ( subcommand_parser = (
self.get_subcommand_parser(options.zun_api_version) self.get_subcommand_parser(api_major_version)
) )
self.parser = subcommand_parser self.parser = subcommand_parser
@ -488,12 +521,13 @@ class OpenStackZunShell(object):
os_user_domain_id, os_user_domain_name, os_user_domain_id, os_user_domain_name,
os_project_domain_id, os_project_domain_name, os_project_domain_id, os_project_domain_name,
os_auth_url, os_auth_system, endpoint_type, 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_username, args.os_project_name, args.os_project_id,
args.os_user_domain_id, args.os_user_domain_name, args.os_user_domain_id, args.os_user_domain_name,
args.os_project_domain_id, args.os_project_domain_name, args.os_project_domain_id, args.os_project_domain_name,
args.os_auth_url, args.os_auth_system, args.endpoint_type, 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": if os_auth_system and os_auth_system != "keystone":
@ -571,7 +605,7 @@ class OpenStackZunShell(object):
try: try:
client = { client = {
'1': client_v1, '1': client_v1,
}[options.zun_api_version] }[api_major_version]
except KeyError: except KeyError:
client = client_v1 client = client_v1
@ -593,6 +627,7 @@ class OpenStackZunShell(object):
zun_url=bypass_url, zun_url=bypass_url,
endpoint_type=endpoint_type, endpoint_type=endpoint_type,
insecure=insecure, insecure=insecure,
api_version=zun_api_version,
**kwargs) **kwargs)
args.func(self.cs, args) args.func(self.cs, args)

View File

@ -248,7 +248,7 @@ class ShellTest(utils.TestCase):
service_type='container', region_name=expected_region_name, service_type='container', region_name=expected_region_name,
project_domain_id='', project_domain_name='', project_domain_id='', project_domain_name='',
user_domain_id='', user_domain_name='', profile=None, 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): def test_main_option_region(self):
self.make_env() self.make_env()
@ -275,7 +275,7 @@ class ShellTest(utils.TestCase):
service_type='container', region_name=None, service_type='container', region_name=None,
project_domain_id='', project_domain_name='', project_domain_id='', project_domain_name='',
user_domain_id='', user_domain_name='', profile=None, 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') @mock.patch('zunclient.v1.client.Client')
def test_main_endpoint_internal(self, mock_client): def test_main_endpoint_internal(self, mock_client):
@ -288,7 +288,7 @@ class ShellTest(utils.TestCase):
service_type='container', region_name=None, service_type='container', region_name=None,
project_domain_id='', project_domain_name='', project_domain_id='', project_domain_name='',
user_domain_id='', user_domain_name='', profile=None, user_domain_id='', user_domain_name='', profile=None,
zun_url=None, insecure=False) zun_url=None, insecure=False, api_version='latest')
class ShellTestKeystoneV3(ShellTest): class ShellTestKeystoneV3(ShellTest):
@ -318,4 +318,5 @@ class ShellTestKeystoneV3(ShellTest):
service_type='container', region_name=None, service_type='container', region_name=None,
project_domain_id='', project_domain_name='Default', project_domain_id='', project_domain_name='Default',
user_domain_id='', user_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')

View File

@ -31,7 +31,8 @@ class ClientTest(testtools.TestCase):
region_name=None, region_name=None,
service_name=None, service_name=None,
service_type='container', service_type='container',
session=session) session=session,
api_version=None)
@mock.patch('zunclient.common.httpclient.SessionClient') @mock.patch('zunclient.common.httpclient.SessionClient')
@mock.patch('keystoneauth1.token_endpoint.Token') @mock.patch('keystoneauth1.token_endpoint.Token')
@ -51,7 +52,8 @@ class ClientTest(testtools.TestCase):
region_name=None, region_name=None,
service_name=None, service_name=None,
service_type='container', service_type='container',
session=session) session=session,
api_version=None)
@mock.patch('zunclient.common.httpclient.SessionClient') @mock.patch('zunclient.common.httpclient.SessionClient')
@mock.patch('keystoneauth1.loading.get_plugin_loader') @mock.patch('keystoneauth1.loading.get_plugin_loader')
@ -76,7 +78,8 @@ class ClientTest(testtools.TestCase):
region_name=None, region_name=None,
service_name=None, service_name=None,
service_type='container', service_type='container',
session=mock.ANY) session=mock.ANY,
api_version=None)
@mock.patch('zunclient.common.httpclient.SessionClient') @mock.patch('zunclient.common.httpclient.SessionClient')
@mock.patch('keystoneauth1.loading.get_plugin_loader') @mock.patch('keystoneauth1.loading.get_plugin_loader')
@ -102,7 +105,8 @@ class ClientTest(testtools.TestCase):
region_name=None, region_name=None,
service_name=None, service_name=None,
service_type='container', service_type='container',
session=mock.ANY) session=mock.ANY,
api_version=None)
@mock.patch('zunclient.common.httpclient.SessionClient') @mock.patch('zunclient.common.httpclient.SessionClient')
@mock.patch('keystoneauth1.loading.get_plugin_loader') @mock.patch('keystoneauth1.loading.get_plugin_loader')
@ -142,7 +146,8 @@ class ClientTest(testtools.TestCase):
service_name=None, service_name=None,
service_type='container', service_type='container',
session=session, session=session,
endpoint_override='zunurl') endpoint_override='zunurl',
api_version=None)
@mock.patch('zunclient.common.httpclient.SessionClient') @mock.patch('zunclient.common.httpclient.SessionClient')
@mock.patch('keystoneauth1.session.Session') @mock.patch('keystoneauth1.session.Session')
@ -158,4 +163,5 @@ class ClientTest(testtools.TestCase):
service_name=None, service_name=None,
service_type='container', service_type='container',
session=session, session=session,
endpoint_override='zunurl') endpoint_override='zunurl',
api_version=None)

View File

@ -35,7 +35,7 @@ class Client(object):
interface='public', service_name=None, insecure=False, interface='public', service_name=None, insecure=False,
user_domain_id=None, user_domain_name=None, user_domain_id=None, user_domain_name=None,
project_domain_id=None, project_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 # 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 # remove it from the rest of our code since it's not a keystone
@ -111,6 +111,7 @@ class Client(object):
interface=interface, interface=interface,
region_name=region_name, region_name=region_name,
session=session, session=session,
api_version=api_version,
**client_kwargs) **client_kwargs)
self.containers = containers.ContainerManager(self.http_client) self.containers = containers.ContainerManager(self.http_client)
self.images = images.ImageManager(self.http_client) self.images = images.ImageManager(self.http_client)