# Copyright 2016 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 functools

import netaddr
from neutron_lib import constants
from oslo_utils import uuidutils

from neutron.common import utils
from neutron.services.trunk.drivers.openvswitch.agent import ovsdb_handler
from neutron.services.trunk.drivers.openvswitch.agent import trunk_manager
from neutron.services.trunk.drivers.openvswitch import utils as trunk_ovs_utils
from neutron.tests.fullstack import base
from neutron.tests.fullstack.resources import environment
from neutron.tests.fullstack.resources import machine


def trunk_bridge_does_not_exist(trunk_id):
    """Return true if trunk bridge for given ID does not exists."""
    bridge = trunk_manager.TrunkBridge(trunk_id)
    return not bridge.exists()


def make_ip_network(port, network):
    """Make an IPNetwork object from port and network.

    Function returns IPNetwork object containing fixed IP address from port
    dictionary with prefixlen from network object.

    :param port: Port dictionary returned by Neutron API
    :param network: IPNetwork object in which the port's IP will be assigned.
    """
    ip_address = netaddr.IPAddress(
        port['fixed_ips'][0]['ip_address'])
    return netaddr.IPNetwork(
        (ip_address.value, network.prefixlen))


class TrunkTestException(Exception):
    pass


class Network(object):
    """A helper class to keep persistent info about assigned addresses."""
    def __init__(self, prefix, network_cidr, tag=None):
        self.prefix = prefix
        self.network = netaddr.IPNetwork(network_cidr)
        self.neutron_network = None
        self.neutron_subnet = None
        self.tag = tag
        # Currently, only vlan is supported. Pass via __init__ once more are
        # supported.
        self.segmentation_type = 'vlan'

    @property
    def cidr(self):
        return str(self.network.cidr)

    @property
    def gateway(self):
        """Return lowest possible IP in the given subnet."""
        return str(netaddr.IPAddress(self.network.first + 1))

    @property
    def id(self):
        return self.neutron_network['id']

    @property
    def name(self):
        return "%s-network" % self.prefix

    @property
    def subnet_name(self):
        return "%s-subnet" % self.prefix


