3 # vim: ts=4 sw=4 smarttab expandtab
5 # Processed in Makefile to add python #! line and version variable
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.
14 Copyright (C) 2013 Inktank Storage, Inc.
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.
22 from time import sleep
33 from typing import Dict, List, Sequence, Tuple
40 CEPH_GIT_VER = "@CEPH_GIT_VER@"
41 CEPH_GIT_NICE_VER = "@CEPH_GIT_NICE_VER@"
42 CEPH_RELEASE = "@CEPH_RELEASE@"
43 CEPH_RELEASE_NAME = "@CEPH_RELEASE_NAME@"
44 CEPH_RELEASE_TYPE = "@CEPH_RELEASE_TYPE@"
46 # priorities from src/common/perf_counters.h
50 PRIO_UNINTERESTING = 2
53 PRIO_DEFAULT = PRIO_INTERESTING
55 # Make life easier on developers:
56 # If our parent dir contains CMakeCache.txt and bin/init-ceph,
57 # assume we're running from a build dir (i.e. src/build/bin/ceph)
58 # and tweak sys.path and LD_LIBRARY_PATH to use built files.
59 # Since this involves re-execing, if CEPH_DBG is set in the environment
60 # re-exec with -mpdb. Also, if CEPH_DEV is in the env, suppress
61 # the warning message about the DEVELOPER MODE.
63 MYPATH = os.path.abspath(__file__)
64 MYDIR = os.path.dirname(MYPATH)
65 MYPDIR = os.path.dirname(MYDIR)
66 DEVMODEMSG = '*** DEVELOPER MODE: setting PATH, PYTHONPATH and LD_LIBRARY_PATH ***'
69 def add_to_ld_path(path_name, path):
70 paths = re.split('[ :]', os.environ.get(path_name, ''))
75 os.environ[path_name] = ':'.join(paths)
79 def respawn_in_path(lib_path, pybind_path, pythonlib_path, asan_lib_path):
80 if platform.system() == "Darwin":
81 lib_path_var = "DYLD_LIBRARY_PATH"
83 lib_path_var = "LD_LIBRARY_PATH"
86 preload_libcxx = os.environ.get('CEPH_PRELOAD_LIBCXX')
88 ld_paths_changed += add_to_ld_path('LD_PRELOAD', preload_libcxx)
90 ld_paths_changed += add_to_ld_path('LD_PRELOAD', asan_lib_path)
91 ld_paths_changed += add_to_ld_path(lib_path_var, lib_path)
92 if ld_paths_changed > 0:
93 if "CEPH_DEV" not in os.environ:
94 print(DEVMODEMSG, file=sys.stderr)
96 if 'CEPH_DBG' in os.environ:
97 execv_cmd += ['@Python3_EXECUTABLE@', '-mpdb']
99 os.execvp(execv_cmd[0], execv_cmd)
101 sys.path.insert(0, pybind_path)
102 sys.path.insert(0, pythonlib_path)
105 def get_pythonlib_dir():
106 """Returns the name of a distutils build directory"""
107 return "lib.{version[0]}".format(version=sys.version_info)
110 def get_cmake_variables(*names):
111 vars = dict((name, None) for name in names)
112 for line in open(os.path.join(MYPDIR, "CMakeCache.txt")):
113 # parse lines like "WITH_ASAN:BOOL=ON"
115 if line.startswith("{}:".format(name)):
116 type_value = line.split(":")[1].strip()
117 t, v = type_value.split("=")
119 v = v.upper() in ('TRUE', '1', 'Y', 'YES', 'ON')
122 if all(vars.values()):
124 return [vars[name] for name in names]
127 if os.path.exists(os.path.join(MYPDIR, "CMakeCache.txt")) \
128 and os.path.exists(os.path.join(MYPDIR, "bin/init-ceph")):
129 src_path, with_asan, asan_lib_path = \
130 get_cmake_variables("ceph_SOURCE_DIR", "WITH_ASAN", "ASAN_LIBRARY")
132 # Huh, maybe we're not really in a cmake environment?
135 # Developer mode, but in a cmake build dir instead of the src dir
136 lib_path = os.path.join(MYPDIR, "lib")
137 bin_path = os.path.join(MYPDIR, "bin")
138 pybind_path = os.path.join(src_path, "src", "pybind")
139 pythonlib_path = os.path.join(lib_path,
142 respawn_in_path(lib_path, pybind_path, pythonlib_path,
143 asan_lib_path if with_asan else None)
145 if 'PATH' in os.environ and bin_path not in os.environ['PATH']:
146 os.environ['PATH'] = os.pathsep.join([bin_path, os.environ['PATH']])
157 from ceph_argparse import \
158 concise_sig, descsort_key, parse_json_funcsigs, \
159 validate_command, find_cmd_target, \
160 json_command, run_in_thread, Flag
162 from ceph_daemon import admin_socket, DaemonWatcher, Termsize
164 # just a couple of globals
167 cluster_handle = None
172 sys.stdout.buffer.write(buf)
176 ret, outbuf, outs = json_command(cluster_handle, prefix='osd ls')
178 raise RuntimeError('Can\'t contact mon for osd list')
179 return [line.decode('utf-8') for line in outbuf.split(b'\n') if line]
183 ret, outbuf, outs = json_command(cluster_handle, prefix='mon dump',
184 argdict={'format': 'json'})
186 raise RuntimeError('Can\'t contact mon for mon list')
187 d = json.loads(outbuf.decode('utf-8'))
188 return [m['name'] for m in d['mons']]
192 ret, outbuf, outs = json_command(cluster_handle, prefix='fs dump',
193 argdict={'format': 'json'})
195 raise RuntimeError('Can\'t contact mon for mds list')
196 d = json.loads(outbuf.decode('utf-8'))
198 for info in d['standbys']:
199 l.append(info['name'])
200 for fs in d['filesystems']:
201 for info in fs['mdsmap']['info'].values():
202 l.append(info['name'])
207 ret, outbuf, outs = json_command(cluster_handle, prefix='mgr dump',
208 argdict={'format': 'json'})
210 raise RuntimeError('Can\'t contact mon for mgr list')
212 d = json.loads(outbuf.decode('utf-8'))
214 l.append(d['active_name'])
215 # we can only send tell commands to the active mgr
216 #for i in d['standbys']:
217 # l.append(i['name'])
221 def ids_by_service(service):
222 ids = {"mon": monids,
226 return ids[service]()
229 def validate_target(target):
231 this function will return true iff target is a correct
232 target, such as mon.a/osd.2/mds.a/mgr.
234 target: array, likes ['osd', '2']
235 return: bool, or raise RuntimeError
239 # for case "service.id"
240 service_name, service_id = target[0], target[1]
242 exist_ids = ids_by_service(service_name)
244 print('WARN: {0} is not a legal service name, should be one of mon/osd/mds/mgr'.format(service_name),
248 if service_id in exist_ids or len(exist_ids) > 0 and service_id == '*':
251 print('WARN: the service id you provided does not exist. service id should '
252 'be one of {0}.'.format('/'.join(exist_ids)), file=sys.stderr)
255 elif len(target) == 1 and target[0] in ['mgr', 'mon']:
258 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)
262 # these args must be passed to all child programs
265 'client_name': '--name',
266 'cluster': '--cluster',
267 'cephconf': '--conf',
271 def parse_cmdargs(args=None, target='') -> Tuple[argparse.ArgumentParser,
275 Consume generic arguments from the start of the ``args``
276 list. Call this first to handle arguments that are not
277 handled by a command description provided by the server.
279 :returns: three tuple of ArgumentParser instance, Namespace instance
280 containing parsed values, and list of un-handled arguments
282 # alias: let the line-wrapping be sane
283 AP = argparse.ArgumentParser
285 # format our own help
286 parser = AP(description='Ceph administration tool', add_help=False)
288 parser.add_argument('--completion', action='store_true',
289 help=argparse.SUPPRESS)
291 parser.add_argument('-h', '--help', help='request mon help',
294 parser.add_argument('-c', '--conf', dest='cephconf',
295 help='ceph configuration file')
296 parser.add_argument('-i', '--in-file', dest='input_file',
297 help='input file, or "-" for stdin')
298 parser.add_argument('-o', '--out-file', dest='output_file',
299 help='output file, or "-" for stdout')
300 parser.add_argument('--setuser', dest='setuser',
301 help='set user file permission')
302 parser.add_argument('--setgroup', dest='setgroup',
303 help='set group file permission')
304 parser.add_argument('--id', '--user', dest='client_id',
305 help='client id for authentication')
306 parser.add_argument('--name', '-n', dest='client_name',
307 help='client name for authentication')
308 parser.add_argument('--cluster', help='cluster name')
310 parser.add_argument('--admin-daemon', dest='admin_socket',
311 help='submit admin-socket command (e.g. "help\" for' \
312 'a list of available commands)')
314 parser.add_argument('-s', '--status', action='store_true',
315 help='show cluster status')
317 parser.add_argument('-w', '--watch', action='store_true',
318 help='watch live cluster changes')
319 parser.add_argument('--watch-debug', action='store_true',
320 help='watch debug events')
321 parser.add_argument('--watch-info', action='store_true',
322 help='watch info events')
323 parser.add_argument('--watch-sec', action='store_true',
324 help='watch security events')
325 parser.add_argument('--watch-warn', action='store_true',
326 help='watch warn events')
327 parser.add_argument('--watch-error', action='store_true',
328 help='watch error events')
330 parser.add_argument('-W', '--watch-channel', dest="watch_channel",
331 help="watch live cluster changes on a specific channel "
332 "(e.g., cluster, audit, cephadm, or '*' for all)")
334 parser.add_argument('--version', '-v', action="store_true", help="display version")
335 parser.add_argument('--verbose', action="store_true", help="make verbose")
336 parser.add_argument('--concise', dest='verbose', action="store_false",
337 help="make less verbose")
339 parser.add_argument('-f', '--format', choices=['json', 'json-pretty',
340 'xml', 'xml-pretty', 'plain', 'yaml'],
341 help="Note: yaml is only valid for orch commands", dest='output_format')
343 parser.add_argument('--connect-timeout', dest='cluster_timeout',
345 help='set a timeout for connecting to the cluster')
347 parser.add_argument('--block', action='store_true',
348 help='block until completion (scrub and deep-scrub only)')
349 parser.add_argument('--period', '-p', default=1, type=float,
350 help='polling period, default 1.0 second (for ' \
351 'polling commands only)')
353 # returns a Namespace with the parsed args, and a list of all extras
354 parsed_args, extras = parser.parse_known_args(args)
356 return parser, parsed_args, extras
360 print('\n', s, '\n', '=' * len(s))
363 def do_basic_help(parser, args):
365 Print basic parser help
366 If the cluster is available, get and print monitor help
368 hdr('General usage:')
370 print_locally_handled_command_help()
373 def print_locally_handled_command_help():
374 hdr("Local commands:")
376 ping <mon.id> Send simple presence/life test to a mon
377 <mon.id> may be 'mon.*' for all mons
378 daemon {type.id|path} <cmd>
379 Same as --admin-daemon, but auto-find admin socket
380 daemonperf {type.id | path} [stat-pats] [priority] [<interval>] [<count>]
381 daemonperf {type.id | path} list|ls [stat-pats] [priority]
382 Get selected perf stats from daemon/admin socket
383 Optional shell-glob comma-delim match string stat-pats
384 Optional selection priority (can abbreviate name):
385 critical, interesting, useful, noninteresting, debug
386 List shows a table of all available stats
387 Run <count> times (default forever),
388 once per <interval> seconds (default 1)
389 """, file=sys.stdout)
392 def do_extended_help(parser, args, target, partial) -> int:
393 def help_for_sigs(sigs, partial=None):
396 out = format_help(parse_json_funcsigs(sigs, 'cli'),
398 if not out and partial:
399 # shorten partial until we get at least one matching command prefix
400 partial = ' '.join(partial.split()[:-1])
402 sys.stdout.write(out)
404 except BrokenPipeError:
407 def help_for_target(target, partial=None):
408 # wait for osdmap because we know this is sent after the mgrmap
409 # and monmap (it's alphabetical).
410 cluster_handle.wait_for_latest_osdmap()
411 ret, outbuf, outs = json_command(cluster_handle, target=target,
412 prefix='get_command_descriptions',
415 if (ret == -errno.EPERM or ret == -errno.EACCES) and target[0] in ('osd', 'mds'):
416 print("Permission denied. Check that your user has 'allow *' "
417 "capabilities for the target daemon type.", file=sys.stderr)
418 elif ret == -errno.EPERM:
419 print("Permission denied. Check your user has proper "
420 "capabilities configured", file=sys.stderr)
422 print("couldn't get command descriptions for {0}: {1} ({2})".
423 format(target, outs, ret), file=sys.stderr)
426 return help_for_sigs(outbuf.decode('utf-8'), partial)
428 assert(cluster_handle.state == "connected")
429 return help_for_target(target, partial)
431 DONTSPLIT = string.ascii_letters + '{[<>]}'
434 def wrap(s, width, indent):
436 generator to transform s into a sequence of strings width or shorter,
437 for wrapping text to a specific column width.
438 Attempt to break on anything but DONTSPLIT characters.
439 indent is amount to indent 2nd-through-nth lines.
441 so "long string long string long string" width=11 indent=1 becomes
442 'long string', ' long string', ' long string' so that it can be printed
455 # no splitting; just possibly indent
462 while (splitpos > 0) and (s[splitpos-1] in DONTSPLIT):
469 # prior result means we're mid-iteration, indent
472 # first time, set leader and width for next
473 leader = ' ' * indent
474 width -= 1 # for subsequent space additions
476 # remove any leading spaces in this chunk of s
477 result += s[:splitpos].lstrip()
483 def format_help(cmddict, partial=None) -> str:
485 Formats all the cmdsigs and helptexts from cmddict into a sorted-by-
486 cmdsig 2-column display, with each column wrapped and indented to
487 fit into (terminal_width / 2) characters.
491 for cmd in sorted(cmddict.values(), key=descsort_key):
495 flags = cmd.get('flags', 0)
496 if flags & (Flag.OBSOLETE | Flag.DEPRECATED | Flag.HIDDEN):
498 concise = concise_sig(cmd['sig'])
499 if partial and not concise.startswith(partial):
501 width = Termsize().cols - 1 # 1 for the line between sig and help
502 sig_width = int(width / 2)
503 # make sure width == sig_width + help_width, even (width % 2 > 0)
504 help_width = int(width / 2) + (width % 2)
505 siglines = [l for l in wrap(concise, sig_width, 1)]
506 helplines = [l for l in wrap(cmd['help'], help_width, 1)]
508 # make lists the same length
509 maxlen = max(len(siglines), len(helplines))
510 siglines.extend([''] * (maxlen - len(siglines)))
511 helplines.extend([''] * (maxlen - len(helplines)))
513 # so we can zip them for output
514 for s, h in zip(siglines, helplines):
515 fullusage += '{s:{w}s} {h}\n'.format(s=s, h=h, w=sig_width)
520 def ceph_conf(parsed_args, field, name, pid=None):
522 bindir = os.path.dirname(__file__)
523 if shutil.which(cmd):
525 elif shutil.which(cmd, path=bindir):
526 args = [os.path.join(bindir, cmd)]
528 raise RuntimeError('"ceph-conf" not found')
531 args.extend(['--name', name])
533 args.extend(['--pid', pid])
535 # add any args in GLOBAL_ARGS
536 for key, val in GLOBAL_ARGS.items():
537 # ignore name in favor of argument name, if any
538 if name and key == 'client_name':
540 if getattr(parsed_args, key):
541 args.extend([val, getattr(parsed_args, key)])
543 args.extend(['--show-config-value', field])
544 p = subprocess.Popen(
546 stdout=subprocess.PIPE,
547 stderr=subprocess.PIPE)
548 outdata, errdata = p.communicate()
549 if p.returncode != 0:
550 raise RuntimeError('unable to get conf option %s for %s: %s' % (field, name, errdata))
551 return outdata.rstrip()
556 if sys.stdin.isatty():
559 line = input(PROMPT).rstrip()
560 if line in ['q', 'quit', 'Q', 'exit']:
567 line = sys.stdin.readline()
575 def do_command(parsed_args, target, cmdargs, sigdict, inbuf, verbose):
576 ''' Validate a command, and handle the polling flag '''
578 valid_dict = validate_command(sigdict, cmdargs, verbose)
579 # Validate input args against list of sigs
581 if parsed_args.output_format:
582 valid_dict['format'] = parsed_args.output_format
584 print("Submitting command: ", valid_dict, file=sys.stderr)
586 return -errno.EINVAL, '', 'invalid command'
588 next_header_print = 0
589 # Set extra options for polling commands only:
590 if valid_dict.get('poll', False):
591 valid_dict['width'] = Termsize().cols
594 # Only print the header for polling commands
595 if next_header_print == 0 and valid_dict.get('poll', False):
596 valid_dict['print_header'] = True
597 next_header_print = Termsize().rows - 3
598 next_header_print -= 1
599 ret, outbuf, outs = json_command(cluster_handle, target=target,
600 argdict=valid_dict, inbuf=inbuf, verbose=verbose)
601 if valid_dict.get('poll', False):
602 valid_dict['print_header'] = False
603 if not valid_dict.get('poll', False):
604 # Don't print here if it's not a polling command
608 print('Error: {0} {1}'.format(ret, errno.errorcode.get(ret, 'Unknown')),
612 print(outbuf.decode('utf-8'))
614 print(outs, file=sys.stderr)
615 if parsed_args.period <= 0:
617 sleep(parsed_args.period)
618 except KeyboardInterrupt:
620 return errno.EINTR, '', ''
621 if ret == errno.ETIMEDOUT:
624 outs = ("Connection timed out. Please check the client's " +
625 "permission and connection.")
626 return ret, outbuf, outs
629 def new_style_command(parsed_args,
633 inbuf, verbose) -> Tuple[int, bytes, str]:
635 Do new-style command dance.
636 target: daemon to receive command: mon (any) or osd.N
637 sigdict - the parsed output from the new monitor describing commands
638 inbuf - any -i input file data
642 for cmdtag in sorted(sigdict.keys()):
643 cmd = sigdict[cmdtag]
645 print('{0}: {1}'.format(cmdtag, concise_sig(sig)))
648 # Non interactive mode
649 ret, outbuf, outs = do_command(parsed_args, target, cmdargs, sigdict, inbuf, verbose)
651 # Interactive mode (ceph cli)
652 if sys.stdin.isatty():
653 # do the command-interpreter looping
654 # for input to do readline cmd editing
655 import readline # noqa
659 interactive_input = read_input()
661 # leave user an uncluttered prompt
663 if interactive_input is None:
665 cmdargs = parse_cmdargs(shlex.split(interactive_input))[2]
667 target = find_cmd_target(cmdargs)
668 except Exception as e:
669 print('error handling command target: {0}'.format(e),
672 if len(cmdargs) and cmdargs[0] == 'tell':
673 print('Can not use \'tell\' in interactive mode.',
676 ret, outbuf, outs = do_command(parsed_args, target, cmdargs,
677 sigdict, inbuf, verbose)
680 errstr = errno.errorcode.get(ret, 'Unknown')
681 print('Error {0}: {1}'.format(errstr, outs), file=sys.stderr)
684 print(outs, file=sys.stderr)
686 print(outbuf.decode('utf-8'))
688 return ret, outbuf, outs
691 def complete(sigdict, args, target):
693 Command completion. Match as much of [args] as possible,
694 and print every possible match separated by newlines.
697 # XXX this looks a lot like the front of validate_command(). Refactor?
699 # Repulsive hack to handle tell: lop off 'tell' and target
700 # and validate the rest of the command. 'target' is already
701 # determined in our callers, so it's ok to remove it here.
702 if len(args) and args[0] == 'tell':
704 # look for best match, accumulate possibles in bestcmds
705 # (so we can maybe give a more-useful error message)
709 for cmdtag, cmd in sigdict.items():
710 flags = cmd.get('flags', 0)
711 if flags & (Flag.OBSOLETE | Flag.HIDDEN):
715 # iterate over all arguments, except last one
716 for arg in args[0:-1]:
718 # an out of argument definitions
720 found_match = arg in sig[j].complete(arg)
721 if not found_match and sig[j].req:
722 # no elements that match
727 # successfully matched all - except last one - arguments
728 if j < len(sig) and len(args) > 0:
729 comps += sig[j].complete(args[-1])
734 if match_count == 1 and len(comps) == 0:
735 # only one command matched and no hints yet => add help
736 comps = comps + [' ', '#'+match_cmd['help']]
737 print('\n'.join(sorted(set(comps))))
741 def ping_monitor(cluster_handle, name, timeout):
742 if 'mon.' not in name:
743 print('"ping" expects a monitor to ping; try "ping mon.<id>"', file=sys.stderr)
746 mon_id = name[len('mon.'):]
748 run_in_thread(cluster_handle.connect, timeout=timeout)
750 s = run_in_thread(cluster_handle.ping_monitor, m)
752 print("mon.{0}".format(m) + '\n' + "Error connecting to monitor.")
754 print("mon.{0}".format(m) + '\n' + s)
756 s = run_in_thread(cluster_handle.ping_monitor, mon_id)
761 def get_admin_socket(parsed_args, name):
762 path = ceph_conf(parsed_args, 'admin_socket', name)
764 if stat.S_ISSOCK(os.stat(path).st_mode):
768 # try harder, probably the "name" option is in the form of
770 parts = name.rsplit('.', 1)
771 if len(parts) > 1 and parts[-1].isnumeric():
773 return ceph_conf(parsed_args, 'admin_socket', name, pid)
778 def maybe_daemon_command(parsed_args, childargs):
780 Check if --admin-socket, daemon, or daemonperf command
781 if it is, returns (boolean handled, return code if handled == True)
786 if parsed_args.admin_socket:
787 sockpath = parsed_args.admin_socket
788 elif len(childargs) > 0 and childargs[0] in ["daemon", "daemonperf"]:
789 daemon_perf = (childargs[0] == "daemonperf")
790 # Treat "daemon <path>" or "daemon <name>" like --admin_daemon <path>
791 # Handle "daemonperf <path>" the same but requires no trailing args
792 require_args = 2 if daemon_perf else 3
793 if len(childargs) >= require_args:
794 if childargs[1].find('/') >= 0:
795 sockpath = childargs[1]
797 # try resolve daemon name
799 sockpath = get_admin_socket(parsed_args, childargs[1])
800 except Exception as e:
801 print('Can\'t get admin socket path: ' + str(e), file=sys.stderr)
802 return True, errno.EINVAL
804 childargs = childargs[2:]
806 print('{0} requires at least {1} arguments'.format(childargs[0], require_args),
808 return True, errno.EINVAL
810 if sockpath and daemon_perf:
811 return True, daemonperf(childargs, sockpath)
814 raw_write(admin_socket(sockpath, childargs, parsed_args.output_format))
815 except Exception as e:
816 print('admin_socket: {0}'.format(e), file=sys.stderr)
817 return True, errno.EINVAL
831 def daemonperf(childargs: Sequence[str], sockpath: str):
833 Handle daemonperf command; returns errno or 0
835 daemonperf <daemon> [priority string] [statpats] [interval] [count]
836 daemonperf <daemon> list|ls [statpats]
845 def prio_from_name(arg):
848 'critical': PRIO_CRITICAL,
849 'interesting': PRIO_INTERESTING,
850 'useful': PRIO_USEFUL,
851 'uninteresting': PRIO_UNINTERESTING,
852 'debugonly': PRIO_DEBUGONLY,
858 for name, val in PRIOMAP.items():
859 if name.startswith(arg):
863 # consume and analyze non-numeric args
864 while len(childargs) and not isnum(childargs[0]):
865 arg = childargs.pop(0)
867 if arg in ['list', 'ls']:
871 prio = prio_from_name(arg)
876 statpats = arg.split(',')
879 priority = PRIO_DEFAULT
881 if len(childargs) > 0:
883 interval = float(childargs.pop(0))
887 print('daemonperf: interval should be a positive number', file=sys.stderr)
890 if len(childargs) > 0:
891 arg = childargs.pop(0)
892 if (not isnum(arg)) or (int(arg) < 0):
893 print('daemonperf: count should be a positive integer', file=sys.stderr)
897 watcher = DaemonWatcher(sockpath, statpats, priority)
901 watcher.run(interval, count)
906 def get_scrub_timestamps(childargs: Sequence[str]) -> Dict[str,
908 last_scrub_stamp = "last_" + childargs[1].replace('-', '_') + "_stamp"
911 if childargs[2] in ['all', 'any', '*']:
913 devnull = open(os.devnull, 'w')
914 out = subprocess.check_output(['ceph', 'pg', 'dump', '--format=json-pretty'],
917 pgstats = json.loads(out)['pg_map']['pg_stats']
919 pgstats = json.loads(out)['pg_stats']
921 if scruball or stat['up_primary'] == int(childargs[2]):
922 scrub_tuple = (stat['up_primary'], stat[last_scrub_stamp])
923 results[stat['pgid']] = scrub_tuple
927 def check_scrub_stamps(waitdata, currdata):
928 for pg in waitdata.keys():
929 # Try to handle the case where a pg may not exist in current results
930 if pg in currdata and waitdata[pg][1] == currdata[pg][1]:
935 def waitscrub(childargs, waitdata):
936 print('Waiting for {0} to complete...'.format(childargs[1]), file=sys.stdout)
937 currdata = get_scrub_timestamps(childargs)
938 while not check_scrub_stamps(waitdata, currdata):
940 currdata = get_scrub_timestamps(childargs)
941 print('{0} completed'.format(childargs[1]), file=sys.stdout)
944 def wait(childargs: Sequence[str], waitdata):
945 if childargs[1] in ['scrub', 'deep-scrub']:
946 waitscrub(childargs, waitdata)
950 ceph_args = os.environ.get('CEPH_ARGS')
952 if "injectargs" in sys.argv:
953 i = sys.argv.index("injectargs")
954 sys.argv = sys.argv[:i] + ceph_args.split() + sys.argv[i:]
956 sys.argv.extend([arg for arg in ceph_args.split()
957 if '--admin-socket' not in arg])
958 parser, parsed_args, childargs = parse_cmdargs()
960 if parsed_args.version:
961 print('ceph version {0} ({1}) {2} ({3})'.format(
965 CEPH_RELEASE_TYPE)) # noqa
968 # --watch-channel|-W implies -w
969 if parsed_args.watch_channel:
970 parsed_args.watch = True
971 elif parsed_args.watch and not parsed_args.watch_channel:
972 parsed_args.watch_channel = 'cluster'
975 verbose = parsed_args.verbose
978 print("parsed_args: {0}, childargs: {1}".format(parsed_args, childargs), file=sys.stderr)
980 # pass on --id, --name, --conf
981 name = 'client.admin'
982 if parsed_args.client_id:
983 name = 'client.' + parsed_args.client_id
984 if parsed_args.client_name:
985 name = parsed_args.client_name
987 conffile = rados.Rados.DEFAULT_CONF_FILES
988 if parsed_args.cephconf:
989 conffile = parsed_args.cephconf
990 # For now, --admin-daemon is handled as usual. Try it
991 # first in case we can't connect() to the cluster
993 done, ret = maybe_daemon_command(parsed_args, childargs)
998 if parsed_args.cluster_timeout:
999 timeout = parsed_args.cluster_timeout
1002 if parsed_args.help:
1003 do_basic_help(parser, childargs)
1005 # handle any 'generic' ceph arguments that we didn't parse here
1006 global cluster_handle
1008 # rados.Rados() will call rados_create2, and then read the conf file,
1009 # and then set the keys from the dict. So we must do these
1010 # "pre-file defaults" first (see common_preinit in librados)
1012 'log_to_stderr': 'true',
1013 'err_to_stderr': 'true',
1014 'log_flush_on_exit': 'true',
1017 if 'injectargs' in childargs:
1018 position = childargs.index('injectargs')
1019 injectargs = childargs[position:]
1020 childargs = childargs[:position]
1022 print('Separate childargs {0} from injectargs {1}'.format(childargs, injectargs),
1028 if parsed_args.cluster:
1029 clustername = parsed_args.cluster
1032 cluster_handle = run_in_thread(rados.Rados,
1033 name=name, clustername=clustername,
1034 conf_defaults=conf_defaults,
1036 retargs = run_in_thread(cluster_handle.conf_parse_argv, childargs)
1037 except rados.Error as e:
1038 print('Error initializing cluster client: {0!r}'.format(e), file=sys.stderr)
1045 # -- means "stop parsing args", but we don't want to see it either
1046 if '--' in childargs:
1047 childargs.remove('--')
1048 if injectargs and '--' in injectargs:
1049 injectargs.remove('--')
1053 if parsed_args.block:
1054 if (len(childargs) >= 2 and
1055 childargs[0] == 'osd' and
1056 childargs[1] in ['deep-scrub', 'scrub']):
1058 waitdata = get_scrub_timestamps(childargs)
1060 if parsed_args.help:
1061 # short default timeout for -h
1065 if childargs and childargs[0] == 'ping' and not parsed_args.help:
1066 if len(childargs) < 2:
1067 print('"ping" requires a monitor name as argument: "ping mon.<id>"', file=sys.stderr)
1069 if parsed_args.completion:
1070 # for completion let timeout be really small
1073 if childargs and childargs[0] == 'ping' and not parsed_args.help:
1074 return ping_monitor(cluster_handle, childargs[1], timeout)
1075 result = run_in_thread(cluster_handle.connect, timeout=timeout)
1076 if type(result) is tuple and result[0] == -errno.EINTR:
1077 print('Cluster connection interrupted or timed out', file=sys.stderr)
1079 except KeyboardInterrupt:
1080 print('Cluster connection aborted', file=sys.stderr)
1082 except rados.PermissionDeniedError as e:
1083 print(str(e), file=sys.stderr)
1085 except Exception as e:
1086 print(str(e), file=sys.stderr)
1089 if parsed_args.help:
1091 if len(childargs) >= 2 and childargs[0] == 'tell':
1092 target = childargs[1].split('.', 1)
1093 if not validate_target(target):
1094 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)
1096 childargs = childargs[2:]
1097 hdr('Tell %s commands:' % target[0])
1099 hdr('Monitor commands:')
1100 target = ('mon', '')
1102 print('[Contacting monitor, timeout after %d seconds]' % timeout)
1104 return do_extended_help(parser, childargs, target, ' '.join(childargs))
1106 # implement "tell service.id help"
1107 if len(childargs) >= 3 and childargs[0] == 'tell' and childargs[2] == 'help':
1108 target = childargs[1].split('.', 1)
1109 if validate_target(target):
1110 hdr('Tell %s commands' % target[0])
1111 return do_extended_help(parser, childargs, target, None)
1113 print('target {0} doesn\'t exists, please pass correct target to tell command, such as mon.a/'
1114 'osd.1/mds.a/mgr'.format(childargs[1]), file=sys.stderr)
1117 # implement -w/--watch_*
1118 # This is ugly, but Namespace() isn't quite rich enough.
1120 for k, v in parsed_args._get_kwargs():
1121 if k.startswith('watch') and v:
1124 elif k != "watch_channel":
1125 level = k.replace('watch_', '')
1127 # an awfully simple callback
1128 def watch_cb(arg, line, channel, name, who, stamp_sec, stamp_nsec, seq, level, msg):
1130 channel = channel.decode('utf-8')
1131 if parsed_args.watch_channel in (channel, '*'):
1132 print(line.decode('utf-8'))
1135 # first do a ceph status
1136 ret, outbuf, outs = json_command(cluster_handle, prefix='status')
1138 print("status query failed: ", outs, file=sys.stderr)
1140 print(outbuf.decode('utf-8'))
1142 # this instance keeps the watch connection alive, but is
1144 run_in_thread(cluster_handle.monitor_log2, level, watch_cb, 0)
1146 # loop forever letting watch_cb print lines
1149 except KeyboardInterrupt:
1150 # or until ^C, at least
1153 # read input file, if any
1155 if parsed_args.input_file:
1157 if parsed_args.input_file == '-':
1158 inbuf = sys.stdin.buffer.read()
1160 with open(parsed_args.input_file, 'rb') as f:
1162 except Exception as e:
1163 print('Can\'t open input file {0}: {1}'.format(parsed_args.input_file, e), file=sys.stderr)
1166 # prepare output file, if any
1167 if parsed_args.output_file:
1169 if parsed_args.output_file == '-':
1170 outf = sys.stdout.buffer
1172 outf = open(parsed_args.output_file, 'wb')
1173 except Exception as e:
1174 print('Can\'t open output file {0}: {1}'.format(parsed_args.output_file, e), file=sys.stderr)
1176 if parsed_args.setuser:
1178 ownerid = pwd.getpwnam(parsed_args.setuser).pw_uid
1179 os.fchown(outf.fileno(), ownerid, -1)
1180 except OSError as e:
1181 print('Failed to change user ownership of {0} to {1}: {2}'.format(outf, parsed_args.setuser, e))
1183 if parsed_args.setgroup:
1185 groupid = grp.getgrnam(parsed_args.setgroup).gr_gid
1186 os.fchown(outf.fileno(), -1, groupid)
1187 except OSError as e:
1188 print('Failed to change group ownership of {0} to {1}: {2}'.format(outf, parsed_args.setgroup, e))
1191 # -s behaves like a command (ceph status).
1192 if parsed_args.status:
1193 childargs.insert(0, 'status')
1196 target = find_cmd_target(childargs)
1197 except Exception as e:
1198 print('error handling command target: {0}'.format(e), file=sys.stderr)
1201 # Repulsive hack to handle tell: lop off 'tell' and target
1202 # and validate the rest of the command. 'target' is already
1203 # determined in our callers, so it's ok to remove it here.
1205 if len(childargs) and childargs[0] == 'tell':
1206 childargs = childargs[2:]
1211 childargs = injectargs
1212 if not len(childargs):
1213 print('"{0} tell" requires additional arguments.'.format(sys.argv[0]),
1214 'Try "{0} tell <name> <command> [options...]" instead.'.format(sys.argv[0]),
1218 # fetch JSON sigs from command
1219 # each line contains one command signature (a placeholder name
1220 # of the form 'cmdNNN' followed by an array of argument descriptors)
1221 # as part of the validated argument JSON object
1223 if target[1] == '*':
1225 targets = [(service, o) for o in ids_by_service(service)]
1230 for target in targets:
1231 # prettify? prefix output with target, if there was a wildcard used
1234 if not parsed_args.output_file and len(targets) > 1:
1235 prefix = '{0}.{1}: '.format(*target)
1238 ret, outbuf, outs = json_command(cluster_handle, target=target,
1239 prefix='get_command_descriptions')
1241 where = '{0}.{1}'.format(*target)
1243 raise RuntimeError('Unexpected return code from {0}: {1}'.
1245 outs = 'problem getting command descriptions from {0}'.format(where)
1247 sigdict = parse_json_funcsigs(outbuf.decode('utf-8'), 'cli')
1249 if parsed_args.completion:
1250 return complete(sigdict, childargs, target)
1252 ret, outbuf, outs = new_style_command(parsed_args, childargs,
1253 target, sigdict, inbuf,
1256 # debug tool: send any successful command *again* to
1257 # verify that it is idempotent.
1258 if not ret and 'CEPH_CLI_TEST_DUP_COMMAND' in os.environ:
1259 ret, outbuf, outs = new_style_command(parsed_args, childargs,
1260 target, sigdict, inbuf,
1265 'Second attempt of previously successful command '
1266 'failed with {0}: {1}'.format(
1267 errno.errorcode.get(ret, 'Unknown'), outs),
1272 if parsed_args.output_file:
1275 # hack: old code printed status line before many json outputs
1276 # (osd dump, etc.) that consumers know to ignore. Add blank line
1277 # to satisfy consumers that skip the first line, but not annoy
1278 # consumers that don't.
1279 if parsed_args.output_format and \
1280 parsed_args.output_format.startswith('json'):
1283 # if we are prettifying things, normalize newlines. sigh.
1285 outbuf = outbuf.rstrip()
1288 print(prefix, end='')
1289 # Write directly to binary stdout
1291 print(suffix, end='')
1292 except IOError as e:
1293 if e.errno != errno.EPIPE:
1298 except IOError as e:
1299 if e.errno != errno.EPIPE:
1304 errstr = errno.errorcode.get(ret, 'Unknown')
1305 print('Error {0}: {1}'.format(errstr, outs), file=sys.stderr)
1308 print(prefix + outs, file=sys.stderr)
1313 # Block until command completion (currently scrub and deep scrub only)
1315 wait(childargs, waitdata)
1317 if parsed_args.output_file and parsed_args.output_file != '-':
1325 if __name__ == '__main__':
1328 # shutdown explicitly; Rados() does not
1329 if retval == 0 and cluster_handle:
1330 run_in_thread(cluster_handle.shutdown)
1331 except KeyboardInterrupt:
1332 print('Interrupted')
1333 retval = errno.EINTR
1336 # flush explicitly because we aren't exiting in the usual way