2 # vim: ts=4 sw=4 smarttab expandtab
4 # Processed in Makefile to add python #! line and version variable
10 ceph.in becomes ceph, the command-line management tool for Ceph clusters.
11 This is a replacement for tools/ceph.cc and tools/common.cc.
13 Copyright (C) 2013 Inktank Storage, Inc.
15 This is free software; you can redistribute it and/or
16 modify it under the terms of the GNU General Public
17 License version 2, as published by the Free Software
18 Foundation. See file COPYING.
25 # Make life easier on developers:
26 # If in src/, and .libs and pybind exist here, assume we're running
27 # from a Ceph source dir and tweak PYTHONPATH and LD_LIBRARY_PATH
30 MYPATH = os.path.abspath(__file__)
31 MYDIR = os.path.dirname(MYPATH)
32 DEVMODEMSG = '*** DEVELOPER MODE: setting PATH, PYTHONPATH and LD_LIBRARY_PATH ***'
34 if MYDIR.endswith('src') and \
35 os.path.exists(os.path.join(MYDIR, '.libs')) and \
36 os.path.exists(os.path.join(MYDIR, 'pybind')):
38 if platform.system() == "Darwin":
39 lib_path_var = "DYLD_LIBRARY_PATH"
41 lib_path_var = "LD_LIBRARY_PATH"
43 py_binary = os.environ.get("PYTHON", "python")
44 MYLIBPATH = os.path.join(MYDIR, '.libs')
45 execv_cmd = ['python']
46 if 'CEPH_DBG' in os.environ:
47 execv_cmd += ['-mpdb']
48 if lib_path_var in os.environ:
49 if MYLIBPATH not in os.environ[lib_path_var]:
50 os.environ[lib_path_var] += ':' + MYLIBPATH
51 print >> sys.stderr, DEVMODEMSG
52 os.execvp(py_binary, execv_cmd + sys.argv)
54 os.environ[lib_path_var] = MYLIBPATH
55 print >> sys.stderr, DEVMODEMSG
56 os.execvp(py_binary, execv_cmd + sys.argv)
57 sys.path.insert(0, os.path.join(MYDIR, 'pybind'))
58 if os.environ.has_key('PATH') and MYDIR not in os.environ['PATH']:
59 os.environ['PATH'] += ':' + MYDIR
72 from ceph_argparse import \
73 concise_sig, descsort, parse_json_funcsigs, \
74 matchnum, validate_command, find_cmd_target, \
75 send_command, json_command
77 # just a couple of globals
82 ############################################################################
85 ret, outbuf, outs = json_command(cluster_handle, prefix='osd ls')
86 if ret == -errno.EINVAL:
88 ret, outbuf, outs = send_command(cluster_handle, cmd=['osd', 'ls'])
90 raise RuntimeError('Can\'t contact mon for osd list')
91 return [i for i in outbuf.split('\n') if i != '']
94 ret, outbuf, outs = json_command(cluster_handle, prefix='mon dump',
95 argdict={'format':'json'})
96 if ret == -errno.EINVAL:
98 ret, outbuf, outs = send_command(cluster_handle,
99 cmd=['mon', 'dump', '--format=json'])
101 raise RuntimeError('Can\'t contact mon for mon list')
102 d = json.loads(outbuf)
103 return [m['name'] for m in d['mons']]
106 ret, outbuf, outs = json_command(cluster_handle, prefix='mds dump',
107 argdict={'format':'json'})
108 if ret == -errno.EINVAL:
110 ret, outbuf, outs = send_command(cluster_handle,
111 cmd=['mds', 'dump', '--format=json'])
113 raise RuntimeError('Can\'t contact mon for mds list')
114 d = json.loads(outbuf)
117 for mdsdict in infodict.values():
118 l.append(mdsdict['name'])
121 # these args must be passed to all child programs
124 'client_name': '--name',
125 'cluster': '--cluster',
126 'cephconf': '--conf',
129 def parse_cmdargs(args=None, target=''):
130 # alias: let the line-wrapping be sane
131 AP = argparse.ArgumentParser
133 # format our own help
134 parser = AP(description='Ceph administration tool', add_help=False)
136 parser.add_argument('--completion', action='store_true',
137 help=argparse.SUPPRESS)
139 parser.add_argument('-h', '--help', help='request mon help',
142 parser.add_argument('-c', '--conf', dest='cephconf',
143 help='ceph configuration file')
144 parser.add_argument('-i', '--in-file', dest='input_file',
146 parser.add_argument('-o', '--out-file', dest='output_file',
149 parser.add_argument('--id', '--user', dest='client_id',
150 help='client id for authentication')
151 parser.add_argument('--name', '-n', dest='client_name',
152 help='client name for authentication')
153 parser.add_argument('--cluster', help='cluster name')
155 parser.add_argument('--admin-daemon', dest='admin_socket',
156 help='submit admin-socket commands (\"help\" for help')
157 parser.add_argument('--admin-socket', dest='admin_socket_nope',
158 help='you probably mean --admin-daemon')
160 parser.add_argument('-s', '--status', action='store_true',
161 help='show cluster status')
163 parser.add_argument('-w', '--watch', action='store_true',
164 help='watch live cluster changes')
165 parser.add_argument('--watch-debug', action='store_true',
166 help='watch debug events')
167 parser.add_argument('--watch-info', action='store_true',
168 help='watch info events')
169 parser.add_argument('--watch-sec', action='store_true',
170 help='watch security events')
171 parser.add_argument('--watch-warn', action='store_true',
172 help='watch warn events')
173 parser.add_argument('--watch-error', action='store_true',
174 help='watch error events')
176 parser.add_argument('--version', '-v', action="store_true", help="display version")
177 parser.add_argument('--verbose', action="store_true", help="make verbose")
178 parser.add_argument('--concise', dest='verbose', action="store_false",
179 help="make less verbose")
181 parser.add_argument('-f', '--format', choices=['json', 'json-pretty',
182 'xml', 'xml-pretty', 'plain'], dest='output_format')
184 parser.add_argument('--connect-timeout', dest='cluster_timeout',
186 help='set a timeout for connecting to the cluster')
188 # returns a Namespace with the parsed args, and a list of all extras
189 parsed_args, extras = parser.parse_known_args(args)
191 return parser, parsed_args, extras
195 print '\n', s, '\n', '=' * len(s)
197 def do_basic_help(parser, args):
199 Print basic parser help
200 If the cluster is available, get and print monitor help
202 hdr('General usage:')
205 def do_extended_help(parser, args):
206 def help_for_sigs(sigs, partial=None):
207 sys.stdout.write(format_help(parse_json_funcsigs(sigs, 'cli'),
210 def help_for_target(target, partial=None):
211 ret, outbuf, outs = json_command(cluster_handle, target=target,
212 prefix='get_command_descriptions',
215 print >> sys.stderr, \
216 "couldn't get command descriptions for {0}: {1}".\
219 help_for_sigs(outbuf, partial)
221 partial = ' '.join(args)
222 if (cluster_handle.state == "connected"):
223 help_for_target(target=('mon', ''), partial=partial)
226 DONTSPLIT = string.letters + '{[<>]}'
228 def wrap(s, width, indent):
230 generator to transform s into a sequence of strings width or shorter,
231 for wrapping text to a specific column width.
232 Attempt to break on anything but DONTSPLIT characters.
233 indent is amount to indent 2nd-through-nth lines.
235 so "long string long string long string" width=11 indent=1 becomes
236 'long string', ' long string', ' long string' so that it can be printed
248 if (len(s) <= width):
249 # no splitting; just possibly indent
256 while (splitpos > 0) and (s[splitpos-1] in DONTSPLIT):
263 # prior result means we're mid-iteration, indent
266 # first time, set leader and width for next
267 leader = ' ' * indent
268 width -= 1 # for subsequent space additions
270 # remove any leading spaces in this chunk of s
271 result += s[:splitpos].lstrip()
278 def format_help(cmddict, partial=None):
280 Formats all the cmdsigs and helptexts from cmddict into a sorted-by-
281 cmdsig 2-column display, with each column wrapped and indented to
282 fit into 40 characters.
286 for cmd in sorted(cmddict.itervalues(), cmp=descsort):
290 concise = concise_sig(cmd['sig'])
291 if partial and not concise.startswith(partial):
293 siglines = [l for l in wrap(concise, 40, 1)]
294 helplines = [l for l in wrap(cmd['help'], 39, 1)]
296 # make lists the same length
297 maxlen = max(len(siglines), len(helplines))
298 siglines.extend([''] * (maxlen - len(siglines)))
299 helplines.extend([''] * (maxlen - len(helplines)))
301 # so we can zip them for output
302 for (s, h) in zip(siglines, helplines):
303 fullusage += '{0:40s} {1}\n'.format(s, h)
307 def admin_socket(asok_path, cmd, format=''):
309 Send a daemon (--admin-daemon) command 'cmd'. asok_path is the
310 path to the admin socket; cmd is a list of strings; format may be
311 set to one of the formatted forms to get output in that form
312 (daemon commands don't support 'plain' output).
315 def do_sockio(path, cmd):
316 """ helper: do all the actual low-level stream I/O """
317 sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
320 sock.sendall(cmd + '\0')
321 len_str = sock.recv(4)
323 raise RuntimeError("no data returned from admin socket")
324 l, = struct.unpack(">I", len_str)
329 bit = sock.recv(l - got)
333 except Exception as e:
334 raise RuntimeError('exception: ' + str(e))
338 cmd_json = do_sockio(asok_path,
339 json.dumps({"prefix":"get_command_descriptions"}))
340 except Exception as e:
341 raise RuntimeError('exception getting command descriptions: ' + str(e))
343 if cmd == 'get_command_descriptions':
346 sigdict = parse_json_funcsigs(cmd_json, 'cli')
347 valid_dict = validate_command(sigdict, cmd)
349 raise RuntimeError('invalid command')
352 valid_dict['format'] = format
355 ret = do_sockio(asok_path, json.dumps(valid_dict))
356 except Exception as e:
357 raise RuntimeError('exception: ' + str(e))
362 def ceph_conf(parsed_args, field, name):
366 args.extend(['--name', name])
368 # add any args in GLOBAL_ARGS
369 for key, val in GLOBAL_ARGS.iteritems():
370 # ignore name in favor of argument name, if any
371 if name and key == 'client_name':
373 if getattr(parsed_args, key):
374 args.extend([val, getattr(parsed_args, key)])
376 args.extend(['--show-config-value', field])
377 p = subprocess.Popen(
379 stdout=subprocess.PIPE,
380 stderr=subprocess.PIPE)
381 outdata, errdata = p.communicate()
383 raise RuntimeError('unable to get conf option %s for %s: %s' % (field, name, errdata))
384 return outdata.rstrip()
388 if sys.stdin.isatty():
391 line = raw_input(PROMPT).rstrip()
392 if line in ['q', 'quit', 'Q']:
399 line = sys.stdin.readline()
407 def new_style_command(parsed_args, cmdargs, target, sigdict, inbuf, verbose):
409 Do new-style command dance.
410 target: daemon to receive command: mon (any) or osd.N
411 sigdict - the parsed output from the new monitor describing commands
412 inbuf - any -i input file data
416 for cmdtag in sorted(sigdict.keys()):
417 cmd = sigdict[cmdtag]
419 print '{0}: {1}'.format(cmdtag, concise_sig(sig))
425 # Validate input args against list of sigs
426 valid_dict = validate_command(sigdict, cmdargs, verbose)
429 if parsed_args.output_format:
430 valid_dict['format'] = parsed_args.output_format
432 return -errno.EINVAL, '', 'invalid command'
434 if sys.stdin.isatty():
435 # do the command-interpreter looping
436 # for raw_input to do readline cmd editing
440 interactive_input = read_input()
441 if interactive_input is None:
443 cmdargs = parse_cmdargs(shlex.split(interactive_input))[2]
445 target = find_cmd_target(cmdargs)
446 except Exception as e:
447 print >> sys.stderr, \
448 'error handling command target: {0}'.format(e)
450 if len(cmdargs) and cmdargs[0] == 'tell':
451 print >> sys.stderr, \
452 'Can not use \'tell\' in interactive mode.'
454 valid_dict = validate_command(sigdict, cmdargs, verbose)
456 if parsed_args.output_format:
457 valid_dict['format'] = parsed_args.output_format
459 print >> sys.stderr, "Submitting command ", valid_dict
460 ret, outbuf, outs = json_command(cluster_handle,
465 print >> sys.stderr, \
466 'Error: {0} {1}'.format(ret, errno.errorcode.get(ret, 'Unknown'))
470 print >> sys.stderr, 'Status:\n', outs
472 print >> sys.stderr, "Invalid command"
475 print >> sys.stderr, "Submitting command ", valid_dict
476 return json_command(cluster_handle, target=target, argdict=valid_dict,
479 def complete(sigdict, args, target):
481 Command completion. Match as much of [args] as possible,
482 and print every possible match separated by newlines.
485 # XXX this looks a lot like the front of validate_command(). Refactor?
487 complete_verbose = 'COMPVERBOSE' in os.environ
489 # Repulsive hack to handle tell: lop off 'tell' and target
490 # and validate the rest of the command. 'target' is already
491 # determined in our callers, so it's ok to remove it here.
492 if len(args) and args[0] == 'tell':
494 # look for best match, accumulate possibles in bestcmds
495 # (so we can maybe give a more-useful error message)
498 for cmdtag, cmd in sigdict.iteritems():
500 matched = matchnum(args, sig, partial=True)
501 if (matched > best_match_cnt):
503 print >> sys.stderr, \
504 "better match: {0} > {1}: {2}:{3} ".format(matched,
505 best_match_cnt, cmdtag, concise_sig(sig))
506 best_match_cnt = matched
507 bestcmds = [{cmdtag:cmd}]
508 elif matched == best_match_cnt:
510 print >> sys.stderr, \
511 "equal match: {0} > {1}: {2}:{3} ".format(matched,
512 best_match_cnt, cmdtag, concise_sig(sig))
513 bestcmds.append({cmdtag:cmd})
515 # look through all matching sigs
517 for cmddict in bestcmds:
518 for cmd in cmddict.itervalues():
521 # we match everything fully, so we want the next desc, or
522 # we match more partially, so we want the partial match
523 fullindex = matchnum(args, sig, partial=False) - 1
524 partindex = matchnum(args, sig, partial=True) - 1
526 print >> sys.stderr, '{}: f {} p {} len {}'.format(sig, fullindex, partindex, len(sig))
527 if fullindex == partindex and fullindex + 1 < len(sig):
528 d = sig[fullindex + 1]
533 print >> sys.stderr, '\n'.join(comps)
534 print '\n'.join(comps)
541 def ping_monitor(cluster_handle, name):
542 if 'mon.' not in name:
543 print >> sys.stderr, '"ping" expects a monitor to ping; try "ping mon.<id>"'
546 mon_id = name[len('mon.'):]
547 s = cluster_handle.ping_monitor(mon_id)
556 ceph_args = os.environ.get('CEPH_ARGS')
558 if "injectargs" in sys.argv:
559 i = sys.argv.index("injectargs")
560 sys.argv = sys.argv[:i] + ceph_args.split() + sys.argv[i:]
562 sys.argv.extend(ceph_args.split())
563 parser, parsed_args, childargs = parse_cmdargs()
565 if parsed_args.version:
566 print 'ceph version {0} ({1})'.format(CEPH_GIT_NICE_VER, CEPH_GIT_VER)
570 verbose = parsed_args.verbose
573 print >> sys.stderr, "parsed_args: {0}, childargs: {1}".format(parsed_args, childargs)
575 if parsed_args.admin_socket_nope:
576 print >> sys.stderr, '--admin-socket is used by daemons; '\
577 'you probably mean --admin-daemon/daemon'
580 # pass on --id, --name, --conf
581 name = 'client.admin'
582 if parsed_args.client_id:
583 name = 'client.' + parsed_args.client_id
584 if parsed_args.client_name:
585 name = parsed_args.client_name
587 # default '' means default conf search
589 if parsed_args.cephconf:
590 conffile = parsed_args.cephconf
591 # For now, --admin-daemon is handled as usual. Try it
592 # first in case we can't connect() to the cluster
594 format = parsed_args.output_format
597 if parsed_args.admin_socket:
598 sockpath = parsed_args.admin_socket
599 elif len(childargs) > 0 and childargs[0] == "daemon":
600 # Treat "daemon <path>" or "daemon <name>" like --admin_daemon <path>
601 if len(childargs) > 2:
602 if childargs[1].find('/') >= 0:
603 sockpath = childargs[1]
605 # try resolve daemon name
607 sockpath = ceph_conf(parsed_args, 'admin_socket',
609 except Exception as e:
610 print >> sys.stderr, \
611 'Can\'t get admin socket path: ' + str(e)
614 childargs = childargs[2:]
616 print >> sys.stderr, 'daemon requires at least 3 arguments'
621 print admin_socket(sockpath, childargs, format)
622 except Exception as e:
623 print >> sys.stderr, 'admin_socket: {0}'.format(e)
628 if parsed_args.cluster_timeout:
629 timeout = parsed_args.cluster_timeout
633 do_basic_help(parser, childargs)
635 # handle any 'generic' ceph arguments that we didn't parse here
636 global cluster_handle
638 # rados.Rados() will call rados_create2, and then read the conf file,
639 # and then set the keys from the dict. So we must do these
640 # "pre-file defaults" first (see common_preinit in librados)
642 'log_to_stderr':'true',
643 'err_to_stderr':'true',
644 'log_flush_on_exit':'true',
647 if 'injectargs' in childargs:
648 position = childargs.index('injectargs')
649 injectargs = childargs[position:]
650 childargs = childargs[:position]
652 print >> sys.stderr, 'Separate childargs {0} from injectargs {1}'.\
653 format(childargs, injectargs)
658 if parsed_args.cluster:
659 clustername = parsed_args.cluster
662 cluster_handle = rados.Rados(name=name, clustername=clustername,
663 conf_defaults=conf_defaults,
665 retargs = cluster_handle.conf_parse_argv(childargs)
666 except rados.Error as e:
667 print >> sys.stderr, 'Error initializing cluster client: {0}'.\
675 # -- means "stop parsing args", but we don't want to see it either
676 if '--' in childargs:
677 childargs.remove('--')
678 if injectargs and '--' in injectargs:
679 injectargs.remove('--')
681 # special deprecation warning for 'ceph <type> tell'
682 # someday 'mds' will be here too
683 if len(childargs) >= 2 and \
684 childargs[0] in ['mon', 'osd'] and \
685 childargs[1] == 'tell':
686 print >> sys.stderr, '"{0} tell" is deprecated; try "tell {0}.<id> <command> [options...]" instead (id can be "*") '.format(childargs[0])
690 # short default timeout for -h
694 hdr('Monitor commands:')
695 print '[Contacting monitor, timeout after %d seconds]' % timeout
697 if childargs and childargs[0] == 'ping':
698 if len(childargs) < 2:
699 print >> sys.stderr, '"ping" requires a monitor name as argument: "ping mon.<id>"'
703 if childargs and childargs[0] == 'ping':
704 return ping_monitor(cluster_handle, childargs[1])
705 cluster_handle.connect(timeout=timeout)
706 except KeyboardInterrupt:
707 print >> sys.stderr, 'Cluster connection aborted'
709 except Exception as e:
710 print >> sys.stderr, 'Error connecting to cluster: {0}'.\
711 format(e.__class__.__name__)
715 return do_extended_help(parser, childargs)
717 # implement -w/--watch_*
718 # This is ugly, but Namespace() isn't quite rich enough.
720 for k, v in parsed_args._get_kwargs():
721 if k.startswith('watch') and v:
725 level = k.replace('watch_', '')
728 # an awfully simple callback
729 def watch_cb(arg, line, who, stamp_sec, stamp_nsec, seq, level, msg):
733 # first do a ceph status
734 ret, outbuf, outs = json_command(cluster_handle, prefix='status')
735 if ret == -errno.EINVAL:
737 ret, outbuf, outs = send_command(cluster_handle, cmd=['status'])
738 # old mon returns status to outs...ick
742 print >> sys.stderr, "status query failed: ", outs
746 # this instance keeps the watch connection alive, but is
748 logwatch = rados.MonitorLog(cluster_handle, level, watch_cb, 0)
750 # loop forever letting watch_cb print lines
753 except KeyboardInterrupt:
754 # or until ^C, at least
757 # read input file, if any
759 if parsed_args.input_file:
761 with open(parsed_args.input_file, 'r') as f:
763 except Exception as e:
764 print >> sys.stderr, 'Can\'t open input file {0}: {1}'.format(parsed_args.input_file, e)
767 # prepare output file, if any
768 if parsed_args.output_file:
770 outf = open(parsed_args.output_file, 'w')
771 except Exception as e:
772 print >> sys.stderr, \
773 'Can\'t open output file {0}: {1}'.\
774 format(parsed_args.output_file, e)
777 # -s behaves like a command (ceph status).
778 if parsed_args.status:
779 childargs.insert(0, 'status')
782 target = find_cmd_target(childargs)
783 except Exception as e:
784 print >> sys.stderr, \
785 'error handling command target: {0}'.format(e)
788 # Repulsive hack to handle tell: lop off 'tell' and target
789 # and validate the rest of the command. 'target' is already
790 # determined in our callers, so it's ok to remove it here.
792 if len(childargs) and childargs[0] == 'tell':
793 childargs = childargs[2:]
798 childargs = injectargs
799 if not len(childargs):
800 print >> sys.stderr, \
801 '"{0} tell" requires additional arguments.'.format(sys.argv[0]), \
802 'Try "{0} tell <name> <command> [options...]" instead.'.format(sys.argv[0])
805 # fetch JSON sigs from command
806 # each line contains one command signature (a placeholder name
807 # of the form 'cmdNNN' followed by an array of argument descriptors)
808 # as part of the validated argument JSON object
813 if target[0] == 'osd':
814 targets = [(target[0], o) for o in osdids()]
815 elif target[0] == 'mon':
816 targets = [(target[0], m) for m in monids()]
819 for target in targets:
820 # prettify? prefix output with target, if there was a wildcard used
823 if not parsed_args.output_file and len(targets) > 1:
824 prefix = '{0}.{1}: '.format(*target)
827 ret, outbuf, outs = json_command(cluster_handle, target=target,
828 prefix='get_command_descriptions')
830 if ret == -errno.EINVAL:
831 # send command to old monitor or OSD
833 print prefix + '{0} to old {1}'.format(' '.join(childargs), target[0])
835 if parsed_args.output_format:
836 childargs.extend(['--format', parsed_args.output_format])
837 ret, outbuf, outs = send_command(cluster_handle, target, childargs,
840 if ret == -errno.EINVAL:
841 # did we race with a mon upgrade? try again!
842 ret, outbuf, outs = json_command(cluster_handle, target=target,
843 prefix='get_command_descriptions')
845 compat = False # yep, carry on
849 outs = 'problem getting command descriptions from {0}.{1}'.format(*target)
851 sigdict = parse_json_funcsigs(outbuf, 'cli')
853 if parsed_args.completion:
854 return complete(sigdict, childargs, target)
856 ret, outbuf, outs = new_style_command(parsed_args, childargs, target,
857 sigdict, inbuf, verbose)
859 # debug tool: send any successful command *again* to
860 # verify that it is idempotent.
861 if not ret and 'CEPH_CLI_TEST_DUP_COMMAND' in os.environ:
862 ret, outbuf, outs = new_style_command(parsed_args, childargs, target,
863 sigdict, inbuf, verbose)
866 print >> sys.stderr, prefix + 'Second attempt of previously successful command failed with {0}: {1}'.format(errno.errorcode.get(ret, 'Unknown'), outs)
870 print >> sys.stderr, prefix + 'Error {0}: {1}'.format(errno.errorcode.get(ret, 'Unknown'), outs)
876 # this assumes outs never has useful command output, only status
879 # old cli/mon would send status string to stdout on non-error
883 print >> sys.stderr, prefix + outs
885 if (parsed_args.output_file):
888 # hack: old code printed status line before many json outputs
889 # (osd dump, etc.) that consumers know to ignore. Add blank line
890 # to satisfy consumers that skip the first line, but not annoy
891 # consumers that don't.
892 if parsed_args.output_format and \
893 parsed_args.output_format.startswith('json') and \
895 sys.stdout.write('\n')
897 # if we are prettifying things, normalize newlines. sigh.
899 outbuf = outbuf.rstrip()
901 sys.stdout.write(prefix + outbuf + suffix)
905 if (parsed_args.output_file):
913 if __name__ == '__main__':
915 # shutdown explicitly; Rados() does not
917 cluster_handle.shutdown()