forked from OSchip/llvm-project
473 lines
18 KiB
Python
473 lines
18 KiB
Python
#!/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)
|