# Copyright 2019 VMware, Inc.  All rights reserved.
#
#    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 sys

import netaddr
from oslo_config import cfg
from oslo_log import log as logging
from oslo_serialization import jsonutils
from oslo_utils import uuidutils

from networking_l2gw.db.l2gateway import l2gateway_models
from neutron.services.qos import qos_plugin
from neutron_lib.api.definitions import allowedaddresspairs as addr_apidef
from neutron_lib.api.definitions import port_security as psec
from neutron_lib.api.definitions import portbindings as pbin
from neutron_lib.api.definitions import provider_net as pnet
from neutron_lib.api import validators
from neutron_lib.callbacks import registry
from neutron_lib import constants as nl_constants
from neutron_lib import context as n_context

from vmware_nsx.common import config
from vmware_nsx.common import exceptions as nsx_exc
from vmware_nsx.common import nsxv_constants
from vmware_nsx.common import utils as c_utils
from vmware_nsx.db import nsx_portbindings_db as portbinding
from vmware_nsx.db import nsxv_db
from vmware_nsx.plugins.nsx_v import availability_zones as nsx_az
from vmware_nsx.services.lbaas.nsx_p.implementation import lb_utils as lb_pol
from vmware_nsx.services.lbaas.nsx_v import lbaas_common as lb_common
from vmware_nsx.services.lbaas.nsx_v3.implementation import lb_utils
from vmware_nsx.services.lbaas.octavia import constants as oct_const
from vmware_nsx.services.qos.nsx_v3 import pol_utils as qos_utils
from vmware_nsx.shell.admin.plugins.common import constants
from vmware_nsx.shell.admin.plugins.common import formatters
from vmware_nsx.shell.admin.plugins.common import utils as admin_utils
from vmware_nsx.shell.admin.plugins.nsxv.resources import utils
from vmware_nsx.shell import resources as shell
from vmware_nsxlib.v3 import nsx_constants as nsxlib_consts

LOG = logging.getLogger(__name__)


def _get_router_from_network(context, plugin, subnet_id):
    subnet = plugin.get_subnet(context.elevated(), subnet_id)
    network_id = subnet['network_id']
    ports = plugin._get_network_interface_ports(
        context.elevated(), network_id)
    if ports:
        return ports[0]['device_id']


all_errors = []
all_warnings = []
n_errors = 0
n_warnings = 0


def log_error(msg):
    global n_errors
    LOG.error("ERROR: %s", msg)
    all_errors.append(msg)
    n_errors = n_errors + 1


def log_warning(msg):
    global n_warnings
    LOG.warning("WARNING: %s", msg)
    all_warnings.append(msg)
    n_warnings = n_warnings + 1


def _validate_ports(plugin, admin_context):
    # Ports validations:
    # Max number of allowed address pairs (allowing 1 for fixed ips)
    num_allowed_addr_pairs = nsxlib_consts.NUM_ALLOWED_IP_ADDRESSES_v4 - 1
    ports = plugin.get_ports(admin_context)
    for port in ports:
        net_id = port['network_id']
        # Too many address pairs in a port
        address_pairs = port.get(addr_apidef.ADDRESS_PAIRS, [])
        if len(address_pairs) > num_allowed_addr_pairs:
            log_warning("%s allowed address pairs for port %s. "
                        "Only %s are allowed." %
                        (len(address_pairs), port['id'],
                         num_allowed_addr_pairs))

        fixed_ips = [fixed.get('ip_address')
                     for fixed in port['fixed_ips']]
        for pair in address_pairs:
            if (port['mac_address'] == pair['mac_address'] and
                pair['ip_address'] in fixed_ips):
                log_error("Port %s address pair cannot be "
                          "identical to the fixed ip." % port['id'])

        # Compute port on external network
        if (port.get('device_owner', '').startswith(
                nl_constants.DEVICE_OWNER_COMPUTE_PREFIX) and
            plugin._network_is_external(admin_context, net_id)):
            log_error("Compute port %s on external network %s is "
                      "not allowed." % (port['id'], net_id))

        # direct vnic ports are allowed only with vlan networks, and port
        # security must be disabled
        vnic = port.get(pbin.VNIC_TYPE)
        if vnic in portbinding.VNIC_TYPES_DIRECT_PASSTHROUGH:
            net = plugin.get_network(admin_context, port['network_id'])
            net_type = net.get(pnet.NETWORK_TYPE)
            if net_type != 'vlan':
                log_error("Port %s vnic type %s is not supported "
                          "with network type %s." % (port['id'],
                          vnic, net_type))
            elif port.get(psec.PORTSECURITY):
                log_error("Security features are not supported for port %s "
                          "with vnic type %s." % (port['id'], vnic))


