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 commands (\"help\" for help)')
313 parser.add_argument('-s', '--status', action='store_true',
314 help='show cluster status')
316 parser.add_argument('-w', '--watch', action='store_true',
317 help='watch live cluster changes')
318 parser.add_argument('--watch-debug', action='store_true',
319 help='watch debug events')
320 parser.add_argument('--watch-info', action='store_true',
321 help='watch info events')
322 parser.add_argument('--watch-sec', action='store_true',
323 help='watch security events')
324 parser.add_argument('--watch-warn', action='store_true',
325 help='watch warn events')
326 parser.add_argument('--watch-error', action='store_true',
327 help='watch error events')
329 parser.add_argument('-W', '--watch-channel', dest="watch_channel",
330 help="watch live cluster changes on a specific channel "
331 "(e.g., cluster, audit, cephadm, or '*' for all)")
333 parser.add_argument('--version', '-v', action="store_true", help="display version")
334 parser.add_argument('--verbose', action="store_true", help="make verbose")
335 parser.add_argument('--concise', dest='verbose', action="store_false",
336 help="make less verbose")
338 parser.add_argument('-f', '--format', choices=['json', 'json-pretty',
339 'xml', 'xml-pretty', 'plain', 'yaml'],
340 help="Note: yaml is only valid for orch commands", dest='output_format')
342 parser.add_argument('--connect-timeout', dest='cluster_timeout',
344 help='set a timeout for connecting to the cluster')
346 parser.add_argument('--block', action='store_true',
347 help='block until completion (scrub and deep-scrub only)')
348 parser.add_argument('--period', '-p', default=1, type=float,
349 help='polling period, default 1.0 second (for ' \
350 'polling commands only)')
352 # returns a Namespace with the parsed args, and a list of all extras
353 parsed_args, extras = parser.parse_known_args(args)
355 return parser, parsed_args, extras
359 print('\n', s, '\n', '=' * len(s))
362 def do_basic_help(parser, args):
364 Print basic parser help
365 If the cluster is available, get and print monitor help
367 hdr('General usage:')
369 print_locally_handled_command_help()
372 def print_locally_handled_command_help():
373 hdr("Local commands:")
375 ping <mon.id> Send simple presence/life test to a mon
376 <mon.id> may be 'mon.*' for all mons
377 daemon {type.id|path} <cmd>
378 Same as --admin-daemon, but auto-find admin socket
379 daemonperf {type.id | path} [stat-pats] [priority] [<interval>] [<count>]
380 daemonperf {type.id | path} list|ls [stat-pats] [priority]
381 Get selected perf stats from daemon/admin socket
382 Optional shell-glob comma-delim match string stat-pats
383 Optional selection priority (can abbreviate name):
384 critical, interesting, useful, noninteresting, debug
385 List shows a table of all available stats
386 Run <count> times (default forever),
387 once per <interval> seconds (default 1)
388 """, file=sys.stdout)
391 def do_extended_help(parser, args, target, partial) -> int:
392 def help_for_sigs(sigs, partial=None):
395 out = format_help(parse_json_funcsigs(sigs, 'cli'),
397 if not out and partial:
398 # shorten partial until we get at least one matching command prefix
399 partial = ' '.join(partial.split()[:-1])
401 sys.stdout.write(out)
403 except BrokenPipeError:
406 def help_for_target(target, partial=None):
407 # wait for osdmap because we know this is sent after the mgrmap
408 # and monmap (it's alphabetical).
409 cluster_handle.wait_for_latest_osdmap()
410 ret, outbuf, outs = json_command(cluster_handle, target=target,
411 prefix='get_command_descriptions',
414 if (ret == -errno.EPERM or ret == -errno.EACCES) and target[0] in ('osd', 'mds'):
415 print("Permission denied. Check that your user has 'allow *' "
416 "capabilities for the target daemon type.", file=sys.stderr)
417 elif ret == -errno.EPERM:
418 print("Permission denied. Check your user has proper "
419 "capabilities configured", file=sys.stderr)
421 print("couldn't get command descriptions for {0}: {1} ({2})".
422 format(target, outs, ret), file=sys.stderr)
425 return help_for_sigs(outbuf.decode('utf-8'), partial)
427 assert(cluster_handle.state == "connected")
428 return help_for_target(target, partial)
430 DONTSPLIT = string.ascii_letters + '{[<>]}'
433 def wrap(s, width, indent):
435 generator to transform s into a sequence of strings width or shorter,
436 for wrapping text to a specific column width.
437 Attempt to break on anything but DONTSPLIT characters.
438 indent is amount to indent 2nd-through-nth lines.
440 so "long string long string long string" width=11 indent=1 becomes
441 'long string', ' long string', ' long string' so that it can be printed
454 # no splitting; just possibly indent
461 while (splitpos > 0) and (s[splitpos-1] in DONTSPLIT):
468 # prior result means we're mid-iteration, indent
471 # first time, set leader and width for next
472 leader = ' ' * indent
473 width -= 1 # for subsequent space additions
475 # remove any leading spaces in this chunk of s
476 result += s[:splitpos].lstrip()
482 def format_help(cmddict, partial=None) -> str:
484 Formats all the cmdsigs and helptexts from cmddict into a sorted-by-
485 cmdsig 2-column display, with each column wrapped and indented to
486 fit into (terminal_width / 2) characters.
490 for cmd in sorted(cmddict.values(), key=descsort_key):
494 flags = cmd.get('flags', 0)
495 if flags & (Flag.OBSOLETE | Flag.DEPRECATED | Flag.HIDDEN):
497 concise = concise_sig(cmd['sig'])
498 if partial and not concise.startswith(partial):
500 width = Termsize().cols - 1 # 1 for the line between sig and help
501 sig_width = int(width / 2)
502 # make sure width == sig_width + help_width, even (width % 2 > 0)
503 help_width = int(width / 2) + (width % 2)
504 siglines = [l for l in wrap(concise, sig_width, 1)]
505 helplines = [l for l in wrap(cmd['help'], help_width, 1)]
507 # make lists the same length
508 maxlen = max(len(siglines), len(helplines))
509 siglines.extend([''] * (maxlen - len(siglines)))
510 helplines.extend([''] * (maxlen - len(helplines)))
512 # so we can zip them for output
513 for s, h in zip(siglines, helplines):
514 fullusage += '{s:{w}s} {h}\n'.format(s=s, h=h, w=sig_width)
519 def ceph_conf(parsed_args, field, name, pid=None):
521 bindir = os.path.dirname(__file__)
522 if shutil.which(cmd):
524 elif shutil.which(cmd, path=bindir):
525 args = [os.path.join(bindir, cmd)]
527 raise RuntimeError('"ceph-conf" not found')
530 args.extend(['--name', name])
532 args.extend(['--pid', pid])
534 # add any args in GLOBAL_ARGS
535 for key, val in GLOBAL_ARGS.items():
536 # ignore name in favor of argument name, if any
537 if name and key == 'client_name':
539 if getattr(parsed_args, key):
540 args.extend([val, getattr(parsed_args, key)])
542 args.extend(['--show-config-value', field])
543 p = subprocess.Popen(
545 stdout=subprocess.PIPE,
546 stderr=subprocess.PIPE)
547 outdata, errdata = p.communicate()
548 if p.returncode != 0:
549 raise RuntimeError('unable to get conf option %s for %s: %s' % (field, name, errdata))
550 return outdata.rstrip()
555 if sys.stdin.isatty():
558 line = input(PROMPT).rstrip()
559 if line in ['q', 'quit', 'Q', 'exit']:
566 line = sys.stdin.readline()
574 def do_command(parsed_args, target, cmdargs, sigdict, inbuf, verbose):
575 ''' Validate a command, and handle the polling flag '''
577 valid_dict = validate_command(sigdict, cmdargs, verbose)
578 # Validate input args against list of sigs
580 if parsed_args.output_format:
581 valid_dict['format'] = parsed_args.output_format
583 print("Submitting command: ", valid_dict, file=sys.stderr)
585 return -errno.EINVAL, '', 'invalid command'
587 next_header_print = 0
588 # Set extra options for polling commands only:
589 if valid_dict.get('poll', False):
590 valid_dict['width'] = Termsize().cols
593 # Only print the header for polling commands
594 if next_header_print == 0 and valid_dict.get('poll', False):
595 valid_dict['print_header'] = True
596 next_header_print = Termsize().rows - 3
597 next_header_print -= 1
598 ret, outbuf, outs = json_command(cluster_handle, target=target,
599 argdict=valid_dict, inbuf=inbuf, verbose=verbose)
600 if valid_dict.get('poll', False):
601 valid_dict['print_header'] = False
602 if not valid_dict.get('poll', False):
603 # Don't print here if it's not a polling command
607 print('Error: {0} {1}'.format(ret, errno.errorcode.get(ret, 'Unknown')),
611 print(outbuf.decode('utf-8'))
613 print(outs, file=sys.stderr)
614 if parsed_args.period <= 0:
616 sleep(parsed_args.period)
617 except KeyboardInterrupt:
619 return errno.EINTR, '', ''
620 if ret == errno.ETIMEDOUT:
623 outs = ("Connection timed out. Please check the client's " +
624 "permission and connection.")
625 return ret, outbuf, outs
628 def new_style_command(parsed_args,
632 inbuf, verbose) -> Tuple[int, bytes, str]:
634 Do new-style command dance.
635 target: daemon to receive command: mon (any) or osd.N
636 sigdict - the parsed output from the new monitor describing commands
637 inbuf - any -i input file data
641 for cmdtag in sorted(sigdict.keys()):
642 cmd = sigdict[cmdtag]
644 print('{0}: {1}'.format(cmdtag, concise_sig(sig)))
647 # Non interactive mode
648 ret, outbuf, outs = do_command(parsed_args, target, cmdargs, sigdict, inbuf, verbose)
650 # Interactive mode (ceph cli)
651 if sys.stdin.isatty():
652 # do the command-interpreter looping
653 # for input to do readline cmd editing
654 import readline # noqa
658 interactive_input = read_input()
660 # leave user an uncluttered prompt
662 if interactive_input is None:
664 cmdargs = parse_cmdargs(shlex.split(interactive_input))[2]
666 target = find_cmd_target(cmdargs)
667 except Exception as e:
668 print('error handling command target: {0}'.format(e),
671 if len(cmdargs) and cmdargs[0] == 'tell':
672 print('Can not use \'tell\' in interactive mode.',
675 ret, outbuf, outs = do_command(parsed_args, target, cmdargs,
676 sigdict, inbuf, verbose)
679 errstr = errno.errorcode.get(ret, 'Unknown')
680 print('Error {0}: {1}'.format(errstr, outs), file=sys.stderr)
683 print(outs, file=sys.stderr)
685 print(outbuf.decode('utf-8'))
687 return ret, outbuf, outs
690 def complete(sigdict, args, target):
692 Command completion. Match as much of [args] as possible,
693 and print every possible match separated by newlines.
696 # XXX this looks a lot like the front of validate_command(). Refactor?
698 # Repulsive hack to handle tell: lop off 'tell' and target
699 # and validate the rest of the command. 'target' is already
700 # determined in our callers, so it's ok to remove it here.
701 if len(args) and args[0] == 'tell':
703 # look for best match, accumulate possibles in bestcmds
704 # (so we can maybe give a more-useful error message)
708 for cmdtag, cmd in sigdict.items():
709 flags = cmd.get('flags', 0)
710 if flags & (Flag.OBSOLETE | Flag.HIDDEN):
714 # iterate over all arguments, except last one
715 for arg in args[0:-1]:
717 # an out of argument definitions
719 found_match = arg in sig[j].complete(arg)
720 if not found_match and sig[j].req:
721 # no elements that match
726 # successfully matched all - except last one - arguments
727 if j < len(sig) and len(args) > 0:
728 comps += sig[j].complete(args[-1])
733 if match_count == 1 and len(comps) == 0:
734 # only one command matched and no hints yet => add help
735 comps = comps + [' ', '#'+match_cmd['help']]
736 print('\n'.join(sorted(set(comps))))
740 def ping_monitor(cluster_handle, name, timeout):
741 if 'mon.' not in name:
742 print('"ping" expects a monitor to ping; try "ping mon.<id>"', file=sys.stderr)
745 mon_id = name[len('mon.'):]
747 run_in_thread(cluster_handle.connect, timeout=timeout)
749 s = run_in_thread(cluster_handle.ping_monitor, m)
751 print("mon.{0}".format(m) + '\n' + "Error connecting to monitor.")
753 print("mon.{0}".format(m) + '\n' + s)
755 s = run_in_thread(cluster_handle.ping_monitor, mon_id)
760 def get_admin_socket(parsed_args, name):
761 path = ceph_conf(parsed_args, 'admin_socket', name)
763 if stat.S_ISSOCK(os.stat(path).st_mode):
767 # try harder, probably the "name" option is in the form of
769 parts = name.rsplit('.', 1)
770 if len(parts) > 1 and parts[-1].isnumeric():
772 return ceph_conf(parsed_args, 'admin_socket', name, pid)
777 def maybe_daemon_command(parsed_args, childargs):
779 Check if --admin-socket, daemon, or daemonperf command
780 if it is, returns (boolean handled, return code if handled == True)
785 if parsed_args.admin_socket:
786 sockpath = parsed_args.admin_socket
787 elif len(childargs) > 0 and childargs[0] in ["daemon", "daemonperf"]:
788 daemon_perf = (childargs[0] == "daemonperf")
789 # Treat "daemon <path>" or "daemon <name>" like --admin_daemon <path>
790 # Handle "daemonperf <path>" the same but requires no trailing args
791 require_args = 2 if daemon_perf else 3
792 if len(childargs) >= require_args:
793 if childargs[1].find('/') >= 0:
794 sockpath = childargs[1]
796 # try resolve daemon name
798 sockpath = get_admin_socket(parsed_args, childargs[1])
799 except Exception as e:
800 print('Can\'t get admin socket path: ' + str(e), file=sys.stderr)
801 return True, errno.EINVAL
803 childargs = childargs[2:]
805 print('{0} requires at least {1} arguments'.format(childargs[0], require_args),
807 return True, errno.EINVAL
809 if sockpath and daemon_perf:
810 return True, daemonperf(childargs, sockpath)
813 raw_write(admin_socket(sockpath, childargs, parsed_args.output_format))
814 except Exception as e:
815 print('admin_socket: {0}'.format(e), file=sys.stderr)
816 return True, errno.EINVAL
830 def daemonperf(childargs: Sequence[str], sockpath: str):
832 Handle daemonperf command; returns errno or 0
834 daemonperf <daemon> [priority string] [statpats] [interval] [count]
835 daemonperf <daemon> list|ls [statpats]
844 def prio_from_name(arg):
847 'critical': PRIO_CRITICAL,
848 'interesting': PRIO_INTERESTING,
849 'useful': PRIO_USEFUL,
850 'uninteresting': PRIO_UNINTERESTING,
851 'debugonly': PRIO_DEBUGONLY,
857 for name, val in PRIOMAP.items():
858 if name.startswith(arg):
862 # consume and analyze non-numeric args
863 while len(childargs) and not isnum(childargs[0]):
864 arg = childargs.pop(0)
866 if arg in ['list', 'ls']:
870 prio = prio_from_name(arg)
875 statpats = arg.split(',')
878 priority = PRIO_DEFAULT
880 if len(childargs) > 0:
882 interval = float(childargs.pop(0))
886 print('daemonperf: interval should be a positive number', file=sys.stderr)
889 if len(childargs) > 0:
890 arg = childargs.pop(0)
891 if (not isnum(arg)) or (int(arg) < 0):
892 print('daemonperf: count should be a positive integer', file=sys.stderr)
896 watcher = DaemonWatcher(sockpath, statpats, priority)
900 watcher.run(interval, count)
905 def get_scrub_timestamps(childargs: Sequence[str]) -> Dict[str,
907 last_scrub_stamp = "last_" + childargs[1].replace('-', '_') + "_stamp"
910 if childargs[2] in ['all', 'any', '*']:
912 devnull = open(os.devnull, 'w')
913 out = subprocess.check_output(['ceph', 'pg', 'dump', '--format=json-pretty'],
916 pgstats = json.loads(out)['pg_map']['pg_stats']
918 pgstats = json.loads(out)['pg_stats']
920 if scruball or stat['up_primary'] == int(childargs[2]):
921 scrub_tuple = (stat['up_primary'], stat[last_scrub_stamp])
922 results[stat['pgid']] = scrub_tuple
926 def check_scrub_stamps(waitdata, currdata):
927 for pg in waitdata.keys():
928 # Try to handle the case where a pg may not exist in current results
929 if pg in currdata and waitdata[pg][1] == currdata[pg][1]:
934 def waitscrub(childargs, waitdata):
935 print('Waiting for {0} to complete...'.format(childargs[1]), file=sys.stdout)
936 currdata = get_scrub_timestamps(childargs)
937 while not check_scrub_stamps(waitdata, currdata):
939 currdata = get_scrub_timestamps(childargs)
940 print('{0} completed'.format(childargs[1]), file=sys.stdout)
943 def wait(childargs: Sequence[str], waitdata):
944 if childargs[1] in ['scrub', 'deep-scrub']:
945 waitscrub(childargs, waitdata)
949 ceph_args = os.environ.get('CEPH_ARGS')
951 if "injectargs" in sys.argv:
952 i = sys.argv.index("injectargs")
953 sys.argv = sys.argv[:i] + ceph_args.split() + sys.argv[i:]
955 sys.argv.extend([arg for arg in ceph_args.split()
956 if '--admin-socket' not in arg])
957 parser, parsed_args, childargs = parse_cmdargs()
959 if parsed_args.version:
960 print('ceph version {0} ({1}) {2} ({3})'.format(
964 CEPH_RELEASE_TYPE)) # noqa
967 # --watch-channel|-W implies -w
968 if parsed_args.watch_channel:
969 parsed_args.watch = True
970 elif parsed_args.watch and not parsed_args.watch_channel:
971 parsed_args.watch_channel = 'cluster'
974 verbose = parsed_args.verbose
977 print("parsed_args: {0}, childargs: {1}".format(parsed_args, childargs), file=sys.stderr)
979 # pass on --id, --name, --conf
980 name = 'client.admin'
981 if parsed_args.client_id:
982 name = 'client.' + parsed_args.client_id
983 if parsed_args.client_name:
984 name = parsed_args.client_name
986 conffile = rados.Rados.DEFAULT_CONF_FILES
987 if parsed_args.cephconf:
988 conffile = parsed_args.cephconf
989 # For now, --admin-daemon is handled as usual. Try it
990 # first in case we can't connect() to the cluster
992 done, ret = maybe_daemon_command(parsed_args, childargs)
997 if parsed_args.cluster_timeout:
998 timeout = parsed_args.cluster_timeout
1001 if parsed_args.help:
1002 do_basic_help(parser, childargs)
1004 # handle any 'generic' ceph arguments that we didn't parse here
1005 global cluster_handle
1007 # rados.Rados() will call rados_create2, and then read the conf file,
1008 # and then set the keys from the dict. So we must do these
1009 # "pre-file defaults" first (see common_preinit in librados)
1011 'log_to_stderr': 'true',
1012 'err_to_stderr': 'true',
1013 'log_flush_on_exit': 'true',
1016 if 'injectargs' in childargs:
1017 position = childargs.index('injectargs')
1018 injectargs = childargs[position:]
1019 childargs = childargs[:position]
1021 print('Separate childargs {0} from injectargs {1}'.format(childargs, injectargs),
1027 if parsed_args.cluster:
1028 clustername = parsed_args.cluster
1031 cluster_handle = run_in_thread(rados.Rados,
1032 name=name, clustername=clustername,
1033 conf_defaults=conf_defaults,
1035 retargs = run_in_thread(cluster_handle.conf_parse_argv, childargs)
1036 except rados.Error as e:
1037 print('Error initializing cluster client: {0!r}'.format(e), file=sys.stderr)
1044 # -- means "stop parsing args", but we don't want to see it either
1045 if '--' in childargs:
1046 childargs.remove('--')
1047 if injectargs and '--' in injectargs:
1048 injectargs.remove('--')
1052 if parsed_args.block:
1053 if (len(childargs) >= 2 and
1054 childargs[0] == 'osd' and
1055 childargs[1] in ['deep-scrub', 'scrub']):
1057 waitdata = get_scrub_timestamps(childargs)
1059 if parsed_args.help:
1060 # short default timeout for -h
1064 if childargs and childargs[0] == 'ping' and not parsed_args.help:
1065 if len(childargs) < 2:
1066 print('"ping" requires a monitor name as argument: "ping mon.<id>"', file=sys.stderr)
1068 if parsed_args.completion:
1069 # for completion let timeout be really small
1072 if childargs and childargs[0] == 'ping' and not parsed_args.help:
1073 return ping_monitor(cluster_handle, childargs[1], timeout)
1074 result = run_in_thread(cluster_handle.connect, timeout=timeout)
1075 if type(result) is tuple and result[0] == -errno.EINTR:
1076 print('Cluster connection interrupted or timed out', file=sys.stderr)
1078 except KeyboardInterrupt:
1079 print('Cluster connection aborted', file=sys.stderr)
1081 except rados.PermissionDeniedError as e:
1082 print(str(e), file=sys.stderr)
1084 except Exception as e:
1085 print(str(e), file=sys.stderr)
1088 if parsed_args.help:
1090 if len(childargs) >= 2 and childargs[0] == 'tell':
1091 target = childargs[1].split('.', 1)
1092 if not validate_target(target):
1093 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)
1095 childargs = childargs[2:]
1096 hdr('Tell %s commands:' % target[0])
1098 hdr('Monitor commands:')
1099 target = ('mon', '')
1101 print('[Contacting monitor, timeout after %d seconds]' % timeout)
1103 return do_extended_help(parser, childargs, target, ' '.join(childargs))
1105 # implement "tell service.id help"
1106 if len(childargs) >= 3 and childargs[0] == 'tell' and childargs[2] == 'help':
1107 target = childargs[1].split('.', 1)
1108 if validate_target(target):
1109 hdr('Tell %s commands' % target[0])
1110 return do_extended_help(parser, childargs, target, None)
1112 print('target {0} doesn\'t exists, please pass correct target to tell command, such as mon.a/'
1113 'osd.1/mds.a/mgr'.format(childargs[1]), file=sys.stderr)
1116 # implement -w/--watch_*
1117 # This is ugly, but Namespace() isn't quite rich enough.
1119 for k, v in parsed_args._get_kwargs():
1120 if k.startswith('watch') and v:
1123 elif k != "watch_channel":
1124 level = k.replace('watch_', '')
1126 # an awfully simple callback
1127 def watch_cb(arg, line, channel, name, who, stamp_sec, stamp_nsec, seq, level, msg):
1129 channel = channel.decode('utf-8')
1130 if parsed_args.watch_channel in (channel, '*'):
1131 print(line.decode('utf-8'))
1134 # first do a ceph status
1135 ret, outbuf, outs = json_command(cluster_handle, prefix='status')
1137 print("status query failed: ", outs, file=sys.stderr)
1139 print(outbuf.decode('utf-8'))
1141 # this instance keeps the watch connection alive, but is
1143 run_in_thread(cluster_handle.monitor_log2, level, watch_cb, 0)
1145 # loop forever letting watch_cb print lines
1148 except KeyboardInterrupt:
1149 # or until ^C, at least
1152 # read input file, if any
1154 if parsed_args.input_file:
1156 if parsed_args.input_file == '-':
1157 inbuf = sys.stdin.buffer.read()
1159 with open(parsed_args.input_file, 'rb') as f:
1161 except Exception as e:
1162 print('Can\'t open input file {0}: {1}'.format(parsed_args.input_file, e), file=sys.stderr)
1165 # prepare output file, if any
1166 if parsed_args.output_file:
1168 if parsed_args.output_file == '-':
1169 outf = sys.stdout.buffer
1171 outf = open(parsed_args.output_file, 'wb')
1172 except Exception as e:
1173 print('Can\'t open output file {0}: {1}'.format(parsed_args.output_file, e), file=sys.stderr)
1175 if parsed_args.setuser:
1177 ownerid = pwd.getpwnam(parsed_args.setuser).pw_uid
1178 os.fchown(outf.fileno(), ownerid, -1)
1179 except OSError as e:
1180 print('Failed to change user ownership of {0} to {1}: {2}'.format(outf, parsed_args.setuser, e))
1182 if parsed_args.setgroup:
1184 groupid = grp.getgrnam(parsed_args.setgroup).gr_gid
1185 os.fchown(outf.fileno(), -1, groupid)
1186 except OSError as e:
1187 print('Failed to change group ownership of {0} to {1}: {2}'.format(outf, parsed_args.setgroup, e))
1190 # -s behaves like a command (ceph status).
1191 if parsed_args.status:
1192 childargs.insert(0, 'status')
1195 target = find_cmd_target(childargs)
1196 except Exception as e:
1197 print('error handling command target: {0}'.format(e), file=sys.stderr)
1200 # Repulsive hack to handle tell: lop off 'tell' and target
1201 # and validate the rest of the command. 'target' is already
1202 # determined in our callers, so it's ok to remove it here.
1204 if len(childargs) and childargs[0] == 'tell':
1205 childargs = childargs[2:]
1210 childargs = injectargs
1211 if not len(childargs):
1212 print('"{0} tell" requires additional arguments.'.format(sys.argv[0]),
1213 'Try "{0} tell <name> <command> [options...]" instead.'.format(sys.argv[0]),
1217 # fetch JSON sigs from command
1218 # each line contains one command signature (a placeholder name
1219 # of the form 'cmdNNN' followed by an array of argument descriptors)
1220 # as part of the validated argument JSON object
1222 if target[1] == '*':
1224 targets = [(service, o) for o in ids_by_service(service)]
1229 for target in targets:
1230 # prettify? prefix output with target, if there was a wildcard used
1233 if not parsed_args.output_file and len(targets) > 1:
1234 prefix = '{0}.{1}: '.format(*target)
1237 ret, outbuf, outs = json_command(cluster_handle, target=target,
1238 prefix='get_command_descriptions')
1240 where = '{0}.{1}'.format(*target)
1242 raise RuntimeError('Unexpected return code from {0}: {1}'.
1244 outs = 'problem getting command descriptions from {0}'.format(where)
1246 sigdict = parse_json_funcsigs(outbuf.decode('utf-8'), 'cli')
1248 if parsed_args.completion:
1249 return complete(sigdict, childargs, target)
1251 ret, outbuf, outs = new_style_command(parsed_args, childargs,
1252 target, sigdict, inbuf,
1255 # debug tool: send any successful command *again* to
1256 # verify that it is idempotent.
1257 if not ret and 'CEPH_CLI_TEST_DUP_COMMAND' in os.environ:
1258 ret, outbuf, outs = new_style_command(parsed_args, childargs,
1259 target, sigdict, inbuf,
1264 'Second attempt of previously successful command '
1265 'failed with {0}: {1}'.format(
1266 errno.errorcode.get(ret, 'Unknown'), outs),
1271 if parsed_args.output_file:
1274 # hack: old code printed status line before many json outputs
1275 # (osd dump, etc.) that consumers know to ignore. Add blank line
1276 # to satisfy consumers that skip the first line, but not annoy
1277 # consumers that don't.
1278 if parsed_args.output_format and \
1279 parsed_args.output_format.startswith('json'):
1282 # if we are prettifying things, normalize newlines. sigh.
1284 outbuf = outbuf.rstrip()
1287 print(prefix, end='')
1288 # Write directly to binary stdout
1290 print(suffix, end='')
1291 except IOError as e:
1292 if e.errno != errno.EPIPE:
1297 except IOError as e:
1298 if e.errno != errno.EPIPE:
1303 errstr = errno.errorcode.get(ret, 'Unknown')
1304 print('Error {0}: {1}'.format(errstr, outs), file=sys.stderr)
1307 print(prefix + outs, file=sys.stderr)
1312 # Block until command completion (currently scrub and deep_scrub only)
1314 wait(childargs, waitdata)
1316 if parsed_args.output_file and parsed_args.output_file != '-':
1324 if __name__ == '__main__':
1327 # shutdown explicitly; Rados() does not
1328 if retval == 0 and cluster_handle:
1329 run_in_thread(cluster_handle.shutdown)
1330 except KeyboardInterrupt:
1331 print('Interrupted')
1332 retval = errno.EINTR
1335 # flush explicitly because we aren't exiting in the usual way