#!/usr/bin/env python

# Copyright C 2002-2012 by Isilon Systems, LLC. All rights not expressly
# granted herein are reserved.
# Created in the United States of America.
#
# This digital media is protected by U.S. and international copyright and
# intellectual property laws. Unless otherwise specified, all information
# and screens appearing as part of this digital medium, including
# software, services, documents, text, images, icons, and logos design;
# the selection, assembly, arrangement, and design thereof; and the code
# that enables its presentation, are the sole property of Isilon Systems,
# Inc.
#
# Except as otherwise required by applicable law, no part of any
# copyrighted material may be reproduced, modified, translated,
# distributed, published, or transmitted by any means, electronic or
# mechanical, for any purpose, without the prior written consent of Isilon
# Systems.
#
# Isilon and Isilon Systems are registered trademarks of Isilon Systems,
# Inc.  Legion is a trademark of Isilon Systems, Inc.  All other
# trademarks and registered trademarks are the property of their
# respective holders.

'''This script is used to verify that a cluster is currently capable of
upgrading/reimaging/joining in the manner specified. It is nondestructive and
can be used outside of a maintenance window'''

import cPickle
import sys
import getopt
import syslog
import traceback
import os

import isi.sys.version as sysver
import isi.app.lib as lib
import isi.sys.update.check as check
import isi.sys.update.util as util

MOUNT_BOX = 'buildbiox.west.isilon.com:/ifs/isilon/snapshots'
BUILD_DIR = '/usr/update/mnt/bb'
VER_FILE_1 = 'boot/kernel/kernel'
VER_PATTERN_1 = r'.*Isilon OneFS v\S+ B_\S+: ([0xX]{0,2}[0-9a-fA-F]+):'
VER_FILE_2 = 'boot/kernel.*/version.rc'
VER_PATTERN_2 = r"kern_osrevision=([0xX]{0,2}[0-9a-fA-F]+)"
LOCAL_CHECKER = '/var/db/versions/checker'
LOCAL_SHIM = '/var/db/versions/shim'

options = {'allow_down_nodes': False,
        'build': None,
        'check_type': None,  # join/reimage/upgrade
        'checker': None,  # installed/localfile
        'daemon': False,
        'from_cache': False,
        'info_source': None,  # node/cluster
        'srctar': None,  # installtar
        'timeout': None,
        'version': None,
        'verbose': False}

def usage(exitno):
    '''Display usage information to the user'''
    print """\
Usage %s [Options]
Options:
  --allow-down-nodes Don't fail on failed rpcs, use what info you can gather.

  -h                 Show Usage.

  --daemon, -d       Output to log files instead of stdout.

  --verbose, -v      Produce verbose output.

  --join             Run check_join from Checker Context File. Exactly one of
                     --join, --reimage (default), or --upgrade must be
                     specified.

  --on_cluster, -m   Get cluster info from gather_cluster_info() and node info
                     from stdin. Exactly one of --on_node (default) and
                     --on_cluster must be specified.

  --on_node          Get node info from gather_node_info() and cluster info
  (default)          from stdin. Exactly one of --on_node and --on_cluster must
                     be specified.

  --reimage          Run check_reimage from Checker Context File. Exactly one
  (default)          of --join, --reimage (default), --upgrade must be
                     specified. Requires --version.

  --timeout=TIMEOUT  Use TIMEOUT as the rpc timeout instead of the default
                     infinity.

  --upgrade, -u      Run check_upgrade from Checker Context File. Exactly one
                     of --join, --reimage, --upgrade must be specified.
                     Requires --version.

  --version=VERSION  Target OneFS Version for check_reimage and check_upgrade.
                     VERSION is a 64 buit integer. Accepts decimal or hex (hex
                     must begin with "0x"). If --version is specified, -b, or
                     --installtar may not be specified.

  --from-cache       In case of rpc error, try retrieving from the cache.

  --checker=FILE     Use the specified Checker Context File. Only one of
                     --installtar, -b, or --checker may be specified.
                     Requires --version.

  --shim=FILE        Use the specified shim. Only one of --installtar, -b, or
                     --shim may be specified. Requires --version.

  --installtar=TAR   Use the Checker Context File inside an install.tar. The
                     target OneFS version will be taken from this file. If
                     --installtar is specified, -b, --checker, or --version may
                     not be specified.

  -b build           Use the Checker Context File and version associated with a
                     particular build. The target OneFS version will be taken
                     from this build. If -b is specified, --installtar,
                     --checker, or --version may not be specified.

A return value of 1 indicates an error executing the script, with detail going
to stderr.  A return value of 0 indicates successful execution of the script.
An empty stdout indicates all checks passes. A non-empty stdout indicates
failed checks, with the details.


""" % sys.argv[0]
    os._exit(exitno)


