Merge "Implements stack lifecycle plugpoints"

This commit is contained in:
Jenkins 2014-09-03 17:46:10 +00:00 committed by Gerrit Code Review
commit b7f295fd7d
7 changed files with 486 additions and 0 deletions

View File

@ -0,0 +1,117 @@
#
# 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.
'''
Utility for fetching and running plug point implementation classes
'''
from heat.engine import resources
from heat.openstack.common.gettextutils import _LE
from heat.openstack.common import log as logging
LOG = logging.getLogger(__name__)
pp_class_instances = None
def get_plug_point_class_instances():
'''
Get list of instances of classes that (may) implement pre and post
stack operation methods.
The list of class instances is sorted using get_ordinal methods
on the plug point classes. If class1.ordinal() < class2.ordinal(),
then class1 will be before before class2 in the list.
'''
global pp_class_instances
if pp_class_instances is None:
pp_class_instances = []
pp_classes = []
try:
slps = resources.global_env().get_stack_lifecycle_plugins()
pp_classes = [cls for name, cls in slps]
except Exception:
LOG.exception(_LE("failed to get lifecycle plug point classes"))
for ppc in pp_classes:
try:
pp_class_instances.append(ppc())
except Exception:
LOG.exception(
_LE("failed to instantiate stack lifecycle class %s"), ppc)
try:
pp_class_instances = sorted(pp_class_instances,
key=lambda ppci: ppci.get_ordinal())
except Exception:
LOG.exception(_LE("failed to sort lifecycle plug point classes"))
return pp_class_instances
def do_pre_ops(cnxt, stack, current_stack=None, action=None):
'''
Call available pre-op methods sequentially, in order determined with
get_ordinal(), with parameters context, stack, current_stack, action
On failure of any pre_op method, will call post-op methods corresponding
to successful calls of pre-op methods
'''
cinstances = get_plug_point_class_instances()
if action is None:
action = stack.action
failure, failure_exception_message, success_count = _do_ops(
cinstances, 'do_pre_op', cnxt, stack, current_stack, action, None)
if failure:
cinstances = cinstances[0:success_count]
_do_ops(cinstances, 'do_post_op', cnxt, stack, current_stack,
action, True)
raise Exception(failure_exception_message)
def do_post_ops(cnxt, stack, current_stack=None, action=None,
is_stack_failure=False):
'''
Call available post-op methods sequentially, in order determined with
get_ordinal(), with parameters context, stack, current_stack,
action, is_stack_failure
'''
cinstances = get_plug_point_class_instances()
if action is None:
action = stack.action
_do_ops(cinstances, 'do_post_op', cnxt, stack, current_stack, action, None)
def _do_ops(cinstances, opname, cnxt, stack, current_stack=None, action=None,
is_stack_failure=None):
success_count = 0
failure = False
failure_exception_message = None
for ci in cinstances:
op = getattr(ci, opname, None)
if callable(op):
try:
if is_stack_failure is not None:
op(cnxt, stack, current_stack, action, is_stack_failure)
else:
op(cnxt, stack, current_stack, action)
success_count += 1
except Exception as ex:
LOG.exception(_LE(
"%(opname) %(ci)s failed for %(a)s on %(sid)s") %
{'opname': opname, 'ci': type(ci),
'a': action, 'sid': stack.id})
failure = True
failure_exception_message = ex.args[0] if ex.args else str(ex)
break
LOG.info(_("done with class=%(c)s, stackid=%(sid)s, action=%(a)s") %
{'c': type(ci), 'sid': stack.id, 'a': action})
return (failure, failure_exception_message, success_count)

View File

