OLD | NEW |
(Empty) | |
| 1 #!/usr/bin/env python |
| 2 |
| 3 '''hive -- Hive Shell |
| 4 |
| 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 |
| 9 sync output. |
| 10 |
| 11 Example: |
| 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 |
| 32 Other Usage Examples: |
| 33 |
| 34 1. You will be asked for your username and password for each host. |
| 35 |
| 36 hive.py host1 host2 host3 ... hostN |
| 37 |
| 38 2. 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 |
| 43 3. Give a username and password on the command-line: |
| 44 |
| 45 hive.py user1:pass2@host1 user2:pass2@host2 ... userN:passN@hostN |
| 46 |
| 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: |
| 50 |
| 51 username:password@host |
| 52 |
| 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. |
| 57 |
| 58 This is a crude script that begs to be multithreaded. But it serves its |
| 59 purpose. |
| 60 |
| 61 PEXPECT LICENSE |
| 62 |
| 63 This license is approved by the OSI and FSF as GPL-compatible. |
| 64 http://opensource.org/licenses/isc-license.txt |
| 65 |
| 66 Copyright (c) 2012, Noah Spurrier <noah@noah.org> |
| 67 PERMISSION TO USE, COPY, MODIFY, AND/OR DISTRIBUTE THIS SOFTWARE FOR ANY |
| 68 PURPOSE WITH OR WITHOUT FEE IS HEREBY GRANTED, PROVIDED THAT THE ABOVE |
| 69 COPYRIGHT NOTICE AND THIS PERMISSION NOTICE APPEAR IN ALL COPIES. |
| 70 THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES |
| 71 WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF |
| 72 MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR |
| 73 ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES |
| 74 WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN |
| 75 ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF |
| 76 OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. |
| 77 |
| 78 ''' |
| 79 |
| 80 from __future__ import print_function |
| 81 |
| 82 from __future__ import absolute_import |
| 83 |
| 84 # TODO add feature to support username:password@host combination |
| 85 # TODO add feature to log each host output in separate file |
| 86 |
| 87 import sys |
| 88 import os |
| 89 import re |
| 90 import optparse |
| 91 import time |
| 92 import getpass |
| 93 import readline |
| 94 import atexit |
| 95 try: |
| 96 import pexpect |
| 97 import pxssh |
| 98 except ImportError: |
| 99 sys.stderr.write("You do not have 'pexpect' installed.\n") |
| 100 sys.stderr.write("On Ubuntu you need the 'python-pexpect' package.\n") |
| 101 sys.stderr.write(" aptitude -y install python-pexpect\n") |
| 102 exit(1) |
| 103 |
| 104 |
| 105 try: |
| 106 raw_input |
| 107 except NameError: |
| 108 raw_input = input |
| 109 |
| 110 |
| 111 histfile = os.path.join(os.environ["HOME"], ".hive_history") |
| 112 try: |
| 113 readline.read_history_file(histfile) |
| 114 except IOError: |
| 115 pass |
| 116 atexit.register(readline.write_history_file, histfile) |
| 117 |
| 118 CMD_HELP='''Hive commands are preceded by a colon : (just think of vi). |
| 119 |
| 120 :target name1 name2 name3 ... |
| 121 |
| 122 set list of hosts to target commands |
| 123 |
| 124 :target all |
| 125 |
| 126 reset list of hosts to target all hosts in the hive. |
| 127 |
| 128 :to name command |
| 129 |
| 130 send a command line to the named host. This is similar to :target, but |
| 131 sends only one command and does not change the list of targets for future |
| 132 commands. |
| 133 |
| 134 :sync |
| 135 |
| 136 set mode to wait for shell prompts after commands are run. This is the |
| 137 default. When Hive first logs into a host it sets a special shell prompt |
| 138 pattern that it can later look for to synchronize output of the hosts. If |
| 139 you 'su' to another user then it can upset the synchronization. If you need |
| 140 to run something like 'su' then use the following pattern: |
| 141 |
| 142 CMD (? for help) > :async |
| 143 CMD (? for help) > sudo su - root |
| 144 CMD (? for help) > :prompt |
| 145 CMD (? for help) > :sync |
| 146 |
| 147 :async |
| 148 |
| 149 set mode to not expect command line prompts (see :sync). Afterwards |
| 150 commands are send to target hosts, but their responses are not read back |
| 151 until :sync is run. This is useful to run before commands that will not |
| 152 return with the special shell prompt pattern that Hive uses to synchronize. |
| 153 |
| 154 :refresh |
| 155 |
| 156 refresh the display. This shows the last few lines of output from all hosts. |
| 157 This is similar to resync, but does not expect the promt. This is useful |
| 158 for seeing what hosts are doing during long running commands. |
| 159 |
| 160 :resync |
| 161 |
| 162 This is similar to :sync, but it does not change the mode. It looks for the |
| 163 prompt and thus consumes all input from all targetted hosts. |
| 164 |
| 165 :prompt |
| 166 |
| 167 force each host to reset command line prompt to the special pattern used to |
| 168 synchronize all the hosts. This is useful if you 'su' to a different user |
| 169 where Hive would not know the prompt to match. |
| 170 |
| 171 :send my text |
| 172 |
| 173 This will send the 'my text' wihtout a line feed to the targetted hosts. |
| 174 This output of the hosts is not automatically synchronized. |
| 175 |
| 176 :control X |
| 177 |
| 178 This will send the given control character to the targetted hosts. |
| 179 For example, ":control c" will send ASCII 3. |
| 180 |
| 181 :exit |
| 182 |
| 183 This will exit the hive shell. |
| 184 |
| 185 ''' |
| 186 |
| 187 def login (args, cli_username=None, cli_password=None): |
| 188 |
| 189 # I have to keep a separate list of host names because Python dicts are not
ordered. |
| 190 # I want to keep the same order as in the args list. |
| 191 host_names = [] |
| 192 hive_connect_info = {} |
| 193 hive = {} |
| 194 # build up the list of connection information (hostname, username, password,
port) |
| 195 for host_connect_string in args: |
| 196 hcd = parse_host_connect_string (host_connect_string) |
| 197 hostname = hcd['hostname'] |
| 198 port = hcd['port'] |
| 199 if port == '': |
| 200 port = None |
| 201 if len(hcd['username']) > 0: |
| 202 username = hcd['username'] |
| 203 elif cli_username is not None: |
| 204 username = cli_username |
| 205 else: |
| 206 username = raw_input('%s username: ' % hostname) |
| 207 if len(hcd['password']) > 0: |
| 208 password = hcd['password'] |
| 209 elif cli_password is not None: |
| 210 password = cli_password |
| 211 else: |
| 212 password = getpass.getpass('%s password: ' % hostname) |
| 213 host_names.append(hostname) |
| 214 hive_connect_info[hostname] = (hostname, username, password, port) |
| 215 # build up the list of hive connections using the connection information. |
| 216 for hostname in host_names: |
| 217 print('connecting to', hostname) |
| 218 try: |
| 219 fout = file("log_"+hostname, "w") |
| 220 hive[hostname] = pxssh.pxssh() |
| 221 # Disable host key checking. |
| 222 hive[hostname].SSH_OPTS = (hive[hostname].SSH_OPTS |
| 223 + " -o 'StrictHostKeyChecking=no'" |
| 224 + " -o 'UserKnownHostsFile /dev/null' ") |
| 225 hive[hostname].force_password = True |
| 226 hive[hostname].login(*hive_connect_info[hostname]) |
| 227 print(hive[hostname].before) |
| 228 hive[hostname].logfile = fout |
| 229 print('- OK') |
| 230 except Exception as e: |
| 231 print('- ERROR', end=' ') |
| 232 print(str(e)) |
| 233 print('Skipping', hostname) |
| 234 hive[hostname] = None |
| 235 return host_names, hive |
| 236 |
| 237 def main (): |
| 238 |
| 239 global options, args, CMD_HELP |
| 240 |
| 241 rows = 24 |
| 242 cols = 80 |
| 243 |
| 244 if options.sameuser: |
| 245 cli_username = raw_input('username: ') |
| 246 else: |
| 247 cli_username = None |
| 248 |
| 249 if options.samepass: |
| 250 cli_password = getpass.getpass('password: ') |
| 251 else: |
| 252 cli_password = None |
| 253 |
| 254 host_names, hive = login(args, cli_username, cli_password) |
| 255 |
| 256 synchronous_mode = True |
| 257 target_hostnames = host_names[:] |
| 258 print('targetting hosts:', ' '.join(target_hostnames)) |
| 259 while True: |
| 260 cmd = raw_input('CMD (? for help) > ') |
| 261 cmd = cmd.strip() |
| 262 if cmd=='?' or cmd==':help' or cmd==':h': |
| 263 print(CMD_HELP) |
| 264 continue |
| 265 elif cmd==':refresh': |
| 266 refresh (hive, target_hostnames, timeout=0.5) |
| 267 for hostname in target_hostnames: |
| 268 print('/' + '=' * (cols - 2)) |
| 269 print('| ' + hostname) |
| 270 print('\\' + '-' * (cols - 2)) |
| 271 if hive[hostname] is None: |
| 272 print('# DEAD: %s' % hostname) |
| 273 else: |
| 274 print(hive[hostname].before) |
| 275 print('#' * 79) |
| 276 continue |
| 277 elif cmd==':resync': |
| 278 resync (hive, target_hostnames, timeout=0.5) |
| 279 for hostname in target_hostnames: |
| 280 print('/' + '=' * (cols - 2)) |
| 281 print('| ' + hostname) |
| 282 print('\\' + '-' * (cols - 2)) |
| 283 if hive[hostname] is None: |
| 284 print('# DEAD: %s' % hostname) |
| 285 else: |
| 286 print(hive[hostname].before) |
| 287 print('#' * 79) |
| 288 continue |
| 289 elif cmd==':sync': |
| 290 synchronous_mode = True |
| 291 resync (hive, target_hostnames, timeout=0.5) |
| 292 continue |
| 293 elif cmd==':async': |
| 294 synchronous_mode = False |
| 295 continue |
| 296 elif cmd==':prompt': |
| 297 for hostname in target_hostnames: |
| 298 try: |
| 299 if hive[hostname] is not None: |
| 300 hive[hostname].set_unique_prompt() |
| 301 except Exception as e: |
| 302 print("Had trouble communicating with %s, so removing it fro
m the target list." % hostname) |
| 303 print(str(e)) |
| 304 hive[hostname] = None |
| 305 continue |
| 306 elif cmd[:5] == ':send': |
| 307 cmd, txt = cmd.split(None,1) |
| 308 for hostname in target_hostnames: |
| 309 try: |
| 310 if hive[hostname] is not None: |
| 311 hive[hostname].send(txt) |
| 312 except Exception as e: |
| 313 print("Had trouble communicating with %s, so removing it fro
m the target list." % hostname) |
| 314 print(str(e)) |
| 315 hive[hostname] = None |
| 316 continue |
| 317 elif cmd[:3] == ':to': |
| 318 cmd, hostname, txt = cmd.split(None,2) |
| 319 print('/' + '=' * (cols - 2)) |
| 320 print('| ' + hostname) |
| 321 print('\\' + '-' * (cols - 2)) |
| 322 if hive[hostname] is None: |
| 323 print('# DEAD: %s' % hostname) |
| 324 continue |
| 325 try: |
| 326 hive[hostname].sendline (txt) |
| 327 hive[hostname].prompt(timeout=2) |
| 328 print(hive[hostname].before) |
| 329 except Exception as e: |
| 330 print("Had trouble communicating with %s, so removing it from th
e target list." % hostname) |
| 331 print(str(e)) |
| 332 hive[hostname] = None |
| 333 continue |
| 334 elif cmd[:7] == ':expect': |
| 335 cmd, pattern = cmd.split(None,1) |
| 336 print('looking for', pattern) |
| 337 try: |
| 338 for hostname in target_hostnames: |
| 339 if hive[hostname] is not None: |
| 340 hive[hostname].expect(pattern) |
| 341 print(hive[hostname].before) |
| 342 except Exception as e: |
| 343 print("Had trouble communicating with %s, so removing it from th
e target list." % hostname) |
| 344 print(str(e)) |
| 345 hive[hostname] = None |
| 346 continue |
| 347 elif cmd[:7] == ':target': |
| 348 target_hostnames = cmd.split()[1:] |
| 349 if len(target_hostnames) == 0 or target_hostnames[0] == all: |
| 350 target_hostnames = host_names[:] |
| 351 print('targetting hosts:', ' '.join(target_hostnames)) |
| 352 continue |
| 353 elif cmd == ':exit' or cmd == ':q' or cmd == ':quit': |
| 354 break |
| 355 elif cmd[:8] == ':control' or cmd[:5] == ':ctrl' : |
| 356 cmd, c = cmd.split(None,1) |
| 357 if ord(c)-96 < 0 or ord(c)-96 > 255: |
| 358 print('/' + '=' * (cols - 2)) |
| 359 print('| Invalid character. Must be [a-zA-Z], @, [, ], \\, ^, _,
or ?') |
| 360 print('\\' + '-' * (cols - 2)) |
| 361 continue |
| 362 for hostname in target_hostnames: |
| 363 try: |
| 364 if hive[hostname] is not None: |
| 365 hive[hostname].sendcontrol(c) |
| 366 except Exception as e: |
| 367 print("Had trouble communicating with %s, so removing it fro
m the target list." % hostname) |
| 368 print(str(e)) |
| 369 hive[hostname] = None |
| 370 continue |
| 371 elif cmd == ':esc': |
| 372 for hostname in target_hostnames: |
| 373 if hive[hostname] is not None: |
| 374 hive[hostname].send(chr(27)) |
| 375 continue |
| 376 # |
| 377 # Run the command on all targets in parallel |
| 378 # |
| 379 for hostname in target_hostnames: |
| 380 try: |
| 381 if hive[hostname] is not None: |
| 382 hive[hostname].sendline (cmd) |
| 383 except Exception as e: |
| 384 print("Had trouble communicating with %s, so removing it from th
e target list." % hostname) |
| 385 print(str(e)) |
| 386 hive[hostname] = None |
| 387 |
| 388 # |
| 389 # print the response for each targeted host. |
| 390 # |
| 391 if synchronous_mode: |
| 392 for hostname in target_hostnames: |
| 393 try: |
| 394 print('/' + '=' * (cols - 2)) |
| 395 print('| ' + hostname) |
| 396 print('\\' + '-' * (cols - 2)) |
| 397 if hive[hostname] is None: |
| 398 print('# DEAD: %s' % hostname) |
| 399 else: |
| 400 hive[hostname].prompt(timeout=2) |
| 401 print(hive[hostname].before) |
| 402 except Exception as e: |
| 403 print("Had trouble communicating with %s, so removing it fro
m the target list." % hostname) |
| 404 print(str(e)) |
| 405 hive[hostname] = None |
| 406 print('#' * 79) |
| 407 |
| 408 def refresh (hive, hive_names, timeout=0.5): |
| 409 |
| 410 '''This waits for the TIMEOUT on each host. |
| 411 ''' |
| 412 |
| 413 # TODO This is ideal for threading. |
| 414 for hostname in hive_names: |
| 415 if hive[hostname] is not None: |
| 416 hive[hostname].expect([pexpect.TIMEOUT,pexpect.EOF],timeout=timeout) |
| 417 |
| 418 def resync (hive, hive_names, timeout=2, max_attempts=5): |
| 419 |
| 420 '''This waits for the shell prompt for each host in an effort to try to get |
| 421 them all to the same state. The timeout is set low so that hosts that are |
| 422 already at the prompt will not slow things down too much. If a prompt match |
| 423 is made for a hosts then keep asking until it stops matching. This is a |
| 424 best effort to consume all input if it printed more than one prompt. It's |
| 425 kind of kludgy. Note that this will always introduce a delay equal to the |
| 426 timeout for each machine. So for 10 machines with a 2 second delay you will |
| 427 get AT LEAST a 20 second delay if not more. ''' |
| 428 |
| 429 # TODO This is ideal for threading. |
| 430 for hostname in hive_names: |
| 431 if hive[hostname] is not None: |
| 432 for attempts in range(0, max_attempts): |
| 433 if not hive[hostname].prompt(timeout=timeout): |
| 434 break |
| 435 |
| 436 def parse_host_connect_string (hcs): |
| 437 |
| 438 '''This parses a host connection string in the form |
| 439 username:password@hostname:port. All fields are options expcet hostname. A |
| 440 dictionary is returned with all four keys. Keys that were not included are |
| 441 set to empty strings ''. Note that if your password has the '@' character |
| 442 then you must backslash escape it. ''' |
| 443 |
| 444 if '@' in hcs: |
| 445 p = re.compile (r'(?P<username>[^@:]*)(:?)(?P<password>.*)(?!\\)@(?P<hos
tname>[^:]*):?(?P<port>[0-9]*)') |
| 446 else: |
| 447 p = re.compile (r'(?P<username>)(?P<password>)(?P<hostname>[^:]*):?(?P<p
ort>[0-9]*)') |
| 448 m = p.search (hcs) |
| 449 d = m.groupdict() |
| 450 d['password'] = d['password'].replace('\\@','@') |
| 451 return d |
| 452 |
| 453 if __name__ == '__main__': |
| 454 start_time = time.time() |
| 455 parser = optparse.OptionParser(formatter=optparse.TitledHelpFormatter(), usa
ge=globals()['__doc__'], version='$Id: hive.py 533 2012-10-20 02:19:33Z noah $',
conflict_handler="resolve") |
| 456 parser.add_option ('-v', '--verbose', action='store_true', default=False, he
lp='verbose output') |
| 457 parser.add_option ('--samepass', action='store_true', default=False, help='U
se same password for each login.') |
| 458 parser.add_option ('--sameuser', action='store_true', default=False, help='U
se same username for each login.') |
| 459 (options, args) = parser.parse_args() |
| 460 if len(args) < 1: |
| 461 parser.error ('missing argument') |
| 462 if options.verbose: print(time.asctime()) |
| 463 main() |
| 464 if options.verbose: print(time.asctime()) |
| 465 if options.verbose: print('TOTAL TIME IN MINUTES:', end=' ') |
| 466 if options.verbose: print((time.time() - start_time) / 60.0) |
OLD | NEW |