]>
git.proxmox.com Git - ceph.git/blob - ceph/src/pybind/ceph_argparse.py
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
8 Copyright (C) 2013 Inktank Storage, Inc.
10 LGPL-2.1 or LGPL-3.0. See file COPYING.
25 from collections
import abc
26 from typing
import cast
, Any
, Callable
, Dict
, Generic
, List
, Optional
, Sequence
, Tuple
, Union
28 if sys
.version_info
>= (3, 8):
29 from typing
import get_args
, get_origin
35 return getattr(tp
, '__args__', ())
38 return getattr(tp
, '__origin__', None)
41 # Flags are from MonCommand.h
51 KWARG_EQUALS
= "--([^=]+)=(.+)"
52 KWARG_SPACE
= "--([^=]+)"
60 class ArgumentError(Exception):
62 Something wrong with arguments
67 class ArgumentNumber(ArgumentError
):
69 Wrong number of a repeated argument
74 class ArgumentFormat(ArgumentError
):
76 Argument value has wrong format
81 class ArgumentMissing(ArgumentError
):
83 Argument value missing in a command
88 class ArgumentValid(ArgumentError
):
90 Argument value is otherwise invalid (doesn't match choices, for instance)
95 class ArgumentTooFew(ArgumentError
):
97 Fewer arguments than descriptors in signature; may mean to continue
98 the search, so gets a special exception type
102 class ArgumentPrefix(ArgumentError
):
104 Special for mismatched prefix; less severe, don't report by default
109 class JsonFormat(Exception):
111 some syntactic or semantic issue with the JSON
116 class CephArgtype(object):
118 Base class for all Ceph argument types
120 Instantiating an object sets any validation parameters
121 (allowable strings, numeric ranges, etc.). The 'valid'
122 method validates a string against that initialized instance,
123 throwing ArgumentError if there's a problem.
125 def __init__(self
, **kwargs
):
127 set any per-instance validation parameters here
128 from kwargs (fixed string sets, integer ranges, etc)
132 def valid(self
, s
, partial
=False):
134 Run validation against given string s (generally one word);
135 partial means to accept partial string matches (begins-with).
136 If cool, set self.val to the value that should be returned
137 (a copy of the input string, or a numeric or boolean interpretation
138 thereof, for example)
139 if not, throw ArgumentError(msg-as-to-why)
145 return string representation of description of type. Note,
146 this is not a representation of the actual value. Subclasses
147 probably also override __str__() to give a more user-friendly
148 'name/type' description for use in command format help messages.
150 return self
.__class
__.__name
__
154 where __repr__ (ideally) returns a string that could be used to
155 reproduce the object, __str__ returns one you'd like to see in
156 print messages. Use __str__ to format the argtype descriptor
157 as it would be useful in a command usage message.
159 return '<{0}>'.format(self
.__class
__.__name
__)
161 def __call__(self
, v
):
164 def complete(self
, s
):
168 def _compound_type_to_argdesc(tp
, attrs
, positional
):
169 # generate argdesc from Sequence[T], Tuple[T,..] and Optional[T]
170 orig_type
= get_origin(tp
)
171 type_args
= get_args(tp
)
172 if orig_type
in (abc
.Sequence
, Sequence
, List
, list):
173 assert len(type_args
) == 1
175 return CephArgtype
.to_argdesc(type_args
[0], attrs
, positional
=positional
)
176 elif orig_type
is Tuple
:
177 assert len(type_args
) >= 1
178 inner_tp
= type_args
[0]
179 assert type_args
.count(inner_tp
) == len(type_args
), \
180 f
'all elements in {tp} should be identical'
181 attrs
['n'] = str(len(type_args
))
182 return CephArgtype
.to_argdesc(inner_tp
, attrs
, positional
=positional
)
183 elif get_origin(tp
) is Union
:
184 # should be Union[t, NoneType]
185 assert len(type_args
) == 2 and isinstance(None, type_args
[1])
186 return CephArgtype
.to_argdesc(type_args
[0], attrs
, True, positional
)
188 raise ValueError(f
"unknown type '{tp}': '{attrs}'")
191 def to_argdesc(tp
, attrs
, has_default
=False, positional
=True):
193 attrs
['req'] = 'false'
195 attrs
['positional'] = 'false'
203 return CEPH_ARG_TYPES
[tp
]().argdesc(attrs
)
205 if isinstance(tp
, CephArgtype
):
206 return tp
.argdesc(attrs
)
207 elif isinstance(tp
, type) and issubclass(tp
, enum
.Enum
):
208 return CephChoices(tp
=tp
).argdesc(attrs
)
210 return CephArgtype
._compound
_type
_to
_argdesc
(tp
, attrs
, positional
)
212 def argdesc(self
, attrs
):
213 attrs
['type'] = type(self
).__name
__
214 return ','.join(f
'{k}={v}' for k
, v
in attrs
.items())
217 def _cast_to_compound_type(tp
, v
):
218 orig_type
= get_origin(tp
)
219 type_args
= get_args(tp
)
220 if orig_type
in (abc
.Sequence
, Sequence
, List
, list):
223 return [CephArgtype
.cast_to(type_args
[0], e
) for e
in v
]
224 elif orig_type
is Tuple
:
225 return tuple(CephArgtype
.cast_to(type_args
[0], e
) for e
in v
)
226 elif get_origin(tp
) is Union
:
227 # should be Union[t, NoneType]
228 assert len(type_args
) == 2 and isinstance(None, type_args
[1])
229 return CephArgtype
.cast_to(type_args
[0], v
)
231 raise ValueError(f
"unknown type '{tp}': '{v}'")
241 if tp
in PYTHON_TYPES
:
243 elif isinstance(tp
, type) and issubclass(tp
, enum
.Enum
):
246 return CephArgtype
._cast
_to
_compound
_type
(tp
, v
)
249 class CephInt(CephArgtype
):
251 range-limited integers, [+|-][0-9]+ or 0x[0-9a-f]+
252 range: list of 1 or 2 ints, [min] or [min,max]
254 def __init__(self
, range=''):
258 self
.range = [int(x
) for x
in range.split('|')]
260 def valid(self
, s
, partial
=False):
264 raise ArgumentValid("{0} doesn't represent an int".format(s
))
265 if len(self
.range) == 2:
266 if val
< self
.range[0] or val
> self
.range[1]:
267 raise ArgumentValid(f
"{val} not in range {self.range}")
268 elif len(self
.range) == 1:
269 if val
< self
.range[0]:
270 raise ArgumentValid(f
"{val} not in range {self.range}")
275 if len(self
.range) == 1:
276 r
= '[{0}-]'.format(self
.range[0])
277 if len(self
.range) == 2:
278 r
= '[{0}-{1}]'.format(self
.range[0], self
.range[1])
280 return '<int{0}>'.format(r
)
282 def argdesc(self
, attrs
):
284 attrs
['range'] = '|'.join(str(v
) for v
in self
.range)
285 return super().argdesc(attrs
)
288 class CephFloat(CephArgtype
):
290 range-limited float type
291 range: list of 1 or 2 floats, [min] or [min, max]
293 def __init__(self
, range=''):
297 self
.range = [float(x
) for x
in range.split('|')]
299 def valid(self
, s
, partial
=False):
303 raise ArgumentValid("{0} doesn't represent a float".format(s
))
304 if len(self
.range) == 2:
305 if val
< self
.range[0] or val
> self
.range[1]:
306 raise ArgumentValid(f
"{val} not in range {self.range}")
307 elif len(self
.range) == 1:
308 if val
< self
.range[0]:
309 raise ArgumentValid(f
"{val} not in range {self.range}")
314 if len(self
.range) == 1:
315 r
= '[{0}-]'.format(self
.range[0])
316 if len(self
.range) == 2:
317 r
= '[{0}-{1}]'.format(self
.range[0], self
.range[1])
318 return '<float{0}>'.format(r
)
320 def argdesc(self
, attrs
):
322 attrs
['range'] = '|'.join(str(v
) for v
in self
.range)
323 return super().argdesc(attrs
)
326 class CephString(CephArgtype
):
328 String; pretty generic. goodchars is a RE char class of valid chars
330 def __init__(self
, goodchars
=''):
331 from string
import printable
333 re
.compile(goodchars
)
335 raise ValueError('CephString(): "{0}" is not a valid RE'.
337 self
.goodchars
= goodchars
338 self
.goodset
= frozenset(
339 [c
for c
in printable
if re
.match(goodchars
, c
)]
342 def valid(self
, s
: str, partial
: bool = False) -> None:
344 if self
.goodset
and not sset
<= self
.goodset
:
345 raise ArgumentFormat("invalid chars {0} in {1}".
346 format(''.join(sset
- self
.goodset
), s
))
349 def __str__(self
) -> str:
352 b
+= '(goodchars {0})'.format(self
.goodchars
)
353 return '<string{0}>'.format(b
)
355 def complete(self
, s
) -> List
[str]:
361 def argdesc(self
, attrs
):
363 attrs
['goodchars'] = self
.goodchars
364 return super().argdesc(attrs
)
367 class CephSocketpath(CephArgtype
):
369 Admin socket path; check that it's readable and S_ISSOCK
371 def valid(self
, s
: str, partial
: bool = False) -> None:
372 mode
= os
.stat(s
).st_mode
373 if not stat
.S_ISSOCK(mode
):
374 raise ArgumentValid('socket path {0} is not a socket'.format(s
))
377 def __str__(self
) -> str:
378 return '<admin-socket-path>'
381 class CephIPAddr(CephArgtype
):
383 IP address (v4 or v6) with optional port
385 def valid(self
, s
, partial
=False):
386 # parse off port, use socket to validate addr
388 p
: Optional
[str] = None
389 if s
.startswith('['):
391 elif s
.find('.') != -1:
399 raise ArgumentValid('{0}: invalid IPv4 port'.format(p
))
404 socket
.inet_pton(socket
.AF_INET
, a
)
406 raise ArgumentValid('{0}: invalid IPv4 address'.format(a
))
409 if s
.startswith('['):
412 raise ArgumentFormat('{0} missing terminating ]'.format(s
))
413 if s
[end
+ 1] == ':':
417 raise ArgumentValid('{0}: bad port number'.format(s
))
423 socket
.inet_pton(socket
.AF_INET6
, a
)
425 raise ArgumentValid('{0} not valid IPv6 address'.format(s
))
426 if p
is not None and int(p
) > 65535:
427 raise ArgumentValid("{0} not a valid port number".format(p
))
433 return '<IPaddr[:port]>'
436 class CephEntityAddr(CephIPAddr
):
438 EntityAddress, that is, IP address[/nonce]
440 def valid(self
, s
: str, partial
: bool = False) -> None:
443 ip
, nonce
= s
.split('/')
446 super(self
.__class
__, self
).valid(ip
)
450 nonce_int
= int(nonce
)
453 if nonce_int
is None or nonce_int
< 0:
455 '{0}: invalid entity, nonce {1} not integer > 0'.
460 def __str__(self
) -> str:
461 return '<EntityAddr>'
464 class CephPoolname(CephArgtype
):
466 Pool name; very little utility
468 def __str__(self
) -> str:
472 class CephObjectname(CephArgtype
):
474 Object name. Maybe should be combined with Pool name as they're always
475 present in pairs, and then could be checked for presence
477 def __str__(self
) -> str:
478 return '<objectname>'
481 class CephPgid(CephArgtype
):
483 pgid, in form N.xxx (N = pool number, xxx = hex pgnum)
485 def valid(self
, s
, partial
=False):
486 if s
.find('.') == -1:
487 raise ArgumentFormat('pgid has no .')
488 poolid_s
, pgnum_s
= s
.split('.', 1)
490 poolid
= int(poolid_s
)
492 raise ArgumentFormat('pool {0} not integer'.format(poolid
))
494 raise ArgumentFormat('pool {0} < 0'.format(poolid
))
496 pgnum
= int(pgnum_s
, 16)
498 raise ArgumentFormat('pgnum {0} not hex integer'.format(pgnum
))
505 class CephName(CephArgtype
):
507 Name (type.id) where:
508 type is osd|mon|client|mds
509 id is a base10 int, if type == osd, or a string otherwise
513 def __init__(self
) -> None:
514 self
.nametype
: Optional
[str] = None
515 self
.nameid
: Optional
[str] = None
517 def valid(self
, s
, partial
=False):
522 self
.nametype
= "mgr"
526 self
.nametype
= "mon"
529 if s
.find('.') == -1:
530 raise ArgumentFormat('CephName: no . in {0}'.format(s
))
532 t
, i
= s
.split('.', 1)
533 if t
not in ('osd', 'mon', 'client', 'mds', 'mgr'):
534 raise ArgumentValid('unknown type ' + t
)
540 raise ArgumentFormat(f
'osd id {i} not integer')
546 return '<name (type.id)>'
549 class CephOsdName(CephArgtype
):
551 Like CephName, but specific to osds: allow <id> alone
553 osd.<id>, or <id>, or *, where id is a base10 int
556 self
.nametype
: Optional
[str] = None
557 self
.nameid
: Optional
[int] = None
559 def valid(self
, s
, partial
=False):
563 if s
.find('.') != -1:
564 t
, i
= s
.split('.', 1)
566 raise ArgumentValid('unknown type ' + t
)
573 raise ArgumentFormat(f
'osd id {i} not integer')
575 raise ArgumentFormat(f
'osd id {v} is less than 0')
581 return '<osdname (id|osd.id)>'
584 class CephChoices(CephArgtype
):
586 Set of string literals; init with valid choices
588 def __init__(self
, strings
='', tp
=None, **kwargs
):
589 self
.strings
= strings
.split('|')
591 if self
.enum
is not None:
592 self
.strings
= list(e
.value
for e
in self
.enum
)
594 def valid(self
, s
, partial
=False):
596 if s
not in self
.strings
:
597 # show as __str__ does: {s1|s2..}
598 raise ArgumentValid("{0} not in {1}".format(s
, self
))
603 for t
in self
.strings
:
607 raise ArgumentValid("{0} not in {1}". format(s
, self
))
610 if len(self
.strings
) == 1:
611 return '{0}'.format(self
.strings
[0])
613 return '{0}'.format('|'.join(self
.strings
))
615 def __call__(self
, v
):
616 if self
.enum
is None:
621 def complete(self
, s
):
622 all_elems
= [token
for token
in self
.strings
if token
.startswith(s
)]
625 def argdesc(self
, attrs
):
626 attrs
['strings'] = '|'.join(self
.strings
)
627 return super().argdesc(attrs
)
630 class CephBool(CephArgtype
):
632 A boolean argument, values may be case insensitive 'true', 'false', '0',
633 '1'. In keyword form, value may be left off (implies true).
635 def __init__(self
, strings
='', **kwargs
):
636 self
.strings
= strings
.split('|')
638 def valid(self
, s
, partial
=False):
639 lower_case
= s
.lower()
640 if lower_case
in ['true', '1']:
642 elif lower_case
in ['false', '0']:
645 raise ArgumentValid("{0} not one of 'true', 'false'".format(s
))
651 class CephFilepath(CephArgtype
):
655 def valid(self
, s
, partial
=False):
656 # set self.val if the specified path is readable or writable
657 s
= os
.path
.abspath(s
)
658 if not os
.access(s
, os
.R_OK
):
659 self
._validate
_writable
_file
(s
)
662 def _validate_writable_file(self
, fname
):
663 if os
.path
.exists(fname
):
664 if os
.path
.isfile(fname
):
665 if not os
.access(fname
, os
.W_OK
):
666 raise ArgumentValid('{0} is not writable'.format(fname
))
668 raise ArgumentValid('{0} is not file'.format(fname
))
670 dirname
= os
.path
.dirname(fname
)
671 if not os
.access(dirname
, os
.W_OK
):
672 raise ArgumentValid('cannot create file in {0}'.format(dirname
))
675 return '<outfilename>'
678 class CephFragment(CephArgtype
):
682 def valid(self
, s
, partial
=False):
683 if s
.find('/') == -1:
684 raise ArgumentFormat('{0}: no /'.format(s
))
685 val
, bits
= s
.split('/')
687 if not val
.startswith('0x'):
688 raise ArgumentFormat("{0} not a hex integer".format(val
))
692 raise ArgumentFormat('can\'t convert {0} to integer'.format(val
))
696 raise ArgumentFormat('can\'t convert {0} to integer'.format(bits
))
700 return "<CephFS fragment ID (0xvvv/bbb)>"
703 class CephUUID(CephArgtype
):
705 CephUUID: pretty self-explanatory
707 def valid(self
, s
: str, partial
: bool = False) -> None:
710 except Exception as e
:
711 raise ArgumentFormat('invalid UUID {0}: {1}'.format(s
, e
))
714 def __str__(self
) -> str:
718 class CephPrefix(CephArgtype
):
720 CephPrefix: magic type for "all the first n fixed strings"
722 def __init__(self
, prefix
: str = '') -> None:
725 def valid(self
, s
: str, partial
: bool = False) -> None:
728 if isinstance(s
, bytes
):
729 # `prefix` can always be converted into unicode when being compared,
730 # but `s` could be anything passed by user.
731 s
= s
.decode('ascii')
732 except UnicodeEncodeError:
733 raise ArgumentPrefix(u
"no match for {0}".format(s
))
734 except UnicodeDecodeError:
735 raise ArgumentPrefix("no match for {0}".format(s
))
738 if self
.prefix
.startswith(s
):
746 raise ArgumentPrefix("no match for {0}".format(s
))
748 def __str__(self
) -> str:
751 def complete(self
, s
) -> List
[str]:
752 if self
.prefix
.startswith(s
):
753 return [self
.prefix
.rstrip(' ')]
758 class argdesc(object):
760 argdesc(typename, name='name', n=numallowed|N,
761 req=False, positional=True,
762 helptext=helptext, **kwargs (type-specific))
765 typename: type(**kwargs) will be constructed
766 later, type.valid(w) will be called with a word in that position
768 name is used for parse errors and for constructing JSON output
769 n is a numeric literal or 'n|N', meaning "at least one, but maybe more"
770 req=False means the argument need not be present in the list
771 positional=False means the argument name must be specified, e.g. "--myoption value"
772 helptext is the associated help for the command
773 anything else are arguments to pass to the type constructor.
775 self.instance is an instance of type t constructed with typeargs.
777 valid() will later be called with input to validate against it,
778 and will store the validated value in self.instance.val for extraction.
780 def __init__(self
, t
, name
=None, n
=1, req
=True, positional
=True, **kwargs
) -> None:
781 if isinstance(t
, basestring
):
783 self
.typeargs
= {'prefix': t
}
785 self
.positional
= True
788 self
.typeargs
= kwargs
789 self
.req
= req
in (True, 'True', 'true')
790 self
.positional
= positional
in (True, 'True', 'true')
795 self
.N
= (n
in ['n', 'N'])
803 self
.instance
= self
.t(**self
.typeargs
)
806 r
= 'argdesc(' + str(self
.t
) + ', '
807 internals
= ['N', 'typeargs', 'instance', 't']
808 for (k
, v
) in self
.__dict
__.items():
809 if k
.startswith('__') or k
in internals
:
812 # undo modification from __init__
813 if k
== 'n' and self
.N
:
815 r
+= '{0}={1}, '.format(k
, v
)
816 for (k
, v
) in self
.typeargs
.items():
817 r
+= '{0}={1}, '.format(k
, v
)
821 if ((self
.t
== CephChoices
and len(self
.instance
.strings
) == 1)
822 or (self
.t
== CephPrefix
)):
823 s
= str(self
.instance
)
825 s
= '{0}({1})'.format(self
.name
, str(self
.instance
))
834 like str(), but omit parameter names (except for CephString,
835 which really needs them)
838 if self
.t
== CephBool
:
839 chunk
= "--{0}".format(self
.name
.replace("_", "-"))
840 elif self
.t
== CephPrefix
:
841 chunk
= str(self
.instance
)
842 elif self
.t
== CephChoices
:
843 if self
.name
== 'format':
844 # this is for talking to legacy clusters only; new clusters
845 # should properly mark format args as non-positional.
846 chunk
= f
'--{self.name} {{{str(self.instance)}}}'
848 chunk
= f
'<{self.name}:{self.instance}>'
849 elif self
.t
== CephOsdName
:
850 # it just so happens all CephOsdName commands are named 'id' anyway,
851 # so <id|osd.id> is perfect.
852 chunk
= '<id|osd.id>'
853 elif self
.t
== CephName
:
854 # CephName commands similarly only have one arg of the
855 # type, so <type.id> is good.
857 elif self
.t
== CephInt
:
858 chunk
= '<{0}:int>'.format(self
.name
)
859 elif self
.t
== CephFloat
:
860 chunk
= '<{0}:float>'.format(self
.name
)
862 chunk
= '<{0}>'.format(self
.name
)
870 if self
.t
== CephBool
:
871 chunk
= "--{0}".format(self
.name
.replace("_", "-"))
872 elif self
.t
== CephPrefix
:
873 chunk
= str(self
.instance
)
874 elif self
.t
== CephChoices
:
875 chunk
= f
'--{self.name} {{{str(self.instance)}}}'
876 elif self
.t
== CephOsdName
:
877 chunk
= f
'--{self.name} <id|osd.id>'
878 elif self
.t
== CephName
:
879 chunk
= f
'--{self.name} <type.id>'
880 elif self
.t
== CephInt
:
881 chunk
= f
'--{self.name} <int>'
882 elif self
.t
== CephFloat
:
883 chunk
= f
'--{self.name} <float>'
885 chunk
= f
'--{self.name} <value>'
889 if not self
.req
: # req should *always* be false
894 def complete(self
, s
):
895 return self
.instance
.complete(s
)
898 def concise_sig(sig
):
900 Return string representation of sig useful for syntax reference in help
902 return ' '.join([d
.helpstr() for d
in sig
])
905 def descsort_key(sh
):
907 sort descriptors by prefixes, defined as the concatenation of all simple
908 strings in the descriptor; this works out to just the leading strings.
910 return concise_sig(sh
['sig'])
913 def parse_funcsig(sig
: Sequence
[Union
[str, Dict
[str, Any
]]]) -> List
[argdesc
]:
915 parse a single descriptor (array of strings or dicts) into a
916 dict of function descriptor/validators (objects of CephXXX type)
918 :returns: list of ``argdesc``
924 if isinstance(desc
, basestring
):
926 desc
= {'type': t
, 'name': 'prefix', 'prefix': desc
}
928 # not a simple string, must be dict
929 if 'type' not in desc
:
930 s
= 'JSON descriptor {0} has no type'.format(sig
)
932 # look up type string in our globals() dict; if it's an
933 # object of type `type`, it must be a
934 # locally-defined class. otherwise, we haven't a clue.
935 if desc
['type'] in globals():
936 t
= globals()[desc
['type']]
937 if not isinstance(t
, type):
938 s
= 'unknown type {0}'.format(desc
['type'])
941 s
= 'unknown type {0}'.format(desc
['type'])
945 for key
, val
in desc
.items():
946 if key
not in ['type', 'name', 'n', 'req', 'positional']:
948 newsig
.append(argdesc(t
,
949 name
=desc
.get('name', None),
951 req
=desc
.get('req', True),
952 positional
=desc
.get('positional', True),
957 def parse_json_funcsigs(s
: str,
958 consumer
: str) -> Dict
[str, Dict
[str, List
[argdesc
]]]:
960 A function signature is mostly an array of argdesc; it's represented
963 "cmd001": {"sig":[ "type": type, "name": name, "n": num, "req":true|false <other param>], "help":helptext, "module":modulename, "perm":perms, "avail":availability}
969 A set of sigs is in an dict mapped by a unique number:
972 "sig": ["type.. ], "help":helptext...
975 "sig": [.. ], "help":helptext...
979 Parse the string s and return a dict of dicts, keyed by opcode;
980 each dict contains 'sig' with the array of descriptors, and 'help'
981 with the helptext, 'module' with the module name, 'perm' with a
982 string representing required permissions in that module to execute
983 this command (and also whether it is a read or write command from
984 the cluster state perspective), and 'avail' as a hint for
985 whether the command should be advertised by CLI, REST, or both.
986 If avail does not contain 'consumer', don't include the command
987 in the returned dict.
990 overall
= json
.loads(s
)
991 except Exception as e
:
992 print("Couldn't parse JSON {0}: {1}".format(s
, e
), file=sys
.stderr
)
995 for cmdtag
, cmd
in overall
.items():
997 s
= "JSON descriptor {0} has no 'sig'".format(cmdtag
)
999 # check 'avail' and possibly ignore this command
1001 if consumer
not in cmd
['avail']:
1003 # rewrite the 'sig' item with the argdesc-ized version, and...
1004 cmd
['sig'] = parse_funcsig(cmd
['sig'])
1005 # just take everything else as given
1006 sigdict
[cmdtag
] = cmd
1010 ArgValT
= Union
[bool, int, float, str, Tuple
[str, str]]
1012 def validate_one(word
: str,
1015 partial
: bool = False) -> List
[ArgValT
]:
1017 validate_one(word, desc, is_kwarg, partial=False)
1019 validate word against the constructed instance of the type
1020 in desc. May raise exception. If it returns false (and doesn't
1021 raise an exception), desc.instance.val will
1022 contain the validated value (in the appropriate type).
1025 # CephString option might contain "," in it
1026 allow_csv
= is_kwarg
or desc
.t
is not CephString
1027 if desc
.N
and allow_csv
:
1028 for part
in word
.split(','):
1029 desc
.instance
.valid(part
, partial
)
1030 vals
.append(desc
.instance
.val
)
1032 desc
.instance
.valid(word
, partial
)
1033 vals
.append(desc
.instance
.val
)
1036 desc
.n
= desc
.numseen
+ 1
1040 def matchnum(args
: List
[str],
1041 signature
: List
[argdesc
],
1042 partial
: bool = False) -> int:
1044 matchnum(s, signature, partial=False)
1046 Returns number of arguments matched in s against signature.
1047 Can be used to determine most-likely command for full or partial
1048 matches (partial applies to string matches).
1051 mysig
= copy
.deepcopy(signature
)
1055 while desc
.numseen
< desc
.n
:
1056 # if there are no more arguments, return
1062 # only allow partial matching if we're on the last supplied
1063 # word; avoid matching foo bar and foot bar just because
1065 validate_one(word
, desc
, False, partial
and (len(words
) == 0))
1067 except ArgumentError
:
1068 # matchnum doesn't care about type of error
1073 # this wasn't required, so word may match the next desc
1074 words
.insert(0, word
)
1077 # it was required, and didn't match, return
1084 ValidatedArg
= Union
[bool, int, float, str,
1087 ValidatedArgs
= Dict
[str, ValidatedArg
]
1090 def store_arg(desc
: argdesc
, args
: Sequence
[ValidatedArg
], d
: ValidatedArgs
):
1092 Store argument described by, and held in, thanks to valid(),
1093 desc into the dictionary d, keyed by desc.name. Three cases:
1095 1) desc.N is set: use args for arg value in "d", desc.instance.val
1096 only contains the last parsed arg in the "args" list
1097 2) prefix: multiple args are joined with ' ' into one d{} item
1098 3) single prefix or other arg: store as simple value
1100 Used in validate() below.
1103 # value should be a list
1105 d
[desc
.name
] += args
1108 elif (desc
.t
== CephPrefix
) and (desc
.name
in d
):
1109 # prefixes' values should be a space-joined concatenation
1110 d
[desc
.name
] += ' ' + desc
.instance
.val
1112 # if first CephPrefix or any other type, just set it
1113 d
[desc
.name
] = desc
.instance
.val
1116 def validate(args
: List
[str],
1117 signature
: Sequence
[argdesc
],
1119 partial
: Optional
[bool] = False) -> ValidatedArgs
:
1121 validate(args, signature, flags=0, partial=False)
1123 args is a list of strings representing a possible
1124 command input following format of signature. Runs a validation; no
1125 exception means it's OK. Return a dict containing all arguments keyed
1126 by their descriptor name, with duplicate args per name accumulated
1127 into a list (or space-separated value for CephPrefix).
1129 Mismatches of prefix are non-fatal, as this probably just means the
1130 search hasn't hit the correct command. Mismatches of non-prefix
1131 arguments are treated as fatal, and an exception raised.
1133 This matching is modified if partial is set: allow partial matching
1134 (with partial dict returned); in this case, there are no exceptions
1138 myargs
= copy
.deepcopy(args
)
1139 mysig
= copy
.deepcopy(signature
)
1140 reqsiglen
= len([desc
for desc
in mysig
if desc
.req
])
1142 d
: ValidatedArgs
= dict()
1143 save_exception
= None
1145 arg_descs_by_name
: Dict
[str, argdesc
] = \
1146 dict((desc
.name
, desc
) for desc
in mysig
if desc
.t
!= CephPrefix
)
1148 # Special case: detect "injectargs" (legacy way of modifying daemon
1149 # configs) and permit "--" string arguments if so.
1150 injectargs
= myargs
and myargs
[0] == "injectargs"
1152 # Make a pass through all arguments
1156 while desc
.numseen
< desc
.n
:
1158 myarg
: Optional
[str] = myargs
.pop(0)
1162 # no arg, but not required? Continue consuming mysig
1163 # in case there are later required args
1164 if myarg
in (None, []):
1167 # did we already get this argument (as a named arg, earlier?)
1171 # A keyword argument?
1173 # argdesc for the keyword argument, if we find one
1176 # Try both styles of keyword argument
1177 kwarg_match
= re
.match(KWARG_EQUALS
, myarg
)
1179 # We have a "--foo=bar" style argument
1180 kwarg_k
, kwarg_v
= kwarg_match
.groups()
1182 # Either "--foo-bar" or "--foo_bar" style is accepted
1183 kwarg_k
= kwarg_k
.replace('-', '_')
1185 kwarg_desc
= arg_descs_by_name
.get(kwarg_k
, None)
1187 # Maybe this is a "--foo bar" or "--bool" style argument
1188 key_match
= re
.match(KWARG_SPACE
, myarg
)
1190 kwarg_k
= key_match
.group(1)
1192 # Permit --foo-bar=123 form or --foo_bar=123 form,
1193 # assuming all command definitions use foo_bar argument
1195 kwarg_k
= kwarg_k
.replace('-', '_')
1197 kwarg_desc
= arg_descs_by_name
.get(kwarg_k
, None)
1199 if kwarg_desc
.t
== CephBool
:
1201 elif len(myargs
): # Some trailing arguments exist
1202 kwarg_v
= myargs
.pop(0)
1204 # Forget it, this is not a valid kwarg
1208 args
= validate_one(kwarg_v
, kwarg_desc
, True)
1210 store_arg(kwarg_desc
, args
, d
)
1213 if not desc
.positional
:
1214 # No more positional args!
1215 raise ArgumentValid(f
"Unexpected argument '{myarg}'")
1217 # Don't handle something as a positional argument if it
1218 # has a leading "--" unless it's a CephChoices (used for
1219 # "--yes-i-really-mean-it")
1220 if myarg
and myarg
.startswith("--"):
1221 # Special cases for instances of confirmation flags
1222 # that were defined as CephString/CephChoices instead of CephBool
1223 # in pre-nautilus versions of Ceph daemons.
1224 is_value
= desc
.t
== CephChoices \
1225 or myarg
== "--yes-i-really-mean-it" \
1226 or myarg
== "--yes-i-really-really-mean-it" \
1227 or myarg
== "--yes-i-really-really-mean-it-not-faking" \
1228 or myarg
== "--force" \
1232 # Didn't get caught by kwarg handling, but has a "--", so
1233 # we must assume it's something invalid, to avoid naively
1234 # passing through mis-typed options as the values of
1235 # positional arguments.
1236 raise ArgumentValid("Unexpected argument '{0}'".format(
1239 # out of arguments for a required param?
1240 # Either return (if partial validation) or raise
1241 if myarg
in (None, []) and desc
.req
:
1242 if desc
.N
and desc
.numseen
< 1:
1243 # wanted N, didn't even get 1
1246 raise ArgumentNumber(
1247 'saw {0} of {1}, expected at least 1'.
1248 format(desc
.numseen
, desc
)
1250 elif not desc
.N
and desc
.numseen
< desc
.n
:
1251 # wanted n, got too few
1254 # special-case the "0 expected 1" case
1255 if desc
.numseen
== 0 and desc
.n
== 1:
1256 raise ArgumentMissing(
1257 'missing required parameter {0}'.format(desc
)
1259 raise ArgumentNumber(
1260 'saw {0} of {1}, expected {2}'.
1261 format(desc
.numseen
, desc
, desc
.n
)
1265 # Have an arg; validate it
1266 assert myarg
is not None
1268 args
= validate_one(myarg
, desc
, False)
1269 except ArgumentError
as e
:
1272 # if not required, just push back; it might match
1274 save_exception
= [myarg
, e
]
1275 myargs
.insert(0, myarg
)
1278 # hm, it was required, so time to return/raise
1283 # Whew, valid arg acquired. Store in dict
1285 store_arg(desc
, args
, d
)
1286 # Clear prior exception
1287 save_exception
= None
1289 # Done with entire list of argdescs
1290 if matchcnt
< reqsiglen
:
1291 raise ArgumentTooFew("not enough arguments given")
1293 if myargs
and not partial
:
1295 print(save_exception
[0], 'not valid: ', save_exception
[1], file=sys
.stderr
)
1296 raise ArgumentError("unused arguments: " + str(myargs
))
1298 if flags
& Flag
.MGR
:
1299 d
['target'] = ('mon-mgr', '')
1301 if flags
& Flag
.POLL
:
1308 def validate_command(sigdict
: Dict
[str, Dict
[str, Any
]],
1310 verbose
: Optional
[bool] = False) -> ValidatedArgs
:
1312 Parse positional arguments into a parameter dict, according to
1313 the command descriptions.
1315 Writes advice about nearly-matching commands ``sys.stderr`` if
1316 the arguments do not match any command.
1318 :param sigdict: A command description dictionary, as returned
1319 from Ceph daemons by the get_command_descriptions
1321 :param args: List of strings, should match one of the command
1322 signatures in ``sigdict``
1324 :returns: A dict of parsed parameters (including ``prefix``),
1325 or an empty dict if the args did not match any signature
1328 print("validate_command: " + " ".join(args
), file=sys
.stderr
)
1329 found
: Optional
[Dict
[str, Any
]] = None
1332 # look for best match, accumulate possibles in bestcmds
1333 # (so we can maybe give a more-useful error message)
1334 best_match_cnt
= 0.0
1335 bestcmds
: List
[Dict
[str, Any
]] = []
1336 for cmd
in sigdict
.values():
1337 flags
= cmd
.get('flags', 0)
1338 if flags
& Flag
.OBSOLETE
:
1341 matched
: float = matchnum(args
, sig
, partial
=True)
1342 if (matched
>= math
.floor(best_match_cnt
) and
1343 matched
== matchnum(args
, sig
, partial
=False)):
1344 # prefer those fully matched over partial patch
1346 if matched
< best_match_cnt
:
1349 print("better match: {0} > {1}: {2} ".format(
1350 matched
, best_match_cnt
, concise_sig(sig
)
1352 if matched
> best_match_cnt
:
1353 best_match_cnt
= matched
1356 bestcmds
.append(cmd
)
1358 # Sort bestcmds by number of req args so we can try shortest first
1359 # (relies on a cmdsig being key,val where val is a list of len 1)
1362 # prefer optional arguments over required ones
1364 return sum(map(lambda sig
: sig
.req
, sigs
))
1366 bestcmds_sorted
= sorted(bestcmds
, key
=grade
)
1368 print("bestcmds_sorted: ", file=sys
.stderr
)
1369 pprint
.PrettyPrinter(stream
=sys
.stderr
).pprint(bestcmds_sorted
)
1371 ex
: Optional
[ArgumentError
] = None
1372 # for everything in bestcmds, look for a true match
1373 for cmd
in bestcmds_sorted
:
1376 valid_dict
= validate(args
, sig
, flags
=cmd
.get('flags', 0))
1379 except ArgumentPrefix
:
1380 # ignore prefix mismatches; we just haven't found
1381 # the right command yet
1383 except ArgumentMissing
as e
:
1385 if len(bestcmds
) == 1:
1388 except ArgumentTooFew
:
1389 # It looked like this matched the beginning, but it
1390 # didn't have enough args supplied. If we're out of
1391 # cmdsigs we'll fall out unfound; if we're not, maybe
1392 # the next one matches completely. Whine, but pass.
1394 print('Not enough args supplied for ',
1395 concise_sig(sig
), file=sys
.stderr
)
1396 except ArgumentError
as e
:
1398 # Solid mismatch on an arg (type, range, etc.)
1399 # Stop now, because we have the right command but
1400 # some other input is invalid
1406 print("Invalid command:", ex
, file=sys
.stderr
)
1407 print(concise_sig(sig
), ': ', cmd
['help'], file=sys
.stderr
)
1409 bestcmds
= [c
for c
in bestcmds
1410 if not c
.get('flags', 0) & (Flag
.DEPRECATED | Flag
.HIDDEN
)]
1411 bestcmds
= bestcmds
[:10] # top 10
1412 print('no valid command found; {0} closest matches:'.format(len(bestcmds
)),
1414 for cmd
in bestcmds
:
1415 print(concise_sig(cmd
['sig']), file=sys
.stderr
)
1419 def find_cmd_target(childargs
: List
[str]) -> Tuple
[str, Optional
[str]]:
1421 Using a minimal validation, figure out whether the command
1422 should be sent to a monitor or an osd. We do this before even
1423 asking for the 'real' set of command signatures, so we can ask the
1425 Returns ('osd', osdid), ('pg', pgid), ('mgr', '') or ('mon', '')
1427 sig
= parse_funcsig(['tell', {'name': 'target', 'type': 'CephName'}])
1429 valid_dict
= validate(childargs
, sig
, partial
=True)
1430 except ArgumentError
:
1433 if len(valid_dict
) == 2:
1434 # revalidate to isolate type and id
1436 # if this fails, something is horribly wrong, as it just
1437 # validated successfully above
1438 name
.valid(valid_dict
['target'])
1439 assert name
.nametype
is not None
1440 return name
.nametype
, name
.nameid
1442 sig
= parse_funcsig(['tell', {'name': 'pgid', 'type': 'CephPgid'}])
1444 valid_dict
= validate(childargs
, sig
, partial
=True)
1445 except ArgumentError
:
1448 if len(valid_dict
) == 2:
1449 # pg doesn't need revalidation; the string is fine
1450 pgid
= valid_dict
['pgid']
1451 assert isinstance(pgid
, str)
1454 # If we reached this far it must mean that so far we've been unable to
1455 # obtain a proper target from childargs. This may mean that we are not
1456 # dealing with a 'tell' command, or that the specified target is invalid.
1457 # If the latter, we likely were unable to catch it because we were not
1458 # really looking for it: first we tried to parse a 'CephName' (osd, mon,
1459 # mds, followed by and id); given our failure to parse, we tried to parse
1460 # a 'CephPgid' instead (e.g., 0.4a). Considering we got this far though
1461 # we were unable to do so.
1463 # We will now check if this is a tell and, if so, forcefully validate the
1464 # target as a 'CephName'. This must be so because otherwise we will end
1465 # up sending garbage to a monitor, which is the default target when a
1466 # target is not explicitly specified.
1468 # 'ceph status' -> target is any one monitor
1469 # 'ceph tell mon.* status -> target is all monitors
1470 # 'ceph tell foo status -> target is invalid!
1471 if len(childargs
) > 1 and childargs
[0] == 'tell':
1473 # CephName.valid() raises on validation error; find_cmd_target()'s
1474 # caller should handle them
1475 name
.valid(childargs
[1])
1476 assert name
.nametype
is not None
1477 assert name
.nameid
is not None
1478 return name
.nametype
, name
.nameid
1480 sig
= parse_funcsig(['pg', {'name': 'pgid', 'type': 'CephPgid'}])
1482 valid_dict
= validate(childargs
, sig
, partial
=True)
1483 except ArgumentError
:
1486 if len(valid_dict
) == 2:
1487 pgid
= valid_dict
['pgid']
1488 assert isinstance(pgid
, str)
1494 class RadosThread(threading
.Thread
):
1495 def __init__(self
, func
, *args
, **kwargs
):
1497 self
.kwargs
= kwargs
1499 self
.exception
= None
1500 threading
.Thread
.__init
__(self
)
1504 self
.retval
= self
.func(*self
.args
, **self
.kwargs
)
1505 except Exception as e
:
1509 def run_in_thread(func
: Callable
[[Any
, Any
], Tuple
[int, bytes
, str]],
1510 *args
: Any
, **kwargs
: Any
) -> Tuple
[int, bytes
, str]:
1511 timeout
= kwargs
.pop('timeout', 0)
1512 if timeout
== 0 or timeout
is None:
1513 # python threading module will just get blocked if timeout is `None`,
1514 # otherwise it will keep polling until timeout or thread stops.
1515 # timeout in integer when converting it to nanoseconds, but since
1516 # python3 uses `int64_t` for the deadline before timeout expires,
1517 # we have to use a safe value which does not overflow after being
1518 # added to current time in microseconds.
1519 timeout
= 24 * 60 * 60
1520 t
= RadosThread(func
, *args
, **kwargs
)
1522 # allow the main thread to exit (presumably, avoid a join() on this
1523 # subthread) before this thread terminates. This allows SIGINT
1524 # exit of a blocked call. See below.
1528 t
.join(timeout
=timeout
)
1529 # ..but allow SIGINT to terminate the waiting. Note: this
1530 # relies on the Linux kernel behavior of delivering the signal
1531 # to the main thread in preference to any subthread (all that's
1532 # strictly guaranteed is that *some* thread that has the signal
1533 # unblocked will receive it). But there doesn't seem to be
1534 # any interface to create a thread with SIGINT blocked.
1536 raise Exception("timed out")
1543 def send_command_retry(*args
: Any
, **kwargs
: Any
) -> Tuple
[int, bytes
, str]:
1546 return send_command(*args
, **kwargs
)
1547 except Exception as e
:
1548 # If our librados instance has not reached state 'connected'
1549 # yet, we'll see an exception like this and retry
1550 if ('get_command_descriptions' in str(e
) and
1551 'object in state configuring' in str(e
)):
1557 def send_command(cluster
,
1558 target
: Tuple
[str, Optional
[str]] = ('mon', ''),
1559 cmd
: Optional
[str] = None,
1560 inbuf
: Optional
[bytes
] = b
'',
1561 timeout
: Optional
[int] = 0,
1562 verbose
: Optional
[bool] = False) -> Tuple
[int, bytes
, str]:
1564 Send a command to a daemon using librados's
1565 mon_command, osd_command, mgr_command, or pg_command. Any bulk input data
1568 Returns (ret, outbuf, outs); ret is the return code, outbuf is
1569 the outbl "bulk useful output" buffer, and outs is any status
1570 or error message (intended for stderr).
1572 If target is osd.N, send command to that osd (except for pgid cmds)
1575 if target
[0] == 'osd':
1577 assert osdid
is not None
1580 print('submit {0} to osd.{1}'.format(cmd
, osdid
),
1582 ret
, outbuf
, outs
= run_in_thread(
1583 cluster
.osd_command
, int(osdid
), cmd
, inbuf
, timeout
=timeout
)
1585 elif target
[0] == 'mgr':
1586 name
= '' # non-None empty string means "current active mgr"
1587 if len(target
) > 1 and target
[1] is not None:
1590 print('submit {0} to {1} name {2}'.format(cmd
, target
[0], name
),
1592 ret
, outbuf
, outs
= run_in_thread(
1593 cluster
.mgr_command
, cmd
, inbuf
, timeout
=timeout
, target
=name
)
1595 elif target
[0] == 'mon-mgr':
1597 print('submit {0} to {1}'.format(cmd
, target
[0]),
1599 ret
, outbuf
, outs
= run_in_thread(
1600 cluster
.mgr_command
, cmd
, inbuf
, timeout
=timeout
)
1602 elif target
[0] == 'pg':
1604 # pgid will already be in the command for the pg <pgid>
1605 # form, but for tell <pgid>, we need to put it in
1607 cmddict
= json
.loads(cmd
)
1608 cmddict
['pgid'] = pgid
1610 cmddict
= dict(pgid
=pgid
)
1611 cmd
= json
.dumps(cmddict
)
1613 print('submit {0} for pgid {1}'.format(cmd
, pgid
),
1615 ret
, outbuf
, outs
= run_in_thread(
1616 cluster
.pg_command
, pgid
, cmd
, inbuf
, timeout
=timeout
)
1618 elif target
[0] == 'mon':
1620 print('{0} to {1}'.format(cmd
, target
[0]),
1622 if len(target
) < 2 or target
[1] == '':
1623 ret
, outbuf
, outs
= run_in_thread(
1624 cluster
.mon_command
, cmd
, inbuf
, timeout
=timeout
)
1626 ret
, outbuf
, outs
= run_in_thread(
1627 cluster
.mon_command
, cmd
, inbuf
, timeout
=timeout
, target
=target
[1])
1628 elif target
[0] == 'mds':
1629 mds_spec
= target
[1]
1632 print('submit {0} to mds.{1}'.format(cmd
, mds_spec
),
1636 from cephfs
import LibCephFS
1638 raise RuntimeError("CephFS unavailable, have you installed libcephfs?")
1640 filesystem
= LibCephFS(rados_inst
=cluster
)
1642 ret
, outbuf
, outs
= \
1643 filesystem
.mds_command(mds_spec
, cmd
, inbuf
)
1644 filesystem
.shutdown()
1646 raise ArgumentValid("Bad target type '{0}'".format(target
[0]))
1648 except Exception as e
:
1649 if not isinstance(e
, ArgumentError
):
1650 raise RuntimeError('"{0}": exception {1}'.format(cmd
, e
))
1654 return ret
, outbuf
, outs
1657 def json_command(cluster
,
1658 target
: Tuple
[str, Optional
[str]] = ('mon', ''),
1659 prefix
: Optional
[str] = None,
1660 argdict
: Optional
[ValidatedArgs
] = None,
1661 inbuf
: Optional
[bytes
] = b
'',
1662 timeout
: Optional
[int] = 0,
1663 verbose
: Optional
[bool] = False) -> Tuple
[int, bytes
, str]:
1665 Serialize a command and up a JSON command and send it with send_command() above.
1666 Prefix may be supplied separately or in argdict. Any bulk input
1667 data comes in inbuf.
1669 If target is osd.N, send command to that osd (except for pgid cmds)
1671 :param cluster: ``rados.Rados`` instance
1672 :param prefix: String to inject into command arguments as 'prefix'
1673 :param argdict: Command arguments
1675 cmddict
: ValidatedArgs
= {}
1677 cmddict
.update({'prefix': prefix
})
1680 cmddict
.update(argdict
)
1681 if 'target' in argdict
:
1682 target
= cast(Tuple
[str, str], argdict
['target'])
1685 if target
[0] == 'osd':
1686 osdtarg
= CephName()
1687 osdtarget
= '{0}.{1}'.format(*target
)
1688 # prefer target from cmddict if present and valid
1689 if 'target' in cmddict
:
1690 osdtarget
= cast(str, cmddict
.pop('target'))
1692 osdtarg
.valid(osdtarget
)
1693 target
= ('osd', osdtarg
.nameid
)
1695 # use the target we were originally given
1697 ret
, outbuf
, outs
= send_command_retry(cluster
,
1698 target
, json
.dumps(cmddict
),
1699 inbuf
, timeout
, verbose
)
1701 except Exception as e
:
1702 if not isinstance(e
, ArgumentError
):
1703 raise RuntimeError('"{0}": exception {1}'.format(argdict
, e
))
1707 return ret
, outbuf
, outs