#!/usr/bin/python

import logging
import os
import os.path
import re
import subprocess
import sys
import time
import types

PROGPATH = sys.argv[0]
PROGNAME = os.path.basename(PROGPATH)
PROGLOCN = os.path.dirname(PROGPATH)

VERSION = '2'
PACKAGE = '1.8'
STAGING = '/dsp_staging'
MDSIZE = '32M'
INSTALLTYPE = 'RELEASE'

# For DEBUG_LEVEL and VERBOSE_LEVEL, the default value None
# means let the INSTALLTYPE value set these automatically.
# If you want to manually override the defaults, simply set
# these to appropriate isi_dsptool strings.
# For example, "--debug", or "-ddd", are acceptable strings.
# See debug_verbosity().

DEBUG_LEVEL = None
VERBOSE_LEVEL = None

MDUNIT = None

DSPTOOL = '/usr/bin/isi_hwtools/isi_dsptool'

DONE_FILE = '/ifs/.ifsvar/run/dsp_driveconfig.done'

LOG_FILE = '/var/log/isi_dsptool.log'
LOG_LEVEL = logging.DEBUG
LOG_FORMAT = "%(asctime)s %(levelname)-8s: %(message)s"
DATE_FORMAT_LOG = "%Y-%m-%d %H:%M:%S"

# VERBOSE is not to be confused with isi_dsptool VEROBSE_LEVEL (above).
# This flag controls the console verbosity of the dsp_install script.
# False == By default, do not report to console (only log file).

VERBOSE = False

_logger = None

def init_logger():
    global _logger
    try:
        logger = logging.getLogger(PROGNAME)
        logger.setLevel(LOG_LEVEL)
        logformat = logging.Formatter(LOG_FORMAT, datefmt=DATE_FORMAT_LOG)
        loghandle = logging.FileHandler(LOG_FILE)
        loghandle.setFormatter(logformat)
        logger.addHandler(loghandle)
    except IOError:
        print("IOError during logger init: %s, %s" % (PROGNAME, LOG_FILE))
        logger = None
    _logger = logger

def close_logger():
    logging.shutdown()

def log(msg, level=logging.INFO):
    global _logger
    if _logger:
        _logger.log(level, "%s: %s" % (PROGNAME, msg))
    if VERBOSE:
        err = level >= logging.ERROR
        tag = {
            logging.CRTICAL: 'CRITICAL: ',
            logging.ERROR: 'ERROR: ',
            logging.WARN: 'WARN: ',
            logging.INFO: 'INFO: ',
            logging.DEBUG: 'DEBUG: ',
            }.get(level, '')
        print >> (err and sys.stderr or sys.stdout), tag+msg

exit_on_error = True
def error(msg, exit=False):
    global exit_on_error
    log(msg, level=logging.ERROR)
    if exit_on_error and exit:
        sys.exit(1)

def fatal(msg):
    error(msg, exit=True)

def debug(msg):
    if LOG_LEVEL <= logging.DEBUG:
        log(msg, level=logging.DEBUG)

def system_command_output(args, shell=True, ignore_stderr=False):
    pipe = subprocess.PIPE
    # Due to annoyances with '*' wildcards, and shell=True or False, etc,
    # always pass a string to Popen.
    if isinstance(args, basestring):
        cmd = args
    else:
        cmd = ' '.join(args)
    p = subprocess.Popen(cmd, stdout=pipe, stderr=pipe, shell=shell)
    o, e = p.communicate()
    rc = p.returncode or (not ignore_stderr and e) and 1 or 0
    return (rc, filter(None, e.split('\n')), filter(None, o.split('\n')))

def determine_mdunit():
    global MDUNIT
    rc, err, out = system_command_output("mount")
    # Ignore errors
    regex = re.compile("^/dev/md([0-9]+)[ ]+on[ ]+%s.*" % STAGING)
    match = filter(None, [regex.match(line) for line in out])
    if len(match) == 0:
        MDUNIT = None
    elif len(match) == 1:
        MDUNIT = int(match[0].group(1))
    else:
        MDUNIT = -1
        error('Failed to determine mdunit: out = %s' % ', '.join(out))

def detach_mddevice():
    global MDUNIT
    if MDUNIT is None:
        # No MD to detach
        pass
    elif MDUNIT == -1:
        error('Unable to determine mdunit to detach')
    else:
        command = ["mdconfig", "-d", "-u", str(MDUNIT)]
        rc, err, out = system_command_output(command)
        if rc or err:
            error('Failure detaching mdunit %s' % MDUNIT)

def unmount_staging():
    need_unmount = False
    rc, err, out = system_command_output("mount")
    if not err:
        for line in out:
            if STAGING in line:
                need_unmount = True
    if need_unmount:
        rc, err, out = system_command_output("umount %s" % STAGING)
        if rc or err:
            error('Failure unmounting staging %s' % err)

# forced_local:
# isi pkg does not pass any flags indicating if --forced_local was
# set by user. However, the isi.ui.shell.pkgcmd code does set
# enviroment variables which we can use:
# def set_local_env():
#     os.environ["PKG_DBDIR"] = "/var/db/isilon-pkg"
#     os.environ["PKG_TMPDIR"] = "/var/tmp"
#     os.environ["PKG_LOCAL_ONLY"] = "true"
# def query_pkg(pkgs, forced_local):
#     if forced_local:
#         set_local_env()

