# Copyright 2014 # The Cloudscaling Group, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import collections import copy import netaddr from novaclient import exceptions as nova_exception import six from ec2api.api import common from ec2api.api import ec2utils from ec2api.api import vpn_connection as vpn_connection_api from ec2api import clients from ec2api.db import api as db_api from ec2api import exception from ec2api.i18n import _ HOST_TARGET = 'host' VPN_TARGET = 'vpn' """Route tables related API implementation """ class Validator(common.Validator): def igw_or_vgw_id(self, id): self.ec2_id(id, ['igw', 'vgw']) def create_route_table(context, vpc_id): vpc = ec2utils.get_db_item(context, vpc_id) route_table = _create_route_table(context, vpc) return {'routeTable': _format_route_table(context, route_table, is_main=False)} def create_route(context, route_table_id, destination_cidr_block, gateway_id=None, instance_id=None, network_interface_id=None, vpc_peering_connection_id=None): return _set_route(context, route_table_id, destination_cidr_block, gateway_id, instance_id, network_interface_id, vpc_peering_connection_id, False) def replace_route(context, route_table_id, destination_cidr_block, gateway_id=None, instance_id=None, network_interface_id=None, vpc_peering_connection_id=None): return _set_route(context, route_table_id, destination_cidr_block, gateway_id, instance_id, network_interface_id, vpc_peering_connection_id, True) def delete_route(context, route_table_id, destination_cidr_block): route_table = ec2utils.get_db_item(context, route_table_id) for route_index, route in enumerate(route_table['routes']): if route['destination_cidr_block'] != destination_cidr_block: continue if route.get('gateway_id', 0) is None: msg = _('cannot remove local route %(destination_cidr_block)s ' 'in route table %(route_table_id)s') msg = msg % {'route_table_id': route_table_id, 'destination_cidr_block': destination_cidr_block} raise exception.InvalidParameterValue(msg) break else: raise exception.InvalidRouteNotFound( route_table_id=route_table_id, destination_cidr_block=destination_cidr_block) update_target = _get_route_target(route) if update_target == VPN_TARGET: vpn_gateway = db_api.get_item_by_id(context, route['gateway_id']) if (not vpn_gateway or vpn_gateway['vpc_id'] != route_table['vpc_id']): update_target = None rollback_route_table_state = copy.deepcopy(route_table) del route_table['routes'][route_index] with common.OnCrashCleaner() as cleaner: db_api.update_item(context, route_table) cleaner.addCleanup(db_api.update_item, context, rollback_route_table_state) if update_target: _update_routes_in_associated_subnets( context, cleaner, route_table, update_target=update_target) return True def enable_vgw_route_propagation(context, route_table_id, gateway_id): route_table = ec2utils.get_db_item(context, route_table_id) # NOTE(ft): AWS returns GatewayNotAttached for all invalid cases of # gateway_id value vpn_gateway = ec2utils.get_db_item(context, gateway_id) if vpn_gateway['vpc_id'] != route_table['vpc_id']: raise exception.GatewayNotAttached(gw_id=vpn_gateway['id'], vpc_id=route_table['vpc_id']) if vpn_gateway['id'] in route_table.setdefault('propagating_gateways', []): return True with common.OnCrashCleaner() as cleaner: _append_propagation_to_route_table_item(context, route_table, vpn_gateway['id']) cleaner.addCleanup(_remove_propagation_from_route_table_item, context, route_table, vpn_gateway['id']) _update_routes_in_associated_subnets(context, cleaner, route_table, update_target=VPN_TARGET) return True def disable_vgw_route_propagation(context, route_table_id, gateway_id): route_table = ec2utils.get_db_item(context, route_table_id) if gateway_id not in route_table.get('propagating_gateways', []): return True vpn_gateway = db_api.get_item_by_id(context, gateway_id) with common.OnCrashCleaner() as cleaner: _remove_propagation_from_route_table_item(context, route_table, gateway_id) cleaner.addCleanup(_append_propagation_to_route_table_item, context, route_table, gateway_id) if vpn_gateway and vpn_gateway['vpc_id'] == route_table['vpc_id']: _update_routes_in_associated_subnets(context, cleaner, route_table, update_target=VPN_TARGET) return True def associate_route_table(context, route_table_id, subnet_id): route_table = ec2utils.get_db_item(context, route_table_id) subnet = ec2utils.get_db_item(context, subnet_id) if route_table['vpc_id'] != subnet['vpc_id']: msg = _('Route table %(rtb_id)s and subnet %(subnet_id)s belong to ' 'different networks') msg = msg % {'rtb_id': route_table_id, 'subnet_id': subnet_id} raise exception.InvalidParameterValue(msg) if 'route_table_id' in subnet: msg = _('The specified association for route table %(rtb_id)s ' 'conflicts with an existing association') msg = msg % {'rtb_id': route_table_id} raise exception.ResourceAlreadyAssociated(msg) with common.OnCrashCleaner() as cleaner: _associate_subnet_item(context, subnet, route_table['id']) cleaner.addCleanup(_disassociate_subnet_item, context, subnet) _update_subnet_routes(context, cleaner, subnet, route_table) return {'associationId': ec2utils.change_ec2_id_kind(subnet['id'], 'rtbassoc')} def replace_route_table_association(context, association_id, route_table_id): route_table = ec2utils.get_db_item(context, route_table_id) if route_table['vpc_id'] == ec2utils.change_ec2_id_kind(association_id, 'vpc'): vpc = db_api.get_item_by_id( context, ec2utils.change_ec2_id_kind(association_id, 'vpc')) if vpc is None: raise exception.InvalidAssociationIDNotFound(id=association_id) rollback_route_table_id = vpc['route_table_id'] with common.OnCrashCleaner() as cleaner: _associate_vpc_item(context, vpc, route_table['id']) cleaner.addCleanup(_associate_vpc_item, context, vpc, rollback_route_table_id) _update_routes_in_associated_subnets( context, cleaner, route_table, default_associations_only=True) else: subnet = db_api.get_item_by_id( context, ec2utils.change_ec2_id_kind(association_id, 'subnet')) if subnet is None or 'route_table_id' not in subnet: raise exception.InvalidAssociationIDNotFound(id=association_id) if subnet['vpc_id'] != route_table['vpc_id']: msg = _('Route table association %(rtbassoc_id)s and route table ' '%(rtb_id)s belong to different networks') msg = msg % {'rtbassoc_id': association_id, 'rtb_id': route_table_id} raise exception.InvalidParameterValue(msg) rollback_route_table_id = subnet['route_table_id'] with common.OnCrashCleaner() as cleaner: _associate_subnet_item(context, subnet, route_table['id']) cleaner.addCleanup(_associate_subnet_item, context, subnet, rollback_route_table_id) _update_subnet_routes(context, cleaner, subnet, route_table) return {'newAssociationId': association_id} def disassociate_route_table(context, association_id): subnet = db_api.get_item_by_id( context, ec2utils.change_ec2_id_kind(association_id, 'subnet')) if not subnet: vpc = db_api.get_item_by_id( context, ec2utils.change_ec2_id_kind(association_id, 'vpc')) if vpc is None: raise exception.InvalidAssociationIDNotFound(id=association_id) msg = _('Cannot disassociate the main route table association ' '%(rtbassoc_id)s') % {'rtbassoc_id': association_id} raise exception.InvalidParameterValue(msg) if 'route_table_id' not in subnet: raise exception.InvalidAssociationIDNotFound(id=association_id) rollback_route_table_id = subnet['route_table_id'] vpc = db_api.get_item_by_id(context, subnet['vpc_id']) main_route_table = db_api.get_item_by_id(context, vpc['route_table_id']) with common.OnCrashCleaner() as cleaner: _disassociate_subnet_item(context, subnet) cleaner.addCleanup(_associate_subnet_item, context, subnet, rollback_route_table_id) _update_subnet_routes(context, cleaner, subnet, main_route_table) return True def delete_route_table(context, route_table_id): route_table = ec2utils.get_db_item(context, route_table_id) vpc = db_api.get_item_by_id(context, route_table['vpc_id']) _delete_route_table(context, route_table['id'], vpc) return True class RouteTableDescriber(common.TaggableItemsDescriber, common.NonOpenstackItemsDescriber): KIND = 'rtb' FILTER_MAP = {'association.route-table-association-id': ( ['associationSet', 'routeTableAssociationId']), 'association.route-table-id': ['associationSet', 'routeTableId'], 'association.subnet-id': ['associationSet', 'subnetId'], 'association.main': ['associationSet', 'main'], 'route-table-id': 'routeTableId', 'route.destination-cidr-block': ['routeSet', 'destinationCidrBlock'], 'route.gateway-id': ['routeSet', 'gatewayId'], 'route.instance-id': ['routeSet', 'instanceId'], 'route.origin': ['routeSet', 'origin'], 'route.state': ['routeSet', 'state'], 'vpc-id': 'vpcId'} def format(self, route_table): return _format_route_table( self.context, route_table, associated_subnet_ids=self.associations[route_table['id']], is_main=(self.vpcs[route_table['vpc_id']]['route_table_id'] == route_table['id']), gateways=self.gateways, network_interfaces=self.network_interfaces, vpn_connections_by_gateway_id=self.vpn_connections_by_gateway_id) def get_db_items(self): associations = collections.defaultdict(list) for subnet in db_api.get_items(self.context, 'subnet'): if 'route_table_id' in subnet: associations[subnet['route_table_id']].append(subnet['id']) self.associations = associations vpcs = db_api.get_items(self.context, 'vpc') self.vpcs = {vpc['id']: vpc for vpc in vpcs} gateways = (db_api.get_items(self.context, 'igw') + db_api.get_items(self.context, 'vgw')) self.gateways = {gw['id']: gw for gw in gateways} # TODO(ft): scan route tables to get only used instances and # network interfaces to reduce DB and Nova throughput network_interfaces = db_api.get_items(self.context, 'eni') self.network_interfaces = {eni['id']: eni for eni in network_interfaces} vpn_connections = db_api.get_items(self.context, 'vpn') vpns_by_gateway_id = {} for vpn in vpn_connections: vpns = vpns_by_gateway_id.setdefault(vpn['vpn_gateway_id'], []) vpns.append(vpn) self.vpn_connections_by_gateway_id = vpns_by_gateway_id return super(RouteTableDescriber, self).get_db_items() def describe_route_tables(context, route_table_id=None, filter=None): ec2utils.check_and_create_default_vpc(context) formatted_route_tables = RouteTableDescriber().describe( context, ids=route_table_id, filter=filter) return {'routeTableSet': formatted_route_tables} def _create_route_table(context, vpc): route_table = {'vpc_id': vpc['id'], 'routes': [{'destination_cidr_block': vpc['cidr_block'], 'gateway_id': None}]} route_table = db_api.add_item(context, 'rtb', route_table) return route_table def _delete_route_table(context, route_table_id, vpc=None, cleaner=None): def get_associated_subnets(): return [s for s in db_api.get_items(context, 'subnet') if s.get('route_table_id') == route_table_id] if (vpc and route_table_id == vpc['route_table_id'] or len(get_associated_subnets()) > 0): msg = _("The routeTable '%(rtb_id)s' has dependencies and cannot " "be deleted.") % {'rtb_id': route_table_id} raise exception.DependencyViolation(msg) if cleaner: route_table = db_api.get_item_by_id(context, route_table_id) db_api.delete_item(context, route_table_id) if cleaner and route_table: cleaner.addCleanup(db_api.restore_item, context, 'rtb', route_table) def _set_route(context, route_table_id, destination_cidr_block, gateway_id, instance_id, network_interface_id, vpc_peering_connection_id, do_replace): route_table = ec2utils.get_db_item(context, route_table_id) vpc = db_api.get_item_by_id(context, route_table['vpc_id']) vpc_ipnet = netaddr.IPNetwork(vpc['cidr_block']) route_ipnet = netaddr.IPNetwork(destination_cidr_block) if route_ipnet in vpc_ipnet: msg = _('Cannot create a more specific route for ' '%(destination_cidr_block)s than local route ' '%(vpc_cidr_block)s in route table %(rtb_id)s') msg = msg % {'rtb_id': route_table_id, 'destination_cidr_block': destination_cidr_block, 'vpc_cidr_block': vpc['cidr_block']} raise exception.InvalidParameterValue(msg) obj_param_count = len([p for p in (gateway_id, network_interface_id, instance_id, vpc_peering_connection_id) if p is not None]) if obj_param_count != 1: msg = _('The request must contain exactly one of gatewayId, ' 'networkInterfaceId, vpcPeeringConnectionId or instanceId') if obj_param_count == 0: raise exception.MissingParameter(msg) else: raise exception.InvalidParameterCombination(msg) rollabck_route_table_state = copy.deepcopy(route_table) if do_replace: route_index, old_route = next( ((i, r) for i, r in enumerate(route_table['routes']) if r['destination_cidr_block'] == destination_cidr_block), (None, None)) if route_index is None: msg = _("There is no route defined for " "'%(destination_cidr_block)s' in the route table. " "Use CreateRoute instead.") msg = msg % {'destination_cidr_block': destination_cidr_block} raise exception.InvalidParameterValue(msg) else: del route_table['routes'][route_index] if gateway_id: gateway = ec2utils.get_db_item(context, gateway_id) if gateway.get('vpc_id') != route_table['vpc_id']: if ec2utils.get_ec2_id_kind(gateway_id) == 'vgw': raise exception.InvalidGatewayIDNotFound(id=gateway['id']) else: # igw raise exception.InvalidParameterValue( _('Route table %(rtb_id)s and network gateway %(igw_id)s ' 'belong to different networks') % {'rtb_id': route_table_id, 'igw_id': gateway_id}) route = {'gateway_id': gateway['id']} elif network_interface_id: network_interface = ec2utils.get_db_item(context, network_interface_id) if network_interface['vpc_id'] != route_table['vpc_id']: msg = _('Route table %(rtb_id)s and interface %(eni_id)s ' 'belong to different networks') msg = msg % {'rtb_id': route_table_id, 'eni_id': network_interface_id} raise exception.InvalidParameterValue(msg) route = {'network_interface_id': network_interface['id']} elif instance_id: # TODO(ft): implement search in DB layer network_interfaces = [eni for eni in db_api.get_items(context, 'eni') if eni.get('instance_id') == instance_id] if len(network_interfaces) == 0: msg = _("Invalid value '%(i_id)s' for instance ID. " "Instance is not in a VPC.") msg = msg % {'i_id': instance_id} raise exception.InvalidParameterValue(msg) elif len(network_interfaces) > 1: raise exception.InvalidInstanceId(instance_id=instance_id) network_interface = network_interfaces[0] if network_interface['vpc_id'] != route_table['vpc_id']: msg = _('Route table %(rtb_id)s and interface %(eni_id)s ' 'belong to different networks') msg = msg % {'rtb_id': route_table_id, 'eni_id': network_interface['id']} raise exception.InvalidParameterValue(msg) route = {'network_interface_id': network_interface['id']} else: raise exception.InvalidRequest('Parameter VpcPeeringConnectionId is ' 'not supported by this implementation') route['destination_cidr_block'] = destination_cidr_block update_target = _get_route_target(route) if do_replace: idempotent_call = False old_target = _get_route_target(old_route) if old_target != update_target: update_target = None else: old_route = next((r for r in route_table['routes'] if r['destination_cidr_block'] == destination_cidr_block), None) idempotent_call = old_route == route if old_route and not idempotent_call: raise exception.RouteAlreadyExists( destination_cidr_block=destination_cidr_block) if not idempotent_call: route_table['routes'].append(route) with common.OnCrashCleaner() as cleaner: db_api.update_item(context, route_table) cleaner.addCleanup(db_api.update_item, context, rollabck_route_table_state) _update_routes_in_associated_subnets(context, cleaner, route_table, update_target=update_target) return True def _format_route_table(context, route_table, is_main=False, associated_subnet_ids=[], gateways={}, network_interfaces={}, vpn_connections_by_gateway_id={}): vpc_id = route_table['vpc_id'] ec2_route_table = { 'routeTableId': route_table['id'], 'vpcId': vpc_id, 'routeSet': [], 'propagatingVgwSet': [ {'gatewayId': vgw_id} for vgw_id in route_table.get('propagating_gateways', [])], # NOTE(ft): AWS returns empty tag set for a route table # if no tag exists 'tagSet': [], } # TODO(ft): refactor to get Nova instances outside of this function nova = clients.nova(context) for route in route_table['routes']: origin = ('CreateRouteTable' if route.get('gateway_id', 0) is None else 'CreateRoute') ec2_route = {'destinationCidrBlock': route['destination_cidr_block'], 'origin': origin} if 'gateway_id' in route: gateway_id = route['gateway_id'] if gateway_id is None: state = 'active' ec2_gateway_id = 'local' else: gateway = gateways.get(gateway_id) state = ('active' if gateway and gateway.get('vpc_id') == vpc_id else 'blackhole') ec2_gateway_id = gateway_id ec2_route.update({'gatewayId': ec2_gateway_id, 'state': state}) else: network_interface_id = route['network_interface_id'] network_interface = network_interfaces.get(network_interface_id) instance_id = (network_interface.get('instance_id') if network_interface else None) state = 'blackhole' if instance_id: instance = db_api.get_item_by_id(context, instance_id) if instance: try: os_instance = nova.servers.get(instance['os_id']) if os_instance and os_instance.status == 'ACTIVE': state = 'active' except nova_exception.NotFound: pass ec2_route.update({'instanceId': instance_id, 'instanceOwnerId': context.project_id}) ec2_route.update({'networkInterfaceId': network_interface_id, 'state': state}) ec2_route_table['routeSet'].append(ec2_route) for vgw_id in route_table.get('propagating_gateways', []): vgw = gateways.get(vgw_id) if vgw and vgw_id in vpn_connections_by_gateway_id: cidrs = set() vpn_connections = vpn_connections_by_gateway_id[vgw_id] for vpn_connection in vpn_connections: cidrs.update(vpn_connection['cidrs']) state = 'active' if vgw['vpc_id'] == vpc_id else 'blackhole' for cidr in cidrs: ec2_route = {'gatewayId': vgw_id, 'destinationCidrBlock': cidr, 'state': state, 'origin': 'EnableVgwRoutePropagation'} ec2_route_table['routeSet'].append(ec2_route) associations = [] if is_main: associations.append({ 'routeTableAssociationId': ec2utils.change_ec2_id_kind(vpc_id, 'rtbassoc'), 'routeTableId': route_table['id'], 'main': True}) for subnet_id in associated_subnet_ids: associations.append({ 'routeTableAssociationId': ec2utils.change_ec2_id_kind(subnet_id, 'rtbassoc'), 'routeTableId': route_table['id'], 'subnetId': subnet_id, 'main': False}) if associations: ec2_route_table['associationSet'] = associations return ec2_route_table def _update_routes_in_associated_subnets(context, cleaner, route_table, default_associations_only=None, update_target=None): if default_associations_only: appropriate_rtb_ids = (None,) else: vpc = db_api.get_item_by_id(context, route_table['vpc_id']) if vpc['route_table_id'] == route_table['id']: appropriate_rtb_ids = (route_table['id'], None) else: appropriate_rtb_ids = (route_table['id'],) neutron = clients.neutron(context) subnets = [subnet for subnet in db_api.get_items(context, 'subnet') if (subnet['vpc_id'] == route_table['vpc_id'] and subnet.get('route_table_id') in appropriate_rtb_ids)] # NOTE(ft): we need to update host routes for both host and vpn target # because vpn-related routes are present in host routes as well _update_host_routes(context, neutron, cleaner, route_table, subnets) if not update_target or update_target == VPN_TARGET: vpn_connection_api._update_vpn_routes(context, neutron, cleaner, route_table, subnets) def _update_subnet_routes(context, cleaner, subnet, route_table): neutron = clients.neutron(context) _update_host_routes(context, neutron, cleaner, route_table, [subnet]) vpn_connection_api._update_vpn_routes(context, neutron, cleaner, route_table, [subnet]) def _update_host_routes(context, neutron, cleaner, route_table, subnets): destinations = _get_active_route_destinations(context, route_table) for subnet in subnets: # TODO(ft): do list subnet w/ filters instead of show one by one os_subnet = neutron.show_subnet(subnet['os_id'])['subnet'] host_routes, gateway_ip = _get_subnet_host_routes_and_gateway_ip( context, route_table, os_subnet['cidr'], destinations) neutron.update_subnet(subnet['os_id'], {'subnet': {'host_routes': host_routes, 'gateway_ip': gateway_ip}}) cleaner.addCleanup( neutron.update_subnet, subnet['os_id'], {'subnet': {'host_routes': os_subnet['host_routes'], 'gateway_ip': os_subnet['gateway_ip']}}) def _get_active_route_destinations(context, route_table): vpn_connections = {vpn['vpn_gateway_id']: vpn for vpn in db_api.get_items(context, 'vpn')} dst_ids = [route[id_key] for route in route_table['routes'] for id_key in ('gateway_id', 'network_interface_id') if route.get(id_key) is not None] dst_ids.extend(route_table.get('propagating_gateways', [])) destinations = {item['id']: item for item in db_api.get_items_by_ids(context, dst_ids) if (item['vpc_id'] == route_table['vpc_id'] and (ec2utils.get_ec2_id_kind(item['id']) != 'vgw' or item['id'] in vpn_connections))} for vpn in six.itervalues(vpn_connections): if vpn['vpn_gateway_id'] in destinations: destinations[vpn['vpn_gateway_id']]['vpn_connection'] = vpn return destinations def _get_subnet_host_routes_and_gateway_ip(context, route_table, cidr_block, destinations=None): if not destinations: destinations = _get_active_route_destinations(context, route_table) gateway_ip = str(netaddr.IPAddress( netaddr.IPNetwork(cidr_block).first + 1)) def get_nexthop(route): if 'gateway_id' in route: gateway_id = route['gateway_id'] if gateway_id and gateway_id not in destinations: return '127.0.0.1' return gateway_ip network_interface = destinations.get(route['network_interface_id']) if not network_interface: return '127.0.0.1' return network_interface['private_ip_address'] host_routes = [] subnet_gateway_is_used = False for route in route_table['routes']: nexthop = get_nexthop(route) cidr = route['destination_cidr_block'] if cidr == '0.0.0.0/0': if nexthop == '127.0.0.1': continue elif nexthop == gateway_ip: subnet_gateway_is_used = True host_routes.append({'destination': cidr, 'nexthop': nexthop}) host_routes.extend( {'destination': cidr, 'nexthop': gateway_ip} for vgw_id in route_table.get('propagating_gateways', []) for cidr in (destinations.get(vgw_id, {}).get('vpn_connection', {}). get('cidrs', []))) if not subnet_gateway_is_used: # NOTE(andrey-mp): add route to metadata server host_routes.append( {'destination': '169.254.169.254/32', 'nexthop': gateway_ip}) # NOTE(ft): gateway_ip is set to None to allow correct handling # of 0.0.0.0/0 route by Neutron. gateway_ip = None return host_routes, gateway_ip def _get_route_target(route): if ec2utils.get_ec2_id_kind(route.get('gateway_id') or '') == 'vgw': return VPN_TARGET else: return HOST_TARGET def _associate_subnet_item(context, subnet, route_table_id): subnet['route_table_id'] = route_table_id db_api.update_item(context, subnet) def _disassociate_subnet_item(context, subnet): subnet.pop('route_table_id') db_api.update_item(context, subnet) def _associate_vpc_item(context, vpc, route_table_id): vpc['route_table_id'] = route_table_id db_api.update_item(context, vpc) def _append_propagation_to_route_table_item(context, route_table, gateway_id): vgws = route_table.setdefault('propagating_gateways', []) vgws.append(gateway_id) db_api.update_item(context, route_table) def _remove_propagation_from_route_table_item(context, route_table, gateway_id): vgws = route_table['propagating_gateways'] vgws.remove(gateway_id) if not vgws: del route_table['propagating_gateways'] db_api.update_item(context, route_table)