# Copyright 2014 IBM Corp.
#
# 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 functools

from oslo_config import cfg
from oslo_log import log as logging
import oslo_messaging
from oslo_utils import excutils

from neutron.common import constants as n_const
from neutron.common import exceptions as n_exc
from neutron.common import rpc as n_rpc
from neutron.common import topics
from neutron.db import agents_db
from neutron.db import db_base_plugin_v2
from neutron.db import external_net_db
from neutron.db import l3_gwmode_db
from neutron.db import portbindings_db
from neutron.db import quota_db  # noqa
from neutron.extensions import portbindings
from neutron.i18n import _LE, _LI, _LW
from neutron.plugins.ibm.common import config  # noqa
from neutron.plugins.ibm.common import constants
from neutron.plugins.ibm.common import exceptions as sdnve_exc
from neutron.plugins.ibm import sdnve_api as sdnve
from neutron.plugins.ibm import sdnve_api_fake as sdnve_fake

LOG = logging.getLogger(__name__)


class SdnveRpcCallbacks(object):

    def __init__(self, notifier):
        self.notifier = notifier  # used to notify the agent

    def sdnve_info(self, rpc_context, **kwargs):
        '''Update new information.'''
        info = kwargs.get('info')
        # Notify all other listening agents
        self.notifier.info_update(rpc_context, info)
        return info


class AgentNotifierApi(object):
    '''Agent side of the SDN-VE rpc API.'''

    def __init__(self, topic):
        target = oslo_messaging.Target(topic=topic, version='1.0')
        self.client = n_rpc.get_client(target)
        self.topic_info_update = topics.get_topic_name(topic,
                                                       constants.INFO,
                                                       topics.UPDATE)

    def info_update(self, context, info):
        cctxt = self.client.prepare(topic=self.topic_info_update, fanout=True)
        cctxt.cast(context, 'info_update', info=info)


def _ha(func):
    '''Supports the high availability feature of the controller.'''

    @functools.wraps(func)
    def hawrapper(self, *args, **kwargs):
        '''This wrapper sets the new controller if necessary

        When a controller is detected to be not responding, and a
        new controller is chosen to be used in its place, this decorator
        makes sure the existing integration bridges are set to point
        to the new controller by calling the set_controller method.
        '''
        ret_func = func(self, *args, **kwargs)
        self.set_controller(args[0])
        return ret_func
    return hawrapper


