"""
#
# 
#   Copyright 2016 RIFT.IO 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.
#
#
# @file test_openstack_install.py
# @author Varun Prasad (varun.prasad@riftio.com)
# @date 10/10/2015
# @brief Test Openstack/os install
#
"""

import logging
import re
import socket
import sys
import time
import tempfile

from keystoneclient.v3 import client
import paramiko
import pytest
import requests
import xmlrpc.client

from gi.repository import RwcalYang
from gi.repository.RwTypes import RwStatus
import rw_peas
import rwlogger


logger = logging.getLogger()
logging.basicConfig(level=logging.INFO)


class Host(object):
    """A wrapper on top of a host, which provides a ssh connection instance.

    Assumption:
    The username/password for the VM is default.
    """
    _USERNAME = "root"
    _PASSWORD = "riftIO"

    def __init__(self, hostname):
        """
        Args:
            hostname (str): Hostname (grunt3.qanet.riftio.com)
        """
        self.hostname = hostname
        try:
            self.ip = socket.gethostbyname(hostname)
        except socket.gaierror:
            logger.error("Unable to resolve the hostname {}".format(hostname))
            sys.exit(1)

        self.ssh = paramiko.SSHClient()
        # Note: Do not load the system keys as the test will fail if the keys
        # change.
        self.ssh.set_missing_host_key_policy(paramiko.WarningPolicy())

    def connect(self):
        """Set up ssh connection.
        """
        logger.debug("Trying to connect to {}: {}".format(
                self.hostname,
                self.ip))

        self.ssh.connect(
                self.ip,
                username=self._USERNAME,
                password=self._PASSWORD)

    def put(self, content, dest):
        """Creates a tempfile and puts it in the destination path in the HOST.
        Args:
            content (str): Content to be written to a file.
            dest (str): Path to store the content.
        """
        temp_file = tempfile.NamedTemporaryFile(delete=False)
        temp_file.write(content.encode("UTF-8"))
        temp_file.close()

        logger.info("Writing {} file in {}".format(dest, self.hostname))
        sftp = self.ssh.open_sftp()
        sftp.put(temp_file.name, dest)
        sftp.close()

    def clear(self):
        """Clean up
        """
        self.ssh.close()


class Grunt(Host):
    """A wrapper on top of grunt machine, provides functionalities to check
    if the grunt is up, IP resolution.
    """
    @property
    def grunt_name(self):
        """Extract the grunt name from the FQDN

        Returns:
            str: e.g. grunt3 from grunt3.qanet.riftio.com
        """
        return self.hostname.split(".")[0]

    @property
    def dns_server(self):
        """Hard-coded for now.
        """
        return "10.95.0.3"

    @property
    def floating_ip(self):
        return "10.95.1.0"

    @property
    def private_ip(self):
        """Construct the private IP from the grunt name. 10.0.xx.0 where xx is
        value of the grunt (3 in case of grunt3)
        """
        host_part = re.sub(r"[a-zA-z]+", "", self.grunt_name)
        return '10.0.{}.0'.format(host_part)

    def is_system_up(self):
        """Checks if system is up using ssh login.

        Returns:
            bool: Indicates if system is UP
        """
        try:
            self.connect()
        except OSError:
            return False

        return True

    def wait_till_system_is_up(self, timeout=50, check_openstack=False):
        """Blocking call to check if system is up.
        Args:
            timeout (int, optional): In mins(~).
            check_openstack (bool, optional): If true will also check if
                openstack is up and running on the system.

        Raises:
            OSError: If system start exceeds the timeout
        """

        TRY_DURATION = 20  # secs
        total_tries = timeout * (60 / TRY_DURATION)  # 3 tries/mins i.e. 20 secs.
        tries = 0

        while tries < total_tries:
            if self.is_system_up():
                if check_openstack and self.is_openstack_up():
                        return
                elif not check_openstack:
                    return

            logger.info("{} down: Sleeping for {} secs. Try {} of {}".format(
                    self.hostname,
                    TRY_DURATION,
                    tries,
                    int(total_tries)))

            time.sleep(TRY_DURATION)
            tries += 1

        raise OSError("Exception in system start {}({})".format(
                self.hostname,
                self.ip))

    def is_openstack_up(self):
        """Checks if openstack is UP, by verifying the URL.

        Returns:
            bool: Indicates if system is UP
        """
        url = "http://{}/dashboard/".format(self.ip)

        logger.info("Checking if openstack({}) is UP".format(url))

        try:
            requests.get(url)
        except requests.ConnectionError:
            return False

        return True


