#!/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}/greenplum_path.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}/greenplum_path.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()
