Merge branch 'master' of https://github.com/spulec/moto into 0.4.1-threadsafe

* 'master' of https://github.com/spulec/moto: (25 commits)
  Add @zkourouma to authors.
  0.4.2
  Fix bug where listener certificate was not being saved correctly when creating an elb. Added test to cover that case.
  [dynamodb2] adds lookup method to Table class
  Add IAM list_groups and list_groups_for_user. Closes #343.
  Fix for deleting Route53 record sets with set identifiers. Closes #342.
  Use dummy date instead of an invalid date
  Adding support for comments on hosted zones.
  Add availability zone support to Subnets created via CloudFormation
  Make availability zone dynamic in Subnet Response templates
  Add filter "availabilityZone" to DescribeSubnets and add availability zone support too
  allow starting without reseting
  Fix bug with empty string for instance vpc_id. Closes #337.
  Fix default security group description.
  Update responses.py
  Add @mrucci to authors.
  Fix merge conflicts.
  Add support for ELB attributes.
  cast to int when doing math.
  General cleanup.
  ...
This commit is contained in:
Jeffrey Gelens 2015-05-29 11:35:14 +02:00
commit e722b67f36
33 changed files with 1381 additions and 167 deletions

View file

@ -186,7 +186,7 @@ class FakeAutoScalingGroup(object):
if self.desired_capacity > curr_instance_count:
# Need more instances
count_needed = self.desired_capacity - curr_instance_count
count_needed = int(self.desired_capacity) - int(curr_instance_count)
reservation = self.autoscaling_backend.ec2_backend.add_instances(
self.launch_config.image_id,
count_needed,

View file

@ -29,10 +29,11 @@ class MockAWS(object):
def __exit__(self, *args):
self.stop()
def start(self):
def start(self, reset=True):
self.__class__.nested_count += 1
for backend in self.backends.values():
backend.reset()
if reset:
for backend in self.backends.values():
backend.reset()
if not HTTPretty.is_enabled():
HTTPretty.enable()

View file

@ -122,7 +122,7 @@ class Item(object):
class Table(object):
def __init__(self, table_name, schema=None, attr=None, throughput=None, indexes=None):
def __init__(self, table_name, schema=None, attr=None, throughput=None, indexes=None, global_indexes=None):
self.name = table_name
self.attr = attr
self.schema = schema
@ -143,6 +143,7 @@ class Table(object):
self.throughput = throughput
self.throughput["NumberOfDecreasesToday"] = 0
self.indexes = indexes
self.global_indexes = global_indexes if global_indexes else []
self.created_at = datetime.datetime.now()
self.items = defaultdict(dict)
@ -158,6 +159,7 @@ class Table(object):
'KeySchema': self.schema,
'ItemCount': len(self),
'CreationDateTime': unix_time(self.created_at),
'GlobalSecondaryIndexes': [index for index in self.global_indexes],
}
}
return results
@ -171,6 +173,24 @@ class Table(object):
count += 1
return count
@property
def hash_key_names(self):
keys = [self.hash_key_attr]
for index in self.global_indexes:
for key in index['KeySchema']:
if key['KeyType'] == 'HASH':
keys.append(key['AttributeName'])
return keys
@property
def range_key_names(self):
keys = [self.range_key_attr]
for index in self.global_indexes:
for key in index['KeySchema']:
if key['KeyType'] == 'RANGE':
keys.append(key['AttributeName'])
return keys
def put_item(self, item_attrs):
hash_value = DynamoType(item_attrs.get(self.hash_key_attr))
if self.has_range_key:
@ -268,6 +288,16 @@ class Table(object):
results.append(result)
return results, scanned_count, last_page
def lookup(self, *args, **kwargs):
if not self.schema:
self.describe()
for x, arg in enumerate(args):
kwargs[self.schema[x].name] = arg
ret = self.get_item(**kwargs)
if not ret.keys():
return None
return ret
class DynamoDBBackend(BaseBackend):
@ -293,12 +323,21 @@ class DynamoDBBackend(BaseBackend):
return None
return table.put_item(item_attrs)
def get_table_keys_name(self, table_name):
def get_table_keys_name(self, table_name, keys):
"""
Given a set of keys, extracts the key and range key
"""
table = self.tables.get(table_name)
if not table:
return None, None
else:
return table.hash_key_attr, table.range_key_attr
hash_key = range_key = None
for key in keys:
if key in table.hash_key_names:
hash_key = key
elif key in table.range_key_names:
range_key = key
return hash_key, range_key
def get_keys_value(self, table, keys):
if table.hash_key_attr not in keys or (table.has_range_key and table.range_key_attr not in keys):

View file

@ -99,10 +99,12 @@ class DynamoHandler(BaseResponse):
# getting attribute definition
attr = body["AttributeDefinitions"]
# getting the indexes
global_indexes = body.get("GlobalSecondaryIndexes", [])
table = dynamodb_backend2.create_table(table_name,
schema=key_schema,
throughput=throughput,
attr=attr)
attr=attr,
global_indexes=global_indexes)
return dynamo_json_dump(table.describe)
def delete_table(self):
@ -216,13 +218,14 @@ class DynamoHandler(BaseResponse):
def query(self):
name = self.body['TableName']
keys = self.body['KeyConditions']
hash_key_name, range_key_name = dynamodb_backend2.get_table_keys_name(name)
key_conditions = self.body['KeyConditions']
hash_key_name, range_key_name = dynamodb_backend2.get_table_keys_name(name, key_conditions.keys())
# hash_key_name, range_key_name = dynamodb_backend2.get_table_keys_name(name)
if hash_key_name is None:
er = "'com.amazonaws.dynamodb.v20120810#ResourceNotFoundException"
return self.error(er)
hash_key = keys[hash_key_name]['AttributeValueList'][0]
if len(keys) == 1:
hash_key = key_conditions[hash_key_name]['AttributeValueList'][0]
if len(key_conditions) == 1:
range_comparison = None
range_values = []
else:
@ -230,7 +233,7 @@ class DynamoHandler(BaseResponse):
er = "com.amazon.coral.validate#ValidationException"
return self.error(er)
else:
range_condition = keys[range_key_name]
range_condition = key_conditions[range_key_name]
if range_condition:
range_comparison = range_condition['ComparisonOperator']
range_values = range_condition['AttributeValueList']

