1#!/usr/bin/env python 2 3"""hive -- Hive Shell 4 5This lets you ssh to a group of servers and control them as if they were one. 6Each command you enter is sent to each host in parallel. The response of each 7host is collected and printed. In normal synchronous mode Hive will wait for 8each host to return the shell command line prompt. The shell prompt is used to 9sync output. 10 11Example: 12 13 $ hive.py --sameuser --samepass host1.example.com host2.example.net 14 username: myusername 15 password: 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 ======================================================================= 21 host1.example.com 22 ----------------------------------------------------------------------- 23 uptime 24 23:49:55 up 74 days, 5:14, 2 users, load average: 0.15, 0.05, 0.01 25 ======================================================================= 26 host2.example.net 27 ----------------------------------------------------------------------- 28 uptime 29 23:53:02 up 1 day, 13:36, 2 users, load average: 0.50, 0.40, 0.46 30 ======================================================================= 31 32Other Usage Examples: 33 341. You will be asked for your username and password for each host. 35 36 hive.py host1 host2 host3 ... hostN 37 382. You will be asked once for your username and password. 39 This will be used for each host. 40 41 hive.py --sameuser --samepass host1 host2 host3 ... hostN 42 433. Give a username and password on the command-line: 44 45 hive.py user1:pass2@host1 user2:pass2@host2 ... userN:passN@hostN 46 47You can use an extended host notation to specify username, password, and host 48instead of entering auth information interactively. Where you would enter a 49host name use this format: 50 51 username:password@host 52 53This 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 56on your machine can see this auth information. This is not secure. 57 58This is a crude script that begs to be multithreaded. But it serves its 59purpose. 60 61Noah Spurrier 62 63$Id: hive.py 509 2008-01-05 21:27:47Z noah $ 64""" 65 66# TODO add feature to support username:password@host combination 67# TODO add feature to log each host output in separate file 68 69import sys, os, re, optparse, traceback, types, time, getpass 70import pexpect, pxssh 71import readline, atexit 72 73#histfile = os.path.join(os.environ["HOME"], ".hive_history") 74#try: 75# readline.read_history_file(histfile) 76#except IOError: 77# pass 78#atexit.register(readline.write_history_file, histfile) 79 80CMD_HELP="""Hive commands are preceded by a colon : (just think of vi). 81 82:target name1 name2 name3 ... 83 84 set list of hosts to target commands 85 86:target all 87 88 reset list of hosts to target all hosts in the hive. 89 90:to name command 91 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 94 commands. 95 96:sync 97 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: 103 104 CMD (? for help) > :async 105 CMD (? for help) > sudo su - root 106 CMD (? for help) > :prompt 107 CMD (? for help) > :sync 108 109:async 110 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. 115 116:refresh 117 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. 121 122:resync 123 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. 126 127:prompt 128 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. 132 133:send my text 134 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. 137 138:control X 139 140 This will send the given control character to the targetted hosts. 141 For example, ":control c" will send ASCII 3. 142 143:exit 144 145 This will exit the hive shell. 146 147""" 148 149def login (args, cli_username=None, cli_password=None): 150 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. 153 host_names = [] 154 hive_connect_info = {} 155 hive = {} 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'] 160 port = hcd['port'] 161 if port == '': 162 port = None 163 if len(hcd['username']) > 0: 164 username = hcd['username'] 165 elif cli_username is not None: 166 username = cli_username 167 else: 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 173 else: 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 180 try: 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 186 print '- OK' 187 except Exception, e: 188 print '- ERROR', 189 print str(e) 190 print 'Skipping', hostname 191 hive[hostname] = None 192 return host_names, hive 193 194def main (): 195 196 global options, args, CMD_HELP 197 198 if options.sameuser: 199 cli_username = raw_input('username: ') 200 else: 201 cli_username = None 202 203 if options.samepass: 204 cli_password = getpass.getpass('password: ') 205 else: 206 cli_password = None 207 208 host_names, hive = login(args, cli_username, cli_password) 209 210 synchronous_mode = True 211 target_hostnames = host_names[:] 212 print 'targetting hosts:', ' '.join(target_hostnames) 213 while True: 214 cmd = raw_input('CMD (? for help) > ') 215 cmd = cmd.strip() 216 if cmd=='?' or cmd==':help' or cmd==':h': 217 print CMD_HELP 218 continue 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 '\\-----------------------------------------------------------------------------' 226 else: 227 print '/=============================================================================' 228 print '| ' + hostname 229 print '\\-----------------------------------------------------------------------------' 230 print hive[hostname].before 231 print '==============================================================================' 232 continue 233 elif cmd==':resync': 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 '\\-----------------------------------------------------------------------------' 240 else: 241 print '/=============================================================================' 242 print '| ' + hostname 243 print '\\-----------------------------------------------------------------------------' 244 print hive[hostname].before 245 print '==============================================================================' 246 continue 247 elif cmd==':sync': 248 synchronous_mode = True 249 resync (hive, target_hostnames, timeout=0.5) 250 continue 251 elif cmd==':async': 252 synchronous_mode = False 253 continue 254 elif cmd==':prompt': 255 for hostname in target_hostnames: 256 try: 257 if hive[hostname] is not None: 258 hive[hostname].set_unique_prompt() 259 except Exception, e: 260 print "Had trouble communicating with %s, so removing it from the target list." % hostname 261 print str(e) 262 hive[hostname] = None 263 continue 264 elif cmd[:5] == ':send': 265 cmd, txt = cmd.split(None,1) 266 for hostname in target_hostnames: 267 try: 268 if hive[hostname] is not None: 269 hive[hostname].send(txt) 270 except Exception, e: 271 print "Had trouble communicating with %s, so removing it from the target list." % hostname 272 print str(e) 273 hive[hostname] = None 274 continue 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 '\\-----------------------------------------------------------------------------' 281 continue 282 try: 283 hive[hostname].sendline (txt) 284 hive[hostname].prompt(timeout=2) 285 print '/=============================================================================' 286 print '| ' + hostname 287 print '\\-----------------------------------------------------------------------------' 288 print hive[hostname].before 289 except Exception, e: 290 print "Had trouble communicating with %s, so removing it from the target list." % hostname 291 print str(e) 292 hive[hostname] = None 293 continue 294 elif cmd[:7] == ':expect': 295 cmd, pattern = cmd.split(None,1) 296 print 'looking for', pattern 297 try: 298 for hostname in target_hostnames: 299 if hive[hostname] is not None: 300 hive[hostname].expect(pattern) 301 print hive[hostname].before 302 except Exception, e: 303 print "Had trouble communicating with %s, so removing it from the target list." % hostname 304 print str(e) 305 hive[hostname] = None 306 continue 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) 312 continue 313 elif cmd == ':exit' or cmd == ':q' or cmd == ':quit': 314 break 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 '\\-----------------------------------------------------------------------------' 321 continue 322 for hostname in target_hostnames: 323 try: 324 if hive[hostname] is not None: 325 hive[hostname].sendcontrol(c) 326 except Exception, e: 327 print "Had trouble communicating with %s, so removing it from the target list." % hostname 328 print str(e) 329 hive[hostname] = None 330 continue 331 elif cmd == ':esc': 332 for hostname in target_hostnames: 333 if hive[hostname] is not None: 334 hive[hostname].send(chr(27)) 335 continue 336 # 337 # Run the command on all targets in parallel 338 # 339 for hostname in target_hostnames: 340 try: 341 if hive[hostname] is not None: 342 hive[hostname].sendline (cmd) 343 except Exception, e: 344 print "Had trouble communicating with %s, so removing it from the target list." % hostname 345 print str(e) 346 hive[hostname] = None 347 348 # 349 # print the response for each targeted host. 350 # 351 if synchronous_mode: 352 for hostname in target_hostnames: 353 try: 354 if hive[hostname] is None: 355 print '/=============================================================================' 356 print '| ' + hostname + ' is DEAD' 357 print '\\-----------------------------------------------------------------------------' 358 else: 359 hive[hostname].prompt(timeout=2) 360 print '/=============================================================================' 361 print '| ' + hostname 362 print '\\-----------------------------------------------------------------------------' 363 print hive[hostname].before 364 except Exception, e: 365 print "Had trouble communicating with %s, so removing it from the target list." % hostname 366 print str(e) 367 hive[hostname] = None 368 print '==============================================================================' 369 370def refresh (hive, hive_names, timeout=0.5): 371 372 """This waits for the TIMEOUT on each host. 373 """ 374 375 # TODO This is ideal for threading. 376 for hostname in hive_names: 377 hive[hostname].expect([pexpect.TIMEOUT,pexpect.EOF],timeout=timeout) 378 379def resync (hive, hive_names, timeout=2, max_attempts=5): 380 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. """ 389 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): 394 break 395 396def parse_host_connect_string (hcs): 397 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. """ 403 404 if '@' in hcs: 405 p = re.compile (r'(?P<username>[^@:]*)(:?)(?P<password>.*)(?!\\)@(?P<hostname>[^:]*):?(?P<port>[0-9]*)') 406 else: 407 p = re.compile (r'(?P<username>)(?P<password>)(?P<hostname>[^:]*):?(?P<port>[0-9]*)') 408 m = p.search (hcs) 409 d = m.groupdict() 410 d['password'] = d['password'].replace('\\@','@') 411 return d 412 413if __name__ == '__main__': 414 try: 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() 421 if len(args) < 1: 422 parser.error ('missing argument') 423 if options.verbose: print time.asctime() 424 main() 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 428 sys.exit(0) 429 except KeyboardInterrupt, e: # Ctrl-C 430 raise e 431 except SystemExit, e: # sys.exit() 432 raise e 433 except Exception, e: 434 print 'ERROR, UNEXPECTED EXCEPTION' 435 print str(e) 436 traceback.print_exc() 437 os._exit(1) 438