# vim: tabstop=4 shiftwidth=4 softtabstop=4
#
# Copyright 2013 VMware, Inc
#
#    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.
#
# @author: Leon Cui, VMware

from neutron.openstack.common import excutils
from neutron.openstack.common import log as logging
from neutron.plugins.vmware.dbexts import vcns_db
from neutron.plugins.vmware.vshield.common import (
    constants as vcns_const)
from neutron.plugins.vmware.vshield.common import (
    exceptions as vcns_exc)
from neutron.services.loadbalancer import constants as lb_constants

LOG = logging.getLogger(__name__)

BALANCE_MAP = {
    lb_constants.LB_METHOD_ROUND_ROBIN: 'round-robin',
    lb_constants.LB_METHOD_LEAST_CONNECTIONS: 'leastconn',
    lb_constants.LB_METHOD_SOURCE_IP: 'source'
}
PROTOCOL_MAP = {
    lb_constants.PROTOCOL_TCP: 'tcp',
    lb_constants.PROTOCOL_HTTP: 'http',
    lb_constants.PROTOCOL_HTTPS: 'tcp'
}
SESSION_PERSISTENCE_METHOD_MAP = {
    lb_constants.SESSION_PERSISTENCE_SOURCE_IP: 'sourceip',
    lb_constants.SESSION_PERSISTENCE_APP_COOKIE: 'cookie',
    lb_constants.SESSION_PERSISTENCE_HTTP_COOKIE: 'cookie'}
SESSION_PERSISTENCE_COOKIE_MAP = {
    lb_constants.SESSION_PERSISTENCE_APP_COOKIE: 'app',
    lb_constants.SESSION_PERSISTENCE_HTTP_COOKIE: 'insert'}


