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.142 2013/10/21 03:21:58 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 = "G:H:NP:ehkn:p:v"
PADWIDTH    = 30
PATHSEP     = os.sep
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] -G 'file dest' -P 'file dest' [-H 'host ..' | serverlistfile] command arg ... \n" +\
    "          where,\n"                                                                 +\
    "\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"                +\
    "            -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"
eBADTXRQ      =  "Bad Transfer Request: %s  Must Have Exactly 1 Source And 1 Destination!"
eFXERROR      =  "File Transfer Error: %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): "


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


#####
# 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)
        else:
            ssh.connect(host, username=user, password=pw)

        sftp = ssh.open_sftp()

        for srcfile in  filelist:
            for destdir in filelist[srcfile]:

                # Make sure we have a trailing path separator
                destination = destdir
                if destination[-1] != PATHSEP:
                    destination += PATHSEP

                if GET:
                    destination += host + "-" + 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:

        sftp.close()
        ssh.close()
        ErrorExit(eFXERROR % (sys.exc_info()[1]))

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


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


# ---------------------- 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

Put_Transfer_List = {}
Get_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 == "-G":
        ProcessTXRQ(val, Get_Transfer_List)

    if opt == "-H":
        HOSTS = val.split()
        
    if opt == "-N":
        PROMPTUSERNAME = True
        
    if opt == "-P":
        ProcessTXRQ(val, Put_Transfer_List)

    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:

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

    try:
        f = open(args[0])
        HOSTS = f.readlines()
        f.close()

    except:
        ErrorExit(eBADFILE % args[0])

    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 any file transfers and
# commands.  Accomodate commenting out hosts in a list.

for host in HOSTS:

    host = host.split(COMMENT)[0].strip()
    if 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 CMD:
            HostCommand(host, UNAME, PWORD, CMD)