# Copyright 2012, Nachi Ueno, NTT MCL, 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.

from oslo.config import cfg

from neutron.common import exceptions as exc
from neutron.common import topics
from neutron import context as neutron_context
from neutron.db import db_base_plugin_v2
from neutron.db import external_net_db
from neutron.db import extraroute_db
from neutron.db import l3_db
from neutron.db import models_v2
from neutron.extensions import flavor as ext_flavor
from neutron.openstack.common import importutils
from neutron.openstack.common import log as logging
from neutron.plugins.metaplugin.common import config  # noqa
from neutron.plugins.metaplugin import meta_db_v2
from neutron.plugins.metaplugin import meta_models_v2


LOG = logging.getLogger(__name__)


# Hooks used to select records which belong a target plugin.
def _meta_network_model_hook(context, original_model, query):
    return query.outerjoin(meta_models_v2.NetworkFlavor,
                           meta_models_v2.NetworkFlavor.network_id ==
                           models_v2.Network.id)


def _meta_port_model_hook(context, original_model, query):
    return query.join(meta_models_v2.NetworkFlavor,
                      meta_models_v2.NetworkFlavor.network_id ==
                      models_v2.Port.network_id)


def _meta_flavor_filter_hook(query, filters):
    if ext_flavor.FLAVOR_NETWORK in filters:
        return query.filter(meta_models_v2.NetworkFlavor.flavor ==
                            filters[ext_flavor.FLAVOR_NETWORK][0])
    return query


# Metaplugin  Exceptions
class FlavorNotFound(exc.NotFound):
    message = _("Flavor %(flavor)s could not be found")


class FaildToAddFlavorBinding(exc.NeutronException):
    message = _("Failed to add flavor binding")


