#!/usr/bin/env python
# -*- coding: utf-8 -*-
'''
    :codeauthor: :email:`Pedro Algarvio (pedro@algarvio.me)`


    jenkins-ng
    ~~~~~~~~~~

    Jenkins execution helper script
'''

# Import python libs
from __future__ import print_function
import os
import re
import sys
import json
import time
import shutil
import argparse
import requests
import subprocess

# Import Salt libs
try:
    from salt.utils.nb_popen import NonBlockingPopen
except ImportError:
    # Salt not installed, or nb_popen was not yet shipped with it
    SALT_LIB = os.path.abspath(
        os.path.dirname(os.path.dirname(__file__))
    )
    if SALT_LIB not in sys.path:
        sys.path.insert(0, SALT_LIB)
    try:
        # Let's try using the current checked out code
        from salt.utils.nb_popen import NonBlockingPopen
    except ImportError:
        # Still an ImportError??? Let's use some "brute-force"
        sys.path.insert(
            0,
            os.path.join(SALT_LIB, 'salt', 'utils')
        )
        from nb_popen import NonBlockingPopen  # pylint: disable=F0401

# Import 3rd-party libs
import yaml

SALT_GIT_URL = 'https://github.com/saltstack/salt.git'


class GetPullRequestAction(argparse.Action):
    '''
    Load the required pull request information
    '''

    def __call__(self, parser, namespace, values, option_string=None):
        headers = {}
        url = 'https://api.github.com/repos/saltstack/salt/pulls/{0}'.format(values)

        github_access_token_path = os.path.join(
            os.environ.get('JENKINS_HOME', os.path.expanduser('~')),
            '.github_token'
        )
        if os.path.isfile(github_access_token_path):
            headers = {
                'Authorization': 'token {0}'.format(
                    open(github_access_token_path).read().strip()
                )
            }

        http_req = requests.get(url, headers=headers)
        if http_req.status_code != 200:
            parser.error(
                'Unable to get the pull request: {0[message]}'.format(http_req.json())
            )

        pr_details = http_req.json()
        setattr(namespace, 'pull_request_git_url', pr_details['head']['repo']['clone_url'])
        setattr(namespace, 'pull_request_git_commit', pr_details['head']['sha'])


def generate_vm_name(options):
    '''
    Generate a random enough vm name
    '''
    if 'BUILD_NUMBER' in os.environ:
        random_part = 'BUILD{0:0>6}'.format(os.environ.get('BUILD_NUMBER'))
    else:
        random_part = os.urandom(3).encode('hex')

    return '{0}-{1}-{2}'.format(options.vm_prefix, options.vm_source, random_part)


def get_vm_name(options):
    '''
    Return the VM name
    '''
    return os.environ.get('JENKINS_VM_NAME', generate_vm_name(options))


def get_minion_external_address(options):
    '''
    Get and store the remote minion external IP
    '''
    if 'minion_external_ip' in options:
        return options.minion_external_ip

    sync_minion(options)

    cmd = []
    if options.peer:
        cmd.extend(['salt-call', '--out=json', 'publish.publish'])
    else:
        cmd.extend(['salt', '--out=json'])

    cmd.extend([
        options.vm_name,
        'grains.get',
        'external_ip'
    ])

    cmd = ' '.join(cmd)
    print('Running CMD: {0!r}'.format(cmd))
    sys.stdout.flush()

    proc = subprocess.Popen(
        cmd,
        shell=True,
        stdout=subprocess.PIPE,
        stderr=subprocess.PIPE,
    )

    stdout, _ = proc.communicate()

    retcode = proc.returncode
    if retcode != 0:
        print('Failed to get the minion external IP. Exit code: {0}'.format(retcode))
        sys.stdout.flush()
        if options.delete_vm:
            delete_vm(options)
        sys.exit(retcode)

    if not stdout.strip():
        print('Failed to get the minion external IP(no output). Exit code: {0}'.format(retcode))
        sys.stdout.flush()
        if options.delete_vm:
            delete_vm(options)
        sys.exit(retcode)

    try:
        external_ip_info = json.loads(stdout.strip())
        if options.peer:
            external_ip = external_ip_info['local']
        else:
            external_ip = external_ip_info[options.vm_name]
    except ValueError:
        print('Failed to load any JSON from {0!r}'.format(stdout.strip()))

    setattr(options, 'minion_external_ip', external_ip)
    return external_ip


