]> git.proxmox.com Git - ceph.git/blame - ceph/src/ceph.in
import 15.2.5
[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
22from __future__ import print_function
11fdf7f2 23from time import sleep
7c673cae 24import codecs
a8e16298 25import grp
7c673cae 26import os
a8e16298 27import pwd
7c673cae 28import sys
11fdf7f2 29import time
7c673cae
FG
30import platform
31
32try:
33 input = raw_input
34except NameError:
35 pass
36
31f18b77
FG
37CEPH_GIT_VER = "@CEPH_GIT_VER@"
38CEPH_GIT_NICE_VER = "@CEPH_GIT_NICE_VER@"
39CEPH_RELEASE = "@CEPH_RELEASE@"
40CEPH_RELEASE_NAME = "@CEPH_RELEASE_NAME@"
41CEPH_RELEASE_TYPE = "@CEPH_RELEASE_TYPE@"
7c673cae 42
7c673cae
FG
43# priorities from src/common/perf_counters.h
44PRIO_CRITICAL = 10
45PRIO_INTERESTING = 8
46PRIO_USEFUL = 5
47PRIO_UNINTERESTING = 2
48PRIO_DEBUGONLY = 0
49
3efd9988 50PRIO_DEFAULT = PRIO_INTERESTING
7c673cae
FG
51
52# Make life easier on developers:
224ce89b
WB
53# If our parent dir contains CMakeCache.txt and bin/init-ceph,
54# assume we're running from a build dir (i.e. src/build/bin/ceph)
55# and tweak sys.path and LD_LIBRARY_PATH to use built files.
56# Since this involves re-execing, if CEPH_DBG is set in the environment
57# re-exec with -mpdb. Also, if CEPH_DEV is in the env, suppress
58# the warning message about the DEVELOPER MODE.
7c673cae
FG
59
60MYPATH = os.path.abspath(__file__)
61MYDIR = os.path.dirname(MYPATH)
224ce89b 62MYPDIR = os.path.dirname(MYDIR)
7c673cae
FG
63DEVMODEMSG = '*** DEVELOPER MODE: setting PATH, PYTHONPATH and LD_LIBRARY_PATH ***'
64
31f18b77 65
11fdf7f2
TL
66def respawn_in_path(lib_path, pybind_path, pythonlib_path, asan_lib_path):
67 execv_cmd = []
7c673cae 68 if 'CEPH_DBG' in os.environ:
9f95a23c 69 execv_cmd += ['@Python3_EXECUTABLE@', '-mpdb']
7c673cae
FG
70
71 if platform.system() == "Darwin":
72 lib_path_var = "DYLD_LIBRARY_PATH"
73 else:
74 lib_path_var = "LD_LIBRARY_PATH"
75
11fdf7f2
TL
76 execv_cmd += sys.argv
77 if asan_lib_path:
78 os.environ['LD_PRELOAD'] = asan_lib_path
7c673cae
FG
79 if lib_path_var in os.environ:
80 if lib_path not in os.environ[lib_path_var]:
81 os.environ[lib_path_var] += ':' + lib_path
31f18b77
FG
82 if "CEPH_DEV" not in os.environ:
83 print(DEVMODEMSG, file=sys.stderr)
11fdf7f2 84 os.execvp(execv_cmd[0], execv_cmd)
7c673cae
FG
85 else:
86 os.environ[lib_path_var] = lib_path
31f18b77
FG
87 if "CEPH_DEV" not in os.environ:
88 print(DEVMODEMSG, file=sys.stderr)
11fdf7f2 89 os.execvp(execv_cmd[0], execv_cmd)
7c673cae
FG
90 sys.path.insert(0, os.path.join(MYDIR, pybind_path))
91 sys.path.insert(0, os.path.join(MYDIR, pythonlib_path))
92
31f18b77 93
7c673cae
FG
94def get_pythonlib_dir():
95 """Returns the name of a distutils build directory"""
96 return "lib.{version[0]}".format(version=sys.version_info)
97
11fdf7f2 98
eafe8130 99def get_cmake_variables(*names):
11fdf7f2
TL
100 vars = dict((name, None) for name in names)
101 for line in open(os.path.join(MYPDIR, "CMakeCache.txt")):
102 # parse lines like "WITH_ASAN:BOOL=ON"
103 for name in names:
104 if line.startswith("{}:".format(name)):
eafe8130
TL
105 type_value = line.split(":")[1].strip()
106 t, v = type_value.split("=")
107 if t == 'BOOL':
108 v = v.upper() in ('TRUE', '1', 'Y', 'YES', 'ON')
109 vars[name] = v
11fdf7f2
TL
110 break
111 if all(vars.values()):
112 break
eafe8130 113 return [vars[name] for name in names]
11fdf7f2
TL
114
115
224ce89b
WB
116if os.path.exists(os.path.join(MYPDIR, "CMakeCache.txt")) \
117 and os.path.exists(os.path.join(MYPDIR, "bin/init-ceph")):
eafe8130
TL
118 src_path, with_asan, asan_lib_path = \
119 get_cmake_variables("ceph_SOURCE_DIR", "WITH_ASAN", "ASAN_LIBRARY")
7c673cae
FG
120 if src_path is None:
121 # Huh, maybe we're not really in a cmake environment?
122 pass
123 else:
124 # Developer mode, but in a cmake build dir instead of the src dir
224ce89b
WB
125 lib_path = os.path.join(MYPDIR, "lib")
126 bin_path = os.path.join(MYPDIR, "bin")
7c673cae
FG
127 pybind_path = os.path.join(src_path, "src", "pybind")
128 pythonlib_path = os.path.join(lib_path,
129 "cython_modules",
130 get_pythonlib_dir())
eafe8130
TL
131 respawn_in_path(lib_path, pybind_path, pythonlib_path,
132 asan_lib_path if with_asan else None)
7c673cae
FG
133
134 if 'PATH' in os.environ and bin_path not in os.environ['PATH']:
11fdf7f2 135 os.environ['PATH'] = os.pathsep.join([bin_path, os.environ['PATH']])
7c673cae
FG
136
137import argparse
138import errno
139import json
140import rados
141import shlex
142import signal
143import string
144import subprocess
145
146from ceph_argparse import \
147 concise_sig, descsort_key, parse_json_funcsigs, \
11fdf7f2
TL
148 validate_command, find_cmd_target, \
149 json_command, run_in_thread, Flag
7c673cae 150
31f18b77 151from ceph_daemon import admin_socket, DaemonWatcher, Termsize
7c673cae
FG
152
153# just a couple of globals
154
155verbose = False
156cluster_handle = None
157
158# Always use Unicode (UTF-8) for stdout
159if sys.version_info[0] >= 3:
160 raw_stdout = sys.stdout.buffer
161 raw_stderr = sys.stderr.buffer
162else:
163 raw_stdout = sys.__stdout__
164 raw_stderr = sys.__stderr__
165 sys.stdout = codecs.getwriter('utf-8')(raw_stdout)
166 sys.stderr = codecs.getwriter('utf-8')(raw_stderr)
167
31f18b77 168
7c673cae
FG
169def raw_write(buf):
170 sys.stdout.flush()
11fdf7f2 171 raw_stdout.write(rados.cstr(buf, ''))
7c673cae 172
7c673cae
FG
173
174def osdids():
175 ret, outbuf, outs = json_command(cluster_handle, prefix='osd ls')
7c673cae
FG
176 if ret:
177 raise RuntimeError('Can\'t contact mon for osd list')
178 return [line.decode('utf-8') for line in outbuf.split(b'\n') if line]
179
31f18b77 180
7c673cae
FG
181def monids():
182 ret, outbuf, outs = json_command(cluster_handle, prefix='mon dump',
31f18b77 183 argdict={'format': 'json'})
7c673cae
FG
184 if ret:
185 raise RuntimeError('Can\'t contact mon for mon list')
186 d = json.loads(outbuf.decode('utf-8'))
187 return [m['name'] for m in d['mons']]
188
31f18b77 189
7c673cae 190def mdsids():
181888fb 191 ret, outbuf, outs = json_command(cluster_handle, prefix='fs dump',
31f18b77 192 argdict={'format': 'json'})
7c673cae
FG
193 if ret:
194 raise RuntimeError('Can\'t contact mon for mds list')
195 d = json.loads(outbuf.decode('utf-8'))
196 l = []
181888fb
FG
197 for info in d['standbys']:
198 l.append(info['name'])
199 for fs in d['filesystems']:
200 for info in fs['mdsmap']['info'].values():
201 l.append(info['name'])
7c673cae
FG
202 return l
203
31f18b77
FG
204
205def mgrids():
206 ret, outbuf, outs = json_command(cluster_handle, prefix='mgr dump',
207 argdict={'format': 'json'})
208 if ret:
209 raise RuntimeError('Can\'t contact mon for mgr list')
210
211 d = json.loads(outbuf.decode('utf-8'))
212 l = []
213 l.append(d['active_name'])
9f95a23c
TL
214 # we can only send tell commands to the active mgr
215 #for i in d['standbys']:
216 # l.append(i['name'])
31f18b77
FG
217 return l
218
219
181888fb
FG
220def ids_by_service(service):
221 ids = {"mon": monids,
222 "osd": osdids,
223 "mds": mdsids,
224 "mgr": mgrids}
225 return ids[service]()
226
227
31f18b77
FG
228def validate_target(target):
229 """
230 this function will return true iff target is a correct
231 target, such as mon.a/osd.2/mds.a/mgr.
232
233 target: array, likes ['osd', '2']
234 return: bool, or raise RuntimeError
235 """
236
237 if len(target) == 2:
238 # for case "service.id"
239 service_name, service_id = target[0], target[1]
181888fb
FG
240 try:
241 exist_ids = ids_by_service(service_name)
242 except KeyError:
31f18b77
FG
243 print('WARN: {0} is not a legal service name, should be one of mon/osd/mds/mgr'.format(service_name),
244 file=sys.stderr)
245 return False
246
3efd9988 247 if service_id in exist_ids or len(exist_ids) > 0 and service_id == '*':
31f18b77
FG
248 return True
249 else:
250 print('WARN: the service id you provided does not exist. service id should '
251 'be one of {0}.'.format('/'.join(exist_ids)), file=sys.stderr)
252 return False
253
254 elif len(target) == 1 and target[0] in ['mgr', 'mon']:
255 return True
256 else:
257 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)
258 return False
259
260
7c673cae
FG
261# these args must be passed to all child programs
262GLOBAL_ARGS = {
263 'client_id': '--id',
264 'client_name': '--name',
265 'cluster': '--cluster',
266 'cephconf': '--conf',
267}
268
31f18b77 269
7c673cae 270def parse_cmdargs(args=None, target=''):
11fdf7f2
TL
271 """
272 Consume generic arguments from the start of the ``args``
273 list. Call this first to handle arguments that are not
274 handled by a command description provided by the server.
275
276 :returns: three tuple of ArgumentParser instance, Namespace instance
277 containing parsed values, and list of un-handled arguments
278 """
7c673cae
FG
279 # alias: let the line-wrapping be sane
280 AP = argparse.ArgumentParser
281
282 # format our own help
283 parser = AP(description='Ceph administration tool', add_help=False)
284
285 parser.add_argument('--completion', action='store_true',
286 help=argparse.SUPPRESS)
287
288 parser.add_argument('-h', '--help', help='request mon help',
289 action='store_true')
290
291 parser.add_argument('-c', '--conf', dest='cephconf',
292 help='ceph configuration file')
293 parser.add_argument('-i', '--in-file', dest='input_file',
c07f9fc5 294 help='input file, or "-" for stdin')
7c673cae 295 parser.add_argument('-o', '--out-file', dest='output_file',
c07f9fc5 296 help='output file, or "-" for stdout')
a8e16298
TL
297 parser.add_argument('--setuser', dest='setuser',
298 help='set user file permission')
299 parser.add_argument('--setgroup', dest='setgroup',
300 help='set group file permission')
7c673cae
FG
301 parser.add_argument('--id', '--user', dest='client_id',
302 help='client id for authentication')
303 parser.add_argument('--name', '-n', dest='client_name',
304 help='client name for authentication')
305 parser.add_argument('--cluster', help='cluster name')
306
307 parser.add_argument('--admin-daemon', dest='admin_socket',
308 help='submit admin-socket commands (\"help\" for help')
7c673cae
FG
309
310 parser.add_argument('-s', '--status', action='store_true',
311 help='show cluster status')
312
313 parser.add_argument('-w', '--watch', action='store_true',
314 help='watch live cluster changes')
315 parser.add_argument('--watch-debug', action='store_true',
316 help='watch debug events')
317 parser.add_argument('--watch-info', action='store_true',
318 help='watch info events')
319 parser.add_argument('--watch-sec', action='store_true',
320 help='watch security events')
321 parser.add_argument('--watch-warn', action='store_true',
322 help='watch warn events')
323 parser.add_argument('--watch-error', action='store_true',
324 help='watch error events')
325
9f95a23c
TL
326 parser.add_argument('-W', '--watch-channel', dest="watch_channel",
327 help="watch live cluster changes on a specific channel "
328 "(e.g., cluster, audit, cephadm, or '*' for all)")
224ce89b 329
7c673cae
FG
330 parser.add_argument('--version', '-v', action="store_true", help="display version")
331 parser.add_argument('--verbose', action="store_true", help="make verbose")
332 parser.add_argument('--concise', dest='verbose', action="store_false",
333 help="make less verbose")
334
335 parser.add_argument('-f', '--format', choices=['json', 'json-pretty',
1911f103 336 'xml', 'xml-pretty', 'plain', 'yaml'], dest='output_format')
7c673cae
FG
337
338 parser.add_argument('--connect-timeout', dest='cluster_timeout',
339 type=int,
340 help='set a timeout for connecting to the cluster')
341
11fdf7f2
TL
342 parser.add_argument('--block', action='store_true',
343 help='block until completion (scrub and deep-scrub only)')
344 parser.add_argument('--period', '-p', default=1, type=float,
345 help='polling period, default 1.0 second (for ' \
346 'polling commands only)')
347
7c673cae
FG
348 # returns a Namespace with the parsed args, and a list of all extras
349 parsed_args, extras = parser.parse_known_args(args)
350
351 return parser, parsed_args, extras
352
353
354def hdr(s):
355 print('\n', s, '\n', '=' * len(s))
356
31f18b77 357
7c673cae
FG
358def do_basic_help(parser, args):
359 """
360 Print basic parser help
361 If the cluster is available, get and print monitor help
362 """
363 hdr('General usage:')
364 parser.print_help()
365 print_locally_handled_command_help()
366
31f18b77 367
7c673cae
FG
368def print_locally_handled_command_help():
369 hdr("Local commands:")
370 print("""
371ping <mon.id> Send simple presence/life test to a mon
372 <mon.id> may be 'mon.*' for all mons
373daemon {type.id|path} <cmd>
374 Same as --admin-daemon, but auto-find admin socket
375daemonperf {type.id | path} [stat-pats] [priority] [<interval>] [<count>]
376daemonperf {type.id | path} list|ls [stat-pats] [priority]
377 Get selected perf stats from daemon/admin socket
378 Optional shell-glob comma-delim match string stat-pats
379 Optional selection priority (can abbreviate name):
380 critical, interesting, useful, noninteresting, debug
381 List shows a table of all available stats
382 Run <count> times (default forever),
383 once per <interval> seconds (default 1)
384 """, file=sys.stdout)
385
386
31f18b77 387def do_extended_help(parser, args, target, partial):
7c673cae
FG
388 def help_for_sigs(sigs, partial=None):
389 sys.stdout.write(format_help(parse_json_funcsigs(sigs, 'cli'),
390 partial=partial))
391
392 def help_for_target(target, partial=None):
d2e6a577
FG
393 # wait for osdmap because we know this is sent after the mgrmap
394 # and monmap (it's alphabetical).
395 cluster_handle.wait_for_latest_osdmap()
7c673cae
FG
396 ret, outbuf, outs = json_command(cluster_handle, target=target,
397 prefix='get_command_descriptions',
398 timeout=10)
399 if ret:
9f95a23c 400 if (ret == -errno.EPERM or ret == -errno.EACCES) and target[0] in ('osd', 'mds'):
11fdf7f2
TL
401 print("Permission denied. Check that your user has 'allow *' "
402 "capabilities for the target daemon type.", file=sys.stderr)
403 elif ret == -errno.EPERM:
404 print("Permission denied. Check your user has proper "
405 "capabilities configured", file=sys.stderr)
406 else:
407 print("couldn't get command descriptions for {0}: {1} ({2})".
408 format(target, outs, ret), file=sys.stderr)
31f18b77 409 return ret
7c673cae 410 else:
31f18b77 411 return help_for_sigs(outbuf.decode('utf-8'), partial)
7c673cae 412
31f18b77
FG
413 assert(cluster_handle.state == "connected")
414 return help_for_target(target, partial)
7c673cae
FG
415
416DONTSPLIT = string.ascii_letters + '{[<>]}'
417
31f18b77 418
7c673cae
FG
419def wrap(s, width, indent):
420 """
421 generator to transform s into a sequence of strings width or shorter,
422 for wrapping text to a specific column width.
423 Attempt to break on anything but DONTSPLIT characters.
424 indent is amount to indent 2nd-through-nth lines.
425
426 so "long string long string long string" width=11 indent=1 becomes
427 'long string', ' long string', ' long string' so that it can be printed
428 as
429 long string
430 long string
431 long string
432
433 Consumes s.
434 """
435 result = ''
436 leader = ''
437 while len(s):
438
31f18b77 439 if len(s) <= width:
7c673cae
FG
440 # no splitting; just possibly indent
441 result = leader + s
442 s = ''
443 yield result
444
445 else:
446 splitpos = width
447 while (splitpos > 0) and (s[splitpos-1] in DONTSPLIT):
448 splitpos -= 1
449
450 if splitpos == 0:
451 splitpos = width
452
453 if result:
454 # prior result means we're mid-iteration, indent
455 result = leader
456 else:
457 # first time, set leader and width for next
458 leader = ' ' * indent
459 width -= 1 # for subsequent space additions
460
461 # remove any leading spaces in this chunk of s
462 result += s[:splitpos].lstrip()
463 s = s[splitpos:]
464
465 yield result
466
31f18b77 467
7c673cae
FG
468def format_help(cmddict, partial=None):
469 """
470 Formats all the cmdsigs and helptexts from cmddict into a sorted-by-
471 cmdsig 2-column display, with each column wrapped and indented to
31f18b77 472 fit into (terminal_width / 2) characters.
7c673cae
FG
473 """
474
475 fullusage = ''
476 for cmd in sorted(cmddict.values(), key=descsort_key):
477
478 if not cmd['help']:
479 continue
31f18b77 480 flags = cmd.get('flags', 0)
11fdf7f2 481 if flags & (Flag.OBSOLETE | Flag.DEPRECATED | Flag.HIDDEN):
7c673cae
FG
482 continue
483 concise = concise_sig(cmd['sig'])
484 if partial and not concise.startswith(partial):
485 continue
31f18b77
FG
486 width = Termsize().cols - 1 # 1 for the line between sig and help
487 sig_width = int(width / 2)
488 # make sure width == sig_width + help_width, even (width % 2 > 0)
489 help_width = int(width / 2) + (width % 2)
490 siglines = [l for l in wrap(concise, sig_width, 1)]
491 helplines = [l for l in wrap(cmd['help'], help_width, 1)]
7c673cae
FG
492
493 # make lists the same length
494 maxlen = max(len(siglines), len(helplines))
495 siglines.extend([''] * (maxlen - len(siglines)))
496 helplines.extend([''] * (maxlen - len(helplines)))
497
498 # so we can zip them for output
31f18b77
FG
499 for s, h in zip(siglines, helplines):
500 fullusage += '{s:{w}s} {h}\n'.format(s=s, h=h, w=sig_width)
7c673cae
FG
501
502 return fullusage
503
504
505def ceph_conf(parsed_args, field, name):
31f18b77 506 args = ['ceph-conf']
7c673cae
FG
507
508 if name:
509 args.extend(['--name', name])
510
511 # add any args in GLOBAL_ARGS
512 for key, val in GLOBAL_ARGS.items():
513 # ignore name in favor of argument name, if any
514 if name and key == 'client_name':
515 continue
516 if getattr(parsed_args, key):
517 args.extend([val, getattr(parsed_args, key)])
518
519 args.extend(['--show-config-value', field])
520 p = subprocess.Popen(
521 args,
522 stdout=subprocess.PIPE,
523 stderr=subprocess.PIPE)
524 outdata, errdata = p.communicate()
92f5a8d4 525 if p.returncode != 0:
7c673cae
FG
526 raise RuntimeError('unable to get conf option %s for %s: %s' % (field, name, errdata))
527 return outdata.rstrip()
528
529PROMPT = 'ceph> '
530
531if sys.stdin.isatty():
532 def read_input():
533 while True:
534 line = input(PROMPT).rstrip()
535 if line in ['q', 'quit', 'Q', 'exit']:
536 return None
537 if line:
538 return line
539else:
540 def read_input():
541 while True:
542 line = sys.stdin.readline()
543 if not line:
544 return None
545 line = line.rstrip()
546 if line:
547 return line
548
549
11fdf7f2
TL
550def do_command(parsed_args, target, cmdargs, sigdict, inbuf, verbose):
551 ''' Validate a command, and handle the polling flag '''
552
553 valid_dict = validate_command(sigdict, cmdargs, verbose)
554 # Validate input args against list of sigs
555 if valid_dict:
556 if parsed_args.output_format:
557 valid_dict['format'] = parsed_args.output_format
558 if verbose:
559 print("Submitting command: ", valid_dict, file=sys.stderr)
560 else:
561 return -errno.EINVAL, '', 'invalid command'
562
563 next_header_print = 0
564 # Set extra options for polling commands only:
565 if valid_dict.get('poll', False):
566 valid_dict['width'] = Termsize().cols
567 while True:
568 try:
569 # Only print the header for polling commands
570 if next_header_print == 0 and valid_dict.get('poll', False):
571 valid_dict['print_header'] = True
572 next_header_print = Termsize().rows - 3
573 next_header_print -= 1
574 ret, outbuf, outs = json_command(cluster_handle, target=target,
9f95a23c 575 argdict=valid_dict, inbuf=inbuf, verbose=verbose)
11fdf7f2
TL
576 if valid_dict.get('poll', False):
577 valid_dict['print_header'] = False
578 if not valid_dict.get('poll', False):
579 # Don't print here if it's not a polling command
580 break
581 if ret:
582 ret = abs(ret)
583 print('Error: {0} {1}'.format(ret, errno.errorcode.get(ret, 'Unknown')),
584 file=sys.stderr)
585 break
586 if outbuf:
587 print(outbuf.decode('utf-8'))
588 if outs:
589 print(outs, file=sys.stderr)
590 if parsed_args.period <= 0:
591 break
592 sleep(parsed_args.period)
593 except KeyboardInterrupt:
594 print('Interrupted')
9f95a23c 595 return errno.EINTR, '', ''
11fdf7f2
TL
596 if ret == errno.ETIMEDOUT:
597 ret = -ret
598 if not outs:
599 outs = ("Connection timed out. Please check the client's " +
600 "permission and connection.")
601 return ret, outbuf, outs
602
603
7c673cae
FG
604def new_style_command(parsed_args, cmdargs, target, sigdict, inbuf, verbose):
605 """
606 Do new-style command dance.
607 target: daemon to receive command: mon (any) or osd.N
608 sigdict - the parsed output from the new monitor describing commands
609 inbuf - any -i input file data
610 verbose - bool
611 """
612 if verbose:
613 for cmdtag in sorted(sigdict.keys()):
614 cmd = sigdict[cmdtag]
615 sig = cmd['sig']
616 print('{0}: {1}'.format(cmdtag, concise_sig(sig)))
617
618 if True:
619 if cmdargs:
11fdf7f2
TL
620 # Non interactive mode
621 ret, outbuf, outs = do_command(parsed_args, target, cmdargs, sigdict, inbuf, verbose)
7c673cae 622 else:
11fdf7f2 623 # Interactive mode (ceph cli)
7c673cae
FG
624 if sys.stdin.isatty():
625 # do the command-interpreter looping
626 # for input to do readline cmd editing
627 import readline # noqa
628
629 while True:
11fdf7f2
TL
630 try:
631 interactive_input = read_input()
632 except EOFError:
633 # leave user an uncluttered prompt
634 return 0, '\n', ''
7c673cae
FG
635 if interactive_input is None:
636 return 0, '', ''
637 cmdargs = parse_cmdargs(shlex.split(interactive_input))[2]
638 try:
639 target = find_cmd_target(cmdargs)
640 except Exception as e:
31f18b77
FG
641 print('error handling command target: {0}'.format(e),
642 file=sys.stderr)
7c673cae
FG
643 continue
644 if len(cmdargs) and cmdargs[0] == 'tell':
31f18b77
FG
645 print('Can not use \'tell\' in interactive mode.',
646 file=sys.stderr)
7c673cae 647 continue
11fdf7f2
TL
648 ret, outbuf, outs = do_command(parsed_args, target, cmdargs,
649 sigdict, inbuf, verbose)
650 if ret < 0:
651 ret = -ret
652 errstr = errno.errorcode.get(ret, 'Unknown')
653 print(u'Error {0}: {1}'.format(errstr, outs), file=sys.stderr)
654 else:
655 if outs:
656 print(outs, file=sys.stderr)
7c673cae 657 if outbuf:
9f95a23c 658 print(outbuf.decode('utf-8'))
7c673cae 659
11fdf7f2 660 return ret, outbuf, outs
7c673cae
FG
661
662
663def complete(sigdict, args, target):
664 """
665 Command completion. Match as much of [args] as possible,
666 and print every possible match separated by newlines.
667 Return exitcode.
668 """
669 # XXX this looks a lot like the front of validate_command(). Refactor?
670
7c673cae
FG
671 # Repulsive hack to handle tell: lop off 'tell' and target
672 # and validate the rest of the command. 'target' is already
673 # determined in our callers, so it's ok to remove it here.
674 if len(args) and args[0] == 'tell':
675 args = args[2:]
676 # look for best match, accumulate possibles in bestcmds
677 # (so we can maybe give a more-useful error message)
678
679 match_count = 0
680 comps = []
681 for cmdtag, cmd in sigdict.items():
e306af50
TL
682 flags = cmd.get('flags', 0)
683 if flags & (Flag.OBSOLETE | Flag.HIDDEN):
684 continue
7c673cae
FG
685 sig = cmd['sig']
686 j = 0
687 # iterate over all arguments, except last one
688 for arg in args[0:-1]:
689 if j > len(sig)-1:
690 # an out of argument definitions
691 break
692 found_match = arg in sig[j].complete(arg)
693 if not found_match and sig[j].req:
694 # no elements that match
695 break
696 if not sig[j].N:
697 j += 1
698 else:
699 # successfully matched all - except last one - arguments
700 if j < len(sig) and len(args) > 0:
701 comps += sig[j].complete(args[-1])
702
703 match_count += 1
704 match_cmd = cmd
705
706 if match_count == 1 and len(comps) == 0:
707 # only one command matched and no hints yet => add help
708 comps = comps + [' ', '#'+match_cmd['help']]
709 print('\n'.join(sorted(set(comps))))
710 return 0
711
712
7c673cae
FG
713def ping_monitor(cluster_handle, name, timeout):
714 if 'mon.' not in name:
715 print('"ping" expects a monitor to ping; try "ping mon.<id>"', file=sys.stderr)
716 return 1
717
718 mon_id = name[len('mon.'):]
31f18b77 719 if mon_id == '*':
7c673cae 720 run_in_thread(cluster_handle.connect, timeout=timeout)
31f18b77 721 for m in monids():
7c673cae
FG
722 s = run_in_thread(cluster_handle.ping_monitor, m)
723 if s is None:
724 print("mon.{0}".format(m) + '\n' + "Error connecting to monitor.")
725 else:
726 print("mon.{0}".format(m) + '\n' + s)
31f18b77 727 else:
7c673cae
FG
728 s = run_in_thread(cluster_handle.ping_monitor, mon_id)
729 print(s)
730 return 0
731
732
733def maybe_daemon_command(parsed_args, childargs):
734 """
735 Check if --admin-socket, daemon, or daemonperf command
736 if it is, returns (boolean handled, return code if handled == True)
737 """
738
739 daemon_perf = False
740 sockpath = None
741 if parsed_args.admin_socket:
742 sockpath = parsed_args.admin_socket
743 elif len(childargs) > 0 and childargs[0] in ["daemon", "daemonperf"]:
744 daemon_perf = (childargs[0] == "daemonperf")
745 # Treat "daemon <path>" or "daemon <name>" like --admin_daemon <path>
746 # Handle "daemonperf <path>" the same but requires no trailing args
747 require_args = 2 if daemon_perf else 3
748 if len(childargs) >= require_args:
749 if childargs[1].find('/') >= 0:
750 sockpath = childargs[1]
751 else:
752 # try resolve daemon name
753 try:
754 sockpath = ceph_conf(parsed_args, 'admin_socket',
755 childargs[1])
756 except Exception as e:
757 print('Can\'t get admin socket path: ' + str(e), file=sys.stderr)
758 return True, errno.EINVAL
759 # for both:
760 childargs = childargs[2:]
761 else:
31f18b77
FG
762 print('{0} requires at least {1} arguments'.format(childargs[0], require_args),
763 file=sys.stderr)
7c673cae
FG
764 return True, errno.EINVAL
765
766 if sockpath and daemon_perf:
767 return True, daemonperf(childargs, sockpath)
768 elif sockpath:
769 try:
770 raw_write(admin_socket(sockpath, childargs, parsed_args.output_format))
771 except Exception as e:
772 print('admin_socket: {0}'.format(e), file=sys.stderr)
773 return True, errno.EINVAL
774 return True, 0
775
776 return False, 0
777
778
779def isnum(s):
780 try:
781 float(s)
782 return True
783 except ValueError:
784 return False
785
31f18b77 786
7c673cae
FG
787def daemonperf(childargs, sockpath):
788 """
789 Handle daemonperf command; returns errno or 0
790
791 daemonperf <daemon> [priority string] [statpats] [interval] [count]
792 daemonperf <daemon> list|ls [statpats]
793 """
794
795 interval = 1
796 count = None
797 statpats = None
798 priority = None
799 do_list = False
800
801 def prio_from_name(arg):
802
803 PRIOMAP = {
804 'critical': PRIO_CRITICAL,
805 'interesting': PRIO_INTERESTING,
806 'useful': PRIO_USEFUL,
807 'uninteresting': PRIO_UNINTERESTING,
808 'debugonly': PRIO_DEBUGONLY,
809 }
810
811 if arg in PRIOMAP:
812 return PRIOMAP[arg]
813 # allow abbreviation
814 for name, val in PRIOMAP.items():
815 if name.startswith(arg):
816 return val
817 return None
818
819 # consume and analyze non-numeric args
820 while len(childargs) and not isnum(childargs[0]):
821 arg = childargs.pop(0)
822 # 'list'?
823 if arg in ['list', 'ls']:
31f18b77 824 do_list = True
7c673cae
FG
825 continue
826 # prio?
827 prio = prio_from_name(arg)
828 if prio is not None:
829 priority = prio
830 continue
831 # statpats
832 statpats = arg.split(',')
833
834 if priority is None:
835 priority = PRIO_DEFAULT
836
837 if len(childargs) > 0:
838 try:
839 interval = float(childargs.pop(0))
840 if interval < 0:
841 raise ValueError
842 except ValueError:
843 print('daemonperf: interval should be a positive number', file=sys.stderr)
844 return errno.EINVAL
845
846 if len(childargs) > 0:
847 arg = childargs.pop(0)
848 if (not isnum(arg)) or (int(arg) < 0):
849 print('daemonperf: count should be a positive integer', file=sys.stderr)
850 return errno.EINVAL
851 count = int(arg)
852
853 watcher = DaemonWatcher(sockpath, statpats, priority)
854 if do_list:
855 watcher.list()
856 else:
857 watcher.run(interval, count)
858
859 return 0
860
11fdf7f2
TL
861def get_scrub_timestamps(childargs):
862 last_scrub_stamp = "last_" + childargs[1].replace('-', '_') + "_stamp"
863 results = dict()
864 scruball = False
865 if childargs[2] in ['all', 'any', '*']:
866 scruball = True
867 devnull = open(os.devnull, 'w')
868 out = subprocess.check_output(['ceph', 'pg', 'dump', '--format=json-pretty'],
869 stderr=devnull)
870 try:
871 pgstats = json.loads(out)['pg_map']['pg_stats']
872 except KeyError:
873 pgstats = json.loads(out)['pg_stats']
874 for stat in pgstats:
875 if scruball or stat['up_primary'] == int(childargs[2]):
876 scrub_tuple = (stat['up_primary'], stat[last_scrub_stamp])
877 results[stat['pgid']] = scrub_tuple
878 return results
879
880def check_scrub_stamps(waitdata, currdata):
881 for pg in waitdata.keys():
882 # Try to handle the case where a pg may not exist in current results
883 if pg in currdata and waitdata[pg][1] == currdata[pg][1]:
884 return False
885 return True
886
887def waitscrub(childargs, waitdata):
888 print(u'Waiting for {0} to complete...'.format(childargs[1]), file=sys.stdout)
889 currdata = get_scrub_timestamps(childargs)
890 while not check_scrub_stamps(waitdata, currdata):
891 time.sleep(3)
892 currdata = get_scrub_timestamps(childargs)
893 print(u'{0} completed'.format(childargs[1]), file=sys.stdout)
894
895def wait(childargs, waitdata):
896 if childargs[1] in ['scrub', 'deep-scrub']:
897 waitscrub(childargs, waitdata)
898
7c673cae
FG
899
900def main():
901 ceph_args = os.environ.get('CEPH_ARGS')
902 if ceph_args:
903 if "injectargs" in sys.argv:
904 i = sys.argv.index("injectargs")
905 sys.argv = sys.argv[:i] + ceph_args.split() + sys.argv[i:]
906 else:
c07f9fc5
FG
907 sys.argv.extend([arg for arg in ceph_args.split()
908 if '--admin-socket' not in arg])
7c673cae
FG
909 parser, parsed_args, childargs = parse_cmdargs()
910
911 if parsed_args.version:
31f18b77
FG
912 print('ceph version {0} ({1}) {2} ({3})'.format(
913 CEPH_GIT_NICE_VER,
914 CEPH_GIT_VER,
915 CEPH_RELEASE_NAME,
916 CEPH_RELEASE_TYPE)) # noqa
7c673cae
FG
917 return 0
918
9f95a23c
TL
919 # --watch-channel|-W implies -w
920 if parsed_args.watch_channel:
921 parsed_args.watch = True
922 elif parsed_args.watch and not parsed_args.watch_channel:
923 parsed_args.watch_channel = 'cluster'
924
7c673cae
FG
925 global verbose
926 verbose = parsed_args.verbose
927
928 if verbose:
929 print("parsed_args: {0}, childargs: {1}".format(parsed_args, childargs), file=sys.stderr)
930
7c673cae
FG
931 # pass on --id, --name, --conf
932 name = 'client.admin'
933 if parsed_args.client_id:
934 name = 'client.' + parsed_args.client_id
935 if parsed_args.client_name:
936 name = parsed_args.client_name
937
938 # default '' means default conf search
939 conffile = ''
940 if parsed_args.cephconf:
941 conffile = parsed_args.cephconf
942 # For now, --admin-daemon is handled as usual. Try it
943 # first in case we can't connect() to the cluster
944
7c673cae
FG
945 done, ret = maybe_daemon_command(parsed_args, childargs)
946 if done:
947 return ret
948
949 timeout = None
950 if parsed_args.cluster_timeout:
951 timeout = parsed_args.cluster_timeout
952
953 # basic help
954 if parsed_args.help:
955 do_basic_help(parser, childargs)
956
957 # handle any 'generic' ceph arguments that we didn't parse here
958 global cluster_handle
959
960 # rados.Rados() will call rados_create2, and then read the conf file,
961 # and then set the keys from the dict. So we must do these
962 # "pre-file defaults" first (see common_preinit in librados)
963 conf_defaults = {
31f18b77
FG
964 'log_to_stderr': 'true',
965 'err_to_stderr': 'true',
966 'log_flush_on_exit': 'true',
7c673cae
FG
967 }
968
969 if 'injectargs' in childargs:
970 position = childargs.index('injectargs')
971 injectargs = childargs[position:]
972 childargs = childargs[:position]
973 if verbose:
31f18b77
FG
974 print('Separate childargs {0} from injectargs {1}'.format(childargs, injectargs),
975 file=sys.stderr)
7c673cae
FG
976 else:
977 injectargs = None
978
979 clustername = None
980 if parsed_args.cluster:
981 clustername = parsed_args.cluster
982
983 try:
984 cluster_handle = run_in_thread(rados.Rados,
985 name=name, clustername=clustername,
986 conf_defaults=conf_defaults,
987 conffile=conffile)
988 retargs = run_in_thread(cluster_handle.conf_parse_argv, childargs)
989 except rados.Error as e:
990 print('Error initializing cluster client: {0!r}'.format(e), file=sys.stderr)
991 return 1
992
993 childargs = retargs
994 if not childargs:
995 childargs = []
996
997 # -- means "stop parsing args", but we don't want to see it either
998 if '--' in childargs:
999 childargs.remove('--')
1000 if injectargs and '--' in injectargs:
1001 injectargs.remove('--')
1002
11fdf7f2
TL
1003 block = False
1004 waitdata = dict()
1005 if parsed_args.block:
1006 if (len(childargs) >= 2 and
1007 childargs[0] == 'osd' and
1008 childargs[1] in ['deep-scrub', 'scrub']):
1009 block = True
1010 waitdata = get_scrub_timestamps(childargs)
1011
7c673cae
FG
1012 if parsed_args.help:
1013 # short default timeout for -h
1014 if not timeout:
1015 timeout = 5
1016
224ce89b 1017 if childargs and childargs[0] == 'ping' and not parsed_args.help:
7c673cae
FG
1018 if len(childargs) < 2:
1019 print('"ping" requires a monitor name as argument: "ping mon.<id>"', file=sys.stderr)
1020 return 1
1021 if parsed_args.completion:
31f18b77 1022 # for completion let timeout be really small
7c673cae
FG
1023 timeout = 3
1024 try:
224ce89b 1025 if childargs and childargs[0] == 'ping' and not parsed_args.help:
7c673cae 1026 return ping_monitor(cluster_handle, childargs[1], timeout)
224ce89b
WB
1027 result = run_in_thread(cluster_handle.connect, timeout=timeout)
1028 if type(result) is tuple and result[0] == -errno.EINTR:
1029 print('Cluster connection interrupted or timed out', file=sys.stderr)
1030 return 1
7c673cae
FG
1031 except KeyboardInterrupt:
1032 print('Cluster connection aborted', file=sys.stderr)
1033 return 1
1034 except rados.PermissionDeniedError as e:
1035 print(str(e), file=sys.stderr)
1036 return errno.EACCES
1037 except Exception as e:
1038 print(str(e), file=sys.stderr)
1039 return 1
224ce89b 1040
7c673cae 1041 if parsed_args.help:
9f95a23c
TL
1042 target = None
1043 if len(childargs) >= 2 and childargs[0] == 'tell':
f6b5b4d7 1044 target = childargs[1].split('.', 1)
9f95a23c
TL
1045 if not validate_target(target):
1046 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)
1047 return 1
1048 childargs = childargs[2:]
1049 hdr('Tell %s commands:' % target[0])
1050 else:
1051 hdr('Monitor commands:')
1052 target = ('mon', '')
224ce89b
WB
1053 if verbose:
1054 print('[Contacting monitor, timeout after %d seconds]' % timeout)
1055
9f95a23c 1056 return do_extended_help(parser, childargs, target, ' '.join(childargs))
7c673cae 1057
31f18b77
FG
1058 # implement "tell service.id help"
1059 if len(childargs) >= 3 and childargs[0] == 'tell' and childargs[2] == 'help':
f6b5b4d7 1060 target = childargs[1].split('.', 1)
31f18b77 1061 if validate_target(target):
9f95a23c 1062 hdr('Tell %s commands' % target[0])
31f18b77
FG
1063 return do_extended_help(parser, childargs, target, None)
1064 else:
1065 print('target {0} doesn\'t exists, please pass correct target to tell command, such as mon.a/'
1066 'osd.1/mds.a/mgr'.format(childargs[1]), file=sys.stderr)
1067 return 1
9f95a23c 1068
7c673cae
FG
1069 # implement -w/--watch_*
1070 # This is ugly, but Namespace() isn't quite rich enough.
1071 level = ''
1072 for k, v in parsed_args._get_kwargs():
1073 if k.startswith('watch') and v:
1074 if k == 'watch':
1075 level = 'info'
224ce89b 1076 elif k != "watch_channel":
7c673cae
FG
1077 level = k.replace('watch_', '')
1078 if level:
7c673cae 1079 # an awfully simple callback
224ce89b
WB
1080 def watch_cb(arg, line, channel, name, who, stamp_sec, stamp_nsec, seq, level, msg):
1081 # Filter on channel
11fdf7f2
TL
1082 if sys.version_info[0] >= 3:
1083 channel = channel.decode('utf-8')
224ce89b
WB
1084 if (channel == parsed_args.watch_channel or \
1085 parsed_args.watch_channel == "*"):
11fdf7f2 1086 print(line.decode('utf-8'))
224ce89b 1087 sys.stdout.flush()
7c673cae
FG
1088
1089 # first do a ceph status
1090 ret, outbuf, outs = json_command(cluster_handle, prefix='status')
7c673cae
FG
1091 if ret:
1092 print("status query failed: ", outs, file=sys.stderr)
1093 return ret
11fdf7f2 1094 print(outbuf.decode('utf-8'))
7c673cae
FG
1095
1096 # this instance keeps the watch connection alive, but is
1097 # otherwise unused
224ce89b 1098 run_in_thread(cluster_handle.monitor_log2, level, watch_cb, 0)
7c673cae
FG
1099
1100 # loop forever letting watch_cb print lines
1101 try:
1102 signal.pause()
1103 except KeyboardInterrupt:
1104 # or until ^C, at least
1105 return 0
1106
1107 # read input file, if any
1108 inbuf = b''
1109 if parsed_args.input_file:
1110 try:
c07f9fc5
FG
1111 if parsed_args.input_file == '-':
1112 inbuf = sys.stdin.read()
1113 else:
1114 with open(parsed_args.input_file, 'rb') as f:
1115 inbuf = f.read()
7c673cae
FG
1116 except Exception as e:
1117 print('Can\'t open input file {0}: {1}'.format(parsed_args.input_file, e), file=sys.stderr)
1118 return 1
1119
1120 # prepare output file, if any
1121 if parsed_args.output_file:
1122 try:
c07f9fc5 1123 if parsed_args.output_file == '-':
9f95a23c 1124 outf = raw_stdout
c07f9fc5
FG
1125 else:
1126 outf = open(parsed_args.output_file, 'wb')
7c673cae
FG
1127 except Exception as e:
1128 print('Can\'t open output file {0}: {1}'.format(parsed_args.output_file, e), file=sys.stderr)
1129 return 1
a8e16298
TL
1130 if parsed_args.setuser:
1131 try:
1132 ownerid = pwd.getpwnam(parsed_args.setuser).pw_uid
1133 os.fchown(outf.fileno(), ownerid, -1)
1134 except OSError as e:
1135 print('Failed to change user ownership of {0} to {1}: {2}'.format(outf, parsed_args.setuser, e))
1136 return 1
1137 if parsed_args.setgroup:
1138 try:
1139 groupid = grp.getgrnam(parsed_args.setgroup).gr_gid
1140 os.fchown(outf.fileno(), -1, groupid)
1141 except OSError as e:
1142 print('Failed to change group ownership of {0} to {1}: {2}'.format(outf, parsed_args.setgroup, e))
1143 return 1
7c673cae
FG
1144
1145 # -s behaves like a command (ceph status).
1146 if parsed_args.status:
1147 childargs.insert(0, 'status')
1148
1149 try:
1150 target = find_cmd_target(childargs)
1151 except Exception as e:
1152 print('error handling command target: {0}'.format(e), file=sys.stderr)
1153 return 1
1154
1155 # Repulsive hack to handle tell: lop off 'tell' and target
1156 # and validate the rest of the command. 'target' is already
1157 # determined in our callers, so it's ok to remove it here.
1158 is_tell = False
1159 if len(childargs) and childargs[0] == 'tell':
1160 childargs = childargs[2:]
1161 is_tell = True
1162
1163 if is_tell:
1164 if injectargs:
1165 childargs = injectargs
1166 if not len(childargs):
1167 print('"{0} tell" requires additional arguments.'.format(sys.argv[0]),
31f18b77
FG
1168 'Try "{0} tell <name> <command> [options...]" instead.'.format(sys.argv[0]),
1169 file=sys.stderr)
7c673cae
FG
1170 return errno.EINVAL
1171
1172 # fetch JSON sigs from command
1173 # each line contains one command signature (a placeholder name
1174 # of the form 'cmdNNN' followed by an array of argument descriptors)
1175 # as part of the validated argument JSON object
1176
7c673cae 1177 if target[1] == '*':
181888fb
FG
1178 service = target[0]
1179 targets = [(service, o) for o in ids_by_service(service)]
1180 else:
1181 targets = [target]
7c673cae
FG
1182
1183 final_ret = 0
1184 for target in targets:
1185 # prettify? prefix output with target, if there was a wildcard used
1186 prefix = ''
1187 suffix = ''
1188 if not parsed_args.output_file and len(targets) > 1:
1189 prefix = '{0}.{1}: '.format(*target)
1190 suffix = '\n'
1191
1192 ret, outbuf, outs = json_command(cluster_handle, target=target,
1193 prefix='get_command_descriptions')
31f18b77
FG
1194 if ret:
1195 where = '{0}.{1}'.format(*target)
1196 if ret > 0:
11fdf7f2 1197 raise RuntimeError('Unexpected return code from {0}: {1}'.
31f18b77
FG
1198 format(where, ret))
1199 outs = 'problem getting command descriptions from {0}'.format(where)
1200 else:
1201 sigdict = parse_json_funcsigs(outbuf.decode('utf-8'), 'cli')
7c673cae 1202
31f18b77
FG
1203 if parsed_args.completion:
1204 return complete(sigdict, childargs, target)
7c673cae 1205
31f18b77
FG
1206 ret, outbuf, outs = new_style_command(parsed_args, childargs,
1207 target, sigdict, inbuf,
1208 verbose)
7c673cae 1209
31f18b77
FG
1210 # debug tool: send any successful command *again* to
1211 # verify that it is idempotent.
1212 if not ret and 'CEPH_CLI_TEST_DUP_COMMAND' in os.environ:
1213 ret, outbuf, outs = new_style_command(parsed_args, childargs,
1214 target, sigdict, inbuf,
1215 verbose)
1216 if ret < 0:
1217 ret = -ret
1218 print(prefix +
1219 'Second attempt of previously successful command '
1220 'failed with {0}: {1}'.format(
1221 errno.errorcode.get(ret, 'Unknown'), outs),
1222 file=sys.stderr)
7c673cae
FG
1223
1224 if ret < 0:
1225 ret = -ret
31f18b77
FG
1226 errstr = errno.errorcode.get(ret, 'Unknown')
1227 print(u'Error {0}: {1}'.format(errstr, outs), file=sys.stderr)
7c673cae
FG
1228 if len(targets) > 1:
1229 final_ret = ret
1230 else:
1231 return ret
1232
31f18b77
FG
1233 if outs:
1234 print(prefix + outs, file=sys.stderr)
7c673cae
FG
1235
1236 sys.stdout.flush()
1237
31f18b77 1238 if parsed_args.output_file:
7c673cae
FG
1239 outf.write(outbuf)
1240 else:
1241 # hack: old code printed status line before many json outputs
1242 # (osd dump, etc.) that consumers know to ignore. Add blank line
1243 # to satisfy consumers that skip the first line, but not annoy
1244 # consumers that don't.
1245 if parsed_args.output_format and \
31f18b77 1246 parsed_args.output_format.startswith('json'):
7c673cae
FG
1247 print()
1248
1249 # if we are prettifying things, normalize newlines. sigh.
1250 if suffix:
1251 outbuf = outbuf.rstrip()
1252 if outbuf:
1253 try:
1254 print(prefix, end='')
1255 # Write directly to binary stdout
1256 raw_write(outbuf)
1257 print(suffix, end='')
1258 except IOError as e:
1259 if e.errno != errno.EPIPE:
1260 raise e
1261
1262 sys.stdout.flush()
1263
11fdf7f2
TL
1264 # Block until command completion (currently scrub and deep_scrub only)
1265 if block:
1266 wait(childargs, waitdata)
1267
c07f9fc5 1268 if parsed_args.output_file and parsed_args.output_file != '-':
7c673cae
FG
1269 outf.close()
1270
1271 if final_ret:
1272 return final_ret
1273
1274 return 0
1275
1276if __name__ == '__main__':
9f95a23c
TL
1277 try:
1278 retval = main()
1279 # shutdown explicitly; Rados() does not
1280 if retval == 0 and cluster_handle:
1281 run_in_thread(cluster_handle.shutdown)
1282 except KeyboardInterrupt:
1283 print('Interrupted')
1284 retval = errno.EINTR
1285
1286 if retval:
1287 # flush explicitly because we aren't exiting in the usual way
1288 sys.stdout.flush()
1289 sys.stderr.flush()
1290 os._exit(retval)
1291 else:
1292 sys.exit(retval)