Newer
Older
tsshbatch / tsshbatch.py
#!/usr/bin/env python
# tsshbatch.py - Non-Interactive ssh Connection
# Copyright (c) 2011-2016 TundraWare Inc.
# Permission Hereby Granted For Unrestricted Personal Or Commercial Use
# See "tsshbatch-license.txt" For Licensing Details
#
# For Updates See:  http://www.tundraware.com/Software/tsshbatch

# A tip of the hat for some of the ideas in the program goes to:
#
#     http://jessenoller.com/2009/02/05/ssh-programming-with-paramiko-completely-different/

#####
# Version Information - Overwritten by makefile during release process
#####

GITID      = '54eff6b tundra Wed Sep 28 14:21:46 2016 -0500'
VERSION    = '1.310'


#####
# Program Housekeeping
#####

PROGNAME     = "tsshbatch.py"
BASENAME     = PROGNAME.split(".py")[0]
PROGENV      = BASENAME.upper()
CMDINCL      = PROGENV + "CMDS"
HOSTINCL     = PROGENV + "HOSTS"

CPRT         = "(c)"
PROGDATE     = "2011-2016"
OWNER        = "TundraWare Inc."
RIGHTS       = "All Rights Reserved."
COPYRIGHT    = "Copyright %s %s, %s  %s" % (CPRT, PROGDATE, OWNER, RIGHTS)

PROGVER      = PROGNAME + " " + VERSION + (" - %s" % COPYRIGHT)
HOMEPAGE     = "http://www.tundraware.com/Software/%s\n" % BASENAME


#####
# Suppress Deprecation Warnings
# Required in some older environments where paramiko version
# is behind the python libs version.
#####

import warnings
warnings.filterwarnings("ignore", "", DeprecationWarning)


#####
# Imports
#####

import collections
import commands
import getopt
import getpass
import os
import paramiko
import shlex
import socket
import sys
import time


#####
# Constants And Literals
#####


ABORTING    = 'Aborting ...'
BANNERTIME  = 'Elapsed Time: %s Seconds'
BANNERMSG   = '%s %s %s On %s At %s' % (PROGNAME, VERSION, '%s', '%s', '%s')
BANNEREND   = 'Ended'
BANNERSTART = 'Started'
COMMENT     = '#'
COMMANDS    = 'Commands'
CFGKEYID    = 'identityfile'
CFGREALHOST = 'hostname'
CONSUCCESS  = 'SUCCESS: Connection Established'
EXECUTE     = '!'
FILEGET     = '.getfile'
FILEPUT     = '.putfile'
GETFILES    = 'Files To GET'
HOSTSEP     = '-'
HOSTNOISE   = '[%s]'
HOSTLIST    = 'Hosts'
INDENTWIDTH = 8
OPTIONSLIST = 'BC:EKG:H:NP:ST:abef:hi:kl:n:p:qstvxy'
PADWIDTH    = 12
PATHDELIM   = ':'
PATHSEP     = os.sep
PUTFILES    = 'Files To PUT'
SEPARATOR   = ' --->  '
STDIN       = '-'
SUDO        = 'sudo'
SUDOPROMPT  = 'READINGSUDOPW'
SUDOARGS    = '-S -p %s' % SUDOPROMPT
SUDOPWHINT  = '(Default: login password): '
SYMTABLE    = 'Symbol Table'
TESTRUN     = 'Test Run For'
TRAILER     = ': '
USERVAR     = 'USER'