def get_minion_python_executable(options):
    '''
    Get and store the remote minion python executable
    '''
    if 'minion_python_executable' in options:
        return options.minion_python_executable

    sync_minion(options)

    cmd = []
    if options.peer:
        cmd.extend(['salt-call', '--out=json', 'publish.publish'])
    else:
        cmd.extend(['salt', '--out=json'])

    cmd.extend([
        options.vm_name,
        'grains.get',
        'pythonexecutable'
    ])

    cmd = ' '.join(cmd)
    print('Running CMD: {0!r}'.format(cmd))

    proc = subprocess.Popen(
        cmd,
        shell=True,
        stdout=subprocess.PIPE,
        stderr=subprocess.PIPE,
    )

    stdout, _ = proc.communicate()

    retcode = proc.returncode
    if retcode != 0:
        print('Failed to get the minion python executable. Exit code: {0}'.format(retcode))
        sys.stdout.flush()
        if options.delete_vm:
            delete_vm(options)
        sys.exit(retcode)

    if not stdout.strip():
        print('Failed to get the minion python executable(no output). Exit code: {0}'.format(retcode))
        sys.stdout.flush()
        if options.delete_vm:
            delete_vm(options)
        sys.exit(retcode)

    try:
        python_executable = json.loads(stdout.strip())
        if options.peer:
            python_executable = python_executable['local']
        else:
            python_executable = python_executable[options.vm_name]
    except ValueError:
        print('Failed to load any JSON from {0!r}'.format(stdout.strip()))

    setattr(options, 'minion_python_executable', python_executable)
    return python_executable


def sync_minion(options):
    if 'salt_minion_synced' not in options:
        cmd = []
        if options.peer:
            cmd.extend(['salt-call', 'publish.runner'])
        else:
            cmd.append('salt')

        cmd.extend([
            build_minion_target(options),
            'saltutil.sync_all'
        ])
        print('Running CMD: {0!r}'.format(' '.join(cmd)))
        sys.stdout.flush()

        proc = NonBlockingPopen(
            ' '.join(cmd),
            shell=True,
            stdout=subprocess.PIPE,
            stderr=subprocess.PIPE,
            stream_stds=True
        )
        proc.poll_and_read_until_finish()
        proc.communicate()

        if proc.returncode != 0:
            print(
                '\nFailed to execute command. Exit code: {0}'.format(
                    proc.returncode
                )
            )
            sys.exit(proc.returncode)

        setattr(options, 'salt_minion_synced', 'yes')


def to_cli_yaml(data):
    '''
    Return a YAML string for CLI usage
    '''
    return yaml.dump(data, default_flow_style=True, indent=0, width=sys.maxint).rstrip()


def build_pillar_data(options):
    '''
    Build a YAML formatted string to properly pass pillar data
    '''
    pillar = {'test_transport': options.test_transport}
    if options.test_git_commit is not None:
        pillar['test_git_commit'] = options.test_git_commit
    if options.test_git_url is not None:
        pillar['test_git_url'] = options.test_git_url
    if options.bootstrap_salt_url is not None:
        pillar['bootstrap_salt_url'] = options.bootstrap_salt_url
    if options.bootstrap_salt_commit is not None:
        pillar['bootstrap_salt_commit'] = options.bootstrap_salt_commit
    if options.pillar:
        pillar.update(dict(options.pillar))
    return to_cli_yaml(pillar)


def build_minion_target(options):
    '''
    Build the minion target string
    '''
    target = options.vm_name
    for grain in options.grain_target:
        target += ' and G@{0}'.format(grain)
    if options.grain_target and not options.peer:
        target = '-C "{0}"'.format(target)
    return target


def delete_vm(options):
    '''
    Stop a VM
    '''
    cmd = []
    if options.peer:
        cmd.extend(['salt-call', 'publish.runner'])
    else:
        cmd.append('salt-run')
    if options.lxc:
        cmd.append('lxc.purge')
    else:
        cmd.append('cloud.destroy')

    cmd.append(options.vm_name)
    print('Running CMD: {0!r}'.format(' '.join(cmd)))
    sys.stdout.flush()

    proc = NonBlockingPopen(
        ' '.join(cmd),
        shell=True,
        stdout=subprocess.PIPE,
        stderr=subprocess.PIPE,
        stream_stds=True
    )
    proc.poll_and_read_until_finish()
    proc.communicate()


