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
32 from typing import Dict, List, Sequence, Tuple
39 CEPH_GIT_VER = "@CEPH_GIT_VER@"
40 CEPH_GIT_NICE_VER = "@CEPH_GIT_NICE_VER@"
41 CEPH_RELEASE = "@CEPH_RELEASE@"
42 CEPH_RELEASE_NAME = "@CEPH_RELEASE_NAME@"
43 CEPH_RELEASE_TYPE = "@CEPH_RELEASE_TYPE@"
45 # priorities from src/common/perf_counters.h
49 PRIO_UNINTERESTING = 2
52 PRIO_DEFAULT = PRIO_INTERESTING
54 # Make life easier on developers:
55 # If our parent dir contains CMakeCache.txt and bin/init-ceph,
56 # assume we're running from a build dir (i.e. src/build/bin/ceph)
57 # and tweak sys.path and LD_LIBRARY_PATH to use built files.
58 # Since this involves re-execing, if CEPH_DBG is set in the environment
59 # re-exec with -mpdb. Also, if CEPH_DEV is in the env, suppress
60 # the warning message about the DEVELOPER MODE.
62 MYPATH = os.path.abspath(__file__)
63 MYDIR = os.path.dirname(MYPATH)
64 MYPDIR = os.path.dirname(MYDIR)
65 DEVMODEMSG = '*** DEVELOPER MODE: setting PATH, PYTHONPATH and LD_LIBRARY_PATH ***'
68 def respawn_in_path(lib_path, pybind_path, pythonlib_path, asan_lib_path):
70 if 'CEPH_DBG' in os.environ:
71 execv_cmd += ['@Python3_EXECUTABLE@', '-mpdb']
73 if platform.system() == "Darwin":
74 lib_path_var = "DYLD_LIBRARY_PATH"
76 lib_path_var = "LD_LIBRARY_PATH"
80 os.environ['LD_PRELOAD'] = asan_lib_path
81 if lib_path_var in os.environ:
82 if lib_path not in os.environ[lib_path_var]:
83 os.environ[lib_path_var] += ':' + lib_path
84 if "CEPH_DEV" not in os.environ:
85 print(DEVMODEMSG, file=sys.stderr)
86 os.execvp(execv_cmd[0], execv_cmd)
88 os.environ[lib_path_var] = lib_path
89 if "CEPH_DEV" not in os.environ:
90 print(DEVMODEMSG, file=sys.stderr)
91 os.execvp(execv_cmd[0], execv_cmd)
92 sys.path.insert(0, pybind_path)
93 sys.path.insert(0, pythonlib_path)
96 def get_pythonlib_dir():
97 """Returns the name of a distutils build directory"""
98 return "lib.{version[0]}".format(version=sys.version_info)
101 def get_cmake_variables(*names):
102 vars = dict((name, None) for name in names)
103 for line in open(os.path.join(MYPDIR, "CMakeCache.txt")):
104 # parse lines like "WITH_ASAN:BOOL=ON"
106 if line.startswith("{}:".format(name)):
107 type_value = line.split(":")[1].strip()
108 t, v = type_value.split("=")
110 v = v.upper() in ('TRUE', '1', 'Y', 'YES', 'ON')
113 if all(vars.values()):
115 return [vars[name] for name in names]
118 if os.path.exists(os.path.join(MYPDIR, "CMakeCache.txt")) \
119 and os.path.exists(os.path.join(MYPDIR, "bin/init-ceph")):
120 src_path, with_asan, asan_lib_path = \
121 get_cmake_variables("ceph_SOURCE_DIR", "WITH_ASAN", "ASAN_LIBRARY")
123 # Huh, maybe we're not really in a cmake environment?
126 # Developer mode, but in a cmake build dir instead of the src dir
127 lib_path = os.path.join(MYPDIR, "lib")
128 bin_path = os.path.join(MYPDIR, "bin")
129 pybind_path = os.path.join(src_path, "src", "pybind")
130 pythonlib_path = os.path.join(lib_path,
133 respawn_in_path(lib_path, pybind_path, pythonlib_path,
134 asan_lib_path if with_asan else None)
136 if 'PATH' in os.environ and bin_path not in os.environ['PATH']:
137 os.environ['PATH'] = os.pathsep.join([bin_path, os.environ['PATH']])
148 from ceph_argparse import \
149 concise_sig, descsort_key, parse_json_funcsigs, \
150 validate_command, find_cmd_target, \
151 json_command, run_in_thread, Flag
153 from ceph_daemon import admin_socket, DaemonWatcher, Termsize
155 # just a couple of globals
158 cluster_handle = None
163 sys.stdout.buffer.write(buf)
167 ret, outbuf, outs = json_command(cluster_handle, prefix='osd ls')
169 raise RuntimeError('Can\'t contact mon for osd list')
170 return [line.decode('utf-8') for line in outbuf.split(b'\n') if line]
174 ret, outbuf, outs = json_command(cluster_handle, prefix='mon dump',
175 argdict={'format': 'json'})
177 raise RuntimeError('Can\'t contact mon for mon list')
178 d = json.loads(outbuf.decode('utf-8'))
179 return [m['name'] for m in d['mons']]
183 ret, outbuf, outs = json_command(cluster_handle, prefix='fs dump',
184 argdict={'format': 'json'})
186 raise RuntimeError('Can\'t contact mon for mds list')
187 d = json.loads(outbuf.decode('utf-8'))
189 for info in d['standbys']:
190 l.append(info['name'])
191 for fs in d['filesystems']:
192 for info in fs['mdsmap']['info'].values():
193 l.append(info['name'])
198 ret, outbuf, outs = json_command(cluster_handle, prefix='mgr dump',
199 argdict={'format': 'json'})
201 raise RuntimeError('Can\'t contact mon for mgr list')
203 d = json.loads(outbuf.decode('utf-8'))
205 l.append(d['active_name'])
206 # we can only send tell commands to the active mgr
207 #for i in d['standbys']:
208 # l.append(i['name'])
212 def ids_by_service(service):
213 ids = {"mon": monids,
217 return ids[service]()
220 def validate_target(target):
222 this function will return true iff target is a correct
223 target, such as mon.a/osd.2/mds.a/mgr.
225 target: array, likes ['osd', '2']
226 return: bool, or raise RuntimeError
230 # for case "service.id"
231 service_name, service_id = target[0], target[1]
233 exist_ids = ids_by_service(service_name)
235 print('WARN: {0} is not a legal service name, should be one of mon/osd/mds/mgr'.format(service_name),
239 if service_id in exist_ids or len(exist_ids) > 0 and service_id == '*':
242 print('WARN: the service id you provided does not exist. service id should '
243 'be one of {0}.'.format('/'.join(exist_ids)), file=sys.stderr)
246 elif len(target) == 1 and target[0] in ['mgr', 'mon']:
249 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)
253 # these args must be passed to all child programs
256 'client_name': '--name',
257 'cluster': '--cluster',
258 'cephconf': '--conf',
262 def parse_cmdargs(args=None, target='') -> Tuple[argparse.ArgumentParser,
266 Consume generic arguments from the start of the ``args``
267 list. Call this first to handle arguments that are not
268 handled by a command description provided by the server.
270 :returns: three tuple of ArgumentParser instance, Namespace instance
271 containing parsed values, and list of un-handled arguments
273 # alias: let the line-wrapping be sane
274 AP = argparse.ArgumentParser
276 # format our own help
277 parser = AP(description='Ceph administration tool', add_help=False)
279 parser.add_argument('--completion', action='store_true',
280 help=argparse.SUPPRESS)
282 parser.add_argument('-h', '--help', help='request mon help',
285 parser.add_argument('-c', '--conf', dest='cephconf',
286 help='ceph configuration file')
287 parser.add_argument('-i', '--in-file', dest='input_file',
288 help='input file, or "-" for stdin')
289 parser.add_argument('-o', '--out-file', dest='output_file',
290 help='output file, or "-" for stdout')
291 parser.add_argument('--setuser', dest='setuser',
292 help='set user file permission')
293 parser.add_argument('--setgroup', dest='setgroup',
294 help='set group file permission')
295 parser.add_argument('--id', '--user', dest='client_id',
296 help='client id for authentication')
297 parser.add_argument('--name', '-n', dest='client_name',
298 help='client name for authentication')
299 parser.add_argument('--cluster', help='cluster name')
301 parser.add_argument('--admin-daemon', dest='admin_socket',
302 help='submit admin-socket commands (\"help\" for help)')
304 parser.add_argument('-s', '--status', action='store_true',
305 help='show cluster status')
307 parser.add_argument('-w', '--watch', action='store_true',
308 help='watch live cluster changes')
309 parser.add_argument('--watch-debug', action='store_true',
310 help='watch debug events')
311 parser.add_argument('--watch-info', action='store_true',
312 help='watch info events')
313 parser.add_argument('--watch-sec', action='store_true',
314 help='watch security events')
315 parser.add_argument('--watch-warn', action='store_true',
316 help='watch warn events')
317 parser.add_argument('--watch-error', action='store_true',
318 help='watch error events')
320 parser.add_argument('-W', '--watch-channel', dest="watch_channel",
321 help="watch live cluster changes on a specific channel "
322 "(e.g., cluster, audit, cephadm, or '*' for all)")
324 parser.add_argument('--version', '-v', action="store_true", help="display version")
325 parser.add_argument('--verbose', action="store_true", help="make verbose")
326 parser.add_argument('--concise', dest='verbose', action="store_false",
327 help="make less verbose")
329 parser.add_argument('-f', '--format', choices=['json', 'json-pretty',
330 'xml', 'xml-pretty', 'plain', 'yaml'], dest='output_format')
332 parser.add_argument('--connect-timeout', dest='cluster_timeout',
334 help='set a timeout for connecting to the cluster')
336 parser.add_argument('--block', action='store_true',
337 help='block until completion (scrub and deep-scrub only)')
338 parser.add_argument('--period', '-p', default=1, type=float,
339 help='polling period, default 1.0 second (for ' \
340 'polling commands only)')
342 # returns a Namespace with the parsed args, and a list of all extras
343 parsed_args, extras = parser.parse_known_args(args)
345 return parser, parsed_args, extras
349 print('\n', s, '\n', '=' * len(s))
352 def do_basic_help(parser, args):
354 Print basic parser help
355 If the cluster is available, get and print monitor help
357 hdr('General usage:')
359 print_locally_handled_command_help()
362 def print_locally_handled_command_help():
363 hdr("Local commands:")
365 ping <mon.id> Send simple presence/life test to a mon
366 <mon.id> may be 'mon.*' for all mons
367 daemon {type.id|path} <cmd>
368 Same as --admin-daemon, but auto-find admin socket
369 daemonperf {type.id | path} [stat-pats] [priority] [<interval>] [<count>]
370 daemonperf {type.id | path} list|ls [stat-pats] [priority]
371 Get selected perf stats from daemon/admin socket
372 Optional shell-glob comma-delim match string stat-pats
373 Optional selection priority (can abbreviate name):
374 critical, interesting, useful, noninteresting, debug
375 List shows a table of all available stats
376 Run <count> times (default forever),
377 once per <interval> seconds (default 1)
378 """, file=sys.stdout)
381 def do_extended_help(parser, args, target, partial) -> int:
382 def help_for_sigs(sigs, partial=None):
384 sys.stdout.write(format_help(parse_json_funcsigs(sigs, 'cli'),
386 except BrokenPipeError:
389 def help_for_target(target, partial=None):
390 # wait for osdmap because we know this is sent after the mgrmap
391 # and monmap (it's alphabetical).
392 cluster_handle.wait_for_latest_osdmap()
393 ret, outbuf, outs = json_command(cluster_handle, target=target,
394 prefix='get_command_descriptions',
397 if (ret == -errno.EPERM or ret == -errno.EACCES) and target[0] in ('osd', 'mds'):
398 print("Permission denied. Check that your user has 'allow *' "
399 "capabilities for the target daemon type.", file=sys.stderr)
400 elif ret == -errno.EPERM:
401 print("Permission denied. Check your user has proper "
402 "capabilities configured", file=sys.stderr)
404 print("couldn't get command descriptions for {0}: {1} ({2})".
405 format(target, outs, ret), file=sys.stderr)
408 return help_for_sigs(outbuf.decode('utf-8'), partial)
410 assert(cluster_handle.state == "connected")
411 return help_for_target(target, partial)
413 DONTSPLIT = string.ascii_letters + '{[<>]}'
416 def wrap(s, width, indent):
418 generator to transform s into a sequence of strings width or shorter,
419 for wrapping text to a specific column width.
420 Attempt to break on anything but DONTSPLIT characters.
421 indent is amount to indent 2nd-through-nth lines.
423 so "long string long string long string" width=11 indent=1 becomes
424 'long string', ' long string', ' long string' so that it can be printed
437 # no splitting; just possibly indent
444 while (splitpos > 0) and (s[splitpos-1] in DONTSPLIT):
451 # prior result means we're mid-iteration, indent
454 # first time, set leader and width for next
455 leader = ' ' * indent
456 width -= 1 # for subsequent space additions
458 # remove any leading spaces in this chunk of s
459 result += s[:splitpos].lstrip()
465 def format_help(cmddict, partial=None) -> str:
467 Formats all the cmdsigs and helptexts from cmddict into a sorted-by-
468 cmdsig 2-column display, with each column wrapped and indented to
469 fit into (terminal_width / 2) characters.
473 for cmd in sorted(cmddict.values(), key=descsort_key):
477 flags = cmd.get('flags', 0)
478 if flags & (Flag.OBSOLETE | Flag.DEPRECATED | Flag.HIDDEN):
480 concise = concise_sig(cmd['sig'])
481 if partial and not concise.startswith(partial):
483 width = Termsize().cols - 1 # 1 for the line between sig and help
484 sig_width = int(width / 2)
485 # make sure width == sig_width + help_width, even (width % 2 > 0)
486 help_width = int(width / 2) + (width % 2)
487 siglines = [l for l in wrap(concise, sig_width, 1)]
488 helplines = [l for l in wrap(cmd['help'], help_width, 1)]
490 # make lists the same length
491 maxlen = max(len(siglines), len(helplines))
492 siglines.extend([''] * (maxlen - len(siglines)))
493 helplines.extend([''] * (maxlen - len(helplines)))
495 # so we can zip them for output
496 for s, h in zip(siglines, helplines):
497 fullusage += '{s:{w}s} {h}\n'.format(s=s, h=h, w=sig_width)
502 def ceph_conf(parsed_args, field, name, pid=None):
504 bindir = os.path.dirname(__file__)
505 if shutil.which(cmd):
507 elif shutil.which(cmd, path=bindir):
508 args = [os.path.join(bindir, cmd)]
510 raise RuntimeError('"ceph-conf" not found')
513 args.extend(['--name', name])
515 args.extend(['--pid', pid])
517 # add any args in GLOBAL_ARGS
518 for key, val in GLOBAL_ARGS.items():
519 # ignore name in favor of argument name, if any
520 if name and key == 'client_name':
522 if getattr(parsed_args, key):
523 args.extend([val, getattr(parsed_args, key)])
525 args.extend(['--show-config-value', field])
526 p = subprocess.Popen(
528 stdout=subprocess.PIPE,
529 stderr=subprocess.PIPE)
530 outdata, errdata = p.communicate()
531 if p.returncode != 0:
532 raise RuntimeError('unable to get conf option %s for %s: %s' % (field, name, errdata))
533 return outdata.rstrip()
538 if sys.stdin.isatty():
541 line = input(PROMPT).rstrip()
542 if line in ['q', 'quit', 'Q', 'exit']:
549 line = sys.stdin.readline()
557 def do_command(parsed_args, target, cmdargs, sigdict, inbuf, verbose):
558 ''' Validate a command, and handle the polling flag '''
560 valid_dict = validate_command(sigdict, cmdargs, verbose)
561 # Validate input args against list of sigs
563 if parsed_args.output_format:
564 valid_dict['format'] = parsed_args.output_format
566 print("Submitting command: ", valid_dict, file=sys.stderr)
568 return -errno.EINVAL, '', 'invalid command'
570 next_header_print = 0
571 # Set extra options for polling commands only:
572 if valid_dict.get('poll', False):
573 valid_dict['width'] = Termsize().cols
576 # Only print the header for polling commands
577 if next_header_print == 0 and valid_dict.get('poll', False):
578 valid_dict['print_header'] = True
579 next_header_print = Termsize().rows - 3
580 next_header_print -= 1
581 ret, outbuf, outs = json_command(cluster_handle, target=target,
582 argdict=valid_dict, inbuf=inbuf, verbose=verbose)
583 if valid_dict.get('poll', False):
584 valid_dict['print_header'] = False
585 if not valid_dict.get('poll', False):
586 # Don't print here if it's not a polling command
590 print('Error: {0} {1}'.format(ret, errno.errorcode.get(ret, 'Unknown')),
594 print(outbuf.decode('utf-8'))
596 print(outs, file=sys.stderr)
597 if parsed_args.period <= 0:
599 sleep(parsed_args.period)
600 except KeyboardInterrupt:
602 return errno.EINTR, '', ''
603 if ret == errno.ETIMEDOUT:
606 outs = ("Connection timed out. Please check the client's " +
607 "permission and connection.")
608 return ret, outbuf, outs
611 def new_style_command(parsed_args,
615 inbuf, verbose) -> Tuple[int, bytes, str]:
617 Do new-style command dance.
618 target: daemon to receive command: mon (any) or osd.N
619 sigdict - the parsed output from the new monitor describing commands
620 inbuf - any -i input file data
624 for cmdtag in sorted(sigdict.keys()):
625 cmd = sigdict[cmdtag]
627 print('{0}: {1}'.format(cmdtag, concise_sig(sig)))
630 # Non interactive mode
631 ret, outbuf, outs = do_command(parsed_args, target, cmdargs, sigdict, inbuf, verbose)
633 # Interactive mode (ceph cli)
634 if sys.stdin.isatty():
635 # do the command-interpreter looping
636 # for input to do readline cmd editing
637 import readline # noqa
641 interactive_input = read_input()
643 # leave user an uncluttered prompt
645 if interactive_input is None:
647 cmdargs = parse_cmdargs(shlex.split(interactive_input))[2]
649 target = find_cmd_target(cmdargs)
650 except Exception as e:
651 print('error handling command target: {0}'.format(e),
654 if len(cmdargs) and cmdargs[0] == 'tell':
655 print('Can not use \'tell\' in interactive mode.',
658 ret, outbuf, outs = do_command(parsed_args, target, cmdargs,
659 sigdict, inbuf, verbose)
662 errstr = errno.errorcode.get(ret, 'Unknown')
663 print('Error {0}: {1}'.format(errstr, outs), file=sys.stderr)
666 print(outs, file=sys.stderr)
668 print(outbuf.decode('utf-8'))
670 return ret, outbuf, outs
673 def complete(sigdict, args, target):
675 Command completion. Match as much of [args] as possible,
676 and print every possible match separated by newlines.
679 # XXX this looks a lot like the front of validate_command(). Refactor?
681 # Repulsive hack to handle tell: lop off 'tell' and target
682 # and validate the rest of the command. 'target' is already
683 # determined in our callers, so it's ok to remove it here.
684 if len(args) and args[0] == 'tell':
686 # look for best match, accumulate possibles in bestcmds
687 # (so we can maybe give a more-useful error message)
691 for cmdtag, cmd in sigdict.items():
692 flags = cmd.get('flags', 0)
693 if flags & (Flag.OBSOLETE | Flag.HIDDEN):
697 # iterate over all arguments, except last one
698 for arg in args[0:-1]:
700 # an out of argument definitions
702 found_match = arg in sig[j].complete(arg)
703 if not found_match and sig[j].req:
704 # no elements that match
709 # successfully matched all - except last one - arguments
710 if j < len(sig) and len(args) > 0:
711 comps += sig[j].complete(args[-1])
716 if match_count == 1 and len(comps) == 0:
717 # only one command matched and no hints yet => add help
718 comps = comps + [' ', '#'+match_cmd['help']]
719 print('\n'.join(sorted(set(comps))))
723 def ping_monitor(cluster_handle, name, timeout):
724 if 'mon.' not in name:
725 print('"ping" expects a monitor to ping; try "ping mon.<id>"', file=sys.stderr)
728 mon_id = name[len('mon.'):]
730 run_in_thread(cluster_handle.connect, timeout=timeout)
732 s = run_in_thread(cluster_handle.ping_monitor, m)
734 print("mon.{0}".format(m) + '\n' + "Error connecting to monitor.")
736 print("mon.{0}".format(m) + '\n' + s)
738 s = run_in_thread(cluster_handle.ping_monitor, mon_id)
743 def get_admin_socket(parsed_args, name):
744 path = ceph_conf(parsed_args, 'admin_socket', name)
746 if stat.S_ISSOCK(os.stat(path).st_mode):
750 # try harder, probably the "name" option is in the form of
752 parts = name.rsplit('.', 1)
753 if len(parts) > 1 and parts[-1].isnumeric():
755 return ceph_conf(parsed_args, 'admin_socket', name, pid)
760 def maybe_daemon_command(parsed_args, childargs):
762 Check if --admin-socket, daemon, or daemonperf command
763 if it is, returns (boolean handled, return code if handled == True)
768 if parsed_args.admin_socket:
769 sockpath = parsed_args.admin_socket
770 elif len(childargs) > 0 and childargs[0] in ["daemon", "daemonperf"]:
771 daemon_perf = (childargs[0] == "daemonperf")
772 # Treat "daemon <path>" or "daemon <name>" like --admin_daemon <path>
773 # Handle "daemonperf <path>" the same but requires no trailing args
774 require_args = 2 if daemon_perf else 3
775 if len(childargs) >= require_args:
776 if childargs[1].find('/') >= 0:
777 sockpath = childargs[1]
779 # try resolve daemon name
781 sockpath = get_admin_socket(parsed_args, childargs[1])
782 except Exception as e:
783 print('Can\'t get admin socket path: ' + str(e), file=sys.stderr)
784 return True, errno.EINVAL
786 childargs = childargs[2:]
788 print('{0} requires at least {1} arguments'.format(childargs[0], require_args),
790 return True, errno.EINVAL
792 if sockpath and daemon_perf:
793 return True, daemonperf(childargs, sockpath)
796 raw_write(admin_socket(sockpath, childargs, parsed_args.output_format))
797 except Exception as e:
798 print('admin_socket: {0}'.format(e), file=sys.stderr)
799 return True, errno.EINVAL
813 def daemonperf(childargs: Sequence[str], sockpath: str):
815 Handle daemonperf command; returns errno or 0
817 daemonperf <daemon> [priority string] [statpats] [interval] [count]
818 daemonperf <daemon> list|ls [statpats]
827 def prio_from_name(arg):
830 'critical': PRIO_CRITICAL,
831 'interesting': PRIO_INTERESTING,
832 'useful': PRIO_USEFUL,
833 'uninteresting': PRIO_UNINTERESTING,
834 'debugonly': PRIO_DEBUGONLY,
840 for name, val in PRIOMAP.items():
841 if name.startswith(arg):
845 # consume and analyze non-numeric args
846 while len(childargs) and not isnum(childargs[0]):
847 arg = childargs.pop(0)
849 if arg in ['list', 'ls']:
853 prio = prio_from_name(arg)
858 statpats = arg.split(',')
861 priority = PRIO_DEFAULT
863 if len(childargs) > 0:
865 interval = float(childargs.pop(0))
869 print('daemonperf: interval should be a positive number', file=sys.stderr)
872 if len(childargs) > 0:
873 arg = childargs.pop(0)
874 if (not isnum(arg)) or (int(arg) < 0):
875 print('daemonperf: count should be a positive integer', file=sys.stderr)
879 watcher = DaemonWatcher(sockpath, statpats, priority)
883 watcher.run(interval, count)
888 def get_scrub_timestamps(childargs: Sequence[str]) -> Dict[str,
890 last_scrub_stamp = "last_" + childargs[1].replace('-', '_') + "_stamp"
893 if childargs[2] in ['all', 'any', '*']:
895 devnull = open(os.devnull, 'w')
896 out = subprocess.check_output(['ceph', 'pg', 'dump', '--format=json-pretty'],
899 pgstats = json.loads(out)['pg_map']['pg_stats']
901 pgstats = json.loads(out)['pg_stats']
903 if scruball or stat['up_primary'] == int(childargs[2]):
904 scrub_tuple = (stat['up_primary'], stat[last_scrub_stamp])
905 results[stat['pgid']] = scrub_tuple
909 def check_scrub_stamps(waitdata, currdata):
910 for pg in waitdata.keys():
911 # Try to handle the case where a pg may not exist in current results
912 if pg in currdata and waitdata[pg][1] == currdata[pg][1]:
917 def waitscrub(childargs, waitdata):
918 print('Waiting for {0} to complete...'.format(childargs[1]), file=sys.stdout)
919 currdata = get_scrub_timestamps(childargs)
920 while not check_scrub_stamps(waitdata, currdata):
922 currdata = get_scrub_timestamps(childargs)
923 print('{0} completed'.format(childargs[1]), file=sys.stdout)
926 def wait(childargs: Sequence[str], waitdata):
927 if childargs[1] in ['scrub', 'deep-scrub']:
928 waitscrub(childargs, waitdata)
932 ceph_args = os.environ.get('CEPH_ARGS')
934 if "injectargs" in sys.argv:
935 i = sys.argv.index("injectargs")
936 sys.argv = sys.argv[:i] + ceph_args.split() + sys.argv[i:]
938 sys.argv.extend([arg for arg in ceph_args.split()
939 if '--admin-socket' not in arg])
940 parser, parsed_args, childargs = parse_cmdargs()
942 if parsed_args.version:
943 print('ceph version {0} ({1}) {2} ({3})'.format(
947 CEPH_RELEASE_TYPE)) # noqa
950 # --watch-channel|-W implies -w
951 if parsed_args.watch_channel:
952 parsed_args.watch = True
953 elif parsed_args.watch and not parsed_args.watch_channel:
954 parsed_args.watch_channel = 'cluster'
957 verbose = parsed_args.verbose
960 print("parsed_args: {0}, childargs: {1}".format(parsed_args, childargs), file=sys.stderr)
962 # pass on --id, --name, --conf
963 name = 'client.admin'
964 if parsed_args.client_id:
965 name = 'client.' + parsed_args.client_id
966 if parsed_args.client_name:
967 name = parsed_args.client_name
969 # default '' means default conf search
971 if parsed_args.cephconf:
972 conffile = parsed_args.cephconf
973 # For now, --admin-daemon is handled as usual. Try it
974 # first in case we can't connect() to the cluster
976 done, ret = maybe_daemon_command(parsed_args, childargs)
981 if parsed_args.cluster_timeout:
982 timeout = parsed_args.cluster_timeout
986 do_basic_help(parser, childargs)
988 # handle any 'generic' ceph arguments that we didn't parse here
989 global cluster_handle
991 # rados.Rados() will call rados_create2, and then read the conf file,
992 # and then set the keys from the dict. So we must do these
993 # "pre-file defaults" first (see common_preinit in librados)
995 'log_to_stderr': 'true',
996 'err_to_stderr': 'true',
997 'log_flush_on_exit': 'true',
1000 if 'injectargs' in childargs:
1001 position = childargs.index('injectargs')
1002 injectargs = childargs[position:]
1003 childargs = childargs[:position]
1005 print('Separate childargs {0} from injectargs {1}'.format(childargs, injectargs),
1011 if parsed_args.cluster:
1012 clustername = parsed_args.cluster
1015 cluster_handle = run_in_thread(rados.Rados,
1016 name=name, clustername=clustername,
1017 conf_defaults=conf_defaults,
1019 retargs = run_in_thread(cluster_handle.conf_parse_argv, childargs)
1020 except rados.Error as e:
1021 print('Error initializing cluster client: {0!r}'.format(e), file=sys.stderr)
1028 # -- means "stop parsing args", but we don't want to see it either
1029 if '--' in childargs:
1030 childargs.remove('--')
1031 if injectargs and '--' in injectargs:
1032 injectargs.remove('--')
1036 if parsed_args.block:
1037 if (len(childargs) >= 2 and
1038 childargs[0] == 'osd' and
1039 childargs[1] in ['deep-scrub', 'scrub']):
1041 waitdata = get_scrub_timestamps(childargs)
1043 if parsed_args.help:
1044 # short default timeout for -h
1048 if childargs and childargs[0] == 'ping' and not parsed_args.help:
1049 if len(childargs) < 2:
1050 print('"ping" requires a monitor name as argument: "ping mon.<id>"', file=sys.stderr)
1052 if parsed_args.completion:
1053 # for completion let timeout be really small
1056 if childargs and childargs[0] == 'ping' and not parsed_args.help:
1057 return ping_monitor(cluster_handle, childargs[1], timeout)
1058 result = run_in_thread(cluster_handle.connect, timeout=timeout)
1059 if type(result) is tuple and result[0] == -errno.EINTR:
1060 print('Cluster connection interrupted or timed out', file=sys.stderr)
1062 except KeyboardInterrupt:
1063 print('Cluster connection aborted', file=sys.stderr)
1065 except rados.PermissionDeniedError as e:
1066 print(str(e), file=sys.stderr)
1068 except Exception as e:
1069 print(str(e), file=sys.stderr)
1072 if parsed_args.help:
1074 if len(childargs) >= 2 and childargs[0] == 'tell':
1075 target = childargs[1].split('.', 1)
1076 if not validate_target(target):
1077 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)
1079 childargs = childargs[2:]
1080 hdr('Tell %s commands:' % target[0])
1082 hdr('Monitor commands:')
1083 target = ('mon', '')
1085 print('[Contacting monitor, timeout after %d seconds]' % timeout)
1087 return do_extended_help(parser, childargs, target, ' '.join(childargs))
1089 # implement "tell service.id help"
1090 if len(childargs) >= 3 and childargs[0] == 'tell' and childargs[2] == 'help':
1091 target = childargs[1].split('.', 1)
1092 if validate_target(target):
1093 hdr('Tell %s commands' % target[0])
1094 return do_extended_help(parser, childargs, target, None)
1096 print('target {0} doesn\'t exists, please pass correct target to tell command, such as mon.a/'
1097 'osd.1/mds.a/mgr'.format(childargs[1]), file=sys.stderr)
1100 # implement -w/--watch_*
1101 # This is ugly, but Namespace() isn't quite rich enough.
1103 for k, v in parsed_args._get_kwargs():
1104 if k.startswith('watch') and v:
1107 elif k != "watch_channel":
1108 level = k.replace('watch_', '')
1110 # an awfully simple callback
1111 def watch_cb(arg, line, channel, name, who, stamp_sec, stamp_nsec, seq, level, msg):
1113 channel = channel.decode('utf-8')
1114 if parsed_args.watch_channel in (channel, '*'):
1115 print(line.decode('utf-8'))
1118 # first do a ceph status
1119 ret, outbuf, outs = json_command(cluster_handle, prefix='status')
1121 print("status query failed: ", outs, file=sys.stderr)
1123 print(outbuf.decode('utf-8'))
1125 # this instance keeps the watch connection alive, but is
1127 run_in_thread(cluster_handle.monitor_log2, level, watch_cb, 0)
1129 # loop forever letting watch_cb print lines
1132 except KeyboardInterrupt:
1133 # or until ^C, at least
1136 # read input file, if any
1138 if parsed_args.input_file:
1140 if parsed_args.input_file == '-':
1141 inbuf = sys.stdin.buffer.read()
1143 with open(parsed_args.input_file, 'rb') as f:
1145 except Exception as e:
1146 print('Can\'t open input file {0}: {1}'.format(parsed_args.input_file, e), file=sys.stderr)
1149 # prepare output file, if any
1150 if parsed_args.output_file:
1152 if parsed_args.output_file == '-':
1153 outf = sys.stdout.buffer
1155 outf = open(parsed_args.output_file, 'wb')
1156 except Exception as e:
1157 print('Can\'t open output file {0}: {1}'.format(parsed_args.output_file, e), file=sys.stderr)
1159 if parsed_args.setuser:
1161 ownerid = pwd.getpwnam(parsed_args.setuser).pw_uid
1162 os.fchown(outf.fileno(), ownerid, -1)
1163 except OSError as e:
1164 print('Failed to change user ownership of {0} to {1}: {2}'.format(outf, parsed_args.setuser, e))
1166 if parsed_args.setgroup:
1168 groupid = grp.getgrnam(parsed_args.setgroup).gr_gid
1169 os.fchown(outf.fileno(), -1, groupid)
1170 except OSError as e:
1171 print('Failed to change group ownership of {0} to {1}: {2}'.format(outf, parsed_args.setgroup, e))
1174 # -s behaves like a command (ceph status).
1175 if parsed_args.status:
1176 childargs.insert(0, 'status')
1179 target = find_cmd_target(childargs)
1180 except Exception as e:
1181 print('error handling command target: {0}'.format(e), file=sys.stderr)
1184 # Repulsive hack to handle tell: lop off 'tell' and target
1185 # and validate the rest of the command. 'target' is already
1186 # determined in our callers, so it's ok to remove it here.
1188 if len(childargs) and childargs[0] == 'tell':
1189 childargs = childargs[2:]
1194 childargs = injectargs
1195 if not len(childargs):
1196 print('"{0} tell" requires additional arguments.'.format(sys.argv[0]),
1197 'Try "{0} tell <name> <command> [options...]" instead.'.format(sys.argv[0]),
1201 # fetch JSON sigs from command
1202 # each line contains one command signature (a placeholder name
1203 # of the form 'cmdNNN' followed by an array of argument descriptors)
1204 # as part of the validated argument JSON object
1206 if target[1] == '*':
1208 targets = [(service, o) for o in ids_by_service(service)]
1213 for target in targets:
1214 # prettify? prefix output with target, if there was a wildcard used
1217 if not parsed_args.output_file and len(targets) > 1:
1218 prefix = '{0}.{1}: '.format(*target)
1221 ret, outbuf, outs = json_command(cluster_handle, target=target,
1222 prefix='get_command_descriptions')
1224 where = '{0}.{1}'.format(*target)
1226 raise RuntimeError('Unexpected return code from {0}: {1}'.
1228 outs = 'problem getting command descriptions from {0}'.format(where)
1230 sigdict = parse_json_funcsigs(outbuf.decode('utf-8'), 'cli')
1232 if parsed_args.completion:
1233 return complete(sigdict, childargs, target)
1235 ret, outbuf, outs = new_style_command(parsed_args, childargs,
1236 target, sigdict, inbuf,
1239 # debug tool: send any successful command *again* to
1240 # verify that it is idempotent.
1241 if not ret and 'CEPH_CLI_TEST_DUP_COMMAND' in os.environ:
1242 ret, outbuf, outs = new_style_command(parsed_args, childargs,
1243 target, sigdict, inbuf,
1248 'Second attempt of previously successful command '
1249 'failed with {0}: {1}'.format(
1250 errno.errorcode.get(ret, 'Unknown'), outs),
1255 if parsed_args.output_file:
1258 # hack: old code printed status line before many json outputs
1259 # (osd dump, etc.) that consumers know to ignore. Add blank line
1260 # to satisfy consumers that skip the first line, but not annoy
1261 # consumers that don't.
1262 if parsed_args.output_format and \
1263 parsed_args.output_format.startswith('json'):
1266 # if we are prettifying things, normalize newlines. sigh.
1268 outbuf = outbuf.rstrip()
1271 print(prefix, end='')
1272 # Write directly to binary stdout
1274 print(suffix, end='')
1275 except IOError as e:
1276 if e.errno != errno.EPIPE:
1281 except IOError as e:
1282 if e.errno != errno.EPIPE:
1287 errstr = errno.errorcode.get(ret, 'Unknown')
1288 print('Error {0}: {1}'.format(errstr, outs), file=sys.stderr)
1291 print(prefix + outs, file=sys.stderr)
1296 # Block until command completion (currently scrub and deep_scrub only)
1298 wait(childargs, waitdata)
1300 if parsed_args.output_file and parsed_args.output_file != '-':
1308 if __name__ == '__main__':
1311 # shutdown explicitly; Rados() does not
1312 if retval == 0 and cluster_handle:
1313 run_in_thread(cluster_handle.shutdown)
1314 except KeyboardInterrupt:
1315 print('Interrupted')
1316 retval = errno.EINTR
1319 # flush explicitly because we aren't exiting in the usual way