View file

@ -1149,6 +1149,10 @@ class SecurityGroupBackend(object):
def __init__(self):
# the key in the dict group is the vpc_id or None (non-vpc)
self.groups = defaultdict(dict)
# Create the default security group
self.create_security_group("default", "default group")
super(SecurityGroupBackend, self).__init__()
def create_security_group(self, name, description, vpc_id=None, force=False):
@ -1212,11 +1216,6 @@ class SecurityGroupBackend(object):
if group.name == name:
return group
if name == 'default':
# If the request is for the default group and it does not exist, create it
default_group = self.create_security_group("default", "The default security group", vpc_id=vpc_id, force=True)
return default_group
def get_security_group_by_name_or_id(self, group_name_or_id, vpc_id):
# try searching by id, fallbacks to name search
group = self.get_security_group_from_id(group_name_or_id)
@ -1309,7 +1308,7 @@ class SecurityGroupIngress(object):
from_port = properties.get("FromPort")
source_security_group_id = properties.get("SourceSecurityGroupId")
source_security_group_name = properties.get("SourceSecurityGroupName")
source_security_owner_id = properties.get("SourceSecurityGroupOwnerId") # IGNORED AT THE MOMENT
# source_security_owner_id = properties.get("SourceSecurityGroupOwnerId") # IGNORED AT THE MOMENT
to_port = properties.get("ToPort")
assert group_id or group_name
@ -1329,7 +1328,6 @@ class SecurityGroupIngress(object):
else:
ip_ranges = []
if group_id:
security_group = ec2_backend.describe_security_groups(group_ids=[group_id])[0]
else:
@ -1697,41 +1695,66 @@ class VPCPeeringConnectionBackend(object):
class Subnet(TaggedEC2Resource):
def __init__(self, ec2_backend, subnet_id, vpc_id, cidr_block):
def __init__(self, ec2_backend, subnet_id, vpc_id, cidr_block, availability_zone):
self.ec2_backend = ec2_backend
self.id = subnet_id
self.vpc_id = vpc_id
self.cidr_block = cidr_block
self._availability_zone = availability_zone
@classmethod
def create_from_cloudformation_json(cls, resource_name, cloudformation_json, region_name):
properties = cloudformation_json['Properties']
vpc_id = properties['VpcId']
cidr_block = properties['CidrBlock']
availability_zone = properties.get('AvailabilityZone')
ec2_backend = ec2_backends[region_name]
subnet = ec2_backend.create_subnet(
vpc_id=vpc_id,
cidr_block=properties['CidrBlock']
cidr_block=cidr_block,
availability_zone=availability_zone,
)
return subnet
@property
def availability_zone(self):
# This could probably be smarter, but there doesn't appear to be a
# way to pull AZs for a region in boto
return self.ec2_backend.region_name + "a"
if self._availability_zone is None:
# This could probably be smarter, but there doesn't appear to be a
# way to pull AZs for a region in boto
return self.ec2_backend.region_name + "a"
else:
return self._availability_zone
@property
def physical_resource_id(self):
return self.id
def get_filter_value(self, filter_name):
"""
API Version 2014-10-01 defines the following filters for DescribeSubnets:
* availabilityZone
* available-ip-address-count
* cidrBlock
* defaultForAz
* state
* subnet-id
* tag:key=value
* tag-key
* tag-value
* vpc-id
Taken from: http://docs.aws.amazon.com/AWSEC2/latest/APIReference/API_DescribeSubnets.html
"""
if filter_name in ['cidr', 'cidrBlock', 'cidr-block']:
return self.cidr_block
elif filter_name == 'vpc-id':
return self.vpc_id
elif filter_name == 'subnet-id':
return self.id
elif filter_name == 'availabilityZone':
return self.availability_zone
filter_value = super(Subnet, self).get_filter_value(filter_name)
@ -1758,9 +1781,9 @@ class SubnetBackend(object):
raise InvalidSubnetIdError(subnet_id)
return subnet
def create_subnet(self, vpc_id, cidr_block):
def create_subnet(self, vpc_id, cidr_block, availability_zone=None):
subnet_id = random_subnet_id()
subnet = Subnet(self, subnet_id, vpc_id, cidr_block)
subnet = Subnet(self, subnet_id, vpc_id, cidr_block, availability_zone)
self.get_vpc(vpc_id) # Validate VPC exists
# AWS associates a new subnet with the default Network ACL

View file

