Merge pull request #18598 from dillaman/wip-21958
[ceph.git] / src / ceph.in
1 # -*- mode:python -*-
2 # vim: ts=4 sw=4 smarttab expandtab
3 #
4 # Processed in Makefile to add python #! line and version variable
5 #
6 #
7
8
9 """
10 ceph.in becomes ceph, the command-line management tool for Ceph clusters.
11 This is a replacement for tools/ceph.cc and tools/common.cc.
12
13 Copyright (C) 2013 Inktank Storage, Inc.
14
15 This is free software; you can redistribute it and/or
16 modify it under the terms of the GNU General Public
17 License version 2, as published by the Free Software
18 Foundation.  See file COPYING.
19 """
20
21 import os
22 import sys
23 import platform
24
25 # Make life easier on developers:
26 # If in src/, and .libs and pybind exist here, assume we're running
27 # from a Ceph source dir and tweak PYTHONPATH and LD_LIBRARY_PATH
28 # to use local files
29
30 MYPATH = os.path.abspath(__file__)
31 MYDIR = os.path.dirname(MYPATH)
32 DEVMODEMSG = '*** DEVELOPER MODE: setting PATH, PYTHONPATH and LD_LIBRARY_PATH ***'
33
34 if MYDIR.endswith('src') and \
35    os.path.exists(os.path.join(MYDIR, '.libs')) and \
36    os.path.exists(os.path.join(MYDIR, 'pybind')):
37
38     if platform.system() == "Darwin":
39         lib_path_var = "DYLD_LIBRARY_PATH"
40     else:
41         lib_path_var = "LD_LIBRARY_PATH"
42
43     py_binary = os.environ.get("PYTHON", "python")
44     MYLIBPATH = os.path.join(MYDIR, '.libs')
45     execv_cmd = ['python']
46     if 'CEPH_DBG' in os.environ:
47         execv_cmd += ['-mpdb']
48     if lib_path_var in os.environ:
49         if MYLIBPATH not in os.environ[lib_path_var]:
50             os.environ[lib_path_var] += ':' + MYLIBPATH
51             print >> sys.stderr, DEVMODEMSG
52             os.execvp(py_binary, execv_cmd + sys.argv)
53     else:
54         os.environ[lib_path_var] = MYLIBPATH
55         print >> sys.stderr, DEVMODEMSG
56         os.execvp(py_binary, execv_cmd + sys.argv)
57     sys.path.insert(0, os.path.join(MYDIR, 'pybind'))
58     if os.environ.has_key('PATH') and MYDIR not in os.environ['PATH']:
59         os.environ['PATH'] += ':' + MYDIR
60
61 import argparse
62 import errno
63 import json
64 import rados
65 import shlex
66 import signal
67 import socket
68 import string
69 import struct
70 import subprocess
71
72 from ceph_argparse import \
73     concise_sig, descsort, parse_json_funcsigs, \
74     matchnum, validate_command, find_cmd_target, \
75     send_command, json_command
76
77 # just a couple of globals
78
79 verbose = False
80 cluster_handle = None
81
82 ############################################################################
83
84 def osdids():
85     ret, outbuf, outs = json_command(cluster_handle, prefix='osd ls')
86     if ret == -errno.EINVAL:
87         # try old mon
88         ret, outbuf, outs = send_command(cluster_handle, cmd=['osd', 'ls'])
89     if ret:
90         raise RuntimeError('Can\'t contact mon for osd list')
91     return [i for i in outbuf.split('\n') if i != '']
92
93 def monids():
94     ret, outbuf, outs = json_command(cluster_handle, prefix='mon dump',
95                                      argdict={'format':'json'})
96     if ret == -errno.EINVAL:
97         # try old mon
98         ret, outbuf, outs = send_command(cluster_handle,
99                                          cmd=['mon', 'dump', '--format=json'])
100     if ret:
101         raise RuntimeError('Can\'t contact mon for mon list')
102     d = json.loads(outbuf)
103     return [m['name'] for m in d['mons']]
104
105 def mdsids():
106     ret, outbuf, outs = json_command(cluster_handle, prefix='mds dump',
107                                      argdict={'format':'json'})
108     if ret == -errno.EINVAL:
109         # try old mon
110         ret, outbuf, outs = send_command(cluster_handle,
111                                          cmd=['mds', 'dump', '--format=json'])
112     if ret:
113         raise RuntimeError('Can\'t contact mon for mds list')
114     d = json.loads(outbuf)
115     l = []
116     infodict = d['info']
117     for mdsdict in infodict.values():
118         l.append(mdsdict['name'])
119     return l
120
121 # these args must be passed to all child programs
122 GLOBAL_ARGS = {
123     'client_id': '--id',
124     'client_name': '--name',
125     'cluster': '--cluster',
126     'cephconf': '--conf',
127 }
128
129 def parse_cmdargs(args=None, target=''):
130     # alias: let the line-wrapping be sane
131     AP = argparse.ArgumentParser
132
133     # format our own help
134     parser = AP(description='Ceph administration tool', add_help=False)
135
136     parser.add_argument('--completion', action='store_true',
137                         help=argparse.SUPPRESS)
138
139     parser.add_argument('-h', '--help', help='request mon help',
140                         action='store_true')
141
142     parser.add_argument('-c', '--conf', dest='cephconf',
143                         help='ceph configuration file')
144     parser.add_argument('-i', '--in-file', dest='input_file',
145                         help='input file')
146     parser.add_argument('-o', '--out-file', dest='output_file',
147                         help='output file')
148
149     parser.add_argument('--id', '--user', dest='client_id',
150                         help='client id for authentication')
151     parser.add_argument('--name', '-n', dest='client_name',
152                         help='client name for authentication')
153     parser.add_argument('--cluster', help='cluster name')
154
155     parser.add_argument('--admin-daemon', dest='admin_socket',
156                         help='submit admin-socket commands (\"help\" for help')
157     parser.add_argument('--admin-socket', dest='admin_socket_nope',
158                         help='you probably mean --admin-daemon')
159
160     parser.add_argument('-s', '--status', action='store_true',
161                         help='show cluster status')
162
163     parser.add_argument('-w', '--watch', action='store_true',
164                         help='watch live cluster changes')
165     parser.add_argument('--watch-debug', action='store_true',
166                         help='watch debug events')
167     parser.add_argument('--watch-info', action='store_true',
168                         help='watch info events')
169     parser.add_argument('--watch-sec', action='store_true',
170                         help='watch security events')
171     parser.add_argument('--watch-warn', action='store_true',
172                         help='watch warn events')
173     parser.add_argument('--watch-error', action='store_true',
174                         help='watch error events')
175
176     parser.add_argument('--version', '-v', action="store_true", help="display version")
177     parser.add_argument('--verbose', action="store_true", help="make verbose")
178     parser.add_argument('--concise', dest='verbose', action="store_false",
179                         help="make less verbose")
180
181     parser.add_argument('-f', '--format', choices=['json', 'json-pretty',
182                         'xml', 'xml-pretty', 'plain'], dest='output_format')
183
184     parser.add_argument('--connect-timeout', dest='cluster_timeout',
185                         type=int,
186                         help='set a timeout for connecting to the cluster')
187
188     # returns a Namespace with the parsed args, and a list of all extras
189     parsed_args, extras = parser.parse_known_args(args)
190
191     return parser, parsed_args, extras
192
193
194 def hdr(s):
195     print '\n', s, '\n', '=' * len(s)
196
197 def do_basic_help(parser, args):
198     """
199     Print basic parser help
200     If the cluster is available, get and print monitor help
201     """
202     hdr('General usage:')
203     parser.print_help()
204
205 def do_extended_help(parser, args):
206     def help_for_sigs(sigs, partial=None):
207         sys.stdout.write(format_help(parse_json_funcsigs(sigs, 'cli'),
208                          partial=partial))
209
210     def help_for_target(target, partial=None):
211         ret, outbuf, outs = json_command(cluster_handle, target=target,
212                                          prefix='get_command_descriptions', 
213                                          timeout=10)
214         if ret:
215             print >> sys.stderr, \
216                 "couldn't get command descriptions for {0}: {1}".\
217                 format(target, outs)
218         else:
219             help_for_sigs(outbuf, partial)
220
221     partial = ' '.join(args)
222     if (cluster_handle.state == "connected"):
223         help_for_target(target=('mon', ''), partial=partial)
224     return 0
225
226 DONTSPLIT = string.letters + '{[<>]}'
227
228 def wrap(s, width, indent):
229     """
230     generator to transform s into a sequence of strings width or shorter,
231     for wrapping text to a specific column width.
232     Attempt to break on anything but DONTSPLIT characters.
233     indent is amount to indent 2nd-through-nth lines.
234
235     so "long string long string long string" width=11 indent=1 becomes
236     'long string', ' long string', ' long string' so that it can be printed
237     as
238     long string
239      long string
240      long string
241
242     Consumes s.
243     """
244     result = ''
245     leader = ''
246     while len(s):
247
248         if (len(s) <= width):
249             # no splitting; just possibly indent
250             result = leader + s
251             s = ''
252             yield result
253
254         else:
255             splitpos = width
256             while (splitpos > 0) and (s[splitpos-1] in DONTSPLIT):
257                 splitpos -= 1
258
259             if splitpos == 0:
260                 splitpos = width
261
262             if result:
263                 # prior result means we're mid-iteration, indent
264                 result = leader
265             else:
266                 # first time, set leader and width for next
267                 leader = ' ' * indent
268                 width -= 1      # for subsequent space additions
269
270             # remove any leading spaces in this chunk of s
271             result += s[:splitpos].lstrip()
272             s = s[splitpos:]
273
274             yield result
275
276     raise StopIteration
277
278 def format_help(cmddict, partial=None):
279     """
280     Formats all the cmdsigs and helptexts from cmddict into a sorted-by-
281     cmdsig 2-column display, with each column wrapped and indented to
282     fit into 40 characters.
283     """
284
285     fullusage = ''
286     for cmd in sorted(cmddict.itervalues(), cmp=descsort):
287
288         if not cmd['help']:
289             continue
290         concise = concise_sig(cmd['sig'])
291         if partial and not concise.startswith(partial):
292             continue
293         siglines = [l for l in wrap(concise, 40, 1)]
294         helplines = [l for l in wrap(cmd['help'], 39, 1)]
295
296         # make lists the same length
297         maxlen = max(len(siglines), len(helplines))
298         siglines.extend([''] * (maxlen - len(siglines)))
299         helplines.extend([''] * (maxlen - len(helplines)))
300
301         # so we can zip them for output
302         for (s, h) in zip(siglines, helplines):
303             fullusage += '{0:40s} {1}\n'.format(s, h)
304
305     return fullusage
306
307 def admin_socket(asok_path, cmd, format=''):
308     """
309     Send a daemon (--admin-daemon) command 'cmd'.  asok_path is the
310     path to the admin socket; cmd is a list of strings; format may be
311     set to one of the formatted forms to get output in that form
312     (daemon commands don't support 'plain' output).
313     """
314
315     def do_sockio(path, cmd):
316         """ helper: do all the actual low-level stream I/O """
317         sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
318         sock.connect(path)
319         try:
320             sock.sendall(cmd + '\0')
321             len_str = sock.recv(4)
322             if len(len_str) < 4:
323                 raise RuntimeError("no data returned from admin socket")
324             l, = struct.unpack(">I", len_str)
325             ret = ''
326
327             got = 0
328             while got < l:
329                 bit = sock.recv(l - got)
330                 ret += bit
331                 got += len(bit)
332
333         except Exception as e:
334             raise RuntimeError('exception: ' + str(e))
335         return ret
336
337     try:
338         cmd_json = do_sockio(asok_path,
339             json.dumps({"prefix":"get_command_descriptions"}))
340     except Exception as e:
341         raise RuntimeError('exception getting command descriptions: ' + str(e))
342
343     if cmd == 'get_command_descriptions':
344         return cmd_json
345
346     sigdict = parse_json_funcsigs(cmd_json, 'cli')
347     valid_dict = validate_command(sigdict, cmd)
348     if not valid_dict:
349         raise RuntimeError('invalid command')
350
351     if format:
352         valid_dict['format'] = format
353
354     try:
355         ret = do_sockio(asok_path, json.dumps(valid_dict))
356     except Exception as e:
357         raise RuntimeError('exception: ' + str(e))
358
359     return ret
360
361
362 def ceph_conf(parsed_args, field, name):
363     args=['ceph-conf']
364
365     if name:
366         args.extend(['--name', name])
367
368     # add any args in GLOBAL_ARGS
369     for key, val in GLOBAL_ARGS.iteritems():
370         # ignore name in favor of argument name, if any
371         if name and key == 'client_name':
372             continue
373         if getattr(parsed_args, key):
374             args.extend([val, getattr(parsed_args, key)])
375
376     args.extend(['--show-config-value', field])
377     p = subprocess.Popen(
378         args,
379         stdout=subprocess.PIPE,
380         stderr=subprocess.PIPE)
381     outdata, errdata = p.communicate()
382     if (len(errdata)):
383         raise RuntimeError('unable to get conf option %s for %s: %s' % (field, name, errdata))
384     return outdata.rstrip()
385
386 PROMPT = 'ceph> '
387
388 if sys.stdin.isatty():
389     def read_input():
390         while True:
391             line = raw_input(PROMPT).rstrip()
392             if line in ['q', 'quit', 'Q']:
393                 return None
394             if line:
395                 return line
396 else:
397     def read_input():
398         while True:
399             line = sys.stdin.readline()
400             if not line:
401                 return None
402             line = line.rstrip()
403             if line:
404                 return line
405
406
407 def new_style_command(parsed_args, cmdargs, target, sigdict, inbuf, verbose):
408     """
409     Do new-style command dance.
410     target: daemon to receive command: mon (any) or osd.N
411     sigdict - the parsed output from the new monitor describing commands
412     inbuf - any -i input file data
413     verbose - bool
414     """
415     if verbose:
416         for cmdtag in sorted(sigdict.keys()):
417             cmd = sigdict[cmdtag]
418             sig = cmd['sig']
419             print '{0}: {1}'.format(cmdtag, concise_sig(sig))
420
421     got_command = False
422
423     if not got_command:
424         if cmdargs:
425             # Validate input args against list of sigs
426             valid_dict = validate_command(sigdict, cmdargs, verbose)
427             if valid_dict:
428                 got_command = True
429                 if parsed_args.output_format:
430                     valid_dict['format'] = parsed_args.output_format
431             else:
432                 return -errno.EINVAL, '', 'invalid command'
433         else:
434             if sys.stdin.isatty():
435                 # do the command-interpreter looping
436                 # for raw_input to do readline cmd editing
437                 import readline
438
439             while True:
440                 interactive_input = read_input()
441                 if interactive_input is None:
442                     return 0, '', ''
443                 cmdargs = parse_cmdargs(shlex.split(interactive_input))[2]
444                 try:
445                     target = find_cmd_target(cmdargs)
446                 except Exception as e:
447                     print >> sys.stderr, \
448                             'error handling command target: {0}'.format(e)
449                     return 1, '', ''
450                 if len(cmdargs) and cmdargs[0] == 'tell':
451                     print >> sys.stderr, \
452                           'Can not use \'tell\' in interactive mode.'
453                     continue
454                 valid_dict = validate_command(sigdict, cmdargs, verbose)
455                 if valid_dict:
456                     if parsed_args.output_format:
457                         valid_dict['format'] = parsed_args.output_format
458                     if verbose:
459                         print >> sys.stderr, "Submitting command ", valid_dict
460                     ret, outbuf, outs = json_command(cluster_handle,
461                                                      target=target,
462                                                      argdict=valid_dict)
463                     if ret:
464                         ret = abs(ret)
465                         print >> sys.stderr, \
466                             'Error: {0} {1}'.format(ret, errno.errorcode.get(ret, 'Unknown'))
467                     if outbuf:
468                         print outbuf
469                     if outs:
470                         print >> sys.stderr, 'Status:\n', outs
471                 else:
472                     print >> sys.stderr, "Invalid command"
473
474     if verbose:
475         print >> sys.stderr, "Submitting command ", valid_dict
476     return json_command(cluster_handle, target=target, argdict=valid_dict,
477                         inbuf=inbuf)
478
479 def complete(sigdict, args, target):
480     """
481     Command completion.  Match as much of [args] as possible,
482     and print every possible match separated by newlines. 
483     Return exitcode.
484     """
485     # XXX this looks a lot like the front of validate_command().  Refactor?
486
487     complete_verbose = 'COMPVERBOSE' in os.environ
488
489     # Repulsive hack to handle tell: lop off 'tell' and target
490     # and validate the rest of the command.  'target' is already
491     # determined in our callers, so it's ok to remove it here.
492     if len(args) and args[0] == 'tell':
493         args = args[2:]
494     # look for best match, accumulate possibles in bestcmds
495     # (so we can maybe give a more-useful error message)
496     best_match_cnt = 0
497     bestcmds = []
498     for cmdtag, cmd in sigdict.iteritems():
499         sig = cmd['sig']
500         matched = matchnum(args, sig, partial=True)
501         if (matched > best_match_cnt):
502             if complete_verbose:
503                 print >> sys.stderr, \
504                     "better match: {0} > {1}: {2}:{3} ".format(matched,
505                                   best_match_cnt, cmdtag, concise_sig(sig))
506             best_match_cnt = matched
507             bestcmds = [{cmdtag:cmd}]
508         elif matched == best_match_cnt:
509             if complete_verbose:
510                 print >> sys.stderr, \
511                     "equal match: {0} > {1}: {2}:{3} ".format(matched,
512                                   best_match_cnt, cmdtag, concise_sig(sig))
513             bestcmds.append({cmdtag:cmd})
514
515     # look through all matching sigs
516     comps = []
517     for cmddict in bestcmds:
518         for cmd in cmddict.itervalues():
519             sig = cmd['sig']
520             # either:
521             #   we match everything fully, so we want the next desc, or
522             #   we match more partially, so we want the partial match
523             fullindex = matchnum(args, sig, partial=False) - 1
524             partindex = matchnum(args, sig, partial=True) - 1
525             if complete_verbose:
526                 print >> sys.stderr, '{}: f {} p {} len {}'.format(sig, fullindex, partindex, len(sig))
527             if fullindex == partindex and fullindex + 1 < len(sig):
528                 d = sig[fullindex + 1]
529             else:
530                 d = sig[partindex]
531             comps.append(str(d))
532     if complete_verbose:
533         print >> sys.stderr, '\n'.join(comps)
534     print '\n'.join(comps)
535
536     return 0
537
538 ###
539 # ping a monitor
540 ###
541 def ping_monitor(cluster_handle, name):
542     if 'mon.' not in name:
543         print >> sys.stderr, '"ping" expects a monitor to ping; try "ping mon.<id>"'
544         return 1
545
546     mon_id = name[len('mon.'):]
547     s = cluster_handle.ping_monitor(mon_id)
548     print s
549     return 0
550
551 ###
552 # main
553 ###
554
555 def main():
556     ceph_args = os.environ.get('CEPH_ARGS')
557     if ceph_args:
558         if "injectargs" in sys.argv:
559             i = sys.argv.index("injectargs")
560             sys.argv = sys.argv[:i] + ceph_args.split() + sys.argv[i:]
561         else:
562             sys.argv.extend(ceph_args.split())
563     parser, parsed_args, childargs = parse_cmdargs()
564
565     if parsed_args.version:
566         print 'ceph version {0} ({1})'.format(CEPH_GIT_NICE_VER, CEPH_GIT_VER)
567         return 0
568
569     global verbose
570     verbose = parsed_args.verbose
571
572     if verbose:
573         print >> sys.stderr, "parsed_args: {0}, childargs: {1}".format(parsed_args, childargs)
574
575     if parsed_args.admin_socket_nope:
576         print >> sys.stderr, '--admin-socket is used by daemons; '\
577         'you probably mean --admin-daemon/daemon'
578         return 1
579
580     # pass on --id, --name, --conf
581     name = 'client.admin'
582     if parsed_args.client_id:
583         name = 'client.' + parsed_args.client_id
584     if parsed_args.client_name:
585         name = parsed_args.client_name
586
587     # default '' means default conf search
588     conffile = ''
589     if parsed_args.cephconf:
590         conffile = parsed_args.cephconf
591     # For now, --admin-daemon is handled as usual.  Try it
592     # first in case we can't connect() to the cluster
593
594     format = parsed_args.output_format
595
596     sockpath = None
597     if parsed_args.admin_socket:
598         sockpath = parsed_args.admin_socket
599     elif len(childargs) > 0 and childargs[0] == "daemon":
600         # Treat "daemon <path>" or "daemon <name>" like --admin_daemon <path>
601         if len(childargs) > 2:
602             if childargs[1].find('/') >= 0:
603                 sockpath = childargs[1]
604             else:
605                 # try resolve daemon name
606                 try:
607                     sockpath = ceph_conf(parsed_args, 'admin_socket',
608                                          childargs[1])
609                 except Exception as e:
610                     print >> sys.stderr, \
611                         'Can\'t get admin socket path: ' + str(e)
612                     return errno.EINVAL
613             # for both:
614             childargs = childargs[2:]
615         else:
616             print >> sys.stderr, 'daemon requires at least 3 arguments'
617             return errno.EINVAL
618
619     if sockpath:
620         try:
621             print admin_socket(sockpath, childargs, format)
622         except Exception as e:
623             print >> sys.stderr, 'admin_socket: {0}'.format(e)
624             return errno.EINVAL
625         return 0
626
627     timeout = None
628     if parsed_args.cluster_timeout:
629         timeout = parsed_args.cluster_timeout
630
631     # basic help
632     if parsed_args.help:
633         do_basic_help(parser, childargs)
634
635     # handle any 'generic' ceph arguments that we didn't parse here
636     global cluster_handle
637
638     # rados.Rados() will call rados_create2, and then read the conf file,
639     # and then set the keys from the dict.  So we must do these
640     # "pre-file defaults" first (see common_preinit in librados)
641     conf_defaults = {
642         'log_to_stderr':'true',
643         'err_to_stderr':'true',
644         'log_flush_on_exit':'true',
645     }
646
647     if 'injectargs' in childargs:
648         position = childargs.index('injectargs')
649         injectargs = childargs[position:]
650         childargs = childargs[:position]
651         if verbose:
652             print >> sys.stderr, 'Separate childargs {0} from injectargs {1}'.\
653                 format(childargs, injectargs)
654     else:
655         injectargs = None
656
657     clustername = 'ceph'
658     if parsed_args.cluster:
659         clustername = parsed_args.cluster
660
661     try:
662         cluster_handle = rados.Rados(name=name, clustername=clustername,
663                                      conf_defaults=conf_defaults,
664                                      conffile=conffile)
665         retargs = cluster_handle.conf_parse_argv(childargs)
666     except rados.Error as e:
667         print >> sys.stderr, 'Error initializing cluster client: {0}'.\
668             format(repr(e))
669         return 1
670
671     childargs = retargs
672     if not childargs:
673         childargs = []
674
675     # -- means "stop parsing args", but we don't want to see it either
676     if '--' in childargs:
677         childargs.remove('--')
678     if injectargs and '--' in injectargs:
679         injectargs.remove('--')
680
681     # special deprecation warning for 'ceph <type> tell'
682     # someday 'mds' will be here too
683     if len(childargs) >= 2 and \
684         childargs[0] in ['mon', 'osd'] and \
685         childargs[1] == 'tell':
686         print >> sys.stderr, '"{0} tell" is deprecated; try "tell {0}.<id> <command> [options...]" instead (id can be "*") '.format(childargs[0])
687         return 1
688
689     if parsed_args.help:
690         # short default timeout for -h
691         if not timeout:
692             timeout = 5
693
694         hdr('Monitor commands:')
695         print '[Contacting monitor, timeout after %d seconds]' % timeout
696
697     if childargs and childargs[0] == 'ping':
698         if len(childargs) < 2:
699             print >> sys.stderr, '"ping" requires a monitor name as argument: "ping mon.<id>"'
700             return 1
701
702     try:
703         if childargs and childargs[0] == 'ping':
704             return ping_monitor(cluster_handle, childargs[1])
705         cluster_handle.connect(timeout=timeout)
706     except KeyboardInterrupt:
707         print >> sys.stderr, 'Cluster connection aborted'
708         return 1
709     except Exception as e:
710         print >> sys.stderr, 'Error connecting to cluster: {0}'.\
711             format(e.__class__.__name__)
712         return 1
713
714     if parsed_args.help:
715         return do_extended_help(parser, childargs)
716
717     # implement -w/--watch_*
718     # This is ugly, but Namespace() isn't quite rich enough.
719     level = ''
720     for k, v in parsed_args._get_kwargs():
721         if k.startswith('watch') and v:
722             if k == 'watch':
723                 level = 'info'
724             else:
725                 level = k.replace('watch_', '')
726     if level:
727
728         # an awfully simple callback
729         def watch_cb(arg, line, who, stamp_sec, stamp_nsec, seq, level, msg):
730             print line
731             sys.stdout.flush()
732
733         # first do a ceph status
734         ret, outbuf, outs = json_command(cluster_handle, prefix='status')
735         if ret == -errno.EINVAL:
736             # try old mon
737             ret, outbuf, outs = send_command(cluster_handle, cmd=['status'])
738             # old mon returns status to outs...ick
739             if ret == 0:
740                 outbuf += outs
741         if ret:
742             print >> sys.stderr, "status query failed: ", outs
743             return ret
744         print outbuf
745
746         # this instance keeps the watch connection alive, but is
747         # otherwise unused
748         logwatch = rados.MonitorLog(cluster_handle, level, watch_cb, 0)
749
750         # loop forever letting watch_cb print lines
751         try:
752             signal.pause()
753         except KeyboardInterrupt:
754             # or until ^C, at least
755             return 0
756
757     # read input file, if any
758     inbuf = ''
759     if parsed_args.input_file:
760         try:
761             with open(parsed_args.input_file, 'r') as f:
762                 inbuf = f.read()
763         except Exception as e:
764             print >> sys.stderr, 'Can\'t open input file {0}: {1}'.format(parsed_args.input_file, e)
765             return 1
766
767     # prepare output file, if any
768     if parsed_args.output_file:
769         try:
770             outf = open(parsed_args.output_file, 'w')
771         except Exception as e:
772             print >> sys.stderr, \
773                 'Can\'t open output file {0}: {1}'.\
774                 format(parsed_args.output_file, e)
775             return 1
776
777     # -s behaves like a command (ceph status).
778     if parsed_args.status:
779         childargs.insert(0, 'status')
780
781     try:
782         target = find_cmd_target(childargs)
783     except Exception as e:
784         print >> sys.stderr, \
785                 'error handling command target: {0}'.format(e)
786         return 1
787
788     # Repulsive hack to handle tell: lop off 'tell' and target
789     # and validate the rest of the command.  'target' is already
790     # determined in our callers, so it's ok to remove it here.
791     is_tell = False
792     if len(childargs) and childargs[0] == 'tell':
793         childargs = childargs[2:]
794         is_tell = True
795
796     if is_tell:
797         if injectargs:
798             childargs = injectargs
799         if not len(childargs):
800             print >> sys.stderr, \
801                 '"{0} tell" requires additional arguments.'.format(sys.argv[0]), \
802                 'Try "{0} tell <name> <command> [options...]" instead.'.format(sys.argv[0])
803             return errno.EINVAL
804
805     # fetch JSON sigs from command
806     # each line contains one command signature (a placeholder name
807     # of the form 'cmdNNN' followed by an array of argument descriptors)
808     # as part of the validated argument JSON object
809
810     targets = [target]
811
812     if target[1] == '*':
813         if target[0] == 'osd':
814             targets = [(target[0], o) for o in osdids()]
815         elif target[0] == 'mon':
816             targets = [(target[0], m) for m in monids()]
817
818     final_ret = 0
819     for target in targets:
820         # prettify?  prefix output with target, if there was a wildcard used
821         prefix = ''
822         suffix = ''
823         if not parsed_args.output_file and len(targets) > 1:
824             prefix = '{0}.{1}: '.format(*target)
825             suffix = '\n'
826
827         ret, outbuf, outs = json_command(cluster_handle, target=target,
828                                          prefix='get_command_descriptions')
829         compat = False
830         if ret == -errno.EINVAL:
831             # send command to old monitor or OSD
832             if verbose:
833                 print prefix + '{0} to old {1}'.format(' '.join(childargs), target[0])
834             compat = True
835             if parsed_args.output_format:
836                 childargs.extend(['--format', parsed_args.output_format])
837             ret, outbuf, outs = send_command(cluster_handle, target, childargs,
838                                              inbuf)
839
840             if ret == -errno.EINVAL:
841                 # did we race with a mon upgrade?  try again!
842                 ret, outbuf, outs = json_command(cluster_handle, target=target,
843                                                  prefix='get_command_descriptions')
844                 if ret == 0:
845                     compat = False  # yep, carry on
846         if not compat:
847             if ret:
848                 if ret < 0:
849                     outs = 'problem getting command descriptions from {0}.{1}'.format(*target)
850             else:
851                 sigdict = parse_json_funcsigs(outbuf, 'cli')
852
853                 if parsed_args.completion:
854                     return complete(sigdict, childargs, target)
855
856                 ret, outbuf, outs = new_style_command(parsed_args, childargs, target,
857                                                       sigdict, inbuf, verbose)
858
859                 # debug tool: send any successful command *again* to
860                 # verify that it is idempotent.
861                 if not ret and 'CEPH_CLI_TEST_DUP_COMMAND' in os.environ:
862                     ret, outbuf, outs = new_style_command(parsed_args, childargs, target,
863                                                           sigdict, inbuf, verbose)
864                     if ret < 0:
865                         ret = -ret
866                         print >> sys.stderr, prefix + 'Second attempt of previously successful command failed with {0}: {1}'.format(errno.errorcode.get(ret, 'Unknown'), outs)
867
868         if ret < 0:
869             ret = -ret
870             print >> sys.stderr, prefix + 'Error {0}: {1}'.format(errno.errorcode.get(ret, 'Unknown'), outs)
871             if len(targets) > 1:
872                 final_ret = ret
873             else:
874                 return ret
875
876         # this assumes outs never has useful command output, only status
877         if compat:
878             if ret == 0:
879                 # old cli/mon would send status string to stdout on non-error
880                 print outs
881         else:
882             if outs:
883                 print >> sys.stderr, prefix + outs
884
885         if (parsed_args.output_file):
886             outf.write(outbuf)
887         else:
888             # hack: old code printed status line before many json outputs
889             # (osd dump, etc.) that consumers know to ignore.  Add blank line
890             # to satisfy consumers that skip the first line, but not annoy
891             # consumers that don't.
892             if parsed_args.output_format and \
893                parsed_args.output_format.startswith('json') and \
894                not compat:
895                 sys.stdout.write('\n')
896
897             # if we are prettifying things, normalize newlines.  sigh.
898             if suffix != '':
899                 outbuf = outbuf.rstrip()
900             if outbuf != '':
901                 sys.stdout.write(prefix + outbuf + suffix)
902
903         sys.stdout.flush()
904
905     if (parsed_args.output_file):
906         outf.close()
907
908     if final_ret:
909         return final_ret
910
911     return 0
912
913 if __name__ == '__main__':
914     retval = main()
915     # shutdown explicitly; Rados() does not
916     if cluster_handle:
917         cluster_handle.shutdown()
918     sys.exit(retval)