# Copyright 2015 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.

from networking_l2gw.db.l2gateway import l2gateway_db
from networking_l2gw.services.l2gateway.common import constants as l2gw_const
from networking_l2gw.services.l2gateway import exceptions as l2gw_exc
from oslo_config import cfg
from oslo_db import exception as db_exc
from oslo_log import log as logging
from oslo_utils import excutils
from oslo_utils import uuidutils

from neutron.callbacks import events
from neutron.callbacks import registry
from neutron.callbacks import resources
from neutron import context
from neutron.extensions import providernet
from neutron import manager
from neutron.plugins.common import utils as n_utils
from neutron_lib import constants
from neutron_lib import exceptions as n_exc

from vmware_nsx._i18n import _, _LE, _LI
from vmware_nsx.common import nsx_constants
from vmware_nsx.common import utils as nsx_utils
from vmware_nsx.db import db as nsx_db
from vmware_nsx.nsxlib.v3 import exceptions as nsxlib_exc

LOG = logging.getLogger(__name__)


class NsxV3Driver(l2gateway_db.L2GatewayMixin):

    """Class to handle API calls for L2 gateway and NSXv3 backend."""
    gateway_resource = l2gw_const.GATEWAY_RESOURCE_NAME

    def __init__(self, plugin):
        # Create a default L2 gateway if default_bridge_cluster is
        # provided in nsx.ini
        super(NsxV3Driver, self).__init__()
        self._plugin = plugin
        LOG.debug("Starting service plugin for NSX L2Gateway")
        self.subscribe_callback_notifications()
        LOG.debug("Initialization complete for NSXv3 driver for "
                  "L2 gateway service plugin.")

    @property
    def _core_plugin(self):
        return manager.NeutronManager.get_plugin()

    def subscribe_callback_notifications(self):
        registry.subscribe(self._prevent_l2gw_port_delete, resources.PORT,
                           events.BEFORE_DELETE)
        registry.subscribe(self._ensure_default_l2_gateway, resources.PROCESS,
                           events.BEFORE_SPAWN)

    def _ensure_default_l2_gateway(self, resource, event, trigger, **kwargs):
        """
        Create a default logical L2 gateway.

        Create a logical L2 gateway in the neutron database if the
        default_bridge_cluster config parameter is set and if it is
        not previously created. If not set, return.
        """
        def_l2gw_name = cfg.CONF.nsx_v3.default_bridge_cluster
        # Return if no default_bridge_cluster set in config
        if not def_l2gw_name:
            LOG.info(_LI("NSX: Default bridge cluster not configured "
                         "in nsx.ini. No default L2 gateway created."))
            return
        admin_ctx = context.get_admin_context()

        def_l2gw_uuid = (
            self._core_plugin.nsxlib.get_bridge_cluster_id_by_name_or_id(
                def_l2gw_name))

        # Optimistically create the default L2 gateway in neutron DB
        device = {'device_name': def_l2gw_uuid,
                  'interfaces': [{'name': 'default-bridge-cluster'}]}
        def_l2gw = {'name': 'default-l2gw',
                    'devices': [device]}
        l2gw_dict = {self.gateway_resource: def_l2gw}
        self.create_l2_gateway(admin_ctx, l2gw_dict)
        l2_gateway = super(NsxV3Driver, self).create_l2_gateway(admin_ctx,
                                                                l2gw_dict)
        # Verify that only one default L2 gateway is created
        def_l2gw_exists = False
        l2gateways = self._get_l2_gateways(admin_ctx)
        for l2gateway in l2gateways:
            # Since we ensure L2 gateway is created with only 1 device, we use
            # the first device in the list.
            if l2gateway['devices'][0]['device_name'] == def_l2gw_uuid:
                if def_l2gw_exists:
                    LOG.info(_LI("Default L2 gateway is already created."))
                    try:
                        # Try deleting this duplicate default L2 gateway
                        self.validate_l2_gateway_for_delete(
                            admin_ctx, l2gateway['id'])
                        super(NsxV3Driver, self).delete_l2_gateway(
                            admin_ctx, l2gateway['id'])
                    except l2gw_exc.L2GatewayInUse:
                        # If the L2 gateway we are trying to delete is in
                        # use then we should delete the L2 gateway which
                        # we just created ensuring there is only one
                        # default L2 gateway in the database.
                        super(NsxV3Driver, self).delete_l2_gateway(
                            admin_ctx, l2_gateway['id'])
                else:
                    def_l2gw_exists = True
        return l2_gateway

    def _prevent_l2gw_port_delete(self, resource, event, trigger, **kwargs):
        context = kwargs.get('context')
        port_id = kwargs.get('port_id')
        port_check = kwargs.get('port_check')
        if port_check:
            self.prevent_l2gw_port_deletion(context, port_id)

    def _validate_device_list(self, devices):
        # In NSXv3, one L2 gateway is mapped to one bridge cluster.
        # So we expect only one device to be configured as part of
        # a L2 gateway resource. The name of the device must be the bridge
        # cluster's UUID.
        if len(devices) != 1:
            msg = _("Only a single device is supported for one L2 gateway")
            raise n_exc.InvalidInput(error_message=msg)
        if not uuidutils.is_uuid_like(devices[0]['device_name']):
            msg = _("Device name must be configured with a UUID")
            raise n_exc.InvalidInput(error_message=msg)
        # One L2 gateway must have only one interface defined.
        interfaces = devices[0].get(l2gw_const.IFACE_NAME_ATTR)
        if len(interfaces) > 1:
            msg = _("Maximum of one interface is supported for one L2 gateway")
            raise n_exc.InvalidInput(error_message=msg)

    def create_l2_gateway(self, context, l2_gateway):
        """Create a logical L2 gateway."""
        gw = l2_gateway[self.gateway_resource]
        devices = gw['devices']
        self._validate_device_list(devices)

    def create_l2_gateway_precommit(self, context, l2_gateway):
        pass

    def create_l2_gateway_postcommit(self, context, l2_gateway):
        pass

    def update_l2_gateway_precommit(self, context, l2_gateway):
        pass

    def update_l2_gateway_postcommit(self, context, l2_gateway):
        pass

    def delete_l2_gateway(self, context, l2_gateway_id):
        pass

    def delete_l2_gateway_precommit(self, context, l2_gateway_id):
        pass

    def delete_l2_gateway_postcommit(self, context, l2_gateway_id):
        pass

    def _validate_network(self, context, network_id):
        network = self._core_plugin.get_network(context, network_id)
        network_type = network.get(providernet.NETWORK_TYPE)
        # If network is a provider network, verify whether it is of type VXLAN
        if network_type and network_type != nsx_utils.NsxV3NetworkTypes.VXLAN:
            msg = (_("Unsupported network type %s for L2 gateway "
                     "connection. Only VXLAN network type supported") %
                   network_type)
            raise n_exc.InvalidInput(error_message=msg)

    def _validate_segment_id(self, seg_id):
        if not seg_id:
            raise l2gw_exc.L2GatewaySegmentationRequired
        return n_utils.is_valid_vlan_tag(seg_id)

    def create_l2_gateway_connection(self, context, l2_gateway_connection):
        gw_connection = l2_gateway_connection.get(self.connection_resource)
        network_id = gw_connection.get(l2gw_const.NETWORK_ID)
        self._validate_network(context, network_id)

    def create_l2_gateway_connection_precommit(self, context, gw_connection):
        pass

    def create_l2_gateway_connection_postcommit(self, context, gw_connection):
        """Create a L2 gateway connection."""
        l2gw_id = gw_connection.get(l2gw_const.L2GATEWAY_ID)
        network_id = gw_connection.get(l2gw_const.NETWORK_ID)
        devices = self._get_l2_gateway_devices(context, l2gw_id)
        # In NSXv3, there will be only one device configured per L2 gateway.
        # The name of the device shall carry the backend bridge cluster's UUID.
        device_name = devices[0].get('device_name')
        # The seg-id will be provided either during gateway create or gateway
        # connection create. l2gateway_db_mixin makes sure that it is
        # configured one way or the other.
        seg_id = gw_connection.get(l2gw_const.SEG_ID)
        if not seg_id:
            # Seg-id was not passed as part of connection-create. Retrieve
            # seg-id from L2 gateway's interface.
            interface = self._get_l2_gw_interfaces(context, devices[0]['id'])
            seg_id = interface[0].get(l2gw_const.SEG_ID)
        self._validate_segment_id(seg_id)
        tenant_id = gw_connection['tenant_id']
        if context.is_admin and not tenant_id:
            tenant_id = context.tenant_id
            gw_connection['tenant_id'] = tenant_id
        try:
            tags = nsx_utils.build_v3_tags_payload(
                gw_connection, resource_type='os-neutron-l2gw-id',
                project_name=context.tenant_name)
            bridge_endpoint = self._core_plugin.nsxlib.create_bridge_endpoint(
                device_name=device_name,
                seg_id=seg_id,
                tags=tags)
        except nsxlib_exc.ManagerError as e:
            LOG.exception(_LE("Unable to create bridge endpoint, rolling back "
                              "changes on neutron. Exception is %s"), e)
            raise l2gw_exc.L2GatewayServiceDriverError(
                method='create_l2_gateway_connection_postcommit')
        #TODO(abhiraut): Consider specifying the name of the port
        # Create a logical port and connect it to the bridge endpoint.
        port_dict = {'port': {
                        'tenant_id': tenant_id,
                        'network_id': network_id,
                        'mac_address': constants.ATTR_NOT_SPECIFIED,
                        'admin_state_up': True,
                        'fixed_ips': [],
                        'device_id': bridge_endpoint['id'],
                        'device_owner': nsx_constants.BRIDGE_ENDPOINT,
                        'name': '', }}
        try:
            #TODO(abhiraut): Consider adding UT for port check once UTs are
            #                refactored
            port = self._core_plugin.create_port(context, port_dict,
                                                 l2gw_port_check=True)
            # Deallocate IP address from the port.
            for fixed_ip in port.get('fixed_ips', []):
                self._core_plugin._delete_ip_allocation(context, network_id,
                                                        fixed_ip['subnet_id'],
                                                        fixed_ip['ip_address'])
            LOG.debug("IP addresses deallocated on port %s", port['id'])
        except (nsxlib_exc.ManagerError,
                n_exc.NeutronException):
            LOG.exception(_LE("Unable to create L2 gateway port, "
                              "rolling back changes on neutron"))
            self._core_plugin.nsxlib.delete_bridge_endpoint(
                bridge_endpoint['id'])
            raise l2gw_exc.L2GatewayServiceDriverError(
                method='create_l2_gateway_connection_postcommit')
        try:
            # Update neutron's database with the mappings.
            nsx_db.add_l2gw_connection_mapping(
                session=context.session,
                connection_id=gw_connection['id'],
                bridge_endpoint_id=bridge_endpoint['id'],
                port_id=port['id'])
        except db_exc.DBError:
            with excutils.save_and_reraise_exception():
                LOG.exception(_LE("Unable to add L2 gateway connection "
                                  "mappings, rolling back changes on neutron"))
                self._core_plugin.nsxlib.delete_bridge_endpoint(
                    bridge_endpoint['id'])
                super(NsxV3Driver,
                      self).delete_l2_gateway_connection(
                          context,
                          gw_connection['id'])
        return gw_connection

    def delete_l2_gateway_connection(self, context, gw_connection):
        pass

    def delete_l2_gateway_connection_precommit(self, context, gw_connection):
        pass

    def delete_l2_gateway_connection_postcommit(self, context, gw_connection):
        """Delete a L2 gateway connection."""
        conn_mapping = nsx_db.get_l2gw_connection_mapping(
            session=context.session,
            connection_id=gw_connection)
        bridge_endpoint_id = conn_mapping.get('bridge_endpoint_id')
        # Delete the logical port from the bridge endpoint.
        self._core_plugin.delete_port(context=context,
                                      port_id=conn_mapping.get('port_id'),
                                      l2gw_port_check=False)
        try:
            self._core_plugin.nsxlib.delete_bridge_endpoint(bridge_endpoint_id)
        except nsxlib_exc.ManagerError as e:
            LOG.exception(_LE("Unable to delete bridge endpoint %(id)s on the "
                              "backend due to exc: %(exc)s"),
                          {'id': bridge_endpoint_id, 'exc': e})
            raise l2gw_exc.L2GatewayServiceDriverError(
                method='delete_l2_gateway_connection_postcommit')

    def prevent_l2gw_port_deletion(self, context, port_id):
        """Prevent core plugin from deleting L2 gateway port."""
        try:
            port = self._core_plugin.get_port(context, port_id)
        except n_exc.PortNotFound:
            return
        if port['device_owner'] == nsx_constants.BRIDGE_ENDPOINT:
            reason = _("has device owner %s") % port['device_owner']
            raise n_exc.ServicePortInUse(port_id=port_id, reason=reason)
