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:
Randall Burt 2013-09-30 12:14:03 -05:00
parent 4e847391ea
commit a833873e91
2 changed files with 343 additions and 0 deletions

View 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,
}

View 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