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
71 from ceph_argparse import \
72 concise_sig, descsort, parse_json_funcsigs, \
73 matchnum, validate_command, find_cmd_target, \
74 send_command, json_command
76 # just a couple of globals
81 ############################################################################
84 ret, outbuf, outs = json_command(cluster_handle, prefix='osd ls')
85 if ret == -errno.EINVAL:
87 ret, outbuf, outs = send_command(cluster_handle, cmd=['osd', 'ls'])
89 raise RuntimeError('Can\'t contact mon for osd list')
90 return [i for i in outbuf.split('\n') if i != '']
93 ret, outbuf, outs = json_command(cluster_handle, prefix='mon dump',
94 argdict={'format':'json'})
95 if ret == -errno.EINVAL:
97 ret, outbuf, outs = send_command(cluster_handle,
98 cmd=['mon', 'dump', '--format=json'])
100 raise RuntimeError('Can\'t contact mon for mon list')
101 d = json.loads(outbuf)
102 return [m['name'] for m in d['mons']]
105 ret, outbuf, outs = json_command(cluster_handle, prefix='mds dump',
106 argdict={'format':'json'})
107 if ret == -errno.EINVAL:
109 ret, outbuf, outs = send_command(cluster_handle,
110 cmd=['mds', 'dump', '--format=json'])
112 raise RuntimeError('Can\'t contact mon for mds list')
113 d = json.loads(outbuf)
116 for mdsdict in infodict.values():
117 l.append(mdsdict['name'])
120 # these args must be passed to all child programs
123 'client_name': '--name',
124 'cluster': '--cluster',
125 'cephconf': '--conf',
128 def parse_cmdargs(args=None, target=''):
129 # alias: let the line-wrapping be sane
130 AP = argparse.ArgumentParser
132 # format our own help
133 parser = AP(description='Ceph administration tool', add_help=False)
135 parser.add_argument('--completion', action='store_true',
136 help=argparse.SUPPRESS)
138 parser.add_argument('-h', '--help', help='request mon help',
141 parser.add_argument('-c', '--conf', dest='cephconf',
142 help='ceph configuration file')
143 parser.add_argument('-i', '--in-file', dest='input_file',
145 parser.add_argument('-o', '--out-file', dest='output_file',
148 parser.add_argument('--id', '--user', dest='client_id',
149 help='client id for authentication')
150 parser.add_argument('--name', '-n', dest='client_name',
151 help='client name for authentication')
152 parser.add_argument('--cluster', help='cluster name')
154 parser.add_argument('--admin-daemon', dest='admin_socket',
155 help='submit admin-socket commands (\"help\" for help')
156 parser.add_argument('--admin-socket', dest='admin_socket_nope',
157 help='you probably mean --admin-daemon')
159 parser.add_argument('-s', '--status', action='store_true',
160 help='show cluster status')
162 parser.add_argument('-w', '--watch', action='store_true',
163 help='watch live cluster changes')
164 parser.add_argument('--watch-debug', action='store_true',
165 help='watch debug events')
166 parser.add_argument('--watch-info', action='store_true',
167 help='watch info events')
168 parser.add_argument('--watch-sec', action='store_true',
169 help='watch security events')
170 parser.add_argument('--watch-warn', action='store_true',
171 help='watch warn events')
172 parser.add_argument('--watch-error', action='store_true',
173 help='watch error events')
175 parser.add_argument('--version', '-v', action="store_true", help="display version")
176 parser.add_argument('--verbose', action="store_true", help="make verbose")
177 parser.add_argument('--concise', dest='verbose', action="store_false",
178 help="make less verbose")
180 parser.add_argument('-f', '--format', choices=['json', 'json-pretty',
181 'xml', 'xml-pretty', 'plain'], dest='output_format')
183 parser.add_argument('--connect-timeout', dest='cluster_timeout',
185 help='set a timeout for connecting to the cluster')
187 # returns a Namespace with the parsed args, and a list of all extras
188 parsed_args, extras = parser.parse_known_args(args)
190 return parser, parsed_args, extras
194 print '\n', s, '\n', '=' * len(s)
196 def do_basic_help(parser, args):
198 Print basic parser help
199 If the cluster is available, get and print monitor help
201 hdr('General usage:')
204 def do_extended_help(parser, args):
205 def help_for_sigs(sigs, partial=None):
206 sys.stdout.write(format_help(parse_json_funcsigs(sigs, 'cli'),
209 def help_for_target(target, partial=None):
210 ret, outbuf, outs = json_command(cluster_handle, target=target,
211 prefix='get_command_descriptions',
214 print >> sys.stderr, \
215 "couldn't get command descriptions for {0}: {1}".\
218 help_for_sigs(outbuf, partial)
220 partial = ' '.join(args)
221 if (cluster_handle.state == "connected"):
222 help_for_target(target=('mon', ''), partial=partial)
225 DONTSPLIT = string.letters + '{[<>]}'
227 def wrap(s, width, indent):
229 generator to transform s into a sequence of strings width or shorter,
230 for wrapping text to a specific column width.
231 Attempt to break on anything but DONTSPLIT characters.
232 indent is amount to indent 2nd-through-nth lines.
234 so "long string long string long string" width=11 indent=1 becomes
235 'long string', ' long string', ' long string' so that it can be printed
247 if (len(s) <= width):
248 # no splitting; just possibly indent
255 while (splitpos > 0) and (s[splitpos-1] in DONTSPLIT):
262 # prior result means we're mid-iteration, indent
265 # first time, set leader and width for next
266 leader = ' ' * indent
267 width -= 1 # for subsequent space additions
269 # remove any leading spaces in this chunk of s
270 result += s[:splitpos].lstrip()
277 def format_help(cmddict, partial=None):
279 Formats all the cmdsigs and helptexts from cmddict into a sorted-by-
280 cmdsig 2-column display, with each column wrapped and indented to
281 fit into 40 characters.
285 for cmd in sorted(cmddict.itervalues(), cmp=descsort):
289 concise = concise_sig(cmd['sig'])
290 if partial and not concise.startswith(partial):
292 siglines = [l for l in wrap(concise, 40, 1)]
293 helplines = [l for l in wrap(cmd['help'], 39, 1)]
295 # make lists the same length
296 maxlen = max(len(siglines), len(helplines))
297 siglines.extend([''] * (maxlen - len(siglines)))
298 helplines.extend([''] * (maxlen - len(helplines)))
300 # so we can zip them for output
301 for (s, h) in zip(siglines, helplines):
302 fullusage += '{0:40s} {1}\n'.format(s, h)
306 def admin_socket(asok_path, cmd, format=''):
308 Send a daemon (--admin-daemon) command 'cmd'. asok_path is the
309 path to the admin socket; cmd is a list of strings; format may be
310 set to one of the formatted forms to get output in that form
311 (daemon commands don't support 'plain' output).
314 def do_sockio(path, cmd):
315 """ helper: do all the actual low-level stream I/O """
316 sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
319 sock.sendall(cmd + '\0')
320 len_str = sock.recv(4)
322 raise RuntimeError("no data returned from admin socket")
323 l, = struct.unpack(">I", len_str)
328 bit = sock.recv(l - got)
332 except Exception as e:
333 raise RuntimeError('exception: ' + str(e))
337 cmd_json = do_sockio(asok_path,
338 json.dumps({"prefix":"get_command_descriptions"}))
339 except Exception as e:
340 raise RuntimeError('exception getting command descriptions: ' + str(e))
342 if cmd == 'get_command_descriptions':
345 sigdict = parse_json_funcsigs(cmd_json, 'cli')
346 valid_dict = validate_command(sigdict, cmd)
348 raise RuntimeError('invalid command')
351 valid_dict['format'] = format
354 ret = do_sockio(asok_path, json.dumps(valid_dict))
355 except Exception as e:
356 raise RuntimeError('exception: ' + str(e))
361 def ceph_conf(parsed_args, field, name):
365 args.extend(['--name', name])
367 # add any args in GLOBAL_ARGS
368 for key, val in GLOBAL_ARGS.iteritems():
369 # ignore name in favor of argument name, if any
370 if name and key == 'client_name':
372 if getattr(parsed_args, key):
373 args.extend([val, getattr(parsed_args, key)])
375 args.extend(['--show-config-value', field])
376 p = subprocess.Popen(
378 stdout=subprocess.PIPE,
379 stderr=subprocess.PIPE)
380 outdata, errdata = p.communicate()
382 raise RuntimeError('unable to get conf option %s for %s: %s' % (field, name, errdata))
383 return outdata.rstrip()
387 def new_style_command(parsed_args, cmdargs, target, sigdict, inbuf, verbose):
389 Do new-style command dance.
390 target: daemon to receive command: mon (any) or osd.N
391 sigdict - the parsed output from the new monitor describing commands
392 inbuf - any -i input file data
396 for cmdtag in sorted(sigdict.keys()):
397 cmd = sigdict[cmdtag]
399 print '{0}: {1}'.format(cmdtag, concise_sig(sig))
405 # Validate input args against list of sigs
406 valid_dict = validate_command(sigdict, cmdargs, verbose)
409 if parsed_args.output_format:
410 valid_dict['format'] = parsed_args.output_format
412 return -errno.EINVAL, '', 'invalid command'
414 # do the command-interpreter looping
415 # for raw_input to do readline cmd editing
418 if sys.stdin.isatty():
424 interactive_input = raw_input(prompt)
425 if interactive_input in ['q', 'quit', 'Q']:
427 cmdargs = parse_cmdargs(interactive_input.split())[2]
429 target = find_cmd_target(cmdargs)
430 except Exception as e:
431 print >> sys.stderr, \
432 'error handling command target: {0}'.format(e)
434 valid_dict = validate_command(sigdict, cmdargs, verbose)
436 if parsed_args.output_format:
437 valid_dict['format'] = parsed_args.output_format
439 print >> sys.stderr, "Submitting command ", valid_dict
440 ret, outbuf, outs = json_command(cluster_handle,
445 print >> sys.stderr, \
446 'Error: {0} {1}'.format(ret, errno.errorcode[ret])
450 print >> sys.stderr, 'Status:\n', outs
452 print >> sys.stderr, "Invalid command"
455 print >> sys.stderr, "Submitting command ", valid_dict
456 return json_command(cluster_handle, target=target, argdict=valid_dict,
459 def complete(sigdict, args, target):
461 Command completion. Match as much of [args] as possible,
462 and print every possible match separated by newlines.
465 # XXX this looks a lot like the front of validate_command(). Refactor?
467 complete_verbose = 'COMPVERBOSE' in os.environ
469 # Repulsive hack to handle tell: lop off 'tell' and target
470 # and validate the rest of the command. 'target' is already
471 # determined in our callers, so it's ok to remove it here.
472 if len(args) and args[0] == 'tell':
474 # look for best match, accumulate possibles in bestcmds
475 # (so we can maybe give a more-useful error message)
478 for cmdtag, cmd in sigdict.iteritems():
480 matched = matchnum(args, sig, partial=True)
481 if (matched > best_match_cnt):
483 print >> sys.stderr, \
484 "better match: {0} > {1}: {2}:{3} ".format(matched,
485 best_match_cnt, cmdtag, concise_sig(sig))
486 best_match_cnt = matched
487 bestcmds = [{cmdtag:cmd}]
488 elif matched == best_match_cnt:
490 print >> sys.stderr, \
491 "equal match: {0} > {1}: {2}:{3} ".format(matched,
492 best_match_cnt, cmdtag, concise_sig(sig))
493 bestcmds.append({cmdtag:cmd})
495 # look through all matching sigs
497 for cmddict in bestcmds:
498 for cmd in cmddict.itervalues():
501 # we match everything fully, so we want the next desc, or
502 # we match more partially, so we want the partial match
503 fullindex = matchnum(args, sig, partial=False) - 1
504 partindex = matchnum(args, sig, partial=True) - 1
506 print >> sys.stderr, '{}: f {} p {} len {}'.format(sig, fullindex, partindex, len(sig))
507 if fullindex == partindex and fullindex + 1 < len(sig):
508 d = sig[fullindex + 1]
513 print >> sys.stderr, '\n'.join(comps)
514 print '\n'.join(comps)
521 def ping_monitor(cluster_handle, name):
522 if 'mon.' not in name:
523 print >> sys.stderr, '"ping" expects a monitor to ping; try "ping mon.<id>"'
526 mon_id = name[len('mon.'):]
527 s = cluster_handle.ping_monitor(mon_id)
536 ceph_args = os.environ.get('CEPH_ARGS')
538 if "injectargs" in sys.argv:
539 i = sys.argv.index("injectargs")
540 sys.argv = sys.argv[:i] + ceph_args.split() + sys.argv[i:]
542 sys.argv.extend(ceph_args.split())
543 parser, parsed_args, childargs = parse_cmdargs()
545 if parsed_args.version:
546 print 'ceph version {0} ({1})'.format(CEPH_GIT_NICE_VER, CEPH_GIT_VER)
550 verbose = parsed_args.verbose
553 print >> sys.stderr, "parsed_args: {0}, childargs: {1}".format(parsed_args, childargs)
555 if parsed_args.admin_socket_nope:
556 print >> sys.stderr, '--admin-socket is used by daemons; '\
557 'you probably mean --admin-daemon/daemon'
560 # pass on --id, --name, --conf
561 name = 'client.admin'
562 if parsed_args.client_id:
563 name = 'client.' + parsed_args.client_id
564 if parsed_args.client_name:
565 name = parsed_args.client_name
567 # default '' means default conf search
569 if parsed_args.cephconf:
570 conffile = parsed_args.cephconf
571 # For now, --admin-daemon is handled as usual. Try it
572 # first in case we can't connect() to the cluster
574 format = parsed_args.output_format
577 if parsed_args.admin_socket:
578 sockpath = parsed_args.admin_socket
579 elif len(childargs) > 0 and childargs[0] == "daemon":
580 # Treat "daemon <path>" or "daemon <name>" like --admin_daemon <path>
581 if len(childargs) > 2:
582 if childargs[1].find('/') >= 0:
583 sockpath = childargs[1]
585 # try resolve daemon name
587 sockpath = ceph_conf(parsed_args, 'admin_socket',
589 except Exception as e:
590 print >> sys.stderr, \
591 'Can\'t get admin socket path: ' + str(e)
594 childargs = childargs[2:]
596 print >> sys.stderr, 'daemon requires at least 3 arguments'
601 print admin_socket(sockpath, childargs, format)
602 except Exception as e:
603 print >> sys.stderr, 'admin_socket: {0}'.format(e)
608 if parsed_args.cluster_timeout:
609 timeout = parsed_args.cluster_timeout
613 do_basic_help(parser, childargs)
615 # handle any 'generic' ceph arguments that we didn't parse here
616 global cluster_handle
618 # rados.Rados() will call rados_create2, and then read the conf file,
619 # and then set the keys from the dict. So we must do these
620 # "pre-file defaults" first (see common_preinit in librados)
622 'log_to_stderr':'true',
623 'err_to_stderr':'true',
624 'log_flush_on_exit':'true',
627 if 'injectargs' in childargs:
628 position = childargs.index('injectargs')
629 injectargs = childargs[position:]
630 childargs = childargs[:position]
632 print >> sys.stderr, 'Separate childargs {0} from injectargs {1}'.\
633 format(childargs, injectargs)
638 if parsed_args.cluster:
639 clustername = parsed_args.cluster
642 cluster_handle = rados.Rados(name=name, clustername=clustername,
643 conf_defaults=conf_defaults,
645 retargs = cluster_handle.conf_parse_argv(childargs)
646 except rados.Error as e:
647 print >> sys.stderr, 'Error initializing cluster client: {0}'.\
655 # -- means "stop parsing args", but we don't want to see it either
656 if '--' in childargs:
657 childargs.remove('--')
658 if injectargs and '--' in injectargs:
659 injectargs.remove('--')
661 # special deprecation warning for 'ceph <type> tell'
662 # someday 'mds' will be here too
663 if len(childargs) >= 2 and \
664 childargs[0] in ['mon', 'osd'] and \
665 childargs[1] == 'tell':
666 print >> sys.stderr, '"{0} tell" is deprecated; try "tell {0}.<id>" instead (id can be "*") '.format(childargs[0])
670 # short default timeout for -h
674 hdr('Monitor commands:')
675 print '[Contacting monitor, timeout after %d seconds]' % timeout
677 if childargs and childargs[0] == 'ping':
678 if len(childargs) < 2:
679 print >> sys.stderr, '"ping" requires a monitor name as argument: "ping mon.<id>"'
683 if childargs and childargs[0] == 'ping':
684 return ping_monitor(cluster_handle, childargs[1])
685 cluster_handle.connect(timeout=timeout)
686 except KeyboardInterrupt:
687 print >> sys.stderr, 'Cluster connection aborted'
689 except Exception as e:
690 print >> sys.stderr, 'Error connecting to cluster: {0}'.\
691 format(e.__class__.__name__)
695 return do_extended_help(parser, childargs)
697 # implement -w/--watch_*
698 # This is ugly, but Namespace() isn't quite rich enough.
700 for k, v in parsed_args._get_kwargs():
701 if k.startswith('watch') and v:
705 level = k.replace('watch_', '')
708 # an awfully simple callback
709 def watch_cb(arg, line, who, stamp_sec, stamp_nsec, seq, level, msg):
713 # first do a ceph status
714 ret, outbuf, outs = json_command(cluster_handle, prefix='status')
715 if ret == -errno.EINVAL:
717 ret, outbuf, outs = send_command(cluster_handle, cmd=['status'])
718 # old mon returns status to outs...ick
722 print >> sys.stderr, "status query failed: ", outs
726 # this instance keeps the watch connection alive, but is
728 logwatch = rados.MonitorLog(cluster_handle, level, watch_cb, 0)
730 # loop forever letting watch_cb print lines
733 except KeyboardInterrupt:
734 # or until ^C, at least
737 # read input file, if any
739 if parsed_args.input_file:
741 with open(parsed_args.input_file, 'r') as f:
743 except Exception as e:
744 print >> sys.stderr, 'Can\'t open input file {0}: {1}'.format(parsed_args.input_file, e)
747 # prepare output file, if any
748 if parsed_args.output_file:
750 outf = open(parsed_args.output_file, 'w')
751 except Exception as e:
752 print >> sys.stderr, \
753 'Can\'t open output file {0}: {1}'.\
754 format(parsed_args.output_file, e)
757 # -s behaves like a command (ceph status).
758 if parsed_args.status:
759 childargs.insert(0, 'status')
762 target = find_cmd_target(childargs)
763 except Exception as e:
764 print >> sys.stderr, \
765 'error handling command target: {0}'.format(e)
768 # Repulsive hack to handle tell: lop off 'tell' and target
769 # and validate the rest of the command. 'target' is already
770 # determined in our callers, so it's ok to remove it here.
772 if len(childargs) and childargs[0] == 'tell':
773 childargs = childargs[2:]
778 childargs = injectargs
779 if not len(childargs):
780 print >> sys.stderr, \
781 'Cannot use \'tell\' with interactive mode'
784 # fetch JSON sigs from command
785 # each line contains one command signature (a placeholder name
786 # of the form 'cmdNNN' followed by an array of argument descriptors)
787 # as part of the validated argument JSON object
792 if target[0] == 'osd':
793 targets = [(target[0], o) for o in osdids()]
794 elif target[0] == 'mon':
795 targets = [(target[0], m) for m in monids()]
798 for target in targets:
799 # prettify? prefix output with target, if there was a wildcard used
802 if not parsed_args.output_file and len(targets) > 1:
803 prefix = '{0}.{1}: '.format(*target)
806 ret, outbuf, outs = json_command(cluster_handle, target=target,
807 prefix='get_command_descriptions')
809 if ret == -errno.EINVAL:
810 # send command to old monitor or OSD
812 print prefix + '{0} to old {1}'.format(' '.join(childargs), target[0])
814 if parsed_args.output_format:
815 childargs.extend(['--format', parsed_args.output_format])
816 ret, outbuf, outs = send_command(cluster_handle, target, childargs,
819 if ret == -errno.EINVAL:
820 # did we race with a mon upgrade? try again!
821 ret, outbuf, outs = json_command(cluster_handle, target=target,
822 prefix='get_command_descriptions')
824 compat = False # yep, carry on
828 outs = 'problem getting command descriptions from {0}.{1}'.format(*target)
830 sigdict = parse_json_funcsigs(outbuf, 'cli')
832 if parsed_args.completion:
833 return complete(sigdict, childargs, target)
835 ret, outbuf, outs = new_style_command(parsed_args, childargs, target,
836 sigdict, inbuf, verbose)
838 # debug tool: send any successful command *again* to
839 # verify that it is idempotent.
840 if not ret and 'CEPH_CLI_TEST_DUP_COMMAND' in os.environ:
841 ret, outbuf, outs = new_style_command(parsed_args, childargs, target,
842 sigdict, inbuf, verbose)
845 print >> sys.stderr, prefix + 'Second attempt of previously successful command failed with {0}: {1}'.format(errno.errorcode[ret], outs)
849 print >> sys.stderr, prefix + 'Error {0}: {1}'.format(errno.errorcode[ret], outs)
855 # this assumes outs never has useful command output, only status
858 # old cli/mon would send status string to stdout on non-error
862 print >> sys.stderr, prefix + outs
864 if (parsed_args.output_file):
867 # hack: old code printed status line before many json outputs
868 # (osd dump, etc.) that consumers know to ignore. Add blank line
869 # to satisfy consumers that skip the first line, but not annoy
870 # consumers that don't.
871 if parsed_args.output_format and \
872 parsed_args.output_format.startswith('json') and \
874 sys.stdout.write('\n')
876 # if we are prettifying things, normalize newlines. sigh.
878 outbuf = outbuf.rstrip()
880 sys.stdout.write(prefix + outbuf + suffix)
884 if (parsed_args.output_file):
892 if __name__ == '__main__':
894 # shutdown explicitly; Rados() does not
896 cluster_handle.shutdown()