# Copyright 2014 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 hashlib
import hmac

import eventlet
import netaddr
from neutron_lib import constants
from neutron_lib import context as neutron_context
from neutron_lib.plugins import directory
from oslo_config import cfg
from oslo_log import log as logging

from vmware_nsx._i18n import _
from vmware_nsx.common import exceptions as nsxv_exc
from vmware_nsx.common import locking
from vmware_nsx.common import nsxv_constants
from vmware_nsx.common import utils
from vmware_nsx.db import nsxv_db
from vmware_nsx.extensions import projectpluginmap
from vmware_nsx.plugins.nsx_v.vshield import (
    nsxv_loadbalancer as nsxv_lb)
from vmware_nsx.plugins.nsx_v.vshield.common import (
    constants as vcns_const)
from vmware_nsx.plugins.nsx_v.vshield.common import exceptions
from vmware_nsx.plugins.nsx_v.vshield import edge_utils
from vmware_nsx.services.lbaas.nsx_v import lbaas_common

METADATA_POOL_NAME = 'MDSrvPool'
METADATA_VSE_NAME = 'MdSrv'
METADATA_IP_ADDR = '169.254.169.254'
METADATA_TCP_PORT = 80
METADATA_HTTPS_PORT = 443
METADATA_HTTPS_VIP_PORT = 8775
INTERNAL_SUBNET = '169.254.128.0/17'
MAX_INIT_THREADS = 3

NET_WAIT_INTERVAL = 240
NET_CHECK_INTERVAL = 10
EDGE_WAIT_INTERVAL = 900
EDGE_CHECK_INTERVAL = 10

LOG = logging.getLogger(__name__)

DEFAULT_EDGE_FIREWALL_RULE = {
    'name': 'VSERule',
    'enabled': True,
    'action': 'allow',
    'source_vnic_groups': ['vse'],
    'destination_vnic_groups': ['external']}


def get_router_fw_rules():
    # build the allowed destination ports list
    int_ports = [METADATA_TCP_PORT,
                 METADATA_HTTPS_PORT,
                 METADATA_HTTPS_VIP_PORT]
    str_ports = [str(p) for p in int_ports]
    # the list of ports can be extended by configuration
    if cfg.CONF.nsxv.metadata_service_allowed_ports:
        str_metadata_ports = [str(p) for p in
                              cfg.CONF.nsxv.metadata_service_allowed_ports]
        str_ports = str_ports + str_metadata_ports
    separator = ','
    dest_ports = separator.join(str_ports)

    fw_rules = [
        DEFAULT_EDGE_FIREWALL_RULE,
        {
            'name': 'MDServiceIP',
            'enabled': True,
            'action': 'allow',
            'destination_ip_address': [METADATA_IP_ADDR],
            'protocol': 'tcp',
            'destination_port': dest_ports
        },
        {
            'name': 'VSEMDInterEdgeNet',
            'enabled': True,
            'action': 'allow',
            'source_vnic_groups': ['vse'],
            'destination_ip_address': [INTERNAL_SUBNET]
        },
        {
            'name': 'MDInterEdgeNet',
            'enabled': True,
            'action': 'deny',
            'destination_ip_address': [INTERNAL_SUBNET]
        }]

    return fw_rules


def get_db_internal_edge_ips(context, az_name):
    ip_list = []
    edge_list = nsxv_db.get_nsxv_internal_edges_by_purpose(
        context.session,
        vcns_const.InternalEdgePurposes.INTER_EDGE_PURPOSE)

    if edge_list:
        # Take only the edges on this availability zone
        ip_list = [edge['ext_ip_address'] for edge in edge_list
        if nsxv_db.get_router_availability_zone(
            context.session, edge['router_id']) == az_name]
    return ip_list


