# Copyright (c) 2016 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.

from unittest import mock

from neutron_lib import constants as n_consts
from neutron_lib.utils import helpers
from neutron_lib.utils import net
from oslo_utils import uuidutils

from neutron.agent.common import ovs_lib
from neutron.common import utils as common_utils
from neutron.objects import trunk as trunk_obj
from neutron.services.trunk.drivers.openvswitch.agent import ovsdb_handler
from neutron.services.trunk.drivers.openvswitch.agent import trunk_manager
from neutron.tests.functional.agent.l2 import base


def generate_tap_device_name():
    return n_consts.TAP_DEVICE_PREFIX + helpers.get_random_string(
        n_consts.DEVICE_NAME_MAX_LEN - len(n_consts.TAP_DEVICE_PREFIX))


class OVSDBHandlerTestCase(base.OVSAgentTestFramework):
    """Test functionality of OVSDBHandler.

    This suite aims for interaction between events coming from OVSDB monitor,
    agent and wiring ports via trunk bridge to integration bridge.
    """
    def setUp(self):
        """Prepare resources.

        Set up trunk_dict representing incoming data from Neutron-server when
        fetching for trunk details. Another resource trunk_br represents the
        trunk bridge which its creation is simulated when creating a port in l2
        agent framework.
        """
        super(OVSDBHandlerTestCase, self).setUp()
        trunk_id = uuidutils.generate_uuid()
        self.trunk_dict = {
            'id': trunk_id,
            'mac_address': net.get_random_mac('fa:16:3e:00:00:00'.split(':')),
            'sub_ports': []}
        self.trunk_port_name = generate_tap_device_name()
        self.trunk_br = trunk_manager.TrunkBridge(trunk_id)
        self.ovsdb_handler = self._prepare_mocked_ovsdb_handler()

    def _prepare_mocked_ovsdb_handler(self):
        handler = ovsdb_handler.OVSDBHandler(
            trunk_manager.TrunkManager(ovs_lib.OVSBridge(self.br_int)))
        mock.patch.object(handler, 'trunk_rpc').start()

        handler.trunk_rpc.get_trunk_details.side_effect = (
            self._mock_get_trunk_details)
        handler.trunk_rpc.update_subport_bindings.side_effect = (
            self._mock_update_subport_binding)

        return handler

    def _mock_get_trunk_details(self, context, parent_port_id):
        if parent_port_id == self.trunk_dict['port_id']:
            return trunk_obj.Trunk(**self.trunk_dict)

    def _mock_update_subport_binding(self, context, subports):
        return {self.trunk_dict['id']: [
            {'id': subport['port_id'], 'mac_address': subport['mac_address']}
            for subport in subports]
        }

    def _plug_ports(self, network, ports, agent, bridge=None, namespace=None):
        # creates only the trunk, the sub_port will be plugged by the
        # trunk manager
        if not self.trunk_br.exists():
            self.trunk_br.create()
            self.addCleanup(self.trunk_br.destroy)
        self.driver.plug(
            network['id'],
            self.trunk_dict['port_id'],
            self.trunk_port_name,
            self.trunk_dict['mac_address'],
            self.trunk_br.br_name)

    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_events = {
                'added': [],
                'removed': [],
                'modified': []
            }
            for event_type in filtered_events:
                for dev in events[event_type]:
                    iface_id = agent.int_br.portid_from_external_ids(
                        dev.get('external_ids', []))
                    is_for_this_test = (
                        iface_id in p_ids or
                        iface_id == self.trunk_dict['port_id'] or
                        dev['name'] == self.trunk_br.br_name)
                    if is_for_this_test:
                        # 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_events[event_type].append(dev)
            return filtered_events
        mock.patch.object(polling_manager, 'get_events',
            side_effect=filter_events).start()

    def _fill_trunk_dict(self, num=3):
        ports = self.create_test_ports(amount=num)
        self.trunk_dict['port_id'] = ports[0]['id']
        self.trunk_dict['sub_ports'] = [trunk_obj.SubPort(
            id=uuidutils.generate_uuid(),
            port_id=ports[i]['id'],
            mac_address=ports[i]['mac_address'],
            segmentation_id=i,
            trunk_id=self.trunk_dict['id'])
            for i in range(1, num)]
        return ports

    def _test_trunk_creation_helper(self, ports):
        self.setup_agent_and_ports(port_dicts=ports)
        self.wait_until_ports_state(self.ports, up=True)
        self.trunk_br.delete_port(self.trunk_port_name)
        self.wait_until_ports_state(self.ports, up=False)
        common_utils.wait_until_true(lambda:
            not self.trunk_br.bridge_exists(self.trunk_br.br_name))

    def test_trunk_creation_with_subports(self):
        ports = self._fill_trunk_dict()
        self._test_trunk_creation_helper(ports[:1])

    def test_trunk_creation_with_no_subports(self):
        ports = self.create_test_ports(amount=1)
        self.trunk_dict['port_id'] = ports[0]['id']
        self._test_trunk_creation_helper(ports)

    def test_resync(self):
        ports = self._fill_trunk_dict()
        self.setup_agent_and_ports(port_dicts=ports)
        self.wait_until_ports_state(self.ports, up=True)
        self.agent.fullsync = True
        self.wait_until_ports_state(self.ports, up=True)

    def test_restart_subport_events(self):
        ports = self._fill_trunk_dict()
        self.setup_agent_and_ports(port_dicts=ports)
        self.wait_until_ports_state(self.ports, up=True)

        # restart and simulate a subport delete
        deleted_port = self.ports[2]
        deleted_sp = trunk_manager.SubPort(
            self.trunk_dict['id'], deleted_port['id'])
        self.stop_agent(self.agent, self.agent_thread)
        self.polling_manager.stop()
        self.trunk_dict['sub_ports'] = self.trunk_dict['sub_ports'][:1]
        self.setup_agent_and_ports(port_dicts=ports[:2])
        # NOTE: the port_dicts passed in setup_agent_and_ports is stored in
        # self.ports so we are waiting here only for ports[:2]
        self.wait_until_ports_state(self.ports, up=True)
        common_utils.wait_until_true(
            lambda: (deleted_sp.patch_port_trunk_name not in
                self.trunk_br.get_port_name_list()))

    def test_cleanup_on_vm_delete(self):
        with mock.patch.object(self.ovsdb_handler, 'handle_trunk_remove'):
            br_int = ovs_lib.OVSBridge(self.br_int)
            ports = self._fill_trunk_dict()
            self.setup_agent_and_ports(port_dicts=ports[:1])
            self.wait_until_ports_state(self.ports, up=True)
            self.trunk_br.delete_port(self.trunk_port_name)
            # We do not expect any instance port to show up on the trunk
            # bridge so we can set a much more aggressive timeout and
            # fail fast(er).
            self.ovsdb_handler.timeout = 1
            self.ovsdb_handler.handle_trunk_add(self.trunk_br.br_name)
            # Check no resources are left behind.
            self.assertFalse(self.trunk_br.exists())
            self.assertFalse(ovsdb_handler.bridge_has_service_port(br_int))

    def test_do_not_delete_trunk_bridge_with_instance_ports(self):
        ports = self._fill_trunk_dict()
        self.setup_agent_and_ports(port_dicts=ports)
        self.wait_until_ports_state(self.ports, up=True)
        self.ovsdb_handler.handle_trunk_remove(self.trunk_br.br_name,
                ports.pop())
        self.assertTrue(self.trunk_br.exists())
