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

from neutron_lib.api.definitions import network as net_def
from neutron_lib.api.definitions import port as port_def
from neutron_lib.api.definitions import subnet as subnet_def
from neutron_lib.callbacks import events
from neutron_lib.callbacks import registry
from neutron_lib.callbacks import resources
from neutron_lib import context as n_context
from neutron_lib.db import api as db_api
from neutron_lib.db import resource_extend
from neutron_lib.db import utils as db_utils
from neutron_lib.plugins import constants as plugin_constants
from neutron_lib.plugins import directory
from oslo_config import cfg
from oslo_log import log as logging
from oslo_utils import excutils
from oslo_utils import uuidutils

from neutron.db import agents_db
from neutron.db import agentschedulers_db
from neutron.db import allowedaddresspairs_db as addr_pair_db
from neutron.db.availability_zone import router as router_az_db
from neutron.db import external_net_db
from neutron.db import extradhcpopt_db
from neutron.db import extraroute_db
from neutron.db import l3_db
from neutron.db import l3_gwmode_db
from neutron.db.models import l3 as l3_db_models
from neutron.db.models import securitygroup as securitygroup_model
from neutron.db import models_v2
from neutron.db import portsecurity_db
from neutron.db import securitygroups_db
from neutron.quota import resource_registry
from neutron_lib.api import validators
from neutron_lib import exceptions as n_exc

from vmware_nsx.common import availability_zones as nsx_com_az
from vmware_nsx.common import config
from vmware_nsx.common import exceptions as nsx_exc
from vmware_nsx.common import locking
from vmware_nsx.common import managers as nsx_managers
from vmware_nsx.common import utils as com_utils
from vmware_nsx.db import (
    routertype as rt_rtr)
from vmware_nsx.db import db as nsx_db
from vmware_nsx.db import nsx_portbindings_db as pbin_db
from vmware_nsx.extensions import advancedserviceproviders as as_providers
from vmware_nsx.extensions import projectpluginmap
from vmware_nsx.plugins.common import plugin as nsx_plugin_common
from vmware_nsx.plugins.dvs import plugin as dvs
from vmware_nsx.plugins.nsx_v import plugin as v
from vmware_nsx.plugins.nsx_v3 import plugin as t
from vmware_nsx.services.lbaas.octavia import octavia_listener
from vmware_nsx.services.lbaas.octavia import tvd_wrapper as octavia_tvd

LOG = logging.getLogger(__name__)
TVD_PLUGIN_TYPE = "Nsx-TVD"


