# Copyright 2017 OpenStack Foundation
# 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 collections

from neutron_lib.agent import l3_extension
from neutron_lib import constants
from neutron_lib.services.qos import constants as qos_consts
from oslo_concurrency import lockutils
from oslo_log import log as logging

from neutron.agent.l3.extensions.qos import base as qos_base
from neutron.agent.linux import ip_lib
from neutron.api.rpc.callbacks import events
from neutron.api.rpc.callbacks import resources
from neutron.api.rpc.handlers import resources_rpc
from neutron.common import coordination

LOG = logging.getLogger(__name__)

FipResource = collections.namedtuple('FipResource', 'id ip_address')


class RouterFipRateLimitMaps(qos_base.RateLimitMaps):
    LOCK_NAME = "fip-qos-cache"

    def __init__(self):
        """Initialize RouterFipRateLimitMaps

        "_router_2_fips" stores the FIP resources (``FipResource`` tuple) per
        router. The "_fips_2_router" dictionary stores the reverse lookup info.
        _router_2_fips = {router_id_1: {fip_res_1, fip_res_2, ...},
                          router_id_2: {fip_res_3, fip_res_4, ...}, ... }
        _fips_2_router = {fip_res_1: router_id_1,
                          fip_res_2: router_id_1,
                          fip_res_3: router_id_2, ... }
        """
        self._router_2_fips = {}
        self._fips_2_router = {}
        """
        The rate limits dict will be:
            xxx_ratelimits = {
                fip_1: (rate, burst),
                fip_2: (IP_DEFAULT_RATE, IP_DEFAULT_BURST), # default
                fip_3: (1, 2),
                fip_4: (3, 4),
            }
        """
        self.ingress_ratelimits = {}
        self.egress_ratelimits = {}
        super(RouterFipRateLimitMaps, self).__init__(self.LOCK_NAME)

    def get_router_id_by_fip(self, fip_res):

        @lockutils.synchronized(self.lock_name)
        def _find_router_id_by_fip_id():
            return self._fips_2_router.get(fip_res)

        return _find_router_id_by_fip_id()

    def get_fips_by_router_id(self, router_id):

        @lockutils.synchronized(self.lock_name)
        def _get_fips_by_router_id():
            return self._router_2_fips.get(router_id, set([]))

        return _get_fips_by_router_id()

    def set_fips(self, router_id, fips):

        @lockutils.synchronized(self.lock_name)
        def _set_fips():
            self._router_2_fips[router_id] = fips
            for fip_res in fips:
                self._fips_2_router[fip_res] = router_id

        if not isinstance(fips, set):
            fips = set(fips)
        return _set_fips()

    def delete_fips(self, fips):

        @lockutils.synchronized(self.lock_name)
        def _delete_fips():
            router_ids = set([])
            for fip_res in fips:
                router_id = self._fips_2_router.pop(fip_res, None)
                if router_id:
                    router_ids.add(router_id)

            for router_id in router_ids:
                stored_fips = self._router_2_fips.pop(router_id)
                current_fips = stored_fips - set(fips)
                if current_fips:
                    self._router_2_fips[router_id] = current_fips

        if not isinstance(fips, set):
            fips = set(fips)
        return _delete_fips()

    def remove_fip_ratelimit_cache(self, direction, fip_id):

        @lockutils.synchronized(self.lock_name)
        def _remove_fip_ratelimit_cache():
            rate_limits = getattr(self, direction + "_ratelimits")
            rate_limits.pop(fip_id, None)

        _remove_fip_ratelimit_cache()

    def set_fip_ratelimit_cache(self, direction, fip_id, rate, burst):

        @lockutils.synchronized(self.lock_name)
        def _set_fip_ratelimit_cache():
            rate_limits = getattr(self, direction + "_ratelimits")
            rate_limits[fip_id] = (rate, burst)

        _set_fip_ratelimit_cache()

    def get_fip_ratelimit_cache(self, direction, fip_id):

        @lockutils.synchronized(self.lock_name)
        def _get_fip_ratelimit_cache():
            rate_limits = getattr(self, direction + "_ratelimits")
            rate, burst = rate_limits.get(fip_id, (qos_base.IP_DEFAULT_RATE,
                                                   qos_base.IP_DEFAULT_BURST))
            return rate, burst

        return _get_fip_ratelimit_cache()

    def clean_router_all_fip_cache(self, router_id):
        for fip_res in self._router_2_fips.pop(router_id, []):
            self._fips_2_router.pop(fip_res, None)
            for direction in constants.VALID_DIRECTIONS:
                self.remove_fip_ratelimit_cache(direction, fip_res.id)
            self.clean_by_resource(fip_res)


