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