5 This lets you ssh to a group of servers and control them as if they were one.
6 Each command you enter is sent to each host in parallel. The response of each
7 host is collected and printed. In normal synchronous mode Hive will wait for
8 each host to return the shell command line prompt. The shell prompt is used to
13 $ hive.py --sameuser --samepass host1.example.com host2.example.net
16 connecting to host1.example.com - OK
17 connecting to host2.example.net - OK
18 targetting hosts: 192.168.1.104 192.168.1.107
19 CMD (? for help) > uptime
20 =======================================================================
22 -----------------------------------------------------------------------
24 23:49:55 up 74 days, 5:14, 2 users, load average: 0.15, 0.05, 0.01
25 =======================================================================
27 -----------------------------------------------------------------------
29 23:53:02 up 1 day, 13:36, 2 users, load average: 0.50, 0.40, 0.46
30 =======================================================================
34 1. You will be asked for your username and password for each host.
36 hive.py host1 host2 host3 ... hostN
38 2. You will be asked once for your username and password.
39 This will be used for each host.
41 hive.py --sameuser --samepass host1 host2 host3 ... hostN
43 3. Give a username and password on the command-line:
45 hive.py user1:pass2@host1 user2:pass2@host2 ... userN:passN@hostN
47 You can use an extended host notation to specify username, password, and host
48 instead of entering auth information interactively. Where you would enter a
49 host name use this format:
51 username:password@host
53 This assumes that ':' is not part of the password. If your password contains a
54 ':' then you can use '\\:' to indicate a ':' and '\\\\' to indicate a single
55 '\\'. Remember that this information will appear in the process listing. Anyone
56 on your machine can see this auth information. This is not secure.
58 This is a crude script that begs to be multithreaded. But it serves its
63 $Id: hive.py 509 2008-01-05 21:27:47Z noah $
66 # TODO add feature to support username:password@host combination
67 # TODO add feature to log each host output in separate file
69 import sys, os, re, optparse, traceback, types, time, getpass
71 import readline, atexit
73 #histfile = os.path.join(os.environ["HOME"], ".hive_history")
75 # readline.read_history_file(histfile)
78 #atexit.register(readline.write_history_file, histfile)
80 CMD_HELP="""Hive commands are preceded by a colon : (just think of vi).
82 :target name1 name2 name3 ...
84 set list of hosts to target commands
88 reset list of hosts to target all hosts in the hive.
92 send a command line to the named host. This is similar to :target, but
93 sends only one command and does not change the list of targets for future
98 set mode to wait for shell prompts after commands are run. This is the
99 default. When Hive first logs into a host it sets a special shell prompt
100 pattern that it can later look for to synchronize output of the hosts. If
101 you 'su' to another user then it can upset the synchronization. If you need
102 to run something like 'su' then use the following pattern:
104 CMD (? for help) > :async
105 CMD (? for help) > sudo su - root
106 CMD (? for help) > :prompt
107 CMD (? for help) > :sync
111 set mode to not expect command line prompts (see :sync). Afterwards
112 commands are send to target hosts, but their responses are not read back
113 until :sync is run. This is useful to run before commands that will not
114 return with the special shell prompt pattern that Hive uses to synchronize.
118 refresh the display. This shows the last few lines of output from all hosts.
119 This is similar to resync, but does not expect the promt. This is useful
120 for seeing what hosts are doing during long running commands.
124 This is similar to :sync, but it does not change the mode. It looks for the
125 prompt and thus consumes all input from all targetted hosts.
129 force each host to reset command line prompt to the special pattern used to
130 synchronize all the hosts. This is useful if you 'su' to a different user
131 where Hive would not know the prompt to match.
135 This will send the 'my text' wihtout a line feed to the targetted hosts.
136 This output of the hosts is not automatically synchronized.
140 This will send the given control character to the targetted hosts.
141 For example, ":control c" will send ASCII 3.
145 This will exit the hive shell.
149 def login (args, cli_username=None, cli_password=None):
151 # I have to keep a separate list of host names because Python dicts are not ordered.
152 # I want to keep the same order as in the args list.
154 hive_connect_info = {}
156 # build up the list of connection information (hostname, username, password, port)
157 for host_connect_string in args:
158 hcd = parse_host_connect_string (host_connect_string)
159 hostname = hcd['hostname']
163 if len(hcd['username']) > 0:
164 username = hcd['username']
165 elif cli_username is not None:
166 username = cli_username
168 username = raw_input('%s username: ' % hostname)
169 if len(hcd['password']) > 0:
170 password = hcd['password']
171 elif cli_password is not None:
172 password = cli_password
174 password = getpass.getpass('%s password: ' % hostname)
175 host_names.append(hostname)
176 hive_connect_info[hostname] = (hostname, username, password, port)
177 # build up the list of hive connections using the connection information.
178 for hostname in host_names:
179 print 'connecting to', hostname
181 fout = file("log_"+hostname, "w")
182 hive[hostname] = pxssh.pxssh()
183 hive[hostname].login(*hive_connect_info[hostname])
184 print hive[hostname].before
185 hive[hostname].logfile = fout
190 print 'Skipping', hostname
191 hive[hostname] = None
192 return host_names, hive
196 global options, args, CMD_HELP
199 cli_username = raw_input('username: ')
204 cli_password = getpass.getpass('password: ')
208 host_names, hive = login(args, cli_username, cli_password)
210 synchronous_mode = True
211 target_hostnames = host_names[:]
212 print 'targetting hosts:', ' '.join(target_hostnames)
214 cmd = raw_input('CMD (? for help) > ')
216 if cmd=='?' or cmd==':help' or cmd==':h':
219 elif cmd==':refresh':
220 refresh (hive, target_hostnames, timeout=0.5)
221 for hostname in target_hostnames:
222 if hive[hostname] is None:
223 print '/============================================================================='
224 print '| ' + hostname + ' is DEAD'
225 print '\\-----------------------------------------------------------------------------'
227 print '/============================================================================='
228 print '| ' + hostname
229 print '\\-----------------------------------------------------------------------------'
230 print hive[hostname].before
231 print '=============================================================================='
234 resync (hive, target_hostnames, timeout=0.5)
235 for hostname in target_hostnames:
236 if hive[hostname] is None:
237 print '/============================================================================='
238 print '| ' + hostname + ' is DEAD'
239 print '\\-----------------------------------------------------------------------------'
241 print '/============================================================================='
242 print '| ' + hostname
243 print '\\-----------------------------------------------------------------------------'
244 print hive[hostname].before
245 print '=============================================================================='
248 synchronous_mode = True
249 resync (hive, target_hostnames, timeout=0.5)
252 synchronous_mode = False
255 for hostname in target_hostnames:
257 if hive[hostname] is not None:
258 hive[hostname].set_unique_prompt()
260 print "Had trouble communicating with %s, so removing it from the target list." % hostname
262 hive[hostname] = None
264 elif cmd[:5] == ':send':
265 cmd, txt = cmd.split(None,1)
266 for hostname in target_hostnames:
268 if hive[hostname] is not None:
269 hive[hostname].send(txt)
271 print "Had trouble communicating with %s, so removing it from the target list." % hostname
273 hive[hostname] = None
275 elif cmd[:3] == ':to':
276 cmd, hostname, txt = cmd.split(None,2)
277 if hive[hostname] is None:
278 print '/============================================================================='
279 print '| ' + hostname + ' is DEAD'
280 print '\\-----------------------------------------------------------------------------'
283 hive[hostname].sendline (txt)
284 hive[hostname].prompt(timeout=2)
285 print '/============================================================================='
286 print '| ' + hostname
287 print '\\-----------------------------------------------------------------------------'
288 print hive[hostname].before
290 print "Had trouble communicating with %s, so removing it from the target list." % hostname
292 hive[hostname] = None
294 elif cmd[:7] == ':expect':
295 cmd, pattern = cmd.split(None,1)
296 print 'looking for', pattern
298 for hostname in target_hostnames:
299 if hive[hostname] is not None:
300 hive[hostname].expect(pattern)
301 print hive[hostname].before
303 print "Had trouble communicating with %s, so removing it from the target list." % hostname
305 hive[hostname] = None
307 elif cmd[:7] == ':target':
308 target_hostnames = cmd.split()[1:]
309 if len(target_hostnames) == 0 or target_hostnames[0] == all:
310 target_hostnames = host_names[:]
311 print 'targetting hosts:', ' '.join(target_hostnames)
313 elif cmd == ':exit' or cmd == ':q' or cmd == ':quit':
315 elif cmd[:8] == ':control' or cmd[:5] == ':ctrl' :
316 cmd, c = cmd.split(None,1)
317 if ord(c)-96 < 0 or ord(c)-96 > 255:
318 print '/============================================================================='
319 print '| Invalid character. Must be [a-zA-Z], @, [, ], \\, ^, _, or ?'
320 print '\\-----------------------------------------------------------------------------'
322 for hostname in target_hostnames:
324 if hive[hostname] is not None:
325 hive[hostname].sendcontrol(c)
327 print "Had trouble communicating with %s, so removing it from the target list." % hostname
329 hive[hostname] = None
332 for hostname in target_hostnames:
333 if hive[hostname] is not None:
334 hive[hostname].send(chr(27))
337 # Run the command on all targets in parallel
339 for hostname in target_hostnames:
341 if hive[hostname] is not None:
342 hive[hostname].sendline (cmd)
344 print "Had trouble communicating with %s, so removing it from the target list." % hostname
346 hive[hostname] = None
349 # print the response for each targeted host.
352 for hostname in target_hostnames:
354 if hive[hostname] is None:
355 print '/============================================================================='
356 print '| ' + hostname + ' is DEAD'
357 print '\\-----------------------------------------------------------------------------'
359 hive[hostname].prompt(timeout=2)
360 print '/============================================================================='
361 print '| ' + hostname
362 print '\\-----------------------------------------------------------------------------'
363 print hive[hostname].before
365 print "Had trouble communicating with %s, so removing it from the target list." % hostname
367 hive[hostname] = None
368 print '=============================================================================='
370 def refresh (hive, hive_names, timeout=0.5):
372 """This waits for the TIMEOUT on each host.
375 # TODO This is ideal for threading.
376 for hostname in hive_names:
377 hive[hostname].expect([pexpect.TIMEOUT,pexpect.EOF],timeout=timeout)
379 def resync (hive, hive_names, timeout=2, max_attempts=5):
381 """This waits for the shell prompt for each host in an effort to try to get
382 them all to the same state. The timeout is set low so that hosts that are
383 already at the prompt will not slow things down too much. If a prompt match
384 is made for a hosts then keep asking until it stops matching. This is a
385 best effort to consume all input if it printed more than one prompt. It's
386 kind of kludgy. Note that this will always introduce a delay equal to the
387 timeout for each machine. So for 10 machines with a 2 second delay you will
388 get AT LEAST a 20 second delay if not more. """
390 # TODO This is ideal for threading.
391 for hostname in hive_names:
392 for attempts in xrange(0, max_attempts):
393 if not hive[hostname].prompt(timeout=timeout):
396 def parse_host_connect_string (hcs):
398 """This parses a host connection string in the form
399 username:password@hostname:port. All fields are options expcet hostname. A
400 dictionary is returned with all four keys. Keys that were not included are
401 set to empty strings ''. Note that if your password has the '@' character
402 then you must backslash escape it. """
405 p = re.compile (r'(?P<username>[^@:]*)(:?)(?P<password>.*)(?!\\)@(?P<hostname>[^:]*):?(?P<port>[0-9]*)')
407 p = re.compile (r'(?P<username>)(?P<password>)(?P<hostname>[^:]*):?(?P<port>[0-9]*)')
410 d['password'] = d['password'].replace('\\@','@')
413 if __name__ == '__main__':
415 start_time = time.time()
416 parser = optparse.OptionParser(formatter=optparse.TitledHelpFormatter(), usage=globals()['__doc__'], version='$Id: hive.py 509 2008-01-05 21:27:47Z noah $',conflict_handler="resolve")
417 parser.add_option ('-v', '--verbose', action='store_true', default=False, help='verbose output')
418 parser.add_option ('--samepass', action='store_true', default=False, help='Use same password for each login.')
419 parser.add_option ('--sameuser', action='store_true', default=False, help='Use same username for each login.')
420 (options, args) = parser.parse_args()
422 parser.error ('missing argument')
423 if options.verbose: print time.asctime()
425 if options.verbose: print time.asctime()
426 if options.verbose: print 'TOTAL TIME IN MINUTES:',
427 if options.verbose: print (time.time() - start_time) / 60.0
429 except KeyboardInterrupt, e: # Ctrl-C
431 except SystemExit, e: # sys.exit()
434 print 'ERROR, UNEXPECTED EXCEPTION'
436 traceback.print_exc()