@ -360,6 +360,7 @@ class Environment(object):
self.params = dict((k, v) for (k, v) in six.iteritems(env) self.params = dict((k, v) for (k, v) in six.iteritems(env)
if k != env_fmt.RESOURCE_REGISTRY) if k != env_fmt.RESOURCE_REGISTRY)
self.constraints = {} self.constraints = {}
self.stack_lifecycle_plugins = []
def load(self, env_snippet): def load(self, env_snippet):
self.registry.load(env_snippet.get(env_fmt.RESOURCE_REGISTRY, {})) self.registry.load(env_snippet.get(env_fmt.RESOURCE_REGISTRY, {}))
@ -376,6 +377,11 @@ class Environment(object):
def register_constraint(self, constraint_name, constraint): def register_constraint(self, constraint_name, constraint):
self.constraints[constraint_name] = constraint self.constraints[constraint_name] = constraint
def register_stack_lifecycle_plugin(self, stack_lifecycle_name,
stack_lifecycle_class):
self.stack_lifecycle_plugins.append((stack_lifecycle_name,
stack_lifecycle_class))
def get_class(self, resource_type, resource_name=None): def get_class(self, resource_type, resource_name=None):
return self.registry.get_class(resource_type, resource_name) return self.registry.get_class(resource_type, resource_name)
@ -390,6 +396,9 @@ class Environment(object):
def get_constraint(self, name): def get_constraint(self, name):
return self.constraints.get(name) return self.constraints.get(name)
def get_stack_lifecycle_plugins(self):
return self.stack_lifecycle_plugins
def read_global_environment(env, env_dir=None): def read_global_environment(env, env_dir=None):
if env_dir is None: if env_dir is None:

54
heat/engine/lifecycle_plugin.py Executable file
View File

@ -0,0 +1,54 @@
#
# 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.
class LifecyclePlugin(object):
'''
base class for pre-op and post-op work on a stack
Implementations should extend this class and override the methods
'''
def do_pre_op(self, cnxt, stack, current_stack=None, action=None):
'''
method to be run by heat before stack operations
'''
pass
def do_post_op(self, cnxt, stack, current_stack=None, action=None,
is_stack_failure=False):
'''
Method to be run by heat after stack operations, including failures.
On failure to execute all the registered pre_ops, this method will be
called if and only if the corresponding pre_op was successfully called.
On failures of the actual stack operation, this method will
be called if all the pre operations were successfully called.
'''
pass
def get_ordinal(self):
'''
An ordinal used to order class instances for pre and post
operation execution.
The values returned by get_ordinal are used to create a partial order
for pre and post operation method invocations. The default ordinal
value of 100 may be overridden.
If class1inst.ordinal() < class2inst.ordinal(), then the method on
class1inst will be executed before the method on class2inst.
If class1inst.ordinal() > class2inst.ordinal(), then the method on
class1inst will be executed after the method on class2inst.
If class1inst.ordinal() == class2inst.ordinal(), then the order of
method invocation is indeterminate.
'''
return 100

View File

