# Copyright (c) 2015 Red Hat, Inc.
# 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 os.path

import eventlet
import fixtures
import mock
import netaddr
from oslo_config import fixture as fixture_config
from oslo_utils import uuidutils

from neutron.agent.common import config
from neutron.agent.common import ovs_lib
from neutron.agent.dhcp import agent
from neutron.agent import dhcp_agent
from neutron.agent.linux import dhcp
from neutron.agent.linux import external_process
from neutron.agent.linux import interface
from neutron.agent.linux import ip_lib
from neutron.agent.linux import utils
from neutron.common import constants
from neutron.common import utils as common_utils
from neutron.tests.common import net_helpers
from neutron.tests.functional.agent.linux import helpers
from neutron.tests.functional import base


class DHCPAgentOVSTestFramework(base.BaseSudoTestCase):

    _DHCP_PORT_MAC_ADDRESS = netaddr.EUI("24:77:03:7d:00:4c")
    _DHCP_PORT_MAC_ADDRESS.dialect = netaddr.mac_unix
    _TENANT_PORT_MAC_ADDRESS = netaddr.EUI("24:77:03:7d:00:3a")
    _TENANT_PORT_MAC_ADDRESS.dialect = netaddr.mac_unix

    _IP_ADDRS = {
        4: {'addr': '192.168.10.11',
            'cidr': '192.168.10.0/24',
            'gateway': '192.168.10.1'},
        6: {'addr': '0:0:0:0:0:ffff:c0a8:a0b',
            'cidr': '0:0:0:0:0:ffff:c0a8:a00/120',
            'gateway': '0:0:0:0:0:ffff:c0a8:a01'}, }

    def setUp(self):
        super(DHCPAgentOVSTestFramework, self).setUp()
        config.setup_logging()
        self.conf_fixture = self.useFixture(fixture_config.Config())
        self.conf = self.conf_fixture.conf
        dhcp_agent.register_options(self.conf)

        # NOTE(cbrandily): TempDir fixture creates a folder with 0o700
        # permissions but agent dir must be readable by dnsmasq user (nobody)
        agent_config_dir = self.useFixture(fixtures.TempDir()).path
        self.useFixture(
            helpers.RecursivePermDirFixture(agent_config_dir, 0o555))

        self.conf.set_override("dhcp_confs", agent_config_dir)
        self.conf.set_override(
            'interface_driver',
            'neutron.agent.linux.interface.OVSInterfaceDriver')
        self.conf.set_override('report_interval', 0, 'AGENT')
        br_int = self.useFixture(net_helpers.OVSBridgeFixture()).bridge
        self.conf.set_override('ovs_integration_bridge', br_int.br_name)

        self.mock_plugin_api = mock.patch(
            'neutron.agent.dhcp.agent.DhcpPluginApi').start().return_value
        mock.patch('neutron.agent.rpc.PluginReportStateAPI').start()
        self.agent = agent.DhcpAgentWithStateReport('localhost')

        self.ovs_driver = interface.OVSInterfaceDriver(self.conf)

        self.conf.set_override('check_child_processes_interval', 1, 'AGENT')

    def network_dict_for_dhcp(self, dhcp_enabled=True, ip_version=4):
        net_id = uuidutils.generate_uuid()
        subnet_dict = self.create_subnet_dict(
            net_id, dhcp_enabled, ip_version)
        port_dict = self.create_port_dict(
            net_id, subnet_dict.id,
            mac_address=str(self._DHCP_PORT_MAC_ADDRESS),
            ip_version=ip_version)
        port_dict.device_id = common_utils.get_dhcp_agent_device_id(
            net_id, self.conf.host)
        net_dict = self.create_network_dict(
            net_id, [subnet_dict], [port_dict])
        return net_dict

    def create_subnet_dict(self, net_id, dhcp_enabled=True, ip_version=4):
        sn_dict = dhcp.DictModel({
            "id": uuidutils.generate_uuid(),
            "network_id": net_id,
            "ip_version": ip_version,
            "cidr": self._IP_ADDRS[ip_version]['cidr'],
            "gateway_ip": (self.
                _IP_ADDRS[ip_version]['gateway']),
            "enable_dhcp": dhcp_enabled,
            "dns_nameservers": [],
            "host_routes": [],
            "ipv6_ra_mode": None,
            "ipv6_address_mode": None})
        if ip_version == 6:
            sn_dict['ipv6_address_mode'] = constants.DHCPV6_STATEFUL
        return sn_dict

    def create_port_dict(self, network_id, subnet_id, mac_address,
                         ip_version=4, ip_address=None):
        ip_address = (self._IP_ADDRS[ip_version]['addr']
            if not ip_address else ip_address)
        port_dict = dhcp.DictModel({
            "id": uuidutils.generate_uuid(),
            "name": "foo",
            "mac_address": mac_address,
            "network_id": network_id,
            "admin_state_up": True,
            "device_id": uuidutils.generate_uuid(),
            "device_owner": "foo",
            "fixed_ips": [{"subnet_id": subnet_id,
                           "ip_address": ip_address}], })
        return port_dict

    def create_network_dict(self, net_id, subnets=None, ports=None):
        subnets = [] if not subnets else subnets
        ports = [] if not ports else ports
        net_dict = dhcp.NetModel(d={
            "id": net_id,
            "subnets": subnets,
            "ports": ports,
            "admin_state_up": True,
            "tenant_id": uuidutils.generate_uuid(), })
        return net_dict

    def get_interface_name(self, network, port):
        device_manager = dhcp.DeviceManager(conf=self.conf, plugin=mock.Mock())
        return device_manager.get_interface_name(network, port)

    def configure_dhcp_for_network(self, network, dhcp_enabled=True):
        self.agent.configure_dhcp_for_network(network)
        self.addCleanup(self._cleanup_network, network, dhcp_enabled)

    def _cleanup_network(self, network, dhcp_enabled):
        self.mock_plugin_api.release_dhcp_port.return_value = None
        if dhcp_enabled:
            self.agent.call_driver('disable', network)

    def assert_dhcp_resources(self, network, dhcp_enabled):
        ovs = ovs_lib.BaseOVS()
        port = network.ports[0]
        iface_name = self.get_interface_name(network, port)
        self.assertEqual(dhcp_enabled, ovs.port_exists(iface_name))
        self.assert_dhcp_namespace(network.namespace, dhcp_enabled)
        self.assert_dhcp_device(network.namespace, iface_name, dhcp_enabled)

    def assert_dhcp_namespace(self, namespace, dhcp_enabled):
        ip = ip_lib.IPWrapper()
        self.assertEqual(dhcp_enabled, ip.netns.exists(namespace))

    def assert_dhcp_device(self, namespace, dhcp_iface_name, dhcp_enabled):
        dev = ip_lib.IPDevice(dhcp_iface_name, namespace)
        self.assertEqual(dhcp_enabled, ip_lib.device_exists(
            dhcp_iface_name, namespace))
        if dhcp_enabled:
            self.assertEqual(self._DHCP_PORT_MAC_ADDRESS, dev.link.address)

    def _plug_port_for_dhcp_request(self, network, port):
        namespace = network.namespace
        vif_name = self.get_interface_name(network.id, port)

        self.ovs_driver.plug(network.id, port.id, vif_name, port.mac_address,
                             self.conf['ovs_integration_bridge'],
                             namespace=namespace)

    def _ip_list_for_vif(self, vif_name, namespace):
        ip_device = ip_lib.IPDevice(vif_name, namespace)
        return ip_device.addr.list(ip_version=4)

    def _get_network_port_for_allocation_test(self):
        network = self.network_dict_for_dhcp()
        ip_addr = netaddr.IPNetwork(network.subnets[0].cidr)[1]
        port = self.create_port_dict(
            network.id, network.subnets[0].id,
            mac_address=str(self._TENANT_PORT_MAC_ADDRESS),
            ip_address=str(ip_addr))
        return network, port

    def assert_good_allocation_for_port(self, network, port):
        vif_name = self.get_interface_name(network.id, port)
        self._run_dhclient(vif_name, network)

        predicate = lambda: len(
            self._ip_list_for_vif(vif_name, network.namespace))
        utils.wait_until_true(predicate, 10)

        ip_list = self._ip_list_for_vif(vif_name, network.namespace)
        cidr = ip_list[0].get('cidr')
        ip_addr = str(netaddr.IPNetwork(cidr).ip)
        self.assertEqual(port.fixed_ips[0].ip_address, ip_addr)

    def assert_bad_allocation_for_port(self, network, port):
        vif_name = self.get_interface_name(network.id, port)
        self._run_dhclient(vif_name, network)
        # we need wait some time (10 seconds is enough) and check
        # that dhclient not configured ip-address for interface
        eventlet.sleep(10)

        ip_list = self._ip_list_for_vif(vif_name, network.namespace)
        self.assertEqual([], ip_list)

    def _run_dhclient(self, vif_name, network):
        # NOTE: Before run dhclient we should create resolv.conf file
        # in namespace,  where we will run dhclient for testing address
        # allocation for port, otherwise, dhclient will override
        # system /etc/resolv.conf
        # By default, folder for dhcp-agent's namespace doesn't exist
        # that's why we use AdminDirFixture for create directory
        # with admin permissions in /etc/netns/ and touch resolv.conf in it.
        etc_dir = '/etc/netns/%s' % network.namespace
        self.useFixture(helpers.AdminDirFixture(etc_dir))
        cmd = ['touch', os.path.join(etc_dir, 'resolv.conf')]
        utils.execute(cmd, run_as_root=True)
        dhclient_cmd = ['dhclient', '--no-pid', '-d', '-1', vif_name]
        proc = net_helpers.RootHelperProcess(
            cmd=dhclient_cmd, namespace=network.namespace)
        self.addCleanup(proc.wait)
        self.addCleanup(proc.kill)

    def _get_metadata_proxy_process(self, network):
        return external_process.ProcessManager(
            self.conf,
            network.id,
            network.namespace)


