]> git.proxmox.com Git - ceph.git/blame - ceph/src/ceph.in
import ceph quincy 17.2.1
[ceph.git] / ceph / src / ceph.in
CommitLineData
9f95a23c 1#!@Python3_EXECUTABLE@
7c673cae
FG
2# -*- mode:python -*-
3# vim: ts=4 sw=4 smarttab expandtab
4#
5# Processed in Makefile to add python #! line and version variable
6#
7#
8
9
10"""
11ceph.in becomes ceph, the command-line management tool for Ceph clusters.
12This is a replacement for tools/ceph.cc and tools/common.cc.
13
14Copyright (C) 2013 Inktank Storage, Inc.
15
16This is free software; you can redistribute it and/or
17modify it under the terms of the GNU General Public
18License version 2, as published by the Free Software
19Foundation. See file COPYING.
20"""
21
11fdf7f2 22from time import sleep
a8e16298 23import grp
7c673cae 24import os
a8e16298 25import pwd
20effc67 26import re
f67539c2
TL
27import shutil
28import stat
7c673cae 29import sys
11fdf7f2 30import time
7c673cae
FG
31import platform
32
f67539c2
TL
33from typing import Dict, List, Sequence, Tuple
34
7c673cae
FG
35try:
36 input = raw_input
37except NameError:
38 pass
39
31f18b77
FG
40CEPH_GIT_VER = "@CEPH_GIT_VER@"
41CEPH_GIT_NICE_VER = "@CEPH_GIT_NICE_VER@"
42CEPH_RELEASE = "@CEPH_RELEASE@"
43CEPH_RELEASE_NAME = "@CEPH_RELEASE_NAME@"
44CEPH_RELEASE_TYPE = "@CEPH_RELEASE_TYPE@"
7c673cae 45
7c673cae
FG
46# priorities from src/common/perf_counters.h
47PRIO_CRITICAL = 10
48PRIO_INTERESTING = 8
49PRIO_USEFUL = 5
50PRIO_UNINTERESTING = 2
51PRIO_DEBUGONLY = 0
52
3efd9988 53PRIO_DEFAULT = PRIO_INTERESTING
7c673cae
FG
54
55# Make life easier on developers:
224ce89b
WB
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.
7c673cae
FG
62
63MYPATH = os.path.abspath(__file__)
64MYDIR = os.path.dirname(MYPATH)
224ce89b 65MYPDIR = os.path.dirname(MYDIR)
7c673cae
FG
66DEVMODEMSG = '*** DEVELOPER MODE: setting PATH, PYTHONPATH and LD_LIBRARY_PATH ***'
67
31f18b77 68
20effc67
TL
69def add_to_ld_path(path_name, path):
70 paths = re.split('[ :]', os.environ.get(path_name, ''))
71 if path in paths:
72 return 0
73 else:
74 paths.insert(0, path)
75 os.environ[path_name] = ':'.join(paths)
76 return 1
77
7c673cae 78
20effc67 79def respawn_in_path(lib_path, pybind_path, pythonlib_path, asan_lib_path):
7c673cae
FG
80 if platform.system() == "Darwin":
81 lib_path_var = "DYLD_LIBRARY_PATH"
82 else:
83 lib_path_var = "LD_LIBRARY_PATH"
84
20effc67
TL
85 ld_paths_changed = 0
86 preload_libcxx = os.environ.get('CEPH_PRELOAD_LIBCXX')
87 if preload_libcxx:
88 ld_paths_changed += add_to_ld_path('LD_PRELOAD', preload_libcxx)
11fdf7f2 89 if asan_lib_path:
20effc67
TL
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:
31f18b77
FG
93 if "CEPH_DEV" not in os.environ:
94 print(DEVMODEMSG, file=sys.stderr)
20effc67
TL
95 execv_cmd = []
96 if 'CEPH_DBG' in os.environ:
97 execv_cmd += ['@Python3_EXECUTABLE@', '-mpdb']
98 execv_cmd += sys.argv
11fdf7f2 99 os.execvp(execv_cmd[0], execv_cmd)
20effc67
TL
100 else:
101 sys.path.insert(0, pybind_path)
102 sys.path.insert(0, pythonlib_path)
7c673cae 103
31f18b77 104
7c673cae
FG
105def get_pythonlib_dir():
106 """Returns the name of a distutils build directory"""
107 return "lib.{version[0]}".format(version=sys.version_info)
108
11fdf7f2 109
eafe8130 110def get_cmake_variables(*names):
11fdf7f2
TL
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"
114 for name in names:
115 if line.startswith("{}:".format(name)):
eafe8130
TL
116 type_value = line.split(":")[1].strip()
117 t, v = type_value.split("=")
118 if t == 'BOOL':
119 v = v.upper() in ('TRUE', '1', 'Y', 'YES', 'ON')
120 vars[name] = v
11fdf7f2
TL
121 break
122 if all(vars.values()):
123 break
eafe8130 124 return [vars[name] for name in names]
11fdf7f2
TL
125
126
224ce89b
WB
127if os.path.exists(os.path.join(MYPDIR, "CMakeCache.txt")) \
128 and os.path.exists(os.path.join(MYPDIR, "bin/init-ceph")):
eafe8130
TL
129 src_path, with_asan, asan_lib_path = \
130 get_cmake_variables("ceph_SOURCE_DIR", "WITH_ASAN", "ASAN_LIBRARY")
7c673cae
FG
131 if src_path is None:
132 # Huh, maybe we're not really in a cmake environment?
133 pass
134 else:
135 # Developer mode, but in a cmake build dir instead of the src dir
224ce89b
WB
136 lib_path = os.path.join(MYPDIR, "lib")
137 bin_path = os.path.join(MYPDIR, "bin")
7c673cae
FG
138 pybind_path = os.path.join(src_path, "src", "pybind")
139 pythonlib_path = os.path.join(lib_path,
140 "cython_modules",
141 get_pythonlib_dir())
eafe8130
TL
142 respawn_in_path(lib_path, pybind_path, pythonlib_path,
143 asan_lib_path if with_asan else None)
7c673cae
FG
144
145 if 'PATH' in os.environ and bin_path not in os.environ['PATH']:
11fdf7f2 146 os.environ['PATH'] = os.pathsep.join([bin_path, os.environ['PATH']])
7c673cae
FG
147
148import argparse
149import errno
150import json
151import rados
152import shlex
153import signal
154import string
155import subprocess
156
157from ceph_argparse import \
158 concise_sig, descsort_key, parse_json_funcsigs, \
11fdf7f2
TL
159 validate_command, find_cmd_target, \
160 json_command, run_in_thread, Flag
7c673cae 161
31f18b77 162from ceph_daemon import admin_socket, DaemonWatcher, Termsize
7c673cae
FG
163
164# just a couple of globals
165
166verbose = False
167cluster_handle = None
168
31f18b77 169
7c673cae
FG
170def raw_write(buf):
171 sys.stdout.flush()
f67539c2 172 sys.stdout.buffer.write(buf)
7c673cae 173
7c673cae
FG
174
175def osdids():
176 ret, outbuf, outs = json_command(cluster_handle, prefix='osd ls')
7c673cae
FG
177 if ret:
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]
180
31f18b77 181
7c673cae
FG
182def monids():
183 ret, outbuf, outs = json_command(cluster_handle, prefix='mon dump',
31f18b77 184 argdict={'format': 'json'})
7c673cae
FG
185 if ret:
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']]
189
31f18b77 190
7c673cae 191def mdsids():
181888fb 192 ret, outbuf, outs = json_command(cluster_handle, prefix='fs dump',
31f18b77 193 argdict={'format': 'json'})
7c673cae
FG
194 if ret:
195 raise RuntimeError('Can\'t contact mon for mds list')
196 d = json.loads(outbuf.decode('utf-8'))
197 l = []
181888fb
FG
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'])
7c673cae
FG
203 return l
204
31f18b77
FG
205
206def mgrids():
207 ret, outbuf, outs = json_command(cluster_handle, prefix='mgr dump',
208 argdict={'format': 'json'})
209 if ret:
210 raise RuntimeError('Can\'t contact mon for mgr list')
211
212 d = json.loads(outbuf.decode('utf-8'))
213 l = []
214 l.append(d['active_name'])
9f95a23c
TL
215 # we can only send tell commands to the active mgr
216 #for i in d['standbys']:
217 # l.append(i['name'])
31f18b77
FG
218 return l
219
220
181888fb
FG
221def ids_by_service(service):
222 ids = {"mon": monids,
223 "osd": osdids,
224 "mds": mdsids,
225 "mgr": mgrids}
226 return ids[service]()
227
228
31f18b77
FG
229def validate_target(target):
230 """
231 this function will return true iff target is a correct
232 target, such as mon.a/osd.2/mds.a/mgr.
233
234 target: array, likes ['osd', '2']
235 return: bool, or raise RuntimeError
236 """
237
238 if len(target) == 2:
239 # for case "service.id"
240 service_name, service_id = target[0], target[1]
181888fb
FG
241 try:
242 exist_ids = ids_by_service(service_name)
243 except KeyError:
31f18b77
FG
244 print('WARN: {0} is not a legal service name, should be one of mon/osd/mds/mgr'.format(service_name),
245 file=sys.stderr)
246 return False
247
3efd9988 248 if service_id in exist_ids or len(exist_ids) > 0 and service_id == '*':
31f18b77
FG
249 return True
250 else:
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)
253 return False
254
255 elif len(target) == 1 and target[0] in ['mgr', 'mon']:
256 return True
257 else:
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)
259 return False
260
261
7c673cae
FG
262# these args must be passed to all child programs
263GLOBAL_ARGS = {
264 'client_id': '--id',
265 'client_name': '--name',
266 'cluster': '--cluster',
267 'cephconf': '--conf',
268}
269
31f18b77 270
f67539c2
TL
271def parse_cmdargs(args=None, target='') -> Tuple[argparse.ArgumentParser,
272 argparse.Namespace,
273 List[str]]:
11fdf7f2
TL
274 """
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.
278
279 :returns: three tuple of ArgumentParser instance, Namespace instance
280 containing parsed values, and list of un-handled arguments
281 """
7c673cae
FG
282 # alias: let the line-wrapping be sane
283 AP = argparse.ArgumentParser
284
285 # format our own help
286 parser = AP(description='Ceph administration tool', add_help=False)
287
288 parser.add_argument('--completion', action='store_true',
289 help=argparse.SUPPRESS)
290
291 parser.add_argument('-h', '--help', help='request mon help',
292 action='store_true')
293
294 parser.add_argument('-c', '--conf', dest='cephconf',
295 help='ceph configuration file')
296 parser.add_argument('-i', '--in-file', dest='input_file',
c07f9fc5 297 help='input file, or "-" for stdin')
7c673cae 298 parser.add_argument('-o', '--out-file', dest='output_file',
c07f9fc5 299 help='output file, or "-" for stdout')
a8e16298
TL
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')
7c673cae
FG
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')
309
310 parser.add_argument('--admin-daemon', dest='admin_socket',
f67539c2 311 help='submit admin-socket commands (\"help\" for help)')
7c673cae
FG
312
313 parser.add_argument('-s', '--status', action='store_true',
314 help='show cluster status')
315
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')
328
9f95a23c
TL
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)")
224ce89b 332
7c673cae
FG
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")
337
338 parser.add_argument('-f', '--format', choices=['json', 'json-pretty',
33c7a0ef
TL
339 'xml', 'xml-pretty', 'plain', 'yaml'],
340 help="Note: yaml is only valid for orch commands", dest='output_format')
7c673cae
FG
341
342 parser.add_argument('--connect-timeout', dest='cluster_timeout',
343 type=int,
344 help='set a timeout for connecting to the cluster')
345
11fdf7f2
TL
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)')
351
7c673cae
FG
352 # returns a Namespace with the parsed args, and a list of all extras
353 parsed_args, extras = parser.parse_known_args(args)
354
355 return parser, parsed_args, extras
356
357
358def hdr(s):
359 print('\n', s, '\n', '=' * len(s))
360
31f18b77 361
7c673cae
FG
362def do_basic_help(parser, args):
363 """
364 Print basic parser help
365 If the cluster is available, get and print monitor help
366 """
367 hdr('General usage:')
368 parser.print_help()
369 print_locally_handled_command_help()
370
31f18b77 371
7c673cae
FG
372def print_locally_handled_command_help():
373 hdr("Local commands:")
374 print("""
375ping <mon.id> Send simple presence/life test to a mon
376 <mon.id> may be 'mon.*' for all mons
377daemon {type.id|path} <cmd>
378 Same as --admin-daemon, but auto-find admin socket
379daemonperf {type.id | path} [stat-pats] [priority] [<interval>] [<count>]
380daemonperf {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)
389
390
f67539c2 391def do_extended_help(parser, args, target, partial) -> int:
7c673cae 392 def help_for_sigs(sigs, partial=None):
f67539c2 393 try:
20effc67
TL
394 while True:
395 out = format_help(parse_json_funcsigs(sigs, 'cli'),
396 partial=partial)
397 if not out and partial:
398 # shorten partial until we get at least one matching command prefix
399 partial = ' '.join(partial.split()[:-1])
400 continue
401 sys.stdout.write(out)
402 break
f67539c2
TL
403 except BrokenPipeError:
404 pass
7c673cae
FG
405
406 def help_for_target(target, partial=None):
d2e6a577
FG
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()
7c673cae
FG
410 ret, outbuf, outs = json_command(cluster_handle, target=target,
411 prefix='get_command_descriptions',
412 timeout=10)
413 if ret:
9f95a23c 414 if (ret == -errno.EPERM or ret == -errno.EACCES) and target[0] in ('osd', 'mds'):
11fdf7f2
TL
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)
420 else:
421 print("couldn't get command descriptions for {0}: {1} ({2})".
422 format(target, outs, ret), file=sys.stderr)
31f18b77 423 return ret
7c673cae 424 else:
31f18b77 425 return help_for_sigs(outbuf.decode('utf-8'), partial)
7c673cae 426
31f18b77
FG
427 assert(cluster_handle.state == "connected")
428 return help_for_target(target, partial)
7c673cae
FG
429
430DONTSPLIT = string.ascii_letters + '{[<>]}'
431
31f18b77 432
7c673cae
FG
433def wrap(s, width, indent):
434 """
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.
439
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
442 as
443 long string
444 long string
445 long string
446
447 Consumes s.
448 """
449 result = ''
450 leader = ''
451 while len(s):
452
31f18b77 453 if len(s) <= width:
7c673cae
FG
454 # no splitting; just possibly indent
455 result = leader + s
456 s = ''
457 yield result
458
459 else:
460 splitpos = width
461 while (splitpos > 0) and (s[splitpos-1] in DONTSPLIT):
462 splitpos -= 1
463
464 if splitpos == 0:
465 splitpos = width
466
467 if result:
468 # prior result means we're mid-iteration, indent
469 result = leader
470 else:
471 # first time, set leader and width for next
472 leader = ' ' * indent
473 width -= 1 # for subsequent space additions
474
475 # remove any leading spaces in this chunk of s
476 result += s[:splitpos].lstrip()
477 s = s[splitpos:]
478
479 yield result
480
31f18b77 481
f67539c2 482def format_help(cmddict, partial=None) -> str:
7c673cae
FG
483 """
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
31f18b77 486 fit into (terminal_width / 2) characters.
7c673cae
FG
487 """
488
489 fullusage = ''
490 for cmd in sorted(cmddict.values(), key=descsort_key):
491
492 if not cmd['help']:
493 continue
31f18b77 494 flags = cmd.get('flags', 0)
11fdf7f2 495 if flags & (Flag.OBSOLETE | Flag.DEPRECATED | Flag.HIDDEN):
7c673cae
FG
496 continue
497 concise = concise_sig(cmd['sig'])
498 if partial and not concise.startswith(partial):
499 continue
31f18b77
FG
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)]
7c673cae
FG
506
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)))
511
512 # so we can zip them for output
31f18b77
FG
513 for s, h in zip(siglines, helplines):
514 fullusage += '{s:{w}s} {h}\n'.format(s=s, h=h, w=sig_width)
7c673cae
FG
515
516 return fullusage
517
518
f67539c2
TL
519def ceph_conf(parsed_args, field, name, pid=None):
520 cmd = 'ceph-conf'
521 bindir = os.path.dirname(__file__)
522 if shutil.which(cmd):
523 args = [cmd]
524 elif shutil.which(cmd, path=bindir):
525 args = [os.path.join(bindir, cmd)]
526 else:
527 raise RuntimeError('"ceph-conf" not found')
7c673cae
FG
528
529 if name:
530 args.extend(['--name', name])
f67539c2
TL
531 if pid:
532 args.extend(['--pid', pid])
7c673cae
FG
533
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':
538 continue
539 if getattr(parsed_args, key):
540 args.extend([val, getattr(parsed_args, key)])
541
542 args.extend(['--show-config-value', field])
543 p = subprocess.Popen(
544 args,
545 stdout=subprocess.PIPE,
546 stderr=subprocess.PIPE)
547 outdata, errdata = p.communicate()
92f5a8d4 548 if p.returncode != 0:
7c673cae
FG
549 raise RuntimeError('unable to get conf option %s for %s: %s' % (field, name, errdata))
550 return outdata.rstrip()
551
f67539c2 552
7c673cae
FG
553PROMPT = 'ceph> '
554
555if sys.stdin.isatty():
556 def read_input():
557 while True:
558 line = input(PROMPT).rstrip()
559 if line in ['q', 'quit', 'Q', 'exit']:
560 return None
561 if line:
562 return line
563else:
564 def read_input():
565 while True:
566 line = sys.stdin.readline()
567 if not line:
568 return None
569 line = line.rstrip()
570 if line:
571 return line
572
573
11fdf7f2
TL
574def do_command(parsed_args, target, cmdargs, sigdict, inbuf, verbose):
575 ''' Validate a command, and handle the polling flag '''
576
577 valid_dict = validate_command(sigdict, cmdargs, verbose)
578 # Validate input args against list of sigs
579 if valid_dict:
580 if parsed_args.output_format:
581 valid_dict['format'] = parsed_args.output_format
582 if verbose:
583 print("Submitting command: ", valid_dict, file=sys.stderr)
584 else:
585 return -errno.EINVAL, '', 'invalid command'
586
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
591 while True:
592 try:
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,
9f95a23c 599 argdict=valid_dict, inbuf=inbuf, verbose=verbose)
11fdf7f2
TL
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
604 break
605 if ret:
606 ret = abs(ret)
607 print('Error: {0} {1}'.format(ret, errno.errorcode.get(ret, 'Unknown')),
608 file=sys.stderr)
609 break
610 if outbuf:
611 print(outbuf.decode('utf-8'))
612 if outs:
613 print(outs, file=sys.stderr)
614 if parsed_args.period <= 0:
615 break
616 sleep(parsed_args.period)
617 except KeyboardInterrupt:
618 print('Interrupted')
9f95a23c 619 return errno.EINTR, '', ''
11fdf7f2
TL
620 if ret == errno.ETIMEDOUT:
621 ret = -ret
622 if not outs:
623 outs = ("Connection timed out. Please check the client's " +
624 "permission and connection.")
625 return ret, outbuf, outs
626
627
f67539c2
TL
628def new_style_command(parsed_args,
629 cmdargs,
630 target,
631 sigdict,
632 inbuf, verbose) -> Tuple[int, bytes, str]:
7c673cae
FG
633 """
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
638 verbose - bool
639 """
640 if verbose:
641 for cmdtag in sorted(sigdict.keys()):
642 cmd = sigdict[cmdtag]
643 sig = cmd['sig']
644 print('{0}: {1}'.format(cmdtag, concise_sig(sig)))
645
f67539c2
TL
646 if cmdargs:
647 # Non interactive mode
648 ret, outbuf, outs = do_command(parsed_args, target, cmdargs, sigdict, inbuf, verbose)
649 else:
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
7c673cae 655
f67539c2
TL
656 while True:
657 try:
658 interactive_input = read_input()
659 except EOFError:
660 # leave user an uncluttered prompt
661 return 0, b'\n', ''
662 if interactive_input is None:
663 return 0, b'', ''
664 cmdargs = parse_cmdargs(shlex.split(interactive_input))[2]
665 try:
666 target = find_cmd_target(cmdargs)
667 except Exception as e:
668 print('error handling command target: {0}'.format(e),
669 file=sys.stderr)
670 continue
671 if len(cmdargs) and cmdargs[0] == 'tell':
672 print('Can not use \'tell\' in interactive mode.',
673 file=sys.stderr)
674 continue
675 ret, outbuf, outs = do_command(parsed_args, target, cmdargs,
676 sigdict, inbuf, verbose)
677 if ret < 0:
678 ret = -ret
679 errstr = errno.errorcode.get(ret, 'Unknown')
680 print('Error {0}: {1}'.format(errstr, outs), file=sys.stderr)
681 else:
682 if outs:
683 print(outs, file=sys.stderr)
684 if outbuf:
685 print(outbuf.decode('utf-8'))
7c673cae 686
11fdf7f2 687 return ret, outbuf, outs
7c673cae
FG
688
689
690def complete(sigdict, args, target):
691 """
692 Command completion. Match as much of [args] as possible,
693 and print every possible match separated by newlines.
694 Return exitcode.
695 """
696 # XXX this looks a lot like the front of validate_command(). Refactor?
697
7c673cae
FG
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':
702 args = args[2:]
703 # look for best match, accumulate possibles in bestcmds
704 # (so we can maybe give a more-useful error message)
705
706 match_count = 0
707 comps = []
708 for cmdtag, cmd in sigdict.items():
e306af50
TL
709 flags = cmd.get('flags', 0)
710 if flags & (Flag.OBSOLETE | Flag.HIDDEN):
711 continue
7c673cae
FG
712 sig = cmd['sig']
713 j = 0
714 # iterate over all arguments, except last one
715 for arg in args[0:-1]:
716 if j > len(sig)-1:
717 # an out of argument definitions
718 break
719 found_match = arg in sig[j].complete(arg)
720 if not found_match and sig[j].req:
721 # no elements that match
722 break
723 if not sig[j].N:
724 j += 1
725 else:
726 # successfully matched all - except last one - arguments
727 if j < len(sig) and len(args) > 0:
728 comps += sig[j].complete(args[-1])
729
730 match_count += 1
731 match_cmd = cmd
732
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))))
737 return 0
738
739
7c673cae
FG
740def 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)
743 return 1
744
745 mon_id = name[len('mon.'):]
31f18b77 746 if mon_id == '*':
7c673cae 747 run_in_thread(cluster_handle.connect, timeout=timeout)
31f18b77 748 for m in monids():
7c673cae
FG
749 s = run_in_thread(cluster_handle.ping_monitor, m)
750 if s is None:
751 print("mon.{0}".format(m) + '\n' + "Error connecting to monitor.")
752 else:
753 print("mon.{0}".format(m) + '\n' + s)
31f18b77 754 else:
7c673cae
FG
755 s = run_in_thread(cluster_handle.ping_monitor, mon_id)
756 print(s)
757 return 0
758
759
f67539c2
TL
760def get_admin_socket(parsed_args, name):
761 path = ceph_conf(parsed_args, 'admin_socket', name)
762 try:
763 if stat.S_ISSOCK(os.stat(path).st_mode):
764 return path
765 except OSError:
766 pass
767 # try harder, probably the "name" option is in the form of
768 # "${name}.${pid}"?
769 parts = name.rsplit('.', 1)
770 if len(parts) > 1 and parts[-1].isnumeric():
771 name, pid = parts
772 return ceph_conf(parsed_args, 'admin_socket', name, pid)
773 else:
774 return path
775
776
7c673cae
FG
777def maybe_daemon_command(parsed_args, childargs):
778 """
779 Check if --admin-socket, daemon, or daemonperf command
780 if it is, returns (boolean handled, return code if handled == True)
781 """
782
783 daemon_perf = False
784 sockpath = None
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]
795 else:
796 # try resolve daemon name
797 try:
f67539c2 798 sockpath = get_admin_socket(parsed_args, childargs[1])
7c673cae
FG
799 except Exception as e:
800 print('Can\'t get admin socket path: ' + str(e), file=sys.stderr)
801 return True, errno.EINVAL
802 # for both:
803 childargs = childargs[2:]
804 else:
31f18b77
FG
805 print('{0} requires at least {1} arguments'.format(childargs[0], require_args),
806 file=sys.stderr)
7c673cae
FG
807 return True, errno.EINVAL
808
809 if sockpath and daemon_perf:
810 return True, daemonperf(childargs, sockpath)
811 elif sockpath:
812 try:
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
817 return True, 0
818
819 return False, 0
820
821
822def isnum(s):
823 try:
824 float(s)
825 return True
826 except ValueError:
827 return False
828
31f18b77 829
f67539c2 830def daemonperf(childargs: Sequence[str], sockpath: str):
7c673cae
FG
831 """
832 Handle daemonperf command; returns errno or 0
833
834 daemonperf <daemon> [priority string] [statpats] [interval] [count]
835 daemonperf <daemon> list|ls [statpats]
836 """
837
838 interval = 1
839 count = None
840 statpats = None
841 priority = None
842 do_list = False
843
844 def prio_from_name(arg):
845
846 PRIOMAP = {
847 'critical': PRIO_CRITICAL,
848 'interesting': PRIO_INTERESTING,
849 'useful': PRIO_USEFUL,
850 'uninteresting': PRIO_UNINTERESTING,
851 'debugonly': PRIO_DEBUGONLY,
852 }
853
854 if arg in PRIOMAP:
855 return PRIOMAP[arg]
856 # allow abbreviation
857 for name, val in PRIOMAP.items():
858 if name.startswith(arg):
859 return val
860 return None
861
862 # consume and analyze non-numeric args
863 while len(childargs) and not isnum(childargs[0]):
864 arg = childargs.pop(0)
865 # 'list'?
866 if arg in ['list', 'ls']:
31f18b77 867 do_list = True
7c673cae
FG
868 continue
869 # prio?
870 prio = prio_from_name(arg)
871 if prio is not None:
872 priority = prio
873 continue
874 # statpats
875 statpats = arg.split(',')
876
877 if priority is None:
878 priority = PRIO_DEFAULT
879
880 if len(childargs) > 0:
881 try:
882 interval = float(childargs.pop(0))
883 if interval < 0:
884 raise ValueError
885 except ValueError:
886 print('daemonperf: interval should be a positive number', file=sys.stderr)
887 return errno.EINVAL
888
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)
893 return errno.EINVAL
894 count = int(arg)
895
896 watcher = DaemonWatcher(sockpath, statpats, priority)
897 if do_list:
898 watcher.list()
899 else:
900 watcher.run(interval, count)
901
902 return 0
903
f67539c2
TL
904
905def get_scrub_timestamps(childargs: Sequence[str]) -> Dict[str,
906 Tuple[str, str]]:
11fdf7f2
TL
907 last_scrub_stamp = "last_" + childargs[1].replace('-', '_') + "_stamp"
908 results = dict()
909 scruball = False
910 if childargs[2] in ['all', 'any', '*']:
911 scruball = True
912 devnull = open(os.devnull, 'w')
913 out = subprocess.check_output(['ceph', 'pg', 'dump', '--format=json-pretty'],
914 stderr=devnull)
915 try:
916 pgstats = json.loads(out)['pg_map']['pg_stats']
917 except KeyError:
918 pgstats = json.loads(out)['pg_stats']
919 for stat in pgstats:
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
923 return results
924
f67539c2 925
11fdf7f2
TL
926def 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]:
930 return False
931 return True
932
f67539c2 933
11fdf7f2 934def waitscrub(childargs, waitdata):
f67539c2 935 print('Waiting for {0} to complete...'.format(childargs[1]), file=sys.stdout)
11fdf7f2
TL
936 currdata = get_scrub_timestamps(childargs)
937 while not check_scrub_stamps(waitdata, currdata):
938 time.sleep(3)
939 currdata = get_scrub_timestamps(childargs)
f67539c2
TL
940 print('{0} completed'.format(childargs[1]), file=sys.stdout)
941
11fdf7f2 942
f67539c2 943def wait(childargs: Sequence[str], waitdata):
11fdf7f2
TL
944 if childargs[1] in ['scrub', 'deep-scrub']:
945 waitscrub(childargs, waitdata)
946
7c673cae
FG
947
948def main():
949 ceph_args = os.environ.get('CEPH_ARGS')
950 if 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:]
954 else:
c07f9fc5
FG
955 sys.argv.extend([arg for arg in ceph_args.split()
956 if '--admin-socket' not in arg])
7c673cae
FG
957 parser, parsed_args, childargs = parse_cmdargs()
958
959 if parsed_args.version:
31f18b77
FG
960 print('ceph version {0} ({1}) {2} ({3})'.format(
961 CEPH_GIT_NICE_VER,
962 CEPH_GIT_VER,
963 CEPH_RELEASE_NAME,
964 CEPH_RELEASE_TYPE)) # noqa
7c673cae
FG
965 return 0
966
9f95a23c
TL
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'
972
7c673cae
FG
973 global verbose
974 verbose = parsed_args.verbose
975
976 if verbose:
977 print("parsed_args: {0}, childargs: {1}".format(parsed_args, childargs), file=sys.stderr)
978
7c673cae
FG
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
985
20effc67 986 conffile = rados.Rados.DEFAULT_CONF_FILES
7c673cae
FG
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
991
7c673cae
FG
992 done, ret = maybe_daemon_command(parsed_args, childargs)
993 if done:
994 return ret
995
996 timeout = None
997 if parsed_args.cluster_timeout:
998 timeout = parsed_args.cluster_timeout
999
1000 # basic help
1001 if parsed_args.help:
1002 do_basic_help(parser, childargs)
1003
1004 # handle any 'generic' ceph arguments that we didn't parse here
1005 global cluster_handle
1006
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)
1010 conf_defaults = {
31f18b77
FG
1011 'log_to_stderr': 'true',
1012 'err_to_stderr': 'true',
1013 'log_flush_on_exit': 'true',
7c673cae
FG
1014 }
1015
1016 if 'injectargs' in childargs:
1017 position = childargs.index('injectargs')
1018 injectargs = childargs[position:]
1019 childargs = childargs[:position]
1020 if verbose:
31f18b77
FG
1021 print('Separate childargs {0} from injectargs {1}'.format(childargs, injectargs),
1022 file=sys.stderr)
7c673cae
FG
1023 else:
1024 injectargs = None
1025
1026 clustername = None
1027 if parsed_args.cluster:
1028 clustername = parsed_args.cluster
1029
1030 try:
1031 cluster_handle = run_in_thread(rados.Rados,
1032 name=name, clustername=clustername,
1033 conf_defaults=conf_defaults,
1034 conffile=conffile)
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)
1038 return 1
1039
1040 childargs = retargs
1041 if not childargs:
1042 childargs = []
1043
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('--')
1049
11fdf7f2
TL
1050 block = False
1051 waitdata = dict()
1052 if parsed_args.block:
1053 if (len(childargs) >= 2 and
1054 childargs[0] == 'osd' and
1055 childargs[1] in ['deep-scrub', 'scrub']):
1056 block = True
1057 waitdata = get_scrub_timestamps(childargs)
1058
7c673cae
FG
1059 if parsed_args.help:
1060 # short default timeout for -h
1061 if not timeout:
1062 timeout = 5
1063
224ce89b 1064 if childargs and childargs[0] == 'ping' and not parsed_args.help:
7c673cae
FG
1065 if len(childargs) < 2:
1066 print('"ping" requires a monitor name as argument: "ping mon.<id>"', file=sys.stderr)
1067 return 1
1068 if parsed_args.completion:
31f18b77 1069 # for completion let timeout be really small
7c673cae
FG
1070 timeout = 3
1071 try:
224ce89b 1072 if childargs and childargs[0] == 'ping' and not parsed_args.help:
7c673cae 1073 return ping_monitor(cluster_handle, childargs[1], timeout)
224ce89b
WB
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)
1077 return 1
7c673cae
FG
1078 except KeyboardInterrupt:
1079 print('Cluster connection aborted', file=sys.stderr)
1080 return 1
1081 except rados.PermissionDeniedError as e:
1082 print(str(e), file=sys.stderr)
1083 return errno.EACCES
1084 except Exception as e:
1085 print(str(e), file=sys.stderr)
1086 return 1
224ce89b 1087
7c673cae 1088 if parsed_args.help:
9f95a23c
TL
1089 target = None
1090 if len(childargs) >= 2 and childargs[0] == 'tell':
f6b5b4d7 1091 target = childargs[1].split('.', 1)
9f95a23c
TL
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)
1094 return 1
1095 childargs = childargs[2:]
1096 hdr('Tell %s commands:' % target[0])
1097 else:
1098 hdr('Monitor commands:')
1099 target = ('mon', '')
224ce89b
WB
1100 if verbose:
1101 print('[Contacting monitor, timeout after %d seconds]' % timeout)
1102
9f95a23c 1103 return do_extended_help(parser, childargs, target, ' '.join(childargs))
7c673cae 1104
31f18b77
FG
1105 # implement "tell service.id help"
1106 if len(childargs) >= 3 and childargs[0] == 'tell' and childargs[2] == 'help':
f6b5b4d7 1107 target = childargs[1].split('.', 1)
31f18b77 1108 if validate_target(target):
9f95a23c 1109 hdr('Tell %s commands' % target[0])
31f18b77
FG
1110 return do_extended_help(parser, childargs, target, None)
1111 else:
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)
1114 return 1
9f95a23c 1115
7c673cae
FG
1116 # implement -w/--watch_*
1117 # This is ugly, but Namespace() isn't quite rich enough.
1118 level = ''
1119 for k, v in parsed_args._get_kwargs():
1120 if k.startswith('watch') and v:
1121 if k == 'watch':
1122 level = 'info'
224ce89b 1123 elif k != "watch_channel":
7c673cae
FG
1124 level = k.replace('watch_', '')
1125 if level:
7c673cae 1126 # an awfully simple callback
224ce89b
WB
1127 def watch_cb(arg, line, channel, name, who, stamp_sec, stamp_nsec, seq, level, msg):
1128 # Filter on channel
f67539c2
TL
1129 channel = channel.decode('utf-8')
1130 if parsed_args.watch_channel in (channel, '*'):
11fdf7f2 1131 print(line.decode('utf-8'))
224ce89b 1132 sys.stdout.flush()
7c673cae
FG
1133
1134 # first do a ceph status
1135 ret, outbuf, outs = json_command(cluster_handle, prefix='status')
7c673cae
FG
1136 if ret:
1137 print("status query failed: ", outs, file=sys.stderr)
1138 return ret
11fdf7f2 1139 print(outbuf.decode('utf-8'))
7c673cae
FG
1140
1141 # this instance keeps the watch connection alive, but is
1142 # otherwise unused
224ce89b 1143 run_in_thread(cluster_handle.monitor_log2, level, watch_cb, 0)
7c673cae
FG
1144
1145 # loop forever letting watch_cb print lines
1146 try:
1147 signal.pause()
1148 except KeyboardInterrupt:
1149 # or until ^C, at least
1150 return 0
1151
1152 # read input file, if any
1153 inbuf = b''
1154 if parsed_args.input_file:
1155 try:
c07f9fc5 1156 if parsed_args.input_file == '-':
f67539c2 1157 inbuf = sys.stdin.buffer.read()
c07f9fc5
FG
1158 else:
1159 with open(parsed_args.input_file, 'rb') as f:
1160 inbuf = f.read()
7c673cae
FG
1161 except Exception as e:
1162 print('Can\'t open input file {0}: {1}'.format(parsed_args.input_file, e), file=sys.stderr)
1163 return 1
1164
1165 # prepare output file, if any
1166 if parsed_args.output_file:
1167 try:
c07f9fc5 1168 if parsed_args.output_file == '-':
f67539c2 1169 outf = sys.stdout.buffer
c07f9fc5
FG
1170 else:
1171 outf = open(parsed_args.output_file, 'wb')
7c673cae
FG
1172 except Exception as e:
1173 print('Can\'t open output file {0}: {1}'.format(parsed_args.output_file, e), file=sys.stderr)
1174 return 1
a8e16298
TL
1175 if parsed_args.setuser:
1176 try:
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))
1181 return 1
1182 if parsed_args.setgroup:
1183 try:
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))
1188 return 1
7c673cae
FG
1189
1190 # -s behaves like a command (ceph status).
1191 if parsed_args.status:
1192 childargs.insert(0, 'status')
1193
1194 try:
1195 target = find_cmd_target(childargs)
1196 except Exception as e:
1197 print('error handling command target: {0}'.format(e), file=sys.stderr)
1198 return 1
1199
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.
1203 is_tell = False
1204 if len(childargs) and childargs[0] == 'tell':
1205 childargs = childargs[2:]
1206 is_tell = True
1207
1208 if is_tell:
1209 if injectargs:
1210 childargs = injectargs
1211 if not len(childargs):
1212 print('"{0} tell" requires additional arguments.'.format(sys.argv[0]),
31f18b77
FG
1213 'Try "{0} tell <name> <command> [options...]" instead.'.format(sys.argv[0]),
1214 file=sys.stderr)
7c673cae
FG
1215 return errno.EINVAL
1216
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
1221
7c673cae 1222 if target[1] == '*':
181888fb
FG
1223 service = target[0]
1224 targets = [(service, o) for o in ids_by_service(service)]
1225 else:
1226 targets = [target]
7c673cae
FG
1227
1228 final_ret = 0
1229 for target in targets:
1230 # prettify? prefix output with target, if there was a wildcard used
1231 prefix = ''
1232 suffix = ''
1233 if not parsed_args.output_file and len(targets) > 1:
1234 prefix = '{0}.{1}: '.format(*target)
1235 suffix = '\n'
1236
1237 ret, outbuf, outs = json_command(cluster_handle, target=target,
1238 prefix='get_command_descriptions')
31f18b77
FG
1239 if ret:
1240 where = '{0}.{1}'.format(*target)
1241 if ret > 0:
11fdf7f2 1242 raise RuntimeError('Unexpected return code from {0}: {1}'.
31f18b77
FG
1243 format(where, ret))
1244 outs = 'problem getting command descriptions from {0}'.format(where)
1245 else:
1246 sigdict = parse_json_funcsigs(outbuf.decode('utf-8'), 'cli')
7c673cae 1247
31f18b77
FG
1248 if parsed_args.completion:
1249 return complete(sigdict, childargs, target)
7c673cae 1250
31f18b77
FG
1251 ret, outbuf, outs = new_style_command(parsed_args, childargs,
1252 target, sigdict, inbuf,
1253 verbose)
7c673cae 1254
31f18b77
FG
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,
1260 verbose)
1261 if ret < 0:
1262 ret = -ret
1263 print(prefix +
1264 'Second attempt of previously successful command '
1265 'failed with {0}: {1}'.format(
1266 errno.errorcode.get(ret, 'Unknown'), outs),
1267 file=sys.stderr)
7c673cae 1268
7c673cae
FG
1269 sys.stdout.flush()
1270
31f18b77 1271 if parsed_args.output_file:
7c673cae
FG
1272 outf.write(outbuf)
1273 else:
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 \
31f18b77 1279 parsed_args.output_format.startswith('json'):
7c673cae
FG
1280 print()
1281
1282 # if we are prettifying things, normalize newlines. sigh.
1283 if suffix:
1284 outbuf = outbuf.rstrip()
1285 if outbuf:
1286 try:
1287 print(prefix, end='')
1288 # Write directly to binary stdout
1289 raw_write(outbuf)
1290 print(suffix, end='')
1291 except IOError as e:
1292 if e.errno != errno.EPIPE:
1293 raise e
f67539c2 1294 final_e = None
f91f0fd5
TL
1295 try:
1296 sys.stdout.flush()
1297 except IOError as e:
1298 if e.errno != errno.EPIPE:
f67539c2
TL
1299 final_e = e
1300
1301 if ret < 0:
1302 ret = -ret
1303 errstr = errno.errorcode.get(ret, 'Unknown')
1304 print('Error {0}: {1}'.format(errstr, outs), file=sys.stderr)
1305 final_ret = ret
1306 elif outs:
1307 print(prefix + outs, file=sys.stderr)
7c673cae 1308
f67539c2
TL
1309 if final_e:
1310 raise final_e
7c673cae 1311
11fdf7f2
TL
1312 # Block until command completion (currently scrub and deep_scrub only)
1313 if block:
1314 wait(childargs, waitdata)
1315
c07f9fc5 1316 if parsed_args.output_file and parsed_args.output_file != '-':
7c673cae
FG
1317 outf.close()
1318
1319 if final_ret:
1320 return final_ret
1321
1322 return 0
1323
1324if __name__ == '__main__':
9f95a23c
TL
1325 try:
1326 retval = 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
1333
1334 if retval:
1335 # flush explicitly because we aren't exiting in the usual way
1336 sys.stdout.flush()
1337 sys.stderr.flush()
1338 os._exit(retval)
1339 else:
1340 sys.exit(retval)