- #!/usr/bin/env python
- # tsshbatch.py - Non-Interactive ssh Connection
- # Copyright (c) 2011-2014 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/
-
- #####
- # Program Housekeeping
- #####
-
- PROGNAME = "tsshbatch.py"
- BASENAME = PROGNAME.split(".py")[0]
- PROGENV = BASENAME.upper()
- CMDINCL = PROGENV + "CMDS"
- HOSTINCL = PROGENV + "HOSTS"
-
- CVSID = "$Id: x.py,v 1.2 2014/07/25 21:28:45 tundra Exp $"
- VERSION = CVSID.split()[2]
- CPRT = "(c)"
- DATE = "2011-2014"
- OWNER = "TundraWare Inc."
- RIGHTS = "All Rights Reserved."
- COPYRIGHT = "Copyright %s %s, %s %s" % (CPRT, DATE, 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 getopt
- import getpass
- import os
- import paramiko
- import shlex
- import socket
- import sys
-
-
- #####
- # Constants And Literals
- #####
-
-
- ABORTING = 'Aborting ...'
- COMMENT = '#'
- COMMANDS = 'Commands'
- CONSUCCESS = 'SUCCESS: Connection Established'
- GETFILES = 'Files To GET'
- HOSTSEP = ':'
- HOSTNOISE = '[%s]'
- HOSTLIST = 'Hosts'
- INDENTWIDTH = 8
- OPTIONSLIST = 'EKG:H:NP:ST:aef:hkn:p:tvxy'
- PADWIDTH = 12
- PATHDELIM = ':'
- PATHSEP = os.sep
- PUTFILES = 'Files To PUT'
- SEPARATOR = ' ---> '
- STDIN = '-'
- SUDO = 'sudo'
- SUDOPROMPT = 'READINGSUDOPW'
- SUDOARGS = '-S -k -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 [-EKNSTaehkvxy -G 'file dest' -P 'file dest' -f cmdfile -n name -p pw ] -H 'host ..' | hostlistfile [command arg ... ]\n" +\
- " where,\n" +\
- "\n" +\
- " -E Write error output to stdout instead of 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\n" +\
- " -a Don't abort program after failed file transfers." +\
- " -e Don't report remote host stderr output\n" +\
- " -f cmdfile Read commands from file\n" +\
- " -h Display help\n" +\
- " -k Use key exchange-based authentication\n" +\
- " -n name Specify login name\n" +\
- " -p pw Specify login password\n" +\
- " -t Run in test mode, don't actually execute commands\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'
-
-
- #####
- # Hostname substitution support
- #####
-
-
- HOSTNAME = '<HOSTNAME>'
- HOSTSHORT = '<HOSTSHORT>'
- HostName = ""
- HostShort = ""
-
- SymbolTable = {}
-
-
- #####
- # Error Messages
- #####
-
- eBADARG = "Invalid command line: %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
- #####
-
- iTXFILE = "Writing %s To %s ..."
-
-
- #####
- # Prompts
- #####
-
- pPASS = "Password: "
- pSUDO = "%s Password: " % SUDO
- pUSER = "Username (%s): "
-
-
- #####
- # Options That Can Be Overriden By User
- ####
-
- ABORTONFXERROR = True # Abort after a file transfer error
- GETSUDOPW = False # Prompt for sudo password
- Hosts = [] # List of hosts to target
- KEYEXCHANGE = False # Do key exchange-based auth?
- NOISY = False # Print output with extra detail
- 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
- TESTMODE = True # Run program in test mode, don't actually execute commands
- TIMEOUT = 15 # Connection attempt timeout (sec)
- UNAME = "" # Login name
-
-
- #####
- # Global Data Structures
- #####
-
- Commands = []
- FileIncludeStack = []
- Get_Transfer_List = {}
- Put_Transfer_List = {}
-
-
- #####
- # Functions
- #####
-
- # Gets rid of comments and strips leading/trailing whitespace
-
- def ConditionLine(line):
- return line.split(COMMENT)[0].strip()
-
- # End of 'ConditionLine()'
-
-
- #####
- # Print Message(s) To stderr
- #####
-
- def PrintStderr(msg, TERMINATOR="\n"):
-
- # If we've been told to redirect to stdout, do so instead
-
- if REDIRSTDERR:
- PrintStdout(msg, TERMINATOR)
-
- else:
-
- sys.stderr.write(msg + TERMINATOR)
- sys.stderr.flush()
-
- # End of 'PrintStderr()'
-
-
- #####
- # Print Message(s) To stdout
- #####
-
- def PrintStdout(msg, TERMINATOR="\n"):
- sys.stdout.write(msg + TERMINATOR)
- sys.stdout.flush()
-
- # End of 'PrintStdout()'
-
-
- #####
- # Display An Error Message And Exit
- #####
-
- def ErrorExit(msg):
-
- if msg:
- PrintStderr(msg)
-
- os._exit(1)
-
- # End Of 'ErrorExit()'
-
-
- #####
- # Transfer Files To A Host
- #####
-
- def HostFileTransfer(host, user, pw, filelist, GET=False):
-
- try:
- ssh = paramiko.SSHClient()
-
- # Connect and run the command, reporting results as we go
-
- ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
-
- if KEYEXCHANGE:
- ssh.connect(host, timeout=TIMEOUT)
- else:
- ssh.connect(host, username=user, password=pw, timeout=TIMEOUT)
-
- sftp = ssh.open_sftp()
-
- for src in filelist:
-
- # Resolve references to hostname in source file specification
-
- srcfile = src.replace(HOSTNAME, HostName)
- srcfile = src.replace(HOSTSHORT, HostShort)
-
- for destdir in filelist[src]:
-
- # Resolve references to hostname in destination directory specification
-
- destdir = destdir.replace(HOSTNAME, HostName)
- destdir = destdir.replace(HOSTSHORT, HostShort)
-
- # Make sure we have a trailing path separator
- destination = destdir
- if destination[-1] != PATHSEP:
- destination += PATHSEP
-
- if GET:
- destination += host + HOSTSEP + os.path.basename(srcfile)
- PrintStdout(iTXFILE % (host + ":" + srcfile, destination))
- sftp.get(srcfile, destination)
-
- else:
- destination += os.path.basename(srcfile)
- PrintStdout(iTXFILE % (srcfile, host + ":" + destination))
- sftp.put(srcfile, destination)
-
- sftp.close()
- ssh.close()
-
- except:
-
- PrintReport([host, eFXERROR % str(sys.exc_info()[1])], HANDLER=PrintStderr)
-
- try:
- sftp.close()
- ssh.close()
-
- except:
- pass
-
- # Do we continue or not?
- if ABORTONFXERROR:
- ErrorExit("")
-
- # End of 'HostFileTransfer()'
-
-
- def HostCommands(host, user, pw, sudopw, commands):
-
- ssh = paramiko.SSHClient()
-
- # Connect and run the command, reporting results as we go
-
- try:
- ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
-
- if KEYEXCHANGE:
- ssh.connect(host, timeout=TIMEOUT)
-
- else:
- ssh.connect(host, username=user, password=pw, timeout=TIMEOUT)
-
- # Set up a pty
- chan = ssh.invoke_shell()
-
- PrintReport([host, CONSUCCESS])
-
- # Run all requested commands
-
- for command in commands:
-
- # Resolve references to hostname within the command string
-
- command = command.replace(HOSTNAME, HostName)
- command = command.replace(HOSTSHORT, HostShort)
-
- # It's possible to get blank lines from stdin.
- # Ignore them.
-
- if not command:
- continue
-
- # If this is a sudo run, we have to use ptys to work around requiretty settings
-
- if command.startswith(SUDO + " "):
-
- command = command.replace(SUDO, "%s %s" % (SUDO, SUDOARGS), 1)
-
- # Invoke The Command
-
- chan.send(command + "&& exit\n")
-
- # Provide password
-
- rxbuffer = ''
- while not rxbuffer.endswith(SUDOPROMPT):
- rxbuffer += chan.recv(9999)
-
- chan.send("%s\n" % sudopw)
-
- results = ''
- READMORE = True
- while READMORE:
-
- newresults = chan.recv(9999)
- if newresults:
- results += newresults
-
- else:
- READMORE = False
-
- # Convert the result which is one big string into a list of strings on newline boundaries
-
- results = results.replace("\r", "")
- listresults = results.split("logout")[0].split("\n")
- listresults = [e + "\n" for e in listresults]
-
- PrintReport([host + " (stdout)" + " [%s]" % command, "\n"] + listresults + ["\n"])
-
-
-
-
- # 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)
- ssh.close()
- raise SystemExit
-
- else:
-
- stdin, stdout, stderr = ssh.exec_command(command)
-
- PrintReport([host + " (stdout)" + " [%s]" % command, "\n"] + stdout.readlines() + ["\n"])
-
- if REPORTERR:
- PrintReport([host + " (stderr)" + " [%s]" % command, "\n"] + stderr.readlines() + ["\n"], HANDLER=PrintStderr)
-
- # 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)
-
- ssh.close()
-
- # 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):
-
- 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 NOISY:
- hostnoise = HOSTNOISE % hostname
-
- for r in results[2:]: # Command Results
- HANDLER(hostnoise + INDENTWIDTH * " " + r.strip())
-
- # 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
-
- try:
-
- f = open(filename)
- for line in f.readlines():
-
- # Cleanup comments and whitespace
-
- line = ConditionLine(line)
-
- # Process variable definitions
-
- if 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()
-
- 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(VarSub(line))
- f.close()
-
- FileIncludeStack.pop() # Remove this invocation from the stack
- return listcontainer
-
- except:
- ErrorExit(eBADFILE % filename)
-
- # End of 'ReadFile()'
-
-
- #####
- # 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 ---------------------- #
-
- #####
- # 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 == "-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 == "-e":
- REPORTERR = False
-
- if opt == "-f":
- Commands = ReadFile(val, os.getenv(CMDINCL), Commands)
-
- if opt == "-h":
- PrintStdout(USAGE)
- sys.exit()
-
- if opt == "-k":
- KEYEXCHANGE = True
-
- if opt == "-n":
- UNAME = val
-
- if opt == "-p":
- PWORD = val
-
- if opt == "-t":
- TESTMODE = True
-
- if opt == "-v":
- PrintStdout(CVSID)
- sys.exit()
-
- if opt == "-x":
- TESTMODE = False
-
- if opt == "-y":
- NOISY = True
-
-
- #####
- # Host & Command Line Command Definition Processing
- #####
-
- # Get the list of hosts if not specified on command line.
- # The assumption is that the first argument is the file
- # containing the list of targeted hosts and the remaining
- # arguments form the command.
-
- if not Hosts:
-
- # Even if we are only doing file transfers and no command
- # is specified, we have to have at least one argument here
- # to tell us what hosts we're working on.
-
- if not args:
- ErrorExit(eNOHOSTS)
-
- Hosts = ReadFile(args[0], os.getenv(HOSTINCL), Hosts)
- command = " ".join(args[1:])
-
- # If hosts were passed on the command line, all the arguments
- # are understood to form the command.
-
- else:
-
- # First, do variable substitution on passed hosts
-
- for index in range(len(Hosts)):
- Hosts[index] = VarSub(Hosts[index])
-
- # Now save the command
- 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(VarSub(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)
-
- if not KEYEXCHANGE:
-
- # 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
-
- # Preset commandline and/or program option variable password takes precedence
-
- if 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 command.startswith(SUDO + " "):
- 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
-
- #####
- # 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]:
- unrolled.append(source + (PADWIDTH*3 - len(source)) * " "+ SEPARATOR + dest)
-
- 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 :
-
- for host in Hosts:
-
- HostName = host
- HostShort = host.split('.')[0]
-
- 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)