From 53ec30e3ba67c09bdf5faa86a34aeafb2992be2a Mon Sep 17 00:00:00 2001 From: Andy Altepeter Date: Mon, 23 Feb 2015 10:45:16 -0600 Subject: [PATCH 01/22] support 'tag-key' instance type --- moto/ec2/utils.py | 24 +++++++++++++++++---- tests/test_ec2/test_instances.py | 36 ++++++++++++++++++++++++++++++++ 2 files changed, 56 insertions(+), 4 deletions(-) diff --git a/moto/ec2/utils.py b/moto/ec2/utils.py index 11fb32ffe..8b2dc2b03 100644 --- a/moto/ec2/utils.py +++ b/moto/ec2/utils.py @@ -310,7 +310,9 @@ def get_object_value(obj, attr): def is_tag_filter(filter_name): - return filter_name.startswith('tag:') + return (filter_name.startswith('tag:') or + filter_name.startswith('tag-value') or + filter_name.startswith('tag-key')) def get_obj_tag(obj, filter_name): @@ -318,10 +320,24 @@ def get_obj_tag(obj, filter_name): tags = dict((tag['key'], tag['value']) for tag in obj.get_tags()) return tags.get(tag_name) +def get_obj_tag_names(obj): + tags = set((tag['key'] for tag in obj.get_tags())) + return tags + +def get_obj_tag_values(obj): + tags = set((tag['value'] for tag in obj.get_tags())) + return tags def tag_filter_matches(obj, filter_name, filter_values): - tag_value = get_obj_tag(obj, filter_name) - return tag_value in filter_values + if filter_name == 'tag-key': + tag_names = get_obj_tag_names(obj) + return len(set(filter_values).intersection(tag_names)) > 0 + elif filter_name == 'tag-value': + tag_values = get_obj_tag_values(obj) + return len(set(filter_values).intersection(tag_values)) > 0 + else: + tag_value = get_obj_tag(obj, filter_name) + return tag_value in filter_values filter_dict_attribute_mapping = { @@ -331,7 +347,7 @@ filter_dict_attribute_mapping = { 'source-dest-check': 'source_dest_check', 'vpc-id': 'vpc_id', 'group-id': 'security_groups', - 'instance.group-id': 'security_groups' + 'instance.group-id': 'security_groups', } diff --git a/tests/test_ec2/test_instances.py b/tests/test_ec2/test_instances.py index e16bbb126..4f399b034 100644 --- a/tests/test_ec2/test_instances.py +++ b/tests/test_ec2/test_instances.py @@ -240,6 +240,42 @@ def test_get_instances_filtering_by_tag(): reservations[0].instances[0].id.should.equal(instance1.id) reservations[0].instances[1].id.should.equal(instance3.id) +@mock_ec2 +def test_get_instances_filtering_by_tag_name(): + conn = boto.connect_ec2() + reservation = conn.run_instances('ami-1234abcd', min_count=3) + instance1, instance2, instance3 = reservation.instances + instance1.add_tag('tag1') + instance1.add_tag('tag2') + instance2.add_tag('tag1') + instance2.add_tag('tag2X') + instance3.add_tag('tag3') + + reservations = conn.get_all_instances(filters={'tag-key' : 'tagX'}) + # get_all_instances should return no instances + reservations.should.have.length_of(0) + + reservations = conn.get_all_instances(filters={'tag-key' : 'tag1'}) + # get_all_instances should return both instances with this tag value + reservations.should.have.length_of(1) + reservations[0].instances.should.have.length_of(2) + reservations[0].instances[0].id.should.equal(instance1.id) + reservations[0].instances[1].id.should.equal(instance2.id) + + reservations = conn.get_all_instances(filters={'tag-key' : 'tag1', 'tag-key' : 'tag2'}) + # get_all_instances should return the instance with both tag values + reservations.should.have.length_of(1) + reservations[0].instances.should.have.length_of(1) + reservations[0].instances[0].id.should.equal(instance1.id) + + reservations = conn.get_all_instances(filters={'tag-key' : ['tag1', 'tag3']}) + # get_all_instances should return both instances with one of the acceptable tag values + reservations.should.have.length_of(1) + reservations[0].instances.should.have.length_of(3) + reservations[0].instances[0].id.should.equal(instance1.id) + reservations[0].instances[1].id.should.equal(instance2.id) + reservations[0].instances[2].id.should.equal(instance3.id) + @mock_ec2 def test_instance_start_and_stop(): conn = boto.connect_ec2('the_key', 'the_secret') From d2d82333f902f77acf9fe7aaefea0df8b549d244 Mon Sep 17 00:00:00 2001 From: Andy Altepeter Date: Mon, 23 Feb 2015 10:52:47 -0600 Subject: [PATCH 02/22] support tag-value instance filter --- tests/test_ec2/test_instances.py | 43 +++++++++++++++++++++++++++----- 1 file changed, 37 insertions(+), 6 deletions(-) diff --git a/tests/test_ec2/test_instances.py b/tests/test_ec2/test_instances.py index 4f399b034..cd000f4b5 100644 --- a/tests/test_ec2/test_instances.py +++ b/tests/test_ec2/test_instances.py @@ -240,6 +240,43 @@ def test_get_instances_filtering_by_tag(): reservations[0].instances[0].id.should.equal(instance1.id) reservations[0].instances[1].id.should.equal(instance3.id) +@mock_ec2 +def test_get_instances_filtering_by_tag_value(): + conn = boto.connect_ec2() + reservation = conn.run_instances('ami-1234abcd', min_count=3) + instance1, instance2, instance3 = reservation.instances + instance1.add_tag('tag1', 'value1') + instance1.add_tag('tag2', 'value2') + instance2.add_tag('tag1', 'value1') + instance2.add_tag('tag2', 'wrong value') + instance3.add_tag('tag2', 'value2') + + reservations = conn.get_all_instances(filters={'tag-value' : 'value0'}) + # get_all_instances should return no instances + reservations.should.have.length_of(0) + + reservations = conn.get_all_instances(filters={'tag-value' : 'value1'}) + # get_all_instances should return both instances with this tag value + reservations.should.have.length_of(1) + reservations[0].instances.should.have.length_of(2) + reservations[0].instances[0].id.should.equal(instance1.id) + reservations[0].instances[1].id.should.equal(instance2.id) + + reservations = conn.get_all_instances(filters={'tag-value' : ['value2', 'value1']}) + # get_all_instances should return both instances with one of the acceptable tag values + reservations.should.have.length_of(1) + reservations[0].instances.should.have.length_of(3) + reservations[0].instances[0].id.should.equal(instance1.id) + reservations[0].instances[1].id.should.equal(instance2.id) + reservations[0].instances[2].id.should.equal(instance3.id) + + reservations = conn.get_all_instances(filters={'tag-value' : ['value2', 'bogus']}) + # get_all_instances should return both instances with one of the acceptable tag values + reservations.should.have.length_of(1) + reservations[0].instances.should.have.length_of(2) + reservations[0].instances[0].id.should.equal(instance1.id) + reservations[0].instances[1].id.should.equal(instance3.id) + @mock_ec2 def test_get_instances_filtering_by_tag_name(): conn = boto.connect_ec2() @@ -262,12 +299,6 @@ def test_get_instances_filtering_by_tag_name(): reservations[0].instances[0].id.should.equal(instance1.id) reservations[0].instances[1].id.should.equal(instance2.id) - reservations = conn.get_all_instances(filters={'tag-key' : 'tag1', 'tag-key' : 'tag2'}) - # get_all_instances should return the instance with both tag values - reservations.should.have.length_of(1) - reservations[0].instances.should.have.length_of(1) - reservations[0].instances[0].id.should.equal(instance1.id) - reservations = conn.get_all_instances(filters={'tag-key' : ['tag1', 'tag3']}) # get_all_instances should return both instances with one of the acceptable tag values reservations.should.have.length_of(1) From e17c7bbd7aa5b573202536bb880a2a7812cbaec2 Mon Sep 17 00:00:00 2001 From: Andy Altepeter Date: Mon, 23 Feb 2015 11:03:59 -0600 Subject: [PATCH 03/22] support 'instance_type' filter --- moto/ec2/utils.py | 1 + tests/test_ec2/test_instances.py | 38 ++++++++++++++++++++++++++++++++ 2 files changed, 39 insertions(+) diff --git a/moto/ec2/utils.py b/moto/ec2/utils.py index 8b2dc2b03..0b2b2ff49 100644 --- a/moto/ec2/utils.py +++ b/moto/ec2/utils.py @@ -348,6 +348,7 @@ filter_dict_attribute_mapping = { 'vpc-id': 'vpc_id', 'group-id': 'security_groups', 'instance.group-id': 'security_groups', + 'instance-type': 'instance_type' } diff --git a/tests/test_ec2/test_instances.py b/tests/test_ec2/test_instances.py index cd000f4b5..51d980a52 100644 --- a/tests/test_ec2/test_instances.py +++ b/tests/test_ec2/test_instances.py @@ -139,6 +139,44 @@ def test_get_instances_filtering_by_instance_id(): reservations = conn.get_all_instances(filters={'instance-id': 'non-existing-id'}) reservations.should.have.length_of(0) + +@mock_ec2 +def test_get_instances_filtering_by_instance_type(): + conn = boto.connect_ec2() + reservation1 = conn.run_instances('ami-1234abcd', instance_type='m1.small') + instance1 = reservation1.instances[0] + reservation2 = conn.run_instances('ami-1234abcd', instance_type='m1.small') + instance2 = reservation2.instances[0] + reservation3 = conn.run_instances('ami-1234abcd', instance_type='t1.micro') + instance3 = reservation3.instances[0] + + reservations = conn.get_all_instances(filters={'instance-type': 'm1.small'}) + # get_all_instances should return instance1,2 + reservations.should.have.length_of(2) + reservations[0].instances.should.have.length_of(1) + reservations[1].instances.should.have.length_of(1) + reservations[0].instances[0].id.should.equal(instance1.id) + reservations[1].instances[0].id.should.equal(instance2.id) + + reservations = conn.get_all_instances(filters={'instance-type': 't1.micro'}) + # get_all_instances should return one + reservations.should.have.length_of(1) + reservations[0].instances.should.have.length_of(1) + reservations[0].instances[0].id.should.equal(instance3.id) + + reservations = conn.get_all_instances(filters={'instance-type': ['t1.micro', 'm1.small']}) + reservations.should.have.length_of(3) + reservations[0].instances.should.have.length_of(1) + reservations[1].instances.should.have.length_of(1) + reservations[2].instances.should.have.length_of(1) + reservations[0].instances[0].id.should.equal(instance1.id) + reservations[1].instances[0].id.should.equal(instance3.id) + reservations[2].instances[0].id.should.equal(instance2.id) + + reservations = conn.get_all_instances(filters={'instance-type': 'bogus'}) + #bogus instance-type should return none + reservations.should.have.length_of(0) + @mock_ec2 def test_get_instances_filtering_by_reason_code(): conn = boto.connect_ec2() From e736482bec7be7871bd4edd270f8c064961c20fc Mon Sep 17 00:00:00 2001 From: Jeff Legge Date: Mon, 23 Feb 2015 13:34:37 -0800 Subject: [PATCH 04/22] Update minimum support boto version. boto 2.20.0 introduces kinesis. alternatively, this requirement could be relaxed by using conditional imports. --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 37dab094f..36d0dd5f2 100644 --- a/setup.py +++ b/setup.py @@ -4,7 +4,7 @@ from setuptools import setup, find_packages install_requires = [ "Jinja2", - "boto", + "boto>=2.20.0", "flask", "httpretty>=0.6.1", "requests", From b66ee09abec7900a526f101ad05f0c36e1bfd7cd Mon Sep 17 00:00:00 2001 From: Andy Altepeter Date: Tue, 24 Feb 2015 07:39:50 -0600 Subject: [PATCH 05/22] fix test for ec2 instance type filter --- tests/test_ec2/test_instances.py | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/tests/test_ec2/test_instances.py b/tests/test_ec2/test_instances.py index 51d980a52..308254595 100644 --- a/tests/test_ec2/test_instances.py +++ b/tests/test_ec2/test_instances.py @@ -155,8 +155,8 @@ def test_get_instances_filtering_by_instance_type(): reservations.should.have.length_of(2) reservations[0].instances.should.have.length_of(1) reservations[1].instances.should.have.length_of(1) - reservations[0].instances[0].id.should.equal(instance1.id) - reservations[1].instances[0].id.should.equal(instance2.id) + instance_ids = [ reservations[0].instances[0].id, reservations[1].instances[0].id ] + set(instance_ids).should.equal(set([instance1.id, instance2.id])) reservations = conn.get_all_instances(filters={'instance-type': 't1.micro'}) # get_all_instances should return one @@ -169,9 +169,12 @@ def test_get_instances_filtering_by_instance_type(): reservations[0].instances.should.have.length_of(1) reservations[1].instances.should.have.length_of(1) reservations[2].instances.should.have.length_of(1) - reservations[0].instances[0].id.should.equal(instance1.id) - reservations[1].instances[0].id.should.equal(instance3.id) - reservations[2].instances[0].id.should.equal(instance2.id) + instance_ids = [ + reservations[0].instances[0].id, + reservations[1].instances[0].id, + reservations[2].instances[0].id, + ] + set(instance_ids).should.equal(set([instance1.id, instance2.id, instance3.id])) reservations = conn.get_all_instances(filters={'instance-type': 'bogus'}) #bogus instance-type should return none From b987914c72d400eaf7ffece492af99844c1a3770 Mon Sep 17 00:00:00 2001 From: Andy Altepeter Date: Tue, 24 Feb 2015 07:43:43 -0600 Subject: [PATCH 06/22] slight change in formatting --- tests/test_ec2/test_instances.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/test_ec2/test_instances.py b/tests/test_ec2/test_instances.py index 308254595..640a24eaf 100644 --- a/tests/test_ec2/test_instances.py +++ b/tests/test_ec2/test_instances.py @@ -155,7 +155,8 @@ def test_get_instances_filtering_by_instance_type(): reservations.should.have.length_of(2) reservations[0].instances.should.have.length_of(1) reservations[1].instances.should.have.length_of(1) - instance_ids = [ reservations[0].instances[0].id, reservations[1].instances[0].id ] + instance_ids = [ reservations[0].instances[0].id, + reservations[1].instances[0].id ] set(instance_ids).should.equal(set([instance1.id, instance2.id])) reservations = conn.get_all_instances(filters={'instance-type': 't1.micro'}) From 20d8318997dfd13bd700f458848679240fa3f8e1 Mon Sep 17 00:00:00 2001 From: Hugo Lopes Tavares Date: Tue, 24 Feb 2015 17:56:26 -0500 Subject: [PATCH 07/22] Add support to tag filtering to Security Groups --- moto/ec2/models.py | 7 ++++++- tests/test_ec2/test_security_groups.py | 10 ++++++++++ 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/moto/ec2/models.py b/moto/ec2/models.py index ac0bb4ddc..1336eedb8 100644 --- a/moto/ec2/models.py +++ b/moto/ec2/models.py @@ -92,7 +92,9 @@ from .utils import ( filter_reservations, random_network_acl_id, random_network_acl_subnet_association_id, - random_vpn_gateway_id) + random_vpn_gateway_id, + is_tag_filter, +) def validate_resource_ids(resource_ids): @@ -1113,6 +1115,9 @@ class SecurityGroup(TaggedEC2Resource): for ingress in self.ingress_rules: if getattr(ingress, ingress_attr) in filter_value: return True + elif is_tag_filter(key): + tag_value = self.get_filter_value(key) + return tag_value in filter_value else: attr_name = to_attr(key) return getattr(self, attr_name) in filter_value diff --git a/tests/test_ec2/test_security_groups.py b/tests/test_ec2/test_security_groups.py index 861e492b8..e555bd694 100644 --- a/tests/test_ec2/test_security_groups.py +++ b/tests/test_ec2/test_security_groups.py @@ -248,3 +248,13 @@ def test_security_group_tagging(): group = conn.get_all_security_groups("test-sg")[0] group.tags.should.have.length_of(1) group.tags["Test"].should.equal("Tag") + + +@mock_ec2 +def test_security_group_tag_filtering(): + conn = boto.connect_ec2() + sg = conn.create_security_group("test-sg", "Test SG") + sg.add_tag("test-tag", "test-value") + + groups = conn.get_all_security_groups(filters={"tag:test-tag": "test-value"}) + groups.should.have.length_of(1) From dc0a6bb17e60ba7043bb108b502e8e6b36197662 Mon Sep 17 00:00:00 2001 From: Steve Pulec Date: Sun, 22 Feb 2015 10:58:51 -0500 Subject: [PATCH 08/22] Add publish command. --- Makefile | 2 ++ setup.cfg | 2 ++ 2 files changed, 4 insertions(+) create mode 100644 setup.cfg diff --git a/Makefile b/Makefile index 7521a6d88..2b83f8948 100644 --- a/Makefile +++ b/Makefile @@ -8,3 +8,5 @@ test: rm -f .coverage @nosetests -sv --with-coverage ./tests/ +publish: + python setup.py sdist bdist_wheel upload diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 000000000..3480374bc --- /dev/null +++ b/setup.cfg @@ -0,0 +1,2 @@ +[bdist_wheel] +universal=1 \ No newline at end of file From 0c929f8dbfaccc451fe6c4014951ca6eb5b79c18 Mon Sep 17 00:00:00 2001 From: Steve Pulec Date: Tue, 24 Feb 2015 18:00:22 -0500 Subject: [PATCH 09/22] Add @aaltepet to authors. --- AUTHORS.md | 1 + 1 file changed, 1 insertion(+) diff --git a/AUTHORS.md b/AUTHORS.md index fa286f1a4..f4604a04d 100644 --- a/AUTHORS.md +++ b/AUTHORS.md @@ -35,3 +35,4 @@ Moto is written by Steve Pulec with contributions from: * [Gary Dalton](https://github.com/gary-dalton) * [Chris Henry](https://github.com/chrishenry) * [Mike Fuller](https://github.com/mfulleratlassian) +* [Andy](https://github.com/aaltepet] From dc351dfc9e3de2c8efd5c6818c35824ae1e846d7 Mon Sep 17 00:00:00 2001 From: Hugo Lopes Tavares Date: Tue, 24 Feb 2015 19:36:21 -0500 Subject: [PATCH 10/22] Add support to AWS::EC2::SecurityGroupIngress creation --- moto/cloudformation/parsing.py | 1 + moto/ec2/models.py | 66 ++++++++++ .../test_cloudformation_stack_integration.py | 114 ++++++++++++++++++ 3 files changed, 181 insertions(+) diff --git a/moto/cloudformation/parsing.py b/moto/cloudformation/parsing.py index 4d3f53384..6528c41d2 100644 --- a/moto/cloudformation/parsing.py +++ b/moto/cloudformation/parsing.py @@ -26,6 +26,7 @@ MODEL_MAP = { "AWS::EC2::Route": ec2_models.Route, "AWS::EC2::RouteTable": ec2_models.RouteTable, "AWS::EC2::SecurityGroup": ec2_models.SecurityGroup, + "AWS::EC2::SecurityGroupIngress": ec2_models.SecurityGroupIngress, "AWS::EC2::Subnet": ec2_models.Subnet, "AWS::EC2::SubnetRouteTableAssociation": ec2_models.SubnetRouteTableAssociation, "AWS::EC2::Volume": ec2_models.Volume, diff --git a/moto/ec2/models.py b/moto/ec2/models.py index 1336eedb8..fc81174a3 100644 --- a/moto/ec2/models.py +++ b/moto/ec2/models.py @@ -121,6 +121,9 @@ class TaggedEC2Resource(object): tags = self.ec2_backend.describe_tags(filters={'resource-id': [self.id]}) return tags + def add_tag(self, key, value): + self.ec2_backend.create_tags([self.id], {key: value}) + def get_filter_value(self, filter_name): tags = self.get_tags() @@ -1073,6 +1076,11 @@ class SecurityGroup(TaggedEC2Resource): vpc_id=vpc_id, ) + for tag in properties.get("Tags", []): + tag_key = tag["Key"] + tag_value = tag["Value"] + security_group.add_tag(tag_key, tag_value) + for ingress_rule in properties.get('SecurityGroupIngress', []): source_group_id = ingress_rule.get('SourceSecurityGroupId') @@ -1287,6 +1295,64 @@ class SecurityGroupBackend(object): raise InvalidPermissionNotFoundError() +class SecurityGroupIngress(object): + + def __init__(self, security_group, properties): + self.security_group = security_group + self.properties = properties + + @classmethod + def create_from_cloudformation_json(cls, resource_name, cloudformation_json, region_name): + properties = cloudformation_json['Properties'] + + ec2_backend = ec2_backends[region_name] + group_name = properties.get('GroupName') + group_id = properties.get('GroupId') + ip_protocol = properties.get("IpProtocol") + cidr_ip = properties.get("CidrIp") + 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 + to_port = properties.get("ToPort") + + assert group_id or group_name + assert source_security_group_name or cidr_ip or source_security_group_id + assert ip_protocol + + if source_security_group_id: + source_security_group_ids = [source_security_group_id] + else: + source_security_group_ids = None + if source_security_group_name: + source_security_group_names = [source_security_group_name] + else: + source_security_group_names = None + if cidr_ip: + ip_ranges = [cidr_ip] + else: + ip_ranges = [] + + + if group_id: + security_group = ec2_backend.describe_security_groups(group_ids=[group_id])[0] + else: + security_group = ec2_backend.describe_security_groups(groupnames=[group_name])[0] + + ec2_backend.authorize_security_group_ingress( + group_name=security_group.name, + group_id=security_group.id, + ip_protocol=ip_protocol, + from_port=from_port, + to_port=to_port, + ip_ranges=ip_ranges, + source_group_ids=source_security_group_ids, + source_group_names=source_security_group_names, + ) + + return cls(security_group, properties) + + class VolumeAttachment(object): def __init__(self, volume, instance, device): self.volume = volume diff --git a/tests/test_cloudformation/test_cloudformation_stack_integration.py b/tests/test_cloudformation/test_cloudformation_stack_integration.py index 0b123c3ed..ca60bf016 100644 --- a/tests/test_cloudformation/test_cloudformation_stack_integration.py +++ b/tests/test_cloudformation/test_cloudformation_stack_integration.py @@ -1014,3 +1014,117 @@ def test_vpc_peering_creation(): peering_connections = vpc_conn.get_all_vpc_peering_connections() peering_connections.should.have.length_of(1) + + +@mock_cloudformation +@mock_ec2 +def test_security_group_ingress_separate_from_security_group_by_id(): + template = { + "AWSTemplateFormatVersion": "2010-09-09", + "Resources": { + "test-security-group1": { + "Type": "AWS::EC2::SecurityGroup", + "Properties": { + "GroupDescription": "test security group", + "Tags": [ + { + "Key": "sg-name", + "Value": "sg1" + } + ] + }, + }, + "test-security-group2": { + "Type": "AWS::EC2::SecurityGroup", + "Properties": { + "GroupDescription": "test security group", + "Tags": [ + { + "Key": "sg-name", + "Value": "sg2" + } + ] + }, + }, + "test-sg-ingress": { + "Type": "AWS::EC2::SecurityGroupIngress", + "Properties": { + "GroupId": {"Ref": "test-security-group1"}, + "IpProtocol": "tcp", + "FromPort": "80", + "ToPort": "8080", + "SourceSecurityGroupId": {"Ref": "test-security-group2"}, + } + } + } + } + + template_json = json.dumps(template) + cf_conn = boto.cloudformation.connect_to_region("us-west-1") + cf_conn.create_stack( + "test_stack", + template_body=template_json, + ) + ec2_conn = boto.ec2.connect_to_region("us-west-1") + + security_group1 = ec2_conn.get_all_security_groups(filters={"tag:sg-name": "sg1"})[0] + security_group2 = ec2_conn.get_all_security_groups(filters={"tag:sg-name": "sg2"})[0] + + security_group1.rules.should.have.length_of(1) + security_group1.rules[0].grants.should.have.length_of(1) + security_group1.rules[0].grants[0].group_id.should.equal(security_group2.id) + security_group1.rules[0].ip_protocol.should.equal('tcp') + security_group1.rules[0].from_port.should.equal('80') + security_group1.rules[0].to_port.should.equal('8080') + + + +@mock_cloudformation +@mock_ec2 +def test_security_group_ingress_separate_from_security_group_by_id(): + ec2_conn = boto.ec2.connect_to_region("us-west-1") + ec2_conn.create_security_group("test-security-group1", "test security group") + + template = { + "AWSTemplateFormatVersion": "2010-09-09", + "Resources": { + "test-security-group2": { + "Type": "AWS::EC2::SecurityGroup", + "Properties": { + "GroupDescription": "test security group", + "Tags": [ + { + "Key": "sg-name", + "Value": "sg2" + } + ] + }, + }, + "test-sg-ingress": { + "Type": "AWS::EC2::SecurityGroupIngress", + "Properties": { + "GroupName": "test-security-group1", + "IpProtocol": "tcp", + "FromPort": "80", + "ToPort": "8080", + "SourceSecurityGroupId": {"Ref": "test-security-group2"}, + } + } + } + } + + template_json = json.dumps(template) + cf_conn = boto.cloudformation.connect_to_region("us-west-1") + cf_conn.create_stack( + "test_stack", + template_body=template_json, + ) + security_group1 = ec2_conn.get_all_security_groups(groupnames=["test-security-group1"])[0] + security_group2 = ec2_conn.get_all_security_groups(filters={"tag:sg-name": "sg2"})[0] + + security_group1.rules.should.have.length_of(1) + security_group1.rules[0].grants.should.have.length_of(1) + security_group1.rules[0].grants[0].group_id.should.equal(security_group2.id) + security_group1.rules[0].ip_protocol.should.equal('tcp') + security_group1.rules[0].from_port.should.equal('80') + security_group1.rules[0].to_port.should.equal('8080') From 4beda260076a2b4027a22be7b819ac66298a67e6 Mon Sep 17 00:00:00 2001 From: Hugo Lopes Tavares Date: Wed, 25 Feb 2015 18:11:00 -0500 Subject: [PATCH 11/22] Change SecurityGroupBackend.{authorize,revoke}_security_group_ingress() methods to receive group name or id, never both --- moto/ec2/models.py | 31 ++++----- moto/ec2/responses/security_groups.py | 10 +-- .../test_cloudformation_stack_integration.py | 66 +++++++++++++++++++ 3 files changed, 82 insertions(+), 25 deletions(-) diff --git a/moto/ec2/models.py b/moto/ec2/models.py index fc81174a3..c5d3c256c 100644 --- a/moto/ec2/models.py +++ b/moto/ec2/models.py @@ -1085,8 +1085,7 @@ class SecurityGroup(TaggedEC2Resource): source_group_id = ingress_rule.get('SourceSecurityGroupId') ec2_backend.authorize_security_group_ingress( - group_name=security_group.name, - group_id=security_group.id, + group_name_or_id=security_group.id, ip_protocol=ingress_rule['IpProtocol'], from_port=ingress_rule['FromPort'], to_port=ingress_rule['ToPort'], @@ -1218,9 +1217,15 @@ class SecurityGroupBackend(object): 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) + if group is None: + group = self.get_security_group_from_name(group_name_or_id, vpc_id) + return group + def authorize_security_group_ingress(self, - group_name, - group_id, + group_name_or_id, ip_protocol, from_port, to_port, @@ -1228,12 +1233,7 @@ class SecurityGroupBackend(object): source_group_names=None, source_group_ids=None, vpc_id=None): - # to auth a group in a VPC you need the group_id the name isn't enough - - if group_name: - group = self.get_security_group_from_name(group_name, vpc_id) - elif group_id: - group = self.get_security_group_from_id(group_id) + group = self.get_security_group_by_name_or_id(group_name_or_id, vpc_id) if ip_ranges and not isinstance(ip_ranges, list): ip_ranges = [ip_ranges] @@ -1261,8 +1261,7 @@ class SecurityGroupBackend(object): group.ingress_rules.append(security_rule) def revoke_security_group_ingress(self, - group_name, - group_id, + group_name_or_id, ip_protocol, from_port, to_port, @@ -1271,10 +1270,7 @@ class SecurityGroupBackend(object): source_group_ids=None, vpc_id=None): - if group_name: - group = self.get_security_group_from_name(group_name, vpc_id) - elif group_id: - group = self.get_security_group_from_id(group_id) + group = self.get_security_group_by_name_or_id(group_name_or_id, vpc_id) source_groups = [] for source_group_name in source_group_names: @@ -1340,8 +1336,7 @@ class SecurityGroupIngress(object): security_group = ec2_backend.describe_security_groups(groupnames=[group_name])[0] ec2_backend.authorize_security_group_ingress( - group_name=security_group.name, - group_id=security_group.id, + group_name_or_id=security_group.id, ip_protocol=ip_protocol, from_port=from_port, to_port=to_port, diff --git a/moto/ec2/responses/security_groups.py b/moto/ec2/responses/security_groups.py index 38fadb883..eec27c3aa 100644 --- a/moto/ec2/responses/security_groups.py +++ b/moto/ec2/responses/security_groups.py @@ -4,14 +4,10 @@ from moto.ec2.utils import filters_from_querystring def process_rules_from_querystring(querystring): - - name = None - group_id = None - try: - name = querystring.get('GroupName')[0] + group_name_or_id = querystring.get('GroupName')[0] except: - group_id = querystring.get('GroupId')[0] + group_name_or_id = querystring.get('GroupId')[0] ip_protocol = querystring.get('IpPermissions.1.IpProtocol')[0] from_port = querystring.get('IpPermissions.1.FromPort')[0] @@ -30,7 +26,7 @@ def process_rules_from_querystring(querystring): elif 'IpPermissions.1.Groups' in key: source_groups.append(value[0]) - return (name, group_id, ip_protocol, from_port, to_port, ip_ranges, source_groups, source_group_ids) + return (group_name_or_id, ip_protocol, from_port, to_port, ip_ranges, source_groups, source_group_ids) class SecurityGroups(BaseResponse): diff --git a/tests/test_cloudformation/test_cloudformation_stack_integration.py b/tests/test_cloudformation/test_cloudformation_stack_integration.py index ca60bf016..0ca96db20 100644 --- a/tests/test_cloudformation/test_cloudformation_stack_integration.py +++ b/tests/test_cloudformation/test_cloudformation_stack_integration.py @@ -1128,3 +1128,69 @@ def test_security_group_ingress_separate_from_security_group_by_id(): security_group1.rules[0].ip_protocol.should.equal('tcp') security_group1.rules[0].from_port.should.equal('80') security_group1.rules[0].to_port.should.equal('8080') + + +@mock_cloudformation +@mock_ec2 +def test_security_group_ingress_separate_from_security_group_by_id_using_vpc(): + vpc_conn = boto.vpc.connect_to_region("us-west-1") + vpc = vpc_conn.create_vpc("10.0.0.0/16") + + template = { + "AWSTemplateFormatVersion": "2010-09-09", + "Resources": { + "test-security-group1": { + "Type": "AWS::EC2::SecurityGroup", + "Properties": { + "GroupDescription": "test security group", + "VpcId": vpc.id, + "Tags": [ + { + "Key": "sg-name", + "Value": "sg1" + } + ] + }, + }, + "test-security-group2": { + "Type": "AWS::EC2::SecurityGroup", + "Properties": { + "GroupDescription": "test security group", + "VpcId": vpc.id, + "Tags": [ + { + "Key": "sg-name", + "Value": "sg2" + } + ] + }, + }, + "test-sg-ingress": { + "Type": "AWS::EC2::SecurityGroupIngress", + "Properties": { + "GroupId": {"Ref": "test-security-group1"}, + "VpcId": vpc.id, + "IpProtocol": "tcp", + "FromPort": "80", + "ToPort": "8080", + "SourceSecurityGroupId": {"Ref": "test-security-group2"}, + } + } + } + } + + template_json = json.dumps(template) + cf_conn = boto.cloudformation.connect_to_region("us-west-1") + cf_conn.create_stack( + "test_stack", + template_body=template_json, + ) + security_group1 = vpc_conn.get_all_security_groups(filters={"tag:sg-name": "sg1"})[0] + security_group2 = vpc_conn.get_all_security_groups(filters={"tag:sg-name": "sg2"})[0] + + security_group1.rules.should.have.length_of(1) + security_group1.rules[0].grants.should.have.length_of(1) + security_group1.rules[0].grants[0].group_id.should.equal(security_group2.id) + security_group1.rules[0].ip_protocol.should.equal('tcp') + security_group1.rules[0].from_port.should.equal('80') + security_group1.rules[0].to_port.should.equal('8080') From 3a357c0fe31f26e9a5d36280ab4f9469a958c97a Mon Sep 17 00:00:00 2001 From: Mike Grima Date: Thu, 26 Feb 2015 16:08:54 -0800 Subject: [PATCH 12/22] Added in test for the boto IAM method: list_instance_profiles_for_role() --- moto/core/responses.py | 5 ++++- moto/iam/models.py | 10 ++++++++++ moto/iam/responses.py | 40 +++++++++++++++++++++++++++++++++++++- tests/test_iam/test_iam.py | 28 ++++++++++++++++++++++++++ 4 files changed, 81 insertions(+), 2 deletions(-) diff --git a/moto/core/responses.py b/moto/core/responses.py index a99ac78c8..5558e9a02 100644 --- a/moto/core/responses.py +++ b/moto/core/responses.py @@ -137,7 +137,10 @@ class BaseResponse(_TemplateEnvironmentMixin): if action in method_names: method = getattr(self, action) try: - response = method() + if action != 'list_instance_profiles_for_role': + response = method() + else: + response = method(role_name=self.querystring['RoleName'][0]) except HTTPException as http_error: response = http_error.description, dict(status=http_error.code) if isinstance(response, six.string_types): diff --git a/moto/iam/models.py b/moto/iam/models.py index 5468e0805..388984f51 100644 --- a/moto/iam/models.py +++ b/moto/iam/models.py @@ -291,6 +291,16 @@ class IAMBackend(BaseBackend): def get_instance_profiles(self): return self.instance_profiles.values() + def get_instance_profiles_for_role(self, role_name): + found_profiles = [] + + for profile in self.get_instance_profiles(): + if len(profile.roles) > 0: + if profile.roles[0].name == role_name: + found_profiles.append(profile) + + return found_profiles + def add_role_to_instance_profile(self, profile_name, role_name): profile = self.get_instance_profile(profile_name) role = self.get_role(role_name) diff --git a/moto/iam/responses.py b/moto/iam/responses.py index 78030f288..e1722162b 100644 --- a/moto/iam/responses.py +++ b/moto/iam/responses.py @@ -87,6 +87,12 @@ class IamResponse(BaseResponse): template = self.response_template(LIST_INSTANCE_PROFILES_TEMPLATE) return template.render(instance_profiles=profiles) + def list_instance_profiles_for_role(self, role_name=None): + profiles = iam_backend.get_instance_profiles_for_role(role_name=role_name) + + template = self.response_template(LIST_INSTANCE_PROFILES_FOR_ROLE_TEMPLATE) + return template.render(instance_profiles=profiles) + def upload_server_certificate(self): cert_name = self._get_param('ServerCertificateName') cert_body = self._get_param('CertificateBody') @@ -601,4 +607,36 @@ CREDENTIAL_REPORT = """ fa788a82-aa8a-11e4-a278-1786c418872b" -""" \ No newline at end of file +""" + +LIST_INSTANCE_PROFILES_FOR_ROLE_TEMPLATE = """ + + false + + {% for profile in instance_profiles %} + + {{ profile.id }} + + {% for role in profile.roles %} + + {{ role.path }} + arn:aws:iam::123456789012:role{{ role.path }}S3Access + {{ role.name }} + {{ role.assume_policy_document }} + 2012-05-09T15:45:35Z + {{ role.id }} + + {% endfor %} + + {{ profile.name }} + {{ profile.path }} + arn:aws:iam::123456789012:instance-profile{{ profile.path }}Webserver + 2012-05-09T16:27:11Z + + {% endfor %} + + + + 6a8c3992-99f4-11e1-a4c3-27EXAMPLE804 + +""" \ No newline at end of file diff --git a/tests/test_iam/test_iam.py b/tests/test_iam/test_iam.py index 454703fec..ee77297b0 100644 --- a/tests/test_iam/test_iam.py +++ b/tests/test_iam/test_iam.py @@ -62,6 +62,34 @@ def test_create_role_and_instance_profile(): conn.list_roles().roles[0].role_name.should.equal('my-role') conn.list_instance_profiles().instance_profiles[0].instance_profile_name.should.equal("my-profile") +@mock_iam() +def test_list_instance_profiles_for_role(): + conn = boto.connect_iam() + + conn.create_role(role_name="my-role", assume_role_policy_document="some policy", path="my-path") + conn.create_role(role_name="my-role2", assume_role_policy_document="some policy2", path="my-path2") + + profile_name_list = ['my-profile', 'my-profile2'] + profile_path_list = ['my-path', 'my-path2'] + for profile_count in range(0,2): + conn.create_instance_profile(profile_name_list[profile_count], path=profile_path_list[profile_count]) + + for profile_count in range(0,2): + conn.add_role_to_instance_profile(profile_name_list[profile_count], "my-role") + + profile_dump = conn.list_instance_profiles_for_role(role_name="my-role") + profile_list = profile_dump['list_instance_profiles_for_role_response']['list_instance_profiles_for_role_result']['instance_profiles'] + for profile_count in range(0,len(profile_list)): + profile_name_list.remove(profile_list[profile_count]["instance_profile_name"]) + profile_path_list.remove(profile_list[profile_count]["path"]) + profile_list[profile_count]["roles"]["member"]["role_name"].should.equal("my-role") + + len(profile_name_list).should.equal(0) + len(profile_path_list).should.equal(0) + + profile_dump2 = conn.list_instance_profiles_for_role(role_name="my-role2") + profile_list = profile_dump2['list_instance_profiles_for_role_response']['list_instance_profiles_for_role_result']['instance_profiles'] + len(profile_list).should.equal(0) @mock_iam() def test_list_role_policies(): From 70315fd67ce92ca723ca22457171c7d5d7444056 Mon Sep 17 00:00:00 2001 From: Mike Grima Date: Fri, 27 Feb 2015 12:22:31 -0800 Subject: [PATCH 13/22] Fixed how parameters are passed in following clarification on GitHub comments. --- moto/core/responses.py | 5 +---- moto/iam/responses.py | 3 ++- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/moto/core/responses.py b/moto/core/responses.py index 5558e9a02..a99ac78c8 100644 --- a/moto/core/responses.py +++ b/moto/core/responses.py @@ -137,10 +137,7 @@ class BaseResponse(_TemplateEnvironmentMixin): if action in method_names: method = getattr(self, action) try: - if action != 'list_instance_profiles_for_role': - response = method() - else: - response = method(role_name=self.querystring['RoleName'][0]) + response = method() except HTTPException as http_error: response = http_error.description, dict(status=http_error.code) if isinstance(response, six.string_types): diff --git a/moto/iam/responses.py b/moto/iam/responses.py index e1722162b..4ebfb74ec 100644 --- a/moto/iam/responses.py +++ b/moto/iam/responses.py @@ -87,7 +87,8 @@ class IamResponse(BaseResponse): template = self.response_template(LIST_INSTANCE_PROFILES_TEMPLATE) return template.render(instance_profiles=profiles) - def list_instance_profiles_for_role(self, role_name=None): + def list_instance_profiles_for_role(self): + role_name = self._get_param('RoleName') profiles = iam_backend.get_instance_profiles_for_role(role_name=role_name) template = self.response_template(LIST_INSTANCE_PROFILES_FOR_ROLE_TEMPLATE) From a9167ac20d15e9d6f7d08bedfa42d1d753f226e7 Mon Sep 17 00:00:00 2001 From: Steve Pulec Date: Fri, 27 Feb 2015 15:32:48 -0500 Subject: [PATCH 14/22] Add @mikegrima to authors --- AUTHORS.md | 1 + 1 file changed, 1 insertion(+) diff --git a/AUTHORS.md b/AUTHORS.md index f4604a04d..d14310f20 100644 --- a/AUTHORS.md +++ b/AUTHORS.md @@ -36,3 +36,4 @@ Moto is written by Steve Pulec with contributions from: * [Chris Henry](https://github.com/chrishenry) * [Mike Fuller](https://github.com/mfulleratlassian) * [Andy](https://github.com/aaltepet] +* [Mike Grima](https://github.com/mikegrima) From c4793fc1f9ba60cc282ecd5206d1b41628e7c972 Mon Sep 17 00:00:00 2001 From: Steve Pulec Date: Fri, 27 Feb 2015 15:33:03 -0500 Subject: [PATCH 15/22] Fix authors --- AUTHORS.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/AUTHORS.md b/AUTHORS.md index d14310f20..71bc6319e 100644 --- a/AUTHORS.md +++ b/AUTHORS.md @@ -35,5 +35,5 @@ Moto is written by Steve Pulec with contributions from: * [Gary Dalton](https://github.com/gary-dalton) * [Chris Henry](https://github.com/chrishenry) * [Mike Fuller](https://github.com/mfulleratlassian) -* [Andy](https://github.com/aaltepet] +* [Andy](https://github.com/aaltepet) * [Mike Grima](https://github.com/mikegrima) From 5ba84212424ee4ef634281d4c1debc74957d4cd8 Mon Sep 17 00:00:00 2001 From: jraby Date: Fri, 27 Feb 2015 18:42:37 -0500 Subject: [PATCH 16/22] Fix reduced_min_part_size so that tests run --- tests/test_s3/test_s3.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/test_s3/test_s3.py b/tests/test_s3/test_s3.py index 2a14650e3..9a4048a67 100644 --- a/tests/test_s3/test_s3.py +++ b/tests/test_s3/test_s3.py @@ -3,6 +3,7 @@ from __future__ import unicode_literals from six.moves.urllib.request import urlopen from six.moves.urllib.error import HTTPError +from functools import wraps from io import BytesIO import boto @@ -29,6 +30,7 @@ def reduced_min_part_size(f): import moto.s3.models as s3model orig_size = s3model.UPLOAD_PART_MIN_SIZE + @wraps(f) def wrapped(*args, **kwargs): try: s3model.UPLOAD_PART_MIN_SIZE = REDUCED_PART_SIZE From 4a14d8d3b345b4fea15512cc9d8f553ea1f8c2dc Mon Sep 17 00:00:00 2001 From: jraby Date: Fri, 27 Feb 2015 18:48:51 -0500 Subject: [PATCH 17/22] Add test_multipart_duplicate_upload Test to make sure we do not duplicate data when uploading the same part twice --- tests/test_s3/test_s3.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/tests/test_s3/test_s3.py b/tests/test_s3/test_s3.py index 2a14650e3..93ede287a 100644 --- a/tests/test_s3/test_s3.py +++ b/tests/test_s3/test_s3.py @@ -208,7 +208,23 @@ def test_multipart_invalid_order(): bucket.complete_multipart_upload.when.called_with( multipart.key_name, multipart.id, xml).should.throw(S3ResponseError) +@mock_s3 +@reduced_min_part_size +def test_multipart_duplicate_upload(): + conn = boto.connect_s3('the_key', 'the_secret') + bucket = conn.create_bucket("foobar") + multipart = bucket.initiate_multipart_upload("the-key") + part1 = b'0' * REDUCED_PART_SIZE + multipart.upload_part_from_file(BytesIO(part1), 1) + # same part again + multipart.upload_part_from_file(BytesIO(part1), 1) + part2 = b'1' * 1024 + multipart.upload_part_from_file(BytesIO(part2), 2) + multipart.complete_upload() + # We should get only one copy of part 1. + bucket.get_key("the-key").get_contents_as_string().should.equal(part1 + part2) + @mock_s3 def test_list_multiparts(): # Create Bucket so that test can run From 3ac97318e1ddbd45c93004f3219efae1272fec3f Mon Sep 17 00:00:00 2001 From: Steve Pulec Date: Thu, 5 Mar 2015 19:05:00 -0500 Subject: [PATCH 18/22] Fix etag for reduced min part size. --- tests/test_s3/test_s3.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_s3/test_s3.py b/tests/test_s3/test_s3.py index 9a4048a67..2cf94189d 100644 --- a/tests/test_s3/test_s3.py +++ b/tests/test_s3/test_s3.py @@ -188,7 +188,7 @@ def test_multipart_etag(): multipart.complete_upload() # we should get both parts as the key contents bucket.get_key("the-key").etag.should.equal( - '"140f92a6df9f9e415f74a1463bcee9bb-2"') + '"66d1a1a2ed08fd05c137f316af4ff255-2"') @mock_s3 From 07dd6e554eccae30a27b2d04ac5aadc0680b7625 Mon Sep 17 00:00:00 2001 From: Steve Pulec Date: Thu, 5 Mar 2015 19:32:02 -0500 Subject: [PATCH 19/22] [S3]Only add multipart part_id to partlist if it is not already in there. Closes #324. --- moto/s3/models.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/moto/s3/models.py b/moto/s3/models.py index fd41125ed..6c7788e7a 100644 --- a/moto/s3/models.py +++ b/moto/s3/models.py @@ -152,7 +152,8 @@ class FakeMultipart(object): key = FakeKey(part_id, value) self.parts[part_id] = key - insort(self.partlist, part_id) + if part_id not in self.partlist: + insort(self.partlist, part_id) return key def list_parts(self): From e23edaa47f17d850a69a0461ff2ae31b77f83750 Mon Sep 17 00:00:00 2001 From: Jair Henrique Date: Fri, 6 Mar 2015 14:10:47 -0300 Subject: [PATCH 20/22] add mock for list_endpoints_by_platform_application method --- moto/sns/responses.py | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/moto/sns/responses.py b/moto/sns/responses.py index 90d338f88..cf500376a 100644 --- a/moto/sns/responses.py +++ b/moto/sns/responses.py @@ -184,3 +184,25 @@ class SNSResponse(BaseResponse): } } }) + + def list_endpoints_by_platform_application(self): + return json.dumps({ + "ListEndpointsByPlatformApplicationResponse": { + "ListEndpointsByPlatformApplicationResult": { + "Endpoints": [ + { + "Attributes": { + "Token": "TOKEN", + "Enabled": "true", + "CustomUserData": "" + }, + "EndpointArn": "FAKE_ARN_ENDPOINT" + } + ], + "NextToken": None + }, + "ResponseMetadata": { + "RequestId": "384ac68d-3775-11df-8963-01868b7c937a", + } + } + }) From bdb0f1e31506b298867dd2ede3c0d0d1f9d2c684 Mon Sep 17 00:00:00 2001 From: Jair Henrique Date: Fri, 6 Mar 2015 14:35:17 -0300 Subject: [PATCH 21/22] add tests for list_endpoints_by_platform_application --- tests/test_sns/test_application.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 tests/test_sns/test_application.py diff --git a/tests/test_sns/test_application.py b/tests/test_sns/test_application.py new file mode 100644 index 000000000..24c5a1fbd --- /dev/null +++ b/tests/test_sns/test_application.py @@ -0,0 +1,16 @@ +from __future__ import unicode_literals +import boto + +from moto import mock_sns + + +@mock_sns +def test_get_list_endpoints_by_platform_application(): + conn = boto.connect_sns() + endpoint_list = conn.list_endpoints_by_platform_application( + platform_application_arn='fake_arn' + )['ListEndpointsByPlatformApplicationResponse']['ListEndpointsByPlatformApplicationResult']['Endpoints'] + + endpoint_list.should.have.length_of(1) + endpoint_list[0]['Attributes']['Enabled'].should.equal('true') + endpoint_list[0]['EndpointArn'].should.equal('FAKE_ARN_ENDPOINT') From 5da5c571a950e59982cf9f15b42ac32452e7afe9 Mon Sep 17 00:00:00 2001 From: Michael Date: Wed, 20 May 2015 09:36:40 +0200 Subject: [PATCH 22/22] filtering the items is needed because of defaultdict is not threadsafe and returns an empty dict which results in an exception here --- moto/dynamodb2/models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/moto/dynamodb2/models.py b/moto/dynamodb2/models.py index bc79b7e9a..f24d7398d 100644 --- a/moto/dynamodb2/models.py +++ b/moto/dynamodb2/models.py @@ -220,7 +220,7 @@ class Table(object): results = [] last_page = True # Once pagination is implemented, change this - possible_results = [item for item in list(self.all_items()) if item.hash_key == hash_key] + possible_results = [item for item in list(self.all_items()) if isinstance(item, Item) and item.hash_key == hash_key] if range_comparison: for result in possible_results: if result.range_key.compare(range_comparison, range_objs):