#!/usr/bin/env python
#
# Copyright (c) 2012-2022 Dell Inc. or its subsidiaries. All Rights Reserved.
#
from __future__ import print_function

import logging
import os.path
import re
import subprocess
import sys
import types
from builtins import map

import six

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

VERSION = "2"
PACKAGE = "1.61"
STAGING = "/dsp_staging"
MDSIZE = "288M"
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(tag + msg, file=(sys.stderr if err else sys.stdout))


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, six.string_types):
        cmd = args
    else:
        cmd = " ".join(args)
    p = subprocess.Popen(cmd, stdout=pipe, stderr=pipe, shell=shell)
    o, e = p.communicate()
    o_str = o.decode("utf-8")
    e_str = e.decode("utf-8")
    rc = p.returncode or (not ignore_stderr and e_str) and 1 or 0
    return (
        rc,
        [_f for _f in e_str.split("\n") if _f],
        [_f for _f in o_str.split("\n") if _f],
    )


def determine_mdunit():
    global MDUNIT
    rc, err, out = system_command_output("mount")
    # Ignore errors
    regex = re.compile(r"^/dev/md([0-9]+)[ ]+on[ ]+%s.*" % STAGING)
    match = [_f for _f in [regex.match(line) for line in out] if _f]
    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, six.string_types):
                    return cmd
                else:
                    return cmd()

            command = list(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 = script not 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)