class Cobbler(Host):
    """A thin wrapper on cobbler and provides an interface using XML rpc client.

    Assumption:
    System instances are already added to cobbler(with ipmi). Adding instances
    can also be automated, can be taken up sometime later.
    """
    def __init__(self, hostname, username="cobbler", password="cobbler"):
        """
        Args:
            hostname (str): Cobbler host.
            username (str, optional): username.
            password (str, optional): password
        """
        super().__init__(hostname)

        url = "https://{}/cobbler_api".format(hostname)

        self.server = xmlrpc.client.ServerProxy(url)
        logger.info("obtained a cobbler instance for the host {}".format(hostname))

        self.token = self.server.login(username, password)
        self.connect()

    def create_profile(self, profile_name, ks_file):
        """Create the profile for the system.

        Args:
            profile_name (str): Name of the profile.
            ks_file (str): Path of the kick start file.
        """
        profile_attrs = {
                "name": profile_name,
                "kickstart": ks_file,
                "repos": ['riftware', 'rift-misc', 'fc21-x86_64-updates',
                          'fc21-x86_64', 'openstack-kilo'],
                "owners": ["admin"],
                "distro": "FC21.3-x86_64"
                }

        profile_id = self.server.new_profile(self.token)
        for key, value in profile_attrs.items():
            self.server.modify_profile(profile_id, key, value, self.token)
        self.server.save_profile(profile_id, self.token)

    def create_snippet(self, snippet_name, snippet_content):
        """Unfortunately the XML rpc apis don't provide a direct interface to
        create snippets, so falling back on the default sftp methods.

        Args:
            snippet_name (str): Name.
            snippet_content (str): snippet's content.

        Returns:
            str: path where the snippet is stored
        """
        path = "/var/lib/cobbler/snippets/{}".format(snippet_name)
        self.put(snippet_content, path)
        return path

    def create_kickstart(self, ks_name, ks_content):
        """Creates and returns the path of the ks file.

        Args:
            ks_name (str): Name of the ks file to be saved.
            ks_content (str): Content for ks file.

        Returns:
            str: path where the ks file is saved.
        """
        path = "/var/lib/cobbler/kickstarts/{}".format(ks_name)
        self.put(ks_content, path)
        return path

    def boot_system(self, grunt, profile_name, false_boot=False):
        """Boots the system with the profile specified. Also enable net-boot

        Args:
            grunt (Grunt): instance of grunt
            profile_name (str): A valid profile name.
            false_boot (bool, optional): debug only option.
        """
        if false_boot:
            return

        system_id = self.server.get_system_handle(
                grunt.grunt_name,
                self.token)
        self.server.modify_system(
                system_id,
                "profile",
                profile_name,
                self.token)

        self.server.modify_system(
                system_id,
                "netboot_enabled",
                "True",
                self.token)
        self.server.save_system(system_id, self.token)
        self.server.power_system(system_id, "reboot", self.token)


class OpenstackTest(object):
    """Driver class to automate the installation.
    """
    def __init__(
            self,
            cobbler,
            controller,
            compute_nodes=None,
            test_prefix="openstack_test"):
        """
        Args:
            cobbler (Cobbler): Instance of Cobbler
            controller (Controller): Controller node instance
            compute_nodes (TYPE, optional): A list of Grunt nodes to be set up
                    as compute nodes.
            test_prefix (str, optional): All entities created by the script are
                    prefixed with this string.
        """
        self.cobbler = cobbler
        self.controller = controller
        self.compute_nodes = [] if compute_nodes is None else compute_nodes
        self.test_prefix = test_prefix

    def _prepare_snippet(self):
        """Prepares the config based on the controller and compute nodes.

        Returns:
            str: Openstack config content.
        """
        content = ""

        config = {}
        config['host_name'] = self.controller.grunt_name
        config['ip'] = self.controller.ip
        config['dns_server'] = self.controller.dns_server
        config['private_ip'] = self.controller.private_ip
        config['floating_ip'] = self.controller.floating_ip

        content += Template.GRUNT_CONFIG.format(**config)
        for compute_node in self.compute_nodes:
            config["host_name"] = compute_node.grunt_name
            content += Template.GRUNT_CONFIG.format(**config)

        content = Template.SNIPPET_TEMPLATE.format(config=content)

        return content

    def prepare_profile(self):
        """Creates the cobbler profile.
        """
        snippet_content = self._prepare_snippet()
        self.cobbler.create_snippet(
                "{}.cfg".format(self.test_prefix),
                snippet_content)

        ks_content = Template.KS_TEMPATE
        ks_file = self.cobbler.create_kickstart(
                "{}.ks".format(self.test_prefix),
                ks_content)

        self.cobbler.create_profile(self.test_prefix, ks_file)
        return self.test_prefix

    def _get_cal_account(self):
        """
        Creates an object for class RwcalYang.CloudAccount()
        """
        account                        = RwcalYang.CloudAccount()
        account.account_type           = "openstack"
        account.openstack.key          = "{}_user".format(self.test_prefix)
        account.openstack.secret       = "mypasswd"
        account.openstack.auth_url     = 'http://{}:35357/v3/'.format(self.controller.ip)
        account.openstack.tenant       = self.test_prefix

        return account

    def start(self):
        """Starts the installation.
        """
        profile_name = self.prepare_profile()

        self.cobbler.boot_system(self.controller, profile_name)
        self.controller.wait_till_system_is_up(check_openstack=True)

        try:
            logger.info("Controller system is UP. Setting up compute nodes")
            for compute_node in self.compute_nodes:
                self.cobbler.boot_system(compute_node, profile_name)
                compute_node.wait_till_system_is_up()
        except OSError as e:
            logger.error("System set-up failed {}".format(e))
            sys.exit(1)

        # Currently we don't have wrapper on top of users/projects so using
        # keystone API directly
        acct = self._get_cal_account()

        keystone_conn = client.Client(
                auth_url=acct.openstack.auth_url,
                username='admin',
                password='mypasswd')

        # Create a test project
        project = keystone_conn.projects.create(
                acct.openstack.tenant,
                "default",
                description="Openstack test project")

        # Create an user
        user = keystone_conn.users.create(
                acct.openstack.key,
                password=acct.openstack.secret,
                default_project=project)

        # Make the newly created user as ADMIN
        admin_role = keystone_conn.roles.list(name="admin")[0]
        keystone_conn.roles.grant(
                admin_role.id,
                user=user.id,
                project=project.id)

        # nova API needs to be restarted, otherwise the new service doesn't play
        # well
        self.controller.ssh.exec_command("source keystonerc_admin && "
                "service openstack-nova-api restart")
        time.sleep(10)

        return acct

    def clear(self):
        """Close out all SFTP connections.
        """
        nodes = [self.controller]
        nodes.extend(self.compute_nodes)
        for node in nodes:
            node.clear()


