# 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)
