Fix merge conflicts. Add basic cloudformation support. Closes #111.

This commit is contained in:
Steve Pulec 2014-03-27 19:12:53 -04:00
parent 069c48b43a
commit ef876dd27e
28 changed files with 2473 additions and 11 deletions

View File

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

View File

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

View File

@ -0,0 +1,2 @@
from .models import cloudformation_backend
mock_cloudformation = cloudformation_backend.decorator

View File

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

View File

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

View File

@ -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 = """<DescribeStacksResult>
<Stacks>
{% for stack in stacks %}
<member>
<StackName>{{ stack.name }}</StackName>
<StackId>{{ stack.stack_id }}</StackId>
<CreationTime>2010-07-27T22:28:28Z</CreationTime>
<StackStatus>CREATE_COMPLETE</StackStatus>
<DisableRollback>false</DisableRollback>
<Outputs></Outputs>
</member>
{% endfor %}
</Stacks>
</DescribeStacksResult>"""
LIST_STACKS_RESPONSE = """<ListStacksResponse>
<ListStacksResult>
<StackSummaries>
{% for stack in stacks %}
<member>
<StackId>{{ stack.id }}</StackId>
<StackStatus>CREATE_IN_PROGRESS</StackStatus>
<StackName>{{ stack.name }}</StackName>
<CreationTime>2011-05-23T15:47:44Z</CreationTime>
<TemplateDescription>{{ stack.description }}</TemplateDescription>
</member>
{% endfor %}
</StackSummaries>
</ListStacksResult>
</ListStacksResponse>"""
LIST_STACKS_RESOURCES_RESPONSE = """<DescribeStackResourcesResult>
<StackResources>
{% for resource in stack.stack_resources %}
<member>
<StackId>{{ stack.stack_id }}</StackId>
<StackName>{{ stack.name }}</StackName>
<LogicalResourceId>{{ resource.logical_resource_id }}</LogicalResourceId>
<PhysicalResourceId>{{ resource.physical_resource_id }}</PhysicalResourceId>
<ResourceType>{{ resource.type }}</ResourceType>
<Timestamp>2010-07-27T22:27:28Z</Timestamp>
<ResourceStatus>CREATE_COMPLETE</ResourceStatus>
</member>
{% endfor %}
</StackResources>
</DescribeStackResourcesResult>"""

View File

@ -0,0 +1,9 @@
from .responses import CloudFormationResponse
url_bases = [
"https?://cloudformation.(.+).amazonaws.com",
]
url_paths = {
'{0}/$': CloudFormationResponse().dispatch,
}

View File

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

View File

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

View File

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

View File

