Merge "Implements 'microversions' api type - Part 2"
This commit is contained in:
commit
40a4070f28
@ -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:
|
||||
|
@ -2282,10 +2282,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)
|
||||
|
||||
|
||||
|
@ -374,3 +374,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