class SdnvePluginV2(db_base_plugin_v2.NeutronDbPluginV2,
                    external_net_db.External_net_db_mixin,
                    portbindings_db.PortBindingMixin,
                    l3_gwmode_db.L3_NAT_db_mixin,
                    agents_db.AgentDbMixin,
                    ):

    '''
    Implement the Neutron abstractions using SDN-VE SDN Controller.
    '''

    __native_bulk_support = False
    __native_pagination_support = False
    __native_sorting_support = False

    supported_extension_aliases = ["binding", "router", "external-net",
                                   "agent", "quotas"]

    def __init__(self, configfile=None):
        self.base_binding_dict = {
            portbindings.VIF_TYPE: portbindings.VIF_TYPE_OVS,
            portbindings.VIF_DETAILS: {portbindings.CAP_PORT_FILTER: False}}

        super(SdnvePluginV2, self).__init__()
        self.setup_rpc()
        self.sdnve_controller_select()
        if self.fake_controller:
            self.sdnve_client = sdnve_fake.FakeClient()
        else:
            self.sdnve_client = sdnve.Client()

    def sdnve_controller_select(self):
        self.fake_controller = cfg.CONF.SDNVE.use_fake_controller

    def setup_rpc(self):
        # RPC support
        self.topic = topics.PLUGIN
        self.conn = n_rpc.create_connection(new=True)
        self.notifier = AgentNotifierApi(topics.AGENT)
        self.endpoints = [SdnveRpcCallbacks(self.notifier),
                          agents_db.AgentExtRpcCallback()]
        self.conn.create_consumer(self.topic, self.endpoints,
                                  fanout=False)
        # Consume from all consumers in threads
        self.conn.consume_in_threads()

    def _update_base_binding_dict(self, tenant_type):
        if tenant_type == constants.TENANT_TYPE_OVERLAY:
            self.base_binding_dict[
                portbindings.VIF_TYPE] = portbindings.VIF_TYPE_BRIDGE
        if tenant_type == constants.TENANT_TYPE_OF:
            self.base_binding_dict[
                portbindings.VIF_TYPE] = portbindings.VIF_TYPE_OVS

    def set_controller(self, context):
        LOG.info(_LI("Set a new controller if needed."))
        new_controller = self.sdnve_client.sdnve_get_controller()
        if new_controller:
            self.notifier.info_update(
                context,
                {'new_controller': new_controller})
            LOG.info(_LI("Set the controller to a new controller: %s"),
                     new_controller)

    def _process_request(self, request, current):
        new_request = dict(
            (k, v) for k, v in request.items()
            if v != current.get(k))

        msg = _("Original SDN-VE HTTP request: %(orig)s; New request: %(new)s")
        LOG.debug(msg, {'orig': request, 'new': new_request})
        return new_request

    #
    # Network
    #

    @_ha
    def create_network(self, context, network):
        LOG.debug("Create network in progress: %r", network)
        session = context.session

        tenant_id = self._get_tenant_id_for_create(context, network['network'])
        # Create a new SDN-VE tenant if need be
        sdnve_tenant = self.sdnve_client.sdnve_check_and_create_tenant(
            tenant_id)
        if sdnve_tenant is None:
            raise sdnve_exc.SdnveException(
                msg=_('Create net failed: no SDN-VE tenant.'))

        with session.begin(subtransactions=True):
            net = super(SdnvePluginV2, self).create_network(context, network)
            self._process_l3_create(context, net, network['network'])

        # Create SDN-VE network
        (res, data) = self.sdnve_client.sdnve_create('network', net)
        if res not in constants.HTTP_ACCEPTABLE:
            super(SdnvePluginV2, self).delete_network(context, net['id'])
            raise sdnve_exc.SdnveException(
                msg=(_('Create net failed in SDN-VE: %s') % res))

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

    @_ha
    def update_network(self, context, id, network):
        LOG.debug("Update network in progress: %r", network)
        session = context.session

        processed_request = {}
        with session.begin(subtransactions=True):
            original_network = super(SdnvePluginV2, self).get_network(
                context, id)
            processed_request['network'] = self._process_request(
                network['network'], original_network)
            net = super(SdnvePluginV2, self).update_network(
                context, id, network)
            self._process_l3_update(context, net, network['network'])

        if processed_request['network']:
            (res, data) = self.sdnve_client.sdnve_update(
                'network', id, processed_request['network'])
            if res not in constants.HTTP_ACCEPTABLE:
                net = super(SdnvePluginV2, self).update_network(
                    context, id, {'network': original_network})
                raise sdnve_exc.SdnveException(
                    msg=(_('Update net failed in SDN-VE: %s') % res))

        return net

    @_ha
    def delete_network(self, context, id):
        LOG.debug("Delete network in progress: %s", id)
        session = context.session

        with session.begin(subtransactions=True):
            self._process_l3_delete(context, id)
            super(SdnvePluginV2, self).delete_network(context, id)

        (res, data) = self.sdnve_client.sdnve_delete('network', id)
        if res not in constants.HTTP_ACCEPTABLE:
            LOG.error(
                _LE("Delete net failed after deleting the network in DB: %s"),
                res)

    @_ha
    def get_network(self, context, id, fields=None):
        LOG.debug("Get network in progress: %s", id)
        return super(SdnvePluginV2, self).get_network(context, id, fields)

    @_ha
    def get_networks(self, context, filters=None, fields=None, sorts=None,
                     limit=None, marker=None, page_reverse=False):
        LOG.debug("Get networks in progress")
        return super(SdnvePluginV2, self).get_networks(
            context, filters, fields, sorts, limit, marker, page_reverse)

    #
    # Port
    #

    @_ha
    def create_port(self, context, port):
        LOG.debug("Create port in progress: %r", port)
        session = context.session

        # Set port status as 'ACTIVE' to avoid needing the agent
        port['port']['status'] = n_const.PORT_STATUS_ACTIVE
        port_data = port['port']

        with session.begin(subtransactions=True):
            port = super(SdnvePluginV2, self).create_port(context, port)
            if 'id' not in port:
                return port
            # If the tenant_id is set to '' by create_port, add the id to
            # the request being sent to the controller as the controller
            # requires a tenant id
            tenant_id = port.get('tenant_id')
            if not tenant_id:
                LOG.debug("Create port does not have tenant id info")
                original_network = super(SdnvePluginV2, self).get_network(
                    context, port['network_id'])
                original_tenant_id = original_network['tenant_id']
                port['tenant_id'] = original_tenant_id
                LOG.debug(
                    "Create port does not have tenant id info; "
                    "obtained is: %s",
                    port['tenant_id'])

            os_tenant_id = tenant_id
            id_na, tenant_type = self.sdnve_client.sdnve_get_tenant_byid(
                os_tenant_id)
            self._update_base_binding_dict(tenant_type)
            self._process_portbindings_create_and_update(context,
                                                         port_data, port)

        # NOTE(mb): Remove this block when controller is updated
        # Remove the information that the controller does not accept
        sdnve_port = port.copy()
        sdnve_port.pop('device_id', None)
        sdnve_port.pop('device_owner', None)

        (res, data) = self.sdnve_client.sdnve_create('port', sdnve_port)
        if res not in constants.HTTP_ACCEPTABLE:
            super(SdnvePluginV2, self).delete_port(context, port['id'])
            raise sdnve_exc.SdnveException(
                msg=(_('Create port failed in SDN-VE: %s') % res))

        LOG.debug("Created port: %s", port.get('id', 'id not found'))
        return port

    @_ha
    def update_port(self, context, id, port):
        LOG.debug("Update port in progress: %r", port)
        session = context.session

        processed_request = {}
        with session.begin(subtransactions=True):
            original_port = super(SdnvePluginV2, self).get_port(
                context, id)
            processed_request['port'] = self._process_request(
                port['port'], original_port)
            updated_port = super(SdnvePluginV2, self).update_port(
                context, id, port)

            os_tenant_id = updated_port['tenant_id']
            id_na, tenant_type = self.sdnve_client.sdnve_get_tenant_byid(
                os_tenant_id)
            self._update_base_binding_dict(tenant_type)
            self._process_portbindings_create_and_update(context,
                                                         port['port'],
                                                         updated_port)

        if processed_request['port']:
            (res, data) = self.sdnve_client.sdnve_update(
                'port', id, processed_request['port'])
            if res not in constants.HTTP_ACCEPTABLE:
                updated_port = super(SdnvePluginV2, self).update_port(
                    context, id, {'port': original_port})
                raise sdnve_exc.SdnveException(
                    msg=(_('Update port failed in SDN-VE: %s') % res))

        return updated_port

    @_ha
    def delete_port(self, context, id, l3_port_check=True):
        LOG.debug("Delete port in progress: %s", id)

        # if needed, check to see if this is a port owned by
        # an l3-router.  If so, we should prevent deletion.
        if l3_port_check:
            self.prevent_l3_port_deletion(context, id)
        self.disassociate_floatingips(context, id)

        super(SdnvePluginV2, self).delete_port(context, id)

        (res, data) = self.sdnve_client.sdnve_delete('port', id)
        if res not in constants.HTTP_ACCEPTABLE:
            LOG.error(
                _LE("Delete port operation failed in SDN-VE "
                    "after deleting the port from DB: %s"), res)

    #
    # Subnet
    #

    @_ha
    def create_subnet(self, context, subnet):
        LOG.debug("Create subnet in progress: %r", subnet)
        new_subnet = super(SdnvePluginV2, self).create_subnet(context, subnet)

        # Note(mb): Use of null string currently required by controller
        sdnve_subnet = new_subnet.copy()
        if subnet.get('gateway_ip') is None:
            sdnve_subnet['gateway_ip'] = 'null'
        (res, data) = self.sdnve_client.sdnve_create('subnet', sdnve_subnet)
        if res not in constants.HTTP_ACCEPTABLE:
            super(SdnvePluginV2, self).delete_subnet(context,
                                                     new_subnet['id'])
            raise sdnve_exc.SdnveException(
                msg=(_('Create subnet failed in SDN-VE: %s') % res))

        LOG.debug("Subnet created: %s", new_subnet['id'])

        return new_subnet

    @_ha
    def update_subnet(self, context, id, subnet):
        LOG.debug("Update subnet in progress: %r", subnet)
        session = context.session

        processed_request = {}
        with session.begin(subtransactions=True):
            original_subnet = super(SdnvePluginV2, self).get_subnet(
                context, id)
            processed_request['subnet'] = self._process_request(
                subnet['subnet'], original_subnet)
            updated_subnet = super(SdnvePluginV2, self).update_subnet(
                context, id, subnet)

        if processed_request['subnet']:
            # Note(mb): Use of string containing null required by controller
            if 'gateway_ip' in processed_request['subnet']:
                if processed_request['subnet'].get('gateway_ip') is None:
                    processed_request['subnet']['gateway_ip'] = 'null'
            (res, data) = self.sdnve_client.sdnve_update(
                'subnet', id, processed_request['subnet'])
            if res not in constants.HTTP_ACCEPTABLE:
                for key in subnet['subnet'].keys():
                    subnet['subnet'][key] = original_subnet[key]
                super(SdnvePluginV2, self).update_subnet(
                    context, id, subnet)
                raise sdnve_exc.SdnveException(
                    msg=(_('Update subnet failed in SDN-VE: %s') % res))

        return updated_subnet

    @_ha
    def delete_subnet(self, context, id):
        LOG.debug("Delete subnet in progress: %s", id)
        super(SdnvePluginV2, self).delete_subnet(context, id)

        (res, data) = self.sdnve_client.sdnve_delete('subnet', id)
        if res not in constants.HTTP_ACCEPTABLE:
            LOG.error(_LE("Delete subnet operation failed in SDN-VE after "
                          "deleting the subnet from DB: %s"), res)

    #
    # Router
    #

    @_ha
    def create_router(self, context, router):
        LOG.debug("Create router in progress: %r", router)

        if router['router']['admin_state_up'] is False:
            LOG.warning(_LW('Ignoring admin_state_up=False for router=%r.  '
                            'Overriding with True'), router)
            router['router']['admin_state_up'] = True

        tenant_id = self._get_tenant_id_for_create(context, router['router'])
        # Create a new SDN-VE tenant if need be
        sdnve_tenant = self.sdnve_client.sdnve_check_and_create_tenant(
            tenant_id)
        if sdnve_tenant is None:
            raise sdnve_exc.SdnveException(
                msg=_('Create router failed: no SDN-VE tenant.'))

        new_router = super(SdnvePluginV2, self).create_router(context, router)
        # Create SDN-VE router
        (res, data) = self.sdnve_client.sdnve_create('router', new_router)
        if res not in constants.HTTP_ACCEPTABLE:
            super(SdnvePluginV2, self).delete_router(context, new_router['id'])
            raise sdnve_exc.SdnveException(
                msg=(_('Create router failed in SDN-VE: %s') % res))

        LOG.debug("Router created: %r", new_router)
        return new_router

    @_ha
    def update_router(self, context, id, router):
        LOG.debug("Update router in progress: id=%(id)s "
                  "router=%(router)r",
                  {'id': id, 'router': router})
        session = context.session

        processed_request = {}
        if not router['router'].get('admin_state_up', True):
            raise n_exc.NotImplementedError(_('admin_state_up=False '
                                              'routers are not '
                                              'supported.'))

        with session.begin(subtransactions=True):
            original_router = super(SdnvePluginV2, self).get_router(
                context, id)
            processed_request['router'] = self._process_request(
                router['router'], original_router)
            updated_router = super(SdnvePluginV2, self).update_router(
                context, id, router)

        if processed_request['router']:
            egw = processed_request['router'].get('external_gateway_info')
            # Check for existing empty set (different from None) in request
            if egw == {}:
                processed_request['router'][
                    'external_gateway_info'] = {'network_id': 'null'}
            (res, data) = self.sdnve_client.sdnve_update(
                'router', id, processed_request['router'])
            if res not in constants.HTTP_ACCEPTABLE:
                super(SdnvePluginV2, self).update_router(
                    context, id, {'router': original_router})
                raise sdnve_exc.SdnveException(
                    msg=(_('Update router failed in SDN-VE: %s') % res))

        return updated_router

    @_ha
    def delete_router(self, context, id):
        LOG.debug("Delete router in progress: %s", id)

        super(SdnvePluginV2, self).delete_router(context, id)

        (res, data) = self.sdnve_client.sdnve_delete('router', id)
        if res not in constants.HTTP_ACCEPTABLE:
            LOG.error(
                _LE("Delete router operation failed in SDN-VE after "
                    "deleting the router in DB: %s"), res)

    @_ha
    def add_router_interface(self, context, router_id, interface_info):
        LOG.debug("Add router interface in progress: "
                  "router_id=%(router_id)s "
                  "interface_info=%(interface_info)r",
                  {'router_id': router_id, 'interface_info': interface_info})

        new_interface = super(SdnvePluginV2, self).add_router_interface(
            context, router_id, interface_info)
        LOG.debug(
            "SdnvePluginV2.add_router_interface called. Port info: %s",
            new_interface)
        request_info = interface_info.copy()
        request_info['port_id'] = new_interface['port_id']
        # Add the subnet_id to the request sent to the controller
        if 'subnet_id' not in interface_info:
            request_info['subnet_id'] = new_interface['subnet_id']

        (res, data) = self.sdnve_client.sdnve_update(
            'router', router_id + '/add_router_interface', request_info)
        if res not in constants.HTTP_ACCEPTABLE:
            super(SdnvePluginV2, self).remove_router_interface(
                context, router_id, interface_info)
            raise sdnve_exc.SdnveException(
                msg=(_('Update router-add-interface failed in SDN-VE: %s') %
                     res))

        LOG.debug("Added router interface: %r", new_interface)
        return new_interface

    def _add_router_interface_only(self, context, router_id, interface_info):
        LOG.debug("Add router interface only called: "
                  "router_id=%(router_id)s "
                  "interface_info=%(interface_info)r",
                  {'router_id': router_id, 'interface_info': interface_info})

        port_id = interface_info.get('port_id')
        if port_id:
            (res, data) = self.sdnve_client.sdnve_update(
                'router', router_id + '/add_router_interface', interface_info)
            if res not in constants.HTTP_ACCEPTABLE:
                LOG.error(_LE("SdnvePluginV2._add_router_interface_only: "
                              "failed to add the interface in the roll back."
                              " of a remove_router_interface operation"))

    @_ha
    def remove_router_interface(self, context, router_id, interface_info):
        LOG.debug("Remove router interface in progress: "
                  "router_id=%(router_id)s "
                  "interface_info=%(interface_info)r",
                  {'router_id': router_id, 'interface_info': interface_info})

        subnet_id = interface_info.get('subnet_id')
        port_id = interface_info.get('port_id')
        if not subnet_id:
            if not port_id:
                raise sdnve_exc.BadInputException(msg=_('No port ID'))
            myport = super(SdnvePluginV2, self).get_port(context, port_id)
            LOG.debug("SdnvePluginV2.remove_router_interface port: %s",
                      myport)
            myfixed_ips = myport.get('fixed_ips')
            if not myfixed_ips:
                raise sdnve_exc.BadInputException(msg=_('No fixed IP'))
            subnet_id = myfixed_ips[0].get('subnet_id')
            if subnet_id:
                interface_info['subnet_id'] = subnet_id
                LOG.debug(
                    "SdnvePluginV2.remove_router_interface subnet_id: %s",
                    subnet_id)
        else:
            if not port_id:
                # The backend requires port id info in the request
                subnet = super(SdnvePluginV2, self).get_subnet(context,
                                                               subnet_id)
                df = {'device_id': [router_id],
                      'device_owner': [n_const.DEVICE_OWNER_ROUTER_INTF],
                      'network_id': [subnet['network_id']]}
                ports = self.get_ports(context, filters=df)
                if ports:
                    pid = ports[0]['id']
                    interface_info['port_id'] = pid
                    msg = ("SdnvePluginV2.remove_router_interface "
                           "subnet_id: %(sid)s  port_id: %(pid)s")
                    LOG.debug(msg, {'sid': subnet_id, 'pid': pid})

        (res, data) = self.sdnve_client.sdnve_update(
            'router', router_id + '/remove_router_interface', interface_info)

        if res not in constants.HTTP_ACCEPTABLE:
            raise sdnve_exc.SdnveException(
                msg=(_('Update router-remove-interface failed SDN-VE: %s') %
                     res))

        session = context.session
        with session.begin(subtransactions=True):
            try:
                info = super(SdnvePluginV2, self).remove_router_interface(
                    context, router_id, interface_info)
            except Exception:
                with excutils.save_and_reraise_exception():
                    self._add_router_interface_only(context,
                                                    router_id, interface_info)

        return info

    #
    # Floating Ip
    #

    @_ha
    def create_floatingip(self, context, floatingip):
        LOG.debug("Create floatingip in progress: %r",
                  floatingip)
        new_floatingip = super(SdnvePluginV2, self).create_floatingip(
            context, floatingip)

        (res, data) = self.sdnve_client.sdnve_create(
            'floatingip', {'floatingip': new_floatingip})
        if res not in constants.HTTP_ACCEPTABLE:
            super(SdnvePluginV2, self).delete_floatingip(
                context, new_floatingip['id'])
            raise sdnve_exc.SdnveException(
                msg=(_('Creating floating ip operation failed '
                       'in SDN-VE controller: %s') % res))

        LOG.debug("Created floatingip : %r", new_floatingip)
        return new_floatingip

    @_ha
    def update_floatingip(self, context, id, floatingip):
        LOG.debug("Update floatingip in progress: %r", floatingip)
        session = context.session

        processed_request = {}
        with session.begin(subtransactions=True):
            original_floatingip = super(
                SdnvePluginV2, self).get_floatingip(context, id)
            processed_request['floatingip'] = self._process_request(
                floatingip['floatingip'], original_floatingip)
            updated_floatingip = super(
                SdnvePluginV2, self).update_floatingip(context, id, floatingip)

        if processed_request['floatingip']:
            (res, data) = self.sdnve_client.sdnve_update(
                'floatingip', id,
                {'floatingip': processed_request['floatingip']})
            if res not in constants.HTTP_ACCEPTABLE:
                super(SdnvePluginV2, self).update_floatingip(
                    context, id, {'floatingip': original_floatingip})
                raise sdnve_exc.SdnveException(
                    msg=(_('Update floating ip failed in SDN-VE: %s') % res))

        return updated_floatingip

    @_ha
    def delete_floatingip(self, context, id):
        LOG.debug("Delete floatingip in progress: %s", id)
        super(SdnvePluginV2, self).delete_floatingip(context, id)

        (res, data) = self.sdnve_client.sdnve_delete('floatingip', id)
        if res not in constants.HTTP_ACCEPTABLE:
            LOG.error(_LE("Delete floatingip failed in SDN-VE: %s"), res)
