#!/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 targeting 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 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. ''' from __future__ import print_function from __future__ import absolute_import # 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 time import getpass import readline import atexit try: import pexpect from 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) try: raw_input except NameError: raw_input = input 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 targeted 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 targeted hosts. This output of the hosts is not automatically synchronized. :control X This will send the given control character to the targeted 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 as e: print('- ERROR', end=' ') 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('targeting 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 as 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 as 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 as 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 as 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('targeting 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 as 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 as 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 as 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 range(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 optional except 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[^@:]*)(:?)(?P.*)(?!\\)@(?P[^:]*):?(?P[0-9]*)') else: p = re.compile (r'(?P)(?P)(?P[^:]*):?(?P[0-9]*)') m = p.search (hcs) d = m.groupdict() d['password'] = d['password'].replace('\\@','@') return d if __name__ == '__main__': 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:', end=' ') if options.verbose: print((time.time() - start_time) / 60.0)