Newer
Older
tsshbatch / tsshbatch.py
#!/usr/bin/env python
# tsshbatch.py - Non-Interactive ssh Connection
# Copyright (c) 2011-2013 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()
CVSID    = "$Id: tsshbatch.py,v 1.140 2013/10/20 23:00:29 tundra Exp $"
VERSION  = CVSID.split()[2]

CPRT         = "(c)"
DATE         = "2011-2013"
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
#####


COMMENT     = "#"
FAILURE     = "FAILURE: %s"
INDENTWIDTH = 8
OPTIONSLIST = "H:Nehkn:p:v"
PADWIDTH    = 30
SEPERATOR   = " --->  "
SUCCESS     = "SUCCESS"
SUDO        = 'sudo'
SUDOPROMPT  = 'READINGSUDOPW'
SUDOARGS    = '-S -p %s' % SUDOPROMPT
TRAILER     = ": "
USAGE       = \
    PROGVER  + "\n"                                                                      +\
    HOMEPAGE + "\n\n"                                                                    +\
    "Usage:  tsshbatch.py [-Nehkv] [-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"   +\
    "                 -N         Force prompting for username\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"
eFXERROR      =  "File Transfer Error While Attempting %s: %s"
eNOCONNECT    =  "Cannot Connect: %s"
eNOLOGIN      =  "Cannot Login! (Login/Password Bad?)"


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

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


#####
# 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()'


#####
# Tranfer Files To A Host
#####

def HostFileTransfer(host, user, pw, filelist):

    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)

        sftp = ssh.open_sftp()
        for srcfile in  filelist:

            for destfile in filelist[srcfile]:
                sftp.put(srcfile, destfile)

        sftp.close()

    except:

        attempted = "%s -> %s:%s" % (srcfile, host, destfile)
        ErrorExit(eFXERROR % (attempted, sys.exc_info()[1][1]))

    ssh.close()

# End of 'HostFileTransfer()'

    
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 % sys.exc_info()[1].message)], 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(SEPERATOR + 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?
PROMPTUSERNAME = False     # Don't use $USER, prompt for username
PWORD          = ""        # Password
REPORTERR      = True      # Report stderr output from remote host
UNAME          = ""        # Login name

# Place To Keep File Transfer Requests

File_Transfer_List ={}

# 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 == "-H":
        HOSTS = val.split()
        
    if opt == "-N":
        PROMPTUSERNAME = True
        
    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(CVSID)
        sys.exit()

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

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

if not KEYEXCHANGE:

    # Preset commandline and/or program option variable username takes precedence
    if not UNAME:

        current_user = os.getenv("USER")
        
        # By default, use $USER as the login name and don't prompt for it
        if not PROMPTUSERNAME:
            UNAME = current_user

        # Interactive prompting using $USER as default
        else:

            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)


# 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 (interactive, 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.
# Accomodate commenting out hosts in a list.

for host in HOSTS:

    host = host.split(COMMENT)[0].strip()
    if host:

        if File_Transfer_List:
            HostFileTransfer(host, UNAME, PWORD, File_Transfer_List)

        if CMD:
            HostCommand(host, UNAME, PWORD, CMD)