diff options
| author | Noah Spurrier <noah@squaretrade.com> | 2012-10-26 11:19:10 -0700 |
|---|---|---|
| committer | Noah Spurrier <noah@squaretrade.com> | 2012-10-26 11:19:10 -0700 |
| commit | 7999ca657997e78febfb3fb89cfcc066d50bf788 (patch) | |
| tree | 7ff33465bb4f8f79b92add505d11d4b731dfe6a7 /examples/hive.py | |
| parent | 6b65a76c26d72caf0a5b11725750861bf89f2b75 (diff) | |
| download | pexpect-7999ca657997e78febfb3fb89cfcc066d50bf788.tar.gz | |
Moved everything up one directory level.
Diffstat (limited to 'examples/hive.py')
| -rwxr-xr-x | examples/hive.py | 468 |
1 files changed, 468 insertions, 0 deletions
diff --git a/examples/hive.py b/examples/hive.py new file mode 100755 index 0000000..b9ef8c1 --- /dev/null +++ b/examples/hive.py @@ -0,0 +1,468 @@ +#!/usr/bin/env python + +'''hive -- Hive Shell + +This lets you ssh to a group of servers and control them as if they were one. +Each command you enter is sent to each host in parallel. The response of each +host is collected and printed. In normal synchronous mode Hive will wait for +each host to return the shell command line prompt. The shell prompt is used to +sync output. + +Example: + + $ hive.py --sameuser --samepass host1.example.com host2.example.net + username: myusername + password: + connecting to host1.example.com - OK + connecting to host2.example.net - OK + targetting hosts: 192.168.1.104 192.168.1.107 + CMD (? for help) > uptime + ======================================================================= + host1.example.com + ----------------------------------------------------------------------- + uptime + 23:49:55 up 74 days, 5:14, 2 users, load average: 0.15, 0.05, 0.01 + ======================================================================= + host2.example.net + ----------------------------------------------------------------------- + uptime + 23:53:02 up 1 day, 13:36, 2 users, load average: 0.50, 0.40, 0.46 + ======================================================================= + +Other Usage Examples: + +1. You will be asked for your username and password for each host. + + hive.py host1 host2 host3 ... hostN + +2. You will be asked once for your username and password. + This will be used for each host. + + hive.py --sameuser --samepass host1 host2 host3 ... hostN + +3. Give a username and password on the command-line: + + hive.py user1:pass2@host1 user2:pass2@host2 ... userN:passN@hostN + +You can use an extended host notation to specify username, password, and host +instead of entering auth information interactively. Where you would enter a +host name use this format: + + username:password@host + +This assumes that ':' is not part of the password. If your password contains a +':' then you can use '\\:' to indicate a ':' and '\\\\' to indicate a single +'\\'. Remember that this information will appear in the process listing. Anyone +on your machine can see this auth information. This is not secure. + +This is a crude script that begs to be multithreaded. But it serves its +purpose. + +PEXPECT LICENSE + + This license is approved by the OSI and FSF as GPL-compatible. + http://opensource.org/licenses/isc-license.txt + + Copyright (c) 2012, Noah Spurrier <noah@noah.org> + PERMISSION TO USE, COPY, MODIFY, AND/OR DISTRIBUTE THIS SOFTWARE FOR ANY + PURPOSE WITH OR WITHOUT FEE IS HEREBY GRANTED, PROVIDED THAT THE ABOVE + COPYRIGHT NOTICE AND THIS PERMISSION NOTICE APPEAR IN ALL COPIES. + THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +''' + +# TODO add feature to support username:password@host combination +# TODO add feature to log each host output in separate file + +import sys +import os +import re +import optparse +import traceback +import types +import time +import getpass +import readline +import atexit +try: + import pexpect + import pxssh +except ImportError: + sys.stderr.write("You do not have 'pexpect' installed.\n") + sys.stderr.write("On Ubuntu you need the 'python-pexpect' package.\n") + sys.stderr.write(" aptitude -y install python-pexpect\n") + exit(1) + +histfile = os.path.join(os.environ["HOME"], ".hive_history") +try: + readline.read_history_file(histfile) +except IOError: + pass +atexit.register(readline.write_history_file, histfile) + +CMD_HELP='''Hive commands are preceded by a colon : (just think of vi). + +:target name1 name2 name3 ... + + set list of hosts to target commands + +:target all + + reset list of hosts to target all hosts in the hive. + +:to name command + + send a command line to the named host. This is similar to :target, but + sends only one command and does not change the list of targets for future + commands. + +:sync + + set mode to wait for shell prompts after commands are run. This is the + default. When Hive first logs into a host it sets a special shell prompt + pattern that it can later look for to synchronize output of the hosts. If + you 'su' to another user then it can upset the synchronization. If you need + to run something like 'su' then use the following pattern: + + CMD (? for help) > :async + CMD (? for help) > sudo su - root + CMD (? for help) > :prompt + CMD (? for help) > :sync + +:async + + set mode to not expect command line prompts (see :sync). Afterwards + commands are send to target hosts, but their responses are not read back + until :sync is run. This is useful to run before commands that will not + return with the special shell prompt pattern that Hive uses to synchronize. + +:refresh + + refresh the display. This shows the last few lines of output from all hosts. + This is similar to resync, but does not expect the promt. This is useful + for seeing what hosts are doing during long running commands. + +:resync + + This is similar to :sync, but it does not change the mode. It looks for the + prompt and thus consumes all input from all targetted hosts. + +:prompt + + force each host to reset command line prompt to the special pattern used to + synchronize all the hosts. This is useful if you 'su' to a different user + where Hive would not know the prompt to match. + +:send my text + + This will send the 'my text' wihtout a line feed to the targetted hosts. + This output of the hosts is not automatically synchronized. + +:control X + + This will send the given control character to the targetted hosts. + For example, ":control c" will send ASCII 3. + +:exit + + This will exit the hive shell. + +''' + +def login (args, cli_username=None, cli_password=None): + + # I have to keep a separate list of host names because Python dicts are not ordered. + # I want to keep the same order as in the args list. + host_names = [] + hive_connect_info = {} + hive = {} + # build up the list of connection information (hostname, username, password, port) + for host_connect_string in args: + hcd = parse_host_connect_string (host_connect_string) + hostname = hcd['hostname'] + port = hcd['port'] + if port == '': + port = None + if len(hcd['username']) > 0: + username = hcd['username'] + elif cli_username is not None: + username = cli_username + else: + username = raw_input('%s username: ' % hostname) + if len(hcd['password']) > 0: + password = hcd['password'] + elif cli_password is not None: + password = cli_password + else: + password = getpass.getpass('%s password: ' % hostname) + host_names.append(hostname) + hive_connect_info[hostname] = (hostname, username, password, port) + # build up the list of hive connections using the connection information. + for hostname in host_names: + print 'connecting to', hostname + try: + fout = file("log_"+hostname, "w") + hive[hostname] = pxssh.pxssh() + # Disable host key checking. + hive[hostname].SSH_OPTS = (hive[hostname].SSH_OPTS + + " -o 'StrictHostKeyChecking=no'" + + " -o 'UserKnownHostsFile /dev/null' ") + hive[hostname].force_password = True + hive[hostname].login(*hive_connect_info[hostname]) + print hive[hostname].before + hive[hostname].logfile = fout + print '- OK' + except Exception, e: + print '- ERROR', + print str(e) + print 'Skipping', hostname + hive[hostname] = None + return host_names, hive + +def main (): + + global options, args, CMD_HELP + + rows = 24 + cols = 80 + + if options.sameuser: + cli_username = raw_input('username: ') + else: + cli_username = None + + if options.samepass: + cli_password = getpass.getpass('password: ') + else: + cli_password = None + + host_names, hive = login(args, cli_username, cli_password) + + synchronous_mode = True + target_hostnames = host_names[:] + print 'targetting hosts:', ' '.join(target_hostnames) + while True: + cmd = raw_input('CMD (? for help) > ') + cmd = cmd.strip() + if cmd=='?' or cmd==':help' or cmd==':h': + print CMD_HELP + continue + elif cmd==':refresh': + refresh (hive, target_hostnames, timeout=0.5) + for hostname in target_hostnames: + print '/' + '=' * (cols - 2) + print '| ' + hostname + print '\\' + '-' * (cols - 2) + if hive[hostname] is None: + print '# DEAD: %s' % hostname + else: + print hive[hostname].before + print '#' * 79 + continue + elif cmd==':resync': + resync (hive, target_hostnames, timeout=0.5) + for hostname in target_hostnames: + print '/' + '=' * (cols - 2) + print '| ' + hostname + print '\\' + '-' * (cols - 2) + if hive[hostname] is None: + print '# DEAD: %s' % hostname + else: + print hive[hostname].before + print '#' * 79 + continue + elif cmd==':sync': + synchronous_mode = True + resync (hive, target_hostnames, timeout=0.5) + continue + elif cmd==':async': + synchronous_mode = False + continue + elif cmd==':prompt': + for hostname in target_hostnames: + try: + if hive[hostname] is not None: + hive[hostname].set_unique_prompt() + except Exception, e: + print "Had trouble communicating with %s, so removing it from the target list." % hostname + print str(e) + hive[hostname] = None + continue + elif cmd[:5] == ':send': + cmd, txt = cmd.split(None,1) + for hostname in target_hostnames: + try: + if hive[hostname] is not None: + hive[hostname].send(txt) + except Exception, e: + print "Had trouble communicating with %s, so removing it from the target list." % hostname + print str(e) + hive[hostname] = None + continue + elif cmd[:3] == ':to': + cmd, hostname, txt = cmd.split(None,2) + print '/' + '=' * (cols - 2) + print '| ' + hostname + print '\\' + '-' * (cols - 2) + if hive[hostname] is None: + print '# DEAD: %s' % hostname + continue + try: + hive[hostname].sendline (txt) + hive[hostname].prompt(timeout=2) + print hive[hostname].before + except Exception, e: + print "Had trouble communicating with %s, so removing it from the target list." % hostname + print str(e) + hive[hostname] = None + continue + elif cmd[:7] == ':expect': + cmd, pattern = cmd.split(None,1) + print 'looking for', pattern + try: + for hostname in target_hostnames: + if hive[hostname] is not None: + hive[hostname].expect(pattern) + print hive[hostname].before + except Exception, e: + print "Had trouble communicating with %s, so removing it from the target list." % hostname + print str(e) + hive[hostname] = None + continue + elif cmd[:7] == ':target': + target_hostnames = cmd.split()[1:] + if len(target_hostnames) == 0 or target_hostnames[0] == all: + target_hostnames = host_names[:] + print 'targetting hosts:', ' '.join(target_hostnames) + continue + elif cmd == ':exit' or cmd == ':q' or cmd == ':quit': + break + elif cmd[:8] == ':control' or cmd[:5] == ':ctrl' : + cmd, c = cmd.split(None,1) + if ord(c)-96 < 0 or ord(c)-96 > 255: + print '/' + '=' * (cols - 2) + print '| Invalid character. Must be [a-zA-Z], @, [, ], \\, ^, _, or ?' + print '\\' + '-' * (cols - 2) + continue + for hostname in target_hostnames: + try: + if hive[hostname] is not None: + hive[hostname].sendcontrol(c) + except Exception, e: + print "Had trouble communicating with %s, so removing it from the target list." % hostname + print str(e) + hive[hostname] = None + continue + elif cmd == ':esc': + for hostname in target_hostnames: + if hive[hostname] is not None: + hive[hostname].send(chr(27)) + continue + # + # Run the command on all targets in parallel + # + for hostname in target_hostnames: + try: + if hive[hostname] is not None: + hive[hostname].sendline (cmd) + except Exception, e: + print "Had trouble communicating with %s, so removing it from the target list." % hostname + print str(e) + hive[hostname] = None + + # + # print the response for each targeted host. + # + if synchronous_mode: + for hostname in target_hostnames: + try: + print '/' + '=' * (cols - 2) + print '| ' + hostname + print '\\' + '-' * (cols - 2) + if hive[hostname] is None: + print '# DEAD: %s' % hostname + else: + hive[hostname].prompt(timeout=2) + print hive[hostname].before + except Exception, e: + print "Had trouble communicating with %s, so removing it from the target list." % hostname + print str(e) + hive[hostname] = None + print '#' * 79 + +def refresh (hive, hive_names, timeout=0.5): + + '''This waits for the TIMEOUT on each host. + ''' + + # TODO This is ideal for threading. + for hostname in hive_names: + if hive[hostname] is not None: + hive[hostname].expect([pexpect.TIMEOUT,pexpect.EOF],timeout=timeout) + +def resync (hive, hive_names, timeout=2, max_attempts=5): + + '''This waits for the shell prompt for each host in an effort to try to get + them all to the same state. The timeout is set low so that hosts that are + already at the prompt will not slow things down too much. If a prompt match + is made for a hosts then keep asking until it stops matching. This is a + best effort to consume all input if it printed more than one prompt. It's + kind of kludgy. Note that this will always introduce a delay equal to the + timeout for each machine. So for 10 machines with a 2 second delay you will + get AT LEAST a 20 second delay if not more. ''' + + # TODO This is ideal for threading. + for hostname in hive_names: + if hive[hostname] is not None: + for attempts in xrange(0, max_attempts): + if not hive[hostname].prompt(timeout=timeout): + break + +def parse_host_connect_string (hcs): + + '''This parses a host connection string in the form + username:password@hostname:port. All fields are options expcet hostname. A + dictionary is returned with all four keys. Keys that were not included are + set to empty strings ''. Note that if your password has the '@' character + then you must backslash escape it. ''' + + if '@' in hcs: + p = re.compile (r'(?P<username>[^@:]*)(:?)(?P<password>.*)(?!\\)@(?P<hostname>[^:]*):?(?P<port>[0-9]*)') + else: + p = re.compile (r'(?P<username>)(?P<password>)(?P<hostname>[^:]*):?(?P<port>[0-9]*)') + m = p.search (hcs) + d = m.groupdict() + d['password'] = d['password'].replace('\\@','@') + return d + +if __name__ == '__main__': + try: + start_time = time.time() + parser = optparse.OptionParser(formatter=optparse.TitledHelpFormatter(), usage=globals()['__doc__'], version='$Id: hive.py 533 2012-10-20 02:19:33Z noah $',conflict_handler="resolve") + parser.add_option ('-v', '--verbose', action='store_true', default=False, help='verbose output') + parser.add_option ('--samepass', action='store_true', default=False, help='Use same password for each login.') + parser.add_option ('--sameuser', action='store_true', default=False, help='Use same username for each login.') + (options, args) = parser.parse_args() + if len(args) < 1: + parser.error ('missing argument') + if options.verbose: print time.asctime() + main() + if options.verbose: print time.asctime() + if options.verbose: print 'TOTAL TIME IN MINUTES:', + if options.verbose: print (time.time() - start_time) / 60.0 + sys.exit(0) + except KeyboardInterrupt, e: # Ctrl-C + raise e + except SystemExit, e: # sys.exit() + raise e + except Exception, e: + print 'ERROR, UNEXPECTED EXCEPTION' + print str(e) + traceback.print_exc() + os._exit(1) |
