# Copyright 2016 VMware 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 collections

from oslo_log import log as logging

from tempest import config
from tempest.lib.common.utils import data_utils
from tempest.lib.common.utils import test_utils
from tempest.scenario import manager
from tempest import test

from vmware_nsx_tempest._i18n import _LE

CONF = config.CONF

LOG = logging.getLogger(__name__)

Floating_IP_tuple = collections.namedtuple('Floating_IP_tuple',
                                           ['floating_ip', 'server'])


class TestMicroSegmentationOps(manager.NetworkScenarioTest):

    """Test suite for micro-segmentation scenario test

    The purpose of this scenario test is to test micro-segmentation use
    case which is one of the most important features of NSX. In the test,
    two-tier application web and app networks are created, and security
    group rules are defined based on this use case. Verify that VMs on
    these networks have the correct behaviors as expected.

    """

    @classmethod
    def skip_checks(cls):
        super(TestMicroSegmentationOps, cls).skip_checks()
        if not (CONF.network.project_networks_reachable or
                CONF.network.public_network_id):
            msg = ('Either project_networks_reachable must be "true", or '
                   'public_network_id must be defined.')
            raise cls.skipException(msg)
        for ext in ['router', 'security-group']:
            if not test.is_extension_enabled(ext, 'network'):
                msg = "%s extension not enabled." % ext
                raise cls.skipException(msg)
        if not (CONF.network.public_network_cidr):
            msg = "public_network_cidr must be defined in network section."
            raise cls.skipException(msg)

    @classmethod
    def setup_credentials(cls):
        cls.set_network_resources()
        super(TestMicroSegmentationOps, cls).setup_credentials()

    def setUp(self):
        super(TestMicroSegmentationOps, self).setUp()
        self.keypairs = {}

    def _create_security_groups(self):
        web_sg = self._create_empty_security_group(namestart="secgroup-web")
        app_sg = self._create_empty_security_group(namestart="secgroup-app")
        # Common rules to allow the following traffic
        # 1. Egress ICMP IPv4 any any
        # 2. Egress ICMP IPv6 any any
        # 3. Ingress ICMP IPv4 from public network
        # 4. Ingress TCP 22 (SSH) from public network
        common_ruleset = [
            dict(
                direction='egress',
                protocol='icmp'
            ),
            dict(
                direction='egress',
                protocol='icmp',
                ethertype='IPv6'
            ),
            dict(
                direction='ingress',
                protocol='tcp',
                port_range_min=22,
                port_range_max=22,
                remote_ip_prefix=CONF.network.public_network_cidr
            ),
            dict(
                direction='ingress',
                protocol='icmp',
                remote_ip_prefix=CONF.network.public_network_cidr
            )
        ]
        # Rules that are specific to web tier network
        # 1. Ingress ICMP IPv4 from web_sg
        # 2. Ingress TCP 80 (HTTP) any any
        # 3. Ingress TCP 443 (HTTPS) any any
        web_ruleset = [
            dict(
                direction='ingress',
                protocol='icmp',
                remote_group_id=web_sg['id']
            ),
            dict(
                direction='ingress',
                protocol='tcp',
                port_range_min=80,
                port_range_max=80,
            ),
            dict(
                direction='ingress',
                protocol='tcp',
                port_range_min=443,
                port_range_max=443,
            )
        ]
        web_rulesets = common_ruleset + web_ruleset
        # Rules that are specific to app tier network
        # 1. Ingress ICMP IPv4 from app_sg
        # 2. Ingress TCP 22 (SSH) from web_sg
        app_ruleset = [
            dict(
                direction='ingress',
                protocol='icmp',
                remote_group_id=app_sg['id']
            ),
            dict(
                direction='ingress',
                protocol='tcp',
                port_range_min=22,
                port_range_max=22,
                remote_group_id=web_sg['id']
            )
        ]
        app_rulesets = common_ruleset + app_ruleset
        for ruleset in web_rulesets:
            self._create_security_group_rule(secgroup=web_sg, **ruleset)
        for ruleset in app_rulesets:
            self._create_security_group_rule(secgroup=app_sg, **ruleset)
        return (web_sg, app_sg)

    def _create_network_topo(self, **kwargs):
        self.web_net, self.web_subnet, self.router = self.create_networks(
            **kwargs)
        self.app_net = self._create_network()
        self.app_subnet = self._create_subnet(network=self.app_net)
        router_id = self.router['id']
        self.routers_client.add_router_interface(
            router_id, subnet_id=self.app_subnet['id'])
        self.addCleanup(test_utils.call_and_ignore_notfound_exc,
                        self.routers_client.remove_router_interface,
                        router_id, subnet_id=self.app_subnet['id'])

    def _create_servers(self):
        web_server1_name = data_utils.rand_name('web-vm1')
        web_server2_name = data_utils.rand_name('web-vm2')
        app_server1_name = data_utils.rand_name('app-vm1')
        app_server2_name = data_utils.rand_name('app-vm2')
        # Create two VMs on web-tier network
        self.web_server1, self.web_server1_fip_tuple = self._create_server(
            web_server1_name, self.web_net, self.web_sg)
        self.web_server2, self.web_server2_fip_tuple = self._create_server(
            web_server2_name, self.web_net, self.web_sg)
        # Create two VMs on app-tier network
        self.app_server1, self.app_server1_fip_tuple = self._create_server(
            app_server1_name, self.app_net, self.app_sg)
        self.app_server2, self.app_server2_fip_tuple = self._create_server(
            app_server2_name, self.app_net, self.app_sg)

    def _setup_micro_seg_topo(self, **kwargs):
        self.web_sg, self.app_sg = self._create_security_groups()
        self._create_network_topo(**kwargs)
        self._create_servers()

    def _create_server(self, name, network, secgroup, image_id=None):
        keypair = self.create_keypair()
        self.keypairs[keypair['name']] = keypair
        security_groups = [{'name': secgroup['name']}]
        network = {'uuid': network['id']}
        server = self.create_server(name=name, networks=[network],
                                    key_name=keypair['name'],
                                    security_groups=security_groups,
                                    image_id=image_id,
                                    wait_until='ACTIVE')
        floating_ip = self.create_floating_ip(server)
        fip_tuple = Floating_IP_tuple(floating_ip, server)
        return (server, fip_tuple)

    def _get_server_key(self, server):
        return self.keypairs[server['key_name']]['private_key']

    def _check_network_internal_connectivity(self, network, fip_tuple,
                                             should_connect=True):
        floating_ip, server = fip_tuple
        # test internal connectivity to the network ports on the network
        network_ips = (p['fixed_ips'][0]['ip_address'] for p in
                       self._list_ports(tenant_id=server['tenant_id'],
                                        network_id=network['id'])
                       if p['device_owner'].startswith('network'))
        self._check_server_connectivity(floating_ip,
                                        server,
                                        network_ips,
                                        should_connect)

    def _check_network_vm_connectivity(self, network, fip_tuple,
                                       should_connect=True):
        floating_ip, server = fip_tuple
        # test internal connectivity to the other VM on the same network
        compute_ips = (p['fixed_ips'][0]['ip_address'] for p in
                       self._list_ports(tenant_id=server['tenant_id'],
                                        network_id=network['id'])
                       if p['device_owner'].startswith('compute'))
        self._check_server_connectivity(floating_ip,
                                        server,
                                        compute_ips,
                                        should_connect)

    def _check_server_connectivity(self, floating_ip, server, address_list,
                                   should_connect=True):
        ip_address = floating_ip['floating_ip_address']
        private_key = self._get_server_key(server)
        ssh_source = self.get_remote_client(ip_address,
                                            private_key=private_key)
        for remote_ip in address_list:
            if should_connect:
                msg = ("Timed out waiting for %s to become "
                       "reachable") % remote_ip
            else:
                msg = "ip address %s is reachable" % remote_ip
            try:
                self.assertTrue(self._check_remote_connectivity
                                (ssh_source, remote_ip, should_connect),
                                msg)
            except Exception:
                LOG.exception(_LE("Unable to access %{dest}s via ssh to "
                                  "floating-ip %{src}s"),
                              {'dest': remote_ip, 'src': floating_ip})
                raise

    def _check_cross_network_connectivity(self, network, should_connect=False):
        if network['id'] == self.web_net['id']:
            net_id = self.app_net['id']
            floating_ip, server = self.web_server1_fip_tuple
        else:
            net_id = self.web_net['id']
            floating_ip, server = self.app_server1_fip_tuple
        # test internal connectivity to the other VM on the same network
        remote_ips = (p['fixed_ips'][0]['ip_address'] for p in
                      self._list_ports(tenant_id=server['tenant_id'],
                                       network_id=net_id)
                      if p['device_owner'].startswith('compute'))
        self._check_server_connectivity(floating_ip,
                                        server,
                                        remote_ips,
                                        should_connect)

    @test.attr(type='common')
    @test.idempotent_id('91e1ee1f-10d9-4b19-8350-804aea7e57b4')
    def test_micro_segmentation_ops(self):
        """Test micro-segmentation use case

        Create two-tier application web and app networks, define security
        group rules based on the requirements, apply them to the VMs created
        on the network, and verify the connectivity based on the rule.

        """
        self._setup_micro_seg_topo()
        network_server_list = [
            (self.web_net, self.web_server1_fip_tuple),
            (self.web_net, self.web_server2_fip_tuple),
            (self.app_net, self.app_server1_fip_tuple),
            (self.app_net, self.app_server2_fip_tuple)
        ]
        for net, fip_tuple in network_server_list:
            self._check_network_internal_connectivity(network=net,
                                                      fip_tuple=fip_tuple)
            self._check_network_vm_connectivity(network=net,
                                                fip_tuple=fip_tuple)
        for net in [self.web_net, self.app_net]:
            self._check_cross_network_connectivity(network=net)
