Implements 'microversions' api type - Part 2
New decorator "novaclient.api_versions.wraps" replaces original method with substitution. This substitution searches for methods which desire specified api version. Also, this patch updates novaclient shell to discover versioned methods and arguments. Related to bp api-microversion-support Co-Authored-By: Alex Xu <hejie.xu@intel.com> Change-Id: I1939c19664e58e2def684380d64c465dc1cfc132
This commit is contained in:
parent
169b8a08ce
commit
936cf572df
@ -11,6 +11,7 @@
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
import functools
|
||||
import logging
|
||||
import os
|
||||
import pkgutil
|
||||
@ -20,6 +21,7 @@ from oslo_utils import strutils
|
||||
|
||||
from novaclient import exceptions
|
||||
from novaclient.i18n import _, _LW
|
||||
from novaclient import utils
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
if not LOG.handlers:
|
||||
@ -29,6 +31,7 @@ if not LOG.handlers:
|
||||
# key is a deprecated version and value is an alternative version.
|
||||
DEPRECATED_VERSIONS = {"1.1": "2"}
|
||||
|
||||
_SUBSTITUTIONS = {}
|
||||
|
||||
_type_error_msg = _("'%(other)s' should be an instance of '%(cls)s'")
|
||||
|
||||
@ -150,6 +153,31 @@ class APIVersion(object):
|
||||
return "%s.%s" % (self.ver_major, self.ver_minor)
|
||||
|
||||
|
||||
class VersionedMethod(object):
|
||||
|
||||
def __init__(self, name, start_version, end_version, func):
|
||||
"""Versioning information for a single method
|
||||
|
||||
:param name: Name of the method
|
||||
:param start_version: Minimum acceptable version
|
||||
:param end_version: Maximum acceptable_version
|
||||
:param func: Method to call
|
||||
|
||||
Minimum and maximums are inclusive
|
||||
"""
|
||||
self.name = name
|
||||
self.start_version = start_version
|
||||
self.end_version = end_version
|
||||
self.func = func
|
||||
|
||||
def __str__(self):
|
||||
return ("Version Method %s: min: %s, max: %s"
|
||||
% (self.name, self.start_version, self.end_version))
|
||||
|
||||
def __repr__(self):
|
||||
return "<VersionedMethod %s>" % self.name
|
||||
|
||||
|
||||
def get_available_major_versions():
|
||||
# NOTE(andreykurilin): available clients version should not be
|
||||
# hardcoded, so let's discover them.
|
||||
@ -206,3 +234,44 @@ def update_headers(headers, api_version):
|
||||
|
||||
if not api_version.is_null() and api_version.ver_minor != 0:
|
||||
headers["X-OpenStack-Nova-API-Version"] = api_version.get_string()
|
||||
|
||||
|
||||
def add_substitution(versioned_method):
|
||||
_SUBSTITUTIONS.setdefault(versioned_method.name, [])
|
||||
_SUBSTITUTIONS[versioned_method.name].append(versioned_method)
|
||||
|
||||
|
||||
def get_substitutions(func_name, api_version=None):
|
||||
substitutions = _SUBSTITUTIONS.get(func_name, [])
|
||||
if api_version and not api_version.is_null():
|
||||
return [m for m in substitutions
|
||||
if api_version.matches(m.start_version, m.end_version)]
|
||||
return substitutions
|
||||
|
||||
|
||||
def wraps(start_version, end_version=None):
|
||||
start_version = APIVersion(start_version)
|
||||
if end_version:
|
||||
end_version = APIVersion(end_version)
|
||||
else:
|
||||
end_version = APIVersion("%s.latest" % start_version.ver_major)
|
||||
|
||||
def decor(func):
|
||||
func.versioned = True
|
||||
name = utils.get_function_name(func)
|
||||
versioned_method = VersionedMethod(name, start_version,
|
||||
end_version, func)
|
||||
add_substitution(versioned_method)
|
||||
|
||||
@functools.wraps(func)
|
||||
def substitution(obj, *args, **kwargs):
|
||||
methods = get_substitutions(name, obj.api_version)
|
||||
|
||||
if not methods:
|
||||
raise exceptions.VersionNotFoundForAPIMethod(
|
||||
obj.api_version.get_string(), name)
|
||||
else:
|
||||
return max(methods, key=lambda f: f.start_version).func(
|
||||
obj, *args, **kwargs)
|
||||
return substitution
|
||||
return decor
|
||||
|
@ -61,6 +61,10 @@ class Manager(base.HookableMixin):
|
||||
def client(self):
|
||||
return self.api.client
|
||||
|
||||
@property
|
||||
def api_version(self):
|
||||
return self.api.api_version
|
||||
|
||||
def _list(self, url, response_key, obj_class=None, body=None):
|
||||
if body:
|
||||
_resp, body = self.api.client.post(url, body=body)
|
||||
|
@ -82,6 +82,17 @@ class InstanceInErrorState(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class VersionNotFoundForAPIMethod(Exception):
|
||||
msg_fmt = "API version '%(vers)s' is not supported on '%(method)s' method."
|
||||
|
||||
def __init__(self, version, method):
|
||||
self.version = version
|
||||
self.method = method
|
||||
|
||||
def __str__(self):
|
||||
return self.msg_fmt % {"vers": self.version, "method": self.method}
|
||||
|
||||
|
||||
class ClientException(Exception):
|
||||
"""
|
||||
The base exception class for all exceptions this library raises.
|
||||
|
@ -426,7 +426,7 @@ class OpenStackComputeShell(object):
|
||||
|
||||
return parser
|
||||
|
||||
def get_subcommand_parser(self, version):
|
||||
def get_subcommand_parser(self, version, do_help=False):
|
||||
parser = self.get_base_parser()
|
||||
|
||||
self.subcommands = {}
|
||||
@ -435,12 +435,11 @@ class OpenStackComputeShell(object):
|
||||
actions_module = importutils.import_module(
|
||||
"novaclient.v%s.shell" % version.ver_major)
|
||||
|
||||
# TODO(andreykurilin): discover actions based on microversions
|
||||
self._find_actions(subparsers, actions_module)
|
||||
self._find_actions(subparsers, self)
|
||||
self._find_actions(subparsers, actions_module, version, do_help)
|
||||
self._find_actions(subparsers, self, version, do_help)
|
||||
|
||||
for extension in self.extensions:
|
||||
self._find_actions(subparsers, extension.module)
|
||||
self._find_actions(subparsers, extension.module, version, do_help)
|
||||
|
||||
self._add_bash_completion_subparser(subparsers)
|
||||
|
||||
@ -460,12 +459,28 @@ class OpenStackComputeShell(object):
|
||||
self.subcommands['bash_completion'] = subparser
|
||||
subparser.set_defaults(func=self.do_bash_completion)
|
||||
|
||||
def _find_actions(self, subparsers, actions_module):
|
||||
def _find_actions(self, subparsers, actions_module, version, do_help):
|
||||
msg = _(" (Supported by API versions '%(start)s' - '%(end)s')")
|
||||
for attr in (a for a in dir(actions_module) if a.startswith('do_')):
|
||||
# I prefer to be hyphen-separated instead of underscores.
|
||||
command = attr[3:].replace('_', '-')
|
||||
callback = getattr(actions_module, attr)
|
||||
desc = callback.__doc__ or ''
|
||||
if hasattr(callback, "versioned"):
|
||||
subs = api_versions.get_substitutions(
|
||||
utils.get_function_name(callback))
|
||||
if do_help:
|
||||
desc += msg % {'start': subs[0].start_version.get_string(),
|
||||
'end': subs[-1].end_version.get_string()}
|
||||
else:
|
||||
for versioned_method in subs:
|
||||
if version.matches(versioned_method.start_version,
|
||||
versioned_method.end_version):
|
||||
callback = versioned_method.func
|
||||
break
|
||||
else:
|
||||
continue
|
||||
|
||||
action_help = desc.strip()
|
||||
arguments = getattr(callback, 'arguments', [])
|
||||
|
||||
@ -482,7 +497,26 @@ class OpenStackComputeShell(object):
|
||||
)
|
||||
self.subcommands[command] = subparser
|
||||
for (args, kwargs) in arguments:
|
||||
subparser.add_argument(*args, **kwargs)
|
||||
start_version = kwargs.get("start_version", None)
|
||||
if start_version:
|
||||
start_version = api_versions.APIVersion(start_version)
|
||||
end_version = kwargs.get("end_version", None)
|
||||
if end_version:
|
||||
end_version = api_versions.APIVersion(end_version)
|
||||
else:
|
||||
end_version = api_versions.APIVersion(
|
||||
"%s.latest" % start_version.ver_major)
|
||||
if do_help:
|
||||
kwargs["help"] = kwargs.get("help", "") + (msg % {
|
||||
"start": start_version.get_string(),
|
||||
"end": end_version.get_string()})
|
||||
else:
|
||||
if not version.matches(start_version, end_version):
|
||||
continue
|
||||
kw = kwargs.copy()
|
||||
kw.pop("start_version", None)
|
||||
kw.pop("end_version", None)
|
||||
subparser.add_argument(*args, **kw)
|
||||
subparser.set_defaults(func=callback)
|
||||
|
||||
def setup_debugging(self, debug):
|
||||
@ -534,7 +568,8 @@ class OpenStackComputeShell(object):
|
||||
spot = argv.index('--endpoint_type')
|
||||
argv[spot] = '--endpoint-type'
|
||||
|
||||
subcommand_parser = self.get_subcommand_parser(api_version)
|
||||
subcommand_parser = self.get_subcommand_parser(
|
||||
api_version, do_help=("help" in args))
|
||||
self.parser = subcommand_parser
|
||||
|
||||
if options.help or not argv:
|
||||
|
39
novaclient/tests/unit/fake_actions_module.py
Normal file
39
novaclient/tests/unit/fake_actions_module.py
Normal file
@ -0,0 +1,39 @@
|
||||
# Copyright 2011 OpenStack Foundation
|
||||
# All Rights Reserved.
|
||||
#
|
||||
# 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.
|
||||
|
||||
from novaclient import api_versions
|
||||
from novaclient.openstack.common import cliutils
|
||||
|
||||
|
||||
@api_versions.wraps("2.10", "2.20")
|
||||
def do_fake_action():
|
||||
return 1
|
||||
|
||||
|
||||
@api_versions.wraps("2.21", "2.30")
|
||||
def do_fake_action():
|
||||
return 2
|
||||
|
||||
|
||||
@cliutils.arg(
|
||||
'--foo',
|
||||
start_version='2.1',
|
||||
end_version='2.2')
|
||||
@cliutils.arg(
|
||||
'--bar',
|
||||
start_version='2.3',
|
||||
end_version='2.4')
|
||||
def do_fake_action2():
|
||||
return 3
|
@ -168,3 +168,82 @@ class GetAPIVersionTestCase(utils.TestCase):
|
||||
self.assertEqual(mock_apiversion.return_value,
|
||||
api_versions.get_api_version(version))
|
||||
mock_apiversion.assert_called_once_with(version)
|
||||
|
||||
|
||||
class WrapsTestCase(utils.TestCase):
|
||||
|
||||
def _get_obj_with_vers(self, vers):
|
||||
return mock.MagicMock(api_version=api_versions.APIVersion(vers))
|
||||
|
||||
def _side_effect_of_vers_method(self, *args, **kwargs):
|
||||
m = mock.MagicMock(start_version=args[1], end_version=args[2])
|
||||
m.name = args[0]
|
||||
return m
|
||||
|
||||
@mock.patch("novaclient.utils.get_function_name")
|
||||
@mock.patch("novaclient.api_versions.VersionedMethod")
|
||||
def test_end_version_is_none(self, mock_versioned_method, mock_name):
|
||||
func_name = "foo"
|
||||
mock_name.return_value = func_name
|
||||
mock_versioned_method.side_effect = self._side_effect_of_vers_method
|
||||
|
||||
@api_versions.wraps("2.2")
|
||||
def foo(*args, **kwargs):
|
||||
pass
|
||||
|
||||
foo(self._get_obj_with_vers("2.4"))
|
||||
|
||||
mock_versioned_method.assert_called_once_with(
|
||||
func_name, api_versions.APIVersion("2.2"),
|
||||
api_versions.APIVersion("2.latest"), mock.ANY)
|
||||
|
||||
@mock.patch("novaclient.utils.get_function_name")
|
||||
@mock.patch("novaclient.api_versions.VersionedMethod")
|
||||
def test_start_and_end_version_are_presented(self, mock_versioned_method,
|
||||
mock_name):
|
||||
func_name = "foo"
|
||||
mock_name.return_value = func_name
|
||||
mock_versioned_method.side_effect = self._side_effect_of_vers_method
|
||||
|
||||
@api_versions.wraps("2.2", "2.6")
|
||||
def foo(*args, **kwargs):
|
||||
pass
|
||||
|
||||
foo(self._get_obj_with_vers("2.4"))
|
||||
|
||||
mock_versioned_method.assert_called_once_with(
|
||||
func_name, api_versions.APIVersion("2.2"),
|
||||
api_versions.APIVersion("2.6"), mock.ANY)
|
||||
|
||||
@mock.patch("novaclient.utils.get_function_name")
|
||||
@mock.patch("novaclient.api_versions.VersionedMethod")
|
||||
def test_api_version_doesnt_match(self, mock_versioned_method, mock_name):
|
||||
func_name = "foo"
|
||||
mock_name.return_value = func_name
|
||||
mock_versioned_method.side_effect = self._side_effect_of_vers_method
|
||||
|
||||
@api_versions.wraps("2.2", "2.6")
|
||||
def foo(*args, **kwargs):
|
||||
pass
|
||||
|
||||
self.assertRaises(exceptions.VersionNotFoundForAPIMethod,
|
||||
foo, self._get_obj_with_vers("2.1"))
|
||||
|
||||
mock_versioned_method.assert_called_once_with(
|
||||
func_name, api_versions.APIVersion("2.2"),
|
||||
api_versions.APIVersion("2.6"), mock.ANY)
|
||||
|
||||
def test_define_method_is_actually_called(self):
|
||||
checker = mock.MagicMock()
|
||||
|
||||
@api_versions.wraps("2.2", "2.6")
|
||||
def some_func(*args, **kwargs):
|
||||
checker(*args, **kwargs)
|
||||
|
||||
obj = self._get_obj_with_vers("2.4")
|
||||
some_args = ("arg_1", "arg_2")
|
||||
some_kwargs = {"key1": "value1", "key2": "value2"}
|
||||
|
||||
some_func(obj, *some_args, **some_kwargs)
|
||||
|
||||
checker.assert_called_once_with(*((obj,) + some_args), **some_kwargs)
|
||||
|
@ -23,9 +23,11 @@ import requests_mock
|
||||
import six
|
||||
from testtools import matchers
|
||||
|
||||
from novaclient import api_versions
|
||||
import novaclient.client
|
||||
from novaclient import exceptions
|
||||
import novaclient.shell
|
||||
from novaclient.tests.unit import fake_actions_module
|
||||
from novaclient.tests.unit import utils
|
||||
|
||||
FAKE_ENV = {'OS_USERNAME': 'username',
|
||||
@ -402,6 +404,113 @@ class ShellTest(utils.TestCase):
|
||||
self.assertIsInstance(keyring_saver, novaclient.shell.SecretsHelper)
|
||||
|
||||
|
||||
class TestLoadVersionedActions(utils.TestCase):
|
||||
|
||||
def test_load_versioned_actions(self):
|
||||
parser = novaclient.shell.NovaClientArgumentParser()
|
||||
subparsers = parser.add_subparsers(metavar='<subcommand>')
|
||||
shell = novaclient.shell.OpenStackComputeShell()
|
||||
shell.subcommands = {}
|
||||
shell._find_actions(subparsers, fake_actions_module,
|
||||
api_versions.APIVersion("2.15"), False)
|
||||
self.assertIn('fake-action', shell.subcommands.keys())
|
||||
self.assertEqual(
|
||||
1, shell.subcommands['fake-action'].get_default('func')())
|
||||
|
||||
shell.subcommands = {}
|
||||
shell._find_actions(subparsers, fake_actions_module,
|
||||
api_versions.APIVersion("2.25"), False)
|
||||
self.assertIn('fake-action', shell.subcommands.keys())
|
||||
self.assertEqual(
|
||||
2, shell.subcommands['fake-action'].get_default('func')())
|
||||
|
||||
self.assertIn('fake-action2', shell.subcommands.keys())
|
||||
self.assertEqual(
|
||||
3, shell.subcommands['fake-action2'].get_default('func')())
|
||||
|
||||
def test_load_versioned_actions_not_in_version_range(self):
|
||||
parser = novaclient.shell.NovaClientArgumentParser()
|
||||
subparsers = parser.add_subparsers(metavar='<subcommand>')
|
||||
shell = novaclient.shell.OpenStackComputeShell()
|
||||
shell.subcommands = {}
|
||||
shell._find_actions(subparsers, fake_actions_module,
|
||||
api_versions.APIVersion("2.10000"), False)
|
||||
self.assertNotIn('fake-action', shell.subcommands.keys())
|
||||
self.assertIn('fake-action2', shell.subcommands.keys())
|
||||
|
||||
def test_load_versioned_actions_with_help(self):
|
||||
parser = novaclient.shell.NovaClientArgumentParser()
|
||||
subparsers = parser.add_subparsers(metavar='<subcommand>')
|
||||
shell = novaclient.shell.OpenStackComputeShell()
|
||||
shell.subcommands = {}
|
||||
shell._find_actions(subparsers, fake_actions_module,
|
||||
api_versions.APIVersion("2.10000"), True)
|
||||
self.assertIn('fake-action', shell.subcommands.keys())
|
||||
expected_desc = ("(Supported by API versions '%(start)s' - "
|
||||
"'%(end)s')") % {'start': '2.10', 'end': '2.30'}
|
||||
self.assertIn(expected_desc,
|
||||
shell.subcommands['fake-action'].description)
|
||||
|
||||
@mock.patch.object(novaclient.shell.NovaClientArgumentParser,
|
||||
'add_argument')
|
||||
def test_load_versioned_actions_with_args(self, mock_add_arg):
|
||||
parser = novaclient.shell.NovaClientArgumentParser(add_help=False)
|
||||
subparsers = parser.add_subparsers(metavar='<subcommand>')
|
||||
shell = novaclient.shell.OpenStackComputeShell()
|
||||
shell.subcommands = {}
|
||||
shell._find_actions(subparsers, fake_actions_module,
|
||||
api_versions.APIVersion("2.1"), False)
|
||||
self.assertIn('fake-action2', shell.subcommands.keys())
|
||||
mock_add_arg.assert_has_calls([
|
||||
mock.call('-h', '--help', action='help', help='==SUPPRESS=='),
|
||||
mock.call('--foo')])
|
||||
|
||||
@mock.patch.object(novaclient.shell.NovaClientArgumentParser,
|
||||
'add_argument')
|
||||
def test_load_versioned_actions_with_args2(self, mock_add_arg):
|
||||
parser = novaclient.shell.NovaClientArgumentParser(add_help=False)
|
||||
subparsers = parser.add_subparsers(metavar='<subcommand>')
|
||||
shell = novaclient.shell.OpenStackComputeShell()
|
||||
shell.subcommands = {}
|
||||
shell._find_actions(subparsers, fake_actions_module,
|
||||
api_versions.APIVersion("2.4"), False)
|
||||
self.assertIn('fake-action2', shell.subcommands.keys())
|
||||
mock_add_arg.assert_has_calls([
|
||||
mock.call('-h', '--help', action='help', help='==SUPPRESS=='),
|
||||
mock.call('--bar')])
|
||||
|
||||
@mock.patch.object(novaclient.shell.NovaClientArgumentParser,
|
||||
'add_argument')
|
||||
def test_load_versioned_actions_with_args_not_in_version_range(
|
||||
self, mock_add_arg):
|
||||
parser = novaclient.shell.NovaClientArgumentParser(add_help=False)
|
||||
subparsers = parser.add_subparsers(metavar='<subcommand>')
|
||||
shell = novaclient.shell.OpenStackComputeShell()
|
||||
shell.subcommands = {}
|
||||
shell._find_actions(subparsers, fake_actions_module,
|
||||
api_versions.APIVersion("2.10000"), False)
|
||||
self.assertIn('fake-action2', shell.subcommands.keys())
|
||||
mock_add_arg.assert_has_calls([
|
||||
mock.call('-h', '--help', action='help', help='==SUPPRESS==')])
|
||||
|
||||
@mock.patch.object(novaclient.shell.NovaClientArgumentParser,
|
||||
'add_argument')
|
||||
def test_load_versioned_actions_with_args_and_help(self, mock_add_arg):
|
||||
parser = novaclient.shell.NovaClientArgumentParser(add_help=False)
|
||||
subparsers = parser.add_subparsers(metavar='<subcommand>')
|
||||
shell = novaclient.shell.OpenStackComputeShell()
|
||||
shell.subcommands = {}
|
||||
shell._find_actions(subparsers, fake_actions_module,
|
||||
api_versions.APIVersion("2.4"), True)
|
||||
mock_add_arg.assert_has_calls([
|
||||
mock.call('-h', '--help', action='help', help='==SUPPRESS=='),
|
||||
mock.call('-h', '--help', action='help', help='==SUPPRESS=='),
|
||||
mock.call('--foo',
|
||||
help=" (Supported by API versions '2.1' - '2.2')"),
|
||||
mock.call('--bar',
|
||||
help=" (Supported by API versions '2.3' - '2.4')")])
|
||||
|
||||
|
||||
class ShellTestKeystoneV3(ShellTest):
|
||||
def make_env(self, exclude=None, fake_env=FAKE_ENV):
|
||||
if 'OS_AUTH_URL' in fake_env:
|
||||
|
@ -2232,10 +2232,11 @@ class FakeHTTPClient(base_client.HTTPClient):
|
||||
|
||||
class FakeSessionClient(fakes.FakeClient, client.Client):
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
def __init__(self, api_version, *args, **kwargs):
|
||||
client.Client.__init__(self, 'username', 'password',
|
||||
'project_id', 'auth_url',
|
||||
extensions=kwargs.get('extensions'))
|
||||
extensions=kwargs.get('extensions'),
|
||||
api_version=api_version)
|
||||
self.client = FakeSessionMockClient(**kwargs)
|
||||
|
||||
|
||||
|
@ -368,3 +368,13 @@ def record_time(times, enabled, *args):
|
||||
yield
|
||||
end = time.time()
|
||||
times.append((' '.join(args), start, end))
|
||||
|
||||
|
||||
def get_function_name(func):
|
||||
if six.PY2:
|
||||
if hasattr(func, "im_class"):
|
||||
return "%s.%s" % (func.im_class, func.__name__)
|
||||
else:
|
||||
return "%s.%s" % (func.__module__, func.__name__)
|
||||
else:
|
||||
return "%s.%s" % (func.__module__, func.__qualname__)
|
||||
|
Loading…
x
Reference in New Issue
Block a user