Implement a Heat-native resource group
This resource allows for creating groups of identically configured resources via nested stacks. It provides aggregated as well as indexed access to the nested resource attributes. Implements: blueprint native-resource-group Change-Id: I089e6d2f795d30ebe410df151ebc68639ea7102b
This commit is contained in:
parent
4e847391ea
commit
a833873e91
139
heat/engine/resources/resource_group.py
Normal file
139
heat/engine/resources/resource_group.py
Normal file
@ -0,0 +1,139 @@
|
||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
|
||||
#
|
||||
# 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.
|
||||
|
||||
import copy
|
||||
|
||||
from heat.engine import parser
|
||||
from heat.engine import properties
|
||||
from heat.engine import stack_resource
|
||||
from heat.common import exception
|
||||
|
||||
from heat.openstack.common.gettextutils import _
|
||||
|
||||
template_template = {
|
||||
"heat_template_version": "2013-05-23",
|
||||
"resources": {}
|
||||
}
|
||||
|
||||
|
||||
class ResourceGroup(stack_resource.StackResource):
|
||||
"""
|
||||
A resource that creates one or more identically configured nested
|
||||
resources. The attributes of this resource mirror those of the
|
||||
nested resource definition.
|
||||
|
||||
The attributes of this resource mirror the attributes of the resources
|
||||
in the group except a list of attribute values for each resource in
|
||||
the group is returned. Additionally, attributes for individual resources
|
||||
in the group can be accessed using synthetic attributes of the form
|
||||
"resource.[index].[attribute name]"
|
||||
"""
|
||||
|
||||
min_resource_schema = {
|
||||
"type": properties.Schema(
|
||||
properties.STRING,
|
||||
_("The type of the resources in the group"),
|
||||
required=True
|
||||
),
|
||||
"properties": properties.Schema(
|
||||
properties.MAP,
|
||||
_("Property values for the resources in the group")
|
||||
)
|
||||
}
|
||||
|
||||
properties_schema = {
|
||||
"count": properties.Schema(
|
||||
properties.INTEGER,
|
||||
_("The number of instances to create."),
|
||||
default=1,
|
||||
required=True,
|
||||
update_allowed=True,
|
||||
constraints=[
|
||||
properties.Range(1)
|
||||
]
|
||||
),
|
||||
"resource_def": properties.Schema(
|
||||
properties.MAP,
|
||||
_("Resource definition for the resources in the group. The value "
|
||||
"of this property is the definition of a resource just as if it"
|
||||
" had been declared in the template itself."),
|
||||
required=True,
|
||||
schema=min_resource_schema
|
||||
)
|
||||
}
|
||||
|
||||
attributes_schema = {}
|
||||
update_allowed_keys = ("Properties",)
|
||||
|
||||
def validate(self):
|
||||
# validate our basic properties
|
||||
super(ResourceGroup, self).validate()
|
||||
# make sure the nested resource is valid
|
||||
test_tmpl = self._assemble_nested(1, include_all=True)
|
||||
val_templ = parser.Template(test_tmpl)
|
||||
res_def = val_templ["Resources"]["0"]
|
||||
res_class = self.stack.env.get_class(res_def['Type'])
|
||||
res_inst = res_class("%s:resource_def" % self.name, res_def,
|
||||
self.stack)
|
||||
res_inst.validate()
|
||||
|
||||
def handle_create(self):
|
||||
count = self.properties['count']
|
||||
return self.create_with_template(self._assemble_nested(count),
|
||||
{},
|
||||
self.stack.timeout_mins)
|
||||
|
||||
def handle_update(self, new_snippet, tmpl_diff, prop_diff):
|
||||
count = prop_diff.get("count")
|
||||
if count:
|
||||
return self.update_with_template(self._assemble_nested(count),
|
||||
{},
|
||||
self.stack.timeout_mins)
|
||||
|
||||
def handle_delete(self):
|
||||
return self.delete_nested()
|
||||
|
||||
def FnGetAtt(self, key):
|
||||
if key.startswith("resource."):
|
||||
parts = key.split(".", 2)
|
||||
attr_name = parts[-1]
|
||||
try:
|
||||
res = self.nested()[parts[1]]
|
||||
except KeyError:
|
||||
raise exception.InvalidTemplateAttribute(resource=self.name,
|
||||
key=key)
|
||||
else:
|
||||
return res.FnGetAtt(attr_name)
|
||||
else:
|
||||
return [self.nested()[str(v)].FnGetAtt(key) for v
|
||||
in range(self.properties['count'])]
|
||||
|
||||
def _assemble_nested(self, count, include_all=False):
|
||||
child_template = copy.deepcopy(template_template)
|
||||
resource_def = self.properties['resource_def']
|
||||
if not include_all:
|
||||
clean = dict((k, v) for k, v
|
||||
in resource_def['properties'].items() if v)
|
||||
resource_def['properties'] = clean
|
||||
resources = dict((str(k), resource_def)
|
||||
for k in range(count))
|
||||
child_template['resources'] = resources
|
||||
return child_template
|
||||
|
||||
|
||||
def resource_mapping():
|
||||
return {
|
||||
'OS::Heat::ResourceGroup': ResourceGroup,
|
||||
}
|
204
heat/tests/test_resource_group.py
Normal file
204
heat/tests/test_resource_group.py
Normal file
@ -0,0 +1,204 @@
|
||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
|
||||
#
|
||||
# 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.
|
||||
|
||||
import copy
|
||||
|
||||
from heat.common import exception
|
||||
from heat.engine import resource
|
||||
from heat.engine import scheduler
|
||||
from heat.engine.resources import resource_group
|
||||
from heat.tests import common
|
||||
from heat.tests import generic_resource
|
||||
from heat.tests import utils
|
||||
|
||||
template = {
|
||||
"heat_template_version": "2013-05-23",
|
||||
"resources": {
|
||||
"group1": {
|
||||
"type": "OS::Heat::ResourceGroup",
|
||||
"properties": {
|
||||
"count": 2,
|
||||
"resource_def": {
|
||||
"type": "dummy.resource",
|
||||
"properties": {
|
||||
"Foo": "Bar"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
template2 = {
|
||||
"heat_template_version": "2013-05-23",
|
||||
"resources": {
|
||||
"dummy": {
|
||||
"type": "dummy.resource",
|
||||
"properties": {
|
||||
"Foo": "baz"
|
||||
}
|
||||
},
|
||||
"group1": {
|
||||
"type": "OS::Heat::ResourceGroup",
|
||||
"properties": {
|
||||
"count": 2,
|
||||
"resource_def": {
|
||||
"type": "dummy.resource",
|
||||
"properties": {
|
||||
"Foo": {"get_attr": ["dummy", "Foo"]}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class ResourceGroupTest(common.HeatTestCase):
|
||||
|
||||
def setUp(self):
|
||||
common.HeatTestCase.setUp(self)
|
||||
resource._register_class("dummy.resource",
|
||||
generic_resource.ResourceWithProps)
|
||||
utils.setup_dummy_db()
|
||||
|
||||
def test_assemble_nested(self):
|
||||
"""
|
||||
Tests that the nested stack that implements the group is created
|
||||
appropriately based on properties.
|
||||
"""
|
||||
stack = utils.parse_stack(template)
|
||||
snip = stack.t['Resources']['group1']
|
||||
resg = resource_group.ResourceGroup('test', snip, stack)
|
||||
templ = {
|
||||
"heat_template_version": "2013-05-23",
|
||||
"resources": {
|
||||
"0": {
|
||||
"type": "dummy.resource",
|
||||
"properties": {
|
||||
"Foo": "Bar"
|
||||
}
|
||||
},
|
||||
"1": {
|
||||
"type": "dummy.resource",
|
||||
"properties": {
|
||||
"Foo": "Bar"
|
||||
}
|
||||
},
|
||||
"2": {
|
||||
"type": "dummy.resource",
|
||||
"properties": {
|
||||
"Foo": "Bar"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
self.assertEqual(templ, resg._assemble_nested(3))
|
||||
|
||||
def test_assemble_nested_include(self):
|
||||
templ = copy.deepcopy(template)
|
||||
res_def = templ["resources"]["group1"]["properties"]['resource_def']
|
||||
res_def['properties']['Foo'] = None
|
||||
stack = utils.parse_stack(templ)
|
||||
snip = stack.t['Resources']['group1']
|
||||
resg = resource_group.ResourceGroup('test', snip, stack)
|
||||
expect = {
|
||||
"heat_template_version": "2013-05-23",
|
||||
"resources": {
|
||||
"0": {
|
||||
"type": "dummy.resource",
|
||||
"properties": {}
|
||||
}
|
||||
}
|
||||
}
|
||||
self.assertEqual(expect, resg._assemble_nested(1))
|
||||
expect['resources']["0"]['properties'] = {"Foo": None}
|
||||
self.assertEqual(expect, resg._assemble_nested(1, include_all=True))
|
||||
|
||||
def test_invalid_res_type(self):
|
||||
"""Test that error raised for unknown resource type."""
|
||||
tmp = copy.deepcopy(template)
|
||||
grp_props = tmp['resources']['group1']['properties']
|
||||
grp_props['resource_def']['type'] = "idontexist"
|
||||
stack = utils.parse_stack(tmp)
|
||||
snip = stack.t['Resources']['group1']
|
||||
resg = resource_group.ResourceGroup('test', snip, stack)
|
||||
exc = self.assertRaises(exception.StackValidationFailed,
|
||||
resg.validate)
|
||||
self.assertIn('Unknown resource Type', str(exc))
|
||||
|
||||
def test_reference_attr(self):
|
||||
stack = utils.parse_stack(template2)
|
||||
snip = stack.t['Resources']['group1']
|
||||
resgrp = resource_group.ResourceGroup('test', snip, stack)
|
||||
try:
|
||||
resgrp.validate()
|
||||
except exception.StackValidationFailed as exc:
|
||||
# this is not a normal exception failure but a test failure.
|
||||
self.fail(str(exc))
|
||||
|
||||
@utils.stack_delete_after
|
||||
def test_delete(self):
|
||||
"""Test basic delete."""
|
||||
resg = self._create_dummy_stack()
|
||||
self.assertIsNotNone(resg.nested())
|
||||
scheduler.TaskRunner(resg.delete)()
|
||||
self.assertEqual((resg.DELETE, resg.COMPLETE), resg.nested().state)
|
||||
self.assertEqual((resg.DELETE, resg.COMPLETE), resg.state)
|
||||
|
||||
@utils.stack_delete_after
|
||||
def test_update(self):
|
||||
"""Test basic update."""
|
||||
resg = self._create_dummy_stack()
|
||||
new_snip = copy.deepcopy(resg.t)
|
||||
new_snip['Properties']['count'] = 3
|
||||
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(3, len(resg.nested()))
|
||||
|
||||
@utils.stack_delete_after
|
||||
def test_aggregate_attribs(self):
|
||||
"""
|
||||
Test attribute aggregation and that we mimic the nested resource's
|
||||
attributes.
|
||||
"""
|
||||
resg = self._create_dummy_stack()
|
||||
expected = ['0', '1']
|
||||
self.assertEqual(expected, resg.FnGetAtt('foo'))
|
||||
self.assertEqual(expected, resg.FnGetAtt('Foo'))
|
||||
|
||||
@utils.stack_delete_after
|
||||
def test_index_attribs(self):
|
||||
"""Tests getting attributes of individual resources."""
|
||||
resg = self._create_dummy_stack()
|
||||
self.assertEqual("0", resg.FnGetAtt('resource.0.foo'))
|
||||
self.assertEqual("1", resg.FnGetAtt('resource.1.foo'))
|
||||
self.assertRaises(exception.InvalidTemplateAttribute, resg.FnGetAtt,
|
||||
'resource.2.foo')
|
||||
self.assertRaises(exception.InvalidTemplateAttribute, resg.FnGetAtt,
|
||||
'resource.1.bar')
|
||||
|
||||
def _create_dummy_stack(self):
|
||||
stack = utils.parse_stack(template)
|
||||
snip = stack.t['Resources']['group1']
|
||||
resg = resource_group.ResourceGroup('test', snip, stack)
|
||||
scheduler.TaskRunner(resg.create)()
|
||||
self.stack = resg.nested()
|
||||
self.assertEqual(2, len(resg.nested()))
|
||||
self.assertEqual((resg.CREATE, resg.COMPLETE), resg.state)
|
||||
return resg
|
Loading…
Reference in New Issue
Block a user