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…
Reference in New Issue
Block a user