Merge pull request #5216 from SUSE/wip-12269-hammer
[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                 valid_dict = validate_command(sigdict, cmdargs, verbose)
451                 if valid_dict:
452                     if parsed_args.output_format:
453                         valid_dict['format'] = parsed_args.output_format
454                     if verbose:
455                         print >> sys.stderr, "Submitting command ", valid_dict
456                     ret, outbuf, outs = json_command(cluster_handle,
457                                                      target=target,
458                                                      argdict=valid_dict)
459                     if ret:
460                         ret = abs(ret)
461                         print >> sys.stderr, \
462                             'Error: {0} {1}'.format(ret, errno.errorcode.get(ret, 'Unknown'))
463                     if outbuf:
464                         print outbuf
465                     if outs:
466                         print >> sys.stderr, 'Status:\n', outs
467                 else:
468                     print >> sys.stderr, "Invalid command"
469
470     if verbose:
471         print >> sys.stderr, "Submitting command ", valid_dict
472     return json_command(cluster_handle, target=target, argdict=valid_dict,
473                         inbuf=inbuf)
474
475 def complete(sigdict, args, target):
476     """
477     Command completion.  Match as much of [args] as possible,
478     and print every possible match separated by newlines. 
479     Return exitcode.
480     """
481     # XXX this looks a lot like the front of validate_command().  Refactor?
482
483     complete_verbose = 'COMPVERBOSE' in os.environ
484
485     # Repulsive hack to handle tell: lop off 'tell' and target
486     # and validate the rest of the command.  'target' is already
487     # determined in our callers, so it's ok to remove it here.
488     if len(args) and args[0] == 'tell':
489         args = args[2:]
490     # look for best match, accumulate possibles in bestcmds
491     # (so we can maybe give a more-useful error message)
492     best_match_cnt = 0
493     bestcmds = []
494     for cmdtag, cmd in sigdict.iteritems():
495         sig = cmd['sig']
496         matched = matchnum(args, sig, partial=True)
497         if (matched > best_match_cnt):
498             if complete_verbose:
499                 print >> sys.stderr, \
500                     "better match: {0} > {1}: {2}:{3} ".format(matched,
501                                   best_match_cnt, cmdtag, concise_sig(sig))
502             best_match_cnt = matched
503             bestcmds = [{cmdtag:cmd}]
504         elif matched == best_match_cnt:
505             if complete_verbose:
506                 print >> sys.stderr, \
507                     "equal match: {0} > {1}: {2}:{3} ".format(matched,
508                                   best_match_cnt, cmdtag, concise_sig(sig))
509             bestcmds.append({cmdtag:cmd})
510
511     # look through all matching sigs
512     comps = []
513     for cmddict in bestcmds:
514         for cmd in cmddict.itervalues():
515             sig = cmd['sig']
516             # either:
517             #   we match everything fully, so we want the next desc, or
518             #   we match more partially, so we want the partial match
519             fullindex = matchnum(args, sig, partial=False) - 1
520             partindex = matchnum(args, sig, partial=True) - 1
521             if complete_verbose:
522                 print >> sys.stderr, '{}: f {} p {} len {}'.format(sig, fullindex, partindex, len(sig))
523             if fullindex == partindex and fullindex + 1 < len(sig):
524                 d = sig[fullindex + 1]
525             else:
526                 d = sig[partindex]
527             comps.append(str(d))
528     if complete_verbose:
529         print >> sys.stderr, '\n'.join(comps)
530     print '\n'.join(comps)
531
532     return 0
533
534 ###
535 # ping a monitor
536 ###
537 def ping_monitor(cluster_handle, name):
538     if 'mon.' not in name:
539         print >> sys.stderr, '"ping" expects a monitor to ping; try "ping mon.<id>"'
540         return 1
541
542     mon_id = name[len('mon.'):]
543     s = cluster_handle.ping_monitor(mon_id)
544     print s
545     return 0
546
547 ###
548 # main
549 ###
550
551 def main():
552     ceph_args = os.environ.get('CEPH_ARGS')
553     if ceph_args:
554         if "injectargs" in sys.argv:
555             i = sys.argv.index("injectargs")
556             sys.argv = sys.argv[:i] + ceph_args.split() + sys.argv[i:]
557         else:
558             sys.argv.extend(ceph_args.split())
559     parser, parsed_args, childargs = parse_cmdargs()
560
561     if parsed_args.version:
562         print 'ceph version {0} ({1})'.format(CEPH_GIT_NICE_VER, CEPH_GIT_VER)
563         return 0
564
565     global verbose
566     verbose = parsed_args.verbose
567
568     if verbose:
569         print >> sys.stderr, "parsed_args: {0}, childargs: {1}".format(parsed_args, childargs)
570
571     if parsed_args.admin_socket_nope:
572         print >> sys.stderr, '--admin-socket is used by daemons; '\
573         'you probably mean --admin-daemon/daemon'
574         return 1
575
576     # pass on --id, --name, --conf
577     name = 'client.admin'
578     if parsed_args.client_id:
579         name = 'client.' + parsed_args.client_id
580     if parsed_args.client_name:
581         name = parsed_args.client_name
582
583     # default '' means default conf search
584     conffile = ''
585     if parsed_args.cephconf:
586         conffile = parsed_args.cephconf
587     # For now, --admin-daemon is handled as usual.  Try it
588     # first in case we can't connect() to the cluster
589
590     format = parsed_args.output_format
591
592     sockpath = None
593     if parsed_args.admin_socket:
594         sockpath = parsed_args.admin_socket
595     elif len(childargs) > 0 and childargs[0] == "daemon":
596         # Treat "daemon <path>" or "daemon <name>" like --admin_daemon <path>
597         if len(childargs) > 2:
598             if childargs[1].find('/') >= 0:
599                 sockpath = childargs[1]
600             else:
601                 # try resolve daemon name
602                 try:
603                     sockpath = ceph_conf(parsed_args, 'admin_socket',
604                                          childargs[1])
605                 except Exception as e:
606                     print >> sys.stderr, \
607                         'Can\'t get admin socket path: ' + str(e)
608                     return errno.EINVAL
609             # for both:
610             childargs = childargs[2:]
611         else:
612             print >> sys.stderr, 'daemon requires at least 3 arguments'
613             return errno.EINVAL
614
615     if sockpath:
616         try:
617             print admin_socket(sockpath, childargs, format)
618         except Exception as e:
619             print >> sys.stderr, 'admin_socket: {0}'.format(e)
620             return errno.EINVAL
621         return 0
622
623     timeout = None
624     if parsed_args.cluster_timeout:
625         timeout = parsed_args.cluster_timeout
626
627     # basic help
628     if parsed_args.help:
629         do_basic_help(parser, childargs)
630
631     # handle any 'generic' ceph arguments that we didn't parse here
632     global cluster_handle
633
634     # rados.Rados() will call rados_create2, and then read the conf file,
635     # and then set the keys from the dict.  So we must do these
636     # "pre-file defaults" first (see common_preinit in librados)
637     conf_defaults = {
638         'log_to_stderr':'true',
639         'err_to_stderr':'true',
640         'log_flush_on_exit':'true',
641     }
642
643     if 'injectargs' in childargs:
644         position = childargs.index('injectargs')
645         injectargs = childargs[position:]
646         childargs = childargs[:position]
647         if verbose:
648             print >> sys.stderr, 'Separate childargs {0} from injectargs {1}'.\
649                 format(childargs, injectargs)
650     else:
651         injectargs = None
652
653     clustername = 'ceph'
654     if parsed_args.cluster:
655         clustername = parsed_args.cluster
656
657     try:
658         cluster_handle = rados.Rados(name=name, clustername=clustername,
659                                      conf_defaults=conf_defaults,
660                                      conffile=conffile)
661         retargs = cluster_handle.conf_parse_argv(childargs)
662     except rados.Error as e:
663         print >> sys.stderr, 'Error initializing cluster client: {0}'.\
664             format(repr(e))
665         return 1
666
667     childargs = retargs
668     if not childargs:
669         childargs = []
670
671     # -- means "stop parsing args", but we don't want to see it either
672     if '--' in childargs:
673         childargs.remove('--')
674     if injectargs and '--' in injectargs:
675         injectargs.remove('--')
676
677     # special deprecation warning for 'ceph <type> tell'
678     # someday 'mds' will be here too
679     if len(childargs) >= 2 and \
680         childargs[0] in ['mon', 'osd'] and \
681         childargs[1] == 'tell':
682         print >> sys.stderr, '"{0} tell" is deprecated; try "tell {0}.<id> <command> [options...]" instead (id can be "*") '.format(childargs[0])
683         return 1
684
685     if parsed_args.help:
686         # short default timeout for -h
687         if not timeout:
688             timeout = 5
689
690         hdr('Monitor commands:')
691         print '[Contacting monitor, timeout after %d seconds]' % timeout
692
693     if childargs and childargs[0] == 'ping':
694         if len(childargs) < 2:
695             print >> sys.stderr, '"ping" requires a monitor name as argument: "ping mon.<id>"'
696             return 1
697
698     try:
699         if childargs and childargs[0] == 'ping':
700             return ping_monitor(cluster_handle, childargs[1])
701         cluster_handle.connect(timeout=timeout)
702     except KeyboardInterrupt:
703         print >> sys.stderr, 'Cluster connection aborted'
704         return 1
705     except Exception as e:
706         print >> sys.stderr, 'Error connecting to cluster: {0}'.\
707             format(e.__class__.__name__)
708         return 1
709
710     if parsed_args.help:
711         return do_extended_help(parser, childargs)
712
713     # implement -w/--watch_*
714     # This is ugly, but Namespace() isn't quite rich enough.
715     level = ''
716     for k, v in parsed_args._get_kwargs():
717         if k.startswith('watch') and v:
718             if k == 'watch':
719                 level = 'info'
720             else:
721                 level = k.replace('watch_', '')
722     if level:
723
724         # an awfully simple callback
725         def watch_cb(arg, line, who, stamp_sec, stamp_nsec, seq, level, msg):
726             print line
727             sys.stdout.flush()
728
729         # first do a ceph status
730         ret, outbuf, outs = json_command(cluster_handle, prefix='status')
731         if ret == -errno.EINVAL:
732             # try old mon
733             ret, outbuf, outs = send_command(cluster_handle, cmd=['status'])
734             # old mon returns status to outs...ick
735             if ret == 0:
736                 outbuf += outs
737         if ret:
738             print >> sys.stderr, "status query failed: ", outs
739             return ret
740         print outbuf
741
742         # this instance keeps the watch connection alive, but is
743         # otherwise unused
744         logwatch = rados.MonitorLog(cluster_handle, level, watch_cb, 0)
745
746         # loop forever letting watch_cb print lines
747         try:
748             signal.pause()
749         except KeyboardInterrupt:
750             # or until ^C, at least
751             return 0
752
753     # read input file, if any
754     inbuf = ''
755     if parsed_args.input_file:
756         try:
757             with open(parsed_args.input_file, 'r') as f:
758                 inbuf = f.read()
759         except Exception as e:
760             print >> sys.stderr, 'Can\'t open input file {0}: {1}'.format(parsed_args.input_file, e)
761             return 1
762
763     # prepare output file, if any
764     if parsed_args.output_file:
765         try:
766             outf = open(parsed_args.output_file, 'w')
767         except Exception as e:
768             print >> sys.stderr, \
769                 'Can\'t open output file {0}: {1}'.\
770                 format(parsed_args.output_file, e)
771             return 1
772
773     # -s behaves like a command (ceph status).
774     if parsed_args.status:
775         childargs.insert(0, 'status')
776
777     try:
778         target = find_cmd_target(childargs)
779     except Exception as e:
780         print >> sys.stderr, \
781                 'error handling command target: {0}'.format(e)
782         return 1
783
784     # Repulsive hack to handle tell: lop off 'tell' and target
785     # and validate the rest of the command.  'target' is already
786     # determined in our callers, so it's ok to remove it here.
787     is_tell = False
788     if len(childargs) and childargs[0] == 'tell':
789         childargs = childargs[2:]
790         is_tell = True
791
792     if is_tell:
793         if injectargs:
794             childargs = injectargs
795         if not len(childargs):
796             print >> sys.stderr, \
797                 'Cannot use \'tell\' with interactive mode.', \
798                 'For an interactive shell,', \
799                 'please start "{0}" without non-option arguments.'.format(sys.argv[0])
800             return errno.EINVAL
801
802     # fetch JSON sigs from command
803     # each line contains one command signature (a placeholder name
804     # of the form 'cmdNNN' followed by an array of argument descriptors)
805     # as part of the validated argument JSON object
806
807     targets = [target]
808
809     if target[1] == '*':
810         if target[0] == 'osd':
811             targets = [(target[0], o) for o in osdids()]
812         elif target[0] == 'mon':
813             targets = [(target[0], m) for m in monids()]
814
815     final_ret = 0
816     for target in targets:
817         # prettify?  prefix output with target, if there was a wildcard used
818         prefix = ''
819         suffix = ''
820         if not parsed_args.output_file and len(targets) > 1:
821             prefix = '{0}.{1}: '.format(*target)
822             suffix = '\n'
823
824         ret, outbuf, outs = json_command(cluster_handle, target=target,
825                                          prefix='get_command_descriptions')
826         compat = False
827         if ret == -errno.EINVAL:
828             # send command to old monitor or OSD
829             if verbose:
830                 print prefix + '{0} to old {1}'.format(' '.join(childargs), target[0])
831             compat = True
832             if parsed_args.output_format:
833                 childargs.extend(['--format', parsed_args.output_format])
834             ret, outbuf, outs = send_command(cluster_handle, target, childargs,
835                                              inbuf)
836
837             if ret == -errno.EINVAL:
838                 # did we race with a mon upgrade?  try again!
839                 ret, outbuf, outs = json_command(cluster_handle, target=target,
840                                                  prefix='get_command_descriptions')
841                 if ret == 0:
842                     compat = False  # yep, carry on
843         if not compat:
844             if ret:
845                 if ret < 0:
846                     outs = 'problem getting command descriptions from {0}.{1}'.format(*target)
847             else:
848                 sigdict = parse_json_funcsigs(outbuf, 'cli')
849
850                 if parsed_args.completion:
851                     return complete(sigdict, childargs, target)
852
853                 ret, outbuf, outs = new_style_command(parsed_args, childargs, target,
854                                                       sigdict, inbuf, verbose)
855
856                 # debug tool: send any successful command *again* to
857                 # verify that it is idempotent.
858                 if not ret and 'CEPH_CLI_TEST_DUP_COMMAND' in os.environ:
859                     ret, outbuf, outs = new_style_command(parsed_args, childargs, target,
860                                                           sigdict, inbuf, verbose)
861                     if ret < 0:
862                         ret = -ret
863                         print >> sys.stderr, prefix + 'Second attempt of previously successful command failed with {0}: {1}'.format(errno.errorcode.get(ret, 'Unknown'), outs)
864
865         if ret < 0:
866             ret = -ret
867             print >> sys.stderr, prefix + 'Error {0}: {1}'.format(errno.errorcode.get(ret, 'Unknown'), outs)
868             if len(targets) > 1:
869                 final_ret = ret
870             else:
871                 return ret
872
873         # this assumes outs never has useful command output, only status
874         if compat:
875             if ret == 0:
876                 # old cli/mon would send status string to stdout on non-error
877                 print outs
878         else:
879             if outs:
880                 print >> sys.stderr, prefix + outs
881
882         if (parsed_args.output_file):
883             outf.write(outbuf)
884         else:
885             # hack: old code printed status line before many json outputs
886             # (osd dump, etc.) that consumers know to ignore.  Add blank line
887             # to satisfy consumers that skip the first line, but not annoy
888             # consumers that don't.
889             if parsed_args.output_format and \
890                parsed_args.output_format.startswith('json') and \
891                not compat:
892                 sys.stdout.write('\n')
893
894             # if we are prettifying things, normalize newlines.  sigh.
895             if suffix != '':
896                 outbuf = outbuf.rstrip()
897             if outbuf != '':
898                 sys.stdout.write(prefix + outbuf + suffix)
899
900         sys.stdout.flush()
901
902     if (parsed_args.output_file):
903         outf.close()
904
905     if final_ret:
906         return final_ret
907
908     return 0
909
910 if __name__ == '__main__':
911     retval = main()
912     # shutdown explicitly; Rados() does not
913     if cluster_handle:
914         cluster_handle.shutdown()
915     sys.exit(retval)