USAGE       = \
    PROGVER  + "\n"                                                                                     +\
    HOMEPAGE + "\n\n"                                                                                   +\
    "Usage:  tsshbatch.py [-BEKNSTaehkqstvy -C configfile -G 'file dest' -P 'file dest' -f cmdfile -l logfile -n name -p pw ] -H 'host ..' -i 'hostfile ...' [command arg ... ]\n" +\
    "          where,\n"                                                                                +\
    "\n"                                                                                                +\
    "            -B                  Print start and stop statistics (Off)\n"                               +\
    "            -C configfile       Specify location of ssh configuration file (~/.ssh/config)\n"          +\
    "            -E                  Write error output to stdout instead of stderr (Output to stderr)\n"   +\
    "            -K                  Force password prompting - Overrides previous -k\n"                    +\
    "            -G 'file dest'      GET file on host and write local dest directory\n"                     +\
    "            -H '...'            List of targeted hosts passed as a single argument\n"                  +\
    "            -N                  Force prompting for username\n"                                        +\
    "            -P 'file dest'      PUT local file to host dest directory\n"                               +\
    "            -S                  Force prompting for sudo password\n"                                   +\
    "            -T seconds          Timeout for ssh connection attempts (15 sec)\n"                        +\
    "            -a                  Don't abort program after failed file transfers (Abort on failure)\n"  +\
    "            -b                  Don't abort program after failed sudo command (Abort on failure)\n"    +\
    "            -e                  Don't report remote host stderr output (Report host stderr) \n"        +\
    "            -f cmdfile          Read commands from file\n"                                             +\
    "            -h                  Display help\n"                                                        +\
    "            -i 'file file...'   Retrieve list of hosts from hostfile.  Can be repeated.\n"             +\
    "            -k                  Use key exchange-based authentication (Use password auth)\n"           +\
    "            -l logfile          Log errors to logfile (/dev/null)\n"                                   +\
    "            -n name             Specify login name\n"                                                  +\
    "            -p pw               Specify login password\n"                                              +\
    "            -q                  Quiet mode - produce less 'noisy' output\n"                            +\
    "            -s                  Silence all program noise - only return command output\n"              +\
    "            -t                  Run in test mode, don't actually execute commands (Default)\n"         +\
    "            -v                  Display extended program version information\n"                        +\
    "            -x                  Turn off test mode (if on) and execute requests\n"                     +\
    "            -y                  Turn on 'noisy' reporting for additional detail\n"


#####
# Directives & Related Support
#####

ASSIGN      = '='
DEFINE      = '.define'
INCLUDE     = '.include'


#####
# Builtin Definitions
#####

DATE        = '__DATE__'
DATETIME    = '__DATETIME__'
HOSTNAME    = '__HOSTNAME__'
HOSTNUM     = '__HOSTNUM__'
HOSTSHORT   = '__HOSTSHORT__'
LOGINNAME   = '__LOGINNAME__'
TIME        = '__TIME__'


# This is needed to differentiate between user-defined and builtin
# variables later in order to support the user being able to redefine
# them.

BuiltIns    = (DATE, DATETIME, HOSTNAME, HOSTNUM, HOSTSHORT, LOGINNAME, TIME)


#####
# Error Messages
#####

eBADARG       =  "Invalid command line: %s!"
eBADEXEC      =  "Execution variable failed: %s"
eBADFILE      =  "Cannot open '%s'!"
eBADSUDO      =  "sudo Failed (Check Password Or Command!) sudo Error Report:  %s"
eBADTXRQ      =  "Bad Transfer Request: %s  Must Have Exactly 1 Source And 1 Destination!"
eBADDEFINE    =  "Bad Symbol Definition: %s"
eBADTIMEOUT   =  "Timeout Value Must Be an Integer!"
eCMDFAILURE   =  "Failed To Run Command(s): %s"
eFXERROR      =  "File Transfer Error: %s"
eINCLUDECYCLE =  "Circular Include At: %s"
eNOCONNECT    =  "Cannot Connect: %s"
eNOHOSTS      =  "No Hosts Specified!"
eNOLOGIN      =  "Cannot Login! (Login/Password Bad?)"


#####
# Informational Messages
#####

iNOCFGFILE    = "Warning: Cannot Open Configuration File '%s'! Continuing Anyway ..."
iTXFILE       = "Writing %s To %s ..."


#####
# Prompts
#####

pPASS = "Password: "
pSUDO = "%s Password: " % SUDO
pUSER = "Username (%s): "


