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:
commit
e722b67f36
33 changed files with 1381 additions and 167 deletions
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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']
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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():
|
||||
|
|
|
|||
|
|
@ -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>"""
|
||||
|
|
|
|||
|
|
@ -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')
|
||||
|
|
|
|||
|
|
@ -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>"""
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
10
moto/sns/exceptions.py
Normal 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)
|
||||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
}
|
||||
}
|
||||
})
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue