# Copyright (c) 2015 Red Hat, Inc.
# Copyright (c) 2015 SUSE Linux Products GmbH
# 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 random

import eventlet
import mock
from neutron_lib import constants as n_const
from neutron_lib.utils import net
from oslo_config import cfg
from oslo_utils import uuidutils

from neutron.agent.common import ovs_lib
from neutron.agent.l2 import l2_agent_extensions_manager as ext_manager
from neutron.agent.linux import interface
from neutron.agent.linux import polling
from neutron.common import utils
from neutron.conf.agent import common as agent_config
from neutron.conf import common as common_config
from neutron.conf.plugins.ml2.drivers import agent
from neutron.conf.plugins.ml2.drivers import ovs_conf
from neutron.plugins.ml2.drivers.openvswitch.agent.common import constants
from neutron.plugins.ml2.drivers.openvswitch.agent.openflow.ovs_ofctl \
    import br_int
from neutron.plugins.ml2.drivers.openvswitch.agent.openflow.ovs_ofctl \
    import br_phys
from neutron.plugins.ml2.drivers.openvswitch.agent.openflow.ovs_ofctl \
    import br_tun
from neutron.plugins.ml2.drivers.openvswitch.agent import ovs_neutron_agent \
    as ovs_agent
from neutron.tests.common import net_helpers
from neutron.tests.functional.agent.linux import base