#####
# Options That Can Be Overriden By User
####

ABORTBADSUDO   = True            # Abort after a sudo promotion error
ABORTONFXERROR = True            # Abort after a file transfer error
BANNERSON      = False           # Print start/stop banner info
GETSUDOPW      = False           # Prompt for sudo password
Hosts          = []              # List of hosts to target
KEYEXCHANGE    = False           # Do key exchange-based auth?
LOGFILE        = "/dev/null"     # Where paramiko logging output goes
PROMPTUSERNAME = False           # Don't use $USER, prompt for username
PWORD          = ""              # Password
REDIRSTDERR    = False           # Redirect stderr to stdout
REPORTERR      = True            # Report stderr output from remote host
SSHCFGFILE     = "~/.ssh/config" # Default ssh configuration file location
TESTMODE       = True            # Run program in test mode, don't actually execute commands
TIMEOUT        = 15              # Connection attempt timeout (sec)
UNAME          = ""              # Login name


# Noise levels

NOISELEVEL     = 0            # Normal noisy
SILENT         = 1            # No program noise at all
QUIET          = 2            # Make output less noisy
NOISY          = 3            # Print output with extra detail


#####
# Global Data Structures & Variables
#####

Commands          = []
FileIncludeStack  = []
Get_Transfer_List = collections.OrderedDict()
Put_Transfer_List = collections.OrderedDict()
SSH_Configuration = {}
SymbolTable       = {}


#####
# Functions
#####

# Gets rid of comments and strips leading/trailing whitespace

def ConditionLine(line):
    return line.split(COMMENT)[0].strip()

# End of 'ConditionLine()'


#####
# Check To See If A Key Exists In A String, Excluding Quoted Substrings
#####

def KeyInString(key, string):

    """ Look for 'key' in 'string', but exclude segments of the string
        that are contained within single- or double quotes.
    """

    quote_chars = ('"', "'")

    InLiteral = False
    search = ""
    index = 0
    while index < len(string):

        char = string[index]

        if InLiteral:
            if char == quote_char:
                InLiteral = False

        elif char in quote_chars:
            quote_char = char
            InLiteral = True

        else:
            search += char

        index += 1

    status = False
    if search.count(key) > 0:
        status = True

    return status

# End of KeyInString


#####
# Print Message(s) To stderr
#####

def PrintStderr(msg, EOL="\n"):

    # If we've been told to redirect to stdout, do so instead

    if REDIRSTDERR:
        PrintStdout(msg, EOL)

    else:

        sys.stderr.write(msg + EOL)
        sys.stderr.flush()

# End of 'PrintStderr()'


#####
# Print Message(s) To stdout
#####

def PrintStdout(msg, EOL="\n"):
    sys.stdout.write(msg + EOL)
    sys.stdout.flush()

# End of 'PrintStdout()'


#####
# Display An Error Message And Exit
#####

def ErrorExit(msg):

    if msg:
        PrintStderr(msg)

    # If requested, print startup banner

    if BANNERSON:

        PrintStdout(BANNERMSG % (BANNERSTART, time.strftime("%Y-%m-%d"), time.strftime("%H:%M:%S")))
        PrintStdout(BANNERTIME % float(time.time() - StartTime))


    os._exit(1)

# End Of 'ErrorExit()'


#####
# Transfer Files To A Host
#####