def _validate_networks(plugin, admin_context, transit_networks):
    # Networks & subnets validations:
    networks = plugin.get_networks(admin_context)
    for net in networks:
        # Skip internal networks
        if net['project_id'] == nsxv_constants.INTERNAL_TENANT_ID:
            continue

        # Skip public networks
        if plugin._network_is_external(admin_context, net['id']):
            continue

        # portgroup provider networks are not supported
        # This includes FLAT and PORTGROUP networks
        net_type = net.get(pnet.NETWORK_TYPE)
        overlay_net = bool(net_type != c_utils.NsxVNetworkTypes.VLAN)
        if (net_type in [c_utils.NsxVNetworkTypes.PORTGROUP,
                         c_utils.NsxVNetworkTypes.FLAT]):
            log_error("Network %s of type %s is not supported." %
                      (net['id'], net_type))

        subnets = plugin._get_subnets_by_network(admin_context, net['id'])
        n_dhcp_subnets = 0

        # Multiple DHCP subnets per network
        for subnet in subnets:
            if subnet['enable_dhcp']:
                n_dhcp_subnets = n_dhcp_subnets + 1
        if n_dhcp_subnets > 1:
            log_error("Network %s has %s dhcp subnets. Only 1 is "
                      "allowed." % (net['id'], n_dhcp_subnets))

        # Network attached to multiple routers
        router_ids = set(plugin._get_network_router_ids(
            admin_context, net['id']))
        if len(router_ids) > 1:
            log_error("Network %s has interfaces on multiple "
                      "routers (%s). Only 1 is allowed."
                      % (net['id'], ",".join(router_ids)))

        if (cfg.CONF.vlan_transparent and
            net.get('vlan_transparent') is True):
            if len(router_ids) > 0:
                log_error("VLAN Transparent network %s cannot be "
                          "attached to a logical router." % net['id'])
            if n_dhcp_subnets > 0:
                log_error("DHCP is not supported for VLAN "
                          "transparent network %s." % net['id'])

        # Subnets overlapping with the transit network
        ipv6_subnets = 0
        intf_ports = plugin._get_network_interface_ports(
            admin_context, net['id'])
        for subnet in subnets:
            # get the subnet IPs
            if ('allocation_pools' in subnet and
                validators.is_attr_set(subnet['allocation_pools'])):
                # use the pools instead of the cidr
                subnet_networks = [
                    netaddr.IPRange(pool.get('start'), pool.get('end'))
                    for pool in subnet.get('allocation_pools')]
            else:
                cidr = subnet.get('cidr')
                if not validators.is_attr_set(cidr):
                    return
                subnet_networks = [netaddr.IPNetwork(subnet['cidr'])]

            for subnet_net in subnet_networks:
                if (netaddr.IPSet(subnet_net) &
                    netaddr.IPSet(transit_networks)):
                    log_error("Subnet %s overlaps with the transit "
                              "network ips: %s." %
                              (subnet['id'], transit_networks))

            # Cannot support non-dhcp overlay subnet attached to a router
            # if there is also a dhcp subnet on the same network and the
            # same ipver
            if (overlay_net and n_dhcp_subnets > 0 and
                not subnet['enable_dhcp'] and
                subnet.get('ip_version', 4) == 4):
                # look for a router interface for this subnet
                for if_port in intf_ports:
                    if if_port['fixed_ips']:
                        if_sub = if_port['fixed_ips'][0]['subnet_id']
                        if subnet['id'] == if_sub:
                            log_error("Network %s has non-dhcp "
                                      "subnet attached to a router, and "
                                      "another dhcp subnet. This is not "
                                      "allowed." % net['id'])

            # Cannot use a non-gateway subnet attached to a router
            if not subnet['gateway_ip']:
                for if_port in intf_ports:
                    if if_port['fixed_ips']:
                        if_sub = if_port['fixed_ips'][0]['subnet_id']
                        if subnet['id'] == if_sub:
                            log_error("Subnet %s attached to a "
                                      "router must have a gateway IP." %
                                      subnet['id'])
            else:
                # The gateway ip must belong to the subnet
                gw_ip = netaddr.IPAddress(subnet['gateway_ip'])
                cidr = netaddr.IPNetwork(subnet['cidr'])
                if gw_ip.version != cidr.version:
                    log_error("Subnet %s gateway ip version %s "
                              "does not match subnet cidr." %
                              (subnet['id'], gw_ip.version))
                if gw_ip not in cidr:
                    log_error("Subnet %s gateway ip %s does not belong to "
                              "subnet cidr %s" %
                              (subnet['id'], subnet['gateway_ip'],
                               subnet['cidr']))

            # only 2 dns_nameservers allowed
            if len(subnet.get('dns_nameservers', [])) > 2:
                log_error("Subnet %s cannot have more than 2 "
                          "dns_nameservers." % subnet['id'])

            if subnet.get('ip_version') == 6:
                ipv6_subnets = ipv6_subnets + 1

        if ipv6_subnets > 1:
            log_error("Network %s cannot have more than 1 "
                      "IPv6 subnets." % net['id'])


