Merge "ResourceGroup add remove_policies property"
This commit is contained in:
commit
5622e02a60
@ -13,6 +13,7 @@
|
||||
|
||||
import collections
|
||||
import copy
|
||||
import six
|
||||
|
||||
from heat.common import exception
|
||||
from heat.common.i18n import _
|
||||
@ -69,9 +70,9 @@ class ResourceGroup(stack_resource.StackResource):
|
||||
support_status = support.SupportStatus(version='2014.1')
|
||||
|
||||
PROPERTIES = (
|
||||
COUNT, INDEX_VAR, RESOURCE_DEF,
|
||||
COUNT, INDEX_VAR, RESOURCE_DEF, REMOVAL_POLICIES
|
||||
) = (
|
||||
'count', 'index_var', 'resource_def',
|
||||
'count', 'index_var', 'resource_def', 'removal_policies'
|
||||
)
|
||||
|
||||
_RESOURCE_DEF_KEYS = (
|
||||
@ -80,6 +81,12 @@ class ResourceGroup(stack_resource.StackResource):
|
||||
'type', 'properties',
|
||||
)
|
||||
|
||||
_REMOVAL_POLICIES_KEYS = (
|
||||
REMOVAL_RSRC_LIST,
|
||||
) = (
|
||||
'resource_list',
|
||||
)
|
||||
|
||||
ATTRIBUTES = (
|
||||
REFS, ATTR_ATTRIBUTES,
|
||||
) = (
|
||||
@ -126,6 +133,36 @@ class ResourceGroup(stack_resource.StackResource):
|
||||
},
|
||||
required=True
|
||||
),
|
||||
REMOVAL_POLICIES: properties.Schema(
|
||||
properties.Schema.LIST,
|
||||
_('Policies for removal of resources on update'),
|
||||
schema=properties.Schema(
|
||||
properties.Schema.MAP,
|
||||
_('Policy to be processed when doing an update which '
|
||||
'requires removal of specific resources.'),
|
||||
schema={
|
||||
REMOVAL_RSRC_LIST: properties.Schema(
|
||||
properties.Schema.LIST,
|
||||
_('List of resources to be removed '
|
||||
'when doing an update which requires removal of '
|
||||
'specific resources. '
|
||||
'The resource may be specified several ways: '
|
||||
'(1) The resource name, as in the nested stack, '
|
||||
'(2) The resource reference returned from '
|
||||
'get_resource in a template, as available via '
|
||||
'the \'refs\' attribute '
|
||||
'Note this is destructive on update when specified; '
|
||||
'even if the count is not being reduced, and once '
|
||||
'a resource name is removed, it\'s name is never '
|
||||
'reused in subsequent updates'
|
||||
),
|
||||
default=[]
|
||||
),
|
||||
},
|
||||
),
|
||||
update_allowed=True,
|
||||
default=[]
|
||||
),
|
||||
}
|
||||
|
||||
attributes_schema = {
|
||||
@ -150,8 +187,51 @@ class ResourceGroup(stack_resource.StackResource):
|
||||
self.stack)
|
||||
res_inst.validate()
|
||||
|
||||
def _name_blacklist(self):
|
||||
"""Resolve the remove_policies to names for removal."""
|
||||
|
||||
# To avoid reusing names after removal, we store a comma-separated
|
||||
# blacklist in the resource data
|
||||
db_rsrc_names = self.data().get('name_blacklist')
|
||||
if db_rsrc_names:
|
||||
current_blacklist = db_rsrc_names.split(',')
|
||||
else:
|
||||
current_blacklist = []
|
||||
|
||||
# Now we iterate over the removal policies, and update the blacklist
|
||||
# with any additional names
|
||||
rsrc_names = list(current_blacklist)
|
||||
for r in self.properties[self.REMOVAL_POLICIES]:
|
||||
if self.REMOVAL_RSRC_LIST in r:
|
||||
# Tolerate string or int list values
|
||||
for n in r[self.REMOVAL_RSRC_LIST]:
|
||||
str_n = six.text_type(n)
|
||||
if str_n in self.nested() and str_n not in rsrc_names:
|
||||
rsrc_names.append(str_n)
|
||||
continue
|
||||
rsrc = self.nested().resource_by_refid(str_n)
|
||||
if rsrc and str_n not in rsrc_names:
|
||||
rsrc_names.append(rsrc.name)
|
||||
|
||||
# If the blacklist has changed, update the resource data
|
||||
if rsrc_names != current_blacklist:
|
||||
self.data_set('name_blacklist', ','.join(rsrc_names))
|
||||
return rsrc_names
|
||||
|
||||
def _resource_names(self):
|
||||
return [str(n) for n in range(self.properties.get(self.COUNT))]
|
||||
name_blacklist = self._name_blacklist()
|
||||
req_count = self.properties.get(self.COUNT)
|
||||
|
||||
def gen_names():
|
||||
count = 0
|
||||
index = 0
|
||||
while count < req_count:
|
||||
if str(index) not in name_blacklist:
|
||||
yield str(index)
|
||||
count += 1
|
||||
index += 1
|
||||
|
||||
return list(gen_names())
|
||||
|
||||
def handle_create(self):
|
||||
names = self._resource_names()
|
||||
|
@ -92,6 +92,24 @@ template_repl = {
|
||||
}
|
||||
}
|
||||
|
||||
template_repl2 = {
|
||||
"heat_template_version": "2013-05-23",
|
||||
"resources": {
|
||||
"group1": {
|
||||
"type": "OS::Heat::ResourceGroup",
|
||||
"properties": {
|
||||
"count": 2,
|
||||
"resource_def": {
|
||||
"type": "dummy.resource",
|
||||
"properties": {
|
||||
"Foo": "Bar%index%"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
template_attr = {
|
||||
"heat_template_version": "2014-10-16",
|
||||
"resources": {
|
||||
@ -379,6 +397,177 @@ class ResourceGroupTest(common.HeatTestCase):
|
||||
resource_names = [r.name for r in resg.nested().iter_resources()]
|
||||
self.assertEqual(['0', '1'], sorted(resource_names))
|
||||
|
||||
def test_update_remove_resource_list_name(self):
|
||||
"""Test update specifying victims."""
|
||||
resg = self._create_dummy_stack()
|
||||
self.assertEqual(2, len(resg.nested()))
|
||||
resource_names = [r.name for r in resg.nested().iter_resources()]
|
||||
self.assertEqual(['0', '1'], sorted(resource_names))
|
||||
|
||||
new_snip = copy.deepcopy(resg.t)
|
||||
new_snip['Properties']['count'] = 5
|
||||
scheduler.TaskRunner(resg.update, new_snip)()
|
||||
self.stack = resg.nested()
|
||||
self.assertEqual((resg.UPDATE, resg.COMPLETE), resg.state)
|
||||
self.assertEqual((resg.UPDATE, resg.COMPLETE), resg.nested().state)
|
||||
self.assertEqual(5, len(resg.nested()))
|
||||
resource_names = [r.name for r in resg.nested().iter_resources()]
|
||||
self.assertEqual(['0', '1', '2', '3', '4'], sorted(resource_names))
|
||||
|
||||
# Reduce by three, specifying the middle resources to be removed
|
||||
reduce_snip = copy.deepcopy(resg.t)
|
||||
reduce_snip['Properties']['count'] = 2
|
||||
reduce_snip['Properties']['removal_policies'] = [{'resource_list':
|
||||
['1', '2', '3']}]
|
||||
scheduler.TaskRunner(resg.update, reduce_snip)()
|
||||
self.assertEqual((resg.UPDATE, resg.COMPLETE), resg.state)
|
||||
self.assertEqual((resg.UPDATE, resg.COMPLETE), resg.nested().state)
|
||||
self.assertEqual(2, len(resg.nested()))
|
||||
resource_names = [r.name for r in resg.nested().iter_resources()]
|
||||
self.assertEqual(['0', '4'], sorted(resource_names))
|
||||
|
||||
# Increase to 3 again leaving the force remove, the indexes are skipped
|
||||
increase_snip = copy.deepcopy(resg.t)
|
||||
increase_snip['Properties']['count'] = 3
|
||||
scheduler.TaskRunner(resg.update, increase_snip)()
|
||||
self.assertEqual((resg.UPDATE, resg.COMPLETE), resg.state)
|
||||
self.assertEqual((resg.UPDATE, resg.COMPLETE), resg.nested().state)
|
||||
self.assertEqual(3, len(resg.nested()))
|
||||
resource_names = [r.name for r in resg.nested().iter_resources()]
|
||||
self.assertEqual(['0', '4', '5'], sorted(resource_names))
|
||||
|
||||
# Increase to 5 clearing the resource_list, the blacklist should be
|
||||
# maintained so no resource names are reused
|
||||
increase_snip2 = copy.deepcopy(resg.t)
|
||||
increase_snip2['Properties']['count'] = 5
|
||||
del(increase_snip2['Properties']['removal_policies'])
|
||||
scheduler.TaskRunner(resg.update, increase_snip2)()
|
||||
self.assertEqual((resg.UPDATE, resg.COMPLETE), resg.state)
|
||||
self.assertEqual((resg.UPDATE, resg.COMPLETE), resg.nested().state)
|
||||
self.assertEqual(5, len(resg.nested()))
|
||||
resource_names = [r.name for r in resg.nested().iter_resources()]
|
||||
self.assertEqual(['0', '4', '5', '6', '7'], sorted(resource_names))
|
||||
|
||||
# Reduce by 3 only passing two resource_list victims, the remaining
|
||||
# removal should be the largest numbered/newest, as normal
|
||||
reduce_snip = copy.deepcopy(resg.t)
|
||||
reduce_snip['Properties']['count'] = 2
|
||||
reduce_snip['Properties']['removal_policies'] = [{'resource_list':
|
||||
['4', '5']}]
|
||||
scheduler.TaskRunner(resg.update, reduce_snip)()
|
||||
self.assertEqual((resg.UPDATE, resg.COMPLETE), resg.state)
|
||||
self.assertEqual((resg.UPDATE, resg.COMPLETE), resg.nested().state)
|
||||
self.assertEqual(2, len(resg.nested()))
|
||||
resource_names = [r.name for r in resg.nested().iter_resources()]
|
||||
self.assertEqual(['0', '6'], sorted(resource_names))
|
||||
|
||||
def test_update_remove_resource_list_refid(self):
|
||||
"""Test update specifying victims."""
|
||||
resg = self._create_dummy_stack()
|
||||
self.assertEqual(2, len(resg.nested()))
|
||||
resource_names = [r.name for r in resg.nested().iter_resources()]
|
||||
self.assertEqual(['0', '1'], sorted(resource_names))
|
||||
|
||||
# Update to remove a specific resource ref without affecting the size
|
||||
# we should remove resource 0 and build a replacement
|
||||
r_id = resg.nested()['0'].FnGetRefId()
|
||||
self.assertIsNotNone(r_id)
|
||||
reduce_snip = copy.deepcopy(resg.t)
|
||||
reduce_snip['Properties']['count'] = 2
|
||||
reduce_snip['Properties']['removal_policies'] = [
|
||||
{'resource_list': [r_id]}]
|
||||
scheduler.TaskRunner(resg.update, reduce_snip)()
|
||||
self.assertEqual((resg.UPDATE, resg.COMPLETE), resg.state)
|
||||
self.assertEqual((resg.UPDATE, resg.COMPLETE), resg.nested().state)
|
||||
self.assertEqual(2, len(resg.nested()))
|
||||
resource_names = [r.name for r in resg.nested().iter_resources()]
|
||||
self.assertEqual(['1', '2'], sorted(resource_names))
|
||||
self.assertIsNone(resg.nested().resource_by_refid(r_id))
|
||||
|
||||
# We now should not do anything on subsequent updates
|
||||
reduce_snip = copy.deepcopy(resg.t)
|
||||
del(reduce_snip['Properties']['removal_policies'])
|
||||
scheduler.TaskRunner(resg.update, reduce_snip)()
|
||||
self.assertEqual((resg.UPDATE, resg.COMPLETE), resg.state)
|
||||
self.assertEqual((resg.UPDATE, resg.COMPLETE), resg.nested().state)
|
||||
self.assertEqual(2, len(resg.nested()))
|
||||
resource_names = [r.name for r in resg.nested().iter_resources()]
|
||||
self.assertEqual(['1', '2'], sorted(resource_names))
|
||||
self.assertIsNone(resg.nested().resource_by_refid(r_id))
|
||||
|
||||
def test_update_remove_add_index_replacement(self):
|
||||
"""Test update removal/add indexes are consistent."""
|
||||
resg = self._create_dummy_stack(template_data=template_repl2)
|
||||
self.assertEqual(2, len(resg.nested()))
|
||||
resource_names = [r.name for r in resg.nested().iter_resources()]
|
||||
self.assertEqual(['0', '1'], sorted(resource_names))
|
||||
|
||||
new_snip = copy.deepcopy(resg.t)
|
||||
new_snip['Properties']['count'] = 5
|
||||
scheduler.TaskRunner(resg.update, new_snip)()
|
||||
self.stack = resg.nested()
|
||||
self.assertEqual((resg.UPDATE, resg.COMPLETE), resg.state)
|
||||
self.assertEqual((resg.UPDATE, resg.COMPLETE), resg.nested().state)
|
||||
self.assertEqual(5, len(resg.nested()))
|
||||
resource_names = [r.name for r in resg.nested().iter_resources()]
|
||||
self.assertEqual(['0', '1', '2', '3', '4'], sorted(resource_names))
|
||||
for r in ['0', '1', '2', '3', '4']:
|
||||
prop_val = 'Bar%s' % r
|
||||
self.assertEqual(prop_val, resg.nested()[r].properties.get('Foo'))
|
||||
|
||||
# Reduce by three, specifying the middle resources to be removed
|
||||
reduce_snip = copy.deepcopy(resg.t)
|
||||
reduce_snip['Properties']['count'] = 2
|
||||
reduce_snip['Properties']['removal_policies'] = [{'resource_list':
|
||||
['1', '2', '3']}]
|
||||
scheduler.TaskRunner(resg.update, reduce_snip)()
|
||||
self.assertEqual((resg.UPDATE, resg.COMPLETE), resg.state)
|
||||
self.assertEqual((resg.UPDATE, resg.COMPLETE), resg.nested().state)
|
||||
self.assertEqual(2, len(resg.nested()))
|
||||
resource_names = [r.name for r in resg.nested().iter_resources()]
|
||||
self.assertEqual(['0', '4'], sorted(resource_names))
|
||||
self.assertEqual('Bar0', resg.nested()['0'].properties.get('Foo'))
|
||||
self.assertEqual('Bar4', resg.nested()['4'].properties.get('Foo'))
|
||||
|
||||
# Increase to 3 again leaving the force remove, the indexes are skipped
|
||||
increase_snip = copy.deepcopy(resg.t)
|
||||
increase_snip['Properties']['count'] = 3
|
||||
scheduler.TaskRunner(resg.update, increase_snip)()
|
||||
self.assertEqual((resg.UPDATE, resg.COMPLETE), resg.state)
|
||||
self.assertEqual((resg.UPDATE, resg.COMPLETE), resg.nested().state)
|
||||
self.assertEqual(3, len(resg.nested()))
|
||||
resource_names = [r.name for r in resg.nested().iter_resources()]
|
||||
self.assertEqual(['0', '4', '5'], sorted(resource_names))
|
||||
self.assertEqual('Bar0', resg.nested()['0'].properties.get('Foo'))
|
||||
self.assertEqual('Bar4', resg.nested()['4'].properties.get('Foo'))
|
||||
self.assertEqual('Bar5', resg.nested()['5'].properties.get('Foo'))
|
||||
|
||||
def test_invalid_removal_policies_nolist(self):
|
||||
"""Test that error raised for malformed removal_policies."""
|
||||
tmp = copy.deepcopy(template)
|
||||
grp_props = tmp['resources']['group1']['properties']
|
||||
grp_props['removal_policies'] = 'notallowed'
|
||||
stack = utils.parse_stack(tmp)
|
||||
snip = stack.t.resource_definitions(stack)['group1']
|
||||
resg = resource_group.ResourceGroup('test', snip, stack)
|
||||
exc = self.assertRaises(exception.StackValidationFailed,
|
||||
resg.validate)
|
||||
errstr = 'removal_policies "\'notallowed\'" is not a list'
|
||||
self.assertIn(errstr, six.text_type(exc))
|
||||
|
||||
def test_invalid_removal_policies_nomap(self):
|
||||
"""Test that error raised for malformed removal_policies."""
|
||||
tmp = copy.deepcopy(template)
|
||||
grp_props = tmp['resources']['group1']['properties']
|
||||
grp_props['removal_policies'] = ['notallowed']
|
||||
stack = utils.parse_stack(tmp)
|
||||
snip = stack.t.resource_definitions(stack)['group1']
|
||||
resg = resource_group.ResourceGroup('test', snip, stack)
|
||||
exc = self.assertRaises(exception.StackValidationFailed,
|
||||
resg.validate)
|
||||
errstr = '"notallowed" is not a map'
|
||||
self.assertIn(errstr, six.text_type(exc))
|
||||
|
||||
def test_aggregate_attribs(self):
|
||||
"""
|
||||
Test attribute aggregation and that we mimic the nested resource's
|
||||
|
Loading…
Reference in New Issue
Block a user