]> git.proxmox.com Git - ceph.git/blame - ceph/src/pybind/ceph_argparse.py
import 15.2.0 Octopus source
[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
9f95a23c 10LGPL-2.1 or LGPL-3.0. 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:
9f95a23c 697 s += '...'
7c673cae 698 if not self.req:
9f95a23c 699 s = '[' + s + ']'
7c673cae
FG
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 """
9f95a23c 707 if self.t == CephBool:
11fdf7f2 708 chunk = "--{0}".format(self.name.replace("_", "-"))
9f95a23c 709 elif self.t == CephPrefix or self.t == CephChoices:
7c673cae 710 chunk = str(self.instance)
9f95a23c
TL
711 elif self.t == CephOsdName:
712 # it just so happens all CephOsdName commands are named 'id' anyway,
713 # so <id|osd.id> is perfect.
714 chunk = '<id|osd.id>'
715 elif self.t == CephName:
716 # CephName commands similarly only have one arg of the
717 # type, so <type.id> is good.
718 chunk = '<type.id>'
719 elif self.t == CephInt:
720 chunk = '<{0}:int>'.format(self.name)
721 elif self.t == CephFloat:
722 chunk = '<{0}:float>'.format(self.name)
723 else:
724 chunk = '<{0}>'.format(self.name)
7c673cae
FG
725 s = chunk
726 if self.N:
9f95a23c 727 s += '...'
7c673cae 728 if not self.req:
9f95a23c 729 s = '[' + s + ']'
7c673cae
FG
730 return s
731
732 def complete(self, s):
733 return self.instance.complete(s)
734
735
736def concise_sig(sig):
737 """
738 Return string representation of sig useful for syntax reference in help
739 """
740 return ' '.join([d.helpstr() for d in sig])
741
742
743def descsort_key(sh):
744 """
745 sort descriptors by prefixes, defined as the concatenation of all simple
746 strings in the descriptor; this works out to just the leading strings.
747 """
748 return concise_sig(sh['sig'])
749
750
751def descsort(sh1, sh2):
752 """
753 Deprecated; use (key=descsort_key) instead of (cmp=descsort)
754 """
755 return cmp(descsort_key(sh1), descsort_key(sh2))
756
757
758def parse_funcsig(sig):
759 """
760 parse a single descriptor (array of strings or dicts) into a
761 dict of function descriptor/validators (objects of CephXXX type)
11fdf7f2
TL
762
763 :returns: list of ``argdesc``
7c673cae
FG
764 """
765 newsig = []
766 argnum = 0
767 for desc in sig:
768 argnum += 1
769 if isinstance(desc, basestring):
770 t = CephPrefix
771 desc = {'type': t, 'name': 'prefix', 'prefix': desc}
772 else:
773 # not a simple string, must be dict
774 if 'type' not in desc:
775 s = 'JSON descriptor {0} has no type'.format(sig)
776 raise JsonFormat(s)
777 # look up type string in our globals() dict; if it's an
778 # object of type `type`, it must be a
779 # locally-defined class. otherwise, we haven't a clue.
780 if desc['type'] in globals():
781 t = globals()[desc['type']]
782 if not isinstance(t, type):
783 s = 'unknown type {0}'.format(desc['type'])
784 raise JsonFormat(s)
785 else:
786 s = 'unknown type {0}'.format(desc['type'])
787 raise JsonFormat(s)
788
789 kwargs = dict()
790 for key, val in desc.items():
791 if key not in ['type', 'name', 'n', 'req']:
792 kwargs[key] = val
793 newsig.append(argdesc(t,
794 name=desc.get('name', None),
795 n=desc.get('n', 1),
796 req=desc.get('req', True),
797 **kwargs))
798 return newsig
799
800
801def parse_json_funcsigs(s, consumer):
802 """
803 A function signature is mostly an array of argdesc; it's represented
804 in JSON as
805 {
806 "cmd001": {"sig":[ "type": type, "name": name, "n": num, "req":true|false <other param>], "help":helptext, "module":modulename, "perm":perms, "avail":availability}
807 .
808 .
809 .
810 ]
811
812 A set of sigs is in an dict mapped by a unique number:
813 {
814 "cmd1": {
815 "sig": ["type.. ], "help":helptext...
816 }
817 "cmd2"{
818 "sig": [.. ], "help":helptext...
819 }
820 }
821
822 Parse the string s and return a dict of dicts, keyed by opcode;
823 each dict contains 'sig' with the array of descriptors, and 'help'
824 with the helptext, 'module' with the module name, 'perm' with a
825 string representing required permissions in that module to execute
826 this command (and also whether it is a read or write command from
827 the cluster state perspective), and 'avail' as a hint for
828 whether the command should be advertised by CLI, REST, or both.
829 If avail does not contain 'consumer', don't include the command
830 in the returned dict.
831 """
832 try:
833 overall = json.loads(s)
834 except Exception as e:
835 print("Couldn't parse JSON {0}: {1}".format(s, e), file=sys.stderr)
836 raise e
837 sigdict = {}
838 for cmdtag, cmd in overall.items():
839 if 'sig' not in cmd:
840 s = "JSON descriptor {0} has no 'sig'".format(cmdtag)
841 raise JsonFormat(s)
842 # check 'avail' and possibly ignore this command
843 if 'avail' in cmd:
844 if consumer not in cmd['avail']:
845 continue
846 # rewrite the 'sig' item with the argdesc-ized version, and...
847 cmd['sig'] = parse_funcsig(cmd['sig'])
848 # just take everything else as given
849 sigdict[cmdtag] = cmd
850 return sigdict
851
852
853def validate_one(word, desc, partial=False):
854 """
855 validate_one(word, desc, partial=False)
856
857 validate word against the constructed instance of the type
858 in desc. May raise exception. If it returns false (and doesn't
859 raise an exception), desc.instance.val will
860 contain the validated value (in the appropriate type).
861 """
862 desc.instance.valid(word, partial)
863 desc.numseen += 1
864 if desc.N:
865 desc.n = desc.numseen + 1
866
867
868def matchnum(args, signature, partial=False):
869 """
870 matchnum(s, signature, partial=False)
871
872 Returns number of arguments matched in s against signature.
873 Can be used to determine most-likely command for full or partial
874 matches (partial applies to string matches).
875 """
876 words = args[:]
877 mysig = copy.deepcopy(signature)
878 matchcnt = 0
879 for desc in mysig:
11fdf7f2 880 desc.numseen = 0
7c673cae
FG
881 while desc.numseen < desc.n:
882 # if there are no more arguments, return
883 if not words:
884 return matchcnt
885 word = words.pop(0)
886
887 try:
888 # only allow partial matching if we're on the last supplied
889 # word; avoid matching foo bar and foot bar just because
890 # partial is set
891 validate_one(word, desc, partial and (len(words) == 0))
892 valid = True
893 except ArgumentError:
894 # matchnum doesn't care about type of error
895 valid = False
896
897 if not valid:
898 if not desc.req:
899 # this wasn't required, so word may match the next desc
900 words.insert(0, word)
901 break
902 else:
903 # it was required, and didn't match, return
904 return matchcnt
905 if desc.req:
906 matchcnt += 1
907 return matchcnt
908
909
7c673cae
FG
910def store_arg(desc, d):
911 '''
912 Store argument described by, and held in, thanks to valid(),
913 desc into the dictionary d, keyed by desc.name. Three cases:
914
915 1) desc.N is set: value in d is a list
916 2) prefix: multiple args are joined with ' ' into one d{} item
917 3) single prefix or other arg: store as simple value
918
919 Used in validate() below.
920 '''
921 if desc.N:
922 # value should be a list
923 if desc.name in d:
924 d[desc.name] += [desc.instance.val]
925 else:
926 d[desc.name] = [desc.instance.val]
927 elif (desc.t == CephPrefix) and (desc.name in d):
928 # prefixes' values should be a space-joined concatenation
929 d[desc.name] += ' ' + desc.instance.val
930 else:
931 # if first CephPrefix or any other type, just set it
932 d[desc.name] = desc.instance.val
933
934
935def validate(args, signature, flags=0, partial=False):
936 """
937 validate(args, signature, flags=0, partial=False)
938
11fdf7f2 939 args is a list of strings representing a possible
7c673cae
FG
940 command input following format of signature. Runs a validation; no
941 exception means it's OK. Return a dict containing all arguments keyed
942 by their descriptor name, with duplicate args per name accumulated
943 into a list (or space-separated value for CephPrefix).
944
945 Mismatches of prefix are non-fatal, as this probably just means the
946 search hasn't hit the correct command. Mismatches of non-prefix
947 arguments are treated as fatal, and an exception raised.
948
949 This matching is modified if partial is set: allow partial matching
950 (with partial dict returned); in this case, there are no exceptions
951 raised.
952 """
953
954 myargs = copy.deepcopy(args)
955 mysig = copy.deepcopy(signature)
956 reqsiglen = len([desc for desc in mysig if desc.req])
957 matchcnt = 0
958 d = dict()
959 save_exception = None
960
11fdf7f2
TL
961 arg_descs_by_name = dict([desc.name, desc] for desc in mysig
962 if desc.t != CephPrefix)
963
964 # Special case: detect "injectargs" (legacy way of modifying daemon
965 # configs) and permit "--" string arguments if so.
966 injectargs = myargs and myargs[0] == "injectargs"
967
968 # Make a pass through all arguments
7c673cae 969 for desc in mysig:
11fdf7f2
TL
970 desc.numseen = 0
971
7c673cae 972 while desc.numseen < desc.n:
11fdf7f2
TL
973 if myargs:
974 myarg = myargs.pop(0)
975 else:
976 myarg = None
7c673cae
FG
977
978 # no arg, but not required? Continue consuming mysig
979 # in case there are later required args
31f18b77 980 if myarg in (None, []) and not desc.req:
7c673cae
FG
981 break
982
11fdf7f2
TL
983 # A keyword argument?
984 if myarg:
985 # argdesc for the keyword argument, if we find one
986 kwarg_desc = None
987
988 # Track whether we need to push value back onto
989 # myargs in the case that this isn't a valid k=v
990 consumed_next = False
991
992 # Try both styles of keyword argument
993 kwarg_match = re.match(KWARG_EQUALS, myarg)
994 if kwarg_match:
995 # We have a "--foo=bar" style argument
996 kwarg_k, kwarg_v = kwarg_match.groups()
997
998 # Either "--foo-bar" or "--foo_bar" style is accepted
999 kwarg_k = kwarg_k.replace('-', '_')
1000
1001 kwarg_desc = arg_descs_by_name.get(kwarg_k, None)
1002 else:
1003 # Maybe this is a "--foo bar" or "--bool" style argument
1004 key_match = re.match(KWARG_SPACE, myarg)
1005 if key_match:
1006 kwarg_k = key_match.group(1)
1007
1008 # Permit --foo-bar=123 form or --foo_bar=123 form,
1009 # assuming all command definitions use foo_bar argument
1010 # naming style
1011 kwarg_k = kwarg_k.replace('-', '_')
1012
1013 kwarg_desc = arg_descs_by_name.get(kwarg_k, None)
1014 if kwarg_desc:
1015 if kwarg_desc.t == CephBool:
1016 kwarg_v = 'true'
1017 elif len(myargs): # Some trailing arguments exist
1018 kwarg_v = myargs.pop(0)
1019 else:
1020 # Forget it, this is not a valid kwarg
1021 kwarg_desc = None
1022
1023 if kwarg_desc:
1024 validate_one(kwarg_v, kwarg_desc)
9f95a23c 1025 matchcnt += 1
11fdf7f2
TL
1026 store_arg(kwarg_desc, d)
1027 continue
1028
1029 # Don't handle something as a positional argument if it
1030 # has a leading "--" unless it's a CephChoices (used for
1031 # "--yes-i-really-mean-it")
1032 if myarg and myarg.startswith("--"):
1033 # Special cases for instances of confirmation flags
1034 # that were defined as CephString/CephChoices instead of CephBool
1035 # in pre-nautilus versions of Ceph daemons.
1036 is_value = desc.t == CephChoices \
1037 or myarg == "--yes-i-really-mean-it" \
1038 or myarg == "--yes-i-really-really-mean-it" \
1039 or myarg == "--yes-i-really-really-mean-it-not-faking" \
1040 or myarg == "--force" \
1041 or injectargs
1042
1043 if not is_value:
1044 # Didn't get caught by kwarg handling, but has a "--", so
1045 # we must assume it's something invalid, to avoid naively
1046 # passing through mis-typed options as the values of
1047 # positional arguments.
1048 raise ArgumentValid("Unexpected argument '{0}'".format(
1049 myarg))
1050
7c673cae
FG
1051 # out of arguments for a required param?
1052 # Either return (if partial validation) or raise
31f18b77 1053 if myarg in (None, []) and desc.req:
7c673cae
FG
1054 if desc.N and desc.numseen < 1:
1055 # wanted N, didn't even get 1
1056 if partial:
1057 return d
1058 raise ArgumentNumber(
1059 'saw {0} of {1}, expected at least 1'.
1060 format(desc.numseen, desc)
1061 )
1062 elif not desc.N and desc.numseen < desc.n:
1063 # wanted n, got too few
1064 if partial:
1065 return d
1066 # special-case the "0 expected 1" case
1067 if desc.numseen == 0 and desc.n == 1:
94b18763 1068 raise ArgumentMissing(
7c673cae
FG
1069 'missing required parameter {0}'.format(desc)
1070 )
1071 raise ArgumentNumber(
1072 'saw {0} of {1}, expected {2}'.
1073 format(desc.numseen, desc, desc.n)
1074 )
1075 break
1076
1077 # Have an arg; validate it
1078 try:
1079 validate_one(myarg, desc)
1080 valid = True
1081 except ArgumentError as e:
1082 valid = False
11fdf7f2 1083
7c673cae
FG
1084 # argument mismatch
1085 if not desc.req:
1086 # if not required, just push back; it might match
1087 # the next arg
11fdf7f2 1088 save_exception = [ myarg, e ]
7c673cae
FG
1089 myargs.insert(0, myarg)
1090 break
1091 else:
1092 # hm, it was required, so time to return/raise
1093 if partial:
1094 return d
11fdf7f2 1095 raise
7c673cae
FG
1096
1097 # Whew, valid arg acquired. Store in dict
1098 matchcnt += 1
1099 store_arg(desc, d)
1100 # Clear prior exception
1101 save_exception = None
1102
1103 # Done with entire list of argdescs
1104 if matchcnt < reqsiglen:
1105 raise ArgumentTooFew("not enough arguments given")
1106
1107 if myargs and not partial:
1108 if save_exception:
1109 print(save_exception[0], 'not valid: ', save_exception[1], file=sys.stderr)
1110 raise ArgumentError("unused arguments: " + str(myargs))
1111
11fdf7f2 1112 if flags & Flag.MGR:
9f95a23c 1113 d['target'] = ('mon-mgr','')
7c673cae 1114
11fdf7f2
TL
1115 if flags & Flag.POLL:
1116 d['poll'] = True
1117
7c673cae
FG
1118 # Finally, success
1119 return d
1120
1121
7c673cae
FG
1122def validate_command(sigdict, args, verbose=False):
1123 """
11fdf7f2
TL
1124 Parse positional arguments into a parameter dict, according to
1125 the command descriptions.
1126
9f95a23c 1127 Writes advice about nearly-matching commands ``sys.stderr`` if
11fdf7f2
TL
1128 the arguments do not match any command.
1129
1130 :param sigdict: A command description dictionary, as returned
1131 from Ceph daemons by the get_command_descriptions
1132 command.
1133 :param args: List of strings, should match one of the command
1134 signatures in ``sigdict``
1135
1136 :returns: A dict of parsed parameters (including ``prefix``),
1137 or an empty dict if the args did not match any signature
7c673cae
FG
1138 """
1139 if verbose:
1140 print("validate_command: " + " ".join(args), file=sys.stderr)
1141 found = []
1142 valid_dict = {}
7c673cae 1143
11fdf7f2
TL
1144 # look for best match, accumulate possibles in bestcmds
1145 # (so we can maybe give a more-useful error message)
1146 best_match_cnt = 0
1147 bestcmds = []
1148 for cmd in sigdict.values():
1149 flags = cmd.get('flags', 0)
1150 if flags & Flag.OBSOLETE:
1151 continue
1152 sig = cmd['sig']
1153 matched = matchnum(args, sig, partial=True)
1154 if (matched >= math.floor(best_match_cnt) and
1155 matched == matchnum(args, sig, partial=False)):
1156 # prefer those fully matched over partial patch
1157 matched += 0.5
1158 if matched < best_match_cnt:
1159 continue
7c673cae 1160 if verbose:
11fdf7f2
TL
1161 print("better match: {0} > {1}: {2} ".format(
1162 matched, best_match_cnt, concise_sig(sig)
1163 ), file=sys.stderr)
1164 if matched > best_match_cnt:
1165 best_match_cnt = matched
1166 bestcmds = [cmd]
94b18763 1167 else:
11fdf7f2
TL
1168 bestcmds.append(cmd)
1169
9f95a23c 1170 # Sort bestcmds by number of req args so we can try shortest first
11fdf7f2 1171 # (relies on a cmdsig being key,val where val is a list of len 1)
11fdf7f2 1172
9f95a23c
TL
1173 def grade(cmd):
1174 # prefer optional arguments over required ones
1175 sigs = cmd['sig']
1176 return sum(map(lambda sig: sig.req, sigs))
1177
1178 bestcmds_sorted = sorted(bestcmds, key=grade)
11fdf7f2
TL
1179 if verbose:
1180 print("bestcmds_sorted: ", file=sys.stderr)
1181 pprint.PrettyPrinter(stream=sys.stderr).pprint(bestcmds_sorted)
1182
1183 ex = None
1184 # for everything in bestcmds, look for a true match
1185 for cmd in bestcmds_sorted:
1186 sig = cmd['sig']
1187 try:
1188 valid_dict = validate(args, sig, flags=cmd.get('flags', 0))
1189 found = cmd
1190 break
1191 except ArgumentPrefix:
1192 # ignore prefix mismatches; we just haven't found
1193 # the right command yet
1194 pass
1195 except ArgumentMissing as e:
1196 ex = e
1197 if len(bestcmds) == 1:
1198 found = cmd
1199 break
1200 except ArgumentTooFew:
1201 # It looked like this matched the beginning, but it
1202 # didn't have enough args supplied. If we're out of
1203 # cmdsigs we'll fall out unfound; if we're not, maybe
1204 # the next one matches completely. Whine, but pass.
1205 if verbose:
1206 print('Not enough args supplied for ',
1207 concise_sig(sig), file=sys.stderr)
1208 except ArgumentError as e:
1209 ex = e
1210 # Solid mismatch on an arg (type, range, etc.)
1211 # Stop now, because we have the right command but
1212 # some other input is invalid
1213 found = cmd
1214 break
1215
1216 if found:
1217 if not valid_dict:
1218 print("Invalid command:", ex, file=sys.stderr)
1219 print(concise_sig(sig), ': ', cmd['help'], file=sys.stderr)
1220 else:
1221 bestcmds = [c for c in bestcmds
1222 if not c.get('flags', 0) & (Flag.DEPRECATED | Flag.HIDDEN)]
1223 bestcmds = bestcmds[:10] # top 10
1224 print('no valid command found; {0} closest matches:'.format(len(bestcmds)), file=sys.stderr)
1225 for cmd in bestcmds:
1226 print(concise_sig(cmd['sig']), file=sys.stderr)
1227 return valid_dict
7c673cae
FG
1228
1229
94b18763 1230
7c673cae
FG
1231def find_cmd_target(childargs):
1232 """
1233 Using a minimal validation, figure out whether the command
1234 should be sent to a monitor or an osd. We do this before even
1235 asking for the 'real' set of command signatures, so we can ask the
1236 right daemon.
1237 Returns ('osd', osdid), ('pg', pgid), ('mgr', '') or ('mon', '')
1238 """
1239 sig = parse_funcsig(['tell', {'name': 'target', 'type': 'CephName'}])
1240 try:
1241 valid_dict = validate(childargs, sig, partial=True)
1242 except ArgumentError:
1243 pass
1244 else:
1245 if len(valid_dict) == 2:
1246 # revalidate to isolate type and id
1247 name = CephName()
1248 # if this fails, something is horribly wrong, as it just
1249 # validated successfully above
1250 name.valid(valid_dict['target'])
1251 return name.nametype, name.nameid
1252
1253 sig = parse_funcsig(['tell', {'name': 'pgid', 'type': 'CephPgid'}])
1254 try:
1255 valid_dict = validate(childargs, sig, partial=True)
1256 except ArgumentError:
1257 pass
1258 else:
1259 if len(valid_dict) == 2:
1260 # pg doesn't need revalidation; the string is fine
1261 return 'pg', valid_dict['pgid']
1262
1263 # If we reached this far it must mean that so far we've been unable to
1264 # obtain a proper target from childargs. This may mean that we are not
1265 # dealing with a 'tell' command, or that the specified target is invalid.
1266 # If the latter, we likely were unable to catch it because we were not
1267 # really looking for it: first we tried to parse a 'CephName' (osd, mon,
1268 # mds, followed by and id); given our failure to parse, we tried to parse
1269 # a 'CephPgid' instead (e.g., 0.4a). Considering we got this far though
1270 # we were unable to do so.
1271 #
1272 # We will now check if this is a tell and, if so, forcefully validate the
1273 # target as a 'CephName'. This must be so because otherwise we will end
1274 # up sending garbage to a monitor, which is the default target when a
1275 # target is not explicitly specified.
1276 # e.g.,
1277 # 'ceph status' -> target is any one monitor
1278 # 'ceph tell mon.* status -> target is all monitors
1279 # 'ceph tell foo status -> target is invalid!
1280 if len(childargs) > 1 and childargs[0] == 'tell':
1281 name = CephName()
1282 # CephName.valid() raises on validation error; find_cmd_target()'s
1283 # caller should handle them
1284 name.valid(childargs[1])
1285 return name.nametype, name.nameid
1286
1287 sig = parse_funcsig(['pg', {'name': 'pgid', 'type': 'CephPgid'}])
1288 try:
1289 valid_dict = validate(childargs, sig, partial=True)
1290 except ArgumentError:
1291 pass
1292 else:
1293 if len(valid_dict) == 2:
1294 return 'pg', valid_dict['pgid']
1295
1296 return 'mon', ''
1297
1298
1299class RadosThread(threading.Thread):
11fdf7f2 1300 def __init__(self, func, *args, **kwargs):
7c673cae
FG
1301 self.args = args
1302 self.kwargs = kwargs
11fdf7f2 1303 self.func = func
7c673cae
FG
1304 self.exception = None
1305 threading.Thread.__init__(self)
1306
1307 def run(self):
1308 try:
11fdf7f2 1309 self.retval = self.func(*self.args, **self.kwargs)
7c673cae
FG
1310 except Exception as e:
1311 self.exception = e
1312
1313
11fdf7f2 1314def run_in_thread(func, *args, **kwargs):
7c673cae
FG
1315 interrupt = False
1316 timeout = kwargs.pop('timeout', 0)
11fdf7f2
TL
1317 if timeout == 0 or timeout == None:
1318 # python threading module will just get blocked if timeout is `None`,
1319 # otherwise it will keep polling until timeout or thread stops.
9f95a23c
TL
1320 # wait for INT32_MAX, as python 3.6.8 use int32_t to present the
1321 # timeout in integer when converting it to nanoseconds
1322 timeout = (1 << (32 - 1)) - 1
11fdf7f2 1323 t = RadosThread(func, *args, **kwargs)
7c673cae
FG
1324
1325 # allow the main thread to exit (presumably, avoid a join() on this
1326 # subthread) before this thread terminates. This allows SIGINT
1327 # exit of a blocked call. See below.
1328 t.daemon = True
1329
1330 t.start()
11fdf7f2
TL
1331 t.join(timeout=timeout)
1332 # ..but allow SIGINT to terminate the waiting. Note: this
1333 # relies on the Linux kernel behavior of delivering the signal
1334 # to the main thread in preference to any subthread (all that's
1335 # strictly guaranteed is that *some* thread that has the signal
1336 # unblocked will receive it). But there doesn't seem to be
1337 # any interface to create a thread with SIGINT blocked.
1338 if t.is_alive():
1339 raise Exception("timed out")
1340 elif t.exception:
7c673cae 1341 raise t.exception
11fdf7f2
TL
1342 else:
1343 return t.retval
7c673cae
FG
1344
1345
1346def send_command_retry(*args, **kwargs):
1347 while True:
1348 try:
1349 return send_command(*args, **kwargs)
1350 except Exception as e:
11fdf7f2
TL
1351 # If our librados instance has not reached state 'connected'
1352 # yet, we'll see an exception like this and retry
7c673cae
FG
1353 if ('get_command_descriptions' in str(e) and
1354 'object in state configuring' in str(e)):
1355 continue
1356 else:
1357 raise
1358
1359def send_command(cluster, target=('mon', ''), cmd=None, inbuf=b'', timeout=0,
1360 verbose=False):
1361 """
1362 Send a command to a daemon using librados's
11fdf7f2 1363 mon_command, osd_command, mgr_command, or pg_command. Any bulk input data
7c673cae
FG
1364 comes in inbuf.
1365
1366 Returns (ret, outbuf, outs); ret is the return code, outbuf is
1367 the outbl "bulk useful output" buffer, and outs is any status
1368 or error message (intended for stderr).
1369
1370 If target is osd.N, send command to that osd (except for pgid cmds)
1371 """
1372 cmd = cmd or []
1373 try:
1374 if target[0] == 'osd':
1375 osdid = target[1]
1376
1377 if verbose:
1378 print('submit {0} to osd.{1}'.format(cmd, osdid),
1379 file=sys.stderr)
1380 ret, outbuf, outs = run_in_thread(
11fdf7f2 1381 cluster.osd_command, osdid, cmd, inbuf, timeout=timeout)
7c673cae
FG
1382
1383 elif target[0] == 'mgr':
9f95a23c
TL
1384 name = '' # non-None empty string means "current active mgr"
1385 if len(target) > 1 and target[1] is not None:
1386 name = target[1]
1387 if verbose:
1388 print('submit {0} to {1} name {2}'.format(cmd, target[0], name),
1389 file=sys.stderr)
1390 ret, outbuf, outs = run_in_thread(
1391 cluster.mgr_command, cmd, inbuf, timeout=timeout, target=name)
1392
1393 elif target[0] == 'mon-mgr':
1394 if verbose:
1395 print('submit {0} to {1}'.format(cmd, target[0]),
1396 file=sys.stderr)
7c673cae 1397 ret, outbuf, outs = run_in_thread(
11fdf7f2 1398 cluster.mgr_command, cmd, inbuf, timeout=timeout)
7c673cae
FG
1399
1400 elif target[0] == 'pg':
1401 pgid = target[1]
1402 # pgid will already be in the command for the pg <pgid>
1403 # form, but for tell <pgid>, we need to put it in
1404 if cmd:
1405 cmddict = json.loads(cmd[0])
1406 cmddict['pgid'] = pgid
1407 else:
1408 cmddict = dict(pgid=pgid)
1409 cmd = [json.dumps(cmddict)]
1410 if verbose:
1411 print('submit {0} for pgid {1}'.format(cmd, pgid),
1412 file=sys.stderr)
1413 ret, outbuf, outs = run_in_thread(
11fdf7f2 1414 cluster.pg_command, pgid, cmd, inbuf, timeout=timeout)
7c673cae
FG
1415
1416 elif target[0] == 'mon':
1417 if verbose:
1418 print('{0} to {1}'.format(cmd, target[0]),
1419 file=sys.stderr)
31f18b77 1420 if len(target) < 2 or target[1] == '':
7c673cae 1421 ret, outbuf, outs = run_in_thread(
11fdf7f2 1422 cluster.mon_command, cmd, inbuf, timeout=timeout)
7c673cae
FG
1423 else:
1424 ret, outbuf, outs = run_in_thread(
11fdf7f2 1425 cluster.mon_command, cmd, inbuf, timeout=timeout, target=target[1])
7c673cae
FG
1426 elif target[0] == 'mds':
1427 mds_spec = target[1]
1428
1429 if verbose:
1430 print('submit {0} to mds.{1}'.format(cmd, mds_spec),
1431 file=sys.stderr)
1432
1433 try:
1434 from cephfs import LibCephFS
1435 except ImportError:
1436 raise RuntimeError("CephFS unavailable, have you installed libcephfs?")
1437
b32b8144 1438 filesystem = LibCephFS(rados_inst=cluster)
7c673cae
FG
1439 filesystem.init()
1440 ret, outbuf, outs = \
1441 filesystem.mds_command(mds_spec, cmd, inbuf)
1442 filesystem.shutdown()
1443 else:
1444 raise ArgumentValid("Bad target type '{0}'".format(target[0]))
1445
1446 except Exception as e:
1447 if not isinstance(e, ArgumentError):
1448 raise RuntimeError('"{0}": exception {1}'.format(cmd, e))
1449 else:
1450 raise
1451
1452 return ret, outbuf, outs
1453
1454
1455def json_command(cluster, target=('mon', ''), prefix=None, argdict=None,
1456 inbuf=b'', timeout=0, verbose=False):
1457 """
11fdf7f2 1458 Serialize a command and up a JSON command and send it with send_command() above.
7c673cae
FG
1459 Prefix may be supplied separately or in argdict. Any bulk input
1460 data comes in inbuf.
1461
1462 If target is osd.N, send command to that osd (except for pgid cmds)
11fdf7f2
TL
1463
1464 :param cluster: ``rados.Rados`` instance
1465 :param prefix: String to inject into command arguments as 'prefix'
1466 :param argdict: Command arguments
7c673cae
FG
1467 """
1468 cmddict = {}
1469 if prefix:
1470 cmddict.update({'prefix': prefix})
11fdf7f2 1471
7c673cae
FG
1472 if argdict:
1473 cmddict.update(argdict)
1474 if 'target' in argdict:
1475 target = argdict.get('target')
1476
7c673cae
FG
1477 try:
1478 if target[0] == 'osd':
1479 osdtarg = CephName()
1480 osdtarget = '{0}.{1}'.format(*target)
1481 # prefer target from cmddict if present and valid
1482 if 'target' in cmddict:
1483 osdtarget = cmddict.pop('target')
1484 try:
1485 osdtarg.valid(osdtarget)
1486 target = ('osd', osdtarg.nameid)
1487 except:
1488 # use the target we were originally given
1489 pass
7c673cae
FG
1490 ret, outbuf, outs = send_command_retry(cluster,
1491 target, [json.dumps(cmddict)],
1492 inbuf, timeout, verbose)
1493
1494 except Exception as e:
1495 if not isinstance(e, ArgumentError):
1496 raise RuntimeError('"{0}": exception {1}'.format(argdict, e))
1497 else:
1498 raise
1499
1500 return ret, outbuf, outs