def _validate_routers(plugin, admin_context):
    # Routers validations:
    routers = plugin.get_routers(admin_context)
    for router in routers:
        # Interface subnets overlap with the GW subnet
        gw_subnets = plugin._find_router_gw_subnets(admin_context, router)
        gw_cidrs = [subnet['cidr'] for subnet in gw_subnets]
        gw_ip_set = netaddr.IPSet(gw_cidrs)

        if_cidrs = plugin._find_router_subnets_cidrs(
            admin_context, router['id'])
        if_ip_set = netaddr.IPSet(if_cidrs)

        if gw_ip_set & if_ip_set:
            log_error("Interface network of router %s cannot "
                      "overlap with router GW network" % router['id'])

        # router without external gw cannot be attached to a vlan subnet
        router_db = plugin._get_router(admin_context, router['id'])
        if not router_db.gw_port:
            router_subnets = plugin._load_router_subnet_cidrs_from_db(
                admin_context, router['id'])
            for subnet in router_subnets:
                net_id = subnet['network_id']
                net = plugin.get_network(admin_context, net_id)
                net_type = net.get(pnet.NETWORK_TYPE)
                if net_type == c_utils.NsxVNetworkTypes.VLAN:
                    log_error("Vlan network %s cannot be attached "
                              "to router %s without a gateway" % (net_id,
                              router['id']))


