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