# Copyright 2015 Red Hat, Inc.
#
#    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 itertools
import shutil

import netaddr
from oslo_log import log as logging

from neutron_lib.api.definitions import portbindings as pbs
from neutron_lib import constants

from neutron.agent.common import async_process
from neutron.agent.linux import ip_lib
from neutron.tests.common import machine_fixtures
from neutron.tests.common import net_helpers
from neutron.tests.fullstack import base as fullstack_base


FULLSTACK_DHCLIENT_SCRIPT = 'fullstack-dhclient-script'
LOG = logging.getLogger(__name__)


class FakeFullstackMachinesList(list):
    """A list of items implementing the FakeFullstackMachine interface."""

    def block_until_all_boot(self):
        for vm in self:
            vm.block_until_boot()

    def block_until_all_dhcp_config_done(self):
        for vm in self:
            vm.block_until_dhcp_config_done()

    def ping_all(self):
        # Generate an iterable of all unique pairs. For example:
        # itertools.permutations(range(3), 2) results in:
        # ((0, 1), (0, 2), (1, 0), (1, 2), (2, 0), (2, 1))
        for vm_1, vm_2 in itertools.permutations(self, 2):
            vm_1.block_until_ping(vm_2.ip)

    def ping6_all(self):
        # Generate an iterable of all unique pairs. For example:
        # itertools.permutations(range(3), 2) results in:
        # ((0, 1), (0, 2), (1, 0), (1, 2), (2, 0), (2, 1))
        for vm_1, vm_2 in itertools.permutations(self, 2):
            vm_1.block_until_ping(vm_2.ipv6)


