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