Merge "Restrict update/replace of resource"

This commit is contained in:
Jenkins 2016-02-09 22:51:30 +00:00 committed by Gerrit Code Review
commit a6bd36a57d
8 changed files with 470 additions and 36 deletions

View File

@ -191,3 +191,37 @@ You can specify endpoints using the ``event_sinks`` property::
- type: zaqar-queue
target: myqueue
ttl: 1200
Restrict update or replace of a given resource
-----------------------------------------------
If you want to restrict update or replace of a resource when your stack is
being updated, you can set ``restricted_actions`` in the ``resources``
section of ``resource_registry``.
To restrict update or replace, add ``restricted_actions: update`` or
``restricted_actions: replace`` to the resource dictionary. You can also
use ``[update, replace]`` to restrict both actions.
You can combine restrcited actions with other ``resources`` properties such
as provider templates or type mapping or hooks::
resource_registry:
resources:
my_server:
"OS::DBInstance": file:///home/mine/all_my_cool_templates/db.yaml
restricted_actions: replace
hooks: pre-create
nested_stack:
nested_resource:
restricted_actions: update
another_resource:
restricted_actions: [update, replace]
It is possible to perform a wild card match using an asterisk (`*`) in the
resource name. For example, the following entry restricts replace for
``app_server`` and ``database_server``, but not ``server`` or ``app_network``::
resource_registry:
resources:
"*_server":
restricted_actions: replace

View File

@ -236,6 +236,10 @@ class InvalidBreakPointHook(HeatException):
msg_fmt = _("%(message)s")
class InvalidRestrictedAction(HeatException):
msg_fmt = _("%(message)s")
class ResourceNotAvailable(HeatException):
msg_fmt = _("The Resource (%(resource_name)s) is not available.")
@ -312,6 +316,10 @@ class ResourceActionNotSupported(HeatException):
msg_fmt = _("%(action)s is not supported for resource.")
class ResourceActionRestricted(HeatException):
msg_fmt = _("%(action)s is restricted for resource.")
class ResourcePropertyConflict(HeatException):
msg_fmt = _('Cannot define the following properties '
'at the same time: %(props)s.')

View File

@ -40,11 +40,17 @@ HOOK_TYPES = (
'pre-create', 'pre-update', 'pre-delete'
)
RESTRICTED_ACTIONS = (UPDATE, REPLACE) = ('update', 'replace')
def valid_hook_type(hook):
return hook in HOOK_TYPES
def valid_restricted_actions(action):
return action in RESTRICTED_ACTIONS
def is_hook_definition(key, value):
is_valid_hook = False
if key == 'hooks':
@ -62,6 +68,25 @@ def is_hook_definition(key, value):
return is_valid_hook
def is_valid_restricted_action(key, value):
valid_action = False
if key == 'restricted_actions':
if isinstance(value, six.string_types):
valid_action = valid_restricted_actions(value)
elif isinstance(value, collections.Sequence):
valid_action = all(valid_restricted_actions(
action) for action in value)
if not valid_action:
msg = (_('Invalid restricted_action type "%(value)s" for '
'resource, acceptable restricted_action '
'types are: %(types)s') %
{'value': value, 'types': RESTRICTED_ACTIONS})
raise exception.InvalidRestrictedAction(message=msg)
return valid_action
class ResourceInfo(object):
"""Base mapping of resource type to implementation."""
@ -239,22 +264,22 @@ class ResourceRegistry(object):
for k, v in iter(registry.items()):
if v is None:
self._register_info(path + [k], None)
elif is_hook_definition(k, v):
self._register_hook(path + [k], v)
elif is_hook_definition(k, v) or is_valid_restricted_action(k, v):
self._register_item(path + [k], v)
elif isinstance(v, dict):
self._load_registry(path + [k], v)
else:
self._register_info(path + [k],
ResourceInfo(self, path + [k], v))
def _register_hook(self, path, hook):
def _register_item(self, path, item):
name = path[-1]
registry = self._registry
for key in path[:-1]:
if key not in registry:
registry[key] = {}
registry = registry[key]
registry[name] = hook
registry[name] = item
def _register_info(self, path, info):
"""Place the new info in the correct location in the registry.
@ -326,6 +351,33 @@ class ResourceRegistry(object):
if info.path[-1] in registry:
registry.pop(info.path[-1])
def get_rsrc_restricted_actions(self, resource_name):
"""Returns a set of restricted actions.
For a given resource we get the set of restricted actions.
Actions are set in this format via `resources`:
{
"restricted_actions": [update, replace]
}
A restricted_actions value is either `update`, `replace` or a list
of those values. Resources support wildcard matching. The asterisk
sign matches everything.
"""
ress = self._registry['resources']
restricted_actions = set()
for name_pattern, resource in six.iteritems(ress):
if fnmatch.fnmatchcase(resource_name, name_pattern):
if 'restricted_actions' in resource:
actions = resource['restricted_actions']
if isinstance(actions, six.string_types):
restricted_actions.add(actions)
elif isinstance(actions, collections.Sequence):
restricted_actions |= set(actions)
return restricted_actions
def matches_hook(self, resource_name, hook):
"""Return whether a resource have a hook set in the environment.
@ -491,7 +543,8 @@ class ResourceRegistry(object):
for k, v in iter(level.items()):
if isinstance(v, dict):
tmp[k] = _as_dict(v)
elif is_hook_definition(k, v):
elif is_hook_definition(
k, v) or is_valid_restricted_action(k, v):
tmp[k] = v
elif v.user_resource:
tmp[k] = v.value