def download_unittest_reports(options):
    '''
    Download the generated unit test reports from minion
    '''
    print('Downloading remote unittest reports...')
    sys.stdout.flush()

    xml_reports_path = os.path.join(options.workspace, 'xml-test-reports')
    if os.path.isdir(xml_reports_path):
        shutil.rmtree(xml_reports_path)

    if options.scp:
        cmds = (
            ' '.join(build_scp_command(options,
                                       '-r',
                                       'root@{0}:/tmp/xml-unitests-output/*'.format(
                                           get_minion_external_address(options)
                                       ),
                                       os.path.join(options.workspace, 'xml-test-reports'))),
        )
    else:
        os.makedirs(xml_reports_path)
        cmds = (
            '{0} {1} archive.tar zcvf /tmp/xml-test-reports.tar.gz \'*.xml\' cwd=/tmp/xml-unitests-output/',
            '{0} {1} cp.push /tmp/xml-test-reports.tar.gz',
            'mv -f /var/cache/salt/master/minions/{2}/files/tmp/xml-test-reports.tar.gz {3} && '
            'tar zxvf {3}/xml-test-reports.tar.gz -C {3}/xml-test-reports && '
            'rm -f {3}/xml-test-reports.tar.gz'
        )

    for cmd in cmds:
        cmd = cmd.format(
            'salt-call publish.publish' if options.lxc else 'salt',
            build_minion_target(options),
            options.vm_name,
            options.workspace
        )
        print('Running CMD: {0!r}'.format(cmd))
        sys.stdout.flush()

        proc = NonBlockingPopen(
            cmd,
            shell=True,
            stdout=subprocess.PIPE,
            stderr=subprocess.PIPE,
            stream_stds=True
        )
        proc.poll_and_read_until_finish()
        proc.communicate()
        if proc.returncode != 0:
            print(
                '\nFailed to execute command. Exit code: {0}'.format(
                    proc.returncode
                )
            )
        time.sleep(0.25)


def download_coverage_report(options):
    '''
    Download the generated coverage report from minion
    '''
    print('Downloading remote coverage report...')
    sys.stdout.flush()

    if os.path.isfile(os.path.join(options.workspace, 'coverage.xml')):
        os.unlink(os.path.join(options.workspace, 'coverage.xml'))

    if options.scp:
        cmds = (
            ' '.join(build_scp_command(options,
                                       'root@{0}:/tmp/coverage.xml'.format(
                                           get_minion_external_address(options)
                                       ),
                                       os.path.join(options.workspace, 'coverage.xml'))),
        )
    else:
        cmds = (
            '{0} {1} archive.gzip /tmp/coverage.xml',
            '{0} {1} cp.push /tmp/coverage.xml.gz',
            'gunzip /var/cache/salt/master/minions/{2}/files/tmp/coverage.xml.gz',
            'mv /var/cache/salt/master/minions/{2}/files/tmp/coverage.xml {3}'
        )

    for cmd in cmds:
        cmd = cmd.format(
            'salt-call publish.publish' if options.lxc else 'salt',
            build_minion_target(options),
            options.vm_name,
            options.workspace
        )
        print('Running CMD: {0!r}'.format(cmd))
        sys.stdout.flush()

        proc = NonBlockingPopen(
            cmd,
            shell=True,
            stdout=subprocess.PIPE,
            stderr=subprocess.PIPE,
            stream_stds=True
        )
        proc.poll_and_read_until_finish()
        proc.communicate()
        if proc.returncode != 0:
            print(
                '\nFailed to execute command. Exit code: {0}'.format(
                    proc.returncode
                )
            )
        time.sleep(0.25)


def download_remote_logs(options):
    '''
    Download the generated logs from minion
    '''
    print('Downloading remote logs...')
    sys.stdout.flush()

    for fname in ('salt-runtests.log', 'minion.log'):
        if os.path.isfile(os.path.join(options.workspace, fname)):
            os.unlink(os.path.join(options.workspace, fname))

    if not options.remote_log_path:
        options.remote_log_path = [
            '/tmp/salt-runtests.log',
            '/var/log/salt/minion'
        ]

    cmds = []

    if options.scp:
        for remote_log in options.remote_log_path:
            cmds.append(
                ' '.join(build_scp_command(options,
                                           '-r',
                                           'root@{0}:{1}'.format(
                                               get_minion_external_address(options),
                                               remote_log
                                           ),
                                           os.path.join(
                                               options.workspace,
                                               '{0}{1}'.format(
                                                   os.path.basename(remote_log),
                                                   '' if remote_log.endswith('.log') else '.log'
                                                )
                                           )))
            )
    else:
        for remote_log in options.remote_log_path:
            cmds.extend([
                '{{0}} {{1}} archive.gzip {0}'.format(remote_log),
                '{{0}} {{1}} cp.push {0}.gz'.format(remote_log),
                'gunzip /var/cache/salt/master/minions/{{2}}/files{0}.gz'.format(remote_log),
                'mv /var/cache/salt/master/minions/{{2}}/files{0} {{3}}/{1}'.format(
                    remote_log,
                    '{0}{1}'.format(
                        os.path.basename(remote_log),
                        '' if remote_log.endswith('.log') else '.log'
                    )
                )
            ])

    for cmd in cmds:
        cmd = cmd.format(
            'salt-call publish.publish' if options.lxc else 'salt',
            build_minion_target(options),
            options.vm_name,
            options.workspace
        )
        print('Running CMD: {0!r}'.format(cmd))
        sys.stdout.flush()

        proc = NonBlockingPopen(
            cmd,
            shell=True,
            stdout=subprocess.PIPE,
            stderr=subprocess.PIPE,
            stream_stds=True
        )
        proc.poll_and_read_until_finish()
        proc.communicate()
        if proc.returncode != 0:
            print(
                '\nFailed to execute command. Exit code: {0}'.format(
                    proc.returncode
                )
            )
        time.sleep(0.25)


