#!/usr/bin/python -u
# "-u": Force stdin, stdout and stderr to be  totally  unbuffered.
#       To get more accurate log files
# 
# see also 
# http://www.faqts.com/knowledge_base/entry/versions/index.phtml?aid=4419

import datetime
import glob
import logging
import multiprocessing
import os
import shutil
import sys
import time
import subprocess
from ConfigParser import SafeConfigParser

# check if we run from a bzr checkout
if os.path.exists("./UpgradeTestBackend.py"):
    sys.path.insert(0, "../")

from optparse import OptionParser
from AutoUpgradeTester.UpgradeTestBackend import UpgradeTestBackend

# bigger exitcode means more problematic error,
# exitcode 101 means generic error
class FailedToBootstrapError(Exception):
    """ Failed to initial bootstrap the test system """
    exitcode = 99

class FailedToUpgradeError(Exception):
    """ Failed to upgrade the test system """
    exitcode = 98

class FailedPostUpgradeTestError(Exception):
    """ Some post upgrade tests failed """
    exitcode = 97

class OutputThread(multiprocessing.Process):
    def __init__(self, filename):
        multiprocessing.Process.__init__(self)
        self.file = os.open(filename, os.O_RDONLY)
    def run(self):
        while True:
            # the read() seems to not block for some reason
            # but return "" ?!?
            s = os.read(self.file, 1024)
            if s:
                print s,
            else:
                time.sleep(0.1)

# FIXME: move this into the generic backend code
def login(backend):
    """ login into a backend """
    backend.bootstrap()
    backend.login()

def createBackend(backend_name, profile):
    """ create a backend of the given name for the given profile """
    try:
        backend_full_name = "AutoUpgradeTester.%s" % backend_name
        backend_modul = __import__(backend_full_name, fromlist="AutoUpgradeTester")
        backend_class = getattr(backend_modul, backend_name)
    except (ImportError, AttributeError, TypeError), e:
        logging.exception("can not import '%s'" % backend_name)
        return None
    return backend_class(profile)

class HtmlOutputStream:
    HTML_HEADER=r"""
<?xml version="1.0" encoding="ascii"?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
          "DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
<head>
  <title>Auto upgrade tester</title>
<style type="text/css">
.error { background-color:#FF6600; }
.warning { background-color:#FFA000; }
.aright { text-align:right; }
table { width:90%%; }
</style>
</head>
<body>
<h1>Automatic upgrade tester test results</h1>

<p>Upgrade test started %(date)s</p>

<table border="1">
<tr><th>Profile</th><th>Result</th><th>Bugs</th><th>Date Finished</th><th>Runtime</th><th>Full Logs</th></tr>
"""

    HTML_FOOTER="""
</table>
<p>Upgrade test finished %s</p>
</body>
"""

    def __init__(self, outputdir=None):
        self.html_output = None
        if outputdir:
            self.outputdir = outputdir
            if not os.path.exists(outputdir):
                os.makedirs(outputdir)
            self.html_output = open(os.path.join(outputdir, "index.html"), "w")
    def write(self, s):
        if self.html_output:
            self.html_output.write(s)
            self.html_output.flush()
    def copy_results_and_add_table_link(self, profile_name, resultdir):
        if not self.html_output:
            return
        # copy logs & sanitize permissions
        targetdir = os.path.join(self.outputdir, profile_name)
        html_output.copytree(resultdir, targetdir)
        for f in glob.glob(targetdir+"/*"):
            os.chmod(f, 0644)
        # write html line that links to output dir
        s = "<td><a href=\"./%(logdir)s\">Logs for %(profile)s test</a></td>" %  {
            'logdir' : profile_name,
            'profile': profile_name, }
        html_output.write(s)
    def copytree(self, src, dst):
        if self.html_output:
            shutil.copytree(src, dst)
    def write_html_header(self, time_started):
        self.write(self.HTML_HEADER % { 'date' : time_started })
    def write_html_footer(self):
        self.write(self.HTML_FOOTER % datetime.datetime.now())
    
