# Copyright 2018 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

from neutron_lib import constants
from oslo_log import log as logging
from oslo_utils import uuidutils

from neutron.agent.common import ovs_lib
from neutron.agent.linux import iptables_firewall
from neutron.agent.linux import iptables_manager
from neutron.agent.linux.openvswitch_firewall import iptables as ovs_iptables
from neutron.common import utils
from neutron.tests.common import machine_fixtures
from neutron.tests.fullstack import base
from neutron.tests.fullstack.resources import environment
from neutron.tests.fullstack.resources import machine

LOG = logging.getLogger(__name__)


class IptablesNotConfiguredException(Exception):
    pass


class VmsUnreachableException(Exception):
    pass


class FirewallMigrationTestCase(base.BaseFullStackTestCase):
    def setUp(self):
        host_descriptions = [
            environment.HostDescription(
                l3_agent=False,
                l2_agent_type=constants.AGENT_TYPE_OVS,
                firewall_driver='iptables_hybrid',
                dhcp_agent=False,
            )]
        env = environment.Environment(
            environment.EnvironmentDescription(),
            host_descriptions)
        super(FirewallMigrationTestCase, self).setUp(env)
        # fullstack doesn't separate nodes running ovs agent so iptables rules
        # are implemented in root namespace
        self.iptables_manager = iptables_manager.IptablesManager()

    def _prepare_resources(self):
        self.tenant_uuid = uuidutils.generate_uuid()
        network = self.safe_client.create_network(self.tenant_uuid)
        self.safe_client.create_subnet(
            self.tenant_uuid, network['id'], '20.0.0.0/24', enable_dhcp=False)
        vms = machine.FakeFullstackMachinesList(
            self.useFixture(
                machine.FakeFullstackMachine(
                    self.environment.hosts[0],
                    network['id'],
                    self.tenant_uuid,
                    self.safe_client,
                    use_dhcp=False))
            for i in range(2))
        vms.block_until_all_boot()

        for vm in vms:
            self._add_icmp_security_group_rule(vm)

        return vms

    def _add_icmp_security_group_rule(self, vm):
        sg_id = self.safe_client.create_security_group(self.tenant_uuid)['id']
        self.safe_client.create_security_group_rule(
            self.tenant_uuid, sg_id,
            direction=constants.INGRESS_DIRECTION,
            ethertype=constants.IPv4,
            protocol=constants.PROTO_NAME_ICMP)
        self.safe_client.client.update_port(
            vm.neutron_port['id'],
            body={'port': {'security_groups': [sg_id]}})
        self.addCleanup(
            self.safe_client.client.update_port,
            vm.neutron_port['id'],
            body={'port': {'security_groups': []}})

    def _validate_iptables_rules(self, vms):
        """Check if rules from iptables firewall are configured.

        Raises IptablesNotConfiguredException exception if no rules are found.
        """
        for vm in vms:
            vm_tap_device = iptables_firewall.get_hybrid_port_name(
                vm.neutron_port['id'])
            filter_rules = self.iptables_manager.get_rules_for_table('filter')
            if not any(vm_tap_device in line for line in filter_rules):
                raise IptablesNotConfiguredException(
                    "There are no iptables rules configured for interface %s" %
                    vm_tap_device)

    def _switch_firewall(self, firewall_driver):
        """Switch firewall_driver to given driver and restart the agent."""
        l2_agent = self.environment.hosts[0].l2_agent
        l2_agent_config = l2_agent.agent_cfg_fixture.config
        l2_agent_config['securitygroup']['firewall_driver'] = firewall_driver
        l2_agent.agent_cfg_fixture.write_config_to_configfile()
        l2_agent.restart()

        int_bridge = ovs_lib.OVSBridge(
            l2_agent_config['ovs']['integration_bridge'])
        predicate = functools.partial(
            ovs_iptables.is_bridge_cleaned, int_bridge)
        utils.wait_until_true(
            predicate,
            exception=RuntimeError(
                "Bridge %s hasn't been marked as clean." % int_bridge.br_name))

    def test_migration(self):
        vms = self._prepare_resources()
        # Make sure ICMP packets can get through with iptables firewall
        vms.ping_all()
        self._validate_iptables_rules(vms)
        self._switch_firewall('openvswitch')
        # Make sure security groups still work after migration
        vms.ping_all()

        self.assertRaises(
            IptablesNotConfiguredException, self._validate_iptables_rules, vms)

        # Remove security groups so traffic cannot get through
        for vm in vms:
            self.safe_client.client.update_port(
                vm.neutron_port['id'],
                body={'port': {'security_groups': []}})

        # TODO(jlibosva): Test all permutations and don't fail on the first one
        self.assertRaises(machine_fixtures.FakeMachineException, vms.ping_all)

        # Add back some security groups allowing ICMP and test traffic can now
        # get through
        for vm in vms:
            self._add_icmp_security_group_rule(vm)
        vms.ping_all()