def HostFileTransfer(host, user, pw, filelist, GET=False):

    # Get an sftp connection and move files
    try:
        ssh = SSH_Connect(host, user, pw, TIMEOUT)
        sftp = ssh.open_sftp()

        for src in filelist:

            # Process any .define substitions

            srcfile = VarSub(src)

            for destdir in filelist[src]:

                # Process any .define substitutions

                destdir = VarSub(destdir)

                # Make sure we have a trailing path separator

                destination = destdir
                if destination[-1] != PATHSEP:
                    destination += PATHSEP

                try:

                    if GET:
                        destination += host + HOSTSEP + os.path.basename(srcfile)
                        PrintStdout(iTXFILE %  (host + ":" + srcfile, destination))
                        sftp.get(srcfile, destination)
                        os.chmod(destination, sftp.stat(srcfile).st_mode)

                    else:
                        destination += os.path.basename(srcfile)
                        PrintStdout(iTXFILE %  (srcfile, host + ":" + destination))
                        sftp.put(srcfile, destination)
                        sftp.chmod(destination, os.stat(srcfile).st_mode)

                except:

                    PrintReport([host, eFXERROR % str(sys.exc_info()[1])], HANDLER=PrintStderr)

                    # Do we continue after failed file transfers or not?
                    if ABORTONFXERROR:
                        try:
                            sftp.close()
                            ssh.close()

                        except:
                            pass

                        ErrorExit("")

        sftp.close()
        ssh.close()

    except:

        PrintReport([host, eFXERROR % str(sys.exc_info()[1])], HANDLER=PrintStderr)

        # Do we continue after failed connection attempts or not?
        if ABORTONFXERROR:
            try:
                sftp.close()
                ssh.close()

            except:
                pass

            ErrorExit("")

# End of 'HostFileTransfer()'


def HostCommands(host, user, pw, sudopw, commands):

    # Figure out if we want report formatting

    Format = True
    if NOISELEVEL == SILENT:
        Format = False

    # Connect and run the command, reporting results as we go
    try:
        ssh = SSH_Connect(host, user, pw, TIMEOUT)
        if NOISELEVEL !=  QUIET:
            PrintReport([host, CONSUCCESS], FORMAT=Format)

        # Run all requested commands

        for command in commands:

            # Dereference variables

            command = VarSub(command)

            # It's possible to get blank lines from stdin.
            # Ignore them.

            if not command:
                continue

            # If this is a sudo run, force password to be read
            # from stdin thereby avoiding fiddling around with ptys.

            if KeyInString(SUDO + " ", command):
                command = command.replace(SUDO, "%s %s" % (SUDO, SUDOARGS), 1)

            stdin, stdout, stderr = ssh.exec_command(command)

            # If doing a sudo command, send the password

            if KeyInString(SUDO + " ", command):
                stdin.write("%s\n" % sudopw)
                stdin.flush()

                # If all we see on stderr at this point is our original
                # prompt, then then the sudo promotion worked.  A bad
                # password or bad command will generate additional noise
                # from sudo telling us to try again or that there was a
                # command error.

                sudonoise = " ".join(stderr.readline().split(SUDOPROMPT)).strip()

                if sudonoise:                 # sudo had problems

                    PrintReport([host + " [%s]" % command, eCMDFAILURE % (eBADSUDO % sudonoise)] + ["\n"], HANDLER=PrintStderr)

                    # Abort program on sudo failure.  This is default behavior
                    # but can be overriden on the command line.

                    if ABORTBADSUDO:
                        ssh.close()
                        raise SystemExit
                    else:
                        break

            cmdreport = " [%s]" % command
            if NOISELEVEL == QUIET:
                cmdreport = ""

            PrintReport([host + " (stdout)" + cmdreport, "\n"] + stdout.readlines() + ["\n"], FORMAT=Format)

            if REPORTERR:
                PrintReport([host + " (stderr)" + cmdreport, "\n"] + stderr.readlines() + ["\n"], HANDLER=PrintStderr, FORMAT=Format)

    # Handle aborts

    except SystemExit:
        ErrorExit(ABORTING)

    # Catch authentication problems explicitly

    except paramiko.AuthenticationException:
        PrintReport([host, eCMDFAILURE % eNOLOGIN], HANDLER=PrintStderr)

    # Everything else is some kind of connection problem

    except:
        PrintReport([host, eCMDFAILURE % (eNOCONNECT % str(sys.exc_info()[1]))], HANDLER=PrintStderr)

    # Close any remaining ssh connection
    try:
        ssh.close()
    except:
        pass

