diff --git a/README.md b/README.md index d80e515ed..0e437b5b1 100644 --- a/README.md +++ b/README.md @@ -64,7 +64,7 @@ It gets even better! Moto isn't just S3. Here's the status of the other AWS serv | Data Pipeline | @mock_datapipeline| basic endpoints done | |------------------------------------------------------------------------------| | DynamoDB | @mock_dynamodb | core endpoints done | -| DynamoDB2 | @mock_dynamodb2 | core endpoints done - no indexes | +| DynamoDB2 | @mock_dynamodb2 | core endpoints + partial indexes | |------------------------------------------------------------------------------| | EC2 | @mock_ec2 | core endpoints done | | - AMI | | core endpoints done | @@ -73,6 +73,8 @@ It gets even better! Moto isn't just S3. Here's the status of the other AWS serv | - Security Groups | | core endpoints done | | - Tags | | all endpoints done | |------------------------------------------------------------------------------| +| ECS | @mock_ecs | basic endpoints done | +|------------------------------------------------------------------------------| | ELB | @mock_elb | core endpoints done | |------------------------------------------------------------------------------| | EMR | @mock_emr | core endpoints done | diff --git a/moto/__init__.py b/moto/__init__.py index fbbca07be..65b22b1ce 100644 --- a/moto/__init__.py +++ b/moto/__init__.py @@ -12,6 +12,7 @@ from .datapipeline import mock_datapipeline # flake8: noqa from .dynamodb import mock_dynamodb # flake8: noqa from .dynamodb2 import mock_dynamodb2 # flake8: noqa from .ec2 import mock_ec2 # flake8: noqa +from .ecs import mock_ecs # flake8: noqa from .elb import mock_elb # flake8: noqa from .emr import mock_emr # flake8: noqa from .glacier import mock_glacier # flake8: noqa diff --git a/moto/autoscaling/models.py b/moto/autoscaling/models.py index d67d7dd1c..7a119ecb2 100644 --- a/moto/autoscaling/models.py +++ b/moto/autoscaling/models.py @@ -275,7 +275,10 @@ class AutoScalingBackend(BaseBackend): 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) + if health_check_period is None: + health_check_period = 300 + else: + health_check_period = make_int(health_check_period) group = FakeAutoScalingGroup( name=name, @@ -385,4 +388,3 @@ class AutoScalingBackend(BaseBackend): autoscaling_backends = {} for region, ec2_backend in ec2_backends.items(): autoscaling_backends[region] = AutoScalingBackend(ec2_backend, elb_backends[region]) - diff --git a/moto/dynamodb2/models.py b/moto/dynamodb2/models.py index 2b1a1b5ee..77b1434c3 100644 --- a/moto/dynamodb2/models.py +++ b/moto/dynamodb2/models.py @@ -124,11 +124,22 @@ class Item(object): def update_with_attribute_updates(self, attribute_updates): for attribute_name, update_action in attribute_updates.items(): action = update_action['Action'] + if action == 'DELETE' and not 'Value' in update_action: + if attribute_name in self.attrs: + del self.attrs[attribute_name] + continue new_value = list(update_action['Value'].values())[0] if action == 'PUT': # TODO deal with other types if isinstance(new_value, list) or isinstance(new_value, set): self.attrs[attribute_name] = DynamoType({"SS": new_value}) + elif isinstance(new_value, dict): + self.attrs[attribute_name] = DynamoType({"M": new_value}) + elif update_action['Value'].keys() == ['N']: + self.attrs[attribute_name] = DynamoType({"N": new_value}) + elif update_action['Value'].keys() == ['NULL']: + if attribute_name in self.attrs: + del self.attrs[attribute_name] else: self.attrs[attribute_name] = DynamoType({"S": new_value}) @@ -278,20 +289,63 @@ class Table(object): except KeyError: return None - def query(self, hash_key, range_comparison, range_objs): + def query(self, hash_key, range_comparison, range_objs, index_name=None): results = [] last_page = True # Once pagination is implemented, change this - possible_results = [item for item in list(self.all_items()) if isinstance(item, Item) and item.hash_key == hash_key] + if index_name: + all_indexes = (self.global_indexes or []) + (self.indexes or []) + indexes_by_name = dict((i['IndexName'], i) for i in all_indexes) + if index_name not in indexes_by_name: + raise ValueError('Invalid index: %s for table: %s. Available indexes are: %s' % ( + index_name, self.name, ', '.join(indexes_by_name.keys()) + )) + + index = indexes_by_name[index_name] + try: + index_hash_key = [key for key in index['KeySchema'] if key['KeyType'] == 'HASH'][0] + except IndexError: + raise ValueError('Missing Hash Key. KeySchema: %s' % index['KeySchema']) + + possible_results = [] + for item in self.all_items(): + if not isinstance(item, Item): + continue + item_hash_key = item.attrs.get(index_hash_key['AttributeName']) + if item_hash_key and item_hash_key == hash_key: + possible_results.append(item) + else: + possible_results = [item for item in list(self.all_items()) if isinstance(item, Item) and item.hash_key == hash_key] + + if index_name: + try: + index_range_key = [key for key in index['KeySchema'] if key['KeyType'] == 'RANGE'][0] + except IndexError: + index_range_key = None + if range_comparison: - for result in possible_results: - if result.range_key.compare(range_comparison, range_objs): - results.append(result) + if index_name and not index_range_key: + raise ValueError('Range Key comparison but no range key found for index: %s' % index_name) + + elif index_name: + for result in possible_results: + if result.attrs.get(index_range_key['AttributeName']).compare(range_comparison, range_objs): + results.append(result) + else: + for result in possible_results: + if result.range_key.compare(range_comparison, range_objs): + results.append(result) else: # If we're not filtering on range key, return all values results = possible_results - results.sort(key=lambda item: item.range_key) + if index_name: + + if index_range_key: + results.sort(key=lambda item: item.attrs[index_range_key['AttributeName']].value + if item.attrs.get(index_range_key['AttributeName']) else None) + else: + results.sort(key=lambda item: item.range_key) return results, last_page def all_items(self): @@ -361,6 +415,38 @@ class DynamoDBBackend(BaseBackend): table.throughput = throughput return table + def update_table_global_indexes(self, name, global_index_updates): + table = self.tables[name] + gsis_by_name = dict((i['IndexName'], i) for i in table.global_indexes) + for gsi_update in global_index_updates: + gsi_to_create = gsi_update.get('Create') + gsi_to_update = gsi_update.get('Update') + gsi_to_delete = gsi_update.get('Delete') + + if gsi_to_delete: + index_name = gsi_to_delete['IndexName'] + if index_name not in gsis_by_name: + raise ValueError('Global Secondary Index does not exist, but tried to delete: %s' % + gsi_to_delete['IndexName']) + + del gsis_by_name[index_name] + + if gsi_to_update: + index_name = gsi_to_update['IndexName'] + if index_name not in gsis_by_name: + raise ValueError('Global Secondary Index does not exist, but tried to update: %s' % + gsi_to_update['IndexName']) + gsis_by_name[index_name].update(gsi_to_update) + + if gsi_to_create: + if gsi_to_create['IndexName'] in gsis_by_name: + raise ValueError('Global Secondary Index already exists: %s' % gsi_to_create['IndexName']) + + gsis_by_name[gsi_to_create['IndexName']] = gsi_to_create + + table.global_indexes = gsis_by_name.values() + return table + def put_item(self, table_name, item_attrs, expected=None, overwrite=False): table = self.tables.get(table_name) if not table: @@ -400,7 +486,7 @@ class DynamoDBBackend(BaseBackend): hash_key, range_key = self.get_keys_value(table, keys) return table.get_item(hash_key, range_key) - def query(self, table_name, hash_key_dict, range_comparison, range_value_dicts): + def query(self, table_name, hash_key_dict, range_comparison, range_value_dicts, index_name=None): table = self.tables.get(table_name) if not table: return None, None @@ -408,7 +494,7 @@ class DynamoDBBackend(BaseBackend): hash_key = DynamoType(hash_key_dict) range_values = [DynamoType(range_value) for range_value in range_value_dicts] - return table.query(hash_key, range_comparison, range_values) + return table.query(hash_key, range_comparison, range_values, index_name) def scan(self, table_name, filters): table = self.tables.get(table_name) @@ -425,12 +511,20 @@ class DynamoDBBackend(BaseBackend): def update_item(self, table_name, key, update_expression, attribute_updates): table = self.get_table(table_name) - if table.hash_key_attr in key: - # Sometimes the key is wrapped in a dict with the key name - key = key[table.hash_key_attr] + if all([table.hash_key_attr in key, table.range_key_attr in key]): + # Covers cases where table has hash and range keys, ``key`` param will be a dict + hash_value = DynamoType(key[table.hash_key_attr]) + range_value = DynamoType(key[table.range_key_attr]) + elif table.hash_key_attr in key: + # Covers tables that have a range key where ``key`` param is a dict + hash_value = DynamoType(key[table.hash_key_attr]) + range_value = None + else: + # Covers other cases + hash_value = DynamoType(key) + range_value = None - hash_value = DynamoType(key) - item = table.get_item(hash_value) + item = table.get_item(hash_value, range_value) if update_expression: item.update(update_expression) else: diff --git a/moto/dynamodb2/responses.py b/moto/dynamodb2/responses.py index dbf9191ea..08a03c08c 100644 --- a/moto/dynamodb2/responses.py +++ b/moto/dynamodb2/responses.py @@ -125,8 +125,11 @@ class DynamoHandler(BaseResponse): def update_table(self): name = self.body['TableName'] - throughput = self.body["ProvisionedThroughput"] - table = dynamodb_backend2.update_table_throughput(name, throughput) + if 'GlobalSecondaryIndexUpdates' in self.body: + table = dynamodb_backend2.update_table_global_indexes(name, self.body['GlobalSecondaryIndexUpdates']) + if 'ProvisionedThroughput' in self.body: + throughput = self.body["ProvisionedThroughput"] + table = dynamodb_backend2.update_table_throughput(name, throughput) return dynamo_json_dump(table.describe) def describe_table(self): @@ -241,11 +244,31 @@ class DynamoHandler(BaseResponse): if key_condition_expression: value_alias_map = self.body['ExpressionAttributeValues'] + table = dynamodb_backend2.get_table(name) + index_name = self.body.get('IndexName') + if index_name: + all_indexes = (table.global_indexes or []) + (table.indexes or []) + indexes_by_name = dict((i['IndexName'], i) for i in all_indexes) + if index_name not in indexes_by_name: + raise ValueError('Invalid index: %s for table: %s. Available indexes are: %s' % ( + index_name, name, ', '.join(indexes_by_name.keys()) + )) + + index = indexes_by_name[index_name]['KeySchema'] + else: + index = table.schema + + key_map = [column for _, column in sorted((k, v) for k, v in self.body['ExpressionAttributeNames'].items())] + if " AND " in key_condition_expression: expressions = key_condition_expression.split(" AND ", 1) - hash_key_expression = expressions[0] + + index_hash_key = [key for key in index if key['KeyType'] == 'HASH'][0] + hash_key_index_in_key_map = key_map.index(index_hash_key['AttributeName']) + + hash_key_expression = expressions.pop(hash_key_index_in_key_map).strip('()') # TODO implement more than one range expression and OR operators - range_key_expression = expressions[1].replace(")", "") + range_key_expression = expressions[0].strip('()') range_key_expression_components = range_key_expression.split() range_comparison = range_key_expression_components[1] if 'AND' in range_key_expression: @@ -293,24 +316,26 @@ class DynamoHandler(BaseResponse): range_comparison = None range_values = [] - items, last_page = dynamodb_backend2.query(name, hash_key, range_comparison, range_values) + index_name = self.body.get('IndexName') + items, last_page = dynamodb_backend2.query(name, hash_key, range_comparison, range_values, index_name=index_name) if items is None: er = 'com.amazonaws.dynamodb.v20111205#ResourceNotFoundException' return self.error(er) - limit = self.body.get("Limit") - if limit: - items = items[:limit] - reversed = self.body.get("ScanIndexForward") if reversed is False: items.reverse() + limit = self.body.get("Limit") + if limit: + items = items[:limit] + result = { "Count": len(items), - "Items": [item.attrs for item in items], "ConsumedCapacityUnits": 1, } + if self.body.get('Select', '').upper() != 'COUNT': + result["Items"] = [item.attrs for item in items] # Implement this when we do pagination # if not last_page: diff --git a/moto/ec2/exceptions.py b/moto/ec2/exceptions.py index 5c3d64727..9e8f93004 100644 --- a/moto/ec2/exceptions.py +++ b/moto/ec2/exceptions.py @@ -92,6 +92,14 @@ class InvalidVpnConnectionIdError(EC2ClientError): .format(network_acl_id)) +class InvalidCustomerGatewayIdError(EC2ClientError): + def __init__(self, customer_gateway_id): + super(InvalidCustomerGatewayIdError, self).__init__( + "InvalidCustomerGatewayID.NotFound", + "The customer gateway ID '{0}' does not exist" + .format(customer_gateway_id)) + + class InvalidNetworkInterfaceIdError(EC2ClientError): def __init__(self, eni_id): super(InvalidNetworkInterfaceIdError, self).__init__( diff --git a/moto/ec2/models.py b/moto/ec2/models.py index 4ae2c00c3..779322df1 100644 --- a/moto/ec2/models.py +++ b/moto/ec2/models.py @@ -55,7 +55,8 @@ from .exceptions import ( InvalidCIDRSubnetError, InvalidNetworkAclIdError, InvalidVpnGatewayIdError, - InvalidVpnConnectionIdError + InvalidVpnConnectionIdError, + InvalidCustomerGatewayIdError, ) from .utils import ( EC2_RESOURCE_TO_PREFIX, @@ -95,6 +96,7 @@ from .utils import ( random_network_acl_subnet_association_id, random_vpn_gateway_id, random_vpn_connection_id, + random_customer_gateway_id, is_tag_filter, ) @@ -340,6 +342,9 @@ class Instance(BotoInstance, TaggedEC2Resource): if self.subnet_id: subnet = ec2_backend.get_subnet(self.subnet_id) self.vpc_id = subnet.vpc_id + self._placement.zone = subnet.availability_zone + else: + self._placement.zone = ec2_backend.region_name + 'a' self.block_device_mapping = BlockDeviceMapping() @@ -1430,6 +1435,36 @@ class Volume(TaggedEC2Resource): else: return 'available' + def get_filter_value(self, filter_name): + + if filter_name.startswith('attachment') and not self.attachment: + return None + if filter_name == 'attachment.attach-time': + return self.attachment.attach_time + if filter_name == 'attachment.device': + return self.attachment.device + if filter_name == 'attachment.instance-id': + return self.attachment.instance.id + + if filter_name == 'create-time': + return self.create_time + + if filter_name == 'size': + return self.size + + if filter_name == 'snapshot-id': + return self.snapshot_id + + if filter_name == 'status': + return self.status + + filter_value = super(Volume, self).get_filter_value(filter_name) + + if filter_value is None: + self.ec2_backend.raise_not_implemented_error("The filter '{0}' for DescribeVolumes".format(filter_name)) + + return filter_value + class Snapshot(TaggedEC2Resource): def __init__(self, ec2_backend, snapshot_id, volume, description): @@ -1440,6 +1475,30 @@ class Snapshot(TaggedEC2Resource): self.create_volume_permission_groups = set() self.ec2_backend = ec2_backend + def get_filter_value(self, filter_name): + + if filter_name == 'description': + return self.description + + if filter_name == 'snapshot-id': + return self.id + + if filter_name == 'start-time': + return self.start_time + + if filter_name == 'volume-id': + return self.volume.id + + if filter_name == 'volume-size': + return self.volume.size + + filter_value = super(Snapshot, self).get_filter_value(filter_name) + + if filter_value is None: + self.ec2_backend.raise_not_implemented_error("The filter '{0}' for DescribeSnapshots".format(filter_name)) + + return filter_value + class EBSBackend(object): def __init__(self): @@ -1459,7 +1518,10 @@ class EBSBackend(object): self.volumes[volume_id] = volume return volume - def describe_volumes(self): + def describe_volumes(self, filters=None): + if filters: + volumes = self.volumes.values() + return generic_filter(filters, volumes) return self.volumes.values() def get_volume(self, volume_id): @@ -1505,7 +1567,10 @@ class EBSBackend(object): self.snapshots[snapshot_id] = snapshot return snapshot - def describe_snapshots(self): + def describe_snapshots(self, filters=None): + if filters: + snapshots = self.snapshots.values() + return generic_filter(filters, snapshots) return self.snapshots.values() def get_snapshot(self, snapshot_id): @@ -2798,6 +2863,45 @@ class VpnGatewayBackend(object): return detached +class CustomerGateway(TaggedEC2Resource): + def __init__(self, ec2_backend, id, type, ip_address, bgp_asn): + self.ec2_backend = ec2_backend + self.id = id + self.type = type + self.ip_address = ip_address + self.bgp_asn = bgp_asn + self.attachments = {} + super(CustomerGateway, self).__init__() + + +class CustomerGatewayBackend(object): + def __init__(self): + self.customer_gateways = {} + super(CustomerGatewayBackend, self).__init__() + + def create_customer_gateway(self, type='ipsec.1', ip_address=None, bgp_asn=None): + customer_gateway_id = random_customer_gateway_id() + customer_gateway = CustomerGateway(self, customer_gateway_id, type, ip_address, bgp_asn) + self.customer_gateways[customer_gateway_id] = customer_gateway + return customer_gateway + + def get_all_customer_gateways(self, filters=None): + customer_gateways = self.customer_gateways.values() + return generic_filter(filters, customer_gateways) + + def get_customer_gateway(self, customer_gateway_id): + customer_gateway = self.customer_gateways.get(customer_gateway_id, None) + if not customer_gateway: + raise InvalidCustomerGatewayIdError(customer_gateway_id) + return customer_gateway + + def delete_customer_gateway(self, customer_gateway_id): + deleted = self.customer_gateways.pop(customer_gateway_id, None) + if not deleted: + raise InvalidCustomerGatewayIdError(customer_gateway_id) + return deleted + + class EC2Backend(BaseBackend, InstanceBackend, TagBackend, AmiBackend, RegionsAndZonesBackend, SecurityGroupBackend, EBSBackend, VPCBackend, SubnetBackend, SubnetRouteTableAssociationBackend, @@ -2806,7 +2910,7 @@ class EC2Backend(BaseBackend, InstanceBackend, TagBackend, AmiBackend, RouteTableBackend, RouteBackend, InternetGatewayBackend, VPCGatewayAttachmentBackend, SpotRequestBackend, ElasticAddressBackend, KeyPairBackend, DHCPOptionsSetBackend, - NetworkAclBackend, VpnGatewayBackend): + NetworkAclBackend, VpnGatewayBackend, CustomerGatewayBackend): def __init__(self, region_name): super(EC2Backend, self).__init__() @@ -2831,7 +2935,7 @@ class EC2Backend(BaseBackend, InstanceBackend, TagBackend, AmiBackend, for resource_id in resource_ids: resource_prefix = get_prefix(resource_id) if resource_prefix == EC2_RESOURCE_TO_PREFIX['customer-gateway']: - self.raise_not_implemented_error('DescribeCustomerGateways') + self.get_customer_gateway(customer_gateway_id=resource_id) elif resource_prefix == EC2_RESOURCE_TO_PREFIX['dhcp-options']: self.describe_dhcp_options(options_ids=[resource_id]) elif resource_prefix == EC2_RESOURCE_TO_PREFIX['image']: diff --git a/moto/ec2/responses/customer_gateways.py b/moto/ec2/responses/customer_gateways.py index 150ed755b..a6d460c88 100644 --- a/moto/ec2/responses/customer_gateways.py +++ b/moto/ec2/responses/customer_gateways.py @@ -1,13 +1,82 @@ from __future__ import unicode_literals from moto.core.responses import BaseResponse +from moto.ec2.utils import filters_from_querystring class CustomerGateways(BaseResponse): + def create_customer_gateway(self): - raise NotImplementedError('CustomerGateways(AmazonVPC).create_customer_gateway is not yet implemented') + # raise NotImplementedError('CustomerGateways(AmazonVPC).create_customer_gateway is not yet implemented') + type = self.querystring.get('Type', None)[0] + ip_address = self.querystring.get('IpAddress', None)[0] + bgp_asn = self.querystring.get('BgpAsn', None)[0] + customer_gateway = self.ec2_backend.create_customer_gateway(type, ip_address=ip_address, bgp_asn=bgp_asn) + template = self.response_template(CREATE_CUSTOMER_GATEWAY_RESPONSE) + return template.render(customer_gateway=customer_gateway) def delete_customer_gateway(self): - raise NotImplementedError('CustomerGateways(AmazonVPC).delete_customer_gateway is not yet implemented') + customer_gateway_id = self.querystring.get('CustomerGatewayId')[0] + delete_status = self.ec2_backend.delete_customer_gateway(customer_gateway_id) + template = self.response_template(DELETE_CUSTOMER_GATEWAY_RESPONSE) + return template.render(customer_gateway=delete_status) def describe_customer_gateways(self): - raise NotImplementedError('CustomerGateways(AmazonVPC).describe_customer_gateways is not yet implemented') + filters = filters_from_querystring(self.querystring) + customer_gateways = self.ec2_backend.get_all_customer_gateways(filters) + template = self.response_template(DESCRIBE_CUSTOMER_GATEWAYS_RESPONSE) + return template.render(customer_gateways=customer_gateways) + + +CREATE_CUSTOMER_GATEWAY_RESPONSE = """ + + 7a62c49f-347e-4fc4-9331-6e8eEXAMPLE + + {{ customer_gateway.id }} + pending + {{ customer_gateway.type }} + {{ customer_gateway.ip_address }} + {{ customer_gateway.bgp_asn }} + + {% for tag in customer_gateway.get_tags() %} + + {{ tag.resource_id }} + {{ tag.resource_type }} + {{ tag.key }} + {{ tag.value }} + + {% endfor %} + + +""" + +DELETE_CUSTOMER_GATEWAY_RESPONSE = """ + + 7a62c49f-347e-4fc4-9331-6e8eEXAMPLE + {{ delete_status }} +""" + +DESCRIBE_CUSTOMER_GATEWAYS_RESPONSE = """ + + 7a62c49f-347e-4fc4-9331-6e8eEXAMPLE + + {% for customer_gateway in customer_gateways %} + + {{ customer_gateway.id }} + {{ customer_gateway.state }} + available + {{ customer_gateway.ip_address }} + {{ customer_gateway.bgp_asn }} + + {% for tag in customer_gateway.get_tags() %} + + {{ tag.resource_id }} + {{ tag.resource_type }} + {{ tag.key }} + {{ tag.value }} + + {% endfor %} + + + {% endfor %} + +""" diff --git a/moto/ec2/responses/elastic_block_store.py b/moto/ec2/responses/elastic_block_store.py index 5adb4c7d0..2bb4af3a8 100644 --- a/moto/ec2/responses/elastic_block_store.py +++ b/moto/ec2/responses/elastic_block_store.py @@ -1,5 +1,6 @@ from __future__ import unicode_literals from moto.core.responses import BaseResponse +from moto.ec2.utils import filters_from_querystring class ElasticBlockStore(BaseResponse): @@ -43,22 +44,22 @@ class ElasticBlockStore(BaseResponse): return DELETE_VOLUME_RESPONSE def describe_snapshots(self): + filters = filters_from_querystring(self.querystring) # querystring for multiple snapshotids results in SnapshotId.1, SnapshotId.2 etc snapshot_ids = ','.join([','.join(s[1]) for s in self.querystring.items() if 'SnapshotId' in s[0]]) - snapshots = self.ec2_backend.describe_snapshots() + snapshots = self.ec2_backend.describe_snapshots(filters=filters) # Describe snapshots to handle filter on snapshot_ids snapshots = [s for s in snapshots if s.id in snapshot_ids] if snapshot_ids else snapshots - # snapshots = self.ec2_backend.describe_snapshots() template = self.response_template(DESCRIBE_SNAPSHOTS_RESPONSE) return template.render(snapshots=snapshots) def describe_volumes(self): + filters = filters_from_querystring(self.querystring) # querystring for multiple volumeids results in VolumeId.1, VolumeId.2 etc volume_ids = ','.join([','.join(v[1]) for v in self.querystring.items() if 'VolumeId' in v[0]]) - volumes = self.ec2_backend.describe_volumes() + volumes = self.ec2_backend.describe_volumes(filters=filters) # Describe volumes to handle filter on volume_ids volumes = [v for v in volumes if v.id in volume_ids] if volume_ids else volumes - # volumes = self.ec2_backend.describe_volumes() template = self.response_template(DESCRIBE_VOLUMES_RESPONSE) return template.render(volumes=volumes) diff --git a/moto/ec2/utils.py b/moto/ec2/utils.py index e0edde7d6..5b7743bf4 100644 --- a/moto/ec2/utils.py +++ b/moto/ec2/utils.py @@ -89,6 +89,10 @@ def random_vpn_connection_id(): return random_id(prefix=EC2_RESOURCE_TO_PREFIX['vpn-connection']) +def random_customer_gateway_id(): + return random_id(prefix=EC2_RESOURCE_TO_PREFIX['customer-gateway']) + + def random_volume_id(): return random_id(prefix=EC2_RESOURCE_TO_PREFIX['volume']) @@ -314,7 +318,7 @@ def get_object_value(obj, attr): def is_tag_filter(filter_name): - return (filter_name.startswith('tag:') or + return (filter_name.startswith('tag:') or filter_name.startswith('tag-value') or filter_name.startswith('tag-key')) diff --git a/moto/ecs/__init__.py b/moto/ecs/__init__.py new file mode 100644 index 000000000..0844b256c --- /dev/null +++ b/moto/ecs/__init__.py @@ -0,0 +1,11 @@ +from __future__ import unicode_literals +from .models import ecs_backends +from ..core.models import MockAWS + +ecs_backend = ecs_backends['us-east-1'] + +def mock_ecs(func=None): + if func: + return MockAWS(ecs_backends)(func) + else: + return MockAWS(ecs_backends) diff --git a/moto/ecs/models.py b/moto/ecs/models.py new file mode 100644 index 000000000..820c99fd1 --- /dev/null +++ b/moto/ecs/models.py @@ -0,0 +1,204 @@ +from __future__ import unicode_literals +import uuid + +from moto.core import BaseBackend +from moto.ec2 import ec2_backends + + +class BaseObject(object): + def camelCase(self, key): + words = [] + for i, word in enumerate(key.split('_')): + if i > 0: + words.append(word.title()) + else: + words.append(word) + return ''.join(words) + + def gen_response_object(self): + response_object = self.__dict__.copy() + for key, value in response_object.items(): + if '_' in key: + response_object[self.camelCase(key)] = value + del response_object[key] + return response_object + + @property + def response_object(self): + return self.gen_response_object() + + +class Cluster(BaseObject): + def __init__(self, cluster_name): + self.active_services_count = 0 + self.arn = 'arn:aws:ecs:us-east-1:012345678910:cluster/{0}'.format(cluster_name) + self.name = cluster_name + self.pending_tasks_count = 0 + self.registered_container_instances_count = 0 + self.running_tasks_count = 0 + self.status = 'ACTIVE' + + @property + def response_object(self): + response_object = self.gen_response_object() + response_object['clusterArn'] = self.arn + response_object['clusterName'] = self.name + del response_object['arn'], response_object['name'] + return response_object + + +class TaskDefinition(BaseObject): + def __init__(self, family, revision, container_definitions, volumes=None): + self.family = family + self.arn = 'arn:aws:ecs:us-east-1:012345678910:task-definition/{0}:{1}'.format(family, revision) + self.container_definitions = container_definitions + if volumes is not None: + self.volumes = volumes + + @property + def response_object(self): + response_object = self.gen_response_object() + response_object['taskDefinitionArn'] = response_object['arn'] + del response_object['arn'] + return response_object + + +class Service(BaseObject): + def __init__(self, cluster, service_name, task_definition, desired_count): + self.cluster_arn = cluster.arn + self.arn = 'arn:aws:ecs:us-east-1:012345678910:service/{0}'.format(service_name) + self.name = service_name + self.status = 'ACTIVE' + self.running_count = 0 + self.task_definition = task_definition.arn + self.desired_count = desired_count + self.events = [] + self.load_balancers = [] + self.pending_count = 0 + + @property + def response_object(self): + response_object = self.gen_response_object() + del response_object['name'], response_object['arn'] + response_object['serviceName'] = self.name + response_object['serviceArn'] = self.arn + return response_object + + +class EC2ContainerServiceBackend(BaseBackend): + def __init__(self): + self.clusters = {} + self.task_definitions = {} + self.services = {} + + def fetch_task_definition(self, task_definition_str): + task_definition_components = task_definition_str.split(':') + if len(task_definition_components) == 2: + family, revision = task_definition_components + revision = int(revision) + else: + family = task_definition_components[0] + revision = -1 + if family in self.task_definitions and 0 < revision <= len(self.task_definitions[family]): + return self.task_definitions[family][revision - 1] + elif family in self.task_definitions and revision == -1: + return self.task_definitions[family][revision] + else: + raise Exception("{0} is not a task_definition".format(task_definition_str)) + + def create_cluster(self, cluster_name): + cluster = Cluster(cluster_name) + self.clusters[cluster_name] = cluster + return cluster + + def list_clusters(self): + """ + maxSize and pagination not implemented + """ + return [cluster.arn for cluster in self.clusters.values()] + + def delete_cluster(self, cluster_str): + cluster_name = cluster_str.split('/')[-1] + if cluster_name in self.clusters: + return self.clusters.pop(cluster_name) + else: + raise Exception("{0} is not a cluster".format(cluster_name)) + + def register_task_definition(self, family, container_definitions, volumes): + if family in self.task_definitions: + revision = len(self.task_definitions[family]) + 1 + else: + self.task_definitions[family] = [] + revision = 1 + task_definition = TaskDefinition(family, revision, container_definitions, volumes) + self.task_definitions[family].append(task_definition) + + return task_definition + + def list_task_definitions(self): + """ + Filtering not implemented + """ + task_arns = [] + for task_definition_list in self.task_definitions.values(): + task_arns.extend([task_definition.arn for task_definition in task_definition_list]) + return task_arns + + def deregister_task_definition(self, task_definition_str): + task_definition_name = task_definition_str.split('/')[-1] + family, revision = task_definition_name.split(':') + revision = int(revision) + if family in self.task_definitions and 0 < revision <= len(self.task_definitions[family]): + return self.task_definitions[family].pop(revision - 1) + else: + raise Exception("{0} is not a task_definition".format(task_definition_name)) + + def create_service(self, cluster_str, service_name, task_definition_str, desired_count): + cluster_name = cluster_str.split('/')[-1] + if cluster_name in self.clusters: + cluster = self.clusters[cluster_name] + else: + raise Exception("{0} is not a cluster".format(cluster_name)) + task_definition = self.fetch_task_definition(task_definition_str) + desired_count = desired_count if desired_count is not None else 0 + service = Service(cluster, service_name, task_definition, desired_count) + cluster_service_pair = '{0}:{1}'.format(cluster_name, service_name) + self.services[cluster_service_pair] = service + return service + + def list_services(self, cluster_str): + cluster_name = cluster_str.split('/')[-1] + service_arns = [] + for key, value in self.services.items(): + if cluster_name + ':' in key: + service_arns.append(self.services[key].arn) + return sorted(service_arns) + + def update_service(self, cluster_str, service_name, task_definition_str, desired_count): + cluster_name = cluster_str.split('/')[-1] + cluster_service_pair = '{0}:{1}'.format(cluster_name, service_name) + if cluster_service_pair in self.services: + if task_definition_str is not None: + task_definition = self.fetch_task_definition(task_definition_str) + self.services[cluster_service_pair].task_definition = task_definition + if desired_count is not None: + self.services[cluster_service_pair].desired_count = desired_count + return self.services[cluster_service_pair] + else: + raise Exception("cluster {0} or service {1} does not exist".format(cluster_name, service_name)) + + def delete_service(self, cluster_name, service_name): + cluster_service_pair = '{0}:{1}'.format(cluster_name, service_name) + if cluster_service_pair in self.services: + service = self.services[cluster_service_pair] + if service.desired_count > 0: + raise Exception("Service must have desiredCount=0") + else: + return self.services.pop(cluster_service_pair) + else: + raise Exception("cluster {0} or service {1} does not exist".format(cluster_name, service_name)) + + +ecs_backends = {} +for region, ec2_backend in ec2_backends.items(): + ecs_backends[region] = EC2ContainerServiceBackend() diff --git a/moto/ecs/responses.py b/moto/ecs/responses.py new file mode 100644 index 000000000..de2fdb48b --- /dev/null +++ b/moto/ecs/responses.py @@ -0,0 +1,102 @@ +from __future__ import unicode_literals +import json +import uuid + +from moto.core.responses import BaseResponse +from .models import ecs_backends + + +class EC2ContainerServiceResponse(BaseResponse): + @property + def ecs_backend(self): + return ecs_backends[self.region] + + @property + def request_params(self): + try: + return json.loads(self.body.decode()) + except ValueError: + return {} + + def _get_param(self, param): + return self.request_params.get(param, None) + + def create_cluster(self): + cluster_name = self._get_param('clusterName') + cluster = self.ecs_backend.create_cluster(cluster_name) + return json.dumps({ + 'cluster': cluster.response_object + }) + + def list_clusters(self): + cluster_arns = self.ecs_backend.list_clusters() + return json.dumps({ + 'clusterArns': cluster_arns, + 'nextToken': str(uuid.uuid1()) + }) + + def delete_cluster(self): + cluster_str = self._get_param('cluster') + cluster = self.ecs_backend.delete_cluster(cluster_str) + return json.dumps({ + 'cluster': cluster.response_object + }) + + def register_task_definition(self): + family = self._get_param('family') + container_definitions = self._get_param('containerDefinitions') + volumes = self._get_param('volumes') + task_definition = self.ecs_backend.register_task_definition(family, container_definitions, volumes) + return json.dumps({ + 'taskDefinition': task_definition.response_object + }) + + def list_task_definitions(self): + task_definition_arns = self.ecs_backend.list_task_definitions() + return json.dumps({ + 'taskDefinitionArns': task_definition_arns, + 'nextToken': str(uuid.uuid1()) + }) + + def deregister_task_definition(self): + task_definition_str = self._get_param('taskDefinition') + task_definition = self.ecs_backend.deregister_task_definition(task_definition_str) + return json.dumps({ + 'taskDefinition': task_definition.response_object + }) + + def create_service(self): + cluster_str = self._get_param('cluster') + service_name = self._get_param('serviceName') + task_definition_str = self._get_param('taskDefinition') + desired_count = self._get_int_param('desiredCount') + service = self.ecs_backend.create_service(cluster_str, service_name, task_definition_str, desired_count) + return json.dumps({ + 'service': service.response_object + }) + + def list_services(self): + cluster_str = self._get_param('cluster') + service_arns = self.ecs_backend.list_services(cluster_str) + return json.dumps({ + 'serviceArns': service_arns, + 'nextToken': str(uuid.uuid1()) + }) + + def update_service(self): + cluster_str = self._get_param('cluster') + service_name = self._get_param('service') + task_definition = self._get_param('taskDefinition') + desired_count = self._get_int_param('desiredCount') + service = self.ecs_backend.update_service(cluster_str, service_name, task_definition, desired_count) + return json.dumps({ + 'service': service.response_object + }) + + def delete_service(self): + service_name = self._get_param('service') + cluster_name = self._get_param('cluster') + service = self.ecs_backend.delete_service(cluster_name, service_name) + return json.dumps({ + 'service': service.response_object + }) diff --git a/moto/ecs/urls.py b/moto/ecs/urls.py new file mode 100644 index 000000000..1e0d5fbf9 --- /dev/null +++ b/moto/ecs/urls.py @@ -0,0 +1,10 @@ +from __future__ import unicode_literals +from .responses import EC2ContainerServiceResponse + +url_bases = [ + "https?://ecs.(.+).amazonaws.com", +] + +url_paths = { + '{0}/$': EC2ContainerServiceResponse.dispatch, +} diff --git a/tests/test_autoscaling/test_autoscaling.py b/tests/test_autoscaling/test_autoscaling.py index 81a331dfb..6a5f768bf 100644 --- a/tests/test_autoscaling/test_autoscaling.py +++ b/tests/test_autoscaling/test_autoscaling.py @@ -1,5 +1,6 @@ from __future__ import unicode_literals import boto +import boto3 import boto.ec2.autoscale from boto.ec2.autoscale.launchconfig import LaunchConfiguration from boto.ec2.autoscale.group import AutoScalingGroup @@ -103,7 +104,7 @@ def test_create_autoscaling_groups_defaults(): group.desired_capacity.should.equal(2) group.vpc_zone_identifier.should.equal('') group.default_cooldown.should.equal(300) - group.health_check_period.should.equal(None) + group.health_check_period.should.equal(300) group.health_check_type.should.equal("EC2") list(group.load_balancers).should.equal([]) group.placement_group.should.equal(None) @@ -378,3 +379,43 @@ def test_autoscaling_group_with_elb(): elb = elb_conn.get_all_load_balancers()[0] elb.instances.should.have.length_of(0) + +''' +Boto3 +''' + + +@mock_autoscaling +def test_create_autoscaling_group(): + client = boto3.client('autoscaling', region_name='us-east-1') + _ = client.create_launch_configuration( + LaunchConfigurationName='test_launch_configuration' + ) + response = client.create_auto_scaling_group( + AutoScalingGroupName='test_asg', + LaunchConfigurationName='test_launch_configuration', + MinSize=0, + MaxSize=20, + DesiredCapacity=5 + ) + response['ResponseMetadata']['HTTPStatusCode'].should.equal(200) + + +@mock_autoscaling +def test_describe_autoscaling_groups(): + client = boto3.client('autoscaling', region_name='us-east-1') + _ = client.create_launch_configuration( + LaunchConfigurationName='test_launch_configuration' + ) + _ = client.create_auto_scaling_group( + AutoScalingGroupName='test_asg', + LaunchConfigurationName='test_launch_configuration', + MinSize=0, + MaxSize=20, + DesiredCapacity=5 + ) + response = client.describe_auto_scaling_groups( + AutoScalingGroupNames=["test_asg"] + ) + response['ResponseMetadata']['HTTPStatusCode'].should.equal(200) + response['AutoScalingGroups'][0]['AutoScalingGroupName'].should.equal('test_asg') \ No newline at end of file diff --git a/tests/test_dynamodb2/test_dynamodb_table_with_range_key.py b/tests/test_dynamodb2/test_dynamodb_table_with_range_key.py index 12e3aa15b..a90f06caf 100644 --- a/tests/test_dynamodb2/test_dynamodb_table_with_range_key.py +++ b/tests/test_dynamodb2/test_dynamodb_table_with_range_key.py @@ -1,5 +1,7 @@ from __future__ import unicode_literals +from decimal import Decimal + import boto import boto3 from boto3.dynamodb.conditions import Key @@ -141,6 +143,36 @@ def test_item_add_and_describe_and_update(): }) +@requires_boto_gte("2.9") +@mock_dynamodb2 +def test_item_partial_save(): + table = create_table() + + data = { + 'forum_name': 'LOLCat Forum', + 'subject': 'The LOLz', + 'Body': 'http://url_to_lolcat.gif', + 'SentBy': 'User A', + } + + table.put_item(data=data) + returned_item = table.get_item(forum_name="LOLCat Forum", subject='The LOLz') + + returned_item['SentBy'] = 'User B' + returned_item.partial_save() + + returned_item = table.get_item( + forum_name='LOLCat Forum', + subject='The LOLz' + ) + dict(returned_item).should.equal({ + 'forum_name': 'LOLCat Forum', + 'subject': 'The LOLz', + 'Body': 'http://url_to_lolcat.gif', + 'SentBy': 'User B', + }) + + @requires_boto_gte("2.9") @mock_dynamodb2 def test_item_put_without_table(): @@ -538,6 +570,30 @@ def test_query_with_global_indexes(): list(results).should.have.length_of(0) +@mock_dynamodb2 +def test_reverse_query(): + conn = boto.dynamodb2.layer1.DynamoDBConnection() + + table = Table.create('messages', schema=[ + HashKey('subject'), + RangeKey('created_at', data_type='N') + ]) + + for i in range(10): + table.put_item({ + 'subject': "Hi", + 'created_at': i + }) + + results = table.query_2(subject__eq="Hi", + created_at__lt=6, + limit=4, + reverse=True) + + expected = [Decimal(5), Decimal(4), Decimal(3), Decimal(2)] + [r['created_at'] for r in results].should.equal(expected) + + @mock_dynamodb2 def test_lookup(): from decimal import Decimal @@ -693,7 +749,7 @@ def test_boto3_conditions(): results['Count'].should.equal(1) results = table.query( - KeyConditionExpression=Key('forum_name').eq('the-key') & Key("subject").begins_with('7') + KeyConditionExpression=Key("subject").begins_with('7') & Key('forum_name').eq('the-key') ) results['Count'].should.equal(1) @@ -701,3 +757,471 @@ def test_boto3_conditions(): KeyConditionExpression=Key('forum_name').eq('the-key') & Key("subject").between('567', '890') ) results['Count'].should.equal(1) + + +def _create_table_with_range_key(): + dynamodb = boto3.resource('dynamodb', region_name='us-east-1') + + # Create the DynamoDB table. + table = dynamodb.create_table( + TableName='users', + KeySchema=[ + { + 'AttributeName': 'forum_name', + 'KeyType': 'HASH' + }, + { + 'AttributeName': 'subject', + 'KeyType': 'RANGE' + }, + ], + GlobalSecondaryIndexes=[{ + 'IndexName': 'TestGSI', + 'KeySchema': [ + { + 'AttributeName': 'username', + 'KeyType': 'HASH', + }, + { + 'AttributeName': 'created', + 'KeyType': 'RANGE', + } + ], + 'Projection': { + 'ProjectionType': 'ALL', + }, + 'ProvisionedThroughput': { + 'ReadCapacityUnits': 5, + 'WriteCapacityUnits': 5 + } + }], + AttributeDefinitions=[ + { + 'AttributeName': 'forum_name', + 'AttributeType': 'S' + }, + { + 'AttributeName': 'subject', + 'AttributeType': 'S' + }, + ], + ProvisionedThroughput={ + 'ReadCapacityUnits': 5, + 'WriteCapacityUnits': 5 + } + ) + return dynamodb.Table('users') + + +@mock_dynamodb2 +def test_update_item_range_key_set(): + table = _create_table_with_range_key() + table.put_item(Item={ + 'forum_name': 'the-key', + 'subject': '123', + 'username': 'johndoe', + 'created': Decimal('3'), + }) + + item_key = {'forum_name': 'the-key', 'subject': '123'} + table.update_item( + Key=item_key, + AttributeUpdates={ + 'username': { + 'Action': u'PUT', + 'Value': 'johndoe2' + }, + 'created': { + 'Action': u'PUT', + 'Value': Decimal('4'), + }, + 'mapfield': { + 'Action': u'PUT', + 'Value': {'key': 'value'}, + } + }, + ) + + returned_item = dict((k, str(v) if isinstance(v, Decimal) else v) + for k, v in table.get_item(Key=item_key)['Item'].items()) + dict(returned_item).should.equal({ + 'username': "johndoe2", + 'forum_name': 'the-key', + 'subject': '123', + 'created': '4', + 'mapfield': {'key': 'value'}, + }) + + +@mock_dynamodb2 +def test_boto3_query_gsi_range_comparison(): + table = _create_table_with_range_key() + + table.put_item(Item={ + 'forum_name': 'the-key', + 'subject': '123', + 'username': 'johndoe', + 'created': 3, + }) + table.put_item(Item={ + 'forum_name': 'the-key', + 'subject': '456', + 'username': 'johndoe', + 'created': 1, + }) + table.put_item(Item={ + 'forum_name': 'the-key', + 'subject': '789', + 'username': 'johndoe', + 'created': 2, + }) + table.put_item(Item={ + 'forum_name': 'the-key', + 'subject': '159', + 'username': 'janedoe', + 'created': 2, + }) + table.put_item(Item={ + 'forum_name': 'the-key', + 'subject': '601', + 'username': 'janedoe', + 'created': 5, + }) + + # Test a query returning all johndoe items + results = table.query( + KeyConditionExpression=Key('username').eq('johndoe') & Key("created").gt('0'), + ScanIndexForward=True, + IndexName='TestGSI', + ) + expected = ["456", "789", "123"] + for index, item in enumerate(results['Items']): + item["subject"].should.equal(expected[index]) + + # Return all johndoe items again, but in reverse + results = table.query( + KeyConditionExpression=Key('username').eq('johndoe') & Key("created").gt('0'), + ScanIndexForward=False, + IndexName='TestGSI', + ) + for index, item in enumerate(reversed(results['Items'])): + item["subject"].should.equal(expected[index]) + + # Filter the creation to only return some of the results + # And reverse order of hash + range key + results = table.query( + KeyConditionExpression=Key("created").gt('1') & Key('username').eq('johndoe'), + ConsistentRead=True, + IndexName='TestGSI', + ) + results['Count'].should.equal(2) + + # Filter to return no results + results = table.query( + KeyConditionExpression=Key('username').eq('janedoe') & Key("created").gt('9'), + IndexName='TestGSI', + ) + results['Count'].should.equal(0) + + results = table.query( + KeyConditionExpression=Key('username').eq('janedoe') & Key("created").eq('5'), + IndexName='TestGSI', + ) + results['Count'].should.equal(1) + + # Test range key sorting + results = table.query( + KeyConditionExpression=Key('username').eq('johndoe') & Key("created").gt('0'), + IndexName='TestGSI', + ) + expected = [Decimal('1'), Decimal('2'), Decimal('3')] + for index, item in enumerate(results['Items']): + item["created"].should.equal(expected[index]) + + +@mock_dynamodb2 +def test_update_table_throughput(): + dynamodb = boto3.resource('dynamodb', region_name='us-east-1') + + # Create the DynamoDB table. + table = dynamodb.create_table( + TableName='users', + KeySchema=[ + { + 'AttributeName': 'forum_name', + 'KeyType': 'HASH' + }, + { + 'AttributeName': 'subject', + 'KeyType': 'RANGE' + }, + ], + AttributeDefinitions=[ + { + 'AttributeName': 'forum_name', + 'AttributeType': 'S' + }, + { + 'AttributeName': 'subject', + 'AttributeType': 'S' + }, + ], + ProvisionedThroughput={ + 'ReadCapacityUnits': 5, + 'WriteCapacityUnits': 6 + } + ) + table = dynamodb.Table('users') + + table.provisioned_throughput['ReadCapacityUnits'].should.equal(5) + table.provisioned_throughput['WriteCapacityUnits'].should.equal(6) + + table.update(ProvisionedThroughput={ + 'ReadCapacityUnits': 10, + 'WriteCapacityUnits': 11, + }) + + table = dynamodb.Table('users') + + table.provisioned_throughput['ReadCapacityUnits'].should.equal(10) + table.provisioned_throughput['WriteCapacityUnits'].should.equal(11) + + +@mock_dynamodb2 +def test_update_table_gsi_throughput(): + dynamodb = boto3.resource('dynamodb', region_name='us-east-1') + + # Create the DynamoDB table. + table = dynamodb.create_table( + TableName='users', + KeySchema=[ + { + 'AttributeName': 'forum_name', + 'KeyType': 'HASH' + }, + { + 'AttributeName': 'subject', + 'KeyType': 'RANGE' + }, + ], + GlobalSecondaryIndexes=[{ + 'IndexName': 'TestGSI', + 'KeySchema': [ + { + 'AttributeName': 'username', + 'KeyType': 'HASH', + }, + { + 'AttributeName': 'created', + 'KeyType': 'RANGE', + } + ], + 'Projection': { + 'ProjectionType': 'ALL', + }, + 'ProvisionedThroughput': { + 'ReadCapacityUnits': 3, + 'WriteCapacityUnits': 4 + } + }], + AttributeDefinitions=[ + { + 'AttributeName': 'forum_name', + 'AttributeType': 'S' + }, + { + 'AttributeName': 'subject', + 'AttributeType': 'S' + }, + ], + ProvisionedThroughput={ + 'ReadCapacityUnits': 5, + 'WriteCapacityUnits': 6 + } + ) + table = dynamodb.Table('users') + + gsi_throughput = table.global_secondary_indexes[0]['ProvisionedThroughput'] + gsi_throughput['ReadCapacityUnits'].should.equal(3) + gsi_throughput['WriteCapacityUnits'].should.equal(4) + + table.provisioned_throughput['ReadCapacityUnits'].should.equal(5) + table.provisioned_throughput['WriteCapacityUnits'].should.equal(6) + + table.update(GlobalSecondaryIndexUpdates=[{ + 'Update': { + 'IndexName': 'TestGSI', + 'ProvisionedThroughput': { + 'ReadCapacityUnits': 10, + 'WriteCapacityUnits': 11, + } + }, + }]) + + table = dynamodb.Table('users') + + # Primary throughput has not changed + table.provisioned_throughput['ReadCapacityUnits'].should.equal(5) + table.provisioned_throughput['WriteCapacityUnits'].should.equal(6) + + gsi_throughput = table.global_secondary_indexes[0]['ProvisionedThroughput'] + gsi_throughput['ReadCapacityUnits'].should.equal(10) + gsi_throughput['WriteCapacityUnits'].should.equal(11) + + + +@mock_dynamodb2 +def test_update_table_gsi_create(): + dynamodb = boto3.resource('dynamodb', region_name='us-east-1') + + # Create the DynamoDB table. + table = dynamodb.create_table( + TableName='users', + KeySchema=[ + { + 'AttributeName': 'forum_name', + 'KeyType': 'HASH' + }, + { + 'AttributeName': 'subject', + 'KeyType': 'RANGE' + }, + ], + AttributeDefinitions=[ + { + 'AttributeName': 'forum_name', + 'AttributeType': 'S' + }, + { + 'AttributeName': 'subject', + 'AttributeType': 'S' + }, + ], + ProvisionedThroughput={ + 'ReadCapacityUnits': 5, + 'WriteCapacityUnits': 6 + } + ) + table = dynamodb.Table('users') + + table.global_secondary_indexes.should.have.length_of(0) + + table.update(GlobalSecondaryIndexUpdates=[{ + 'Create': { + 'IndexName': 'TestGSI', + 'KeySchema': [ + { + 'AttributeName': 'username', + 'KeyType': 'HASH', + }, + { + 'AttributeName': 'created', + 'KeyType': 'RANGE', + } + ], + 'Projection': { + 'ProjectionType': 'ALL', + }, + 'ProvisionedThroughput': { + 'ReadCapacityUnits': 3, + 'WriteCapacityUnits': 4 + } + }, + }]) + + table = dynamodb.Table('users') + table.global_secondary_indexes.should.have.length_of(1) + + gsi_throughput = table.global_secondary_indexes[0]['ProvisionedThroughput'] + assert gsi_throughput['ReadCapacityUnits'].should.equal(3) + assert gsi_throughput['WriteCapacityUnits'].should.equal(4) + + # Check update works + table.update(GlobalSecondaryIndexUpdates=[{ + 'Update': { + 'IndexName': 'TestGSI', + 'ProvisionedThroughput': { + 'ReadCapacityUnits': 10, + 'WriteCapacityUnits': 11, + } + }, + }]) + table = dynamodb.Table('users') + + gsi_throughput = table.global_secondary_indexes[0]['ProvisionedThroughput'] + assert gsi_throughput['ReadCapacityUnits'].should.equal(10) + assert gsi_throughput['WriteCapacityUnits'].should.equal(11) + + table.update(GlobalSecondaryIndexUpdates=[{ + 'Delete': { + 'IndexName': 'TestGSI', + }, + }]) + + table = dynamodb.Table('users') + table.global_secondary_indexes.should.have.length_of(0) + + +@mock_dynamodb2 +def test_update_table_gsi_throughput(): + dynamodb = boto3.resource('dynamodb', region_name='us-east-1') + + # Create the DynamoDB table. + table = dynamodb.create_table( + TableName='users', + KeySchema=[ + { + 'AttributeName': 'forum_name', + 'KeyType': 'HASH' + }, + { + 'AttributeName': 'subject', + 'KeyType': 'RANGE' + }, + ], + GlobalSecondaryIndexes=[{ + 'IndexName': 'TestGSI', + 'KeySchema': [ + { + 'AttributeName': 'username', + 'KeyType': 'HASH', + }, + { + 'AttributeName': 'created', + 'KeyType': 'RANGE', + } + ], + 'Projection': { + 'ProjectionType': 'ALL', + }, + 'ProvisionedThroughput': { + 'ReadCapacityUnits': 3, + 'WriteCapacityUnits': 4 + } + }], + AttributeDefinitions=[ + { + 'AttributeName': 'forum_name', + 'AttributeType': 'S' + }, + { + 'AttributeName': 'subject', + 'AttributeType': 'S' + }, + ], + ProvisionedThroughput={ + 'ReadCapacityUnits': 5, + 'WriteCapacityUnits': 6 + } + ) + table = dynamodb.Table('users') + table.global_secondary_indexes.should.have.length_of(1) + + table.update(GlobalSecondaryIndexUpdates=[{ + 'Delete': { + 'IndexName': 'TestGSI', + }, + }]) + + table = dynamodb.Table('users') + table.global_secondary_indexes.should.have.length_of(0) diff --git a/tests/test_ec2/test_customer_gateways.py b/tests/test_ec2/test_customer_gateways.py index 80fddf287..efd6ce993 100644 --- a/tests/test_ec2/test_customer_gateways.py +++ b/tests/test_ec2/test_customer_gateways.py @@ -1,10 +1,46 @@ from __future__ import unicode_literals import boto import sure # noqa +from nose.tools import assert_raises +from nose.tools import assert_false +from boto.exception import EC2ResponseError from moto import mock_ec2 @mock_ec2 -def test_customer_gateways(): - pass +def test_create_customer_gateways(): + conn = boto.connect_vpc('the_key', 'the_secret') + + customer_gateway = conn.create_customer_gateway('ipsec.1', '205.251.242.54', 65534) + customer_gateway.should_not.be.none + customer_gateway.id.should.match(r'cgw-\w+') + customer_gateway.type.should.equal('ipsec.1') + customer_gateway.bgp_asn.should.equal(65534) + customer_gateway.ip_address.should.equal('205.251.242.54') + +@mock_ec2 +def test_describe_customer_gateways(): + conn = boto.connect_vpc('the_key', 'the_secret') + customer_gateway = conn.create_customer_gateway('ipsec.1', '205.251.242.54', 65534) + cgws = conn.get_all_customer_gateways() + cgws.should.have.length_of(1) + cgws[0].id.should.match(customer_gateway.id) + +@mock_ec2 +def test_delete_customer_gateways(): + conn = boto.connect_vpc('the_key', 'the_secret') + + customer_gateway = conn.create_customer_gateway('ipsec.1', '205.251.242.54', 65534) + customer_gateway.should_not.be.none + cgws = conn.get_all_customer_gateways() + cgws[0].id.should.match(customer_gateway.id) + deleted = conn.delete_customer_gateway(customer_gateway.id) + cgws = conn.get_all_customer_gateways() + cgws.should.have.length_of(0) + +@mock_ec2 +def test_delete_customer_gateways_bad_id(): + conn = boto.connect_vpc('the_key', 'the_secret') + with assert_raises(EC2ResponseError) as cm: + conn.delete_customer_gateway('cgw-0123abcd') diff --git a/tests/test_ec2/test_elastic_block_store.py b/tests/test_ec2/test_elastic_block_store.py index 06b55841e..3d50e83b7 100644 --- a/tests/test_ec2/test_elastic_block_store.py +++ b/tests/test_ec2/test_elastic_block_store.py @@ -48,6 +48,63 @@ def test_filter_volume_by_id(): vol2.should.have.length_of(2) +@mock_ec2 +def test_volume_filters(): + conn = boto.connect_ec2('the_key', 'the_secret') + + reservation = conn.run_instances('ami-1234abcd') + instance = reservation.instances[0] + + instance.update() + + volume1 = conn.create_volume(80, "us-east-1a") + volume2 = conn.create_volume(36, "us-east-1b") + volume3 = conn.create_volume(20, "us-east-1c") + + snapshot = volume3.create_snapshot(description='testsnap') + volume4 = conn.create_volume(25, "us-east-1a", snapshot=snapshot) + + conn.create_tags([volume1.id], {'testkey1': 'testvalue1'}) + conn.create_tags([volume2.id], {'testkey2': 'testvalue2'}) + + volume1.update() + volume2.update() + volume3.update() + volume4.update() + + block_mapping = instance.block_device_mapping['/dev/sda1'] + + volumes_by_attach_time = conn.get_all_volumes(filters={'attachment.attach-time': block_mapping.attach_time}) + set([vol.id for vol in volumes_by_attach_time]).should.equal(set([block_mapping.volume_id])) + + volumes_by_attach_device = conn.get_all_volumes(filters={'attachment.device': '/dev/sda1'}) + set([vol.id for vol in volumes_by_attach_device]).should.equal(set([block_mapping.volume_id])) + + volumes_by_attach_instance_id = conn.get_all_volumes(filters={'attachment.instance-id': instance.id}) + set([vol.id for vol in volumes_by_attach_instance_id]).should.equal(set([block_mapping.volume_id])) + + volumes_by_create_time = conn.get_all_volumes(filters={'create-time': volume4.create_time}) + set([vol.create_time for vol in volumes_by_create_time]).should.equal(set([volume4.create_time])) + + volumes_by_size = conn.get_all_volumes(filters={'size': volume2.size}) + set([vol.id for vol in volumes_by_size]).should.equal(set([volume2.id])) + + volumes_by_snapshot_id = conn.get_all_volumes(filters={'snapshot-id': snapshot.id}) + set([vol.id for vol in volumes_by_snapshot_id]).should.equal(set([volume4.id])) + + volumes_by_status = conn.get_all_volumes(filters={'status': 'in-use'}) + set([vol.id for vol in volumes_by_status]).should.equal(set([block_mapping.volume_id])) + + volumes_by_tag_key = conn.get_all_volumes(filters={'tag-key': 'testkey1'}) + set([vol.id for vol in volumes_by_tag_key]).should.equal(set([volume1.id])) + + volumes_by_tag_value = conn.get_all_volumes(filters={'tag-value': 'testvalue1'}) + set([vol.id for vol in volumes_by_tag_value]).should.equal(set([volume1.id])) + + volumes_by_tag = conn.get_all_volumes(filters={'tag:testkey1': 'testvalue1'}) + set([vol.id for vol in volumes_by_tag]).should.equal(set([volume1.id])) + + @mock_ec2 def test_volume_attach_and_detach(): conn = boto.connect_ec2('the_key', 'the_secret') @@ -139,6 +196,44 @@ def test_filter_snapshot_by_id(): s.region.name.should.equal(conn.region.name) +@mock_ec2 +def test_snapshot_filters(): + conn = boto.connect_ec2('the_key', 'the_secret') + volume1 = conn.create_volume(20, "us-east-1a") + volume2 = conn.create_volume(25, "us-east-1a") + + snapshot1 = volume1.create_snapshot(description='testsnapshot1') + snapshot2 = volume1.create_snapshot(description='testsnapshot2') + snapshot3 = volume2.create_snapshot(description='testsnapshot3') + + conn.create_tags([snapshot1.id], {'testkey1': 'testvalue1'}) + conn.create_tags([snapshot2.id], {'testkey2': 'testvalue2'}) + + snapshots_by_description = conn.get_all_snapshots(filters={'description': 'testsnapshot1'}) + set([snap.id for snap in snapshots_by_description]).should.equal(set([snapshot1.id])) + + snapshots_by_id = conn.get_all_snapshots(filters={'snapshot-id': snapshot1.id}) + set([snap.id for snap in snapshots_by_id]).should.equal(set([snapshot1.id])) + + snapshots_by_start_time = conn.get_all_snapshots(filters={'start-time': snapshot1.start_time}) + set([snap.start_time for snap in snapshots_by_start_time]).should.equal(set([snapshot1.start_time])) + + snapshots_by_volume_id = conn.get_all_snapshots(filters={'volume-id': volume1.id}) + set([snap.id for snap in snapshots_by_volume_id]).should.equal(set([snapshot1.id, snapshot2.id])) + + snapshots_by_volume_size = conn.get_all_snapshots(filters={'volume-size': volume1.size}) + set([snap.id for snap in snapshots_by_volume_size]).should.equal(set([snapshot1.id, snapshot2.id])) + + snapshots_by_tag_key = conn.get_all_snapshots(filters={'tag-key': 'testkey1'}) + set([snap.id for snap in snapshots_by_tag_key]).should.equal(set([snapshot1.id])) + + snapshots_by_tag_value = conn.get_all_snapshots(filters={'tag-value': 'testvalue1'}) + set([snap.id for snap in snapshots_by_tag_value]).should.equal(set([snapshot1.id])) + + snapshots_by_tag = conn.get_all_snapshots(filters={'tag:testkey1': 'testvalue1'}) + set([snap.id for snap in snapshots_by_tag]).should.equal(set([snapshot1.id])) + + @mock_ec2 def test_snapshot_attribute(): conn = boto.connect_ec2('the_key', 'the_secret') diff --git a/tests/test_ec2/test_instances.py b/tests/test_ec2/test_instances.py index 0dda4eb9c..e80d1f979 100644 --- a/tests/test_ec2/test_instances.py +++ b/tests/test_ec2/test_instances.py @@ -51,21 +51,23 @@ def test_instance_launch_and_terminate(): reservations[0].id.should.equal(reservation.id) instances = reservations[0].instances instances.should.have.length_of(1) - instances[0].id.should.equal(instance.id) - instances[0].state.should.equal('running') - instances[0].launch_time.should.equal("2014-01-01T05:00:00.000Z") - instances[0].vpc_id.should.equal(None) + instance = instances[0] + instance.id.should.equal(instance.id) + instance.state.should.equal('running') + instance.launch_time.should.equal("2014-01-01T05:00:00.000Z") + instance.vpc_id.should.equal(None) + instance.placement.should.equal('us-east-1a') - root_device_name = instances[0].root_device_name - instances[0].block_device_mapping[root_device_name].status.should.equal('in-use') - volume_id = instances[0].block_device_mapping[root_device_name].volume_id + root_device_name = instance.root_device_name + instance.block_device_mapping[root_device_name].status.should.equal('in-use') + volume_id = instance.block_device_mapping[root_device_name].volume_id volume_id.should.match(r'vol-\w+') volume = conn.get_all_volumes(volume_ids=[volume_id])[0] volume.attach_data.instance_id.should.equal(instance.id) volume.status.should.equal('in-use') - conn.terminate_instances([instances[0].id]) + conn.terminate_instances([instance.id]) reservations = conn.get_all_instances() instance = reservations[0].instances[0] diff --git a/tests/test_ecs/test_ecs_boto3.py b/tests/test_ecs/test_ecs_boto3.py new file mode 100644 index 000000000..e902c5d91 --- /dev/null +++ b/tests/test_ecs/test_ecs_boto3.py @@ -0,0 +1,336 @@ +from __future__ import unicode_literals +import boto3 +import sure # noqa + +from moto import mock_ecs + + +@mock_ecs +def test_create_cluster(): + client = boto3.client('ecs', region_name='us-east-1') + response = client.create_cluster( + clusterName='test_ecs_cluster' + ) + response['cluster']['clusterName'].should.equal('test_ecs_cluster') + response['cluster']['clusterArn'].should.equal('arn:aws:ecs:us-east-1:012345678910:cluster/test_ecs_cluster') + response['cluster']['status'].should.equal('ACTIVE') + response['cluster']['registeredContainerInstancesCount'].should.equal(0) + response['cluster']['runningTasksCount'].should.equal(0) + response['cluster']['pendingTasksCount'].should.equal(0) + response['cluster']['activeServicesCount'].should.equal(0) + + +@mock_ecs +def test_list_clusters(): + client = boto3.client('ecs', region_name='us-east-1') + _ = client.create_cluster( + clusterName='test_cluster0' + ) + _ = client.create_cluster( + clusterName='test_cluster1' + ) + response = client.list_clusters() + response['clusterArns'].should.contain('arn:aws:ecs:us-east-1:012345678910:cluster/test_cluster0') + response['clusterArns'].should.contain('arn:aws:ecs:us-east-1:012345678910:cluster/test_cluster1') + + +@mock_ecs +def test_delete_cluster(): + client = boto3.client('ecs', region_name='us-east-1') + _ = client.create_cluster( + clusterName='test_ecs_cluster' + ) + response = client.delete_cluster(cluster='test_ecs_cluster') + response['cluster']['clusterName'].should.equal('test_ecs_cluster') + response['cluster']['clusterArn'].should.equal('arn:aws:ecs:us-east-1:012345678910:cluster/test_ecs_cluster') + response['cluster']['status'].should.equal('ACTIVE') + response['cluster']['registeredContainerInstancesCount'].should.equal(0) + response['cluster']['runningTasksCount'].should.equal(0) + response['cluster']['pendingTasksCount'].should.equal(0) + response['cluster']['activeServicesCount'].should.equal(0) + + response = client.list_clusters() + len(response['clusterArns']).should.equal(0) + + +@mock_ecs +def test_register_task_definition(): + client = boto3.client('ecs', region_name='us-east-1') + response = client.register_task_definition( + family='test_ecs_task', + containerDefinitions=[ + { + 'name': 'hello_world', + 'image': 'docker/hello-world:latest', + 'cpu': 1024, + 'memory': 400, + 'essential': True, + 'environment': [{ + 'name': 'AWS_ACCESS_KEY_ID', + 'value': 'SOME_ACCESS_KEY' + }], + 'logConfiguration': {'logDriver': 'json-file'} + } + ] + ) + type(response['taskDefinition']).should.be(dict) + response['taskDefinition']['taskDefinitionArn'].should.equal('arn:aws:ecs:us-east-1:012345678910:task-definition/test_ecs_task:1') + response['taskDefinition']['containerDefinitions'][0]['name'].should.equal('hello_world') + response['taskDefinition']['containerDefinitions'][0]['image'].should.equal('docker/hello-world:latest') + response['taskDefinition']['containerDefinitions'][0]['cpu'].should.equal(1024) + response['taskDefinition']['containerDefinitions'][0]['memory'].should.equal(400) + response['taskDefinition']['containerDefinitions'][0]['essential'].should.equal(True) + response['taskDefinition']['containerDefinitions'][0]['environment'][0]['name'].should.equal('AWS_ACCESS_KEY_ID') + response['taskDefinition']['containerDefinitions'][0]['environment'][0]['value'].should.equal('SOME_ACCESS_KEY') + response['taskDefinition']['containerDefinitions'][0]['logConfiguration']['logDriver'].should.equal('json-file') + + +@mock_ecs +def test_list_task_definitions(): + client = boto3.client('ecs', region_name='us-east-1') + _ = client.register_task_definition( + family='test_ecs_task', + containerDefinitions=[ + { + 'name': 'hello_world', + 'image': 'docker/hello-world:latest', + 'cpu': 1024, + 'memory': 400, + 'essential': True, + 'environment': [{ + 'name': 'AWS_ACCESS_KEY_ID', + 'value': 'SOME_ACCESS_KEY' + }], + 'logConfiguration': {'logDriver': 'json-file'} + } + ] + ) + _ = client.register_task_definition( + family='test_ecs_task', + containerDefinitions=[ + { + 'name': 'hello_world2', + 'image': 'docker/hello-world2:latest', + 'cpu': 1024, + 'memory': 400, + 'essential': True, + 'environment': [{ + 'name': 'AWS_ACCESS_KEY_ID', + 'value': 'SOME_ACCESS_KEY2' + }], + 'logConfiguration': {'logDriver': 'json-file'} + } + ] + ) + response = client.list_task_definitions() + len(response['taskDefinitionArns']).should.equal(2) + response['taskDefinitionArns'][0].should.equal('arn:aws:ecs:us-east-1:012345678910:task-definition/test_ecs_task:1') + response['taskDefinitionArns'][1].should.equal('arn:aws:ecs:us-east-1:012345678910:task-definition/test_ecs_task:2') + + +@mock_ecs +def test_deregister_task_definition(): + client = boto3.client('ecs', region_name='us-east-1') + _ = client.register_task_definition( + family='test_ecs_task', + containerDefinitions=[ + { + 'name': 'hello_world', + 'image': 'docker/hello-world:latest', + 'cpu': 1024, + 'memory': 400, + 'essential': True, + 'environment': [{ + 'name': 'AWS_ACCESS_KEY_ID', + 'value': 'SOME_ACCESS_KEY' + }], + 'logConfiguration': {'logDriver': 'json-file'} + } + ] + ) + response = client.deregister_task_definition( + taskDefinition='test_ecs_task:1' + ) + type(response['taskDefinition']).should.be(dict) + response['taskDefinition']['taskDefinitionArn'].should.equal('arn:aws:ecs:us-east-1:012345678910:task-definition/test_ecs_task:1') + response['taskDefinition']['containerDefinitions'][0]['name'].should.equal('hello_world') + response['taskDefinition']['containerDefinitions'][0]['image'].should.equal('docker/hello-world:latest') + response['taskDefinition']['containerDefinitions'][0]['cpu'].should.equal(1024) + response['taskDefinition']['containerDefinitions'][0]['memory'].should.equal(400) + response['taskDefinition']['containerDefinitions'][0]['essential'].should.equal(True) + response['taskDefinition']['containerDefinitions'][0]['environment'][0]['name'].should.equal('AWS_ACCESS_KEY_ID') + response['taskDefinition']['containerDefinitions'][0]['environment'][0]['value'].should.equal('SOME_ACCESS_KEY') + response['taskDefinition']['containerDefinitions'][0]['logConfiguration']['logDriver'].should.equal('json-file') + + +@mock_ecs +def test_create_service(): + client = boto3.client('ecs', region_name='us-east-1') + _ = client.create_cluster( + clusterName='test_ecs_cluster' + ) + _ = client.register_task_definition( + family='test_ecs_task', + containerDefinitions=[ + { + 'name': 'hello_world', + 'image': 'docker/hello-world:latest', + 'cpu': 1024, + 'memory': 400, + 'essential': True, + 'environment': [{ + 'name': 'AWS_ACCESS_KEY_ID', + 'value': 'SOME_ACCESS_KEY' + }], + 'logConfiguration': {'logDriver': 'json-file'} + } + ] + ) + response = client.create_service( + cluster='test_ecs_cluster', + serviceName='test_ecs_service', + taskDefinition='test_ecs_task', + desiredCount=2 + ) + response['service']['clusterArn'].should.equal('arn:aws:ecs:us-east-1:012345678910:cluster/test_ecs_cluster') + response['service']['desiredCount'].should.equal(2) + len(response['service']['events']).should.equal(0) + len(response['service']['loadBalancers']).should.equal(0) + response['service']['pendingCount'].should.equal(0) + response['service']['runningCount'].should.equal(0) + response['service']['serviceArn'].should.equal('arn:aws:ecs:us-east-1:012345678910:service/test_ecs_service') + response['service']['serviceName'].should.equal('test_ecs_service') + response['service']['status'].should.equal('ACTIVE') + response['service']['taskDefinition'].should.equal('arn:aws:ecs:us-east-1:012345678910:task-definition/test_ecs_task:1') + + +@mock_ecs +def test_list_services(): + client = boto3.client('ecs', region_name='us-east-1') + _ = client.create_cluster( + clusterName='test_ecs_cluster' + ) + _ = client.register_task_definition( + family='test_ecs_task', + containerDefinitions=[ + { + 'name': 'hello_world', + 'image': 'docker/hello-world:latest', + 'cpu': 1024, + 'memory': 400, + 'essential': True, + 'environment': [{ + 'name': 'AWS_ACCESS_KEY_ID', + 'value': 'SOME_ACCESS_KEY' + }], + 'logConfiguration': {'logDriver': 'json-file'} + } + ] + ) + _ = client.create_service( + cluster='test_ecs_cluster', + serviceName='test_ecs_service1', + taskDefinition='test_ecs_task', + desiredCount=2 + ) + _ = client.create_service( + cluster='test_ecs_cluster', + serviceName='test_ecs_service2', + taskDefinition='test_ecs_task', + desiredCount=2 + ) + response = client.list_services( + cluster='test_ecs_cluster' + ) + len(response['serviceArns']).should.equal(2) + response['serviceArns'][0].should.equal('arn:aws:ecs:us-east-1:012345678910:service/test_ecs_service1') + response['serviceArns'][1].should.equal('arn:aws:ecs:us-east-1:012345678910:service/test_ecs_service2') + + +@mock_ecs +def test_update_service(): + client = boto3.client('ecs', region_name='us-east-1') + _ = client.create_cluster( + clusterName='test_ecs_cluster' + ) + _ = client.register_task_definition( + family='test_ecs_task', + containerDefinitions=[ + { + 'name': 'hello_world', + 'image': 'docker/hello-world:latest', + 'cpu': 1024, + 'memory': 400, + 'essential': True, + 'environment': [{ + 'name': 'AWS_ACCESS_KEY_ID', + 'value': 'SOME_ACCESS_KEY' + }], + 'logConfiguration': {'logDriver': 'json-file'} + } + ] + ) + response = client.create_service( + cluster='test_ecs_cluster', + serviceName='test_ecs_service', + taskDefinition='test_ecs_task', + desiredCount=2 + ) + response['service']['desiredCount'].should.equal(2) + + response = client.update_service( + cluster='test_ecs_cluster', + service='test_ecs_service', + desiredCount=0 + ) + response['service']['desiredCount'].should.equal(0) + + +@mock_ecs +def test_delete_service(): + client = boto3.client('ecs', region_name='us-east-1') + _ = client.create_cluster( + clusterName='test_ecs_cluster' + ) + _ = client.register_task_definition( + family='test_ecs_task', + containerDefinitions=[ + { + 'name': 'hello_world', + 'image': 'docker/hello-world:latest', + 'cpu': 1024, + 'memory': 400, + 'essential': True, + 'environment': [{ + 'name': 'AWS_ACCESS_KEY_ID', + 'value': 'SOME_ACCESS_KEY' + }], + 'logConfiguration': {'logDriver': 'json-file'} + } + ] + ) + _ = client.create_service( + cluster='test_ecs_cluster', + serviceName='test_ecs_service', + taskDefinition='test_ecs_task', + desiredCount=2 + ) + _ = client.update_service( + cluster='test_ecs_cluster', + service='test_ecs_service', + desiredCount=0 + ) + response = client.delete_service( + cluster='test_ecs_cluster', + service='test_ecs_service' + ) + response['service']['clusterArn'].should.equal('arn:aws:ecs:us-east-1:012345678910:cluster/test_ecs_cluster') + response['service']['desiredCount'].should.equal(0) + len(response['service']['events']).should.equal(0) + len(response['service']['loadBalancers']).should.equal(0) + response['service']['pendingCount'].should.equal(0) + response['service']['runningCount'].should.equal(0) + response['service']['serviceArn'].should.equal('arn:aws:ecs:us-east-1:012345678910:service/test_ecs_service') + response['service']['serviceName'].should.equal('test_ecs_service') + response['service']['status'].should.equal('ACTIVE') + response['service']['taskDefinition'].should.equal('arn:aws:ecs:us-east-1:012345678910:task-definition/test_ecs_task:1') \ No newline at end of file