def testUpgrade(backend_name, profile, add_pkgs, quiet=False,
                html_output=HtmlOutputStream()):
    """ test upgrade for the given backend/profile """
    backend = createBackend(backend_name, profile)
    assert(backend != None)
    if not os.path.exists(profile):
        print "ERROR: Can't find '%s' " % profile
        html_output.write("<tr><td>Can't find profile '%s'</td>" % profile)
        html_output.write(4*"<td></td>")
        return 2
    print "Storing the result in '%s'" % backend.resultdir
    profile_name = os.path.split(os.path.normpath(profile))[1]
    # setup output
    outfile = os.path.join(backend.resultdir, "bootstrap.log")
    fd = os.open(outfile, os.O_WRONLY|os.O_CREAT|os.O_TRUNC, 0644)
    out = OutputThread(outfile)
    out.daemon = True
    if not quiet:
        out.start()
    old_stdout = os.dup(1)
    old_stderr = os.dup(2)
    os.dup2(fd, 1)
    os.dup2(fd, 2)
    time_started = datetime.datetime.now()
    print "%s log started" % time_started
    result = 0
    try:
        # write what we working on
        html_output.write("<td>%s</td>" % profile_name)
        # init 
        if not backend.bootstrap():
            print "FAILED: bootstrap for '%s'" % profile
            html_output.write("<td class=\"error\">Failed to bootstrap</td>")
            raise FailedToBootstrapError("Failed to bootstrap '%s'" % profile)
        if add_pkgs:
            if not backend.installPackages(add_pkgs):
                print "FAILED: installPacakges '%s'" % add_pkgs
                html_output.write("<td class=\"error\">Failed to add pkgs '%s'</td>" % ",".join(add_pkgs))
                raise Exception, "Failed to install packages '%s'" % add_pkgs
        if not backend.upgrade():
            print "FAILED: upgrade for '%s'" % profile
            html_output.write("<td class=\"error\">Failed to upgrade</td>")
            raise FailedToUpgradeError("Failed to upgrade %s" % profile)
        if not backend.test():
            print "FAILED: test for '%s'" % profile
            html_output.write("<td class=\"warning\">Upgraded, but post upgrade test failed</td>")
            raise FailedPostUpgradeTestError("Failed in post upgrade test %s" % profile)
        print "profile: %s worked" % profile
        html_output.write("<td>ok</td>")
    except (FailedToBootstrapError, FailedToUpgradeError, FailedPostUpgradeTestError) as e:
        print e
        result = e.exitcode
    except Exception, e:
        import traceback
        traceback.print_exc()
        print "Caught exception in testUpgrade for '%s' (%s)" % (profile, e)
        html_output.write("<td class=\"error\">Unknown failure (should not happen)</td>")
        result = 2
    finally:
        # print out result details
        print "Logs can be found here:"
        for n in ["bootstrap.log", "main.log", "apt.log"]:
            print " %s/%s" % (backend.resultdir, n)

        time_ended = datetime.datetime.now()
        print "%s log ended." % time_ended
        print "Duration: %s" % (time_ended - time_started)

        # give the output time to settle and kill the daemon
        time.sleep(2)
        if out.is_alive():
            out.terminate()
            out.join()
        # write result line
        s="<td></td><td>%(date)s</td><td class=\"aright\">%(runtime)s</td>" % {
            'date'    : datetime.datetime.now(),
            'runtime' : datetime.datetime.now() - time_started}
        html_output.write(s)
        html_output.copy_results_and_add_table_link(profile_name, backend.resultdir)
        html_output.write("</tr>")
    # close and restore file descriptors
    os.close(fd)
    os.dup2(old_stdout, 1)
    os.dup2(old_stderr, 2)
    return result

def profileFromAptClone(aptclonefile):
    """ Create a profile from an apt-clone archive

    :param aptclonefile: Path to an apt-clone archive
    :return: path to profile or None if it fails to recognize the clone file
    """
    profile = None
    try:
        cmd = ('apt-clone', 'info', aptclonefile)
        output = subprocess.check_output(cmd)
        clone_properties = {}
        for k, v in [ line.split(': ') for line in output.split('\n') if ': ' in line]:
            clone_properties[k] = v
        try:
            clone_date = datetime.datetime.strptime(clone_properties['Date'], '%c').strftime('_%Y%m%d-%H%M%S')
        except:
            clone_date = ''
        profilename = "%s_%s_%s%s" % (
            clone_properties['Distro'],
            clone_properties['Arch'],
            clone_properties['Hostname'],
            clone_date)

        # Generate configuration file for this profile
        # FIXME:
        # update packaging to grant write access on base for upgrade-tester user
        ppath = 'profile' if os.path.exists("./UpgradeTestBackend.py") else base
        profile = os.path.join(ppath, profilename)
        profilecfg = SafeConfigParser()
        if not os.path.exists(profile):
            os.makedirs(profile)

        profilecfg.set('DEFAULT', 'SourceRelease', clone_properties['Distro'])
        profilecfg.add_section('NonInteractive')
        profilecfg.set('NonInteractive', 'ProfileName', profilename)
        profilecfg.set('NonInteractive', 'AptcloneFile', os.path.abspath(aptclonefile))
        profilecfg.add_section('KVM')
        profilecfg.set('KVM', 'Arch', clone_properties['Arch'])
        with open(os.path.join(profile, 'DistUpgrade.cfg'), 'wb') as profilefile:
            profilecfg.write(profilefile)

    except subprocess.CalledProcessError:
        return None

    return profile

