Merge "Restrict update/replace of resource"
This commit is contained in:
commit
a6bd36a57d
@ -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
|
||||
|
@ -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.')
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
@ -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'))
|
||||
|
@ -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()
|
||||
|
@ -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
|
||||
# ========
|
||||
|
Loading…
Reference in New Issue
Block a user