@ -103,7 +103,7 @@ CREATE_VOLUME_RESPONSE = """<CreateVolumeResponse xmlns="http://ec2.amazonaws.co
<snapshotId/>
<availabilityZone>{{ volume.zone.name }}</availabilityZone>
<status>creating</status>
<createTime>YYYY-MM-DDTHH:MM:SS.000Z</createTime>
<createTime>2013-10-04T17:38:53.000Z</createTime>
<volumeType>standard</volumeType>
</CreateVolumeResponse>"""
@ -117,7 +117,7 @@ DESCRIBE_VOLUMES_RESPONSE = """<DescribeVolumesResponse xmlns="http://ec2.amazon
<snapshotId/>
<availabilityZone>{{ volume.zone.name }}</availabilityZone>
<status>{{ volume.status }}</status>
<createTime>YYYY-MM-DDTHH:MM:SS.SSSZ</createTime>
<createTime>2013-10-04T17:38:53.000Z</createTime>
<attachmentSet>
{% if volume.attachment %}
<item>
@ -125,7 +125,7 @@ DESCRIBE_VOLUMES_RESPONSE = """<DescribeVolumesResponse xmlns="http://ec2.amazon
<instanceId>{{ volume.attachment.instance.id }}</instanceId>
<device>{{ volume.attachment.device }}</device>
<status>attached</status>
<attachTime>YYYY-MM-DDTHH:MM:SS.SSSZ</attachTime>
<attachTime>2013-10-04T17:38:53.000Z</attachTime>
<deleteOnTermination>false</deleteOnTermination>
</item>
{% endif %}
@ -157,7 +157,7 @@ ATTACHED_VOLUME_RESPONSE = """<AttachVolumeResponse xmlns="http://ec2.amazonaws.
<instanceId>{{ attachment.instance.id }}</instanceId>
<device>{{ attachment.device }}</device>
<status>attaching</status>
<attachTime>YYYY-MM-DDTHH:MM:SS.000Z</attachTime>
<attachTime>2013-10-04T17:38:53.000Z</attachTime>
</AttachVolumeResponse>"""
DETATCH_VOLUME_RESPONSE = """<DetachVolumeResponse xmlns="http://ec2.amazonaws.com/doc/2012-12-01/">
@ -166,7 +166,7 @@ DETATCH_VOLUME_RESPONSE = """<DetachVolumeResponse xmlns="http://ec2.amazonaws.c
<instanceId>{{ attachment.instance.id }}</instanceId>
<device>{{ attachment.device }}</device>
<status>detaching</status>
<attachTime>YYYY-MM-DDTHH:MM:SS.000Z</attachTime>
<attachTime>2013-10-04T17:38:53.000Z</attachTime>
</DetachVolumeResponse>"""
CREATE_SNAPSHOT_RESPONSE = """<CreateSnapshotResponse xmlns="http://ec2.amazonaws.com/doc/2012-12-01/">
@ -174,7 +174,7 @@ CREATE_SNAPSHOT_RESPONSE = """<CreateSnapshotResponse xmlns="http://ec2.amazonaw
<snapshotId>{{ snapshot.id }}</snapshotId>
<volumeId>{{ snapshot.volume.id }}</volumeId>
<status>pending</status>
<startTime>YYYY-MM-DDTHH:MM:SS.000Z</startTime>
<startTime>2013-10-04T17:38:53.000Z</startTime>
<progress>60%</progress>
<ownerId>111122223333</ownerId>
<volumeSize>{{ snapshot.volume.size }}</volumeSize>
@ -189,7 +189,7 @@ DESCRIBE_SNAPSHOTS_RESPONSE = """<DescribeSnapshotsResponse xmlns="http://ec2.am
<snapshotId>{{ snapshot.id }}</snapshotId>
<volumeId>{{ snapshot.volume.id }}</volumeId>
<status>pending</status>
<startTime>YYYY-MM-DDTHH:MM:SS.SSSZ</startTime>
<startTime>2013-10-04T17:38:53.000Z</startTime>
<progress>30%</progress>
<ownerId>111122223333</ownerId>
<volumeSize>{{ snapshot.volume.size }}</volumeSize>

View file

@ -214,8 +214,10 @@ EC2_RUN_INSTANCES = """<RunInstancesResponse xmlns="http://ec2.amazonaws.com/doc
<state>enabled</state>
</monitoring>
{% if instance.nics %}
<subnetId>{{ instance.nics[0].subnet.id }}</subnetId>
<vpcId>{{ instance.nics[0].subnet.vpc_id }}</vpcId>
{% if instance.nics[0].subnet %}
<subnetId>{{ instance.nics[0].subnet.id }}</subnetId>
<vpcId>{{ instance.nics[0].subnet.vpc_id }}</vpcId>
{% endif %}
<privateIpAddress>{{ instance.private_ip }}</privateIpAddress>
{% if instance.public_ip %}
<ipAddress>{{ instance.public_ip }}</ipAddress>
@ -245,8 +247,10 @@ EC2_RUN_INSTANCES = """<RunInstancesResponse xmlns="http://ec2.amazonaws.com/doc
{% for nic in instance.nics.values() %}
<item>
<networkInterfaceId>{{ nic.id }}</networkInterfaceId>
<subnetId>{{ nic.subnet.id }}</subnetId>
<vpcId>{{ nic.subnet.vpc_id }}</vpcId>
{% if nic.subnet %}
<subnetId>{{ nic.subnet.id }}</subnetId>
<vpcId>{{ nic.subnet.vpc_id }}</vpcId>
{% endif %}
<description>Primary network interface</description>
<ownerId>111122223333</ownerId>
<status>in-use</status>
@ -338,8 +342,10 @@ EC2_DESCRIBE_INSTANCES = """<DescribeInstancesResponse xmlns='http://ec2.amazona
<state>disabled</state>
</monitoring>
{% if instance.nics %}
<subnetId>{{ instance.nics[0].subnet.id }}</subnetId>
<vpcId>{{ instance.nics[0].subnet.vpc_id }}</vpcId>
{% if instance.nics[0].subnet %}
<subnetId>{{ instance.nics[0].subnet.id }}</subnetId>
<vpcId>{{ instance.nics[0].subnet.vpc_id }}</vpcId>
{% endif %}
<privateIpAddress>{{ instance.private_ip }}</privateIpAddress>
{% if instance.nics[0].public_ip %}
<ipAddress>{{ instance.nics[0].public_ip }}</ipAddress>
@ -390,8 +396,10 @@ EC2_DESCRIBE_INSTANCES = """<DescribeInstancesResponse xmlns='http://ec2.amazona
{% for nic in instance.nics.values() %}
<item>
<networkInterfaceId>{{ nic.id }}</networkInterfaceId>
<subnetId>{{ nic.subnet.id }}</subnetId>
<vpcId>{{ nic.subnet.vpc_id }}</vpcId>
{% if nic.subnet %}
<subnetId>{{ nic.subnet.id }}</subnetId>
<vpcId>{{ nic.subnet.vpc_id }}</vpcId>
{% endif %}
<description>Primary network interface</description>
<ownerId>111122223333</ownerId>
<status>in-use</status>

View file

