#!/usr/bin/python3

# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.

"""
This is a script used to automatically log details from an Anaconda
install back to Beaker. Derived from Cobbler's anamon.
"""

import os
import errno
import subprocess
import sys
import threading
import time
import base64
import shlex
from xmlrpc.client import Server


class UploadCommand(object):

    def __init__(self, alias):
        self.size = 0
        self.data = ''
        self.last_length = 0
        self.alias = alias
        self.session = Server(xmlrpc_url, allow_none=True)

    def upload(self):
        tries = 1
        retries = 3
        while tries <= retries:
            debug("recipe_upload_file(%r, '/', %r, %r, %r, %r, ...)\n"
                  % (recipe_id, self.alias, self.size, '', self.last_length))
            if self.session.recipe_upload_file(recipe_id, '/', self.alias,
                                               self.size, '', self.last_length, self.data):
                self.last_length += self.size
                return True
            tries += 1
        return False


class WatchedCommand(UploadCommand):
    """ Reads stdout of a subprocess and uploads changes via XMLRPC. Works
        with regular (tail) and interactive (tail -f) commands. """

    def __init__(self, command, args_list, alias):
        super(WatchedCommand, self).__init__(alias)
        self.command = command
        self.args_list = args_list
        self.output = ''

    def watch(self):
        try:
            proc = subprocess.Popen([self.command] + self.args_list, stdout=subprocess.PIPE)
        except OSError as e:
            debug("'%s' failed to run; got '%s'. Closing thread..." % (self.command, e))
            return

        while True:
            time.sleep(4.5)
            self.output = proc.stdout.readline()
            # If the process dies, let's stop
            if not self.output and proc.poll() is not None:
                break
            self.upload()

    def upload(self):
        self.size = len(self.output)
        self.data = base64.encodebytes(self.output).decode('ascii')
        super(WatchedCommand, self).upload()


class WatchedFile(UploadCommand):
    def __init__(self, fn, alias):
        super(WatchedFile, self).__init__(alias)
        self.fn = fn

    def exists(self):
        return os.access(self.fn, os.F_OK)

    def changed(self):
        if not self.exists():
            return 0
        size = os.stat(self.fn)[6]

        return size > self.last_length

    def upload(self, block_size=262144):
        """
        Upload a file in chunks via XMLRPC
        """

        with open(self.fn, 'rb') as fd:
            while True:
                fd.seek(self.last_length, os.SEEK_SET)
                contents = fd.read(block_size)
                self.size = len(contents)
                if self.size == 0:
                    break
                self.data = base64.encodebytes(contents).decode('ascii')
                del contents
                status = super(WatchedFile, self).upload()
                if not status:
                    break

    def update(self):
        if not self.changed():
            return
        try:
            self.upload()
        except:
            raise


class MountWatcher:

    def __init__(self,mp):
        self.mountpoint = mp
        self.zero()

    def zero(self):
        self.line=''
        self.time = time.time()

    def update(self):
        found = 0
        try:
            fd = open('/proc/mounts')
        except IOError as e:
            if e.errno != errno.ENOENT:
                raise
        else:
            while 1:
                line = fd.readline()
                if not line:
                    break
                parts = line.split()
                mp = parts[1]
                if mp == self.mountpoint:
                    found = 1
                    if line != self.line:
                        self.line = line
                        self.time = time.time()
            fd.close()
        if not found:
            self.zero()

    def stable(self):
        self.update()
        if self.line and (time.time() - self.time > 60):
            return 1
        else:
            return 0


