# Copyright 2016 OVH SAS
#
#    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

from neutron_lib import constants
from oslo_utils import uuidutils

from neutron.agent.linux import ip_lib
from neutron.tests.fullstack.agents import dhcp_agent
from neutron.tests.fullstack import base
from neutron.tests.fullstack.resources import environment
from neutron.tests.fullstack.resources import machine
from neutron.tests.unit import testlib_api

load_tests = testlib_api.module_load_tests


class BaseDhcpAgentTest(base.BaseFullStackTestCase):

    boot_vm_for_test = True
    dhcp_scheduler_class = None
    api_workers = 1
    number_of_hosts = 2

    def setUp(self):
        host_descriptions = [
            environment.HostDescription(
                dhcp_agent=True,
                l2_agent_type=constants.AGENT_TYPE_OVS
            ) for _ in range(self.number_of_hosts)]

        env = environment.Environment(
            environment.EnvironmentDescription(
                l2_pop=False,
                arp_responder=False,
                agent_down_time=self.agent_down_time,
                dhcp_scheduler_class=self.dhcp_scheduler_class,
                api_workers=self.api_workers,
            ),
            host_descriptions)

        super().setUp(env)
        self.project_id = uuidutils.generate_uuid()
        if self.boot_vm_for_test:
            self._create_network_subnet_and_vm()

    def _spawn_vm(self):
        host = random.choice(self.environment.hosts)
        vm = self.useFixture(
            machine.FakeFullstackMachine(
                host,
                self.network['id'],
                self.project_id,
                self.safe_client,
                use_dhcp=True))
        vm.block_until_boot()
        vm.block_until_dhcp_config_done()
        return vm

    def _create_network_subnet_and_vm(self):
        self.network = self.safe_client.create_network(self.project_id)

        self.subnet = self.safe_client.create_subnet(
            self.project_id, self.network['id'],
            cidr='10.0.0.0/24',
            gateway_ip='10.0.0.1',
            name='subnet-test',
            enable_dhcp=True)

        self.vm = self._spawn_vm()


class TestDhcpAgentNoHA(BaseDhcpAgentTest):

    number_of_hosts = 1  # Force no HA
    agent_down_time = 60

    def test_mtu_update(self):
        # The test case needs access to devices in nested namespaces. ip_lib
        # doesn't support it, and it's probably unsafe to touch the library for
        # testing matters.
        # TODO(jlibosva) revisit when ip_lib supports nested namespaces
        if self.environment.hosts[0].dhcp_agent.namespace is not None:
            self.skipTest("ip_lib doesn't support nested namespaces")

        self.vm.block_until_dhcp_config_done()

        namespace = dhcp_agent._get_namespace_name(
            self.network['id'],
            suffix=self.environment.hosts[0].dhcp_agent.get_namespace_suffix())
        self.assert_namespace_exists(namespace)

        ip = ip_lib.IPWrapper(namespace)
        devices = ip.get_devices()
        self.assertEqual(1, len(devices))

        dhcp_dev = devices[0]
        mtu = dhcp_dev.link.mtu
        self.assertEqual(1450, mtu)

        mtu -= 1
        self.safe_client.update_network(self.network['id'], mtu=mtu)
        base.wait_until_true(lambda: dhcp_dev.link.mtu == mtu)


class TestDhcpAgentHA(BaseDhcpAgentTest):

    agent_down_time = 30

    def _wait_until_network_rescheduled(self, old_agent):
        def _agent_rescheduled():
            network_agents = self.client.list_dhcp_agent_hosting_networks(
                self.network['id'])['agents']
            if network_agents:
                return network_agents[0]['id'] != old_agent['id']
            return False

        base.wait_until_true(_agent_rescheduled, timeout=120)

    def _kill_dhcp_agent(self, agent):
        for host in self.environment.hosts:
            hostname = host.dhcp_agent.get_agent_hostname()
            if hostname == agent['host']:
                host.dhcp_agent.kill()
                self._wait_until_agent_down(agent['id'])
                break

    def _add_network_to_new_agent(self):
        dhcp_agents = self.client.list_agents(
            agent_type=constants.AGENT_TYPE_DHCP)['agents']
        dhcp_agents_ids = [agent['id'] for agent in dhcp_agents]

        current_agents = self.client.list_dhcp_agent_hosting_networks(
            self.network['id'])['agents']
        current_agents_ids = [agent['id'] for agent in current_agents]

        new_agents_ids = list(set(dhcp_agents_ids) - set(current_agents_ids))
        if new_agents_ids:
            new_agent_id = random.choice(new_agents_ids)
            self.client.add_network_to_dhcp_agent(
                new_agent_id, {'network_id': self.network['id']})

    def test_reschedule_network_on_new_agent(self):
        network_dhcp_agents = self.client.list_dhcp_agent_hosting_networks(
            self.network['id'])['agents']
        self.assertEqual(1, len(network_dhcp_agents))

        self._kill_dhcp_agent(network_dhcp_agents[0])
        self._wait_until_network_rescheduled(network_dhcp_agents[0])

        # ensure that only one agent is handling DHCP for this network
        new_network_dhcp_agents = self.client.list_dhcp_agent_hosting_networks(
            self.network['id'])['agents']
        self.assertEqual(1, len(new_network_dhcp_agents))

        # check if new vm will get IP from new DHCP agent
        self._spawn_vm()

    def test_multiple_agents_for_network(self):
        network_dhcp_agents = self.client.list_dhcp_agent_hosting_networks(
            self.network['id'])['agents']
        self.assertEqual(1, len(network_dhcp_agents))

        self._add_network_to_new_agent()
        # ensure that two agents are handling DHCP for this network
        network_dhcp_agents = self.client.list_dhcp_agent_hosting_networks(
            self.network['id'])['agents']
        self.assertEqual(2, len(network_dhcp_agents))

        self._kill_dhcp_agent(network_dhcp_agents[0])

        # check if new vm will get IP from DHCP agent which is still alive
        self._spawn_vm()