@ -7,7 +7,15 @@ class Subnets(BaseResponse):
def create_subnet(self):
vpc_id = self.querystring.get('VpcId')[0]
cidr_block = self.querystring.get('CidrBlock')[0]
subnet = self.ec2_backend.create_subnet(vpc_id, cidr_block)
if 'AvailabilityZone' in self.querystring:
availability_zone = self.querystring['AvailabilityZone'][0]
else:
availability_zone = None
subnet = self.ec2_backend.create_subnet(
vpc_id,
cidr_block,
availability_zone,
)
template = self.response_template(CREATE_SUBNET_RESPONSE)
return template.render(subnet=subnet)
@ -33,7 +41,7 @@ CREATE_SUBNET_RESPONSE = """
<vpcId>{{ subnet.vpc_id }}</vpcId>
<cidrBlock>{{ subnet.cidr_block }}</cidrBlock>
<availableIpAddressCount>251</availableIpAddressCount>
<availabilityZone>us-east-1a</availabilityZone>
<availabilityZone>{{ subnet.availability_zone }}</availabilityZone>
<tagSet>
{% for tag in subnet.get_tags() %}
<item>
@ -64,7 +72,7 @@ DESCRIBE_SUBNETS_RESPONSE = """
<vpcId>{{ subnet.vpc_id }}</vpcId>
<cidrBlock>{{ subnet.cidr_block }}</cidrBlock>
<availableIpAddressCount>251</availableIpAddressCount>
<availabilityZone>us-east-1a</availabilityZone>
<availabilityZone>{{ subnet.availability_zone }}</availabilityZone>
<tagSet>
{% for tag in subnet.get_tags() %}
<item>

View file

