#!/usr/libexec/isilon/isi_for_array

# Copyright C 2002-2008 by Isilon Systems, Inc. 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.

"""
 $Id: isi_for_array 380474 2013-12-23 19:39:19Z jbauman $
"""

import getopt
import getpass
import os
import re
import signal
import string
import sys
import tempfile
import xml.dom.minidom

import isi.fs.restripe.group as group

try:
    True
except NameError:
    True = 1
    False = 0

ssh_only_flags = '-k, -u, -p, and -q flags are only valid for ssh transport'

def usage(exitCode):
    sys.stderr.write('''
Usage: %s [OPTION]... [COMMAND]...

Options:
  -a, --array-name ARRAY        Use ARRAY.
  -A, --array-file FILE         Read array information from FILE. The default is
                                to first look for $HOME/.array.xml, and then
                                /etc/ifs/array.xml.
  -d, --directory DIR           Execute commands from DIR on remote machine.
                                Defaults to current working directory. An empty
                                DIR will result in commands being executed in
                                the users home directory on the remote machine.
  -D, --diskless                Run commands from diskless nodes.
  -h, --help                    Print this message and exit.
  -I, --ignore-errors           Suppress printing of messages for nodes that
                                return nonzero exit status. Still return the
                                max exit status from all nodes.
  -k, --known-hosts-file FILE   Use FILE for ssh known hosts file instead of
                                default /dev/null.
  -l, -u, --user USER           Login as USER instead of default root.
  -n, --nodes NODES             Run commands on the given nodes. Can be
                                specified multiple times. Must be a list of
                                either node names or ranges of node IDs. For
                                example: '1,3-5,neal8,10'. If no nodes are
                                explicitly listed, the whole array is used.
  -p, --pw, --password PASSWORD Use PASSWORD instead of default 'a'.
  -P, --pre-command COMMAND     Execute COMMAND before any other commands. This
                                is useful for setting up the environment. May
                                be specified multiple times. An COMMAND of '-'
                                resets the list of commands.
  -q, --query-password          Prompt the user for a password.
  -Q, --quiet                   Do not print host prefix for each output line
  -s, --serial                  Run commands in serial, not parallel.
  -S, --storage                 Run commands from storage nodes.
  -t, --transport TRANSPORT     Network transport. Default is rpc.
                                Valid transports: rpc, ssh
  -T, --throttle COUNT          Adjust throttling, or 0 to disable throttling.
                                Default is 24.
  -x, --exclude-nodes NODES     Don't run commands on NODES. This argument is
                                specified the same way as the -n option is.
  -X, --exclude-down-nodes      Don't run commands on down nodes. Cluster local
                                use Only.

  Note: %s

Environment Variables:

  ISI_ARRAY_NAME                Specifies default array NAME.
  ISI_ARRAY_FILE                Specifies default array FILE.
  ISI_ARRAY_NODES               Specifies default NODES.

  ISI_RDO_DIRECTORY             Specifies default DIRECTORY.
  ISI_RDO_KNOWN_HOSTS_FILE      Specifies default ssh known hosts FILE.
  ISI_RDO_USER                  Specifies default USER.
  ISI_RDO_PASSWORD              Specifies default PASSWORD.
  ISI_RDO_PASSWORD_FILE         Specifies default FILE to read password from.
                                This option is overridden by ISI_RDO_PASSWORD,
                                if both are set.
  ISI_RDO_PRE_COMMANDS          Specifies COMMANDS to run before command-line
                                COMMANDS. COMMANDS are separated by newlines.

Options specified later on the command line override environment variables as
well as options specified earlier on the command line. The exceptions to this
rule are pre-commands and node inclusion/exclusion. The -n and -x flags are
cumulative and evaluated in order.

If no COMMANDS are given on the command line, they are read from standard input.

''' % (os.path.basename(sys.argv[0]), ssh_only_flags))
    sys.exit(exitCode)

