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