--- /dev/null
+#!/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.
+
+Noah Spurrier
+
+$Id: hive.py 509 2008-01-05 21:27:47Z noah $
+"""
+
+# TODO add feature to support username:password@host combination
+# TODO add feature to log each host output in separate file
+
+import sys, os, re, optparse, traceback, types, time, getpass
+import pexpect, pxssh
+import readline, atexit
+
+#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()
+ 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
+
+ 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:
+ if hive[hostname] is None:
+ print '/============================================================================='
+ print '| ' + hostname + ' is DEAD'
+ print '\\-----------------------------------------------------------------------------'
+ else:
+ print '/============================================================================='
+ print '| ' + hostname
+ print '\\-----------------------------------------------------------------------------'
+ print hive[hostname].before
+ print '=============================================================================='
+ continue
+ elif cmd==':resync':
+ resync (hive, target_hostnames, timeout=0.5)
+ for hostname in target_hostnames:
+ if hive[hostname] is None:
+ print '/============================================================================='
+ print '| ' + hostname + ' is DEAD'
+ print '\\-----------------------------------------------------------------------------'
+ else:
+ print '/============================================================================='
+ print '| ' + hostname
+ print '\\-----------------------------------------------------------------------------'
+ print hive[hostname].before
+ print '=============================================================================='
+ 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)
+ if hive[hostname] is None:
+ print '/============================================================================='
+ print '| ' + hostname + ' is DEAD'
+ print '\\-----------------------------------------------------------------------------'
+ continue
+ try:
+ hive[hostname].sendline (txt)
+ hive[hostname].prompt(timeout=2)
+ print '/============================================================================='
+ print '| ' + hostname
+ print '\\-----------------------------------------------------------------------------'
+ 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 '/============================================================================='
+ print '| Invalid character. Must be [a-zA-Z], @, [, ], \\, ^, _, or ?'
+ print '\\-----------------------------------------------------------------------------'
+ 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:
+ if hive[hostname] is None:
+ print '/============================================================================='
+ print '| ' + hostname + ' is DEAD'
+ print '\\-----------------------------------------------------------------------------'
+ else:
+ hive[hostname].prompt(timeout=2)
+ print '/============================================================================='
+ print '| ' + hostname
+ print '\\-----------------------------------------------------------------------------'
+ 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 '=============================================================================='
+
+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:
+ 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:
+ 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 509 2008-01-05 21:27:47Z 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)