@ -28,6 +28,12 @@ def _register_constraints(env, type_pairs):
env.register_constraint(constraint_name, constraint) env.register_constraint(constraint_name, constraint)
def _register_stack_lifecycle_plugins(env, type_pairs):
for stack_lifecycle_name, stack_lifecycle_class in type_pairs:
env.register_stack_lifecycle_plugin(stack_lifecycle_name,
stack_lifecycle_class)
def _get_mapping(namespace): def _get_mapping(namespace):
mgr = extension.ExtensionManager( mgr = extension.ExtensionManager(
namespace=namespace, namespace=namespace,
@ -64,6 +70,9 @@ def _load_global_environment(env):
def _load_global_resources(env): def _load_global_resources(env):
_register_constraints(env, _get_mapping('heat.constraints')) _register_constraints(env, _get_mapping('heat.constraints'))
_register_stack_lifecycle_plugins(
env,
_get_mapping('heat.stack_lifecycle_plugins'))
manager = plugin_manager.PluginManager(__name__) manager = plugin_manager.PluginManager(__name__)
# Sometimes resources should not be available for registration in Heat due # Sometimes resources should not be available for registration in Heat due

View File

@ -24,6 +24,7 @@ from heat.common import context as common_context
from heat.common import exception from heat.common import exception
from heat.common.exception import StackValidationFailed from heat.common.exception import StackValidationFailed
from heat.common import identifier from heat.common import identifier
from heat.common import lifecycle_plugin_utils
from heat.db import api as db_api from heat.db import api as db_api
from heat.engine import dependencies from heat.engine import dependencies
from heat.engine import environment from heat.engine import environment
@ -549,6 +550,15 @@ class Stack(collections.Mapping):
A task to perform an action on the stack and all of the resources A task to perform an action on the stack and all of the resources
in forward or reverse dependency order as specified by reverse in forward or reverse dependency order as specified by reverse
''' '''
try:
lifecycle_plugin_utils.do_pre_ops(self.context, self,
None, action)
except Exception as e:
self.state_set(action, self.FAILED, e.args[0] if e.args else
'Failed stack pre-ops: %s' % six.text_type(e))
if callable(post_func):
post_func()
return
self.state_set(action, self.IN_PROGRESS, self.state_set(action, self.IN_PROGRESS,
'Stack %s started' % action) 'Stack %s started' % action)
@ -586,6 +596,8 @@ class Stack(collections.Mapping):
if callable(post_func): if callable(post_func):
post_func() post_func()
lifecycle_plugin_utils.do_post_ops(self.context, self, None, action,
(self.status == self.FAILED))
def check(self): def check(self):
self.updated_time = datetime.utcnow() self.updated_time = datetime.utcnow()
@ -672,6 +684,13 @@ class Stack(collections.Mapping):
"Invalid action %s" % action) "Invalid action %s" % action)
return return
try:
lifecycle_plugin_utils.do_pre_ops(self.context, self,
newstack, action)
except Exception as e:
self.state_set(action, self.FAILED, e.args[0] if e.args else
'Failed stack pre-ops: %s' % six.text_type(e))
return
if self.status == self.IN_PROGRESS: if self.status == self.IN_PROGRESS:
if action == self.ROLLBACK: if action == self.ROLLBACK:
LOG.debug("Starting update rollback for %s" % self.name) LOG.debug("Starting update rollback for %s" % self.name)
@ -745,6 +764,9 @@ class Stack(collections.Mapping):
self.status_reason = reason self.status_reason = reason
self.store() self.store()
lifecycle_plugin_utils.do_post_ops(self.context, self,
newstack, action,
(self.status == self.FAILED))
notification.send(self) notification.send(self)
@ -762,6 +784,8 @@ class Stack(collections.Mapping):
"Invalid action %s" % action) "Invalid action %s" % action)
return return
# Note abandon is a delete with
# stack.set_deletion_policy(resource.RETAIN)
stack_status = self.COMPLETE stack_status = self.COMPLETE
reason = 'Stack %s completed successfully' % action reason = 'Stack %s completed successfully' % action
self.state_set(action, self.IN_PROGRESS, 'Stack %s started' % self.state_set(action, self.IN_PROGRESS, 'Stack %s started' %
@ -816,6 +840,15 @@ class Stack(collections.Mapping):
for snapshot in snapshots: for snapshot in snapshots:
self.delete_snapshot(snapshot) self.delete_snapshot(snapshot)
if not backup:
try:
lifecycle_plugin_utils.do_pre_ops(self.context, self,
None, action)
except Exception as e:
self.state_set(action, self.FAILED,
e.args[0] if e.args else
'Failed stack pre-ops: %s' % six.text_type(e))
return
action_task = scheduler.DependencyTaskGroup(self.dependencies, action_task = scheduler.DependencyTaskGroup(self.dependencies,
resource.Resource.destroy, resource.Resource.destroy,
reverse=True) reverse=True)
@ -880,6 +913,10 @@ class Stack(collections.Mapping):
LOG.info(_("Tried to delete stack that does not exist " LOG.info(_("Tried to delete stack that does not exist "
"%s ") % self.id) "%s ") % self.id)
if not backup:
lifecycle_plugin_utils.do_post_ops(self.context, self,
None, action,
(self.status == self.FAILED))
if stack_status != self.FAILED: if stack_status != self.FAILED:
# delete the stack # delete the stack
try: try:

View File

@ -0,0 +1,258 @@
#
# 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.
import mock
from heat.common import lifecycle_plugin_utils
from heat.engine import lifecycle_plugin
from heat.engine import resources
from heat.tests.common import HeatTestCase
empty_template = '''
heat_template_version: '2013-05-23'
description: Empty stack
resources:
'''
class LifecyclePluginUtilsTests(HeatTestCase):
"""
Basic tests for the helper methods in
:module:'heat.common.lifecycle_plugin_utils'.
"""
def setUp(self):
super(LifecyclePluginUtilsTests, self).setUp()
def tearDown(self):
super(LifecyclePluginUtilsTests, self).tearDown()
lifecycle_plugin_utils.pp_class_instances = None
def mock_lcp_class_map(self, lcp_mappings):
self.m.UnsetStubs()
self.m.StubOutWithMock(resources.global_env(),
'get_stack_lifecycle_plugins')
resources.global_env().get_stack_lifecycle_plugins().\
MultipleTimes().AndReturn(lcp_mappings)
self.m.ReplayAll()
# reset cache
lifecycle_plugin_utils.pp_class_instances = None
def test_get_plug_point_class_instances(self):
"""Tests the get_plug_point_class_instances function."""
lcp_mappings = [('A::B::C1', TestLifecycleCallout1)]
self.mock_lcp_class_map(lcp_mappings)
pp_cinstances = lifecycle_plugin_utils.get_plug_point_class_instances()
self.assertIsNotNone(pp_cinstances)
self.assertTrue(self.is_iterable(pp_cinstances),
"not iterable: %s" % pp_cinstances)
self.assertTrue(len(pp_cinstances) == 1)
self.assertEqual(TestLifecycleCallout1, pp_cinstances[0].__class__)
def test_do_pre_and_post_callouts(self):
lcp_mappings = [('A::B::C1', TestLifecycleCallout1)]
self.mock_lcp_class_map(lcp_mappings)
mc = mock.Mock()
mc.__setattr__("pre_counter_for_unit_test", 0)
mc.__setattr__("post_counter_for_unit_test", 0)
ms = mock.Mock()
ms.__setattr__("action", 'A')
lifecycle_plugin_utils.do_pre_ops(mc, ms, None, None)
self.assertEqual(mc.pre_counter_for_unit_test, 1)
lifecycle_plugin_utils.do_post_ops(mc, ms, None, None)
self.assertEqual(mc.post_counter_for_unit_test, 1)
return
def test_class_instantiation_and_sorting(self):
lcp_mappings = []
self.mock_lcp_class_map(lcp_mappings)
pp_cis = lifecycle_plugin_utils.get_plug_point_class_instances()
self.assertEqual(len(pp_cis), 0)
# order should change with sort
lcp_mappings = [('A::B::C2', TestLifecycleCallout2),
('A::B::C1', TestLifecycleCallout1)]
self.mock_lcp_class_map(lcp_mappings)
pp_cis = lifecycle_plugin_utils.get_plug_point_class_instances()
self.assertEqual(len(pp_cis), 2)
self.assertEqual(pp_cis[0].get_ordinal(), 100)
self.assertEqual(pp_cis[1].get_ordinal(), 101)
self.assertEqual(TestLifecycleCallout1, pp_cis[0].__class__)
self.assertEqual(TestLifecycleCallout2, pp_cis[1].__class__)
# order should NOT change with sort
lcp_mappings = [('A::B::C1', TestLifecycleCallout1),
('A::B::C2', TestLifecycleCallout2)]
self.mock_lcp_class_map(lcp_mappings)
pp_cis = lifecycle_plugin_utils.get_plug_point_class_instances()
self.assertEqual(len(pp_cis), 2)
self.assertEqual(pp_cis[0].get_ordinal(), 100)
self.assertEqual(pp_cis[1].get_ordinal(), 101)
self.assertEqual(TestLifecycleCallout1, pp_cis[0].__class__)
self.assertEqual(TestLifecycleCallout2, pp_cis[1].__class__)
# sort failure due to exception in thrown by ordinal
lcp_mappings = [('A::B::C2', TestLifecycleCallout2),
('A::B::C3', TestLifecycleCallout3),
('A::B::C1', TestLifecycleCallout1)]
self.mock_lcp_class_map(lcp_mappings)
pp_cis = lifecycle_plugin_utils.get_plug_point_class_instances()
self.assertEqual(len(pp_cis), 3)
self.assertEqual(pp_cis[2].get_ordinal(), 100)
self.assertEqual(pp_cis[0].get_ordinal(), 101)
# (can sort fail partially? If so then this test may break)
self.assertEqual(TestLifecycleCallout2, pp_cis[0].__class__)
self.assertEqual(TestLifecycleCallout3, pp_cis[1].__class__)
self.assertEqual(TestLifecycleCallout1, pp_cis[2].__class__)
return
def test_do_pre_op_failure(self):
lcp_mappings = [('A::B::C5', TestLifecycleCallout1),
('A::B::C4', TestLifecycleCallout4)]
self.mock_lcp_class_map(lcp_mappings)
mc = mock.Mock()
mc.__setattr__("pre_counter_for_unit_test", 0)
mc.__setattr__("post_counter_for_unit_test", 0)
ms = mock.Mock()
ms.__setattr__("action", 'A')
failed = False
try:
lifecycle_plugin_utils.do_pre_ops(mc, ms, None, None)
except Exception:
failed = True
self.assertTrue(failed)
self.assertEqual(mc.pre_counter_for_unit_test, 1)
self.assertEqual(mc.post_counter_for_unit_test, 1)
return
def test_do_post_op_failure(self):
lcp_mappings = [('A::B::C1', TestLifecycleCallout1),
('A::B::C5', TestLifecycleCallout5)]
self.mock_lcp_class_map(lcp_mappings)
mc = mock.Mock()
mc.__setattr__("pre_counter_for_unit_test", 0)
mc.__setattr__("post_counter_for_unit_test", 0)
ms = mock.Mock()
ms.__setattr__("action", 'A')
lifecycle_plugin_utils.do_post_ops(mc, ms, None, None)
self.assertEqual(mc.post_counter_for_unit_test, 1)
return
def test_exercise_base_lifecycle_plugin_class(self):
lcp = lifecycle_plugin.LifecyclePlugin()
ordinal = lcp.get_ordinal()
lcp.do_pre_op(None, None, None)
lcp.do_post_op(None, None, None)
self.assertEqual(ordinal, 100)
return
def is_iterable(self, obj):
#special case string
if not object:
return False
if isinstance(obj, str):
return False
#Test for iterabilityy
try:
for m in obj:
break
except TypeError:
return False
return True
class TestLifecycleCallout1(lifecycle_plugin.LifecyclePlugin):
'''
Sample test class for testing pre-op and post-op work on a stack
'''
def do_pre_op(self, cnxt, stack, current_stack=None, action=None):
cnxt.pre_counter_for_unit_test += 1
def do_post_op(self, cnxt, stack, current_stack=None, action=None,
is_stack_failure=False):
cnxt.post_counter_for_unit_test += 1
def get_ordinal(self):
return 100
class TestLifecycleCallout2(lifecycle_plugin.LifecyclePlugin):
'''
Sample test class for testing pre-op and post-op work on a stack,
different ordinal, and increment counters by 2
'''
def do_pre_op(self, cnxt, stack, current_stack=None, action=None):
cnxt.pre_counter_for_unit_test += 2
def do_post_op(self, cnxt, stack, current_stack=None, action=None,
is_stack_failure=False):
cnxt.post_counter_for_unit_test += 2
def get_ordinal(self):
return 101
class TestLifecycleCallout3(lifecycle_plugin.LifecyclePlugin):
'''
Sample test class for testing pre-op and post-op work on a stack,
methods raise exceptions
'''
def do_pre_op(self, cnxt, stack, current_stack=None, action=None):
raise Exception()
def do_post_op(self, cnxt, stack, current_stack=None, action=None,
is_stack_failure=False):
raise Exception()
def get_ordinal(self):
raise Exception()
class TestLifecycleCallout4(lifecycle_plugin.LifecyclePlugin):
'''
Sample test class for testing pre-op and post-op work on a stack;
do_pre_op, do_post_op both throw exception
'''
def do_pre_op(self, cnxt, stack, current_stack=None, action=None):
raise Exception()
def do_post_op(self, cnxt, stack, current_stack=None, action=None,
is_stack_failure=False):
raise Exception()
def get_ordinal(self):
return 103
class TestLifecycleCallout5(lifecycle_plugin.LifecyclePlugin):
'''
Sample test class for testing pre-op and post-op work on a stack;
do_post_op throws exception
'''
def do_pre_op(self, cnxt, stack, current_stack=None, action=None):
cnxt.pre_counter_for_unit_test += 1
def do_post_op(self, cnxt, stack, current_stack=None, action=None,
is_stack_failure=False):
raise Exception()
def get_ordinal(self):
return 100

View File

@ -59,6 +59,8 @@ heat.constraints =
iso_8601 = heat.engine.resources.iso_8601:ISO8601Constraint iso_8601 = heat.engine.resources.iso_8601:ISO8601Constraint
nova.keypair = heat.engine.resources.nova_keypair:KeypairConstraint nova.keypair = heat.engine.resources.nova_keypair:KeypairConstraint
heat.stack_lifecycle_plugins =
heat.templates = heat.templates =
heat_template_version.2013-05-23 = heat.engine.hot.template:HOTemplate heat_template_version.2013-05-23 = heat.engine.hot.template:HOTemplate
heat_template_version.2014-10-16 = heat.engine.hot.template:HOTemplate heat_template_version.2014-10-16 = heat.engine.hot.template:HOTemplate