def echo_parseable_environment(options):
    '''
    Echo NAME=VAL parseable output
    '''
    output = [
        'JENKINS_VM_NAME={0}'.format(options.vm_name),
        'JENKINS_VM_SOURCE={0}'.format(options.vm_source),
    ]
    if 'pull_request_git_url' in options and 'pull_request_git_commit' in options:
        output.extend([
            'SALT_PR_GIT_URL={0}'.format(options.pull_request_git_url),
            'SALT_PR_GIT_COMMIT={0}'.format(options.pull_request_git_commit)
        ])

    sys.stdout.write('\n\n{0}\n\n'.format('\n'.join(output)))
    sys.stdout.flush()


def prepare_ssh_access(options):
    '''
    Generate a temporary SSH key, valid for one hour, and set it as an
    authorized key in the minion's root user account on the remote system.
    '''
    print('Generating temporary SSH Key')
    ssh_key_path = os.path.join(options.workspace, 'jenkins_ssh_key_test')
    subprocess.call(
        'ssh-keygen -t ecdsa -b 521 -C "$(whoami)@$(hostname)-$(date --rfc-3339=seconds)" '
        '-f {0} -N \'\' -V -10m:+1h'.format(ssh_key_path),
        shell=True,
    )
    cmd = []
    if options.peer:
        cmd.extend(['salt-call', 'publish.publish'])
    else:
        cmd.append('salt')
    pub_key_contents = open('{0}.pub'.format(ssh_key_path)).read().strip()
    enc, key, comment = pub_key_contents.split(' ', 2)
    cmd.extend([
        build_minion_target(options),
        'ssh.set_auth_key',
        'root',
        '{0!r}'.format(key),
        'enc={0}'.format(enc),
        'comment={0!r}'.format(comment)
    ])

    cmd = ' '.join(cmd)
    print('Running CMD: {0!r}'.format(cmd))
    sys.stdout.flush()

    proc = NonBlockingPopen(
        cmd,
        shell=True,
        stdout=subprocess.PIPE,
        stderr=subprocess.PIPE,
        stream_stds=True
    )
    proc.poll_and_read_until_finish()
    proc.communicate()
    if proc.returncode != 0:
        print(
            '\nFailed to execute command. Exit code: {0}'.format(
                proc.returncode
            )
        )


def build_scp_command(options, *arguments):
    '''
    Build the SCP command with the required options
    '''
    return [
        'scp',
        '-i',
        os.path.join(options.workspace, 'jenkins_ssh_key_test'),
        # Don't add new hosts to the host key database
        '-oStrictHostKeyChecking=no',
        # Set hosts key database path to /dev/null, i.e., non-existing
        '-oUserKnownHostsFile=/dev/null',
        # Don't re-use the SSH connection. Less failures.
        '-oControlPath=none',
    ] + list(arguments)


