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