# Copyright 2015 Hewlett-Packard Development Company, L.P.
#
# 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 concurrent.futures
import re
import threading
import time

from oslo_utils import uuidutils
from rally.common import logging
from rally.plugins.openstack import scenario as rally_base
from rally.task import atomic

import vpn_utils

LOG = logging.getLogger(__name__)
LOCK = threading.RLock()
MAX_RESOURCES = 2


class VpnBase(rally_base.OpenStackScenario):

    def setup(self, **kwargs):
        """Create and initialize data structures to hold various resources"""

        with LOCK:
            LOG.debug('SETUP RESOURCES')
            self.neutron_admin_client = self.admin_clients("neutron")
            if kwargs['use_admin_client']:
                self.neutron_client = self.neutron_admin_client
                self.keystone_client = self.admin_clients("keystone")
                self.nova_client = self.admin_clients("nova")
            else:
                self.neutron_client = self.clients("neutron")
                self.nova_client = self.clients("nova")
            self.suffixes = [uuidutils.generate_uuid(),
                             uuidutils.generate_uuid()]
            self.remote_key_files = ['rally_keypair_' + x
                                     for x in self.suffixes]
            self.local_key_files = ['/tmp/' + x for x in self.remote_key_files]
            self.private_key_file = kwargs["private_key"]
            self.keypairs = []
            self.tenant_ids = []
            self.ns_controller_tuples = []
            self.qrouterns_compute_tuples = []
            self.router_ids = []
            self.rally_router_gw_ips = []
            self.rally_routers = []
            self.rally_networks = []
            self.rally_subnets = []
            self.rally_cidrs = []
            self.ike_policy = None
            self.ipsec_policy = None
            self.vpn_services = []
            self.ipsec_site_connections = []
            self.servers = []
            self.server_private_ips = []
            self.server_fips = []

    def create_tenants(self):
        """Create tenants"""

        for x in range(MAX_RESOURCES):
            tenant_id = vpn_utils.create_tenant(
                self.keystone_client, self.suffixes[x])
            with LOCK:
                self.tenant_ids.append(tenant_id)

    def create_networks(self, **kwargs):
        """Create networks to test vpn connectivity"""

        for x in range(MAX_RESOURCES):
            if self.tenant_ids:
                router, network, subnet, cidr = vpn_utils.create_network(
                    self.neutron_client, self.neutron_admin_client,
                    self.suffixes[x], tenant_id=self.tenant_ids[x],
                    DVR_flag=kwargs["DVR_flag"],
                    ext_net_name=kwargs["ext-net"])
            else:
                router, network, subnet, cidr = vpn_utils.create_network(
                    self.neutron_client, self.neutron_admin_client,
                    self.suffixes[x], DVR_flag=kwargs["DVR_flag"],
                    ext_net_name=kwargs["ext-net"])
            with LOCK:
                self.rally_cidrs.append(cidr)
                self.rally_subnets.append(subnet)
                self.rally_networks.append(network)
                self.rally_routers.append(router)
                self.router_ids.append(router["router"]['id'])
                self.rally_router_gw_ips.append(
                    router["router"]["external_gateway_info"]
                    ["external_fixed_ips"][0]["ip_address"])

            if kwargs["DVR_flag"]:
                ns, controller = vpn_utils.wait_for_namespace_creation(
                    "snat-", router["router"]['id'],
                    kwargs['controller_creds'],
                    self.private_key_file,
                    kwargs['namespace_creation_timeout'])
            else:
                ns, controller = vpn_utils.wait_for_namespace_creation(
                    "qrouter-", router["router"]['id'],
                    kwargs['controller_creds'],
                    self.private_key_file,
                    kwargs['namespace_creation_timeout'])
            with LOCK:
                self.ns_controller_tuples.append((ns, controller))

    def create_servers(self, **kwargs):
        """Create servers"""

        for x in range(MAX_RESOURCES):
            kwargs.update({
                "nics":
                    [{"net-id": self.rally_networks[x]["network"]["id"]}],
                "sec_group_suffix": self.suffixes[x],
                "server_suffix": self.suffixes[x]
            })
            keypair = vpn_utils.create_keypair(
                self.nova_client, self.suffixes[x])
            server = vpn_utils.create_server(
                self.nova_client, keypair, **kwargs)
            vpn_utils.assert_server_status(server, **kwargs)
            with LOCK:
                self.servers.append(server)
                self.keypairs.append(keypair)
                self.server_private_ips.append(vpn_utils.get_server_ip(
                    self.nova_client, server.id, self.suffixes[x]))
            if kwargs["DVR_flag"]:
                qrouter, compute = vpn_utils.wait_for_namespace_creation(
                    "qrouter-", self.router_ids[x],
                    kwargs['compute_creds'],
                    self.private_key_file,
                    kwargs['namespace_creation_timeout'])

                vpn_utils.write_key_to_compute_node(
                    keypair, self.local_key_files[x],
                    self.remote_key_files[x], compute,
                    self.private_key_file)
                with LOCK:
                    self.qrouterns_compute_tuples.append((qrouter, compute))
            else:
                vpn_utils.write_key_to_local_path(self.keypairs[x],
                                                  self.local_key_files[x])
                fip = vpn_utils.add_floating_ip(self.nova_client, server)
                with LOCK:
                    self.server_fips.append(fip)

    def check_route(self):
        """Verify route exists between the router gateways"""

        LOG.debug("VERIFY ROUTE EXISTS BETWEEN THE ROUTER GATEWAYS")
        for tuple in self.ns_controller_tuples:
            for ip in self.rally_router_gw_ips:
                assert vpn_utils.ping_router_gateway(
                        tuple, ip, self.private_key_file), (
                        "PING TO IP " + ip + " FAILED")

    @atomic.action_timer("_create_ike_policy")
    def _create_ike_policy(self, **kwargs):
        """Create IKE policy

        :return: IKE policy
        """
        LOG.debug('CREATING IKE_POLICY')
        ike_policy = self.neutron_client.create_ikepolicy({
            "ikepolicy": {
                "phase1_negotiation_mode":
                    kwargs.get("phase1_negotiation_mode", "main"),
                "auth_algorithm": kwargs.get("auth_algorithm", "sha1"),
                "encryption_algorithm":
                    kwargs.get("encryption_algorithm", "aes-128"),
                "pfs": kwargs.get("pfs", "group5"),
                "lifetime": {
                    "units": "seconds",
                    "value": kwargs.get("value", 7200)},
                "ike_version": kwargs.get("ike_version", "v1"),
                "name": "rally_ikepolicy"
            }
        })
        return ike_policy

    @atomic.action_timer("_create_ipsec_policy")
    def _create_ipsec_policy(self, **kwargs):
        """Create IPSEC policy

        :return: IPSEC policy
        """
        LOG.debug('CREATING IPSEC_POLICY')
        ipsec_policy = self.neutron_client.create_ipsecpolicy({
            "ipsecpolicy": {
                "name": "rally_ipsecpolicy",
                "transform_protocol": kwargs.get("transform_protocol", "esp"),
                "auth_algorithm": kwargs.get("auth_algorithm", "sha1"),
                "encapsulation_mode":
                    kwargs.get("encapsulation_mode", "tunnel"),
                "encryption_algorithm":
                    kwargs.get("encryption_algorithm", "aes-128"),
                "pfs": kwargs.get("pfs", "group5"),
                "lifetime": {
                    "units": "seconds",
                    "value": kwargs.get("value", 7200)
                }
            }
        })
        return ipsec_policy

    @atomic.action_timer("_create_vpn_service")
    def _create_vpn_service(self, rally_subnet, rally_router, vpn_suffix=None):
        """Create VPN service endpoints

        :param rally_subnet: local subnet
        :param rally_router: router endpoint
        :param vpn_suffix: suffix name for vpn service
        :return: VPN service
        """
        LOG.debug('CREATING VPN_SERVICE')
        vpn_service = self.neutron_client.create_vpnservice({
            "vpnservice": {
                "subnet_id": rally_subnet["subnet"]["id"],
                "router_id": rally_router["router"]["id"],
                "name": "rally_vpn_service_" + vpn_suffix,
                "admin_state_up": True
            }
        })
        return vpn_service

    def create_vpn_services(self):
        """Create VPN services"""

        for x in range(MAX_RESOURCES):
            vpn_service = self._create_vpn_service(
                self.rally_subnets[x], self.rally_routers[x], self.suffixes[x])
            with LOCK:
                self.vpn_services.append(vpn_service)

    @atomic.action_timer("_create_ipsec_site_connection")
    def _create_ipsec_site_connection(self, local_index, peer_index, **kwargs):
        """Create IPSEC site connection

        :param local_index: parameter to point to the local end-point
        :param peer_index: parameter to point to the peer end-point
        :return: IPSEC site connection
        """
        LOG.debug('CREATING IPSEC_SITE_CONNECTION')
        ipsec_site_conn = self.neutron_client.create_ipsec_site_connection({
            "ipsec_site_connection": {
                "psk": kwargs.get("secret", "secret"),
                "initiator": "bi-directional",
                "ipsecpolicy_id": self.ipsec_policy["ipsecpolicy"]["id"],
                "admin_state_up": True,
                "peer_cidrs": self.rally_cidrs[peer_index],
                "mtu": kwargs.get("mtu", "1500"),
                "ikepolicy_id": self.ike_policy["ikepolicy"]["id"],
                "dpd": {
                    "action": "disabled",
                    "interval": 60,
                    "timeout": 240
                },
                "vpnservice_id":
                    self.vpn_services[local_index]["vpnservice"]["id"],
                "peer_address": self.rally_router_gw_ips[peer_index],
                "peer_id": self.rally_router_gw_ips[peer_index],
                "name": "rally_ipsec_site_connection_" +
                        self.suffixes[local_index]
            }
        })
        return ipsec_site_conn

    def create_ipsec_site_connections(self, **kwargs):
        """Create IPSEC site connections"""

        a = self._create_ipsec_site_connection(0, 1, **kwargs)
        b = self._create_ipsec_site_connection(1, 0, **kwargs)
        with LOCK:
            self.ipsec_site_connections = [a, b]

    def _get_resource(self, resource_tag, resource_id):
        """Get the resource(vpn_service or ipsec_site_connection)

        :param resource_tag: "vpnservice" or "ipsec_site_connection"
        :param resource_id: id of the resource
        :return: resource (vpn_service or ipsec_site_connection)
        """
        if resource_tag == "vpnservice":
            vpn_service = self.neutron_client.show_vpnservice(resource_id)
            if vpn_service:
                return vpn_service
        elif resource_tag == 'ipsec_site_connection':
            ipsec_site_conn = self.neutron_client.show_ipsec_site_connection(
                resource_id)
            if ipsec_site_conn:
                return ipsec_site_conn

    def _wait_for_status_change(self, resource, resource_tag, final_status,
                                wait_timeout=60, check_interval=1):
        """Wait for resource's status change

        Wait till the status of the resource changes to final state or till
        the time exceeds the wait_timeout value.
        :param resource: resource whose status has to be checked
        :param final_status: desired final status of the resource
        :param resource_tag: to identify the resource as vpnservice or
                             ipsec_site_connection
        :param wait_timeout: timeout value in seconds
        :param check_interval: time to sleep before each check for the status
        change
        :return: resource
        """
        LOG.debug('WAIT_FOR_%s_STATUS_CHANGE ', resource[resource_tag]['id'])

        start_time = time.time()
        while True:
            resource = self._get_resource(
                resource_tag, resource[resource_tag]['id'])
            current_status = resource[resource_tag]['status']
            if current_status == final_status:
                return resource
            time.sleep(check_interval)
            if time.time() - start_time > wait_timeout:
                raise Exception(
                    "Timeout waiting for resource {} to change to {} status".
                    format(resource[resource_tag]['name'], final_status))

    @atomic.action_timer("wait_time_for_status_change")
    def _assert_statuses(self, ipsec_site_conn, vpn_service,
                         final_status, **kwargs):
        """Assert statuses of vpn_service and ipsec_site_connection

        :param ipsec_site_conn: ipsec_site_connection object
        :param vpn_service: vpn_service object
        :param final_status: status of vpn and ipsec_site_connection object
        """

        vpn_service = self._wait_for_status_change(
            vpn_service,
            resource_tag="vpnservice",
            final_status=final_status,
            wait_timeout=kwargs.get("vpn_service_creation_timeout"),
            check_interval=5)

        ipsec_site_conn = self._wait_for_status_change(
            ipsec_site_conn,
            resource_tag="ipsec_site_connection",
            final_status=final_status,
            wait_timeout=kwargs.get("ipsec_site_connection_creation_timeout"),
            check_interval=5)

        LOG.debug("VPN SERVICE STATUS %s", vpn_service['vpnservice']['status'])
        LOG.debug("IPSEC_SITE_CONNECTION STATUS %s",
                  ipsec_site_conn['ipsec_site_connection']['status'])

    def assert_statuses(self, final_status, **kwargs):
        """Assert active statuses for VPN services and VPN connections

        :param final_status: the final status you expect the resource to be in
        """

        LOG.debug("ASSERTING ACTIVE STATUSES FOR VPN-SERVICES AND "
                  "IPSEC-SITE-CONNECTIONS")
        for x in range(MAX_RESOURCES):
            self._assert_statuses(
                self.ipsec_site_connections[x], self.vpn_services[x],
                final_status, **kwargs)

    def _get_qg_interface(self, peer_index):
        """Get the qg- interface

        :param peer_index: parameter to point to the local end-point
        :return: qg-interface
        """
        qg = vpn_utils.get_interfaces(
            self.ns_controller_tuples[peer_index],
            self.private_key_file)
        p = re.compile(r"qg-\w+-\w+")
        for line in qg:
            m = p.search(line)
            if m:
                return m.group()
        return None

    @atomic.action_timer("_verify_vpn_connection")
    def _verify_vpn_connectivity(self, local_index, peer_index, **kwargs):
        """Verify the vpn connectivity between the endpoints

        Get the qg- interface from the snat namespace corresponding to the
        peer router and start a tcp dump. Concurrently, SSH into the nova
        instance  on the local subnet from the qrouter namespace and try
        to ping the nova instance on the peer subnet. Inspect the captured
        packets to see if they are encrypted.
        :param local_index: parameter to point to the local end-point
        :param peer_index: parameter to point to the peer end-point
        :return: True if vpn connectivity test passes
                 False if the test fails
        """
        qg_interface = self._get_qg_interface(peer_index)
        if qg_interface:
            with concurrent.futures.ThreadPoolExecutor(max_workers=2) as e:
                tcpdump_future = e.submit(vpn_utils.start_tcpdump,
                         self.ns_controller_tuples[peer_index],
                         qg_interface, self.private_key_file)
                if kwargs["DVR_flag"]:
                    ssh_future = e.submit(
                        vpn_utils.ssh_and_ping_server,
                        self.server_private_ips[local_index],
                        self.server_private_ips[peer_index],
                        self.qrouterns_compute_tuples[local_index],
                        self.remote_key_files[local_index],
                        self.private_key_file)
                else:
                    ssh_future = e.submit(
                        vpn_utils.ssh_and_ping_server_with_fip,
                        self.server_fips[local_index],
                        self.server_private_ips[peer_index],
                        self.local_key_files[local_index],
                        self.private_key_file)

            assert ssh_future.result(), "SSH/Ping failed"
            for line in tcpdump_future.result():
                if 'ESP' in line:
                    return True
        return False

    def verify_vpn_connectivity(self, **kwargs):
        """Verify VPN connectivity"""

        LOG.debug("VERIFY THE VPN CONNECTIVITY")
        with LOCK:
            assert self._verify_vpn_connectivity(
                0, 1, **kwargs), "VPN CONNECTION FAILED"
        with LOCK:
            assert self._verify_vpn_connectivity(
                1, 0, **kwargs), "VPN CONNECTION FAILED"

    def update_router(self, router_id, admin_state_up=False):
        """Update router's admin_state_up field

        :param router_id: uuid of the router
        :param admin_state_up: True or False
        """
        LOG.debug('UPDATE ROUTER')
        router_args = {'router': {'admin_state_up': admin_state_up}}
        self.neutron_client.update_router(router_id, router_args)

    @atomic.action_timer("_delete_ipsec_site_connection")
    def _delete_ipsec_site_connections(self):
        """Delete IPSEC site connections"""

        for site_conn in self.ipsec_site_connections:
            LOG.debug("DELETING IPSEC_SITE_CONNECTION %s",
                      site_conn['ipsec_site_connection']['id'])
            self.neutron_client.delete_ipsec_site_connection(
                site_conn['ipsec_site_connection']['id'])

    @atomic.action_timer("_delete_vpn_service")
    def _delete_vpn_services(self):
        """Delete VPN service endpoints"""

        for vpn_service in self.vpn_services:
            LOG.debug("DELETING VPN_SERVICE %s",
                      vpn_service['vpnservice']['id'])
            self.neutron_client.delete_vpnservice(
                vpn_service['vpnservice']['id'])

    @atomic.action_timer("_delete_ipsec_policy")
    def _delete_ipsec_policy(self):
        """Delete IPSEC policy"""

        LOG.debug("DELETING IPSEC POLICY")
        if self.ipsec_policy:
            self.neutron_client.delete_ipsecpolicy(
                self.ipsec_policy['ipsecpolicy']['id'])

    @atomic.action_timer("_delete_ike_policy")
    def _delete_ike_policy(self):
        """Delete IKE policy"""

        LOG.debug('DELETING IKE POLICY')
        if self.ike_policy:
            self.neutron_client.delete_ikepolicy(
                self.ike_policy['ikepolicy']['id'])

    @atomic.action_timer("cleanup")
    def cleanup(self):
        """Clean the resources"""

        vpn_utils.delete_servers(self.nova_client, self.servers)
        if self.server_fips:
            vpn_utils.delete_floating_ips(self.nova_client, self.server_fips)
        vpn_utils.delete_keypairs(self.nova_client, self.keypairs)

        if self.qrouterns_compute_tuples:
            vpn_utils.delete_hosts_from_knownhosts_file(
                self.server_private_ips, self.qrouterns_compute_tuples,
                self.private_key_file)
            vpn_utils.delete_keyfiles(
                self.local_key_files, self.remote_key_files,
                self.qrouterns_compute_tuples, self.private_key_file)
        else:
            vpn_utils.delete_hosts_from_knownhosts_file(
                    self.server_private_ips)
            vpn_utils.delete_keyfiles(self.local_key_files)

        self._delete_ipsec_site_connections()
        self._delete_vpn_services()
        self._delete_ipsec_policy()
        self._delete_ike_policy()
        vpn_utils.delete_networks(
            self.neutron_client, self.neutron_admin_client, self.rally_routers,
            self.rally_networks, self.rally_subnets)
        if self.tenant_ids:
            vpn_utils.delete_tenants(self.keystone_client, self.tenant_ids)
