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