class NsxVMetadataProxyHandler(object):
    """A metadata proxy handler for a specific availability zone"""
    def __init__(self, nsxv_plugin, availability_zone):
        LOG.debug('Initializing metadata for availability zone %s',
                  availability_zone.name)
        self.nsxv_plugin = nsxv_plugin
        context = neutron_context.get_admin_context()
        self.az = availability_zone

        # Init cannot run concurrently on multiple nodes
        with locking.LockManager.get_lock('nsx-metadata-init'):
            # if the core plugin is the TVD - we need to add project
            # plugin mapping for the internal project
            core_plugin = directory.get_plugin()
            if core_plugin.is_tvd_plugin():
                try:
                    core_plugin.create_project_plugin_map(
                        context,
                        {'project_plugin_map':
                            {'plugin': projectpluginmap.NsxPlugins.NSX_V,
                             'project': nsxv_constants.INTERNAL_TENANT_ID}},
                        internal=True)
                except projectpluginmap.ProjectPluginAlreadyExists:
                    pass
            self.internal_net, self.internal_subnet = (
                self._get_internal_network_and_subnet(context))

            LOG.debug('Metadata internal net for AZ %(az)s is %(net)s, '
                      'subnet is %(sub)s',
                      {'az': availability_zone.name,
                       'net': self.internal_net,
                       'sub': self.internal_subnet})
            self.proxy_edge_ips = self._get_proxy_edges(context)
            LOG.debug('Metadata proxy internal IPs for AZ %(az)s is '
                      '%(addresses)s', {'az': availability_zone.name,
                                        'addresses': self.proxy_edge_ips})

    def _create_metadata_internal_network(self, context, cidr):
        # Neutron requires a network to have some tenant_id
        tenant_id = nsxv_constants.INTERNAL_TENANT_ID
        net_name = 'inter-edge-net'
        if not self.az.is_default():
            net_name = '%s-%s' % (net_name, self.az.name)
        net_data = {'network': {'name': net_name,
                                'admin_state_up': True,
                                'port_security_enabled': False,
                                'shared': False,
                                'availability_zone_hints': [self.az.name],
                                'tenant_id': tenant_id}}
        net = self.nsxv_plugin.create_network(context, net_data)

        subnet_data = {'subnet':
                       {'cidr': cidr,
                        'name': 'inter-edge-subnet',
                        'gateway_ip': constants.ATTR_NOT_SPECIFIED,
                        'allocation_pools': constants.ATTR_NOT_SPECIFIED,
                        'ip_version': 4,
                        'dns_nameservers': constants.ATTR_NOT_SPECIFIED,
                        'host_routes': constants.ATTR_NOT_SPECIFIED,
                        'enable_dhcp': False,
                        'network_id': net['id'],
                        'tenant_id': tenant_id}}

        subnet = self.nsxv_plugin.create_subnet(
            context,
            subnet_data)

        return net['id'], subnet['id']

    def _get_internal_net_by_az(self, context):
        # Get the internal network for the current az
        int_net = nsxv_db.get_nsxv_internal_network_for_az(
            context.session,
            vcns_const.InternalEdgePurposes.INTER_EDGE_PURPOSE,
            self.az.name)

        if int_net:
            return int_net['network_id']

    def _get_internal_network_and_subnet(self, context):
        # Try to find internal net, internal subnet. If not found, create new
        internal_net = self._get_internal_net_by_az(context)
        internal_subnet = None

        if internal_net:
            internal_subnet = self.nsxv_plugin.get_subnets(
                context,
                fields=['id'],
                filters={'network_id': [internal_net]})[0]['id']

        if internal_net is None or internal_subnet is None:
            if cfg.CONF.nsxv.metadata_initializer:
                # Couldn't find net, subnet - create new
                try:
                    internal_net, internal_subnet = (
                        self._create_metadata_internal_network(
                            context, INTERNAL_SUBNET))
                except Exception as e:
                    nsxv_db.delete_nsxv_internal_network(
                        context.session,
                        vcns_const.InternalEdgePurposes.INTER_EDGE_PURPOSE,
                        internal_net)

                    # if network is created, clean up
                    if internal_net:
                        self.nsxv_plugin.delete_network(context,
                                                        internal_net)

                    error = (_("Exception %s while creating internal "
                               "network for metadata service") % e)
                    LOG.exception(error)
                    raise nsxv_exc.NsxPluginException(err_msg=error)

                # Update the new network_id in DB
                nsxv_db.create_nsxv_internal_network(
                    context.session,
                    nsxv_constants.INTER_EDGE_PURPOSE,
                    self.az.name,
                    internal_net)
            else:
                error = _('Metadata initialization is incomplete on '
                          'initializer node')
                raise nsxv_exc.NsxPluginException(err_msg=error)

        return internal_net, internal_subnet

    def _get_edge_internal_ip(self, context, rtr_id):
        filters = {
            'network_id': [self.internal_net],
            'device_id': [rtr_id]}
        ports = self.nsxv_plugin.get_ports(context, filters=filters)
        if ports:
            return ports[0]['fixed_ips'][0]['ip_address']
        LOG.error("No port found for metadata for %s", rtr_id)

    def _get_edge_rtr_id_by_ext_ip(self, context, edge_ip):
        rtr_list = nsxv_db.get_nsxv_internal_edge(
            context.session, edge_ip)
        if rtr_list:
            return rtr_list[0]['router_id']

    def _get_edge_id_by_rtr_id(self, context, rtr_id):
        binding = nsxv_db.get_nsxv_router_binding(
            context.session,
            rtr_id)

        if binding:
            return binding['edge_id']

    def _get_proxy_edges(self, context):
        proxy_edge_ips = []

        db_edge_ips = get_db_internal_edge_ips(context, self.az.name)
        if len(db_edge_ips) > len(self.az.mgt_net_proxy_ips):
            error = (_('Number of configured metadata proxy IPs is smaller '
                      'than number of Edges which are already provisioned '
                      'for availability zone %s'), self.az.name)
            raise nsxv_exc.NsxPluginException(err_msg=error)

        pool = eventlet.GreenPool(min(MAX_INIT_THREADS,
                                      len(self.az.mgt_net_proxy_ips)))

        # Edge IPs that exist in both lists have to be validated that their
        # Edge appliance settings are valid
        for edge_inner_ip in pool.imap(
                self._setup_proxy_edge_route_and_connectivity,
                list(set(db_edge_ips) & set(self.az.mgt_net_proxy_ips))):
            proxy_edge_ips.append(edge_inner_ip)

        # Edges that exist only in the CFG list, should be paired with Edges
        # that exist only in the DB list. The existing Edge from the list will
        # be reconfigured to match the new config
        edge_to_convert_ips = (
            list(set(db_edge_ips) - set(self.az.mgt_net_proxy_ips)))
        edge_ip_to_set = (
            list(set(self.az.mgt_net_proxy_ips) - set(db_edge_ips)))

        if edge_to_convert_ips:
            if cfg.CONF.nsxv.metadata_initializer:
                for edge_inner_ip in pool.imap(
                        self._setup_proxy_edge_external_interface_ip,
                        zip(edge_to_convert_ips, edge_ip_to_set)):
                    proxy_edge_ips.append(edge_inner_ip)
            else:
                error = _('Metadata initialization is incomplete on '
                          'initializer node')
                raise nsxv_exc.NsxPluginException(err_msg=error)

        # Edges that exist in the CFG list but do not have a matching DB
        # element will be created.
        remaining_cfg_ips = edge_ip_to_set[len(edge_to_convert_ips):]
        if remaining_cfg_ips:
            if cfg.CONF.nsxv.metadata_initializer:
                for edge_inner_ip in pool.imap(
                        self._setup_new_proxy_edge, remaining_cfg_ips):
                    proxy_edge_ips.append(edge_inner_ip)

                pool.waitall()
            else:
                error = _('Metadata initialization is incomplete on '
                          'initializer node')
                raise nsxv_exc.NsxPluginException(err_msg=error)

        return proxy_edge_ips

    def _setup_proxy_edge_route_and_connectivity(self, rtr_ext_ip,
                                                 rtr_id=None, edge_id=None):
        # Use separate context per each as we use this in tread context
        context = neutron_context.get_admin_context()
        if not rtr_id:
            rtr_id = self._get_edge_rtr_id_by_ext_ip(context, rtr_ext_ip)
        if not edge_id:
            edge_id = self._get_edge_id_by_rtr_id(context, rtr_id)
        if not rtr_id or not edge_id:
            # log this error and return without the ip, but don't fail
            LOG.error("Failed find edge for router %(rtr_id)s with ip "
                      "%(rtr_ext_ip)s",
                      {'rtr_id': rtr_id, 'rtr_ext_ip': rtr_ext_ip})
            return

        # Read and validate DGW. If different, replace with new value
        try:
            # This may fail if the edge was deleted on backend
            h, routes = self.nsxv_plugin.nsx_v.vcns.get_routes(edge_id)
        except exceptions.ResourceNotFound as e:
            # log this error and return without the ip, but don't fail
            LOG.error("Failed to get routes for metadata proxy edge "
                      "%(edge)s: %(err)s",
                      {'edge': edge_id, 'err': e})
            return

        dgw = routes.get('defaultRoute', {}).get('gatewayAddress')

        if dgw != self.az.mgt_net_default_gateway:
            if cfg.CONF.nsxv.metadata_initializer:
                self.nsxv_plugin._update_routes(
                    context, rtr_id,
                    self.az.mgt_net_default_gateway)
            else:
                error = _('Metadata initialization is incomplete on '
                          'initializer node')
                raise nsxv_exc.NsxPluginException(err_msg=error)

        # Read and validate connectivity
        h, if_data = self.nsxv_plugin.nsx_v.get_interface(
            edge_id, vcns_const.EXTERNAL_VNIC_INDEX)
        a_groups = if_data.get('addressGroups', {}).get('addressGroups')
        if a_groups:
            cur_ip = a_groups[0].get('primaryAddress')
        else:
            cur_ip = None

        cur_pgroup = if_data['portgroupId']
        if (if_data and cur_pgroup != self.az.mgt_net_moid or
            cur_ip != rtr_ext_ip):
            if cfg.CONF.nsxv.metadata_initializer:
                self.nsxv_plugin.nsx_v.update_interface(
                    rtr_id,
                    edge_id,
                    vcns_const.EXTERNAL_VNIC_INDEX,
                    self.az.mgt_net_moid,
                    address=rtr_ext_ip,
                    netmask=self.az.mgt_net_proxy_netmask,
                    secondary=[])
            else:
                error = _('Metadata initialization is incomplete on '
                          'initializer node')
                raise nsxv_exc.NsxPluginException(err_msg=error)

        # Read and validate LB pool member configuration
        # When the Nova IP address is changed in the ini file, we should apply
        # this change to the LB pool
        lb_obj = nsxv_lb.NsxvLoadbalancer.get_loadbalancer(
            self.nsxv_plugin.nsx_v.vcns, edge_id)

        vs = lb_obj.virtual_servers.get(METADATA_VSE_NAME)
        update_md_proxy = False
        if vs:
            md_members = {member.payload['ipAddress']: member.payload['name']
                          for member in vs.default_pool.members.values()}

            if len(cfg.CONF.nsxv.nova_metadata_ips) == len(md_members):
                m_ips = md_members.keys()
                m_to_convert = (list(set(m_ips) -
                                     set(cfg.CONF.nsxv.nova_metadata_ips)))
                m_ip_to_set = (list(set(cfg.CONF.nsxv.nova_metadata_ips) -
                                    set(m_ips)))
                if m_to_convert or m_ip_to_set:
                    update_md_proxy = True
                for m_ip in m_to_convert:
                    m_name = md_members[m_ip]
                    vs.default_pool.members[m_name].payload['ipAddress'] = (
                        m_ip_to_set.pop())
            else:
                LOG.error('Number of metadata members should not change')

            try:
                # This may fail if the edge is powered off right now
                if update_md_proxy:
                    lb_obj.submit_to_backend(self.nsxv_plugin.nsx_v.vcns,
                                             edge_id)
            except exceptions.RequestBad as e:
                # log the error and continue
                LOG.error("Failed to update load balancer on metadata "
                          "proxy edge %(edge)s: %(err)s",
                          {'edge': edge_id, 'err': e})

        edge_ip = self._get_edge_internal_ip(context, rtr_id)

        if edge_ip:
            return edge_ip

    def _setup_proxy_edge_external_interface_ip(self, rtr_ext_ips):
        # Use separate context per each as we use this in tread context
        context = neutron_context.get_admin_context()

        rtr_old_ext_ip, rtr_new_ext_ip = rtr_ext_ips

        rtr_id = self._get_edge_rtr_id_by_ext_ip(context, rtr_old_ext_ip)
        edge_id = self._get_edge_id_by_rtr_id(context, rtr_id)

        # Replace DB entry as we cannot update the table PK
        nsxv_db.delete_nsxv_internal_edge(context.session, rtr_old_ext_ip)

        edge_ip = self._setup_proxy_edge_route_and_connectivity(
            rtr_new_ext_ip, rtr_id, edge_id)

        nsxv_db.create_nsxv_internal_edge(
            context.session, rtr_new_ext_ip,
            vcns_const.InternalEdgePurposes.INTER_EDGE_PURPOSE, rtr_id)

        if edge_ip:
            return edge_ip

    def _setup_new_proxy_edge(self, rtr_ext_ip):
        # Use separate context per each as we use this in tread context
        context = neutron_context.get_admin_context()

        rtr_id = None

        try:
            rtr_name = 'metadata_proxy_router'
            if not self.az.is_default():
                rtr_name = '%s-%s' % (rtr_name, self.az.name)
            router_data = {
                'router': {
                    'name': rtr_name,
                    'admin_state_up': True,
                    'router_type': 'exclusive',
                    'availability_zone_hints': [self.az.name],
                    'tenant_id': nsxv_constants.INTERNAL_TENANT_ID}}

            rtr = self.nsxv_plugin.create_router(
                context,
                router_data,
                allow_metadata=False)

            rtr_id = rtr['id']
            edge_id = self._get_edge_id_by_rtr_id(context, rtr_id)
            if not edge_id:
                LOG.error('No edge create for router - %s', rtr_id)
                if rtr_id:
                    self.nsxv_plugin.delete_router(context, rtr_id)
                return

            self.nsxv_plugin.nsx_v.update_interface(
                rtr['id'],
                edge_id,
                vcns_const.EXTERNAL_VNIC_INDEX,
                self.az.mgt_net_moid,
                address=rtr_ext_ip,
                netmask=self.az.mgt_net_proxy_netmask,
                secondary=[])

            port_data = {
                'port': {
                    'network_id': self.internal_net,
                    'name': None,
                    'admin_state_up': True,
                    'device_id': rtr_id,
                    'device_owner': (constants.DEVICE_OWNER_NETWORK_PREFIX +
                                     'md_interface'),
                    'fixed_ips': constants.ATTR_NOT_SPECIFIED,
                    'mac_address': constants.ATTR_NOT_SPECIFIED,
                    'port_security_enabled': False,
                    'tenant_id': nsxv_constants.INTERNAL_TENANT_ID}}

            port = self.nsxv_plugin.base_create_port(context, port_data)

            address_groups = self._get_address_groups(
                context, self.internal_net, rtr_id, is_proxy=True)

            edge_ip = port['fixed_ips'][0]['ip_address']
            with locking.LockManager.get_lock(edge_id):
                edge_utils.update_internal_interface(
                    self.nsxv_plugin.nsx_v, context, rtr_id,
                    self.internal_net, address_groups)

            self._setup_metadata_lb(rtr_id,
                                    port['fixed_ips'][0]['ip_address'],
                                    cfg.CONF.nsxv.nova_metadata_port,
                                    cfg.CONF.nsxv.nova_metadata_port,
                                    cfg.CONF.nsxv.nova_metadata_ips,
                                    proxy_lb=True)

            firewall_rules = [
                DEFAULT_EDGE_FIREWALL_RULE,
                {
                    'action': 'allow',
                    'enabled': True,
                    'source_ip_address': [INTERNAL_SUBNET]}]

            edge_utils.update_firewall(
                self.nsxv_plugin.nsx_v,
                context,
                rtr_id,
                {'firewall_rule_list': firewall_rules},
                allow_external=False)

            if self.az.mgt_net_default_gateway:
                self.nsxv_plugin._update_routes(
                    context, rtr_id,
                    self.az.mgt_net_default_gateway)

            nsxv_db.create_nsxv_internal_edge(
                context.session, rtr_ext_ip,
                vcns_const.InternalEdgePurposes.INTER_EDGE_PURPOSE, rtr_id)

            return edge_ip

        except Exception as e:
            LOG.exception("Exception %s while creating internal edge "
                          "for metadata service", e)

            ports = self.nsxv_plugin.get_ports(
                context, filters={'device_id': [rtr_id]})

            for port in ports:
                self.nsxv_plugin.delete_port(context, port['id'],
                                             l3_port_check=True,
                                             nw_gw_port_check=True,
                                             allow_delete_internal=True)

            nsxv_db.delete_nsxv_internal_edge(
                context.session,
                rtr_ext_ip)

            if rtr_id:
                self.nsxv_plugin.delete_router(context, rtr_id)

    def _get_address_groups(self, context, network_id, device_id, is_proxy):

        filters = {'network_id': [network_id],
                   'device_id': [device_id]}
        ports = self.nsxv_plugin.get_ports(context, filters=filters)

        subnets = self.nsxv_plugin.get_subnets(context, filters=filters)

        address_groups = []
        for subnet in subnets:
            address_group = {}
            net = netaddr.IPNetwork(subnet['cidr'])
            address_group['subnetMask'] = str(net.netmask)
            address_group['subnetPrefixLength'] = str(net.prefixlen)
            for port in ports:
                fixed_ips = port['fixed_ips']
                for fip in fixed_ips:
                    s_id = fip['subnet_id']
                    ip_addr = fip['ip_address']
                    if s_id == subnet['id'] and netaddr.valid_ipv4(ip_addr):
                        address_group['primaryAddress'] = ip_addr
                        break

            # For Edge appliances which aren't the metadata proxy Edge
            #  we add the metadata IP address
            if not is_proxy and network_id == self.internal_net:
                address_group['secondaryAddresses'] = {
                    'type': 'secondary_addresses',
                    'ipAddress': [METADATA_IP_ADDR]}

            address_groups.append(address_group)
        return address_groups

    def _create_ssl_cert(self, edge_id=None):
        # Create a self signed certificate in the backend if both Cert details
        # and private key are not supplied in nsx.ini
        if (not cfg.CONF.nsxv.metadata_nova_client_cert and
            not cfg.CONF.nsxv.metadata_nova_client_priv_key):
            h = self.nsxv_plugin.nsx_v.vcns.create_csr(edge_id)[0]
            # Extract the CSR ID from header
            csr_id = lbaas_common.extract_resource_id(h['location'])
            # Create a self signed certificate
            cert = self.nsxv_plugin.nsx_v.vcns.create_csr_cert(csr_id)[1]
            cert_id = cert['objectId']
        else:
            # Raise an error if either the Cert path or the private key is not
            # configured
            error = None
            if not cfg.CONF.nsxv.metadata_nova_client_cert:
                error = _('Metadata certificate path not configured')
            elif not cfg.CONF.nsxv.metadata_nova_client_priv_key:
                error = _('Metadata client private key not configured')
            if error:
                raise nsxv_exc.NsxPluginException(err_msg=error)
            pem_encoding = utils.read_file(
                cfg.CONF.nsxv.metadata_nova_client_cert)
            priv_key = utils.read_file(
                cfg.CONF.nsxv.metadata_nova_client_priv_key)
            request = {
                'pemEncoding': pem_encoding,
                'privateKey': priv_key}
            cert = self.nsxv_plugin.nsx_v.vcns.upload_edge_certificate(
                edge_id, request)[1]
            cert_id = cert.get('certificates')[0]['objectId']
        return cert_id

    def _setup_metadata_lb(self, rtr_id, vip, v_port, s_port, member_ips,
                           proxy_lb=False, context=None):

        if context is None:
            context = neutron_context.get_admin_context()

        edge_id = self._get_edge_id_by_rtr_id(context, rtr_id)
        LOG.debug('Setting up Edge device %s', edge_id)

        lb_obj = nsxv_lb.NsxvLoadbalancer()

        protocol = 'HTTP'
        ssl_pass_through = False
        cert_id = None
        # Set protocol to HTTPS with default port of 443 if metadata_insecure
        # is set to False.
        if not cfg.CONF.nsxv.metadata_insecure:
            protocol = 'HTTPS'
            if proxy_lb:
                v_port = METADATA_HTTPS_VIP_PORT
            else:
                v_port = METADATA_HTTPS_PORT
                # Create the certificate on the backend
                cert_id = self._create_ssl_cert(edge_id)
            ssl_pass_through = proxy_lb
        mon_type = protocol if proxy_lb else 'tcp'
        # Create virtual server
        virt_srvr = nsxv_lb.NsxvLBVirtualServer(
            name=METADATA_VSE_NAME,
            ip_address=vip,
            protocol=protocol,
            port=v_port)

        # For router Edge, we add X-LB-Proxy-ID header
        if not proxy_lb:
            md_app_rule = nsxv_lb.NsxvLBAppRule(
                'insert-mdp',
                'reqadd X-Metadata-Provider:' + edge_id)
            virt_srvr.add_app_rule(md_app_rule)

            # When shared proxy is configured, insert authentication string
            if cfg.CONF.nsxv.metadata_shared_secret:
                signature = hmac.new(
                    bytearray(cfg.CONF.nsxv.metadata_shared_secret, 'ascii'),
                    bytearray(edge_id, 'ascii'),
                    hashlib.sha256).hexdigest()
                sign_app_rule = nsxv_lb.NsxvLBAppRule(
                    'insert-auth',
                    'reqadd X-Metadata-Provider-Signature:' + signature)
                virt_srvr.add_app_rule(sign_app_rule)

        # Create app profile
        #  XFF is inserted in router LBs
        app_profile = nsxv_lb.NsxvLBAppProfile(
            name='MDSrvProxy',
            template=protocol,
            server_ssl_enabled=not cfg.CONF.nsxv.metadata_insecure,
            ssl_pass_through=ssl_pass_through,
            insert_xff=not proxy_lb,
            client_ssl_cert=cert_id)

        virt_srvr.set_app_profile(app_profile)

        # Create pool, members and monitor
        pool = nsxv_lb.NsxvLBPool(
            name=METADATA_POOL_NAME)

        monitor = nsxv_lb.NsxvLBMonitor(name='MDSrvMon',
                                        mon_type=mon_type.lower())
        pool.add_monitor(monitor)

        i = 0
        for member_ip in member_ips:
            i += 1
            member = nsxv_lb.NsxvLBPoolMember(
                name='Member-%d' % i,
                ip_address=member_ip,
                port=s_port,
                monitor_port=s_port)
            pool.add_member(member)

        virt_srvr.set_default_pool(pool)
        lb_obj.add_virtual_server(virt_srvr)

        lb_obj.submit_to_backend(self.nsxv_plugin.nsx_v.vcns, edge_id)

    def configure_router_edge(self, context, rtr_id):
        LOG.debug('Configuring metadata infrastructure for %s', rtr_id)
        ctx = neutron_context.get_admin_context()
        # Connect router interface to inter-edge network
        port_data = {
            'port': {
                'network_id': self.internal_net,
                'name': None,
                'admin_state_up': True,
                'device_id': rtr_id,
                'device_owner': constants.DEVICE_OWNER_ROUTER_GW,
                'fixed_ips': constants.ATTR_NOT_SPECIFIED,
                'mac_address': constants.ATTR_NOT_SPECIFIED,
                'port_security_enabled': False,
                'tenant_id': nsxv_constants.INTERNAL_TENANT_ID}}

        self.nsxv_plugin.base_create_port(ctx, port_data)

        address_groups = self._get_address_groups(
            ctx,
            self.internal_net,
            rtr_id,
            is_proxy=False)

        edge_utils.update_internal_interface(
            self.nsxv_plugin.nsx_v,
            context,
            rtr_id,
            self.internal_net,
            address_groups=address_groups)

        self._setup_metadata_lb(rtr_id,
                                METADATA_IP_ADDR,
                                METADATA_TCP_PORT,
                                cfg.CONF.nsxv.nova_metadata_port,
                                self.proxy_edge_ips,
                                proxy_lb=False,
                                context=context)

    def cleanup_router_edge(self, context, rtr_id, warn=False):
        filters = {
            'network_id': [self.internal_net],
            'device_id': [rtr_id]}
        ctx = context.elevated()
        ports = self.nsxv_plugin.get_ports(ctx, filters=filters)

        if ports:
            if warn:
                LOG.warning("cleanup_router_edge found port %(port)s for "
                            "router %(router)s - deleting it now.",
                            {'port': ports[0]['id'], 'router': rtr_id})
            try:
                self.nsxv_plugin.delete_port(
                    ctx, ports[0]['id'],
                    l3_port_check=False,
                    allow_delete_internal=True)
            except Exception as e:
                LOG.error("Failed to delete md_proxy port %(port)s: "
                          "%(e)s", {'port': ports[0]['id'], 'e': e})

    def is_md_subnet(self, subnet_id):
        return self.internal_subnet == subnet_id