@resource_extend.has_resource_extenders
class NsxTVDPlugin(agentschedulers_db.AZDhcpAgentSchedulerDbMixin,
                   addr_pair_db.AllowedAddressPairsMixin,
                   agents_db.AgentDbMixin,
                   nsx_plugin_common.NsxPluginBase,
                   rt_rtr.RouterType_mixin,
                   external_net_db.External_net_db_mixin,
                   extraroute_db.ExtraRoute_db_mixin,
                   extradhcpopt_db.ExtraDhcpOptMixin,
                   router_az_db.RouterAvailabilityZoneMixin,
                   l3_gwmode_db.L3_NAT_db_mixin,
                   pbin_db.NsxPortBindingMixin,
                   portsecurity_db.PortSecurityDbMixin,
                   securitygroups_db.SecurityGroupDbMixin,
                   nsx_com_az.NSXAvailabilityZonesPluginCommon,
                   projectpluginmap.ProjectPluginMapPluginBase):

    supported_extension_aliases = [projectpluginmap.ALIAS]

    __native_bulk_support = True
    __native_pagination_support = True
    __native_sorting_support = True

    @resource_registry.tracked_resources(
        network=models_v2.Network,
        port=models_v2.Port,
        subnet=models_v2.Subnet,
        subnetpool=models_v2.SubnetPool,
        security_group=securitygroup_model.SecurityGroup,
        security_group_rule=securitygroup_model.SecurityGroupRule,
        router=l3_db_models.Router,
        floatingip=l3_db_models.FloatingIP)
    def __init__(self):
        self._extension_manager = nsx_managers.ExtensionManager()
        LOG.info("Start NSX TVD Plugin")
        self.init_is_complete = False
        # Validate configuration
        config.validate_nsx_config_options()
        super(NsxTVDPlugin, self).__init__()

        # init the different supported plugins
        self.init_plugins()

        # init the extensions supported by any of the plugins
        self.init_extensions()

        self._unsubscribe_callback_events()

        registry.subscribe(self.spawn_complete,
                           resources.PROCESS,
                           events.AFTER_SPAWN)

        registry.subscribe(self.init_complete,
                           resources.PROCESS,
                           events.AFTER_INIT)

    @staticmethod
    def plugin_type():
        return TVD_PLUGIN_TYPE

    @staticmethod
    def is_tvd_plugin():
        return True

    @com_utils.retry_upon_exception(Exception, 0.5, 2,
                                    cfg.CONF.nsx_tvd.init_retries)
    def _call_plugin_init_with_retry(self, map_type, plugin_class):
        try:
            self.plugins[map_type] = plugin_class()
        except Exception as e:
            with excutils.save_and_reraise_exception():
                LOG.warning("%s plugin failed to initialized: %s",
                            map_type.upper(), e)

    def _init_plugin(self, map_type, plugin_class):
        if map_type not in cfg.CONF.nsx_tvd.enabled_plugins:
            # skip this plugin
            LOG.info("%s plugin was not enabled by the configuration",
                     map_type.upper())
            return
        try:
            self._call_plugin_init_with_retry(map_type, plugin_class)
        except Exception as e:
            LOG.warning("%s plugin will not be supported",
                        map_type.upper())
            if map_type == self.default_plugin:
                msg = (_("The default plugin %(def)s failed to start. "
                         "Reason: %(reason)s") % {'def': self.default_plugin,
                                                  'reason': e})
                LOG.error(msg)
                raise nsx_exc.NsxPluginException(err_msg=msg)
        else:
            LOG.info("%s plugin will be supported", map_type.upper())

    def init_plugins(self):
        # initialize all supported plugins
        self.plugins = {}
        self.as_providers = {}
        # update the default plugin for new projects
        self.default_plugin = cfg.CONF.nsx_tvd.default_plugin
        plugins = [(projectpluginmap.NsxPlugins.NSX_T, t.NsxV3Plugin),
                   (projectpluginmap.NsxPlugins.NSX_V, v.NsxVPluginV2),
                   (projectpluginmap.NsxPlugins.DVS, dvs.NsxDvsV2)]
        for (map_type, plugin_class) in plugins:
            self._init_plugin(map_type, plugin_class)
        if not len(self.plugins):
            msg = _("No active plugins were found")
            raise nsx_exc.NsxPluginException(err_msg=msg)

        for k, val in self.plugins.items():
            if as_providers.ALIAS in val.supported_extension_aliases:
                self.as_providers[k] = val
        LOG.info("NSX-TVD plugin will use %s as the default plugin",
            self.default_plugin)

        # validate the availability zones configuration
        self.init_availability_zones()

    def get_plugin_by_type(self, plugin_type):
        return self.plugins.get(plugin_type)

    def init_extensions(self):
        # Support all the extensions supported by any of the plugins
        extensions = []
        for plugin in self.plugins.values():
            extensions.extend(plugin.supported_extension_aliases)
        self.supported_extension_aliases.extend(list(set(extensions)))

        # mark extensions which are supported by only one of the plugins
        self._unsupported_fields = {}
        for plugin in self.plugins.values():
            # TODO(asarfaty): add other resources here
            plugin_type = plugin.plugin_type()
            self._unsupported_fields[plugin_type] = {'router': [],
                                                     'port': [],
                                                     'security_group': []}

            # router size and type are supported only by the V plugin
            if plugin_type in [t.NsxV3Plugin.plugin_type(),
                               dvs.NsxDvsV2.plugin_type()]:
                self._unsupported_fields[plugin_type]['router'] = [
                    'router_size', 'router_type']

            # port mac learning, and provider sg are not supported by
            # the dvs plugin
            if plugin_type in [dvs.NsxDvsV2.plugin_type()]:
                self._unsupported_fields[plugin_type]['port'] = [
                    'mac_learning_enabled', 'provider_security_groups']

            # security group policy can be supported only by nsx-v
            if plugin_type in [t.NsxV3Plugin.plugin_type(),
                               dvs.NsxDvsV2.plugin_type()]:
                self._unsupported_fields[plugin_type]['security_group'] = [
                    'policy']

    def init_availability_zones(self):
        # Make sure there are no overlaps between v/t availability zones
        if (self.plugins.get(projectpluginmap.NsxPlugins.NSX_V) and
            self.plugins.get(projectpluginmap.NsxPlugins.NSX_T) and
            bool(set(cfg.CONF.nsxv.availability_zones) &
                 set(cfg.CONF.nsx_v3.availability_zones))):
            msg = _("Cannot use the same availability zones in NSX-V and T")
            raise nsx_exc.NsxPluginException(err_msg=msg)

    def _get_octavia_objects(self, plugin_type):
        plugin = self.get_plugin_by_type(plugin_type)
        if plugin:
            return plugin._get_octavia_objects()
        return {'loadbalancer': None, 'listener': None, 'pool': None,
                'member': None, 'healthmonitor': None, 'l7policy': None,
                'l7rule': None}

    def init_complete(self, resource, event, trigger, payload=None):
        with locking.LockManager.get_lock('plugin-init-complete-tvd'):
            if self.init_is_complete:
                # Should be called only once per worker
                return
            self.init_octavia()
            self.init_is_complete = True

    def init_octavia(self):
        # Init Octavia listener and endpoints
        v_objects = self._get_octavia_objects(
            projectpluginmap.NsxPlugins.NSX_V)
        t_objects = self._get_octavia_objects(
            projectpluginmap.NsxPlugins.NSX_T)

        self.octavia_listener = octavia_listener.NSXOctaviaListener(
            loadbalancer=octavia_tvd.OctaviaTVDWrapper(
                v_objects['loadbalancer'], t_objects['loadbalancer']),
            listener=octavia_tvd.OctaviaTVDWrapper(
                v_objects['listener'], t_objects['listener']),
            pool=octavia_tvd.OctaviaTVDWrapper(
                v_objects['pool'], t_objects['pool']),
            member=octavia_tvd.OctaviaTVDWrapper(
                v_objects['member'], t_objects['member']),
            healthmonitor=octavia_tvd.OctaviaTVDWrapper(
                v_objects['healthmonitor'], t_objects['healthmonitor']),
            l7policy=octavia_tvd.OctaviaTVDWrapper(
                v_objects['l7policy'], t_objects['l7policy']),
            l7rule=octavia_tvd.OctaviaTVDWrapper(
                v_objects['l7rule'], t_objects['l7rule']))

    def spawn_complete(self, resource, event, trigger, payload=None):
        # This method should run only once, but after init_complete
        if not self.init_is_complete:
            self.init_complete(None, None, None)
        self.init_octavia_stats_collector()

    def init_octavia_stats_collector(self):
        self.octavia_stats_collector = (
            octavia_listener.NSXOctaviaStatisticsCollector(
                self,
                octavia_tvd.stats_getter))

    def start_rpc_listeners(self):
        # Run the start_rpc_listeners of one of the sub-plugins
        for plugin in self.plugins.values():
            if plugin.rpc_workers_supported():
                return plugin.start_rpc_listeners()

    def _unsubscribe_callback_events(self):
        # unsubscribe the callback that should be called on all plugins
        # other that NSX-T.
        registry.unsubscribe_all(
            l3_db.L3_NAT_dbonly_mixin._prevent_l3_port_delete_callback)

        # Instead we will subscribe our internal callback.
        registry.subscribe(self._prevent_l3_port_delete_callback,
                           resources.PORT, events.BEFORE_DELETE)

    @staticmethod
    def _prevent_l3_port_delete_callback(resource, event,
                                         trigger, payload=None):
        """Register a callback to replace the default one

        This callback will prevent port deleting only if the port plugin
        is not NSX-T (in NSX-T plugin it was already handled)
        """
        context = payload.context
        port_id = payload.resource_id
        port_check = payload.metadata['port_check']
        l3plugin = directory.get_plugin(plugin_constants.L3)
        if l3plugin and port_check:
            # if not nsx-t - call super code
            core_plugin = directory.get_plugin()
            db_port = core_plugin._get_port(context, port_id)
            p = core_plugin._get_plugin_from_net_id(
                context, db_port['network_id'])
            if p.plugin_type() != projectpluginmap.NsxPlugins.NSX_T:
                l3plugin.prevent_l3_port_deletion(context, port_id)

    def _validate_obj_extensions(self, data, plugin_type, obj_type):
        """prevent configuration of unsupported extensions"""
        for field in self._unsupported_fields[plugin_type][obj_type]:
            if validators.is_attr_set(data.get(field)):
                err_msg = (_('Can not support %(field)s extension for '
                             '%(obj_type)s %(p)s plugin') % {
                           'field': field,
                           'obj_type': obj_type,
                           'p': plugin_type})
                raise n_exc.InvalidInput(error_message=err_msg)

    def _cleanup_obj_fields(self, data, plugin_type, obj_type):
        """Remove data of unsupported extensions"""
        for field in self._unsupported_fields[plugin_type][obj_type]:
            if field in data:
                del data[field]

    def _list_availability_zones(self, context, filters=None):
        p = self._get_plugin_for_request(context, filters)
        if p:
            return p._list_availability_zones(context, filters=filters)
        return []

    def validate_availability_zones(self, context, resource_type,
                                    availability_zones):
        p = self._get_plugin_from_project(context, context.project_id)
        return p.validate_availability_zones(context, resource_type,
                                             availability_zones)

    def _get_plugin_from_net_id(self, context, net_id):
        # get the network using the super plugin - here we use the
        # _get_network (so as not to call the make dict method)
        network = self._get_network(context, net_id)
        return self._get_plugin_from_project(context, network['tenant_id'])

    def get_network_availability_zones(self, net_db):
        ctx = n_context.get_admin_context()
        p = self._get_plugin_from_project(ctx, net_db['tenant_id'])
        return p.get_network_availability_zones(net_db)

    def create_network(self, context, network):
        net_data = network['network']
        tenant_id = net_data['tenant_id']
        self._ensure_default_security_group(context, tenant_id)
        p = self._get_plugin_from_project(context, tenant_id)
        return p.create_network(context, network)

    @db_api.retry_if_session_inactive()
    def create_network_bulk(self, context, networks):
        #Implement create bulk so that the plugin calculation will be done once
        objects = []
        items = networks['networks']

        # look at the first network to find out the project & plugin
        net_data = items[0]['network']
        tenant_id = net_data['tenant_id']
        self._ensure_default_security_group(context, tenant_id)
        p = self._get_plugin_from_project(context, tenant_id)

        # create all networks one by one
        try:
            with db_api.CONTEXT_WRITER.using(context):
                for item in items:
                    objects.append(p.create_network(context, item))
        except Exception:
            with excutils.save_and_reraise_exception():
                LOG.error("An exception occurred while creating "
                          "the networks:%(item)s",
                          {'item': item})
        return objects

    def delete_network(self, context, id):
        p = self._get_plugin_from_net_id(context, id)
        p.delete_network(context, id)

    def get_network(self, context, id, fields=None):
        p = self._get_plugin_from_net_id(context, id)
        return p.get_network(context, id, fields=fields)

    def _get_plugin_for_request(self, context, filters, keys=None):
        project_id = context.project_id
        if filters:
            if filters.get('tenant_id'):
                project_id = filters.get('tenant_id')
            elif filters.get('project_id'):
                project_id = filters.get('project_id')
            else:
                # we have specific filters on the request. If those are
                # specific enough, we should not filter by project
                if filters.get('id'):
                    return
                if keys:
                    for key in keys:
                        if filters.get(key):
                            return
            # If there are multiple tenants/projects being requested then
            # we will not filter according to the plugin
            if isinstance(project_id, list):
                return
        return self._get_plugin_from_project(context, project_id)

    def get_networks(self, context, filters=None, fields=None,
                     sorts=None, limit=None, marker=None,
                     page_reverse=False):
        # Read project plugin to filter relevant projects according to
        # plugin
        req_p = self._get_plugin_for_request(context, filters,
                                             keys=['shared'])
        filters = filters or {}
        with db_api.CONTEXT_READER.using(context):
            networks = (
                super(NsxTVDPlugin, self).get_networks(
                    context, filters, fields, sorts,
                    limit, marker, page_reverse))
            for net in networks[:]:
                p = self._get_plugin_from_project(context, net['tenant_id'])
                if p == req_p or req_p is None:
                    p._extend_get_network_dict_provider(context, net)
                else:
                    networks.remove(net)
        return (networks if not fields else
                [db_utils.resource_fields(network,
                                          fields) for network in networks])

    def update_network(self, context, id, network):
        p = self._get_plugin_from_net_id(context, id)
        return p.update_network(context, id, network)

    def create_port(self, context, port):
        net_id = port['port']['network_id']
        p = self._get_plugin_from_net_id(context, net_id)
        self._validate_obj_extensions(
            port['port'], p.plugin_type(), 'port')
        new_port = p.create_port(context, port)
        self._cleanup_obj_fields(
            new_port, p.plugin_type(), 'port')
        return new_port

    def update_port(self, context, id, port):
        db_port = self._get_port(context, id)
        p = self._get_plugin_from_net_id(context, db_port['network_id'])
        self._validate_obj_extensions(
            port['port'], p.plugin_type(), 'port')
        return p.update_port(context, id, port)

    def delete_port(self, context, id, **kwargs):
        db_port = self._get_port(context, id)
        p = self._get_plugin_from_net_id(context, db_port['network_id'])
        p.delete_port(context, id, **kwargs)

    def get_port(self, context, id, fields=None):
        db_port = self._get_port(context, id)
        p = self._get_plugin_from_net_id(context, db_port['network_id'])
        port = p.get_port(context, id, fields=fields)
        self._cleanup_obj_fields(
            port, p.plugin_type(), 'port')
        return port

    def get_ports(self, context, filters=None, fields=None,
                  sorts=None, limit=None, marker=None,
                  page_reverse=False):
        # Read project plugin to filter relevant projects according to
        # plugin
        req_p = self._get_plugin_for_request(context, filters,
                                             keys=['device_id',
                                                   'network_id',
                                                   'fixed_ips'])
        filters = filters or {}
        with db_api.CONTEXT_READER.using(context):
            ports = (
                super(NsxTVDPlugin, self).get_ports(
                    context, filters, fields, sorts,
                    limit, marker, page_reverse))
            # Add port extensions
            for port in ports[:]:
                port_model = None
                if 'id' in port:
                    port_model = self._get_port(context, port['id'])
                    resource_extend.apply_funcs('ports', port, port_model)
                p = self._get_plugin_from_net_id(context, port['network_id'])
                if p == req_p or req_p is None:
                    if hasattr(p, '_extend_get_port_dict_qos_and_binding'):
                        p._extend_get_port_dict_qos_and_binding(context, port)
                    else:
                        if not port_model:
                            port_model = port
                        p._extend_port_dict_binding(port, port_model)
                    if hasattr(p,
                               '_remove_provider_security_groups_from_list'):
                        p._remove_provider_security_groups_from_list(port)
                    self._cleanup_obj_fields(
                        port, p.plugin_type(), 'port')
                else:
                    ports.remove(port)
        return (ports if not fields else
                [db_utils.resource_fields(port, fields) for port in ports])

    def _get_subnet_plugin_by_id(self, context, subnet_id):
        db_subnet = self._get_subnet_object(context, subnet_id)
        return self._get_plugin_from_net_id(context, db_subnet.network_id)

    def get_subnet(self, context, id, fields=None):
        p = self._get_subnet_plugin_by_id(context, id)
        return p.get_subnet(context, id, fields=fields)

    def get_subnets(self, context, filters=None, fields=None, sorts=None,
                    limit=None, marker=None, page_reverse=False):
        # Check if we need to invoke metadata search. Here we are unable to
        # filter according to projects as this is from the nova api service
        # so we invoke on all plugins that support this extension
        if ((fields and as_providers.ADV_SERVICE_PROVIDERS in fields) or
            (filters and filters.get(as_providers.ADV_SERVICE_PROVIDERS))):
            for plugin in self.as_providers.values():
                f = copy.copy(filters)
                subnets = plugin.get_subnets(context, filters=f,
                                             fields=fields, sorts=sorts,
                                             limit=limit, marker=marker,
                                             page_reverse=page_reverse)
                if subnets:
                    return subnets
            return []
        # Read project plugin to filter relevant projects according to
        # plugin
        req_p = self._get_plugin_for_request(context, filters)
        filters = filters or {}
        subnets = super(NsxTVDPlugin, self).get_subnets(
            context, filters=filters, fields=fields, sorts=sorts,
            limit=limit, marker=marker, page_reverse=page_reverse)
        for subnet in subnets[:]:
            p = self._get_plugin_from_project(context, subnet['tenant_id'])
            if req_p and p != req_p:
                subnets.remove(subnet)
        return subnets

    def delete_subnet(self, context, id):
        p = self._get_subnet_plugin_by_id(context, id)
        p.delete_subnet(context, id)

    def _get_subnet_plugin(self, context, subnet_data):
        # get the plugin of the associated network
        net_id = subnet_data['network_id']
        net_plugin = self._get_plugin_from_net_id(context, net_id)
        # make sure it matches the plugin of the current tenant
        tenant_id = subnet_data['tenant_id']
        tenant_plugin = self._get_plugin_from_project(context, tenant_id)
        if tenant_plugin.plugin_type() != net_plugin.plugin_type():
            err_msg = (_('Subnet should belong to the %s plugin '
                         'as the network') % net_plugin.plugin_type())
            raise n_exc.InvalidInput(error_message=err_msg)
        return net_plugin

    def create_subnet(self, context, subnet):
        p = self._get_subnet_plugin(context, subnet['subnet'])
        return p.create_subnet(context, subnet)

    def create_subnet_bulk(self, context, subnets):
        # look at the first subnet to find out the project & plugin
        items = subnets['subnets']
        p = self._get_subnet_plugin(context, items[0]['subnet'])
        return p.create_subnet_bulk(context, subnets)

    def update_subnet(self, context, id, subnet):
        p = self._get_subnet_plugin_by_id(context, id)
        return p.update_subnet(context, id, subnet)

    def get_router_availability_zones(self, router):
        ctx = n_context.get_admin_context()
        p = self._get_plugin_from_project(ctx, router['tenant_id'])
        return p.get_router_availability_zones(router)

    def _validate_router_gw_plugin(self, context, router_plugin,
                                   gw_info):
        if gw_info and gw_info.get('network_id'):
            net_plugin = self._get_plugin_from_net_id(
                context, gw_info['network_id'])
            if net_plugin.plugin_type() != router_plugin.plugin_type():
                err_msg = (_('Router gateway should belong to the %s plugin '
                             'as the router') % router_plugin.plugin_type())
                raise n_exc.InvalidInput(error_message=err_msg)

    def _validate_router_interface_plugin(self, context, router_plugin,
                                          interface_info):
        is_port, is_sub = self._validate_interface_info(interface_info)
        if is_port:
            net_id = self._get_port(
                context, interface_info['port_id'])['network_id']
        elif is_sub:
            net_id = self._get_subnet_object(
                context, interface_info['subnet_id']).network_id
        net_plugin = self._get_plugin_from_net_id(context, net_id)
        if net_plugin.plugin_type() != router_plugin.plugin_type():
            err_msg = (_('Router interface should belong to the %s plugin '
                         'as the router') % router_plugin.plugin_type())
            raise n_exc.InvalidInput(error_message=err_msg)

    def _get_plugin_from_router_id(self, context, router_id):
        # get the router using the super plugin - here we use the
        # _get_router (so as not to call the make dict method)
        router = self._get_router(context, router_id)
        return self._get_plugin_from_project(context, router['tenant_id'])

    def create_router(self, context, router):
        tenant_id = router['router']['tenant_id']
        self._ensure_default_security_group(context, tenant_id)
        p = self._get_plugin_from_project(context, tenant_id)
        self._validate_router_gw_plugin(context, p, router['router'].get(
            'external_gateway_info'))
        self._validate_obj_extensions(
            router['router'], p.plugin_type(), 'router')
        new_router = p.create_router(context, router)
        self._cleanup_obj_fields(
            new_router, p.plugin_type(), 'router')
        return new_router

    def update_router(self, context, router_id, router):
        p = self._get_plugin_from_router_id(context, router_id)
        self._validate_router_gw_plugin(context, p, router['router'].get(
            'external_gateway_info'))
        self._validate_obj_extensions(
            router['router'], p.plugin_type(), 'router')
        return p.update_router(context, router_id, router)

    def get_router(self, context, id, fields=None):
        p = self._get_plugin_from_router_id(context, id)
        router = p.get_router(context, id, fields=fields)
        self._cleanup_obj_fields(router, p.plugin_type(), 'router')
        return router

    def delete_router(self, context, id):
        p = self._get_plugin_from_router_id(context, id)
        p.delete_router(context, id)

    def add_router_interface(self, context, router_id, interface_info):
        p = self._get_plugin_from_router_id(context, router_id)
        self._validate_router_interface_plugin(context, p, interface_info)
        return p.add_router_interface(context, router_id, interface_info)

    def remove_router_interface(self, context, router_id, interface_info):
        p = self._get_plugin_from_router_id(context, router_id)
        return p.remove_router_interface(context, router_id, interface_info)

    def _validate_fip_router_plugin(self, context, fip_plugin, fip_data):
        if 'router_id' in fip_data:
            router_plugin = self._get_plugin_from_router_id(
                context, fip_data['router_id'])
            if router_plugin.plugin_type() != fip_plugin.plugin_type():
                err_msg = (_('Floatingip router should belong to the %s '
                             'plugin as the floatingip') %
                           fip_plugin.plugin_type())
                raise n_exc.InvalidInput(error_message=err_msg)

    def get_routers(self, context, filters=None, fields=None,
                    sorts=None, limit=None, marker=None,
                    page_reverse=False):
        # Read project plugin to filter relevant projects according to
        # plugin
        req_p = self._get_plugin_for_request(context, filters)
        routers = super(NsxTVDPlugin, self).get_routers(
            context, filters=filters, fields=fields, sorts=sorts,
            limit=limit, marker=marker, page_reverse=page_reverse)
        for router in routers[:]:
            p = self._get_plugin_from_project(context, router['tenant_id'])
            if req_p and p != req_p:
                routers.remove(router)
        return routers

    def create_floatingip(self, context, floatingip):
        net_id = floatingip['floatingip']['floating_network_id']
        p = self._get_plugin_from_net_id(context, net_id)
        self._validate_fip_router_plugin(context, p, floatingip['floatingip'])
        return p.create_floatingip(context, floatingip)

    def update_floatingip(self, context, id, floatingip):
        fip = self._get_floatingip(context, id)
        net_id = fip['floating_network_id']
        p = self._get_plugin_from_net_id(context, net_id)
        self._validate_fip_router_plugin(context, p, floatingip['floatingip'])
        return p.update_floatingip(context, id, floatingip)

    def delete_floatingip(self, context, id):
        fip = self._get_floatingip(context, id)
        net_id = fip['floating_network_id']
        p = self._get_plugin_from_net_id(context, net_id)
        return p.delete_floatingip(context, id)

    def get_floatingip(self, context, id, fields=None):
        fip = self._get_floatingip(context, id)
        net_id = fip['floating_network_id']
        p = self._get_plugin_from_net_id(context, net_id)
        return p.get_floatingip(context, id, fields=fields)

    def get_floatingips(self, context, filters=None, fields=None,
                        sorts=None, limit=None, marker=None,
                        page_reverse=False):
        # Read project plugin to filter relevant projects according to
        # plugin
        req_p = self._get_plugin_for_request(context, filters,
                                             keys=['port_id'])
        fips = super(NsxTVDPlugin, self).get_floatingips(
            context, filters=filters, fields=fields, sorts=sorts,
            limit=limit, marker=marker, page_reverse=page_reverse)
        for fip in fips[:]:
            p = self._get_plugin_from_project(context,
                                              fip['tenant_id'])
            if req_p and p != req_p:
                fips.remove(fip)
        return fips

    def disassociate_floatingips(self, context, port_id):
        db_port = self._get_port(context, port_id)
        p = self._get_plugin_from_net_id(context, db_port['network_id'])
        return p.disassociate_floatingips(context, port_id)

    def _get_plugin_from_sg_id(self, context, sg_id):
        sg = self._get_security_group(context, sg_id)
        return self._get_plugin_from_project(context, sg['tenant_id'])

    def create_security_group(self, context, security_group,
                              default_sg=False):
        if not default_sg:
            secgroup = security_group['security_group']
            tenant_id = secgroup['tenant_id']
            self._ensure_default_security_group(context, tenant_id)

        p = self._get_plugin_from_project(context, context.project_id)
        self._validate_obj_extensions(
            security_group['security_group'], p.plugin_type(),
            'security_group')

        new_sg = p.create_security_group(context, security_group,
                                         default_sg=default_sg)
        self._cleanup_obj_fields(
            new_sg, p.plugin_type(), 'security_group')
        return new_sg

    def delete_security_group(self, context, id):
        p = self._get_plugin_from_sg_id(context, id)
        p.delete_security_group(context, id)

    def update_security_group(self, context, id, security_group):
        p = self._get_plugin_from_sg_id(context, id)
        self._validate_obj_extensions(
            security_group['security_group'], p.plugin_type(),
            'security_group')
        return p.update_security_group(context, id, security_group)

    def get_security_group(self, context, id, fields=None):
        p = self._get_plugin_from_sg_id(context, id)
        sg = p.get_security_group(context, id, fields=fields)
        self._cleanup_obj_fields(
            sg, p.plugin_type(), 'security_group')
        return sg

    def get_security_groups(self, context, filters=None, fields=None,
                            sorts=None, limit=None,
                            marker=None, page_reverse=False, default_sg=False):
        # Read project plugin to filter relevant projects according to
        # plugin
        req_p = self._get_plugin_for_request(context, filters)
        sgs = super(NsxTVDPlugin, self).get_security_groups(
            context, filters=filters, fields=fields, sorts=sorts,
            limit=limit, marker=marker, page_reverse=page_reverse,
            default_sg=default_sg)
        for sg in sgs[:]:
            p = self._get_plugin_from_project(context, sg['tenant_id'])
            if req_p and p != req_p:
                sgs.remove(sg)
        return sgs

    def create_security_group_rule_bulk(self, context, security_group_rules):
        p = self._get_plugin_from_project(context, context.project_id)
        return p.create_security_group_rule_bulk(context,
                                                 security_group_rules)

    def create_security_group_rule(self, context, security_group_rule):
        p = self._get_plugin_from_project(context, context.project_id)
        return p.create_security_group_rule(context, security_group_rule)

    def delete_security_group_rule(self, context, id):
        rule_db = self._get_security_group_rule(context, id)
        sg_id = rule_db['security_group_id']
        p = self._get_plugin_from_sg_id(context, sg_id)
        p.delete_security_group_rule(context, id)

    def get_security_group_rules(self, context, filters=None, fields=None,
                                 sorts=None, limit=None, marker=None,
                                 page_reverse=False):
        # Read project plugin to filter relevant projects according to
        # plugin
        req_p = self._get_plugin_for_request(context, filters)
        rules = super(NsxTVDPlugin, self).get_security_group_rules(
            context, filters=filters, fields=fields, sorts=sorts,
            limit=limit, marker=marker, page_reverse=page_reverse)
        for rule in rules[:]:
            p = self._get_plugin_from_project(context, rule['tenant_id'])
            if req_p and p != req_p:
                rules.remove(rule)
        return rules

    @staticmethod
    @resource_extend.extends([net_def.COLLECTION_NAME])
    def _ext_extend_network_dict(result, netdb):
        ctx = n_context.get_admin_context()
        # get the core plugin as this is a static method with no 'self'
        plugin = directory.get_plugin()
        p = plugin._get_plugin_from_project(ctx, netdb['tenant_id'])
        with db_api.CONTEXT_WRITER.using(ctx):
            p._extension_manager.extend_network_dict(
                ctx.session, netdb, result)

    @staticmethod
    @resource_extend.extends([port_def.COLLECTION_NAME])
    def _ext_extend_port_dict(result, portdb):
        ctx = n_context.get_admin_context()
        # get the core plugin as this is a static method with no 'self'
        plugin = directory.get_plugin()
        p = plugin._get_plugin_from_project(ctx, portdb['tenant_id'])
        with db_api.CONTEXT_WRITER.using(ctx):
            p._extension_manager.extend_port_dict(
                ctx.session, portdb, result)

    @staticmethod
    @resource_extend.extends([subnet_def.COLLECTION_NAME])
    def _ext_extend_subnet_dict(result, subnetdb):
        ctx = n_context.get_admin_context()
        # get the core plugin as this is a static method with no 'self'
        plugin = directory.get_plugin()
        p = plugin._get_plugin_from_project(ctx, subnetdb['tenant_id'])
        with db_api.CONTEXT_WRITER.using(ctx):
            p._extension_manager.extend_subnet_dict(
                ctx.session, subnetdb, result)

    def _get_project_plugin_dict(self, data):
        return {'id': data['project'],
                'project': data['project'],
                'plugin': data['plugin'],
                'tenant_id': data['project']}

    def create_project_plugin_map(self, context, project_plugin_map,
                                  internal=False):
        data = project_plugin_map['project_plugin_map']

        # validations:
        # 1. validate it doesn't already exist
        if nsx_db.get_project_plugin_mapping(
            context.session, data['project']):
            raise projectpluginmap.ProjectPluginAlreadyExists(
                project_id=data['project'])
        if not internal:
            # 2. only admin user is allowed
            if not context.is_admin:
                raise projectpluginmap.ProjectPluginAdminOnly()
            # 3. Validate the project id
            # TODO(asarfaty): Validate project id exists in keystone
            if not uuidutils.is_uuid_like(data['project']):
                raise projectpluginmap.ProjectPluginIllegalId(
                    project_id=data['project'])
            # 4. Check that plugin is available
            if data['plugin'] not in self.plugins:
                raise projectpluginmap.ProjectPluginNotAvailable(
                    plugin=data['plugin'])

        # Add the entry to the DB and return it
        LOG.info("Adding mapping between project %(project)s and plugin "
                 "%(plugin)s", {'project': data['project'],
                                'plugin': data['plugin']})
        nsx_db.add_project_plugin_mapping(context.session,
                                          data['project'],
                                          data['plugin'])
        return self._get_project_plugin_dict(data)

    def get_project_plugin_map(self, context, id, fields=None):
        data = nsx_db.get_project_plugin_mapping(context.session, id)
        if data:
            return self._get_project_plugin_dict(data)
        raise n_exc.ObjectNotFound(id=id)

    def get_project_plugin_maps(self, context, filters=None, fields=None,
                                sorts=None, limit=None, marker=None,
                                page_reverse=False):
        # TODO(asarfaty) filter the results
        mappings = nsx_db.get_project_plugin_mappings(context.session)
        return [self._get_project_plugin_dict(data) for data in mappings]

    def get_plugin_type_from_project(self, context, project_id):
        """Get the correct plugin type for this project.

        Look for the project in the DB.
        If not there - add an entry with the default plugin
        """
        plugin_type = self.default_plugin
        if not project_id:
            # if the project_id is empty - return the default one and do not
            # add to db (used by admin context to get actions)
            return plugin_type

        mapping = nsx_db.get_project_plugin_mapping(
            context.session, project_id)
        if mapping:
            plugin_type = mapping['plugin']
        else:
            # add a new entry with the default plugin
            try:
                self.create_project_plugin_map(
                    context,
                    {'project_plugin_map': {'plugin': plugin_type,
                                            'project': project_id}},
                    internal=True)
            except projectpluginmap.ProjectPluginAlreadyExists:
                # Maybe added by another thread
                pass
        if not self.plugins.get(plugin_type):
            msg = (_("Cannot use unsupported plugin %(plugin)s for project "
                     "%(project)s") % {'plugin': plugin_type,
                                       'project': project_id})
            raise nsx_exc.NsxPluginException(err_msg=msg)

        LOG.debug("Using %s plugin for project %s", plugin_type, project_id)
        return plugin_type

    def _get_plugin_from_project(self, context, project_id):
        """Get the correct plugin for this project.

        Look for the project in the DB.
        If not there - add an entry with the default plugin
        """
        plugin_type = self.get_plugin_type_from_project(context, project_id)
        return self.plugins[plugin_type]

    def get_housekeeper(self, context, name, fields=None):
        p = self._get_plugin_from_project(context, context.project_id)
        if hasattr(p, 'housekeeper'):
            return p.housekeeper.get(name)
        msg = _("Housekeeper %s not found") % name
        raise nsx_exc.NsxPluginException(err_msg=msg)

    def get_housekeepers(self, context, filters=None, fields=None, sorts=None,
                         limit=None, marker=None, page_reverse=False):
        p = self._get_plugin_for_request(context, filters)
        if p and hasattr(p, 'housekeeper'):
            return p.housekeeper.list()
        return []

    def update_housekeeper(self, context, name, housekeeper):
        p = self._get_plugin_from_project(context, context.project_id)
        if hasattr(p, 'housekeeper'):
            p.housekeeper.run(context, name)
        return p.housekeeper.get(name)

    def get_address_scopes(self, context, filters=None, fields=None,
                           sorts=None, limit=None, marker=None,
                           page_reverse=False):
        # Read project plugin to filter relevant projects according to
        # plugin
        req_p = self._get_plugin_for_request(context, filters)
        address_scopes = super(NsxTVDPlugin, self).get_address_scopes(
            context, filters=filters, fields=fields, sorts=sorts,
            limit=limit, marker=marker, page_reverse=page_reverse)
        for address_scope in address_scopes[:]:
            p = self._get_plugin_from_project(context,
                                              address_scope['tenant_id'])
            if req_p and p != req_p:
                address_scopes.remove(address_scope)
        return address_scopes

    def get_subnetpools(self, context, filters=None, fields=None,
                        sorts=None, limit=None, marker=None,
                        page_reverse=False):
        # Read project plugin to filter relevant projects according to
        # plugin
        req_p = self._get_plugin_for_request(context, filters)
        pools = super(NsxTVDPlugin, self).get_subnetpools(
            context, filters=filters, fields=fields, sorts=sorts,
            limit=limit, marker=marker, page_reverse=page_reverse)
        for pool in pools[:]:
            p = self._get_plugin_from_project(context,
                                              pool['tenant_id'])
            if req_p and p != req_p:
                pools.remove(pool)
        return pools

    def get_nsx_policy(self, context, id, fields=None):
        # Extension supported only by the nsxv plugin
        p = self._get_plugin_from_project(context, context.project_id)
        if p.plugin_type() != v.NsxVPluginV2.plugin_type():
            err_msg = (_('Can not support %(field)s extension for '
                         '%(p)s plugin') % {
                       'field': 'nsx-policy',
                       'p': p.plugin_type()})
            raise n_exc.InvalidInput(error_message=err_msg)

        return p.get_nsx_policy(context, id, fields=fields)

    def get_nsx_policies(self, context, filters=None, fields=None,
                         sorts=None, limit=None, marker=None,
                         page_reverse=False):
        # Extension supported only by the nsxv plugin
        p = self._get_plugin_from_project(context, context.project_id)
        if p.plugin_type() != v.NsxVPluginV2.plugin_type():
            return []
        return p.get_nsx_policies(context, filters=filters, fields=fields,
                                  sorts=sorts, limit=limit, marker=marker,
                                  page_reverse=page_reverse)
