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'], dest='output_format')
341 parser.add_argument('--connect-timeout', dest='cluster_timeout',
343 help='set a timeout for connecting to the cluster')
345 parser.add_argument('--block', action='store_true',
346 help='block until completion (scrub and deep-scrub only)')
347 parser.add_argument('--period', '-p', default=1, type=float,
348 help='polling period, default 1.0 second (for ' \
349 'polling commands only)')
351 # returns a Namespace with the parsed args, and a list of all extras
352 parsed_args, extras = parser.parse_known_args(args)
354 return parser, parsed_args, extras
358 print('\n', s, '\n', '=' * len(s))
361 def do_basic_help(parser, args):
363 Print basic parser help
364 If the cluster is available, get and print monitor help
366 hdr('General usage:')
368 print_locally_handled_command_help()
371 def print_locally_handled_command_help():
372 hdr("Local commands:")
374 ping <mon.id> Send simple presence/life test to a mon
375 <mon.id> may be 'mon.*' for all mons
376 daemon {type.id|path} <cmd>
377 Same as --admin-daemon, but auto-find admin socket
378 daemonperf {type.id | path} [stat-pats] [priority] [<interval>] [<count>]
379 daemonperf {type.id | path} list|ls [stat-pats] [priority]
380 Get selected perf stats from daemon/admin socket
381 Optional shell-glob comma-delim match string stat-pats
382 Optional selection priority (can abbreviate name):
383 critical, interesting, useful, noninteresting, debug
384 List shows a table of all available stats
385 Run <count> times (default forever),
386 once per <interval> seconds (default 1)
387 """, file=sys.stdout)
390 def do_extended_help(parser, args, target, partial) -> int:
391 def help_for_sigs(sigs, partial=None):
394 out = format_help(parse_json_funcsigs(sigs, 'cli'),
396 if not out and partial:
397 # shorten partial until we get at least one matching command prefix
398 partial = ' '.join(partial.split()[:-1])
400 sys.stdout.write(out)
402 except BrokenPipeError:
405 def help_for_target(target, partial=None):
406 # wait for osdmap because we know this is sent after the mgrmap
407 # and monmap (it's alphabetical).
408 cluster_handle.wait_for_latest_osdmap()
409 ret, outbuf, outs = json_command(cluster_handle, target=target,
410 prefix='get_command_descriptions',
413 if (ret == -errno.EPERM or ret == -errno.EACCES) and target[0] in ('osd', 'mds'):
414 print("Permission denied. Check that your user has 'allow *' "
415 "capabilities for the target daemon type.", file=sys.stderr)
416 elif ret == -errno.EPERM:
417 print("Permission denied. Check your user has proper "
418 "capabilities configured", file=sys.stderr)
420 print("couldn't get command descriptions for {0}: {1} ({2})".
421 format(target, outs, ret), file=sys.stderr)
424 return help_for_sigs(outbuf.decode('utf-8'), partial)
426 assert(cluster_handle.state == "connected")
427 return help_for_target(target, partial)
429 DONTSPLIT = string.ascii_letters + '{[<>]}'
432 def wrap(s, width, indent):
434 generator to transform s into a sequence of strings width or shorter,
435 for wrapping text to a specific column width.
436 Attempt to break on anything but DONTSPLIT characters.
437 indent is amount to indent 2nd-through-nth lines.
439 so "long string long string long string" width=11 indent=1 becomes
440 'long string', ' long string', ' long string' so that it can be printed
453 # no splitting; just possibly indent
460 while (splitpos > 0) and (s[splitpos-1] in DONTSPLIT):
467 # prior result means we're mid-iteration, indent
470 # first time, set leader and width for next
471 leader = ' ' * indent
472 width -= 1 # for subsequent space additions
474 # remove any leading spaces in this chunk of s
475 result += s[:splitpos].lstrip()
481 def format_help(cmddict, partial=None) -> str:
483 Formats all the cmdsigs and helptexts from cmddict into a sorted-by-
484 cmdsig 2-column display, with each column wrapped and indented to
485 fit into (terminal_width / 2) characters.
489 for cmd in sorted(cmddict.values(), key=descsort_key):
493 flags = cmd.get('flags', 0)
494 if flags & (Flag.OBSOLETE | Flag.DEPRECATED | Flag.HIDDEN):
496 concise = concise_sig(cmd['sig'])
497 if partial and not concise.startswith(partial):
499 width = Termsize().cols - 1 # 1 for the line between sig and help
500 sig_width = int(width / 2)
501 # make sure width == sig_width + help_width, even (width % 2 > 0)
502 help_width = int(width / 2) + (width % 2)
503 siglines = [l for l in wrap(concise, sig_width, 1)]
504 helplines = [l for l in wrap(cmd['help'], help_width, 1)]
506 # make lists the same length
507 maxlen = max(len(siglines), len(helplines))
508 siglines.extend([''] * (maxlen - len(siglines)))
509 helplines.extend([''] * (maxlen - len(helplines)))
511 # so we can zip them for output
512 for s, h in zip(siglines, helplines):
513 fullusage += '{s:{w}s} {h}\n'.format(s=s, h=h, w=sig_width)
518 def ceph_conf(parsed_args, field, name, pid=None):
520 bindir = os.path.dirname(__file__)
521 if shutil.which(cmd):
523 elif shutil.which(cmd, path=bindir):
524 args = [os.path.join(bindir, cmd)]
526 raise RuntimeError('"ceph-conf" not found')
529 args.extend(['--name', name])
531 args.extend(['--pid', pid])
533 # add any args in GLOBAL_ARGS
534 for key, val in GLOBAL_ARGS.items():
535 # ignore name in favor of argument name, if any
536 if name and key == 'client_name':
538 if getattr(parsed_args, key):
539 args.extend([val, getattr(parsed_args, key)])
541 args.extend(['--show-config-value', field])
542 p = subprocess.Popen(
544 stdout=subprocess.PIPE,
545 stderr=subprocess.PIPE)
546 outdata, errdata = p.communicate()
547 if p.returncode != 0:
548 raise RuntimeError('unable to get conf option %s for %s: %s' % (field, name, errdata))
549 return outdata.rstrip()
554 if sys.stdin.isatty():
557 line = input(PROMPT).rstrip()
558 if line in ['q', 'quit', 'Q', 'exit']:
565 line = sys.stdin.readline()
573 def do_command(parsed_args, target, cmdargs, sigdict, inbuf, verbose):
574 ''' Validate a command, and handle the polling flag '''
576 valid_dict = validate_command(sigdict, cmdargs, verbose)
577 # Validate input args against list of sigs
579 if parsed_args.output_format:
580 valid_dict['format'] = parsed_args.output_format
582 print("Submitting command: ", valid_dict, file=sys.stderr)
584 return -errno.EINVAL, '', 'invalid command'
586 next_header_print = 0
587 # Set extra options for polling commands only:
588 if valid_dict.get('poll', False):
589 valid_dict['width'] = Termsize().cols
592 # Only print the header for polling commands
593 if next_header_print == 0 and valid_dict.get('poll', False):
594 valid_dict['print_header'] = True
595 next_header_print = Termsize().rows - 3
596 next_header_print -= 1
597 ret, outbuf, outs = json_command(cluster_handle, target=target,
598 argdict=valid_dict, inbuf=inbuf, verbose=verbose)
599 if valid_dict.get('poll', False):
600 valid_dict['print_header'] = False
601 if not valid_dict.get('poll', False):
602 # Don't print here if it's not a polling command
606 print('Error: {0} {1}'.format(ret, errno.errorcode.get(ret, 'Unknown')),
610 print(outbuf.decode('utf-8'))
612 print(outs, file=sys.stderr)
613 if parsed_args.period <= 0:
615 sleep(parsed_args.period)
616 except KeyboardInterrupt:
618 return errno.EINTR, '', ''
619 if ret == errno.ETIMEDOUT:
622 outs = ("Connection timed out. Please check the client's " +
623 "permission and connection.")
624 return ret, outbuf, outs
627 def new_style_command(parsed_args,
631 inbuf, verbose) -> Tuple[int, bytes, str]:
633 Do new-style command dance.
634 target: daemon to receive command: mon (any) or osd.N
635 sigdict - the parsed output from the new monitor describing commands
636 inbuf - any -i input file data
640 for cmdtag in sorted(sigdict.keys()):
641 cmd = sigdict[cmdtag]
643 print('{0}: {1}'.format(cmdtag, concise_sig(sig)))
646 # Non interactive mode
647 ret, outbuf, outs = do_command(parsed_args, target, cmdargs, sigdict, inbuf, verbose)
649 # Interactive mode (ceph cli)
650 if sys.stdin.isatty():
651 # do the command-interpreter looping
652 # for input to do readline cmd editing
653 import readline # noqa
657 interactive_input = read_input()
659 # leave user an uncluttered prompt
661 if interactive_input is None:
663 cmdargs = parse_cmdargs(shlex.split(interactive_input))[2]
665 target = find_cmd_target(cmdargs)
666 except Exception as e:
667 print('error handling command target: {0}'.format(e),
670 if len(cmdargs) and cmdargs[0] == 'tell':
671 print('Can not use \'tell\' in interactive mode.',
674 ret, outbuf, outs = do_command(parsed_args, target, cmdargs,
675 sigdict, inbuf, verbose)
678 errstr = errno.errorcode.get(ret, 'Unknown')
679 print('Error {0}: {1}'.format(errstr, outs), file=sys.stderr)
682 print(outs, file=sys.stderr)
684 print(outbuf.decode('utf-8'))
686 return ret, outbuf, outs
689 def complete(sigdict, args, target):
691 Command completion. Match as much of [args] as possible,
692 and print every possible match separated by newlines.
695 # XXX this looks a lot like the front of validate_command(). Refactor?
697 # Repulsive hack to handle tell: lop off 'tell' and target
698 # and validate the rest of the command. 'target' is already
699 # determined in our callers, so it's ok to remove it here.
700 if len(args) and args[0] == 'tell':
702 # look for best match, accumulate possibles in bestcmds
703 # (so we can maybe give a more-useful error message)
707 for cmdtag, cmd in sigdict.items():
708 flags = cmd.get('flags', 0)
709 if flags & (Flag.OBSOLETE | Flag.HIDDEN):
713 # iterate over all arguments, except last one
714 for arg in args[0:-1]:
716 # an out of argument definitions
718 found_match = arg in sig[j].complete(arg)
719 if not found_match and sig[j].req:
720 # no elements that match
725 # successfully matched all - except last one - arguments
726 if j < len(sig) and len(args) > 0:
727 comps += sig[j].complete(args[-1])
732 if match_count == 1 and len(comps) == 0:
733 # only one command matched and no hints yet => add help
734 comps = comps + [' ', '#'+match_cmd['help']]
735 print('\n'.join(sorted(set(comps))))
739 def ping_monitor(cluster_handle, name, timeout):
740 if 'mon.' not in name:
741 print('"ping" expects a monitor to ping; try "ping mon.<id>"', file=sys.stderr)
744 mon_id = name[len('mon.'):]
746 run_in_thread(cluster_handle.connect, timeout=timeout)
748 s = run_in_thread(cluster_handle.ping_monitor, m)
750 print("mon.{0}".format(m) + '\n' + "Error connecting to monitor.")
752 print("mon.{0}".format(m) + '\n' + s)
754 s = run_in_thread(cluster_handle.ping_monitor, mon_id)
759 def get_admin_socket(parsed_args, name):
760 path = ceph_conf(parsed_args, 'admin_socket', name)
762 if stat.S_ISSOCK(os.stat(path).st_mode):
766 # try harder, probably the "name" option is in the form of
768 parts = name.rsplit('.', 1)
769 if len(parts) > 1 and parts[-1].isnumeric():
771 return ceph_conf(parsed_args, 'admin_socket', name, pid)
776 def maybe_daemon_command(parsed_args, childargs):
778 Check if --admin-socket, daemon, or daemonperf command
779 if it is, returns (boolean handled, return code if handled == True)
784 if parsed_args.admin_socket:
785 sockpath = parsed_args.admin_socket
786 elif len(childargs) > 0 and childargs[0] in ["daemon", "daemonperf"]:
787 daemon_perf = (childargs[0] == "daemonperf")
788 # Treat "daemon <path>" or "daemon <name>" like --admin_daemon <path>
789 # Handle "daemonperf <path>" the same but requires no trailing args
790 require_args = 2 if daemon_perf else 3
791 if len(childargs) >= require_args:
792 if childargs[1].find('/') >= 0:
793 sockpath = childargs[1]
795 # try resolve daemon name
797 sockpath = get_admin_socket(parsed_args, childargs[1])
798 except Exception as e:
799 print('Can\'t get admin socket path: ' + str(e), file=sys.stderr)
800 return True, errno.EINVAL
802 childargs = childargs[2:]
804 print('{0} requires at least {1} arguments'.format(childargs[0], require_args),
806 return True, errno.EINVAL
808 if sockpath and daemon_perf:
809 return True, daemonperf(childargs, sockpath)
812 raw_write(admin_socket(sockpath, childargs, parsed_args.output_format))
813 except Exception as e:
814 print('admin_socket: {0}'.format(e), file=sys.stderr)
815 return True, errno.EINVAL
829 def daemonperf(childargs: Sequence[str], sockpath: str):
831 Handle daemonperf command; returns errno or 0
833 daemonperf <daemon> [priority string] [statpats] [interval] [count]
834 daemonperf <daemon> list|ls [statpats]
843 def prio_from_name(arg):
846 'critical': PRIO_CRITICAL,
847 'interesting': PRIO_INTERESTING,
848 'useful': PRIO_USEFUL,
849 'uninteresting': PRIO_UNINTERESTING,
850 'debugonly': PRIO_DEBUGONLY,
856 for name, val in PRIOMAP.items():
857 if name.startswith(arg):
861 # consume and analyze non-numeric args
862 while len(childargs) and not isnum(childargs[0]):
863 arg = childargs.pop(0)
865 if arg in ['list', 'ls']:
869 prio = prio_from_name(arg)
874 statpats = arg.split(',')
877 priority = PRIO_DEFAULT
879 if len(childargs) > 0:
881 interval = float(childargs.pop(0))
885 print('daemonperf: interval should be a positive number', file=sys.stderr)
888 if len(childargs) > 0:
889 arg = childargs.pop(0)
890 if (not isnum(arg)) or (int(arg) < 0):
891 print('daemonperf: count should be a positive integer', file=sys.stderr)
895 watcher = DaemonWatcher(sockpath, statpats, priority)
899 watcher.run(interval, count)
904 def get_scrub_timestamps(childargs: Sequence[str]) -> Dict[str,
906 last_scrub_stamp = "last_" + childargs[1].replace('-', '_') + "_stamp"
909 if childargs[2] in ['all', 'any', '*']:
911 devnull = open(os.devnull, 'w')
912 out = subprocess.check_output(['ceph', 'pg', 'dump', '--format=json-pretty'],
915 pgstats = json.loads(out)['pg_map']['pg_stats']
917 pgstats = json.loads(out)['pg_stats']
919 if scruball or stat['up_primary'] == int(childargs[2]):
920 scrub_tuple = (stat['up_primary'], stat[last_scrub_stamp])
921 results[stat['pgid']] = scrub_tuple
925 def check_scrub_stamps(waitdata, currdata):
926 for pg in waitdata.keys():
927 # Try to handle the case where a pg may not exist in current results
928 if pg in currdata and waitdata[pg][1] == currdata[pg][1]:
933 def waitscrub(childargs, waitdata):
934 print('Waiting for {0} to complete...'.format(childargs[1]), file=sys.stdout)
935 currdata = get_scrub_timestamps(childargs)
936 while not check_scrub_stamps(waitdata, currdata):
938 currdata = get_scrub_timestamps(childargs)
939 print('{0} completed'.format(childargs[1]), file=sys.stdout)
942 def wait(childargs: Sequence[str], waitdata):
943 if childargs[1] in ['scrub', 'deep-scrub']:
944 waitscrub(childargs, waitdata)
948 ceph_args = os.environ.get('CEPH_ARGS')
950 if "injectargs" in sys.argv:
951 i = sys.argv.index("injectargs")
952 sys.argv = sys.argv[:i] + ceph_args.split() + sys.argv[i:]
954 sys.argv.extend([arg for arg in ceph_args.split()
955 if '--admin-socket' not in arg])
956 parser, parsed_args, childargs = parse_cmdargs()
958 if parsed_args.version:
959 print('ceph version {0} ({1}) {2} ({3})'.format(
963 CEPH_RELEASE_TYPE)) # noqa
966 # --watch-channel|-W implies -w
967 if parsed_args.watch_channel:
968 parsed_args.watch = True
969 elif parsed_args.watch and not parsed_args.watch_channel:
970 parsed_args.watch_channel = 'cluster'
973 verbose = parsed_args.verbose
976 print("parsed_args: {0}, childargs: {1}".format(parsed_args, childargs), file=sys.stderr)
978 # pass on --id, --name, --conf
979 name = 'client.admin'
980 if parsed_args.client_id:
981 name = 'client.' + parsed_args.client_id
982 if parsed_args.client_name:
983 name = parsed_args.client_name
985 conffile = rados.Rados.DEFAULT_CONF_FILES
986 if parsed_args.cephconf:
987 conffile = parsed_args.cephconf
988 # For now, --admin-daemon is handled as usual. Try it
989 # first in case we can't connect() to the cluster
991 done, ret = maybe_daemon_command(parsed_args, childargs)
996 if parsed_args.cluster_timeout:
997 timeout = parsed_args.cluster_timeout
1000 if parsed_args.help:
1001 do_basic_help(parser, childargs)
1003 # handle any 'generic' ceph arguments that we didn't parse here
1004 global cluster_handle
1006 # rados.Rados() will call rados_create2, and then read the conf file,
1007 # and then set the keys from the dict. So we must do these
1008 # "pre-file defaults" first (see common_preinit in librados)
1010 'log_to_stderr': 'true',
1011 'err_to_stderr': 'true',
1012 'log_flush_on_exit': 'true',
1015 if 'injectargs' in childargs:
1016 position = childargs.index('injectargs')
1017 injectargs = childargs[position:]
1018 childargs = childargs[:position]
1020 print('Separate childargs {0} from injectargs {1}'.format(childargs, injectargs),
1026 if parsed_args.cluster:
1027 clustername = parsed_args.cluster
1030 cluster_handle = run_in_thread(rados.Rados,
1031 name=name, clustername=clustername,
1032 conf_defaults=conf_defaults,
1034 retargs = run_in_thread(cluster_handle.conf_parse_argv, childargs)
1035 except rados.Error as e:
1036 print('Error initializing cluster client: {0!r}'.format(e), file=sys.stderr)
1043 # -- means "stop parsing args", but we don't want to see it either
1044 if '--' in childargs:
1045 childargs.remove('--')
1046 if injectargs and '--' in injectargs:
1047 injectargs.remove('--')
1051 if parsed_args.block:
1052 if (len(childargs) >= 2 and
1053 childargs[0] == 'osd' and
1054 childargs[1] in ['deep-scrub', 'scrub']):
1056 waitdata = get_scrub_timestamps(childargs)
1058 if parsed_args.help:
1059 # short default timeout for -h
1063 if childargs and childargs[0] == 'ping' and not parsed_args.help:
1064 if len(childargs) < 2:
1065 print('"ping" requires a monitor name as argument: "ping mon.<id>"', file=sys.stderr)
1067 if parsed_args.completion:
1068 # for completion let timeout be really small
1071 if childargs and childargs[0] == 'ping' and not parsed_args.help:
1072 return ping_monitor(cluster_handle, childargs[1], timeout)
1073 result = run_in_thread(cluster_handle.connect, timeout=timeout)
1074 if type(result) is tuple and result[0] == -errno.EINTR:
1075 print('Cluster connection interrupted or timed out', file=sys.stderr)
1077 except KeyboardInterrupt:
1078 print('Cluster connection aborted', file=sys.stderr)
1080 except rados.PermissionDeniedError as e:
1081 print(str(e), file=sys.stderr)
1083 except Exception as e:
1084 print(str(e), file=sys.stderr)
1087 if parsed_args.help:
1089 if len(childargs) >= 2 and childargs[0] == 'tell':
1090 target = childargs[1].split('.', 1)
1091 if not validate_target(target):
1092 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)
1094 childargs = childargs[2:]
1095 hdr('Tell %s commands:' % target[0])
1097 hdr('Monitor commands:')
1098 target = ('mon', '')
1100 print('[Contacting monitor, timeout after %d seconds]' % timeout)
1102 return do_extended_help(parser, childargs, target, ' '.join(childargs))
1104 # implement "tell service.id help"
1105 if len(childargs) >= 3 and childargs[0] == 'tell' and childargs[2] == 'help':
1106 target = childargs[1].split('.', 1)
1107 if validate_target(target):
1108 hdr('Tell %s commands' % target[0])
1109 return do_extended_help(parser, childargs, target, None)
1111 print('target {0} doesn\'t exists, please pass correct target to tell command, such as mon.a/'
1112 'osd.1/mds.a/mgr'.format(childargs[1]), file=sys.stderr)
1115 # implement -w/--watch_*
1116 # This is ugly, but Namespace() isn't quite rich enough.
1118 for k, v in parsed_args._get_kwargs():
1119 if k.startswith('watch') and v:
1122 elif k != "watch_channel":
1123 level = k.replace('watch_', '')
1125 # an awfully simple callback
1126 def watch_cb(arg, line, channel, name, who, stamp_sec, stamp_nsec, seq, level, msg):
1128 channel = channel.decode('utf-8')
1129 if parsed_args.watch_channel in (channel, '*'):
1130 print(line.decode('utf-8'))
1133 # first do a ceph status
1134 ret, outbuf, outs = json_command(cluster_handle, prefix='status')
1136 print("status query failed: ", outs, file=sys.stderr)
1138 print(outbuf.decode('utf-8'))
1140 # this instance keeps the watch connection alive, but is
1142 run_in_thread(cluster_handle.monitor_log2, level, watch_cb, 0)
1144 # loop forever letting watch_cb print lines
1147 except KeyboardInterrupt:
1148 # or until ^C, at least
1151 # read input file, if any
1153 if parsed_args.input_file:
1155 if parsed_args.input_file == '-':
1156 inbuf = sys.stdin.buffer.read()
1158 with open(parsed_args.input_file, 'rb') as f:
1160 except Exception as e:
1161 print('Can\'t open input file {0}: {1}'.format(parsed_args.input_file, e), file=sys.stderr)
1164 # prepare output file, if any
1165 if parsed_args.output_file:
1167 if parsed_args.output_file == '-':
1168 outf = sys.stdout.buffer
1170 outf = open(parsed_args.output_file, 'wb')
1171 except Exception as e:
1172 print('Can\'t open output file {0}: {1}'.format(parsed_args.output_file, e), file=sys.stderr)
1174 if parsed_args.setuser:
1176 ownerid = pwd.getpwnam(parsed_args.setuser).pw_uid
1177 os.fchown(outf.fileno(), ownerid, -1)
1178 except OSError as e:
1179 print('Failed to change user ownership of {0} to {1}: {2}'.format(outf, parsed_args.setuser, e))
1181 if parsed_args.setgroup:
1183 groupid = grp.getgrnam(parsed_args.setgroup).gr_gid
1184 os.fchown(outf.fileno(), -1, groupid)
1185 except OSError as e:
1186 print('Failed to change group ownership of {0} to {1}: {2}'.format(outf, parsed_args.setgroup, e))
1189 # -s behaves like a command (ceph status).
1190 if parsed_args.status:
1191 childargs.insert(0, 'status')
1194 target = find_cmd_target(childargs)
1195 except Exception as e:
1196 print('error handling command target: {0}'.format(e), file=sys.stderr)
1199 # Repulsive hack to handle tell: lop off 'tell' and target
1200 # and validate the rest of the command. 'target' is already
1201 # determined in our callers, so it's ok to remove it here.
1203 if len(childargs) and childargs[0] == 'tell':
1204 childargs = childargs[2:]
1209 childargs = injectargs
1210 if not len(childargs):
1211 print('"{0} tell" requires additional arguments.'.format(sys.argv[0]),
1212 'Try "{0} tell <name> <command> [options...]" instead.'.format(sys.argv[0]),
1216 # fetch JSON sigs from command
1217 # each line contains one command signature (a placeholder name
1218 # of the form 'cmdNNN' followed by an array of argument descriptors)
1219 # as part of the validated argument JSON object
1221 if target[1] == '*':
1223 targets = [(service, o) for o in ids_by_service(service)]
1228 for target in targets:
1229 # prettify? prefix output with target, if there was a wildcard used
1232 if not parsed_args.output_file and len(targets) > 1:
1233 prefix = '{0}.{1}: '.format(*target)
1236 ret, outbuf, outs = json_command(cluster_handle, target=target,
1237 prefix='get_command_descriptions')
1239 where = '{0}.{1}'.format(*target)
1241 raise RuntimeError('Unexpected return code from {0}: {1}'.
1243 outs = 'problem getting command descriptions from {0}'.format(where)
1245 sigdict = parse_json_funcsigs(outbuf.decode('utf-8'), 'cli')
1247 if parsed_args.completion:
1248 return complete(sigdict, childargs, target)
1250 ret, outbuf, outs = new_style_command(parsed_args, childargs,
1251 target, sigdict, inbuf,
1254 # debug tool: send any successful command *again* to
1255 # verify that it is idempotent.
1256 if not ret and 'CEPH_CLI_TEST_DUP_COMMAND' in os.environ:
1257 ret, outbuf, outs = new_style_command(parsed_args, childargs,
1258 target, sigdict, inbuf,
1263 'Second attempt of previously successful command '
1264 'failed with {0}: {1}'.format(
1265 errno.errorcode.get(ret, 'Unknown'), outs),
1270 if parsed_args.output_file:
1273 # hack: old code printed status line before many json outputs
1274 # (osd dump, etc.) that consumers know to ignore. Add blank line
1275 # to satisfy consumers that skip the first line, but not annoy
1276 # consumers that don't.
1277 if parsed_args.output_format and \
1278 parsed_args.output_format.startswith('json'):
1281 # if we are prettifying things, normalize newlines. sigh.
1283 outbuf = outbuf.rstrip()
1286 print(prefix, end='')
1287 # Write directly to binary stdout
1289 print(suffix, end='')
1290 except IOError as e:
1291 if e.errno != errno.EPIPE:
1296 except IOError as e:
1297 if e.errno != errno.EPIPE:
1302 errstr = errno.errorcode.get(ret, 'Unknown')
1303 print('Error {0}: {1}'.format(errstr, outs), file=sys.stderr)
1306 print(prefix + outs, file=sys.stderr)
1311 # Block until command completion (currently scrub and deep_scrub only)
1313 wait(childargs, waitdata)
1315 if parsed_args.output_file and parsed_args.output_file != '-':
1323 if __name__ == '__main__':
1326 # shutdown explicitly; Rados() does not
1327 if retval == 0 and cluster_handle:
1328 run_in_thread(cluster_handle.shutdown)
1329 except KeyboardInterrupt:
1330 print('Interrupted')
1331 retval = errno.EINTR
1334 # flush explicitly because we aren't exiting in the usual way