# Copyright (c) 2016 Cisco Systems 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 gbpservice._i18n import _LE
from gbpservice._i18n import _LI
from gbpservice.neutron.plugins.ml2plus import driver_api

from neutron.db import api as db_api
from neutron.plugins.ml2.common import exceptions as ml2_exc
from neutron.plugins.ml2 import managers
from neutron.quota import resource_registry
from oslo_log import log
from oslo_utils import excutils

LOG = log.getLogger(__name__)


class MechanismManager(managers.MechanismManager):

    def __init__(self):
        super(MechanismManager, self).__init__()

    def _call_on_drivers(self, method_name, context,
            continue_on_failure=False, raise_db_retriable=False):
        super(MechanismManager, self)._call_on_drivers(
                method_name, context, continue_on_failure=False,
                raise_db_retriable=False)
        if method_name.endswith('_precommit'):
            # This does the same thing as:
            # https://github.com/openstack/neutron/blob/newton-eol/neutron/
            # api/v2/base.py#L489
            # but from within the scope of the plugin's transaction, such
            # that if it fails, everything that happened prior to this in
            # precommit phase can also be rolled back.
            resource_name = method_name.replace('_precommit', '').replace(
                    'create_', '').replace('update_', '').replace(
                            'delete_', '')
            tracked_resource = resource_registry.get_resource(resource_name)
            tracked_resource._dirty_tenants.add(context.current['tenant_id'])
            resource_registry.set_resources_dirty(context._plugin_context)

    def _call_on_extended_drivers(self, method_name, context,
                                  continue_on_failure=False,
                                  raise_db_retriable=False):
        """Call a method on all extended mechanism drivers.

        :param method_name: name of the method to call
        :param context: context parameter to pass to each method call
        :param continue_on_failure: whether or not to continue to call
        all mechanism drivers once one has raised an exception
        :param raise_db_retriable: whether or not to treat retriable db
        exception by mechanism drivers to propagate up to upper layer so
        that upper layer can handle it or error in ML2 player
        :raises: neutron.plugins.ml2.common.MechanismDriverError
        if any mechanism driver call fails. or DB retriable error when
        raise_db_retriable=False. See neutron.db.api.is_retriable for
        what db exception is retriable
        """
        errors = []
        for driver in self.ordered_mech_drivers:
            if isinstance(driver.obj, driver_api.MechanismDriver):
                try:
                    getattr(driver.obj, method_name)(context)
                except Exception as e:
                    if raise_db_retriable and db_api.is_retriable(e):
                        with excutils.save_and_reraise_exception():
                            LOG.debug("DB exception raised by Mechanism "
                                      "driver '%(name)s' in %(method)s",
                                      {'name': driver.name,
                                       'method': method_name},
                                      exc_info=e)
                    LOG.exception(
                        _LE("Mechanism driver '%(name)s' failed in "
                            "%(method)s"),
                        {'name': driver.name, 'method': method_name}
                    )
                    errors.append(e)
                    if not continue_on_failure:
                        break
        if errors:
            raise ml2_exc.MechanismDriverError(
                method=method_name,
                errors=errors
            )

    def ensure_tenant(self, plugin_context, tenant_id):
        for driver in self.ordered_mech_drivers:
            if isinstance(driver.obj, driver_api.MechanismDriver):
                try:
                    driver.obj.ensure_tenant(plugin_context, tenant_id)
                except Exception as e:
                    if db_api.is_retriable(e):
                        with excutils.save_and_reraise_exception():
                            LOG.debug("DB exception raised by Mechanism "
                                      "driver '%(name)s' in ensure_tenant",
                                      {'name': driver.name},
                                      exc_info=e)
                    LOG.exception(_LE("Mechanism driver '%s' failed in "
                                      "ensure_tenant"), driver.name)
                    raise ml2_exc.MechanismDriverError(method="ensure_tenant")

    def create_subnetpool_precommit(self, context):
        self._call_on_extended_drivers("create_subnetpool_precommit",
                                       context, raise_db_retriable=True)

    def create_subnetpool_postcommit(self, context):
        self._call_on_extended_drivers("create_subnetpool_postcommit",
                                       context)

    def update_subnetpool_precommit(self, context):
        self._call_on_extended_drivers("update_subnetpool_precommit",
                                       context, raise_db_retriable=True)

    def update_subnetpool_postcommit(self, context):
        self._call_on_extended_drivers("update_subnetpool_postcommit",
                                       context, continue_on_failure=True)

    def delete_subnetpool_precommit(self, context):
        self._call_on_extended_drivers("delete_subnetpool_precommit",
                                       context, raise_db_retriable=True)

    def delete_subnetpool_postcommit(self, context):
        self._call_on_extended_drivers("delete_subnetpool_postcommit",
                                       context, continue_on_failure=True)

    def create_address_scope_precommit(self, context):
        self._call_on_extended_drivers("create_address_scope_precommit",
                                       context, raise_db_retriable=True)

    def create_address_scope_postcommit(self, context):
        self._call_on_extended_drivers("create_address_scope_postcommit",
                                       context)

    def update_address_scope_precommit(self, context):
        self._call_on_extended_drivers("update_address_scope_precommit",
                                       context, raise_db_retriable=True)

    def update_address_scope_postcommit(self, context):
        self._call_on_extended_drivers("update_address_scope_postcommit",
                                       context, continue_on_failure=True)

    def delete_address_scope_precommit(self, context):
        self._call_on_extended_drivers("delete_address_scope_precommit",
                                       context, raise_db_retriable=True)

    def delete_address_scope_postcommit(self, context):
        self._call_on_extended_drivers("delete_address_scope_postcommit",
                                       context, continue_on_failure=True)

    def create_security_group_precommit(self, context):
        self._call_on_extended_drivers("create_security_group_precommit",
                                       context, raise_db_retriable=True)

    def create_security_group_postcommit(self, context):
        self._call_on_extended_drivers("create_security_group_postcommit",
                                       context)

    def update_security_group_precommit(self, context):
        self._call_on_extended_drivers("update_security_group_precommit",
                                       context, raise_db_retriable=True)

    def update_security_group_postcommit(self, context):
        self._call_on_extended_drivers("update_security_group_postcommit",
                                       context, continue_on_failure=True)

    def delete_security_group_precommit(self, context):
        self._call_on_extended_drivers("delete_security_group_precommit",
                                       context, raise_db_retriable=True)

    def delete_security_group_postcommit(self, context):
        self._call_on_extended_drivers("delete_security_group_postcommit",
                                       context, continue_on_failure=True)

    def create_security_group_rule_precommit(self, context):
        self._call_on_extended_drivers("create_security_group_rule_precommit",
                                       context, raise_db_retriable=True)

    def create_security_group_rule_postcommit(self, context):
        self._call_on_extended_drivers("create_security_group_rule_postcommit",
                                       context)

    def update_security_group_rule_precommit(self, context):
        self._call_on_extended_drivers("update_security_group_rule_precommit",
                                       context, raise_db_retriable=True)

    def update_security_group_rule_postcommit(self, context):
        self._call_on_extended_drivers("update_security_group_rule_postcommit",
                                       context, continue_on_failure=True)

    def delete_security_group_rule_precommit(self, context):
        self._call_on_extended_drivers("delete_security_group_rule_precommit",
                                       context, raise_db_retriable=True)

    def delete_security_group_rule_postcommit(self, context):
        self._call_on_extended_drivers("delete_security_group_rule_postcommit",
                                       context, continue_on_failure=True)