def forced_local():
    forced_local = os.environ.get("PKG_LOCAL_ONLY", False)
    return forced_local and "--forced_local" or ""

def debug_verbosity():
    dlevel, vlevel = "", ""
    if INSTALLTYPE == "INTERNAL":
        dlevel, vlevel = "-ddd", "-vvv"
    if INSTALLTYPE == "RELEASE":
        dlevel, vlevel = "", ""
    if DEBUG_LEVEL:
        dlevel = DEBUG_LEVEL
    if VERBOSE_LEVEL:
        vlevel = VERBOSE_LEVEL
    return "%s %s" % (dlevel, vlevel)

actions = {
    'iscript': [
    ('mkdir %s' % STAGING, ["mkdir", "-p", STAGING]),
    ('mdmfs -s %s' % MDSIZE, ["mdmfs", "-S", "-s", MDSIZE, "md", STAGING]),
    ('rm %s' % DONE_FILE, ["rm", "-f", DONE_FILE]),
    ],
    'piscript': [
    ('install staging files', [DSPTOOL, "install",
                               debug_verbosity,
                               forced_local,
                               "--version", VERSION,
                               "--package", PACKAGE,
                               "--staging", STAGING]),
    ('determine mdunit', (determine_mdunit, )),
    ('unmount %s' % STAGING, (unmount_staging, )),
    ('detach mddevice', (detach_mddevice, )),
    # Only delete the contents of staging, since isi pkg wants it around
    ('delete %s' % STAGING, ["rm", "-rf", "%s/*" % STAGING]),
    ],
    'dscript': [
    ],
    'pdscript': [
    ('determine mdunit', (determine_mdunit, )),
    ('unmount %s' % STAGING, (unmount_staging, )),
    ('detach mddevice', (detach_mddevice, )),
    ('delete %s' % STAGING, ["rm", "-rf", STAGING]),
    ('rm %s' % DONE_FILE, ["rm", "-f", DONE_FILE]),
    ],
    }

def exec_action(action, command):
    rc, err, out = None, [], []
    try:
        if isinstance(command, types.FunctionType):
            command = command()
            debug("=> %s: %s" % (action, command))
            rc, err, out = system_command_output(command)
        elif isinstance(command, list):
            # If there are any functions, evaluate them now
            def evalcmdf(cmd):
                if isinstance(cmd, basestring):
                    return cmd
                else:
                    return cmd()
            command = map(evalcmdf, command)
            debug("=> %s: %s" % (action, command))
            rc, err, out = system_command_output(command)
        elif isinstance(command, tuple):
            debug("=> %s" % (action))
            rc, err = command[0](*command[1:]), []
        else:
            error('Invalid command: exec_action(%s, %s)' % (action, command))
            rc, err, out = 1, [], []
        valid = [ (0, [], []), (None, [], []) ]
        debug("<= %s: %s" % (action,
                             (rc, err, out) in valid and "OK"
                             or str((rc, err, out))))
    except Exception:
        error('Unhandled exception in exec_action(%s, %s): %s' %
              (action, command, sys.exc_info()[1]))
        rc = 1
    if rc or err:
        error('Failed to %s' % action)
        return 1
    return 0

def exec_actions(script):
    global exit_on_error
    exit_on_error = not script in ['pdscript']
    result = 0
    for action, command in actions.get(script):
        rc = exec_action(action, command)
        if rc:
            result = 1
            if exit_on_error:
                break
    return result

# iscript:
# *IF* there are any failures during this setup (such as mdconfig),
# *THEN* always clean up, mdconfig -d, etc.
# XXX TODO: try/finally?, to clean up, mdconfig -d if any errors
# (complicated a bit by our use of func error() which exits...)

# 'pdscript':
# *IF* there are any failures during this cleanup (such as mdconfig),
# *THEN* always push forward, with others, and return PASS,
# so that 'isi pkg delete' will always complete (isi pkg delete
# failure results in roll-back, results in NOT uninstalling pkg!)
# Use global exit_on_error for this.

def main(args):
    result = 1
    script = pkgdir = pkgname = None
    if PROGNAME.startswith("+INSTALL"):
        script = 'iscript'
    elif PROGNAME.startswith("+POST-INSTALL"):
        script = 'piscript'
    elif PROGNAME.startswith("+DEINSTALL"):
        script = 'dscript'
    elif PROGNAME.startswith("+POST-DEINSTALL"):
        script = 'pdscript'
    else:
        error("Invalid program call: %s: %s" % (PROGNAME, args))
    if script:
        minlens = {'iscript':2, 'piscript':2, 'dscript':1, 'pdscript':1}
        minlen = minlens.get(script, 0)
        if len(args) < minlen: # Could be > minlen ??
            error("Invalid arguments: %s" % args)
        elif minlen == 1:
            pkgdir, pkgname = None, args[0]
        elif minlen == 2:
            pkgdir, pkgname = args[0], args[1]
        else:
            error("Invalid minlen=%s" % minlen)
    if pkgname:
        log("%s: %s: %s" % (script, pkgname, pkgdir))
        result = exec_actions(script)
    return (script, result)

if __name__ == '__main__':
    result, script = 0, None
    try:
        init_logger()
        try:
            script, result = main(sys.argv[1:])
        except Exception:
            error("Unhandled Exception: %s" % sys.exc_info()[1])
            result = 1
    finally:
        log("%s: %s" % (script, result and 'FAIL' or 'PASS'))
        close_logger()
    sys.exit(result and 1 or 0)
