blob: f3105599d21ad01407edfa5d6033e5da95503b80 [file] [log] [blame]
#!/usr/bin/env python3
'''
gpssh -- ssh access to multiple hosts at once
Usage: gpssh [--version] [-?v] [-h host] [-f hostfile] [cmd]
--version : print version information
-? : print this help screen
-v : verbose mode
-e : echo commands as they are executed
-h host : the host to connect to (multiple -h is okay)
-f file : a file listing all hosts to connect to
-D : do not filter multi-homed hosts
-s : source gpdb environment while login
cmd : the command to execute. If not present,
go into interactive mode
'''
import os
import sys
# this inserts the vendored pexpect from gpMgmt/bin/lib/pexpect/ into the path.
# Note that IDEs may show the local macos version of pexpect, which is NOT THE SAME
sys.path.insert(1, sys.path[0] + '/lib')
import getopt
import atexit
import signal
import pexpect
import time
import configparser
from gppylib.util import ssh_utils
from gppylib.gpparseopts import OptParser
from gppylib.commands.gp import get_coordinatordatadir
#
# all the command line options
#
class __globals__:
script_name = os.path.split(__file__)[-1]
USER = os.environ.get('LOGNAME') or os.environ.get('USER')
opt = {}
opt['-v'] = False
opt['-e'] = False
opt['-h'] = []
opt['-f'] = False
opt['-D'] = False
opt['-s'] = False
argcmd = None
session = None
DELAY_BEFORE_SEND = None
PROMPT_VALIDATION_TIMEOUT = None
SYNC_RETRIES = None
GV = __globals__()
################
def usage(exitarg):
parser = OptParser()
try:
parser.print_help()
except:
print(__doc__)
sys.exit(exitarg)
def print_version():
print('%s version $Revision$' % GV.script_name)
sys.exit(0)
def parseCommandLine():
try:
(options, args) = getopt.getopt(sys.argv[1:], '?evsh:f:D:u:d:t:', ['version'])
except Exception as e:
usage('Error: ' + str(e))
for (switch, val) in options:
if (switch == '-?'):
usage(0)
elif (switch[1] in 'evDs'):
GV.opt[switch] = True
elif (switch[1] in 'f'):
GV.opt[switch] = val
elif (switch == '-h'):
GV.opt[switch].append(val)
elif (switch == '--version'):
print_version()
elif (switch[1] in 'u'):
GV.USER = val
elif (switch == '-d'):
GV.DELAY_BEFORE_SEND = float(val)
elif (switch == '-t'):
GV.PROMPT_VALIDATION_TIMEOUT = float(val)
hf = (len(GV.opt['-h']) and 1 or 0) + (GV.opt['-f'] and 1 or 0)
if hf != 1:
usage('Error: please specify at least one of -h or -f args, but not both')
if (len(args) >= 1):
GV.argcmd = " ".join(args)
def parseConfigFile():
if GV.DELAY_BEFORE_SEND is not None and GV.PROMPT_VALIDATION_TIMEOUT is not None:
GV.SYNC_RETRIES = 3
if GV.opt['-v']:
print('Skip parsing gpssh.conf as both -d and -t are being used')
return
try:
config = configparser.RawConfigParser()
config.read(get_coordinatordatadir() + '/gpssh.conf')
if GV.DELAY_BEFORE_SEND is None:
GV.DELAY_BEFORE_SEND = config.getfloat('gpssh', 'delaybeforesend')
if GV.PROMPT_VALIDATION_TIMEOUT is None:
GV.PROMPT_VALIDATION_TIMEOUT = config.getfloat('gpssh', 'prompt_validation_timeout')
if GV.SYNC_RETRIES is None:
GV.SYNC_RETRIES = config.getint('gpssh', 'sync_retries')
except Exception:
if GV.opt['-v']:
print('[WARN] Reference default values as $COORDINATOR_DATA_DIRECTORY/gpssh.conf could not be found')
if GV.DELAY_BEFORE_SEND is None:
GV.DELAY_BEFORE_SEND = 0.05
if GV.PROMPT_VALIDATION_TIMEOUT is None:
GV.PROMPT_VALIDATION_TIMEOUT = 1.0
if GV.SYNC_RETRIES is None:
GV.SYNC_RETRIES = 3
if GV.DELAY_BEFORE_SEND < 0.0:
print('[ERROR] delaybeforesend cannot be negative')
sys.exit(1)
if GV.PROMPT_VALIDATION_TIMEOUT <= 0.0:
print('[ERROR] prompt_validation_timeout cannot be negative or zero')
sys.exit(1)
if GV.SYNC_RETRIES < 0:
print('[ERROR] sync_retries cannot be negative')
sys.exit(1)
if GV.opt['-v']:
print('Using delaybeforesend %s and prompt_validation_timeout %s' % (GV.DELAY_BEFORE_SEND,
GV.PROMPT_VALIDATION_TIMEOUT))
def sessionCleanup():
while True:
try:
return_code = 0
if GV.session:
if GV.session.verbose: print('\n[Cleanup...]');
return_code = GV.session.close()
GV.session = None
return return_code
except KeyboardInterrupt:
pass
sigint_time = 0
def sigint_handle(signum, frame):
global sigint_time
now = time.time()
if now - sigint_time >= 3:
sigint_time = now
raise KeyboardInterrupt
signal.signal(signal.SIGINT, signal.SIG_IGN)
print('\n[Exiting...]')
sys.exit(1)
def sighup_handle(signum, frame):
sys.exit(1)
def interactive():
try:
import readline
# Read in the saved command history, if any
histfile = os.path.join(os.environ["HOME"], ".gshist")
# Set the maximum number of commands to 100
readline.set_history_length(500)
try:
readline.read_history_file(histfile)
except IOError:
pass
# MPP-4054 - let's check the permissions before we register
try:
f = open(histfile, 'a')
atexit.register(readline.write_history_file, histfile)
f.close()
except IOError:
print("\n[WARN] Unable to write to gpssh history file: '%s'. Please check permissions." % histfile)
except Exception as e:
print("Note: command history unsupported on this machine ...")
atexit.register(sessionCleanup)
signal.signal(signal.SIGINT, sigint_handle)
signal.signal(signal.SIGHUP, sighup_handle)
while True:
try:
if not GV.session:
GV.session = ssh_utils.Session()
GV.session.verbose = GV.opt['-v']
GV.session.login(GV.opt['-h'], GV.USER, GV.DELAY_BEFORE_SEND, GV.PROMPT_VALIDATION_TIMEOUT, GV.SYNC_RETRIES)
GV.session.echoCommand = GV.opt['-e']
if GV.opt['-s']:
GV.session.executeCommand("source {0}/cloudberry-env.sh".format(os.environ["GPHOME"]))
GV.session.cmdloop()
except pexpect.EOF:
print('\n[Unexpected EOF from some hosts...]')
pass
except ssh_utils.Session.SessionCmdExit:
print('')
break
except ssh_utils.Session.SessionError as e:
print('Error: %s' % e)
pass
except KeyboardInterrupt:
print('\n[Interrupt...]')
GV.session.reset()
pass
def main():
if sys.version_info < (2, 5, 0):
sys.exit(
'''Error: %s is supported on Python versions 2.5 or greater
Please upgrade python installed on this machine.''' % os.path.split(sys.argv[0])[-1])
try:
# Parse options from the command line
parseCommandLine()
# Parse gpssh.conf file
parseConfigFile()
# Acquire the list of hosts from command line arguments
hostlist = ssh_utils.HostList()
for h in GV.opt['-h']:
hostlist.add(h)
if GV.opt['-f']:
hostlist.parseFile(GV.opt['-f'])
# Filter out non-unique hostnames unless the -D option is provided
if not GV.opt['-D']:
GV.opt['-h'] = hostlist.filterMultiHomedHosts()
else:
GV.opt['-h'] = hostlist.get()
if len(GV.opt['-h']) == 0:
usage('Error: missing hosts in -h and/or -f arguments')
# If a single command was passed to us, implement a single command session
if GV.argcmd:
try:
GV.session = ssh_utils.Session()
GV.session.verbose = GV.opt['-v']
GV.session.login(GV.opt['-h'], GV.USER, GV.DELAY_BEFORE_SEND, GV.PROMPT_VALIDATION_TIMEOUT, GV.SYNC_RETRIES)
GV.session.echoCommand = GV.opt['-e']
if GV.opt['-s']:
GV.session.executeCommand("source {0}/cloudberry-env.sh".format(os.environ["GPHOME"]))
output = GV.session.executeCommand(GV.argcmd)
GV.session.writeCommandOutput(output)
if GV.session.verbose: print('[INFO] completed successfully')
sys.stdout.flush()
except ssh_utils.Session.SessionError as e:
print('Error: %s' % e)
pass
else: # Otherwise, implement an interactive session
interactive()
return_code = sessionCleanup()
sys.exit(return_code)
except KeyboardInterrupt:
sessionCleanup()
sys.exit('\nInterrupted...')
#############
if __name__ == '__main__':
main()