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
82 #histfile = os.path.join(os.environ["HOME"], ".hive_history")
84 # readline.read_history_file(histfile)
87 #atexit.register(readline.write_history_file, histfile)
89 CMD_HELP = """Hive commands are preceded by a colon : (just think of vi).
91 :target name1 name2 name3 ...
93 set list of hosts to target commands
97 reset list of hosts to target all hosts in the hive.
101 send a command line to the named host. This is similar to :target, but
102 sends only one command and does not change the list of targets for future
107 set mode to wait for shell prompts after commands are run. This is the
108 default. When Hive first logs into a host it sets a special shell prompt
109 pattern that it can later look for to synchronize output of the hosts. If
110 you 'su' to another user then it can upset the synchronization. If you need
111 to run something like 'su' then use the following pattern:
113 CMD (? for help) > :async
114 CMD (? for help) > sudo su - root
115 CMD (? for help) > :prompt
116 CMD (? for help) > :sync
120 set mode to not expect command line prompts (see :sync). Afterwards
121 commands are send to target hosts, but their responses are not read back
122 until :sync is run. This is useful to run before commands that will not
123 return with the special shell prompt pattern that Hive uses to synchronize.
127 refresh the display. This shows the last few lines of output from all hosts.
128 This is similar to resync, but does not expect the promt. This is useful
129 for seeing what hosts are doing during long running commands.
133 This is similar to :sync, but it does not change the mode. It looks for the
134 prompt and thus consumes all input from all targetted hosts.
138 force each host to reset command line prompt to the special pattern used to
139 synchronize all the hosts. This is useful if you 'su' to a different user
140 where Hive would not know the prompt to match.
144 This will send the 'my text' wihtout a line feed to the targetted hosts.
145 This output of the hosts is not automatically synchronized.
149 This will send the given control character to the targetted hosts.
150 For example, ":control c" will send ASCII 3.
154 This will exit the hive shell.
159 def login(args, cli_username=None, cli_password=None):
161 # I have to keep a separate list of host names because Python dicts are not ordered.
162 # I want to keep the same order as in the args list.
164 hive_connect_info = {}
166 # build up the list of connection information (hostname, username,
168 for host_connect_string in args:
169 hcd = parse_host_connect_string(host_connect_string)
170 hostname = hcd['hostname']
174 if len(hcd['username']) > 0:
175 username = hcd['username']
176 elif cli_username is not None:
177 username = cli_username
179 username = raw_input('%s username: ' % hostname)
180 if len(hcd['password']) > 0:
181 password = hcd['password']
182 elif cli_password is not None:
183 password = cli_password
185 password = getpass.getpass('%s password: ' % hostname)
186 host_names.append(hostname)
187 hive_connect_info[hostname] = (hostname, username, password, port)
188 # build up the list of hive connections using the connection information.
189 for hostname in host_names:
190 print 'connecting to', hostname
192 fout = file("log_" + hostname, "w")
193 hive[hostname] = pxssh.pxssh()
194 hive[hostname].login(*hive_connect_info[hostname])
195 print hive[hostname].before
196 hive[hostname].logfile = fout
198 except Exception as e:
201 print 'Skipping', hostname
202 hive[hostname] = None
203 return host_names, hive
208 global options, args, CMD_HELP
211 cli_username = raw_input('username: ')
216 cli_password = getpass.getpass('password: ')
220 host_names, hive = login(args, cli_username, cli_password)
222 synchronous_mode = True
223 target_hostnames = host_names[:]
224 print 'targetting hosts:', ' '.join(target_hostnames)
226 cmd = raw_input('CMD (? for help) > ')
228 if cmd == '?' or cmd == ':help' or cmd == ':h':
231 elif cmd == ':refresh':
232 refresh(hive, target_hostnames, timeout=0.5)
233 for hostname in target_hostnames:
234 if hive[hostname] is None:
235 print '/============================================================================='
236 print '| ' + hostname + ' is DEAD'
237 print '\\-----------------------------------------------------------------------------'
239 print '/============================================================================='
240 print '| ' + hostname
241 print '\\-----------------------------------------------------------------------------'
242 print hive[hostname].before
243 print '=============================================================================='
245 elif cmd == ':resync':
246 resync(hive, target_hostnames, timeout=0.5)
247 for hostname in target_hostnames:
248 if hive[hostname] is None:
249 print '/============================================================================='
250 print '| ' + hostname + ' is DEAD'
251 print '\\-----------------------------------------------------------------------------'
253 print '/============================================================================='
254 print '| ' + hostname
255 print '\\-----------------------------------------------------------------------------'
256 print hive[hostname].before
257 print '=============================================================================='
260 synchronous_mode = True
261 resync(hive, target_hostnames, timeout=0.5)
263 elif cmd == ':async':
264 synchronous_mode = False
266 elif cmd == ':prompt':
267 for hostname in target_hostnames:
269 if hive[hostname] is not None:
270 hive[hostname].set_unique_prompt()
271 except Exception as e:
272 print "Had trouble communicating with %s, so removing it from the target list." % hostname
274 hive[hostname] = None
276 elif cmd[:5] == ':send':
277 cmd, txt = cmd.split(None, 1)
278 for hostname in target_hostnames:
280 if hive[hostname] is not None:
281 hive[hostname].send(txt)
282 except Exception as e:
283 print "Had trouble communicating with %s, so removing it from the target list." % hostname
285 hive[hostname] = None
287 elif cmd[:3] == ':to':
288 cmd, hostname, txt = cmd.split(None, 2)
289 if hive[hostname] is None:
290 print '/============================================================================='
291 print '| ' + hostname + ' is DEAD'
292 print '\\-----------------------------------------------------------------------------'
295 hive[hostname].sendline(txt)
296 hive[hostname].prompt(timeout=2)
297 print '/============================================================================='
298 print '| ' + hostname
299 print '\\-----------------------------------------------------------------------------'
300 print hive[hostname].before
301 except Exception as e:
302 print "Had trouble communicating with %s, so removing it from the target list." % hostname
304 hive[hostname] = None
306 elif cmd[:7] == ':expect':
307 cmd, pattern = cmd.split(None, 1)
308 print 'looking for', pattern
310 for hostname in target_hostnames:
311 if hive[hostname] is not None:
312 hive[hostname].expect(pattern)
313 print hive[hostname].before
314 except Exception as e:
315 print "Had trouble communicating with %s, so removing it from the target list." % hostname
317 hive[hostname] = None
319 elif cmd[:7] == ':target':
320 target_hostnames = cmd.split()[1:]
321 if len(target_hostnames) == 0 or target_hostnames[0] == all:
322 target_hostnames = host_names[:]
323 print 'targetting hosts:', ' '.join(target_hostnames)
325 elif cmd == ':exit' or cmd == ':q' or cmd == ':quit':
327 elif cmd[:8] == ':control' or cmd[:5] == ':ctrl':
328 cmd, c = cmd.split(None, 1)
329 if ord(c) - 96 < 0 or ord(c) - 96 > 255:
330 print '/============================================================================='
331 print '| Invalid character. Must be [a-zA-Z], @, [, ], \\, ^, _, or ?'
332 print '\\-----------------------------------------------------------------------------'
334 for hostname in target_hostnames:
336 if hive[hostname] is not None:
337 hive[hostname].sendcontrol(c)
338 except Exception as e:
339 print "Had trouble communicating with %s, so removing it from the target list." % hostname
341 hive[hostname] = None
344 for hostname in target_hostnames:
345 if hive[hostname] is not None:
346 hive[hostname].send(chr(27))
349 # Run the command on all targets in parallel
351 for hostname in target_hostnames:
353 if hive[hostname] is not None:
354 hive[hostname].sendline(cmd)
355 except Exception as e:
356 print "Had trouble communicating with %s, so removing it from the target list." % hostname
358 hive[hostname] = None
361 # print the response for each targeted host.
364 for hostname in target_hostnames:
366 if hive[hostname] is None:
367 print '/============================================================================='
368 print '| ' + hostname + ' is DEAD'
369 print '\\-----------------------------------------------------------------------------'
371 hive[hostname].prompt(timeout=2)
372 print '/============================================================================='
373 print '| ' + hostname
374 print '\\-----------------------------------------------------------------------------'
375 print hive[hostname].before
376 except Exception as e:
377 print "Had trouble communicating with %s, so removing it from the target list." % hostname
379 hive[hostname] = None
380 print '=============================================================================='
383 def refresh(hive, hive_names, timeout=0.5):
384 """This waits for the TIMEOUT on each host.
387 # TODO This is ideal for threading.
388 for hostname in hive_names:
389 hive[hostname].expect([pexpect.TIMEOUT, pexpect.EOF], timeout=timeout)
392 def resync(hive, hive_names, timeout=2, max_attempts=5):
393 """This waits for the shell prompt for each host in an effort to try to get
394 them all to the same state. The timeout is set low so that hosts that are
395 already at the prompt will not slow things down too much. If a prompt match
396 is made for a hosts then keep asking until it stops matching. This is a
397 best effort to consume all input if it printed more than one prompt. It's
398 kind of kludgy. Note that this will always introduce a delay equal to the
399 timeout for each machine. So for 10 machines with a 2 second delay you will
400 get AT LEAST a 20 second delay if not more. """
402 # TODO This is ideal for threading.
403 for hostname in hive_names:
404 for attempts in xrange(0, max_attempts):
405 if not hive[hostname].prompt(timeout=timeout):
409 def parse_host_connect_string(hcs):
410 """This parses a host connection string in the form
411 username:password@hostname:port. All fields are options expcet hostname. A
412 dictionary is returned with all four keys. Keys that were not included are
413 set to empty strings ''. Note that if your password has the '@' character
414 then you must backslash escape it. """
418 r'(?P<username>[^@:]*)(:?)(?P<password>.*)(?!\\)@(?P<hostname>[^:]*):?(?P<port>[0-9]*)')
421 r'(?P<username>)(?P<password>)(?P<hostname>[^:]*):?(?P<port>[0-9]*)')
424 d['password'] = d['password'].replace('\\@', '@')
427 if __name__ == '__main__':
429 start_time = time.time()
430 parser = optparse.OptionParser(
431 formatter=optparse.TitledHelpFormatter(),
432 usage=globals()['__doc__'],
433 version='$Id: hive.py 509 2008-01-05 21:27:47Z noah $',
434 conflict_handler="resolve")
440 help='verbose output')
445 help='Use same password for each login.')
450 help='Use same username for each login.')
451 (options, args) = parser.parse_args()
453 parser.error('missing argument')
460 print 'TOTAL TIME IN MINUTES:',
462 print (time.time() - start_time) / 60.0
464 except KeyboardInterrupt as e: # Ctrl-C
466 except SystemExit as e: # sys.exit()
468 except Exception as e:
469 print 'ERROR, UNEXPECTED EXCEPTION'
471 traceback.print_exc()