def _validate_loadbalancers(plugin, admin_context):
    # Octavia loadbalancers validation:
    filters = {'device_owner': [nl_constants.DEVICE_OWNER_LOADBALANCERV2,
                                oct_const.DEVICE_OWNER_OCTAVIA]}
    lbs_map = {}
    lb_ports = plugin.get_ports(admin_context, filters=filters)
    for port in lb_ports:
        lb_id = port.get('device_id')
        fixed_ips = port.get('fixed_ips', [])
        if fixed_ips:
            subnet_id = fixed_ips[0]['subnet_id']
            network = lb_utils.get_network_from_subnet(
                admin_context, plugin, subnet_id)
            lb_rtr_id = _get_router_from_network(
                admin_context, plugin, subnet_id)
            # only 20 loadbalancers are allowed on the same router
            if lb_rtr_id not in lbs_map:
                lbs_map[lb_rtr_id] = 1
            else:
                lbs_map[lb_rtr_id] = lbs_map[lb_rtr_id] + 1
                if lbs_map[lb_rtr_id] == lb_pol.SERVICE_LB_TAG_MAX + 1:
                    log_error("Router %s has over %s attached "
                              "loadbalancers. This is not supported." %
                              (lb_rtr_id, lb_pol.SERVICE_LB_TAG_MAX))
            # Loadbalancer vip subnet must be connected to a router or
            # belong to an external network
            if (not lb_rtr_id and network and
                not network.get('router:external')):
                log_error("Loadbalancer %s subnet %s is not "
                          "external nor connected to a router." %
                          (port.get('device_id'), subnet_id))

        if not lb_id:
            continue

        lb_id = lb_id[3:]
        lb_binding = nsxv_db.get_nsxv_lbaas_loadbalancer_binding(
            admin_context.session, lb_id)
        if not lb_binding or not lb_binding['edge_id']:
            LOG.info("Cannot find edge for Loadbalancer %s", lb_id)
            continue
        edge_id = lb_binding['edge_id']

        # Multiple listeners on the same pool is not supported
        result = plugin.nsx_v.vcns.get_vips(edge_id)
        if len(result) == 2:
            edge_vs = result[1]
            pools = []
            for vip in edge_vs.get('virtualServer', []):
                if not vip.get('defaultPoolId'):
                    continue
                if vip['defaultPoolId'] in pools:
                    log_error("Found multiple listeners using the "
                              "same default pool with loadbalancer %s. "
                              "This is not supported." % lb_id)
                    break
                pools.append(vip['defaultPoolId'])

        # Cannot support LB with members from various subnets not uplinked
        # to the same edge router. This can be indicated by multiple
        # internal interfaces on the LB edge
        is_old_lb = lb_common.is_lb_on_router_edge(
            admin_context, plugin, edge_id)
        if not is_old_lb:
            filters = {'device_id': [lb_id],
                       'device_owner': [lb_common.LBAAS_DEVICE_OWNER]}
            lb_ports = plugin.get_ports(admin_context, filters=filters)
            # get the subnets of those ports
            lb_subnets = list(set([port['fixed_ips'][0]['subnet_id']
                                   for port in lb_ports]))
            # make sure all subnets are connected to the same router
            lb_routers = []
            if lb_rtr_id:
                lb_routers = [lb_rtr_id]
            for sub_id in lb_subnets:
                # skip external subnets
                network = lb_utils.get_network_from_subnet(
                    admin_context, plugin, sub_id)
                if network.get('router:external'):
                    # Member on external subnet must have a fip but this cannot
                    # be checked here are the member ip is unknown
                    continue
                router_id = _get_router_from_network(
                    admin_context, plugin, sub_id)
                if not router_id:
                    log_error("Found member of subnet %s not "
                              "uplinked to any router on loadbalancer "
                              "%s. This is not supported." %
                              (sub_id, lb_id))
                elif router_id not in lb_routers:
                    lb_routers.append(router_id)
            if len(lb_routers) > 1:
                log_error("Found members/vips from different "
                          "subnets or uplinks to different routers on "
                          "loadbalancer %s. This is not supported." %
                          lb_id)
                break

            # Make sure this router has a gateway
            if lb_routers:
                router_db = plugin._get_router(admin_context, lb_routers[0])
                if not router_db.gw_port:
                    log_error("Loadbalancer's %s subnets are connected to a "
                              "router without a gateway. This is not "
                              "supported." % lb_id)
                    break