class DHCPAgentOVSTestCase(DHCPAgentOVSTestFramework):

    def test_create_subnet_with_dhcp(self):
        dhcp_enabled = True
        for version in [4, 6]:
            network = self.network_dict_for_dhcp(
                dhcp_enabled, ip_version=version)
            self.configure_dhcp_for_network(network=network,
                                            dhcp_enabled=dhcp_enabled)
            self.assert_dhcp_resources(network, dhcp_enabled)

    def test_agent_mtu_set_on_interface_driver(self):
        network = self.network_dict_for_dhcp()
        network["mtu"] = 789
        self.configure_dhcp_for_network(network=network)
        port = network.ports[0]
        iface_name = self.get_interface_name(network, port)
        dev = ip_lib.IPDevice(iface_name, network.namespace)
        self.assertEqual(789, dev.link.mtu)

    def test_good_address_allocation(self):
        network, port = self._get_network_port_for_allocation_test()
        network.ports.append(port)
        self.configure_dhcp_for_network(network=network)
        self._plug_port_for_dhcp_request(network, port)
        self.assert_good_allocation_for_port(network, port)

    def test_bad_address_allocation(self):
        network, port = self._get_network_port_for_allocation_test()
        network.ports.append(port)
        self.configure_dhcp_for_network(network=network)
        bad_mac_address = netaddr.EUI(self._TENANT_PORT_MAC_ADDRESS.value + 1)
        bad_mac_address.dialect = netaddr.mac_unix
        port.mac_address = str(bad_mac_address)
        self._plug_port_for_dhcp_request(network, port)
        self.assert_bad_allocation_for_port(network, port)

    def _spawn_network_metadata_proxy(self):
        network = self.network_dict_for_dhcp()
        self.conf.set_override('enable_isolated_metadata', True)
        self.addCleanup(self.agent.disable_isolated_metadata_proxy, network)
        self.configure_dhcp_for_network(network=network)
        pm = self._get_metadata_proxy_process(network)
        utils.wait_until_true(
            lambda: pm.active,
            timeout=5,
            sleep=0.01,
            exception=RuntimeError("Metadata proxy didn't spawn"))
        return (pm, network)

    def test_metadata_proxy_respawned(self):
        pm, network = self._spawn_network_metadata_proxy()
        old_pid = pm.pid

        utils.execute(['kill', '-9', old_pid], run_as_root=True)
        utils.wait_until_true(
            lambda: pm.active and pm.pid != old_pid,
            timeout=5,
            sleep=0.1,
            exception=RuntimeError("Metadata proxy didn't respawn"))

    def test_stale_metadata_proxy_killed(self):
        pm, network = self._spawn_network_metadata_proxy()

        self.conf.set_override('enable_isolated_metadata', False)
        self.configure_dhcp_for_network(network=network)
        utils.wait_until_true(
            lambda: not pm.active,
            timeout=5,
            sleep=0.1,
            exception=RuntimeError("Stale metadata proxy didn't get killed"))
