#!python

"""ants
====


Run ansible-pull with a set of defined parameters
and log the content of these runs.
"""

__version__ = "2.0b2"

__author__ = "Balz Aschwanden"
__email__ = "balz.aschwanden@unibas.ch"
__copyright__ = "Copyright 2017, University of Basel"

__credits__ = ["Balz Aschwanden", "Jan Welker"]
__license__ = "GPL"


import datetime
import os
import shutil
import subprocess
import sys

from antslib import argparser, configer, logger

CFG = configer.read_config("main")

ANTS_PATH = os.path.dirname(os.path.realpath(__file__))
DESTINATION = os.path.expanduser(CFG["destination"])
_ROOT = os.path.abspath(os.path.dirname(__file__))
_PROJECT_ROOT = os.path.dirname(os.path.abspath(os.path.dirname(__file__)))


def find_file(f):
    """Search for a file in environment PATH and return first match or None"""
    paths = os.environ["PATH"].split(":")
    paths.insert(0, _ROOT)
    for path in paths:
        file_path = os.path.join(path, f)
        logger.console_logger.debug("Looking for %s in %s" % (f, path))
        if os.path.isfile(file_path):
            logger.console_logger.debug("Found %s at %s" % (f, file_path))
            return file_path
    return None


def parse_proc(proc):
    """Read subprocess output and dispatch it to logger. Return rc of process.


    Cases handled separately:
        * task_line
            * Line with the name of a task.
            * Printed directly befor the task status.
        * recap_line
            * A single line a the end of an Ansible run.
            * It indicates the number of failes/changed/ok tasks.
            * The line after 'PLAY RECAP' contains the recap
    """
    task_line = None
    recap_line = None
    get_recap = False
    start_run_time = datetime.datetime.now()
    for line in iter(proc.stdout.readline, b""):
        line = line.decode("utf-8")
        logger.write_log(line, task_line)
        if line.startswith("TASK"):
            task_line = line
        if get_recap:
            recap_line = line
        get_recap = bool(line.startswith("PLAY RECAP"))

    # https://stackoverflow.com/questions/37942022/returncode-of-popen-object-is-none-after-the-process-is-terminated
    proc.stdout.close()
    rc = proc.wait()
    end_run_time = datetime.datetime.now()
    logger.log_recap(start_run_time, end_run_time, recap_line, rc)
    return rc


def run_ansible(args):
    """Run ansible-pull and return rc of subprocess.


    The ansible python api is provided as is and the core team
    reserve the right to push breakting changed.

    Hence, we call the cli directly and do not attempt to work with the api.

    Documentation:
    https://docs.ansible.com/ansible/latest/dev_guide/developing_api.html
    """
    ansible_pull_exe = args.ansible_pull_exe
    if not ansible_pull_exe:
        logger.console_logger.debug(
            "Variable ansible_pull_exe is not set. Searching in PATH."
        )
        ansible_pull_exe = find_file("ansible-pull")
    if not ansible_pull_exe:
        sys.exit("Could not find executable ansible-pull. Aborting.")
    if not os.access(ansible_pull_exe, os.X_OK):
        sys.exit("File is not executable at %s. Aborting." % ansible_pull_exe)
    logger.console_logger.debug("Using %s" % ansible_pull_exe)

    inventory = args.inventory
    if not os.path.isfile(inventory):
        sys.exit("Could not find file at %s. Aborting." % inventory)

    if not os.access(inventory, os.X_OK):
        logger.console_logger.debug(
            "Inventory file at %s is not executable." % inventory
        )
        logger.console_logger.debug("Using static inventory file at %s." % inventory)

    cmd = [
        ansible_pull_exe,
        "--clean",
        "-f",
        "-i",
        inventory,
        "-d",
        args.destination,
        "-U",
        args.git_repo,
        "-C",
        args.branch,
        args.playbook,
    ]

    if os.path.isfile(args.ssh_key):
        logger.console_logger.debug("Found ssh key at %s" % args.ssh_key)
        cmd.append("--private-key")
        cmd.append(args.ssh_key)
    else:
        logger.console_logger.debug("No key found at %s" % args.ssh_key)

    if args.verbose:
        cmd.append("-%s" % ("v" * args.verbose))

    if args.check:
        cmd.append("--check")

    if not args.stricthostkeychecking:
        logger.console_logger.debug(
            "Strict host key checking for ansible-pull is disabled."
        )
        cmd.append("--accept-host-key")

    if args.wait:
        logger.console_logger.debug(
            "Running ansible-pull with a random wait intervall of %s sec"
            % CFG["wait_interval"]
        )
        cmd.append("-s")
        cmd.append(CFG["wait_interval"])

    if args.tags:
        cmd.append("--tags")
        cmd.append(args.tags)

    if args.skip_tags:
        cmd.append("--skip-tags")
        cmd.append(args.skip_tags)

    # Add user configured search path and default search path for callback plugins
    ANSIBLE_CALLBACK_PLUGINS = CFG["ansible_callback_plugins_base_path"]
    if args.ansible_callback_plugins:
        ANSIBLE_CALLBACK_PLUGINS = "%s:%s" % (
            args.ansible_callback_plugins,
            ANSIBLE_CALLBACK_PLUGINS,
        )
    logger.console_logger.debug(
        "Add env variable ANSIBLE_CALLBACK_PLUGINS: %s" % ANSIBLE_CALLBACK_PLUGINS
    )
    os.environ["ANSIBLE_CALLBACK_PLUGINS"] = ANSIBLE_CALLBACK_PLUGINS

    if CFG["ansible_callback_whitelist"]:
        logger.console_logger.debug(
            "Add env variable ANSIBLE_CALLBACK_WHITELIST: %s"
            % args.ansible_callback_whitelist
        )
        os.environ["ANSIBLE_CALLBACK_WHITELIST"] = args.ansible_callback_whitelist
        cfg_callback_plugins = configer.read_config("callback_plugins")
        for key, value in cfg_callback_plugins.items():
            logger.console_logger.debug("Add env variable: %s: %s" % (key, value))
            os.environ[key] = value

    proc = subprocess.Popen(
        cmd, bufsize=1, stdout=subprocess.PIPE, stderr=subprocess.PIPE
    )
    return parse_proc(proc)


def __main__():
    args = argparser.parse_args(
        __version__, os.path.join(CFG["log_dir"], "recap.log"), DESTINATION, CFG
    )
    if args.verbose:
        logger.console_logger.setLevel(logger.logging.DEBUG)
    if args.quiet:
        logger.console_logger.disabled = True
    if not configer.is_root():
        sys.exit("Script must be run as root")
    logger.console_logger.debug("Running ansible-pull in verbose mode")
    logger.status_file_rollover()

    if args.refresh:
        if os.path.exists(args.destination):
            msg = "Deleting local git repo at %s" % args.destination
            logger.console_logger.info(msg)
            logger.logfile_logger.info(
                "************************************************************************"
            )
            logger.logfile_logger.info("Running ants client in refresh mode")
            logger.logfile_logger.info(msg)
            logger.logfile_logger.info(
                "************************************************************************"
            )
            try:
                shutil.rmtree(args.destination)
            except OSError as error:
                logger.console_logger.error(error)
                raise

    run_ansible(args)


if __name__ == "__main__":
    __main__()