View File

@ -930,6 +930,46 @@ class Resource(object):
update_tmpl_id_and_requires()
raise
def preview_update(self, after, before, after_props, before_props,
prev_resource, check_init_complete=False):
"""Simulates update without actually updating the resource.
Raises UpdateReplace, if replacement is required or returns True,
if in-place update is required.
"""
if self._needs_update(after, before, after_props, before_props,
prev_resource, check_init_complete):
tmpl_diff = self.update_template_diff(function.resolve(after),
before)
if tmpl_diff and self.needs_replace_with_tmpl_diff(tmpl_diff):
raise exception.UpdateReplace(self)
self.update_template_diff_properties(after_props,
before_props)
return True
def _check_restricted_actions(self, actions, after, before,
after_porps, before_props,
prev_resource):
"""Checks for restricted actions.
Raises ResourceActionRestricted, if the resource requires update
or replace and the required action is restricted.
Else, Raises UpdateReplace, if replacement is required or returns
True, if in-place update is required.
"""
try:
if self.preview_update(after, before, after_porps, before_props,
prev_resource, check_init_complete=True):
if 'update' in actions:
raise exception.ResourceActionRestricted(action='update')
return True
except exception.UpdateReplace:
if 'replace' in actions:
raise exception.ResourceActionRestricted(action='replace')
raise
@scheduler.wrappertask
def update(self, after, before=None, prev_resource=None):
"""Return a task to update the resource.
@ -974,9 +1014,21 @@ class Resource(object):
self.UPDATE, environment.HOOK_PRE_UPDATE)
try:
if not self._needs_update(after, before, after_props, before_props,
prev_resource):
return
registry = self.stack.env.registry
restr_actions = registry.get_rsrc_restricted_actions(self.name)
if restr_actions:
if not self._check_restricted_actions(restr_actions,
after, before,
after_props,
before_props,
prev_resource):
return
else:
if not self._needs_update(after, before,
after_props, before_props,
prev_resource):
return
if not cfg.CONF.convergence_engine:
if (self.action, self.status) in (
(self.CREATE, self.IN_PROGRESS),
@ -988,6 +1040,7 @@ class Resource(object):
LOG.info(_LI('updating %s'), six.text_type(self))
self.updated_time = datetime.utcnow()
with self._action_recorder(action, exception.UpdateReplace):
after_props.validate()
@ -1006,8 +1059,14 @@ class Resource(object):
self.t = after
self.reparse()
self._update_stored_properties()
except exception.ResourceActionRestricted as ae:
# catch all ResourceActionRestricted exceptions
failure = exception.ResourceFailure(ae, self, action)
self._add_event(action, self.FAILED, six.text_type(ae))
raise failure
except exception.UpdateReplace:
# catch all UpdateReplace expections
# catch all UpdateReplace exceptions
try:
if (self.stack.action == 'ROLLBACK' and
self.stack.status == 'IN_PROGRESS' and

View File

@ -132,32 +132,47 @@ class StackUpdate(object):
yield new_res.create()
def _check_replace_restricted(self, res):
registry = res.stack.env.registry
restricted_actions = registry.get_rsrc_restricted_actions(res.name)
existing_res = self.existing_stack[res.name]
if 'replace' in restricted_actions:
ex = exception.ResourceActionRestricted(action='replace')
failure = exception.ResourceFailure(ex, existing_res,
existing_res.UPDATE)
existing_res._add_event(existing_res.UPDATE, existing_res.FAILED,
six.text_type(ex))
raise failure
@scheduler.wrappertask
def _process_new_resource_update(self, new_res):
res_name = new_res.name
if (res_name in self.existing_stack and
type(self.existing_stack[res_name]) is type(new_res)):
existing_res = self.existing_stack[res_name]
try:
yield self._update_in_place(existing_res,
new_res)
except exception.UpdateReplace:
pass
else:
# Save updated resource definition to backup stack
# cause it allows the backup stack resources to be synchronized
LOG.debug("Backing up updated Resource %s" % res_name)
definition = existing_res.t.reparse(self.previous_stack,
existing_res.stack.t)
self.previous_stack.t.add_resource(definition)
self.previous_stack.t.store(self.previous_stack.context)
if res_name in self.existing_stack:
if type(self.existing_stack[res_name]) is type(new_res):
existing_res = self.existing_stack[res_name]
try:
yield self._update_in_place(existing_res,
new_res)
except exception.UpdateReplace:
pass
else:
# Save updated resource definition to backup stack
# cause it allows the backup stack resources to be
# synchronized
LOG.debug("Backing up updated Resource %s" % res_name)
definition = existing_res.t.reparse(self.previous_stack,
existing_res.stack.t)
self.previous_stack.t.add_resource(definition)
self.previous_stack.t.store(self.previous_stack.context)
LOG.info(_LI("Resource %(res_name)s for stack %(stack_name)s "
"updated"),
{'res_name': res_name,
'stack_name': self.existing_stack.name})
return
LOG.info(_LI("Resource %(res_name)s for stack "
"%(stack_name)s updated"),
{'res_name': res_name,
'stack_name': self.existing_stack.name})
return
else:
self._check_replace_restricted(new_res)
yield self._create_resource(new_res)
@ -244,12 +259,10 @@ class StackUpdate(object):
continue
try:
if current_res._needs_update(updated_res.frozen_definition(),
current_res.frozen_definition(),
updated_props, current_props,
None, check_init_complete=False):
current_res.update_template_diff_properties(updated_props,
current_props)
if current_res.preview_update(updated_res.frozen_definition(),
current_res.frozen_definition(),
updated_props, current_props,
None):
updated_keys.append(key)
except exception.UpdateReplace:
replaced_keys.append(key)

View File

@ -961,3 +961,101 @@ class HookMatchTest(common.HeatTestCase):
'all', environment.HOOK_PRE_UPDATE))
self.assertTrue(registry.matches_hook(
'all', environment.HOOK_PRE_DELETE))
class ActionRestrictedTest(common.HeatTestCase):
def test_plain_matches(self):
resources = {
u'a': {
u'OS::Fruit': u'apples.yaml',
u'restricted_actions': [u'update', u'replace'],
},
u'b': {
u'OS::Food': u'fruity.yaml',
},
u'nested': {
u'res': {
u'restricted_actions': 'update',
},
},
}
registry = environment.ResourceRegistry(None, {})
registry.load({
u'OS::Fruit': u'apples.yaml',
'resources': resources})
self.assertIn(environment.UPDATE,
registry.get_rsrc_restricted_actions('a'))
self.assertNotIn(environment.UPDATE,
registry.get_rsrc_restricted_actions('b'))
self.assertNotIn(environment.UPDATE,
registry.get_rsrc_restricted_actions('OS::Fruit'))
self.assertNotIn(environment.UPDATE,
registry.get_rsrc_restricted_actions('res'))
self.assertNotIn(environment.UPDATE,
registry.get_rsrc_restricted_actions('unknown'))
def test_wildcard_matches(self):
resources = {
u'prefix_*': {
u'restricted_actions': 'update',
},
u'*_suffix': {
u'restricted_actions': 'update',
},
u'*': {
u'restricted_actions': 'replace',
},
}
registry = environment.ResourceRegistry(None, {})
registry.load({'resources': resources})
self.assertIn(environment.UPDATE,
registry.get_rsrc_restricted_actions('prefix_'))
self.assertIn(environment.UPDATE,
registry.get_rsrc_restricted_actions('prefix_some'))
self.assertNotIn(environment.UPDATE,
registry.get_rsrc_restricted_actions('some_prefix'))
self.assertIn(environment.UPDATE,
registry.get_rsrc_restricted_actions('_suffix'))
self.assertIn(environment.UPDATE,
registry.get_rsrc_restricted_actions('some_suffix'))
self.assertNotIn(environment.UPDATE,
registry.get_rsrc_restricted_actions('_suffix_blah'))
self.assertIn(environment.REPLACE,
registry.get_rsrc_restricted_actions('some_prefix'))
self.assertIn(environment.REPLACE,
registry.get_rsrc_restricted_actions('_suffix_blah'))
def test_restricted_action_types(self):
resources = {
u'update': {
u'restricted_actions': 'update',
},
u'replace': {
u'restricted_actions': 'replace',
},
u'all': {
u'restricted_actions': ['update', 'replace'],
},
}
registry = environment.ResourceRegistry(None, {})
registry.load({'resources': resources})
self.assertIn(environment.UPDATE,
registry.get_rsrc_restricted_actions('update'))
self.assertNotIn(environment.UPDATE,
registry.get_rsrc_restricted_actions('replace'))
self.assertIn(environment.REPLACE,
registry.get_rsrc_restricted_actions('replace'))
self.assertNotIn(environment.REPLACE,
registry.get_rsrc_restricted_actions('update'))
self.assertIn(environment.UPDATE,
registry.get_rsrc_restricted_actions('all'))
self.assertIn(environment.REPLACE,
registry.get_rsrc_restricted_actions('all'))

View File

@ -36,6 +36,8 @@ from heat.engine import environment
from heat.engine import properties
from heat.engine import resource
from heat.engine import resources
from heat.engine.resources.openstack.heat import none_resource
from heat.engine.resources.openstack.heat import test_resource
from heat.engine import rsrc_defn
from heat.engine import scheduler
from heat.engine import stack as parser
@ -3399,3 +3401,127 @@ class TestLiveStateUpdate(common.HeatTestCase):
# schema for correct work of other tests.
for prop in six.itervalues(res.properties.props):
prop.schema.update_allowed = False
class ResourceUpdateRestrictionTest(common.HeatTestCase):
def setUp(self):
super(ResourceUpdateRestrictionTest, self).setUp()
resource._register_class('TestResourceType',
test_resource.TestResource)
resource._register_class('NoneResourceType',
none_resource.NoneResource)
self.tmpl = {
'heat_template_version': '2013-05-23',
'resources': {
'bar': {
'type': 'TestResourceType',
'properties': {
'value': '1234',
'update_replace': False
}
}
}
}
def create_resource(self):
self.stack = parser.Stack(utils.dummy_context(), 'test_stack',
template.Template(self.tmpl, env=self.env),
stack_id=str(uuid.uuid4()))
res = self.stack['bar']
scheduler.TaskRunner(res.create)()
return res
def test_update_restricted(self):
self.env_snippet = {u'resource_registry': {
u'resources': {
'bar': {'restricted_actions': 'update'}
}
}
}
self.env = environment.Environment()
self.env.load(self.env_snippet)
res = self.create_resource()
ev = self.patchobject(res, '_add_event')
props = self.tmpl['resources']['bar']['properties']
props['value'] = '4567'
snippet = rsrc_defn.ResourceDefinition('bar',
'TestResourceType',
props)
error = self.assertRaises(exception.ResourceFailure,
scheduler.TaskRunner(res.update, snippet))
self.assertEqual('ResourceActionRestricted: resources.bar: '
'update is restricted for resource.',
six.text_type(error))
self.assertEqual((res.CREATE, res.COMPLETE), res.state)
ev.assert_called_with(res.UPDATE, res.FAILED,
'update is restricted for resource.')
def test_replace_rstricted(self):
self.env_snippet = {u'resource_registry': {
u'resources': {
'bar': {'restricted_actions': 'replace'}
}
}
}
self.env = environment.Environment()
self.env.load(self.env_snippet)
res = self.create_resource()
ev = self.patchobject(res, '_add_event')
props = self.tmpl['resources']['bar']['properties']
props['update_replace'] = True
snippet = rsrc_defn.ResourceDefinition('bar',
'TestResourceType',
props)
error = self.assertRaises(exception.ResourceFailure,
scheduler.TaskRunner(res.update, snippet))
self.assertEqual('ResourceActionRestricted: resources.bar: '
'replace is restricted for resource.',
six.text_type(error))
self.assertEqual((res.CREATE, res.COMPLETE), res.state)
ev.assert_called_with(res.UPDATE, res.FAILED,
'replace is restricted for resource.')
def test_update_with_replace_rstricted(self):
self.env_snippet = {u'resource_registry': {
u'resources': {
'bar': {'restricted_actions': 'replace'}
}
}
}
self.env = environment.Environment()
self.env.load(self.env_snippet)
res = self.create_resource()
ev = self.patchobject(res, '_add_event')
props = self.tmpl['resources']['bar']['properties']
props['value'] = '4567'
snippet = rsrc_defn.ResourceDefinition('bar',
'TestResourceType',
props)
self.assertIsNone(scheduler.TaskRunner(res.update, snippet)())
self.assertEqual((res.UPDATE, res.COMPLETE), res.state)
ev.assert_called_with(res.UPDATE, res.COMPLETE,
'state changed')
def test_replace_with_update_rstricted(self):
self.env_snippet = {u'resource_registry': {
u'resources': {
'bar': {'restricted_actions': 'update'}
}
}
}
self.env = environment.Environment()
self.env.load(self.env_snippet)
res = self.create_resource()
ev = self.patchobject(res, '_add_event')
prep_replace = self.patchobject(res, 'prepare_for_replace')
props = self.tmpl['resources']['bar']['properties']
props['update_replace'] = True
snippet = rsrc_defn.ResourceDefinition('bar',
'TestResourceType',
props)
error = self.assertRaises(exception.UpdateReplace,
scheduler.TaskRunner(res.update, snippet))
self.assertIn('requires replacement', six.text_type(error))
self.assertEqual(1, prep_replace.call_count)
ev.assert_not_called()

View File

@ -262,6 +262,49 @@ class StackUpdateTest(common.HeatTestCase):
stored_props = loaded_stack['AResource']._stored_properties_data
self.assertEqual({'Foo': 'xyz'}, stored_props)
def test_update_replace_resticted(self):
env = environment.Environment()
env_snippet = {u'resource_registry': {
u'resources': {
'AResource': {'restricted_actions': 'update'}
}
}
}
env.load(env_snippet)
tmpl = {'HeatTemplateFormatVersion': '2012-12-12',
'Resources': {'AResource': {'Type': 'ResourceWithPropsType',
'Properties': {'Foo': 'abc'}}}}
self.stack = stack.Stack(self.ctx, 'update_test_stack',
template.Template(tmpl))
self.stack.store()
self.stack.create()
self.assertEqual((stack.Stack.CREATE, stack.Stack.COMPLETE),
self.stack.state)
tmpl1 = {'HeatTemplateFormatVersion': '2012-12-12',
'Resources': {'AResource': {'Type': 'ResourceWithPropsType',
'Properties': {'Foo': 'xyz'}}}}
updated_stack = stack.Stack(self.ctx, 'updated_stack',
template.Template(tmpl1, env=env))
self.stack.update(updated_stack)
self.assertEqual((stack.Stack.UPDATE, stack.Stack.COMPLETE),
self.stack.state)
tmpl2 = {'HeatTemplateFormatVersion': '2012-12-12',
'Resources': {'AResource': {'Type': 'GenericResourceType'}}}
env_snippet['resource_registry']['resources'][
'AResource']['restricted_actions'] = 'replace'
env.load(env_snippet)
updated_stack = stack.Stack(self.ctx, 'updated_stack',
template.Template(tmpl2, env=env))
self.stack.update(updated_stack)
self.assertEqual((stack.Stack.UPDATE, stack.Stack.FAILED),
self.stack.state)
def test_update_modify_ok_replace_int(self):
# create
# ========