]>
Commit | Line | Data |
---|---|---|
7c673cae FG |
1 | #!@PYTHON_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 | import codecs | |
24 | import os | |
25 | import sys | |
26 | import platform | |
27 | ||
28 | try: | |
29 | input = raw_input | |
30 | except NameError: | |
31 | pass | |
32 | ||
33 | CEPH_GIT_VER="@CEPH_GIT_VER@" | |
34 | CEPH_GIT_NICE_VER="@CEPH_GIT_NICE_VER@" | |
35 | ||
36 | # Flags from src/mon/Monitor.h | |
37 | FLAG_NOFORWARD = (1 << 0) | |
38 | FLAG_OBSOLETE = (1 << 1) | |
39 | FLAG_DEPRECATED = (1 << 2) | |
40 | ||
41 | # priorities from src/common/perf_counters.h | |
42 | PRIO_CRITICAL = 10 | |
43 | PRIO_INTERESTING = 8 | |
44 | PRIO_USEFUL = 5 | |
45 | PRIO_UNINTERESTING = 2 | |
46 | PRIO_DEBUGONLY = 0 | |
47 | ||
48 | PRIO_DEFAULT = PRIO_USEFUL | |
49 | ||
50 | # Make life easier on developers: | |
51 | # If in src/, and .libs and pybind exist here, assume we're running | |
52 | # from a Ceph source dir and tweak PYTHONPATH and LD_LIBRARY_PATH | |
53 | # to use local files | |
54 | ||
55 | MYPATH = os.path.abspath(__file__) | |
56 | MYDIR = os.path.dirname(MYPATH) | |
57 | DEVMODEMSG = '*** DEVELOPER MODE: setting PATH, PYTHONPATH and LD_LIBRARY_PATH ***' | |
58 | ||
59 | def respawn_in_path(lib_path, pybind_path, pythonlib_path): | |
60 | execv_cmd = ['python'] | |
61 | if 'CEPH_DBG' in os.environ: | |
62 | execv_cmd += ['-mpdb'] | |
63 | ||
64 | if platform.system() == "Darwin": | |
65 | lib_path_var = "DYLD_LIBRARY_PATH" | |
66 | else: | |
67 | lib_path_var = "LD_LIBRARY_PATH" | |
68 | ||
69 | py_binary = os.environ.get("PYTHON", "python") | |
70 | ||
71 | if lib_path_var in os.environ: | |
72 | if lib_path not in os.environ[lib_path_var]: | |
73 | os.environ[lib_path_var] += ':' + lib_path | |
74 | print(DEVMODEMSG, file=sys.stderr) | |
75 | os.execvp(py_binary, execv_cmd + sys.argv) | |
76 | else: | |
77 | os.environ[lib_path_var] = lib_path | |
78 | print(DEVMODEMSG, file=sys.stderr) | |
79 | os.execvp(py_binary, execv_cmd + sys.argv) | |
80 | sys.path.insert(0, os.path.join(MYDIR, pybind_path)) | |
81 | sys.path.insert(0, os.path.join(MYDIR, pythonlib_path)) | |
82 | ||
83 | def get_pythonlib_dir(): | |
84 | """Returns the name of a distutils build directory""" | |
85 | return "lib.{version[0]}".format(version=sys.version_info) | |
86 | ||
87 | if os.path.exists(os.path.join(os.getcwd(), "CMakeCache.txt")) \ | |
88 | and os.path.exists(os.path.join(os.getcwd(), "bin/init-ceph")): | |
89 | src_path = None | |
90 | for l in open("./CMakeCache.txt"): | |
91 | if l.startswith("ceph_SOURCE_DIR:STATIC="): | |
92 | src_path = l.split("=")[1].strip() | |
93 | ||
94 | ||
95 | if src_path is None: | |
96 | # Huh, maybe we're not really in a cmake environment? | |
97 | pass | |
98 | else: | |
99 | # Developer mode, but in a cmake build dir instead of the src dir | |
100 | lib_path = os.path.join(os.getcwd(), "lib") | |
101 | bin_path = os.path.join(os.getcwd(), "bin") | |
102 | pybind_path = os.path.join(src_path, "src", "pybind") | |
103 | pythonlib_path = os.path.join(lib_path, | |
104 | "cython_modules", | |
105 | get_pythonlib_dir()) | |
106 | ||
107 | respawn_in_path(lib_path, pybind_path, pythonlib_path) | |
108 | ||
109 | if 'PATH' in os.environ and bin_path not in os.environ['PATH']: | |
110 | os.environ['PATH'] += ':' + bin_path | |
111 | ||
112 | import argparse | |
113 | import errno | |
114 | import json | |
115 | import rados | |
116 | import shlex | |
117 | import signal | |
118 | import string | |
119 | import subprocess | |
120 | ||
121 | from ceph_argparse import \ | |
122 | concise_sig, descsort_key, parse_json_funcsigs, \ | |
123 | matchnum, validate_command, find_cmd_target, \ | |
124 | send_command, json_command, run_in_thread | |
125 | ||
126 | from ceph_daemon import DaemonWatcher, admin_socket | |
127 | ||
128 | # just a couple of globals | |
129 | ||
130 | verbose = False | |
131 | cluster_handle = None | |
132 | ||
133 | # Always use Unicode (UTF-8) for stdout | |
134 | if sys.version_info[0] >= 3: | |
135 | raw_stdout = sys.stdout.buffer | |
136 | raw_stderr = sys.stderr.buffer | |
137 | else: | |
138 | raw_stdout = sys.__stdout__ | |
139 | raw_stderr = sys.__stderr__ | |
140 | sys.stdout = codecs.getwriter('utf-8')(raw_stdout) | |
141 | sys.stderr = codecs.getwriter('utf-8')(raw_stderr) | |
142 | ||
143 | def raw_write(buf): | |
144 | sys.stdout.flush() | |
145 | raw_stdout.write(buf) | |
146 | ||
147 | ############################################################################ | |
148 | ||
149 | def osdids(): | |
150 | ret, outbuf, outs = json_command(cluster_handle, prefix='osd ls') | |
151 | if ret == -errno.EINVAL: | |
152 | # try old mon | |
153 | ret, outbuf, outs = send_command(cluster_handle, cmd=['osd', 'ls']) | |
154 | if ret: | |
155 | raise RuntimeError('Can\'t contact mon for osd list') | |
156 | return [line.decode('utf-8') for line in outbuf.split(b'\n') if line] | |
157 | ||
158 | def monids(): | |
159 | ret, outbuf, outs = json_command(cluster_handle, prefix='mon dump', | |
160 | argdict={'format':'json'}) | |
161 | if ret == -errno.EINVAL: | |
162 | # try old mon | |
163 | ret, outbuf, outs = send_command(cluster_handle, | |
164 | cmd=['mon', 'dump', '--format=json']) | |
165 | if ret: | |
166 | raise RuntimeError('Can\'t contact mon for mon list') | |
167 | d = json.loads(outbuf.decode('utf-8')) | |
168 | return [m['name'] for m in d['mons']] | |
169 | ||
170 | def mdsids(): | |
171 | ret, outbuf, outs = json_command(cluster_handle, prefix='mds dump', | |
172 | argdict={'format':'json'}) | |
173 | if ret == -errno.EINVAL: | |
174 | # try old mon | |
175 | ret, outbuf, outs = send_command(cluster_handle, | |
176 | cmd=['mds', 'dump', '--format=json']) | |
177 | if ret: | |
178 | raise RuntimeError('Can\'t contact mon for mds list') | |
179 | d = json.loads(outbuf.decode('utf-8')) | |
180 | l = [] | |
181 | infodict = d['info'] | |
182 | for mdsdict in infodict.values(): | |
183 | l.append(mdsdict['name']) | |
184 | return l | |
185 | ||
186 | # these args must be passed to all child programs | |
187 | GLOBAL_ARGS = { | |
188 | 'client_id': '--id', | |
189 | 'client_name': '--name', | |
190 | 'cluster': '--cluster', | |
191 | 'cephconf': '--conf', | |
192 | } | |
193 | ||
194 | def parse_cmdargs(args=None, target=''): | |
195 | # alias: let the line-wrapping be sane | |
196 | AP = argparse.ArgumentParser | |
197 | ||
198 | # format our own help | |
199 | parser = AP(description='Ceph administration tool', add_help=False) | |
200 | ||
201 | parser.add_argument('--completion', action='store_true', | |
202 | help=argparse.SUPPRESS) | |
203 | ||
204 | parser.add_argument('-h', '--help', help='request mon help', | |
205 | action='store_true') | |
206 | ||
207 | parser.add_argument('-c', '--conf', dest='cephconf', | |
208 | help='ceph configuration file') | |
209 | parser.add_argument('-i', '--in-file', dest='input_file', | |
210 | help='input file') | |
211 | parser.add_argument('-o', '--out-file', dest='output_file', | |
212 | help='output file') | |
213 | ||
214 | parser.add_argument('--id', '--user', dest='client_id', | |
215 | help='client id for authentication') | |
216 | parser.add_argument('--name', '-n', dest='client_name', | |
217 | help='client name for authentication') | |
218 | parser.add_argument('--cluster', help='cluster name') | |
219 | ||
220 | parser.add_argument('--admin-daemon', dest='admin_socket', | |
221 | help='submit admin-socket commands (\"help\" for help') | |
222 | parser.add_argument('--admin-socket', dest='admin_socket_nope', | |
223 | help='you probably mean --admin-daemon') | |
224 | ||
225 | parser.add_argument('-s', '--status', action='store_true', | |
226 | help='show cluster status') | |
227 | ||
228 | parser.add_argument('-w', '--watch', action='store_true', | |
229 | help='watch live cluster changes') | |
230 | parser.add_argument('--watch-debug', action='store_true', | |
231 | help='watch debug events') | |
232 | parser.add_argument('--watch-info', action='store_true', | |
233 | help='watch info events') | |
234 | parser.add_argument('--watch-sec', action='store_true', | |
235 | help='watch security events') | |
236 | parser.add_argument('--watch-warn', action='store_true', | |
237 | help='watch warn events') | |
238 | parser.add_argument('--watch-error', action='store_true', | |
239 | help='watch error events') | |
240 | ||
241 | parser.add_argument('--version', '-v', action="store_true", help="display version") | |
242 | parser.add_argument('--verbose', action="store_true", help="make verbose") | |
243 | parser.add_argument('--concise', dest='verbose', action="store_false", | |
244 | help="make less verbose") | |
245 | ||
246 | parser.add_argument('-f', '--format', choices=['json', 'json-pretty', | |
247 | 'xml', 'xml-pretty', 'plain'], dest='output_format') | |
248 | ||
249 | parser.add_argument('--connect-timeout', dest='cluster_timeout', | |
250 | type=int, | |
251 | help='set a timeout for connecting to the cluster') | |
252 | ||
253 | # returns a Namespace with the parsed args, and a list of all extras | |
254 | parsed_args, extras = parser.parse_known_args(args) | |
255 | ||
256 | return parser, parsed_args, extras | |
257 | ||
258 | ||
259 | def hdr(s): | |
260 | print('\n', s, '\n', '=' * len(s)) | |
261 | ||
262 | def do_basic_help(parser, args): | |
263 | """ | |
264 | Print basic parser help | |
265 | If the cluster is available, get and print monitor help | |
266 | """ | |
267 | hdr('General usage:') | |
268 | parser.print_help() | |
269 | print_locally_handled_command_help() | |
270 | ||
271 | def print_locally_handled_command_help(): | |
272 | hdr("Local commands:") | |
273 | print(""" | |
274 | ping <mon.id> Send simple presence/life test to a mon | |
275 | <mon.id> may be 'mon.*' for all mons | |
276 | daemon {type.id|path} <cmd> | |
277 | Same as --admin-daemon, but auto-find admin socket | |
278 | daemonperf {type.id | path} [stat-pats] [priority] [<interval>] [<count>] | |
279 | daemonperf {type.id | path} list|ls [stat-pats] [priority] | |
280 | Get selected perf stats from daemon/admin socket | |
281 | Optional shell-glob comma-delim match string stat-pats | |
282 | Optional selection priority (can abbreviate name): | |
283 | critical, interesting, useful, noninteresting, debug | |
284 | List shows a table of all available stats | |
285 | Run <count> times (default forever), | |
286 | once per <interval> seconds (default 1) | |
287 | """, file=sys.stdout) | |
288 | ||
289 | ||
290 | def do_extended_help(parser, args): | |
291 | def help_for_sigs(sigs, partial=None): | |
292 | sys.stdout.write(format_help(parse_json_funcsigs(sigs, 'cli'), | |
293 | partial=partial)) | |
294 | ||
295 | def help_for_target(target, partial=None): | |
296 | ret, outbuf, outs = json_command(cluster_handle, target=target, | |
297 | prefix='get_command_descriptions', | |
298 | timeout=10) | |
299 | if ret: | |
300 | print("couldn't get command descriptions for {0}: {1}".\ | |
301 | format(target, outs), file=sys.stderr) | |
302 | else: | |
303 | help_for_sigs(outbuf.decode('utf-8'), partial) | |
304 | ||
305 | partial = ' '.join(args) | |
306 | if (cluster_handle.state == "connected"): | |
307 | help_for_target(target=('mon', ''), partial=partial) | |
308 | return 0 | |
309 | ||
310 | DONTSPLIT = string.ascii_letters + '{[<>]}' | |
311 | ||
312 | def wrap(s, width, indent): | |
313 | """ | |
314 | generator to transform s into a sequence of strings width or shorter, | |
315 | for wrapping text to a specific column width. | |
316 | Attempt to break on anything but DONTSPLIT characters. | |
317 | indent is amount to indent 2nd-through-nth lines. | |
318 | ||
319 | so "long string long string long string" width=11 indent=1 becomes | |
320 | 'long string', ' long string', ' long string' so that it can be printed | |
321 | as | |
322 | long string | |
323 | long string | |
324 | long string | |
325 | ||
326 | Consumes s. | |
327 | """ | |
328 | result = '' | |
329 | leader = '' | |
330 | while len(s): | |
331 | ||
332 | if (len(s) <= width): | |
333 | # no splitting; just possibly indent | |
334 | result = leader + s | |
335 | s = '' | |
336 | yield result | |
337 | ||
338 | else: | |
339 | splitpos = width | |
340 | while (splitpos > 0) and (s[splitpos-1] in DONTSPLIT): | |
341 | splitpos -= 1 | |
342 | ||
343 | if splitpos == 0: | |
344 | splitpos = width | |
345 | ||
346 | if result: | |
347 | # prior result means we're mid-iteration, indent | |
348 | result = leader | |
349 | else: | |
350 | # first time, set leader and width for next | |
351 | leader = ' ' * indent | |
352 | width -= 1 # for subsequent space additions | |
353 | ||
354 | # remove any leading spaces in this chunk of s | |
355 | result += s[:splitpos].lstrip() | |
356 | s = s[splitpos:] | |
357 | ||
358 | yield result | |
359 | ||
360 | raise StopIteration | |
361 | ||
362 | def format_help(cmddict, partial=None): | |
363 | """ | |
364 | Formats all the cmdsigs and helptexts from cmddict into a sorted-by- | |
365 | cmdsig 2-column display, with each column wrapped and indented to | |
366 | fit into 40 characters. | |
367 | """ | |
368 | ||
369 | fullusage = '' | |
370 | for cmd in sorted(cmddict.values(), key=descsort_key): | |
371 | ||
372 | if not cmd['help']: | |
373 | continue | |
374 | flags = cmd.get('flags') | |
375 | if (flags is not None and | |
376 | (flags & (FLAG_OBSOLETE | FLAG_DEPRECATED)) != 0): | |
377 | continue | |
378 | concise = concise_sig(cmd['sig']) | |
379 | if partial and not concise.startswith(partial): | |
380 | continue | |
381 | siglines = [l for l in wrap(concise, 40, 1)] | |
382 | helplines = [l for l in wrap(cmd['help'], 39, 1)] | |
383 | ||
384 | # make lists the same length | |
385 | maxlen = max(len(siglines), len(helplines)) | |
386 | siglines.extend([''] * (maxlen - len(siglines))) | |
387 | helplines.extend([''] * (maxlen - len(helplines))) | |
388 | ||
389 | # so we can zip them for output | |
390 | for (s, h) in zip(siglines, helplines): | |
391 | fullusage += '{0:40s} {1}\n'.format(s, h) | |
392 | ||
393 | return fullusage | |
394 | ||
395 | ||
396 | def ceph_conf(parsed_args, field, name): | |
397 | args=['ceph-conf'] | |
398 | ||
399 | if name: | |
400 | args.extend(['--name', name]) | |
401 | ||
402 | # add any args in GLOBAL_ARGS | |
403 | for key, val in GLOBAL_ARGS.items(): | |
404 | # ignore name in favor of argument name, if any | |
405 | if name and key == 'client_name': | |
406 | continue | |
407 | if getattr(parsed_args, key): | |
408 | args.extend([val, getattr(parsed_args, key)]) | |
409 | ||
410 | args.extend(['--show-config-value', field]) | |
411 | p = subprocess.Popen( | |
412 | args, | |
413 | stdout=subprocess.PIPE, | |
414 | stderr=subprocess.PIPE) | |
415 | outdata, errdata = p.communicate() | |
416 | if (len(errdata)): | |
417 | raise RuntimeError('unable to get conf option %s for %s: %s' % (field, name, errdata)) | |
418 | return outdata.rstrip() | |
419 | ||
420 | PROMPT = 'ceph> ' | |
421 | ||
422 | if sys.stdin.isatty(): | |
423 | def read_input(): | |
424 | while True: | |
425 | line = input(PROMPT).rstrip() | |
426 | if line in ['q', 'quit', 'Q', 'exit']: | |
427 | return None | |
428 | if line: | |
429 | return line | |
430 | else: | |
431 | def read_input(): | |
432 | while True: | |
433 | line = sys.stdin.readline() | |
434 | if not line: | |
435 | return None | |
436 | line = line.rstrip() | |
437 | if line: | |
438 | return line | |
439 | ||
440 | ||
441 | def new_style_command(parsed_args, cmdargs, target, sigdict, inbuf, verbose): | |
442 | """ | |
443 | Do new-style command dance. | |
444 | target: daemon to receive command: mon (any) or osd.N | |
445 | sigdict - the parsed output from the new monitor describing commands | |
446 | inbuf - any -i input file data | |
447 | verbose - bool | |
448 | """ | |
449 | if verbose: | |
450 | for cmdtag in sorted(sigdict.keys()): | |
451 | cmd = sigdict[cmdtag] | |
452 | sig = cmd['sig'] | |
453 | print('{0}: {1}'.format(cmdtag, concise_sig(sig))) | |
454 | ||
455 | if True: | |
456 | if cmdargs: | |
457 | # Validate input args against list of sigs | |
458 | valid_dict = validate_command(sigdict, cmdargs, verbose) | |
459 | if valid_dict: | |
460 | if parsed_args.output_format: | |
461 | valid_dict['format'] = parsed_args.output_format | |
462 | else: | |
463 | return -errno.EINVAL, '', 'invalid command' | |
464 | else: | |
465 | if sys.stdin.isatty(): | |
466 | # do the command-interpreter looping | |
467 | # for input to do readline cmd editing | |
468 | import readline # noqa | |
469 | ||
470 | while True: | |
471 | interactive_input = read_input() | |
472 | if interactive_input is None: | |
473 | return 0, '', '' | |
474 | cmdargs = parse_cmdargs(shlex.split(interactive_input))[2] | |
475 | try: | |
476 | target = find_cmd_target(cmdargs) | |
477 | except Exception as e: | |
478 | print('error handling command target: {0}'.format(e), | |
479 | file=sys.stderr) | |
480 | continue | |
481 | if len(cmdargs) and cmdargs[0] == 'tell': | |
482 | print('Can not use \'tell\' in interactive mode.', | |
483 | file=sys.stderr) | |
484 | continue | |
485 | valid_dict = validate_command(sigdict, cmdargs, verbose) | |
486 | if valid_dict: | |
487 | if parsed_args.output_format: | |
488 | valid_dict['format'] = parsed_args.output_format | |
489 | if verbose: | |
490 | print("Submitting command: ", valid_dict, file=sys.stderr) | |
491 | ret, outbuf, outs = json_command(cluster_handle, | |
492 | target=target, | |
493 | argdict=valid_dict) | |
494 | if ret: | |
495 | ret = abs(ret) | |
496 | print('Error: {0} {1}'.format(ret, errno.errorcode.get(ret, 'Unknown')), | |
497 | file=sys.stderr) | |
498 | if outbuf: | |
499 | print(outbuf) | |
500 | if outs: | |
501 | print('Status:\n', outs, file=sys.stderr) | |
502 | else: | |
503 | print("Invalid command", file=sys.stderr) | |
504 | ||
505 | if verbose: | |
506 | print("Submitting command: ", valid_dict, file=sys.stderr) | |
507 | return json_command(cluster_handle, target=target, argdict=valid_dict, | |
508 | inbuf=inbuf) | |
509 | ||
510 | ||
511 | def complete(sigdict, args, target): | |
512 | """ | |
513 | Command completion. Match as much of [args] as possible, | |
514 | and print every possible match separated by newlines. | |
515 | Return exitcode. | |
516 | """ | |
517 | # XXX this looks a lot like the front of validate_command(). Refactor? | |
518 | ||
519 | complete_verbose = 'COMPVERBOSE' in os.environ | |
520 | ||
521 | # Repulsive hack to handle tell: lop off 'tell' and target | |
522 | # and validate the rest of the command. 'target' is already | |
523 | # determined in our callers, so it's ok to remove it here. | |
524 | if len(args) and args[0] == 'tell': | |
525 | args = args[2:] | |
526 | # look for best match, accumulate possibles in bestcmds | |
527 | # (so we can maybe give a more-useful error message) | |
528 | ||
529 | match_count = 0 | |
530 | comps = [] | |
531 | for cmdtag, cmd in sigdict.items(): | |
532 | sig = cmd['sig'] | |
533 | j = 0 | |
534 | # iterate over all arguments, except last one | |
535 | for arg in args[0:-1]: | |
536 | if j > len(sig)-1: | |
537 | # an out of argument definitions | |
538 | break | |
539 | found_match = arg in sig[j].complete(arg) | |
540 | if not found_match and sig[j].req: | |
541 | # no elements that match | |
542 | break | |
543 | if not sig[j].N: | |
544 | j += 1 | |
545 | else: | |
546 | # successfully matched all - except last one - arguments | |
547 | if j < len(sig) and len(args) > 0: | |
548 | comps += sig[j].complete(args[-1]) | |
549 | ||
550 | match_count += 1 | |
551 | match_cmd = cmd | |
552 | ||
553 | if match_count == 1 and len(comps) == 0: | |
554 | # only one command matched and no hints yet => add help | |
555 | comps = comps + [' ', '#'+match_cmd['help']] | |
556 | print('\n'.join(sorted(set(comps)))) | |
557 | return 0 | |
558 | ||
559 | ||
560 | ### | |
561 | # ping a monitor | |
562 | ### | |
563 | def ping_monitor(cluster_handle, name, timeout): | |
564 | if 'mon.' not in name: | |
565 | print('"ping" expects a monitor to ping; try "ping mon.<id>"', file=sys.stderr) | |
566 | return 1 | |
567 | ||
568 | mon_id = name[len('mon.'):] | |
569 | if (mon_id == '*') : | |
570 | run_in_thread(cluster_handle.connect, timeout=timeout) | |
571 | for m in monids() : | |
572 | s = run_in_thread(cluster_handle.ping_monitor, m) | |
573 | if s is None: | |
574 | print("mon.{0}".format(m) + '\n' + "Error connecting to monitor.") | |
575 | else: | |
576 | print("mon.{0}".format(m) + '\n' + s) | |
577 | else : | |
578 | s = run_in_thread(cluster_handle.ping_monitor, mon_id) | |
579 | print(s) | |
580 | return 0 | |
581 | ||
582 | ||
583 | def maybe_daemon_command(parsed_args, childargs): | |
584 | """ | |
585 | Check if --admin-socket, daemon, or daemonperf command | |
586 | if it is, returns (boolean handled, return code if handled == True) | |
587 | """ | |
588 | ||
589 | daemon_perf = False | |
590 | sockpath = None | |
591 | if parsed_args.admin_socket: | |
592 | sockpath = parsed_args.admin_socket | |
593 | elif len(childargs) > 0 and childargs[0] in ["daemon", "daemonperf"]: | |
594 | daemon_perf = (childargs[0] == "daemonperf") | |
595 | # Treat "daemon <path>" or "daemon <name>" like --admin_daemon <path> | |
596 | # Handle "daemonperf <path>" the same but requires no trailing args | |
597 | require_args = 2 if daemon_perf else 3 | |
598 | if len(childargs) >= require_args: | |
599 | if childargs[1].find('/') >= 0: | |
600 | sockpath = childargs[1] | |
601 | else: | |
602 | # try resolve daemon name | |
603 | try: | |
604 | sockpath = ceph_conf(parsed_args, 'admin_socket', | |
605 | childargs[1]) | |
606 | except Exception as e: | |
607 | print('Can\'t get admin socket path: ' + str(e), file=sys.stderr) | |
608 | return True, errno.EINVAL | |
609 | # for both: | |
610 | childargs = childargs[2:] | |
611 | else: | |
612 | print('{0} requires at least {1} arguments'.format(childargs[0], require_args), | |
613 | file=sys.stderr) | |
614 | return True, errno.EINVAL | |
615 | ||
616 | if sockpath and daemon_perf: | |
617 | return True, daemonperf(childargs, sockpath) | |
618 | elif sockpath: | |
619 | try: | |
620 | raw_write(admin_socket(sockpath, childargs, parsed_args.output_format)) | |
621 | except Exception as e: | |
622 | print('admin_socket: {0}'.format(e), file=sys.stderr) | |
623 | return True, errno.EINVAL | |
624 | return True, 0 | |
625 | ||
626 | return False, 0 | |
627 | ||
628 | ||
629 | def isnum(s): | |
630 | try: | |
631 | float(s) | |
632 | return True | |
633 | except ValueError: | |
634 | return False | |
635 | ||
636 | def daemonperf(childargs, sockpath): | |
637 | """ | |
638 | Handle daemonperf command; returns errno or 0 | |
639 | ||
640 | daemonperf <daemon> [priority string] [statpats] [interval] [count] | |
641 | daemonperf <daemon> list|ls [statpats] | |
642 | """ | |
643 | ||
644 | interval = 1 | |
645 | count = None | |
646 | statpats = None | |
647 | priority = None | |
648 | do_list = False | |
649 | ||
650 | def prio_from_name(arg): | |
651 | ||
652 | PRIOMAP = { | |
653 | 'critical': PRIO_CRITICAL, | |
654 | 'interesting': PRIO_INTERESTING, | |
655 | 'useful': PRIO_USEFUL, | |
656 | 'uninteresting': PRIO_UNINTERESTING, | |
657 | 'debugonly': PRIO_DEBUGONLY, | |
658 | } | |
659 | ||
660 | if arg in PRIOMAP: | |
661 | return PRIOMAP[arg] | |
662 | # allow abbreviation | |
663 | for name, val in PRIOMAP.items(): | |
664 | if name.startswith(arg): | |
665 | return val | |
666 | return None | |
667 | ||
668 | # consume and analyze non-numeric args | |
669 | while len(childargs) and not isnum(childargs[0]): | |
670 | arg = childargs.pop(0) | |
671 | # 'list'? | |
672 | if arg in ['list', 'ls']: | |
673 | do_list = True; | |
674 | continue | |
675 | # prio? | |
676 | prio = prio_from_name(arg) | |
677 | if prio is not None: | |
678 | priority = prio | |
679 | continue | |
680 | # statpats | |
681 | statpats = arg.split(',') | |
682 | ||
683 | if priority is None: | |
684 | priority = PRIO_DEFAULT | |
685 | ||
686 | if len(childargs) > 0: | |
687 | try: | |
688 | interval = float(childargs.pop(0)) | |
689 | if interval < 0: | |
690 | raise ValueError | |
691 | except ValueError: | |
692 | print('daemonperf: interval should be a positive number', file=sys.stderr) | |
693 | return errno.EINVAL | |
694 | ||
695 | if len(childargs) > 0: | |
696 | arg = childargs.pop(0) | |
697 | if (not isnum(arg)) or (int(arg) < 0): | |
698 | print('daemonperf: count should be a positive integer', file=sys.stderr) | |
699 | return errno.EINVAL | |
700 | count = int(arg) | |
701 | ||
702 | watcher = DaemonWatcher(sockpath, statpats, priority) | |
703 | if do_list: | |
704 | watcher.list() | |
705 | else: | |
706 | watcher.run(interval, count) | |
707 | ||
708 | return 0 | |
709 | ||
710 | ### | |
711 | # main | |
712 | ### | |
713 | ||
714 | def main(): | |
715 | ceph_args = os.environ.get('CEPH_ARGS') | |
716 | if ceph_args: | |
717 | if "injectargs" in sys.argv: | |
718 | i = sys.argv.index("injectargs") | |
719 | sys.argv = sys.argv[:i] + ceph_args.split() + sys.argv[i:] | |
720 | else: | |
721 | sys.argv.extend(ceph_args.split()) | |
722 | parser, parsed_args, childargs = parse_cmdargs() | |
723 | ||
724 | if parsed_args.version: | |
725 | print('ceph version {0} ({1})'.format(CEPH_GIT_NICE_VER, CEPH_GIT_VER)) # noqa | |
726 | return 0 | |
727 | ||
728 | global verbose | |
729 | verbose = parsed_args.verbose | |
730 | ||
731 | if verbose: | |
732 | print("parsed_args: {0}, childargs: {1}".format(parsed_args, childargs), file=sys.stderr) | |
733 | ||
734 | if parsed_args.admin_socket_nope: | |
735 | print('--admin-socket is used by daemons; '\ | |
736 | 'you probably mean --admin-daemon/daemon', file=sys.stderr) | |
737 | return 1 | |
738 | ||
739 | # pass on --id, --name, --conf | |
740 | name = 'client.admin' | |
741 | if parsed_args.client_id: | |
742 | name = 'client.' + parsed_args.client_id | |
743 | if parsed_args.client_name: | |
744 | name = parsed_args.client_name | |
745 | ||
746 | # default '' means default conf search | |
747 | conffile = '' | |
748 | if parsed_args.cephconf: | |
749 | conffile = parsed_args.cephconf | |
750 | # For now, --admin-daemon is handled as usual. Try it | |
751 | # first in case we can't connect() to the cluster | |
752 | ||
753 | format = parsed_args.output_format | |
754 | ||
755 | done, ret = maybe_daemon_command(parsed_args, childargs) | |
756 | if done: | |
757 | return ret | |
758 | ||
759 | timeout = None | |
760 | if parsed_args.cluster_timeout: | |
761 | timeout = parsed_args.cluster_timeout | |
762 | ||
763 | # basic help | |
764 | if parsed_args.help: | |
765 | do_basic_help(parser, childargs) | |
766 | ||
767 | # handle any 'generic' ceph arguments that we didn't parse here | |
768 | global cluster_handle | |
769 | ||
770 | # rados.Rados() will call rados_create2, and then read the conf file, | |
771 | # and then set the keys from the dict. So we must do these | |
772 | # "pre-file defaults" first (see common_preinit in librados) | |
773 | conf_defaults = { | |
774 | 'log_to_stderr':'true', | |
775 | 'err_to_stderr':'true', | |
776 | 'log_flush_on_exit':'true', | |
777 | } | |
778 | ||
779 | if 'injectargs' in childargs: | |
780 | position = childargs.index('injectargs') | |
781 | injectargs = childargs[position:] | |
782 | childargs = childargs[:position] | |
783 | if verbose: | |
784 | print('Separate childargs {0} from injectargs {1}'.format(childargs, injectargs), | |
785 | file=sys.stderr) | |
786 | else: | |
787 | injectargs = None | |
788 | ||
789 | clustername = None | |
790 | if parsed_args.cluster: | |
791 | clustername = parsed_args.cluster | |
792 | ||
793 | try: | |
794 | cluster_handle = run_in_thread(rados.Rados, | |
795 | name=name, clustername=clustername, | |
796 | conf_defaults=conf_defaults, | |
797 | conffile=conffile) | |
798 | retargs = run_in_thread(cluster_handle.conf_parse_argv, childargs) | |
799 | except rados.Error as e: | |
800 | print('Error initializing cluster client: {0!r}'.format(e), file=sys.stderr) | |
801 | return 1 | |
802 | ||
803 | childargs = retargs | |
804 | if not childargs: | |
805 | childargs = [] | |
806 | ||
807 | # -- means "stop parsing args", but we don't want to see it either | |
808 | if '--' in childargs: | |
809 | childargs.remove('--') | |
810 | if injectargs and '--' in injectargs: | |
811 | injectargs.remove('--') | |
812 | ||
813 | # special deprecation warning for 'ceph <type> tell' | |
814 | # someday 'mds' will be here too | |
815 | if len(childargs) >= 2 and \ | |
816 | childargs[0] in ['mon', 'osd'] and \ | |
817 | childargs[1] == 'tell': | |
818 | print('"{0} tell" is deprecated; try "tell {0}.<id> <command> [options...]" instead (id can be "*") '.format(childargs[0]), | |
819 | file=sys.stderr) | |
820 | return 1 | |
821 | ||
822 | if parsed_args.help: | |
823 | # short default timeout for -h | |
824 | if not timeout: | |
825 | timeout = 5 | |
826 | ||
827 | hdr('Monitor commands:') | |
828 | print('[Contacting monitor, timeout after %d seconds]' % timeout) | |
829 | ||
830 | if childargs and childargs[0] == 'ping': | |
831 | if len(childargs) < 2: | |
832 | print('"ping" requires a monitor name as argument: "ping mon.<id>"', file=sys.stderr) | |
833 | return 1 | |
834 | if parsed_args.completion: | |
835 | #for completion let timeout be really small | |
836 | timeout = 3 | |
837 | try: | |
838 | if childargs and childargs[0] == 'ping': | |
839 | return ping_monitor(cluster_handle, childargs[1], timeout) | |
840 | run_in_thread(cluster_handle.connect, timeout=timeout) | |
841 | except KeyboardInterrupt: | |
842 | print('Cluster connection aborted', file=sys.stderr) | |
843 | return 1 | |
844 | except rados.PermissionDeniedError as e: | |
845 | print(str(e), file=sys.stderr) | |
846 | return errno.EACCES | |
847 | except Exception as e: | |
848 | print(str(e), file=sys.stderr) | |
849 | return 1 | |
850 | ||
851 | if parsed_args.help: | |
852 | return do_extended_help(parser, childargs) | |
853 | ||
854 | # implement -w/--watch_* | |
855 | # This is ugly, but Namespace() isn't quite rich enough. | |
856 | level = '' | |
857 | for k, v in parsed_args._get_kwargs(): | |
858 | if k.startswith('watch') and v: | |
859 | if k == 'watch': | |
860 | level = 'info' | |
861 | else: | |
862 | level = k.replace('watch_', '') | |
863 | if level: | |
864 | ||
865 | # an awfully simple callback | |
866 | def watch_cb(arg, line, who, stamp_sec, stamp_nsec, seq, level, msg): | |
867 | print(line) | |
868 | sys.stdout.flush() | |
869 | ||
870 | # first do a ceph status | |
871 | ret, outbuf, outs = json_command(cluster_handle, prefix='status') | |
872 | if ret == -errno.EINVAL: | |
873 | # try old mon | |
874 | ret, outbuf, outs = send_command(cluster_handle, cmd=['status']) | |
875 | # old mon returns status to outs...ick | |
876 | if ret == 0: | |
877 | outbuf += outs | |
878 | if ret: | |
879 | print("status query failed: ", outs, file=sys.stderr) | |
880 | return ret | |
881 | print(outbuf) | |
882 | ||
883 | # this instance keeps the watch connection alive, but is | |
884 | # otherwise unused | |
885 | run_in_thread(cluster_handle.monitor_log, level, watch_cb, 0) | |
886 | ||
887 | # loop forever letting watch_cb print lines | |
888 | try: | |
889 | signal.pause() | |
890 | except KeyboardInterrupt: | |
891 | # or until ^C, at least | |
892 | return 0 | |
893 | ||
894 | # read input file, if any | |
895 | inbuf = b'' | |
896 | if parsed_args.input_file: | |
897 | try: | |
898 | with open(parsed_args.input_file, 'rb') as f: | |
899 | inbuf = f.read() | |
900 | except Exception as e: | |
901 | print('Can\'t open input file {0}: {1}'.format(parsed_args.input_file, e), file=sys.stderr) | |
902 | return 1 | |
903 | ||
904 | # prepare output file, if any | |
905 | if parsed_args.output_file: | |
906 | try: | |
907 | outf = open(parsed_args.output_file, 'wb') | |
908 | except Exception as e: | |
909 | print('Can\'t open output file {0}: {1}'.format(parsed_args.output_file, e), file=sys.stderr) | |
910 | return 1 | |
911 | ||
912 | # -s behaves like a command (ceph status). | |
913 | if parsed_args.status: | |
914 | childargs.insert(0, 'status') | |
915 | ||
916 | try: | |
917 | target = find_cmd_target(childargs) | |
918 | except Exception as e: | |
919 | print('error handling command target: {0}'.format(e), file=sys.stderr) | |
920 | return 1 | |
921 | ||
922 | # Repulsive hack to handle tell: lop off 'tell' and target | |
923 | # and validate the rest of the command. 'target' is already | |
924 | # determined in our callers, so it's ok to remove it here. | |
925 | is_tell = False | |
926 | if len(childargs) and childargs[0] == 'tell': | |
927 | childargs = childargs[2:] | |
928 | is_tell = True | |
929 | ||
930 | if is_tell: | |
931 | if injectargs: | |
932 | childargs = injectargs | |
933 | if not len(childargs): | |
934 | print('"{0} tell" requires additional arguments.'.format(sys.argv[0]), | |
935 | 'Try "{0} tell <name> <command> [options...]" instead.'.format(sys.argv[0]), | |
936 | file=sys.stderr) | |
937 | return errno.EINVAL | |
938 | ||
939 | # fetch JSON sigs from command | |
940 | # each line contains one command signature (a placeholder name | |
941 | # of the form 'cmdNNN' followed by an array of argument descriptors) | |
942 | # as part of the validated argument JSON object | |
943 | ||
944 | targets = [target] | |
945 | ||
946 | if target[1] == '*': | |
947 | if target[0] == 'osd': | |
948 | targets = [(target[0], o) for o in osdids()] | |
949 | elif target[0] == 'mon': | |
950 | targets = [(target[0], m) for m in monids()] | |
951 | ||
952 | final_ret = 0 | |
953 | for target in targets: | |
954 | # prettify? prefix output with target, if there was a wildcard used | |
955 | prefix = '' | |
956 | suffix = '' | |
957 | if not parsed_args.output_file and len(targets) > 1: | |
958 | prefix = '{0}.{1}: '.format(*target) | |
959 | suffix = '\n' | |
960 | ||
961 | ret, outbuf, outs = json_command(cluster_handle, target=target, | |
962 | prefix='get_command_descriptions') | |
963 | compat = False | |
964 | if ret == -errno.EINVAL: | |
965 | # send command to old monitor or OSD | |
966 | if verbose: | |
967 | print(prefix + '{0} to old {1}'.format(' '.join(childargs), target[0])) | |
968 | compat = True | |
969 | if parsed_args.output_format: | |
970 | childargs.extend(['--format', parsed_args.output_format]) | |
971 | ret, outbuf, outs = send_command(cluster_handle, target, childargs, | |
972 | inbuf) | |
973 | ||
974 | if ret == -errno.EINVAL: | |
975 | # did we race with a mon upgrade? try again! | |
976 | ret, outbuf, outs = json_command(cluster_handle, target=target, | |
977 | prefix='get_command_descriptions') | |
978 | if ret == 0: | |
979 | compat = False # yep, carry on | |
980 | if not compat: | |
981 | if ret: | |
982 | if ret < 0: | |
983 | outs = 'problem getting command descriptions from {0}.{1}'.format(*target) | |
984 | else: | |
985 | sigdict = parse_json_funcsigs(outbuf.decode('utf-8'), 'cli') | |
986 | ||
987 | if parsed_args.completion: | |
988 | return complete(sigdict, childargs, target) | |
989 | ||
990 | ret, outbuf, outs = new_style_command(parsed_args, childargs, target, | |
991 | sigdict, inbuf, verbose) | |
992 | ||
993 | # debug tool: send any successful command *again* to | |
994 | # verify that it is idempotent. | |
995 | if not ret and 'CEPH_CLI_TEST_DUP_COMMAND' in os.environ: | |
996 | ret, outbuf, outs = new_style_command(parsed_args, childargs, target, | |
997 | sigdict, inbuf, verbose) | |
998 | if ret < 0: | |
999 | ret = -ret | |
1000 | print(prefix + 'Second attempt of previously successful command failed with {0}: {1}'.format(errno.errorcode.get(ret, 'Unknown'), outs), | |
1001 | file=sys.stderr) | |
1002 | ||
1003 | if ret < 0: | |
1004 | ret = -ret | |
1005 | print(u'Error {0}: {1}'.format(errno.errorcode.get(ret, 'Unknown'), outs), file=sys.stderr) | |
1006 | if len(targets) > 1: | |
1007 | final_ret = ret | |
1008 | else: | |
1009 | return ret | |
1010 | ||
1011 | # this assumes outs never has useful command output, only status | |
1012 | if compat: | |
1013 | if ret == 0: | |
1014 | # old cli/mon would send status string to stdout on non-error | |
1015 | print(outs) | |
1016 | else: | |
1017 | if outs: | |
1018 | print(prefix + outs, file=sys.stderr) | |
1019 | ||
1020 | sys.stdout.flush() | |
1021 | ||
1022 | if (parsed_args.output_file): | |
1023 | outf.write(outbuf) | |
1024 | else: | |
1025 | # hack: old code printed status line before many json outputs | |
1026 | # (osd dump, etc.) that consumers know to ignore. Add blank line | |
1027 | # to satisfy consumers that skip the first line, but not annoy | |
1028 | # consumers that don't. | |
1029 | if parsed_args.output_format and \ | |
1030 | parsed_args.output_format.startswith('json') and \ | |
1031 | not compat: | |
1032 | print() | |
1033 | ||
1034 | # if we are prettifying things, normalize newlines. sigh. | |
1035 | if suffix: | |
1036 | outbuf = outbuf.rstrip() | |
1037 | if outbuf: | |
1038 | try: | |
1039 | print(prefix, end='') | |
1040 | # Write directly to binary stdout | |
1041 | raw_write(outbuf) | |
1042 | print(suffix, end='') | |
1043 | except IOError as e: | |
1044 | if e.errno != errno.EPIPE: | |
1045 | raise e | |
1046 | ||
1047 | sys.stdout.flush() | |
1048 | ||
1049 | if (parsed_args.output_file): | |
1050 | outf.close() | |
1051 | ||
1052 | if final_ret: | |
1053 | return final_ret | |
1054 | ||
1055 | return 0 | |
1056 | ||
1057 | if __name__ == '__main__': | |
1058 | retval = main() | |
1059 | # shutdown explicitly; Rados() does not | |
1060 | if cluster_handle: | |
1061 | run_in_thread(cluster_handle.shutdown) | |
1062 | sys.exit(retval) |