@ -1,6 +1,13 @@
from __future__ import unicode_literals
import boto.ec2.elb
from boto.ec2.elb.attributes import (
LbAttributes,
ConnectionSettingAttribute,
ConnectionDrainingAttribute,
AccessLogAttribute,
CrossZoneLoadBalancingAttribute,
)
from moto.core import BaseBackend
@ -29,6 +36,7 @@ class FakeLoadBalancer(object):
self.instance_ids = []
self.zones = zones
self.listeners = []
self.attributes = FakeLoadBalancer.get_default_attributes()
for protocol, lb_port, instance_port, ssl_certificate_id in ports:
listener = FakeListener(
@ -73,6 +81,28 @@ class FakeLoadBalancer(object):
raise NotImplementedError('"Fn::GetAtt" : [ "{0}" , "SourceSecurityGroup.OwnerAlias" ]"')
raise UnformattedGetAttTemplateException()
@classmethod
def get_default_attributes(cls):
attributes = LbAttributes()
cross_zone_load_balancing = CrossZoneLoadBalancingAttribute()
cross_zone_load_balancing.enabled = False
attributes.cross_zone_load_balancing = cross_zone_load_balancing
connection_draining = ConnectionDrainingAttribute()
connection_draining.enabled = False
attributes.connection_draining = connection_draining
access_log = AccessLogAttribute()
access_log.enabled = False
attributes.access_log = access_log
connection_settings = ConnectionSettingAttribute()
connection_settings.idle_timeout = 60
attributes.connecting_settings = connection_settings
return attributes
class ELBBackend(BaseBackend):
@ -151,6 +181,26 @@ class ELBBackend(BaseBackend):
load_balancer.instance_ids = new_instance_ids
return load_balancer
def set_cross_zone_load_balancing_attribute(self, load_balancer_name, attribute):
load_balancer = self.get_load_balancer(load_balancer_name)
load_balancer.attributes.cross_zone_load_balancing = attribute
return load_balancer
def set_access_log_attribute(self, load_balancer_name, attribute):
load_balancer = self.get_load_balancer(load_balancer_name)
load_balancer.attributes.access_log = attribute
return load_balancer
def set_connection_draining_attribute(self, load_balancer_name, attribute):
load_balancer = self.get_load_balancer(load_balancer_name)
load_balancer.attributes.connection_draining = attribute
return load_balancer
def set_connection_settings_attribute(self, load_balancer_name, attribute):
load_balancer = self.get_load_balancer(load_balancer_name)
load_balancer.attributes.connecting_settings = attribute
return load_balancer
elb_backends = {}
for region in boto.ec2.elb.regions():

View file

@ -1,4 +1,10 @@
from __future__ import unicode_literals
from boto.ec2.elb.attributes import (
ConnectionSettingAttribute,
ConnectionDrainingAttribute,
AccessLogAttribute,
CrossZoneLoadBalancingAttribute,
)
from moto.core.responses import BaseResponse
from .models import elb_backends
@ -25,7 +31,7 @@ class ELBResponse(BaseResponse):
break
lb_port = self.querystring['Listeners.member.{0}.LoadBalancerPort'.format(port_index)][0]
instance_port = self.querystring['Listeners.member.{0}.InstancePort'.format(port_index)][0]
ssl_certificate_id = self.querystring.get('Listeners.member.{0}.SSLCertificateId'.format(port_index)[0], None)
ssl_certificate_id = self.querystring.get('Listeners.member.{0}.SSLCertificateId'.format(port_index), [None])[0]
ports.append([protocol, lb_port, instance_port, ssl_certificate_id])
port_index += 1
@ -122,6 +128,64 @@ class ELBResponse(BaseResponse):
load_balancer = self.elb_backend.deregister_instances(load_balancer_name, instance_ids)
return template.render(load_balancer=load_balancer)
def describe_load_balancer_attributes(self):
load_balancer_name = self.querystring.get('LoadBalancerName')[0]
load_balancer = self.elb_backend.describe_load_balancers(load_balancer_name)[0]
template = self.response_template(DESCRIBE_ATTRIBUTES_TEMPLATE)
return template.render(attributes=load_balancer.attributes)
def modify_load_balancer_attributes(self):
load_balancer_name = self.querystring.get('LoadBalancerName')[0]
load_balancer = self.elb_backend.describe_load_balancers(load_balancer_name)[0]
def parse_attribute(attribute_name):
"""
Transform self.querystring parameters matching `LoadBalancerAttributes.attribute_name.attribute_key`
into a dictionary of (attribute_name, attribute_key)` pairs.
"""
attribute_prefix = "LoadBalancerAttributes." + attribute_name
return dict((key.lstrip(attribute_prefix), value[0]) for key, value in self.querystring.items() if key.startswith(attribute_prefix))
cross_zone = parse_attribute("CrossZoneLoadBalancing")
if cross_zone:
attribute = CrossZoneLoadBalancingAttribute()
attribute.enabled = cross_zone["Enabled"] == "true"
self.elb_backend.set_cross_zone_load_balancing_attribute(load_balancer_name, attribute)
access_log = parse_attribute("AccessLog")
if access_log:
attribute = AccessLogAttribute()
attribute.enabled = access_log["Enabled"] == "true"
attribute.s3_bucket_name = access_log["S3BucketName"]
attribute.s3_bucket_prefix = access_log["S3BucketPrefix"]
attribute.emit_interval = access_log["EmitInterval"]
self.elb_backend.set_access_log_attribute(load_balancer_name, attribute)
connection_draining = parse_attribute("ConnectionDraining")
if connection_draining:
attribute = ConnectionDrainingAttribute()
attribute.enabled = connection_draining["Enabled"] == "true"
attribute.timeout = connection_draining["Timeout"]
self.elb_backend.set_connection_draining_attribute(load_balancer_name, attribute)
connection_settings = parse_attribute("ConnectionSettings")
if connection_settings:
attribute = ConnectionSettingAttribute()
attribute.idle_timeout = connection_settings["IdleTimeout"]
self.elb_backend.set_connection_settings_attribute(load_balancer_name, attribute)
template = self.response_template(MODIFY_ATTRIBUTES_TEMPLATE)
return template.render(attributes=load_balancer.attributes)
def describe_instance_health(self):
load_balancer_name = self.querystring.get('LoadBalancerName')[0]
instance_ids = [value[0] for key, value in self.querystring.items() if "Instances.member" in key]
if len(instance_ids) == 0:
instance_ids = self.elb_backend.describe_load_balancers(load_balancer_name)[0].instance_ids
template = self.response_template(DESCRIBE_INSTANCE_HEALTH_TEMPLATE)
return template.render(instance_ids=instance_ids)
CREATE_LOAD_BALANCER_TEMPLATE = """<CreateLoadBalancerResult xmlns="http://elasticloadbalancing.amazonaws.com/doc/2012-06-01/">
<DNSName>tests.us-east-1.elb.amazonaws.com</DNSName>
</CreateLoadBalancerResult>"""
@ -253,3 +317,84 @@ DELETE_LOAD_BALANCER_LISTENERS = """<DeleteLoadBalancerListenersResponse xmlns="
<RequestId>83c88b9d-12b7-11e3-8b82-87b12EXAMPLE</RequestId>
</ResponseMetadata>
</DeleteLoadBalancerListenersResponse>"""
DESCRIBE_ATTRIBUTES_TEMPLATE = """<DescribeLoadBalancerAttributesResponse xmlns="http://elasticloadbalancing.amazonaws.com/doc/2012-06-01/">
<DescribeLoadBalancerAttributesResult>
<LoadBalancerAttributes>
<AccessLog>
<Enabled>{{ attributes.access_log.enabled }}</Enabled>
{% if attributes.access_log.enabled %}
<S3BucketName>{{ attributes.access_log.s3_bucket_name }}</S3BucketName>
<S3BucketPrefix>{{ attributes.access_log.s3_bucket_prefix }}</S3BucketPrefix>
<EmitInterval>{{ attributes.access_log.emit_interval }}</EmitInterval>
{% endif %}
</AccessLog>
<ConnectionSettings>
<IdleTimeout>{{ attributes.connecting_settings.idle_timeout }}</IdleTimeout>
</ConnectionSettings>
<CrossZoneLoadBalancing>
<Enabled>{{ attributes.cross_zone_load_balancing.enabled }}</Enabled>
</CrossZoneLoadBalancing>
<ConnectionDraining>
<Enabled>{{ attributes.connection_draining.enabled }}</Enabled>
{% if attributes.connection_draining.enabled %}
<Timeout>{{ attributes.connection_draining.timeout }}</Timeout>
{% endif %}
</ConnectionDraining>
</LoadBalancerAttributes>
</DescribeLoadBalancerAttributesResult>
<ResponseMetadata>
<RequestId>83c88b9d-12b7-11e3-8b82-87b12EXAMPLE</RequestId>
</ResponseMetadata>
</DescribeLoadBalancerAttributesResponse>
"""
MODIFY_ATTRIBUTES_TEMPLATE = """<ModifyLoadBalancerAttributesResponse xmlns="http://elasticloadbalancing.amazonaws.com/doc/2012-06-01/">
<ModifyLoadBalancerAttributesResult>
<LoadBalancerName>my-loadbalancer</LoadBalancerName>
<LoadBalancerAttributes>
<AccessLog>
<Enabled>{{ attributes.access_log.enabled }}</Enabled>
{% if attributes.access_log.enabled %}
<S3BucketName>{{ attributes.access_log.s3_bucket_name }}</S3BucketName>
<S3BucketPrefix>{{ attributes.access_log.s3_bucket_prefix }}</S3BucketPrefix>
<EmitInterval>{{ attributes.access_log.emit_interval }}</EmitInterval>
{% endif %}
</AccessLog>
<ConnectionSettings>
<IdleTimeout>{{ attributes.connecting_settings.idle_timeout }}</IdleTimeout>
</ConnectionSettings>
<CrossZoneLoadBalancing>
<Enabled>{{ attributes.cross_zone_load_balancing.enabled }}</Enabled>
</CrossZoneLoadBalancing>
<ConnectionDraining>
<Enabled>{{ attributes.connection_draining.enabled }}</Enabled>
{% if attributes.connection_draining.enabled %}
<Timeout>{{ attributes.connection_draining.timeout }}</Timeout>
{% endif %}
</ConnectionDraining>
</LoadBalancerAttributes>
</ModifyLoadBalancerAttributesResult>
<ResponseMetadata>
<RequestId>83c88b9d-12b7-11e3-8b82-87b12EXAMPLE</RequestId>
</ResponseMetadata>
</ModifyLoadBalancerAttributesResponse>
"""
DESCRIBE_INSTANCE_HEALTH_TEMPLATE = """<DescribeInstanceHealthResponse xmlns="http://elasticloadbalancing.amazonaws.com/doc/2012-06-01/">
<DescribeInstanceHealthResult>
<InstanceStates>
{% for instance_id in instance_ids %}
<member>
<Description>N/A</Description>
<InstanceId>{{ instance_id }}</InstanceId>
<State>InService</State>
<ReasonCode>N/A</ReasonCode>
</member>
{% endfor %}
</InstanceStates>
</DescribeInstanceHealthResult>
<ResponseMetadata>
<RequestId>1549581b-12b7-11e3-895e-1334aEXAMPLE</RequestId>
</ResponseMetadata>
</DescribeInstanceHealthResponse>"""

View file

@ -6,6 +6,7 @@ from .utils import random_access_key, random_alphanumeric, random_resource_id
from datetime import datetime
import base64
class Role(object):
def __init__(self, role_id, name, assume_role_policy_document, path):
@ -212,16 +213,16 @@ class User(object):
access_key_2_last_rotated = date_created.strftime(date_format)
return '{0},{1},{2},{3},{4},{5},not_supported,false,{6},{7},{8},{9},false,N/A,false,N/A'.format(self.name,
self.arn,
date_created.strftime(date_format),
password_enabled,
password_last_used,
date_created.strftime(date_format),
access_key_1_active,
access_key_1_last_rotated,
access_key_2_active,
access_key_2_last_rotated
)
self.arn,
date_created.strftime(date_format),
password_enabled,
password_last_used,
date_created.strftime(date_format),
access_key_1_active,
access_key_1_last_rotated,
access_key_2_active,
access_key_2_last_rotated
)
class IAMBackend(BaseBackend):
@ -337,6 +338,18 @@ class IAMBackend(BaseBackend):
return group
def list_groups(self):
return self.groups.values()
def get_groups_for_user(self, user_name):
user = self.get_user(user_name)
groups = []
for group in self.list_groups():
if user in group.users:
groups.append(group)
return groups
def create_user(self, user_name, path='/'):
if user_name in self.users:
raise BotoServerError(409, 'Conflict')

