From 73fa5ba6625a29b62c86e2b9dfb3b00029c21129 Mon Sep 17 00:00:00 2001 From: huangtianhua Date: Tue, 11 Nov 2014 10:49:54 +0800 Subject: [PATCH] Implement 'InstanceId' for autoscaling group Implement 'InstanceId' for AWS::AutoScaling::AutoScalingGroup resource to be compatible with AWSCloudFormation. Implements: blueprint implement-instanceid-for-autoscalinggroup Change-Id: I939798202d27c3a16a502a148efe37e44077bda8 --- .../engine/resources/aws/autoscaling_group.py | 60 +++++++++++-- heat/tests/test_autoscaling.py | 88 ++++++++++++++++++- 2 files changed, 139 insertions(+), 9 deletions(-) diff --git a/heat/engine/resources/aws/autoscaling_group.py b/heat/engine/resources/aws/autoscaling_group.py index 7e64a84149..aca069ccc3 100644 --- a/heat/engine/resources/aws/autoscaling_group.py +++ b/heat/engine/resources/aws/autoscaling_group.py @@ -23,9 +23,12 @@ from heat.common.i18n import _LE from heat.common.i18n import _LI from heat.engine import attributes from heat.engine import constraints +from heat.engine import function from heat.engine.notification import autoscaling as notification from heat.engine import properties +from heat.engine import resource from heat.engine.resources import instance_group as instgrp +from heat.engine import rsrc_defn from heat.engine import support from heat.openstack.common import log as logging from heat.scaling import cooldown @@ -78,10 +81,12 @@ class AutoScalingGroup(instgrp.InstanceGroup, cooldown.CooldownMixin): AVAILABILITY_ZONES, LAUNCH_CONFIGURATION_NAME, MAX_SIZE, MIN_SIZE, COOLDOWN, DESIRED_CAPACITY, HEALTH_CHECK_GRACE_PERIOD, HEALTH_CHECK_TYPE, LOAD_BALANCER_NAMES, VPCZONE_IDENTIFIER, TAGS, + INSTANCE_ID, ) = ( 'AvailabilityZones', 'LaunchConfigurationName', 'MaxSize', 'MinSize', 'Cooldown', 'DesiredCapacity', 'HealthCheckGracePeriod', 'HealthCheckType', 'LoadBalancerNames', 'VPCZoneIdentifier', 'Tags', + 'InstanceId', ) _TAG_KEYS = ( @@ -117,9 +122,18 @@ class AutoScalingGroup(instgrp.InstanceGroup, cooldown.CooldownMixin): LAUNCH_CONFIGURATION_NAME: properties.Schema( properties.Schema.STRING, _('The reference to a LaunchConfiguration resource.'), - required=True, update_allowed=True ), + INSTANCE_ID: properties.Schema( + properties.Schema.STRING, + _('The ID of an existing instance to use to ' + 'create the Auto Scaling group. If specify this property, ' + 'will create the group use an existing instance instead of ' + 'a launch configuration.'), + constraints=[ + constraints.CustomConstraint("nova.server") + ] + ), MAX_SIZE: properties.Schema( properties.Schema.INTEGER, _('Maximum number of instances in the group.'), @@ -216,9 +230,32 @@ class AutoScalingGroup(instgrp.InstanceGroup, cooldown.CooldownMixin): return self.create_with_template(self.child_template(), self._environment()) + def _make_launch_config_resource(self, name, props): + lc_res_type = 'AWS::AutoScaling::LaunchConfiguration' + lc_res_def = rsrc_defn.ResourceDefinition(name, + lc_res_type, + props) + lc_res = resource.Resource(name, lc_res_def, self.stack) + return lc_res + def _get_conf_properties(self): - conf, props = super(AutoScalingGroup, self)._get_conf_properties() - vpc_zone_ids = self.properties.get(AutoScalingGroup.VPCZONE_IDENTIFIER) + instance_id = self.properties.get(self.INSTANCE_ID) + if instance_id: + server = self.client_plugin('nova').get_server(instance_id) + instance_props = { + 'ImageId': server.image['id'], + 'InstanceType': server.flavor['id'], + 'KeyName': server.key_name, + 'SecurityGroups': [sg['name'] + for sg in server.security_groups] + } + conf = self._make_launch_config_resource(self.name, + instance_props) + props = function.resolve(conf.properties.data) + else: + conf, props = super(AutoScalingGroup, self)._get_conf_properties() + + vpc_zone_ids = self.properties.get(self.VPCZONE_IDENTIFIER) if vpc_zone_ids: props['SubnetId'] = vpc_zone_ids[0] @@ -326,10 +363,6 @@ class AutoScalingGroup(instgrp.InstanceGroup, cooldown.CooldownMixin): return super(AutoScalingGroup, self)._tags() + autoscaling_tag def validate(self): - res = super(AutoScalingGroup, self).validate() - if res: - return res - # check validity of group size min_size = self.properties[self.MIN_SIZE] max_size = self.properties[self.MAX_SIZE] @@ -356,6 +389,19 @@ class AutoScalingGroup(instgrp.InstanceGroup, cooldown.CooldownMixin): len(self.properties[self.VPCZONE_IDENTIFIER]) != 1): raise exception.NotSupported(feature=_("Anything other than one " "VPCZoneIdentifier")) + # validate properties InstanceId and LaunchConfigurationName + # for aws auto scaling group. + # should provide just only one of + if self.type() == 'AWS::AutoScaling::AutoScalingGroup': + instanceId = self.properties.get(self.INSTANCE_ID) + launch_config = self.properties.get( + self.LAUNCH_CONFIGURATION_NAME) + if bool(instanceId) == bool(launch_config): + msg = _("Either 'InstanceId' or 'LaunchConfigurationName' " + "must be provided.") + raise exception.StackValidationFailed(message=msg) + + super(AutoScalingGroup, self).validate() def _resolve_attribute(self, name): ''' diff --git a/heat/tests/test_autoscaling.py b/heat/tests/test_autoscaling.py index 31a26ee4b8..21ee6ef6b3 100644 --- a/heat/tests/test_autoscaling.py +++ b/heat/tests/test_autoscaling.py @@ -24,6 +24,7 @@ from heat.common import exception from heat.common import grouputils from heat.common import short_id from heat.common import template_format +from heat.engine.clients.os import nova from heat.engine.notification import autoscaling as notification from heat.engine import parser from heat.engine import resource @@ -111,12 +112,14 @@ class AutoScalingTest(common.HeatTestCase): self.assertEqual((rsrc.CREATE, rsrc.COMPLETE), rsrc.state) return rsrc - def _stub_create(self, num, with_error=None): + def _stub_create(self, num, with_error=None, with_lcn=True): self.m.StubOutWithMock(instance.Instance, 'handle_create') self.m.StubOutWithMock(instance.Instance, 'check_create_complete') self.stub_ImageConstraint_validate() self.stub_FlavorConstraint_validate() - self.stub_SnapshotConstraint_validate() + # create with launch config name, need to stub snapshot constraint + if with_lcn: + self.stub_SnapshotConstraint_validate() if with_error: instance.Instance.handle_create().AndRaise( exception.Error(with_error)) @@ -701,3 +704,84 @@ class AutoScalingTest(common.HeatTestCase): rsrc.delete() self.m.VerifyAll() + + def _stub_nova_server_get(self, not_found=False): + mock_server = mock.MagicMock() + mock_server.image = {'id': 'dd619705-468a-4f7d-8a06-b84794b3561a'} + mock_server.flavor = {'id': '1'} + mock_server.key_name = 'test' + mock_server.security_groups = [{u'name': u'hth_test'}] + if not_found: + self.patchobject(nova.NovaClientPlugin, 'get_server', + side_effect=exception.ServerNotFound( + server='5678')) + else: + self.patchobject(nova.NovaClientPlugin, 'get_server', + return_value=mock_server) + + def test_validate_without_InstanceId_and_LaunchConfigurationName(self): + t = template_format.parse(as_template) + agp = t['Resources']['WebServerGroup']['Properties'] + agp.pop('LaunchConfigurationName') + agp.pop('LoadBalancerNames') + stack = utils.parse_stack(t, params=self.params) + rsrc = stack['WebServerGroup'] + error_msg = ("Either 'InstanceId' or 'LaunchConfigurationName' " + "must be provided.") + exc = self.assertRaises(exception.StackValidationFailed, + rsrc.validate) + self.assertIn(error_msg, six.text_type(exc)) + + def test_validate_with_InstanceId_and_LaunchConfigurationName(self): + t = template_format.parse(as_template) + agp = t['Resources']['WebServerGroup']['Properties'] + agp['InstanceId'] = '5678' + stack = utils.parse_stack(t, params=self.params) + rsrc = stack['WebServerGroup'] + error_msg = ("Either 'InstanceId' or 'LaunchConfigurationName' " + "must be provided.") + exc = self.assertRaises(exception.StackValidationFailed, + rsrc.validate) + self.assertIn(error_msg, six.text_type(exc)) + + def test_scaling_group_create_with_instanceid(self): + t = template_format.parse(as_template) + agp = t['Resources']['WebServerGroup']['Properties'] + agp['InstanceId'] = '5678' + agp.pop('LaunchConfigurationName') + agp.pop('LoadBalancerNames') + stack = utils.parse_stack(t, params=self.params) + rsrc = stack['WebServerGroup'] + self.stub_KeypairConstraint_validate() + self._stub_nova_server_get() + self._stub_create(1, with_lcn=False) + self.m.ReplayAll() + + scheduler.TaskRunner(rsrc.create)() + self.assertEqual((rsrc.CREATE, rsrc.COMPLETE), rsrc.state) + instance_definition = rsrc._get_instance_definition() + ins_props = instance_definition['Properties'] + self.assertEqual('dd619705-468a-4f7d-8a06-b84794b3561a', + ins_props['ImageId']) + self.assertEqual('test', ins_props['KeyName']) + self.assertEqual(['hth_test'], ins_props['SecurityGroups']) + self.assertEqual('1', ins_props['InstanceType']) + + self.m.VerifyAll() + + def test_scaling_group_create_with_instanceid_not_found(self): + t = template_format.parse(as_template) + agp = t['Resources']['WebServerGroup']['Properties'] + agp.pop('LaunchConfigurationName') + agp['InstanceId'] = '5678' + stack = utils.parse_stack(t, params=self.params) + rsrc = stack['WebServerGroup'] + self._stub_nova_server_get(not_found=True) + self.m.ReplayAll() + msg = ("Property error : WebServerGroup: InstanceId Error validating " + "value '5678': The server (5678) could not be found") + exc = self.assertRaises(exception.StackValidationFailed, + rsrc.validate) + self.assertIn(msg, six.text_type(exc)) + + self.m.VerifyAll()