text: #!/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
  import os
  import re
  import optparse
  import traceback
  import types
  import time
  import getpass
  import pexpect
  import pxssh
  import readline
  import 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 as 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 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)
              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 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 '/============================================================================='
                  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 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:
                      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 as 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 as e:  # Ctrl-C
          raise e
      except SystemExit as e:  # sys.exit()
          raise e
      except Exception as e:
          print 'ERROR, UNEXPECTED EXCEPTION'
          print str(e)
          traceback.print_exc()
          os._exit(1)
  