Merge pull request #56974 from rhcs-dashboard/fix-xmlsec-issue
[ceph.git] / src / ceph.in
1 #!@Python3_EXECUTABLE@
2 # -*- mode:python -*-
3 # vim: ts=4 sw=4 smarttab expandtab
4 #
5 # Processed in Makefile to add python #! line and version variable
6 #
7 #
8
9
10 """
11 ceph.in becomes ceph, the command-line management tool for Ceph clusters.
12 This is a replacement for tools/ceph.cc and tools/common.cc.
13
14 Copyright (C) 2013 Inktank Storage, Inc.
15
16 This is free software; you can redistribute it and/or
17 modify it under the terms of the GNU General Public
18 License version 2, as published by the Free Software
19 Foundation.  See file COPYING.
20 """
21
22 from time import sleep
23 import codecs
24 import grp
25 import os
26 import pwd
27 import shutil
28 import sys
29 import time
30 import platform
31
32 try:
33     input = raw_input
34 except NameError:
35     pass
36
37 CEPH_GIT_VER = "@CEPH_GIT_VER@"
38 CEPH_GIT_NICE_VER = "@CEPH_GIT_NICE_VER@"
39 CEPH_RELEASE = "@CEPH_RELEASE@"
40 CEPH_RELEASE_NAME = "@CEPH_RELEASE_NAME@"
41 CEPH_RELEASE_TYPE = "@CEPH_RELEASE_TYPE@"
42
43 # priorities from src/common/perf_counters.h
44 PRIO_CRITICAL = 10
45 PRIO_INTERESTING = 8
46 PRIO_USEFUL = 5
47 PRIO_UNINTERESTING = 2
48 PRIO_DEBUGONLY = 0
49
50 PRIO_DEFAULT = PRIO_INTERESTING
51
52 # Make life easier on developers:
53 # If our parent dir contains CMakeCache.txt and bin/init-ceph,
54 # assume we're running from a build dir (i.e. src/build/bin/ceph)
55 # and tweak sys.path and LD_LIBRARY_PATH to use built files.
56 # Since this involves re-execing, if CEPH_DBG is set in the environment
57 # re-exec with -mpdb.  Also, if CEPH_DEV is in the env, suppress
58 # the warning message about the DEVELOPER MODE.
59
60 MYPATH = os.path.abspath(__file__)
61 MYDIR = os.path.dirname(MYPATH)
62 MYPDIR = os.path.dirname(MYDIR)
63 DEVMODEMSG = '*** DEVELOPER MODE: setting PATH, PYTHONPATH and LD_LIBRARY_PATH ***'
64
65
66 def respawn_in_path(lib_path, pybind_path, pythonlib_path, asan_lib_path):
67     execv_cmd = []
68     if 'CEPH_DBG' in os.environ:
69         execv_cmd += ['@Python3_EXECUTABLE@', '-mpdb']
70
71     if platform.system() == "Darwin":
72         lib_path_var = "DYLD_LIBRARY_PATH"
73     else:
74         lib_path_var = "LD_LIBRARY_PATH"
75
76     execv_cmd += sys.argv
77     if asan_lib_path:
78         os.environ['LD_PRELOAD'] = asan_lib_path
79     if lib_path_var in os.environ:
80         if lib_path not in os.environ[lib_path_var]:
81             os.environ[lib_path_var] += ':' + lib_path
82             if "CEPH_DEV" not in os.environ:
83                 print(DEVMODEMSG, file=sys.stderr)
84             os.execvp(execv_cmd[0], execv_cmd)
85     else:
86         os.environ[lib_path_var] = lib_path
87         if "CEPH_DEV" not in os.environ:
88             print(DEVMODEMSG, file=sys.stderr)
89         os.execvp(execv_cmd[0], execv_cmd)
90     sys.path.insert(0, pybind_path)
91     sys.path.insert(0, pythonlib_path)
92
93
94 def get_pythonlib_dir():
95     """Returns the name of a distutils build directory"""
96     return "lib.{version[0]}".format(version=sys.version_info)
97
98
99 def get_cmake_variables(*names):
100     vars = dict((name, None) for name in names)
101     for line in open(os.path.join(MYPDIR, "CMakeCache.txt")):
102         # parse lines like "WITH_ASAN:BOOL=ON"
103         for name in names:
104             if line.startswith("{}:".format(name)):
105                 type_value = line.split(":")[1].strip()
106                 t, v = type_value.split("=")
107                 if t == 'BOOL':
108                     v = v.upper() in ('TRUE', '1', 'Y', 'YES', 'ON')
109                 vars[name] = v
110                 break
111         if all(vars.values()):
112             break
113     return [vars[name] for name in names]
114
115
116 if os.path.exists(os.path.join(MYPDIR, "CMakeCache.txt")) \
117      and os.path.exists(os.path.join(MYPDIR, "bin/init-ceph")):
118     src_path, with_asan, asan_lib_path = \
119         get_cmake_variables("ceph_SOURCE_DIR", "WITH_ASAN", "ASAN_LIBRARY")
120     if src_path is None:
121         # Huh, maybe we're not really in a cmake environment?
122         pass
123     else:
124         # Developer mode, but in a cmake build dir instead of the src dir
125         lib_path = os.path.join(MYPDIR, "lib")
126         bin_path = os.path.join(MYPDIR, "bin")
127         pybind_path = os.path.join(src_path, "src", "pybind")
128         pythonlib_path = os.path.join(lib_path,
129                                       "cython_modules",
130                                       get_pythonlib_dir())
131         respawn_in_path(lib_path, pybind_path, pythonlib_path,
132                         asan_lib_path if with_asan else None)
133
134         if 'PATH' in os.environ and bin_path not in os.environ['PATH']:
135             os.environ['PATH'] = os.pathsep.join([bin_path, os.environ['PATH']])
136
137 import argparse
138 import errno
139 import json
140 import rados
141 import shlex
142 import signal
143 import string
144 import subprocess
145
146 from ceph_argparse import \
147     concise_sig, descsort_key, parse_json_funcsigs, \
148     validate_command, find_cmd_target, \
149     json_command, run_in_thread, Flag
150
151 from ceph_daemon import admin_socket, DaemonWatcher, Termsize
152
153 # just a couple of globals
154
155 verbose = False
156 cluster_handle = None
157
158
159 def raw_write(buf):
160     sys.stdout.flush()
161     sys.stdout.buffer.write(rados.cstr(buf, ''))
162
163
164 def osdids():
165     ret, outbuf, outs = json_command(cluster_handle, prefix='osd ls')
166     if ret:
167         raise RuntimeError('Can\'t contact mon for osd list')
168     return [line.decode('utf-8') for line in outbuf.split(b'\n') if line]
169
170
171 def monids():
172     ret, outbuf, outs = json_command(cluster_handle, prefix='mon dump',
173                                      argdict={'format': 'json'})
174     if ret:
175         raise RuntimeError('Can\'t contact mon for mon list')
176     d = json.loads(outbuf.decode('utf-8'))
177     return [m['name'] for m in d['mons']]
178
179
180 def mdsids():
181     ret, outbuf, outs = json_command(cluster_handle, prefix='fs dump',
182                                      argdict={'format': 'json'})
183     if ret:
184         raise RuntimeError('Can\'t contact mon for mds list')
185     d = json.loads(outbuf.decode('utf-8'))
186     l = []
187     for info in d['standbys']:
188         l.append(info['name'])
189     for fs in d['filesystems']:
190         for info in fs['mdsmap']['info'].values():
191             l.append(info['name'])
192     return l
193
194
195 def mgrids():
196     ret, outbuf, outs = json_command(cluster_handle, prefix='mgr dump',
197                                      argdict={'format': 'json'})
198     if ret:
199         raise RuntimeError('Can\'t contact mon for mgr list')
200
201     d = json.loads(outbuf.decode('utf-8'))
202     l = []
203     l.append(d['active_name'])
204     # we can only send tell commands to the active mgr
205     #for i in d['standbys']:
206     #    l.append(i['name'])
207     return l
208
209
210 def ids_by_service(service):
211     ids = {"mon": monids,
212            "osd": osdids,
213            "mds": mdsids,
214            "mgr": mgrids}
215     return ids[service]()
216
217
218 def validate_target(target):
219     """
220       this function will return true iff target is a correct
221       target, such as mon.a/osd.2/mds.a/mgr.
222
223       target: array, likes ['osd', '2']
224       return: bool, or raise RuntimeError
225     """
226
227     if len(target) == 2:
228         # for case "service.id"
229         service_name, service_id = target[0], target[1]
230         try:
231             exist_ids = ids_by_service(service_name)
232         except KeyError:
233             print('WARN: {0} is not a legal service name, should be one of mon/osd/mds/mgr'.format(service_name),
234                   file=sys.stderr)
235             return False
236
237         if service_id in exist_ids or len(exist_ids) > 0 and service_id == '*':
238             return True
239         else:
240             print('WARN: the service id you provided does not exist. service id should '
241                   'be one of {0}.'.format('/'.join(exist_ids)), file=sys.stderr)
242             return False
243
244     elif len(target) == 1 and target[0] in ['mgr', 'mon']:
245         return True
246     else:
247         print('WARN: \"{0}\" is not a legal target. it should be one of mon.<id>/osd.<int>/mds.<id>/mgr'.format('.'.join(target)), file=sys.stderr)
248         return False
249
250
251 # these args must be passed to all child programs
252 GLOBAL_ARGS = {
253     'client_id': '--id',
254     'client_name': '--name',
255     'cluster': '--cluster',
256     'cephconf': '--conf',
257 }
258
259
260 def parse_cmdargs(args=None, target=''):
261     """
262     Consume generic arguments from the start of the ``args``
263     list.  Call this first to handle arguments that are not
264     handled by a command description provided by the server.
265
266     :returns: three tuple of ArgumentParser instance, Namespace instance
267               containing parsed values, and list of un-handled arguments
268     """
269     # alias: let the line-wrapping be sane
270     AP = argparse.ArgumentParser
271
272     # format our own help
273     parser = AP(description='Ceph administration tool', add_help=False)
274
275     parser.add_argument('--completion', action='store_true',
276                         help=argparse.SUPPRESS)
277
278     parser.add_argument('-h', '--help', help='request mon help',
279                         action='store_true')
280
281     parser.add_argument('-c', '--conf', dest='cephconf',
282                         help='ceph configuration file')
283     parser.add_argument('-i', '--in-file', dest='input_file',
284                         help='input file, or "-" for stdin')
285     parser.add_argument('-o', '--out-file', dest='output_file',
286                         help='output file, or "-" for stdout')
287     parser.add_argument('--setuser', dest='setuser',
288                         help='set user file permission')
289     parser.add_argument('--setgroup', dest='setgroup',
290                         help='set group file permission')
291     parser.add_argument('--id', '--user', dest='client_id',
292                         help='client id for authentication')
293     parser.add_argument('--name', '-n', dest='client_name',
294                         help='client name for authentication')
295     parser.add_argument('--cluster', help='cluster name')
296
297     parser.add_argument('--admin-daemon', dest='admin_socket',
298                         help='submit admin-socket commands (\"help\" for help)')
299
300     parser.add_argument('-s', '--status', action='store_true',
301                         help='show cluster status')
302
303     parser.add_argument('-w', '--watch', action='store_true',
304                         help='watch live cluster changes')
305     parser.add_argument('--watch-debug', action='store_true',
306                         help='watch debug events')
307     parser.add_argument('--watch-info', action='store_true',
308                         help='watch info events')
309     parser.add_argument('--watch-sec', action='store_true',
310                         help='watch security events')
311     parser.add_argument('--watch-warn', action='store_true',
312                         help='watch warn events')
313     parser.add_argument('--watch-error', action='store_true',
314                         help='watch error events')
315
316     parser.add_argument('-W', '--watch-channel', dest="watch_channel",
317                         help="watch live cluster changes on a specific channel "
318                         "(e.g., cluster, audit, cephadm, or '*' for all)")
319
320     parser.add_argument('--version', '-v', action="store_true", help="display version")
321     parser.add_argument('--verbose', action="store_true", help="make verbose")
322     parser.add_argument('--concise', dest='verbose', action="store_false",
323                         help="make less verbose")
324
325     parser.add_argument('-f', '--format', choices=['json', 'json-pretty',
326                         'xml', 'xml-pretty', 'plain', 'yaml'], dest='output_format')
327
328     parser.add_argument('--connect-timeout', dest='cluster_timeout',
329                         type=int,
330                         help='set a timeout for connecting to the cluster')
331
332     parser.add_argument('--block', action='store_true',
333                         help='block until completion (scrub and deep-scrub only)')
334     parser.add_argument('--period', '-p', default=1, type=float,
335                         help='polling period, default 1.0 second (for ' \
336                         'polling commands only)')
337
338     # returns a Namespace with the parsed args, and a list of all extras
339     parsed_args, extras = parser.parse_known_args(args)
340
341     return parser, parsed_args, extras
342
343
344 def hdr(s):
345     print('\n', s, '\n', '=' * len(s))
346
347
348 def do_basic_help(parser, args):
349     """
350     Print basic parser help
351     If the cluster is available, get and print monitor help
352     """
353     hdr('General usage:')
354     parser.print_help()
355     print_locally_handled_command_help()
356
357
358 def print_locally_handled_command_help():
359     hdr("Local commands:")
360     print("""
361 ping <mon.id>           Send simple presence/life test to a mon
362                         <mon.id> may be 'mon.*' for all mons
363 daemon {type.id|path} <cmd>
364                         Same as --admin-daemon, but auto-find admin socket
365 daemonperf {type.id | path} [stat-pats] [priority] [<interval>] [<count>]
366 daemonperf {type.id | path} list|ls [stat-pats] [priority]
367                         Get selected perf stats from daemon/admin socket
368                         Optional shell-glob comma-delim match string stat-pats
369                         Optional selection priority (can abbreviate name):
370                          critical, interesting, useful, noninteresting, debug
371                         List shows a table of all available stats
372                         Run <count> times (default forever),
373                          once per <interval> seconds (default 1)
374     """, file=sys.stdout)
375
376
377 def do_extended_help(parser, args, target, partial):
378     def help_for_sigs(sigs, partial=None):
379         try:
380             sys.stdout.write(format_help(parse_json_funcsigs(sigs, 'cli'),
381                              partial=partial))
382         except BrokenPipeError:
383             pass
384
385     def help_for_target(target, partial=None):
386         # wait for osdmap because we know this is sent after the mgrmap
387         # and monmap (it's alphabetical).
388         cluster_handle.wait_for_latest_osdmap()
389         ret, outbuf, outs = json_command(cluster_handle, target=target,
390                                          prefix='get_command_descriptions',
391                                          timeout=10)
392         if ret:
393             if (ret == -errno.EPERM or ret == -errno.EACCES) and target[0] in ('osd', 'mds'):
394                 print("Permission denied.  Check that your user has 'allow *' "
395                       "capabilities for the target daemon type.", file=sys.stderr)
396             elif ret == -errno.EPERM:
397                 print("Permission denied.  Check your user has proper "
398                       "capabilities configured", file=sys.stderr)
399             else:
400                 print("couldn't get command descriptions for {0}: {1} ({2})".
401                       format(target, outs, ret), file=sys.stderr)
402             return ret
403         else:
404             return help_for_sigs(outbuf.decode('utf-8'), partial)
405
406     assert(cluster_handle.state == "connected")
407     return help_for_target(target, partial)
408
409 DONTSPLIT = string.ascii_letters + '{[<>]}'
410
411
412 def wrap(s, width, indent):
413     """
414     generator to transform s into a sequence of strings width or shorter,
415     for wrapping text to a specific column width.
416     Attempt to break on anything but DONTSPLIT characters.
417     indent is amount to indent 2nd-through-nth lines.
418
419     so "long string long string long string" width=11 indent=1 becomes
420     'long string', ' long string', ' long string' so that it can be printed
421     as
422     long string
423      long string
424      long string
425
426     Consumes s.
427     """
428     result = ''
429     leader = ''
430     while len(s):
431
432         if len(s) <= width:
433             # no splitting; just possibly indent
434             result = leader + s
435             s = ''
436             yield result
437
438         else:
439             splitpos = width
440             while (splitpos > 0) and (s[splitpos-1] in DONTSPLIT):
441                 splitpos -= 1
442
443             if splitpos == 0:
444                 splitpos = width
445
446             if result:
447                 # prior result means we're mid-iteration, indent
448                 result = leader
449             else:
450                 # first time, set leader and width for next
451                 leader = ' ' * indent
452                 width -= 1      # for subsequent space additions
453
454             # remove any leading spaces in this chunk of s
455             result += s[:splitpos].lstrip()
456             s = s[splitpos:]
457
458             yield result
459
460
461 def format_help(cmddict, partial=None):
462     """
463     Formats all the cmdsigs and helptexts from cmddict into a sorted-by-
464     cmdsig 2-column display, with each column wrapped and indented to
465     fit into (terminal_width / 2) characters.
466     """
467
468     fullusage = ''
469     for cmd in sorted(cmddict.values(), key=descsort_key):
470
471         if not cmd['help']:
472             continue
473         flags = cmd.get('flags', 0)
474         if flags & (Flag.OBSOLETE | Flag.DEPRECATED | Flag.HIDDEN):
475             continue
476         concise = concise_sig(cmd['sig'])
477         if partial and not concise.startswith(partial):
478             continue
479         width = Termsize().cols - 1  # 1 for the line between sig and help
480         sig_width = int(width / 2)
481         # make sure width == sig_width + help_width, even (width % 2 > 0)
482         help_width = int(width / 2) + (width % 2)
483         siglines = [l for l in wrap(concise, sig_width, 1)]
484         helplines = [l for l in wrap(cmd['help'], help_width, 1)]
485
486         # make lists the same length
487         maxlen = max(len(siglines), len(helplines))
488         siglines.extend([''] * (maxlen - len(siglines)))
489         helplines.extend([''] * (maxlen - len(helplines)))
490
491         # so we can zip them for output
492         for s, h in zip(siglines, helplines):
493             fullusage += '{s:{w}s} {h}\n'.format(s=s, h=h, w=sig_width)
494
495     return fullusage
496
497
498 def ceph_conf(parsed_args, field, name):
499     cmd = 'ceph-conf'
500     bindir = os.path.dirname(__file__)
501     if shutil.which(cmd):
502         args = [cmd]
503     elif shutil.which(cmd, path=bindir):
504         args = [os.path.join(bindir, cmd)]
505     else:
506         raise RuntimeError('"ceph-conf" not found')
507
508     if name:
509         args.extend(['--name', name])
510
511     # add any args in GLOBAL_ARGS
512     for key, val in GLOBAL_ARGS.items():
513         # ignore name in favor of argument name, if any
514         if name and key == 'client_name':
515             continue
516         if getattr(parsed_args, key):
517             args.extend([val, getattr(parsed_args, key)])
518
519     args.extend(['--show-config-value', field])
520     p = subprocess.Popen(
521         args,
522         stdout=subprocess.PIPE,
523         stderr=subprocess.PIPE)
524     outdata, errdata = p.communicate()
525     if p.returncode != 0:
526         raise RuntimeError('unable to get conf option %s for %s: %s' % (field, name, errdata))
527     return outdata.rstrip()
528
529 PROMPT = 'ceph> '
530
531 if sys.stdin.isatty():
532     def read_input():
533         while True:
534             line = input(PROMPT).rstrip()
535             if line in ['q', 'quit', 'Q', 'exit']:
536                 return None
537             if line:
538                 return line
539 else:
540     def read_input():
541         while True:
542             line = sys.stdin.readline()
543             if not line:
544                 return None
545             line = line.rstrip()
546             if line:
547                 return line
548
549
550 def do_command(parsed_args, target, cmdargs, sigdict, inbuf, verbose):
551     ''' Validate a command, and handle the polling flag '''
552
553     valid_dict = validate_command(sigdict, cmdargs, verbose)
554     # Validate input args against list of sigs
555     if valid_dict:
556         if parsed_args.output_format:
557             valid_dict['format'] = parsed_args.output_format
558         if verbose:
559             print("Submitting command: ", valid_dict, file=sys.stderr)
560     else:
561         return -errno.EINVAL, '', 'invalid command'
562
563     next_header_print = 0
564     # Set extra options for polling commands only:
565     if valid_dict.get('poll', False):
566         valid_dict['width'] = Termsize().cols
567     while True:
568         try:
569             # Only print the header for polling commands
570             if next_header_print == 0 and valid_dict.get('poll', False):
571                 valid_dict['print_header'] = True
572                 next_header_print = Termsize().rows - 3
573             next_header_print -= 1
574             ret, outbuf, outs = json_command(cluster_handle, target=target,
575                 argdict=valid_dict, inbuf=inbuf, verbose=verbose)
576             if valid_dict.get('poll', False):
577                 valid_dict['print_header'] = False
578             if not valid_dict.get('poll', False):
579                 # Don't print here if it's not a polling command
580                 break
581             if ret:
582                 ret = abs(ret)
583                 print('Error: {0} {1}'.format(ret, errno.errorcode.get(ret, 'Unknown')),
584                         file=sys.stderr)
585                 break
586             if outbuf:
587                 print(outbuf.decode('utf-8'))
588             if outs:
589                 print(outs, file=sys.stderr)
590             if parsed_args.period <= 0:
591                 break
592             sleep(parsed_args.period)
593         except KeyboardInterrupt:
594             print('Interrupted')
595             return errno.EINTR, '', ''
596     if ret == errno.ETIMEDOUT:
597         ret = -ret
598         if not outs:
599             outs = ("Connection timed out. Please check the client's " +
600                     "permission and connection.")
601     return ret, outbuf, outs
602
603
604 def new_style_command(parsed_args, cmdargs, target, sigdict, inbuf, verbose):
605     """
606     Do new-style command dance.
607     target: daemon to receive command: mon (any) or osd.N
608     sigdict - the parsed output from the new monitor describing commands
609     inbuf - any -i input file data
610     verbose - bool
611     """
612     if verbose:
613         for cmdtag in sorted(sigdict.keys()):
614             cmd = sigdict[cmdtag]
615             sig = cmd['sig']
616             print('{0}: {1}'.format(cmdtag, concise_sig(sig)))
617
618     if True:
619         if cmdargs:
620             # Non interactive mode
621             ret, outbuf, outs = do_command(parsed_args, target, cmdargs, sigdict, inbuf, verbose)
622         else:
623             # Interactive mode (ceph cli)
624             if sys.stdin.isatty():
625                 # do the command-interpreter looping
626                 # for input to do readline cmd editing
627                 import readline  # noqa
628
629             while True:
630                 try:
631                     interactive_input = read_input()
632                 except EOFError:
633                     # leave user an uncluttered prompt
634                     return 0, '\n', ''
635                 if interactive_input is None:
636                     return 0, '', ''
637                 cmdargs = parse_cmdargs(shlex.split(interactive_input))[2]
638                 try:
639                     target = find_cmd_target(cmdargs)
640                 except Exception as e:
641                     print('error handling command target: {0}'.format(e),
642                           file=sys.stderr)
643                     continue
644                 if len(cmdargs) and cmdargs[0] == 'tell':
645                     print('Can not use \'tell\' in interactive mode.',
646                           file=sys.stderr)
647                     continue
648                 ret, outbuf, outs = do_command(parsed_args, target, cmdargs,
649                                                sigdict, inbuf, verbose)
650                 if ret < 0:
651                     ret = -ret
652                     errstr = errno.errorcode.get(ret, 'Unknown')
653                     print('Error {0}: {1}'.format(errstr, outs), file=sys.stderr)
654                 else:
655                     if outs:
656                         print(outs, file=sys.stderr)
657                     if outbuf:
658                         print(outbuf.decode('utf-8'))
659
660     return ret, outbuf, outs
661
662
663 def complete(sigdict, args, target):
664     """
665     Command completion.  Match as much of [args] as possible,
666     and print every possible match separated by newlines.
667     Return exitcode.
668     """
669     # XXX this looks a lot like the front of validate_command().  Refactor?
670
671     # Repulsive hack to handle tell: lop off 'tell' and target
672     # and validate the rest of the command.  'target' is already
673     # determined in our callers, so it's ok to remove it here.
674     if len(args) and args[0] == 'tell':
675         args = args[2:]
676     # look for best match, accumulate possibles in bestcmds
677     # (so we can maybe give a more-useful error message)
678
679     match_count = 0
680     comps = []
681     for cmdtag, cmd in sigdict.items():
682         flags = cmd.get('flags', 0)
683         if flags & (Flag.OBSOLETE | Flag.HIDDEN):
684             continue
685         sig = cmd['sig']
686         j = 0
687         # iterate over all arguments, except last one
688         for arg in args[0:-1]:
689             if j > len(sig)-1:
690                 # an out of argument definitions
691                 break
692             found_match = arg in sig[j].complete(arg)
693             if not found_match and sig[j].req:
694                 # no elements that match
695                 break
696             if not sig[j].N:
697                 j += 1
698         else:
699             # successfully matched all - except last one - arguments
700             if j < len(sig) and len(args) > 0:
701                 comps += sig[j].complete(args[-1])
702
703             match_count += 1
704             match_cmd = cmd
705
706     if match_count == 1 and len(comps) == 0:
707         # only one command matched and no hints yet => add help
708         comps = comps + [' ', '#'+match_cmd['help']]
709     print('\n'.join(sorted(set(comps))))
710     return 0
711
712
713 def ping_monitor(cluster_handle, name, timeout):
714     if 'mon.' not in name:
715         print('"ping" expects a monitor to ping; try "ping mon.<id>"', file=sys.stderr)
716         return 1
717
718     mon_id = name[len('mon.'):]
719     if mon_id == '*':
720         run_in_thread(cluster_handle.connect, timeout=timeout)
721         for m in monids():
722             s = run_in_thread(cluster_handle.ping_monitor, m)
723             if s is None:
724                 print("mon.{0}".format(m) + '\n' + "Error connecting to monitor.")
725             else:
726                 print("mon.{0}".format(m) + '\n' + s)
727     else:
728             s = run_in_thread(cluster_handle.ping_monitor, mon_id)
729             print(s)
730     return 0
731
732
733 def maybe_daemon_command(parsed_args, childargs):
734     """
735     Check if --admin-socket, daemon, or daemonperf command
736     if it is, returns (boolean handled, return code if handled == True)
737     """
738
739     daemon_perf = False
740     sockpath = None
741     if parsed_args.admin_socket:
742         sockpath = parsed_args.admin_socket
743     elif len(childargs) > 0 and childargs[0] in ["daemon", "daemonperf"]:
744         daemon_perf = (childargs[0] == "daemonperf")
745         # Treat "daemon <path>" or "daemon <name>" like --admin_daemon <path>
746         # Handle "daemonperf <path>" the same but requires no trailing args
747         require_args = 2 if daemon_perf else 3
748         if len(childargs) >= require_args:
749             if childargs[1].find('/') >= 0:
750                 sockpath = childargs[1]
751             else:
752                 # try resolve daemon name
753                 try:
754                     sockpath = ceph_conf(parsed_args, 'admin_socket',
755                                          childargs[1])
756                 except Exception as e:
757                     print('Can\'t get admin socket path: ' + str(e), file=sys.stderr)
758                     return True, errno.EINVAL
759             # for both:
760             childargs = childargs[2:]
761         else:
762             print('{0} requires at least {1} arguments'.format(childargs[0], require_args),
763                   file=sys.stderr)
764             return True, errno.EINVAL
765
766     if sockpath and daemon_perf:
767         return True, daemonperf(childargs, sockpath)
768     elif sockpath:
769         try:
770             raw_write(admin_socket(sockpath, childargs, parsed_args.output_format))
771         except Exception as e:
772             print('admin_socket: {0}'.format(e), file=sys.stderr)
773             return True, errno.EINVAL
774         return True, 0
775
776     return False, 0
777
778
779 def isnum(s):
780     try:
781         float(s)
782         return True
783     except ValueError:
784         return False
785
786
787 def daemonperf(childargs, sockpath):
788     """
789     Handle daemonperf command; returns errno or 0
790
791     daemonperf <daemon> [priority string] [statpats] [interval] [count]
792     daemonperf <daemon> list|ls [statpats]
793     """
794
795     interval = 1
796     count = None
797     statpats = None
798     priority = None
799     do_list = False
800
801     def prio_from_name(arg):
802
803         PRIOMAP = {
804             'critical': PRIO_CRITICAL,
805             'interesting': PRIO_INTERESTING,
806             'useful': PRIO_USEFUL,
807             'uninteresting': PRIO_UNINTERESTING,
808             'debugonly': PRIO_DEBUGONLY,
809         }
810
811         if arg in PRIOMAP:
812             return PRIOMAP[arg]
813         # allow abbreviation
814         for name, val in PRIOMAP.items():
815             if name.startswith(arg):
816                 return val
817         return None
818
819     # consume and analyze non-numeric args
820     while len(childargs) and not isnum(childargs[0]):
821         arg = childargs.pop(0)
822         # 'list'?
823         if arg in ['list', 'ls']:
824             do_list = True
825             continue
826         # prio?
827         prio = prio_from_name(arg)
828         if prio is not None:
829             priority = prio
830             continue
831         # statpats
832         statpats = arg.split(',')
833
834     if priority is None:
835         priority = PRIO_DEFAULT
836
837     if len(childargs) > 0:
838         try:
839             interval = float(childargs.pop(0))
840             if interval < 0:
841                 raise ValueError
842         except ValueError:
843             print('daemonperf: interval should be a positive number', file=sys.stderr)
844             return errno.EINVAL
845
846     if len(childargs) > 0:
847         arg = childargs.pop(0)
848         if (not isnum(arg)) or (int(arg) < 0):
849             print('daemonperf: count should be a positive integer', file=sys.stderr)
850             return errno.EINVAL
851         count = int(arg)
852
853     watcher = DaemonWatcher(sockpath, statpats, priority)
854     if do_list:
855         watcher.list()
856     else:
857         watcher.run(interval, count)
858
859     return 0
860
861 def get_scrub_timestamps(childargs):
862     last_scrub_stamp = "last_" + childargs[1].replace('-', '_') + "_stamp"
863     results = dict()
864     scruball = False
865     if childargs[2] in ['all', 'any', '*']:
866         scruball = True
867     devnull = open(os.devnull, 'w')
868     out = subprocess.check_output(['ceph', 'pg', 'dump', '--format=json-pretty'],
869                                   stderr=devnull)
870     try:
871         pgstats = json.loads(out)['pg_map']['pg_stats']
872     except KeyError:
873         pgstats = json.loads(out)['pg_stats']
874     for stat in pgstats:
875         if scruball or stat['up_primary'] == int(childargs[2]):
876             scrub_tuple = (stat['up_primary'], stat[last_scrub_stamp])
877             results[stat['pgid']] = scrub_tuple
878     return results
879
880 def check_scrub_stamps(waitdata, currdata):
881     for pg in waitdata.keys():
882        # Try to handle the case where a pg may not exist in current results
883        if pg in currdata and waitdata[pg][1] == currdata[pg][1]:
884            return False
885     return True
886
887 def waitscrub(childargs, waitdata):
888     print('Waiting for {0} to complete...'.format(childargs[1]), file=sys.stdout)
889     currdata = get_scrub_timestamps(childargs)
890     while not check_scrub_stamps(waitdata, currdata):
891         time.sleep(3)
892         currdata = get_scrub_timestamps(childargs)
893     print('{0} completed'.format(childargs[1]), file=sys.stdout)
894
895 def wait(childargs, waitdata):
896     if childargs[1] in ['scrub', 'deep-scrub']:
897         waitscrub(childargs, waitdata)
898
899
900 def main():
901     ceph_args = os.environ.get('CEPH_ARGS')
902     if ceph_args:
903         if "injectargs" in sys.argv:
904             i = sys.argv.index("injectargs")
905             sys.argv = sys.argv[:i] + ceph_args.split() + sys.argv[i:]
906         else:
907             sys.argv.extend([arg for arg in ceph_args.split()
908                              if '--admin-socket' not in arg])
909     parser, parsed_args, childargs = parse_cmdargs()
910
911     if parsed_args.version:
912         print('ceph version {0} ({1}) {2} ({3})'.format(
913             CEPH_GIT_NICE_VER,
914             CEPH_GIT_VER,
915             CEPH_RELEASE_NAME,
916             CEPH_RELEASE_TYPE))  # noqa
917         return 0
918
919     # --watch-channel|-W implies -w
920     if parsed_args.watch_channel:
921         parsed_args.watch = True
922     elif parsed_args.watch and not parsed_args.watch_channel:
923         parsed_args.watch_channel = 'cluster'
924
925     global verbose
926     verbose = parsed_args.verbose
927
928     if verbose:
929         print("parsed_args: {0}, childargs: {1}".format(parsed_args, childargs), file=sys.stderr)
930
931     # pass on --id, --name, --conf
932     name = 'client.admin'
933     if parsed_args.client_id:
934         name = 'client.' + parsed_args.client_id
935     if parsed_args.client_name:
936         name = parsed_args.client_name
937
938     # default '' means default conf search
939     conffile = ''
940     if parsed_args.cephconf:
941         conffile = parsed_args.cephconf
942     # For now, --admin-daemon is handled as usual.  Try it
943     # first in case we can't connect() to the cluster
944
945     done, ret = maybe_daemon_command(parsed_args, childargs)
946     if done:
947         return ret
948
949     timeout = None
950     if parsed_args.cluster_timeout:
951         timeout = parsed_args.cluster_timeout
952
953     # basic help
954     if parsed_args.help:
955         do_basic_help(parser, childargs)
956
957     # handle any 'generic' ceph arguments that we didn't parse here
958     global cluster_handle
959
960     # rados.Rados() will call rados_create2, and then read the conf file,
961     # and then set the keys from the dict.  So we must do these
962     # "pre-file defaults" first (see common_preinit in librados)
963     conf_defaults = {
964         'log_to_stderr': 'true',
965         'err_to_stderr': 'true',
966         'log_flush_on_exit': 'true',
967     }
968
969     if 'injectargs' in childargs:
970         position = childargs.index('injectargs')
971         injectargs = childargs[position:]
972         childargs = childargs[:position]
973         if verbose:
974             print('Separate childargs {0} from injectargs {1}'.format(childargs, injectargs),
975                   file=sys.stderr)
976     else:
977         injectargs = None
978
979     clustername = None
980     if parsed_args.cluster:
981         clustername = parsed_args.cluster
982
983     try:
984         cluster_handle = run_in_thread(rados.Rados,
985                                        name=name, clustername=clustername,
986                                        conf_defaults=conf_defaults,
987                                        conffile=conffile)
988         retargs = run_in_thread(cluster_handle.conf_parse_argv, childargs)
989     except rados.Error as e:
990         print('Error initializing cluster client: {0!r}'.format(e), file=sys.stderr)
991         return 1
992
993     childargs = retargs
994     if not childargs:
995         childargs = []
996
997     # -- means "stop parsing args", but we don't want to see it either
998     if '--' in childargs:
999         childargs.remove('--')
1000     if injectargs and '--' in injectargs:
1001         injectargs.remove('--')
1002
1003     block = False
1004     waitdata = dict()
1005     if parsed_args.block:
1006         if (len(childargs) >= 2 and
1007                 childargs[0] == 'osd' and
1008                 childargs[1] in ['deep-scrub', 'scrub']):
1009             block = True
1010             waitdata = get_scrub_timestamps(childargs)
1011
1012     if parsed_args.help:
1013         # short default timeout for -h
1014         if not timeout:
1015             timeout = 5
1016
1017     if childargs and childargs[0] == 'ping' and not parsed_args.help:
1018         if len(childargs) < 2:
1019             print('"ping" requires a monitor name as argument: "ping mon.<id>"', file=sys.stderr)
1020             return 1
1021     if parsed_args.completion:
1022         # for completion let timeout be really small
1023         timeout = 3
1024     try:
1025         if childargs and childargs[0] == 'ping' and not parsed_args.help:
1026             return ping_monitor(cluster_handle, childargs[1], timeout)
1027         result = run_in_thread(cluster_handle.connect, timeout=timeout)
1028         if type(result) is tuple and result[0] == -errno.EINTR:
1029             print('Cluster connection interrupted or timed out', file=sys.stderr)
1030             return 1
1031     except KeyboardInterrupt:
1032         print('Cluster connection aborted', file=sys.stderr)
1033         return 1
1034     except rados.PermissionDeniedError as e:
1035         print(str(e), file=sys.stderr)
1036         return errno.EACCES
1037     except Exception as e:
1038         print(str(e), file=sys.stderr)
1039         return 1
1040
1041     if parsed_args.help:
1042         target = None
1043         if len(childargs) >= 2 and childargs[0] == 'tell':
1044             target = childargs[1].split('.', 1)
1045             if not validate_target(target):
1046                 print('target {0} doesn\'t exist; please pass correct target to tell command (e.g., mon.a, osd.1, mds.a, mgr)'.format(childargs[1]), file=sys.stderr)
1047                 return 1
1048             childargs = childargs[2:]
1049             hdr('Tell %s commands:' % target[0])
1050         else:
1051             hdr('Monitor commands:')
1052             target = ('mon', '')
1053         if verbose:
1054             print('[Contacting monitor, timeout after %d seconds]' % timeout)
1055
1056         return do_extended_help(parser, childargs, target, ' '.join(childargs))
1057
1058     # implement "tell service.id help"
1059     if len(childargs) >= 3 and childargs[0] == 'tell' and childargs[2] == 'help':
1060         target = childargs[1].split('.', 1)
1061         if validate_target(target):
1062             hdr('Tell %s commands' % target[0])
1063             return do_extended_help(parser, childargs, target, None)
1064         else:
1065             print('target {0} doesn\'t exists, please pass correct target to tell command, such as mon.a/'
1066                   'osd.1/mds.a/mgr'.format(childargs[1]), file=sys.stderr)
1067             return 1
1068
1069     # implement -w/--watch_*
1070     # This is ugly, but Namespace() isn't quite rich enough.
1071     level = ''
1072     for k, v in parsed_args._get_kwargs():
1073         if k.startswith('watch') and v:
1074             if k == 'watch':
1075                 level = 'info'
1076             elif k != "watch_channel":
1077                 level = k.replace('watch_', '')
1078     if level:
1079         # an awfully simple callback
1080         def watch_cb(arg, line, channel, name, who, stamp_sec, stamp_nsec, seq, level, msg):
1081             # Filter on channel
1082             channel = channel.decode('utf-8')
1083             if parsed_args.watch_channel in (channel, '*'):
1084                 print(line.decode('utf-8'))
1085                 sys.stdout.flush()
1086
1087         # first do a ceph status
1088         ret, outbuf, outs = json_command(cluster_handle, prefix='status')
1089         if ret:
1090             print("status query failed: ", outs, file=sys.stderr)
1091             return ret
1092         print(outbuf.decode('utf-8'))
1093
1094         # this instance keeps the watch connection alive, but is
1095         # otherwise unused
1096         run_in_thread(cluster_handle.monitor_log2, level, watch_cb, 0)
1097
1098         # loop forever letting watch_cb print lines
1099         try:
1100             signal.pause()
1101         except KeyboardInterrupt:
1102             # or until ^C, at least
1103             return 0
1104
1105     # read input file, if any
1106     inbuf = b''
1107     if parsed_args.input_file:
1108         try:
1109             if parsed_args.input_file == '-':
1110                 inbuf = sys.stdin.buffer.read()
1111             else:
1112                 with open(parsed_args.input_file, 'rb') as f:
1113                     inbuf = f.read()
1114         except Exception as e:
1115             print('Can\'t open input file {0}: {1}'.format(parsed_args.input_file, e), file=sys.stderr)
1116             return 1
1117
1118     # prepare output file, if any
1119     if parsed_args.output_file:
1120         try:
1121             if parsed_args.output_file == '-':
1122                 outf = sys.stdout.buffer
1123             else:
1124                 outf = open(parsed_args.output_file, 'wb')
1125         except Exception as e:
1126             print('Can\'t open output file {0}: {1}'.format(parsed_args.output_file, e), file=sys.stderr)
1127             return 1
1128         if parsed_args.setuser:
1129             try:
1130                 ownerid = pwd.getpwnam(parsed_args.setuser).pw_uid
1131                 os.fchown(outf.fileno(), ownerid, -1)
1132             except OSError as e:
1133                 print('Failed to change user ownership of {0} to {1}: {2}'.format(outf, parsed_args.setuser, e))
1134                 return 1
1135         if parsed_args.setgroup:
1136             try:
1137                 groupid = grp.getgrnam(parsed_args.setgroup).gr_gid
1138                 os.fchown(outf.fileno(), -1, groupid)
1139             except OSError as e:
1140                 print('Failed to change group ownership of {0} to {1}: {2}'.format(outf, parsed_args.setgroup, e))
1141                 return 1
1142
1143     # -s behaves like a command (ceph status).
1144     if parsed_args.status:
1145         childargs.insert(0, 'status')
1146
1147     try:
1148         target = find_cmd_target(childargs)
1149     except Exception as e:
1150         print('error handling command target: {0}'.format(e), file=sys.stderr)
1151         return 1
1152
1153     # Repulsive hack to handle tell: lop off 'tell' and target
1154     # and validate the rest of the command.  'target' is already
1155     # determined in our callers, so it's ok to remove it here.
1156     is_tell = False
1157     if len(childargs) and childargs[0] == 'tell':
1158         childargs = childargs[2:]
1159         is_tell = True
1160
1161     if is_tell:
1162         if injectargs:
1163             childargs = injectargs
1164         if not len(childargs):
1165             print('"{0} tell" requires additional arguments.'.format(sys.argv[0]),
1166                   'Try "{0} tell <name> <command> [options...]" instead.'.format(sys.argv[0]),
1167                   file=sys.stderr)
1168             return errno.EINVAL
1169
1170     # fetch JSON sigs from command
1171     # each line contains one command signature (a placeholder name
1172     # of the form 'cmdNNN' followed by an array of argument descriptors)
1173     # as part of the validated argument JSON object
1174
1175     if target[1] == '*':
1176         service = target[0]
1177         targets = [(service, o) for o in ids_by_service(service)]
1178     else:
1179         targets = [target]
1180
1181     final_ret = 0
1182     for target in targets:
1183         # prettify?  prefix output with target, if there was a wildcard used
1184         prefix = ''
1185         suffix = ''
1186         if not parsed_args.output_file and len(targets) > 1:
1187             prefix = '{0}.{1}: '.format(*target)
1188             suffix = '\n'
1189
1190         ret, outbuf, outs = json_command(cluster_handle, target=target,
1191                                          prefix='get_command_descriptions')
1192         if ret:
1193             where = '{0}.{1}'.format(*target)
1194             if ret > 0:
1195                 raise RuntimeError('Unexpected return code from {0}: {1}'.
1196                                    format(where, ret))
1197             outs = 'problem getting command descriptions from {0}'.format(where)
1198         else:
1199             sigdict = parse_json_funcsigs(outbuf.decode('utf-8'), 'cli')
1200
1201             if parsed_args.completion:
1202                 return complete(sigdict, childargs, target)
1203
1204             ret, outbuf, outs = new_style_command(parsed_args, childargs,
1205                                                   target, sigdict, inbuf,
1206                                                   verbose)
1207
1208             # debug tool: send any successful command *again* to
1209             # verify that it is idempotent.
1210             if not ret and 'CEPH_CLI_TEST_DUP_COMMAND' in os.environ:
1211                 ret, outbuf, outs = new_style_command(parsed_args, childargs,
1212                                                       target, sigdict, inbuf,
1213                                                       verbose)
1214                 if ret < 0:
1215                     ret = -ret
1216                     print(prefix +
1217                           'Second attempt of previously successful command '
1218                           'failed with {0}: {1}'.format(
1219                               errno.errorcode.get(ret, 'Unknown'), outs),
1220                           file=sys.stderr)
1221
1222         if ret < 0:
1223             ret = -ret
1224             errstr = errno.errorcode.get(ret, 'Unknown')
1225             print('Error {0}: {1}'.format(errstr, outs), file=sys.stderr)
1226             if len(targets) > 1:
1227                 final_ret = ret
1228             else:
1229                 return ret
1230
1231         if outs:
1232             print(prefix + outs, file=sys.stderr)
1233
1234         sys.stdout.flush()
1235
1236         if parsed_args.output_file:
1237             outf.write(outbuf)
1238         else:
1239             # hack: old code printed status line before many json outputs
1240             # (osd dump, etc.) that consumers know to ignore.  Add blank line
1241             # to satisfy consumers that skip the first line, but not annoy
1242             # consumers that don't.
1243             if parsed_args.output_format and \
1244                parsed_args.output_format.startswith('json'):
1245                 print()
1246
1247             # if we are prettifying things, normalize newlines.  sigh.
1248             if suffix:
1249                 outbuf = outbuf.rstrip()
1250             if outbuf:
1251                 try:
1252                     print(prefix, end='')
1253                     # Write directly to binary stdout
1254                     raw_write(outbuf)
1255                     print(suffix, end='')
1256                 except IOError as e:
1257                     if e.errno != errno.EPIPE:
1258                         raise e
1259         try:
1260             sys.stdout.flush()
1261         except IOError as e:
1262             if e.errno != errno.EPIPE:
1263                 raise e
1264
1265
1266     # Block until command completion (currently scrub and deep_scrub only)
1267     if block:
1268         wait(childargs, waitdata)
1269
1270     if parsed_args.output_file and parsed_args.output_file != '-':
1271         outf.close()
1272
1273     if final_ret:
1274         return final_ret
1275
1276     return 0
1277
1278 if __name__ == '__main__':
1279     try:
1280         retval = main()
1281         # shutdown explicitly; Rados() does not
1282         if retval == 0 and cluster_handle:
1283             run_in_thread(cluster_handle.shutdown)
1284     except KeyboardInterrupt:
1285         print('Interrupted')
1286         retval = errno.EINTR
1287
1288     if retval:
1289         # flush explicitly because we aren't exiting in the usual way
1290         sys.stdout.flush()
1291         sys.stderr.flush()
1292         os._exit(retval)
1293     else:
1294         sys.exit(retval)