summaryrefslogtreecommitdiff
path: root/pexpect/examples/hive.py
blob: ad9ce3979c6ccf7d5d54d833c81904ae2e8d142d (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
#!/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.

PEXPECT LICENSE

    This license is approved by the OSI and FSF as GPL-compatible.
        http://opensource.org/licenses/isc-license.txt

    Copyright (c) 2012, Noah Spurrier <noah@noah.org>
    PERMISSION TO USE, COPY, MODIFY, AND/OR DISTRIBUTE THIS SOFTWARE FOR ANY
    PURPOSE WITH OR WITHOUT FEE IS HEREBY GRANTED, PROVIDED THAT THE ABOVE
    COPYRIGHT NOTICE AND THIS PERMISSION NOTICE APPEAR IN ALL COPIES.
    THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
    WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
    MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
    ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
    WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
    ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
    OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.

"""

# 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 readline
import atexit
try:
    import pexpect
    import pxssh
except ImportError:
    sys.stderr.write("You do not have 'pexpect' installed.\n")
    sys.stderr.write("On Ubuntu you need the 'python-pexpect' package.\n")
    sys.stderr.write("    aptitude -y install python-pexpect\n")
    exit(1)

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()
            # Disable host key checking.
            hive[hostname].SSH_OPTS = (hive[hostname].SSH_OPTS
                    + " -o 'StrictHostKeyChecking=no'"
                    + " -o 'UserKnownHostsFile /dev/null' ")
            hive[hostname].force_password = True
            hive[hostname].login(*hive_connect_info[hostname])
            print hive[hostname].before
            hive[hostname].logfile = fout
            print '- OK'
        except Exception, e:
            print '- ERROR',
            print str(e)
            print 'Skipping', hostname
            hive[hostname] = None
    return host_names, hive

def main ():

    global options, args, CMD_HELP

    rows = 24
    cols = 80

    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:
                print '/' + '=' * (cols - 2)
                print '| ' + hostname
                print '\\' + '-' * (cols - 2)
                if hive[hostname] is None:
                    print '# DEAD: %s' % hostname
                else:
                    print hive[hostname].before
            print '#' * 79
            continue
        elif cmd==':resync':
            resync (hive, target_hostnames, timeout=0.5)
            for hostname in target_hostnames:
                print '/' + '=' * (cols - 2)
                print '| ' + hostname
                print '\\' + '-' * (cols - 2)
                if hive[hostname] is None:
                    print '# DEAD: %s' % hostname
                else:
                    print hive[hostname].before
            print '#' * 79
            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, 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, 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)
            print '/' + '=' * (cols - 2)
            print '| ' + hostname
            print '\\' + '-' * (cols - 2)
            if hive[hostname] is None:
                print '# DEAD: %s' % hostname
                continue
            try:
                hive[hostname].sendline (txt)
                hive[hostname].prompt(timeout=2)
                print hive[hostname].before
            except Exception, 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, 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 '/' + '=' * (cols - 2)
                print '| Invalid character. Must be [a-zA-Z], @, [, ], \\, ^, _, or ?'
                print '\\' + '-' * (cols - 2)
                continue
            for hostname in target_hostnames:
                try:
                    if hive[hostname] is not None:
                        hive[hostname].sendcontrol(c)
                except Exception, 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, 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:
                    print '/' + '=' * (cols - 2)
                    print '| ' + hostname
                    print '\\' + '-' * (cols - 2)
                    if hive[hostname] is None:
                        print '# DEAD: %s' % hostname
                    else:
                        hive[hostname].prompt(timeout=2)
                        print hive[hostname].before
                except Exception, e:
                    print "Had trouble communicating with %s, so removing it from the target list." % hostname
                    print str(e)
                    hive[hostname] = None
            print '#' * 79

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:
        if hive[hostname] is not None:
            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:
        if hive[hostname] is not None:
            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$',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, e: # Ctrl-C
        raise e
    except SystemExit, e: # sys.exit()
        raise e
    except Exception, e:
        print 'ERROR, UNEXPECTED EXCEPTION'
        print str(e)
        traceback.print_exc()
        os._exit(1)