class TestDhcpAgentHARaceCondition(BaseDhcpAgentTest):

    agent_down_time = 30
    boot_vm_for_test = False
    api_workers = 2
    dhcp_scheduler_class = ('neutron.tests.fullstack.schedulers.dhcp.'
                            'AlwaysTheOtherAgentScheduler')

    def setUp(self):
        super().setUp()
        self._create_network_with_multiple_subnets()

    def _create_network_with_multiple_subnets(self):
        self.network = self.safe_client.create_network(self.project_id)

        funcs = []
        args = []
        for i in range(4):
            funcs.append(self.safe_client.create_subnet)
            args.append((
                self.project_id, self.network['id'], '10.0.%s.0/24' % i,
                '10.0.%s.1' % i, 'subnet-test-%s' % i, True
            ))
        self._simulate_concurrent_requests_process_and_raise(funcs, args)

    def test_dhcp_agent_ha_with_race_condition(self):
        # NOTE(ralonsoh): the concurrent creation in the same thread could
        # fail because the context and the session is the same for all DB
        # calls.
        network_dhcp_agents = self.client.list_dhcp_agent_hosting_networks(
            self.network['id'])['agents']
        self.assertEqual(1, len(network_dhcp_agents))


class TestSubnetDeleteRace(BaseDhcpAgentTest):
    agent_down_time = 30
    boot_vm_for_test = False

    def setUp(self):
        host_descriptions = [
            environment.HostDescription(
                dhcp_agent=True, l2_agent_type=constants.AGENT_TYPE_OVS),
            environment.HostDescription(
                dhcp_agent=False, l2_agent_type=constants.AGENT_TYPE_OVS,
                segmented_physnet=True),
        ]

        env = environment.Environment(
            environment.EnvironmentDescription(
                network_type='vlan',
                mech_drivers='openvswitch',
                l2_pop=False,
                arp_responder=False,
                agent_down_time=self.agent_down_time,
                service_plugins='router,segments'
            ),
            host_descriptions)

        # Note(lajoskatona): Here, call grandparent's (BaseFullStackTestCase)
        # setup(), to avoid BaseDhcpAgentTest mess up environment.
        super(BaseDhcpAgentTest, self).setUp(env)
        self.project_id = uuidutils.generate_uuid()
        if self.boot_vm_for_test:
            self._create_network_subnet_and_vm()

    def test_subnet_delete_race_condition(self):
        agents = self.client.list_agents(agent_type=constants.AGENT_TYPE_OVS)
        agents = agents['agents']
        self.assertEqual(2, len(agents))

        def get_ovs_physnet(idx):
            return list(
                agents[idx]['configurations']['bridge_mappings'].keys())[0]

        self.network = self.safe_client.create_network(
            tenant_id=self.project_id, network_type='vlan',
            segmentation_id=103, physical_network=get_ovs_physnet(0))

        self.segment2 = self.safe_client.create_segment(
            project_id=self.project_id, network=self.network['id'],
            network_type='vlan', name='segment_2', segmentation_id=103,
            physical_network=get_ovs_physnet(1))

        subnet = self.safe_client.create_subnet(
            self.project_id,
            self.network['id'],
            cidr='10.0.11.0/24',
            gateway_ip='10.0.11.1',
            name='subnet-test',
            enable_dhcp=True)

        self.vm = self._spawn_vm()

        dhcp_ports = self.safe_client.list_ports(**{
            'device_owner': 'network:dhcp',
            'network_id': self.network['id']
        })
        self.assertEqual(1, len(dhcp_ports))
        self.assertEqual('ACTIVE', dhcp_ports[0]['status'])

        self.vm.destroy(delete_port=True)

        dhcp_ports = self.safe_client.list_ports(**{
            'device_owner': 'network:dhcp',
            'network_id': self.network['id']
        })
        self.assertEqual(1, len(dhcp_ports))

        self.client.delete_subnet(subnet['id'])

        def _is_subnet_deleted():
            snets = self.safe_client.list_subnets()
            if len(snets) == 0:
                return True
            return False

        base.wait_until_true(_is_subnet_deleted)
        # Note(lajoskatona): Here cleanup does its job and it will fail if the
        # segment or network deletion is impossible