def get_options():
    '''Parse the command line and return a dictionary of options present'''
    try:
        opts = getopt.getopt(sys.argv[1:], 'b:dfhmruv', ['allow-down-nodes',
                'checker=', 'shim=', 'daemon', 'from-cache', 'installtar=',
                'join', 'on_cluster', 'on_node', 'reimage', 'timeout=',
                'upgrade', 'verbose', 'version='])[0]
    except getopt.GetoptError:
        usage(1)

    for opt, arg in opts:
        if opt == '--allow-down-nodes':
            options['allow_down_nodes'] = True

        if opt == '-b':
            if options['version'] or options['srctar']:
                usage(1)
            options['srctar'] = BUILD_DIR + '/' + arg + '/obj/install.tar'
            _mount_bb()

        if opt == '--checker':
            if options['srctar']:
                usage(1)
            options['checker'] = arg

        if opt == '--shim':
            if options['srctar']:
                usage(1)
            options['shim'] = arg

        if opt == '--daemon' or opt == '-d':
            options['daemon'] = True

        if opt == '--from-cache':
            options['from_cache'] = True

        if opt == '-h':
            usage(0)

        if opt == '--installtar':
            if options['version'] or options['srctar']:
                usage(1)
            options['srctar'] = arg

        if opt == '--join':
            if options['check_type']:
                usage(1)
            options['check_type'] = 'join'

        if opt == '--on_node':
            if options['info_source']:
                usage(1)
            options['info_source'] = 'node'

        if opt == '--on_cluster' or opt == '-m':
            if options['info_source']:
                usage(1)
            options['info_source'] = 'cluster'

        if opt == '--reimage':
            if options['check_type']:
                usage(1)
            options['check_type'] = 'reimage'

        if opt == '--timeout':
            options['timeout'] = float(arg)

        if opt == '--upgrade' or opt == '-u':
            if options['check_type']:
                usage(1)
            options['check_type'] = 'upgrade'

        if opt == '--version':
            if options['srctar']:
                usage(1)
            try:
                options['version'] = int(arg)
            except ValueError:
                options['version'] = int(arg, 16)

        if opt == '--verbose' or opt == '-v':
            options['verbose'] = True

    options['info_source'] = options['info_source'] or 'node'

    options['check_type'] = options['check_type'] or 'reimage'

    # It's only OK to not specify a version if we are either in join mode or
    # we can get the version from the specified install.tar
    if not options['version'] and not (options['check_type'] == 'join' or \
            options['srctar']):
        usage(1)


def log(msg, level=syslog.LOG_ERR):
    '''Displays the message as appropriate (stderr / logfile)'''
    if options.get('daemon'):
        syslog.syslog(level | syslog.LOG_USER,
                "isi_version_checker " + msg)
    if not options.get('daemon') or level < syslog.LOG_INFO:
        print >> sys.stderr, msg


def _mount_bb():
    '''Mounts buildbiox for use in accessing the tar associated with a build'''
    if (not os.path.exists(BUILD_DIR)):
        os.makedirs(BUILD_DIR)
    cmd = '/sbin/mount_nfs -T -3 -R 1 -a 4 -o tcp -r32768 -w32768 ' + \
            '%s %s' % (MOUNT_BOX, BUILD_DIR)
    ret, out = lib.procs.get_cmd_output(cmd, raw=True)
    if ret:
        log("Could not mount build dir: \n%s" % out)


def _umount_bb():
    '''Unmounts buildbiox after we have used the tar associated with a build'''
    if (os.path.ismount(BUILD_DIR)):
        ret, out = lib.procs.get_cmd_output('/sbin/umount ' + BUILD_DIR)
        if ret:
            log("Could not umount build dir: \n%s" % out)


def _get_version_from_tar():
    '''Returns the version associated with a given install.tar'''
    tar_file = options.get('srctar')
    if not os.path.isfile(tar_file):
        log("Couldn't find specified file: %s" % tar_file)
        return None
    try:
        util_ver_obj = util.Version(VER_PATTERN_1, VER_FILE_1)
        str_version = util_ver_obj.get_version(tar_file)
    except util.UtilException:
        util_ver_obj = util.Version(VER_PATTERN_2, VER_FILE_2)
        str_version = util_ver_obj.get_version(tar_file)
    try:
        int_version = int(str_version)
    except ValueError:
        int_version = int(str_version, 16)

    return int_version