class MetaPluginV2(db_base_plugin_v2.NeutronDbPluginV2,
                   external_net_db.External_net_db_mixin,
                   extraroute_db.ExtraRoute_db_mixin):

    def __init__(self, configfile=None):
        super(MetaPluginV2, self).__init__()
        LOG.debug(_("Start initializing metaplugin"))
        self.supported_extension_aliases = ['flavor', 'external-net']
        if cfg.CONF.META.supported_extension_aliases:
            cfg_aliases = cfg.CONF.META.supported_extension_aliases.split(',')
            self.supported_extension_aliases += cfg_aliases

        # Ignore config option overapping
        def _is_opt_registered(opts, opt):
            if opt.dest in opts:
                return True
            else:
                return False

        cfg._is_opt_registered = _is_opt_registered

        self.plugins = {}

        plugin_list = [plugin_set.split(':')
                       for plugin_set
                       in cfg.CONF.META.plugin_list.split(',')]
        self.rpc_flavor = cfg.CONF.META.rpc_flavor
        topic_save = topics.PLUGIN
        topic_fake = topic_save + '-metaplugin'
        for flavor, plugin_provider in plugin_list:
            # Rename topic used by a plugin other than rpc_flavor during
            # loading the plugin instance if rpc_flavor is specified.
            # This enforces the plugin specified by rpc_flavor is only
            # consumer of 'q-plugin'. It is a bit tricky but there is no
            # bad effect.
            if self.rpc_flavor and self.rpc_flavor != flavor:
                topics.PLUGIN = topic_fake
            self.plugins[flavor] = self._load_plugin(plugin_provider)
            topics.PLUGIN = topic_save

        self.l3_plugins = {}
        if cfg.CONF.META.l3_plugin_list:
            l3_plugin_list = [plugin_set.split(':')
                              for plugin_set
                              in cfg.CONF.META.l3_plugin_list.split(',')]
            for flavor, plugin_provider in l3_plugin_list:
                if flavor in self.plugins:
                    self.l3_plugins[flavor] = self.plugins[flavor]
                else:
                    # For l3 only plugin
                    self.l3_plugins[flavor] = self._load_plugin(
                        plugin_provider)

        self.default_flavor = cfg.CONF.META.default_flavor
        if self.default_flavor not in self.plugins:
            raise exc.Invalid(_('default_flavor %s is not plugin list') %
                              self.default_flavor)

        if self.l3_plugins:
            self.default_l3_flavor = cfg.CONF.META.default_l3_flavor
            if self.default_l3_flavor not in self.l3_plugins:
                raise exc.Invalid(_('default_l3_flavor %s is not plugin list')
                                  % self.default_l3_flavor)
            self.supported_extension_aliases += ['router', 'ext-gw-mode',
                                                 'extraroute']

        if self.rpc_flavor and self.rpc_flavor not in self.plugins:
            raise exc.Invalid(_('rpc_flavor %s is not plugin list') %
                              self.rpc_flavor)

        self.extension_map = {}
        if not cfg.CONF.META.extension_map == '':
            extension_list = [method_set.split(':')
                              for method_set
                              in cfg.CONF.META.extension_map.split(',')]
            for method_name, flavor in extension_list:
                self.extension_map[method_name] = flavor

        # Register hooks.
        # The hooks are applied for each target plugin instance when
        # calling the base class to get networks/ports so that only records
        # which belong to the plugin are selected.
        #NOTE: Doing registration here (within __init__()) is to avoid
        # registration when merely importing this file. This is only
        # for running whole unit tests.
        db_base_plugin_v2.NeutronDbPluginV2.register_model_query_hook(
            models_v2.Network,
            'metaplugin_net',
            _meta_network_model_hook,
            None,
            _meta_flavor_filter_hook)
        db_base_plugin_v2.NeutronDbPluginV2.register_model_query_hook(
            models_v2.Port,
            'metaplugin_port',
            _meta_port_model_hook,
            None,
            _meta_flavor_filter_hook)

    def _load_plugin(self, plugin_provider):
        LOG.debug(_("Plugin location: %s"), plugin_provider)
        plugin_klass = importutils.import_class(plugin_provider)
        return plugin_klass()

    def _get_plugin(self, flavor):
        if flavor not in self.plugins:
            raise FlavorNotFound(flavor=flavor)
        return self.plugins[flavor]

    def _get_l3_plugin(self, flavor):
        if flavor not in self.l3_plugins:
            raise FlavorNotFound(flavor=flavor)
        return self.l3_plugins[flavor]

    def __getattr__(self, key):
        # At first,  try to pickup extension command from extension_map

        if key in self.extension_map:
            flavor = self.extension_map[key]
            plugin = self._get_plugin(flavor)
            if plugin and hasattr(plugin, key):
                return getattr(plugin, key)

        # Second, try to match extension method in order of plugin list
        for flavor, plugin in self.plugins.items():
            if hasattr(plugin, key):
                return getattr(plugin, key)

        # if no plugin support the method, then raise
        raise AttributeError

    def _extend_network_dict(self, context, network):
        flavor = self._get_flavor_by_network_id(context, network['id'])
        network[ext_flavor.FLAVOR_NETWORK] = flavor

    def start_rpc_listeners(self):
        return self.plugins[self.rpc_flavor].start_rpc_listeners()

    def rpc_workers_supported(self):
        #NOTE: If a plugin which supports multiple RPC workers is desired
        # to handle RPC, rpc_flavor must be specified.
        return (self.rpc_flavor and
                self.plugins[self.rpc_flavor].rpc_workers_supported())

    def create_network(self, context, network):
        n = network['network']
        flavor = n.get(ext_flavor.FLAVOR_NETWORK)
        if str(flavor) not in self.plugins:
            flavor = self.default_flavor
        plugin = self._get_plugin(flavor)
        net = plugin.create_network(context, network)
        LOG.debug(_("Created network: %(net_id)s with flavor "
                    "%(flavor)s"), {'net_id': net['id'], 'flavor': flavor})
        try:
            meta_db_v2.add_network_flavor_binding(context.session,
                                                  flavor, str(net['id']))
        except Exception:
            LOG.exception(_('Failed to add flavor bindings'))
            plugin.delete_network(context, net['id'])
            raise FaildToAddFlavorBinding()

        LOG.debug(_("Created network: %s"), net['id'])
        self._extend_network_dict(context, net)
        return net

    def update_network(self, context, id, network):
        flavor = meta_db_v2.get_flavor_by_network(context.session, id)
        plugin = self._get_plugin(flavor)
        return plugin.update_network(context, id, network)

    def delete_network(self, context, id):
        flavor = meta_db_v2.get_flavor_by_network(context.session, id)
        plugin = self._get_plugin(flavor)
        return plugin.delete_network(context, id)

    def get_network(self, context, id, fields=None):
        flavor = meta_db_v2.get_flavor_by_network(context.session, id)
        plugin = self._get_plugin(flavor)
        net = plugin.get_network(context, id, fields)
        net['id'] = id
        if not fields or ext_flavor.FLAVOR_NETWORK in fields:
            self._extend_network_dict(context, net)
        if fields and 'id' not in fields:
            del net['id']
        return net

    def get_networks(self, context, filters=None, fields=None):
        nets = []
        for flavor, plugin in self.plugins.items():
            if (filters and ext_flavor.FLAVOR_NETWORK in filters and
                    not flavor in filters[ext_flavor.FLAVOR_NETWORK]):
                continue
            if filters:
                #NOTE: copy each time since a target plugin may modify
                # plugin_filters.
                plugin_filters = filters.copy()
            else:
                plugin_filters = {}
            plugin_filters[ext_flavor.FLAVOR_NETWORK] = [flavor]
            plugin_nets = plugin.get_networks(context, plugin_filters, fields)
            for net in plugin_nets:
                if not fields or ext_flavor.FLAVOR_NETWORK in fields:
                    net[ext_flavor.FLAVOR_NETWORK] = flavor
                nets.append(net)
        return nets

    def _get_flavor_by_network_id(self, context, network_id):
        return meta_db_v2.get_flavor_by_network(context.session, network_id)

    def _get_flavor_by_router_id(self, context, router_id):
        return meta_db_v2.get_flavor_by_router(context.session, router_id)

    def _get_plugin_by_network_id(self, context, network_id):
        flavor = self._get_flavor_by_network_id(context, network_id)
        return self._get_plugin(flavor)

    def create_port(self, context, port):
        p = port['port']
        if 'network_id' not in p:
            raise exc.NotFound
        plugin = self._get_plugin_by_network_id(context, p['network_id'])
        return plugin.create_port(context, port)

    def update_port(self, context, id, port):
        port_in_db = self._get_port(context, id)
        plugin = self._get_plugin_by_network_id(context,
                                                port_in_db['network_id'])
        return plugin.update_port(context, id, port)

    def delete_port(self, context, id, l3_port_check=True):
        port_in_db = self._get_port(context, id)
        plugin = self._get_plugin_by_network_id(context,
                                                port_in_db['network_id'])
        return plugin.delete_port(context, id, l3_port_check)

    # This is necessary since there is a case that
    # NeutronManager.get_plugin()._make_port_dict is called.
    def _make_port_dict(self, port):
        context = neutron_context.get_admin_context()
        plugin = self._get_plugin_by_network_id(context,
                                                port['network_id'])
        return plugin._make_port_dict(port)

    def get_port(self, context, id, fields=None):
        port_in_db = self._get_port(context, id)
        plugin = self._get_plugin_by_network_id(context,
                                                port_in_db['network_id'])
        return plugin.get_port(context, id, fields)

    def get_ports(self, context, filters=None, fields=None):
        all_ports = []
        for flavor, plugin in self.plugins.items():
            if filters:
                #NOTE: copy each time since a target plugin may modify
                # plugin_filters.
                plugin_filters = filters.copy()
            else:
                plugin_filters = {}
            plugin_filters[ext_flavor.FLAVOR_NETWORK] = [flavor]
            ports = plugin.get_ports(context, plugin_filters, fields)
            all_ports += ports
        return all_ports

    def create_subnet(self, context, subnet):
        s = subnet['subnet']
        if 'network_id' not in s:
            raise exc.NotFound
        plugin = self._get_plugin_by_network_id(context,
                                                s['network_id'])
        return plugin.create_subnet(context, subnet)

    def update_subnet(self, context, id, subnet):
        s = self.get_subnet(context, id)
        plugin = self._get_plugin_by_network_id(context,
                                                s['network_id'])
        return plugin.update_subnet(context, id, subnet)

    def delete_subnet(self, context, id):
        s = self.get_subnet(context, id)
        plugin = self._get_plugin_by_network_id(context,
                                                s['network_id'])
        return plugin.delete_subnet(context, id)

    def _extend_router_dict(self, context, router):
        flavor = self._get_flavor_by_router_id(context, router['id'])
        router[ext_flavor.FLAVOR_ROUTER] = flavor

    def create_router(self, context, router):
        r = router['router']
        flavor = r.get(ext_flavor.FLAVOR_ROUTER)
        if str(flavor) not in self.l3_plugins:
            flavor = self.default_l3_flavor
        plugin = self._get_l3_plugin(flavor)
        r_in_db = plugin.create_router(context, router)
        LOG.debug(_("Created router: %(router_id)s with flavor "
                    "%(flavor)s"),
                  {'router_id': r_in_db['id'], 'flavor': flavor})
        try:
            meta_db_v2.add_router_flavor_binding(context.session,
                                                 flavor, str(r_in_db['id']))
        except Exception:
            LOG.exception(_('Failed to add flavor bindings'))
            plugin.delete_router(context, r_in_db['id'])
            raise FaildToAddFlavorBinding()

        LOG.debug(_("Created router: %s"), r_in_db['id'])
        self._extend_router_dict(context, r_in_db)
        return r_in_db

    def update_router(self, context, id, router):
        flavor = meta_db_v2.get_flavor_by_router(context.session, id)
        plugin = self._get_l3_plugin(flavor)
        return plugin.update_router(context, id, router)

    def delete_router(self, context, id):
        flavor = meta_db_v2.get_flavor_by_router(context.session, id)
        plugin = self._get_l3_plugin(flavor)
        return plugin.delete_router(context, id)

    def get_router(self, context, id, fields=None):
        flavor = meta_db_v2.get_flavor_by_router(context.session, id)
        plugin = self._get_l3_plugin(flavor)
        router = plugin.get_router(context, id, fields)
        if not fields or ext_flavor.FLAVOR_ROUTER in fields:
            self._extend_router_dict(context, router)
        return router

    def get_routers_with_flavor(self, context, filters=None,
                                fields=None):
        collection = self._model_query(context, l3_db.Router)
        r_model = meta_models_v2.RouterFlavor
        collection = collection.join(r_model,
                                     l3_db.Router.id == r_model.router_id)
        if filters:
            for key, value in filters.iteritems():
                if key == ext_flavor.FLAVOR_ROUTER:
                    column = meta_models_v2.RouterFlavor.flavor
                else:
                    column = getattr(l3_db.Router, key, None)
                if column:
                    collection = collection.filter(column.in_(value))
        return [self._make_router_dict(c, fields) for c in collection]

    def get_routers(self, context, filters=None, fields=None):
        routers = self.get_routers_with_flavor(context, filters,
                                               None)
        return [self.get_router(context, router['id'],
                                fields)
                for router in routers]