class EdgeLbDriver():
    """Implementation of driver APIs for
       Edge Loadbalancer feature configuration
    """

    def _convert_lb_vip(self, context, edge_id, vip, app_profileid):
        pool_id = vip.get('pool_id')
        poolid_map = vcns_db.get_vcns_edge_pool_binding(
            context.session, pool_id, edge_id)
        pool_vseid = poolid_map['pool_vseid']
        return {
            'name': vip.get(
                'name', '') + vip['id'][-vcns_const.SUFFIX_LENGTH:],
            'description': vip.get('description'),
            'ipAddress': vip.get('address'),
            'protocol': vip.get('protocol'),
            'port': vip.get('protocol_port'),
            'connectionLimit': max(0, vip.get('connection_limit')),
            'defaultPoolId': pool_vseid,
            'applicationProfileId': app_profileid
        }

    def _restore_lb_vip(self, context, edge_id, vip_vse):
        pool_binding = vcns_db.get_vcns_edge_pool_binding_by_vseid(
            context.session,
            edge_id,
            vip_vse['defaultPoolId'])

        return {
            'name': vip_vse['name'][:-vcns_const.SUFFIX_LENGTH],
            'address': vip_vse['ipAddress'],
            'protocol': vip_vse['protocol'],
            'protocol_port': vip_vse['port'],
            'pool_id': pool_binding['pool_id']
        }

    def _convert_lb_pool(self, context, edge_id, pool, members):
        vsepool = {
            'name': pool.get(
                'name', '') + pool['id'][-vcns_const.SUFFIX_LENGTH:],
            'description': pool.get('description'),
            'algorithm': BALANCE_MAP.get(
                pool.get('lb_method'),
                'round-robin'),
            'transparent': True,
            'member': [],
            'monitorId': []
        }
        for member in members:
            vsepool['member'].append({
                'ipAddress': member['address'],
                'weight': member['weight'],
                'port': member['protocol_port']
            })
        ##TODO(linb) right now, vse only accept at most one monitor per pool
        monitors = pool.get('health_monitors')
        if not monitors:
            return vsepool
        monitorid_map = vcns_db.get_vcns_edge_monitor_binding(
            context.session,
            monitors[0],
            edge_id)
        vsepool['monitorId'].append(monitorid_map['monitor_vseid'])
        return vsepool

    def _restore_lb_pool(self, context, edge_id, pool_vse):
        #TODO(linb): Get more usefule info
        return {
            'name': pool_vse['name'][:-vcns_const.SUFFIX_LENGTH],
        }

    def _convert_lb_monitor(self, context, monitor):
        return {
            'type': PROTOCOL_MAP.get(
                monitor.get('type'), 'http'),
            'interval': monitor.get('delay'),
            'timeout': monitor.get('timeout'),
            'maxRetries': monitor.get('max_retries'),
            'name': monitor.get('id')
        }

    def _restore_lb_monitor(self, context, edge_id, monitor_vse):
        return {
            'delay': monitor_vse['interval'],
            'timeout': monitor_vse['timeout'],
            'max_retries': monitor_vse['maxRetries'],
            'id': monitor_vse['name']
        }

    def _convert_app_profile(self, name, sess_persist, protocol):
        vcns_app_profile = {
            'insertXForwardedFor': False,
            'name': name,
            'serverSslEnabled': False,
            'sslPassthrough': False,
            'template': protocol,
        }
        # Since SSL Termination is not supported right now, so just use
        # sslPassthrough mehtod if the protocol is HTTPS.
        if protocol == lb_constants.PROTOCOL_HTTPS:
            vcns_app_profile['sslPassthrough'] = True

        if sess_persist.get('type'):
            # If protocol is not HTTP, only sourceip is supported
            if (protocol != lb_constants.PROTOCOL_HTTP and
                sess_persist['type'] != (
                    lb_constants.SESSION_PERSISTENCE_SOURCE_IP)):
                msg = (_("Invalid %(protocol)s persistence method: %(type)s") %
                       {'protocol': protocol,
                        'type': sess_persist['type']})
                raise vcns_exc.VcnsBadRequest(resource='sess_persist', msg=msg)
            persistence = {
                'method': SESSION_PERSISTENCE_METHOD_MAP.get(
                    sess_persist['type'])}
            if sess_persist['type'] in SESSION_PERSISTENCE_COOKIE_MAP:
                if sess_persist.get('cookie_name'):
                    persistence['cookieName'] = sess_persist['cookie_name']
                else:
                    persistence['cookieName'] = 'default_cookie_name'
                persistence['cookieMode'] = SESSION_PERSISTENCE_COOKIE_MAP.get(
                    sess_persist['type'])
            vcns_app_profile['persistence'] = persistence
        return vcns_app_profile

    def create_vip(self, context, edge_id, vip):
        app_profile = self._convert_app_profile(
            vip['name'], (vip.get('session_persistence') or {}),
            vip.get('protocol'))
        try:
            header, response = self.vcns.create_app_profile(
                edge_id, app_profile)
        except vcns_exc.VcnsApiException:
            with excutils.save_and_reraise_exception():
                LOG.exception(_("Failed to create app profile on edge: %s"),
                              edge_id)
        objuri = header['location']
        app_profileid = objuri[objuri.rfind("/") + 1:]

        vip_new = self._convert_lb_vip(context, edge_id, vip, app_profileid)
        try:
            header, response = self.vcns.create_vip(
                edge_id, vip_new)
        except vcns_exc.VcnsApiException:
            with excutils.save_and_reraise_exception():
                LOG.exception(_("Failed to create vip on vshield edge: %s"),
                              edge_id)
                self.vcns.delete_app_profile(edge_id, app_profileid)
        objuri = header['location']
        vip_vseid = objuri[objuri.rfind("/") + 1:]

        # Add the vip mapping
        map_info = {
            "vip_id": vip['id'],
            "vip_vseid": vip_vseid,
            "edge_id": edge_id,
            "app_profileid": app_profileid
        }
        vcns_db.add_vcns_edge_vip_binding(context.session, map_info)

    def _get_vip_binding(self, session, id):
        vip_binding = vcns_db.get_vcns_edge_vip_binding(session, id)
        if not vip_binding:
            msg = (_("vip_binding not found with id: %(id)s "
                     "edge_id: %(edge_id)s") % {
                   'id': id,
                   'edge_id': vip_binding[vcns_const.EDGE_ID]})
            LOG.error(msg)
            raise vcns_exc.VcnsNotFound(
                resource='router_service_binding', msg=msg)
        return vip_binding

    def get_vip(self, context, id):
        vip_binding = vcns_db.get_vcns_edge_vip_binding(context.session, id)
        edge_id = vip_binding[vcns_const.EDGE_ID]
        vip_vseid = vip_binding['vip_vseid']
        try:
            response = self.vcns.get_vip(edge_id, vip_vseid)[1]
        except vcns_exc.VcnsApiException:
            with excutils.save_and_reraise_exception():
                LOG.exception(_("Failed to get vip on edge"))
        return self._restore_lb_vip(context, edge_id, response)

    def update_vip(self, context, vip, session_persistence_update=True):
        vip_binding = self._get_vip_binding(context.session, vip['id'])
        edge_id = vip_binding[vcns_const.EDGE_ID]
        vip_vseid = vip_binding.get('vip_vseid')
        if session_persistence_update:
            app_profileid = vip_binding.get('app_profileid')
            app_profile = self._convert_app_profile(
                vip['name'], vip.get('session_persistence', {}),
                vip.get('protocol'))
            try:
                self.vcns.update_app_profile(
                    edge_id, app_profileid, app_profile)
            except vcns_exc.VcnsApiException:
                with excutils.save_and_reraise_exception():
                    LOG.exception(_("Failed to update app profile on "
                                    "edge: %s") % edge_id)

        vip_new = self._convert_lb_vip(context, edge_id, vip, app_profileid)
        try:
            self.vcns.update_vip(edge_id, vip_vseid, vip_new)
        except vcns_exc.VcnsApiException:
            with excutils.save_and_reraise_exception():
                LOG.exception(_("Failed to update vip on edge: %s") % edge_id)

    def delete_vip(self, context, id):
        vip_binding = self._get_vip_binding(context.session, id)
        edge_id = vip_binding[vcns_const.EDGE_ID]
        vip_vseid = vip_binding['vip_vseid']
        app_profileid = vip_binding['app_profileid']

        try:
            self.vcns.delete_vip(edge_id, vip_vseid)
        except vcns_exc.ResourceNotFound:
            LOG.exception(_("vip not found on edge: %s") % edge_id)
        except vcns_exc.VcnsApiException:
            with excutils.save_and_reraise_exception():
                LOG.exception(_("Failed to delete vip on edge: %s") % edge_id)

        try:
            self.vcns.delete_app_profile(edge_id, app_profileid)
        except vcns_exc.ResourceNotFound:
            LOG.exception(_("app profile not found on edge: %s") % edge_id)
        except vcns_exc.VcnsApiException:
            with excutils.save_and_reraise_exception():
                LOG.exception(_("Failed to delete app profile on edge: %s") %
                              edge_id)

        vcns_db.delete_vcns_edge_vip_binding(context.session, id)

    def create_pool(self, context, edge_id, pool, members):
        pool_new = self._convert_lb_pool(context, edge_id, pool, members)
        try:
            header = self.vcns.create_pool(edge_id, pool_new)[0]
        except vcns_exc.VcnsApiException:
            with excutils.save_and_reraise_exception():
                LOG.exception(_("Failed to create pool"))

        objuri = header['location']
        pool_vseid = objuri[objuri.rfind("/") + 1:]

        # update the pool mapping table
        map_info = {
            "pool_id": pool['id'],
            "pool_vseid": pool_vseid,
            "edge_id": edge_id
        }
        vcns_db.add_vcns_edge_pool_binding(context.session, map_info)

    def get_pool(self, context, id, edge_id):
        pool_binding = vcns_db.get_vcns_edge_pool_binding(
            context.session, id, edge_id)
        if not pool_binding:
            msg = (_("pool_binding not found with id: %(id)s "
                     "edge_id: %(edge_id)s") % {'id': id, 'edge_id': edge_id})
            LOG.error(msg)
            raise vcns_exc.VcnsNotFound(
                resource='router_service_binding', msg=msg)
        pool_vseid = pool_binding['pool_vseid']
        try:
            response = self.vcns.get_pool(edge_id, pool_vseid)[1]
        except vcns_exc.VcnsApiException:
            with excutils.save_and_reraise_exception():
                LOG.exception(_("Failed to get pool on edge"))
        return self._restore_lb_pool(context, edge_id, response)

    def update_pool(self, context, edge_id, pool, members):
        pool_binding = vcns_db.get_vcns_edge_pool_binding(
            context.session, pool['id'], edge_id)
        pool_vseid = pool_binding['pool_vseid']
        pool_new = self._convert_lb_pool(context, edge_id, pool, members)
        try:
            self.vcns.update_pool(edge_id, pool_vseid, pool_new)
        except vcns_exc.VcnsApiException:
            with excutils.save_and_reraise_exception():
                LOG.exception(_("Failed to update pool"))

    def delete_pool(self, context, id, edge_id):
        pool_binding = vcns_db.get_vcns_edge_pool_binding(
            context.session, id, edge_id)
        pool_vseid = pool_binding['pool_vseid']
        try:
            self.vcns.delete_pool(edge_id, pool_vseid)
        except vcns_exc.VcnsApiException:
            with excutils.save_and_reraise_exception():
                LOG.exception(_("Failed to delete pool"))
        vcns_db.delete_vcns_edge_pool_binding(
            context.session, id, edge_id)

    def create_health_monitor(self, context, edge_id, health_monitor):
        monitor_new = self._convert_lb_monitor(context, health_monitor)
        try:
            header = self.vcns.create_health_monitor(edge_id, monitor_new)[0]
        except vcns_exc.VcnsApiException:
            with excutils.save_and_reraise_exception():
                LOG.exception(_("Failed to create monitor on edge: %s"),
                              edge_id)

        objuri = header['location']
        monitor_vseid = objuri[objuri.rfind("/") + 1:]

        # update the health_monitor mapping table
        map_info = {
            "monitor_id": health_monitor['id'],
            "monitor_vseid": monitor_vseid,
            "edge_id": edge_id
        }
        vcns_db.add_vcns_edge_monitor_binding(context.session, map_info)

    def get_health_monitor(self, context, id, edge_id):
        monitor_binding = vcns_db.get_vcns_edge_monitor_binding(
            context.session, id, edge_id)
        if not monitor_binding:
            msg = (_("monitor_binding not found with id: %(id)s "
                     "edge_id: %(edge_id)s") % {'id': id, 'edge_id': edge_id})
            LOG.error(msg)
            raise vcns_exc.VcnsNotFound(
                resource='router_service_binding', msg=msg)
        monitor_vseid = monitor_binding['monitor_vseid']
        try:
            response = self.vcns.get_health_monitor(edge_id, monitor_vseid)[1]
        except vcns_exc.VcnsApiException as e:
            with excutils.save_and_reraise_exception():
                LOG.exception(_("Failed to get monitor on edge: %s"),
                              e.response)
        return self._restore_lb_monitor(context, edge_id, response)

    def update_health_monitor(self, context, edge_id,
                              old_health_monitor, health_monitor):
        monitor_binding = vcns_db.get_vcns_edge_monitor_binding(
            context.session,
            old_health_monitor['id'], edge_id)
        monitor_vseid = monitor_binding['monitor_vseid']
        monitor_new = self._convert_lb_monitor(
            context, health_monitor)
        try:
            self.vcns.update_health_monitor(
                edge_id, monitor_vseid, monitor_new)
        except vcns_exc.VcnsApiException:
            with excutils.save_and_reraise_exception():
                LOG.exception(_("Failed to update monitor on edge: %s"),
                              edge_id)

    def delete_health_monitor(self, context, id, edge_id):
        monitor_binding = vcns_db.get_vcns_edge_monitor_binding(
            context.session, id, edge_id)
        monitor_vseid = monitor_binding['monitor_vseid']
        try:
            self.vcns.delete_health_monitor(edge_id, monitor_vseid)
        except vcns_exc.VcnsApiException:
            with excutils.save_and_reraise_exception():
                LOG.exception(_("Failed to delete monitor"))
        vcns_db.delete_vcns_edge_monitor_binding(
            context.session, id, edge_id)
