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