def _validate_security_groups(plugin, admin_context):
    # Security groups without policies
    sgs = plugin.get_security_groups(admin_context)
    for sg in sgs:
        if plugin._is_policy_security_group(admin_context, sg['id']):
            log_error("Security group %s has NSX policy. This is not "
                      "supported." % sg['id'])


def _get_config_ext_nets():
    config.register_nsxv_azs(cfg.CONF, cfg.CONF.nsxv.availability_zones)
    zones = nsx_az.NsxVAvailabilityZones()
    nets = []
    for az in zones.list_availability_zones_objects():
        nets.append(az.external_network)
    return nets


def _validate_non_neutron_networks(admin_context):
    # Look for orphaned neutron networks and non neutron backend networks
    backend_networks = utils.get_networks()
    missing_networks = utils.get_orphaned_networks(backend_networks)
    config_networks = _get_config_ext_nets()

    missing_morefs = []
    for net in missing_networks:
        log_warning("NSX backend network %s:%s is missing from Neutron "
                    "and is probably an orphaned. Please delete it." %
                    (net.get('moref'), net.get('name')))
        missing_morefs.append(net.get('moref'))

    for net in backend_networks:
        moref = net['moref']
        name = net['name']
        net_type = net['type']

        if moref in missing_morefs:
            # Already reported
            continue

        if ((len(name) < 36 or not uuidutils.is_uuid_like(name)) and
            net_type in ['DistributedVirtualPortgroup', 'VirtualWire']):
            if (net_type == 'DistributedVirtualPortgroup' and
                name.startswith('edge-')):
                continue

            if moref in config_networks:
                continue

            if name:
                # Find the vlan networks
                id_from_name = name[-36:]
                if nsxv_db.get_network_bindings(admin_context.session,
                                                id_from_name):
                    continue
            if net_type == 'DistributedVirtualPortgroup':
                if nsxv_db.get_network_bindings_by_physical_net(
                    admin_context.session, moref):
                    continue
            if net_type == 'VirtualWire':
                # Find internal networks for distributed routers
                filters = {'lswitch_id': moref}
                if nsxv_db.get_nsxv_router_bindings(admin_context.session,
                                                    like_filters=filters):
                    continue

            log_warning("NSX backend network %s:%s is not a "
                        "Neutron network and cannot be migrated. "
                        "Please delete it or migrate it manually." %
                        (moref, name))


def _validate_non_neutron_edges():
    # Look for orphaned or non-neutron edges
    orphaned_edges = utils.get_orphaned_edges_data()
    for edge in orphaned_edges:
        log_warning("NSX %s:%s does not belong to Neutron. "
                    "Please delete it." %
                    (edge.get('id'), edge.get('name')))


def _validate_qos(admin_context):
    # Validate QoS limits
    qos_plugin_inst = qos_plugin.QoSPlugin()
    policies = qos_plugin_inst.get_policies(admin_context)
    for policy in policies:
        for rule in policy.get('rules', []):
            if rule.get('type') == 'bandwidth_limit':
                # Validate the limits
                if rule.get('max_kbps') < qos_utils.MAX_KBPS_MIN_VALUE:
                    log_error("QoS Policy %s has max_kbps below the "
                              "minimal value of %s. This is not supported." %
                              (policy['id'], rule['max_kbps']))
                if rule.get('max_burst_kbps') > qos_utils.MAX_BURST_MAX_VALUE:
                    log_error("QoS Policy %s has max_burst_kbps above "
                              "the maximal value of %s. This is not "
                              "supported." %
                              (policy['id'], rule['max_burst_kbps']))


def _validate_l2gw(admin_context):
    # L2GW is not supported with the policy plugin
    try:
        l2gws = admin_context.session.query(l2gateway_models.L2Gateway).all()
    except Exception:
        # L2GW DB was not initialized
        pass
    else:
        if len(l2gws):
            log_error("Found %s L2Gws: %s. Networking-l2gw is not "
                      "supported." % (len(l2gws), [l2gw.id for l2gw in l2gws]))


