#!/usr/bin/env python # tsshbatch.py - Non-Interactive ssh Connection # Copyright (c) 2011 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() RCSID = "$Id: tsshbatch.py,v 1.134 2012/01/05 18:37:43 tundra Exp $" VERSION = RCSID.split()[2] CPRT = "(c)" DATE = "2011" 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 ##### FAILURE = "FAILURE" INDENTWIDTH = 8 OPTIONSLIST = "H:ehkn:p:v" PADWIDTH = 30 SEPARATOR = " ---> " SUCCESS = "SUCCESS" SUDO = 'sudo' SUDOPROMPT = 'READINGSUDOPW' SUDOARGS = '-S -p %s' % SUDOPROMPT TRAILER = ": " USAGE = \ PROGVER + "\n" +\ HOMEPAGE + "\n\n" +\ "Usage: tsshbatch.py [-ehkv] [-n name] [-p pw] [-H 'host host ..' | serverlistfile] command arg arg arg \n" +\ " where,\n" +\ "\n" +\ " -H '...' List of targeted hosts passed as a single argument\n" +\ " -e Don't report remote host stderr output\n" +\ " -h Display help\n" +\ " -k Use key exchange-based authentication\n" +\ " -n name Specify login name\n" +\ " -p pw Specify login password\n" +\ " -v Display extended program version information\n" ##### # Error Messages ##### eBADARG = "Invalid command line: %s!" eBADFILE = "Cannot open '%s'!" eBADSUDO = "sudo Failed, Check Password Or Command! sudo Error Report: %s" eFEWARGS = "Too few command line arguments!" eNOCONNECT = "Cannot Connect! (Name/Address Bad? Destination Unreachable?)" eNOLOGIN = "Cannot Login! (Login/Password Bad?)" ##### # Prompts ##### pPASS = "Password: " pSUDO = "%s Password: " % SUDO pUSER = "Username: " ##### # Print Message(s) To stderr ##### def PrintStderr(msg, TERMINATOR="\n"): sys.stderr.write(msg + TERMINATOR) # End of 'PrintStderr()' ##### # Print Message(s) To stdout ##### def PrintStdout(msg, TERMINATOR="\n"): sys.stdout.write(msg + TERMINATOR) # End of 'PrintStdout()' ##### # Display An Error Message And Exit ##### def ErrorExit(msg): PrintStderr(msg) sys.exit(1) # End Of 'ErrorExit()' ##### # Process A Command On A Host ##### def HostCommand(host, user, pw, command): 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) else: ssh.connect(host, username=user, password=pw) # If this is a sudo run, force password to be read # from stdin thereby avoiding fiddling around with ptys. if command.startswith(SUDO): command = command.replace(SUDO, "%s %s" % (SUDO, SUDOARGS), 1) # Run the command stdin, stdout, stderr = ssh.exec_command(command) # If doing a sudo command, send the password if command.startswith(SUDO): stdin.write("%s\n" % pw) 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 = stderr.readline().split(SUDOPROMPT) if len(sudonoise) > 1: # sudo had problems sudonoise = sudonoise[1].strip() else: # sudo OK sudonoise = "" if sudonoise: PrintReport([host, FAILURE, eBADSUDO % sudonoise], HANDLER=PrintStderr) ssh.close() return PrintReport([host + " (stdout)", SUCCESS] + stdout.readlines()) if REPORTERR: PrintReport([host + " (stderr)", ""] + stderr.readlines(), HANDLER=PrintStderr) # Catch authentication problems explicitly except paramiko.AuthenticationException: PrintReport([host, FAILURE, eNOLOGIN], HANDLER=PrintStderr) # Everything else is some kind of connection problem except: PrintReport([host, FAILURE, eNOCONNECT], HANDLER=PrintStderr) ssh.close() # End of 'HostCommand()' ##### # 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): HANDLER(SEPARATOR + results[0] + TRAILER + (PADWIDTH - len(results[0])) * " " + results[1]) for r in results[2:]: # Command Results HANDLER(INDENTWIDTH * " " + r.strip()) # End of 'PrintReport()' # ---------------------- Program Entry Point ---------------------- # ##### # Process Any Options User Set In The Environment Or On Command Line ##### # Options that can be overriden by user HOSTS = "" # List of hosts to target KEYEXCHANGE = False # Do key exchange-based auth? PWORD = "" # Password REPORTERR = True # Report stderr output from remote host UNAME = "" # Login name # 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) # Unless the -H option is selected, this program # requires a minimum of 2 command line arguments # to run (hostlist file and command). If the user # specifies -H, then all we need is the command. MINARGS = 2 for opt, val in opts: if opt == "-H": HOSTS = val.split() MINARGS = 1 if opt == "-e": REPORTERR = False if opt == "-h": PrintStdout(USAGE) sys.exit() if opt == "-k": KEYEXCHANGE = True if opt == "-n": UNAME = val if opt == "-p": PWORD = val if opt == "-v": PrintStdout(RCSID) sys.exit() # Make sure we have enough args to do the job if len(args) < MINARGS: ErrorExit(eFEWARGS) ##### # Go Do The Requested Work ##### # If we're not doing key exchange-based authentication, get # user name & password. # Only do this if they've not been set in the environment # variable/command line if not KEYEXCHANGE: if not UNAME: UNAME = raw_input(pUSER) if not PWORD: PWORD = getpass.getpass(pPASS) # Host list and command parsing # 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: try: f = open(args[0]) HOSTS = f.readlines() f.close() except: ErrorExit(eBADFILE % sys.argv[1]) CMD = " ".join(args[1:]) # If hosts were passed on the command line, all the arguments # are understood to form the command. else: CMD = " ".join(args[0:]) # If user want 'sudo' execution, they MUST provide a password because # key exchange-based authentication is not part of sudo. If the # password has not been set by some other means (command line or # environment variable), ask for it here. CMD = CMD.strip() if CMD.startswith(SUDO) and not PWORD: PWORD = getpass.getpass(pSUDO) # Iterate over the list of hosts, executing the command for host in HOSTS: host = host.strip() if host: HostCommand(host, UNAME, PWORD, CMD)