# End of 'HostCommands()'


#####
# Print Report
#####

# Expects input as [host, success/failure message, result1, result2, ...]
# Uses print handler to stdout by default but can be overriden at call
# time to invoke any arbitrary handler function.

def PrintReport(results, HANDLER=PrintStdout, FORMAT=True):


    if FORMAT:

        hostname = results[0]
        HANDLER(SEPARATOR + hostname +
                TRAILER +
                (PADWIDTH - len(results[0])) * " " +
                results[1])

        # Prepend the host name if we've asked for noisy reporting

        hostnoise =""
        if NOISELEVEL == NOISY:
            hostnoise = HOSTNOISE % hostname

        for r in results[2:]:                             # Command Results
            HANDLER(hostnoise + INDENTWIDTH * " " + r.strip())

    # In (silent) unformatted mode we just return results with no
    # headers or formatting.  We suppress the last line in the list.
    # It is an empty line introduced for formatting by the caller, but
    # isn't really part of the returned output.
    else:

        for r in results[2:-1]:
            HANDLER(r, EOL="")

# End of 'PrintReport()'


#####
# Process A File Transfer Request
#####

def ProcessTXRQ(request, storage):

    src_dest = request.split()
    if len(src_dest) != 2:
        ErrorExit(eBADTXRQ % src_dest)

    else:

        if src_dest[0] not in storage:
            storage[src_dest[0]] = [src_dest[1],]

        else:
            storage[src_dest[0]].append(src_dest[1])

# End of 'ProcessTXRQ'


#####
# Read File Handling Comments And Directives
#####

def ReadFile(fname, envvar, listcontainer, containingfile=""):

    # Check to see if we can find the file, searching the
    # the relevant include environment variable path, if any

    filename = SearchPath(fname, envvar)
    if not filename:
        ErrorExit(eBADFILE % fname)

    # Make sure we don't have a cyclic include reference

    if filename in FileIncludeStack:
        ErrorExit(eINCLUDECYCLE % containingfile + SEPARATOR + filename)

    else:
        FileIncludeStack.append(filename)  # Push it on to the stack history

    # Line parsing starts here
    try:

        f = open(filename)
        for line in f.readlines():

            # Cleanup comments and whitespace

            line = ConditionLine(line)

            # Process file transfer requests

            if line.startswith(FILEGET):
                val = line.split(FILEGET)[1].strip()
                ProcessTXRQ(val, Get_Transfer_List)

            elif line.startswith(FILEPUT):
                val = line.split(FILEPUT)[1].strip()
                ProcessTXRQ(val, Put_Transfer_List)

            # Process variable definitions

            elif line.startswith(DEFINE):

                line = line.split(DEFINE)[1]
                if line.count(ASSIGN) == 0:
                    ErrorExit(eBADDEFINE % line)

                else:

                    name = line.split(ASSIGN)[0].strip()
                    val  = "=".join(line.split(ASSIGN)[1:]).strip()

                    # Process references to execution variables

                    if val.startswith(EXECUTE):

                        try:

                            origval = val
                            status, val = commands.getstatusoutput(val[1:].strip())

                            # Blow out if the command failed

                            if status:
                                raise

                        except:

                            PrintReport([PROGNAME, eBADEXEC % origval], HANDLER=PrintStderr)
                            ErrorExit("")

                    if name:
                        SymbolTable[name] = val

                    else:
                        ErrorExit(eBADDEFINE % line)

            # Process file includes
            elif line:
                if line.startswith(INCLUDE):
                    fname = ConditionLine(line.split(INCLUDE)[1])
                    ReadFile(fname, envvar, listcontainer, containingfile=filename)

                # It's a normal line - do variable substitution and save
                else:
                    listcontainer.append(line)
        f.close()

        FileIncludeStack.pop()   # Remove this invocation from the stack
        return listcontainer

    except:
        ErrorExit(eBADFILE % filename)

