Remove hooks
This extension point was deprecated in 13.0.0 (Mitaka) as it was unmaintainable. Now, over four years later, it's finally time to remove them. A combination of notifications, versioned or otherwise, and dynamic vendordata should be used instead. Change-Id: Idb9a0c06d8abdb158bcee5be12c35dcb67257e60 Signed-off-by: Stephen Finucane <stephenfin@redhat.com>
This commit is contained in:
parent
45a88f08b4
commit
b72980960e
@ -58,7 +58,6 @@ from nova.db import base
|
|||||||
from nova.db.sqlalchemy import api as db_api
|
from nova.db.sqlalchemy import api as db_api
|
||||||
from nova import exception
|
from nova import exception
|
||||||
from nova import exception_wrapper
|
from nova import exception_wrapper
|
||||||
from nova import hooks
|
|
||||||
from nova.i18n import _
|
from nova.i18n import _
|
||||||
from nova.image import glance
|
from nova.image import glance
|
||||||
from nova.network import constants
|
from nova.network import constants
|
||||||
@ -1927,7 +1926,6 @@ class API(base.Base):
|
|||||||
"is specified.")
|
"is specified.")
|
||||||
raise exception.InvalidFixedIpAndMaxCountRequest(reason=msg)
|
raise exception.InvalidFixedIpAndMaxCountRequest(reason=msg)
|
||||||
|
|
||||||
@hooks.add_hook("create_instance")
|
|
||||||
def create(self, context, instance_type,
|
def create(self, context, instance_type,
|
||||||
image_href, kernel_id=None, ramdisk_id=None,
|
image_href, kernel_id=None, ramdisk_id=None,
|
||||||
min_count=None, max_count=None,
|
min_count=None, max_count=None,
|
||||||
|
@ -73,7 +73,6 @@ import nova.conf
|
|||||||
import nova.context
|
import nova.context
|
||||||
from nova import exception
|
from nova import exception
|
||||||
from nova import exception_wrapper
|
from nova import exception_wrapper
|
||||||
from nova import hooks
|
|
||||||
from nova.i18n import _
|
from nova.i18n import _
|
||||||
from nova.image import glance
|
from nova.image import glance
|
||||||
from nova import manager
|
from nova import manager
|
||||||
@ -2159,7 +2158,6 @@ class ComputeManager(manager.Manager):
|
|||||||
'Trusted image certificates provided on host that does not '
|
'Trusted image certificates provided on host that does not '
|
||||||
'support certificate validation.')
|
'support certificate validation.')
|
||||||
|
|
||||||
@hooks.add_hook('build_instance')
|
|
||||||
@wrap_exception()
|
@wrap_exception()
|
||||||
@reverts_task_state
|
@reverts_task_state
|
||||||
@wrap_instance_event(prefix='compute')
|
@wrap_instance_event(prefix='compute')
|
||||||
@ -2949,7 +2947,6 @@ class ComputeManager(manager.Manager):
|
|||||||
if exc_info is not None and raise_exc:
|
if exc_info is not None and raise_exc:
|
||||||
six.reraise(exc_info[0], exc_info[1], exc_info[2])
|
six.reraise(exc_info[0], exc_info[1], exc_info[2])
|
||||||
|
|
||||||
@hooks.add_hook("delete_instance")
|
|
||||||
def _delete_instance(self, context, instance, bdms):
|
def _delete_instance(self, context, instance, bdms):
|
||||||
"""Delete an instance on this host.
|
"""Delete an instance on this host.
|
||||||
|
|
||||||
|
167
nova/hooks.py
167
nova/hooks.py
@ -1,167 +0,0 @@
|
|||||||
# Copyright (c) 2012 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.
|
|
||||||
|
|
||||||
"""Decorator and config option definitions for adding custom code (hooks)
|
|
||||||
around callables.
|
|
||||||
|
|
||||||
NOTE: as of Nova 13.0 hooks are DEPRECATED and will be removed in the
|
|
||||||
near future. You should not build any new code using this facility.
|
|
||||||
|
|
||||||
Any method may have the 'add_hook' decorator applied, which yields the
|
|
||||||
ability to invoke Hook objects before or after the method. (i.e. pre and
|
|
||||||
post)
|
|
||||||
|
|
||||||
Hook objects are loaded by HookLoaders. Each named hook may invoke multiple
|
|
||||||
Hooks.
|
|
||||||
|
|
||||||
Example Hook object::
|
|
||||||
|
|
||||||
| class MyHook(object):
|
|
||||||
| def pre(self, *args, **kwargs):
|
|
||||||
| # do stuff before wrapped callable runs
|
|
||||||
|
|
|
||||||
| def post(self, rv, *args, **kwargs):
|
|
||||||
| # do stuff after wrapped callable runs
|
|
||||||
|
|
||||||
Example Hook object with function parameters::
|
|
||||||
|
|
||||||
| class MyHookWithFunction(object):
|
|
||||||
| def pre(self, f, *args, **kwargs):
|
|
||||||
| # do stuff with wrapped function info
|
|
||||||
| def post(self, f, *args, **kwargs):
|
|
||||||
| # do stuff with wrapped function info
|
|
||||||
|
|
||||||
"""
|
|
||||||
|
|
||||||
import functools
|
|
||||||
|
|
||||||
from oslo_log import log as logging
|
|
||||||
import stevedore
|
|
||||||
|
|
||||||
from nova.i18n import _
|
|
||||||
|
|
||||||
LOG = logging.getLogger(__name__)
|
|
||||||
NS = 'nova.hooks'
|
|
||||||
|
|
||||||
_HOOKS = {} # hook name => hook manager
|
|
||||||
|
|
||||||
|
|
||||||
class FatalHookException(Exception):
|
|
||||||
"""Exception which should be raised by hooks to indicate that normal
|
|
||||||
execution of the hooked function should be terminated. Raised exception
|
|
||||||
will be logged and reraised.
|
|
||||||
"""
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class HookManager(stevedore.hook.HookManager):
|
|
||||||
def __init__(self, name):
|
|
||||||
"""Invoke_on_load creates an instance of the Hook class
|
|
||||||
|
|
||||||
:param name: The name of the hooks to load.
|
|
||||||
:type name: str
|
|
||||||
"""
|
|
||||||
super(HookManager, self).__init__(NS, name, invoke_on_load=True)
|
|
||||||
|
|
||||||
def _run(self, name, method_type, args, kwargs, func=None):
|
|
||||||
if method_type not in ('pre', 'post'):
|
|
||||||
msg = _("Wrong type of hook method. "
|
|
||||||
"Only 'pre' and 'post' type allowed")
|
|
||||||
raise ValueError(msg)
|
|
||||||
|
|
||||||
# TODO(stephenfin): Kill this
|
|
||||||
for e in self.extensions:
|
|
||||||
obj = e.obj
|
|
||||||
hook_method = getattr(obj, method_type, None)
|
|
||||||
if hook_method:
|
|
||||||
LOG.warning("Hooks are deprecated as of Nova 13.0 and "
|
|
||||||
"will be removed in a future release")
|
|
||||||
LOG.debug("Running %(name)s %(type)s-hook: %(obj)s",
|
|
||||||
{'name': name, 'type': method_type, 'obj': obj})
|
|
||||||
try:
|
|
||||||
if func:
|
|
||||||
hook_method(func, *args, **kwargs)
|
|
||||||
else:
|
|
||||||
hook_method(*args, **kwargs)
|
|
||||||
except FatalHookException:
|
|
||||||
msg = (
|
|
||||||
"Fatal Exception running %(name)s %(type)s-hook: "
|
|
||||||
"%(obj)s"
|
|
||||||
)
|
|
||||||
LOG.exception(msg, {'name': name, 'type': method_type,
|
|
||||||
'obj': obj})
|
|
||||||
raise
|
|
||||||
except Exception:
|
|
||||||
msg = "Exception running %(name)s %(type)s-hook: %(obj)s"
|
|
||||||
LOG.exception(msg, {'name': name, 'type': method_type,
|
|
||||||
'obj': obj})
|
|
||||||
|
|
||||||
def run_pre(self, name, args, kwargs, f=None):
|
|
||||||
"""Execute optional pre methods of loaded hooks.
|
|
||||||
|
|
||||||
:param name: The name of the loaded hooks.
|
|
||||||
:param args: Positional arguments which would be transmitted into
|
|
||||||
all pre methods of loaded hooks.
|
|
||||||
:param kwargs: Keyword args which would be transmitted into all pre
|
|
||||||
methods of loaded hooks.
|
|
||||||
:param f: Target function.
|
|
||||||
"""
|
|
||||||
self._run(name=name, method_type='pre', args=args, kwargs=kwargs,
|
|
||||||
func=f)
|
|
||||||
|
|
||||||
def run_post(self, name, rv, args, kwargs, f=None):
|
|
||||||
"""Execute optional post methods of loaded hooks.
|
|
||||||
|
|
||||||
:param name: The name of the loaded hooks.
|
|
||||||
:param rv: Return values of target method call.
|
|
||||||
:param args: Positional arguments which would be transmitted into
|
|
||||||
all post methods of loaded hooks.
|
|
||||||
:param kwargs: Keyword args which would be transmitted into all post
|
|
||||||
methods of loaded hooks.
|
|
||||||
:param f: Target function.
|
|
||||||
"""
|
|
||||||
self._run(name=name, method_type='post', args=(rv,) + args,
|
|
||||||
kwargs=kwargs, func=f)
|
|
||||||
|
|
||||||
|
|
||||||
def add_hook(name, pass_function=False):
|
|
||||||
"""Execute optional pre and post methods around the decorated
|
|
||||||
function. This is useful for customization around callables.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def outer(f):
|
|
||||||
f.__hook_name__ = name
|
|
||||||
|
|
||||||
@functools.wraps(f)
|
|
||||||
def inner(*args, **kwargs):
|
|
||||||
manager = _HOOKS.setdefault(name, HookManager(name))
|
|
||||||
|
|
||||||
function = None
|
|
||||||
if pass_function:
|
|
||||||
function = f
|
|
||||||
|
|
||||||
manager.run_pre(name, args, kwargs, f=function)
|
|
||||||
rv = f(*args, **kwargs)
|
|
||||||
manager.run_post(name, rv, args, kwargs, f=function)
|
|
||||||
|
|
||||||
return rv
|
|
||||||
|
|
||||||
return inner
|
|
||||||
return outer
|
|
||||||
|
|
||||||
|
|
||||||
def reset():
|
|
||||||
"""Clear loaded hooks."""
|
|
||||||
_HOOKS.clear()
|
|
@ -37,7 +37,6 @@ import nova.conf
|
|||||||
from nova import context as nova_context
|
from nova import context as nova_context
|
||||||
from nova.db import base
|
from nova.db import base
|
||||||
from nova import exception
|
from nova import exception
|
||||||
from nova import hooks
|
|
||||||
from nova.i18n import _
|
from nova.i18n import _
|
||||||
from nova.network import constants
|
from nova.network import constants
|
||||||
from nova.network import model as network_model
|
from nova.network import model as network_model
|
||||||
@ -102,7 +101,6 @@ def get_binding_profile(port):
|
|||||||
return port.get(constants.BINDING_PROFILE, {}) or {}
|
return port.get(constants.BINDING_PROFILE, {}) or {}
|
||||||
|
|
||||||
|
|
||||||
@hooks.add_hook('instance_network_info')
|
|
||||||
def update_instance_cache_with_nw_info(impl, context, instance, nw_info=None):
|
def update_instance_cache_with_nw_info(impl, context, instance, nw_info=None):
|
||||||
if instance.deleted:
|
if instance.deleted:
|
||||||
LOG.debug('Instance is deleted, no further info cache update',
|
LOG.debug('Instance is deleted, no further info cache update',
|
||||||
|
@ -748,12 +748,6 @@ class NoDBTestCase(TestCase):
|
|||||||
USES_DB = False
|
USES_DB = False
|
||||||
|
|
||||||
|
|
||||||
class BaseHookTestCase(NoDBTestCase):
|
|
||||||
def assert_has_hook(self, expected_name, func):
|
|
||||||
self.assertTrue(hasattr(func, '__hook_name__'))
|
|
||||||
self.assertEqual(expected_name, func.__hook_name__)
|
|
||||||
|
|
||||||
|
|
||||||
class MatchType(object):
|
class MatchType(object):
|
||||||
"""Matches any instance of a specified type
|
"""Matches any instance of a specified type
|
||||||
|
|
||||||
|
@ -13425,18 +13425,3 @@ class CheckRequestedImageTestCase(test.TestCase):
|
|||||||
self.compute_api._validate_flavor_image,
|
self.compute_api._validate_flavor_image,
|
||||||
self.context, image['id'], image, self.instance_type,
|
self.context, image['id'], image, self.instance_type,
|
||||||
None)
|
None)
|
||||||
|
|
||||||
|
|
||||||
class ComputeHooksTestCase(test.BaseHookTestCase):
|
|
||||||
def test_delete_instance_has_hook(self):
|
|
||||||
delete_func = compute_manager.ComputeManager._delete_instance
|
|
||||||
self.assert_has_hook('delete_instance', delete_func)
|
|
||||||
|
|
||||||
def test_create_instance_has_hook(self):
|
|
||||||
create_func = compute.API.create
|
|
||||||
self.assert_has_hook('create_instance', create_func)
|
|
||||||
|
|
||||||
def test_build_instance_has_hook(self):
|
|
||||||
build_instance_func = (compute_manager.ComputeManager.
|
|
||||||
_do_build_and_run_instance)
|
|
||||||
self.assert_has_hook('build_instance', build_instance_func)
|
|
||||||
|
@ -6025,15 +6025,6 @@ class ComputeManagerBuildInstanceTestCase(test.NoDBTestCase):
|
|||||||
mock_finish.assert_called_once_with(self.context, self.instance.uuid,
|
mock_finish.assert_called_once_with(self.context, self.instance.uuid,
|
||||||
mock.ANY, exc_val=mock.ANY, exc_tb=mock.ANY, want_result=False)
|
mock.ANY, exc_val=mock.ANY, exc_tb=mock.ANY, want_result=False)
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def _assert_build_instance_hook_called(mock_hooks, result):
|
|
||||||
# NOTE(coreywright): we want to test the return value of
|
|
||||||
# _do_build_and_run_instance, but it doesn't bubble all the way up, so
|
|
||||||
# mock the hooking, which allows us to test that too, though a little
|
|
||||||
# too intimately
|
|
||||||
mock_hooks.setdefault().run_post.assert_called_once_with(
|
|
||||||
'build_instance', result, mock.ANY, mock.ANY, f=None)
|
|
||||||
|
|
||||||
@mock.patch.object(objects.Instance, 'save')
|
@mock.patch.object(objects.Instance, 'save')
|
||||||
@mock.patch.object(nova.compute.manager.ComputeManager,
|
@mock.patch.object(nova.compute.manager.ComputeManager,
|
||||||
'_default_block_device_names')
|
'_default_block_device_names')
|
||||||
@ -6349,12 +6340,23 @@ class ComputeManagerBuildInstanceTestCase(test.NoDBTestCase):
|
|||||||
@mock.patch.object(objects.InstanceActionEvent, 'event_start')
|
@mock.patch.object(objects.InstanceActionEvent, 'event_start')
|
||||||
@mock.patch.object(objects.Instance, 'save')
|
@mock.patch.object(objects.Instance, 'save')
|
||||||
@mock.patch.object(manager.ComputeManager, '_build_and_run_instance')
|
@mock.patch.object(manager.ComputeManager, '_build_and_run_instance')
|
||||||
@mock.patch('nova.hooks._HOOKS')
|
def _test_build_and_run_instance(self, mock_build, mock_save,
|
||||||
def _test_build_and_run_instance(self, mock_hooks, mock_build, mock_save,
|
|
||||||
mock_start, mock_finish):
|
mock_start, mock_finish):
|
||||||
self._do_build_instance_update(mock_save)
|
self._do_build_instance_update(mock_save)
|
||||||
|
|
||||||
self.compute.build_and_run_instance(self.context, self.instance,
|
orig_do_build_and_run = self.compute._do_build_and_run_instance
|
||||||
|
|
||||||
|
def _wrapped_do_build_and_run_instance(*args, **kwargs):
|
||||||
|
ret = orig_do_build_and_run(*args, **kwargs)
|
||||||
|
self.assertEqual(build_results.ACTIVE, ret)
|
||||||
|
return ret
|
||||||
|
|
||||||
|
with mock.patch.object(
|
||||||
|
self.compute, '_do_build_and_run_instance',
|
||||||
|
side_effect=_wrapped_do_build_and_run_instance,
|
||||||
|
):
|
||||||
|
self.compute.build_and_run_instance(
|
||||||
|
self.context, self.instance,
|
||||||
self.image, request_spec={},
|
self.image, request_spec={},
|
||||||
filter_properties=self.filter_properties,
|
filter_properties=self.filter_properties,
|
||||||
injected_files=self.injected_files,
|
injected_files=self.injected_files,
|
||||||
@ -6364,8 +6366,6 @@ class ComputeManagerBuildInstanceTestCase(test.NoDBTestCase):
|
|||||||
block_device_mapping=self.block_device_mapping, node=self.node,
|
block_device_mapping=self.block_device_mapping, node=self.node,
|
||||||
limits=self.limits, host_list=fake_host_list)
|
limits=self.limits, host_list=fake_host_list)
|
||||||
|
|
||||||
self._assert_build_instance_hook_called(mock_hooks,
|
|
||||||
build_results.ACTIVE)
|
|
||||||
self._instance_action_events(mock_start, mock_finish)
|
self._instance_action_events(mock_start, mock_finish)
|
||||||
self._assert_build_instance_update(mock_save)
|
self._assert_build_instance_update(mock_save)
|
||||||
mock_build.assert_called_once_with(self.context, self.instance,
|
mock_build.assert_called_once_with(self.context, self.instance,
|
||||||
@ -6411,8 +6411,7 @@ class ComputeManagerBuildInstanceTestCase(test.NoDBTestCase):
|
|||||||
@mock.patch.object(manager.ComputeManager, '_set_instance_obj_error_state')
|
@mock.patch.object(manager.ComputeManager, '_set_instance_obj_error_state')
|
||||||
@mock.patch.object(conductor_api.ComputeTaskAPI, 'build_instances')
|
@mock.patch.object(conductor_api.ComputeTaskAPI, 'build_instances')
|
||||||
@mock.patch.object(manager.ComputeManager, '_build_and_run_instance')
|
@mock.patch.object(manager.ComputeManager, '_build_and_run_instance')
|
||||||
@mock.patch('nova.hooks._HOOKS')
|
def test_build_abort_exception(self, mock_build_run,
|
||||||
def test_build_abort_exception(self, mock_hooks, mock_build_run,
|
|
||||||
mock_build, mock_set, mock_nil, mock_add,
|
mock_build, mock_set, mock_nil, mock_add,
|
||||||
mock_clean_vol, mock_clean_net, mock_save,
|
mock_clean_vol, mock_clean_net, mock_save,
|
||||||
mock_start, mock_finish):
|
mock_start, mock_finish):
|
||||||
@ -6420,7 +6419,19 @@ class ComputeManagerBuildInstanceTestCase(test.NoDBTestCase):
|
|||||||
mock_build_run.side_effect = exception.BuildAbortException(reason='',
|
mock_build_run.side_effect = exception.BuildAbortException(reason='',
|
||||||
instance_uuid=self.instance.uuid)
|
instance_uuid=self.instance.uuid)
|
||||||
|
|
||||||
self.compute.build_and_run_instance(self.context, self.instance,
|
orig_do_build_and_run = self.compute._do_build_and_run_instance
|
||||||
|
|
||||||
|
def _wrapped_do_build_and_run_instance(*args, **kwargs):
|
||||||
|
ret = orig_do_build_and_run(*args, **kwargs)
|
||||||
|
self.assertEqual(build_results.FAILED, ret)
|
||||||
|
return ret
|
||||||
|
|
||||||
|
with mock.patch.object(
|
||||||
|
self.compute, '_do_build_and_run_instance',
|
||||||
|
side_effect=_wrapped_do_build_and_run_instance,
|
||||||
|
):
|
||||||
|
self.compute.build_and_run_instance(
|
||||||
|
self.context, self.instance,
|
||||||
self.image, request_spec={},
|
self.image, request_spec={},
|
||||||
filter_properties=self.filter_properties,
|
filter_properties=self.filter_properties,
|
||||||
injected_files=self.injected_files,
|
injected_files=self.injected_files,
|
||||||
@ -6432,8 +6443,6 @@ class ComputeManagerBuildInstanceTestCase(test.NoDBTestCase):
|
|||||||
|
|
||||||
self._instance_action_events(mock_start, mock_finish)
|
self._instance_action_events(mock_start, mock_finish)
|
||||||
self._assert_build_instance_update(mock_save)
|
self._assert_build_instance_update(mock_save)
|
||||||
self._assert_build_instance_hook_called(mock_hooks,
|
|
||||||
build_results.FAILED)
|
|
||||||
mock_build_run.assert_called_once_with(self.context, self.instance,
|
mock_build_run.assert_called_once_with(self.context, self.instance,
|
||||||
self.image, self.injected_files, self.admin_pass,
|
self.image, self.injected_files, self.admin_pass,
|
||||||
self.requested_networks, self.security_groups,
|
self.requested_networks, self.security_groups,
|
||||||
@ -6458,16 +6467,28 @@ class ComputeManagerBuildInstanceTestCase(test.NoDBTestCase):
|
|||||||
@mock.patch.object(manager.ComputeManager, '_set_instance_obj_error_state')
|
@mock.patch.object(manager.ComputeManager, '_set_instance_obj_error_state')
|
||||||
@mock.patch.object(conductor_api.ComputeTaskAPI, 'build_instances')
|
@mock.patch.object(conductor_api.ComputeTaskAPI, 'build_instances')
|
||||||
@mock.patch.object(manager.ComputeManager, '_build_and_run_instance')
|
@mock.patch.object(manager.ComputeManager, '_build_and_run_instance')
|
||||||
@mock.patch('nova.hooks._HOOKS')
|
def test_rescheduled_exception(self, mock_build_run,
|
||||||
def test_rescheduled_exception(self, mock_hooks, mock_build_run,
|
|
||||||
mock_build, mock_set, mock_nil,
|
mock_build, mock_set, mock_nil,
|
||||||
mock_save, mock_start, mock_finish):
|
mock_save, mock_start, mock_finish):
|
||||||
self._do_build_instance_update(mock_save, reschedule_update=True)
|
self._do_build_instance_update(mock_save, reschedule_update=True)
|
||||||
mock_build_run.side_effect = exception.RescheduledException(reason='',
|
mock_build_run.side_effect = exception.RescheduledException(reason='',
|
||||||
instance_uuid=self.instance.uuid)
|
instance_uuid=self.instance.uuid)
|
||||||
|
|
||||||
with mock.patch.object(
|
orig_do_build_and_run = self.compute._do_build_and_run_instance
|
||||||
self.compute.network_api, 'get_instance_nw_info',
|
|
||||||
|
def _wrapped_do_build_and_run_instance(*args, **kwargs):
|
||||||
|
ret = orig_do_build_and_run(*args, **kwargs)
|
||||||
|
self.assertEqual(build_results.RESCHEDULED, ret)
|
||||||
|
return ret
|
||||||
|
|
||||||
|
with test.nested(
|
||||||
|
mock.patch.object(
|
||||||
|
self.compute, '_do_build_and_run_instance',
|
||||||
|
side_effect=_wrapped_do_build_and_run_instance,
|
||||||
|
),
|
||||||
|
mock.patch.object(
|
||||||
|
self.compute.network_api, 'get_instance_nw_info',
|
||||||
|
),
|
||||||
):
|
):
|
||||||
self.compute.build_and_run_instance(
|
self.compute.build_and_run_instance(
|
||||||
self.context, self.instance,
|
self.context, self.instance,
|
||||||
@ -6481,8 +6502,6 @@ class ComputeManagerBuildInstanceTestCase(test.NoDBTestCase):
|
|||||||
node=self.node, limits=self.limits,
|
node=self.node, limits=self.limits,
|
||||||
host_list=fake_host_list)
|
host_list=fake_host_list)
|
||||||
|
|
||||||
self._assert_build_instance_hook_called(mock_hooks,
|
|
||||||
build_results.RESCHEDULED)
|
|
||||||
self._instance_action_events(mock_start, mock_finish)
|
self._instance_action_events(mock_start, mock_finish)
|
||||||
self._assert_build_instance_update(mock_save, reschedule_update=True)
|
self._assert_build_instance_update(mock_save, reschedule_update=True)
|
||||||
mock_build_run.assert_called_once_with(self.context, self.instance,
|
mock_build_run.assert_called_once_with(self.context, self.instance,
|
||||||
@ -6692,15 +6711,26 @@ class ComputeManagerBuildInstanceTestCase(test.NoDBTestCase):
|
|||||||
@mock.patch.object(manager.ComputeManager, '_set_instance_obj_error_state')
|
@mock.patch.object(manager.ComputeManager, '_set_instance_obj_error_state')
|
||||||
@mock.patch.object(compute_utils, 'add_instance_fault_from_exc')
|
@mock.patch.object(compute_utils, 'add_instance_fault_from_exc')
|
||||||
@mock.patch.object(manager.ComputeManager, '_build_and_run_instance')
|
@mock.patch.object(manager.ComputeManager, '_build_and_run_instance')
|
||||||
@mock.patch('nova.hooks._HOOKS')
|
def test_rescheduled_exception_without_retry(self,
|
||||||
def test_rescheduled_exception_without_retry(self, mock_hooks,
|
|
||||||
mock_build_run, mock_add, mock_set, mock_clean_net, mock_clean_vol,
|
mock_build_run, mock_add, mock_set, mock_clean_net, mock_clean_vol,
|
||||||
mock_nil, mock_save, mock_start, mock_finish):
|
mock_nil, mock_save, mock_start, mock_finish):
|
||||||
self._do_build_instance_update(mock_save)
|
self._do_build_instance_update(mock_save)
|
||||||
mock_build_run.side_effect = exception.RescheduledException(reason='',
|
mock_build_run.side_effect = exception.RescheduledException(reason='',
|
||||||
instance_uuid=self.instance.uuid)
|
instance_uuid=self.instance.uuid)
|
||||||
|
|
||||||
self.compute.build_and_run_instance(self.context, self.instance,
|
orig_do_build_and_run = self.compute._do_build_and_run_instance
|
||||||
|
|
||||||
|
def _wrapped_do_build_and_run_instance(*args, **kwargs):
|
||||||
|
ret = orig_do_build_and_run(*args, **kwargs)
|
||||||
|
self.assertEqual(build_results.FAILED, ret)
|
||||||
|
return ret
|
||||||
|
|
||||||
|
with mock.patch.object(
|
||||||
|
self.compute, '_do_build_and_run_instance',
|
||||||
|
side_effect=_wrapped_do_build_and_run_instance,
|
||||||
|
):
|
||||||
|
self.compute.build_and_run_instance(
|
||||||
|
self.context, self.instance,
|
||||||
self.image, request_spec={},
|
self.image, request_spec={},
|
||||||
filter_properties={},
|
filter_properties={},
|
||||||
injected_files=self.injected_files,
|
injected_files=self.injected_files,
|
||||||
@ -6710,8 +6740,6 @@ class ComputeManagerBuildInstanceTestCase(test.NoDBTestCase):
|
|||||||
block_device_mapping=self.block_device_mapping, node=self.node,
|
block_device_mapping=self.block_device_mapping, node=self.node,
|
||||||
limits=self.limits, host_list=fake_host_list)
|
limits=self.limits, host_list=fake_host_list)
|
||||||
|
|
||||||
self._assert_build_instance_hook_called(mock_hooks,
|
|
||||||
build_results.FAILED)
|
|
||||||
self._instance_action_events(mock_start, mock_finish)
|
self._instance_action_events(mock_start, mock_finish)
|
||||||
self._assert_build_instance_update(mock_save)
|
self._assert_build_instance_update(mock_save)
|
||||||
mock_build_run.assert_called_once_with(self.context, self.instance,
|
mock_build_run.assert_called_once_with(self.context, self.instance,
|
||||||
@ -6739,8 +6767,7 @@ class ComputeManagerBuildInstanceTestCase(test.NoDBTestCase):
|
|||||||
'_nil_out_instance_obj_host_and_node')
|
'_nil_out_instance_obj_host_and_node')
|
||||||
@mock.patch.object(conductor_api.ComputeTaskAPI, 'build_instances')
|
@mock.patch.object(conductor_api.ComputeTaskAPI, 'build_instances')
|
||||||
@mock.patch.object(manager.ComputeManager, '_build_and_run_instance')
|
@mock.patch.object(manager.ComputeManager, '_build_and_run_instance')
|
||||||
@mock.patch('nova.hooks._HOOKS')
|
def test_rescheduled_exception_do_not_deallocate_network(self,
|
||||||
def test_rescheduled_exception_do_not_deallocate_network(self, mock_hooks,
|
|
||||||
mock_build_run, mock_build, mock_nil,
|
mock_build_run, mock_build, mock_nil,
|
||||||
mock_clean_net, mock_save, mock_start,
|
mock_clean_net, mock_save, mock_start,
|
||||||
mock_finish):
|
mock_finish):
|
||||||
@ -6748,7 +6775,19 @@ class ComputeManagerBuildInstanceTestCase(test.NoDBTestCase):
|
|||||||
mock_build_run.side_effect = exception.RescheduledException(reason='',
|
mock_build_run.side_effect = exception.RescheduledException(reason='',
|
||||||
instance_uuid=self.instance.uuid)
|
instance_uuid=self.instance.uuid)
|
||||||
|
|
||||||
self.compute.build_and_run_instance(self.context, self.instance,
|
orig_do_build_and_run = self.compute._do_build_and_run_instance
|
||||||
|
|
||||||
|
def _wrapped_do_build_and_run_instance(*args, **kwargs):
|
||||||
|
ret = orig_do_build_and_run(*args, **kwargs)
|
||||||
|
self.assertEqual(build_results.RESCHEDULED, ret)
|
||||||
|
return ret
|
||||||
|
|
||||||
|
with mock.patch.object(
|
||||||
|
self.compute, '_do_build_and_run_instance',
|
||||||
|
side_effect=_wrapped_do_build_and_run_instance,
|
||||||
|
):
|
||||||
|
self.compute.build_and_run_instance(
|
||||||
|
self.context, self.instance,
|
||||||
self.image, request_spec={},
|
self.image, request_spec={},
|
||||||
filter_properties=self.filter_properties,
|
filter_properties=self.filter_properties,
|
||||||
injected_files=self.injected_files,
|
injected_files=self.injected_files,
|
||||||
@ -6759,8 +6798,6 @@ class ComputeManagerBuildInstanceTestCase(test.NoDBTestCase):
|
|||||||
node=self.node, limits=self.limits,
|
node=self.node, limits=self.limits,
|
||||||
host_list=fake_host_list)
|
host_list=fake_host_list)
|
||||||
|
|
||||||
self._assert_build_instance_hook_called(mock_hooks,
|
|
||||||
build_results.RESCHEDULED)
|
|
||||||
self._instance_action_events(mock_start, mock_finish)
|
self._instance_action_events(mock_start, mock_finish)
|
||||||
self._assert_build_instance_update(mock_save, reschedule_update=True)
|
self._assert_build_instance_update(mock_save, reschedule_update=True)
|
||||||
mock_build_run.assert_called_once_with(self.context, self.instance,
|
mock_build_run.assert_called_once_with(self.context, self.instance,
|
||||||
@ -6784,15 +6821,26 @@ class ComputeManagerBuildInstanceTestCase(test.NoDBTestCase):
|
|||||||
'_nil_out_instance_obj_host_and_node')
|
'_nil_out_instance_obj_host_and_node')
|
||||||
@mock.patch.object(conductor_api.ComputeTaskAPI, 'build_instances')
|
@mock.patch.object(conductor_api.ComputeTaskAPI, 'build_instances')
|
||||||
@mock.patch.object(manager.ComputeManager, '_build_and_run_instance')
|
@mock.patch.object(manager.ComputeManager, '_build_and_run_instance')
|
||||||
@mock.patch('nova.hooks._HOOKS')
|
def test_rescheduled_exception_deallocate_network(self,
|
||||||
def test_rescheduled_exception_deallocate_network(self, mock_hooks,
|
|
||||||
mock_build_run, mock_build, mock_nil, mock_clean,
|
mock_build_run, mock_build, mock_nil, mock_clean,
|
||||||
mock_save, mock_start, mock_finish):
|
mock_save, mock_start, mock_finish):
|
||||||
self._do_build_instance_update(mock_save, reschedule_update=True)
|
self._do_build_instance_update(mock_save, reschedule_update=True)
|
||||||
mock_build_run.side_effect = exception.RescheduledException(reason='',
|
mock_build_run.side_effect = exception.RescheduledException(reason='',
|
||||||
instance_uuid=self.instance.uuid)
|
instance_uuid=self.instance.uuid)
|
||||||
|
|
||||||
self.compute.build_and_run_instance(self.context, self.instance,
|
orig_do_build_and_run = self.compute._do_build_and_run_instance
|
||||||
|
|
||||||
|
def _wrapped_do_build_and_run_instance(*args, **kwargs):
|
||||||
|
ret = orig_do_build_and_run(*args, **kwargs)
|
||||||
|
self.assertEqual(build_results.RESCHEDULED, ret)
|
||||||
|
return ret
|
||||||
|
|
||||||
|
with mock.patch.object(
|
||||||
|
self.compute, '_do_build_and_run_instance',
|
||||||
|
side_effect=_wrapped_do_build_and_run_instance,
|
||||||
|
):
|
||||||
|
self.compute.build_and_run_instance(
|
||||||
|
self.context, self.instance,
|
||||||
self.image, request_spec={},
|
self.image, request_spec={},
|
||||||
filter_properties=self.filter_properties,
|
filter_properties=self.filter_properties,
|
||||||
injected_files=self.injected_files,
|
injected_files=self.injected_files,
|
||||||
@ -6802,8 +6850,6 @@ class ComputeManagerBuildInstanceTestCase(test.NoDBTestCase):
|
|||||||
block_device_mapping=self.block_device_mapping, node=self.node,
|
block_device_mapping=self.block_device_mapping, node=self.node,
|
||||||
limits=self.limits, host_list=fake_host_list)
|
limits=self.limits, host_list=fake_host_list)
|
||||||
|
|
||||||
self._assert_build_instance_hook_called(mock_hooks,
|
|
||||||
build_results.RESCHEDULED)
|
|
||||||
self._instance_action_events(mock_start, mock_finish)
|
self._instance_action_events(mock_start, mock_finish)
|
||||||
self._assert_build_instance_update(mock_save, reschedule_update=True)
|
self._assert_build_instance_update(mock_save, reschedule_update=True)
|
||||||
mock_build_run.assert_called_once_with(self.context, self.instance,
|
mock_build_run.assert_called_once_with(self.context, self.instance,
|
||||||
@ -6832,8 +6878,7 @@ class ComputeManagerBuildInstanceTestCase(test.NoDBTestCase):
|
|||||||
@mock.patch.object(manager.ComputeManager, '_set_instance_obj_error_state')
|
@mock.patch.object(manager.ComputeManager, '_set_instance_obj_error_state')
|
||||||
@mock.patch.object(conductor_api.ComputeTaskAPI, 'build_instances')
|
@mock.patch.object(conductor_api.ComputeTaskAPI, 'build_instances')
|
||||||
@mock.patch.object(manager.ComputeManager, '_build_and_run_instance')
|
@mock.patch.object(manager.ComputeManager, '_build_and_run_instance')
|
||||||
@mock.patch('nova.hooks._HOOKS')
|
def _test_build_and_run_exceptions(self, exc, mock_build_run,
|
||||||
def _test_build_and_run_exceptions(self, exc, mock_hooks, mock_build_run,
|
|
||||||
mock_build, mock_set, mock_nil, mock_add, mock_clean_vol,
|
mock_build, mock_set, mock_nil, mock_add, mock_clean_vol,
|
||||||
mock_clean_net, mock_save, mock_start, mock_finish,
|
mock_clean_net, mock_save, mock_start, mock_finish,
|
||||||
set_error=False, cleanup_volumes=False,
|
set_error=False, cleanup_volumes=False,
|
||||||
@ -6841,7 +6886,19 @@ class ComputeManagerBuildInstanceTestCase(test.NoDBTestCase):
|
|||||||
self._do_build_instance_update(mock_save)
|
self._do_build_instance_update(mock_save)
|
||||||
mock_build_run.side_effect = exc
|
mock_build_run.side_effect = exc
|
||||||
|
|
||||||
self.compute.build_and_run_instance(self.context, self.instance,
|
orig_do_build_and_run = self.compute._do_build_and_run_instance
|
||||||
|
|
||||||
|
def _wrapped_do_build_and_run_instance(*args, **kwargs):
|
||||||
|
ret = orig_do_build_and_run(*args, **kwargs)
|
||||||
|
self.assertEqual(build_results.FAILED, ret)
|
||||||
|
return ret
|
||||||
|
|
||||||
|
with mock.patch.object(
|
||||||
|
self.compute, '_do_build_and_run_instance',
|
||||||
|
side_effect=_wrapped_do_build_and_run_instance,
|
||||||
|
):
|
||||||
|
self.compute.build_and_run_instance(
|
||||||
|
self.context, self.instance,
|
||||||
self.image, request_spec={},
|
self.image, request_spec={},
|
||||||
filter_properties=self.filter_properties,
|
filter_properties=self.filter_properties,
|
||||||
injected_files=self.injected_files,
|
injected_files=self.injected_files,
|
||||||
@ -6851,8 +6908,6 @@ class ComputeManagerBuildInstanceTestCase(test.NoDBTestCase):
|
|||||||
block_device_mapping=self.block_device_mapping, node=self.node,
|
block_device_mapping=self.block_device_mapping, node=self.node,
|
||||||
limits=self.limits, host_list=fake_host_list)
|
limits=self.limits, host_list=fake_host_list)
|
||||||
|
|
||||||
self._assert_build_instance_hook_called(mock_hooks,
|
|
||||||
build_results.FAILED)
|
|
||||||
self._instance_action_events(mock_start, mock_finish)
|
self._instance_action_events(mock_start, mock_finish)
|
||||||
self._assert_build_instance_update(mock_save)
|
self._assert_build_instance_update(mock_save)
|
||||||
if cleanup_volumes:
|
if cleanup_volumes:
|
||||||
|
@ -1,199 +0,0 @@
|
|||||||
# Copyright (c) 2012 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.
|
|
||||||
|
|
||||||
"""Tests for hook customization."""
|
|
||||||
|
|
||||||
import stevedore
|
|
||||||
|
|
||||||
from nova import hooks
|
|
||||||
from nova import test
|
|
||||||
|
|
||||||
|
|
||||||
class SampleHookA(object):
|
|
||||||
name = "a"
|
|
||||||
|
|
||||||
def _add_called(self, op, kwargs):
|
|
||||||
called = kwargs.get('called', None)
|
|
||||||
if called is not None:
|
|
||||||
called.append(op + self.name)
|
|
||||||
|
|
||||||
def pre(self, *args, **kwargs):
|
|
||||||
self._add_called("pre", kwargs)
|
|
||||||
|
|
||||||
|
|
||||||
class SampleHookB(SampleHookA):
|
|
||||||
name = "b"
|
|
||||||
|
|
||||||
def post(self, rv, *args, **kwargs):
|
|
||||||
self._add_called("post", kwargs)
|
|
||||||
|
|
||||||
|
|
||||||
class SampleHookC(SampleHookA):
|
|
||||||
name = "c"
|
|
||||||
|
|
||||||
def pre(self, f, *args, **kwargs):
|
|
||||||
self._add_called("pre" + f.__name__, kwargs)
|
|
||||||
|
|
||||||
def post(self, f, rv, *args, **kwargs):
|
|
||||||
self._add_called("post" + f.__name__, kwargs)
|
|
||||||
|
|
||||||
|
|
||||||
class SampleHookExceptionPre(SampleHookA):
|
|
||||||
name = "epre"
|
|
||||||
exception = Exception()
|
|
||||||
|
|
||||||
def pre(self, f, *args, **kwargs):
|
|
||||||
raise self.exception
|
|
||||||
|
|
||||||
|
|
||||||
class SampleHookExceptionPost(SampleHookA):
|
|
||||||
name = "epost"
|
|
||||||
exception = Exception()
|
|
||||||
|
|
||||||
def post(self, f, rv, *args, **kwargs):
|
|
||||||
raise self.exception
|
|
||||||
|
|
||||||
|
|
||||||
class MockEntryPoint(object):
|
|
||||||
|
|
||||||
def __init__(self, cls):
|
|
||||||
self.cls = cls
|
|
||||||
|
|
||||||
def load(self):
|
|
||||||
return self.cls
|
|
||||||
|
|
||||||
|
|
||||||
class MockedHookTestCase(test.BaseHookTestCase):
|
|
||||||
PLUGINS = []
|
|
||||||
|
|
||||||
def setUp(self):
|
|
||||||
super(MockedHookTestCase, self).setUp()
|
|
||||||
|
|
||||||
hooks.reset()
|
|
||||||
|
|
||||||
hook_manager = hooks.HookManager.make_test_instance(self.PLUGINS)
|
|
||||||
self.stub_out('nova.hooks.HookManager', lambda x: hook_manager)
|
|
||||||
|
|
||||||
|
|
||||||
class HookTestCase(MockedHookTestCase):
|
|
||||||
PLUGINS = [
|
|
||||||
stevedore.extension.Extension('test_hook',
|
|
||||||
MockEntryPoint(SampleHookA), SampleHookA, SampleHookA()),
|
|
||||||
stevedore.extension.Extension('test_hook',
|
|
||||||
MockEntryPoint(SampleHookB), SampleHookB, SampleHookB()),
|
|
||||||
]
|
|
||||||
|
|
||||||
def setUp(self):
|
|
||||||
super(HookTestCase, self).setUp()
|
|
||||||
|
|
||||||
hooks.reset()
|
|
||||||
|
|
||||||
@hooks.add_hook('test_hook')
|
|
||||||
def _hooked(self, a, b=1, c=2, called=None):
|
|
||||||
return 42
|
|
||||||
|
|
||||||
def test_basic(self):
|
|
||||||
self.assertEqual(42, self._hooked(1))
|
|
||||||
|
|
||||||
mgr = hooks._HOOKS['test_hook']
|
|
||||||
self.assert_has_hook('test_hook', self._hooked)
|
|
||||||
self.assertEqual(2, len(mgr.extensions))
|
|
||||||
self.assertEqual(SampleHookA, mgr.extensions[0].plugin)
|
|
||||||
self.assertEqual(SampleHookB, mgr.extensions[1].plugin)
|
|
||||||
|
|
||||||
def test_order_of_execution(self):
|
|
||||||
called_order = []
|
|
||||||
self._hooked(42, called=called_order)
|
|
||||||
self.assertEqual(['prea', 'preb', 'postb'], called_order)
|
|
||||||
|
|
||||||
|
|
||||||
class HookTestCaseWithFunction(MockedHookTestCase):
|
|
||||||
PLUGINS = [
|
|
||||||
stevedore.extension.Extension('function_hook',
|
|
||||||
MockEntryPoint(SampleHookC), SampleHookC, SampleHookC()),
|
|
||||||
]
|
|
||||||
|
|
||||||
@hooks.add_hook('function_hook', pass_function=True)
|
|
||||||
def _hooked(self, a, b=1, c=2, called=None):
|
|
||||||
return 42
|
|
||||||
|
|
||||||
def test_basic(self):
|
|
||||||
self.assertEqual(42, self._hooked(1))
|
|
||||||
mgr = hooks._HOOKS['function_hook']
|
|
||||||
|
|
||||||
self.assert_has_hook('function_hook', self._hooked)
|
|
||||||
self.assertEqual(1, len(mgr.extensions))
|
|
||||||
self.assertEqual(SampleHookC, mgr.extensions[0].plugin)
|
|
||||||
|
|
||||||
def test_order_of_execution(self):
|
|
||||||
called_order = []
|
|
||||||
self._hooked(42, called=called_order)
|
|
||||||
self.assertEqual(['pre_hookedc', 'post_hookedc'], called_order)
|
|
||||||
|
|
||||||
|
|
||||||
class HookFailPreTestCase(MockedHookTestCase):
|
|
||||||
PLUGINS = [
|
|
||||||
stevedore.extension.Extension('fail_pre',
|
|
||||||
MockEntryPoint(SampleHookExceptionPre),
|
|
||||||
SampleHookExceptionPre, SampleHookExceptionPre()),
|
|
||||||
]
|
|
||||||
|
|
||||||
@hooks.add_hook('fail_pre', pass_function=True)
|
|
||||||
def _hooked(self, a, b=1, c=2, called=None):
|
|
||||||
return 42
|
|
||||||
|
|
||||||
def test_hook_fail_should_still_return(self):
|
|
||||||
self.assertEqual(42, self._hooked(1))
|
|
||||||
|
|
||||||
mgr = hooks._HOOKS['fail_pre']
|
|
||||||
self.assert_has_hook('fail_pre', self._hooked)
|
|
||||||
self.assertEqual(1, len(mgr.extensions))
|
|
||||||
self.assertEqual(SampleHookExceptionPre, mgr.extensions[0].plugin)
|
|
||||||
|
|
||||||
def test_hook_fail_should_raise_fatal(self):
|
|
||||||
self.stub_out('nova.tests.unit.test_hooks.'
|
|
||||||
'SampleHookExceptionPre.exception',
|
|
||||||
hooks.FatalHookException())
|
|
||||||
|
|
||||||
self.assertRaises(hooks.FatalHookException,
|
|
||||||
self._hooked, 1)
|
|
||||||
|
|
||||||
|
|
||||||
class HookFailPostTestCase(MockedHookTestCase):
|
|
||||||
PLUGINS = [
|
|
||||||
stevedore.extension.Extension('fail_post',
|
|
||||||
MockEntryPoint(SampleHookExceptionPost),
|
|
||||||
SampleHookExceptionPost, SampleHookExceptionPost()),
|
|
||||||
]
|
|
||||||
|
|
||||||
@hooks.add_hook('fail_post', pass_function=True)
|
|
||||||
def _hooked(self, a, b=1, c=2, called=None):
|
|
||||||
return 42
|
|
||||||
|
|
||||||
def test_hook_fail_should_still_return(self):
|
|
||||||
self.assertEqual(42, self._hooked(1))
|
|
||||||
|
|
||||||
mgr = hooks._HOOKS['fail_post']
|
|
||||||
self.assert_has_hook('fail_post', self._hooked)
|
|
||||||
self.assertEqual(1, len(mgr.extensions))
|
|
||||||
self.assertEqual(SampleHookExceptionPost, mgr.extensions[0].plugin)
|
|
||||||
|
|
||||||
def test_hook_fail_should_raise_fatal(self):
|
|
||||||
self.stub_out('nova.tests.unit.test_hooks.'
|
|
||||||
'SampleHookExceptionPost.exception',
|
|
||||||
hooks.FatalHookException())
|
|
||||||
|
|
||||||
self.assertRaises(hooks.FatalHookException,
|
|
||||||
self._hooked, 1)
|
|
13
releasenotes/notes/remove-hooks-96d08645404d327c.yaml
Normal file
13
releasenotes/notes/remove-hooks-96d08645404d327c.yaml
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
---
|
||||||
|
upgrade:
|
||||||
|
- |
|
||||||
|
Support for hooks has been removed. In previous versions of nova, these
|
||||||
|
provided a mechanism to extend nova with custom code through a plugin
|
||||||
|
mechanism. However, they were deprecated in 13.0.0 (Mitaka) as
|
||||||
|
unmaintainable long-term. `Versioned notifications`__ and `vendordata`__
|
||||||
|
should be used instead.
|
||||||
|
For more information, refer to `this thread`__.
|
||||||
|
|
||||||
|
__ https://docs.openstack.org/nova/latest/reference/notifications.html
|
||||||
|
__ https://docs.openstack.org/nova/latest/admin/vendordata.html
|
||||||
|
__ http://lists.openstack.org/pipermail/openstack-dev/2016-February/087782.html
|
Loading…
Reference in New Issue
Block a user