Merge pull request #229 from DreadPirateShawn/RouteTablesAssociateDisassociate
Route Tables: Added support for associate/disassociate subnets.
This commit is contained in:
		
						commit
						c6f464a154
					
				| @ -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,51 @@ 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): | ||||
|         # 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() | ||||
|         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) | ||||
| 
 | ||||
|     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) | ||||
| 
 | ||||
| 
 | ||||
| class Route(object): | ||||
|  | ||||
| @ -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] | ||||
| @ -76,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 = """ | ||||
| @ -151,18 +162,14 @@ DESCRIBE_ROUTE_TABLES_RESPONSE = """ | ||||
|             {% endfor %} | ||||
|           </routeSet> | ||||
|           <associationSet> | ||||
|             {% if route_table.association_id %} | ||||
|             {% for association_id,subnet_id in route_table.associations.items() %} | ||||
|               <item> | ||||
|                   <routeTableAssociationId>{{ route_table.association_id }}</routeTableAssociationId> | ||||
|                   <routeTableId>{{ route_table.id }}</routeTableId> | ||||
|                 {% if not route_table.subnet_id %} | ||||
|                   <main>true</main> | ||||
|                 {% endif %} | ||||
|                 {% if route_table.subnet_id %} | ||||
|                   <subnetId>{{ route_table.subnet_id }}</subnetId> | ||||
|                 {% endif %} | ||||
|                 <routeTableAssociationId>{{ association_id }}</routeTableAssociationId> | ||||
|                 <routeTableId>{{ route_table.id }}</routeTableId> | ||||
|                 <main>false</main> | ||||
|                 <subnetId>{{ subnet_id }}</subnetId> | ||||
|               </item> | ||||
|             {% endif %} | ||||
|             {% endfor %} | ||||
|           </associationSet> | ||||
|          <tagSet/> | ||||
|        </item> | ||||
| @ -184,3 +191,24 @@ DELETE_ROUTE_TABLE_RESPONSE = """ | ||||
|    <return>true</return> | ||||
| </DeleteRouteTableResponse> | ||||
| """ | ||||
| 
 | ||||
| ASSOCIATE_ROUTE_TABLE_RESPONSE = """ | ||||
| <AssociateRouteTableResponse xmlns="http://ec2.amazonaws.com/doc/2014-06-15/"> | ||||
|    <requestId>59dbff89-35bd-4eac-99ed-be587EXAMPLE</requestId> | ||||
|    <associationId>{{ association_id }}</associationId> | ||||
| </AssociateRouteTableResponse> | ||||
| """ | ||||
| 
 | ||||
| DISASSOCIATE_ROUTE_TABLE_RESPONSE = """ | ||||
| <DisassociateRouteTableResponse xmlns="http://ec2.amazonaws.com/doc/2014-06-15/"> | ||||
|    <requestId>59dbff89-35bd-4eac-99ed-be587EXAMPLE</requestId> | ||||
|    <return>true</return> | ||||
| </DisassociateRouteTableResponse> | ||||
| """ | ||||
| 
 | ||||
| REPLACE_ROUTE_TABLE_ASSOCIATION_RESPONSE = """ | ||||
| <ReplaceRouteTableAssociationResponse xmlns="http://ec2.amazonaws.com/doc/2014-06-15/"> | ||||
|    <requestId>59dbff89-35bd-4eac-99ed-be587EXAMPLE</requestId> | ||||
|    <newAssociationId>{{ association_id }}</newAssociationId> | ||||
| </ReplaceRouteTableAssociationResponse> | ||||
| """ | ||||
|  | ||||
| @ -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']) | ||||
| 
 | ||||
|  | ||||
| @ -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,180 @@ 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] | ||||
|     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) | ||||
| 
 | ||||
|     # 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) | ||||
|     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 | ||||
| 
 | ||||
| 
 | ||||
| @requires_boto_gte("2.16.0") | ||||
| @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) | ||||
| 
 | ||||
|     # 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) | ||||
|     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') | ||||
|  | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user