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