Skip to content

Commit

Permalink
Implements stack lifecycle plugpoints
Browse files Browse the repository at this point in the history
Stack lifecycle plugpoints have been proposed in
http://summit.openstack.org/cfp/details/86
and
https://etherpad.openstack.org/p/juno-summit-heat-callbacks

Implements: blueprint stack-lifecycle-plugpoint
Change-Id: I8c7b5d0113392e54fe0f35933c2c10da277fd90b
  • Loading branch information
BillArnold committed Aug 31, 2014
1 parent 3547e29 commit 2f56353
Show file tree
Hide file tree
Showing 7 changed files with 486 additions and 0 deletions.
117 changes: 117 additions & 0 deletions heat/common/lifecycle_plugin_utils.py
Original file line number Diff line number Diff line change
@@ -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)
9 changes: 9 additions & 0 deletions heat/engine/environment.py
Original file line number Diff line number Diff line change
Expand Up @@ -364,6 +364,7 @@ def __init__(self, env=None, user_env=True):
self.params = dict((k, v) for (k, v) in six.iteritems(env)
if k != RESOURCE_REGISTRY)
self.constraints = {}
self.stack_lifecycle_plugins = []

def load(self, env_snippet):
self.registry.load(env_snippet.get(RESOURCE_REGISTRY, {}))
Expand All @@ -380,6 +381,11 @@ def register_class(self, resource_type, resource_class):
def register_constraint(self, 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):
return self.registry.get_class(resource_type, resource_name)

Expand All @@ -394,6 +400,9 @@ def get_resource_info(self, resource_type, resource_name=None,
def get_constraint(self, 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):
if env_dir is None:
Expand Down
54 changes: 54 additions & 0 deletions heat/engine/lifecycle_plugin.py
Original file line number Diff line number Diff line change
@@ -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
9 changes: 9 additions & 0 deletions heat/engine/resources/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,12 @@ def _register_constraints(env, type_pairs):
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):
mgr = extension.ExtensionManager(
namespace=namespace,
Expand Down Expand Up @@ -64,6 +70,9 @@ def _load_global_environment(env):

def _load_global_resources(env):
_register_constraints(env, _get_mapping('heat.constraints'))
_register_stack_lifecycle_plugins(
env,
_get_mapping('heat.stack_lifecycle_plugins'))

manager = plugin_manager.PluginManager(__name__)
# Sometimes resources should not be available for registration in Heat due
Expand Down
37 changes: 37 additions & 0 deletions heat/engine/stack.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
from heat.common import exception
from heat.common.exception import StackValidationFailed
from heat.common import identifier
from heat.common import lifecycle_plugin_utils
from heat.db import api as db_api
from heat.engine import dependencies
from heat.engine import environment
Expand Down Expand Up @@ -545,6 +546,15 @@ def stack_task(self, action, reverse=False, post_func=None,
A task to perform an action on the stack and all of the resources
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,
'Stack %s started' % action)

Expand Down Expand Up @@ -581,6 +591,8 @@ def resource_action(r):

if callable(post_func):
post_func()
lifecycle_plugin_utils.do_post_ops(self.context, self, None, action,
(self.status == self.FAILED))

def check(self):
self.updated_time = datetime.utcnow()
Expand Down Expand Up @@ -667,6 +679,13 @@ def update_task(self, newstack, action=UPDATE):
"Invalid action %s" % action)
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 action == self.ROLLBACK:
LOG.debug("Starting update rollback for %s" % self.name)
Expand Down Expand Up @@ -739,6 +758,9 @@ def update_task(self, newstack, action=UPDATE):
self.status_reason = reason

self.store()
lifecycle_plugin_utils.do_post_ops(self.context, self,
newstack, action,
(self.status == self.FAILED))

notification.send(self)

Expand All @@ -756,6 +778,8 @@ def delete(self, action=DELETE, backup=False):
"Invalid action %s" % action)
return

# Note abandon is a delete with
# stack.set_deletion_policy(resource.RETAIN)
stack_status = self.COMPLETE
reason = 'Stack %s completed successfully' % action
self.state_set(action, self.IN_PROGRESS, 'Stack %s started' %
Expand Down Expand Up @@ -810,6 +834,15 @@ def failed(child):
for snapshot in snapshots:
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,
resource.Resource.destroy,
reverse=True)
Expand Down Expand Up @@ -874,6 +907,10 @@ def failed(child):
LOG.info(_("Tried to delete stack that does not exist "
"%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:
# delete the stack
try:
Expand Down
Loading

0 comments on commit 2f56353

Please sign in to comment.