Index: third_party/pexpect/examples/hive.py |
diff --git a/third_party/pexpect/examples/hive.py b/third_party/pexpect/examples/hive.py |
new file mode 100644 |
index 0000000000000000000000000000000000000000..00ddbeaba9715f5cd1eeacb4859b8f474d07f45e |
--- /dev/null |
+++ b/third_party/pexpect/examples/hive.py |
@@ -0,0 +1,466 @@ |
+#!/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. |
+ |
+''' |
+ |
+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 |
+ 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 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 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('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 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('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 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 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__': |
+ 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) |