class FakeFullstackMachine(machine_fixtures.FakeMachineBase):
    NO_RESOLV_CONF_DHCLIENT_SCRIPT_PATH = (
        shutil.which(FULLSTACK_DHCLIENT_SCRIPT))

    def __init__(self, host, network_id, tenant_id, safe_client,
                 neutron_port=None, bridge_name=None, use_dhcp=False,
                 use_dhcp6=False):
        super().__init__()
        self.host = host
        self.tenant_id = tenant_id
        self.network_id = network_id
        self.safe_client = safe_client
        self.neutron_port = neutron_port
        self.bridge_name = bridge_name
        self.use_dhcp = use_dhcp
        self.dhclient_async = None
        self.use_dhcp6 = use_dhcp6

    def _setUp(self):
        super()._setUp()

        self.bridge = self._get_bridge()

        if not self.neutron_port:
            self.neutron_port = self.safe_client.create_port(
                network_id=self.network_id,
                tenant_id=self.tenant_id,
                hostname=self.host.hostname)
        mac_address = self.neutron_port['mac_address']
        hybrid_plug = self.neutron_port[pbs.VIF_DETAILS].get(
            pbs.OVS_HYBRID_PLUG, False)

        self.bind_port_if_needed()
        self.port = self.useFixture(
            net_helpers.PortFixture.get(
                self.bridge, self.namespace, mac_address,
                self.neutron_port['id'], hybrid_plug)).port

        for fixed_ip in self.neutron_port['fixed_ips']:
            self._configure_ipaddress(self.neutron_port['id'], fixed_ip)

    def bind_port_if_needed(self):
        if self.neutron_port[pbs.VIF_TYPE] == pbs.VIF_TYPE_UNBOUND:
            self.safe_client.client.update_port(
                self.neutron_port['id'],
                {'port': {pbs.HOST_ID: self.host.hostname}})
            self.addCleanup(self.safe_client.client.update_port,
                            self.neutron_port['id'],
                            {'port': {pbs.HOST_ID: ''}})

    def _get_bridge(self):
        if self.bridge_name is None:
            return self.host.get_bridge()
        agent_type = self.host.host_desc.l2_agent_type
        if agent_type != constants.AGENT_TYPE_OVS:
            raise NotImplementedError(
                "Support for %s agent is not implemented." % agent_type)
        return self.useFixture(
            net_helpers.OVSTrunkBridgeFixture(self.bridge_name)).bridge

    def _configure_ipaddress(self, port_id, fixed_ip):
        subnet_id = fixed_ip['subnet_id']
        subnet = self.safe_client.client.show_subnet(subnet_id)
        if (netaddr.IPAddress(fixed_ip['ip_address']).version ==
                constants.IP_VERSION_6):
            # v6Address/default_route is auto-configured.
            self._ipv6 = fixed_ip['ip_address']
            self.gateway_ipv6 = subnet['subnet']['gateway_ip']
            if self.use_dhcp6:
                self._configure_ipaddress_via_dhcp(
                    port_id,
                    version=constants.IP_VERSION_6)
        else:
            self._ip = fixed_ip['ip_address']
            prefixlen = netaddr.IPNetwork(subnet['subnet']['cidr']).prefixlen
            self._ip_cidr = f'{self._ip}/{prefixlen}'
            self.gateway_ip = subnet['subnet']['gateway_ip']

            if self.use_dhcp:
                self._configure_ipaddress_via_dhcp(port_id)
            else:
                self._configure_static_ipaddress()

    def _configure_static_ipaddress(self):
        self.port.addr.add(self.ip_cidr)
        if self.gateway_ip:
            net = netaddr.IPNetwork(self.ip_cidr)
            gateway_ip = netaddr.IPAddress(self.gateway_ip)
            if gateway_ip in net:
                net_helpers.set_namespace_gateway(self.port, self.gateway_ip)

    def _configure_ipaddress_via_dhcp(self, port_id,
                                      version=constants.IP_VERSION_4):
        self._start_async_dhclient(port_id, version)
        self.addCleanup(self._stop_async_dhclient)

    def _start_async_dhclient(self, port_id, version=constants.IP_VERSION_4):
        cmd = ["dhclient", '-%s' % version,
               '-lf',
               '{}/{}.lease'.format(self.host.neutron_config.temp_dir,
                                    port_id),
               '-sf', self.NO_RESOLV_CONF_DHCLIENT_SCRIPT_PATH,
               '--no-pid', '-d', self.port.name]
        self.dhclient_async = async_process.AsyncProcess(
            cmd, run_as_root=True, respawn_interval=5,
            namespace=self.namespace)
        self.dhclient_async.start()

    def _stop_async_dhclient(self):
        if not self.dhclient_async:
            return
        try:
            self.dhclient_async.stop()
            cmd = self.dhclient_async.cmd
            stdout = list(self.dhclient_async.iter_stdout())
            stderr = list(self.dhclient_async.iter_stderr())
            LOG.debug('Stopping async dhclient [%(cmd)s]. stdout: '
                      '[%(stdout)s] - stderr: [%(stderr)s]',
                      {'cmd': cmd, 'stdout': stdout, 'stderr': stderr})
        except async_process.AsyncProcessException:
            # If it was already stopped than we don't care about it
            pass

    @property
    def ipv6(self):
        return self._ipv6

    @property
    def ip(self):
        return self._ip

    @property
    def ip_cidr(self):
        return self._ip_cidr

    def ip_configured(self):
        for port_ip in self.port.addr.list(ip_version=constants.IP_VERSION_4):
            if port_ip.get('cidr') == self.ip_cidr:
                return True
        return False

    def gateway_configured(self):
        gateway_info = self.port.route.get_gateway()
        if not gateway_info:
            return False
        return gateway_info.get('via') == self.gateway_ip

    def block_until_boot(self):
        fullstack_base.wait_until_true(
            lambda: (self.safe_client.client.show_port(self.neutron_port['id'])
                     ['port']['status'] == 'ACTIVE'),
            sleep=3)

    def block_until_dhcp_config_done(self):
        fullstack_base.wait_until_true(
            lambda: self.ip_configured() and self.gateway_configured(),
            exception=machine_fixtures.FakeMachineException(
                "Address %s or gateway %s not configured properly on "
                "port %s" % (self.ip_cidr, self.gateway_ip, self.port.name)
            )
        )

    def destroy(self, delete_port=False):
        """Destroy this fake machine.

        This should simulate deletion of a vm. It doesn't call cleanUp().
        """
        if delete_port:
            self.safe_client.client.delete_port(self.neutron_port['id'])
        else:
            self.safe_client.client.update_port(self.neutron_port['id'],
                                                {'port': {pbs.HOST_ID: ''}})
        # All associated vlan interfaces are deleted too
        self.bridge.delete_port(self.port.name)
        ip_lib.delete_network_namespace(self.namespace)


class FakeFullstackTrunkMachine(FakeFullstackMachine):
    def __init__(self, trunk, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.trunk = trunk

    def add_vlan_interface(self, mac_address, ip_address, segmentation_id):
        """Add VLAN interface to VM's namespace.

        :param mac_address: MAC address to be set on VLAN interface.
        :param ip_address: The IPNetwork instance containing IP address
                           assigned to the interface.
        :param segmentation_id: VLAN tag added to the interface.
        """
        net_helpers.create_vlan_interface(
            self.namespace, self.port.name, mac_address, ip_address,
            segmentation_id)