View file

@ -131,6 +131,18 @@ class IamResponse(BaseResponse):
template = self.response_template(GET_GROUP_TEMPLATE)
return template.render(group=group)
def list_groups(self):
groups = iam_backend.list_groups()
template = self.response_template(LIST_GROUPS_TEMPLATE)
return template.render(groups=groups)
def list_groups_for_user(self):
user_name = self._get_param('UserName')
groups = iam_backend.get_groups_for_user(user_name)
template = self.response_template(LIST_GROUPS_FOR_USER_TEMPLATE)
return template.render(groups=groups)
def create_user(self):
user_name = self._get_param('UserName')
path = self._get_param('Path')
@ -502,6 +514,45 @@ GET_GROUP_TEMPLATE = """<GetGroupResponse>
</ResponseMetadata>
</GetGroupResponse>"""
LIST_GROUPS_TEMPLATE = """<ListGroupsResponse>
<ListGroupsResult>
<Groups>
{% for group in groups %}
<member>
<Path>{{ group.path }}</Path>
<GroupName>{{ group.name }}</GroupName>
<GroupId>{{ group.id }}</GroupId>
<Arn>arn:aws:iam::123456789012:group/{{ group.path }}</Arn>
</member>
{% endfor %}
</Groups>
<IsTruncated>false</IsTruncated>
</ListGroupsResult>
<ResponseMetadata>
<RequestId>7a62c49f-347e-4fc4-9331-6e8eEXAMPLE</RequestId>
</ResponseMetadata>
</ListGroupsResponse>"""
LIST_GROUPS_FOR_USER_TEMPLATE = """<ListGroupsForUserResponse>
<ListGroupsForUserResult>
<Groups>
{% for group in groups %}
<member>
<Path>{{ group.path }}</Path>
<GroupName>{{ group.name }}</GroupName>
<GroupId>{{ group.id }}</GroupId>
<Arn>arn:aws:iam::123456789012:group/{{ group.path }}</Arn>
</member>
{% endfor %}
</Groups>
<IsTruncated>false</IsTruncated>
</ListGroupsForUserResult>
<ResponseMetadata>
<RequestId>7a62c49f-347e-4fc4-9331-6e8eEXAMPLE</RequestId>
</ResponseMetadata>
</ListGroupsForUserResponse>"""
USER_TEMPLATE = """<{{ action }}UserResponse>
<{{ action }}UserResult>
<User>
@ -640,4 +691,4 @@ LIST_INSTANCE_PROFILES_FOR_ROLE_TEMPLATE = """<ListInstanceProfilesForRoleRespon
<ResponseMetadata>
<RequestId>6a8c3992-99f4-11e1-a4c3-27EXAMPLE804</RequestId>
</ResponseMetadata>
</ListInstanceProfilesForRoleResponse>"""
</ListInstanceProfilesForRoleResponse>"""

View file

@ -106,9 +106,10 @@ class RecordSet(object):
class FakeZone(object):
def __init__(self, name, id_):
def __init__(self, name, id_, comment=None):
self.name = name
self.id = id_
self.comment = comment
self.rrsets = []
def add_rrset(self, record_set):
@ -116,9 +117,12 @@ class FakeZone(object):
self.rrsets.append(record_set)
return record_set
def delete_rrset(self, name):
def delete_rrset_by_name(self, name):
self.rrsets = [record_set for record_set in self.rrsets if record_set.name != name]
def delete_rrset_by_id(self, set_identifier):
self.rrsets = [record_set for record_set in self.rrsets if record_set.set_identifier != set_identifier]
def get_record_sets(self, type_filter, name_filter):
record_sets = list(self.rrsets) # Copy the list
if type_filter:
@ -170,9 +174,9 @@ class Route53Backend(BaseBackend):
self.zones = {}
self.health_checks = {}
def create_hosted_zone(self, name):
def create_hosted_zone(self, name, comment=None):
new_id = get_random_hex()
new_zone = FakeZone(name, new_id)
new_zone = FakeZone(name, new_id, comment=comment)
self.zones[new_id] = new_zone
return new_zone

