#!/usr/bin/env python
# -*- coding: utf-8 -*-
'''
This script is used to test Salt from a Jenkins server, specifically
jenkins.saltstack.com.

This script is intended to be shell-centric!!
'''

# Import python libs
from __future__ import absolute_import, print_function
import glob
import os
import re
import sys
import json
import time
import shutil
import optparse
import subprocess
import random

# Import Salt libs
import salt.utils
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

# Import 3rd-party libs
import yaml
try:
    import requests
    HAS_REQUESTS = True
except ImportError:
    HAS_REQUESTS = False

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


def build_pillar_data(options):
    '''
    Build a YAML formatted string to properly pass pillar data
    '''
    pillar = {'test_transport': options.test_transport,
              'cloud_only': options.cloud_only,
              'with_coverage': options.test_without_coverage is False}
    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.package_source_dir:
        pillar['package_source_dir'] = options.package_source_dir
    if options.package_build_dir:
        pillar['package_build_dir'] = options.package_build_dir
    if options.package_artifact_dir:
        pillar['package_artifact_dir'] = options.package_artifact_dir
    if options.pillar:
        pillar.update(dict(options.pillar))
    return yaml.dump(pillar, default_flow_style=True, indent=0, width=sys.maxint).rstrip()


def build_minion_target(options, vm_name):
    target = vm_name
    for grain in options.grain_target:
        target += ' and G@{0}'.format(grain)
    if options.grain_target:
        return '"{0}"'.format(target)
    return target


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.platform, random_part)