class ExtensionManager(managers.ExtensionManager):

    def __init__(self):
        super(ExtensionManager, self).__init__()

    def _call_on_extended_drivers(self, method_name, plugin_context, data,
                                  result):
        """Call a method on all extended extension drivers."""
        for driver in self.ordered_ext_drivers:
            if isinstance(driver.obj, driver_api.ExtensionDriver):
                try:
                    getattr(driver.obj, method_name)(plugin_context, data,
                                                     result)
                except Exception:
                    with excutils.save_and_reraise_exception():
                        LOG.info(_LI("Extension driver '%(name)s' failed in "
                                     "%(method)s"),
                                 {'name': driver.name, 'method': method_name})

    # Overrides ML2 implementation to avoid eating retriable
    # exceptions, as well as to support calling only on extension
    # drivers extended for ML2Plus.
    def _call_on_dict_driver(self, method_name, session, base_model, result,
                             extended_only=False, has_base_model=True):

        # Bulk operations might not be implemented by all drivers
        def noop(*args, **kwargs):
            pass

        for driver in self.ordered_ext_drivers:
            if not extended_only or isinstance(
                    driver.obj, driver_api.ExtensionDriver):
                try:
                    if not has_base_model:
                        getattr(driver.obj, method_name, noop)(session,
                                                               result)
                    else:
                        getattr(driver.obj, method_name, noop)(session,
                                                               base_model,
                                                               result)
                except Exception as e:
                    if db_api.is_retriable(e):
                        with excutils.save_and_reraise_exception():
                            LOG.debug(
                                "DB exception raised by extension driver "
                                "'%(name)s' in %(method)s",
                                {'name': driver.name, 'method': method_name},
                                exc_info=e)
                    LOG.exception(
                        "Extension driver '%(name)s' failed in %(method)s",
                        {'name': driver.name, 'method': method_name})
                    raise ml2_exc.ExtensionDriverError(driver=driver.name)

    def process_create_subnetpool(self, plugin_context, data, result):
        self._call_on_extended_drivers("process_create_subnetpool",
                                       plugin_context, data, result)

    def process_update_subnetpool(self, plugin_context, data, result):
        self._call_on_extended_drivers("process_update_subnetpool",
                                       plugin_context, data, result)

    def extend_subnetpool_dict(self, session, base_model, result):
        self._call_on_dict_driver("extend_subnetpool_dict",
                                  session, base_model, result, True)

    def process_create_address_scope(self, plugin_context, data, result):
        self._call_on_extended_drivers("process_create_address_scope",
                                       plugin_context, data, result)

    def process_update_address_scope(self, plugin_context, data, result):
        self._call_on_extended_drivers("process_update_address_scope",
                                       plugin_context, data, result)

    def extend_address_scope_dict(self, session, base_model, result):
        self._call_on_dict_driver("extend_address_scope_dict",
                                  session, base_model, result, True)

    def extend_subnetpool_dict_bulk(self, session, result):
        self._call_on_dict_driver("extend_subnetpool_dict_bulk",
                                  session, None, result, True,
                                  has_base_model=False)

    def extend_address_scope_dict_bulk(self, session, result):
        self._call_on_dict_driver("extend_address_scope_dict_bulk",
                                  session, None, result, True,
                                  has_base_model=False)

    def extend_network_dict_bulk(self, session, result):
        self._call_on_dict_driver("extend_network_dict_bulk", session,
                                  None, result, has_base_model=False)

    def extend_subnet_dict_bulk(self, session, result):
        self._call_on_dict_driver("extend_subnet_dict_bulk", session,
                                  None, result, has_base_model=False)

    def extend_port_dict_bulk(self, session, result):
        self._call_on_dict_driver("extend_port_dict_bulk", session, None,
                                  result, has_base_model=False)
