From 91a74424e568b7297f5d3f129b3ed9a33874057d Mon Sep 17 00:00:00 2001 From: Joseph Lawson Date: Mon, 20 Oct 2014 11:45:47 -0400 Subject: [PATCH 01/13] Handle Name Type resources for CloudFormation --- moto/cloudformation/parsing.py | 30 +++++++++++++++++++ moto/cloudformation/utils.py | 8 +++++ .../test_cloudformation/test_stack_parsing.py | 29 ++++++++++++++++++ 3 files changed, 67 insertions(+) diff --git a/moto/cloudformation/parsing.py b/moto/cloudformation/parsing.py index c3bf64620..46adf1540 100644 --- a/moto/cloudformation/parsing.py +++ b/moto/cloudformation/parsing.py @@ -7,6 +7,7 @@ from moto.ec2 import models as ec2_models from moto.elb import models as elb_models from moto.iam import models as iam_models from moto.sqs import models as sqs_models +from .utils import random_suffix MODEL_MAP = { "AWS::AutoScaling::AutoScalingGroup": autoscaling_models.FakeAutoScalingGroup, @@ -29,6 +30,20 @@ MODEL_MAP = { "AWS::SQS::Queue": sqs_models.Queue, } +# http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-name.html +NAME_TYPE_MAP = { + "AWS::CloudWatch::Alarm": "Alarm", + "AWS::DynamoDB::Table": "TableName", + "AWS::ElastiCache::CacheCluster": "ClusterName", + "AWS::ElasticBeanstalk::Application": "ApplicationName", + "AWS::ElasticBeanstalk::Environment": "EnvironmentName", + "AWS::ElasticLoadBalancing::LoadBalancer": "LoadBalancerName", + "AWS::RDS::DBInstance": "DBInstanceIdentifier", + "AWS::S3::Bucket": "BucketName", + "AWS::SNS::Topic": "TopicName", + "AWS::SQS::Queue": "QueueName" +} + # Just ignore these models types for now NULL_MODELS = [ "AWS::CloudFormation::WaitCondition", @@ -73,6 +88,12 @@ def resource_class_from_type(resource_type): return MODEL_MAP.get(resource_type) +def resource_name_property_from_type(resource_type): + if resource_type not in NAME_TYPE_MAP: + return None + return NAME_TYPE_MAP.get(resource_type) + + def parse_resource(resource_name, resource_json, resources_map): resource_type = resource_json['Type'] resource_class = resource_class_from_type(resource_type) @@ -80,6 +101,15 @@ def parse_resource(resource_name, resource_json, resources_map): return None resource_json = clean_json(resource_json, resources_map) + resource_name_property = resource_name_property_from_type(resource_type) + if resource_name_property: + if not 'Properties' in resource_json: + resource_json['Properties'] = dict() + if not resource_name_property in resource_json['Properties']: + resource_json['Properties'][resource_name_property] = '{0}-{1}-{2}'.format( + resources_map.get('AWS::StackName'), + resource_name, + random_suffix()) resource = resource_class.create_from_cloudformation_json(resource_name, resource_json) resource.type = resource_type resource.logical_resource_id = resource_name diff --git a/moto/cloudformation/utils.py b/moto/cloudformation/utils.py index 09cffd6a0..1d629c76b 100644 --- a/moto/cloudformation/utils.py +++ b/moto/cloudformation/utils.py @@ -1,7 +1,15 @@ from __future__ import unicode_literals import uuid +import six +import random def generate_stack_id(stack_name): random_id = uuid.uuid4() return "arn:aws:cloudformation:us-east-1:123456789:stack/{0}/{1}".format(stack_name, random_id) + + +def random_suffix(): + size = 12 + chars = list(range(10)) + ['A-Z'] + return ''.join(six.text_type(random.choice(chars)) for x in range(size)) diff --git a/tests/test_cloudformation/test_stack_parsing.py b/tests/test_cloudformation/test_stack_parsing.py index 4b99039ae..8df64f5ef 100644 --- a/tests/test_cloudformation/test_stack_parsing.py +++ b/tests/test_cloudformation/test_stack_parsing.py @@ -25,7 +25,24 @@ dummy_template = { }, } +name_type_template = { + "AWSTemplateFormatVersion": "2010-09-09", + + "Description": "Create a multi-az, load balanced, Auto Scaled sample web site. The Auto Scaling trigger is based on the CPU utilization of the web servers. The AMI is chosen based on the region in which the stack is run. This example creates a web service running across all availability zones in a region. The instances are load balanced with a simple health check. The web site is available on port 80, however, the instances can be configured to listen on any port (8888 by default). **WARNING** This template creates one or more Amazon EC2 instances. You will be billed for the AWS resources used if you create a stack from this template.", + + "Resources": { + "WebServerGroup": { + + "Type": "AWS::SQS::Queue", + "Properties": { + "VisibilityTimeout": 60, + } + }, + }, +} + dummy_template_json = json.dumps(dummy_template) +name_type_template_json = json.dumps(name_type_template) def test_parse_stack_resources(): @@ -46,3 +63,15 @@ def test_parse_stack_resources(): def test_missing_resource_logs(logger): resource_class_from_type("foobar") logger.warning.assert_called_with('No Moto CloudFormation support for %s', 'foobar') + + +def test_parse_stack_with_name_type_resource(): + stack = FakeStack( + stack_id="test_id", + name="test_stack", + template=name_type_template_json) + + stack.resource_map.should.have.length_of(1) + list(stack.resource_map.keys())[0].should.equal('WebServerGroup') + queue = list(stack.resource_map.values())[0] + queue.should.be.a(Queue) From 832769b8a7b08707985545588fbae9e0381ab42f Mon Sep 17 00:00:00 2001 From: Joseph Lawson Date: Mon, 20 Oct 2014 15:16:39 -0400 Subject: [PATCH 02/13] Name resources via CloudFormation naming convention and tag EC2 resources. * Make sure taggable resources are tagged per convention: http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-stack-parameters.html * Update CloudFormation parsing to use logical resource ids and name resources as CloudFomation would. * Fix tests for CloudFormation stack integration. --- moto/cloudformation/parsing.py | 37 ++++++++++++------- .../test_cloudformation_stack_integration.py | 26 +++++++------ 2 files changed, 39 insertions(+), 24 deletions(-) diff --git a/moto/cloudformation/parsing.py b/moto/cloudformation/parsing.py index 46adf1540..71597c109 100644 --- a/moto/cloudformation/parsing.py +++ b/moto/cloudformation/parsing.py @@ -94,7 +94,7 @@ def resource_name_property_from_type(resource_type): return NAME_TYPE_MAP.get(resource_type) -def parse_resource(resource_name, resource_json, resources_map): +def parse_resource(logical_id, resource_json, resources_map): resource_type = resource_json['Type'] resource_class = resource_class_from_type(resource_type) if not resource_class: @@ -108,11 +108,17 @@ def parse_resource(resource_name, resource_json, resources_map): if not resource_name_property in resource_json['Properties']: resource_json['Properties'][resource_name_property] = '{0}-{1}-{2}'.format( resources_map.get('AWS::StackName'), - resource_name, + logical_id, random_suffix()) + resource_name = resource_json['Properties'][resource_name_property] + else: + resource_name = '{0}-{1}-{2}'.format(resources_map.get('AWS::StackName'), + logical_id, + random_suffix()) + resource = resource_class.create_from_cloudformation_json(resource_name, resource_json) resource.type = resource_type - resource.logical_resource_id = resource_name + resource.logical_resource_id = logical_id return resource @@ -136,24 +142,24 @@ class ResourceMap(collections.Mapping): } def __getitem__(self, key): - resource_name = key + resource_logical_id = key - if resource_name in self._parsed_resources: - return self._parsed_resources[resource_name] + if resource_logical_id in self._parsed_resources: + return self._parsed_resources[resource_logical_id] else: - resource_json = self._resource_json_map.get(resource_name) - new_resource = parse_resource(resource_name, resource_json, self) - self._parsed_resources[resource_name] = new_resource + resource_json = self._resource_json_map.get(resource_logical_id) + new_resource = parse_resource(resource_logical_id, resource_json, self) + self._parsed_resources[resource_logical_id] = new_resource return new_resource def __iter__(self): - return iter(self.resource_names) + return iter(self.resources) def __len__(self): return len(self._resource_json_map) @property - def resource_names(self): + def resources(self): return self._resource_json_map.keys() def load_parameters(self): @@ -167,5 +173,10 @@ class ResourceMap(collections.Mapping): # Since this is a lazy map, to create every object we just need to # iterate through self. - for resource_name in self.resource_names: - self[resource_name] + tags = {'aws:cloudformation:stack-name': self.get('AWS::StackName'), + 'aws:cloudformation:stack-id': self.get('AWS::StackId')} + for resource in self.resources: + self[resource] + if isinstance(self[resource], ec2_models.TaggedEC2Resource): + tags['aws:cloudformation:logical-id'] = resource + ec2_models.ec2_backend.create_tags([self[resource].physical_resource_id],tags) diff --git a/tests/test_cloudformation/test_cloudformation_stack_integration.py b/tests/test_cloudformation/test_cloudformation_stack_integration.py index befb6adf8..499762d33 100644 --- a/tests/test_cloudformation/test_cloudformation_stack_integration.py +++ b/tests/test_cloudformation/test_cloudformation_stack_integration.py @@ -75,7 +75,7 @@ def test_stack_ec2_integration(): stack = conn.describe_stacks()[0] instance = stack.describe_resources()[0] instance.resource_type.should.equal('AWS::EC2::Instance') - instance.logical_resource_id.should.equal("WebServerGroup") + instance.logical_resource_id.should.contain("WebServerGroup") instance.physical_resource_id.should.equal(ec2_instance.id) @@ -186,7 +186,7 @@ def test_stack_security_groups(): ec2_conn = boto.connect_ec2() security_groups = ec2_conn.get_all_security_groups() for group in security_groups: - if group.name == "InstanceSecurityGroup": + if "InstanceSecurityGroup" in group.name: instance_group = group else: other_group = group @@ -245,6 +245,7 @@ def test_autoscaling_group_with_elb(): "InstancePort": "80", "Protocol": "HTTP" }], + "LoadBalancerName": "my-elb", "HealthCheck": { "Target": "80", "HealthyThreshold": "3", @@ -267,7 +268,7 @@ def test_autoscaling_group_with_elb(): autoscale_conn = boto.connect_autoscale() autoscale_group = autoscale_conn.get_all_groups()[0] - autoscale_group.launch_config_name.should.equal("my-launch-config") + autoscale_group.launch_config_name.should.contain("my-launch-config") autoscale_group.load_balancers[0].should.equal('my-elb') # Confirm the Launch config was actually created @@ -280,13 +281,13 @@ def test_autoscaling_group_with_elb(): stack = conn.describe_stacks()[0] resources = stack.describe_resources() as_group_resource = [resource for resource in resources if resource.resource_type == 'AWS::AutoScaling::AutoScalingGroup'][0] - as_group_resource.physical_resource_id.should.equal("my-as-group") + as_group_resource.physical_resource_id.should.contain("my-as-group") launch_config_resource = [resource for resource in resources if resource.resource_type == 'AWS::AutoScaling::LaunchConfiguration'][0] - launch_config_resource.physical_resource_id.should.equal("my-launch-config") + launch_config_resource.physical_resource_id.should.contain("my-launch-config") elb_resource = [resource for resource in resources if resource.resource_type == 'AWS::ElasticLoadBalancing::LoadBalancer'][0] - elb_resource.physical_resource_id.should.equal("my-elb") + elb_resource.physical_resource_id.should.contain("my-elb") @mock_ec2() @@ -427,18 +428,21 @@ def test_iam_roles(): iam_conn = boto.connect_iam() - role = iam_conn.get_role("my-role") - role.role_name.should.equal("my-role") + role_result = iam_conn.list_roles()['list_roles_response']['list_roles_result']['roles'][0] + role = iam_conn.get_role(role_result.role_name) + role.role_name.should.contain("my-role") role.path.should.equal("my-path") - instance_profile = iam_conn.get_instance_profile("my-instance-profile") - instance_profile.instance_profile_name.should.equal("my-instance-profile") + instance_profile_response = iam_conn.list_instance_profiles()['list_instance_profiles_response'] + cfn_instance_profile = instance_profile_response['list_instance_profiles_result']['instance_profiles'][0] + instance_profile = iam_conn.get_instance_profile(cfn_instance_profile.instance_profile_name) + instance_profile.instance_profile_name.should.contain("my-instance-profile") instance_profile.path.should.equal("my-path") instance_profile.role_id.should.equal(role.role_id) autoscale_conn = boto.connect_autoscale() launch_config = autoscale_conn.get_all_launch_configurations()[0] - launch_config.instance_profile_name.should.equal("my-instance-profile") + launch_config.instance_profile_name.should.contain("my-instance-profile") stack = conn.describe_stacks()[0] resources = stack.describe_resources() From 1d9ffafaa5f026461a9c0f36e652b616080ce49f Mon Sep 17 00:00:00 2001 From: Joseph Lawson Date: Tue, 21 Oct 2014 12:45:03 -0400 Subject: [PATCH 03/13] create CloudFormation outputs and enable 'Fn::GetAtt' to work. --- moto/cloudformation/models.py | 9 ++- moto/cloudformation/parsing.py | 59 +++++++++++++++ moto/cloudformation/responses.py | 9 ++- moto/ec2/models.py | 46 ++++++++++++ moto/elb/models.py | 16 ++++ moto/iam/models.py | 35 +++++++++ moto/s3/models.py | 10 +++ moto/sns/models.py | 8 ++ moto/sqs/models.py | 10 +++ .../test_cloudformation/test_stack_parsing.py | 75 +++++++++++++++++-- 10 files changed, 269 insertions(+), 8 deletions(-) diff --git a/moto/cloudformation/models.py b/moto/cloudformation/models.py index 20672f5a6..d4420cafd 100644 --- a/moto/cloudformation/models.py +++ b/moto/cloudformation/models.py @@ -3,7 +3,7 @@ import json from moto.core import BaseBackend -from .parsing import ResourceMap +from .parsing import ResourceMap, OutputMap from .utils import generate_stack_id @@ -19,10 +19,17 @@ class FakeStack(object): self.resource_map = ResourceMap(stack_id, name, template_dict) self.resource_map.create() + self.output_map = OutputMap(self.resource_map, template_dict) + self.output_map.create() + @property def stack_resources(self): return self.resource_map.values() + @property + def stack_outputs(self): + return self.output_map.values() + class CloudFormationBackend(BaseBackend): diff --git a/moto/cloudformation/parsing.py b/moto/cloudformation/parsing.py index 71597c109..4dbe687d9 100644 --- a/moto/cloudformation/parsing.py +++ b/moto/cloudformation/parsing.py @@ -8,6 +8,8 @@ from moto.elb import models as elb_models from moto.iam import models as iam_models from moto.sqs import models as sqs_models from .utils import random_suffix +from boto.cloudformation.stack import Output +from boto.exception import BotoServerError MODEL_MAP = { "AWS::AutoScaling::AutoScalingGroup": autoscaling_models.FakeAutoScalingGroup, @@ -69,6 +71,19 @@ def clean_json(resource_json, resources_map): else: return resource + if 'Fn::GetAtt' in resource_json: + resource = resources_map[resource_json['Fn::GetAtt'][0]] + try: + return resource.get_cfn_attribute(resource_json['Fn::GetAtt'][1]) + except NotImplementedError as n: + raise NotImplementedError(n.message.format(resource_json['Fn::GetAtt'][0])) + except AttributeError: + raise BotoServerError( + 400, + 'Bad Request', + 'Template error: resource {0} does not support attribute type {1} in Fn::GetAtt'.format( + resource_json['Fn::GetAtt'][0], resource_json['Fn::GetAtt'][1])) + cleaned_json = {} for key, value in resource_json.items(): cleaned_json[key] = clean_json(value, resources_map) @@ -122,6 +137,15 @@ def parse_resource(logical_id, resource_json, resources_map): return resource +def parse_output(output_logical_id, output_json, resources_map): + output_json = clean_json(output_json, resources_map) + output = Output() + output.key = output_logical_id + output.value = output_json['Value'] + output.description = output_json.get('Description') + return output + + class ResourceMap(collections.Mapping): """ This is a lazy loading map for resources. This allows us to create resources @@ -180,3 +204,38 @@ class ResourceMap(collections.Mapping): if isinstance(self[resource], ec2_models.TaggedEC2Resource): tags['aws:cloudformation:logical-id'] = resource ec2_models.ec2_backend.create_tags([self[resource].physical_resource_id],tags) + + +class OutputMap(collections.Mapping): + def __init__(self, resources, template): + self._template = template + self._output_json_map = template.get('Outputs') + + # Create the default resources + self._resource_map = resources + self._parsed_outputs = dict() + + def __getitem__(self, key): + output_logical_id = key + + if output_logical_id in self._parsed_outputs: + return self._parsed_outputs[output_logical_id] + else: + output_json = self._output_json_map.get(output_logical_id) + new_output = parse_output(output_logical_id, output_json, self._resource_map) + self._parsed_outputs[output_logical_id] = new_output + return new_output + + def __iter__(self): + return iter(self.outputs) + + def __len__(self): + return len(self._output_json_map) + + @property + def outputs(self): + return self._output_json_map.keys() if self._output_json_map else [] + + def create(self): + for output in self.outputs: + self[output] diff --git a/moto/cloudformation/responses.py b/moto/cloudformation/responses.py index 23d0be777..7d6690323 100644 --- a/moto/cloudformation/responses.py +++ b/moto/cloudformation/responses.py @@ -88,7 +88,14 @@ DESCRIBE_STACKS_TEMPLATE = """ 2010-07-27T22:28:28Z CREATE_COMPLETE false - + + {% for output in stack.stack_outputs %} + + {{ output.key }} + {{ output.value }} + + {% endfor %} + {% endfor %} diff --git a/moto/ec2/models.py b/moto/ec2/models.py index 32b6b1660..b75be944f 100644 --- a/moto/ec2/models.py +++ b/moto/ec2/models.py @@ -9,6 +9,7 @@ from boto.ec2.instance import Instance as BotoInstance, Reservation from boto.ec2.blockdevicemapping import BlockDeviceMapping, BlockDeviceType from boto.ec2.spotinstancerequest import SpotInstanceRequest as BotoSpotRequest from boto.ec2.launchspecification import LaunchSpecification +from boto.exception import BotoServerError from moto.core import BaseBackend from moto.core.models import Model @@ -185,6 +186,15 @@ class NetworkInterface(object): else: return self._group_set + def get_cfn_attribute(self, attribute_name): + if attribute_name == 'PrimaryPrivateIpAddress': + return self.private_ip_address + elif attribute_name == 'SecondaryPrivateIpAddresses': + raise NotImplementedError('"Fn::GetAtt" : [ "{0}" , "SecondaryPrivateIpAddresses" ]"') + raise BotoServerError(400, + 'Bad Request', + 'Template error: resource {0} does not support attribute type {1} in Fn::GetAtt') + class NetworkInterfaceBackend(object): def __init__(self): @@ -412,6 +422,21 @@ class Instance(BotoInstance, TaggedEC2Resource): eni.attachment_id = None eni.device_index = None + def get_cfn_attribute(self, attribute_name): + if attribute_name == 'AvailabilityZone': + return self.placement + elif attribute_name == 'PrivateDnsName': + return self.private_dns_name + elif attribute_name == 'PublicDnsName': + return self.public_dns_name + elif attribute_name == 'PrivateIp': + return self.private_ip_address + elif attribute_name == 'PublicIp': + return self.ip_address + raise BotoServerError(400, + 'Bad Request', + 'Template error: resource {0} does not support attribute type {1} in Fn::GetAtt') + class InstanceBackend(object): @@ -971,6 +996,13 @@ class SecurityGroup(object): return False return True + def get_cfn_attribute(self, attribute_name): + if attribute_name == 'GroupId': + return self.id + raise BotoServerError(400, + 'Bad Request', + 'Template error: resource {0} does not support attribute type {1} in Fn::GetAtt') + class SecurityGroupBackend(object): @@ -1494,6 +1526,13 @@ class Subnet(TaggedEC2Resource): return filter_value + def get_cfn_attribute(self, attribute_name): + if attribute_name == 'AvailabilityZone': + raise NotImplementedError('"Fn::GetAtt" : [ "{0}" , "AvailabilityZone" ]"') + raise BotoServerError(400, + 'Bad Request', + 'Template error: resource {0} does not support attribute type {1} in Fn::GetAtt') + class SubnetBackend(object): def __init__(self): @@ -1929,6 +1968,13 @@ class ElasticAddress(object): def physical_resource_id(self): return self.allocation_id + def get_cfn_attribute(self, attribute_name): + if attribute_name == 'AllocationId': + return self.allocation_id + raise BotoServerError(400, + 'Bad Request', + 'Template error: resource {0} does not support attribute type {1} in Fn::GetAtt') + class ElasticAddressBackend(object): diff --git a/moto/elb/models.py b/moto/elb/models.py index 0f3da0fa3..026a3e4f4 100644 --- a/moto/elb/models.py +++ b/moto/elb/models.py @@ -1,5 +1,6 @@ from __future__ import unicode_literals from moto.core import BaseBackend +from boto.exception import BotoServerError class FakeHealthCheck(object): @@ -56,6 +57,21 @@ class FakeLoadBalancer(object): def physical_resource_id(self): return self.name + def get_cfn_attribute(self, attribute_name): + if attribute_name == 'CanonicalHostedZoneName': + raise NotImplementedError('"Fn::GetAtt" : [ "{0}" , "CanonicalHostedZoneName" ]"') + elif attribute_name == 'CanonicalHostedZoneNameID': + raise NotImplementedError('"Fn::GetAtt" : [ "{0}" , "CanonicalHostedZoneNameID" ]"') + elif attribute_name == 'DNSName': + raise NotImplementedError('"Fn::GetAtt" : [ "{0}" , "DNSName" ]"') + elif attribute_name == 'SourceSecurityGroup.GroupName': + raise NotImplementedError('"Fn::GetAtt" : [ "{0}" , "SourceSecurityGroup.GroupName" ]"') + elif attribute_name == 'SourceSecurityGroup.OwnerAlias': + raise NotImplementedError('"Fn::GetAtt" : [ "{0}" , "SourceSecurityGroup.OwnerAlias" ]"') + raise BotoServerError(400, + 'Bad Request', + 'Template error: resource {0} does not support attribute type {1} in Fn::GetAtt') + class ELBBackend(BaseBackend): diff --git a/moto/iam/models.py b/moto/iam/models.py index 8e072f20a..477192dfa 100644 --- a/moto/iam/models.py +++ b/moto/iam/models.py @@ -30,6 +30,13 @@ class Role(object): def physical_resource_id(self): return self.id + def get_cfn_attribute(self, attribute_name): + if attribute_name == 'Arn': + raise NotImplementedError('"Fn::GetAtt" : [ "{0}" , "Arn" ]"') + raise BotoServerError(400, + 'Bad Request', + 'Template error: resource {0} does not support attribute type {1} in Fn::GetAtt') + class InstanceProfile(object): def __init__(self, instance_profile_id, name, path, roles): @@ -53,6 +60,13 @@ class InstanceProfile(object): def physical_resource_id(self): return self.name + def get_cfn_attribute(self, attribute_name): + if attribute_name == 'Arn': + raise NotImplementedError('"Fn::GetAtt" : [ "{0}" , "Arn" ]"') + raise BotoServerError(400, + 'Bad Request', + 'Template error: resource {0} does not support attribute type {1} in Fn::GetAtt') + class Certificate(object): def __init__(self, cert_name, cert_body, private_key, cert_chain=None, path=None): @@ -78,6 +92,13 @@ class AccessKey(object): "%Y-%m-%d-%H-%M-%S" ) + def get_cfn_attribute(self, attribute_name): + if attribute_name == 'SecretAccessKey': + return self.secret_access_key + raise BotoServerError(400, + 'Bad Request', + 'Template error: resource {0} does not support attribute type {1} in Fn::GetAtt') + class Group(object): def __init__(self, name, path='/'): @@ -91,6 +112,13 @@ class Group(object): self.users = [] + def get_cfn_attribute(self, attribute_name): + if attribute_name == 'Arn': + raise NotImplementedError('"Fn::GetAtt" : [ "{0}" , "Arn" ]"') + raise BotoServerError(400, + 'Bad Request', + 'Template error: resource {0} does not support attribute type {1} in Fn::GetAtt') + class User(object): def __init__(self, name, path='/'): @@ -143,6 +171,13 @@ class User(object): else: raise BotoServerError(404, 'Not Found') + def get_cfn_attribute(self, attribute_name): + if attribute_name == 'Arn': + raise NotImplementedError('"Fn::GetAtt" : [ "{0}" , "Arn" ]"') + raise BotoServerError(400, + 'Bad Request', + 'Template error: resource {0} does not support attribute type {1} in Fn::GetAtt') + class IAMBackend(BaseBackend): diff --git a/moto/s3/models.py b/moto/s3/models.py index f9ed3c0df..2a342fd81 100644 --- a/moto/s3/models.py +++ b/moto/s3/models.py @@ -12,6 +12,7 @@ from moto.core import BaseBackend from moto.core.utils import iso_8601_datetime, rfc_1123_datetime from .exceptions import BucketAlreadyExists, MissingBucket from .utils import clean_key_name, _VersionedKeyStore +from boto.exception import BotoServerError UPLOAD_ID_BYTES = 43 UPLOAD_PART_MIN_SIZE = 5242880 @@ -170,6 +171,15 @@ class FakeBucket(object): def is_versioned(self): return self.versioning_status == 'Enabled' + def get_cfn_attribute(self, attribute_name): + if attribute_name == 'DomainName': + raise NotImplementedError('"Fn::GetAtt" : [ "{0}" , "DomainName" ]"') + elif attribute_name == 'WebsiteURL': + raise NotImplementedError('"Fn::GetAtt" : [ "{0}" , "WebsiteURL" ]"') + raise BotoServerError(400, + 'Bad Request', + 'Template error: resource {0} does not support attribute type {1} in Fn::GetAtt') + class S3Backend(BaseBackend): diff --git a/moto/sns/models.py b/moto/sns/models.py index 8d28159f4..765295dc7 100644 --- a/moto/sns/models.py +++ b/moto/sns/models.py @@ -8,6 +8,7 @@ from moto.core import BaseBackend from moto.core.utils import iso_8601_datetime from moto.sqs.models import sqs_backend from .utils import make_arn_for_topic, make_arn_for_subscription +from boto.exception import BotoServerError DEFAULT_ACCOUNT_ID = 123456789012 @@ -33,6 +34,13 @@ class Topic(object): subscription.publish(message, message_id) return message_id + def get_cfn_attribute(self, attribute_name): + if attribute_name == 'TopicName': + return self.name + raise BotoServerError(400, + 'Bad Request', + 'Template error: resource {0} does not support attribute type {1} in Fn::GetAtt') + class Subscription(object): def __init__(self, topic, endpoint, protocol): diff --git a/moto/sqs/models.py b/moto/sqs/models.py index 8bb92b710..3a6746697 100644 --- a/moto/sqs/models.py +++ b/moto/sqs/models.py @@ -4,6 +4,7 @@ import hashlib import time import re +from boto.exception import BotoServerError from moto.core import BaseBackend from moto.core.utils import camelcase_to_underscores, get_random_message_id from .utils import generate_receipt_handle, unix_time_millis @@ -152,6 +153,15 @@ class Queue(object): def add_message(self, message): self._messages.append(message) + def get_cfn_attribute(self, attribute_name): + if attribute_name == 'Arn': + return self.queue_arn + elif attribute_name == 'QueueName': + return self.name + raise BotoServerError(400, + 'Bad Request', + 'Template error: resource {0} does not support attribute type {1} in Fn::GetAtt') + class SQSBackend(BaseBackend): def __init__(self): diff --git a/tests/test_cloudformation/test_stack_parsing.py b/tests/test_cloudformation/test_stack_parsing.py index 8df64f5ef..53ef69456 100644 --- a/tests/test_cloudformation/test_stack_parsing.py +++ b/tests/test_cloudformation/test_stack_parsing.py @@ -7,6 +7,8 @@ import sure # noqa from moto.cloudformation.models import FakeStack from moto.cloudformation.parsing import resource_class_from_type from moto.sqs.models import Queue +from boto.cloudformation.stack import Output +from boto.exception import BotoServerError dummy_template = { "AWSTemplateFormatVersion": "2010-09-09", @@ -14,8 +16,7 @@ dummy_template = { "Description": "Create a multi-az, load balanced, Auto Scaled sample web site. The Auto Scaling trigger is based on the CPU utilization of the web servers. The AMI is chosen based on the region in which the stack is run. This example creates a web service running across all availability zones in a region. The instances are load balanced with a simple health check. The web site is available on port 80, however, the instances can be configured to listen on any port (8888 by default). **WARNING** This template creates one or more Amazon EC2 instances. You will be billed for the AWS resources used if you create a stack from this template.", "Resources": { - "WebServerGroup": { - + "Queue": { "Type": "AWS::SQS::Queue", "Properties": { "QueueName": "my-queue", @@ -31,8 +32,7 @@ name_type_template = { "Description": "Create a multi-az, load balanced, Auto Scaled sample web site. The Auto Scaling trigger is based on the CPU utilization of the web servers. The AMI is chosen based on the region in which the stack is run. This example creates a web service running across all availability zones in a region. The instances are load balanced with a simple health check. The web site is available on port 80, however, the instances can be configured to listen on any port (8888 by default). **WARNING** This template creates one or more Amazon EC2 instances. You will be billed for the AWS resources used if you create a stack from this template.", "Resources": { - "WebServerGroup": { - + "Queue": { "Type": "AWS::SQS::Queue", "Properties": { "VisibilityTimeout": 60, @@ -41,8 +41,40 @@ name_type_template = { }, } +output_dict = { + "Outputs": { + "Output1": { + "Value": {"Ref": "Queue"}, + "Description": "This is a description." + } + } +} + +bad_output = { + "Outputs": { + "Output1": { + "Value": {"Fn::GetAtt": ["Queue", "InvalidAttribute"]} + } + } +} + +get_attribute_output = { + "Outputs": { + "Output1": { + "Value": {"Fn::GetAtt": ["Queue", "QueueName"]} + } + } +} + +outputs_template = dict(dummy_template.items() + output_dict.items()) +bad_outputs_template = dict(dummy_template.items() + bad_output.items()) +get_attribute_outputs_template = dict(dummy_template.items() + get_attribute_output.items()) + dummy_template_json = json.dumps(dummy_template) name_type_template_json = json.dumps(name_type_template) +output_type_template_json = json.dumps(outputs_template) +bad_output_template_json = json.dumps(bad_outputs_template) +get_attribute_outputs_template_json = json.dumps(get_attribute_outputs_template) def test_parse_stack_resources(): @@ -53,7 +85,7 @@ def test_parse_stack_resources(): ) stack.resource_map.should.have.length_of(1) - list(stack.resource_map.keys())[0].should.equal('WebServerGroup') + list(stack.resource_map.keys())[0].should.equal('Queue') queue = list(stack.resource_map.values())[0] queue.should.be.a(Queue) queue.name.should.equal("my-queue") @@ -72,6 +104,37 @@ def test_parse_stack_with_name_type_resource(): template=name_type_template_json) stack.resource_map.should.have.length_of(1) - list(stack.resource_map.keys())[0].should.equal('WebServerGroup') + list(stack.resource_map.keys())[0].should.equal('Queue') queue = list(stack.resource_map.values())[0] queue.should.be.a(Queue) + + +def test_parse_stack_with_outputs(): + stack = FakeStack( + stack_id="test_id", + name="test_stack", + template=output_type_template_json) + + stack.output_map.should.have.length_of(1) + list(stack.output_map.keys())[0].should.equal('Output1') + output = list(stack.output_map.values())[0] + output.should.be.a(Output) + output.description.should.equal("This is a description.") + + +def test_parse_stack_with_get_attribute_outputs(): + stack = FakeStack( + stack_id="test_id", + name="test_stack", + template=get_attribute_outputs_template_json) + + stack.output_map.should.have.length_of(1) + list(stack.output_map.keys())[0].should.equal('Output1') + output = list(stack.output_map.values())[0] + output.should.be.a(Output) + output.value.should.equal("my-queue") + + +def test_parse_stack_with_bad_get_attribute_outputs(): + FakeStack.when.called_with( + "test_id", "test_stack", bad_output_template_json).should.throw(BotoServerError) \ No newline at end of file From 83f187fa7e255898e94a7f46c3573e2d97a62832 Mon Sep 17 00:00:00 2001 From: Joseph Lawson Date: Tue, 21 Oct 2014 13:49:25 -0400 Subject: [PATCH 04/13] fix python 3 issue merging dicts --- tests/test_cloudformation/test_stack_parsing.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_cloudformation/test_stack_parsing.py b/tests/test_cloudformation/test_stack_parsing.py index 53ef69456..9cb30aba0 100644 --- a/tests/test_cloudformation/test_stack_parsing.py +++ b/tests/test_cloudformation/test_stack_parsing.py @@ -66,8 +66,8 @@ get_attribute_output = { } } -outputs_template = dict(dummy_template.items() + output_dict.items()) -bad_outputs_template = dict(dummy_template.items() + bad_output.items()) +outputs_template = dict(list(dummy_template.items()) + list(output_dict.items())) +bad_outputs_template = dict(list(dummy_template.items()) + list(bad_output.items())) get_attribute_outputs_template = dict(dummy_template.items() + get_attribute_output.items()) dummy_template_json = json.dumps(dummy_template) From 20a69255c30f5d327052ba732f262a29c2af883a Mon Sep 17 00:00:00 2001 From: Joseph Lawson Date: Tue, 21 Oct 2014 14:51:26 -0400 Subject: [PATCH 05/13] tweak Fn::GetAtt to return resource_json if resource is not implemented. DRY This is better than failing out with a misleading Boto 400 error which should only happen when get_cfn_attribute is called but fails. --- moto/cloudformation/exceptions.py | 6 ++++++ moto/cloudformation/parsing.py | 16 +++++++++++----- moto/ec2/models.py | 26 ++++++++++---------------- moto/elb/models.py | 6 ++---- moto/iam/models.py | 25 ++++++++++--------------- moto/s3/models.py | 6 ++---- moto/sns/models.py | 6 ++---- moto/sqs/models.py | 7 +++---- 8 files changed, 46 insertions(+), 52 deletions(-) create mode 100644 moto/cloudformation/exceptions.py diff --git a/moto/cloudformation/exceptions.py b/moto/cloudformation/exceptions.py new file mode 100644 index 000000000..a0a4eb63c --- /dev/null +++ b/moto/cloudformation/exceptions.py @@ -0,0 +1,6 @@ +from __future__ import unicode_literals + + +class UnformattedGetAttTemplateException(Exception): + description = 'Template error: resource {0} does not support attribute type {1} in Fn::GetAtt' + status_code = 400 diff --git a/moto/cloudformation/parsing.py b/moto/cloudformation/parsing.py index 4dbe687d9..b0f29fc21 100644 --- a/moto/cloudformation/parsing.py +++ b/moto/cloudformation/parsing.py @@ -8,6 +8,7 @@ from moto.elb import models as elb_models from moto.iam import models as iam_models from moto.sqs import models as sqs_models from .utils import random_suffix +from .exceptions import UnformattedGetAttTemplateException from boto.cloudformation.stack import Output from boto.exception import BotoServerError @@ -72,17 +73,22 @@ def clean_json(resource_json, resources_map): return resource if 'Fn::GetAtt' in resource_json: + resource = resources_map[resource_json['Fn::GetAtt'][0]] + if resource is None: + return resource_json try: return resource.get_cfn_attribute(resource_json['Fn::GetAtt'][1]) except NotImplementedError as n: - raise NotImplementedError(n.message.format(resource_json['Fn::GetAtt'][0])) - except AttributeError: + logger.warning(n.message.format(resource_json['Fn::GetAtt'][0])) + except UnformattedGetAttTemplateException: raise BotoServerError( - 400, + UnformattedGetAttTemplateException.status_code, 'Bad Request', - 'Template error: resource {0} does not support attribute type {1} in Fn::GetAtt'.format( - resource_json['Fn::GetAtt'][0], resource_json['Fn::GetAtt'][1])) + UnformattedGetAttTemplateException.description.format( + resource_json['Fn::GetAtt'][0], resource_json['Fn::GetAtt'][1])) + except Exception as e: + pass cleaned_json = {} for key, value in resource_json.items(): diff --git a/moto/ec2/models.py b/moto/ec2/models.py index b75be944f..77f489eb3 100644 --- a/moto/ec2/models.py +++ b/moto/ec2/models.py @@ -9,7 +9,6 @@ from boto.ec2.instance import Instance as BotoInstance, Reservation from boto.ec2.blockdevicemapping import BlockDeviceMapping, BlockDeviceType from boto.ec2.spotinstancerequest import SpotInstanceRequest as BotoSpotRequest from boto.ec2.launchspecification import LaunchSpecification -from boto.exception import BotoServerError from moto.core import BaseBackend from moto.core.models import Model @@ -187,13 +186,12 @@ class NetworkInterface(object): return self._group_set def get_cfn_attribute(self, attribute_name): + from moto.cloudformation.exceptions import UnformattedGetAttTemplateException if attribute_name == 'PrimaryPrivateIpAddress': return self.private_ip_address elif attribute_name == 'SecondaryPrivateIpAddresses': raise NotImplementedError('"Fn::GetAtt" : [ "{0}" , "SecondaryPrivateIpAddresses" ]"') - raise BotoServerError(400, - 'Bad Request', - 'Template error: resource {0} does not support attribute type {1} in Fn::GetAtt') + raise UnformattedGetAttTemplateException() class NetworkInterfaceBackend(object): @@ -423,6 +421,7 @@ class Instance(BotoInstance, TaggedEC2Resource): eni.device_index = None def get_cfn_attribute(self, attribute_name): + from moto.cloudformation.exceptions import UnformattedGetAttTemplateException if attribute_name == 'AvailabilityZone': return self.placement elif attribute_name == 'PrivateDnsName': @@ -433,9 +432,7 @@ class Instance(BotoInstance, TaggedEC2Resource): return self.private_ip_address elif attribute_name == 'PublicIp': return self.ip_address - raise BotoServerError(400, - 'Bad Request', - 'Template error: resource {0} does not support attribute type {1} in Fn::GetAtt') + raise UnformattedGetAttTemplateException() class InstanceBackend(object): @@ -997,11 +994,10 @@ class SecurityGroup(object): return True def get_cfn_attribute(self, attribute_name): + from moto.cloudformation.exceptions import UnformattedGetAttTemplateException if attribute_name == 'GroupId': return self.id - raise BotoServerError(400, - 'Bad Request', - 'Template error: resource {0} does not support attribute type {1} in Fn::GetAtt') + raise UnformattedGetAttTemplateException() class SecurityGroupBackend(object): @@ -1527,11 +1523,10 @@ class Subnet(TaggedEC2Resource): return filter_value def get_cfn_attribute(self, attribute_name): + from moto.cloudformation.exceptions import UnformattedGetAttTemplateException if attribute_name == 'AvailabilityZone': raise NotImplementedError('"Fn::GetAtt" : [ "{0}" , "AvailabilityZone" ]"') - raise BotoServerError(400, - 'Bad Request', - 'Template error: resource {0} does not support attribute type {1} in Fn::GetAtt') + raise UnformattedGetAttTemplateException() class SubnetBackend(object): @@ -1969,11 +1964,10 @@ class ElasticAddress(object): return self.allocation_id def get_cfn_attribute(self, attribute_name): + from moto.cloudformation.exceptions import UnformattedGetAttTemplateException if attribute_name == 'AllocationId': return self.allocation_id - raise BotoServerError(400, - 'Bad Request', - 'Template error: resource {0} does not support attribute type {1} in Fn::GetAtt') + raise UnformattedGetAttTemplateException() class ElasticAddressBackend(object): diff --git a/moto/elb/models.py b/moto/elb/models.py index 026a3e4f4..c0eb4e686 100644 --- a/moto/elb/models.py +++ b/moto/elb/models.py @@ -1,6 +1,5 @@ from __future__ import unicode_literals from moto.core import BaseBackend -from boto.exception import BotoServerError class FakeHealthCheck(object): @@ -58,6 +57,7 @@ class FakeLoadBalancer(object): return self.name def get_cfn_attribute(self, attribute_name): + from moto.cloudformation.exceptions import UnformattedGetAttTemplateException if attribute_name == 'CanonicalHostedZoneName': raise NotImplementedError('"Fn::GetAtt" : [ "{0}" , "CanonicalHostedZoneName" ]"') elif attribute_name == 'CanonicalHostedZoneNameID': @@ -68,9 +68,7 @@ class FakeLoadBalancer(object): raise NotImplementedError('"Fn::GetAtt" : [ "{0}" , "SourceSecurityGroup.GroupName" ]"') elif attribute_name == 'SourceSecurityGroup.OwnerAlias': raise NotImplementedError('"Fn::GetAtt" : [ "{0}" , "SourceSecurityGroup.OwnerAlias" ]"') - raise BotoServerError(400, - 'Bad Request', - 'Template error: resource {0} does not support attribute type {1} in Fn::GetAtt') + raise UnformattedGetAttTemplateException() class ELBBackend(BaseBackend): diff --git a/moto/iam/models.py b/moto/iam/models.py index 477192dfa..304a3ee16 100644 --- a/moto/iam/models.py +++ b/moto/iam/models.py @@ -31,11 +31,10 @@ class Role(object): return self.id def get_cfn_attribute(self, attribute_name): + from moto.cloudformation.exceptions import UnformattedGetAttTemplateException if attribute_name == 'Arn': raise NotImplementedError('"Fn::GetAtt" : [ "{0}" , "Arn" ]"') - raise BotoServerError(400, - 'Bad Request', - 'Template error: resource {0} does not support attribute type {1} in Fn::GetAtt') + raise UnformattedGetAttTemplateException() class InstanceProfile(object): @@ -61,11 +60,10 @@ class InstanceProfile(object): return self.name def get_cfn_attribute(self, attribute_name): + from moto.cloudformation.exceptions import UnformattedGetAttTemplateException if attribute_name == 'Arn': raise NotImplementedError('"Fn::GetAtt" : [ "{0}" , "Arn" ]"') - raise BotoServerError(400, - 'Bad Request', - 'Template error: resource {0} does not support attribute type {1} in Fn::GetAtt') + raise UnformattedGetAttTemplateException() class Certificate(object): @@ -93,11 +91,10 @@ class AccessKey(object): ) def get_cfn_attribute(self, attribute_name): + from moto.cloudformation.exceptions import UnformattedGetAttTemplateException if attribute_name == 'SecretAccessKey': return self.secret_access_key - raise BotoServerError(400, - 'Bad Request', - 'Template error: resource {0} does not support attribute type {1} in Fn::GetAtt') + raise UnformattedGetAttTemplateException() class Group(object): @@ -113,11 +110,10 @@ class Group(object): self.users = [] def get_cfn_attribute(self, attribute_name): + from moto.cloudformation.exceptions import UnformattedGetAttTemplateException if attribute_name == 'Arn': raise NotImplementedError('"Fn::GetAtt" : [ "{0}" , "Arn" ]"') - raise BotoServerError(400, - 'Bad Request', - 'Template error: resource {0} does not support attribute type {1} in Fn::GetAtt') + raise UnformattedGetAttTemplateException() class User(object): @@ -172,11 +168,10 @@ class User(object): raise BotoServerError(404, 'Not Found') def get_cfn_attribute(self, attribute_name): + from moto.cloudformation.exceptions import UnformattedGetAttTemplateException if attribute_name == 'Arn': raise NotImplementedError('"Fn::GetAtt" : [ "{0}" , "Arn" ]"') - raise BotoServerError(400, - 'Bad Request', - 'Template error: resource {0} does not support attribute type {1} in Fn::GetAtt') + raise UnformattedGetAttTemplateException() class IAMBackend(BaseBackend): diff --git a/moto/s3/models.py b/moto/s3/models.py index 2a342fd81..6e003edda 100644 --- a/moto/s3/models.py +++ b/moto/s3/models.py @@ -12,7 +12,6 @@ from moto.core import BaseBackend from moto.core.utils import iso_8601_datetime, rfc_1123_datetime from .exceptions import BucketAlreadyExists, MissingBucket from .utils import clean_key_name, _VersionedKeyStore -from boto.exception import BotoServerError UPLOAD_ID_BYTES = 43 UPLOAD_PART_MIN_SIZE = 5242880 @@ -172,13 +171,12 @@ class FakeBucket(object): return self.versioning_status == 'Enabled' def get_cfn_attribute(self, attribute_name): + from moto.cloudformation.exceptions import UnformattedGetAttTemplateException if attribute_name == 'DomainName': raise NotImplementedError('"Fn::GetAtt" : [ "{0}" , "DomainName" ]"') elif attribute_name == 'WebsiteURL': raise NotImplementedError('"Fn::GetAtt" : [ "{0}" , "WebsiteURL" ]"') - raise BotoServerError(400, - 'Bad Request', - 'Template error: resource {0} does not support attribute type {1} in Fn::GetAtt') + raise UnformattedGetAttTemplateException() class S3Backend(BaseBackend): diff --git a/moto/sns/models.py b/moto/sns/models.py index 765295dc7..9b11d9530 100644 --- a/moto/sns/models.py +++ b/moto/sns/models.py @@ -8,7 +8,6 @@ from moto.core import BaseBackend from moto.core.utils import iso_8601_datetime from moto.sqs.models import sqs_backend from .utils import make_arn_for_topic, make_arn_for_subscription -from boto.exception import BotoServerError DEFAULT_ACCOUNT_ID = 123456789012 @@ -35,11 +34,10 @@ class Topic(object): return message_id def get_cfn_attribute(self, attribute_name): + from moto.cloudformation.exceptions import UnformattedGetAttTemplateException if attribute_name == 'TopicName': return self.name - raise BotoServerError(400, - 'Bad Request', - 'Template error: resource {0} does not support attribute type {1} in Fn::GetAtt') + raise UnformattedGetAttTemplateException() class Subscription(object): diff --git a/moto/sqs/models.py b/moto/sqs/models.py index 3a6746697..f50806e8a 100644 --- a/moto/sqs/models.py +++ b/moto/sqs/models.py @@ -4,7 +4,7 @@ import hashlib import time import re -from boto.exception import BotoServerError + from moto.core import BaseBackend from moto.core.utils import camelcase_to_underscores, get_random_message_id from .utils import generate_receipt_handle, unix_time_millis @@ -154,13 +154,12 @@ class Queue(object): self._messages.append(message) def get_cfn_attribute(self, attribute_name): + from moto.cloudformation.exceptions import UnformattedGetAttTemplateException if attribute_name == 'Arn': return self.queue_arn elif attribute_name == 'QueueName': return self.name - raise BotoServerError(400, - 'Bad Request', - 'Template error: resource {0} does not support attribute type {1} in Fn::GetAtt') + raise UnformattedGetAttTemplateException() class SQSBackend(BaseBackend): From a8b967eab05f27a7ac94f9d62c82d45f8bc188c1 Mon Sep 17 00:00:00 2001 From: Joseph Lawson Date: Tue, 21 Oct 2014 15:53:38 -0400 Subject: [PATCH 06/13] eip resources have no required properties. --- moto/ec2/models.py | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/moto/ec2/models.py b/moto/ec2/models.py index 77f489eb3..17955cc09 100644 --- a/moto/ec2/models.py +++ b/moto/ec2/models.py @@ -1946,13 +1946,15 @@ class ElasticAddress(object): @classmethod def create_from_cloudformation_json(cls, resource_name, cloudformation_json): - properties = cloudformation_json['Properties'] + properties = cloudformation_json.get('Properties') + instance_id = None + if properties: + eip = ec2_backend.allocate_address( + domain=properties.get('Domain')) + instance_id = properties.get('InstanceId') + else: + eip = ec2_backend.allocate_address() - eip = ec2_backend.allocate_address( - domain=properties['Domain'] - ) - - instance_id = properties.get('InstanceId') if instance_id: instance = ec2_backend.get_instance_by_id(instance_id) ec2_backend.associate_address(instance, address=eip.public_ip) From 9301b4634606c3b2f6d7dda60df653f99b7715d5 Mon Sep 17 00:00:00 2001 From: Joseph Lawson Date: Tue, 21 Oct 2014 15:55:08 -0400 Subject: [PATCH 07/13] another python 3 fix on dict --- tests/test_cloudformation/test_stack_parsing.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_cloudformation/test_stack_parsing.py b/tests/test_cloudformation/test_stack_parsing.py index 9cb30aba0..286d5de74 100644 --- a/tests/test_cloudformation/test_stack_parsing.py +++ b/tests/test_cloudformation/test_stack_parsing.py @@ -68,7 +68,7 @@ get_attribute_output = { outputs_template = dict(list(dummy_template.items()) + list(output_dict.items())) bad_outputs_template = dict(list(dummy_template.items()) + list(bad_output.items())) -get_attribute_outputs_template = dict(dummy_template.items() + get_attribute_output.items()) +get_attribute_outputs_template = dict(list(dummy_template.items()) + list(get_attribute_output.items())) dummy_template_json = json.dumps(dummy_template) name_type_template_json = json.dumps(name_type_template) From d55a0b6ef0352f4b06ba256e33a7d43cd5914655 Mon Sep 17 00:00:00 2001 From: Joseph Lawson Date: Tue, 21 Oct 2014 15:59:28 -0400 Subject: [PATCH 08/13] default eip domain to 'standard' for cloudformation creations --- moto/ec2/models.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/moto/ec2/models.py b/moto/ec2/models.py index 17955cc09..6038db092 100644 --- a/moto/ec2/models.py +++ b/moto/ec2/models.py @@ -1949,11 +1949,12 @@ class ElasticAddress(object): properties = cloudformation_json.get('Properties') instance_id = None if properties: + domain=properties.get('Domain') eip = ec2_backend.allocate_address( - domain=properties.get('Domain')) + domain=domain if domain else 'standard') instance_id = properties.get('InstanceId') else: - eip = ec2_backend.allocate_address() + eip = ec2_backend.allocate_address(domain='standard') if instance_id: instance = ec2_backend.get_instance_by_id(instance_id) From 54794651d3d7ccb3b917e371ee3e118857f93680 Mon Sep 17 00:00:00 2001 From: Joseph Lawson Date: Tue, 21 Oct 2014 16:29:51 -0400 Subject: [PATCH 09/13] implement Fn::Join --- moto/cloudformation/parsing.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/moto/cloudformation/parsing.py b/moto/cloudformation/parsing.py index b0f29fc21..bbee79d33 100644 --- a/moto/cloudformation/parsing.py +++ b/moto/cloudformation/parsing.py @@ -73,7 +73,6 @@ def clean_json(resource_json, resources_map): return resource if 'Fn::GetAtt' in resource_json: - resource = resources_map[resource_json['Fn::GetAtt'][0]] if resource is None: return resource_json @@ -90,6 +89,13 @@ def clean_json(resource_json, resources_map): except Exception as e: pass + if 'Fn::Join' in resource_json: + join_list = [] + for val in resource_json['Fn::Join'][1]: + cleaned_val = clean_json(val, resources_map) + join_list.append(cleaned_val if cleaned_val else '{0}'.format(val)) + return resource_json['Fn::Join'][0].join(join_list) + cleaned_json = {} for key, value in resource_json.items(): cleaned_json[key] = clean_json(value, resources_map) From d4cbc71b0a1c578340d826a7bfcbf07ec0d763bf Mon Sep 17 00:00:00 2001 From: Joseph Lawson Date: Tue, 21 Oct 2014 16:37:54 -0400 Subject: [PATCH 10/13] fix eip physical_resource_id not returning an public_ip if it is in EC2 classic. --- moto/ec2/models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/moto/ec2/models.py b/moto/ec2/models.py index 6038db092..b7ab12666 100644 --- a/moto/ec2/models.py +++ b/moto/ec2/models.py @@ -1964,7 +1964,7 @@ class ElasticAddress(object): @property def physical_resource_id(self): - return self.allocation_id + return self.allocation_id if self.allocation_id else self.public_ip def get_cfn_attribute(self, attribute_name): from moto.cloudformation.exceptions import UnformattedGetAttTemplateException From 835259607a5552cb505c6add019a56dccbf85cde Mon Sep 17 00:00:00 2001 From: Joseph Lawson Date: Tue, 21 Oct 2014 21:55:08 -0400 Subject: [PATCH 11/13] test eip allocation via CloudFormation for VPC and EC2 classic --- .../fixtures/ec2_classic_eip.py | 9 ++++ tests/test_cloudformation/fixtures/vpc_eip.py | 12 +++++ .../test_cloudformation_stack_integration.py | 45 ++++++++++++++++++- 3 files changed, 65 insertions(+), 1 deletion(-) create mode 100644 tests/test_cloudformation/fixtures/ec2_classic_eip.py create mode 100644 tests/test_cloudformation/fixtures/vpc_eip.py diff --git a/tests/test_cloudformation/fixtures/ec2_classic_eip.py b/tests/test_cloudformation/fixtures/ec2_classic_eip.py new file mode 100644 index 000000000..626e90ada --- /dev/null +++ b/tests/test_cloudformation/fixtures/ec2_classic_eip.py @@ -0,0 +1,9 @@ +from __future__ import unicode_literals + +template = { + "Resources": { + "EC2EIP": { + "Type": "AWS::EC2::EIP" + } + } +} diff --git a/tests/test_cloudformation/fixtures/vpc_eip.py b/tests/test_cloudformation/fixtures/vpc_eip.py new file mode 100644 index 000000000..c7a46c830 --- /dev/null +++ b/tests/test_cloudformation/fixtures/vpc_eip.py @@ -0,0 +1,12 @@ +from __future__ import unicode_literals + +template = { + "Resources": { + "VPCEIP": { + "Type": "AWS::EC2::EIP", + "Properties": { + "Domain": "vpc" + } + } + } +} diff --git a/tests/test_cloudformation/test_cloudformation_stack_integration.py b/tests/test_cloudformation/test_cloudformation_stack_integration.py index 499762d33..0161ee0ad 100644 --- a/tests/test_cloudformation/test_cloudformation_stack_integration.py +++ b/tests/test_cloudformation/test_cloudformation_stack_integration.py @@ -12,7 +12,12 @@ from moto import ( mock_iam, ) -from .fixtures import single_instance_with_ebs_volume, vpc_single_instance_in_subnet +from .fixtures import ( + single_instance_with_ebs_volume, + vpc_single_instance_in_subnet, + ec2_classic_eip, + vpc_eip +) @mock_cloudformation() @@ -476,3 +481,41 @@ def test_single_instance_with_ebs_volume(): resources = stack.describe_resources() ebs_volume = [resource for resource in resources if resource.resource_type == 'AWS::EC2::Volume'][0] ebs_volume.physical_resource_id.should.equal(volume.id) + + +@mock_ec2() +@mock_cloudformation() +def test_classic_eip(): + + template_json = json.dumps(ec2_classic_eip.template) + conn = boto.connect_cloudformation() + conn.create_stack( + "test_stack", + template_body=template_json, + ) + ec2_conn = boto.connect_ec2() + eip = ec2_conn.get_all_addresses()[0] + + stack = conn.describe_stacks()[0] + resources = stack.describe_resources() + cfn_eip = [resource for resource in resources if resource.resource_type == 'AWS::EC2::EIP'][0] + cfn_eip.physical_resource_id.should.equal(eip.public_ip) + + +@mock_ec2() +@mock_cloudformation() +def test_vpc_eip(): + + template_json = json.dumps(vpc_eip.template) + conn = boto.connect_cloudformation() + conn.create_stack( + "test_stack", + template_body=template_json, + ) + ec2_conn = boto.connect_ec2() + eip = ec2_conn.get_all_addresses()[0] + + stack = conn.describe_stacks()[0] + resources = stack.describe_resources() + cfn_eip = [resource for resource in resources if resource.resource_type == 'AWS::EC2::EIP'][0] + cfn_eip.physical_resource_id.should.equal(eip.allocation_id) From 184d5be54ab67a42c83ef0100a1a34c9d98b9508 Mon Sep 17 00:00:00 2001 From: Joseph Lawson Date: Tue, 21 Oct 2014 21:58:39 -0400 Subject: [PATCH 12/13] remove debug exception catch --- moto/cloudformation/parsing.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/moto/cloudformation/parsing.py b/moto/cloudformation/parsing.py index bbee79d33..9691131c8 100644 --- a/moto/cloudformation/parsing.py +++ b/moto/cloudformation/parsing.py @@ -86,8 +86,6 @@ def clean_json(resource_json, resources_map): 'Bad Request', UnformattedGetAttTemplateException.description.format( resource_json['Fn::GetAtt'][0], resource_json['Fn::GetAtt'][1])) - except Exception as e: - pass if 'Fn::Join' in resource_json: join_list = [] From 8aabda0786c884349b69e83852a835d8596affcd Mon Sep 17 00:00:00 2001 From: Joseph Lawson Date: Tue, 21 Oct 2014 22:05:27 -0400 Subject: [PATCH 13/13] add test for Fn::Join --- tests/test_cloudformation/fixtures/fn_join.py | 23 +++++++++++++++++++ .../test_cloudformation_stack_integration.py | 21 ++++++++++++++++- 2 files changed, 43 insertions(+), 1 deletion(-) create mode 100644 tests/test_cloudformation/fixtures/fn_join.py diff --git a/tests/test_cloudformation/fixtures/fn_join.py b/tests/test_cloudformation/fixtures/fn_join.py new file mode 100644 index 000000000..79b62d01e --- /dev/null +++ b/tests/test_cloudformation/fixtures/fn_join.py @@ -0,0 +1,23 @@ +from __future__ import unicode_literals + +template = { + "Resources": { + "EC2EIP": { + "Type": "AWS::EC2::EIP" + } + }, + "Outputs": { + "EIP": { + "Description": "EIP for joining", + "Value": { + "Fn::Join": [ + ":", + [ + "test eip", + {"Ref": "EC2EIP"} + ] + ] + } + } + } +} diff --git a/tests/test_cloudformation/test_cloudformation_stack_integration.py b/tests/test_cloudformation/test_cloudformation_stack_integration.py index 0161ee0ad..3cda40928 100644 --- a/tests/test_cloudformation/test_cloudformation_stack_integration.py +++ b/tests/test_cloudformation/test_cloudformation_stack_integration.py @@ -16,7 +16,8 @@ from .fixtures import ( single_instance_with_ebs_volume, vpc_single_instance_in_subnet, ec2_classic_eip, - vpc_eip + vpc_eip, + fn_join ) @@ -519,3 +520,21 @@ def test_vpc_eip(): resources = stack.describe_resources() cfn_eip = [resource for resource in resources if resource.resource_type == 'AWS::EC2::EIP'][0] cfn_eip.physical_resource_id.should.equal(eip.allocation_id) + + +@mock_ec2() +@mock_cloudformation() +def test_fn_join(): + + template_json = json.dumps(fn_join.template) + conn = boto.connect_cloudformation() + conn.create_stack( + "test_stack", + template_body=template_json, + ) + ec2_conn = boto.connect_ec2() + eip = ec2_conn.get_all_addresses()[0] + + stack = conn.describe_stacks()[0] + fn_join_output = stack.outputs[0] + fn_join_output.value.should.equal('test eip:{0}'.format(eip.public_ip))