Merge "Implements 'microversions' api type - Part 2"

This commit is contained in:
Jenkins 2015-07-27 23:13:36 +00:00 committed by Gerrit Code Review
commit 40a4070f28
9 changed files with 367 additions and 10 deletions

View File

@ -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

View File

@ -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)

View File

@ -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.

View File

@ -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:

View 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

View File

@ -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)

View File

@ -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:

View File

@ -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)

View File

@ -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__)