def delete_vm(options):
    '''
    Stop a VM
    '''
    cmd = 'salt-cloud -d {0} -y'.format(options.delete_vm)
    print('Running CMD: {0}'.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(interval=0.5)
    proc.communicate()


def echo_parseable_environment(options, parser):
    '''
    Echo NAME=VAL parseable output
    '''
    output = []

    if options.platform:
        name = generate_vm_name(options)
        output.extend([
            'JENKINS_SALTCLOUD_VM_PLATFORM={0}'.format(options.platform),
            'JENKINS_SALTCLOUD_VM_NAME={0}'.format(name)
        ])

    if options.provider:
        output.append(
            'JENKINS_SALTCLOUD_VM_PROVIDER={0}'.format(options.provider)
        )

    if options.pull_request:
        # This is a Jenkins triggered Pull Request
        # We need some more data about the Pull Request available to the
        # environment
        if HAS_REQUESTS is False:
            parser.error(
                'The python \'requests\' library needs to be installed'
            )

        headers = {}
        url = 'https://api.github.com/repos/saltstack/salt/pulls/{0}'.format(options.pull_request)

        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()
        output.extend([
            'SALT_PR_GIT_URL={0}'.format(pr_details['head']['repo']['clone_url']),
            'SALT_PR_GIT_BRANCH={0}'.format(pr_details['head']['ref']),
            'SALT_PR_GIT_COMMIT={0}'.format(pr_details['head']['sha']),
            'SALT_PR_GIT_BASE_BRANCH={0}'.format(pr_details['base']['ref']),
        ])

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


def download_unittest_reports(options):
    print('Downloading remote unittest reports...')
    sys.stdout.flush()

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

    os.makedirs(xml_reports_path)

    cmds = (
        'salt {0} archive.tar zcvf /tmp/xml-test-reports.tar.gz \'*.xml\' cwd=/tmp/xml-unittests-output/',
        'salt {0} cp.push /tmp/xml-test-reports.tar.gz',
        'mv -f /var/cache/salt/master/minions/{1}/files/tmp/xml-test-reports.tar.gz {2} && '
        'tar zxvf {2}/xml-test-reports.tar.gz -C {2}/xml-test-reports && '
        'rm -f {2}/xml-test-reports.tar.gz'
    )

    vm_name = options.download_unittest_reports
    for cmd in cmds:
        cmd = cmd.format(build_minion_target(options, vm_name), vm_name, workspace)
        print('Running CMD: {0}'.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(interval=0.5)
        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):
    print('Downloading remote coverage report...')
    sys.stdout.flush()

    workspace = options.workspace
    vm_name = options.download_coverage_report

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

    cmds = (
        'salt {0} archive.gzip /tmp/coverage.xml',
        'salt {0} cp.push /tmp/coverage.xml.gz',
        'gunzip /var/cache/salt/master/minions/{1}/files/tmp/coverage.xml.gz',
        'mv /var/cache/salt/master/minions/{1}/files/tmp/coverage.xml {2}'
    )

    for cmd in cmds:
        cmd = cmd.format(build_minion_target(options, vm_name), vm_name, workspace)
        print('Running CMD: {0}'.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(interval=0.5)
        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):
    print('Downloading remote logs...')
    sys.stdout.flush()

    workspace = options.workspace
    vm_name = options.download_remote_logs

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

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

    cmds = []

    for remote_log in options.remote_log_path:
        cmds.extend([
            'salt {{0}} archive.gzip {0}'.format(remote_log),
            'salt {{0}} cp.push {0}.gz'.format(remote_log),
            'gunzip /var/cache/salt/master/minions/{{1}}/files{0}.gz'.format(remote_log),
            'mv /var/cache/salt/master/minions/{{1}}/files{0} {{2}}/{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(build_minion_target(options, vm_name), vm_name, workspace)
        print('Running CMD: {0}'.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(interval=0.5)
        proc.communicate()
        if proc.returncode != 0:
            print(
                '\nFailed to execute command. Exit code: {0}'.format(
                    proc.returncode
                )
            )
        time.sleep(0.25)


def download_packages(options):
    print('Downloading packages...')
    sys.stdout.flush()

    workspace = options.workspace
    vm_name = options.download_packages

    for fglob in ('salt-*.rpm',
                  'salt-*.deb',
                  'salt-*.pkg.xz',
                  'salt-buildpackage.log'):
        for fname in glob.glob(os.path.join(workspace, fglob)):
            if os.path.isfile(fname):
                os.unlink(fname)

    cmds = [
        ('salt {{0}} archive.tar czf {0}.tar.gz sources=\'*.*\' cwd={0}'
         .format(options.package_artifact_dir)),
        'salt {{0}} cp.push {0}.tar.gz'.format(options.package_artifact_dir),
        ('tar -C {{2}} -xzf /var/cache/salt/master/minions/{{1}}/files{0}.tar.gz'
         .format(options.package_artifact_dir)),
    ]

    for cmd in cmds:
        cmd = cmd.format(build_minion_target(options, vm_name), vm_name, workspace)
        print('Running CMD: {0}'.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(interval=0.5)
        proc.communicate()
        if proc.returncode != 0:
            print(
                '\nFailed to execute command. Exit code: {0}'.format(
                    proc.returncode
                )
            )
        time.sleep(0.25)


def run(opts):
    '''
    RUN!
    '''
    vm_name = os.environ.get(
        'JENKINS_SALTCLOUD_VM_NAME',
        generate_vm_name(opts)
    )

    if opts.download_remote_reports:
        if opts.test_without_coverage is False:
            opts.download_coverage_report = vm_name
        opts.download_unittest_reports = vm_name
        opts.download_packages = vm_name

    if opts.bootstrap_salt_commit is not None:
        if opts.bootstrap_salt_url is None:
            opts.bootstrap_salt_url = 'https://github.com/saltstack/salt.git'
        cmd = (
            'salt-cloud -l debug'
            ' --script-args "-D -g {bootstrap_salt_url} -n git {1}"'
            ' -p {provider}_{platform} {0}'.format(
                vm_name,
                os.environ.get(
                    'SALT_MINION_BOOTSTRAP_RELEASE',
                    opts.bootstrap_salt_commit
                ),
                **opts.__dict__
            )
        )
    else:
        cmd = (
            'salt-cloud -l debug'
            ' --script-args "-D -n git {1}" -p {provider}_{platform} {0}'.format(
                vm_name,
                os.environ.get(
                    'SALT_MINION_BOOTSTRAP_RELEASE',
                    opts.bootstrap_salt_commit
                ),
                **opts.__dict__
            )
        )
    if opts.splay is not None:
        # Sleep a random number of seconds
        cloud_downtime = random.randint(0, opts.splay)
        print('Sleeping random period before calling salt-cloud: {0}'.format(cloud_downtime))
        time.sleep(cloud_downtime)
    print('Running CMD: {0}'.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(interval=0.5)
    proc.communicate()

    retcode = proc.returncode
    if retcode != 0:
        print('Failed to bootstrap VM. Exit code: {0}'.format(retcode))
        sys.stdout.flush()
        if opts.clean and 'JENKINS_SALTCLOUD_VM_NAME' not in os.environ:
            delete_vm(opts)
        sys.exit(retcode)

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

    # Sleep a random number of seconds
    bootstrap_downtime = random.randint(0, opts.splay)
    print('Sleeping for {0} seconds to allow the minion to breathe a little'.format(bootstrap_downtime))
    sys.stdout.flush()
    time.sleep(bootstrap_downtime)

    if opts.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 = 'salt -t 100 {0} --out json test.version'.format(build_minion_target(opts, vm_name))
        print('Running CMD: {0}'.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 opts.clean and 'JENKINS_SALTCLOUD_VM_NAME' not in os.environ:
                delete_vm(opts)
            sys.exit(retcode)

        outstr = salt.utils.to_str(stdout).strip()
        if not outstr:
            print('Failed to get the bootstrapped minion version(no output). Exit code: {0}'.format(retcode))
            sys.stdout.flush()
            if opts.clean and 'JENKINS_SALTCLOUD_VM_NAME' not in os.environ:
                delete_vm(opts)
            sys.exit(retcode)

        try:
            version_info = json.loads(outstr)
            bootstrap_minion_version = os.environ.get(
                'SALT_MINION_BOOTSTRAP_RELEASE',
                opts.bootstrap_salt_commit[:7]
            )
            print('Minion reported salt version: {0}'.format(version_info))
            if bootstrap_minion_version not in version_info[vm_name]:
                print('\n\nATTENTION!!!!\n')
                print('The boostrapped minion version commit does not contain the desired commit:')
                print(
                    ' \'{0}\' does not contain \'{1}\''.format(
                        version_info[vm_name],
                        bootstrap_minion_version
                    )
                )
                print('\n\n')
                sys.stdout.flush()
                #if opts.clean and 'JENKINS_SALTCLOUD_VM_NAME' not in os.environ:
                #    delete_vm(opts)
                #sys.exit(retcode)
            else:
                print('matches!')
        except ValueError:
            print('Failed to load any JSON from \'{0}\''.format(outstr))

    if opts.cloud_only:
        # Run Cloud Provider tests preparation SLS
        cloud_provider_downtime = random.randint(3, opts.splay)
        time.sleep(cloud_provider_downtime)
        cmd = (
            'salt -t 900 {target} state.sls {cloud_prep_sls} pillar="{pillar}" '
            '--no-color'.format(
                target=build_minion_target(opts, vm_name),
                cloud_prep_sls='cloud-only',
                pillar=build_pillar_data(opts),
            )
        )
    else:
        # Run standard preparation SLS
        standard_sls_downtime = random.randint(3, opts.splay)
        time.sleep(standard_sls_downtime)
        cmd = (
            'salt -t 1800 {target} state.sls {prep_sls} pillar="{pillar}" '
            '--no-color'.format(
                target=build_minion_target(opts, vm_name),
                prep_sls=opts.prep_sls,
                pillar=build_pillar_data(opts),
            )
        )
    print('Running CMD: {0}'.format(cmd))
    sys.stdout.flush()

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

    if stdout:
        print(salt.utils.to_str(stdout))
    if stderr:
        print(salt.utils.to_str(stderr))
    sys.stdout.flush()

    retcode = proc.returncode
    if retcode != 0:
        print('Failed to execute the preparation SLS file. Exit code: {0}'.format(retcode))
        sys.stdout.flush()
        if opts.clean and 'JENKINS_SALTCLOUD_VM_NAME' not in os.environ:
            delete_vm(opts)
        sys.exit(retcode)

    if opts.cloud_only:
        cloud_provider_pillar = random.randint(3, opts.splay)
        time.sleep(cloud_provider_pillar)
        # Run Cloud Provider tests pillar preparation SLS
        cmd = (
            'salt -t 600 {target} state.sls {cloud_prep_sls} pillar="{pillar}" '
            '--no-color'.format(
                target=build_minion_target(opts, vm_name),
                cloud_prep_sls='cloud-test-configs',
                pillar=build_pillar_data(opts),
            )
        )
        print('Running CMD: {0}'.format(cmd))
        sys.stdout.flush()

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

        if stdout:
            # DO NOT print the state return here!
            print('Cloud configuration files provisioned via pillar.')
        if stderr:
            print(salt.utils.to_str(stderr))
        sys.stdout.flush()

        retcode = proc.returncode
        if retcode != 0:
            print('Failed to execute the preparation SLS file. Exit code: {0}'.format(retcode))
            sys.stdout.flush()
            if opts.clean and 'JENKINS_SALTCLOUD_VM_NAME' not in os.environ:
                delete_vm(opts)
            sys.exit(retcode)

    if opts.prep_sls_2 is not None:
        sls_2_downtime = random.randint(3, opts.splay)
        time.sleep(sls_2_downtime)

        # Run the 2nd preparation SLS
        cmd = (
            'salt -t 30 {target} state.sls {prep_sls_2} pillar="{pillar}" '
            '--no-color'.format(
                prep_sls_2=opts.prep_sls_2,
                pillar=build_pillar_data(opts),
                target=build_minion_target(opts, vm_name),
            )
        )
        print('Running CMD: {0}'.format(cmd))
        sys.stdout.flush()

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

        if stdout:
            print(salt.utils.to_str(stdout))
        if stderr:
            print(salt.utils.to_str(stderr))
        sys.stdout.flush()

        retcode = proc.returncode
        if retcode != 0:
            print('Failed to execute the 2nd preparation SLS file. Exit code: {0}'.format(retcode))
            sys.stdout.flush()
            if opts.clean and 'JENKINS_SALTCLOUD_VM_NAME' not in os.environ:
                delete_vm(opts)
            sys.exit(retcode)

    # Run remote checks
    if opts.test_git_url is not None:
        test_git_downtime = random.randint(1, opts.splay)
        time.sleep(test_git_downtime)
        # Let's find out if the cloned repository if checked out from the
        # desired repository
        print('Grabbing the cloned repository remotes information ... ')
        cmd = 'salt -t 100 {0} --out json git.remote_get /testing'.format(build_minion_target(opts, vm_name))
        print('Running CMD: {0}'.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 cloned repository remote. Exit code: {0}'.format(retcode))
            sys.stdout.flush()
            if opts.clean and 'JENKINS_SALTCLOUD_VM_NAME' not in os.environ:
                delete_vm(opts)
            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 opts.clean and 'JENKINS_SALTCLOUD_VM_NAME' not in os.environ:
                delete_vm(opts)
            sys.exit(retcode)

        try:
            remotes_info = json.loads(stdout.strip())
            if remotes_info is None or remotes_info[vm_name] is None or opts.test_git_url not in remotes_info[vm_name]:
                print('The cloned repository remote is not the desired one:')
                print(' \'{0}\' is not in {1}'.format(opts.test_git_url, remotes_info))
                sys.stdout.flush()
                if opts.clean and 'JENKINS_SALTCLOUD_VM_NAME' not in os.environ:
                    delete_vm(opts)
                sys.exit(retcode)
            print('matches!')
        except ValueError:
            print('Failed to load any JSON from \'{0}\''.format(salt.utils.to_str(stdout).strip()))

    if opts.test_git_commit is not None:
        test_git_commit_downtime = random.randint(1, opts.splay)
        time.sleep(test_git_commit_downtime)

        # Let's find out if the cloned repository is checked out at the desired
        # commit
        print('Grabbing the cloned repository commit information ... ')
        cmd = 'salt -t 100 {0} --out json git.revision /testing'.format(build_minion_target(opts, vm_name))
        print('Running CMD: {0}'.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 opts.clean and 'JENKINS_SALTCLOUD_VM_NAME' not in os.environ:
                delete_vm(opts)
            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 opts.clean and 'JENKINS_SALTCLOUD_VM_NAME' not in os.environ:
                delete_vm(opts)
            sys.exit(retcode)

        try:
            revision_info = json.loads(stdout.strip())
            if revision_info[vm_name][7:] != opts.test_git_commit[7:]:
                print('The cloned repository commit is not the desired one:')
                print(' \'{0}\' != \'{1}\''.format(revision_info[vm_name][:7], opts.test_git_commit[:7]))
                sys.stdout.flush()
                if opts.clean and 'JENKINS_SALTCLOUD_VM_NAME' not in os.environ:
                    delete_vm(opts)
                sys.exit(retcode)
            print('matches!')
        except ValueError:
            print('Failed to load any JSON from \'{0}\''.format(salt.utils.to_str(stdout).strip()))

    # Run tests here
    test_begin_downtime = random.randint(3, opts.splay)
    time.sleep(test_begin_downtime)
    cmd = (