From 5d046c76e564811ac7e68fdef619a4160a0c9862 Mon Sep 17 00:00:00 2001 From: dreadpirateshawn Date: Mon, 13 Oct 2014 16:19:54 -0700 Subject: [PATCH 1/5] Route Tables: Added support for associate/disassociate subnets. --- moto/ec2/models.py | 35 ++++++++-- moto/ec2/responses/route_tables.py | 41 +++++++---- moto/ec2/utils.py | 5 ++ tests/test_ec2/test_route_tables.py | 102 +++++++++++++++++++++++++++- 4 files changed, 164 insertions(+), 19 deletions(-) diff --git a/moto/ec2/models.py b/moto/ec2/models.py index 32b6b1660..5579c8744 100644 --- a/moto/ec2/models.py +++ b/moto/ec2/models.py @@ -73,6 +73,7 @@ from .utils import ( random_snapshot_id, random_spot_request_id, random_subnet_id, + random_subnet_association_id, random_volume_id, random_vpc_id, random_vpc_peering_connection_id, @@ -1560,8 +1561,7 @@ class RouteTable(TaggedEC2Resource): self.id = route_table_id self.vpc_id = vpc_id self.main = main - self.association_id = None - self.subnet_id = None + self.associations = {} self.routes = {} @classmethod @@ -1588,6 +1588,12 @@ class RouteTable(TaggedEC2Resource): return 'false' elif filter_name == "vpc-id": return self.vpc_id + elif filter_name == "association.route-table-id": + return self.id + elif filter_name == "association.route-table-association-id": + return self.associations.keys() + elif filter_name == "association.subnet-id": + return self.associations.values() filter_value = super(RouteTable, self).get_filter_value(filter_name) @@ -1631,10 +1637,27 @@ class RouteTableBackend(object): return generic_filter(filters, route_tables) def delete_route_table(self, route_table_id): - deleted = self.route_tables.pop(route_table_id, None) - if not deleted: - raise InvalidRouteTableIdError(route_table_id) - return deleted + route_table = self.get_route_table(route_table_id) + if route_table.associations: + raise DependencyViolationError( + "The routeTable '{0}' has dependencies and cannot be deleted." + .format(route_table_id) + ) + self.route_tables.pop(route_table_id) + return True + + def associate_route_table(self, route_table_id, subnet_id): + route_table = self.get_route_table(route_table_id) + subnet = self.get_subnet(subnet_id) # Validate subnet exists + association_id = random_subnet_association_id() + route_table.associations[association_id] = subnet_id + return association_id + + def disassociate_route_table(self, association_id): + for route_table in self.route_tables.values(): + if association_id in route_table.associations: + return route_table.associations.pop(association_id, None) + raise InvalidAssociationIdError(association_id) class Route(object): diff --git a/moto/ec2/responses/route_tables.py b/moto/ec2/responses/route_tables.py index 65e9ad144..58f9db949 100644 --- a/moto/ec2/responses/route_tables.py +++ b/moto/ec2/responses/route_tables.py @@ -8,7 +8,11 @@ from moto.ec2.utils import route_table_ids_from_querystring, filters_from_querys class RouteTables(BaseResponse): def associate_route_table(self): - raise NotImplementedError('RouteTables(AmazonVPC).associate_route_table is not yet implemented') + route_table_id = self.querystring.get('RouteTableId')[0] + subnet_id = self.querystring.get('SubnetId')[0] + association_id = ec2_backend.associate_route_table(route_table_id, subnet_id) + template = Template(ASSOCIATE_ROUTE_TABLE_RESPONSE) + return template.render(association_id=association_id) def create_route(self): route_table_id = self.querystring.get('RouteTableId')[0] @@ -55,7 +59,10 @@ class RouteTables(BaseResponse): return template.render(route_tables=route_tables) def disassociate_route_table(self): - raise NotImplementedError('RouteTables(AmazonVPC).disassociate_route_table is not yet implemented') + association_id = self.querystring.get('AssociationId')[0] + ec2_backend.disassociate_route_table(association_id) + template = Template(DISASSOCIATE_ROUTE_TABLE_RESPONSE) + return template.render() def replace_route(self): route_table_id = self.querystring.get('RouteTableId')[0] @@ -151,18 +158,14 @@ DESCRIBE_ROUTE_TABLES_RESPONSE = """ {% endfor %} - {% if route_table.association_id %} + {% for association_id,subnet_id in route_table.associations.items() %} - {{ route_table.association_id }} - {{ route_table.id }} - {% if not route_table.subnet_id %} -
true
- {% endif %} - {% if route_table.subnet_id %} - {{ route_table.subnet_id }} - {% endif %} + {{ association_id }} + {{ route_table.id }} +
false
+ {{ subnet_id }}
- {% endif %} + {% endfor %}
@@ -184,3 +187,17 @@ DELETE_ROUTE_TABLE_RESPONSE = """ true """ + +ASSOCIATE_ROUTE_TABLE_RESPONSE = """ + + 59dbff89-35bd-4eac-99ed-be587EXAMPLE + {{ association_id }} + +""" + +DISASSOCIATE_ROUTE_TABLE_RESPONSE = """ + + 59dbff89-35bd-4eac-99ed-be587EXAMPLE + true + +""" diff --git a/moto/ec2/utils.py b/moto/ec2/utils.py index 1a590d6d7..e16a69353 100644 --- a/moto/ec2/utils.py +++ b/moto/ec2/utils.py @@ -14,6 +14,7 @@ EC2_RESOURCE_TO_PREFIX = { 'network-interface-attachment': 'eni-attach', 'reserved-instance': 'uuid4', 'route-table': 'rtb', + 'route-table-association': 'rtbassoc', 'security-group': 'sg', 'snapshot': 'snap', 'spot-instance-request': 'sir', @@ -67,6 +68,10 @@ def random_subnet_id(): return random_id(prefix=EC2_RESOURCE_TO_PREFIX['subnet']) +def random_subnet_association_id(): + return random_id(prefix=EC2_RESOURCE_TO_PREFIX['route-table-association']) + + def random_volume_id(): return random_id(prefix=EC2_RESOURCE_TO_PREFIX['volume']) diff --git a/tests/test_ec2/test_route_tables.py b/tests/test_ec2/test_route_tables.py index 9df76be2f..ffcb9bb66 100644 --- a/tests/test_ec2/test_route_tables.py +++ b/tests/test_ec2/test_route_tables.py @@ -77,7 +77,7 @@ def test_route_tables_additional(): @mock_ec2 -def test_route_tables_filters(): +def test_route_tables_filters_standard(): conn = boto.connect_vpc('the_key', 'the_secret') vpc1 = conn.create_vpc("10.0.0.0/16") @@ -114,6 +114,106 @@ def test_route_tables_filters(): conn.get_all_route_tables.when.called_with(filters={'not-implemented-filter': 'foobar'}).should.throw(NotImplementedError) +@mock_ec2 +def test_route_tables_filters_associations(): + conn = boto.connect_vpc('the_key', 'the_secret') + + vpc = conn.create_vpc("10.0.0.0/16") + subnet1 = conn.create_subnet(vpc.id, "10.0.0.0/18") + subnet2 = conn.create_subnet(vpc.id, "10.0.1.0/18") + subnet3 = conn.create_subnet(vpc.id, "10.0.2.0/18") + route_table1 = conn.create_route_table(vpc.id) + route_table2 = conn.create_route_table(vpc.id) + + association_id1 = conn.associate_route_table(route_table1.id, subnet1.id) + association_id2 = conn.associate_route_table(route_table1.id, subnet2.id) + association_id3 = conn.associate_route_table(route_table2.id, subnet3.id) + + all_route_tables = conn.get_all_route_tables() + all_route_tables.should.have.length_of(3) + + # Filter by association ID + association1_route_tables = conn.get_all_route_tables(filters={'association.route-table-association-id':association_id1}) + association1_route_tables.should.have.length_of(1) + association1_route_tables[0].id.should.equal(route_table1.id) + association1_route_tables[0].associations.should.have.length_of(2) + + # Filter by route table ID + route_table2_route_tables = conn.get_all_route_tables(filters={'association.route-table-id':route_table2.id}) + route_table2_route_tables.should.have.length_of(1) + route_table2_route_tables[0].id.should.equal(route_table2.id) + route_table2_route_tables[0].associations.should.have.length_of(1) + + # Filter by subnet ID + subnet_route_tables = conn.get_all_route_tables(filters={'association.subnet-id':subnet1.id}) + subnet_route_tables.should.have.length_of(1) + subnet_route_tables[0].id.should.equal(route_table1.id) + association1_route_tables[0].associations.should.have.length_of(2) + + +@mock_ec2 +def test_route_table_associations(): + conn = boto.connect_vpc('the_key', 'the_secret') + vpc = conn.create_vpc("10.0.0.0/16") + subnet = conn.create_subnet(vpc.id, "10.0.0.0/18") + route_table = conn.create_route_table(vpc.id) + + all_route_tables = conn.get_all_route_tables() + all_route_tables.should.have.length_of(2) + + # Refresh + route_table = conn.get_all_route_tables(route_table.id)[0] + print route_table.__dict__ + route_table.associations.should.have.length_of(0) + + # Associate + association_id = conn.associate_route_table(route_table.id, subnet.id) + + # Refresh + route_table = conn.get_all_route_tables(route_table.id)[0] + route_table.associations.should.have.length_of(1) + + route_table.associations[0].id.should.equal(association_id) + route_table.associations[0].main.should.equal(False) + route_table.associations[0].route_table_id.should.equal(route_table.id) + route_table.associations[0].subnet_id.should.equal(subnet.id) + + # Error: Attempt delete associated route table. + with assert_raises(EC2ResponseError) as cm: + conn.delete_route_table(route_table.id) + cm.exception.code.should.equal('DependencyViolation') + cm.exception.status.should.equal(400) + cm.exception.request_id.should_not.be.none + + # Disassociate + conn.disassociate_route_table(association_id) + + # Refresh + route_table = conn.get_all_route_tables(route_table.id)[0] + route_table.associations.should.have.length_of(0) + + # Error: Disassociate with invalid association ID + with assert_raises(EC2ResponseError) as cm: + conn.disassociate_route_table(association_id) + cm.exception.code.should.equal('InvalidAssociationID.NotFound') + cm.exception.status.should.equal(400) + cm.exception.request_id.should_not.be.none + + # Error: Associate with invalid subnet ID + with assert_raises(EC2ResponseError) as cm: + conn.associate_route_table(route_table.id, "subnet-1234abcd") + cm.exception.code.should.equal('InvalidSubnetID.NotFound') + cm.exception.status.should.equal(400) + cm.exception.request_id.should_not.be.none + + # Error: Associate with invalid route table ID + with assert_raises(EC2ResponseError) as cm: + conn.associate_route_table("rtb-1234abcd", subnet.id) + cm.exception.code.should.equal('InvalidRouteTableID.NotFound') + cm.exception.status.should.equal(400) + cm.exception.request_id.should_not.be.none + + @mock_ec2 def test_routes_additional(): conn = boto.connect_vpc('the_key', 'the_secret') From d6b93af7fd83d0925a34e6fd7055616a7681645a Mon Sep 17 00:00:00 2001 From: dreadpirateshawn Date: Tue, 14 Oct 2014 07:55:36 -0700 Subject: [PATCH 2/5] Route Tables: Added support for associate/disassociate subnets. (removed wayward print) --- tests/test_ec2/test_route_tables.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/test_ec2/test_route_tables.py b/tests/test_ec2/test_route_tables.py index ffcb9bb66..f5ed8caf7 100644 --- a/tests/test_ec2/test_route_tables.py +++ b/tests/test_ec2/test_route_tables.py @@ -163,7 +163,6 @@ def test_route_table_associations(): # Refresh route_table = conn.get_all_route_tables(route_table.id)[0] - print route_table.__dict__ route_table.associations.should.have.length_of(0) # Associate From db044df0a9a92ffa9bd8bc09e56e80e63b2fe042 Mon Sep 17 00:00:00 2001 From: dreadpirateshawn Date: Tue, 14 Oct 2014 11:23:42 -0700 Subject: [PATCH 3/5] Route Tables: Added support for associate/disassociate subnets. (added replace route table association) --- moto/ec2/models.py | 8 ++++ moto/ec2/responses/route_tables.py | 13 +++++- tests/test_ec2/test_route_tables.py | 66 +++++++++++++++++++++++++++++ 3 files changed, 86 insertions(+), 1 deletion(-) diff --git a/moto/ec2/models.py b/moto/ec2/models.py index 5579c8744..c9e76a14c 100644 --- a/moto/ec2/models.py +++ b/moto/ec2/models.py @@ -1659,6 +1659,14 @@ class RouteTableBackend(object): return route_table.associations.pop(association_id, None) raise InvalidAssociationIdError(association_id) + def replace_route_table_association(self, association_id, route_table_id): + route_tables_by_association_id = ec2_backend.get_all_route_tables(filters={'association.route-table-association-id':[association_id]}) + if not route_tables_by_association_id: + raise InvalidAssociationIdError(association_id) + previous_route_table = route_tables_by_association_id[0] + subnet_id = previous_route_table.associations.pop(association_id,None) + return self.associate_route_table(route_table_id, subnet_id) + class Route(object): def __init__(self, route_table, destination_cidr_block, local=False, diff --git a/moto/ec2/responses/route_tables.py b/moto/ec2/responses/route_tables.py index 58f9db949..4d73bc917 100644 --- a/moto/ec2/responses/route_tables.py +++ b/moto/ec2/responses/route_tables.py @@ -83,7 +83,11 @@ class RouteTables(BaseResponse): return template.render() def replace_route_table_association(self): - raise NotImplementedError('RouteTables(AmazonVPC).replace_route_table_association is not yet implemented') + route_table_id = self.querystring.get('RouteTableId')[0] + association_id = self.querystring.get('AssociationId')[0] + new_association_id = ec2_backend.replace_route_table_association(association_id, route_table_id) + template = Template(REPLACE_ROUTE_TABLE_ASSOCIATION_RESPONSE) + return template.render(association_id=new_association_id) CREATE_ROUTE_RESPONSE = """ @@ -201,3 +205,10 @@ DISASSOCIATE_ROUTE_TABLE_RESPONSE = """ true """ + +REPLACE_ROUTE_TABLE_ASSOCIATION_RESPONSE = """ + + 59dbff89-35bd-4eac-99ed-be587EXAMPLE + {{ association_id }} + +""" diff --git a/tests/test_ec2/test_route_tables.py b/tests/test_ec2/test_route_tables.py index f5ed8caf7..919410c14 100644 --- a/tests/test_ec2/test_route_tables.py +++ b/tests/test_ec2/test_route_tables.py @@ -213,6 +213,72 @@ def test_route_table_associations(): cm.exception.request_id.should_not.be.none +@mock_ec2 +def test_route_table_replace_route_table_association(): + """ + Note: Boto has deprecated replace_route_table_assocation (which returns status) + and now uses replace_route_table_assocation_with_assoc (which returns association ID). + """ + conn = boto.connect_vpc('the_key', 'the_secret') + vpc = conn.create_vpc("10.0.0.0/16") + subnet = conn.create_subnet(vpc.id, "10.0.0.0/18") + route_table1 = conn.create_route_table(vpc.id) + route_table2 = conn.create_route_table(vpc.id) + + all_route_tables = conn.get_all_route_tables() + all_route_tables.should.have.length_of(3) + + # Refresh + route_table1 = conn.get_all_route_tables(route_table1.id)[0] + route_table1.associations.should.have.length_of(0) + + # Associate + association_id1 = conn.associate_route_table(route_table1.id, subnet.id) + + # Refresh + route_table1 = conn.get_all_route_tables(route_table1.id)[0] + route_table2 = conn.get_all_route_tables(route_table2.id)[0] + + # Validate + route_table1.associations.should.have.length_of(1) + route_table2.associations.should.have.length_of(0) + + route_table1.associations[0].id.should.equal(association_id1) + route_table1.associations[0].main.should.equal(False) + route_table1.associations[0].route_table_id.should.equal(route_table1.id) + route_table1.associations[0].subnet_id.should.equal(subnet.id) + + # Replace Association + association_id2 = conn.replace_route_table_association_with_assoc(association_id1, route_table2.id) + + # Refresh + route_table1 = conn.get_all_route_tables(route_table1.id)[0] + route_table2 = conn.get_all_route_tables(route_table2.id)[0] + + # Validate + route_table1.associations.should.have.length_of(0) + route_table2.associations.should.have.length_of(1) + + route_table2.associations[0].id.should.equal(association_id2) + route_table2.associations[0].main.should.equal(False) + route_table2.associations[0].route_table_id.should.equal(route_table2.id) + route_table2.associations[0].subnet_id.should.equal(subnet.id) + + # Error: Replace association with invalid association ID + with assert_raises(EC2ResponseError) as cm: + conn.replace_route_table_association_with_assoc("rtbassoc-1234abcd", route_table1.id) + cm.exception.code.should.equal('InvalidAssociationID.NotFound') + cm.exception.status.should.equal(400) + cm.exception.request_id.should_not.be.none + + # Error: Replace association with invalid route table ID + with assert_raises(EC2ResponseError) as cm: + conn.replace_route_table_association_with_assoc(association_id2, "rtb-1234abcd") + cm.exception.code.should.equal('InvalidRouteTableID.NotFound') + cm.exception.status.should.equal(400) + cm.exception.request_id.should_not.be.none + + @mock_ec2 def test_routes_additional(): conn = boto.connect_vpc('the_key', 'the_secret') From 50d74b48117a33eeba9b8d30d4837d6aa89f8506 Mon Sep 17 00:00:00 2001 From: dreadpirateshawn Date: Tue, 14 Oct 2014 11:34:58 -0700 Subject: [PATCH 4/5] Route Tables: Added support for associate/disassociate subnets. (added test threshold for Boto 2.16) --- tests/test_ec2/test_route_tables.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/test_ec2/test_route_tables.py b/tests/test_ec2/test_route_tables.py index 919410c14..31236ad25 100644 --- a/tests/test_ec2/test_route_tables.py +++ b/tests/test_ec2/test_route_tables.py @@ -213,6 +213,7 @@ def test_route_table_associations(): cm.exception.request_id.should_not.be.none +@requires_boto_gte("2.16.0") @mock_ec2 def test_route_table_replace_route_table_association(): """ From 89fd71eb1698afde264595de8f5b446cd985a7b3 Mon Sep 17 00:00:00 2001 From: dreadpirateshawn Date: Tue, 14 Oct 2014 14:23:14 -0700 Subject: [PATCH 5/5] Route Tables: Added support for associate/disassociate subnets. (fixed idempotency) --- moto/ec2/models.py | 16 ++++++++++++++++ tests/test_ec2/test_route_tables.py | 8 ++++++++ 2 files changed, 24 insertions(+) diff --git a/moto/ec2/models.py b/moto/ec2/models.py index c9e76a14c..e78137ca3 100644 --- a/moto/ec2/models.py +++ b/moto/ec2/models.py @@ -1647,6 +1647,14 @@ class RouteTableBackend(object): return True def associate_route_table(self, route_table_id, subnet_id): + # Idempotent if association already exists. + route_tables_by_subnet = ec2_backend.get_all_route_tables(filters={'association.subnet-id':[subnet_id]}) + if route_tables_by_subnet: + for association_id,check_subnet_id in route_tables_by_subnet[0].associations.items(): + if subnet_id == check_subnet_id: + return association_id + + # Association does not yet exist, so create it. route_table = self.get_route_table(route_table_id) subnet = self.get_subnet(subnet_id) # Validate subnet exists association_id = random_subnet_association_id() @@ -1660,9 +1668,17 @@ class RouteTableBackend(object): raise InvalidAssociationIdError(association_id) def replace_route_table_association(self, association_id, route_table_id): + # Idempotent if association already exists. + new_route_table = ec2_backend.get_route_table(route_table_id) + if association_id in new_route_table.associations: + return association_id + + # Find route table which currently has the association, error if none. route_tables_by_association_id = ec2_backend.get_all_route_tables(filters={'association.route-table-association-id':[association_id]}) if not route_tables_by_association_id: raise InvalidAssociationIdError(association_id) + + # Remove existing association, create new one. previous_route_table = route_tables_by_association_id[0] subnet_id = previous_route_table.associations.pop(association_id,None) return self.associate_route_table(route_table_id, subnet_id) diff --git a/tests/test_ec2/test_route_tables.py b/tests/test_ec2/test_route_tables.py index 31236ad25..58b65bbb1 100644 --- a/tests/test_ec2/test_route_tables.py +++ b/tests/test_ec2/test_route_tables.py @@ -177,6 +177,10 @@ def test_route_table_associations(): route_table.associations[0].route_table_id.should.equal(route_table.id) route_table.associations[0].subnet_id.should.equal(subnet.id) + # Associate is idempotent + association_id_idempotent = conn.associate_route_table(route_table.id, subnet.id) + association_id_idempotent.should.equal(association_id) + # Error: Attempt delete associated route table. with assert_raises(EC2ResponseError) as cm: conn.delete_route_table(route_table.id) @@ -265,6 +269,10 @@ def test_route_table_replace_route_table_association(): route_table2.associations[0].route_table_id.should.equal(route_table2.id) route_table2.associations[0].subnet_id.should.equal(subnet.id) + # Replace Association is idempotent + association_id_idempotent = conn.replace_route_table_association_with_assoc(association_id2, route_table2.id) + association_id_idempotent.should.equal(association_id2) + # Error: Replace association with invalid association ID with assert_raises(EC2ResponseError) as cm: conn.replace_route_table_association_with_assoc("rtbassoc-1234abcd", route_table1.id)