Add breakpoint support
This covers most of the features specified in: http://specs.openstack.org/openstack/heat-specs/specs/juno/stack-breakpoint.html The breakpoints are specified via hooks in the stack's environment. The only thing missing from the blueprint is stepping through a stack. Partial-Blueprint: stack-breakpoint Change-Id: Iddc019464484af18ca6f21f11660649e30d63aca
This commit is contained in:
parent
073a9d3404
commit
f5e428a0bb
@ -129,3 +129,55 @@ resource. The supported URL types are "http, https and file".
|
|||||||
resources:
|
resources:
|
||||||
my_db_server:
|
my_db_server:
|
||||||
"OS::DBInstance": file:///home/mine/all_my_cool_templates/db.yaml
|
"OS::DBInstance": file:///home/mine/all_my_cool_templates/db.yaml
|
||||||
|
|
||||||
|
|
||||||
|
7) Pause stack creation/update on a given resource
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
If you want to debug your stack as it's being created or updated or if you want
|
||||||
|
to run it in phases you can set `pre-create` and `pre-update` hooks in the
|
||||||
|
`resources` section of `resource_registry`.
|
||||||
|
|
||||||
|
To set a hook, add either `hooks: pre-create` or `hooks: pre-update` to the
|
||||||
|
resource's dictionary. You can also use the `[pre-create, pre-update]` to stop
|
||||||
|
on both actions.
|
||||||
|
|
||||||
|
Hooks can be combined with other `resources` properties (e.g. provider
|
||||||
|
templates or type mapping).
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
::
|
||||||
|
|
||||||
|
resource_registry:
|
||||||
|
resources:
|
||||||
|
my_server:
|
||||||
|
"OS::DBInstance": file:///home/mine/all_my_cool_templates/db.yaml
|
||||||
|
hooks: pre-create
|
||||||
|
nested_stack:
|
||||||
|
nested_resource:
|
||||||
|
hooks: pre-update
|
||||||
|
another_resource:
|
||||||
|
hooks: [pre-create, pre-update]
|
||||||
|
|
||||||
|
When Heat encounters a resource that has a hook, it will pause the resource
|
||||||
|
action until the hook is cleared. Any resources that depend on it will wait as
|
||||||
|
well. Any resources that don't will be created in parallel (unless they have
|
||||||
|
hooks, too).
|
||||||
|
|
||||||
|
It is also possible to do a partial match by putting an asterisk (`*`) in the
|
||||||
|
name.
|
||||||
|
|
||||||
|
This example:
|
||||||
|
|
||||||
|
::
|
||||||
|
|
||||||
|
resource_registry:
|
||||||
|
resources:
|
||||||
|
"*_server":
|
||||||
|
hooks: pre-create
|
||||||
|
|
||||||
|
will pause while creating `app_server` and `database_server` but not `server`
|
||||||
|
or `app_network`.
|
||||||
|
|
||||||
|
Hook is cleared by signalling the resource with `{unset_hook: pre-create}` (or
|
||||||
|
`pre-update`).
|
||||||
|
@ -11,7 +11,9 @@
|
|||||||
# License for the specific language governing permissions and limitations
|
# License for the specific language governing permissions and limitations
|
||||||
# under the License.
|
# under the License.
|
||||||
|
|
||||||
|
import collections
|
||||||
import copy
|
import copy
|
||||||
|
import fnmatch
|
||||||
import glob
|
import glob
|
||||||
import itertools
|
import itertools
|
||||||
import os.path
|
import os.path
|
||||||
@ -32,6 +34,24 @@ from heat.engine import support
|
|||||||
LOG = log.getLogger(__name__)
|
LOG = log.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
HOOK_TYPES = (HOOK_PRE_CREATE, HOOK_PRE_UPDATE) = ('pre-create', 'pre-update')
|
||||||
|
|
||||||
|
|
||||||
|
def valid_hook_type(hook):
|
||||||
|
return hook in HOOK_TYPES
|
||||||
|
|
||||||
|
|
||||||
|
def is_hook_definition(key, value):
|
||||||
|
if key == 'hooks':
|
||||||
|
if isinstance(value, six.string_types):
|
||||||
|
return valid_hook_type(value)
|
||||||
|
elif isinstance(value, collections.Sequence):
|
||||||
|
return all(valid_hook_type(hook) for hook in value)
|
||||||
|
else:
|
||||||
|
return False
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
class ResourceInfo(object):
|
class ResourceInfo(object):
|
||||||
"""Base mapping of resource type to implementation."""
|
"""Base mapping of resource type to implementation."""
|
||||||
|
|
||||||
@ -174,12 +194,23 @@ class ResourceRegistry(object):
|
|||||||
for k, v in iter(registry.items()):
|
for k, v in iter(registry.items()):
|
||||||
if v is None:
|
if v is None:
|
||||||
self._register_info(path + [k], None)
|
self._register_info(path + [k], None)
|
||||||
|
elif is_hook_definition(k, v):
|
||||||
|
self._register_hook(path + [k], v)
|
||||||
elif isinstance(v, dict):
|
elif isinstance(v, dict):
|
||||||
self._load_registry(path + [k], v)
|
self._load_registry(path + [k], v)
|
||||||
else:
|
else:
|
||||||
self._register_info(path + [k],
|
self._register_info(path + [k],
|
||||||
ResourceInfo(self, path + [k], v))
|
ResourceInfo(self, path + [k], v))
|
||||||
|
|
||||||
|
def _register_hook(self, path, hook):
|
||||||
|
name = path[-1]
|
||||||
|
registry = self._registry
|
||||||
|
for key in path[:-1]:
|
||||||
|
if key not in registry:
|
||||||
|
registry[key] = {}
|
||||||
|
registry = registry[key]
|
||||||
|
registry[name] = hook
|
||||||
|
|
||||||
def _register_info(self, path, info):
|
def _register_info(self, path, info):
|
||||||
"""place the new info in the correct location in the registry.
|
"""place the new info in the correct location in the registry.
|
||||||
path: a list of keys ['resources', 'my_server', 'OS::Nova::Server']
|
path: a list of keys ['resources', 'my_server', 'OS::Nova::Server']
|
||||||
@ -242,6 +273,53 @@ class ResourceRegistry(object):
|
|||||||
if info.path[-1] in registry:
|
if info.path[-1] in registry:
|
||||||
registry.pop(info.path[-1])
|
registry.pop(info.path[-1])
|
||||||
|
|
||||||
|
def matches_hook(self, resource_name, hook):
|
||||||
|
'''Return whether a resource have a hook set in the environment.
|
||||||
|
|
||||||
|
For a given resource and a hook type, we check to see if the the passed
|
||||||
|
group of resources has the right hook associated with the name.
|
||||||
|
|
||||||
|
Hooks are set in this format via `resources`:
|
||||||
|
|
||||||
|
{
|
||||||
|
"res_name": {
|
||||||
|
"hooks": [pre-create, pre-update]
|
||||||
|
},
|
||||||
|
"*_suffix": {
|
||||||
|
"hooks": pre-create
|
||||||
|
},
|
||||||
|
"prefix_*": {
|
||||||
|
"hooks": pre-update
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
A hook value is either `pre-create`, `pre-update` or a list of those
|
||||||
|
values. Resources support wildcard matching. The asterisk sign matches
|
||||||
|
everything.
|
||||||
|
'''
|
||||||
|
ress = self._registry['resources']
|
||||||
|
for name_pattern, resource in six.iteritems(ress):
|
||||||
|
if fnmatch.fnmatchcase(resource_name, name_pattern):
|
||||||
|
if 'hooks' in resource:
|
||||||
|
hooks = resource['hooks']
|
||||||
|
if isinstance(hooks, six.string_types):
|
||||||
|
if hook == hooks:
|
||||||
|
return True
|
||||||
|
elif isinstance(hooks, collections.Sequence):
|
||||||
|
if hook in hooks:
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
def remove_resources_except(self, resource_name):
|
||||||
|
ress = self._registry['resources']
|
||||||
|
new_resources = {}
|
||||||
|
for name, res in six.iteritems(ress):
|
||||||
|
if fnmatch.fnmatchcase(resource_name, name):
|
||||||
|
new_resources.update(res)
|
||||||
|
if resource_name in ress:
|
||||||
|
new_resources.update(ress[resource_name])
|
||||||
|
self._registry['resources'] = new_resources
|
||||||
|
|
||||||
def iterable_by(self, resource_type, resource_name=None):
|
def iterable_by(self, resource_type, resource_name=None):
|
||||||
is_templ_type = resource_type.endswith(('.yaml', '.template'))
|
is_templ_type = resource_type.endswith(('.yaml', '.template'))
|
||||||
if self.global_registry is not None and is_templ_type:
|
if self.global_registry is not None and is_templ_type:
|
||||||
@ -334,6 +412,8 @@ class ResourceRegistry(object):
|
|||||||
for k, v in iter(level.items()):
|
for k, v in iter(level.items()):
|
||||||
if isinstance(v, dict):
|
if isinstance(v, dict):
|
||||||
tmp[k] = _as_dict(v)
|
tmp[k] = _as_dict(v)
|
||||||
|
elif is_hook_definition(k, v):
|
||||||
|
tmp[k] = v
|
||||||
elif v.user_resource:
|
elif v.user_resource:
|
||||||
tmp[k] = v.value
|
tmp[k] = v.value
|
||||||
return tmp
|
return tmp
|
||||||
@ -445,7 +525,8 @@ class Environment(object):
|
|||||||
return self.stack_lifecycle_plugins
|
return self.stack_lifecycle_plugins
|
||||||
|
|
||||||
|
|
||||||
def get_child_environment(parent_env, child_params, item_to_remove=None):
|
def get_child_environment(parent_env, child_params, item_to_remove=None,
|
||||||
|
child_resource_name=None):
|
||||||
"""Build a child environment using the parent environment and params.
|
"""Build a child environment using the parent environment and params.
|
||||||
|
|
||||||
This is built from the child_params and the parent env so some
|
This is built from the child_params and the parent env so some
|
||||||
@ -456,6 +537,10 @@ def get_child_environment(parent_env, child_params, item_to_remove=None):
|
|||||||
parent env to take presdence).
|
parent env to take presdence).
|
||||||
2. child parameters must overwrite the parent's as they won't be relevant
|
2. child parameters must overwrite the parent's as they won't be relevant
|
||||||
in the child template.
|
in the child template.
|
||||||
|
|
||||||
|
If `child_resource_name` is provided, resources in the registry will be
|
||||||
|
replaced with the contents of the matching child resource plus anything
|
||||||
|
that passes a wildcard match.
|
||||||
"""
|
"""
|
||||||
def is_flat_params(env_or_param):
|
def is_flat_params(env_or_param):
|
||||||
if env_or_param is None:
|
if env_or_param is None:
|
||||||
@ -478,6 +563,9 @@ def get_child_environment(parent_env, child_params, item_to_remove=None):
|
|||||||
|
|
||||||
if item_to_remove is not None:
|
if item_to_remove is not None:
|
||||||
new_env.registry.remove_item(item_to_remove)
|
new_env.registry.remove_item(item_to_remove)
|
||||||
|
|
||||||
|
if child_resource_name:
|
||||||
|
new_env.registry.remove_resources_except(child_resource_name)
|
||||||
return new_env
|
return new_env
|
||||||
|
|
||||||
|
|
||||||
|
@ -31,6 +31,7 @@ from heat.common import identifier
|
|||||||
from heat.common import short_id
|
from heat.common import short_id
|
||||||
from heat.common import timeutils
|
from heat.common import timeutils
|
||||||
from heat.engine import attributes
|
from heat.engine import attributes
|
||||||
|
from heat.engine import environment
|
||||||
from heat.engine import event
|
from heat.engine import event
|
||||||
from heat.engine import function
|
from heat.engine import function
|
||||||
from heat.engine import properties
|
from heat.engine import properties
|
||||||
@ -265,6 +266,34 @@ class Resource(object):
|
|||||||
rs.update_and_save({'rsrc_metadata': metadata})
|
rs.update_and_save({'rsrc_metadata': metadata})
|
||||||
self._rsrc_metadata = metadata
|
self._rsrc_metadata = metadata
|
||||||
|
|
||||||
|
def _break_if_required(self, action, hook):
|
||||||
|
'''Block the resource until the hook is cleared if there is one.'''
|
||||||
|
if self.stack.env.registry.matches_hook(self.name, hook):
|
||||||
|
self._add_event(self.action, self.status,
|
||||||
|
_("%(a)s paused until Hook %(h)s is cleared")
|
||||||
|
% {'a': action, 'h': hook})
|
||||||
|
self.trigger_hook(hook)
|
||||||
|
LOG.info(_LI('Reached hook on %s'), six.text_type(self))
|
||||||
|
while self.has_hook(hook) and self.status != self.FAILED:
|
||||||
|
try:
|
||||||
|
yield
|
||||||
|
except Exception:
|
||||||
|
self.clear_hook(hook)
|
||||||
|
self._add_event(
|
||||||
|
self.action, self.status,
|
||||||
|
"Failure occured while waiting.")
|
||||||
|
|
||||||
|
def has_hook(self, hook):
|
||||||
|
# Clear the cache to make sure the data is up to date:
|
||||||
|
self._data = None
|
||||||
|
return self.data().get(hook) == "True"
|
||||||
|
|
||||||
|
def trigger_hook(self, hook):
|
||||||
|
self.data_set(hook, "True")
|
||||||
|
|
||||||
|
def clear_hook(self, hook):
|
||||||
|
self.data_delete(hook)
|
||||||
|
|
||||||
def type(self):
|
def type(self):
|
||||||
return self.t.resource_type
|
return self.t.resource_type
|
||||||
|
|
||||||
@ -550,6 +579,13 @@ class Resource(object):
|
|||||||
% six.text_type(self.state))
|
% six.text_type(self.state))
|
||||||
raise exception.ResourceFailure(exc, self, action)
|
raise exception.ResourceFailure(exc, self, action)
|
||||||
|
|
||||||
|
# This method can be called when we replace a resource, too. In that
|
||||||
|
# case, a hook has already been dealt with in `Resource.update` so we
|
||||||
|
# shouldn't do it here again:
|
||||||
|
if self.stack.action == self.stack.CREATE:
|
||||||
|
yield self._break_if_required(
|
||||||
|
self.CREATE, environment.HOOK_PRE_CREATE)
|
||||||
|
|
||||||
LOG.info(_LI('creating %s'), six.text_type(self))
|
LOG.info(_LI('creating %s'), six.text_type(self))
|
||||||
|
|
||||||
# Re-resolve the template, since if the resource Ref's
|
# Re-resolve the template, since if the resource Ref's
|
||||||
@ -689,6 +725,9 @@ class Resource(object):
|
|||||||
after_props = after.properties(self.properties_schema,
|
after_props = after.properties(self.properties_schema,
|
||||||
self.context)
|
self.context)
|
||||||
|
|
||||||
|
yield self._break_if_required(
|
||||||
|
self.UPDATE, environment.HOOK_PRE_UPDATE)
|
||||||
|
|
||||||
if not self._needs_update(after, before, after_props, before_props,
|
if not self._needs_update(after, before, after_props, before_props,
|
||||||
prev_resource):
|
prev_resource):
|
||||||
return
|
return
|
||||||
@ -1081,6 +1120,20 @@ class Resource(object):
|
|||||||
return 'alarm state changed to %(state)s' % details
|
return 'alarm state changed to %(state)s' % details
|
||||||
|
|
||||||
return 'Unknown'
|
return 'Unknown'
|
||||||
|
|
||||||
|
# Clear the hook without interfering with resources'
|
||||||
|
# `handle_signal` callbacks:
|
||||||
|
if (details and 'unset_hook' in details and
|
||||||
|
environment.valid_hook_type(details.get('unset_hook'))):
|
||||||
|
hook = details['unset_hook']
|
||||||
|
if self.has_hook(hook):
|
||||||
|
self.clear_hook(hook)
|
||||||
|
LOG.info(_LI('Clearing %(hook)s hook on %(resource)s'),
|
||||||
|
{'hook': hook, 'resource': six.text_type(self)})
|
||||||
|
self._add_event(self.action, self.status,
|
||||||
|
"Hook %s is cleared" % hook)
|
||||||
|
return
|
||||||
|
|
||||||
if not callable(getattr(self, 'handle_signal', None)):
|
if not callable(getattr(self, 'handle_signal', None)):
|
||||||
raise exception.ResourceActionNotSupported(action='signal')
|
raise exception.ResourceActionNotSupported(action='signal')
|
||||||
|
|
||||||
|
@ -166,6 +166,7 @@ class StackResource(resource.Resource):
|
|||||||
|
|
||||||
child_env = environment.get_child_environment(
|
child_env = environment.get_child_environment(
|
||||||
self.stack.env, child_params,
|
self.stack.env, child_params,
|
||||||
|
child_resource_name=self.name,
|
||||||
item_to_remove=self.resource_info)
|
item_to_remove=self.resource_info)
|
||||||
|
|
||||||
parsed_template = self._child_parsed_template(child_template,
|
parsed_template = self._child_parsed_template(child_template,
|
||||||
@ -229,6 +230,7 @@ class StackResource(resource.Resource):
|
|||||||
child_env = environment.get_child_environment(
|
child_env = environment.get_child_environment(
|
||||||
self.stack.env,
|
self.stack.env,
|
||||||
user_params,
|
user_params,
|
||||||
|
child_resource_name=self.name,
|
||||||
item_to_remove=self.resource_info)
|
item_to_remove=self.resource_info)
|
||||||
|
|
||||||
new_nested_depth = self._child_nested_depth()
|
new_nested_depth = self._child_nested_depth()
|
||||||
@ -369,7 +371,9 @@ class StackResource(resource.Resource):
|
|||||||
|
|
||||||
child_env = environment.get_child_environment(
|
child_env = environment.get_child_environment(
|
||||||
self.stack.env,
|
self.stack.env,
|
||||||
user_params, item_to_remove=self.resource_info)
|
user_params,
|
||||||
|
child_resource_name=self.name,
|
||||||
|
item_to_remove=self.resource_info)
|
||||||
parsed_template = self._child_parsed_template(child_template,
|
parsed_template = self._child_parsed_template(child_template,
|
||||||
child_env)
|
child_env)
|
||||||
|
|
||||||
|
@ -549,3 +549,268 @@ class ChildEnvTest(common.HeatTestCase):
|
|||||||
cenv = environment.get_child_environment(penv, None)
|
cenv = environment.get_child_environment(penv, None)
|
||||||
res = cenv.get_resource_info('OS::Food', resource_name='abc')
|
res = cenv.get_resource_info('OS::Food', resource_name='abc')
|
||||||
self.assertIsNotNone(res)
|
self.assertIsNotNone(res)
|
||||||
|
|
||||||
|
def test_drill_down_to_child_resource(self):
|
||||||
|
env = {
|
||||||
|
u'resource_registry': {
|
||||||
|
u'OS::Food': u'fruity.yaml',
|
||||||
|
u'resources': {
|
||||||
|
u'a': {
|
||||||
|
u'OS::Fruit': u'apples.yaml',
|
||||||
|
u'hooks': 'pre-create',
|
||||||
|
},
|
||||||
|
u'nested': {
|
||||||
|
u'b': {
|
||||||
|
u'OS::Fruit': u'carrots.yaml',
|
||||||
|
},
|
||||||
|
u'nested_res': {
|
||||||
|
u'hooks': 'pre-create',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
penv = environment.Environment(env)
|
||||||
|
cenv = environment.get_child_environment(
|
||||||
|
penv, None, child_resource_name=u'nested')
|
||||||
|
registry = cenv.user_env_as_dict()['resource_registry']
|
||||||
|
resources = registry['resources']
|
||||||
|
self.assertIn('nested_res', resources)
|
||||||
|
self.assertIn('hooks', resources['nested_res'])
|
||||||
|
self.assertIsNotNone(
|
||||||
|
cenv.get_resource_info('OS::Food', resource_name='abc'))
|
||||||
|
self.assertIsNone(
|
||||||
|
cenv.get_resource_info('OS::Fruit', resource_name='a'))
|
||||||
|
res = cenv.get_resource_info('OS::Fruit', resource_name='b')
|
||||||
|
self.assertIsNotNone(res)
|
||||||
|
self.assertEqual(u'carrots.yaml', res.value)
|
||||||
|
|
||||||
|
def test_drill_down_non_matching_wildcard(self):
|
||||||
|
env = {
|
||||||
|
u'resource_registry': {
|
||||||
|
u'resources': {
|
||||||
|
u'nested': {
|
||||||
|
u'c': {
|
||||||
|
u'OS::Fruit': u'carrots.yaml',
|
||||||
|
u'hooks': 'pre-create',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
u'*_doesnt_match_nested': {
|
||||||
|
u'nested_res': {
|
||||||
|
u'hooks': 'pre-create',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
penv = environment.Environment(env)
|
||||||
|
cenv = environment.get_child_environment(
|
||||||
|
penv, None, child_resource_name=u'nested')
|
||||||
|
registry = cenv.user_env_as_dict()['resource_registry']
|
||||||
|
resources = registry['resources']
|
||||||
|
self.assertIn('c', resources)
|
||||||
|
self.assertNotIn('nested_res', resources)
|
||||||
|
res = cenv.get_resource_info('OS::Fruit', resource_name='c')
|
||||||
|
self.assertIsNotNone(res)
|
||||||
|
self.assertEqual(u'carrots.yaml', res.value)
|
||||||
|
|
||||||
|
def test_drill_down_matching_wildcard(self):
|
||||||
|
env = {
|
||||||
|
u'resource_registry': {
|
||||||
|
u'resources': {
|
||||||
|
u'nested': {
|
||||||
|
u'c': {
|
||||||
|
u'OS::Fruit': u'carrots.yaml',
|
||||||
|
u'hooks': 'pre-create',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
u'nest*': {
|
||||||
|
u'nested_res': {
|
||||||
|
u'hooks': 'pre-create',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
penv = environment.Environment(env)
|
||||||
|
cenv = environment.get_child_environment(
|
||||||
|
penv, None, child_resource_name=u'nested')
|
||||||
|
registry = cenv.user_env_as_dict()['resource_registry']
|
||||||
|
resources = registry['resources']
|
||||||
|
self.assertIn('c', resources)
|
||||||
|
self.assertIn('nested_res', resources)
|
||||||
|
res = cenv.get_resource_info('OS::Fruit', resource_name='c')
|
||||||
|
self.assertIsNotNone(res)
|
||||||
|
self.assertEqual(u'carrots.yaml', res.value)
|
||||||
|
|
||||||
|
def test_drill_down_prefer_exact_match(self):
|
||||||
|
env = {
|
||||||
|
u'resource_registry': {
|
||||||
|
u'resources': {
|
||||||
|
u'*esource': {
|
||||||
|
u'hooks': 'pre-create',
|
||||||
|
},
|
||||||
|
u'res*': {
|
||||||
|
u'hooks': 'pre-create',
|
||||||
|
},
|
||||||
|
u'resource': {
|
||||||
|
u'OS::Fruit': u'carrots.yaml',
|
||||||
|
u'hooks': 'pre-update',
|
||||||
|
},
|
||||||
|
u'resource*': {
|
||||||
|
u'hooks': 'pre-create',
|
||||||
|
},
|
||||||
|
u'*resource': {
|
||||||
|
u'hooks': 'pre-create',
|
||||||
|
},
|
||||||
|
u'*sour*': {
|
||||||
|
u'hooks': 'pre-create',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
penv = environment.Environment(env)
|
||||||
|
cenv = environment.get_child_environment(
|
||||||
|
penv, None, child_resource_name=u'resource')
|
||||||
|
registry = cenv.user_env_as_dict()['resource_registry']
|
||||||
|
resources = registry['resources']
|
||||||
|
self.assertEqual(u'carrots.yaml', resources[u'OS::Fruit'])
|
||||||
|
self.assertEqual('pre-update', resources[u'hooks'])
|
||||||
|
|
||||||
|
|
||||||
|
class ResourceRegistryTest(common.HeatTestCase):
|
||||||
|
|
||||||
|
def test_resources_load(self):
|
||||||
|
resources = {
|
||||||
|
u'pre_create': {
|
||||||
|
u'OS::Fruit': u'apples.yaml',
|
||||||
|
u'hooks': 'pre-create',
|
||||||
|
},
|
||||||
|
u'pre_update': {
|
||||||
|
u'hooks': 'pre-update',
|
||||||
|
},
|
||||||
|
u'both': {
|
||||||
|
u'hooks': ['pre-create', 'pre-update'],
|
||||||
|
},
|
||||||
|
u'b': {
|
||||||
|
u'OS::Food': u'fruity.yaml',
|
||||||
|
},
|
||||||
|
u'nested': {
|
||||||
|
u'res': {
|
||||||
|
u'hooks': 'pre-create',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
registry = environment.ResourceRegistry(None, {})
|
||||||
|
registry.load({'resources': resources})
|
||||||
|
self.assertIsNotNone(registry.get_resource_info(
|
||||||
|
'OS::Fruit', resource_name='pre_create'))
|
||||||
|
self.assertIsNotNone(registry.get_resource_info(
|
||||||
|
'OS::Food', resource_name='b'))
|
||||||
|
resources = registry.as_dict()['resources']
|
||||||
|
self.assertEqual('pre-create',
|
||||||
|
resources['pre_create']['hooks'])
|
||||||
|
self.assertEqual('pre-update',
|
||||||
|
resources['pre_update']['hooks'])
|
||||||
|
self.assertEqual(['pre-create', 'pre-update'],
|
||||||
|
resources['both']['hooks'])
|
||||||
|
self.assertEqual('pre-create',
|
||||||
|
resources['nested']['res']['hooks'])
|
||||||
|
|
||||||
|
|
||||||
|
class HookMatchTest(common.HeatTestCase):
|
||||||
|
|
||||||
|
def test_plain_matches(self):
|
||||||
|
resources = {
|
||||||
|
u'a': {
|
||||||
|
u'OS::Fruit': u'apples.yaml',
|
||||||
|
u'hooks': [u'pre-create', u'pre-update'],
|
||||||
|
},
|
||||||
|
u'b': {
|
||||||
|
u'OS::Food': u'fruity.yaml',
|
||||||
|
},
|
||||||
|
u'nested': {
|
||||||
|
u'res': {
|
||||||
|
u'hooks': 'pre-create',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
registry = environment.ResourceRegistry(None, {})
|
||||||
|
registry.load({
|
||||||
|
u'OS::Fruit': u'apples.yaml',
|
||||||
|
'resources': resources})
|
||||||
|
|
||||||
|
self.assertTrue(registry.matches_hook(
|
||||||
|
'a', environment.HOOK_PRE_CREATE))
|
||||||
|
self.assertFalse(registry.matches_hook(
|
||||||
|
'b', environment.HOOK_PRE_CREATE))
|
||||||
|
self.assertFalse(registry.matches_hook(
|
||||||
|
'OS::Fruit', environment.HOOK_PRE_CREATE))
|
||||||
|
self.assertFalse(registry.matches_hook(
|
||||||
|
'res', environment.HOOK_PRE_CREATE))
|
||||||
|
self.assertFalse(registry.matches_hook(
|
||||||
|
'unknown', environment.HOOK_PRE_CREATE))
|
||||||
|
|
||||||
|
def test_wildcard_matches(self):
|
||||||
|
resources = {
|
||||||
|
u'prefix_*': {
|
||||||
|
u'hooks': 'pre-create',
|
||||||
|
},
|
||||||
|
u'*_suffix': {
|
||||||
|
u'hooks': 'pre-create',
|
||||||
|
},
|
||||||
|
u'*': {
|
||||||
|
u'hooks': 'pre-update',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
registry = environment.ResourceRegistry(None, {})
|
||||||
|
registry.load({'resources': resources})
|
||||||
|
|
||||||
|
self.assertTrue(registry.matches_hook(
|
||||||
|
'prefix_', environment.HOOK_PRE_CREATE))
|
||||||
|
self.assertTrue(registry.matches_hook(
|
||||||
|
'prefix_some', environment.HOOK_PRE_CREATE))
|
||||||
|
self.assertFalse(registry.matches_hook(
|
||||||
|
'some_prefix', environment.HOOK_PRE_CREATE))
|
||||||
|
|
||||||
|
self.assertTrue(registry.matches_hook(
|
||||||
|
'_suffix', environment.HOOK_PRE_CREATE))
|
||||||
|
self.assertTrue(registry.matches_hook(
|
||||||
|
'some_suffix', environment.HOOK_PRE_CREATE))
|
||||||
|
self.assertFalse(registry.matches_hook(
|
||||||
|
'_suffix_blah', environment.HOOK_PRE_CREATE))
|
||||||
|
|
||||||
|
self.assertTrue(registry.matches_hook(
|
||||||
|
'some_prefix', environment.HOOK_PRE_UPDATE))
|
||||||
|
self.assertTrue(registry.matches_hook(
|
||||||
|
'_suffix_blah', environment.HOOK_PRE_UPDATE))
|
||||||
|
|
||||||
|
def test_hook_types(self):
|
||||||
|
resources = {
|
||||||
|
u'pre_create': {
|
||||||
|
u'hooks': 'pre-create',
|
||||||
|
},
|
||||||
|
u'pre_update': {
|
||||||
|
u'hooks': 'pre-update',
|
||||||
|
},
|
||||||
|
u'both': {
|
||||||
|
u'hooks': ['pre-create', 'pre-update'],
|
||||||
|
},
|
||||||
|
}
|
||||||
|
registry = environment.ResourceRegistry(None, {})
|
||||||
|
registry.load({'resources': resources})
|
||||||
|
|
||||||
|
self.assertTrue(registry.matches_hook(
|
||||||
|
'pre_create', environment.HOOK_PRE_CREATE))
|
||||||
|
self.assertFalse(registry.matches_hook(
|
||||||
|
'pre_create', environment.HOOK_PRE_UPDATE))
|
||||||
|
|
||||||
|
self.assertTrue(registry.matches_hook(
|
||||||
|
'pre_update', environment.HOOK_PRE_UPDATE))
|
||||||
|
self.assertFalse(registry.matches_hook(
|
||||||
|
'pre_update', environment.HOOK_PRE_CREATE))
|
||||||
|
|
||||||
|
self.assertTrue(registry.matches_hook(
|
||||||
|
'both', environment.HOOK_PRE_CREATE))
|
||||||
|
self.assertTrue(registry.matches_hook(
|
||||||
|
'both', environment.HOOK_PRE_UPDATE))
|
||||||
|
@ -1812,3 +1812,92 @@ class ReducePhysicalResourceNameTest(common.HeatTestCase):
|
|||||||
else:
|
else:
|
||||||
# check that nothing has changed
|
# check that nothing has changed
|
||||||
self.assertEqual(self.original, reduced)
|
self.assertEqual(self.original, reduced)
|
||||||
|
|
||||||
|
|
||||||
|
class ResourceHookTest(common.HeatTestCase):
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
super(ResourceHookTest, self).setUp()
|
||||||
|
|
||||||
|
resource._register_class('GenericResourceType',
|
||||||
|
generic_rsrc.GenericResource)
|
||||||
|
resource._register_class('ResourceWithCustomConstraint',
|
||||||
|
generic_rsrc.ResourceWithCustomConstraint)
|
||||||
|
|
||||||
|
self.env = environment.Environment()
|
||||||
|
self.env.load({u'resource_registry':
|
||||||
|
{u'OS::Test::GenericResource': u'GenericResourceType',
|
||||||
|
u'OS::Test::ResourceWithCustomConstraint':
|
||||||
|
u'ResourceWithCustomConstraint'}})
|
||||||
|
|
||||||
|
self.stack = parser.Stack(utils.dummy_context(), 'test_stack',
|
||||||
|
parser.Template(empty_template,
|
||||||
|
env=self.env),
|
||||||
|
stack_id=str(uuid.uuid4()))
|
||||||
|
|
||||||
|
def test_hook(self):
|
||||||
|
snippet = rsrc_defn.ResourceDefinition('res',
|
||||||
|
'GenericResourceType')
|
||||||
|
res = resource.Resource('res', snippet, self.stack)
|
||||||
|
|
||||||
|
res.data = mock.Mock(return_value={})
|
||||||
|
self.assertFalse(res.has_hook('pre-create'))
|
||||||
|
self.assertFalse(res.has_hook('pre-update'))
|
||||||
|
|
||||||
|
res.data = mock.Mock(return_value={'pre-create': 'True'})
|
||||||
|
self.assertTrue(res.has_hook('pre-create'))
|
||||||
|
self.assertFalse(res.has_hook('pre-update'))
|
||||||
|
|
||||||
|
res.data = mock.Mock(return_value={'pre-create': 'False'})
|
||||||
|
self.assertFalse(res.has_hook('pre-create'))
|
||||||
|
self.assertFalse(res.has_hook('pre-update'))
|
||||||
|
|
||||||
|
res.data = mock.Mock(return_value={'pre-update': 'True'})
|
||||||
|
self.assertFalse(res.has_hook('pre-create'))
|
||||||
|
self.assertTrue(res.has_hook('pre-update'))
|
||||||
|
|
||||||
|
def test_set_hook(self):
|
||||||
|
snippet = rsrc_defn.ResourceDefinition('res',
|
||||||
|
'GenericResourceType')
|
||||||
|
res = resource.Resource('res', snippet, self.stack)
|
||||||
|
|
||||||
|
res.data_set = mock.Mock()
|
||||||
|
res.data_delete = mock.Mock()
|
||||||
|
|
||||||
|
res.trigger_hook('pre-create')
|
||||||
|
res.data_set.assert_called_with('pre-create', 'True')
|
||||||
|
|
||||||
|
res.trigger_hook('pre-update')
|
||||||
|
res.data_set.assert_called_with('pre-update', 'True')
|
||||||
|
|
||||||
|
res.clear_hook('pre-create')
|
||||||
|
res.data_delete.assert_called_with('pre-create')
|
||||||
|
|
||||||
|
def test_signal_clear_hook(self):
|
||||||
|
snippet = rsrc_defn.ResourceDefinition('res',
|
||||||
|
'GenericResourceType')
|
||||||
|
res = resource.Resource('res', snippet, self.stack)
|
||||||
|
|
||||||
|
res.clear_hook = mock.Mock()
|
||||||
|
res.has_hook = mock.Mock(return_value=True)
|
||||||
|
self.assertRaises(exception.ResourceActionNotSupported,
|
||||||
|
res.signal, None)
|
||||||
|
self.assertFalse(res.clear_hook.called)
|
||||||
|
|
||||||
|
self.assertRaises(exception.ResourceActionNotSupported,
|
||||||
|
res.signal, {})
|
||||||
|
self.assertFalse(res.clear_hook.called)
|
||||||
|
|
||||||
|
self.assertRaises(exception.ResourceActionNotSupported,
|
||||||
|
res.signal, {'unset_hook': 'unknown_hook'})
|
||||||
|
self.assertFalse(res.clear_hook.called)
|
||||||
|
|
||||||
|
res.signal({'unset_hook': 'pre-create'})
|
||||||
|
res.clear_hook.assert_called_with('pre-create')
|
||||||
|
|
||||||
|
res.signal({'unset_hook': 'pre-update'})
|
||||||
|
res.clear_hook.assert_called_with('pre-update')
|
||||||
|
|
||||||
|
res.has_hook = mock.Mock(return_value=False)
|
||||||
|
self.assertRaises(exception.ResourceActionNotSupported,
|
||||||
|
res.signal, {'unset_hook': 'pre-create'})
|
||||||
|
@ -237,8 +237,11 @@ class StackResourceTest(common.HeatTestCase):
|
|||||||
parent_resource._validate_nested_resources = validation_mock
|
parent_resource._validate_nested_resources = validation_mock
|
||||||
|
|
||||||
result = parent_resource.preview()
|
result = parent_resource.preview()
|
||||||
mock_env_class.assert_called_once_with(self.parent_stack.env,
|
mock_env_class.assert_called_once_with(
|
||||||
params, item_to_remove=None)
|
self.parent_stack.env,
|
||||||
|
params,
|
||||||
|
child_resource_name='test',
|
||||||
|
item_to_remove=None)
|
||||||
self.assertEqual('preview_nested_stack', result)
|
self.assertEqual('preview_nested_stack', result)
|
||||||
mock_stack_class.assert_called_once_with(
|
mock_stack_class.assert_called_once_with(
|
||||||
mock.ANY,
|
mock.ANY,
|
||||||
@ -276,8 +279,11 @@ class StackResourceTest(common.HeatTestCase):
|
|||||||
parent_resource._validate_nested_resources = validation_mock
|
parent_resource._validate_nested_resources = validation_mock
|
||||||
|
|
||||||
result = parent_resource.preview()
|
result = parent_resource.preview()
|
||||||
mock_env_class.assert_called_once_with(self.parent_stack.env,
|
mock_env_class.assert_called_once_with(
|
||||||
params, item_to_remove=None)
|
self.parent_stack.env,
|
||||||
|
params,
|
||||||
|
child_resource_name='test',
|
||||||
|
item_to_remove=None)
|
||||||
self.assertEqual('preview_nested_stack', result)
|
self.assertEqual('preview_nested_stack', result)
|
||||||
mock_stack_class.assert_called_once_with(
|
mock_stack_class.assert_called_once_with(
|
||||||
mock.ANY,
|
mock.ANY,
|
||||||
|
Loading…
Reference in New Issue
Block a user