]>
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 | ||
9f95a23c | 10 | LGPL-2.1 or LGPL-3.0. 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: | |
9f95a23c | 697 | s += '...' |
7c673cae | 698 | if not self.req: |
9f95a23c | 699 | s = '[' + s + ']' |
7c673cae FG |
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 | """ | |
9f95a23c | 707 | if self.t == CephBool: |
11fdf7f2 | 708 | chunk = "--{0}".format(self.name.replace("_", "-")) |
9f95a23c | 709 | elif self.t == CephPrefix or self.t == CephChoices: |
7c673cae | 710 | chunk = str(self.instance) |
9f95a23c TL |
711 | elif self.t == CephOsdName: |
712 | # it just so happens all CephOsdName commands are named 'id' anyway, | |
713 | # so <id|osd.id> is perfect. | |
714 | chunk = '<id|osd.id>' | |
715 | elif self.t == CephName: | |
716 | # CephName commands similarly only have one arg of the | |
717 | # type, so <type.id> is good. | |
718 | chunk = '<type.id>' | |
719 | elif self.t == CephInt: | |
720 | chunk = '<{0}:int>'.format(self.name) | |
721 | elif self.t == CephFloat: | |
722 | chunk = '<{0}:float>'.format(self.name) | |
723 | else: | |
724 | chunk = '<{0}>'.format(self.name) | |
7c673cae FG |
725 | s = chunk |
726 | if self.N: | |
9f95a23c | 727 | s += '...' |
7c673cae | 728 | if not self.req: |
9f95a23c | 729 | s = '[' + s + ']' |
7c673cae FG |
730 | return s |
731 | ||
732 | def complete(self, s): | |
733 | return self.instance.complete(s) | |
734 | ||
735 | ||
736 | def concise_sig(sig): | |
737 | """ | |
738 | Return string representation of sig useful for syntax reference in help | |
739 | """ | |
740 | return ' '.join([d.helpstr() for d in sig]) | |
741 | ||
742 | ||
743 | def descsort_key(sh): | |
744 | """ | |
745 | sort descriptors by prefixes, defined as the concatenation of all simple | |
746 | strings in the descriptor; this works out to just the leading strings. | |
747 | """ | |
748 | return concise_sig(sh['sig']) | |
749 | ||
750 | ||
751 | def descsort(sh1, sh2): | |
752 | """ | |
753 | Deprecated; use (key=descsort_key) instead of (cmp=descsort) | |
754 | """ | |
755 | return cmp(descsort_key(sh1), descsort_key(sh2)) | |
756 | ||
757 | ||
758 | def parse_funcsig(sig): | |
759 | """ | |
760 | parse a single descriptor (array of strings or dicts) into a | |
761 | dict of function descriptor/validators (objects of CephXXX type) | |
11fdf7f2 TL |
762 | |
763 | :returns: list of ``argdesc`` | |
7c673cae FG |
764 | """ |
765 | newsig = [] | |
766 | argnum = 0 | |
767 | for desc in sig: | |
768 | argnum += 1 | |
769 | if isinstance(desc, basestring): | |
770 | t = CephPrefix | |
771 | desc = {'type': t, 'name': 'prefix', 'prefix': desc} | |
772 | else: | |
773 | # not a simple string, must be dict | |
774 | if 'type' not in desc: | |
775 | s = 'JSON descriptor {0} has no type'.format(sig) | |
776 | raise JsonFormat(s) | |
777 | # look up type string in our globals() dict; if it's an | |
778 | # object of type `type`, it must be a | |
779 | # locally-defined class. otherwise, we haven't a clue. | |
780 | if desc['type'] in globals(): | |
781 | t = globals()[desc['type']] | |
782 | if not isinstance(t, type): | |
783 | s = 'unknown type {0}'.format(desc['type']) | |
784 | raise JsonFormat(s) | |
785 | else: | |
786 | s = 'unknown type {0}'.format(desc['type']) | |
787 | raise JsonFormat(s) | |
788 | ||
789 | kwargs = dict() | |
790 | for key, val in desc.items(): | |
791 | if key not in ['type', 'name', 'n', 'req']: | |
792 | kwargs[key] = val | |
793 | newsig.append(argdesc(t, | |
794 | name=desc.get('name', None), | |
795 | n=desc.get('n', 1), | |
796 | req=desc.get('req', True), | |
797 | **kwargs)) | |
798 | return newsig | |
799 | ||
800 | ||
801 | def parse_json_funcsigs(s, consumer): | |
802 | """ | |
803 | A function signature is mostly an array of argdesc; it's represented | |
804 | in JSON as | |
805 | { | |
806 | "cmd001": {"sig":[ "type": type, "name": name, "n": num, "req":true|false <other param>], "help":helptext, "module":modulename, "perm":perms, "avail":availability} | |
807 | . | |
808 | . | |
809 | . | |
810 | ] | |
811 | ||
812 | A set of sigs is in an dict mapped by a unique number: | |
813 | { | |
814 | "cmd1": { | |
815 | "sig": ["type.. ], "help":helptext... | |
816 | } | |
817 | "cmd2"{ | |
818 | "sig": [.. ], "help":helptext... | |
819 | } | |
820 | } | |
821 | ||
822 | Parse the string s and return a dict of dicts, keyed by opcode; | |
823 | each dict contains 'sig' with the array of descriptors, and 'help' | |
824 | with the helptext, 'module' with the module name, 'perm' with a | |
825 | string representing required permissions in that module to execute | |
826 | this command (and also whether it is a read or write command from | |
827 | the cluster state perspective), and 'avail' as a hint for | |
828 | whether the command should be advertised by CLI, REST, or both. | |
829 | If avail does not contain 'consumer', don't include the command | |
830 | in the returned dict. | |
831 | """ | |
832 | try: | |
833 | overall = json.loads(s) | |
834 | except Exception as e: | |
835 | print("Couldn't parse JSON {0}: {1}".format(s, e), file=sys.stderr) | |
836 | raise e | |
837 | sigdict = {} | |
838 | for cmdtag, cmd in overall.items(): | |
839 | if 'sig' not in cmd: | |
840 | s = "JSON descriptor {0} has no 'sig'".format(cmdtag) | |
841 | raise JsonFormat(s) | |
842 | # check 'avail' and possibly ignore this command | |
843 | if 'avail' in cmd: | |
844 | if consumer not in cmd['avail']: | |
845 | continue | |
846 | # rewrite the 'sig' item with the argdesc-ized version, and... | |
847 | cmd['sig'] = parse_funcsig(cmd['sig']) | |
848 | # just take everything else as given | |
849 | sigdict[cmdtag] = cmd | |
850 | return sigdict | |
851 | ||
852 | ||
853 | def validate_one(word, desc, partial=False): | |
854 | """ | |
855 | validate_one(word, desc, partial=False) | |
856 | ||
857 | validate word against the constructed instance of the type | |
858 | in desc. May raise exception. If it returns false (and doesn't | |
859 | raise an exception), desc.instance.val will | |
860 | contain the validated value (in the appropriate type). | |
861 | """ | |
862 | desc.instance.valid(word, partial) | |
863 | desc.numseen += 1 | |
864 | if desc.N: | |
865 | desc.n = desc.numseen + 1 | |
866 | ||
867 | ||
868 | def matchnum(args, signature, partial=False): | |
869 | """ | |
870 | matchnum(s, signature, partial=False) | |
871 | ||
872 | Returns number of arguments matched in s against signature. | |
873 | Can be used to determine most-likely command for full or partial | |
874 | matches (partial applies to string matches). | |
875 | """ | |
876 | words = args[:] | |
877 | mysig = copy.deepcopy(signature) | |
878 | matchcnt = 0 | |
879 | for desc in mysig: | |
11fdf7f2 | 880 | desc.numseen = 0 |
7c673cae FG |
881 | while desc.numseen < desc.n: |
882 | # if there are no more arguments, return | |
883 | if not words: | |
884 | return matchcnt | |
885 | word = words.pop(0) | |
886 | ||
887 | try: | |
888 | # only allow partial matching if we're on the last supplied | |
889 | # word; avoid matching foo bar and foot bar just because | |
890 | # partial is set | |
891 | validate_one(word, desc, partial and (len(words) == 0)) | |
892 | valid = True | |
893 | except ArgumentError: | |
894 | # matchnum doesn't care about type of error | |
895 | valid = False | |
896 | ||
897 | if not valid: | |
898 | if not desc.req: | |
899 | # this wasn't required, so word may match the next desc | |
900 | words.insert(0, word) | |
901 | break | |
902 | else: | |
903 | # it was required, and didn't match, return | |
904 | return matchcnt | |
905 | if desc.req: | |
906 | matchcnt += 1 | |
907 | return matchcnt | |
908 | ||
909 | ||
7c673cae FG |
910 | def store_arg(desc, d): |
911 | ''' | |
912 | Store argument described by, and held in, thanks to valid(), | |
913 | desc into the dictionary d, keyed by desc.name. Three cases: | |
914 | ||
915 | 1) desc.N is set: value in d is a list | |
916 | 2) prefix: multiple args are joined with ' ' into one d{} item | |
917 | 3) single prefix or other arg: store as simple value | |
918 | ||
919 | Used in validate() below. | |
920 | ''' | |
921 | if desc.N: | |
922 | # value should be a list | |
923 | if desc.name in d: | |
924 | d[desc.name] += [desc.instance.val] | |
925 | else: | |
926 | d[desc.name] = [desc.instance.val] | |
927 | elif (desc.t == CephPrefix) and (desc.name in d): | |
928 | # prefixes' values should be a space-joined concatenation | |
929 | d[desc.name] += ' ' + desc.instance.val | |
930 | else: | |
931 | # if first CephPrefix or any other type, just set it | |
932 | d[desc.name] = desc.instance.val | |
933 | ||
934 | ||
935 | def validate(args, signature, flags=0, partial=False): | |
936 | """ | |
937 | validate(args, signature, flags=0, partial=False) | |
938 | ||
11fdf7f2 | 939 | args is a list of strings representing a possible |
7c673cae FG |
940 | command input following format of signature. Runs a validation; no |
941 | exception means it's OK. Return a dict containing all arguments keyed | |
942 | by their descriptor name, with duplicate args per name accumulated | |
943 | into a list (or space-separated value for CephPrefix). | |
944 | ||
945 | Mismatches of prefix are non-fatal, as this probably just means the | |
946 | search hasn't hit the correct command. Mismatches of non-prefix | |
947 | arguments are treated as fatal, and an exception raised. | |
948 | ||
949 | This matching is modified if partial is set: allow partial matching | |
950 | (with partial dict returned); in this case, there are no exceptions | |
951 | raised. | |
952 | """ | |
953 | ||
954 | myargs = copy.deepcopy(args) | |
955 | mysig = copy.deepcopy(signature) | |
956 | reqsiglen = len([desc for desc in mysig if desc.req]) | |
957 | matchcnt = 0 | |
958 | d = dict() | |
959 | save_exception = None | |
960 | ||
11fdf7f2 TL |
961 | arg_descs_by_name = dict([desc.name, desc] for desc in mysig |
962 | if desc.t != CephPrefix) | |
963 | ||
964 | # Special case: detect "injectargs" (legacy way of modifying daemon | |
965 | # configs) and permit "--" string arguments if so. | |
966 | injectargs = myargs and myargs[0] == "injectargs" | |
967 | ||
968 | # Make a pass through all arguments | |
7c673cae | 969 | for desc in mysig: |
11fdf7f2 TL |
970 | desc.numseen = 0 |
971 | ||
7c673cae | 972 | while desc.numseen < desc.n: |
11fdf7f2 TL |
973 | if myargs: |
974 | myarg = myargs.pop(0) | |
975 | else: | |
976 | myarg = None | |
7c673cae FG |
977 | |
978 | # no arg, but not required? Continue consuming mysig | |
979 | # in case there are later required args | |
31f18b77 | 980 | if myarg in (None, []) and not desc.req: |
7c673cae FG |
981 | break |
982 | ||
11fdf7f2 TL |
983 | # A keyword argument? |
984 | if myarg: | |
985 | # argdesc for the keyword argument, if we find one | |
986 | kwarg_desc = None | |
987 | ||
988 | # Track whether we need to push value back onto | |
989 | # myargs in the case that this isn't a valid k=v | |
990 | consumed_next = False | |
991 | ||
992 | # Try both styles of keyword argument | |
993 | kwarg_match = re.match(KWARG_EQUALS, myarg) | |
994 | if kwarg_match: | |
995 | # We have a "--foo=bar" style argument | |
996 | kwarg_k, kwarg_v = kwarg_match.groups() | |
997 | ||
998 | # Either "--foo-bar" or "--foo_bar" style is accepted | |
999 | kwarg_k = kwarg_k.replace('-', '_') | |
1000 | ||
1001 | kwarg_desc = arg_descs_by_name.get(kwarg_k, None) | |
1002 | else: | |
1003 | # Maybe this is a "--foo bar" or "--bool" style argument | |
1004 | key_match = re.match(KWARG_SPACE, myarg) | |
1005 | if key_match: | |
1006 | kwarg_k = key_match.group(1) | |
1007 | ||
1008 | # Permit --foo-bar=123 form or --foo_bar=123 form, | |
1009 | # assuming all command definitions use foo_bar argument | |
1010 | # naming style | |
1011 | kwarg_k = kwarg_k.replace('-', '_') | |
1012 | ||
1013 | kwarg_desc = arg_descs_by_name.get(kwarg_k, None) | |
1014 | if kwarg_desc: | |
1015 | if kwarg_desc.t == CephBool: | |
1016 | kwarg_v = 'true' | |
1017 | elif len(myargs): # Some trailing arguments exist | |
1018 | kwarg_v = myargs.pop(0) | |
1019 | else: | |
1020 | # Forget it, this is not a valid kwarg | |
1021 | kwarg_desc = None | |
1022 | ||
1023 | if kwarg_desc: | |
1024 | validate_one(kwarg_v, kwarg_desc) | |
9f95a23c | 1025 | matchcnt += 1 |
11fdf7f2 TL |
1026 | store_arg(kwarg_desc, d) |
1027 | continue | |
1028 | ||
1029 | # Don't handle something as a positional argument if it | |
1030 | # has a leading "--" unless it's a CephChoices (used for | |
1031 | # "--yes-i-really-mean-it") | |
1032 | if myarg and myarg.startswith("--"): | |
1033 | # Special cases for instances of confirmation flags | |
1034 | # that were defined as CephString/CephChoices instead of CephBool | |
1035 | # in pre-nautilus versions of Ceph daemons. | |
1036 | is_value = desc.t == CephChoices \ | |
1037 | or myarg == "--yes-i-really-mean-it" \ | |
1038 | or myarg == "--yes-i-really-really-mean-it" \ | |
1039 | or myarg == "--yes-i-really-really-mean-it-not-faking" \ | |
1040 | or myarg == "--force" \ | |
1041 | or injectargs | |
1042 | ||
1043 | if not is_value: | |
1044 | # Didn't get caught by kwarg handling, but has a "--", so | |
1045 | # we must assume it's something invalid, to avoid naively | |
1046 | # passing through mis-typed options as the values of | |
1047 | # positional arguments. | |
1048 | raise ArgumentValid("Unexpected argument '{0}'".format( | |
1049 | myarg)) | |
1050 | ||
7c673cae FG |
1051 | # out of arguments for a required param? |
1052 | # Either return (if partial validation) or raise | |
31f18b77 | 1053 | if myarg in (None, []) and desc.req: |
7c673cae FG |
1054 | if desc.N and desc.numseen < 1: |
1055 | # wanted N, didn't even get 1 | |
1056 | if partial: | |
1057 | return d | |
1058 | raise ArgumentNumber( | |
1059 | 'saw {0} of {1}, expected at least 1'. | |
1060 | format(desc.numseen, desc) | |
1061 | ) | |
1062 | elif not desc.N and desc.numseen < desc.n: | |
1063 | # wanted n, got too few | |
1064 | if partial: | |
1065 | return d | |
1066 | # special-case the "0 expected 1" case | |
1067 | if desc.numseen == 0 and desc.n == 1: | |
94b18763 | 1068 | raise ArgumentMissing( |
7c673cae FG |
1069 | 'missing required parameter {0}'.format(desc) |
1070 | ) | |
1071 | raise ArgumentNumber( | |
1072 | 'saw {0} of {1}, expected {2}'. | |
1073 | format(desc.numseen, desc, desc.n) | |
1074 | ) | |
1075 | break | |
1076 | ||
1077 | # Have an arg; validate it | |
1078 | try: | |
1079 | validate_one(myarg, desc) | |
1080 | valid = True | |
1081 | except ArgumentError as e: | |
1082 | valid = False | |
11fdf7f2 | 1083 | |
7c673cae FG |
1084 | # argument mismatch |
1085 | if not desc.req: | |
1086 | # if not required, just push back; it might match | |
1087 | # the next arg | |
11fdf7f2 | 1088 | save_exception = [ myarg, e ] |
7c673cae FG |
1089 | myargs.insert(0, myarg) |
1090 | break | |
1091 | else: | |
1092 | # hm, it was required, so time to return/raise | |
1093 | if partial: | |
1094 | return d | |
11fdf7f2 | 1095 | raise |
7c673cae FG |
1096 | |
1097 | # Whew, valid arg acquired. Store in dict | |
1098 | matchcnt += 1 | |
1099 | store_arg(desc, d) | |
1100 | # Clear prior exception | |
1101 | save_exception = None | |
1102 | ||
1103 | # Done with entire list of argdescs | |
1104 | if matchcnt < reqsiglen: | |
1105 | raise ArgumentTooFew("not enough arguments given") | |
1106 | ||
1107 | if myargs and not partial: | |
1108 | if save_exception: | |
1109 | print(save_exception[0], 'not valid: ', save_exception[1], file=sys.stderr) | |
1110 | raise ArgumentError("unused arguments: " + str(myargs)) | |
1111 | ||
11fdf7f2 | 1112 | if flags & Flag.MGR: |
9f95a23c | 1113 | d['target'] = ('mon-mgr','') |
7c673cae | 1114 | |
11fdf7f2 TL |
1115 | if flags & Flag.POLL: |
1116 | d['poll'] = True | |
1117 | ||
7c673cae FG |
1118 | # Finally, success |
1119 | return d | |
1120 | ||
1121 | ||
7c673cae FG |
1122 | def validate_command(sigdict, args, verbose=False): |
1123 | """ | |
11fdf7f2 TL |
1124 | Parse positional arguments into a parameter dict, according to |
1125 | the command descriptions. | |
1126 | ||
9f95a23c | 1127 | Writes advice about nearly-matching commands ``sys.stderr`` if |
11fdf7f2 TL |
1128 | the arguments do not match any command. |
1129 | ||
1130 | :param sigdict: A command description dictionary, as returned | |
1131 | from Ceph daemons by the get_command_descriptions | |
1132 | command. | |
1133 | :param args: List of strings, should match one of the command | |
1134 | signatures in ``sigdict`` | |
1135 | ||
1136 | :returns: A dict of parsed parameters (including ``prefix``), | |
1137 | or an empty dict if the args did not match any signature | |
7c673cae FG |
1138 | """ |
1139 | if verbose: | |
1140 | print("validate_command: " + " ".join(args), file=sys.stderr) | |
1141 | found = [] | |
1142 | valid_dict = {} | |
7c673cae | 1143 | |
11fdf7f2 TL |
1144 | # look for best match, accumulate possibles in bestcmds |
1145 | # (so we can maybe give a more-useful error message) | |
1146 | best_match_cnt = 0 | |
1147 | bestcmds = [] | |
1148 | for cmd in sigdict.values(): | |
1149 | flags = cmd.get('flags', 0) | |
1150 | if flags & Flag.OBSOLETE: | |
1151 | continue | |
1152 | sig = cmd['sig'] | |
1153 | matched = matchnum(args, sig, partial=True) | |
1154 | if (matched >= math.floor(best_match_cnt) and | |
1155 | matched == matchnum(args, sig, partial=False)): | |
1156 | # prefer those fully matched over partial patch | |
1157 | matched += 0.5 | |
1158 | if matched < best_match_cnt: | |
1159 | continue | |
7c673cae | 1160 | if verbose: |
11fdf7f2 TL |
1161 | print("better match: {0} > {1}: {2} ".format( |
1162 | matched, best_match_cnt, concise_sig(sig) | |
1163 | ), file=sys.stderr) | |
1164 | if matched > best_match_cnt: | |
1165 | best_match_cnt = matched | |
1166 | bestcmds = [cmd] | |
94b18763 | 1167 | else: |
11fdf7f2 TL |
1168 | bestcmds.append(cmd) |
1169 | ||
9f95a23c | 1170 | # Sort bestcmds by number of req args so we can try shortest first |
11fdf7f2 | 1171 | # (relies on a cmdsig being key,val where val is a list of len 1) |
11fdf7f2 | 1172 | |
9f95a23c TL |
1173 | def grade(cmd): |
1174 | # prefer optional arguments over required ones | |
1175 | sigs = cmd['sig'] | |
1176 | return sum(map(lambda sig: sig.req, sigs)) | |
1177 | ||
1178 | bestcmds_sorted = sorted(bestcmds, key=grade) | |
11fdf7f2 TL |
1179 | if verbose: |
1180 | print("bestcmds_sorted: ", file=sys.stderr) | |
1181 | pprint.PrettyPrinter(stream=sys.stderr).pprint(bestcmds_sorted) | |
1182 | ||
1183 | ex = None | |
1184 | # for everything in bestcmds, look for a true match | |
1185 | for cmd in bestcmds_sorted: | |
1186 | sig = cmd['sig'] | |
1187 | try: | |
1188 | valid_dict = validate(args, sig, flags=cmd.get('flags', 0)) | |
1189 | found = cmd | |
1190 | break | |
1191 | except ArgumentPrefix: | |
1192 | # ignore prefix mismatches; we just haven't found | |
1193 | # the right command yet | |
1194 | pass | |
1195 | except ArgumentMissing as e: | |
1196 | ex = e | |
1197 | if len(bestcmds) == 1: | |
1198 | found = cmd | |
1199 | break | |
1200 | except ArgumentTooFew: | |
1201 | # It looked like this matched the beginning, but it | |
1202 | # didn't have enough args supplied. If we're out of | |
1203 | # cmdsigs we'll fall out unfound; if we're not, maybe | |
1204 | # the next one matches completely. Whine, but pass. | |
1205 | if verbose: | |
1206 | print('Not enough args supplied for ', | |
1207 | concise_sig(sig), file=sys.stderr) | |
1208 | except ArgumentError as e: | |
1209 | ex = e | |
1210 | # Solid mismatch on an arg (type, range, etc.) | |
1211 | # Stop now, because we have the right command but | |
1212 | # some other input is invalid | |
1213 | found = cmd | |
1214 | break | |
1215 | ||
1216 | if found: | |
1217 | if not valid_dict: | |
1218 | print("Invalid command:", ex, file=sys.stderr) | |
1219 | print(concise_sig(sig), ': ', cmd['help'], file=sys.stderr) | |
1220 | else: | |
1221 | bestcmds = [c for c in bestcmds | |
1222 | if not c.get('flags', 0) & (Flag.DEPRECATED | Flag.HIDDEN)] | |
1223 | bestcmds = bestcmds[:10] # top 10 | |
1224 | print('no valid command found; {0} closest matches:'.format(len(bestcmds)), file=sys.stderr) | |
1225 | for cmd in bestcmds: | |
1226 | print(concise_sig(cmd['sig']), file=sys.stderr) | |
1227 | return valid_dict | |
7c673cae FG |
1228 | |
1229 | ||
94b18763 | 1230 | |
7c673cae FG |
1231 | def find_cmd_target(childargs): |
1232 | """ | |
1233 | Using a minimal validation, figure out whether the command | |
1234 | should be sent to a monitor or an osd. We do this before even | |
1235 | asking for the 'real' set of command signatures, so we can ask the | |
1236 | right daemon. | |
1237 | Returns ('osd', osdid), ('pg', pgid), ('mgr', '') or ('mon', '') | |
1238 | """ | |
1239 | sig = parse_funcsig(['tell', {'name': 'target', 'type': 'CephName'}]) | |
1240 | try: | |
1241 | valid_dict = validate(childargs, sig, partial=True) | |
1242 | except ArgumentError: | |
1243 | pass | |
1244 | else: | |
1245 | if len(valid_dict) == 2: | |
1246 | # revalidate to isolate type and id | |
1247 | name = CephName() | |
1248 | # if this fails, something is horribly wrong, as it just | |
1249 | # validated successfully above | |
1250 | name.valid(valid_dict['target']) | |
1251 | return name.nametype, name.nameid | |
1252 | ||
1253 | sig = parse_funcsig(['tell', {'name': 'pgid', 'type': 'CephPgid'}]) | |
1254 | try: | |
1255 | valid_dict = validate(childargs, sig, partial=True) | |
1256 | except ArgumentError: | |
1257 | pass | |
1258 | else: | |
1259 | if len(valid_dict) == 2: | |
1260 | # pg doesn't need revalidation; the string is fine | |
1261 | return 'pg', valid_dict['pgid'] | |
1262 | ||
1263 | # If we reached this far it must mean that so far we've been unable to | |
1264 | # obtain a proper target from childargs. This may mean that we are not | |
1265 | # dealing with a 'tell' command, or that the specified target is invalid. | |
1266 | # If the latter, we likely were unable to catch it because we were not | |
1267 | # really looking for it: first we tried to parse a 'CephName' (osd, mon, | |
1268 | # mds, followed by and id); given our failure to parse, we tried to parse | |
1269 | # a 'CephPgid' instead (e.g., 0.4a). Considering we got this far though | |
1270 | # we were unable to do so. | |
1271 | # | |
1272 | # We will now check if this is a tell and, if so, forcefully validate the | |
1273 | # target as a 'CephName'. This must be so because otherwise we will end | |
1274 | # up sending garbage to a monitor, which is the default target when a | |
1275 | # target is not explicitly specified. | |
1276 | # e.g., | |
1277 | # 'ceph status' -> target is any one monitor | |
1278 | # 'ceph tell mon.* status -> target is all monitors | |
1279 | # 'ceph tell foo status -> target is invalid! | |
1280 | if len(childargs) > 1 and childargs[0] == 'tell': | |
1281 | name = CephName() | |
1282 | # CephName.valid() raises on validation error; find_cmd_target()'s | |
1283 | # caller should handle them | |
1284 | name.valid(childargs[1]) | |
1285 | return name.nametype, name.nameid | |
1286 | ||
1287 | sig = parse_funcsig(['pg', {'name': 'pgid', 'type': 'CephPgid'}]) | |
1288 | try: | |
1289 | valid_dict = validate(childargs, sig, partial=True) | |
1290 | except ArgumentError: | |
1291 | pass | |
1292 | else: | |
1293 | if len(valid_dict) == 2: | |
1294 | return 'pg', valid_dict['pgid'] | |
1295 | ||
1296 | return 'mon', '' | |
1297 | ||
1298 | ||
1299 | class RadosThread(threading.Thread): | |
11fdf7f2 | 1300 | def __init__(self, func, *args, **kwargs): |
7c673cae FG |
1301 | self.args = args |
1302 | self.kwargs = kwargs | |
11fdf7f2 | 1303 | self.func = func |
7c673cae FG |
1304 | self.exception = None |
1305 | threading.Thread.__init__(self) | |
1306 | ||
1307 | def run(self): | |
1308 | try: | |
11fdf7f2 | 1309 | self.retval = self.func(*self.args, **self.kwargs) |
7c673cae FG |
1310 | except Exception as e: |
1311 | self.exception = e | |
1312 | ||
1313 | ||
11fdf7f2 | 1314 | def run_in_thread(func, *args, **kwargs): |
7c673cae FG |
1315 | interrupt = False |
1316 | timeout = kwargs.pop('timeout', 0) | |
11fdf7f2 TL |
1317 | if timeout == 0 or timeout == None: |
1318 | # python threading module will just get blocked if timeout is `None`, | |
1319 | # otherwise it will keep polling until timeout or thread stops. | |
9f95a23c TL |
1320 | # wait for INT32_MAX, as python 3.6.8 use int32_t to present the |
1321 | # timeout in integer when converting it to nanoseconds | |
1322 | timeout = (1 << (32 - 1)) - 1 | |
11fdf7f2 | 1323 | t = RadosThread(func, *args, **kwargs) |
7c673cae FG |
1324 | |
1325 | # allow the main thread to exit (presumably, avoid a join() on this | |
1326 | # subthread) before this thread terminates. This allows SIGINT | |
1327 | # exit of a blocked call. See below. | |
1328 | t.daemon = True | |
1329 | ||
1330 | t.start() | |
11fdf7f2 TL |
1331 | t.join(timeout=timeout) |
1332 | # ..but allow SIGINT to terminate the waiting. Note: this | |
1333 | # relies on the Linux kernel behavior of delivering the signal | |
1334 | # to the main thread in preference to any subthread (all that's | |
1335 | # strictly guaranteed is that *some* thread that has the signal | |
1336 | # unblocked will receive it). But there doesn't seem to be | |
1337 | # any interface to create a thread with SIGINT blocked. | |
1338 | if t.is_alive(): | |
1339 | raise Exception("timed out") | |
1340 | elif t.exception: | |
7c673cae | 1341 | raise t.exception |
11fdf7f2 TL |
1342 | else: |
1343 | return t.retval | |
7c673cae FG |
1344 | |
1345 | ||
1346 | def send_command_retry(*args, **kwargs): | |
1347 | while True: | |
1348 | try: | |
1349 | return send_command(*args, **kwargs) | |
1350 | except Exception as e: | |
11fdf7f2 TL |
1351 | # If our librados instance has not reached state 'connected' |
1352 | # yet, we'll see an exception like this and retry | |
7c673cae FG |
1353 | if ('get_command_descriptions' in str(e) and |
1354 | 'object in state configuring' in str(e)): | |
1355 | continue | |
1356 | else: | |
1357 | raise | |
1358 | ||
1359 | def send_command(cluster, target=('mon', ''), cmd=None, inbuf=b'', timeout=0, | |
1360 | verbose=False): | |
1361 | """ | |
1362 | Send a command to a daemon using librados's | |
11fdf7f2 | 1363 | mon_command, osd_command, mgr_command, or pg_command. Any bulk input data |
7c673cae FG |
1364 | comes in inbuf. |
1365 | ||
1366 | Returns (ret, outbuf, outs); ret is the return code, outbuf is | |
1367 | the outbl "bulk useful output" buffer, and outs is any status | |
1368 | or error message (intended for stderr). | |
1369 | ||
1370 | If target is osd.N, send command to that osd (except for pgid cmds) | |
1371 | """ | |
1372 | cmd = cmd or [] | |
1373 | try: | |
1374 | if target[0] == 'osd': | |
1375 | osdid = target[1] | |
1376 | ||
1377 | if verbose: | |
1378 | print('submit {0} to osd.{1}'.format(cmd, osdid), | |
1379 | file=sys.stderr) | |
1380 | ret, outbuf, outs = run_in_thread( | |
11fdf7f2 | 1381 | cluster.osd_command, osdid, cmd, inbuf, timeout=timeout) |
7c673cae FG |
1382 | |
1383 | elif target[0] == 'mgr': | |
9f95a23c TL |
1384 | name = '' # non-None empty string means "current active mgr" |
1385 | if len(target) > 1 and target[1] is not None: | |
1386 | name = target[1] | |
1387 | if verbose: | |
1388 | print('submit {0} to {1} name {2}'.format(cmd, target[0], name), | |
1389 | file=sys.stderr) | |
1390 | ret, outbuf, outs = run_in_thread( | |
1391 | cluster.mgr_command, cmd, inbuf, timeout=timeout, target=name) | |
1392 | ||
1393 | elif target[0] == 'mon-mgr': | |
1394 | if verbose: | |
1395 | print('submit {0} to {1}'.format(cmd, target[0]), | |
1396 | file=sys.stderr) | |
7c673cae | 1397 | ret, outbuf, outs = run_in_thread( |
11fdf7f2 | 1398 | cluster.mgr_command, cmd, inbuf, timeout=timeout) |
7c673cae FG |
1399 | |
1400 | elif target[0] == 'pg': | |
1401 | pgid = target[1] | |
1402 | # pgid will already be in the command for the pg <pgid> | |
1403 | # form, but for tell <pgid>, we need to put it in | |
1404 | if cmd: | |
1405 | cmddict = json.loads(cmd[0]) | |
1406 | cmddict['pgid'] = pgid | |
1407 | else: | |
1408 | cmddict = dict(pgid=pgid) | |
1409 | cmd = [json.dumps(cmddict)] | |
1410 | if verbose: | |
1411 | print('submit {0} for pgid {1}'.format(cmd, pgid), | |
1412 | file=sys.stderr) | |
1413 | ret, outbuf, outs = run_in_thread( | |
11fdf7f2 | 1414 | cluster.pg_command, pgid, cmd, inbuf, timeout=timeout) |
7c673cae FG |
1415 | |
1416 | elif target[0] == 'mon': | |
1417 | if verbose: | |
1418 | print('{0} to {1}'.format(cmd, target[0]), | |
1419 | file=sys.stderr) | |
31f18b77 | 1420 | if len(target) < 2 or target[1] == '': |
7c673cae | 1421 | ret, outbuf, outs = run_in_thread( |
11fdf7f2 | 1422 | cluster.mon_command, cmd, inbuf, timeout=timeout) |
7c673cae FG |
1423 | else: |
1424 | ret, outbuf, outs = run_in_thread( | |
11fdf7f2 | 1425 | cluster.mon_command, cmd, inbuf, timeout=timeout, target=target[1]) |
7c673cae FG |
1426 | elif target[0] == 'mds': |
1427 | mds_spec = target[1] | |
1428 | ||
1429 | if verbose: | |
1430 | print('submit {0} to mds.{1}'.format(cmd, mds_spec), | |
1431 | file=sys.stderr) | |
1432 | ||
1433 | try: | |
1434 | from cephfs import LibCephFS | |
1435 | except ImportError: | |
1436 | raise RuntimeError("CephFS unavailable, have you installed libcephfs?") | |
1437 | ||
b32b8144 | 1438 | filesystem = LibCephFS(rados_inst=cluster) |
7c673cae FG |
1439 | filesystem.init() |
1440 | ret, outbuf, outs = \ | |
1441 | filesystem.mds_command(mds_spec, cmd, inbuf) | |
1442 | filesystem.shutdown() | |
1443 | else: | |
1444 | raise ArgumentValid("Bad target type '{0}'".format(target[0])) | |
1445 | ||
1446 | except Exception as e: | |
1447 | if not isinstance(e, ArgumentError): | |
1448 | raise RuntimeError('"{0}": exception {1}'.format(cmd, e)) | |
1449 | else: | |
1450 | raise | |
1451 | ||
1452 | return ret, outbuf, outs | |
1453 | ||
1454 | ||
1455 | def json_command(cluster, target=('mon', ''), prefix=None, argdict=None, | |
1456 | inbuf=b'', timeout=0, verbose=False): | |
1457 | """ | |
11fdf7f2 | 1458 | Serialize a command and up a JSON command and send it with send_command() above. |
7c673cae FG |
1459 | Prefix may be supplied separately or in argdict. Any bulk input |
1460 | data comes in inbuf. | |
1461 | ||
1462 | If target is osd.N, send command to that osd (except for pgid cmds) | |
11fdf7f2 TL |
1463 | |
1464 | :param cluster: ``rados.Rados`` instance | |
1465 | :param prefix: String to inject into command arguments as 'prefix' | |
1466 | :param argdict: Command arguments | |
7c673cae FG |
1467 | """ |
1468 | cmddict = {} | |
1469 | if prefix: | |
1470 | cmddict.update({'prefix': prefix}) | |
11fdf7f2 | 1471 | |
7c673cae FG |
1472 | if argdict: |
1473 | cmddict.update(argdict) | |
1474 | if 'target' in argdict: | |
1475 | target = argdict.get('target') | |
1476 | ||
7c673cae FG |
1477 | try: |
1478 | if target[0] == 'osd': | |
1479 | osdtarg = CephName() | |
1480 | osdtarget = '{0}.{1}'.format(*target) | |
1481 | # prefer target from cmddict if present and valid | |
1482 | if 'target' in cmddict: | |
1483 | osdtarget = cmddict.pop('target') | |
1484 | try: | |
1485 | osdtarg.valid(osdtarget) | |
1486 | target = ('osd', osdtarg.nameid) | |
1487 | except: | |
1488 | # use the target we were originally given | |
1489 | pass | |
7c673cae FG |
1490 | ret, outbuf, outs = send_command_retry(cluster, |
1491 | target, [json.dumps(cmddict)], | |
1492 | inbuf, timeout, verbose) | |
1493 | ||
1494 | except Exception as e: | |
1495 | if not isinstance(e, ArgumentError): | |
1496 | raise RuntimeError('"{0}": exception {1}'.format(argdict, e)) | |
1497 | else: | |
1498 | raise | |
1499 | ||
1500 | return ret, outbuf, outs |