def main():
    '''
    Main script execution
    '''
    parser = argparse.ArgumentParser(
        description='Jenkins execution helper'
    )
    parser.add_argument(
        '-w', '--workspace',
        default=os.path.abspath(os.environ.get('WORKSPACE', os.getcwd())),
        help='Path to the execution workspace'
    )

    # Output Options
    output_group = parser.add_argument_group('Output Options')
    output_group.add_argument(
        '--no-color',
        '--no-colour',
        action='store_true',
        default=False,
        help='Don\'t use colors'
    )
    output_group.add_argument(
        '--echo-parseable-output',
        action='store_true',
        default=False,
        help='Print Jenkins related environment variables and exit'
    )
    output_group.add_argument(
        '--pull-request',
        type=int,
        action=GetPullRequestAction,
        default=None,
        help='Include the Pull Request information in parseable output'
    )

    # Deployment Selection
    deployment_group = parser.add_argument_group('Deployment Selection')
    deployment_group.add_argument(
        '--pre-mortem',
        action='store_true',
        default=False,
        help='Don\'t try to deploy an new VM. Consider a VM deployed and only '
             'execute post test suite execution commands. Right before killing the VM'
    )
    deployment_group_mutually_exclusive = deployment_group.add_mutually_exclusive_group()
    deployment_group_mutually_exclusive.add_argument(
        '--cloud',
        action='store_true',
        default=True,
        help='Salt Cloud Deployment'
    )
    deployment_group_mutually_exclusive.add_argument(
        '--lxc',
        action='store_true',
        default=False,
        help='Salt LXC Deployment'
    )
    deployment_group.add_argument(
        '--lxc-host',
        default=None,
        help='The host where to deploy the LXC VM'
    )

    # Execution Selections
    execution_group = parser.add_argument_group('Execution Selection')
    execution_group.add_argument(
        '--peer', action='store_true', default=False, help='Run salt commands through the peer system'
    )
    execution_group.add_argument(
        '--scp', action='store_true', default=False,
        help='Download logs and reports using SCP'
    )

    bootstrap_script_options = parser.add_argument_group(
        'Bootstrap Script Options',
        'In case there\'s a need to provide the bootstrap script from an alternate URL and/or from a specific commit.'
    )
    bootstrap_script_options.add_argument(
        '--bootstrap-salt-url',
        default=None,
        help='The salt git repository url used to bootstrap a minion'
    )
    bootstrap_script_options.add_argument(
        '--bootstrap-salt-commit',
        default=None,
        help='The salt git commit used to bootstrap a minion'
    )

    vm_options_group = parser.add_argument_group('VM Options')
    vm_options_group.add_argument('vm_name', nargs='?', help='Virtual machine name')
    vm_options_group.add_argument(
        '--vm-prefix',
        default=os.environ.get('JENKINS_VM_NAME_PREFIX', 'ZJENKINS'),
        help='The bootstrapped machine name prefix. Default: %(default)r'
    )
    vm_options_group.add_argument(
        '--vm-source',
        default=os.environ.get('JENKINS_VM_SOURCE', None),
        help='The VM source. In case of --cloud usage, the could profile name. In case of --lxc usage, the image name.'
    )
    vm_options_group.add_argument(
        '--grain-target',
        action='append',
        default=[],
        help='Match minions using compound matchers, the minion ID, plus the passed grain.'
    )

    vm_preparation_group = parser.add_argument_group(
        'VM Preparation Options',
        'Salt SLS selection to prepare the VM. The defaults are based on the salt-jenkins repository. See '
        'https://github.com/saltstack/salt-jenkins'
    )
    vm_preparation_group.add_argument(
        '--prep-sls',
        default='git.salt',
        help='The sls file to execute to prepare the system. Default: %(default)r'
    )
    vm_preparation_group.add_argument(
        '--prep-sls-2',
        default=None,
        help='An optional 2nd system preparation SLS'
    )
    vm_preparation_group.add_argument(
        '--sls',
        default='testrun-no-deps',
        help='The final sls file to execute.'
    )
    vm_preparation_group.add_argument(
        '--pillar',
        action='append',
        nargs=2,
        help='Pillar (key, value)s to pass to the sls file. Example: \'--pillar pillar_key pillar_value\''
    )

    vm_actions = parser.add_argument_group(
        'VM Actions',
        'Action to execute on a running VM'
    )
    vm_actions.add_argument(
        '--delete-vm',
        action='store_true',
        default=False,
        help='Delete a running VM'
    )
    vm_actions.add_argument(
        '--download-remote-reports',
        default=False,
        action='store_true',
        help='Download remote reports when running remote \'testrun\' state'
    )
    vm_actions.add_argument(
        '--download-unittest-reports',
        default=False,
        action='store_true',
        help='Download the XML unittest results'
    )
    vm_actions.add_argument(
        '--download-coverage-report',
        default=False,
        action='store_true',
        help='Download the XML coverage reports'
    )
    vm_actions.add_argument(
        '--download-remote-logs',
        default=False,
        action='store_true',
        help='Download remote minion and runtests log files'
    )

    vm_actions.add_argument(
        '--remote-log-path',
        action='append',
        default=[],
        help='Provide additional log paths to download from remote minion'
    )

    testing_source_options = parser.add_argument_group(
        'Testing Options',
        'In case there\'s a need to provide a different repository and/or commit from which the tests suite should be '
        'executed on'
    )
    testing_source_options.add_argument(
        '--test-transport',
        default='zeromq',
        choices=('zeromq', 'raet'),
        help='Set to raet to run integration tests with raet transport. Default: %default')
    testing_source_options.add_argument(
        '--test-git-url',
        default=None,
        help='The testing git repository url')
    testing_source_options.add_argument(
        '--test-git-commit',
        default=None,
        help='The testing git commit to track')

    options = parser.parse_args()

    if not options.vm_source and not options.vm_name:
        parser.error('Unable to get VM name from environ nor generate it without --vm-source')

    if options.lxc:
        if not options.lxc_host:
            parser.error('Need to provide where to deploy the LXC VM by passing it to --lxc-host')
        options.cloud = False

    if not options.vm_name:
        options.vm_name = get_vm_name(options)

    if options.echo_parseable_output:
        echo_parseable_environment(options)
        parser.exit()

    if options.lxc:
        options.cloud = False

    if options.pre_mortem:
        # Run any actions supposed to be executed right before killing the VM
        if options.download_remote_reports:
            # Download unittest reports
            download_unittest_reports(options)
            # Download coverage report
            download_coverage_report(options)
        else:
            if options.download_unittest_reports:
                download_unittest_reports(options)
            if options.download_coverage_report:
                download_coverage_report(options)

        if options.download_remote_logs:
            download_remote_logs(options)

        if options.delete_vm:
            delete_vm(options)

        parser.exit()

    # RUN IT!!!
    cmd = []
    minion_target = build_minion_target(options)

    if options.peer:
        cmd.extend(['salt-call', 'publish.runner'])
    else:
        cmd.append('salt-run')

    if options.lxc:
        cmd.append('lxc.init')
        if options.peer:
            cmd.append(
                'arg="{0}"'.format(
                    to_cli_yaml([
                        options.vm_name,
                        'host={0}'.format(options.lxc_host),
                        'image={0}'.format(options.vm_source)
                    ])
                )
            )
        else:
            cmd.extend([options.vm_name,
                        'host={0}'.format(options.lxc_host),
                        'image={0}'.format(options.vm_source)])
    else:
        cmd.append('cloud.profile')
        if options.peer:
            cmd.append(
                'arg="{0}"'.format(
                    to_cli_yaml([options.vm_source, options.vm_name])
                )
            )
        else:
            cmd.extend([options.vm_source, options.vm_name])

    if options.cloud:
        if options.bootstrap_salt_commit is not None:
            if options.bootstrap_salt_url is None:
                options.bootstrap_salt_url = 'https://github.com/saltstack/salt.git'
            cmd.append(
                'script_args="-D -g {bootstrap_salt_url} -n git {bootstrap_salt_commit}"'
            )
        else:
            cmd.append('script-args="-D"')

    cmd = ' '.join(cmd).format(**options.__dict__)

    print('Running CMD: {0!r}'.format(cmd))
    sys.stdout.flush()

    proc = NonBlockingPopen(
        cmd,
        shell=True,
        stdout=subprocess.PIPE,
        stderr=subprocess.PIPE,
        stream_stds=True
    )
    proc.poll_and_read_until_finish()
    proc.communicate()

    retcode = proc.returncode
    if retcode != 0:
        print('Failed to bootstrap VM. Exit code: {0}'.format(retcode))
        sys.stdout.flush()
        if options.delete_vm:
            delete_vm(options)
        sys.exit(retcode)

    print('VM Bootstrapped. Exit code: {0}'.format(retcode))
    sys.stdout.flush()

    print('Sleeping for 5 seconds to allow the minion to breathe a little')
    sys.stdout.flush()
    time.sleep(5)

    if options.scp:
        prepare_ssh_access(options)

    if options.cloud:
        if options.bootstrap_salt_commit is not None:
            # Let's find out if the installed version matches the passed in pillar
            # information
            print('Grabbing bootstrapped minion version information ... ')
            cmd = []
            if options.peer:
                cmd.extend(['salt-call', '--out=json', 'publish.publish'])
            else:
                cmd.extend(['salt', '-t', '100', '--out=json'])
            cmd.extend([minion_target, 'test.version'])

            if options.peer and ' ' in minion_target:
                cmd.append('expr_form="compound"')

            cmd = ' '.join(cmd)
            print('Running CMD: {0!r}'.format(cmd))
            sys.stdout.flush()

            proc = subprocess.Popen(
                cmd,
                shell=True,
                stdout=subprocess.PIPE,
                stderr=subprocess.PIPE,
            )
            stdout, _ = proc.communicate()

            retcode = proc.returncode
            if retcode != 0:
                print('Failed to get the bootstrapped minion version. Exit code: {0}'.format(retcode))
                sys.stdout.flush()
                if options.delete_vm:
                    delete_vm(options)
                sys.exit(retcode)

            if not stdout.strip():
                print('Failed to get the bootstrapped minion version(no output). Exit code: {0}'.format(retcode))
                sys.stdout.flush()
                if options.delete_vm:
                    delete_vm(options)
                sys.exit(retcode)

            try:
                version_info = json.loads(stdout.strip())
                if options.peer:
                    version_info = version_info['local']
                else:
                    version_info = version_info[options.vm_name]

                bootstrap_salt_commit = options.bootstrap_salt_commit
                if re.match('v[0-9]+', bootstrap_salt_commit):
                    # We've been passed a tag
                    bootstrap_salt_commit = bootstrap_salt_commit[1:]
                else:
                    # Most likely a git SHA
                    bootstrap_salt_commit = bootstrap_salt_commit[:7]

                if bootstrap_salt_commit not in version_info:
                    print('\n\nATTENTION!!!!\n')
                    print('The boostrapped minion version commit does not contain the desired commit:')
                    print(' {0!r} does not contain {1!r}'.format(version_info, bootstrap_salt_commit))
                    print('\n\n')
                    sys.stdout.flush()
                    #if options.delete_vm:
                    #    delete_vm(options)
                    #sys.exit(retcode)
                else:
                    print('matches!')
            except ValueError:
                print('Failed to load any JSON from {0!r}'.format(stdout.strip()))

    # Run preparation SLS
    time.sleep(3)
    cmd = []
    if options.peer:
        cmd.append('salt-call')
        if options.no_color:
            cmd.append('--no-color')
        cmd.append('publish.publish')
    else:
        cmd.extend(['salt', '-t', '1800'])
        if options.no_color:
            cmd.append('--no-color')

    cmd.extend([
        minion_target, 'state.sls', options.prep_sls, 'pillar="{0}"'.format(build_pillar_data(options))
    ])

    if options.peer and ' ' in minion_target:
        cmd.append('expr_form="compound"')

    cmd = ' '.join(cmd)
    print('Running CMD: {0!r}'.format(cmd))
    sys.stdout.flush()

    proc = NonBlockingPopen(
        cmd,
        shell=True,
        stdout=subprocess.PIPE,
        stderr=subprocess.PIPE,
        stream_stds=True
    )
    proc.poll_and_read_until_finish()
    proc.communicate()
    if proc.returncode != 0:
        print('Failed to execute the preparation SLS file. Exit code: {0}'.format(proc.returncode))
        sys.stdout.flush()
        if options.delete_vm:
            delete_vm(options)
        sys.exit(proc.returncode)

    if options.prep_sls_2 is not None:
        time.sleep(3)

        # Run the 2nd preparation SLS
        cmd = []
        if options.peer:
            cmd.append('salt-call')
            if options.no_color:
                cmd.append('--no-color')
            if options.peer:
                cmd.append('publish.publish')
        else:
            cmd.extend(['salt', '-t', '1800'])
            if options.no_color:
                cmd.append('--no-color')

        cmd.extend([
            minion_target, 'state.sls', options.prep_sls_2, 'pillar="{0}"'.format(build_pillar_data(options))
        ])

        if options.peer and ' ' in minion_target:
            cmd.append('expr_form="compound"')

        cmd = ' '.join(cmd)
        print('Running CMD: {0!r}'.format(cmd))
        sys.stdout.flush()

        proc = NonBlockingPopen(
            cmd,
            shell=True,
            stdout=subprocess.PIPE,
            stderr=subprocess.PIPE,
            stream_stds=True
        )
        proc.poll_and_read_until_finish()
        proc.communicate()
        if proc.returncode != 0:
            print('Failed to execute the 2nd preparation SLS file. Exit code: {0}'.format(proc.returncode))
            sys.stdout.flush()
            if options.delete_vm:
                delete_vm(options)
            sys.exit(proc.returncode)

    # Run remote checks
    if options.test_git_url is not None:
        time.sleep(1)
        # Let's find out if the cloned repository if checked out from the
        # desired repository
        print('Grabbing the cloned repository remotes information ... ')
        cmd = []
        if options.peer:
            cmd.extend(['salt-call', '--out=json', 'publish.publish'])
        else:
            cmd.extend(['salt', '-t', '100', '--out=json'])

        cmd.extend([minion_target, 'git.remote_get', '/testing'])

        if options.peer and ' ' in minion_target:
            cmd.append('expr_form="compound"')

        print('Running CMD: {0!r}'.format(cmd))

        sys.stdout.flush()
        proc = subprocess.Popen(
            cmd,
            shell=True,
            stdout=subprocess.PIPE,
            stderr=subprocess.PIPE,
        )
        stdout, _ = proc.communicate()
        sys.stdout.flush()

        retcode = proc.returncode
        if retcode != 0:
            print('Failed to get the cloned repository remote. Exit code: {0}'.format(retcode))
            sys.stdout.flush()
            if options.delete_vm:
                delete_vm(options)
            sys.exit(retcode)

        if not stdout:
            print('Failed to get the cloned repository remote(no output). Exit code: {0}'.format(retcode))
            sys.stdout.flush()
            if options.delete_vm:
                delete_vm(options)
            sys.exit(retcode)

        try:
            remotes_info = json.loads(stdout.strip())
            if remotes_info is not None:
                if options.peer:
                    remotes_info = remotes_info['local']
                else:
                    remotes_info = remotes_info[options.vm_name]

            if remotes_info is None or options.test_git_url not in remotes_info:
                print('The cloned repository remote is not the desired one:')
                print(' {0!r} is not in {1}'.format(options.test_git_url, remotes_info))
                sys.stdout.flush()
                if options.delete_vm:
                    delete_vm(options)
                sys.exit(retcode)
            print('matches!')
        except ValueError:
            print('Failed to load any JSON from {0!r}'.format(stdout.strip()))

    if options.test_git_commit is not None:
        time.sleep(1)

        # Let's find out if the cloned repository is checked out at the desired
        # commit
        print('Grabbing the cloned repository commit information ... ')
        cmd = []
        if options.peer:
            cmd.extend(['salt-call', '--out=json', 'publish.publish'])
        else:
            cmd.extend(['salt', '-t', '100', '--out=json'])

        cmd.extend([minion_target, 'git.revision', '/testing'])

        if options.peer and ' ' in minion_target:
            cmd.append('expr_form="compound"')

        cmd = ' '.join(cmd)

        print('Running CMD: {0!r}'.format(cmd))
        sys.stdout.flush()
        proc = subprocess.Popen(
            cmd,
            shell=True,
            stdout=subprocess.PIPE,
            stderr=subprocess.PIPE,
        )
        stdout, _ = proc.communicate()
        sys.stdout.flush()

        retcode = proc.returncode
        if retcode != 0:
            print('Failed to get the cloned repository revision. Exit code: {0}'.format(retcode))
            sys.stdout.flush()
            if options.delete_vm:
                delete_vm(options)
            sys.exit(retcode)

        if not stdout:
            print('Failed to get the cloned repository revision(no output). Exit code: {0}'.format(retcode))
            sys.stdout.flush()
            if options.delete_vm:
                delete_vm(options)
            sys.exit(retcode)

        try:
            revision_info = json.loads(stdout.strip())
            if options.peer:
                revision_info = revision_info['local']
            else:
                revision_info = revision_info[options.vm_name]

            if revision_info[:7] != options.test_git_commit[:7]:
                print('The cloned repository commit is not the desired one:')
                print(' {0!r} != {1!r}'.format(revision_info[:7], options.test_git_commit[:7]))
                sys.stdout.flush()
                if options.delete_vm:
                    delete_vm(options)
                sys.exit(retcode)
            print('matches!')
        except ValueError:
            print('Failed to load any JSON from {0!r}'.format(stdout.strip()))

    # Run tests here

    time.sleep(3)
    cmd = []
    if options.peer:
        cmd.append('salt-call')
        if options.no_color:
            cmd.append('--no-color')
        cmd.append('publish.publish')
    else:
        cmd.extend(['salt', '-t', '1800'])
        if options.no_color:
            cmd.append('--no-color')

    cmd.extend([
        minion_target,
        'state.sls',
        options.sls,
        'pillar="{0}"'.format(build_pillar_data(options))
    ])

    if options.peer and ' ' in minion_target:
        cmd.append('expr_form="compound"')

    cmd = ' '.join(cmd)

    print('Running CMD: {0!r}'.format(cmd))
    sys.stdout.flush()

    proc = subprocess.Popen(
        cmd,
        shell=True,
        stdout=subprocess.PIPE,
        stderr=subprocess.PIPE,
    )
    stdout, _ = proc.communicate()

    if stdout:
        print(stdout)
    sys.stdout.flush()

    try:
        match = re.search(r'Test Suite Exit Code: (?P<exitcode>[\d]+)', stdout)
        retcode = int(match.group('exitcode'))
    except AttributeError:
        # No regex matching
        retcode = 1
    except ValueError:
        # Not a number!?
        retcode = 1
    except TypeError:
        # No output!?
        retcode = 1
        if stdout:
            # Anything else, raise the exception
            raise

    if options.download_remote_reports:
        # Download unittest reports
        download_unittest_reports(options)
        # Download coverage report
        download_coverage_report(options)

    if options.download_remote_logs:
        download_remote_logs(options)

    if options.delete_vm:
        delete_vm(options)
    parser.exit(proc.returncode)


if __name__ == '__main__':
    main()