View file

@ -9,7 +9,8 @@ def list_or_create_hostzone_response(request, full_url, headers):
if request.method == "POST":
elements = xmltodict.parse(request.body)
new_zone = route53_backend.create_hosted_zone(elements["CreateHostedZoneRequest"]["Name"])
comment = elements["CreateHostedZoneRequest"]["HostedZoneConfig"]["Comment"]
new_zone = route53_backend.create_hosted_zone(elements["CreateHostedZoneRequest"]["Name"], comment=comment)
template = Template(CREATE_HOSTED_ZONE_RESPONSE)
return 201, headers, template.render(zone=new_zone)
@ -57,7 +58,10 @@ def rrset_response(request, full_url, headers):
record_set['ResourceRecords'] = [x['Value'] for x in record_set['ResourceRecords'].values()]
the_zone.add_rrset(record_set)
elif action == "DELETE":
the_zone.delete_rrset(record_set["Name"])
if 'SetIdentifier' in record_set:
the_zone.delete_rrset_by_id(record_set["SetIdentifier"])
else:
the_zone.delete_rrset_by_name(record_set["Name"])
return 200, headers, CHANGE_RRSET_RESPONSE
@ -125,6 +129,9 @@ GET_HOSTED_ZONE_RESPONSE = """<GetHostedZoneResponse xmlns="https://route53.amaz
<Id>/hostedzone/{{ zone.id }}</Id>
<Name>{{ zone.name }}</Name>
<ResourceRecordSetCount>{{ zone.rrsets|count }}</ResourceRecordSetCount>
<Config>
<Comment>{{ zone.comment }}</Comment>
</Config>
</HostedZone>
<DelegationSet>
<NameServer>moto.test.com</NameServer>
@ -150,6 +157,9 @@ LIST_HOSTED_ZONES_RESPONSE = """<ListHostedZonesResponse xmlns="https://route53.
<HostedZone>
<Id>{{ zone.id }}</Id>
<Name>{{ zone.name }}</Name>
<Config>
<Comment>{{ zone.comment }}</Comment>
</Config>
<ResourceRecordSetCount>{{ zone.rrsets|count }}</ResourceRecordSetCount>
</HostedZone>
{% endfor %}

10
moto/sns/exceptions.py Normal file
View file

@ -0,0 +1,10 @@
from __future__ import unicode_literals
from moto.core.exceptions import RESTError
class SNSNotFoundError(RESTError):
code = 404
def __init__(self, message):
super(SNSNotFoundError, self).__init__(
"NotFound", message)

View file

@ -11,6 +11,7 @@ from moto.compat import OrderedDict
from moto.core import BaseBackend
from moto.core.utils import iso_8601_datetime_with_milliseconds
from moto.sqs import sqs_backends
from .exceptions import SNSNotFoundError
from .utils import make_arn_for_topic, make_arn_for_subscription
DEFAULT_ACCOUNT_ID = 123456789012
@ -93,10 +94,52 @@ class Subscription(object):
}
class PlatformApplication(object):
def __init__(self, region, name, platform, attributes):
self.region = region
self.name = name
self.platform = platform
self.attributes = attributes
@property
def arn(self):
return "arn:aws:sns:{region}:123456789012:app/{platform}/{name}".format(
region=self.region,
platform=self.platform,
name=self.name,
)
class PlatformEndpoint(object):
def __init__(self, region, application, custom_user_data, token, attributes):
self.region = region
self.application = application
self.custom_user_data = custom_user_data
self.token = token
self.attributes = attributes
self.id = uuid.uuid4()
@property
def arn(self):
return "arn:aws:sns:{region}:123456789012:endpoint/{platform}/{name}/{id}".format(
region=self.region,
platform=self.application.platform,
name=self.application.name,
id=self.id,
)
def publish(self, message):
message_id = six.text_type(uuid.uuid4())
# This is where we would actually send a message
return message_id
class SNSBackend(BaseBackend):
def __init__(self):
self.topics = OrderedDict()
self.subscriptions = OrderedDict()
self.applications = {}
self.platform_endpoints = {}
def create_topic(self, name):
topic = Topic(name, self)
@ -121,7 +164,10 @@ class SNSBackend(BaseBackend):
self.topics.pop(arn)
def get_topic(self, arn):
return self.topics[arn]
try:
return self.topics[arn]
except KeyError:
raise SNSNotFoundError("Topic with arn {0} not found".format(arn))
def set_topic_attribute(self, topic_arn, attribute_name, attribute_value):
topic = self.get_topic(topic_arn)
@ -144,11 +190,61 @@ class SNSBackend(BaseBackend):
else:
return self._get_values_nexttoken(self.subscriptions, next_token)
def publish(self, topic_arn, message):
topic = self.get_topic(topic_arn)
message_id = topic.publish(message)
def publish(self, arn, message):
try:
topic = self.get_topic(arn)
message_id = topic.publish(message)
except SNSNotFoundError:
endpoint = self.get_endpoint(arn)
message_id = endpoint.publish(message)
return message_id
def create_platform_application(self, region, name, platform, attributes):
application = PlatformApplication(region, name, platform, attributes)
self.applications[application.arn] = application
return application
def get_application(self, arn):
try:
return self.applications[arn]
except KeyError:
raise SNSNotFoundError("Application with arn {0} not found".format(arn))
def set_application_attributes(self, arn, attributes):
application = self.get_application(arn)
application.attributes.update(attributes)
return application
def list_platform_applications(self):
return self.applications.values()
def delete_platform_application(self, platform_arn):
self.applications.pop(platform_arn)
def create_platform_endpoint(self, region, application, custom_user_data, token, attributes):
platform_endpoint = PlatformEndpoint(region, application, custom_user_data, token, attributes)
self.platform_endpoints[platform_endpoint.arn] = platform_endpoint
return platform_endpoint
def list_endpoints_by_platform_application(self, application_arn):
return [
endpoint for endpoint
in self.platform_endpoints.values()
if endpoint.application.arn == application_arn
]
def get_endpoint(self, arn):
try:
return self.platform_endpoints[arn]
except KeyError:
raise SNSNotFoundError("Endpoint with arn {0} not found".format(arn))
def set_endpoint_attributes(self, arn, attributes):
endpoint = self.get_endpoint(arn)
endpoint.attributes.update(attributes)
return endpoint
sns_backends = {}
for region in boto.sns.regions():
sns_backends[region.name] = SNSBackend()