# End of 'ReadFile()'


#####
# Setup An ssh Connection
#####

def SSH_Connect(host, user, pw, time):

    ssh = paramiko.SSHClient()
    ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())

    keyfiles = None
    if host in SSH_Configuration:
        entry  = SSH_Configuration[host]
        keyfiles, host = entry[0], entry[1]

    if KEYEXCHANGE:
        ssh.connect(host, username=user, key_filename=keyfiles, timeout=time)
    else:
        ssh.connect(host, username=user, password=pw, allow_agent=False, look_for_keys=False, timeout=time)

    return ssh

# End of 'SSH_Connect()'


#####
# Read Any Local SSH Configuration
#####

def SSH_GetConfig(filename):

   # NOTE: We only support the "IdentityFile" and "HostName"
   # configuration directives here.

    retval = {}
    sshconfig = paramiko.SSHConfig()
    try:
        f = open(os.path.expanduser(filename))

    # File open failed, but we go on anyway
    except:
        PrintStderr(iNOCFGFILE % filename)
        return retval

    sshconfig.parse(f)
    f.close()

    # Return local configuration as a dictonary with hostnames as keys

    for host in sshconfig.get_hostnames():
        if host != "*":
            cfgentry = sshconfig.lookup(host)
            output = []

            keyfiles = []
            if CFGKEYID in cfgentry:
                keyfiles = cfgentry[CFGKEYID]

            realhost = host
            if CFGREALHOST in cfgentry:
                realhost = cfgentry[CFGREALHOST]

            retval[host] = [keyfiles, realhost]

    return retval

# End of 'SSH_GetConfig()'


#####
# Search A Path For A File, Returning First Match
#####

def SearchPath(filename, pathlist, delimiter=PATHDELIM):

    # What we'll return if we find nothing
    retval = ""

    # Handle fully qualified filenames
    # But ignore this, if its a directory with a matching name

    if os.path.exists(filename) and os.path.isfile(filename):
        retval =  os.path.realpath(filename)

    # Find first instance along specified path if one has been specified
    elif pathlist:

        paths = pathlist.split(delimiter)
        for path in paths:

            if path and path[-1] != PATHSEP:
                path += PATHSEP

                path += filename

                if os.path.exists(path):
                    retval = os.path.realpath(path)
                    break
    return retval

# End of 'SearchPath()'


#####
# Do Variable Substitution In A String
#####

def VarSub(line):

    for symbol in SymbolTable:
        line = line.replace(symbol, SymbolTable[symbol])

    return line

# End of 'VarSub()'


# ---------------------- Program Entry Point ---------------------- #

#####
# Note starting time
#####

StartTime = time.time() # Need this here in case we error abort
                        # on command line processing.  Gets
                        # reset below for normal processing.

#####
# Process Any Options User Set In The Environment Or On Command Line
#####

# Handle any options set in the environment

OPTIONS = sys.argv[1:]
envopt = os.getenv(PROGENV)
if envopt:
    OPTIONS = shlex.split(envopt) + OPTIONS

# Combine them with those given on the command line
# This allows the command line to override defaults
# set in the environment

try:
    opts, args = getopt.getopt(OPTIONS, OPTIONSLIST)

except getopt.GetoptError, (errmsg, badarg):
    ErrorExit(eBADARG % errmsg)