def main():
    # don't treat ^C special
    signal.signal(signal.SIGINT, signal.SIG_DFL)

    password = None
    preArgs = ['isi_rdo']
    isiNodesArgs = ['isi_nodes']
    query = False
    quiet = False
    excludeDownNodes = False
    ignoreErrors = False
    # Keep large clusters from overloading a node.
    throttleRate = 24

    # parse options
    try:
        opts, args = getopt.getopt(sys.argv[1:],
             'a:A:d:Dhk:l:n:p:P:qQsSu:x:t:T:XI',
            ['array-name=', 'array-file=', 'directory=', 'diskless',
             'exclude-nodes=', 'help', 'known-hosts-file=', 'user=', 'nodes=',
             'pw=', 'password=', 'pre-command=', 'query-password', 'quiet',
             'serial', 'storage', 'user', 'transport=', 'throttle=',
             'exclude-down-nodes', 'ignore-errors'])
        for o, a in opts:
            if o in ['-a', '--array-name']:
                isiNodesArgs += [o, a]
            if o in ['-A', '--array-file']:
                isiNodesArgs += [o, a]
            if o in ['-d', '--directory']:
                preArgs += [o, a]
            if o in ['-D', '--diskless']:
                isiNodesArgs += [o]
            if o in ['-h', '--help']:
                usage(0)
            if o in ['-k', '--known-hosts-file']:
                preArgs += [o, a]
            if o in ['-l', '-u', '--user']:
                preArgs += [o, a]
            if o in ['-n', '--nodes']:
                isiNodesArgs += [o, a]
            if o in ['-p', '--pw', '--password']:
                password = o
                preArgs += [o, a]
            if o in ['-P', '--pre-command']:
                preArgs += [o, a]
            if o in ['-q', '--query-password']:
                query = o
            if o in ['-Q', '--quiet']:
                quiet = o
            if o in ['-s', '--serial']:
                throttleRate = 1
            if o in ['-S', '--storage']:
                isiNodesArgs += [o]
            if o in ['-t', '--transport']:
                preArgs += [o, a]
            if o in ['-T', '--throttle']:
                throttleRate = int(a)
            if o in ['-x', '--exclude-nodes']:
                isiNodesArgs += [o, a]
            if o in ['-X', '--exclude-down-nodes']:
                excludeDownNodes = True
            if o in ['-I', '--ignore-errors']:
                ignoreErrors = True
    except getopt.GetoptError:
        usage(1)

    try:
        if excludeDownNodes == True:
            gi = group.GroupInfo()
            down_node_ids = gi.getDownNodeList()
            if down_node_ids:
                for ni in down_node_ids:
                    isiNodesArgs += ['-x', str(gi.getLnn(ni))]

        # figure out which nodes to run on
        nodes = runIsiNodes(isiNodesArgs)

        # query for the password if they requested it
        if query:
            if password:
                error("can't specify both %s and %s" % (password, query))
            password = getpass.getpass()
        else:
            password = None

        # either pass commands through stdin or as arguments
        input = None
        postArgs = []
        if len(args) > 0:
            postArgs += args
        else:
            input = ''
            while True:
                data = os.read(0, 1024)
                if not len(data):
                    break
                else:
                    input += data

        # run everything
        children = {}
        errors = []
        exitStatus = 0
        longestName = ""
        for nodeName, nodeAddress in nodes:
            if len(nodeName) > len(longestName):
                longestName = nodeName
        
        while nodes or children:
            # Run up to throttleRate children.
            while nodes and (throttleRate == 0 or
                     len(children) < throttleRate):
                (nodeName, nodeAddress) = nodes.pop(0)
                child = os.fork()
                if not child:
                    if password != None:
                        tempFile = tempfile.TemporaryFile('w+b', -1,
                            'isi_for_array')
                        tempFile.write(password)
                        tempFile.flush()
                        tempFile.seek(0)
                        # as of Python 2.3, TemporaryFile fd's are not
                        # inherited by child procs so dup it for the exec below.
                        dupfd = os.dup(tempFile.fileno())
                        tempFile.close()
                        preArgs += ['-q', '/dev/fd/%d' % dupfd]
                    if input != None:
                        tempFile = tempfile.TemporaryFile('w+b', -1,
                            'isi_for_array')
                        tempFile.write(input)
                        tempFile.flush()
                        tempFile.seek(0)
                        # dup fd over stdin for use in exec'd proc
                        os.dup2(tempFile.fileno(), 0)
                        tempFile.close()
                    if not quiet:
                        preArgs += ['-o', '%-*s '%
                            (len(longestName) + 1, nodeName + ':')]
                    preArgs += [nodeAddress]
                    os.execvp('isi_rdo', preArgs + postArgs)
                else:
                    children[child] = nodeName

            # Wait for a child to complete so we can spawn another.
            (pid, status) = waitForPid()
            if status:
                errors.append("%s exited with status %d" % (children[pid],
                                                            status))
            if pid:
                try:
                    del children[pid]
                    exitStatus = max(status, exitStatus)
                except KeyError:
                    # Not a child we're monitoring.
                    pass

        if not quiet and not ignoreErrors:
            for error in errors:
                print error
        sys.exit(exitStatus)

    except EnvironmentError, e:
        error(str(e))

def runIsiNodes(args):
    nodes = []
    pipeRead, pipeWrite = os.pipe()
    child = os.fork()
    if child:
        os.close(pipeWrite)
        pipeIn = os.fdopen(pipeRead, 'r')
        pattern = re.compile('(\S+) (.+)')
        while True:
            line = pipeIn.readline()
            if not len(line):
                break
            match = pattern.match(line)
            if not match:
                error('bad line from isi_nodes: %s' % line.rstrip())
            nodes.append((match.group(2), match.group(1)))
        (_, status) = waitForPid()
        if status:
            error('isi_nodes exited with status %d' % status)
        if not nodes:
            error('isi_nodes failed')
        return nodes
    else:
        os.close(pipeRead)
        os.dup2(pipeWrite, 1)
        if pipeWrite != 1:
            os.close(pipeWrite)
        os.execvp('isi_nodes', args + [r'%{internal_address} %{node}'])

def waitForPid():
    while True:
        pid, status = os.waitpid(-1, 0)
        if pid > 0:
            if os.WIFEXITED(status):
                return (pid, os.WEXITSTATUS(status))
            elif os.WIFSIGNALED(status):
                return (pid, 1)

def error(str):
    print >> sys.stderr, os.path.basename(sys.argv[0]) + ':', str
    sys.exit(1)

if __name__ == '__main__':
    main()