View file

@ -12,6 +12,14 @@ class SNSResponse(BaseResponse):
def backend(self):
return sns_backends[self.region]
def _get_attributes(self):
attributes = self._get_list_prefix('Attributes.entry')
return dict(
(attribute['key'], attribute['value'])
for attribute
in attributes
)
def create_topic(self):
name = self._get_param('Name')
topic = self.backend.create_topic(name)
@ -170,9 +178,11 @@ class SNSResponse(BaseResponse):
})
def publish(self):
target_arn = self._get_param('TargetArn')
topic_arn = self._get_param('TopicArn')
arn = target_arn if target_arn else topic_arn
message = self._get_param('Message')
message_id = self.backend.publish(topic_arn, message)
message_id = self.backend.publish(arn, message)
return json.dumps({
"PublishResponse": {
@ -185,19 +195,116 @@ class SNSResponse(BaseResponse):
}
})
def create_platform_application(self):
name = self._get_param('Name')
platform = self._get_param('Platform')
attributes = self._get_attributes()
platform_application = self.backend.create_platform_application(self.region, name, platform, attributes)
return json.dumps({
"CreatePlatformApplicationResponse": {
"CreatePlatformApplicationResult": {
"PlatformApplicationArn": platform_application.arn,
},
"ResponseMetadata": {
"RequestId": "384ac68d-3775-11df-8963-01868b7c937b",
}
}
})
def get_platform_application_attributes(self):
arn = self._get_param('PlatformApplicationArn')
application = self.backend.get_application(arn)
return json.dumps({
"GetPlatformApplicationAttributesResponse": {
"GetPlatformApplicationAttributesResult": {
"Attributes": application.attributes,
},
"ResponseMetadata": {
"RequestId": "384ac68d-3775-11df-8963-01868b7c937f",
}
}
})
def set_platform_application_attributes(self):
arn = self._get_param('PlatformApplicationArn')
attributes = self._get_attributes()
self.backend.set_application_attributes(arn, attributes)
return json.dumps({
"SetPlatformApplicationAttributesResponse": {
"ResponseMetadata": {
"RequestId": "384ac68d-3775-12df-8963-01868b7c937f",
}
}
})
def list_platform_applications(self):
applications = self.backend.list_platform_applications()
return json.dumps({
"ListPlatformApplicationsResponse": {
"ListPlatformApplicationsResult": {
"PlatformApplications": [{
"PlatformApplicationArn": application.arn,
"attributes": application.attributes,
} for application in applications],
"NextToken": None
},
"ResponseMetadata": {
"RequestId": "384ac68d-3775-11df-8963-01868b7c937c",
}
}
})
def delete_platform_application(self):
platform_arn = self._get_param('PlatformApplicationArn')
self.backend.delete_platform_application(platform_arn)
return json.dumps({
"DeletePlatformApplicationResponse": {
"ResponseMetadata": {
"RequestId": "384ac68d-3775-11df-8963-01868b7c937e",
}
}
})
def create_platform_endpoint(self):
application_arn = self._get_param('PlatformApplicationArn')
application = self.backend.get_application(application_arn)
custom_user_data = self._get_param('CustomUserData')
token = self._get_param('Token')
attributes = self._get_attributes()
platform_endpoint = self.backend.create_platform_endpoint(
self.region, application, custom_user_data, token, attributes)
return json.dumps({
"CreatePlatformEndpointResponse": {
"CreatePlatformEndpointResult": {
"EndpointArn": platform_endpoint.arn,
},
"ResponseMetadata": {
"RequestId": "384ac68d-3779-11df-8963-01868b7c937b",
}
}
})
def list_endpoints_by_platform_application(self):
application_arn = self._get_param('PlatformApplicationArn')
endpoints = self.backend.list_endpoints_by_platform_application(application_arn)
return json.dumps({
"ListEndpointsByPlatformApplicationResponse": {
"ListEndpointsByPlatformApplicationResult": {
"Endpoints": [
{
"Attributes": {
"Token": "TOKEN",
"Enabled": "true",
"CustomUserData": ""
},
"EndpointArn": "FAKE_ARN_ENDPOINT"
}
"Attributes": endpoint.attributes,
"EndpointArn": endpoint.arn,
} for endpoint in endpoints
],
"NextToken": None
},
@ -206,3 +313,32 @@ class SNSResponse(BaseResponse):
}
}
})
def get_endpoint_attributes(self):
arn = self._get_param('EndpointArn')
endpoint = self.backend.get_endpoint(arn)
return json.dumps({
"GetEndpointAttributesResponse": {
"GetEndpointAttributesResult": {
"Attributes": endpoint.attributes,
},
"ResponseMetadata": {
"RequestId": "384ac68d-3775-11df-8963-01868b7c937f",
}
}
})
def set_endpoint_attributes(self):
arn = self._get_param('EndpointArn')
attributes = self._get_attributes()
self.backend.set_endpoint_attributes(arn, attributes)
return json.dumps({
"SetEndpointAttributesResponse": {
"ResponseMetadata": {
"RequestId": "384bc68d-3775-12df-8963-01868b7c937f",
}
}
})