def anamon_loop():

    default_watchlist = [
        WatchedFile("/tmp/anaconda.log", "anaconda.log"),
        WatchedFile("/tmp/syslog", "sys.log"),
        WatchedFile("/tmp/X.log", "X.log"),
        WatchedFile("/tmp/lvmout", "lvmout.log"),
        WatchedFile("/tmp/storage.log", "storage.log"),
        WatchedFile("/tmp/program.log", "program.log"),
        WatchedFile("/tmp/vncserver.log", "vncserver.log"),
        WatchedFile("/tmp/ks.cfg", "ks.cfg"),
        WatchedFile("/run/install/ks.cfg", "ks.cfg"),
        WatchedFile("/tmp/ks-script.log", "ks-script.log"),
        WatchedFile("/tmp/anacdump.txt", "anacdump.txt"),
        WatchedFile("/tmp/modprobe.conf", "modprobe.conf"),
        WatchedFile("/tmp/ifcfg.log", "ifcfg.log"),
        WatchedFile("/tmp/packaging.log", "packaging.log"),
        WatchedFile("/tmp/yum.log", "yum.log"),
        WatchedFile("/tmp/dnf.librepo.log", "dnf.librepo.log"),
        WatchedFile("/tmp/hawkey.log", "hawkey.log"),
        WatchedFile("/tmp/lvm.log", "lvm.log"),
    ]

    # Setup '/mnt/sysimage' watcher
    sysimage = MountWatcher("/mnt/sysimage")

    # Monitor for {install,upgrade}.log changes
    package_logs = list()
    package_logs.append(WatchedFile("/mnt/sysimage/root/install.log", "install.log"))
    package_logs.append(WatchedFile("/mnt/sysimage/tmp/install.log", "tmp+install.log"))
    package_logs.append(WatchedFile("/mnt/sysimage/root/upgrade.log", "upgrade.log"))
    package_logs.append(WatchedFile("/mnt/sysimage/tmp/upgrade.log", "tmp+upgrade.log"))
    package_logs.append(WatchedFile("/mnt/sysimage/root/install.log.syslog", "install.log.syslog"))

    # Monitor for bootloader configuration changes
    bootloader_cfgs = list()
    bootloader_cfgs.append(WatchedFile("/mnt/sysimage/boot/grub/grub.conf", "grub.conf"))
    bootloader_cfgs.append(WatchedFile("/mnt/sysimage/boot/etc/yaboot.conf", "yaboot.conf"))
    bootloader_cfgs.append(WatchedFile("/mnt/sysimage/boot/efi/efi/redhat/elilo.conf", "elilo.conf"))
    bootloader_cfgs.append(WatchedFile("/mnt/sysimage/etc/zipl.conf", "zipl.conf"))

    watched_commands = [
        WatchedCommand("journalctl", ["-f"], "systemd_journal.log"),
    ]

    for proc in watched_commands:
        t = threading.Thread(target=proc.watch)
        t.start()

    # Were we asked to watch specific files?
    watchlist = list()
    waitlist = list()
    if watchfiles:
        # Create WatchedFile objects for each requested file
        for watchfile in watchfiles:
            if os.path.exists(watchfile):
                watchfilebase = os.path.basename(watchfile)
                watchlog = WatchedFile(watchfile, watchfilebase)
                watchlist.append(watchlog)

    # Use the default watchlist and waitlist
    else:
        watchlist = list(default_watchlist)
        waitlist.extend(package_logs)
        waitlist.extend(bootloader_cfgs)

    # Monitor loop
    already_added_anaconda_tbs = []
    anaconda_tb_pattern = 'anaconda-tb'
    while 1:
        time.sleep(5)
        for f in os.listdir('/tmp'):
            if f.startswith(anaconda_tb_pattern):
                f = os.path.join('/tmp', f)
                if f not in already_added_anaconda_tbs:
                    watchlist.append(WatchedFile(f, os.path.basename(f)))
                    already_added_anaconda_tbs.append(f)

        # Not all log files are available at the start, we'll loop through the
        # waitlist to determine when each file can be added to the watchlist
        for watch in waitlist[:]:
            if sysimage.stable() and watch.exists():
                debug("Adding %s from wait list to watch list\n" % watch.alias)
                watchlist.append(watch)
                waitlist.remove(watch)

        # Send any updates
        for wf in watchlist:
            wf.update()

        # If asked to run_once, exit now
        if exit:
            break

# Establish some defaults
recipe_id = None
xmlrpc_url = ''
daemon = 1
debug = lambda x,**y: None
watchfiles = []
exit = False

# Process command-line args
n = 0
while n < len(sys.argv):
    arg = sys.argv[n]
    if arg == '--recipe-id':
        n = n+1
        recipe_id = sys.argv[n]
    elif arg == '--watchfile':
        n = n+1
        watchfiles.extend(shlex.split(sys.argv[n]))
    elif arg == '--exit':
        exit = True
    elif arg == '--xmlrpc-url':
        n = n+1
        xmlrpc_url = sys.argv[n]
    elif arg == '--debug':
        debug = lambda x,**y: sys.stderr.write(x % y)
    elif arg == '--fg':
        daemon = 0
    n = n+1


# Fork and loop
if daemon:
    if not os.fork():
        # Redirect the standard I/O file descriptors to the specified file.
        DEVNULL = getattr(os, "devnull", "/dev/null")
        os.open(DEVNULL, os.O_RDWR) # standard input (0)
        os.dup2(0, 1)               # Duplicate standard input to standard output (1)
        os.dup2(0, 2)               # Duplicate standard input to standard error (2)

        anamon_loop()
        # Child process should exit using _exit
        os._exit(1)
    sys.exit(0)
else:
    anamon_loop()