@ -113,6 +113,9 @@ DESCRIBE_ADDRESS_RESPONSE = """<DescribeAddressesResponse xmlns="http://ec2.amaz
{% else %}
<instanceId/>
{% endif %}
{% if address.allocation_id %}
<allocationId>{{ address.allocation_id }}</allocationId>
{% endif %}
{% if address.association_id %}
<associationId>{{ address.association_id }}</associationId>
{% endif %}

View File

@ -50,6 +50,14 @@ def random_eip_association_id():
return random_id(prefix='eipassoc')
def random_gateway_id():
return random_id(prefix='igw')
def random_route_table_id():
return random_id(prefix='rtb')
def random_eip_allocation_id():
return random_id(prefix='eipalloc')
@ -123,8 +131,6 @@ def keypair_names_from_querystring(querystring_dict):
keypair_names.append(value[0])
return keypair_names
filter_dict_attribute_mapping = {
'instance-state-name': 'state'
}

View File

@ -33,6 +33,25 @@ class FakeLoadBalancer(object):
)
self.listeners.append(listener)
@classmethod
def create_from_cloudformation_json(cls, resource_name, cloudformation_json):
properties = cloudformation_json['Properties']
new_elb = elb_backend.create_load_balancer(
name=properties.get('LoadBalancerName', resource_name),
zones=properties.get('AvailabilityZones'),
ports=[],
)
instance_ids = cloudformation_json.get('Instances', [])
for instance_id in instance_ids:
elb_backend.register_instances(new_elb.name, [instance_id])
return new_elb
@property
def physical_resource_id(self):
return self.name
class ELBBackend(BaseBackend):

2
moto/iam/__init__.py Normal file
View File

@ -0,0 +1,2 @@
from .models import iam_backend
mock_iam = iam_backend.decorator

99
moto/iam/models.py Normal file
View File

@ -0,0 +1,99 @@
from moto.core import BaseBackend
from .utils import random_resource_id
class Role(object):
def __init__(self, role_id, name, assume_role_policy_document, path, policies):
self.id = role_id
self.name = name
self.assume_role_policy_document = assume_role_policy_document
self.path = path
self.policies = policies
@classmethod
def create_from_cloudformation_json(cls, resource_name, cloudformation_json):
properties = cloudformation_json['Properties']
return iam_backend.create_role(
role_name=resource_name,
assume_role_policy_document=properties['AssumeRolePolicyDocument'],
path=properties['Path'],
policies=properties.get('Policies', []),
)
@property
def physical_resource_id(self):
return self.id
class InstanceProfile(object):
def __init__(self, instance_profile_id, name, path, roles):
self.id = instance_profile_id
self.name = name
self.path = path
self.roles = roles if roles else []
@classmethod
def create_from_cloudformation_json(cls, resource_name, cloudformation_json):
properties = cloudformation_json['Properties']
role_ids = properties['Roles']
return iam_backend.create_instance_profile(
name=resource_name,
path=properties['Path'],
role_ids=role_ids,
)
@property
def physical_resource_id(self):
return self.name
class IAMBackend(BaseBackend):
def __init__(self):
self.instance_profiles = {}
self.roles = {}
super(IAMBackend, self).__init__()
def create_role(self, role_name, assume_role_policy_document, path, policies):
role_id = random_resource_id()
role = Role(role_id, role_name, assume_role_policy_document, path, policies)
self.roles[role_id] = role
return role
def get_role_by_id(self, role_id):
return self.roles.get(role_id)
def get_role(self, role_name):
for role in self.get_roles():
if role.name == role_name:
return role
def get_roles(self):
return self.roles.values()
def create_instance_profile(self, name, path, role_ids):
instance_profile_id = random_resource_id()
roles = [iam_backend.get_role_by_id(role_id) for role_id in role_ids]
instance_profile = InstanceProfile(instance_profile_id, name, path, roles)
self.instance_profiles[instance_profile_id] = instance_profile
return instance_profile
def get_instance_profile(self, profile_name):
for profile in self.get_instance_profiles():
if profile.name == profile_name:
return profile
def get_instance_profiles(self):
return self.instance_profiles.values()
def add_role_to_instance_profile(self, profile_name, role_name):
profile = self.get_instance_profile(profile_name)
role = self.get_role(role_name)
profile.roles.append(role)
iam_backend = IAMBackend()

184
moto/iam/responses.py Normal file
View File

@ -0,0 +1,184 @@
from jinja2 import Template
from moto.core.responses import BaseResponse
from .models import iam_backend
class IamResponse(BaseResponse):
def _get_param(self, param_name):
return self.querystring.get(param_name, [None])[0]
def create_role(self):
role_name = self._get_param('RoleName')
path = self._get_param('Path')
assume_role_policy_document = self._get_param('AssumeRolePolicyDocument')
role = iam_backend.create_role(role_name, assume_role_policy_document, path, policies=[])
template = Template(CREATE_ROLE_TEMPLATE)
return template.render(role=role)
def get_role(self):
role_name = self._get_param('RoleName')
role = iam_backend.get_role(role_name)
template = Template(GET_ROLE_TEMPLATE)
return template.render(role=role)
def create_instance_profile(self):
profile_name = self._get_param('InstanceProfileName')
path = self._get_param('Path')
profile = iam_backend.create_instance_profile(profile_name, path, role_ids=[])
template = Template(CREATE_INSTANCE_PROFILE_TEMPLATE)
return template.render(profile=profile)
def get_instance_profile(self):
profile_name = self._get_param('InstanceProfileName')
profile = iam_backend.get_instance_profile(profile_name)
template = Template(GET_INSTANCE_PROFILE_TEMPLATE)
return template.render(profile=profile)
def add_role_to_instance_profile(self):
profile_name = self._get_param('InstanceProfileName')
role_name = self._get_param('RoleName')
iam_backend.add_role_to_instance_profile(profile_name, role_name)
template = Template(ADD_ROLE_TO_INSTANCE_PROFILE_TEMPLATE)
return template.render()
def list_roles(self):
roles = iam_backend.get_roles()
template = Template(LIST_ROLES_TEMPLATE)
return template.render(roles=roles)
def list_instance_profiles(self):
profiles = iam_backend.get_instance_profiles()
template = Template(LIST_INSTANCE_PROFILES_TEMPLATE)
return template.render(instance_profiles=profiles)
CREATE_INSTANCE_PROFILE_TEMPLATE = """<CreateInstanceProfileResponse xmlns="https://iam.amazonaws.com/doc/2010-05-08/">
<CreateInstanceProfileResult>
<InstanceProfile>
<InstanceProfileId>{{ profile.id }}</InstanceProfileId>
<Roles/>
<InstanceProfileName>{{ profile.name }}</InstanceProfileName>
<Path>{{ profile.path }}</Path>
<Arn>arn:aws:iam::123456789012:instance-profile/application_abc/component_xyz/Webserver</Arn>
<CreateDate>2012-05-09T16:11:10.222Z</CreateDate>
</InstanceProfile>
</CreateInstanceProfileResult>
<ResponseMetadata>
<RequestId>974142ee-99f1-11e1-a4c3-27EXAMPLE804</RequestId>
</ResponseMetadata>
</CreateInstanceProfileResponse>"""
GET_INSTANCE_PROFILE_TEMPLATE = """<GetInstanceProfileResponse xmlns="https://iam.amazonaws.com/doc/2010-05-08/">
<GetInstanceProfileResult>
<InstanceProfile>
<InstanceProfileId>{{ profile.id }}</InstanceProfileId>
<Roles>
{% for role in profile.roles %}
<member>
<Path>{{ role.path }}</Path>
<Arn>arn:aws:iam::123456789012:role/application_abc/component_xyz/S3Access</Arn>
<RoleName>{{ role.name }}</RoleName>
<AssumeRolePolicyDocument>{{ role.assume_role_policy_document }}</AssumeRolePolicyDocument>
<CreateDate>2012-05-09T15:45:35Z</CreateDate>
<RoleId>{{ role.id }}</RoleId>
</member>
{% endfor %}
</Roles>
<InstanceProfileName>{{ profile.name }}</InstanceProfileName>
<Path>{{ profile.path }}</Path>
<Arn>arn:aws:iam::123456789012:instance-profile/application_abc/component_xyz/Webserver</Arn>
<CreateDate>2012-05-09T16:11:10Z</CreateDate>
</InstanceProfile>
</GetInstanceProfileResult>
<ResponseMetadata>
<RequestId>37289fda-99f2-11e1-a4c3-27EXAMPLE804</RequestId>
</ResponseMetadata>
</GetInstanceProfileResponse>"""
CREATE_ROLE_TEMPLATE = """<CreateRoleResponse xmlns="https://iam.amazonaws.com/doc/2010-05-08/">
<CreateRoleResult>
<Role>
<Path>{{ role.path }}</Path>
<Arn>arn:aws:iam::123456789012:role/application_abc/component_xyz/S3Access</Arn>
<RoleName>{{ role.name }}</RoleName>
<AssumeRolePolicyDocument>{{ role.assume_role_policy_document }}</AssumeRolePolicyDocument>
<CreateDate>2012-05-08T23:34:01.495Z</CreateDate>
<RoleId>{{ role.id }}</RoleId>
</Role>
</CreateRoleResult>
<ResponseMetadata>
<RequestId>4a93ceee-9966-11e1-b624-b1aEXAMPLE7c</RequestId>
</ResponseMetadata>
</CreateRoleResponse>"""
GET_ROLE_TEMPLATE = """<GetRoleResponse xmlns="https://iam.amazonaws.com/doc/2010-05-08/">
<GetRoleResult>
<Role>
<Path>{{ role.path }}</Path>
<Arn>arn:aws:iam::123456789012:role/application_abc/component_xyz/S3Access</Arn>
<RoleName>{{ role.name }}</RoleName>
<AssumeRolePolicyDocument>{{ role.assume_role_policy_document }}</AssumeRolePolicyDocument>
<CreateDate>2012-05-08T23:34:01Z</CreateDate>
<RoleId>{{ role.id }}</RoleId>
</Role>
</GetRoleResult>
<ResponseMetadata>
<RequestId>df37e965-9967-11e1-a4c3-270EXAMPLE04</RequestId>
</ResponseMetadata>
</GetRoleResponse>"""
ADD_ROLE_TO_INSTANCE_PROFILE_TEMPLATE = """<AddRoleToInstanceProfileResponse xmlns="https://iam.amazonaws.com/doc/2010-05-08/">
<ResponseMetadata>
<RequestId>12657608-99f2-11e1-a4c3-27EXAMPLE804</RequestId>
</ResponseMetadata>
</AddRoleToInstanceProfileResponse>"""
LIST_ROLES_TEMPLATE = """<ListRolesResponse xmlns="https://iam.amazonaws.com/doc/2010-05-08/">
<ListRolesResult>
<IsTruncated>false</IsTruncated>
<Roles>
{% for role in roles %}
<member>
<Path>{{ role.path }}</Path>
<Arn>arn:aws:iam::123456789012:role/application_abc/component_xyz/S3Access</Arn>
<RoleName>{{ role.name }}</RoleName>
<AssumeRolePolicyDocument>{{ role.assume_role_policy_document }}</AssumeRolePolicyDocument>
<CreateDate>2012-05-09T15:45:35Z</CreateDate>
<RoleId>{{ role.id }}</RoleId>
</member>
{% endfor %}
</Roles>
</ListRolesResult>
<ResponseMetadata>
<RequestId>20f7279f-99ee-11e1-a4c3-27EXAMPLE804</RequestId>
</ResponseMetadata>
</ListRolesResponse>"""
LIST_INSTANCE_PROFILES_TEMPLATE = """<ListInstanceProfilesResponse xmlns="https://iam.amazonaws.com/doc/2010-05-08/">
<ListInstanceProfilesResult>
<IsTruncated>false</IsTruncated>
<InstanceProfiles>
{% for instance in instance_profiles %}
<member>
<Id>{{ instance.id }}</Id>
<Roles/>
<InstanceProfileName>{{ instance.name }}</InstanceProfileName>
<Path>{{ instance.path }}</Path>
<Arn>arn:aws:iam::123456789012:instance-profile/application_abc/component_xyz/Database</Arn>
<CreateDate>2012-05-09T16:27:03Z</CreateDate>
</member>
{% endfor %}
</InstanceProfiles>
</ListInstanceProfilesResult>
<ResponseMetadata>
<RequestId>fd74fa8d-99f3-11e1-a4c3-27EXAMPLE804</RequestId>
</ResponseMetadata>
</ListInstanceProfilesResponse>"""

9
moto/iam/urls.py Normal file
View File

@ -0,0 +1,9 @@
from .responses import IamResponse
url_bases = [
"https?://iam.amazonaws.com",
]
url_paths = {
'{0}/$': IamResponse().dispatch,
}

9
moto/iam/utils.py Normal file
View File

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

View File

@ -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 = {}

View File

View File

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

View File

@ -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": [
"",
[
"<?php\n",
"echo '<h1>AWS CloudFormation sample PHP application</h1>';\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"
}
}
}
}

View File

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

View File

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

View File

View File

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

View File

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