def _validate_config():
    # General config options / per AZ which are unsupported
    config.register_nsxv_azs(cfg.CONF, cfg.CONF.nsxv.availability_zones)
    zones = nsx_az.NsxVAvailabilityZones()
    unsupported_configs = ['edge_ha', 'edge_host_groups']
    for az in zones.list_availability_zones_objects():
        for attr in unsupported_configs:
            if getattr(az, attr):
                log_warning("\'%s\' configuration is not supported "
                            "and will not be honored by NSX-T (availability "
                            "zone %s)" % (attr, az.name))


@admin_utils.output_header
@admin_utils.unpack_payload
def validate_config_for_migration(resource, event, trigger, **kwargs):
    """Validate the nsxv configuration before migration to nsx-t"""
    # Read the command line parameters
    transit_networks = ["100.64.0.0/16"]
    strict = False
    out_file = None
    if kwargs.get('property'):
        # input validation
        properties = admin_utils.parse_multi_keyval_opt(kwargs['property'])
        transit_network = properties.get('transit-network')
        if transit_network:
            transit_networks = [transit_network]
        strict = bool(properties.get('strict', 'false').lower() == 'true')
        out_file = properties.get('summary-file-name')
    LOG.info("Running migration config validation in %sstrict mode",
             '' if strict else 'non-')

    global all_errors
    all_errors = []
    global all_warnings
    all_warnings = []
    global n_errors
    n_errors = 0
    global n_warnings
    n_warnings = 0

    admin_context = n_context.get_admin_context()

    _validate_config()

    try:
        with utils.NsxVPluginWrapper() as plugin:
            # The migration is supported only for NSX 6.4.9 and above
            nsx_ver = plugin.nsx_v.vcns.get_version()
            if not c_utils.is_nsxv_version_6_4_9(nsx_ver):
                log_error("Migration with NSX-V version %s is not "
                          "supported." % nsx_ver)

            _validate_ports(plugin, admin_context)
            _validate_networks(plugin, admin_context, transit_networks)
            _validate_routers(plugin, admin_context)
            _validate_loadbalancers(plugin, admin_context)
            _validate_security_groups(plugin, admin_context)

    except nsx_exc.NsxPluginException:
        log_error("NSX-V configuration cannot be migrated because the "
                  "plugin is currently down. This may be caused by a "
                  "connectivity issue with the NSX-v")

    else:
        _validate_non_neutron_networks(admin_context)
        _validate_non_neutron_edges()
        _validate_qos(admin_context)
        _validate_l2gw(admin_context)

    LOG.info("\nPre-migration validation is complete")
    if n_errors:
        LOG.info("\nFound %s errors:", n_errors)
        for msg in all_errors:
            LOG.info(msg)
    if n_warnings:
        LOG.info("\nFound %s warnings:", n_warnings)
        for msg in all_warnings:
            LOG.info(msg)

    if out_file:
        f = open(out_file, "w")
        if n_errors:
            f.write("Found %s errors:\n" % n_errors)
            for msg in all_errors:
                f.write("%s\n" % msg)
        if n_warnings:
            f.write("Found %s warnings:\n" % n_warnings)
            for msg in all_warnings:
                f.write("%s\n" % msg)
        f.close()

    if strict:
        n_errors = n_errors + n_warnings

    if n_errors > 0:
        plural = n_errors > 1
        LOG.info("\nThe NSX-V plugin configuration is not ready to be "
                 "migrated to NSX-T. %s issue%s found.", n_errors,
                 's were' if plural else ' was')
        sys.exit(n_errors)

    LOG.info("\nThe NSX-V plugin configuration is ready to be migrated to "
             "NSX-T.")