class OVSAgentTestFramework(base.BaseOVSLinuxTestCase):

    def setUp(self):
        super(OVSAgentTestFramework, self).setUp()
        agent_rpc = ('neutron.plugins.ml2.drivers.openvswitch.agent.'
                     'ovs_neutron_agent.OVSPluginApi')
        mock.patch(agent_rpc).start()
        mock.patch('neutron.agent.rpc.PluginReportStateAPI').start()
        self.br_int = utils.get_rand_name(n_const.DEVICE_NAME_MAX_LEN,
                                          prefix='br-int')
        self.br_tun = utils.get_rand_name(n_const.DEVICE_NAME_MAX_LEN,
                                          prefix='br-tun')
        self.br_phys = utils.get_rand_name(n_const.DEVICE_NAME_MAX_LEN,
                                           prefix='br-phys')
        patch_name_len = n_const.DEVICE_NAME_MAX_LEN - len("-patch-tun")
        self.patch_tun = "%s-patch-tun" % self.br_int[patch_name_len:]
        self.patch_int = "%s-patch-int" % self.br_tun[patch_name_len:]
        self.ovs = ovs_lib.BaseOVS()
        self.config = self._configure_agent()
        self.driver = interface.OVSInterfaceDriver(self.config)
        self.namespace = self.useFixture(net_helpers.NamespaceFixture()).name

    def _get_config_opts(self):
        config = cfg.ConfigOpts()
        config.register_opts(common_config.core_opts)
        agent.register_agent_opts(config)
        ovs_conf.register_ovs_agent_opts(config)
        agent_config.register_interface_opts(config)
        agent_config.register_interface_driver_opts_helper(config)
        agent_config.register_agent_state_opts_helper(config)
        ext_manager.register_opts(config)
        return config

    def _configure_agent(self):
        config = self._get_config_opts()
        config.set_override(
            'interface_driver',
            'neutron.agent.linux.interface.OVSInterfaceDriver')
        config.set_override('integration_bridge', self.br_int, "OVS")
        config.set_override('ovs_integration_bridge', self.br_int)
        config.set_override('tunnel_bridge', self.br_tun, "OVS")
        config.set_override('int_peer_patch_port', self.patch_tun, "OVS")
        config.set_override('tun_peer_patch_port', self.patch_int, "OVS")
        config.set_override('host', 'ovs-agent')
        return config

    def _bridge_classes(self):
        return {
            'br_int': br_int.OVSIntegrationBridge,
            'br_phys': br_phys.OVSPhysicalBridge,
            'br_tun': br_tun.OVSTunnelBridge
        }

    def create_agent(self, create_tunnels=True, ancillary_bridge=None,
                     local_ip='192.168.10.1'):
        if create_tunnels:
            tunnel_types = [n_const.TYPE_VXLAN]
        else:
            tunnel_types = None
        bridge_mappings = ['physnet:%s' % self.br_phys]
        self.config.set_override('tunnel_types', tunnel_types, "AGENT")
        self.config.set_override('polling_interval', 1, "AGENT")
        self.config.set_override('local_ip', local_ip, "OVS")
        self.config.set_override('bridge_mappings', bridge_mappings, "OVS")
        # Physical bridges should be created prior to running
        self._bridge_classes()['br_phys'](self.br_phys).create()
        ext_mgr = ext_manager.L2AgentExtensionsManager(self.config)
        agent = ovs_agent.OVSNeutronAgent(self._bridge_classes(),
                                          ext_mgr, self.config)
        self.addCleanup(self.ovs.delete_bridge, self.br_int)
        if tunnel_types:
            self.addCleanup(self.ovs.delete_bridge, self.br_tun)
        self.addCleanup(self.ovs.delete_bridge, self.br_phys)
        agent.sg_agent = mock.Mock()
        agent.ancillary_brs = []
        if ancillary_bridge:
            agent.ancillary_brs.append(ancillary_bridge)
        return agent

    def _mock_get_events(self, agent, polling_manager, ports):
        get_events = polling_manager.get_events
        p_ids = [p['id'] for p in ports]

        def filter_events():
            events = get_events()
            filtered_ports = []
            for dev in events['added']:
                iface_id = agent.int_br.portid_from_external_ids(
                    dev.get('external_ids', []))
                if iface_id in p_ids:
                    # if the event is not about a port that was created by
                    # this test, we filter the event out. Since these tests are
                    # not run in isolation processing all the events might make
                    # some test fail ( e.g. the agent might keep resycing
                    # because it keeps finding not ready ports that are created
                    # by other tests)
                    filtered_ports.append(dev)
            return {'added': filtered_ports, 'removed': events['removed']}
        polling_manager.get_events = mock.Mock(side_effect=filter_events)

    def stop_agent(self, agent, rpc_loop_thread):
        agent.run_daemon_loop = False
        rpc_loop_thread.wait()

    def start_agent(self, agent, ports=None, unplug_ports=None):
        if unplug_ports is None:
            unplug_ports = []
        if ports is None:
            ports = []
        self.setup_agent_rpc_mocks(agent, unplug_ports)
        polling_manager = polling.InterfacePollingMinimizer()
        self._mock_get_events(agent, polling_manager, ports)
        self.addCleanup(polling_manager.stop)
        polling_manager.start()
        utils.wait_until_true(
            polling_manager._monitor.is_active)
        agent.check_ovs_status = mock.Mock(
            return_value=constants.OVS_NORMAL)
        self.agent_thread = eventlet.spawn(agent.rpc_loop,
                                           polling_manager)

        self.addCleanup(self.stop_agent, agent, self.agent_thread)
        return polling_manager

    def _create_test_port_dict(self):
        return {'id': uuidutils.generate_uuid(),
                'mac_address': net.get_random_mac(
                    'fa:16:3e:00:00:00'.split(':')),
                'fixed_ips': [{
                    'ip_address': '10.%d.%d.%d' % (
                         random.randint(3, 254),
                         random.randint(3, 254),
                         random.randint(3, 254))}],
                'vif_name': utils.get_rand_name(
                    self.driver.DEV_NAME_LEN, self.driver.DEV_NAME_PREFIX)}

    def _create_test_network_dict(self):
        return {'id': uuidutils.generate_uuid(),
                'tenant_id': uuidutils.generate_uuid()}

    def _plug_ports(self, network, ports, agent,
                    bridge=None, namespace=None):
        if namespace is None:
            namespace = self.namespace
        for port in ports:
            bridge = bridge or agent.int_br
            self.driver.plug(
                network.get('id'), port.get('id'), port.get('vif_name'),
                port.get('mac_address'),
                bridge.br_name, namespace=namespace)
            ip_cidrs = ["%s/8" % (port.get('fixed_ips')[0][
                'ip_address'])]
            self.driver.init_l3(port.get('vif_name'), ip_cidrs,
                                namespace=namespace)

    def _unplug_ports(self, ports, agent):
        for port in ports:
            self.driver.unplug(
                port.get('vif_name'), agent.int_br.br_name, self.namespace)

    def _get_device_details(self, port, network):
        dev = {'device': port['id'],
               'port_id': port['id'],
               'network_id': network['id'],
               'network_type': network.get('network_type', 'vlan'),
               'physical_network': network.get('physical_network', 'physnet'),
               'segmentation_id': network.get('segmentation_id', 1),
               'fixed_ips': port['fixed_ips'],
               'device_owner': n_const.DEVICE_OWNER_COMPUTE_PREFIX,
               'admin_state_up': True}
        return dev

    def assert_bridge(self, br, exists=True):
        self.assertEqual(exists, self.ovs.bridge_exists(br))

    def assert_patch_ports(self, agent):

        def get_peer(port):
            return agent.int_br.db_get_val(
                'Interface', port, 'options', check_error=True)

        utils.wait_until_true(
            lambda: get_peer(self.patch_int) == {'peer': self.patch_tun})
        utils.wait_until_true(
            lambda: get_peer(self.patch_tun) == {'peer': self.patch_int})

    def assert_bridge_ports(self):
        for port in [self.patch_tun, self.patch_int]:
            self.assertTrue(self.ovs.port_exists(port))

    def assert_vlan_tags(self, ports, agent):
        for port in ports:
            res = agent.int_br.db_get_val('Port', port.get('vif_name'), 'tag')
            self.assertTrue(res)

    def _expected_plugin_rpc_call(self, call, expected_devices, is_up=True):
        """Helper to check expected rpc call are received

        :param call: The call to check
        :param expected_devices: The device for which call is expected
        :param is_up: True if expected_devices are devices that are set up,
               False if expected_devices are devices that are set down
        """
        if is_up:
            rpc_devices = [
                dev for args in call.call_args_list for dev in args[0][1]]
        else:
            rpc_devices = [
                dev for args in call.call_args_list for dev in args[0][2]]
        for dev in rpc_devices:
            if dev in expected_devices:
                expected_devices.remove(dev)
        # reset mock otherwise if the mock is called again the same call param
        # will be processed again
        call.reset_mock()
        return not expected_devices

    def create_test_ports(self, amount=3, **kwargs):
        ports = []
        for x in range(amount):
            ports.append(self._create_test_port_dict(**kwargs))
        return ports

    def _mock_update_device(self, context, devices_up, devices_down, agent_id,
                            host=None, agent_restarted=False):
        dev_up = []
        dev_down = []
        for port in self.ports:
            if devices_up and port['id'] in devices_up:
                dev_up.append(port['id'])
            if devices_down and port['id'] in devices_down:
                dev_down.append({'device': port['id'], 'exists': True})
        return {'devices_up': dev_up,
                'failed_devices_up': [],
                'devices_down': dev_down,
                'failed_devices_down': []}

    def setup_agent_rpc_mocks(self, agent, unplug_ports):
        def mock_device_details(context, devices, agent_id, host=None,
                                agent_restarted=False):
            details = []
            for port in self.ports:
                if port['id'] in devices:
                    dev = self._get_device_details(
                        port, self.network)
                    details.append(dev)
            ports_to_unplug = [x for x in unplug_ports if x['id'] in devices]
            if ports_to_unplug:
                self._unplug_ports(ports_to_unplug, self.agent)
            return {'devices': details, 'failed_devices': []}

        (agent.plugin_rpc.get_devices_details_list_and_failed_devices.
            side_effect) = mock_device_details
        agent.plugin_rpc.update_device_list.side_effect = (
            self._mock_update_device)

    def _prepare_resync_trigger(self, agent):
        def mock_device_raise_exception(context, devices_up, devices_down,
                                        agent_id, host=None):
            agent.plugin_rpc.update_device_list.side_effect = (
                self._mock_update_device)
            raise Exception('Exception to trigger resync')

        self.agent.plugin_rpc.update_device_list.side_effect = (
            mock_device_raise_exception)

    def _prepare_failed_dev_up_trigger(self, agent):

        def mock_failed_devices_up(context, devices_up, devices_down,
                                   agent_id, host=None,
                                   agent_restarted=False):
            failed_devices = []
            devices = list(devices_up)
            # first port fails
            if self.ports[0]['id'] in devices_up:
                # reassign side_effect so that next RPC call will succeed
                agent.plugin_rpc.update_device_list.side_effect = (
                    self._mock_update_device)
                devices.remove(self.ports[0]['id'])
                failed_devices.append(self.ports[0]['id'])
            return {'devices_up': devices,
                    'failed_devices_up': failed_devices,
                    'devices_down': [],
                    'failed_devices_down': []}

        self.agent.plugin_rpc.update_device_list.side_effect = (
            mock_failed_devices_up)

    def _prepare_failed_dev_down_trigger(self, agent):

        def mock_failed_devices_down(context, devices_up, devices_down,
                                     agent_id, host=None,
                                     agent_restarted=False):
            # first port fails
            failed_port_id = self.ports[0]['id']
            failed_devices_down = []
            dev_down = [
                {'device': p['id'], 'exists': True}
                for p in self.ports if p['id'] in devices_down and (
                    p['id'] != failed_port_id)]
            # check if it's the call to set devices down and if the device
            # that is supposed to fail is in the call then modify the
            # side_effect so that next RPC call will succeed.
            if devices_down and failed_port_id in devices_down:
                agent.plugin_rpc.update_device_list.side_effect = (
                     self._mock_update_device)
                failed_devices_down.append(failed_port_id)
            return {'devices_up': devices_up,
                    'failed_devices_up': [],
                    'devices_down': dev_down,
                    'failed_devices_down': failed_devices_down}

        self.agent.plugin_rpc.update_device_list.side_effect = (
            mock_failed_devices_down)

    def wait_until_ports_state(self, ports, up, timeout=60):
        port_ids = [p['id'] for p in ports]
        utils.wait_until_true(
            lambda: self._expected_plugin_rpc_call(
                self.agent.plugin_rpc.update_device_list, port_ids, up),
            timeout=timeout)

    def setup_agent_and_ports(self, port_dicts, create_tunnels=True,
                              ancillary_bridge=None,
                              trigger_resync=False,
                              failed_dev_up=False,
                              failed_dev_down=False,
                              network=None):
        self.ports = port_dicts
        self.agent = self.create_agent(create_tunnels=create_tunnels,
                                       ancillary_bridge=ancillary_bridge)
        self.agent.iter_num += 1
        self.polling_manager = self.start_agent(self.agent, ports=self.ports)
        self.network = network or self._create_test_network_dict()
        if trigger_resync:
            self._prepare_resync_trigger(self.agent)
        elif failed_dev_up:
            self._prepare_failed_dev_up_trigger(self.agent)
        elif failed_dev_down:
            self._prepare_failed_dev_down_trigger(self.agent)

        self._plug_ports(self.network, self.ports, self.agent,
                         bridge=ancillary_bridge)

    def plug_ports_to_phys_br(self, network, ports, namespace=None):
        physical_network = network.get('physical_network', 'physnet')
        phys_segmentation_id = network.get('segmentation_id', None)
        network_type = network.get('network_type', 'flat')

        phys_br = self.agent.phys_brs[physical_network]

        self._plug_ports(network, ports, self.agent, bridge=phys_br,
                         namespace=namespace)

        if network_type == 'flat':
            # NOTE(slaweq): for OVS implementations remove the DEAD VLAN tag
            # on ports that belongs to flat network. DEAD VLAN tag is added
            # to each newly created port. This is related to lp#1767422
            for port in ports:
                phys_br.clear_db_attribute("Port", port['vif_name'], "tag")
        elif phys_segmentation_id and network_type == 'vlan':
            for port in ports:
                phys_br.set_db_attribute(
                    "Port", port['vif_name'], "tag", phys_segmentation_id)
