Newer
Older
tsshbatch / tsshbatch.py
@tundra tundra on 22 Oct 2013 13 KB Minor change to sudo pw hint.
#!/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.149 2013/10/23 02:47:19 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     = "#"
CONSUCCESS  = "SUCCESS: Connection Established"
FAILURE     = "FAILURE: %s"
HOSTSEP     = ":"
INDENTWIDTH = 8
OPTIONSLIST = "G:H:NP:ehkn:p:v"
PADWIDTH    = 12
PATHSEP     = os.sep
SEPARATOR   = " --->  "
STDIN       = "-"
SUDO        = 'sudo'
SUDOPROMPT  = 'READINGSUDOPW'
SUDOARGS    = '-S -p %s' % SUDOPROMPT
SUDOPWHINT  = ' (Default: login password)'
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 To %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)
    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):

    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 + 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:

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

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

        PrintReport([host, CONSUCCESS])

        # Run all requested commands

        for command in commands:

            # 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 command.startswith(SUDO + " "):
                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 command.startswith(SUDO + " "):
                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, FAILURE % (eBADSUDO % sudonoise)] + ["\n"], HANDLER=PrintStderr)
                    ssh.close()
                    return
                    
            PrintReport([host + " (stdout)" + " [%s]" % command, "\n"] + stdout.readlines() + ["\n"])

            if REPORTERR:
                PrintReport([host + " (stderr)" + " [%s]" % command, "\n"] + stderr.readlines() + ["\n"], 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].strerror)], 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):

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


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

# Data Structures

Commands          = []
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])

    command = " ".join(args[1:])
    
# If hosts were passed on the command line, all the arguments
# are understood to form the command.
    
else:
    command = " ".join(args[0:])

# Cleanup the command and check to see if the user really wants to use
# stdin instead of explicit command line spefication of the desired
# remote command.  

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

command = command.strip()
if (command == STDIN):
    for command in sys.stdin.readlines():
        Commands.append(command.strip())

elif command:
    Commands = [command,]

# If user want 'sudo' execution, they MUST provide a password
# regardkess if how they intially authenticate to the system.
# This allows the user to have a different password (or a key) for
# logging in than for promoting privlege.

# If the user did use password authentication to initially
# authenticate, it's used as the default in the sudo password
# dialog below.

hint   = ""
if PWORD:
    hint = SUDOPWHINT

SUDOPW = ""
for command in Commands:
    if command.startswith(SUDO + " "):
        SUDOPW = getpass.getpass(pSUDO + hint)
        if PWORD and not SUDOPW:
            SUDOPW = PWORD

# 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 Commands:
            HostCommands(host, UNAME, PWORD, SUDOPW, Commands)