]>
Commit | Line | Data |
---|---|---|
7c673cae FG |
1 | """ |
2 | Types and routines used by the ceph CLI as well as the RESTful | |
3 | interface. These have to do with querying the daemons for | |
4 | command-description information, validating user command input against | |
5 | those descriptions, and submitting the command to the appropriate | |
6 | daemon. | |
7 | ||
8 | Copyright (C) 2013 Inktank Storage, Inc. | |
9 | ||
10 | LGPL2. See file COPYING. | |
11 | """ | |
12 | from __future__ import print_function | |
13 | import copy | |
14 | import errno | |
15 | import json | |
16 | import os | |
17 | import pprint | |
18 | import re | |
19 | import socket | |
20 | import stat | |
21 | import sys | |
22 | import threading | |
23 | import uuid | |
24 | ||
25 | ||
26 | FLAG_MGR = 8 # command is intended for mgr | |
27 | ||
28 | ||
29 | try: | |
30 | basestring | |
31 | except NameError: | |
32 | basestring = str | |
33 | ||
34 | ||
35 | class ArgumentError(Exception): | |
36 | """ | |
37 | Something wrong with arguments | |
38 | """ | |
39 | pass | |
40 | ||
41 | ||
42 | class ArgumentNumber(ArgumentError): | |
43 | """ | |
44 | Wrong number of a repeated argument | |
45 | """ | |
46 | pass | |
47 | ||
48 | ||
49 | class ArgumentFormat(ArgumentError): | |
50 | """ | |
51 | Argument value has wrong format | |
52 | """ | |
53 | pass | |
54 | ||
55 | ||
94b18763 FG |
56 | class ArgumentMissing(ArgumentError): |
57 | """ | |
58 | Argument value missing in a command | |
59 | """ | |
60 | pass | |
61 | ||
62 | ||
7c673cae FG |
63 | class ArgumentValid(ArgumentError): |
64 | """ | |
65 | Argument value is otherwise invalid (doesn't match choices, for instance) | |
66 | """ | |
67 | pass | |
68 | ||
69 | ||
70 | class ArgumentTooFew(ArgumentError): | |
71 | """ | |
72 | Fewer arguments than descriptors in signature; may mean to continue | |
73 | the search, so gets a special exception type | |
74 | """ | |
75 | ||
76 | ||
77 | class ArgumentPrefix(ArgumentError): | |
78 | """ | |
79 | Special for mismatched prefix; less severe, don't report by default | |
80 | """ | |
81 | pass | |
82 | ||
83 | ||
84 | class JsonFormat(Exception): | |
85 | """ | |
86 | some syntactic or semantic issue with the JSON | |
87 | """ | |
88 | pass | |
89 | ||
90 | ||
91 | class CephArgtype(object): | |
92 | """ | |
93 | Base class for all Ceph argument types | |
94 | ||
95 | Instantiating an object sets any validation parameters | |
96 | (allowable strings, numeric ranges, etc.). The 'valid' | |
97 | method validates a string against that initialized instance, | |
98 | throwing ArgumentError if there's a problem. | |
99 | """ | |
100 | def __init__(self, **kwargs): | |
101 | """ | |
102 | set any per-instance validation parameters here | |
103 | from kwargs (fixed string sets, integer ranges, etc) | |
104 | """ | |
105 | pass | |
106 | ||
107 | def valid(self, s, partial=False): | |
108 | """ | |
109 | Run validation against given string s (generally one word); | |
110 | partial means to accept partial string matches (begins-with). | |
111 | If cool, set self.val to the value that should be returned | |
112 | (a copy of the input string, or a numeric or boolean interpretation | |
113 | thereof, for example) | |
114 | if not, throw ArgumentError(msg-as-to-why) | |
115 | """ | |
116 | self.val = s | |
117 | ||
118 | def __repr__(self): | |
119 | """ | |
120 | return string representation of description of type. Note, | |
121 | this is not a representation of the actual value. Subclasses | |
122 | probably also override __str__() to give a more user-friendly | |
123 | 'name/type' description for use in command format help messages. | |
124 | """ | |
125 | a = '' | |
126 | if hasattr(self, 'typeargs'): | |
127 | a = self.typeargs | |
128 | return '{0}(\'{1}\')'.format(self.__class__.__name__, a) | |
129 | ||
130 | def __str__(self): | |
131 | """ | |
132 | where __repr__ (ideally) returns a string that could be used to | |
133 | reproduce the object, __str__ returns one you'd like to see in | |
134 | print messages. Use __str__ to format the argtype descriptor | |
135 | as it would be useful in a command usage message. | |
136 | """ | |
137 | return '<{0}>'.format(self.__class__.__name__) | |
138 | ||
139 | def complete(self, s): | |
140 | return [] | |
141 | ||
142 | ||
143 | class CephInt(CephArgtype): | |
144 | """ | |
145 | range-limited integers, [+|-][0-9]+ or 0x[0-9a-f]+ | |
146 | range: list of 1 or 2 ints, [min] or [min,max] | |
147 | """ | |
148 | def __init__(self, range=''): | |
149 | if range == '': | |
150 | self.range = list() | |
151 | else: | |
152 | self.range = list(range.split('|')) | |
153 | self.range = [int(x) for x in self.range] | |
154 | ||
155 | def valid(self, s, partial=False): | |
156 | try: | |
157 | val = int(s) | |
158 | except ValueError: | |
159 | raise ArgumentValid("{0} doesn't represent an int".format(s)) | |
160 | if len(self.range) == 2: | |
161 | if val < self.range[0] or val > self.range[1]: | |
162 | raise ArgumentValid("{0} not in range {1}".format(val, self.range)) | |
163 | elif len(self.range) == 1: | |
164 | if val < self.range[0]: | |
165 | raise ArgumentValid("{0} not in range {1}".format(val, self.range)) | |
166 | self.val = val | |
167 | ||
168 | def __str__(self): | |
169 | r = '' | |
170 | if len(self.range) == 1: | |
171 | r = '[{0}-]'.format(self.range[0]) | |
172 | if len(self.range) == 2: | |
173 | r = '[{0}-{1}]'.format(self.range[0], self.range[1]) | |
174 | ||
175 | return '<int{0}>'.format(r) | |
176 | ||
177 | ||
178 | class CephFloat(CephArgtype): | |
179 | """ | |
180 | range-limited float type | |
181 | range: list of 1 or 2 floats, [min] or [min, max] | |
182 | """ | |
183 | def __init__(self, range=''): | |
184 | if range == '': | |
185 | self.range = list() | |
186 | else: | |
187 | self.range = list(range.split('|')) | |
188 | self.range = [float(x) for x in self.range] | |
189 | ||
190 | def valid(self, s, partial=False): | |
191 | try: | |
192 | val = float(s) | |
193 | except ValueError: | |
194 | raise ArgumentValid("{0} doesn't represent a float".format(s)) | |
195 | if len(self.range) == 2: | |
196 | if val < self.range[0] or val > self.range[1]: | |
197 | raise ArgumentValid("{0} not in range {1}".format(val, self.range)) | |
198 | elif len(self.range) == 1: | |
199 | if val < self.range[0]: | |
200 | raise ArgumentValid("{0} not in range {1}".format(val, self.range)) | |
201 | self.val = val | |
202 | ||
203 | def __str__(self): | |
204 | r = '' | |
205 | if len(self.range) == 1: | |
206 | r = '[{0}-]'.format(self.range[0]) | |
207 | if len(self.range) == 2: | |
208 | r = '[{0}-{1}]'.format(self.range[0], self.range[1]) | |
209 | return '<float{0}>'.format(r) | |
210 | ||
211 | ||
212 | class CephString(CephArgtype): | |
213 | """ | |
214 | String; pretty generic. goodchars is a RE char class of valid chars | |
215 | """ | |
216 | def __init__(self, goodchars=''): | |
217 | from string import printable | |
218 | try: | |
219 | re.compile(goodchars) | |
220 | except: | |
221 | raise ValueError('CephString(): "{0}" is not a valid RE'. | |
222 | format(goodchars)) | |
223 | self.goodchars = goodchars | |
224 | self.goodset = frozenset( | |
225 | [c for c in printable if re.match(goodchars, c)] | |
226 | ) | |
227 | ||
228 | def valid(self, s, partial=False): | |
229 | sset = set(s) | |
230 | if self.goodset and not sset <= self.goodset: | |
231 | raise ArgumentFormat("invalid chars {0} in {1}". | |
232 | format(''.join(sset - self.goodset), s)) | |
233 | self.val = s | |
234 | ||
235 | def __str__(self): | |
236 | b = '' | |
237 | if self.goodchars: | |
238 | b += '(goodchars {0})'.format(self.goodchars) | |
239 | return '<string{0}>'.format(b) | |
240 | ||
241 | def complete(self, s): | |
242 | if s == '': | |
243 | return [] | |
244 | else: | |
245 | return [s] | |
246 | ||
247 | ||
248 | class CephSocketpath(CephArgtype): | |
249 | """ | |
250 | Admin socket path; check that it's readable and S_ISSOCK | |
251 | """ | |
252 | def valid(self, s, partial=False): | |
253 | mode = os.stat(s).st_mode | |
254 | if not stat.S_ISSOCK(mode): | |
255 | raise ArgumentValid('socket path {0} is not a socket'.format(s)) | |
256 | self.val = s | |
257 | ||
258 | def __str__(self): | |
259 | return '<admin-socket-path>' | |
260 | ||
261 | ||
262 | class CephIPAddr(CephArgtype): | |
263 | """ | |
264 | IP address (v4 or v6) with optional port | |
265 | """ | |
266 | def valid(self, s, partial=False): | |
267 | # parse off port, use socket to validate addr | |
268 | type = 6 | |
269 | if s.startswith('['): | |
270 | type = 6 | |
271 | elif s.find('.') != -1: | |
272 | type = 4 | |
273 | if type == 4: | |
274 | port = s.find(':') | |
275 | if port != -1: | |
276 | a = s[:port] | |
277 | p = s[port + 1:] | |
278 | if int(p) > 65535: | |
279 | raise ArgumentValid('{0}: invalid IPv4 port'.format(p)) | |
280 | else: | |
281 | a = s | |
282 | p = None | |
283 | try: | |
284 | socket.inet_pton(socket.AF_INET, a) | |
285 | except: | |
286 | raise ArgumentValid('{0}: invalid IPv4 address'.format(a)) | |
287 | else: | |
288 | # v6 | |
289 | if s.startswith('['): | |
290 | end = s.find(']') | |
291 | if end == -1: | |
292 | raise ArgumentFormat('{0} missing terminating ]'.format(s)) | |
293 | if s[end + 1] == ':': | |
294 | try: | |
295 | p = int(s[end + 2]) | |
296 | except: | |
297 | raise ArgumentValid('{0}: bad port number'.format(s)) | |
298 | a = s[1:end] | |
299 | else: | |
300 | a = s | |
301 | p = None | |
302 | try: | |
303 | socket.inet_pton(socket.AF_INET6, a) | |
304 | except: | |
305 | raise ArgumentValid('{0} not valid IPv6 address'.format(s)) | |
306 | if p is not None and int(p) > 65535: | |
307 | raise ArgumentValid("{0} not a valid port number".format(p)) | |
308 | self.val = s | |
309 | self.addr = a | |
310 | self.port = p | |
311 | ||
312 | def __str__(self): | |
313 | return '<IPaddr[:port]>' | |
314 | ||
315 | ||
316 | class CephEntityAddr(CephIPAddr): | |
317 | """ | |
318 | EntityAddress, that is, IP address[/nonce] | |
319 | """ | |
320 | def valid(self, s, partial=False): | |
321 | nonce = None | |
322 | if '/' in s: | |
323 | ip, nonce = s.split('/') | |
324 | else: | |
325 | ip = s | |
326 | super(self.__class__, self).valid(ip) | |
327 | if nonce: | |
328 | nonce_int = None | |
329 | try: | |
330 | nonce_int = int(nonce) | |
331 | except ValueError: | |
332 | pass | |
333 | if nonce_int is None or nonce_int < 0: | |
334 | raise ArgumentValid( | |
335 | '{0}: invalid entity, nonce {1} not integer > 0'. | |
336 | format(s, nonce) | |
337 | ) | |
338 | self.val = s | |
339 | ||
340 | def __str__(self): | |
341 | return '<EntityAddr>' | |
342 | ||
343 | ||
344 | class CephPoolname(CephArgtype): | |
345 | """ | |
346 | Pool name; very little utility | |
347 | """ | |
348 | def __str__(self): | |
349 | return '<poolname>' | |
350 | ||
351 | ||
352 | class CephObjectname(CephArgtype): | |
353 | """ | |
354 | Object name. Maybe should be combined with Pool name as they're always | |
355 | present in pairs, and then could be checked for presence | |
356 | """ | |
357 | def __str__(self): | |
358 | return '<objectname>' | |
359 | ||
360 | ||
361 | class CephPgid(CephArgtype): | |
362 | """ | |
363 | pgid, in form N.xxx (N = pool number, xxx = hex pgnum) | |
364 | """ | |
365 | def valid(self, s, partial=False): | |
366 | if s.find('.') == -1: | |
367 | raise ArgumentFormat('pgid has no .') | |
368 | poolid, pgnum = s.split('.', 1) | |
369 | try: | |
370 | poolid = int(poolid) | |
371 | except ValueError: | |
372 | raise ArgumentFormat('pool {0} not integer'.format(poolid)) | |
373 | if poolid < 0: | |
374 | raise ArgumentFormat('pool {0} < 0'.format(poolid)) | |
375 | try: | |
376 | pgnum = int(pgnum, 16) | |
377 | except ValueError: | |
378 | raise ArgumentFormat('pgnum {0} not hex integer'.format(pgnum)) | |
379 | self.val = s | |
380 | ||
381 | def __str__(self): | |
382 | return '<pgid>' | |
383 | ||
384 | ||
385 | class CephName(CephArgtype): | |
386 | """ | |
387 | Name (type.id) where: | |
388 | type is osd|mon|client|mds | |
389 | id is a base10 int, if type == osd, or a string otherwise | |
390 | ||
391 | Also accept '*' | |
392 | """ | |
393 | def __init__(self): | |
394 | self.nametype = None | |
395 | self.nameid = None | |
396 | ||
397 | def valid(self, s, partial=False): | |
398 | if s == '*': | |
399 | self.val = s | |
400 | return | |
401 | elif s == "mgr": | |
402 | self.nametype = "mgr" | |
403 | self.val = s | |
404 | return | |
31f18b77 FG |
405 | elif s == "mon": |
406 | self.nametype = "mon" | |
407 | self.val = s | |
408 | return | |
7c673cae FG |
409 | if s.find('.') == -1: |
410 | raise ArgumentFormat('CephName: no . in {0}'.format(s)) | |
411 | else: | |
412 | t, i = s.split('.', 1) | |
413 | if t not in ('osd', 'mon', 'client', 'mds', 'mgr'): | |
414 | raise ArgumentValid('unknown type ' + t) | |
415 | if t == 'osd': | |
416 | if i != '*': | |
417 | try: | |
418 | i = int(i) | |
419 | except: | |
420 | raise ArgumentFormat('osd id ' + i + ' not integer') | |
421 | self.nametype = t | |
422 | self.val = s | |
423 | self.nameid = i | |
424 | ||
425 | def __str__(self): | |
426 | return '<name (type.id)>' | |
427 | ||
428 | ||
429 | class CephOsdName(CephArgtype): | |
430 | """ | |
431 | Like CephName, but specific to osds: allow <id> alone | |
432 | ||
433 | osd.<id>, or <id>, or *, where id is a base10 int | |
434 | """ | |
435 | def __init__(self): | |
436 | self.nametype = None | |
437 | self.nameid = None | |
438 | ||
439 | def valid(self, s, partial=False): | |
440 | if s == '*': | |
441 | self.val = s | |
442 | return | |
443 | if s.find('.') != -1: | |
444 | t, i = s.split('.', 1) | |
445 | if t != 'osd': | |
446 | raise ArgumentValid('unknown type ' + t) | |
447 | else: | |
448 | t = 'osd' | |
449 | i = s | |
450 | try: | |
451 | i = int(i) | |
452 | except: | |
453 | raise ArgumentFormat('osd id ' + i + ' not integer') | |
454 | if i < 0: | |
455 | raise ArgumentFormat('osd id {0} is less than 0'.format(i)) | |
456 | self.nametype = t | |
457 | self.nameid = i | |
458 | self.val = i | |
459 | ||
460 | def __str__(self): | |
461 | return '<osdname (id|osd.id)>' | |
462 | ||
463 | ||
464 | class CephChoices(CephArgtype): | |
465 | """ | |
466 | Set of string literals; init with valid choices | |
467 | """ | |
468 | def __init__(self, strings='', **kwargs): | |
469 | self.strings = strings.split('|') | |
470 | ||
471 | def valid(self, s, partial=False): | |
472 | if not partial: | |
473 | if s not in self.strings: | |
474 | # show as __str__ does: {s1|s2..} | |
475 | raise ArgumentValid("{0} not in {1}".format(s, self)) | |
476 | self.val = s | |
477 | return | |
478 | ||
479 | # partial | |
480 | for t in self.strings: | |
481 | if t.startswith(s): | |
482 | self.val = s | |
483 | return | |
484 | raise ArgumentValid("{0} not in {1}". format(s, self)) | |
485 | ||
486 | def __str__(self): | |
487 | if len(self.strings) == 1: | |
488 | return '{0}'.format(self.strings[0]) | |
489 | else: | |
490 | return '{0}'.format('|'.join(self.strings)) | |
491 | ||
492 | def complete(self, s): | |
493 | all_elems = [token for token in self.strings if token.startswith(s)] | |
494 | return all_elems | |
495 | ||
496 | ||
497 | class CephFilepath(CephArgtype): | |
498 | """ | |
499 | Openable file | |
500 | """ | |
501 | def valid(self, s, partial=False): | |
502 | try: | |
503 | f = open(s, 'a+') | |
504 | except Exception as e: | |
505 | raise ArgumentValid('can\'t open {0}: {1}'.format(s, e)) | |
506 | f.close() | |
507 | self.val = s | |
508 | ||
509 | def __str__(self): | |
510 | return '<outfilename>' | |
511 | ||
512 | ||
513 | class CephFragment(CephArgtype): | |
514 | """ | |
515 | 'Fragment' ??? XXX | |
516 | """ | |
517 | def valid(self, s, partial=False): | |
518 | if s.find('/') == -1: | |
519 | raise ArgumentFormat('{0}: no /'.format(s)) | |
520 | val, bits = s.split('/') | |
521 | # XXX is this right? | |
522 | if not val.startswith('0x'): | |
523 | raise ArgumentFormat("{0} not a hex integer".format(val)) | |
524 | try: | |
525 | int(val) | |
526 | except: | |
527 | raise ArgumentFormat('can\'t convert {0} to integer'.format(val)) | |
528 | try: | |
529 | int(bits) | |
530 | except: | |
531 | raise ArgumentFormat('can\'t convert {0} to integer'.format(bits)) | |
532 | self.val = s | |
533 | ||
534 | def __str__(self): | |
535 | return "<CephFS fragment ID (0xvvv/bbb)>" | |
536 | ||
537 | ||
538 | class CephUUID(CephArgtype): | |
539 | """ | |
540 | CephUUID: pretty self-explanatory | |
541 | """ | |
542 | def valid(self, s, partial=False): | |
543 | try: | |
544 | uuid.UUID(s) | |
545 | except Exception as e: | |
546 | raise ArgumentFormat('invalid UUID {0}: {1}'.format(s, e)) | |
547 | self.val = s | |
548 | ||
549 | def __str__(self): | |
550 | return '<uuid>' | |
551 | ||
552 | ||
553 | class CephPrefix(CephArgtype): | |
554 | """ | |
555 | CephPrefix: magic type for "all the first n fixed strings" | |
556 | """ | |
557 | def __init__(self, prefix=''): | |
558 | self.prefix = prefix | |
559 | ||
560 | def valid(self, s, partial=False): | |
561 | try: | |
562 | s = str(s) | |
563 | if isinstance(s, bytes): | |
564 | # `prefix` can always be converted into unicode when being compared, | |
565 | # but `s` could be anything passed by user. | |
566 | s = s.decode('ascii') | |
567 | except UnicodeEncodeError: | |
568 | raise ArgumentPrefix(u"no match for {0}".format(s)) | |
569 | except UnicodeDecodeError: | |
570 | raise ArgumentPrefix("no match for {0}".format(s)) | |
571 | ||
572 | if partial: | |
573 | if self.prefix.startswith(s): | |
574 | self.val = s | |
575 | return | |
576 | else: | |
577 | if s == self.prefix: | |
578 | self.val = s | |
579 | return | |
580 | ||
581 | raise ArgumentPrefix("no match for {0}".format(s)) | |
582 | ||
583 | def __str__(self): | |
584 | return self.prefix | |
585 | ||
586 | def complete(self, s): | |
587 | if self.prefix.startswith(s): | |
588 | return [self.prefix.rstrip(' ')] | |
589 | else: | |
590 | return [] | |
591 | ||
592 | ||
593 | class argdesc(object): | |
594 | """ | |
595 | argdesc(typename, name='name', n=numallowed|N, | |
596 | req=False, helptext=helptext, **kwargs (type-specific)) | |
597 | ||
598 | validation rules: | |
599 | typename: type(**kwargs) will be constructed | |
600 | later, type.valid(w) will be called with a word in that position | |
601 | ||
602 | name is used for parse errors and for constructing JSON output | |
603 | n is a numeric literal or 'n|N', meaning "at least one, but maybe more" | |
604 | req=False means the argument need not be present in the list | |
605 | helptext is the associated help for the command | |
606 | anything else are arguments to pass to the type constructor. | |
607 | ||
608 | self.instance is an instance of type t constructed with typeargs. | |
609 | ||
610 | valid() will later be called with input to validate against it, | |
611 | and will store the validated value in self.instance.val for extraction. | |
612 | """ | |
613 | def __init__(self, t, name=None, n=1, req=True, **kwargs): | |
614 | if isinstance(t, basestring): | |
615 | self.t = CephPrefix | |
616 | self.typeargs = {'prefix': t} | |
617 | self.req = True | |
618 | else: | |
619 | self.t = t | |
620 | self.typeargs = kwargs | |
621 | self.req = bool(req == True or req == 'True') | |
622 | ||
623 | self.name = name | |
624 | self.N = (n in ['n', 'N']) | |
625 | if self.N: | |
626 | self.n = 1 | |
627 | else: | |
628 | self.n = int(n) | |
629 | self.instance = self.t(**self.typeargs) | |
630 | ||
631 | def __repr__(self): | |
632 | r = 'argdesc(' + str(self.t) + ', ' | |
633 | internals = ['N', 'typeargs', 'instance', 't'] | |
634 | for (k, v) in self.__dict__.items(): | |
635 | if k.startswith('__') or k in internals: | |
636 | pass | |
637 | else: | |
638 | # undo modification from __init__ | |
639 | if k == 'n' and self.N: | |
640 | v = 'N' | |
641 | r += '{0}={1}, '.format(k, v) | |
642 | for (k, v) in self.typeargs.items(): | |
643 | r += '{0}={1}, '.format(k, v) | |
644 | return r[:-2] + ')' | |
645 | ||
646 | def __str__(self): | |
647 | if ((self.t == CephChoices and len(self.instance.strings) == 1) | |
648 | or (self.t == CephPrefix)): | |
649 | s = str(self.instance) | |
650 | else: | |
651 | s = '{0}({1})'.format(self.name, str(self.instance)) | |
652 | if self.N: | |
653 | s += ' [' + str(self.instance) + '...]' | |
654 | if not self.req: | |
655 | s = '{' + s + '}' | |
656 | return s | |
657 | ||
658 | def helpstr(self): | |
659 | """ | |
660 | like str(), but omit parameter names (except for CephString, | |
661 | which really needs them) | |
662 | """ | |
663 | if self.t == CephString: | |
664 | chunk = '<{0}>'.format(self.name) | |
665 | else: | |
666 | chunk = str(self.instance) | |
667 | s = chunk | |
668 | if self.N: | |
669 | s += ' [' + chunk + '...]' | |
670 | if not self.req: | |
671 | s = '{' + s + '}' | |
672 | return s | |
673 | ||
674 | def complete(self, s): | |
675 | return self.instance.complete(s) | |
676 | ||
677 | ||
678 | def concise_sig(sig): | |
679 | """ | |
680 | Return string representation of sig useful for syntax reference in help | |
681 | """ | |
682 | return ' '.join([d.helpstr() for d in sig]) | |
683 | ||
684 | ||
685 | def descsort_key(sh): | |
686 | """ | |
687 | sort descriptors by prefixes, defined as the concatenation of all simple | |
688 | strings in the descriptor; this works out to just the leading strings. | |
689 | """ | |
690 | return concise_sig(sh['sig']) | |
691 | ||
692 | ||
693 | def descsort(sh1, sh2): | |
694 | """ | |
695 | Deprecated; use (key=descsort_key) instead of (cmp=descsort) | |
696 | """ | |
697 | return cmp(descsort_key(sh1), descsort_key(sh2)) | |
698 | ||
699 | ||
700 | def parse_funcsig(sig): | |
701 | """ | |
702 | parse a single descriptor (array of strings or dicts) into a | |
703 | dict of function descriptor/validators (objects of CephXXX type) | |
704 | """ | |
705 | newsig = [] | |
706 | argnum = 0 | |
707 | for desc in sig: | |
708 | argnum += 1 | |
709 | if isinstance(desc, basestring): | |
710 | t = CephPrefix | |
711 | desc = {'type': t, 'name': 'prefix', 'prefix': desc} | |
712 | else: | |
713 | # not a simple string, must be dict | |
714 | if 'type' not in desc: | |
715 | s = 'JSON descriptor {0} has no type'.format(sig) | |
716 | raise JsonFormat(s) | |
717 | # look up type string in our globals() dict; if it's an | |
718 | # object of type `type`, it must be a | |
719 | # locally-defined class. otherwise, we haven't a clue. | |
720 | if desc['type'] in globals(): | |
721 | t = globals()[desc['type']] | |
722 | if not isinstance(t, type): | |
723 | s = 'unknown type {0}'.format(desc['type']) | |
724 | raise JsonFormat(s) | |
725 | else: | |
726 | s = 'unknown type {0}'.format(desc['type']) | |
727 | raise JsonFormat(s) | |
728 | ||
729 | kwargs = dict() | |
730 | for key, val in desc.items(): | |
731 | if key not in ['type', 'name', 'n', 'req']: | |
732 | kwargs[key] = val | |
733 | newsig.append(argdesc(t, | |
734 | name=desc.get('name', None), | |
735 | n=desc.get('n', 1), | |
736 | req=desc.get('req', True), | |
737 | **kwargs)) | |
738 | return newsig | |
739 | ||
740 | ||
741 | def parse_json_funcsigs(s, consumer): | |
742 | """ | |
743 | A function signature is mostly an array of argdesc; it's represented | |
744 | in JSON as | |
745 | { | |
746 | "cmd001": {"sig":[ "type": type, "name": name, "n": num, "req":true|false <other param>], "help":helptext, "module":modulename, "perm":perms, "avail":availability} | |
747 | . | |
748 | . | |
749 | . | |
750 | ] | |
751 | ||
752 | A set of sigs is in an dict mapped by a unique number: | |
753 | { | |
754 | "cmd1": { | |
755 | "sig": ["type.. ], "help":helptext... | |
756 | } | |
757 | "cmd2"{ | |
758 | "sig": [.. ], "help":helptext... | |
759 | } | |
760 | } | |
761 | ||
762 | Parse the string s and return a dict of dicts, keyed by opcode; | |
763 | each dict contains 'sig' with the array of descriptors, and 'help' | |
764 | with the helptext, 'module' with the module name, 'perm' with a | |
765 | string representing required permissions in that module to execute | |
766 | this command (and also whether it is a read or write command from | |
767 | the cluster state perspective), and 'avail' as a hint for | |
768 | whether the command should be advertised by CLI, REST, or both. | |
769 | If avail does not contain 'consumer', don't include the command | |
770 | in the returned dict. | |
771 | """ | |
772 | try: | |
773 | overall = json.loads(s) | |
774 | except Exception as e: | |
775 | print("Couldn't parse JSON {0}: {1}".format(s, e), file=sys.stderr) | |
776 | raise e | |
777 | sigdict = {} | |
778 | for cmdtag, cmd in overall.items(): | |
779 | if 'sig' not in cmd: | |
780 | s = "JSON descriptor {0} has no 'sig'".format(cmdtag) | |
781 | raise JsonFormat(s) | |
782 | # check 'avail' and possibly ignore this command | |
783 | if 'avail' in cmd: | |
784 | if consumer not in cmd['avail']: | |
785 | continue | |
786 | # rewrite the 'sig' item with the argdesc-ized version, and... | |
787 | cmd['sig'] = parse_funcsig(cmd['sig']) | |
788 | # just take everything else as given | |
789 | sigdict[cmdtag] = cmd | |
790 | return sigdict | |
791 | ||
792 | ||
793 | def validate_one(word, desc, partial=False): | |
794 | """ | |
795 | validate_one(word, desc, partial=False) | |
796 | ||
797 | validate word against the constructed instance of the type | |
798 | in desc. May raise exception. If it returns false (and doesn't | |
799 | raise an exception), desc.instance.val will | |
800 | contain the validated value (in the appropriate type). | |
801 | """ | |
802 | desc.instance.valid(word, partial) | |
803 | desc.numseen += 1 | |
804 | if desc.N: | |
805 | desc.n = desc.numseen + 1 | |
806 | ||
807 | ||
808 | def matchnum(args, signature, partial=False): | |
809 | """ | |
810 | matchnum(s, signature, partial=False) | |
811 | ||
812 | Returns number of arguments matched in s against signature. | |
813 | Can be used to determine most-likely command for full or partial | |
814 | matches (partial applies to string matches). | |
815 | """ | |
816 | words = args[:] | |
817 | mysig = copy.deepcopy(signature) | |
818 | matchcnt = 0 | |
819 | for desc in mysig: | |
820 | setattr(desc, 'numseen', 0) | |
821 | while desc.numseen < desc.n: | |
822 | # if there are no more arguments, return | |
823 | if not words: | |
824 | return matchcnt | |
825 | word = words.pop(0) | |
826 | ||
827 | try: | |
828 | # only allow partial matching if we're on the last supplied | |
829 | # word; avoid matching foo bar and foot bar just because | |
830 | # partial is set | |
831 | validate_one(word, desc, partial and (len(words) == 0)) | |
832 | valid = True | |
833 | except ArgumentError: | |
834 | # matchnum doesn't care about type of error | |
835 | valid = False | |
836 | ||
837 | if not valid: | |
838 | if not desc.req: | |
839 | # this wasn't required, so word may match the next desc | |
840 | words.insert(0, word) | |
841 | break | |
842 | else: | |
843 | # it was required, and didn't match, return | |
844 | return matchcnt | |
845 | if desc.req: | |
846 | matchcnt += 1 | |
847 | return matchcnt | |
848 | ||
849 | ||
850 | def get_next_arg(desc, args): | |
851 | ''' | |
852 | Get either the value matching key 'desc.name' or the next arg in | |
853 | the non-dict list. Return None if args are exhausted. Used in | |
854 | validate() below. | |
855 | ''' | |
856 | arg = None | |
857 | if isinstance(args, dict): | |
858 | arg = args.pop(desc.name, None) | |
859 | # allow 'param=param' to be expressed as 'param' | |
860 | if arg == '': | |
861 | arg = desc.name | |
862 | # Hack, or clever? If value is a list, keep the first element, | |
863 | # push rest back onto myargs for later processing. | |
864 | # Could process list directly, but nesting here is already bad | |
865 | if arg and isinstance(arg, list): | |
866 | args[desc.name] = arg[1:] | |
867 | arg = arg[0] | |
868 | elif args: | |
869 | arg = args.pop(0) | |
870 | if arg and isinstance(arg, list): | |
871 | args = arg[1:] + args | |
872 | arg = arg[0] | |
873 | return arg | |
874 | ||
875 | ||
876 | def store_arg(desc, d): | |
877 | ''' | |
878 | Store argument described by, and held in, thanks to valid(), | |
879 | desc into the dictionary d, keyed by desc.name. Three cases: | |
880 | ||
881 | 1) desc.N is set: value in d is a list | |
882 | 2) prefix: multiple args are joined with ' ' into one d{} item | |
883 | 3) single prefix or other arg: store as simple value | |
884 | ||
885 | Used in validate() below. | |
886 | ''' | |
887 | if desc.N: | |
888 | # value should be a list | |
889 | if desc.name in d: | |
890 | d[desc.name] += [desc.instance.val] | |
891 | else: | |
892 | d[desc.name] = [desc.instance.val] | |
893 | elif (desc.t == CephPrefix) and (desc.name in d): | |
894 | # prefixes' values should be a space-joined concatenation | |
895 | d[desc.name] += ' ' + desc.instance.val | |
896 | else: | |
897 | # if first CephPrefix or any other type, just set it | |
898 | d[desc.name] = desc.instance.val | |
899 | ||
900 | ||
901 | def validate(args, signature, flags=0, partial=False): | |
902 | """ | |
903 | validate(args, signature, flags=0, partial=False) | |
904 | ||
905 | args is a list of either words or k,v pairs representing a possible | |
906 | command input following format of signature. Runs a validation; no | |
907 | exception means it's OK. Return a dict containing all arguments keyed | |
908 | by their descriptor name, with duplicate args per name accumulated | |
909 | into a list (or space-separated value for CephPrefix). | |
910 | ||
911 | Mismatches of prefix are non-fatal, as this probably just means the | |
912 | search hasn't hit the correct command. Mismatches of non-prefix | |
913 | arguments are treated as fatal, and an exception raised. | |
914 | ||
915 | This matching is modified if partial is set: allow partial matching | |
916 | (with partial dict returned); in this case, there are no exceptions | |
917 | raised. | |
918 | """ | |
919 | ||
920 | myargs = copy.deepcopy(args) | |
921 | mysig = copy.deepcopy(signature) | |
922 | reqsiglen = len([desc for desc in mysig if desc.req]) | |
923 | matchcnt = 0 | |
924 | d = dict() | |
925 | save_exception = None | |
926 | ||
927 | for desc in mysig: | |
928 | setattr(desc, 'numseen', 0) | |
929 | while desc.numseen < desc.n: | |
930 | myarg = get_next_arg(desc, myargs) | |
931 | ||
932 | # no arg, but not required? Continue consuming mysig | |
933 | # in case there are later required args | |
31f18b77 | 934 | if myarg in (None, []) and not desc.req: |
7c673cae FG |
935 | break |
936 | ||
937 | # out of arguments for a required param? | |
938 | # Either return (if partial validation) or raise | |
31f18b77 | 939 | if myarg in (None, []) and desc.req: |
7c673cae FG |
940 | if desc.N and desc.numseen < 1: |
941 | # wanted N, didn't even get 1 | |
942 | if partial: | |
943 | return d | |
944 | raise ArgumentNumber( | |
945 | 'saw {0} of {1}, expected at least 1'. | |
946 | format(desc.numseen, desc) | |
947 | ) | |
948 | elif not desc.N and desc.numseen < desc.n: | |
949 | # wanted n, got too few | |
950 | if partial: | |
951 | return d | |
952 | # special-case the "0 expected 1" case | |
953 | if desc.numseen == 0 and desc.n == 1: | |
94b18763 | 954 | raise ArgumentMissing( |
7c673cae FG |
955 | 'missing required parameter {0}'.format(desc) |
956 | ) | |
957 | raise ArgumentNumber( | |
958 | 'saw {0} of {1}, expected {2}'. | |
959 | format(desc.numseen, desc, desc.n) | |
960 | ) | |
961 | break | |
962 | ||
963 | # Have an arg; validate it | |
964 | try: | |
965 | validate_one(myarg, desc) | |
966 | valid = True | |
967 | except ArgumentError as e: | |
968 | valid = False | |
969 | exc = e | |
970 | if not valid: | |
971 | # argument mismatch | |
972 | if not desc.req: | |
973 | # if not required, just push back; it might match | |
974 | # the next arg | |
975 | save_exception = [ myarg, exc ] | |
976 | myargs.insert(0, myarg) | |
977 | break | |
978 | else: | |
979 | # hm, it was required, so time to return/raise | |
980 | if partial: | |
981 | return d | |
982 | raise exc | |
983 | ||
984 | # Whew, valid arg acquired. Store in dict | |
985 | matchcnt += 1 | |
986 | store_arg(desc, d) | |
987 | # Clear prior exception | |
988 | save_exception = None | |
989 | ||
990 | # Done with entire list of argdescs | |
991 | if matchcnt < reqsiglen: | |
992 | raise ArgumentTooFew("not enough arguments given") | |
993 | ||
994 | if myargs and not partial: | |
995 | if save_exception: | |
996 | print(save_exception[0], 'not valid: ', save_exception[1], file=sys.stderr) | |
997 | raise ArgumentError("unused arguments: " + str(myargs)) | |
998 | ||
999 | if flags & FLAG_MGR: | |
1000 | d['target'] = ('mgr','') | |
1001 | ||
1002 | # Finally, success | |
1003 | return d | |
1004 | ||
1005 | ||
1006 | def cmdsiglen(sig): | |
1007 | sigdict = sig.values() | |
1008 | assert len(sigdict) == 1 | |
1009 | some_value = next(iter(sig.values())) | |
1010 | return len(some_value['sig']) | |
1011 | ||
1012 | ||
1013 | def validate_command(sigdict, args, verbose=False): | |
1014 | """ | |
1015 | turn args into a valid dictionary ready to be sent off as JSON, | |
1016 | validated against sigdict. | |
1017 | """ | |
1018 | if verbose: | |
1019 | print("validate_command: " + " ".join(args), file=sys.stderr) | |
1020 | found = [] | |
1021 | valid_dict = {} | |
1022 | if args: | |
1023 | # look for best match, accumulate possibles in bestcmds | |
1024 | # (so we can maybe give a more-useful error message) | |
1025 | best_match_cnt = 0 | |
1026 | bestcmds = [] | |
1027 | for cmdtag, cmd in sigdict.items(): | |
1028 | sig = cmd['sig'] | |
1029 | matched = matchnum(args, sig, partial=True) | |
1030 | if matched > best_match_cnt: | |
1031 | if verbose: | |
1032 | print("better match: {0} > {1}: {2}:{3} ".format( | |
1033 | matched, best_match_cnt, cmdtag, concise_sig(sig) | |
1034 | ), file=sys.stderr) | |
1035 | best_match_cnt = matched | |
1036 | bestcmds = [{cmdtag: cmd}] | |
1037 | elif matched == best_match_cnt: | |
1038 | if verbose: | |
1039 | print("equal match: {0} > {1}: {2}:{3} ".format( | |
1040 | matched, best_match_cnt, cmdtag, concise_sig(sig) | |
1041 | ), file=sys.stderr) | |
1042 | bestcmds.append({cmdtag: cmd}) | |
1043 | ||
1044 | # Sort bestcmds by number of args so we can try shortest first | |
1045 | # (relies on a cmdsig being key,val where val is a list of len 1) | |
1046 | bestcmds_sorted = sorted(bestcmds, key=cmdsiglen) | |
1047 | ||
1048 | if verbose: | |
1049 | print("bestcmds_sorted: ", file=sys.stderr) | |
1050 | pprint.PrettyPrinter(stream=sys.stderr).pprint(bestcmds_sorted) | |
1051 | ||
94b18763 | 1052 | e = None |
7c673cae FG |
1053 | # for everything in bestcmds, look for a true match |
1054 | for cmdsig in bestcmds_sorted: | |
1055 | for cmd in cmdsig.values(): | |
1056 | sig = cmd['sig'] | |
1057 | try: | |
1058 | valid_dict = validate(args, sig, flags=cmd.get('flags', 0)) | |
1059 | found = cmd | |
1060 | break | |
1061 | except ArgumentPrefix: | |
1062 | # ignore prefix mismatches; we just haven't found | |
1063 | # the right command yet | |
1064 | pass | |
94b18763 FG |
1065 | except ArgumentMissing as e: |
1066 | if len(bestcmds) == 1: | |
1067 | found = cmd | |
1068 | break | |
7c673cae FG |
1069 | except ArgumentTooFew: |
1070 | # It looked like this matched the beginning, but it | |
1071 | # didn't have enough args supplied. If we're out of | |
1072 | # cmdsigs we'll fall out unfound; if we're not, maybe | |
1073 | # the next one matches completely. Whine, but pass. | |
1074 | if verbose: | |
1075 | print('Not enough args supplied for ', | |
1076 | concise_sig(sig), file=sys.stderr) | |
1077 | except ArgumentError as e: | |
1078 | # Solid mismatch on an arg (type, range, etc.) | |
1079 | # Stop now, because we have the right command but | |
1080 | # some other input is invalid | |
94b18763 FG |
1081 | found = cmd |
1082 | break | |
1083 | if found or e: | |
7c673cae FG |
1084 | break |
1085 | ||
94b18763 FG |
1086 | if found: |
1087 | if not valid_dict: | |
1088 | print("Invalid command:", e, file=sys.stderr) | |
1089 | print(concise_sig(sig), ': ', cmd['help'], file=sys.stderr) | |
1090 | else: | |
1091 | bestcmds = bestcmds[:10] | |
1092 | print('no valid command found; {0} closest matches:'.format(len(bestcmds)), file=sys.stderr) | |
1093 | for cmdsig in bestcmds: | |
7c673cae FG |
1094 | for (cmdtag, cmd) in cmdsig.items(): |
1095 | print(concise_sig(cmd['sig']), file=sys.stderr) | |
7c673cae FG |
1096 | return valid_dict |
1097 | ||
1098 | ||
94b18763 | 1099 | |
7c673cae FG |
1100 | def find_cmd_target(childargs): |
1101 | """ | |
1102 | Using a minimal validation, figure out whether the command | |
1103 | should be sent to a monitor or an osd. We do this before even | |
1104 | asking for the 'real' set of command signatures, so we can ask the | |
1105 | right daemon. | |
1106 | Returns ('osd', osdid), ('pg', pgid), ('mgr', '') or ('mon', '') | |
1107 | """ | |
1108 | sig = parse_funcsig(['tell', {'name': 'target', 'type': 'CephName'}]) | |
1109 | try: | |
1110 | valid_dict = validate(childargs, sig, partial=True) | |
1111 | except ArgumentError: | |
1112 | pass | |
1113 | else: | |
1114 | if len(valid_dict) == 2: | |
1115 | # revalidate to isolate type and id | |
1116 | name = CephName() | |
1117 | # if this fails, something is horribly wrong, as it just | |
1118 | # validated successfully above | |
1119 | name.valid(valid_dict['target']) | |
1120 | return name.nametype, name.nameid | |
1121 | ||
1122 | sig = parse_funcsig(['tell', {'name': 'pgid', 'type': 'CephPgid'}]) | |
1123 | try: | |
1124 | valid_dict = validate(childargs, sig, partial=True) | |
1125 | except ArgumentError: | |
1126 | pass | |
1127 | else: | |
1128 | if len(valid_dict) == 2: | |
1129 | # pg doesn't need revalidation; the string is fine | |
1130 | return 'pg', valid_dict['pgid'] | |
1131 | ||
1132 | # If we reached this far it must mean that so far we've been unable to | |
1133 | # obtain a proper target from childargs. This may mean that we are not | |
1134 | # dealing with a 'tell' command, or that the specified target is invalid. | |
1135 | # If the latter, we likely were unable to catch it because we were not | |
1136 | # really looking for it: first we tried to parse a 'CephName' (osd, mon, | |
1137 | # mds, followed by and id); given our failure to parse, we tried to parse | |
1138 | # a 'CephPgid' instead (e.g., 0.4a). Considering we got this far though | |
1139 | # we were unable to do so. | |
1140 | # | |
1141 | # We will now check if this is a tell and, if so, forcefully validate the | |
1142 | # target as a 'CephName'. This must be so because otherwise we will end | |
1143 | # up sending garbage to a monitor, which is the default target when a | |
1144 | # target is not explicitly specified. | |
1145 | # e.g., | |
1146 | # 'ceph status' -> target is any one monitor | |
1147 | # 'ceph tell mon.* status -> target is all monitors | |
1148 | # 'ceph tell foo status -> target is invalid! | |
1149 | if len(childargs) > 1 and childargs[0] == 'tell': | |
1150 | name = CephName() | |
1151 | # CephName.valid() raises on validation error; find_cmd_target()'s | |
1152 | # caller should handle them | |
1153 | name.valid(childargs[1]) | |
1154 | return name.nametype, name.nameid | |
1155 | ||
1156 | sig = parse_funcsig(['pg', {'name': 'pgid', 'type': 'CephPgid'}]) | |
1157 | try: | |
1158 | valid_dict = validate(childargs, sig, partial=True) | |
1159 | except ArgumentError: | |
1160 | pass | |
1161 | else: | |
1162 | if len(valid_dict) == 2: | |
1163 | return 'pg', valid_dict['pgid'] | |
1164 | ||
1165 | return 'mon', '' | |
1166 | ||
1167 | ||
1168 | class RadosThread(threading.Thread): | |
1169 | def __init__(self, target, *args, **kwargs): | |
1170 | self.args = args | |
1171 | self.kwargs = kwargs | |
1172 | self.target = target | |
1173 | self.exception = None | |
1174 | threading.Thread.__init__(self) | |
1175 | ||
1176 | def run(self): | |
1177 | try: | |
1178 | self.retval = self.target(*self.args, **self.kwargs) | |
1179 | except Exception as e: | |
1180 | self.exception = e | |
1181 | ||
1182 | ||
1183 | # time in seconds between each call to t.join() for child thread | |
1184 | POLL_TIME_INCR = 0.5 | |
1185 | ||
1186 | ||
1187 | def run_in_thread(target, *args, **kwargs): | |
1188 | interrupt = False | |
1189 | timeout = kwargs.pop('timeout', 0) | |
1190 | countdown = timeout | |
1191 | t = RadosThread(target, *args, **kwargs) | |
1192 | ||
1193 | # allow the main thread to exit (presumably, avoid a join() on this | |
1194 | # subthread) before this thread terminates. This allows SIGINT | |
1195 | # exit of a blocked call. See below. | |
1196 | t.daemon = True | |
1197 | ||
1198 | t.start() | |
1199 | try: | |
1200 | # poll for thread exit | |
1201 | while t.is_alive(): | |
1202 | t.join(POLL_TIME_INCR) | |
1203 | if timeout and t.is_alive(): | |
1204 | countdown = countdown - POLL_TIME_INCR | |
1205 | if countdown <= 0: | |
1206 | raise KeyboardInterrupt | |
1207 | ||
1208 | t.join() # in case t exits before reaching the join() above | |
1209 | except KeyboardInterrupt: | |
1210 | # ..but allow SIGINT to terminate the waiting. Note: this | |
1211 | # relies on the Linux kernel behavior of delivering the signal | |
1212 | # to the main thread in preference to any subthread (all that's | |
1213 | # strictly guaranteed is that *some* thread that has the signal | |
1214 | # unblocked will receive it). But there doesn't seem to be | |
1215 | # any interface to create t with SIGINT blocked. | |
1216 | interrupt = True | |
1217 | ||
1218 | if interrupt: | |
1219 | t.retval = -errno.EINTR, None, 'Interrupted!' | |
1220 | if t.exception: | |
1221 | raise t.exception | |
1222 | return t.retval | |
1223 | ||
1224 | ||
1225 | def send_command_retry(*args, **kwargs): | |
1226 | while True: | |
1227 | try: | |
1228 | return send_command(*args, **kwargs) | |
1229 | except Exception as e: | |
1230 | if ('get_command_descriptions' in str(e) and | |
1231 | 'object in state configuring' in str(e)): | |
1232 | continue | |
1233 | else: | |
1234 | raise | |
1235 | ||
1236 | def send_command(cluster, target=('mon', ''), cmd=None, inbuf=b'', timeout=0, | |
1237 | verbose=False): | |
1238 | """ | |
1239 | Send a command to a daemon using librados's | |
1240 | mon_command, osd_command, or pg_command. Any bulk input data | |
1241 | comes in inbuf. | |
1242 | ||
1243 | Returns (ret, outbuf, outs); ret is the return code, outbuf is | |
1244 | the outbl "bulk useful output" buffer, and outs is any status | |
1245 | or error message (intended for stderr). | |
1246 | ||
1247 | If target is osd.N, send command to that osd (except for pgid cmds) | |
1248 | """ | |
1249 | cmd = cmd or [] | |
1250 | try: | |
1251 | if target[0] == 'osd': | |
1252 | osdid = target[1] | |
1253 | ||
1254 | if verbose: | |
1255 | print('submit {0} to osd.{1}'.format(cmd, osdid), | |
1256 | file=sys.stderr) | |
1257 | ret, outbuf, outs = run_in_thread( | |
1258 | cluster.osd_command, osdid, cmd, inbuf, timeout) | |
1259 | ||
1260 | elif target[0] == 'mgr': | |
1261 | ret, outbuf, outs = run_in_thread( | |
1262 | cluster.mgr_command, cmd, inbuf, timeout) | |
1263 | ||
1264 | elif target[0] == 'pg': | |
1265 | pgid = target[1] | |
1266 | # pgid will already be in the command for the pg <pgid> | |
1267 | # form, but for tell <pgid>, we need to put it in | |
1268 | if cmd: | |
1269 | cmddict = json.loads(cmd[0]) | |
1270 | cmddict['pgid'] = pgid | |
1271 | else: | |
1272 | cmddict = dict(pgid=pgid) | |
1273 | cmd = [json.dumps(cmddict)] | |
1274 | if verbose: | |
1275 | print('submit {0} for pgid {1}'.format(cmd, pgid), | |
1276 | file=sys.stderr) | |
1277 | ret, outbuf, outs = run_in_thread( | |
1278 | cluster.pg_command, pgid, cmd, inbuf, timeout) | |
1279 | ||
1280 | elif target[0] == 'mon': | |
1281 | if verbose: | |
1282 | print('{0} to {1}'.format(cmd, target[0]), | |
1283 | file=sys.stderr) | |
31f18b77 | 1284 | if len(target) < 2 or target[1] == '': |
7c673cae FG |
1285 | ret, outbuf, outs = run_in_thread( |
1286 | cluster.mon_command, cmd, inbuf, timeout) | |
1287 | else: | |
1288 | ret, outbuf, outs = run_in_thread( | |
1289 | cluster.mon_command, cmd, inbuf, timeout, target[1]) | |
1290 | elif target[0] == 'mds': | |
1291 | mds_spec = target[1] | |
1292 | ||
1293 | if verbose: | |
1294 | print('submit {0} to mds.{1}'.format(cmd, mds_spec), | |
1295 | file=sys.stderr) | |
1296 | ||
1297 | try: | |
1298 | from cephfs import LibCephFS | |
1299 | except ImportError: | |
1300 | raise RuntimeError("CephFS unavailable, have you installed libcephfs?") | |
1301 | ||
b32b8144 | 1302 | filesystem = LibCephFS(rados_inst=cluster) |
7c673cae FG |
1303 | filesystem.init() |
1304 | ret, outbuf, outs = \ | |
1305 | filesystem.mds_command(mds_spec, cmd, inbuf) | |
1306 | filesystem.shutdown() | |
1307 | else: | |
1308 | raise ArgumentValid("Bad target type '{0}'".format(target[0])) | |
1309 | ||
1310 | except Exception as e: | |
1311 | if not isinstance(e, ArgumentError): | |
1312 | raise RuntimeError('"{0}": exception {1}'.format(cmd, e)) | |
1313 | else: | |
1314 | raise | |
1315 | ||
1316 | return ret, outbuf, outs | |
1317 | ||
1318 | ||
1319 | def json_command(cluster, target=('mon', ''), prefix=None, argdict=None, | |
1320 | inbuf=b'', timeout=0, verbose=False): | |
1321 | """ | |
1322 | Format up a JSON command and send it with send_command() above. | |
1323 | Prefix may be supplied separately or in argdict. Any bulk input | |
1324 | data comes in inbuf. | |
1325 | ||
1326 | If target is osd.N, send command to that osd (except for pgid cmds) | |
1327 | """ | |
1328 | cmddict = {} | |
1329 | if prefix: | |
1330 | cmddict.update({'prefix': prefix}) | |
1331 | if argdict: | |
1332 | cmddict.update(argdict) | |
1333 | if 'target' in argdict: | |
1334 | target = argdict.get('target') | |
1335 | ||
1336 | # grab prefix for error messages | |
1337 | prefix = cmddict['prefix'] | |
1338 | ||
1339 | try: | |
1340 | if target[0] == 'osd': | |
1341 | osdtarg = CephName() | |
1342 | osdtarget = '{0}.{1}'.format(*target) | |
1343 | # prefer target from cmddict if present and valid | |
1344 | if 'target' in cmddict: | |
1345 | osdtarget = cmddict.pop('target') | |
1346 | try: | |
1347 | osdtarg.valid(osdtarget) | |
1348 | target = ('osd', osdtarg.nameid) | |
1349 | except: | |
1350 | # use the target we were originally given | |
1351 | pass | |
1352 | ||
1353 | ret, outbuf, outs = send_command_retry(cluster, | |
1354 | target, [json.dumps(cmddict)], | |
1355 | inbuf, timeout, verbose) | |
1356 | ||
1357 | except Exception as e: | |
1358 | if not isinstance(e, ArgumentError): | |
1359 | raise RuntimeError('"{0}": exception {1}'.format(argdict, e)) | |
1360 | else: | |
1361 | raise | |
1362 | ||
1363 | return ret, outbuf, outs |