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