Updated LaunchConfig for AutoScaling UpdatePolicy

This is the first part of a series to implement support
for AutoScaling UpdatePolicy.

Updated LaunchConfiguration resource type to return
self.physical_resource_name() in FnGetRefId(). For both
InstanceGroup and AutoScalingGroup, the property
LaunchConfigurationName is added to the list of update allowed
properties. With this change, any property change to the
LaunchConfiguration resource will be result in a different
LaunchConfigurationName on reference resolution and thus will
trigger InstanceGroup and AutoScalingGroup to handle the update.

Change-Id: I94da8f7083b64873c511b953c6bc63dba2b51034
blueprint: as-update-policy
This commit is contained in:
Winson Chan 2013-08-13 00:37:48 -07:00
parent c92aa027fa
commit 91a40824f4
5 changed files with 190 additions and 36 deletions

View File

@ -13,6 +13,7 @@
# License for the specific language governing permissions and limitations # License for the specific language governing permissions and limitations
# under the License. # under the License.
import copy
from heat.common import exception from heat.common import exception
from heat.engine import resource from heat.engine import resource
from heat.engine import signal_responder from heat.engine import signal_responder
@ -73,7 +74,7 @@ class InstanceGroup(stack_resource.StackResource):
'Schema': tags_schema}} 'Schema': tags_schema}}
} }
update_allowed_keys = ('Properties',) update_allowed_keys = ('Properties',)
update_allowed_properties = ('Size',) update_allowed_properties = ('Size', 'LaunchConfigurationName',)
attributes_schema = { attributes_schema = {
"InstanceList": ("A comma-delimited list of server ip addresses. " "InstanceList": ("A comma-delimited list of server ip addresses. "
"(Heat extension)") "(Heat extension)")
@ -154,12 +155,12 @@ class InstanceGroup(stack_resource.StackResource):
launch configuration. launch configuration.
""" """
conf_name = self.properties['LaunchConfigurationName'] conf_name = self.properties['LaunchConfigurationName']
instance_definition = self.stack.t['Resources'][conf_name].copy() conf = self.stack.resource_by_refid(conf_name)
instance_definition = copy.deepcopy(conf.t)
instance_definition['Type'] = 'AWS::EC2::Instance' instance_definition['Type'] = 'AWS::EC2::Instance'
instance_definition['Properties']['Tags'] = self._tags() instance_definition['Properties']['Tags'] = self._tags()
# resolve references within the context of this stack. # resolve references within the context of this stack.
static_parsed = self.stack.resolve_static_data(instance_definition) fully_parsed = self.stack.resolve_runtime_data(instance_definition)
fully_parsed = self.stack.resolve_runtime_data(static_parsed)
resources = {} resources = {}
for i in range(num_instances): for i in range(num_instances):
@ -241,7 +242,8 @@ class AutoScalingGroup(InstanceGroup, CooldownMixin):
# template keys and properties supported for handle_update, # template keys and properties supported for handle_update,
# note trailing comma is required for a single item to get a tuple # note trailing comma is required for a single item to get a tuple
update_allowed_keys = ('Properties',) update_allowed_keys = ('Properties',)
update_allowed_properties = ('MaxSize', 'MinSize', update_allowed_properties = ('LaunchConfigurationName',
'MaxSize', 'MinSize',
'Cooldown', 'DesiredCapacity',) 'Cooldown', 'DesiredCapacity',)
def handle_create(self): def handle_create(self):
@ -364,6 +366,9 @@ class LaunchConfiguration(resource.Resource):
'Schema': tags_schema}}, 'Schema': tags_schema}},
} }
def FnGetRefId(self):
return unicode(self.physical_resource_name())
class ScalingPolicy(signal_responder.SignalResponder, CooldownMixin): class ScalingPolicy(signal_responder.SignalResponder, CooldownMixin):
properties_schema = { properties_schema = {

View File

@ -106,18 +106,21 @@ class AutoScalingTest(HeatTestCase):
self.fc = fakes.FakeKeystoneClient() self.fc = fakes.FakeKeystoneClient()
def create_scaling_group(self, t, stack, resource_name): def create_scaling_group(self, t, stack, resource_name):
rsrc = asc.AutoScalingGroup(resource_name, # create the launch configuration resource
t['Resources'][resource_name], conf = stack.resources['LaunchConfig']
stack) self.assertEqual(None, conf.validate())
scheduler.TaskRunner(conf.create)()
self.assertEqual((conf.CREATE, conf.COMPLETE), conf.state)
# create the group resource
rsrc = stack.resources[resource_name]
self.assertEqual(None, rsrc.validate()) self.assertEqual(None, rsrc.validate())
scheduler.TaskRunner(rsrc.create)() scheduler.TaskRunner(rsrc.create)()
self.assertEqual((rsrc.CREATE, rsrc.COMPLETE), rsrc.state) self.assertEqual((rsrc.CREATE, rsrc.COMPLETE), rsrc.state)
return rsrc return rsrc
def create_scaling_policy(self, t, stack, resource_name): def create_scaling_policy(self, t, stack, resource_name):
rsrc = asc.ScalingPolicy(resource_name, rsrc = stack.resources[resource_name]
t['Resources'][resource_name],
stack)
self.assertEqual(None, rsrc.validate()) self.assertEqual(None, rsrc.validate())
scheduler.TaskRunner(rsrc.create)() scheduler.TaskRunner(rsrc.create)()
self.assertEqual((rsrc.CREATE, rsrc.COMPLETE), rsrc.state) self.assertEqual((rsrc.CREATE, rsrc.COMPLETE), rsrc.state)
@ -231,7 +234,7 @@ class AutoScalingTest(HeatTestCase):
self.assertEqual('WebServerGroup', rsrc.FnGetRefId()) self.assertEqual('WebServerGroup', rsrc.FnGetRefId())
self.assertEqual(['WebServerGroup-0'], rsrc.get_instance_names()) self.assertEqual(['WebServerGroup-0'], rsrc.get_instance_names())
update_snippet = copy.deepcopy(rsrc.parsed_template()) update_snippet = copy.deepcopy(rsrc.parsed_template())
update_snippet['Properties']['LaunchConfigurationName'] = 'foo' update_snippet['Properties']['AvailabilityZones'] = ['foo']
self.assertRaises(resource.UpdateReplace, self.assertRaises(resource.UpdateReplace,
rsrc.update, update_snippet) rsrc.update, update_snippet)
@ -460,9 +463,13 @@ class AutoScalingTest(HeatTestCase):
instance.Instance.handle_create().AndRaise(Exception) instance.Instance.handle_create().AndRaise(Exception)
self.m.ReplayAll() self.m.ReplayAll()
rsrc = asc.AutoScalingGroup('WebServerGroup',
t['Resources']['WebServerGroup'], conf = stack.resources['LaunchConfig']
stack) self.assertEqual(None, conf.validate())
scheduler.TaskRunner(conf.create)()
self.assertEqual((conf.CREATE, conf.COMPLETE), conf.state)
rsrc = stack.resources['WebServerGroup']
self.assertEqual(None, rsrc.validate()) self.assertEqual(None, rsrc.validate())
self.assertRaises(exception.ResourceFailure, self.assertRaises(exception.ResourceFailure,
scheduler.TaskRunner(rsrc.create)) scheduler.TaskRunner(rsrc.create))

View File

@ -16,7 +16,6 @@ import copy
from heat.common import exception from heat.common import exception
from heat.common import template_format from heat.common import template_format
from heat.engine.resources import autoscaling as asc
from heat.engine.resources import instance from heat.engine.resources import instance
from heat.engine import resource from heat.engine import resource
from heat.engine import resources from heat.engine import resources
@ -81,10 +80,10 @@ class InstanceGroupTest(HeatTestCase):
instance_class.check_create_complete( instance_class.check_create_complete(
cookie).MultipleTimes().AndReturn(True) cookie).MultipleTimes().AndReturn(True)
def create_instance_group(self, t, stack, resource_name): def create_resource(self, t, stack, resource_name):
rsrc = asc.InstanceGroup(resource_name, # subsequent resources may need to reference previous created resources
t['Resources'][resource_name], # use the stack's resource objects instead of instantiating new ones
stack) rsrc = stack.resources[resource_name]
self.assertEqual(None, rsrc.validate()) self.assertEqual(None, rsrc.validate())
scheduler.TaskRunner(rsrc.create)() scheduler.TaskRunner(rsrc.create)()
self.assertEqual((rsrc.CREATE, rsrc.COMPLETE), rsrc.state) self.assertEqual((rsrc.CREATE, rsrc.COMPLETE), rsrc.state)
@ -101,8 +100,8 @@ class InstanceGroupTest(HeatTestCase):
instance.Instance.FnGetAtt('PublicIp').AndReturn('1.2.3.4') instance.Instance.FnGetAtt('PublicIp').AndReturn('1.2.3.4')
self.m.ReplayAll() self.m.ReplayAll()
rsrc = self.create_instance_group(t, stack, 'JobServerGroup') conf = self.create_resource(t, stack, 'JobServerConfig')
rsrc = self.create_resource(t, stack, 'JobServerGroup')
self.assertEqual('JobServerGroup', rsrc.FnGetRefId()) self.assertEqual('JobServerGroup', rsrc.FnGetRefId())
self.assertEqual('1.2.3.4', rsrc.FnGetAtt('InstanceList')) self.assertEqual('1.2.3.4', rsrc.FnGetAtt('InstanceList'))
@ -133,8 +132,8 @@ class InstanceGroupTest(HeatTestCase):
self._stub_create(1, instance_class=MyInstance) self._stub_create(1, instance_class=MyInstance)
self.m.ReplayAll() self.m.ReplayAll()
conf = self.create_resource(t, stack, 'JobServerConfig')
rsrc = self.create_instance_group(t, stack, 'JobServerGroup') rsrc = self.create_resource(t, stack, 'JobServerGroup')
self.assertEqual('JobServerGroup', rsrc.FnGetRefId()) self.assertEqual('JobServerGroup', rsrc.FnGetRefId())
rsrc.delete() rsrc.delete()
self.m.VerifyAll() self.m.VerifyAll()
@ -144,9 +143,8 @@ class InstanceGroupTest(HeatTestCase):
t = template_format.parse(ig_template) t = template_format.parse(ig_template)
stack = utils.parse_stack(t) stack = utils.parse_stack(t)
rsrc = asc.InstanceGroup('JobServerGroup', conf = self.create_resource(t, stack, 'JobServerConfig')
t['Resources']['JobServerGroup'], rsrc = stack.resources['JobServerGroup']
stack)
self.m.StubOutWithMock(instance.Instance, 'handle_create') self.m.StubOutWithMock(instance.Instance, 'handle_create')
not_found = exception.ImageNotFound(image_name='bla') not_found = exception.ImageNotFound(image_name='bla')
@ -170,7 +168,8 @@ class InstanceGroupTest(HeatTestCase):
self._stub_create(2) self._stub_create(2)
self.m.ReplayAll() self.m.ReplayAll()
rsrc = self.create_instance_group(t, stack, 'JobServerGroup') conf = self.create_resource(t, stack, 'JobServerConfig')
rsrc = self.create_resource(t, stack, 'JobServerGroup')
self.m.VerifyAll() self.m.VerifyAll()
self.m.UnsetStubs() self.m.UnsetStubs()
@ -206,7 +205,8 @@ class InstanceGroupTest(HeatTestCase):
self._stub_create(2) self._stub_create(2)
self.m.ReplayAll() self.m.ReplayAll()
rsrc = self.create_instance_group(t, stack, 'JobServerGroup') conf = self.create_resource(t, stack, 'JobServerConfig')
rsrc = self.create_resource(t, stack, 'JobServerGroup')
self.m.ReplayAll() self.m.ReplayAll()
@ -226,12 +226,13 @@ class InstanceGroupTest(HeatTestCase):
self._stub_create(2) self._stub_create(2)
self.m.ReplayAll() self.m.ReplayAll()
rsrc = self.create_instance_group(t, stack, 'JobServerGroup') conf = self.create_resource(t, stack, 'JobServerConfig')
rsrc = self.create_resource(t, stack, 'JobServerGroup')
self.m.ReplayAll() self.m.ReplayAll()
update_snippet = copy.deepcopy(rsrc.parsed_template()) update_snippet = copy.deepcopy(rsrc.parsed_template())
update_snippet['Properties']['LaunchConfigurationName'] = 'wibble' update_snippet['Properties']['AvailabilityZones'] = ['wibble']
self.assertRaises(resource.UpdateReplace, self.assertRaises(resource.UpdateReplace,
rsrc.update, update_snippet) rsrc.update, update_snippet)

View File

@ -0,0 +1,137 @@
# 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 re
from heat.common import template_format
from heat.engine.resources import instance
from heat.engine import parser
from heat.tests.common import HeatTestCase
from heat.tests.utils import setup_dummy_db
from heat.tests.utils import parse_stack
ig_template_before = '''
{
"AWSTemplateFormatVersion" : "2010-09-09",
"Description" : "Template to create multiple instances.",
"Parameters" : {},
"Resources" : {
"JobServerGroup" : {
"Type" : "OS::Heat::InstanceGroup",
"Properties" : {
"LaunchConfigurationName" : { "Ref" : "JobServerConfig" },
"Size" : "8",
"AvailabilityZones" : ["nova"]
}
},
"JobServerConfig" : {
"Type" : "AWS::AutoScaling::LaunchConfiguration",
"Properties": {
"ImageId" : "foo",
"InstanceType" : "m1.medium",
"KeyName" : "test",
"SecurityGroups" : [ "sg-1" ],
"UserData" : "jsconfig data"
}
}
}
}
'''
ig_template_after = '''
{
"AWSTemplateFormatVersion" : "2010-09-09",
"Description" : "Template to create multiple instances.",
"Parameters" : {},
"Resources" : {
"JobServerGroup" : {
"Type" : "OS::Heat::InstanceGroup",
"Properties" : {
"LaunchConfigurationName" : { "Ref" : "JobServerConfig" },
"Size" : "8",
"AvailabilityZones" : ["nova"]
}
},
"JobServerConfig" : {
"Type" : "AWS::AutoScaling::LaunchConfiguration",
"Properties": {
"ImageId" : "foo",
"InstanceType" : "m1.large",
"KeyName" : "test",
"SecurityGroups" : [ "sg-1" ],
"UserData" : "jsconfig data"
}
}
}
}
'''
class InstanceGroupTest(HeatTestCase):
def setUp(self):
super(InstanceGroupTest, self).setUp()
setup_dummy_db()
def _stub_create(self, num, instance_class=instance.Instance):
"""
Expect creation of C{num} number of Instances.
:param instance_class: The resource class to expect to be created
instead of instance.Instance.
"""
self.m.StubOutWithMock(parser.Stack, 'validate')
parser.Stack.validate()
self.m.StubOutWithMock(instance_class, 'handle_create')
self.m.StubOutWithMock(instance_class, 'check_create_complete')
cookie = object()
for x in range(num):
instance_class.handle_create().AndReturn(cookie)
instance_class.check_create_complete(cookie).AndReturn(False)
instance_class.check_create_complete(
cookie).MultipleTimes().AndReturn(True)
def get_launch_conf_name(self, stack, ig_name):
return stack.resources[ig_name].properties['LaunchConfigurationName']
def test_instance_group(self):
# setup stack from the initial template
tmpl = template_format.parse(ig_template_before)
stack = parse_stack(tmpl)
# test stack create
# test the number of instance creation
# test that physical resource name of launch configuration is used
size = int(stack.resources['JobServerGroup'].properties['Size'])
self._stub_create(size)
self.m.ReplayAll()
stack.create()
self.m.VerifyAll()
self.assertEqual(stack.status, stack.COMPLETE)
conf = stack.resources['JobServerConfig']
conf_name_pattern = '%s-JobServerConfig-[a-zA-Z0-9]+$' % stack.name
regex_pattern = re.compile(conf_name_pattern)
self.assertTrue(regex_pattern.match(conf.FnGetRefId()))
# test stack update
# test that launch configuration is replaced
conf_name = self.get_launch_conf_name(stack, 'JobServerGroup')
updated_tmpl = template_format.parse(ig_template_after)
updated_stack = parse_stack(updated_tmpl)
stack.update(updated_stack)
updated_conf_name = self.get_launch_conf_name(stack, 'JobServerGroup')
self.assertNotEqual(conf_name, updated_conf_name)

View File

@ -15,7 +15,6 @@ import mox
from heat.engine import environment from heat.engine import environment
from heat.tests.v1_1 import fakes from heat.tests.v1_1 import fakes
from heat.engine.resources import autoscaling
from heat.engine.resources import instance as instances from heat.engine.resources import instance as instances
from heat.engine.resources import nova_utils from heat.engine.resources import nova_utils
from heat.common import template_format from heat.common import template_format
@ -77,7 +76,7 @@ group_template = '''
"Type": "OS::Heat::InstanceGroup", "Type": "OS::Heat::InstanceGroup",
"Properties": { "Properties": {
"AvailabilityZones" : ["nova"], "AvailabilityZones" : ["nova"],
"LaunchConfigurationName": "Config", "LaunchConfigurationName": { "Ref": "Config" },
"Size" : "1" "Size" : "1"
} }
} }
@ -145,9 +144,14 @@ class ServerTagsTest(HeatTestCase):
stack_id=uuidutils.generate_uuid()) stack_id=uuidutils.generate_uuid())
t['Resources']['WebServer']['Properties']['Tags'] = intags t['Resources']['WebServer']['Properties']['Tags'] = intags
group = autoscaling.InstanceGroup('WebServer',
t['Resources']['WebServer'], # create the launch configuration
stack) conf = stack.resources['Config']
self.assertEqual(None, conf.validate())
scheduler.TaskRunner(conf.create)()
self.assertEqual((conf.CREATE, conf.COMPLETE), conf.state)
group = stack.resources['WebServer']
self.m.StubOutWithMock(instances.Instance, 'nova') self.m.StubOutWithMock(instances.Instance, 'nova')
instances.Instance.nova().MultipleTimes().AndReturn(self.fc) instances.Instance.nova().MultipleTimes().AndReturn(self.fc)