###############################################################################
## Begin pytests
###############################################################################


@pytest.fixture(scope="session")
def cal(request):
    """
    Loads rw.cal plugin via libpeas
    """
    plugin = rw_peas.PeasPlugin('rwcal_openstack', 'RwCal-1.0')
    engine, info, extension = plugin()

    # Get the RwLogger context
    rwloggerctx = rwlogger.RwLog.Ctx.new("Cal-Log")

    cal = plugin.get_interface("Cloud")
    try:
        rc = cal.init(rwloggerctx)
        assert rc == RwStatus.SUCCESS
    except:
        logger.error("ERROR:Cal plugin instantiation failed. Aborting tests")
    else:
        logger.info("Openstack Cal plugin successfully instantiated")

    return cal


@pytest.fixture(scope="session")
def account(request):
    """Creates an openstack instance with 1 compute node and returns the newly
    created account.
    """
    cobbler = Cobbler("qacobbler.eng.riftio.com")
    controller = Grunt("grunt3.qanet.riftio.com")
    compute_nodes = [Grunt("grunt5.qanet.riftio.com")]

    test = OpenstackTest(cobbler, controller, compute_nodes)
    account = test.start()

    request.addfinalizer(test.clear)
    return account


def test_list_images(cal, account):
    """Verify if 2 images are present
    """
    status, resources = cal.get_image_list(account)
    assert len(resources.imageinfo_list) == 2

def test_list_flavors(cal, account):
    """Basic flavor checks
    """
    status, resources = cal.get_flavor_list(account)
    assert len(resources.flavorinfo_list) == 5


class Template(object):
    """A container to hold all cobbler related templates.
    """
    GRUNT_CONFIG = """
{host_name})
    CONTROLLER={ip}
    BRGIF=1
    OVSDPDK=N
    TRUSTED=N
    QAT=N
    HUGEPAGE=0
    VLAN=10:14
    PRIVATE_IP={private_ip}
    FLOATING_IP={floating_ip}
    DNS_SERVER={dns_server}
    ;;

    """

    SNIPPET_TEMPLATE = """
# =====================Begining of snippet=================
# snippet openstack_test.cfg
case $name in

{config}

*)
    ;;
esac

# =====================End of snippet=================

"""

    KS_TEMPATE = """
$SNIPPET('rift-repos')
$SNIPPET('rift-base')
%packages
@core
wget
$SNIPPET('rift-grunt-fc21-packages')
ganglia-gmetad
ganglia-gmond
%end

%pre
$SNIPPET('log_ks_pre')
$SNIPPET('kickstart_start')
# Enable installation monitoring
$SNIPPET('pre_anamon')
%end

%post --log=/root/ks_post.log
$SNIPPET('openstack_test.cfg')
$SNIPPET('ganglia')
$SNIPPET('rift-post-yum')
$SNIPPET('rift-post')
$SNIPPET('rift_fix_grub')

$SNIPPET('rdo-post')
echo "banner RDO test" >> /etc/profile

$SNIPPET('kickstart_done')
%end
"""