for opt, val in opts:

    if opt == "-B":
        BANNERSON = True

    if opt == "-C":
        SSHCFGFILE = val

    if opt == "-E":
        REDIRSTDERR = True

    if opt == "-K":
        KEYEXCHANGE = False

    if opt == "-G":
        ProcessTXRQ(val, Get_Transfer_List)

    if opt == "-H":
        Hosts += val.split()

    if opt == "-N":
        PROMPTUSERNAME = True
        KEYEXCHANGE = False

    if opt == "-P":
        ProcessTXRQ(val, Put_Transfer_List)

    if opt == "-S":
        GETSUDOPW = True

    if opt == "-T":
        try:
            TIMEOUT = int(val)
        except:
            ErrorExit(eBADTIMEOUT)

    if opt == "-a":
        ABORTONFXERROR = False

    if opt == "-b":
        ABORTBADSUDO = False

    if opt == "-e":
        REPORTERR = False

    if opt == "-f":
        Commands = ReadFile(val, os.getenv(CMDINCL), Commands)

    if opt == "-h":
        PrintStdout(USAGE)
        sys.exit()

    if opt == "-i":
        for hostfile in val.split():
            Hosts += ReadFile(hostfile, os.getenv(HOSTINCL), Hosts)

    if opt == "-k":
        KEYEXCHANGE = True

    if opt == "-l":
        LOGFILE = val

    if opt == "-n":
        UNAME = val

    if opt == "-p":
        PWORD = val

    if opt == "-q":
        NOISELEVEL = QUIET

    if opt == "-s":
        NOISELEVEL = SILENT

    if opt == "-t":
        TESTMODE = True

    if opt == "-v":
        PrintStdout(GITID)
        sys.exit()

    if opt == "-x":
        TESTMODE = False

    if opt == "-y":
        NOISELEVEL = NOISY


#####
# Intitialize paramiko Logging
#####

paramiko.util.log_to_file(LOGFILE)


#####
# Command Line Command Definition Processing
#####

# Must have a list of hosts or we cannot go on with any
# operation whether file xfer or command execution

if not Hosts:
    ErrorExit(eNOHOSTS)

else:
    command = " ".join(args[0:])

# Put it in a list data structure because this is what the
# HostCommands() function expects.  This is necessary to handle multi
# command input from from a file.

command = ConditionLine(command)

if command:

    # Do variable substitution here like any other command
    Commands.append(command)

#####
# Authentication Credential Processing
#####

# Precedence of authentication credential sources:
#
#     1) Key exchange
#     2) Forced prompting for name via -N
#     3) Command Line/$TSSHBATCH env variable sets name
#     4) Name picked up from $USER  (Default behavior)


# Regardless of authorization type, we need a user name.
# Preset commandline and/or program option variable username takes
# precedence

if not UNAME:
    UNAME = os.getenv(USERVAR)

# By default, use the above as the login name and don't prompt for it
# unless overriden on the command line with -N

if PROMPTUSERNAME:

    current_user = UNAME
    UNAME = raw_input(pUSER %current_user)
    if not UNAME:                 # User just hit return - wants default
        UNAME = current_user

# For password auth, preset commandline and/or program option
# variable password takes precedence

if not KEYEXCHANGE and not PWORD:
    PWORD  = getpass.getpass(pPASS)

#####
# If Needed, Get sudo Password
####

# The need to prompt for a sudo password depends on a number of
# conditions:
#
# If a login password is present either via manual entry or -p, sudo
# will use that without further prompting.  (Default)
#
# The user is prompted for a sudo password under two conditions:
#
#  1) -k option was selected but no password was set with -p
#  2) -S option was selected
#
# If the user IS prompted for a sudo password, any login password
# previously entered - either via -p or interactive entry - will be
# used as the default.  The user can hit enter to accept this or enter
# a different password.  This allows login and sudo passwords to be
# the same or different.

# Find out if we have any sudo commands

SUDOPRESENT = False
for command in Commands:
    if KeyInString(SUDO + " ", command):
            SUDOPRESENT = True

# Check condition 1) above.
# (Condition 2 handled during options processing).

if KEYEXCHANGE and not PWORD:
    GETSUDOPW = True

SUDOPW = PWORD
if SUDOPRESENT and GETSUDOPW:

    sudopwmsg   = pSUDO
    if PWORD:
        sudopwmsg = sudopwmsg[:-2] + " " + SUDOPWHINT

    SUDOPW = getpass.getpass(sudopwmsg)
    if PWORD and not SUDOPW:
        SUDOPW = PWORD


# Report time statistics if requested

