]> git.proxmox.com Git - ceph.git/blob - ceph/src/ceph.in
bump version to 15.2.1-pve1
[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 __future__ import print_function
23 from time import sleep
24 import codecs
25 import grp
26 import os
27 import pwd
28 import sys
29 import time
30 import platform
31
32 try:
33 input = raw_input
34 except NameError:
35 pass
36
37 CEPH_GIT_VER = "@CEPH_GIT_VER@"
38 CEPH_GIT_NICE_VER = "@CEPH_GIT_NICE_VER@"
39 CEPH_RELEASE = "@CEPH_RELEASE@"
40 CEPH_RELEASE_NAME = "@CEPH_RELEASE_NAME@"
41 CEPH_RELEASE_TYPE = "@CEPH_RELEASE_TYPE@"
42
43 # priorities from src/common/perf_counters.h
44 PRIO_CRITICAL = 10
45 PRIO_INTERESTING = 8
46 PRIO_USEFUL = 5
47 PRIO_UNINTERESTING = 2
48 PRIO_DEBUGONLY = 0
49
50 PRIO_DEFAULT = PRIO_INTERESTING
51
52 # Make life easier on developers:
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.
59
60 MYPATH = os.path.abspath(__file__)
61 MYDIR = os.path.dirname(MYPATH)
62 MYPDIR = os.path.dirname(MYDIR)
63 DEVMODEMSG = '*** DEVELOPER MODE: setting PATH, PYTHONPATH and LD_LIBRARY_PATH ***'
64
65
66 def respawn_in_path(lib_path, pybind_path, pythonlib_path, asan_lib_path):
67 execv_cmd = []
68 if 'CEPH_DBG' in os.environ:
69 execv_cmd += ['@Python3_EXECUTABLE@', '-mpdb']
70
71 if platform.system() == "Darwin":
72 lib_path_var = "DYLD_LIBRARY_PATH"
73 else:
74 lib_path_var = "LD_LIBRARY_PATH"
75
76 execv_cmd += sys.argv
77 if asan_lib_path:
78 os.environ['LD_PRELOAD'] = asan_lib_path
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
82 if "CEPH_DEV" not in os.environ:
83 print(DEVMODEMSG, file=sys.stderr)
84 os.execvp(execv_cmd[0], execv_cmd)
85 else:
86 os.environ[lib_path_var] = lib_path
87 if "CEPH_DEV" not in os.environ:
88 print(DEVMODEMSG, file=sys.stderr)
89 os.execvp(execv_cmd[0], execv_cmd)
90 sys.path.insert(0, os.path.join(MYDIR, pybind_path))
91 sys.path.insert(0, os.path.join(MYDIR, pythonlib_path))
92
93
94 def get_pythonlib_dir():
95 """Returns the name of a distutils build directory"""
96 return "lib.{version[0]}".format(version=sys.version_info)
97
98
99 def get_cmake_variables(*names):
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)):
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
110 break
111 if all(vars.values()):
112 break
113 return [vars[name] for name in names]
114
115
116 if os.path.exists(os.path.join(MYPDIR, "CMakeCache.txt")) \
117 and os.path.exists(os.path.join(MYPDIR, "bin/init-ceph")):
118 src_path, with_asan, asan_lib_path = \
119 get_cmake_variables("ceph_SOURCE_DIR", "WITH_ASAN", "ASAN_LIBRARY")
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
125 lib_path = os.path.join(MYPDIR, "lib")
126 bin_path = os.path.join(MYPDIR, "bin")
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())
131 respawn_in_path(lib_path, pybind_path, pythonlib_path,
132 asan_lib_path if with_asan else None)
133
134 if 'PATH' in os.environ and bin_path not in os.environ['PATH']:
135 os.environ['PATH'] = os.pathsep.join([bin_path, os.environ['PATH']])
136
137 import argparse
138 import errno
139 import json
140 import rados
141 import shlex
142 import signal
143 import string
144 import subprocess
145
146 from ceph_argparse import \
147 concise_sig, descsort_key, parse_json_funcsigs, \
148 validate_command, find_cmd_target, \
149 json_command, run_in_thread, Flag
150
151 from ceph_daemon import admin_socket, DaemonWatcher, Termsize
152
153 # just a couple of globals
154
155 verbose = False
156 cluster_handle = None
157
158 # Always use Unicode (UTF-8) for stdout
159 if sys.version_info[0] >= 3:
160 raw_stdout = sys.stdout.buffer
161 raw_stderr = sys.stderr.buffer
162 else:
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
168
169 def raw_write(buf):
170 sys.stdout.flush()
171 raw_stdout.write(rados.cstr(buf, ''))
172
173
174 def osdids():
175 ret, outbuf, outs = json_command(cluster_handle, prefix='osd ls')
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
180
181 def monids():
182 ret, outbuf, outs = json_command(cluster_handle, prefix='mon dump',
183 argdict={'format': 'json'})
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
189
190 def mdsids():
191 ret, outbuf, outs = json_command(cluster_handle, prefix='fs dump',
192 argdict={'format': 'json'})
193 if ret:
194 raise RuntimeError('Can\'t contact mon for mds list')
195 d = json.loads(outbuf.decode('utf-8'))
196 l = []
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'])
202 return l
203
204
205 def 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'])
214 # we can only send tell commands to the active mgr
215 #for i in d['standbys']:
216 # l.append(i['name'])
217 return l
218
219
220 def ids_by_service(service):
221 ids = {"mon": monids,
222 "osd": osdids,
223 "mds": mdsids,
224 "mgr": mgrids}
225 return ids[service]()
226
227
228 def 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]
240 try:
241 exist_ids = ids_by_service(service_name)
242 except KeyError:
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
247 if service_id in exist_ids or len(exist_ids) > 0 and service_id == '*':
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
261 # these args must be passed to all child programs
262 GLOBAL_ARGS = {
263 'client_id': '--id',
264 'client_name': '--name',
265 'cluster': '--cluster',
266 'cephconf': '--conf',
267 }
268
269
270 def parse_cmdargs(args=None, target=''):
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 """
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',
294 help='input file, or "-" for stdin')
295 parser.add_argument('-o', '--out-file', dest='output_file',
296 help='output file, or "-" for stdout')
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')
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')
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
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)")
329
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',
336 'xml', 'xml-pretty', 'plain'], dest='output_format')
337
338 parser.add_argument('--connect-timeout', dest='cluster_timeout',
339 type=int,
340 help='set a timeout for connecting to the cluster')
341
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
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
354 def hdr(s):
355 print('\n', s, '\n', '=' * len(s))
356
357
358 def 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
367
368 def print_locally_handled_command_help():
369 hdr("Local commands:")
370 print("""
371 ping <mon.id> Send simple presence/life test to a mon
372 <mon.id> may be 'mon.*' for all mons
373 daemon {type.id|path} <cmd>
374 Same as --admin-daemon, but auto-find admin socket
375 daemonperf {type.id | path} [stat-pats] [priority] [<interval>] [<count>]
376 daemonperf {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
387 def do_extended_help(parser, args, target, partial):
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):
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()
396 ret, outbuf, outs = json_command(cluster_handle, target=target,
397 prefix='get_command_descriptions',
398 timeout=10)
399 if ret:
400 if (ret == -errno.EPERM or ret == -errno.EACCES) and target[0] in ('osd', 'mds'):
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)
409 return ret
410 else:
411 return help_for_sigs(outbuf.decode('utf-8'), partial)
412
413 assert(cluster_handle.state == "connected")
414 return help_for_target(target, partial)
415
416 DONTSPLIT = string.ascii_letters + '{[<>]}'
417
418
419 def 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
439 if len(s) <= width:
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
467
468 def 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
472 fit into (terminal_width / 2) characters.
473 """
474
475 fullusage = ''
476 for cmd in sorted(cmddict.values(), key=descsort_key):
477
478 if not cmd['help']:
479 continue
480 flags = cmd.get('flags', 0)
481 if flags & (Flag.OBSOLETE | Flag.DEPRECATED | Flag.HIDDEN):
482 continue
483 concise = concise_sig(cmd['sig'])
484 if partial and not concise.startswith(partial):
485 continue
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)]
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
499 for s, h in zip(siglines, helplines):
500 fullusage += '{s:{w}s} {h}\n'.format(s=s, h=h, w=sig_width)
501
502 return fullusage
503
504
505 def ceph_conf(parsed_args, field, name):
506 args = ['ceph-conf']
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()
525 if p.returncode != 0:
526 raise RuntimeError('unable to get conf option %s for %s: %s' % (field, name, errdata))
527 return outdata.rstrip()
528
529 PROMPT = 'ceph> '
530
531 if 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
539 else:
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
550 def 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,
575 argdict=valid_dict, inbuf=inbuf, verbose=verbose)
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')
595 return errno.EINTR, '', ''
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
604 def 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:
620 # Non interactive mode
621 ret, outbuf, outs = do_command(parsed_args, target, cmdargs, sigdict, inbuf, verbose)
622 else:
623 # Interactive mode (ceph cli)
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:
630 try:
631 interactive_input = read_input()
632 except EOFError:
633 # leave user an uncluttered prompt
634 return 0, '\n', ''
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:
641 print('error handling command target: {0}'.format(e),
642 file=sys.stderr)
643 continue
644 if len(cmdargs) and cmdargs[0] == 'tell':
645 print('Can not use \'tell\' in interactive mode.',
646 file=sys.stderr)
647 continue
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)
657 if outbuf:
658 print(outbuf.decode('utf-8'))
659
660 return ret, outbuf, outs
661
662
663 def 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
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():
682 sig = cmd['sig']
683 j = 0
684 # iterate over all arguments, except last one
685 for arg in args[0:-1]:
686 if j > len(sig)-1:
687 # an out of argument definitions
688 break
689 found_match = arg in sig[j].complete(arg)
690 if not found_match and sig[j].req:
691 # no elements that match
692 break
693 if not sig[j].N:
694 j += 1
695 else:
696 # successfully matched all - except last one - arguments
697 if j < len(sig) and len(args) > 0:
698 comps += sig[j].complete(args[-1])
699
700 match_count += 1
701 match_cmd = cmd
702
703 if match_count == 1 and len(comps) == 0:
704 # only one command matched and no hints yet => add help
705 comps = comps + [' ', '#'+match_cmd['help']]
706 print('\n'.join(sorted(set(comps))))
707 return 0
708
709
710 def ping_monitor(cluster_handle, name, timeout):
711 if 'mon.' not in name:
712 print('"ping" expects a monitor to ping; try "ping mon.<id>"', file=sys.stderr)
713 return 1
714
715 mon_id = name[len('mon.'):]
716 if mon_id == '*':
717 run_in_thread(cluster_handle.connect, timeout=timeout)
718 for m in monids():
719 s = run_in_thread(cluster_handle.ping_monitor, m)
720 if s is None:
721 print("mon.{0}".format(m) + '\n' + "Error connecting to monitor.")
722 else:
723 print("mon.{0}".format(m) + '\n' + s)
724 else:
725 s = run_in_thread(cluster_handle.ping_monitor, mon_id)
726 print(s)
727 return 0
728
729
730 def maybe_daemon_command(parsed_args, childargs):
731 """
732 Check if --admin-socket, daemon, or daemonperf command
733 if it is, returns (boolean handled, return code if handled == True)
734 """
735
736 daemon_perf = False
737 sockpath = None
738 if parsed_args.admin_socket:
739 sockpath = parsed_args.admin_socket
740 elif len(childargs) > 0 and childargs[0] in ["daemon", "daemonperf"]:
741 daemon_perf = (childargs[0] == "daemonperf")
742 # Treat "daemon <path>" or "daemon <name>" like --admin_daemon <path>
743 # Handle "daemonperf <path>" the same but requires no trailing args
744 require_args = 2 if daemon_perf else 3
745 if len(childargs) >= require_args:
746 if childargs[1].find('/') >= 0:
747 sockpath = childargs[1]
748 else:
749 # try resolve daemon name
750 try:
751 sockpath = ceph_conf(parsed_args, 'admin_socket',
752 childargs[1])
753 except Exception as e:
754 print('Can\'t get admin socket path: ' + str(e), file=sys.stderr)
755 return True, errno.EINVAL
756 # for both:
757 childargs = childargs[2:]
758 else:
759 print('{0} requires at least {1} arguments'.format(childargs[0], require_args),
760 file=sys.stderr)
761 return True, errno.EINVAL
762
763 if sockpath and daemon_perf:
764 return True, daemonperf(childargs, sockpath)
765 elif sockpath:
766 try:
767 raw_write(admin_socket(sockpath, childargs, parsed_args.output_format))
768 except Exception as e:
769 print('admin_socket: {0}'.format(e), file=sys.stderr)
770 return True, errno.EINVAL
771 return True, 0
772
773 return False, 0
774
775
776 def isnum(s):
777 try:
778 float(s)
779 return True
780 except ValueError:
781 return False
782
783
784 def daemonperf(childargs, sockpath):
785 """
786 Handle daemonperf command; returns errno or 0
787
788 daemonperf <daemon> [priority string] [statpats] [interval] [count]
789 daemonperf <daemon> list|ls [statpats]
790 """
791
792 interval = 1
793 count = None
794 statpats = None
795 priority = None
796 do_list = False
797
798 def prio_from_name(arg):
799
800 PRIOMAP = {
801 'critical': PRIO_CRITICAL,
802 'interesting': PRIO_INTERESTING,
803 'useful': PRIO_USEFUL,
804 'uninteresting': PRIO_UNINTERESTING,
805 'debugonly': PRIO_DEBUGONLY,
806 }
807
808 if arg in PRIOMAP:
809 return PRIOMAP[arg]
810 # allow abbreviation
811 for name, val in PRIOMAP.items():
812 if name.startswith(arg):
813 return val
814 return None
815
816 # consume and analyze non-numeric args
817 while len(childargs) and not isnum(childargs[0]):
818 arg = childargs.pop(0)
819 # 'list'?
820 if arg in ['list', 'ls']:
821 do_list = True
822 continue
823 # prio?
824 prio = prio_from_name(arg)
825 if prio is not None:
826 priority = prio
827 continue
828 # statpats
829 statpats = arg.split(',')
830
831 if priority is None:
832 priority = PRIO_DEFAULT
833
834 if len(childargs) > 0:
835 try:
836 interval = float(childargs.pop(0))
837 if interval < 0:
838 raise ValueError
839 except ValueError:
840 print('daemonperf: interval should be a positive number', file=sys.stderr)
841 return errno.EINVAL
842
843 if len(childargs) > 0:
844 arg = childargs.pop(0)
845 if (not isnum(arg)) or (int(arg) < 0):
846 print('daemonperf: count should be a positive integer', file=sys.stderr)
847 return errno.EINVAL
848 count = int(arg)
849
850 watcher = DaemonWatcher(sockpath, statpats, priority)
851 if do_list:
852 watcher.list()
853 else:
854 watcher.run(interval, count)
855
856 return 0
857
858 def get_scrub_timestamps(childargs):
859 last_scrub_stamp = "last_" + childargs[1].replace('-', '_') + "_stamp"
860 results = dict()
861 scruball = False
862 if childargs[2] in ['all', 'any', '*']:
863 scruball = True
864 devnull = open(os.devnull, 'w')
865 out = subprocess.check_output(['ceph', 'pg', 'dump', '--format=json-pretty'],
866 stderr=devnull)
867 try:
868 pgstats = json.loads(out)['pg_map']['pg_stats']
869 except KeyError:
870 pgstats = json.loads(out)['pg_stats']
871 for stat in pgstats:
872 if scruball or stat['up_primary'] == int(childargs[2]):
873 scrub_tuple = (stat['up_primary'], stat[last_scrub_stamp])
874 results[stat['pgid']] = scrub_tuple
875 return results
876
877 def check_scrub_stamps(waitdata, currdata):
878 for pg in waitdata.keys():
879 # Try to handle the case where a pg may not exist in current results
880 if pg in currdata and waitdata[pg][1] == currdata[pg][1]:
881 return False
882 return True
883
884 def waitscrub(childargs, waitdata):
885 print(u'Waiting for {0} to complete...'.format(childargs[1]), file=sys.stdout)
886 currdata = get_scrub_timestamps(childargs)
887 while not check_scrub_stamps(waitdata, currdata):
888 time.sleep(3)
889 currdata = get_scrub_timestamps(childargs)
890 print(u'{0} completed'.format(childargs[1]), file=sys.stdout)
891
892 def wait(childargs, waitdata):
893 if childargs[1] in ['scrub', 'deep-scrub']:
894 waitscrub(childargs, waitdata)
895
896
897 def main():
898 ceph_args = os.environ.get('CEPH_ARGS')
899 if ceph_args:
900 if "injectargs" in sys.argv:
901 i = sys.argv.index("injectargs")
902 sys.argv = sys.argv[:i] + ceph_args.split() + sys.argv[i:]
903 else:
904 sys.argv.extend([arg for arg in ceph_args.split()
905 if '--admin-socket' not in arg])
906 parser, parsed_args, childargs = parse_cmdargs()
907
908 if parsed_args.version:
909 print('ceph version {0} ({1}) {2} ({3})'.format(
910 CEPH_GIT_NICE_VER,
911 CEPH_GIT_VER,
912 CEPH_RELEASE_NAME,
913 CEPH_RELEASE_TYPE)) # noqa
914 return 0
915
916 # --watch-channel|-W implies -w
917 if parsed_args.watch_channel:
918 parsed_args.watch = True
919 elif parsed_args.watch and not parsed_args.watch_channel:
920 parsed_args.watch_channel = 'cluster'
921
922 global verbose
923 verbose = parsed_args.verbose
924
925 if verbose:
926 print("parsed_args: {0}, childargs: {1}".format(parsed_args, childargs), file=sys.stderr)
927
928 # pass on --id, --name, --conf
929 name = 'client.admin'
930 if parsed_args.client_id:
931 name = 'client.' + parsed_args.client_id
932 if parsed_args.client_name:
933 name = parsed_args.client_name
934
935 # default '' means default conf search
936 conffile = ''
937 if parsed_args.cephconf:
938 conffile = parsed_args.cephconf
939 # For now, --admin-daemon is handled as usual. Try it
940 # first in case we can't connect() to the cluster
941
942 done, ret = maybe_daemon_command(parsed_args, childargs)
943 if done:
944 return ret
945
946 timeout = None
947 if parsed_args.cluster_timeout:
948 timeout = parsed_args.cluster_timeout
949
950 # basic help
951 if parsed_args.help:
952 do_basic_help(parser, childargs)
953
954 # handle any 'generic' ceph arguments that we didn't parse here
955 global cluster_handle
956
957 # rados.Rados() will call rados_create2, and then read the conf file,
958 # and then set the keys from the dict. So we must do these
959 # "pre-file defaults" first (see common_preinit in librados)
960 conf_defaults = {
961 'log_to_stderr': 'true',
962 'err_to_stderr': 'true',
963 'log_flush_on_exit': 'true',
964 }
965
966 if 'injectargs' in childargs:
967 position = childargs.index('injectargs')
968 injectargs = childargs[position:]
969 childargs = childargs[:position]
970 if verbose:
971 print('Separate childargs {0} from injectargs {1}'.format(childargs, injectargs),
972 file=sys.stderr)
973 else:
974 injectargs = None
975
976 clustername = None
977 if parsed_args.cluster:
978 clustername = parsed_args.cluster
979
980 try:
981 cluster_handle = run_in_thread(rados.Rados,
982 name=name, clustername=clustername,
983 conf_defaults=conf_defaults,
984 conffile=conffile)
985 retargs = run_in_thread(cluster_handle.conf_parse_argv, childargs)
986 except rados.Error as e:
987 print('Error initializing cluster client: {0!r}'.format(e), file=sys.stderr)
988 return 1
989
990 childargs = retargs
991 if not childargs:
992 childargs = []
993
994 # -- means "stop parsing args", but we don't want to see it either
995 if '--' in childargs:
996 childargs.remove('--')
997 if injectargs and '--' in injectargs:
998 injectargs.remove('--')
999
1000 block = False
1001 waitdata = dict()
1002 if parsed_args.block:
1003 if (len(childargs) >= 2 and
1004 childargs[0] == 'osd' and
1005 childargs[1] in ['deep-scrub', 'scrub']):
1006 block = True
1007 waitdata = get_scrub_timestamps(childargs)
1008
1009 if parsed_args.help:
1010 # short default timeout for -h
1011 if not timeout:
1012 timeout = 5
1013
1014 if childargs and childargs[0] == 'ping' and not parsed_args.help:
1015 if len(childargs) < 2:
1016 print('"ping" requires a monitor name as argument: "ping mon.<id>"', file=sys.stderr)
1017 return 1
1018 if parsed_args.completion:
1019 # for completion let timeout be really small
1020 timeout = 3
1021 try:
1022 if childargs and childargs[0] == 'ping' and not parsed_args.help:
1023 return ping_monitor(cluster_handle, childargs[1], timeout)
1024 result = run_in_thread(cluster_handle.connect, timeout=timeout)
1025 if type(result) is tuple and result[0] == -errno.EINTR:
1026 print('Cluster connection interrupted or timed out', file=sys.stderr)
1027 return 1
1028 except KeyboardInterrupt:
1029 print('Cluster connection aborted', file=sys.stderr)
1030 return 1
1031 except rados.PermissionDeniedError as e:
1032 print(str(e), file=sys.stderr)
1033 return errno.EACCES
1034 except Exception as e:
1035 print(str(e), file=sys.stderr)
1036 return 1
1037
1038 if parsed_args.help:
1039 target = None
1040 if len(childargs) >= 2 and childargs[0] == 'tell':
1041 target = childargs[1].split('.')
1042 if not validate_target(target):
1043 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)
1044 return 1
1045 childargs = childargs[2:]
1046 hdr('Tell %s commands:' % target[0])
1047 else:
1048 hdr('Monitor commands:')
1049 target = ('mon', '')
1050 if verbose:
1051 print('[Contacting monitor, timeout after %d seconds]' % timeout)
1052
1053 return do_extended_help(parser, childargs, target, ' '.join(childargs))
1054
1055 # implement "tell service.id help"
1056 if len(childargs) >= 3 and childargs[0] == 'tell' and childargs[2] == 'help':
1057 target = childargs[1].split('.')
1058 if validate_target(target):
1059 hdr('Tell %s commands' % target[0])
1060 return do_extended_help(parser, childargs, target, None)
1061 else:
1062 print('target {0} doesn\'t exists, please pass correct target to tell command, such as mon.a/'
1063 'osd.1/mds.a/mgr'.format(childargs[1]), file=sys.stderr)
1064 return 1
1065
1066 # implement -w/--watch_*
1067 # This is ugly, but Namespace() isn't quite rich enough.
1068 level = ''
1069 for k, v in parsed_args._get_kwargs():
1070 if k.startswith('watch') and v:
1071 if k == 'watch':
1072 level = 'info'
1073 elif k != "watch_channel":
1074 level = k.replace('watch_', '')
1075 if level:
1076 # an awfully simple callback
1077 def watch_cb(arg, line, channel, name, who, stamp_sec, stamp_nsec, seq, level, msg):
1078 # Filter on channel
1079 if sys.version_info[0] >= 3:
1080 channel = channel.decode('utf-8')
1081 if (channel == parsed_args.watch_channel or \
1082 parsed_args.watch_channel == "*"):
1083 print(line.decode('utf-8'))
1084 sys.stdout.flush()
1085
1086 # first do a ceph status
1087 ret, outbuf, outs = json_command(cluster_handle, prefix='status')
1088 if ret:
1089 print("status query failed: ", outs, file=sys.stderr)
1090 return ret
1091 print(outbuf.decode('utf-8'))
1092
1093 # this instance keeps the watch connection alive, but is
1094 # otherwise unused
1095 run_in_thread(cluster_handle.monitor_log2, level, watch_cb, 0)
1096
1097 # loop forever letting watch_cb print lines
1098 try:
1099 signal.pause()
1100 except KeyboardInterrupt:
1101 # or until ^C, at least
1102 return 0
1103
1104 # read input file, if any
1105 inbuf = b''
1106 if parsed_args.input_file:
1107 try:
1108 if parsed_args.input_file == '-':
1109 inbuf = sys.stdin.read()
1110 else:
1111 with open(parsed_args.input_file, 'rb') as f:
1112 inbuf = f.read()
1113 except Exception as e:
1114 print('Can\'t open input file {0}: {1}'.format(parsed_args.input_file, e), file=sys.stderr)
1115 return 1
1116
1117 # prepare output file, if any
1118 if parsed_args.output_file:
1119 try:
1120 if parsed_args.output_file == '-':
1121 outf = raw_stdout
1122 else:
1123 outf = open(parsed_args.output_file, 'wb')
1124 except Exception as e:
1125 print('Can\'t open output file {0}: {1}'.format(parsed_args.output_file, e), file=sys.stderr)
1126 return 1
1127 if parsed_args.setuser:
1128 try:
1129 ownerid = pwd.getpwnam(parsed_args.setuser).pw_uid
1130 os.fchown(outf.fileno(), ownerid, -1)
1131 except OSError as e:
1132 print('Failed to change user ownership of {0} to {1}: {2}'.format(outf, parsed_args.setuser, e))
1133 return 1
1134 if parsed_args.setgroup:
1135 try:
1136 groupid = grp.getgrnam(parsed_args.setgroup).gr_gid
1137 os.fchown(outf.fileno(), -1, groupid)
1138 except OSError as e:
1139 print('Failed to change group ownership of {0} to {1}: {2}'.format(outf, parsed_args.setgroup, e))
1140 return 1
1141
1142 # -s behaves like a command (ceph status).
1143 if parsed_args.status:
1144 childargs.insert(0, 'status')
1145
1146 try:
1147 target = find_cmd_target(childargs)
1148 except Exception as e:
1149 print('error handling command target: {0}'.format(e), file=sys.stderr)
1150 return 1
1151
1152 # Repulsive hack to handle tell: lop off 'tell' and target
1153 # and validate the rest of the command. 'target' is already
1154 # determined in our callers, so it's ok to remove it here.
1155 is_tell = False
1156 if len(childargs) and childargs[0] == 'tell':
1157 childargs = childargs[2:]
1158 is_tell = True
1159
1160 if is_tell:
1161 if injectargs:
1162 childargs = injectargs
1163 if not len(childargs):
1164 print('"{0} tell" requires additional arguments.'.format(sys.argv[0]),
1165 'Try "{0} tell <name> <command> [options...]" instead.'.format(sys.argv[0]),
1166 file=sys.stderr)
1167 return errno.EINVAL
1168
1169 # fetch JSON sigs from command
1170 # each line contains one command signature (a placeholder name
1171 # of the form 'cmdNNN' followed by an array of argument descriptors)
1172 # as part of the validated argument JSON object
1173
1174 if target[1] == '*':
1175 service = target[0]
1176 targets = [(service, o) for o in ids_by_service(service)]
1177 else:
1178 targets = [target]
1179
1180 final_ret = 0
1181 for target in targets:
1182 # prettify? prefix output with target, if there was a wildcard used
1183 prefix = ''
1184 suffix = ''
1185 if not parsed_args.output_file and len(targets) > 1:
1186 prefix = '{0}.{1}: '.format(*target)
1187 suffix = '\n'
1188
1189 ret, outbuf, outs = json_command(cluster_handle, target=target,
1190 prefix='get_command_descriptions')
1191 if ret:
1192 where = '{0}.{1}'.format(*target)
1193 if ret > 0:
1194 raise RuntimeError('Unexpected return code from {0}: {1}'.
1195 format(where, ret))
1196 outs = 'problem getting command descriptions from {0}'.format(where)
1197 else:
1198 sigdict = parse_json_funcsigs(outbuf.decode('utf-8'), 'cli')
1199
1200 if parsed_args.completion:
1201 return complete(sigdict, childargs, target)
1202
1203 ret, outbuf, outs = new_style_command(parsed_args, childargs,
1204 target, sigdict, inbuf,
1205 verbose)
1206
1207 # debug tool: send any successful command *again* to
1208 # verify that it is idempotent.
1209 if not ret and 'CEPH_CLI_TEST_DUP_COMMAND' in os.environ:
1210 ret, outbuf, outs = new_style_command(parsed_args, childargs,
1211 target, sigdict, inbuf,
1212 verbose)
1213 if ret < 0:
1214 ret = -ret
1215 print(prefix +
1216 'Second attempt of previously successful command '
1217 'failed with {0}: {1}'.format(
1218 errno.errorcode.get(ret, 'Unknown'), outs),
1219 file=sys.stderr)
1220
1221 if ret < 0:
1222 ret = -ret
1223 errstr = errno.errorcode.get(ret, 'Unknown')
1224 print(u'Error {0}: {1}'.format(errstr, outs), file=sys.stderr)
1225 if len(targets) > 1:
1226 final_ret = ret
1227 else:
1228 return ret
1229
1230 if outs:
1231 print(prefix + outs, file=sys.stderr)
1232
1233 sys.stdout.flush()
1234
1235 if parsed_args.output_file:
1236 outf.write(outbuf)
1237 else:
1238 # hack: old code printed status line before many json outputs
1239 # (osd dump, etc.) that consumers know to ignore. Add blank line
1240 # to satisfy consumers that skip the first line, but not annoy
1241 # consumers that don't.
1242 if parsed_args.output_format and \
1243 parsed_args.output_format.startswith('json'):
1244 print()
1245
1246 # if we are prettifying things, normalize newlines. sigh.
1247 if suffix:
1248 outbuf = outbuf.rstrip()
1249 if outbuf:
1250 try:
1251 print(prefix, end='')
1252 # Write directly to binary stdout
1253 raw_write(outbuf)
1254 print(suffix, end='')
1255 except IOError as e:
1256 if e.errno != errno.EPIPE:
1257 raise e
1258
1259 sys.stdout.flush()
1260
1261 # Block until command completion (currently scrub and deep_scrub only)
1262 if block:
1263 wait(childargs, waitdata)
1264
1265 if parsed_args.output_file and parsed_args.output_file != '-':
1266 outf.close()
1267
1268 if final_ret:
1269 return final_ret
1270
1271 return 0
1272
1273 if __name__ == '__main__':
1274 try:
1275 retval = main()
1276 # shutdown explicitly; Rados() does not
1277 if retval == 0 and cluster_handle:
1278 run_in_thread(cluster_handle.shutdown)
1279 except KeyboardInterrupt:
1280 print('Interrupted')
1281 retval = errno.EINTR
1282
1283 if retval:
1284 # flush explicitly because we aren't exiting in the usual way
1285 sys.stdout.flush()
1286 sys.stderr.flush()
1287 os._exit(retval)
1288 else:
1289 sys.exit(retval)