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