From ef876dd27eaf60b8d682d02c71c05296c6718fc0 Mon Sep 17 00:00:00 2001 From: Steve Pulec Date: Thu, 27 Mar 2014 19:12:53 -0400 Subject: [PATCH] Fix merge conflicts. Add basic cloudformation support. Closes #111. --- moto/__init__.py | 2 + moto/autoscaling/models.py | 64 ++- moto/cloudformation/__init__.py | 2 + moto/cloudformation/models.py | 70 +++ moto/cloudformation/parsing.py | 140 ++++++ moto/cloudformation/responses.py | 128 +++++ moto/cloudformation/urls.py | 9 + moto/cloudformation/utils.py | 6 + moto/core/responses.py | 3 + moto/ec2/models.py | 305 ++++++++++- moto/ec2/responses/elastic_ip_addresses.py | 3 + moto/ec2/utils.py | 10 +- moto/elb/models.py | 19 + moto/iam/__init__.py | 2 + moto/iam/models.py | 99 ++++ moto/iam/responses.py | 184 +++++++ moto/iam/urls.py | 9 + moto/iam/utils.py | 9 + moto/sqs/models.py | 13 + tests/test_cloudformation/__init__.py | 0 .../test_cloudformation/fixtures/__init__.py | 0 .../single_instance_with_ebs_volume.py | 343 +++++++++++++ .../fixtures/vpc_single_instance_in_subnet.py | 402 +++++++++++++++ .../test_cloudformation_stack_crud.py | 115 +++++ .../test_cloudformation_stack_integration.py | 473 ++++++++++++++++++ tests/test_cloudformation/test_server.py | 0 .../test_cloudformation/test_stack_parsing.py | 47 ++ tests/test_iam/test_iam.py | 27 + 28 files changed, 2473 insertions(+), 11 deletions(-) create mode 100644 moto/cloudformation/__init__.py create mode 100644 moto/cloudformation/models.py create mode 100644 moto/cloudformation/parsing.py create mode 100644 moto/cloudformation/responses.py create mode 100644 moto/cloudformation/urls.py create mode 100644 moto/cloudformation/utils.py create mode 100644 moto/iam/__init__.py create mode 100644 moto/iam/models.py create mode 100644 moto/iam/responses.py create mode 100644 moto/iam/urls.py create mode 100644 moto/iam/utils.py create mode 100644 tests/test_cloudformation/__init__.py create mode 100644 tests/test_cloudformation/fixtures/__init__.py create mode 100644 tests/test_cloudformation/fixtures/single_instance_with_ebs_volume.py create mode 100644 tests/test_cloudformation/fixtures/vpc_single_instance_in_subnet.py create mode 100644 tests/test_cloudformation/test_cloudformation_stack_crud.py create mode 100644 tests/test_cloudformation/test_cloudformation_stack_integration.py create mode 100644 tests/test_cloudformation/test_server.py create mode 100644 tests/test_cloudformation/test_stack_parsing.py create mode 100644 tests/test_iam/test_iam.py diff --git a/moto/__init__.py b/moto/__init__.py index 1db4c0ee1..e343c317f 100644 --- a/moto/__init__.py +++ b/moto/__init__.py @@ -2,11 +2,13 @@ import logging logging.getLogger('boto').setLevel(logging.CRITICAL) from .autoscaling import mock_autoscaling +from .cloudformation import mock_cloudformation from .dynamodb import mock_dynamodb from .dynamodb2 import mock_dynamodb2 from .ec2 import mock_ec2 from .elb import mock_elb from .emr import mock_emr +from .iam import mock_iam from .s3 import mock_s3 from .s3bucket_path import mock_s3bucket_path from .ses import mock_ses diff --git a/moto/autoscaling/models.py b/moto/autoscaling/models.py index cb07c4207..a80337099 100644 --- a/moto/autoscaling/models.py +++ b/moto/autoscaling/models.py @@ -33,7 +33,7 @@ class FakeLaunchConfiguration(object): self.name = name self.image_id = image_id self.key_name = key_name - self.security_groups = security_groups + self.security_groups = security_groups if security_groups else [] self.user_data = user_data self.instance_type = instance_type self.instance_monitoring = instance_monitoring @@ -42,6 +42,31 @@ class FakeLaunchConfiguration(object): self.ebs_optimized = ebs_optimized self.associate_public_ip_address = associate_public_ip_address + @classmethod + def create_from_cloudformation_json(cls, resource_name, cloudformation_json): + properties = cloudformation_json['Properties'] + + instance_profile_name = properties.get("IamInstanceProfile") + + config = autoscaling_backend.create_launch_configuration( + name=resource_name, + image_id=properties.get("ImageId"), + key_name=properties.get("KeyName"), + security_groups=properties.get("SecurityGroups"), + user_data=properties.get("UserData"), + instance_type=properties.get("InstanceType"), + instance_monitoring=properties.get("InstanceMonitoring"), + instance_profile_name=instance_profile_name, + spot_price=properties.get("SpotPrice"), + ebs_optimized=properties.get("EbsOptimized"), + associate_public_ip_address=properties.get("AssociatePublicIpAddress"), + ) + return config + + @property + def physical_resource_id(self): + return self.name + @property def instance_monitoring_enabled(self): if self.instance_monitoring: @@ -73,6 +98,34 @@ class FakeAutoScalingGroup(object): self.instances = [] self.set_desired_capacity(desired_capacity) + @classmethod + def create_from_cloudformation_json(cls, resource_name, cloudformation_json): + properties = cloudformation_json['Properties'] + + launch_config_name = properties.get("LaunchConfigurationName") + load_balancer_names = properties.get("LoadBalancerNames", []) + + group = autoscaling_backend.create_autoscaling_group( + name=resource_name, + availability_zones=properties.get("AvailabilityZones", []), + desired_capacity=properties.get("DesiredCapacity"), + max_size=properties.get("MaxSize"), + min_size=properties.get("MinSize"), + launch_config_name=launch_config_name, + vpc_zone_identifier=properties.get("VPCZoneIdentifier"), + default_cooldown=properties.get("Cooldown"), + health_check_period=properties.get("HealthCheckGracePeriod"), + health_check_type=properties.get("HealthCheckType"), + load_balancers=load_balancer_names, + placement_group=None, + termination_policies=properties.get("TerminationPolicies", []), + ) + return group + + @property + def physical_resource_id(self): + return self.name + def update(self, availability_zones, desired_capacity, max_size, min_size, launch_config_name, vpc_zone_identifier, default_cooldown, health_check_period, health_check_type, load_balancers, @@ -164,6 +217,15 @@ class AutoScalingBackend(BaseBackend): default_cooldown, health_check_period, health_check_type, load_balancers, placement_group, termination_policies): + + def make_int(value): + return int(value) if value is not None else value + + max_size = make_int(max_size) + min_size = make_int(min_size) + default_cooldown = make_int(default_cooldown) + health_check_period = make_int(health_check_period) + group = FakeAutoScalingGroup( name=name, availability_zones=availability_zones, diff --git a/moto/cloudformation/__init__.py b/moto/cloudformation/__init__.py new file mode 100644 index 000000000..45726c8b2 --- /dev/null +++ b/moto/cloudformation/__init__.py @@ -0,0 +1,2 @@ +from .models import cloudformation_backend +mock_cloudformation = cloudformation_backend.decorator diff --git a/moto/cloudformation/models.py b/moto/cloudformation/models.py new file mode 100644 index 000000000..50d06fc19 --- /dev/null +++ b/moto/cloudformation/models.py @@ -0,0 +1,70 @@ +import json + +from moto.core import BaseBackend + +from .parsing import ResourceMap +from .utils import generate_stack_id + + +class FakeStack(object): + def __init__(self, stack_id, name, template): + self.stack_id = stack_id + self.name = name + self.template = template + + template_dict = json.loads(self.template) + self.description = template_dict.get('Description') + + self.resource_map = ResourceMap(stack_id, name, template_dict) + self.resource_map.create() + + @property + def stack_resources(self): + return self.resource_map.values() + + +class CloudFormationBackend(BaseBackend): + + def __init__(self): + self.stacks = {} + + def create_stack(self, name, template): + stack_id = generate_stack_id(name) + new_stack = FakeStack(stack_id=stack_id, name=name, template=template) + self.stacks[stack_id] = new_stack + return new_stack + + def describe_stacks(self, names): + stacks = self.stacks.values() + if names: + return [stack for stack in stacks if stack.name in names] + else: + return stacks + + def list_stacks(self): + return self.stacks.values() + + def get_stack(self, name_or_stack_id): + if name_or_stack_id in self.stacks: + # Lookup by stack id + return self.stacks.get(name_or_stack_id) + else: + # Lookup by stack name + return [stack for stack in self.stacks.values() if stack.name == name_or_stack_id][0] + + # def update_stack(self, name, template): + # stack = self.get_stack(name) + # stack.template = template + # return stack + + def delete_stack(self, name_or_stack_id): + if name_or_stack_id in self.stacks: + # Delete by stack id + return self.stacks.pop(name_or_stack_id, None) + else: + # Delete by stack name + stack_to_delete = [stack for stack in self.stacks.values() if stack.name == name_or_stack_id][0] + self.delete_stack(stack_to_delete.stack_id) + + +cloudformation_backend = CloudFormationBackend() diff --git a/moto/cloudformation/parsing.py b/moto/cloudformation/parsing.py new file mode 100644 index 000000000..797743979 --- /dev/null +++ b/moto/cloudformation/parsing.py @@ -0,0 +1,140 @@ +import collections +import logging + +from moto.autoscaling import models as autoscaling_models +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 + +MODEL_MAP = { + "AWS::AutoScaling::AutoScalingGroup": autoscaling_models.FakeAutoScalingGroup, + "AWS::AutoScaling::LaunchConfiguration": autoscaling_models.FakeLaunchConfiguration, + "AWS::EC2::EIP": ec2_models.ElasticAddress, + "AWS::EC2::Instance": ec2_models.Instance, + "AWS::EC2::InternetGateway": ec2_models.InternetGateway, + "AWS::EC2::Route": ec2_models.Route, + "AWS::EC2::RouteTable": ec2_models.RouteTable, + "AWS::EC2::SecurityGroup": ec2_models.SecurityGroup, + "AWS::EC2::Subnet": ec2_models.Subnet, + "AWS::EC2::SubnetRouteTableAssociation": ec2_models.SubnetRouteTableAssociation, + "AWS::EC2::Volume": ec2_models.Volume, + "AWS::EC2::VolumeAttachment": ec2_models.VolumeAttachment, + "AWS::EC2::VPC": ec2_models.VPC, + "AWS::EC2::VPCGatewayAttachment": ec2_models.VPCGatewayAttachment, + "AWS::ElasticLoadBalancing::LoadBalancer": elb_models.FakeLoadBalancer, + "AWS::IAM::InstanceProfile": iam_models.InstanceProfile, + "AWS::IAM::Role": iam_models.Role, + "AWS::SQS::Queue": sqs_models.Queue, +} + +# Just ignore these models types for now +NULL_MODELS = [ + "AWS::CloudFormation::WaitCondition", + "AWS::CloudFormation::WaitConditionHandle", +] + +logger = logging.getLogger("moto") + + +def clean_json(resource_json, resources_map): + """ + Cleanup the a resource dict. For now, this just means replacing any Ref node + with the corresponding physical_resource_id. + + Eventually, this is where we would add things like function parsing (fn::) + """ + if isinstance(resource_json, dict): + if 'Ref' in resource_json: + # Parse resource reference + resource = resources_map[resource_json['Ref']] + if hasattr(resource, 'physical_resource_id'): + return resource.physical_resource_id + else: + return resource + + cleaned_json = {} + for key, value in resource_json.iteritems(): + cleaned_json[key] = clean_json(value, resources_map) + return cleaned_json + elif isinstance(resource_json, list): + return [clean_json(val, resources_map) for val in resource_json] + else: + return resource_json + + +def resource_class_from_type(resource_type): + if resource_type in NULL_MODELS: + return None + if resource_type not in MODEL_MAP: + logger.warning("No Moto CloudFormation support for %s", resource_type) + return None + return MODEL_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) + if not resource_class: + return None + + resource_json = clean_json(resource_json, resources_map) + resource = resource_class.create_from_cloudformation_json(resource_name, resource_json) + resource.type = resource_type + resource.logical_resource_id = resource_name + return resource + + +class ResourceMap(collections.Mapping): + """ + This is a lazy loading map for resources. This allows us to create resources + without needing to create a full dependency tree. Upon creation, each + each resources is passed this lazy map that it can grab dependencies from. + """ + + def __init__(self, stack_id, stack_name, template): + self._template = template + self._resource_json_map = template['Resources'] + + # Create the default resources + self._parsed_resources = { + "AWS::AccountId": "123456789012", + "AWS::Region": "us-east-1", + "AWS::StackId": stack_id, + "AWS::StackName": stack_name, + } + + def __getitem__(self, key): + resource_name = key + + if resource_name in self._parsed_resources: + return self._parsed_resources[resource_name] + 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 + return new_resource + + def __iter__(self): + return iter(self.resource_names) + + def __len__(self): + return len(self._resource_json_map) + + @property + def resource_names(self): + return self._resource_json_map.keys() + + def load_parameters(self): + parameters = self._template.get('Parameters', {}) + for parameter_name, parameter in parameters.items(): + # Just initialize parameters to empty string for now. + self._parsed_resources[parameter_name] = "" + + def create(self): + self.load_parameters() + + # 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] diff --git a/moto/cloudformation/responses.py b/moto/cloudformation/responses.py new file mode 100644 index 000000000..ef31670ae --- /dev/null +++ b/moto/cloudformation/responses.py @@ -0,0 +1,128 @@ +import json + +from jinja2 import Template + +from moto.core.responses import BaseResponse +from .models import cloudformation_backend + + +class CloudFormationResponse(BaseResponse): + + def create_stack(self): + stack_name = self._get_param('StackName') + stack_body = self._get_param('TemplateBody') + + stack = cloudformation_backend.create_stack( + name=stack_name, + template=stack_body, + ) + stack_body = { + 'CreateStackResponse': { + 'CreateStackResult': { + 'StackId': stack.name, + } + } + } + return json.dumps(stack_body) + + def describe_stacks(self): + names = [value[0] for key, value in self.querystring.items() if "StackName" in key] + stacks = cloudformation_backend.describe_stacks(names) + + template = Template(DESCRIBE_STACKS_TEMPLATE) + return template.render(stacks=stacks) + + def describe_stack_resources(self): + stack_name = self._get_param('StackName') + stack = cloudformation_backend.get_stack(stack_name) + + template = Template(LIST_STACKS_RESOURCES_RESPONSE) + return template.render(stack=stack) + + def list_stacks(self): + stacks = cloudformation_backend.list_stacks() + template = Template(LIST_STACKS_RESPONSE) + return template.render(stacks=stacks) + + def get_template(self): + name_or_stack_id = self.querystring.get('StackName')[0] + + stack = cloudformation_backend.get_stack(name_or_stack_id) + return stack.template + + # def update_stack(self): + # stack_name = self._get_param('StackName') + # stack_body = self._get_param('TemplateBody') + + # stack = cloudformation_backend.update_stack( + # name=stack_name, + # template=stack_body, + # ) + # stack_body = { + # 'UpdateStackResponse': { + # 'UpdateStackResult': { + # 'StackId': stack.name, + # } + # } + # } + # return json.dumps(stack_body) + + def delete_stack(self): + name_or_stack_id = self.querystring.get('StackName')[0] + + cloudformation_backend.delete_stack(name_or_stack_id) + return json.dumps({ + 'DeleteStackResponse': { + 'DeleteStackResult': {}, + } + }) + + +DESCRIBE_STACKS_TEMPLATE = """ + + {% for stack in stacks %} + + {{ stack.name }} + {{ stack.stack_id }} + 2010-07-27T22:28:28Z + CREATE_COMPLETE + false + + + {% endfor %} + +""" + + +LIST_STACKS_RESPONSE = """ + + + {% for stack in stacks %} + + {{ stack.id }} + CREATE_IN_PROGRESS + {{ stack.name }} + 2011-05-23T15:47:44Z + {{ stack.description }} + + {% endfor %} + + +""" + + +LIST_STACKS_RESOURCES_RESPONSE = """ + + {% for resource in stack.stack_resources %} + + {{ stack.stack_id }} + {{ stack.name }} + {{ resource.logical_resource_id }} + {{ resource.physical_resource_id }} + {{ resource.type }} + 2010-07-27T22:27:28Z + CREATE_COMPLETE + + {% endfor %} + +""" diff --git a/moto/cloudformation/urls.py b/moto/cloudformation/urls.py new file mode 100644 index 000000000..4d4c0ddb6 --- /dev/null +++ b/moto/cloudformation/urls.py @@ -0,0 +1,9 @@ +from .responses import CloudFormationResponse + +url_bases = [ + "https?://cloudformation.(.+).amazonaws.com", +] + +url_paths = { + '{0}/$': CloudFormationResponse().dispatch, +} diff --git a/moto/cloudformation/utils.py b/moto/cloudformation/utils.py new file mode 100644 index 000000000..e7f5e98c5 --- /dev/null +++ b/moto/cloudformation/utils.py @@ -0,0 +1,6 @@ +import uuid + + +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) diff --git a/moto/core/responses.py b/moto/core/responses.py index 4a0515c4b..ab1188e05 100644 --- a/moto/core/responses.py +++ b/moto/core/responses.py @@ -59,6 +59,9 @@ class BaseResponse(object): return status, headers, body raise NotImplementedError("The {0} action has not been implemented".format(action)) + def _get_param(self, param_name): + return self.querystring.get(param_name, [None])[0] + def metadata_response(request, full_url, headers): """ diff --git a/moto/ec2/models.py b/moto/ec2/models.py index ff17aab85..6f529830a 100644 --- a/moto/ec2/models.py +++ b/moto/ec2/models.py @@ -8,18 +8,20 @@ from moto.core import BaseBackend from .exceptions import InvalidIdError from .utils import ( random_ami_id, + random_eip_allocation_id, + random_eip_association_id, + random_gateway_id, random_instance_id, + random_ip, + random_key_pair, random_reservation_id, + random_route_table_id, random_security_group_id, random_snapshot_id, random_spot_request_id, random_subnet_id, random_volume_id, random_vpc_id, - random_eip_association_id, - random_eip_allocation_id, - random_ip, - random_key_pair, ) @@ -38,6 +40,25 @@ class Instance(BotoInstance): self.user_data = user_data self.security_groups = security_groups + @classmethod + def create_from_cloudformation_json(cls, resource_name, cloudformation_json): + properties = cloudformation_json['Properties'] + + security_group_ids = properties.get('SecurityGroups', []) + group_names = [ec2_backend.get_security_group_from_id(group_id).name for group_id in security_group_ids] + + reservation = ec2_backend.add_instances( + image_id=properties['ImageId'], + user_data=properties.get('UserData'), + count=1, + security_group_names=group_names, + ) + return reservation.instances[0] + + @property + def physical_resource_id(self): + return self.id + def start(self, *args, **kwargs): self._state.name = "running" self._state.code = 16 @@ -138,6 +159,12 @@ class InstanceBackend(object): instances.append(instance) return instances + def get_instance_by_id(self, instance_id): + for reservation in self.all_reservations(): + for instance in reservation.instances: + if instance.id == instance_id: + return instance + def get_reservations_by_instance_ids(self, instance_ids): """ Go through all of the reservations and filter to only return those associated with the given instance_ids. @@ -343,6 +370,37 @@ class SecurityGroup(object): self.egress_rules = [] self.vpc_id = vpc_id + @classmethod + def create_from_cloudformation_json(cls, resource_name, cloudformation_json): + properties = cloudformation_json['Properties'] + + vpc_id = properties.get('VpcId') + security_group = ec2_backend.create_security_group( + name=resource_name, + description=properties.get('GroupDescription'), + vpc_id=vpc_id, + ) + + for ingress_rule in properties.get('SecurityGroupIngress', []): + source_group_id = ingress_rule.get('SourceSecurityGroupId') + + ec2_backend.authorize_security_group_ingress( + group_name=security_group.name, + group_id=security_group.id, + ip_protocol=ingress_rule['IpProtocol'], + from_port=ingress_rule['FromPort'], + to_port=ingress_rule['ToPort'], + ip_ranges=ingress_rule.get('CidrIp'), + source_group_ids=[source_group_id], + vpc_id=vpc_id, + ) + + return security_group + + @property + def physical_resource_id(self): + return self.id + class SecurityGroupBackend(object): @@ -401,7 +459,7 @@ class SecurityGroupBackend(object): ip_protocol, from_port, to_port, - ip_ranges=None, + ip_ranges, source_group_names=None, source_group_ids=None, vpc_id=None): @@ -412,6 +470,12 @@ class SecurityGroupBackend(object): elif group_id: group = self.get_security_group_from_id(group_id) + if ip_ranges and not isinstance(ip_ranges, list): + ip_ranges = [ip_ranges] + + source_group_names = source_group_names if source_group_names else [] + source_group_ids = source_group_ids if source_group_ids else [] + source_groups = [] for source_group_name in source_group_names: source_group = self.get_security_group_from_name(source_group_name, vpc_id) @@ -433,7 +497,7 @@ class SecurityGroupBackend(object): ip_protocol, from_port, to_port, - ip_ranges=None, + ip_ranges, source_group_names=None, source_group_ids=None, vpc_id=None): @@ -467,6 +531,20 @@ class VolumeAttachment(object): self.instance = instance self.device = device + @classmethod + def create_from_cloudformation_json(cls, resource_name, cloudformation_json): + properties = cloudformation_json['Properties'] + + instance_id = properties['InstanceId'] + volume_id = properties['VolumeId'] + + attachment = ec2_backend.attach_volume( + volume_id=volume_id, + instance_id=instance_id, + device_path=properties['Device'], + ) + return attachment + class Volume(object): def __init__(self, volume_id, size, zone): @@ -475,6 +553,20 @@ class Volume(object): self.zone = zone self.attachment = None + @classmethod + def create_from_cloudformation_json(cls, resource_name, cloudformation_json): + properties = cloudformation_json['Properties'] + + volume = ec2_backend.create_volume( + size=properties.get('Size'), + zone_name=properties.get('AvailabilityZone'), + ) + return volume + + @property + def physical_resource_id(self): + return self.id + @property def status(self): if self.attachment: @@ -554,6 +646,19 @@ class VPC(object): self.id = vpc_id self.cidr_block = cidr_block + @classmethod + def create_from_cloudformation_json(cls, resource_name, cloudformation_json): + properties = cloudformation_json['Properties'] + + vpc = ec2_backend.create_vpc( + cidr_block=properties['CidrBlock'], + ) + return vpc + + @property + def physical_resource_id(self): + return self.id + class VPCBackend(object): def __init__(self): @@ -582,6 +687,21 @@ class Subnet(object): self.vpc = vpc self.cidr_block = cidr_block + @classmethod + def create_from_cloudformation_json(cls, resource_name, cloudformation_json): + properties = cloudformation_json['Properties'] + + vpc_id = properties['VpcId'] + subnet = ec2_backend.create_subnet( + vpc_id=vpc_id, + cidr_block=properties['CidrBlock'] + ) + return subnet + + @property + def physical_resource_id(self): + return self.id + class SubnetBackend(object): def __init__(self): @@ -602,6 +722,154 @@ class SubnetBackend(object): return self.subnets.pop(subnet_id, None) +class SubnetRouteTableAssociation(object): + def __init__(self, route_table_id, subnet_id): + self.route_table_id = route_table_id + self.subnet_id = subnet_id + + @classmethod + def create_from_cloudformation_json(cls, resource_name, cloudformation_json): + properties = cloudformation_json['Properties'] + + route_table_id = properties['RouteTableId'] + subnet_id = properties['SubnetId'] + + subnet_association = ec2_backend.create_subnet_association( + route_table_id=route_table_id, + subnet_id=subnet_id, + ) + return subnet_association + + +class SubnetRouteTableAssociationBackend(object): + def __init__(self): + self.subnet_associations = {} + super(SubnetRouteTableAssociationBackend, self).__init__() + + def create_subnet_association(self, route_table_id, subnet_id): + subnet_association = SubnetRouteTableAssociation(route_table_id, subnet_id) + self.subnet_associations["{0}:{1}".format(route_table_id, subnet_id)] = subnet_association + return subnet_association + + +class RouteTable(object): + def __init__(self, route_table_id, vpc_id): + self.id = route_table_id + self.vpc_id = vpc_id + + @classmethod + def create_from_cloudformation_json(cls, resource_name, cloudformation_json): + properties = cloudformation_json['Properties'] + + vpc_id = properties['VpcId'] + route_table = ec2_backend.create_route_table( + vpc_id=vpc_id, + ) + return route_table + + @property + def physical_resource_id(self): + return self.id + + +class RouteTableBackend(object): + def __init__(self): + self.route_tables = {} + super(RouteTableBackend, self).__init__() + + def create_route_table(self, vpc_id): + route_table_id = random_route_table_id() + route_table = RouteTable(route_table_id, vpc_id) + self.route_tables[route_table_id] = route_table + return route_table + + +class Route(object): + def __init__(self, route_table_id, destination_cidr_block, gateway_id): + self.route_table_id = route_table_id + self.destination_cidr_block = destination_cidr_block + self.gateway_id = gateway_id + + @classmethod + def create_from_cloudformation_json(cls, resource_name, cloudformation_json): + properties = cloudformation_json['Properties'] + + gateway_id = properties['GatewayId'] + route_table_id = properties['RouteTableId'] + route_table = ec2_backend.create_route( + route_table_id=route_table_id, + destination_cidr_block=properties['DestinationCidrBlock'], + gateway_id=gateway_id, + ) + return route_table + + +class RouteBackend(object): + def __init__(self): + self.routes = {} + super(RouteBackend, self).__init__() + + def create_route(self, route_table_id, destination_cidr_block, gateway_id): + route = Route(route_table_id, destination_cidr_block, gateway_id) + self.routes[destination_cidr_block] = route + return route + + +class InternetGateway(object): + def __init__(self, gateway_id): + self.id = gateway_id + + @classmethod + def create_from_cloudformation_json(cls, resource_name, cloudformation_json): + return ec2_backend.create_gateway() + + @property + def physical_resource_id(self): + return self.id + + +class InternetGatewayBackend(object): + def __init__(self): + self.gateways = {} + super(InternetGatewayBackend, self).__init__() + + def create_gateway(self): + gateway_id = random_gateway_id() + gateway = InternetGateway(gateway_id) + self.gateways[gateway_id] = gateway + return gateway + + +class VPCGatewayAttachment(object): + def __init__(self, gateway_id, vpc_id): + self.gateway_id = gateway_id + self.vpc_id = vpc_id + + @classmethod + def create_from_cloudformation_json(cls, resource_name, cloudformation_json): + properties = cloudformation_json['Properties'] + + return ec2_backend.create_vpc_gateway_attachment( + gateway_id=properties['InternetGatewayId'], + vpc_id=properties['VpcId'], + ) + + @property + def physical_resource_id(self): + return self.id + + +class VPCGatewayAttachmentBackend(object): + def __init__(self): + self.gateway_attachments = {} + super(VPCGatewayAttachmentBackend, self).__init__() + + def create_vpc_gateway_attachment(self, vpc_id, gateway_id): + attachment = VPCGatewayAttachment(vpc_id, gateway_id) + self.gateway_attachments[gateway_id] = attachment + return attachment + + class SpotInstanceRequest(object): def __init__(self, spot_request_id, price, image_id, type, valid_from, valid_until, launch_group, availability_zone_group, key_name, @@ -678,6 +946,25 @@ class ElasticAddress(object): self.instance = None self.association_id = None + @classmethod + def create_from_cloudformation_json(cls, resource_name, cloudformation_json): + properties = cloudformation_json['Properties'] + + 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, eip.public_ip) + + return eip + + @property + def physical_resource_id(self): + return self.allocation_id + class ElasticAddressBackend(object): @@ -755,8 +1042,10 @@ class ElasticAddressBackend(object): class EC2Backend(BaseBackend, InstanceBackend, TagBackend, AmiBackend, RegionsAndZonesBackend, SecurityGroupBackend, EBSBackend, - VPCBackend, SubnetBackend, SpotRequestBackend, ElasticAddressBackend, - KeyPairBackend): + VPCBackend, SubnetBackend, SubnetRouteTableAssociationBackend, + RouteTableBackend, RouteBackend, InternetGatewayBackend, + VPCGatewayAttachmentBackend, SpotRequestBackend, + ElasticAddressBackend, KeyPairBackend): pass diff --git a/moto/ec2/responses/elastic_ip_addresses.py b/moto/ec2/responses/elastic_ip_addresses.py index 5553ef956..750485df8 100644 --- a/moto/ec2/responses/elastic_ip_addresses.py +++ b/moto/ec2/responses/elastic_ip_addresses.py @@ -113,6 +113,9 @@ DESCRIBE_ADDRESS_RESPONSE = """ + + + {{ profile.id }} + + {{ profile.name }} + {{ profile.path }} + arn:aws:iam::123456789012:instance-profile/application_abc/component_xyz/Webserver + 2012-05-09T16:11:10.222Z + + + + 974142ee-99f1-11e1-a4c3-27EXAMPLE804 + +""" + +GET_INSTANCE_PROFILE_TEMPLATE = """ + + + {{ profile.id }} + + {% for role in profile.roles %} + + {{ role.path }} + arn:aws:iam::123456789012:role/application_abc/component_xyz/S3Access + {{ role.name }} + {{ role.assume_role_policy_document }} + 2012-05-09T15:45:35Z + {{ role.id }} + + {% endfor %} + + {{ profile.name }} + {{ profile.path }} + arn:aws:iam::123456789012:instance-profile/application_abc/component_xyz/Webserver + 2012-05-09T16:11:10Z + + + + 37289fda-99f2-11e1-a4c3-27EXAMPLE804 + +""" + +CREATE_ROLE_TEMPLATE = """ + + + {{ role.path }} + arn:aws:iam::123456789012:role/application_abc/component_xyz/S3Access + {{ role.name }} + {{ role.assume_role_policy_document }} + 2012-05-08T23:34:01.495Z + {{ role.id }} + + + + 4a93ceee-9966-11e1-b624-b1aEXAMPLE7c + +""" + +GET_ROLE_TEMPLATE = """ + + + {{ role.path }} + arn:aws:iam::123456789012:role/application_abc/component_xyz/S3Access + {{ role.name }} + {{ role.assume_role_policy_document }} + 2012-05-08T23:34:01Z + {{ role.id }} + + + + df37e965-9967-11e1-a4c3-270EXAMPLE04 + +""" + +ADD_ROLE_TO_INSTANCE_PROFILE_TEMPLATE = """ + + 12657608-99f2-11e1-a4c3-27EXAMPLE804 + +""" + +LIST_ROLES_TEMPLATE = """ + + false + + {% for role in roles %} + + {{ role.path }} + arn:aws:iam::123456789012:role/application_abc/component_xyz/S3Access + {{ role.name }} + {{ role.assume_role_policy_document }} + 2012-05-09T15:45:35Z + {{ role.id }} + + {% endfor %} + + + + 20f7279f-99ee-11e1-a4c3-27EXAMPLE804 + +""" + +LIST_INSTANCE_PROFILES_TEMPLATE = """ + + false + + {% for instance in instance_profiles %} + + {{ instance.id }} + + {{ instance.name }} + {{ instance.path }} + arn:aws:iam::123456789012:instance-profile/application_abc/component_xyz/Database + 2012-05-09T16:27:03Z + + {% endfor %} + + + + fd74fa8d-99f3-11e1-a4c3-27EXAMPLE804 + +""" diff --git a/moto/iam/urls.py b/moto/iam/urls.py new file mode 100644 index 000000000..f5dbfe75b --- /dev/null +++ b/moto/iam/urls.py @@ -0,0 +1,9 @@ +from .responses import IamResponse + +url_bases = [ + "https?://iam.amazonaws.com", +] + +url_paths = { + '{0}/$': IamResponse().dispatch, +} diff --git a/moto/iam/utils.py b/moto/iam/utils.py new file mode 100644 index 000000000..64facde4d --- /dev/null +++ b/moto/iam/utils.py @@ -0,0 +1,9 @@ +import random +import string + + +def random_resource_id(): + size = 20 + chars = range(10) + list(string.lowercase) + + return ''.join(unicode(random.choice(chars)) for x in range(size)) diff --git a/moto/sqs/models.py b/moto/sqs/models.py index 13aabd424..d3e1ded3b 100644 --- a/moto/sqs/models.py +++ b/moto/sqs/models.py @@ -51,6 +51,19 @@ class Queue(object): self.queue_arn = 'arn:aws:sqs:sqs.us-east-1:123456789012:%s' % self.name self.receive_message_wait_time_seconds = 0 + @classmethod + def create_from_cloudformation_json(cls, resource_name, cloudformation_json): + properties = cloudformation_json['Properties'] + + return sqs_backend.create_queue( + name=properties['QueueName'], + visibility_timeout=properties.get('VisibilityTimeout'), + ) + + @property + def physical_resource_id(self): + return self.name + @property def attributes(self): result = {} diff --git a/tests/test_cloudformation/__init__.py b/tests/test_cloudformation/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/test_cloudformation/fixtures/__init__.py b/tests/test_cloudformation/fixtures/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/test_cloudformation/fixtures/single_instance_with_ebs_volume.py b/tests/test_cloudformation/fixtures/single_instance_with_ebs_volume.py new file mode 100644 index 000000000..596b49391 --- /dev/null +++ b/tests/test_cloudformation/fixtures/single_instance_with_ebs_volume.py @@ -0,0 +1,343 @@ +template = { + "Description": "AWS CloudFormation Sample Template Gollum_Single_Instance_With_EBS_Volume: Gollum is a simple wiki system built on top of Git that powers GitHub Wikis. This template installs a Gollum Wiki stack on a single EC2 instance with an EBS volume for storage and demonstrates using the AWS CloudFormation bootstrap scripts to install the packages and files necessary at instance launch time. **WARNING** This template creates an Amazon EC2 instance and an EBS volume. You will be billed for the AWS resources used if you create a stack from this template.", + "Parameters": { + "SSHLocation": { + "ConstraintDescription": "must be a valid IP CIDR range of the form x.x.x.x/x.", + "Description": "The IP address range that can be used to SSH to the EC2 instances", + "Default": "0.0.0.0/0", + "MinLength": "9", + "AllowedPattern": "(\\d{1,3})\\.(\\d{1,3})\\.(\\d{1,3})\\.(\\d{1,3})/(\\d{1,2})", + "MaxLength": "18", + "Type": "String" + }, + "KeyName": { + "Type": "String", + "Description": "Name of an existing EC2 KeyPair to enable SSH access to the instances", + "MinLength": "1", + "AllowedPattern": "[\\x20-\\x7E]*", + "MaxLength": "255", + "ConstraintDescription": "can contain only ASCII characters." + }, + "InstanceType": { + "Default": "m1.small", + "ConstraintDescription": "must be a valid EC2 instance type.", + "Type": "String", + "Description": "WebServer EC2 instance type", + "AllowedValues": [ + "t1.micro", + "m1.small", + "m1.medium", + "m1.large", + "m1.xlarge", + "m2.xlarge", + "m2.2xlarge", + "m2.4xlarge", + "m3.xlarge", + "m3.2xlarge", + "c1.medium", + "c1.xlarge", + "cc1.4xlarge", + "cc2.8xlarge", + "cg1.4xlarge" + ] + }, + "VolumeSize": { + "Description": "WebServer EC2 instance type", + "Default": "5", + "Type": "Number", + "MaxValue": "1024", + "MinValue": "5", + "ConstraintDescription": "must be between 5 and 1024 Gb." + } + }, + "AWSTemplateFormatVersion": "2010-09-09", + "Outputs": { + "WebsiteURL": { + "Description": "URL for Gollum wiki", + "Value": { + "Fn::Join": [ + "", + [ + "http://", + { + "Fn::GetAtt": [ + "WebServer", + "PublicDnsName" + ] + } + ] + ] + } + } + }, + "Resources": { + "WebServerSecurityGroup": { + "Type": "AWS::EC2::SecurityGroup", + "Properties": { + "SecurityGroupIngress": [ + { + "ToPort": "80", + "IpProtocol": "tcp", + "CidrIp": "0.0.0.0/0", + "FromPort": "80" + }, + { + "ToPort": "22", + "IpProtocol": "tcp", + "CidrIp": { + "Ref": "SSHLocation" + }, + "FromPort": "22" + } + ], + "GroupDescription": "Enable SSH access and HTTP access on the inbound port" + } + }, + "WebServer": { + "Type": "AWS::EC2::Instance", + "Properties": { + "UserData": { + "Fn::Base64": { + "Fn::Join": [ + "", + [ + "#!/bin/bash -v\n", + "yum update -y aws-cfn-bootstrap\n", + "# Helper function\n", + "function error_exit\n", + "{\n", + " /opt/aws/bin/cfn-signal -e 1 -r \"$1\" '", + { + "Ref": "WaitHandle" + }, + "'\n", + " exit 1\n", + "}\n", + "# Install Rails packages\n", + "/opt/aws/bin/cfn-init -s ", + { + "Ref": "AWS::StackId" + }, + " -r WebServer ", + " --region ", + { + "Ref": "AWS::Region" + }, + " || error_exit 'Failed to run cfn-init'\n", + "# Wait for the EBS volume to show up\n", + "while [ ! -e /dev/sdh ]; do echo Waiting for EBS volume to attach; sleep 5; done\n", + "# Format the EBS volume and mount it\n", + "mkdir /var/wikidata\n", + "/sbin/mkfs -t ext3 /dev/sdh1\n", + "mount /dev/sdh1 /var/wikidata\n", + "# Initialize the wiki and fire up the server\n", + "cd /var/wikidata\n", + "git init\n", + "gollum --port 80 --host 0.0.0.0 &\n", + "# If all is well so signal success\n", + "/opt/aws/bin/cfn-signal -e $? -r \"Rails application setup complete\" '", + { + "Ref": "WaitHandle" + }, + "'\n" + ] + ] + } + }, + "KeyName": { + "Ref": "KeyName" + }, + "SecurityGroups": [ + { + "Ref": "WebServerSecurityGroup" + } + ], + "InstanceType": { + "Ref": "InstanceType" + }, + "ImageId": { + "Fn::FindInMap": [ + "AWSRegionArch2AMI", + { + "Ref": "AWS::Region" + }, + { + "Fn::FindInMap": [ + "AWSInstanceType2Arch", + { + "Ref": "InstanceType" + }, + "Arch" + ] + } + ] + } + }, + "Metadata": { + "AWS::CloudFormation::Init": { + "config": { + "packages": { + "rubygems": { + "nokogiri": [ + "1.5.10" + ], + "rdiscount": [], + "gollum": [ + "1.1.1" + ] + }, + "yum": { + "libxslt-devel": [], + "gcc": [], + "git": [], + "rubygems": [], + "ruby-devel": [], + "ruby-rdoc": [], + "make": [], + "libxml2-devel": [] + } + } + } + } + } + }, + "DataVolume": { + "Type": "AWS::EC2::Volume", + "Properties": { + "Tags": [ + { + "Value": "Gollum Data Volume", + "Key": "Usage" + } + ], + "AvailabilityZone": { + "Fn::GetAtt": [ + "WebServer", + "AvailabilityZone" + ] + }, + "Size": "100", + } + }, + "MountPoint": { + "Type": "AWS::EC2::VolumeAttachment", + "Properties": { + "InstanceId": { + "Ref": "WebServer" + }, + "Device": "/dev/sdh", + "VolumeId": { + "Ref": "DataVolume" + } + } + }, + "WaitCondition": { + "DependsOn": "MountPoint", + "Type": "AWS::CloudFormation::WaitCondition", + "Properties": { + "Handle": { + "Ref": "WaitHandle" + }, + "Timeout": "300" + }, + "Metadata": { + "Comment1": "Note that the WaitCondition is dependent on the volume mount point allowing the volume to be created and attached to the EC2 instance", + "Comment2": "The instance bootstrap script waits for the volume to be attached to the instance prior to installing Gollum and signalling completion" + } + }, + "WaitHandle": { + "Type": "AWS::CloudFormation::WaitConditionHandle" + } + }, + "Mappings": { + "AWSInstanceType2Arch": { + "m3.2xlarge": { + "Arch": "64" + }, + "m2.2xlarge": { + "Arch": "64" + }, + "m1.small": { + "Arch": "64" + }, + "c1.medium": { + "Arch": "64" + }, + "cg1.4xlarge": { + "Arch": "64HVM" + }, + "m2.xlarge": { + "Arch": "64" + }, + "t1.micro": { + "Arch": "64" + }, + "cc1.4xlarge": { + "Arch": "64HVM" + }, + "m1.medium": { + "Arch": "64" + }, + "cc2.8xlarge": { + "Arch": "64HVM" + }, + "m1.large": { + "Arch": "64" + }, + "m1.xlarge": { + "Arch": "64" + }, + "m2.4xlarge": { + "Arch": "64" + }, + "c1.xlarge": { + "Arch": "64" + }, + "m3.xlarge": { + "Arch": "64" + } + }, + "AWSRegionArch2AMI": { + "ap-southeast-1": { + "64HVM": "NOT_YET_SUPPORTED", + "32": "ami-b4b0cae6", + "64": "ami-beb0caec" + }, + "ap-southeast-2": { + "64HVM": "NOT_YET_SUPPORTED", + "32": "ami-b3990e89", + "64": "ami-bd990e87" + }, + "us-west-2": { + "64HVM": "NOT_YET_SUPPORTED", + "32": "ami-38fe7308", + "64": "ami-30fe7300" + }, + "us-east-1": { + "64HVM": "ami-0da96764", + "32": "ami-31814f58", + "64": "ami-1b814f72" + }, + "ap-northeast-1": { + "64HVM": "NOT_YET_SUPPORTED", + "32": "ami-0644f007", + "64": "ami-0a44f00b" + }, + "us-west-1": { + "64HVM": "NOT_YET_SUPPORTED", + "32": "ami-11d68a54", + "64": "ami-1bd68a5e" + }, + "eu-west-1": { + "64HVM": "NOT_YET_SUPPORTED", + "32": "ami-973b06e3", + "64": "ami-953b06e1" + }, + "sa-east-1": { + "64HVM": "NOT_YET_SUPPORTED", + "32": "ami-3e3be423", + "64": "ami-3c3be421" + } + } + } +} diff --git a/tests/test_cloudformation/fixtures/vpc_single_instance_in_subnet.py b/tests/test_cloudformation/fixtures/vpc_single_instance_in_subnet.py new file mode 100644 index 000000000..78f2a82d5 --- /dev/null +++ b/tests/test_cloudformation/fixtures/vpc_single_instance_in_subnet.py @@ -0,0 +1,402 @@ +template = { + "Description": "AWS CloudFormation Sample Template vpc_single_instance_in_subnet.template: Sample template showing how to create a VPC and add an EC2 instance with an Elastic IP address and a security group. **WARNING** This template creates an Amazon EC2 instance. You will be billed for the AWS resources used if you create a stack from this template.", + "Parameters": { + "SSHLocation": { + "ConstraintDescription": "must be a valid IP CIDR range of the form x.x.x.x/x.", + "Description": " The IP address range that can be used to SSH to the EC2 instances", + "Default": "0.0.0.0/0", + "MinLength": "9", + "AllowedPattern": "(\\d{1,3})\\.(\\d{1,3})\\.(\\d{1,3})\\.(\\d{1,3})/(\\d{1,2})", + "MaxLength": "18", + "Type": "String" + }, + "KeyName": { + "Type": "String", + "Description": "Name of an existing EC2 KeyPair to enable SSH access to the instance", + "MinLength": "1", + "AllowedPattern": "[\\x20-\\x7E]*", + "MaxLength": "255", + "ConstraintDescription": "can contain only ASCII characters." + }, + "InstanceType": { + "Default": "m1.small", + "ConstraintDescription": "must be a valid EC2 instance type.", + "Type": "String", + "Description": "WebServer EC2 instance type", + "AllowedValues": [ + "t1.micro", + "m1.small", + "m1.medium", + "m1.large", + "m1.xlarge", + "m2.xlarge", + "m2.2xlarge", + "m2.4xlarge", + "m3.xlarge", + "m3.2xlarge", + "c1.medium", + "c1.xlarge", + "cc1.4xlarge", + "cc2.8xlarge", + "cg1.4xlarge" + ] + } + }, + "AWSTemplateFormatVersion": "2010-09-09", + "Outputs": { + "URL": { + "Description": "Newly created application URL", + "Value": { + "Fn::Join": [ + "", + [ + "http://", + { + "Fn::GetAtt": [ + "WebServerInstance", + "PublicIp" + ] + } + ] + ] + } + } + }, + "Resources": { + "Subnet": { + "Type": "AWS::EC2::Subnet", + "Properties": { + "VpcId": { + "Ref": "VPC" + }, + "CidrBlock": "10.0.0.0/24", + "Tags": [ + { + "Value": { + "Ref": "AWS::StackId" + }, + "Key": "Application" + } + ] + } + }, + "WebServerWaitHandle": { + "Type": "AWS::CloudFormation::WaitConditionHandle" + }, + "Route": { + "Type": "AWS::EC2::Route", + "Properties": { + "GatewayId": { + "Ref": "InternetGateway" + }, + "DestinationCidrBlock": "0.0.0.0/0", + "RouteTableId": { + "Ref": "RouteTable" + } + }, + "DependsOn": "AttachGateway" + }, + "SubnetRouteTableAssociation": { + "Type": "AWS::EC2::SubnetRouteTableAssociation", + "Properties": { + "SubnetId": { + "Ref": "Subnet" + }, + "RouteTableId": { + "Ref": "RouteTable" + } + } + }, + "InternetGateway": { + "Type": "AWS::EC2::InternetGateway", + "Properties": { + "Tags": [ + { + "Value": { + "Ref": "AWS::StackId" + }, + "Key": "Application" + } + ] + } + }, + "RouteTable": { + "Type": "AWS::EC2::RouteTable", + "Properties": { + "VpcId": { + "Ref": "VPC" + }, + "Tags": [ + { + "Value": { + "Ref": "AWS::StackId" + }, + "Key": "Application" + } + ] + } + }, + "WebServerWaitCondition": { + "Type": "AWS::CloudFormation::WaitCondition", + "Properties": { + "Handle": { + "Ref": "WebServerWaitHandle" + }, + "Timeout": "300" + }, + "DependsOn": "WebServerInstance" + }, + "VPC": { + "Type": "AWS::EC2::VPC", + "Properties": { + "CidrBlock": "10.0.0.0/16", + "Tags": [ + { + "Value": { + "Ref": "AWS::StackId" + }, + "Key": "Application" + } + ] + } + }, + "InstanceSecurityGroup": { + "Type": "AWS::EC2::SecurityGroup", + "Properties": { + "SecurityGroupIngress": [ + { + "ToPort": "22", + "IpProtocol": "tcp", + "CidrIp": { + "Ref": "SSHLocation" + }, + "FromPort": "22" + }, + { + "ToPort": "80", + "IpProtocol": "tcp", + "CidrIp": "0.0.0.0/0", + "FromPort": "80" + } + ], + "VpcId": { + "Ref": "VPC" + }, + "GroupDescription": "Enable SSH access via port 22" + } + }, + "WebServerInstance": { + "Type": "AWS::EC2::Instance", + "Properties": { + "UserData": { + "Fn::Base64": { + "Fn::Join": [ + "", + [ + "#!/bin/bash\n", + "yum update -y aws-cfn-bootstrap\n", + "# Helper function\n", + "function error_exit\n", + "{\n", + " /opt/aws/bin/cfn-signal -e 1 -r \"$1\" '", + { + "Ref": "WebServerWaitHandle" + }, + "'\n", + " exit 1\n", + "}\n", + "# Install the simple web page\n", + "/opt/aws/bin/cfn-init -s ", + { + "Ref": "AWS::StackId" + }, + " -r WebServerInstance ", + " --region ", + { + "Ref": "AWS::Region" + }, + " || error_exit 'Failed to run cfn-init'\n", + "# Start up the cfn-hup daemon to listen for changes to the Web Server metadata\n", + "/opt/aws/bin/cfn-hup || error_exit 'Failed to start cfn-hup'\n", + "# All done so signal success\n", + "/opt/aws/bin/cfn-signal -e 0 -r \"WebServer setup complete\" '", + { + "Ref": "WebServerWaitHandle" + }, + "'\n" + ] + ] + } + }, + "Tags": [ + { + "Value": { + "Ref": "AWS::StackId" + }, + "Key": "Application" + } + ], + "SecurityGroupIds": [ + { + "Ref": "InstanceSecurityGroup" + } + ], + "KeyName": { + "Ref": "KeyName" + }, + "SubnetId": { + "Ref": "Subnet" + }, + "ImageId": { + "Fn::FindInMap": [ + "RegionMap", + { + "Ref": "AWS::Region" + }, + "AMI" + ] + }, + "InstanceType": { + "Ref": "InstanceType" + } + }, + "Metadata": { + "Comment": "Install a simple PHP application", + "AWS::CloudFormation::Init": { + "config": { + "files": { + "/etc/cfn/cfn-hup.conf": { + "content": { + "Fn::Join": [ + "", + [ + "[main]\n", + "stack=", + { + "Ref": "AWS::StackId" + }, + "\n", + "region=", + { + "Ref": "AWS::Region" + }, + "\n" + ] + ] + }, + "owner": "root", + "group": "root", + "mode": "000400" + }, + "/etc/cfn/hooks.d/cfn-auto-reloader.conf": { + "content": { + "Fn::Join": [ + "", + [ + "[cfn-auto-reloader-hook]\n", + "triggers=post.update\n", + "path=Resources.WebServerInstance.Metadata.AWS::CloudFormation::Init\n", + "action=/opt/aws/bin/cfn-init -s ", + { + "Ref": "AWS::StackId" + }, + " -r WebServerInstance ", + " --region ", + { + "Ref": "AWS::Region" + }, + "\n", + "runas=root\n" + ] + ] + } + }, + "/var/www/html/index.php": { + "content": { + "Fn::Join": [ + "", + [ + "AWS CloudFormation sample PHP application';\n", + "?>\n" + ] + ] + }, + "owner": "apache", + "group": "apache", + "mode": "000644" + } + }, + "services": { + "sysvinit": { + "httpd": { + "ensureRunning": "true", + "enabled": "true" + }, + "sendmail": { + "ensureRunning": "false", + "enabled": "false" + } + } + }, + "packages": { + "yum": { + "httpd": [], + "php": [] + } + } + } + } + } + }, + "IPAddress": { + "Type": "AWS::EC2::EIP", + "Properties": { + "InstanceId": { + "Ref": "WebServerInstance" + }, + "Domain": "vpc" + }, + "DependsOn": "AttachGateway" + }, + "AttachGateway": { + "Type": "AWS::EC2::VPCGatewayAttachment", + "Properties": { + "VpcId": { + "Ref": "VPC" + }, + "InternetGatewayId": { + "Ref": "InternetGateway" + } + } + } + }, + "Mappings": { + "RegionMap": { + "ap-southeast-1": { + "AMI": "ami-74dda626" + }, + "ap-southeast-2": { + "AMI": "ami-b3990e89" + }, + "us-west-2": { + "AMI": "ami-16fd7026" + }, + "us-east-1": { + "AMI": "ami-7f418316" + }, + "ap-northeast-1": { + "AMI": "ami-dcfa4edd" + }, + "us-west-1": { + "AMI": "ami-951945d0" + }, + "eu-west-1": { + "AMI": "ami-24506250" + }, + "sa-east-1": { + "AMI": "ami-3e3be423" + } + } + } +} diff --git a/tests/test_cloudformation/test_cloudformation_stack_crud.py b/tests/test_cloudformation/test_cloudformation_stack_crud.py new file mode 100644 index 000000000..8f47e9665 --- /dev/null +++ b/tests/test_cloudformation/test_cloudformation_stack_crud.py @@ -0,0 +1,115 @@ +import json + +import boto +import sure # noqa + +from moto import mock_cloudformation + +dummy_template = { + "AWSTemplateFormatVersion": "2010-09-09", + "Description": "Stack 1", + "Resources": {}, +} + +dummy_template2 = { + "AWSTemplateFormatVersion": "2010-09-09", + "Description": "Stack 2", + "Resources": {}, +} + +dummy_template_json = json.dumps(dummy_template) +dummy_template_json2 = json.dumps(dummy_template2) + + +@mock_cloudformation +def test_create_stack(): + conn = boto.connect_cloudformation() + conn.create_stack( + "test_stack", + template_body=dummy_template_json, + ) + + stack = conn.describe_stacks()[0] + stack.stack_name.should.equal('test_stack') + stack.get_template().should.equal(dummy_template) + + +@mock_cloudformation +def test_describe_stack_by_name(): + conn = boto.connect_cloudformation() + conn.create_stack( + "test_stack", + template_body=dummy_template_json, + ) + + stack = conn.describe_stacks("test_stack")[0] + stack.stack_name.should.equal('test_stack') + + +@mock_cloudformation +def test_get_template_by_name(): + conn = boto.connect_cloudformation() + conn.create_stack( + "test_stack", + template_body=dummy_template_json, + ) + + template = conn.get_template("test_stack") + template.should.equal(dummy_template) + + +@mock_cloudformation +def test_list_stacks(): + conn = boto.connect_cloudformation() + conn.create_stack( + "test_stack", + template_body=dummy_template_json, + ) + conn.create_stack( + "test_stack2", + template_body=dummy_template_json, + ) + + stacks = conn.list_stacks() + stacks.should.have.length_of(2) + stacks[0].template_description.should.equal("Stack 1") + + +@mock_cloudformation +def test_delete_stack_by_name(): + conn = boto.connect_cloudformation() + conn.create_stack( + "test_stack", + template_body=dummy_template_json, + ) + + conn.list_stacks().should.have.length_of(1) + conn.delete_stack("test_stack") + conn.list_stacks().should.have.length_of(0) + + +@mock_cloudformation +def test_delete_stack_by_id(): + conn = boto.connect_cloudformation() + stack_id = conn.create_stack( + "test_stack", + template_body=dummy_template_json, + ) + + conn.list_stacks().should.have.length_of(1) + conn.delete_stack(stack_id) + conn.list_stacks().should.have.length_of(0) + + +# @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", dummy_template_json2) + +# stack = conn.describe_stacks()[0] +# stack.get_template().should.equal(dummy_template2) diff --git a/tests/test_cloudformation/test_cloudformation_stack_integration.py b/tests/test_cloudformation/test_cloudformation_stack_integration.py new file mode 100644 index 000000000..8afe0a8ec --- /dev/null +++ b/tests/test_cloudformation/test_cloudformation_stack_integration.py @@ -0,0 +1,473 @@ +import json + +import boto +import sure # noqa + +from moto import ( + mock_autoscaling, + mock_cloudformation, + mock_ec2, + mock_elb, + mock_iam, +) + +from .fixtures import single_instance_with_ebs_volume, vpc_single_instance_in_subnet + + +@mock_cloudformation() +def test_stack_sqs_integration(): + sqs_template = { + "AWSTemplateFormatVersion": "2010-09-09", + "Resources": { + "QueueGroup": { + + "Type": "AWS::SQS::Queue", + "Properties": { + "QueueName": "my-queue", + "VisibilityTimeout": 60, + } + }, + }, + } + sqs_template_json = json.dumps(sqs_template) + + conn = boto.connect_cloudformation() + conn.create_stack( + "test_stack", + template_body=sqs_template_json, + ) + + stack = conn.describe_stacks()[0] + queue = stack.describe_resources()[0] + queue.resource_type.should.equal('AWS::SQS::Queue') + queue.logical_resource_id.should.equal("QueueGroup") + queue.physical_resource_id.should.equal("my-queue") + + +@mock_ec2() +@mock_cloudformation() +def test_stack_ec2_integration(): + ec2_template = { + "AWSTemplateFormatVersion": "2010-09-09", + "Resources": { + "WebServerGroup": { + "Type": "AWS::EC2::Instance", + "Properties": { + "ImageId": "ami-1234abcd", + "UserData": "some user data", + } + }, + }, + } + ec2_template_json = json.dumps(ec2_template) + + conn = boto.connect_cloudformation() + conn.create_stack( + "ec2_stack", + template_body=ec2_template_json, + ) + + ec2_conn = boto.connect_ec2() + reservation = ec2_conn.get_all_instances()[0] + ec2_instance = reservation.instances[0] + + 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.physical_resource_id.should.equal(ec2_instance.id) + + +@mock_ec2() +@mock_elb() +@mock_cloudformation() +def test_stack_elb_integration_with_attached_ec2_instances(): + elb_template = { + "AWSTemplateFormatVersion": "2010-09-09", + "Resources": { + "MyELB": { + "Type": "AWS::ElasticLoadBalancing::LoadBalancer", + "Instances": [{"Ref": "Ec2Instance1"}], + "Properties": { + "LoadBalancerName": "test-elb", + "AvailabilityZones": ['us-east1'], + } + }, + "Ec2Instance1": { + "Type": "AWS::EC2::Instance", + "Properties": { + "ImageId": "ami-1234abcd", + "UserData": "some user data", + } + }, + }, + } + elb_template_json = json.dumps(elb_template) + + conn = boto.connect_cloudformation() + conn.create_stack( + "elb_stack", + template_body=elb_template_json, + ) + + elb_conn = boto.connect_elb() + load_balancer = elb_conn.get_all_load_balancers()[0] + + ec2_conn = boto.connect_ec2() + reservation = ec2_conn.get_all_instances()[0] + ec2_instance = reservation.instances[0] + instance_id = ec2_instance.id + + load_balancer.instances[0].id.should.equal(ec2_instance.id) + list(load_balancer.availability_zones).should.equal(['us-east1']) + load_balancer_name = load_balancer.name + + stack = conn.describe_stacks()[0] + stack_resources = stack.describe_resources() + stack_resources.should.have.length_of(2) + for resource in stack_resources: + if resource.resource_type == 'AWS::ElasticLoadBalancing::LoadBalancer': + load_balancer = resource + else: + ec2_instance = resource + + load_balancer.logical_resource_id.should.equal("MyELB") + load_balancer.physical_resource_id.should.equal(load_balancer_name) + ec2_instance.physical_resource_id.should.equal(instance_id) + + +@mock_ec2() +@mock_cloudformation() +def test_stack_security_groups(): + security_group_template = { + "AWSTemplateFormatVersion": "2010-09-09", + "Resources": { + "my-security-group": { + "Type": "AWS::EC2::SecurityGroup", + "Properties": { + "GroupDescription": "My other group", + }, + }, + "Ec2Instance2": { + "Type": "AWS::EC2::Instance", + "Properties": { + "SecurityGroups": [{"Ref": "InstanceSecurityGroup"}], + "ImageId": "ami-1234abcd", + } + }, + "InstanceSecurityGroup": { + "Type": "AWS::EC2::SecurityGroup", + "Properties": { + "GroupDescription": "My security group", + "SecurityGroupIngress": [{ + "IpProtocol": "tcp", + "FromPort": "22", + "ToPort": "22", + "CidrIp": "123.123.123.123/32", + }, { + "IpProtocol": "tcp", + "FromPort": "80", + "ToPort": "8000", + "SourceSecurityGroupId": {"Ref": "my-security-group"}, + }] + } + } + }, + } + security_group_template_json = json.dumps(security_group_template) + + conn = boto.connect_cloudformation() + conn.create_stack( + "security_group_stack", + template_body=security_group_template_json, + ) + + ec2_conn = boto.connect_ec2() + security_groups = ec2_conn.get_all_security_groups() + for group in security_groups: + if group.name == "InstanceSecurityGroup": + instance_group = group + else: + other_group = group + + reservation = ec2_conn.get_all_instances()[0] + ec2_instance = reservation.instances[0] + + ec2_instance.groups[0].id.should.equal(instance_group.id) + instance_group.description.should.equal("My security group") + rule1, rule2 = instance_group.rules + int(rule1.to_port).should.equal(22) + int(rule1.from_port).should.equal(22) + rule1.grants[0].cidr_ip.should.equal("123.123.123.123/32") + rule1.ip_protocol.should.equal('tcp') + + int(rule2.to_port).should.equal(8000) + int(rule2.from_port).should.equal(80) + rule2.ip_protocol.should.equal('tcp') + rule2.grants[0].group_id.should.equal(other_group.id) + + +@mock_autoscaling() +@mock_elb() +@mock_cloudformation() +def test_autoscaling_group_with_elb(): + + web_setup_template = { + "AWSTemplateFormatVersion": "2010-09-09", + + "Resources": { + "my-as-group": { + "Type": "AWS::AutoScaling::AutoScalingGroup", + "Properties": { + "AvailabilityZones": ['us-east1'], + "LaunchConfigurationName": {"Ref": "my-launch-config"}, + "MinSize": "2", + "MaxSize": "2", + "LoadBalancerNames": [{"Ref": "my-elb"}] + }, + }, + + "my-launch-config": { + "Type": "AWS::AutoScaling::LaunchConfiguration", + "Properties": { + "ImageId": "ami-1234abcd", + "UserData": "some user data", + } + }, + + "my-elb": { + "Type": "AWS::ElasticLoadBalancing::LoadBalancer", + "Properties": { + "AvailabilityZones": ['us-east1'], + "Listeners": [{ + "LoadBalancerPort": "80", + "InstancePort": "80", + "Protocol": "HTTP" + }], + "HealthCheck": { + "Target": "80", + "HealthyThreshold": "3", + "UnhealthyThreshold": "5", + "Interval": "30", + "Timeout": "5", + }, + }, + }, + } + } + + web_setup_template_json = json.dumps(web_setup_template) + + conn = boto.connect_cloudformation() + conn.create_stack( + "web_stack", + template_body=web_setup_template_json, + ) + + 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.load_balancers[0].should.equal('my-elb') + + # Confirm the Launch config was actually created + autoscale_conn.get_all_launch_configurations().should.have.length_of(1) + + # Confirm the ELB was actually created + elb_conn = boto.connect_elb() + elb_conn.get_all_load_balancers().should.have.length_of(1) + + 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") + + 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") + + elb_resource = [resource for resource in resources if resource.resource_type == 'AWS::ElasticLoadBalancing::LoadBalancer'][0] + elb_resource.physical_resource_id.should.equal("my-elb") + + +@mock_ec2() +@mock_cloudformation() +def test_vpc_single_instance_in_subnet(): + + template_json = json.dumps(vpc_single_instance_in_subnet.template) + conn = boto.connect_cloudformation() + conn.create_stack( + "test_stack", + template_body=template_json, + ) + + vpc_conn = boto.connect_vpc() + vpc = vpc_conn.get_all_vpcs()[0] + vpc.cidr_block.should.equal("10.0.0.0/16") + + # Add this once we implement the endpoint + # vpc_conn.get_all_internet_gateways().should.have.length_of(1) + + subnet = vpc_conn.get_all_subnets()[0] + subnet.vpc_id.should.equal(vpc.id) + + ec2_conn = boto.connect_ec2() + reservation = ec2_conn.get_all_instances()[0] + instance = reservation.instances[0] + # Check that the EIP is attached the the EC2 instance + eip = ec2_conn.get_all_addresses()[0] + eip.domain.should.equal('vpc') + eip.instance_id.should.equal(instance.id) + + security_group = ec2_conn.get_all_security_groups()[0] + security_group.vpc_id.should.equal(vpc.id) + + stack = conn.describe_stacks()[0] + resources = stack.describe_resources() + vpc_resource = [resource for resource in resources if resource.resource_type == 'AWS::EC2::VPC'][0] + vpc_resource.physical_resource_id.should.equal(vpc.id) + + subnet_resource = [resource for resource in resources if resource.resource_type == 'AWS::EC2::Subnet'][0] + subnet_resource.physical_resource_id.should.equal(subnet.id) + + eip_resource = [resource for resource in resources if resource.resource_type == 'AWS::EC2::EIP'][0] + eip_resource.physical_resource_id.should.equal(eip.allocation_id) + + +@mock_autoscaling() +@mock_iam() +@mock_cloudformation() +def test_iam_roles(): + iam_template = { + "AWSTemplateFormatVersion": "2010-09-09", + + "Resources": { + + "my-launch-config": { + "Properties": { + "IamInstanceProfile": {"Ref": "my-instance-profile"}, + "ImageId": "ami-1234abcd", + }, + "Type": "AWS::AutoScaling::LaunchConfiguration" + }, + "my-instance-profile": { + "Properties": { + "Path": "my-path", + "Roles": [{"Ref": "my-role"}], + }, + "Type": "AWS::IAM::InstanceProfile" + }, + "my-role": { + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": [ + "sts:AssumeRole" + ], + "Effect": "Allow", + "Principal": { + "Service": [ + "ec2.amazonaws.com" + ] + } + } + ] + }, + "Path": "my-path", + "Policies": [ + { + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "ec2:CreateTags", + "ec2:DescribeInstances", + "ec2:DescribeTags" + ], + "Effect": "Allow", + "Resource": [ + "*" + ] + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "EC2_Tags" + }, + { + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "sqs:*" + ], + "Effect": "Allow", + "Resource": [ + "*" + ] + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "SQS" + }, + ] + }, + "Type": "AWS::IAM::Role" + } + } + } + + iam_template_json = json.dumps(iam_template) + conn = boto.connect_cloudformation() + conn.create_stack( + "test_stack", + template_body=iam_template_json, + ) + + iam_conn = boto.connect_iam() + + role = iam_conn.get_role("my-role") + role.role_name.should.equal("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.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") + + stack = conn.describe_stacks()[0] + resources = stack.describe_resources() + instance_profile_resource = [resource for resource in resources if resource.resource_type == 'AWS::IAM::InstanceProfile'][0] + instance_profile_resource.physical_resource_id.should.equal(instance_profile.instance_profile_name) + + role_resource = [resource for resource in resources if resource.resource_type == 'AWS::IAM::Role'][0] + role_resource.physical_resource_id.should.equal(role.role_id) + + +@mock_ec2() +@mock_cloudformation() +def test_single_instance_with_ebs_volume(): + + template_json = json.dumps(single_instance_with_ebs_volume.template) + conn = boto.connect_cloudformation() + conn.create_stack( + "test_stack", + template_body=template_json, + ) + + ec2_conn = boto.connect_ec2() + reservation = ec2_conn.get_all_instances()[0] + ec2_instance = reservation.instances[0] + + volume = ec2_conn.get_all_volumes()[0] + volume.volume_state().should.equal('in-use') + volume.attach_data.instance_id.should.equal(ec2_instance.id) + + stack = conn.describe_stacks()[0] + 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) diff --git a/tests/test_cloudformation/test_server.py b/tests/test_cloudformation/test_server.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/test_cloudformation/test_stack_parsing.py b/tests/test_cloudformation/test_stack_parsing.py new file mode 100644 index 000000000..f517eb153 --- /dev/null +++ b/tests/test_cloudformation/test_stack_parsing.py @@ -0,0 +1,47 @@ +import json + +from mock import patch +import sure # noqa + +from moto.cloudformation.models import FakeStack +from moto.cloudformation.parsing import resource_class_from_type +from moto.sqs.models import Queue + +dummy_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": { + "QueueName": "my-queue", + "VisibilityTimeout": 60, + } + }, + }, +} + +dummy_template_json = json.dumps(dummy_template) + + +def test_parse_stack_resources(): + stack = FakeStack( + stack_id="test_id", + name="test_stack", + template=dummy_template_json, + ) + + stack.resource_map.should.have.length_of(1) + stack.resource_map.keys()[0].should.equal('WebServerGroup') + queue = stack.resource_map.values()[0] + queue.should.be.a(Queue) + queue.name.should.equal("my-queue") + + +@patch("moto.cloudformation.parsing.logger") +def test_missing_resource_logs(logger): + resource_class_from_type("foobar") + logger.warning.assert_called_with('No Moto CloudFormation support for %s', 'foobar') diff --git a/tests/test_iam/test_iam.py b/tests/test_iam/test_iam.py new file mode 100644 index 000000000..73d548e64 --- /dev/null +++ b/tests/test_iam/test_iam.py @@ -0,0 +1,27 @@ +import boto + +import sure # noqa + +from moto import mock_iam + + +@mock_iam() +def test_create_role_and_instance_profile(): + conn = boto.connect_iam() + conn.create_instance_profile("my-profile", path="my-path") + conn.create_role("my-role", assume_role_policy_document="some policy", path="my-path") + + conn.add_role_to_instance_profile("my-profile", "my-role") + + role = conn.get_role("my-role") + role.path.should.equal("my-path") + role.assume_role_policy_document.should.equal("some policy") + + profile = conn.get_instance_profile("my-profile") + profile.path.should.equal("my-path") + role_from_profile = profile.roles.values()[0] + role_from_profile['role_id'].should.equal(role.role_id) + role_from_profile['role_name'].should.equal("my-role") + + conn.list_roles().roles[0].role_name.should.equal('my-role') + conn.list_instance_profiles().instance_profiles[0].instance_profile_name.should.equal("my-profile")