diff --git a/moto/autoscaling/models.py b/moto/autoscaling/models.py
index 5a044c038..eef22b397 100644
--- a/moto/autoscaling/models.py
+++ b/moto/autoscaling/models.py
@@ -3,6 +3,7 @@ from boto.ec2.blockdevicemapping import BlockDeviceType, BlockDeviceMapping
from moto.core import BaseBackend
from moto.ec2 import ec2_backends
from moto.elb import elb_backends
+from moto.elb.exceptions import LoadBalancerNotFoundError
# http://docs.aws.amazon.com/AutoScaling/latest/DeveloperGuide/AS_Concepts.html#Cooldown
DEFAULT_COOLDOWN = 300
@@ -80,6 +81,19 @@ class FakeLaunchConfiguration(object):
)
return config
+ @classmethod
+ def update_from_cloudformation_json(cls, original_resource, new_resource_name, cloudformation_json, region_name):
+ cls.delete_from_cloudformation_json(original_resource.name, cloudformation_json, region_name)
+ return cls.create_from_cloudformation_json(new_resource_name, cloudformation_json, region_name)
+
+ @classmethod
+ def delete_from_cloudformation_json(cls, resource_name, cloudformation_json, region_name):
+ backend = autoscaling_backends[region_name]
+ try:
+ backend.delete_launch_configuration(resource_name)
+ except KeyError:
+ pass
+
def delete(self, region_name):
backend = autoscaling_backends[region_name]
backend.delete_launch_configuration(self.name)
@@ -171,9 +185,29 @@ class FakeAutoScalingGroup(object):
)
return group
+ @classmethod
+ def update_from_cloudformation_json(cls, original_resource, new_resource_name, cloudformation_json, region_name):
+ cls.delete_from_cloudformation_json(original_resource.name, cloudformation_json, region_name)
+ return cls.create_from_cloudformation_json(new_resource_name, cloudformation_json, region_name)
+
+ @classmethod
+ def delete_from_cloudformation_json(cls, resource_name, cloudformation_json, region_name):
+ backend = autoscaling_backends[region_name]
+ try:
+ backend.delete_autoscaling_group(resource_name)
+ except KeyError:
+ pass
+ except LoadBalancerNotFoundError:
+ # sometimes the ELB gets modified before the ASG, so just skip over desired capacity
+ backend.autoscaling_groups.pop(resource_name, None)
+
def delete(self, region_name):
backend = autoscaling_backends[region_name]
- backend.delete_autoscaling_group(self.name)
+ try:
+ backend.delete_autoscaling_group(self.name)
+ except LoadBalancerNotFoundError:
+ # sometimes the ELB gets deleted before the ASG, so just skip over desired capacity
+ backend.autoscaling_groups.pop(self.name, None)
@property
def physical_resource_id(self):
diff --git a/moto/cloudformation/exceptions.py b/moto/cloudformation/exceptions.py
index 1f480e321..8e34b84a8 100644
--- a/moto/cloudformation/exceptions.py
+++ b/moto/cloudformation/exceptions.py
@@ -14,7 +14,7 @@ class ValidationError(BadRequest):
super(ValidationError, self).__init__()
self.description = template.render(
code="ValidationError",
- messgae="Stack:{0} does not exist".format(name_or_id),
+ message="Stack:{0} does not exist".format(name_or_id),
)
@@ -24,7 +24,7 @@ class MissingParameterError(BadRequest):
super(MissingParameterError, self).__init__()
self.description = template.render(
code="Missing Parameter",
- messgae="Missing parameter {0}".format(parameter_name),
+ message="Missing parameter {0}".format(parameter_name),
)
diff --git a/moto/cloudformation/models.py b/moto/cloudformation/models.py
index 0002e8ae5..d6298906f 100644
--- a/moto/cloudformation/models.py
+++ b/moto/cloudformation/models.py
@@ -51,11 +51,11 @@ class FakeStack(object):
self.template = template
self.resource_map.update(json.loads(template))
self.output_map = self._create_output_map()
- self.status = 'UPDATE_COMPLETE'
+ self.status = "UPDATE_COMPLETE"
def delete(self):
self.resource_map.delete()
- self.status = 'DELETE_COMPLETE'
+ self.status = "DELETE_COMPLETE"
class CloudFormationBackend(BaseBackend):
diff --git a/moto/cloudformation/parsing.py b/moto/cloudformation/parsing.py
index 8f25d406e..fb631278a 100644
--- a/moto/cloudformation/parsing.py
+++ b/moto/cloudformation/parsing.py
@@ -115,7 +115,7 @@ def clean_json(resource_json, resources_map):
return result
if 'Fn::GetAtt' in resource_json:
- resource = resources_map[resource_json['Fn::GetAtt'][0]]
+ resource = resources_map.get(resource_json['Fn::GetAtt'][0])
if resource is None:
return resource_json
try:
@@ -208,15 +208,22 @@ def parse_and_create_resource(logical_id, resource_json, resources_map, region_n
def parse_and_update_resource(logical_id, resource_json, resources_map, region_name):
- resource_class, resource_json, resource_name = parse_resource(logical_id, resource_json, resources_map)
- resource = resource_class.update_from_cloudformation_json(resource_name, resource_json, region_name)
- return resource
+ resource_class, new_resource_json, new_resource_name = parse_resource(logical_id, resource_json, resources_map)
+ original_resource = resources_map[logical_id]
+ new_resource = resource_class.update_from_cloudformation_json(
+ original_resource=original_resource,
+ new_resource_name=new_resource_name,
+ cloudformation_json=new_resource_json,
+ region_name=region_name
+ )
+ new_resource.type = resource_json['Type']
+ new_resource.logical_resource_id = logical_id
+ return new_resource
def parse_and_delete_resource(logical_id, resource_json, resources_map, region_name):
resource_class, resource_json, resource_name = parse_resource(logical_id, resource_json, resources_map)
resource_class.delete_from_cloudformation_json(resource_name, resource_json, region_name)
- return None
def parse_condition(condition, resources_map, condition_map):
@@ -289,6 +296,8 @@ class ResourceMap(collections.Mapping):
return self._parsed_resources[resource_logical_id]
else:
resource_json = self._resource_json_map.get(resource_logical_id)
+ if not resource_json:
+ raise KeyError(resource_logical_id)
new_resource = parse_and_create_resource(resource_logical_id, resource_json, self, self._region_name)
self._parsed_resources[resource_logical_id] = new_resource
return new_resource
@@ -369,18 +378,40 @@ class ResourceMap(collections.Mapping):
parse_and_delete_resource(resource_name, resource_json, self, self._region_name)
self._parsed_resources.pop(resource_name)
- for resource_name in new_template:
- if resource_name in old_template and new_template[resource_name] != old_template[resource_name]:
+ resources_to_update = set(name for name in new_template if name in old_template and new_template[name] != old_template[name])
+ tries = 1
+ while resources_to_update and tries < 5:
+ for resource_name in resources_to_update.copy():
resource_json = new_template[resource_name]
-
- changed_resource = parse_and_update_resource(resource_name, resource_json, self, self._region_name)
- self._parsed_resources[resource_name] = changed_resource
+ try:
+ changed_resource = parse_and_update_resource(resource_name, resource_json, self, self._region_name)
+ except Exception as e:
+ # skip over dependency violations, and try again in a second pass
+ last_exception = e
+ else:
+ self._parsed_resources[resource_name] = changed_resource
+ resources_to_update.remove(resource_name)
+ tries += 1
+ if tries == 5:
+ raise last_exception
def delete(self):
- for resource in self.resources:
- parsed_resource = self._parsed_resources.pop(resource)
- if parsed_resource and hasattr(parsed_resource, 'delete'):
- parsed_resource.delete(self._region_name)
+ remaining_resources = set(self.resources)
+ tries = 1
+ while remaining_resources and tries < 5:
+ for resource in remaining_resources.copy():
+ parsed_resource = self._parsed_resources.get(resource)
+ if parsed_resource:
+ try:
+ parsed_resource.delete(self._region_name)
+ except Exception as e:
+ # skip over dependency violations, and try again in a second pass
+ last_exception = e
+ else:
+ remaining_resources.remove(resource)
+ tries += 1
+ if tries == 5:
+ raise last_exception
class OutputMap(collections.Mapping):
diff --git a/moto/cloudformation/responses.py b/moto/cloudformation/responses.py
index 4cdec2afc..711d6f82e 100644
--- a/moto/cloudformation/responses.py
+++ b/moto/cloudformation/responses.py
@@ -79,8 +79,8 @@ class CloudFormationResponse(BaseResponse):
resource = stack_resource
break
else:
- raise ValidationError(logical_resource_id)
-
+ raise ValidationError(logical_resource_id)
+
template = self.response_template(DESCRIBE_STACK_RESOURCE_RESPONSE_TEMPLATE)
return template.render(stack=stack, resource=resource)
@@ -106,7 +106,7 @@ class CloudFormationResponse(BaseResponse):
def get_template(self):
name_or_stack_id = self.querystring.get('StackName')[0]
stack = self.cloudformation_backend.get_stack(name_or_stack_id)
-
+
if self.request_json:
return json.dumps({
"GetTemplateResponse": {
@@ -124,21 +124,24 @@ class CloudFormationResponse(BaseResponse):
def update_stack(self):
stack_name = self._get_param('StackName')
- stack_body = self._get_param('TemplateBody')
+ if self._get_param('UsePreviousTemplate') == "true":
+ stack_body = self.cloudformation_backend.get_stack(stack_name).template
+ else:
+ stack_body = self._get_param('TemplateBody')
stack = self.cloudformation_backend.update_stack(
name=stack_name,
template=stack_body,
)
-
if self.request_json:
- return json.dumps({
+ stack_body = {
'UpdateStackResponse': {
'UpdateStackResult': {
'StackId': stack.name,
}
}
- })
+ }
+ return json.dumps(stack_body)
else:
template = self.response_template(UPDATE_STACK_RESPONSE_TEMPLATE)
return template.render(stack=stack)
@@ -310,6 +313,16 @@ GET_TEMPLATE_RESPONSE_TEMPLATE = """
"""
+UPDATE_STACK_RESPONSE_TEMPLATE = """
+
+ {{ stack.stack_id }}
+
+
+ b9b4b068-3a41-11e5-94eb-example
+
+
+"""
+
DELETE_STACK_RESPONSE_TEMPLATE = """
5ccc7dcd-744c-11e5-be70-example
diff --git a/moto/ec2/models.py b/moto/ec2/models.py
index b8709f374..7ad52d71c 100644
--- a/moto/ec2/models.py
+++ b/moto/ec2/models.py
@@ -434,6 +434,9 @@ class Instance(BotoInstance, TaggedEC2Resource):
self._state_reason = StateReason("Client.UserInitiatedShutdown: User initiated shutdown",
"Client.UserInitiatedShutdown")
+ def delete(self, region):
+ self.terminate()
+
def terminate(self, *args, **kwargs):
for nic in self.nics.values():
nic.stop()
@@ -1135,6 +1138,29 @@ class SecurityGroup(TaggedEC2Resource):
return security_group
+ @classmethod
+ def update_from_cloudformation_json(cls, original_resource, new_resource_name, cloudformation_json, region_name):
+ cls._delete_security_group_given_vpc_id(original_resource.name, original_resource.vpc_id, region_name)
+ return cls.create_from_cloudformation_json(new_resource_name, cloudformation_json, region_name)
+
+ @classmethod
+ def delete_from_cloudformation_json(cls, resource_name, cloudformation_json, region_name):
+ properties = cloudformation_json['Properties']
+ vpc_id = properties.get('VpcId')
+ cls._delete_security_group_given_vpc_id(resource_name, vpc_id, region_name)
+
+ @classmethod
+ def _delete_security_group_given_vpc_id(cls, resource_name, vpc_id, region_name):
+ ec2_backend = ec2_backends[region_name]
+ security_group = ec2_backend.get_security_group_from_name(resource_name, vpc_id)
+ if security_group:
+ security_group.delete(region_name)
+
+ def delete(self, region_name):
+ ''' Not exposed as part of the ELB API - used for CloudFormation. '''
+ backend = ec2_backends[region_name]
+ self.ec2_backend.delete_security_group(group_id=self.id)
+
@property
def physical_resource_id(self):
return self.id
diff --git a/moto/elb/models.py b/moto/elb/models.py
index 43b210b6f..d3cb802c7 100644
--- a/moto/elb/models.py
+++ b/moto/elb/models.py
@@ -64,20 +64,21 @@ class FakeLoadBalancer(object):
self.subnets = subnets or []
self.vpc_id = vpc_id or 'vpc-56e10e3d'
self.tags = {}
+ self.dns_name = "tests.us-east-1.elb.amazonaws.com"
for port in ports:
listener = FakeListener(
- protocol=port['protocol'],
- load_balancer_port=port['load_balancer_port'],
- instance_port=port['instance_port'],
- ssl_certificate_id=port.get('sslcertificate_id'),
+ protocol=(port.get('protocol') or port['Protocol']),
+ load_balancer_port=(port.get('load_balancer_port') or port['LoadBalancerPort']),
+ instance_port=(port.get('instance_port') or port['InstancePort']),
+ ssl_certificate_id=port.get('sslcertificate_id', port.get('SSLCertificateId')),
)
self.listeners.append(listener)
# it is unclear per the AWS documentation as to when or how backend
# information gets set, so let's guess and set it here *shrug*
backend = FakeBackend(
- instance_port=port['instance_port'],
+ instance_port=(port.get('instance_port') or port['InstancePort']),
)
self.backends.append(backend)
@@ -88,15 +89,41 @@ class FakeLoadBalancer(object):
elb_backend = elb_backends[region_name]
new_elb = elb_backend.create_load_balancer(
name=properties.get('LoadBalancerName', resource_name),
- zones=properties.get('AvailabilityZones'),
- ports=[],
+ zones=properties.get('AvailabilityZones', []),
+ ports=properties['Listeners'],
+ scheme=properties.get('Scheme', 'internet-facing'),
)
- instance_ids = cloudformation_json.get('Instances', [])
+ instance_ids = properties.get('Instances', [])
for instance_id in instance_ids:
elb_backend.register_instances(new_elb.name, [instance_id])
+
+ health_check = properties.get('HealthCheck')
+ if health_check:
+ elb_backend.configure_health_check(
+ load_balancer_name=new_elb.name,
+ timeout=health_check['Timeout'],
+ healthy_threshold=health_check['HealthyThreshold'],
+ unhealthy_threshold=health_check['UnhealthyThreshold'],
+ interval=health_check['Interval'],
+ target=health_check['Target'],
+ )
+
return new_elb
+ @classmethod
+ def update_from_cloudformation_json(cls, original_resource, new_resource_name, cloudformation_json, region_name):
+ cls.delete_from_cloudformation_json(original_resource.name, cloudformation_json, region_name)
+ return cls.create_from_cloudformation_json(new_resource_name, cloudformation_json, region_name)
+
+ @classmethod
+ def delete_from_cloudformation_json(cls, resource_name, cloudformation_json, region_name):
+ elb_backend = elb_backends[region_name]
+ try:
+ elb_backend.delete_load_balancer(resource_name)
+ except KeyError:
+ pass
+
@property
def physical_resource_id(self):
return self.name
@@ -108,7 +135,7 @@ class FakeLoadBalancer(object):
elif attribute_name == 'CanonicalHostedZoneNameID':
raise NotImplementedError('"Fn::GetAtt" : [ "{0}" , "CanonicalHostedZoneNameID" ]"')
elif attribute_name == 'DNSName':
- raise NotImplementedError('"Fn::GetAtt" : [ "{0}" , "DNSName" ]"')
+ return self.dns_name
elif attribute_name == 'SourceSecurityGroup.GroupName':
raise NotImplementedError('"Fn::GetAtt" : [ "{0}" , "SourceSecurityGroup.GroupName" ]"')
elif attribute_name == 'SourceSecurityGroup.OwnerAlias':
@@ -149,6 +176,10 @@ class FakeLoadBalancer(object):
if key in self.tags:
del self.tags[key]
+ def delete(self, region):
+ ''' Not exposed as part of the ELB API - used for CloudFormation. '''
+ elb_backends[region].delete_load_balancer(self.name)
+
class ELBBackend(BaseBackend):
diff --git a/moto/elb/responses.py b/moto/elb/responses.py
index 52c649ab3..f5e2393cd 100644
--- a/moto/elb/responses.py
+++ b/moto/elb/responses.py
@@ -12,8 +12,7 @@ from boto.ec2.elb.policies import (
from moto.core.responses import BaseResponse
from .models import elb_backends
-from .exceptions import DuplicateTagKeysError, LoadBalancerNotFoundError, \
- TooManyTagsError
+from .exceptions import DuplicateTagKeysError, LoadBalancerNotFoundError
class ELBResponse(BaseResponse):
@@ -29,16 +28,16 @@ class ELBResponse(BaseResponse):
scheme = self._get_param('Scheme')
subnets = self._get_multi_param("Subnets.member")
- elb = self.elb_backend.create_load_balancer(
+ load_balancer = self.elb_backend.create_load_balancer(
name=load_balancer_name,
zones=availability_zones,
ports=ports,
scheme=scheme,
subnets=subnets,
)
- self._add_tags(elb)
+ self._add_tags(load_balancer)
template = self.response_template(CREATE_LOAD_BALANCER_TEMPLATE)
- return template.render()
+ return template.render(load_balancer=load_balancer)
def create_load_balancer_listeners(self):
load_balancer_name = self._get_param('LoadBalancerName')
@@ -244,7 +243,7 @@ class ELBResponse(BaseResponse):
load_balancer_name = self._get_param('LoadBalancerNames.member.{0}'.format(number))
elb = self.elb_backend.get_load_balancer(load_balancer_name)
if not elb:
- raise LoadBalancerNotFound(load_balancer_name)
+ raise LoadBalancerNotFoundError(load_balancer_name)
key = 'Tag.member.{0}.Key'.format(number)
for t_key, t_val in self.querystring.items():
@@ -263,7 +262,7 @@ class ELBResponse(BaseResponse):
load_balancer_name = self._get_param('LoadBalancerNames.member.{0}'.format(number))
elb = self.elb_backend.get_load_balancer(load_balancer_name)
if not elb:
- raise LoadBalancerNotFound(load_balancer_name)
+ raise LoadBalancerNotFoundError(load_balancer_name)
elbs.append(elb)
template = self.response_template(DESCRIBE_TAGS_TEMPLATE)
@@ -334,7 +333,7 @@ DESCRIBE_TAGS_TEMPLATE = """
- tests.us-east-1.elb.amazonaws.com
+ {{ load_balancer.dns_name }}
1549581b-12b7-11e3-895e-1334aEXAMPLE
@@ -442,7 +441,7 @@ DESCRIBE_LOAD_BALANCERS_TEMPLATE = """
{{ record_set.name }}
diff --git a/moto/sqs/models.py b/moto/sqs/models.py
index 07699ecbc..4e9a3678f 100644
--- a/moto/sqs/models.py
+++ b/moto/sqs/models.py
@@ -137,7 +137,7 @@ class Queue(object):
)
@classmethod
- def update_from_cloudformation_json(cls, resource_name, cloudformation_json, region_name):
+ def update_from_cloudformation_json(cls, original_resource, new_resource_name, cloudformation_json, region_name):
properties = cloudformation_json['Properties']
queue_name = properties['QueueName']
diff --git a/tests/test_cloudformation/test_cloudformation_stack_crud.py b/tests/test_cloudformation/test_cloudformation_stack_crud.py
index 040ed4d50..26084c7b4 100644
--- a/tests/test_cloudformation/test_cloudformation_stack_crud.py
+++ b/tests/test_cloudformation/test_cloudformation_stack_crud.py
@@ -274,6 +274,7 @@ def test_update_stack():
conn.update_stack("test_stack", dummy_template_json2)
stack = conn.describe_stacks()[0]
+ stack.stack_status.should.equal("UPDATE_COMPLETE")
stack.get_template().should.equal({
'GetTemplateResponse': {
'GetTemplateResult': {
@@ -284,3 +285,25 @@ def test_update_stack():
}
}
})
+
+@mock_cloudformation
+def test_update_stack():
+ conn = boto.connect_cloudformation()
+ conn.create_stack(
+ "test_stack",
+ template_body=dummy_template_json,
+ )
+ conn.update_stack("test_stack", use_previous_template=True)
+
+ stack = conn.describe_stacks()[0]
+ stack.stack_status.should.equal("UPDATE_COMPLETE")
+ stack.get_template().should.equal({
+ 'GetTemplateResponse': {
+ 'GetTemplateResult': {
+ 'TemplateBody': dummy_template_json,
+ 'ResponseMetadata': {
+ 'RequestId': '2d06e36c-ac1d-11e0-a958-f9382b6eb86bEXAMPLE'
+ }
+ }
+ }
+ })
diff --git a/tests/test_cloudformation/test_cloudformation_stack_integration.py b/tests/test_cloudformation/test_cloudformation_stack_integration.py
index 609ab4a21..07fb93292 100644
--- a/tests/test_cloudformation/test_cloudformation_stack_integration.py
+++ b/tests/test_cloudformation/test_cloudformation_stack_integration.py
@@ -262,10 +262,17 @@ def test_stack_elb_integration_with_attached_ec2_instances():
"Resources": {
"MyELB": {
"Type": "AWS::ElasticLoadBalancing::LoadBalancer",
- "Instances": [{"Ref": "Ec2Instance1"}],
"Properties": {
+ "Instances": [{"Ref": "Ec2Instance1"}],
"LoadBalancerName": "test-elb",
- "AvailabilityZones": ['us-east1'],
+ "AvailabilityZones": ['us-east-1'],
+ "Listeners": [
+ {
+ "InstancePort": "80",
+ "LoadBalancerPort": "80",
+ "Protocol": "HTTP",
+ }
+ ],
}
},
"Ec2Instance1": {
@@ -293,7 +300,99 @@ def test_stack_elb_integration_with_attached_ec2_instances():
ec2_instance = reservation.instances[0]
load_balancer.instances[0].id.should.equal(ec2_instance.id)
- list(load_balancer.availability_zones).should.equal(['us-east1'])
+ list(load_balancer.availability_zones).should.equal(['us-east-1'])
+
+
+@mock_elb()
+@mock_cloudformation()
+def test_stack_elb_integration_with_health_check():
+ elb_template = {
+ "AWSTemplateFormatVersion": "2010-09-09",
+ "Resources": {
+ "MyELB": {
+ "Type": "AWS::ElasticLoadBalancing::LoadBalancer",
+ "Properties": {
+ "LoadBalancerName": "test-elb",
+ "AvailabilityZones": ['us-west-1'],
+ "HealthCheck": {
+ "HealthyThreshold": "3",
+ "Interval": "5",
+ "Target": "HTTP:80/healthcheck",
+ "Timeout": "4",
+ "UnhealthyThreshold": "2",
+ },
+ "Listeners": [
+ {
+ "InstancePort": "80",
+ "LoadBalancerPort": "80",
+ "Protocol": "HTTP",
+ }
+ ],
+ }
+ },
+ },
+ }
+ elb_template_json = json.dumps(elb_template)
+
+ conn = boto.cloudformation.connect_to_region("us-west-1")
+ conn.create_stack(
+ "elb_stack",
+ template_body=elb_template_json,
+ )
+
+ elb_conn = boto.ec2.elb.connect_to_region("us-west-1")
+ load_balancer = elb_conn.get_all_load_balancers()[0]
+ health_check = load_balancer.health_check
+
+ health_check.healthy_threshold.should.equal(3)
+ health_check.interval.should.equal(5)
+ health_check.target.should.equal("HTTP:80/healthcheck")
+ health_check.timeout.should.equal(4)
+ health_check.unhealthy_threshold.should.equal(2)
+
+
+@mock_elb()
+@mock_cloudformation()
+def test_stack_elb_integration_with_update():
+ elb_template = {
+ "AWSTemplateFormatVersion": "2010-09-09",
+ "Resources": {
+ "MyELB": {
+ "Type": "AWS::ElasticLoadBalancing::LoadBalancer",
+ "Properties": {
+ "LoadBalancerName": "test-elb",
+ "AvailabilityZones": ['us-west-1a'],
+ "Listeners": [
+ {
+ "InstancePort": "80",
+ "LoadBalancerPort": "80",
+ "Protocol": "HTTP",
+ }
+ ],
+ }
+ },
+ },
+ }
+ elb_template_json = json.dumps(elb_template)
+
+ conn = boto.cloudformation.connect_to_region("us-west-1")
+ conn.create_stack(
+ "elb_stack",
+ template_body=elb_template_json,
+ )
+
+ elb_conn = boto.ec2.elb.connect_to_region("us-west-1")
+ load_balancer = elb_conn.get_all_load_balancers()[0]
+ load_balancer.availability_zones[0].should.equal('us-west-1a')
+
+ elb_template['Resources']['MyELB']['Properties']['AvailabilityZones'] = ['us-west-1b']
+ elb_template_json = json.dumps(elb_template)
+ conn.update_stack(
+ "elb_stack",
+ template_body=elb_template_json,
+ )
+ load_balancer = elb_conn.get_all_load_balancers()[0]
+ load_balancer.availability_zones[0].should.equal('us-west-1b')
@mock_ec2()
@@ -451,11 +550,11 @@ def test_autoscaling_group_with_elb():
"Listeners": [{
"LoadBalancerPort": "80",
"InstancePort": "80",
- "Protocol": "HTTP"
+ "Protocol": "HTTP",
}],
"LoadBalancerName": "my-elb",
"HealthCheck": {
- "Target": "80",
+ "Target": "HTTP:80",
"HealthyThreshold": "3",
"UnhealthyThreshold": "5",
"Interval": "30",
@@ -498,6 +597,55 @@ def test_autoscaling_group_with_elb():
elb_resource.physical_resource_id.should.contain("my-elb")
+@mock_autoscaling()
+@mock_cloudformation()
+def test_autoscaling_group_update():
+ asg_template = {
+ "AWSTemplateFormatVersion": "2010-09-09",
+ "Resources": {
+ "my-as-group": {
+ "Type": "AWS::AutoScaling::AutoScalingGroup",
+ "Properties": {
+ "AvailabilityZones": ['us-west-1'],
+ "LaunchConfigurationName": {"Ref": "my-launch-config"},
+ "MinSize": "2",
+ "MaxSize": "2",
+ },
+ },
+
+ "my-launch-config": {
+ "Type": "AWS::AutoScaling::LaunchConfiguration",
+ "Properties": {
+ "ImageId": "ami-1234abcd",
+ "UserData": "some user data",
+ }
+ },
+ },
+ }
+ asg_template_json = json.dumps(asg_template)
+
+ conn = boto.cloudformation.connect_to_region("us-west-1")
+ conn.create_stack(
+ "asg_stack",
+ template_body=asg_template_json,
+ )
+
+ autoscale_conn = boto.ec2.autoscale.connect_to_region("us-west-1")
+ asg = autoscale_conn.get_all_groups()[0]
+ asg.min_size.should.equal(2)
+ asg.max_size.should.equal(2)
+
+ asg_template['Resources']['my-as-group']['Properties']['MaxSize'] = 3
+ asg_template_json = json.dumps(asg_template)
+ conn.update_stack(
+ "asg_stack",
+ template_body=asg_template_json,
+ )
+ asg = autoscale_conn.get_all_groups()[0]
+ asg.min_size.should.equal(2)
+ asg.max_size.should.equal(3)
+
+
@mock_ec2()
@mock_cloudformation()
def test_vpc_single_instance_in_subnet():
@@ -1072,6 +1220,46 @@ def test_route53_associate_health_check():
record_set.health_check.should.equal(health_check_id)
+@mock_cloudformation()
+@mock_route53()
+def test_route53_with_update():
+ route53_conn = boto.connect_route53()
+
+ template_json = json.dumps(route53_health_check.template)
+ cf_conn = boto.cloudformation.connect_to_region("us-west-1")
+ cf_conn.create_stack(
+ "test_stack",
+ template_body=template_json,
+ )
+
+ zones = route53_conn.get_all_hosted_zones()['ListHostedZonesResponse']['HostedZones']
+ list(zones).should.have.length_of(1)
+ zone_id = zones[0]['Id']
+
+ rrsets = route53_conn.get_all_rrsets(zone_id)
+ rrsets.should.have.length_of(1)
+
+ record_set = rrsets[0]
+ record_set.resource_records.should.equal(["my.example.com"])
+
+ route53_health_check.template['Resources']['myDNSRecord']['Properties']['ResourceRecords'] = ["my_other.example.com"]
+ template_json = json.dumps(route53_health_check.template)
+ cf_conn.update_stack(
+ "test_stack",
+ template_body=template_json,
+ )
+
+ zones = route53_conn.get_all_hosted_zones()['ListHostedZonesResponse']['HostedZones']
+ list(zones).should.have.length_of(1)
+ zone_id = zones[0]['Id']
+
+ rrsets = route53_conn.get_all_rrsets(zone_id)
+ rrsets.should.have.length_of(1)
+
+ record_set = rrsets[0]
+ record_set.resource_records.should.equal(["my_other.example.com"])
+
+
@mock_cloudformation()
@mock_sns()
def test_sns_topic():
@@ -1382,6 +1570,51 @@ def test_security_group_ingress_separate_from_security_group_by_id_using_vpc():
security_group1.rules[0].to_port.should.equal('8080')
+@mock_cloudformation
+@mock_ec2
+def test_security_group_with_update():
+ vpc_conn = boto.vpc.connect_to_region("us-west-1")
+ vpc1 = vpc_conn.create_vpc("10.0.0.0/16")
+
+ template = {
+ "AWSTemplateFormatVersion": "2010-09-09",
+ "Resources": {
+ "test-security-group": {
+ "Type": "AWS::EC2::SecurityGroup",
+ "Properties": {
+ "GroupDescription": "test security group",
+ "VpcId": vpc1.id,
+ "Tags": [
+ {
+ "Key": "sg-name",
+ "Value": "sg"
+ }
+ ]
+ },
+ },
+ }
+ }
+
+ template_json = json.dumps(template)
+ cf_conn = boto.cloudformation.connect_to_region("us-west-1")
+ cf_conn.create_stack(
+ "test_stack",
+ template_body=template_json,
+ )
+ security_group = vpc_conn.get_all_security_groups(filters={"tag:sg-name": "sg"})[0]
+ security_group.vpc_id.should.equal(vpc1.id)
+
+ vpc2 = vpc_conn.create_vpc("10.1.0.0/16")
+ template['Resources']['test-security-group']['Properties']['VpcId'] = vpc2.id
+ template_json = json.dumps(template)
+ cf_conn.update_stack(
+ "test_stack",
+ template_body=template_json,
+ )
+ security_group = vpc_conn.get_all_security_groups(filters={"tag:sg-name": "sg"})[0]
+ security_group.vpc_id.should.equal(vpc2.id)
+
+
@mock_cloudformation
@mock_ec2
def test_subnets_should_be_created_with_availability_zone():