Fix merge conflicts and add EC2 Instance delete. Closes #576.

This commit is contained in:
Steve Pulec 2016-04-28 09:21:54 -04:00
parent 0b24c6be57
commit a600deb96a
12 changed files with 461 additions and 50 deletions

View File

@ -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):

View File

@ -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),
)

View File

@ -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):

View File

@ -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):

View File

@ -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 = """<GetTemplateResponse>
</GetTemplateResponse>"""
UPDATE_STACK_RESPONSE_TEMPLATE = """<UpdateStackResponse xmlns="http://cloudformation.amazonaws.com/doc/2010-05-15/">
<UpdateStackResult>
<StackId>{{ stack.stack_id }}</StackId>
</UpdateStackResult>
<ResponseMetadata>
<RequestId>b9b4b068-3a41-11e5-94eb-example</RequestId>
</ResponseMetadata>
</UpdateStackResponse>
"""
DELETE_STACK_RESPONSE_TEMPLATE = """<DeleteStackResponse>
<ResponseMetadata>
<RequestId>5ccc7dcd-744c-11e5-be70-example</RequestId>

View File

@ -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

View File

@ -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):

View File

@ -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 = """<DescribeTagsResponse xmlns="http://elasticloadbalan
CREATE_LOAD_BALANCER_TEMPLATE = """<CreateLoadBalancerResponse xmlns="http://elasticloadbalancing.amazonaws.com/doc/2012-06-01/">
<CreateLoadBalancerResult>
<DNSName>tests.us-east-1.elb.amazonaws.com</DNSName>
<DNSName>{{ load_balancer.dns_name }}</DNSName>
</CreateLoadBalancerResult>
<ResponseMetadata>
<RequestId>1549581b-12b7-11e3-895e-1334aEXAMPLE</RequestId>
@ -442,7 +441,7 @@ DESCRIBE_LOAD_BALANCERS_TEMPLATE = """<DescribeLoadBalancersResponse xmlns="http
<CanonicalHostedZoneName>tests.us-east-1.elb.amazonaws.com</CanonicalHostedZoneName>
<CanonicalHostedZoneNameID>Z3ZONEID</CanonicalHostedZoneNameID>
<Scheme>{{ load_balancer.scheme }}</Scheme>
<DNSName>tests.us-east-1.elb.amazonaws.com</DNSName>
<DNSName>{{ load_balancer.dns_name }}</DNSName>
<BackendServerDescriptions>
{% for backend in load_balancer.backends %}
<member>

View File

@ -85,6 +85,27 @@ class RecordSet(object):
record_set = hosted_zone.add_rrset(properties)
return record_set
@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):
# this will break if you changed the zone the record is in, unfortunately
properties = cloudformation_json['Properties']
zone_name = properties.get("HostedZoneName")
if zone_name:
hosted_zone = route53_backend.get_hosted_zone_by_name(zone_name)
else:
hosted_zone = route53_backend.get_hosted_zone(properties["HostedZoneId"])
try:
hosted_zone.delete_rrset_by_name(resource_name)
except KeyError:
pass
def to_xml(self):
template = Template("""<ResourceRecordSet>
<Name>{{ record_set.name }}</Name>

View File

@ -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']

View File

@ -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'
}
}
}
})

View File

@ -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():