@admin_utils.output_header
@admin_utils.unpack_payload
def list_ports_vif_ids(resource, event, trigger, **kwargs):
    filename = None
    if kwargs.get('property'):
        properties = admin_utils.parse_multi_keyval_opt(kwargs['property'])
        filename = properties.get('map-file')

    admin_context = n_context.get_admin_context()
    table_results = []
    map_results = {}

    with utils.NsxVPluginWrapper() as plugin:
        neutron_ports = plugin.get_ports(admin_context)
        for port in neutron_ports:
            # skip non compute ports
            if (not port.get('device_owner').startswith(
                nl_constants.DEVICE_OWNER_COMPUTE_PREFIX)):
                continue
            device_id = port.get('device_id')
            port_id = port['id']
            vnic_index = plugin._get_port_vnic_index(admin_context, port_id)
            table_results.append({'neutron_id': port_id,
                                  'instance_id': device_id,
                                  'vnic_index': vnic_index})
            if vnic_index is not None:
                map_results[port_id] = '%s:%s' % (device_id, 4000 + vnic_index)

    LOG.info(formatters.output_formatter(
        "Compute ports VID IDs", table_results,
        ['neutron_id', 'instance_id', 'vnic_index']))
    if filename:
        f = open(filename, "w")
        f.write("%s" % jsonutils.dumps(map_results))
        f.close()
        LOG.info("Mapping data saved into %s", filename)


@admin_utils.output_header
@admin_utils.unpack_payload
def get_routers_without_interfaces(resource, event, trigger, **kwargs):
    context = n_context.get_admin_context()
    empty_routers = []
    with utils.NsxVPluginWrapper() as plugin:
        routers = plugin.get_routers(context)
        for router in routers:
            # Routers in the metadata internal project will always have an
            # interface, but it won't be returned by the method below, since
            # the device_owner is network:md_interface
            if router['project_id'] == 'metadata_internal_project':
                LOG.debug("Skipping internal router %s", router['id'])
                continue
            interfaces = plugin._get_router_interfaces(router['id'])
            if not interfaces:
                empty_routers.append(router)
    LOG.info(formatters.output_formatter(
        "Routers without interfaces", empty_routers,
        ['id', 'name', 'project_id']))
    return empty_routers


@admin_utils.output_header
@admin_utils.unpack_payload
def build_edge_mapping_file(resource, event, trigger, **kwargs):
    filename = None
    if kwargs.get('property'):
        properties = admin_utils.parse_multi_keyval_opt(kwargs['property'])
        filename = properties.get('map-file')
    context = n_context.get_admin_context()
    mappings = []
    with utils.NsxVPluginWrapper() as plugin:
        routers = plugin.get_routers(context)
        for router in routers:
            if router.get('distributed'):
                binding = nsxv_db.get_nsxv_router_binding(context.session,
                                                          router['id'])
                if binding:
                    edge_id = binding['edge_id']
                    mappings.append(
                        {'v_edges': [edge_id],
                         'policy_gateway_name': router['id']})
    if not mappings:
        LOG.info("No edge mapping available. Not producing output file")
        return

    data = [{'name': 'dlr_edges_to_migrate',
             'v_edges_to_policy_gateways_mappings': mappings}]
    LOG.info(formatters.output_formatter(
        "DLR/Neutron router id mappings", mappings,
        ['v_edges', 'policy_gateway_name']))
    if filename:
        f = open(filename, "w")
        f.write("%s" % jsonutils.dumps(data))
        f.close()
        LOG.info("Edge mapping data saved into %s", filename)


registry.subscribe(get_routers_without_interfaces,
                   constants.NSX_MIGRATE_V_T,
                   shell.Operations.LIST_RTR_NO_IFACE.value)

registry.subscribe(validate_config_for_migration,
                   constants.NSX_MIGRATE_V_T,
                   shell.Operations.VALIDATE.value)

registry.subscribe(build_edge_mapping_file,
                   constants.NSX_MIGRATE_V_T,
                   shell.Operations.CUTOVER_MAPPINGS.value)

registry.subscribe(list_ports_vif_ids,
                   constants.PORTS,
                   shell.Operations.LIST.value)