class FipQosAgentExtension(qos_base.L3QosAgentExtensionBase,
                           l3_extension.L3AgentExtension):

    def initialize(self, connection, driver_type):
        """Initialize agent extension."""
        self.resource_rpc = resources_rpc.ResourcesPullRpcApi()
        self.fip_qos_map = RouterFipRateLimitMaps()
        self._register_rpc_consumers()

    def _handle_notification(self, context, resource_type,
                             qos_policies, event_type):
        if event_type == events.UPDATED:
            for qos_policy in qos_policies:
                self._process_update_policy(qos_policy)

    def _process_update_policy(self, qos_policy):
        old_qos_policy = self.fip_qos_map.get_policy(qos_policy.id)
        if not old_qos_policy:
            return

        if self._policy_rules_modified(old_qos_policy, qos_policy):
            for fip_res in self.fip_qos_map.get_resources(qos_policy):
                router_id = self.fip_qos_map.get_router_id_by_fip(fip_res)
                router_info = self._get_router_info(router_id)
                if not router_info:
                    continue
                device = self._get_rate_limit_ip_device(router_info)
                dvr_fip_device = self._get_dvr_fip_device(router_info)
                if not device and not dvr_fip_device:
                    LOG.debug("Router %s does not have a floating IP "
                              "related device, skipping.", router_id)
                    continue
                rates = self.get_policy_rates(qos_policy)
                if device:
                    self.process_ip_rates(fip_res, device, rates)
                if dvr_fip_device:
                    self.process_ip_rates(
                        fip_res, dvr_fip_device, rates, with_cache=False)
        self.fip_qos_map.update_policy(qos_policy)

    @coordination.synchronized('qos-floating-ip-{ip}')
    def process_ip_rate_limit(self, ip, direction,
                              device, rate, burst):

        tc_wrapper = self._get_tc_wrapper(device)

        if (rate == qos_base.IP_DEFAULT_RATE and
                burst == qos_base.IP_DEFAULT_BURST):
            # According to the agreements of default value definition,
            # floating IP bandwidth was changed to default value (no limit).
            # NOTE: l3_tc_lib will ignore exception FilterIDForIPNotFound.
            tc_wrapper.clear_ip_rate_limit(direction, ip)
            self.fip_qos_map.remove_fip_ratelimit_cache(direction, ip)
            return

        # Finally just set it, l3_tc_lib will clean the old rules if exists.
        tc_wrapper.set_ip_rate_limit(direction, ip, rate, burst)

    def _get_rate_limit_ip_device(self, router_info):
        ex_gw_port = router_info.get_ex_gw_port()
        if not ex_gw_port:
            return
        agent_mode = router_info.agent_conf.agent_mode
        is_distributed_router = router_info.router.get('distributed')
        if is_distributed_router and agent_mode == (
                constants.L3_AGENT_MODE_DVR_SNAT):
            # DVR edge (or DVR edge ha) router
            if not router_info._is_this_snat_host():
                return
            name = router_info.get_snat_external_device_interface_name(
                ex_gw_port)
        else:
            # DVR local router
            # Legacy/HA router
            name = router_info.get_external_device_interface_name(ex_gw_port)
        if not name:
            # DVR local router in dvr_no_external agent mode may not have
            # such rfp-device.
            return
        namespace = router_info.get_gw_ns_name()
        return ip_lib.IPDevice(name, namespace=namespace)

    def _remove_fip_rate_limit(self, device, fip_res):
        tc_wrapper = self._get_tc_wrapper(device)
        for direction in constants.VALID_DIRECTIONS:
            if device.exists():
                tc_wrapper.clear_ip_rate_limit(direction, fip_res.ip_address)
            self.fip_qos_map.remove_fip_ratelimit_cache(direction, fip_res.id)

    def get_fip_qos_rates(self, context, fip_res, policy_id):
        if policy_id is None:
            self.fip_qos_map.clean_by_resource(fip_res)
            # process_ip_rate_limit will treat value 0 as
            # cleaning the tc filters if exits or no action.
            return {
                constants.INGRESS_DIRECTION: {
                    "rate": qos_base.IP_DEFAULT_RATE,
                    "burst": qos_base.IP_DEFAULT_BURST},
                constants.EGRESS_DIRECTION: {
                    "rate": qos_base.IP_DEFAULT_RATE,
                    "burst": qos_base.IP_DEFAULT_BURST}}
        policy = self.resource_rpc.pull(
            context, resources.QOS_POLICY, policy_id)
        self.fip_qos_map.set_resource_policy(fip_res, policy)
        return self.get_policy_rates(policy)

    def process_ip_rates(self, fip_res, device, rates, with_cache=True):
        for direction in constants.VALID_DIRECTIONS:
            rate = rates.get(direction)
            if with_cache:

                old_rate, old_burst = self.fip_qos_map.get_fip_ratelimit_cache(
                    direction, fip_res.id)
                if old_rate == rate['rate'] and old_burst == rate['burst']:
                    # Two possibilities here:
                    # 1. Floating IP rate limit does not change.
                    # 2. Floating IP bandwidth does not limit.
                    continue

                self.process_ip_rate_limit(fip_res.ip_address, direction,
                                           device, rate['rate'], rate['burst'])

                self.fip_qos_map.set_fip_ratelimit_cache(
                    direction, fip_res.id, rate['rate'], rate['burst'])
            else:
                tc_wrapper = self._get_tc_wrapper(device)
                if (rate['rate'] == qos_base.IP_DEFAULT_RATE and
                        rate['burst'] == qos_base.IP_DEFAULT_BURST):
                    # Default value is no limit
                    tc_wrapper.clear_ip_rate_limit(direction,
                                                   fip_res.ip_address)
                else:
                    tc_wrapper.set_ip_rate_limit(direction, fip_res.ip_address,
                                                 rate['rate'], rate['burst'])

    def _get_dvr_fip_device(self, router_info):
        is_distributed_router = router_info.router.get('distributed')
        agent_mode = router_info.agent_conf.agent_mode
        if is_distributed_router and agent_mode == (
                constants.L3_AGENT_MODE_DVR_SNAT):
            gw_port = router_info.get_ex_gw_port()
            if gw_port and router_info.fip_ns:
                rfp_dev_name = router_info.get_external_device_interface_name(
                    gw_port)
                if router_info.router_namespace.exists() and rfp_dev_name:
                    return ip_lib.IPDevice(
                        rfp_dev_name, namespace=router_info.ns_name)

    def process_floating_ip_addresses(self, context, router_info):
        # Loop all the router floating ips, the corresponding floating IP tc
        # rules will be configured:
        # 1. for legacy and HA router, it will be all floating IPs to qg-device
        #    of qrouter-namespace in (all ha router hosted) network node.
        # 2. for dvr router, we can do this simple. No matter the agent
        #    type is dvr or dvr_snat, we can just set all the
        #    floating IP tc rules to the corresponding device:
        #    2.1 for dvr local router in compute node:
        #        the namespace is qrouter-x, and the device is rfp-device.
        #    2.2 for dvr edge (ha) router in network node:
        #        the namespace is snat-x, and the device is qg-device.
        # 3. for dvr local router, if agent_mod is dvr_no_external, no
        #    floating IP rules will be configured.
        # 4. for dvr router in snat node, we should process the floating
        #    IP QoS again in qrouter-namespace to cover the mixed deployment
        #    with nova-compute scenario.
        is_distributed_router = router_info.router.get('distributed')
        agent_mode = router_info.agent_conf.agent_mode
        LOG.debug("Start processing floating IP QoS for "
                  "router %(router_id)s, router "
                  "distributed: %(distributed)s, "
                  "agent mode: %(agent_mode)s",
                  {"router_id": router_info.router_id,
                   "distributed": is_distributed_router,
                   "agent_mode": agent_mode})
        if is_distributed_router and agent_mode == (
                constants.L3_AGENT_MODE_DVR_NO_EXTERNAL):
            # condition 3: dvr local router and dvr_no_external agent
            return

        device = self._get_rate_limit_ip_device(router_info)
        dvr_fip_device = self._get_dvr_fip_device(router_info)
        if not device and not dvr_fip_device:
            LOG.debug("No relevant QoS device found "
                      "for router: %s", router_info.router_id)
            return

        floating_ips = (router_info.get_floating_ips() +
                        router_info.get_port_forwarding_fips())
        current_fips = self.fip_qos_map.get_fips_by_router_id(
            router_info.router_id)
        new_fips = set([])
        for fip in floating_ips:
            fip_res = FipResource(fip['id'], fip['floating_ip_address'])
            new_fips.add(fip_res)
            rates = self.get_fip_qos_rates(context,
                                           fip_res,
                                           fip.get(qos_consts.QOS_POLICY_ID))
            if device:
                self.process_ip_rates(fip_res, device, rates)

            if dvr_fip_device:
                # NOTE(liuyulong): for scenario 4 (mixed dvr_snat and compute
                # node), because floating IP qos rates may have been
                # processed in dvr snat-namespace, so here the cache was
                # already set. We just install the rules to the device in
                # qrouter-namespace.
                self.process_ip_rates(
                    fip_res, dvr_fip_device, rates, with_cache=False)

        self.fip_qos_map.set_fips(router_info.router_id, new_fips)
        fips_removed = current_fips - new_fips
        for fip_res in fips_removed:
            if device:
                self._remove_fip_rate_limit(device, fip_res)
            if dvr_fip_device:
                self._remove_fip_rate_limit(dvr_fip_device, fip_res)
            self.fip_qos_map.clean_by_resource(fip_res)

    def add_router(self, context, data):
        router_info = self._get_router_info(data['id'])
        if router_info:
            self.process_floating_ip_addresses(context, router_info)

    def update_router(self, context, data):
        router_info = self._get_router_info(data['id'])
        if router_info:
            self.process_floating_ip_addresses(context, router_info)

    def delete_router(self, context, data):
        self.fip_qos_map.clean_router_all_fip_cache(data['id'])

    def ha_state_change(self, context, data):
        pass

    def update_network(self, context, data):
        pass