if BANNERSON:
    StartTime= time.time()   # The the real starting time - the time the work began
    PrintStdout(BANNERMSG % (BANNERSTART, time.strftime("%Y-%m-%d"), time.strftime("%H:%M:%S")))

#####
# Do The Requested Work
#####

# If we're running testmode, just report the final list of
# hosts and commands that would be run

if TESTMODE:

    symtbl = []
    gets   = []
    puts   = []

    # Unroll and format dictionary structures

    symbols = SymbolTable.keys()
    symbols.sort()
    for symbol in symbols:
        symtbl.append(symbol + (PADWIDTH - len(symbol)) * " "+ SEPARATOR + SymbolTable[symbol])

    for xfers, unrolled in ((Get_Transfer_List, gets), (Put_Transfer_List, puts)):

        for source in xfers:
            for dest in xfers[source]:

                # Dereference any variables in the file transfer specification

                source = VarSub(source)
                dest   = VarSub(dest)

                unrolled.append(source + (PADWIDTH*3 - len(source)) * " "+ SEPARATOR + dest)

    # Dereference any variables in the list of commands

    index = 0
    while (index < len(Commands)):

        Commands[index] = VarSub(Commands[index])
        index += 1

    # Dereference any variables in the list of hosts

    index = 0
    while (index < len(Hosts)):

        Hosts[index] = VarSub(Hosts[index])
        index += 1

    for prompt, description, items in ((TESTRUN,  " ".join(OPTIONS), ["\n"]),
                                       (SYMTABLE, "",                 symtbl + ["\n"]),
                                       (HOSTLIST, "",                  Hosts + ["\n"]),
                                       (GETFILES, "",                   gets + ["\n"]),
                                       (PUTFILES, "",                   puts + ["\n"]),
                                       (COMMANDS, "",               Commands + ["\n"])
                                      ):

        PrintReport([prompt, description] + items)


# Otherwise, actually do the work by iterating over the list of hosts,
# executing any file transfers and commands.  Accomodate commenting
# out hosts in a list.

else :

    # Pick up any local configuration data

    SSH_Configuration = SSH_GetConfig(SSHCFGFILE)

    # Check to see if user is trying to override any builtins

    protected = []
    for builtin in BuiltIns:
        if builtin in SymbolTable:
            protected.append(builtin)

    # Now iterate over requested hosts
    hostnum = 0
    for host in Hosts:

        # Update the host counter

        hostnum += 1

        # Add internally generated symbols to the symbol table.
        # That way, both user-defined and builtin symbols will
        # subsequently be substituted.

        # Find out the effective name of the user doing this

        uname = UNAME
        if not uname:
            uname = os.getenv(USERVAR)

        internals = [
                     (DATE, time.strftime("%Y%m%d")),
                     (DATETIME, time.strftime("%Y%m%d%H%M%S")),
                     (HOSTNAME, host),
                     (HOSTNUM, str(hostnum)),
                     (HOSTSHORT, host.split('.')[0]),
                     (LOGINNAME, uname),
                     (TIME, time.strftime("%H%M%S")),
                    ]

        # Install builtins in the symbol table but only if the
        # user isn't overriding them.

        for symbol, value in internals:
            if symbol not in protected:
                SymbolTable[symbol] = value

        # Apply any relevant variable dereferencing

        host = VarSub(host)

        if Get_Transfer_List:
            HostFileTransfer(host, UNAME, PWORD, Get_Transfer_List, GET=True)

        if Put_Transfer_List:
            HostFileTransfer(host, UNAME, PWORD, Put_Transfer_List, GET=False)

        if Commands:
            HostCommands(host, UNAME, PWORD, SUDOPW, Commands)

#####
# If requested, print startup banner
#####

if BANNERSON:

    PrintStdout(BANNERMSG % (BANNEREND, time.strftime("%Y-%m-%d"), time.strftime("%H:%M:%S")))
    PrintStdout(BANNERTIME % float(time.time() - StartTime))