class TestTrunkPlugin(base.BaseFullStackTestCase):
    def setUp(self):
        host_desc = [environment.HostDescription(
            l3_agent=False,
            l2_agent_type=constants.AGENT_TYPE_OVS)]
        env_desc = environment.EnvironmentDescription(service_plugins='trunk')
        env = environment.Environment(env_desc, host_desc)
        super(TestTrunkPlugin, self).setUp(env)

        self.tenant_id = uuidutils.generate_uuid()
        self.trunk_network = Network('trunk', '10.0.0.0/24')
        self.vlan1_network = Network('vlan1', '192.168.0.0/24', tag=10)
        self.vlan2_network = Network('vlan2', '192.168.1.0/24', tag=20)

        self.host = self.environment.hosts[0]

        for network in (
                self.trunk_network, self.vlan1_network, self.vlan2_network):
            self.create_network_and_subnet(network)

    def create_network_and_subnet(self, network):
        """Create network and subnet resources in Neutron based on network
           object.

        The resource names will be <prefix>-network and <prefix>-subnet, where
        prefix is taken from network object.

        :param network: Network object from this module.
        """
        network.neutron_network = self.safe_client.create_network(
            self.tenant_id, network.name)
        network.neutron_subnet = self.safe_client.create_subnet(
            self.tenant_id,
            network.id,
            cidr=network.cidr,
            gateway_ip=network.gateway,
            name=network.subnet_name,
            enable_dhcp=False)

    def create_vlan_aware_vm(self, trunk_network, vlan_networks):
        """Create a fake machine with one untagged port and subports
        according vlan_networks parameter.

        :param trunk_network: Instance of Network where trunk port should be
                              created.
        :param vlan_networks: List of Network instances where subports should
                              be created.
        """
        trunk_parent_port = self.safe_client.create_port(
            self.tenant_id, trunk_network.id)

        vlan_subports = [
            self.safe_client.create_port(self.tenant_id, vlan_network.id,
                mac_address=trunk_parent_port['mac_address'])
            for vlan_network in vlan_networks]

        trunk = self.safe_client.create_trunk(
            self.tenant_id,
            name='mytrunk',
            port_id=trunk_parent_port['id'],
            sub_ports=[
                {'port_id': vlan_subport['id'],
                 'segmentation_type': 'vlan',
                 'segmentation_id': vlan_network.tag}
                for vlan_subport, vlan_network in zip(vlan_subports,
                                                      vlan_networks)
            ],
        )

        vm = self.useFixture(
            machine.FakeFullstackTrunkMachine(
                trunk,
                self.host,
                trunk_network.id,
                self.tenant_id,
                self.safe_client,
                neutron_port=trunk_parent_port,
                bridge_name=trunk_ovs_utils.gen_trunk_br_name(trunk['id'])))

        for port, vlan_network in zip(vlan_subports, vlan_networks):
            ip_network = make_ip_network(port, vlan_network.network)
            vm.add_vlan_interface(
                port['mac_address'], ip_network, vlan_network.tag)
        vm.block_until_boot()

        return vm

    def create_vm_in_network(self, network):
        """Create a fake machine in given network."""
        return self.useFixture(
            machine.FakeFullstackMachine(
                self.host,
                network.id,
                self.tenant_id,
                self.safe_client
            )
        )

    def add_subport_to_vm(self, vm, subport_network):
        """Add subport from subport_network to given vm.

        :param vm: FakeFullstackMachine instance to with subport should be
                   added.
        :param subport_network: Network object representing network containing
                                port for subport.
        """
        subport = self.safe_client.create_port(
            self.tenant_id, subport_network.id,
            mac_address=vm.neutron_port['mac_address'])
        subport_spec = {
            'port_id': subport['id'],
            'segmentation_type': subport_network.segmentation_type,
            'segmentation_id': subport_network.tag
        }

        self.safe_client.trunk_add_subports(
            self.tenant_id, vm.trunk['id'], [subport_spec])
        ip_network = make_ip_network(subport, subport_network.network)
        vm.add_vlan_interface(
            subport['mac_address'], ip_network, subport_network.tag)

    # NOTE(slaweq): As is described in bug
    # https://bugs.launchpad.net/neutron/+bug/1687709 when more than one
    # different ovs-agent with enabled trunk driver is running at a time it
    # might lead to race contitions between them.
    # Because of that ovs_agent used for fullstack tests is monkeypatched and
    # loads trunk driver only if trunk service plugin is enabled.
    # That makes restriction that only a single set of tests with trunk-enabled
    # services will run at the same time.
    def test_trunk_lifecycle(self):
        """Test life-cycle of a fake VM with trunk port.

        This test uses 4 fake machines:
          - vlan_aware_vm (A) that is at the beginning connected to a trunk
            network and a vlan1 network.
          - trunk_network_vm (B) that is connected to the trunk network.
          - vlan1_network_vm (C) that is connected to the vlan1 network.
          - vlan2_network_vm (D) that is connected to a vlan2 network.

        Scenario steps:
          - all the vms from above are created
          - A can talk with B (over the trunk network)
          - A can talk with C (over the vlan1 network)
          - A can not talk with D (no leg on the vlan2 network)

          - subport from the vlan2 network is added to A
          - A can now talk with D (over the vlan2 network)

          - subport from the vlan1 network is removed from A
          - A can talk with B (over the trunk network)
          - A can not talk with C (no leg on the vlan1 network)
          - A can talk with D (over the vlan2 network)

          - A is deleted which leads to removal of trunk bridge
          - no leftovers like patch ports to the trunk bridge should remain on
            an integration bridge
        """

        vlan_aware_vm = self.create_vlan_aware_vm(
            self.trunk_network,
            [self.vlan1_network]
        )
        trunk_id = vlan_aware_vm.trunk['id']

        # Create helper vms with different networks
        trunk_network_vm = self.create_vm_in_network(self.trunk_network)
        vlan1_network_vm = self.create_vm_in_network(self.vlan1_network)
        vlan2_network_vm = self.create_vm_in_network(self.vlan2_network)

        for vm in trunk_network_vm, vlan1_network_vm, vlan2_network_vm:
            vm.block_until_boot()

        # Test connectivity to trunk and subport
        vlan_aware_vm.block_until_ping(trunk_network_vm.ip)
        vlan_aware_vm.block_until_ping(vlan1_network_vm.ip)

        # Subport for vlan2 hasn't been added yet
        vlan_aware_vm.block_until_no_ping(vlan2_network_vm.ip)

        # Add another subport and test
        self.add_subport_to_vm(vlan_aware_vm, self.vlan2_network)
        vlan_aware_vm.block_until_ping(vlan2_network_vm.ip)

        # Remove the first subport
        self.safe_client.trunk_remove_subports(
            self.tenant_id,
            trunk_id,
            [vlan_aware_vm.trunk['sub_ports'][0]])

        # vlan1_network_vm now shouldn't be able to talk to vlan_aware_vm
        vlan_aware_vm.block_until_no_ping(vlan1_network_vm.ip)

        # but trunk and vlan2 should be able to ping
        vlan_aware_vm.block_until_ping(trunk_network_vm.ip)
        vlan_aware_vm.block_until_ping(vlan2_network_vm.ip)

        # Delete vm and check that patch ports and trunk bridge are gone
        vlan_aware_vm.destroy()
        bridge_doesnt_exist_predicate = functools.partial(
            trunk_bridge_does_not_exist, trunk_id)
        utils.wait_until_true(
            bridge_doesnt_exist_predicate,
            exception=TrunkTestException(
                'Trunk bridge with ID %s has not been removed' %
                trunk_id)
        )

        integration_bridge = self.host.get_bridge(None)
        no_patch_ports_predicate = functools.partial(
            lambda bridge: not ovsdb_handler.bridge_has_service_port(bridge),
            integration_bridge,
        )
        try:
            utils.wait_until_true(no_patch_ports_predicate)
        except utils.WaitTimeout:
            # Create exception object after timeout to provide up-to-date list
            # of interfaces
            raise TrunkTestException(
                "Integration bridge %s still has following ports while some of"
                " them are patch ports for trunk that were supposed to be "
                "removed: %s" % (
                    integration_bridge.br_name,
                    integration_bridge.get_iface_name_list()
                )
            )
