Extend microversion stuff to support resource classes
Current implementation of novaclient.api_versions.wraps allow to use versioned methods for shell functions and resource managers. As resource managers can have versioned methods, resource objects can have it too, but it was not implemented yet. This patch fixes this omission. Changes: - Add api_version property to base resource class. It is mapped to api_version property of manager class; - Move `novaclient.utils.generate_function_name` to `novaclient.api_versions._generate_function_name`, since this method is specific to microversion stuff and should not used outside api_versions module; - Rewrite _generate_function_name to handle class(owner) name. Previously, it was improssible to have two classes in one module with versioned methods with equal names. - Remove call of generate_function_name from novaclient.shell. Shell module should not take care about function identifiers. get_substitutions accepts object with __id__ property now. - Mark _add_substitution as private method, since it should not be used outside api_versions module - Split all versioned methods of Server resource from novaclient.v2.servers module. Change-Id: Icfce16bfa6f919d7f8451d592f4a8e276b1f1709
This commit is contained in:
parent
19ff19e1b8
commit
f9bdba2dd7
novaclient
@ -16,13 +16,13 @@ import logging
|
||||
import os
|
||||
import pkgutil
|
||||
import re
|
||||
import traceback
|
||||
|
||||
from oslo_utils import strutils
|
||||
|
||||
import novaclient
|
||||
from novaclient import exceptions
|
||||
from novaclient.i18n import _, _LW
|
||||
from novaclient import utils
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
if not LOG.handlers:
|
||||
@ -340,12 +340,32 @@ def check_headers(response, api_version):
|
||||
_warn_missing_microversion_header(HEADER_NAME)
|
||||
|
||||
|
||||
def add_substitution(versioned_method):
|
||||
def _add_substitution(versioned_method):
|
||||
_SUBSTITUTIONS.setdefault(versioned_method.name, [])
|
||||
_SUBSTITUTIONS[versioned_method.name].append(versioned_method)
|
||||
|
||||
|
||||
def _get_function_name(func):
|
||||
# NOTE(andreykurilin): Based on the facts:
|
||||
# - Python 2 does not have __qualname__ property as Python 3 has;
|
||||
# - we cannot use im_class here, since we need to obtain name of
|
||||
# function in `wraps` decorator during class initialization
|
||||
# ("im_class" property does not exist at that moment)
|
||||
# we need to write own logic to obtain the full function name which
|
||||
# include module name, owner name(optional) and just function name.
|
||||
filename, _lineno, _name, line = traceback.extract_stack()[-4]
|
||||
module, _file_extension = os.path.splitext(filename)
|
||||
module = module.replace("/", ".")
|
||||
if module.endswith(func.__module__):
|
||||
return "%s.[%s].%s" % (func.__module__, line, func.__name__)
|
||||
else:
|
||||
return "%s.%s" % (func.__module__, func.__name__)
|
||||
|
||||
|
||||
def get_substitutions(func_name, api_version=None):
|
||||
if hasattr(func_name, "__id__"):
|
||||
func_name = func_name.__id__
|
||||
|
||||
substitutions = _SUBSTITUTIONS.get(func_name, [])
|
||||
if api_version and not api_version.is_null():
|
||||
return [m for m in substitutions
|
||||
@ -362,10 +382,11 @@ def wraps(start_version, end_version=None):
|
||||
|
||||
def decor(func):
|
||||
func.versioned = True
|
||||
name = utils.get_function_name(func)
|
||||
name = _get_function_name(func)
|
||||
|
||||
versioned_method = VersionedMethod(name, start_version,
|
||||
end_version, func)
|
||||
add_substitution(versioned_method)
|
||||
_add_substitution(versioned_method)
|
||||
|
||||
@functools.wraps(func)
|
||||
def substitution(obj, *args, **kwargs):
|
||||
@ -374,7 +395,6 @@ def wraps(start_version, end_version=None):
|
||||
if not methods:
|
||||
raise exceptions.VersionNotFoundForAPIMethod(
|
||||
obj.api_version.get_string(), name)
|
||||
|
||||
return methods[-1].func(obj, *args, **kwargs)
|
||||
|
||||
# Let's share "arguments" with original method and substitution to
|
||||
@ -383,6 +403,13 @@ def wraps(start_version, end_version=None):
|
||||
func.arguments = []
|
||||
substitution.arguments = func.arguments
|
||||
|
||||
# NOTE(andreykurilin): The way to obtain function's name in Python 2
|
||||
# bases on traceback(see _get_function_name for details). Since the
|
||||
# right versioned method method is used in several places, one object
|
||||
# can have different names. Let's generate name of function one time
|
||||
# and use __id__ property in all other places.
|
||||
substitution.__id__ = name
|
||||
|
||||
return substitution
|
||||
|
||||
return decor
|
||||
|
@ -147,6 +147,10 @@ class Resource(RequestIdMixin):
|
||||
info = ", ".join("%s=%s" % (k, getattr(self, k)) for k in reprkeys)
|
||||
return "<%s %s>" % (self.__class__.__name__, info)
|
||||
|
||||
@property
|
||||
def api_version(self):
|
||||
return self.manager.api_version
|
||||
|
||||
@property
|
||||
def human_id(self):
|
||||
"""Human-readable ID which can be used for bash completion.
|
||||
|
@ -633,8 +633,7 @@ class OpenStackComputeShell(object):
|
||||
desc = callback.__doc__ or ''
|
||||
if hasattr(callback, "versioned"):
|
||||
additional_msg = ""
|
||||
subs = api_versions.get_substitutions(
|
||||
utils.get_function_name(callback))
|
||||
subs = api_versions.get_substitutions(callback)
|
||||
if do_help:
|
||||
additional_msg = msg % {
|
||||
'start': subs[0].start_version.get_string(),
|
||||
|
@ -233,11 +233,9 @@ class WrapsTestCase(utils.TestCase):
|
||||
m.name = args[0]
|
||||
return m
|
||||
|
||||
@mock.patch("novaclient.utils.get_function_name")
|
||||
@mock.patch("novaclient.api_versions._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")
|
||||
@ -247,15 +245,13 @@ class WrapsTestCase(utils.TestCase):
|
||||
foo(self._get_obj_with_vers("2.4"))
|
||||
|
||||
mock_versioned_method.assert_called_once_with(
|
||||
func_name, api_versions.APIVersion("2.2"),
|
||||
mock_name.return_value, api_versions.APIVersion("2.2"),
|
||||
api_versions.APIVersion("2.latest"), mock.ANY)
|
||||
|
||||
@mock.patch("novaclient.utils.get_function_name")
|
||||
@mock.patch("novaclient.api_versions._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")
|
||||
@ -265,14 +261,12 @@ class WrapsTestCase(utils.TestCase):
|
||||
foo(self._get_obj_with_vers("2.4"))
|
||||
|
||||
mock_versioned_method.assert_called_once_with(
|
||||
func_name, api_versions.APIVersion("2.2"),
|
||||
mock_name.return_value, api_versions.APIVersion("2.2"),
|
||||
api_versions.APIVersion("2.6"), mock.ANY)
|
||||
|
||||
@mock.patch("novaclient.utils.get_function_name")
|
||||
@mock.patch("novaclient.api_versions._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")
|
||||
@ -283,7 +277,7 @@ class WrapsTestCase(utils.TestCase):
|
||||
foo, self._get_obj_with_vers("2.1"))
|
||||
|
||||
mock_versioned_method.assert_called_once_with(
|
||||
func_name, api_versions.APIVersion("2.2"),
|
||||
mock_name.return_value, api_versions.APIVersion("2.2"),
|
||||
api_versions.APIVersion("2.6"), mock.ANY)
|
||||
|
||||
def test_define_method_is_actually_called(self):
|
||||
@ -301,7 +295,8 @@ class WrapsTestCase(utils.TestCase):
|
||||
|
||||
checker.assert_called_once_with(*((obj,) + some_args), **some_kwargs)
|
||||
|
||||
def test_arguments_property_is_copied(self):
|
||||
@mock.patch("novaclient.api_versions._get_function_name")
|
||||
def test_arguments_property_is_copied(self, mock_name):
|
||||
@nutils.arg("argument_1")
|
||||
@api_versions.wraps("2.666", "2.777")
|
||||
@nutils.arg("argument_2")
|
||||
@ -309,14 +304,44 @@ class WrapsTestCase(utils.TestCase):
|
||||
pass
|
||||
|
||||
versioned_method = api_versions.get_substitutions(
|
||||
nutils.get_function_name(some_func),
|
||||
api_versions.APIVersion("2.700"))[0]
|
||||
mock_name.return_value, api_versions.APIVersion("2.700"))[0]
|
||||
|
||||
self.assertEqual(some_func.arguments,
|
||||
versioned_method.func.arguments)
|
||||
self.assertIn((("argument_1",), {}), versioned_method.func.arguments)
|
||||
self.assertIn((("argument_2",), {}), versioned_method.func.arguments)
|
||||
|
||||
def test_several_methods_with_same_name_in_one_module(self):
|
||||
|
||||
class A(object):
|
||||
api_version = api_versions.APIVersion("777.777")
|
||||
|
||||
@api_versions.wraps("777.777")
|
||||
def f(self):
|
||||
return 1
|
||||
|
||||
class B(object):
|
||||
api_version = api_versions.APIVersion("777.777")
|
||||
|
||||
@api_versions.wraps("777.777")
|
||||
def f(self):
|
||||
return 2
|
||||
|
||||
self.assertEqual(1, A().f())
|
||||
self.assertEqual(2, B().f())
|
||||
|
||||
def test_generate_function_name(self):
|
||||
expected_name = "novaclient.tests.unit.test_api_versions.fake_func"
|
||||
|
||||
self.assertNotIn(expected_name, api_versions._SUBSTITUTIONS)
|
||||
|
||||
@api_versions.wraps("7777777.7777777")
|
||||
def fake_func():
|
||||
pass
|
||||
|
||||
self.assertIn(expected_name, api_versions._SUBSTITUTIONS)
|
||||
self.assertEqual(expected_name, fake_func.__id__)
|
||||
|
||||
|
||||
class DiscoverVersionTestCase(utils.TestCase):
|
||||
def setUp(self):
|
||||
|
@ -1144,11 +1144,6 @@ class ServersV225Test(ServersV219Test):
|
||||
{'os-migrateLive': {'host': 'hostname',
|
||||
'block_migration': 'auto'}})
|
||||
|
||||
def test_live_migrate_server_with_disk_over_commit(self):
|
||||
s = self.cs.servers.get(1234)
|
||||
self.assertRaises(ValueError, s.live_migrate, 'hostname',
|
||||
'auto', 'True')
|
||||
|
||||
|
||||
class ServersV226Test(ServersV225Test):
|
||||
def setUp(self):
|
||||
|
@ -465,13 +465,3 @@ 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__)
|
||||
|
@ -50,18 +50,25 @@ class Server(base.Resource):
|
||||
"""
|
||||
return self.manager.delete(self)
|
||||
|
||||
@api_versions.wraps("2.0", "2.18")
|
||||
def update(self, name=None):
|
||||
"""
|
||||
Update the name and the description for this server.
|
||||
|
||||
:param name: Update the server's name.
|
||||
:returns: :class:`Server`
|
||||
"""
|
||||
return self.manager.update(self, name=name)
|
||||
|
||||
@api_versions.wraps("2.19")
|
||||
def update(self, name=None, description=None):
|
||||
"""
|
||||
Update the name and the description for this server.
|
||||
|
||||
:param name: Update the server's name.
|
||||
:param description: Update the server's description(
|
||||
allowed for 2.19-latest).
|
||||
:param description: Update the server's description.
|
||||
:returns: :class:`Server`
|
||||
"""
|
||||
if (description is not None and
|
||||
self.manager.api_version < api_versions.APIVersion("2.19")):
|
||||
raise exceptions.UnsupportedAttribute("description", "2.19")
|
||||
update_kwargs = {"name": name}
|
||||
if description is not None:
|
||||
update_kwargs["description"] = description
|
||||
@ -403,43 +410,41 @@ class Server(base.Resource):
|
||||
except Exception:
|
||||
return {}
|
||||
|
||||
@api_versions.wraps("2.0", "2.24")
|
||||
def live_migrate(self, host=None,
|
||||
block_migration=None,
|
||||
block_migration=False,
|
||||
disk_over_commit=None):
|
||||
"""
|
||||
Migrates a running instance to a new machine.
|
||||
|
||||
:param host: destination host name.
|
||||
:param block_migration: if True, do block_migration, the default
|
||||
value None will be mapped to False for 2.0 -
|
||||
2.24, 'auto' for higher than 2.25
|
||||
value is False and None will be mapped to False
|
||||
:param disk_over_commit: if True, allow disk over commit, the default
|
||||
value None will be mapped to False. It will
|
||||
not be supported since 2.25
|
||||
value is None which is mapped to False
|
||||
:returns: An instance of novaclient.base.TupleWithMeta
|
||||
"""
|
||||
if block_migration is None:
|
||||
block_migration = False
|
||||
if disk_over_commit is None:
|
||||
disk_over_commit = False
|
||||
return self.manager.live_migrate(self, host,
|
||||
block_migration,
|
||||
disk_over_commit)
|
||||
|
||||
if (self.manager.api_version < api_versions.APIVersion("2.25")):
|
||||
# NOTE(eliqiao): We do this to keep old version api has same
|
||||
# default value if user don't pass these parameters when using
|
||||
# SDK
|
||||
if block_migration is None:
|
||||
block_migration = False
|
||||
if disk_over_commit is None:
|
||||
disk_over_commit = False
|
||||
@api_versions.wraps("2.25")
|
||||
def live_migrate(self, host=None, block_migration=None):
|
||||
"""
|
||||
Migrates a running instance to a new machine.
|
||||
|
||||
return self.manager.live_migrate(self, host,
|
||||
block_migration,
|
||||
disk_over_commit)
|
||||
else:
|
||||
if block_migration is None:
|
||||
block_migration = 'auto'
|
||||
if disk_over_commit is not None:
|
||||
raise ValueError("Setting 'disk_over_commit' argument is "
|
||||
"prohibited after microversion 2.25.")
|
||||
|
||||
return self.manager.live_migrate(self, host,
|
||||
block_migration)
|
||||
:param host: destination host name.
|
||||
:param block_migration: if True, do block_migration, the default
|
||||
value is None which is mapped to 'auto'.
|
||||
:returns: An instance of novaclient.base.TupleWithMeta
|
||||
"""
|
||||
if block_migration is None:
|
||||
block_migration = "auto"
|
||||
return self.manager.live_migrate(self, host, block_migration)
|
||||
|
||||
def reset_state(self, state='error'):
|
||||
"""
|
||||
@ -480,29 +485,31 @@ class Server(base.Resource):
|
||||
"""
|
||||
return self.manager.list_security_group(self)
|
||||
|
||||
def evacuate(self, host=None, on_shared_storage=None, password=None):
|
||||
@api_versions.wraps("2.0", "2.13")
|
||||
def evacuate(self, host=None, on_shared_storage=True, password=None):
|
||||
"""
|
||||
Evacuate an instance from failed host to specified host.
|
||||
|
||||
:param host: Name of the target host
|
||||
:param on_shared_storage: Specifies whether instance files located
|
||||
on shared storage. After microversion 2.14, this
|
||||
parameter must have its default value of None.
|
||||
on shared storage.
|
||||
:param password: string to set as admin password on the evacuated
|
||||
server.
|
||||
:returns: An instance of novaclient.base.TupleWithMeta
|
||||
"""
|
||||
if api_versions.APIVersion("2.14") <= self.manager.api_version:
|
||||
if on_shared_storage is not None:
|
||||
raise ValueError("Setting 'on_shared_storage' argument is "
|
||||
"prohibited after microversion 2.14.")
|
||||
return self.manager.evacuate(self, host, password)
|
||||
else:
|
||||
# microversions 2.0 - 2.13
|
||||
if on_shared_storage is None:
|
||||
on_shared_storage = True
|
||||
return self.manager.evacuate(self, host, on_shared_storage,
|
||||
password)
|
||||
return self.manager.evacuate(self, host, on_shared_storage, password)
|
||||
|
||||
@api_versions.wraps("2.14")
|
||||
def evacuate(self, host=None, password=None):
|
||||
"""
|
||||
Evacuate an instance from failed host to specified host.
|
||||
|
||||
:param host: Name of the target host
|
||||
:param password: string to set as admin password on the evacuated
|
||||
server.
|
||||
:returns: An instance of novaclient.base.TupleWithMeta
|
||||
"""
|
||||
return self.manager.evacuate(self, host, password)
|
||||
|
||||
def interface_list(self):
|
||||
"""
|
||||
@ -526,30 +533,35 @@ class Server(base.Resource):
|
||||
"""Trigger crash dump in an instance"""
|
||||
return self.manager.trigger_crash_dump(self)
|
||||
|
||||
@api_versions.wraps('2.26')
|
||||
def tag_list(self):
|
||||
"""
|
||||
Get list of tags from an instance.
|
||||
"""
|
||||
return self.manager.tag_list(self)
|
||||
|
||||
@api_versions.wraps('2.26')
|
||||
def delete_tag(self, tag):
|
||||
"""
|
||||
Remove single tag from an instance.
|
||||
"""
|
||||
return self.manager.delete_tag(self, tag)
|
||||
|
||||
@api_versions.wraps('2.26')
|
||||
def delete_all_tags(self):
|
||||
"""
|
||||
Remove all tags from an instance.
|
||||
"""
|
||||
return self.manager.delete_all_tags(self)
|
||||
|
||||
@api_versions.wraps('2.26')
|
||||
def set_tags(self, tags):
|
||||
"""
|
||||
Set list of tags to an instance.
|
||||
"""
|
||||
return self.manager.set_tags(self, tags)
|
||||
|
||||
@api_versions.wraps('2.26')
|
||||
def add_tag(self, tag):
|
||||
"""
|
||||
Add single tag to an instance.
|
||||
|
Loading…
x
Reference in New Issue
Block a user