def _get_info(shim):
    '''Gather node_info / cluster_info. Returns (node_info, cluster_info)'''
    node_info = None
    cluster_info = None

    check_type = options.get('check_type')
    info_source = options.get('info_source')
    timeout = options.get('timeout')
    allow_down_nodes = options.get('allow_down_nodes')
    from_cache = options.get('from_cache')

    # Join: node_info and cluster_info
    # Reimage: node_info
    # Upgrade: cluster_info

    # Cluster info
    if check_type in ['join', 'upgrade']:
        if info_source == 'node':
            try:
                cluster_info = cPickle.load(sys.stdin)
            except EOFError:
                log("Error: received no cluster info on stdin.")
                return (None, None)
        elif info_source == 'cluster':
            try:
                cluster_info = sysver.gather_cluster_info(shim, timeout,
                        allow_down_nodes, from_cache)
            except sysver.GatherError as err:
                log("Error while gathering cluster info: %s" % str(err))
                return (None, None)

    # Node info
    if check_type in ['join', 'reimage']:
        if info_source == 'node':
            try:
                nodegatherer = sysver.Gatherer(shim)
                node_info = nodegatherer.gather()
            except sysver.GatherError as err:
                log("Error while gathering node info: %s" % str(err))
                return (None, None)
        elif info_source == 'cluster':
            try:
                node_info = cPickle.load(sys.stdin)
            except EOFError:
                log("Error: Received no node info on stdin.")
                return (None, None)
            sysver.update_cache(node_info)

    return (node_info, cluster_info)


def _get_checker():
    '''Returns the shim and the raw contents of the checker as a string'''
    # Determine if we should run the installed checker or the new one
    # If they specify a checker file, _always_ run that
    # If the current version is greater than the dest version, run local checks
    current_version = None
    current_version_int = None
    context = None
    shim = None

    if options.get('check_type') in ['reimage', 'upgrade']:
        if options.get('srctar'):
            options['version'] = _get_version_from_tar()

        try:
            current_version = util.Version(VER_PATTERN_1).get_running_version()
        except util.UtilException:
            current_version = util.Version(VER_PATTERN_2).get_running_version()

        try:
            current_version_int = int(current_version)
        except ValueError:
            current_version_int = int(current_version, 16)

        if not options.get('version') or not current_version_int:
            log("Unable to gather version information")
            return None

        # only use the install.tar shim/checker if it is newer
        if options.get('srctar') and options['version'] > current_version_int:
            log("Using shim, checker from %s (%s)\n" % (options['srctar'],
                    hex(options['version'])), syslog.LOG_INFO)

            # Get the checker
            cmd = "/usr/bin/tar -Oxf '" + options['srctar'] \
                    + "' var/db/versions/checker"
            ret1, context = lib.procs.get_cmd_output(cmd, raw=True)
            if ret1:
                log("Fatal error getting checker")

            # Get the shim
            cmd2 = "/usr/bin/tar -Oxf '" + options['srctar'] \
                    + "' var/db/versions/shim"
            ret2, shim = lib.procs.get_cmd_output(cmd2, raw=True)
            if ret2:
                log("Fatal error getting shim")

            _umount_bb()

            if ret1 or ret2:
                return None

    if not shim:
        actual_shim = options.get('shim') or LOCAL_SHIM
        log("Using shim %s\n" % actual_shim, syslog.LOG_INFO)
        shimfile = open(actual_shim)
        shim = shimfile.read()
        shimfile.close()

    if not context:
        actual_checker = options.get('checker') or LOCAL_CHECKER
        log("Using checker %s\n" % actual_checker, syslog.LOG_INFO)

        checkerfile = open(actual_checker, 'r')
        context = checkerfile.read()
        checkerfile.close()


    return shim, context


def run_checks():
    '''Run the appropriate preupgrade checks for the given options'''
    try:
        shim, checker_context = _get_checker()
    except IOError, (errnumber, errorstring):
        log("IOError %d, could not retrieve checker: %s" % (errnumber,
                errorstring))
        return False
    if not checker_context:
        log("Unable to get checker file, aborting compatibility check")
        return False

    try:
        node_info, cluster_info = _get_info(shim)
    except Exception as err:
        log("Could not gather cluster / node info:\n%s" % err)
        return False
    if not (node_info or cluster_info):
        log("Could not gather cluster / node info")
        return False

    exec checker_context in locals()

    extended_results = []

    try:
        if options['check_type'] == 'reimage':
            res = check_reimage(node_info, options['version'])
        elif options['check_type'] == 'upgrade':
            extended_results += \
                    check.run_current_preupgrade_checks(options['verbose'])
            res = check_upgrade(cluster_info, options['version'])
        elif options['check_type'] == 'join':
            res = check_join(cluster_info, node_info)
    except KeyError:
        log("Error while running %s checks" % options['check_type'])
        return False

    if options.get('daemon') and res != True:
        print res
        return res

    extended_results += checker_get_extended_results()

    for result in extended_results:
        if result:
            log(result.to_string(indent='* '), syslog.LOG_INFO)
    if not res:
        log("All checks passed.")

    return True


if __name__ == "__main__":
    try:
        get_options()
        completed = run_checks()
        if not completed:
            log("Unable to complete all checks")
            os._exit(1)
    except Exception as main_err:
        log("isi_version_checker encountered an unexpected error.")
        log(str(main_err))
        errmsg = reduce(lambda x, y: x + y,
                traceback.format_exception(sys.exc_info()[0],
                sys.exc_info()[1], sys.exc_info()[2]))
        log("failed \n %s" % errmsg)
        _umount_bb()
        os._exit(3)