if __name__ == "__main__":
    logging.basicConfig(level=logging.INFO)

    parser = OptionParser()
    parser.add_option("--additinoal", "--additional-pkgs", dest="add_pkgs", 
                      default="",
                      help="add additional pkgs before running the upgrade")
    parser.add_option("-a", "--auto", dest="auto", default=False,
                      action="store_true",
                      help="run all tests in profile/ dir")
    parser.add_option("--bootstrap-only", "--bootstrap-only",dest="bootstrap_only", 
                      default=False,
                      action="store_true",
                      help="only bootstrap the image")
    parser.add_option("--tests-only", "", default=False,
                      action="store_true",
                      help="only run post_upgrade_tests in the image")
    parser.add_option("-l", "--login", dest="login", default=False,
                      action="store_true",
                      help="log into the a profile")
    parser.add_option("-b", "--backend", dest="backend",
                      default="UpgradeTestBackendQemu",
                      help="UpgradeTestBackend to use: UpgradeTestBackendChroot, UpgradeTestBackendQemu")
    parser.add_option("-d", "--diff", dest="gen_diff",
                      default=False, action="store_true",
                      help="generate a diff of the upgraded image versus a fresh installed image")
    parser.add_option("--quiet", "--quiet", default=False, action="store_true",
                      help="quiet operation")
    parser.add_option("", "--html-output-dir", default=None,
                      help="html output directory")
    
    (options, args) = parser.parse_args()

    # save for later
    fd1 = os.dup(1)
    fd2 = os.dup(2)

    # FIXME: move this to a configuration
    base="/usr/share/auto-upgrade-tester/profiles/"
    # check if we have something to do
    profiles = args
    if not profiles and not options.auto:
        print sys.argv[0]
        print "No profile specified, available default profiles:"
        print "\n".join(os.listdir(base))
        print 
        print "Or use '-a' for 'auto' mode"
        sys.exit(1)

    # generic
    res = 0
    add_pkgs = []
    if  options.add_pkgs:
        add_pkgs = options.add_pkgs.split(",")
    # auto mode
    if options.auto:
        print "running in auto-mode"
        for d in os.listdir(base):
            os.dup2(fd1, 1)
            os.dup2(fd2, 2)
            print "Testing '%s'" % d
            res = max(res, testUpgrade(options.backend, base+d, add_pkgs))
        sys.exit(res)
    # profile mode, test the given profiles
    time_started = datetime.datetime.now()

    # clean output dir
    html_output = HtmlOutputStream(options.html_output_dir)
    html_output.write_html_header(time_started)
    for profile in profiles:

        # case of an apt-clone archive
        if os.path.isfile(profile):
            profilename = profileFromAptClone(profile)
            if not profilename:
                print "File is not a valid apt-clone image: %s" % profilename
                continue
            profile = profilename
            print "Using generated profile: %s" % profile

        if not "/" in profile:
            profile = base + profile
        try:
            if options.login:
                backend = createBackend(options.backend, profile)
                login(backend)
            elif options.bootstrap_only:
                backend = createBackend(options.backend, profile)
                backend.bootstrap(force=True)
            elif options.tests_only:
                backend = createBackend(options.backend, profile)
                backend.test()
            elif options.gen_diff:
                backend = createBackend(options.backend, profile)
                backend.genDiff()
            else:
                print "Testing '%s'" % profile
                now = datetime.datetime.now()
                current_res = testUpgrade(options.backend, profile, 
                                          add_pkgs, options.quiet,
                                          html_output)
                if current_res == 0: 
                    print "Profile '%s' worked" % profile
                else:
                    print "Profile '%s' FAILED" % profile
                res = max(res, current_res)
        except Exception, e:
            import traceback
            print "ERROR: exception caught '%s' " % e
            html_output.write('<pre class="error">unhandled ERROR %s:\n%s</pre>' % (e, traceback.format_exc()))
            traceback.print_exc()
            if hasattr(e, "exitcode"):
                res = max(res, e.exitcode)
            else:
                res = max(res, 101)
    html_output.write_html_footer()

    # return exit code
    sys.exit(res)
