1 # Copyright 2012-2013 GRNET S.A. All rights reserved.
3 # Redistribution and use in source and binary forms, with or
4 # without modification, are permitted provided that the following
7 # 1. Redistributions of source code must retain the above
8 # copyright notice, this list of conditions and the following
11 # 2. Redistributions in binary form must reproduce the above
12 # copyright notice, this list of conditions and the following
13 # disclaimer in the documentation and/or other materials
14 # provided with the distribution.
16 # THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS
17 # OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
18 # WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
19 # PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR
20 # CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21 # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22 # LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
23 # USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
24 # AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
25 # LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
26 # ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
27 # POSSIBILITY OF SUCH DAMAGE.
29 # The views and conclusions contained in the software and
30 # documentation are those of the authors and should not be
31 # interpreted as representing official policies, either expressed
32 # or implied, of GRNET S.A.
34 from kamaki.cli.config import Config
35 from kamaki.cli.errors import CLISyntaxError, raiseCLIError
36 from kamaki.cli.utils import split_input, to_bytes
38 from datetime import datetime as dtm
39 from time import mktime
40 from sys import stderr
42 from logging import getLogger
43 from argparse import ArgumentParser, ArgumentError
44 from argparse import RawDescriptionHelpFormatter
45 from progress.bar import ShadyBar as KamakiProgressBar
47 log = getLogger(__name__)
50 class Argument(object):
51 """An argument that can be parsed from command line or otherwise.
52 This is the top-level Argument class. It is suggested to extent this
53 class into more specific argument types.
56 def __init__(self, arity, help=None, parsed_name=None, default=None):
57 self.arity = int(arity)
58 self.help = '%s' % help or ''
60 assert parsed_name, 'No parsed name for argument %s' % self
61 self.parsed_name = list(parsed_name) if isinstance(
62 parsed_name, list) or isinstance(parsed_name, tuple) else (
63 '%s' % parsed_name).split()
64 for name in self.parsed_name:
65 assert name.count(' ') == 0, '%s: Invalid parse name "%s"' % (
67 msg = '%s: Invalid parse name "%s" should start with a "-"' % (
69 assert name.startswith('-'), msg
71 self.default = default if (default or self.arity) else False
75 return getattr(self, '_value', self.default)
78 def value(self, newvalue):
79 self._value = newvalue
81 def update_parser(self, parser, name):
82 """Update argument parser with self info"""
83 action = 'append' if self.arity < 0 else (
84 'store' if self.arity else 'store_true')
87 dest=name, action=action, default=self.default, help=self.help)
90 class ConfigArgument(Argument):
91 """Manage a kamaki configuration (file)"""
93 def __init__(self, help, parsed_name=('-c', '--config')):
94 super(ConfigArgument, self).__init__(1, help, parsed_name, None)
99 return super(ConfigArgument, self).value
102 def value(self, config_file):
104 self._value = Config(config_file)
105 self.file_path = config_file
107 self._value = Config(self.file_path)
109 self._value = Config()
111 def get(self, group, term):
112 """Get a configuration setting from the Config object"""
113 return self.value.get(group, term)
119 return [term[:-slen] for term in self.value.keys('global') if (
120 term.endswith(suffix))]
126 return [(k[:-slen], v) for k, v in self.value.items('global') if (
129 def get_global(self, option):
130 return self.value.get('global', option)
132 def get_cloud(self, cloud, option):
133 return self.value.get_cloud(cloud, option)
136 _config_arg = ConfigArgument('Path to config file')
139 class RuntimeConfigArgument(Argument):
140 """Set a run-time setting option (not persistent)"""
142 def __init__(self, config_arg, help='', parsed_name=None, default=None):
143 super(self.__class__, self).__init__(1, help, parsed_name, default)
144 self._config_arg = config_arg
148 return super(RuntimeConfigArgument, self).value
151 def value(self, options):
152 if options == self.default:
154 if not isinstance(options, list):
155 options = ['%s' % options]
156 for option in options:
157 keypath, sep, val = option.partition('=')
160 CLISyntaxError('Argument Syntax Error '),
162 '%s is missing a "="',
163 ' (usage: -o section.key=val)' % option])
164 section, sep, key = keypath.partition('.')
168 self._config_arg.value.override(
174 class FlagArgument(Argument):
176 :value: true if set, false otherwise
179 def __init__(self, help='', parsed_name=None, default=False):
180 super(FlagArgument, self).__init__(0, help, parsed_name, default)
183 class ValueArgument(Argument):
186 :value returns: given value or default
189 def __init__(self, help='', parsed_name=None, default=None):
190 super(ValueArgument, self).__init__(1, help, parsed_name, default)
193 class CommaSeparatedListArgument(ValueArgument):
196 :value returns: list of the comma separated values
201 return self._value or list()
204 def value(self, newvalue):
205 self._value = newvalue.split(',') if newvalue else list()
208 class IntArgument(ValueArgument):
212 """integer (type checking)"""
213 return getattr(self, '_value', self.default)
216 def value(self, newvalue):
217 if newvalue == self.default:
218 self._value = newvalue
221 if int(newvalue) == float(newvalue):
222 self._value = int(newvalue)
224 raise ValueError('Raise int argument error')
226 raiseCLIError(CLISyntaxError(
228 details=['Value %s not an int' % newvalue]))
231 class DataSizeArgument(ValueArgument):
232 """Input: a string of the form <number><unit>
233 Output: the number of bytes
234 Units: B, KiB, KB, MiB, MB, GiB, GB, TiB, TB
239 return getattr(self, '_value', self.default)
241 def _calculate_limit(self, user_input):
244 limit = int(user_input)
247 digits = [str(num) for num in range(0, 10)] + ['.']
248 while user_input[index] in digits:
250 limit = user_input[:index]
251 format = user_input[index:]
253 return to_bytes(limit, format)
254 except Exception as qe:
255 msg = 'Failed to convert %s to bytes' % user_input,
256 raiseCLIError(qe, msg, details=[
257 'Syntax: containerlimit set <limit>[format] [container]',
258 'e.g.,: containerlimit set 2.3GB mycontainer',
260 '(*1024): B, KiB, MiB, GiB, TiB',
261 '(*1000): B, KB, MB, GB, TB'])
265 def value(self, new_value):
267 self._value = self._calculate_limit(new_value)
270 class DateArgument(ValueArgument):
272 DATE_FORMAT = '%a %b %d %H:%M:%S %Y'
274 INPUT_FORMATS = [DATE_FORMAT, '%d-%m-%Y', '%H:%M:%S %d-%m-%Y']
278 v = getattr(self, '_value', self.default)
279 return mktime(v.timetuple()) if v else None
283 v = getattr(self, '_value', self.default)
284 return v.strftime(self.DATE_FORMAT) if v else None
288 return self.timestamp
291 def value(self, newvalue):
292 self._value = self.format_date(newvalue) if newvalue else self.default
294 def format_date(self, datestr):
295 for format in self.INPUT_FORMATS:
297 t = dtm.strptime(datestr, format)
300 return t # .strftime(self.DATE_FORMAT)
301 raiseCLIError(None, 'Date Argument Error', details=[
302 '%s not a valid date' % datestr,
303 'Correct formats:\n\t%s' % self.INPUT_FORMATS])
306 class VersionArgument(FlagArgument):
307 """A flag argument with that prints current version"""
312 return super(self.__class__, self).value
315 def value(self, newvalue):
316 self._value = newvalue
319 print('kamaki %s' % kamaki.__version__)
322 class RepeatableArgument(Argument):
323 """A value argument that can be repeated"""
325 def __init__(self, help='', parsed_name=None, default=[]):
326 super(RepeatableArgument, self).__init__(
327 -1, help, parsed_name, default)
330 class KeyValueArgument(Argument):
331 """A Key=Value Argument that can be repeated
333 :syntax: --<arg> key1=value1 --<arg> key2=value2 ...
336 def __init__(self, help='', parsed_name=None, default=[]):
337 super(KeyValueArgument, self).__init__(-1, help, parsed_name, default)
342 :returns: (dict) {key1: val1, key2: val2, ...}
344 return super(KeyValueArgument, self).value
347 def value(self, keyvalue_pairs):
349 :param keyvalue_pairs: (str) ['key1=val1', 'key2=val2', ...]
351 self._value = getattr(self, '_value', self.value) or {}
353 for pair in keyvalue_pairs:
354 key, sep, val = pair.partition('=')
355 assert sep, ' %s misses a "=" (usage: key1=val1 )\n' % (pair)
356 self._value[key] = val
357 except Exception as e:
358 raiseCLIError(e, 'KeyValueArgument Syntax Error')
361 class ProgressBarArgument(FlagArgument):
362 """Manage a progress bar"""
364 def __init__(self, help='', parsed_name='', default=True):
365 self.suffix = '%(percent)d%%'
366 super(ProgressBarArgument, self).__init__(help, parsed_name, default)
369 """Get a modifiable copy of this bar"""
370 newarg = ProgressBarArgument(
371 self.help, self.parsed_name, self.default)
372 newarg._value = self._value
376 self, message, message_len=25, countdown=False, timeout=100):
377 """Get a generator to handle progress of the bar (gen.next())"""
381 self.bar = KamakiProgressBar()
386 bar_phases = list(self.bar.phases)
387 self.bar.empty_fill, bar_phases[0] = bar_phases[-1], ''
389 self.bar.phases = bar_phases
390 self.bar.bar_prefix = ' '
391 self.bar.bar_suffix = ' '
392 self.bar.max = timeout or 100
393 self.bar.suffix = '%(remaining)ds to timeout'
395 self.bar.suffix = '%(percent)d%% - %(eta)ds'
396 self.bar.eta = timeout or 100
397 self.bar.message = message.ljust(message_len)
401 for i in self.bar.iter(range(int(n))):
407 """Stop progress bar, return terminal cursor to user"""
410 mybar = getattr(self, 'bar', None)
417 cloud=ValueArgument('Chose a cloud to connect to', ('--cloud')),
418 help=Argument(0, 'Show help message', ('-h', '--help')),
419 debug=FlagArgument('Include debug output', ('-d', '--debug')),
420 #include=FlagArgument(
421 # 'Include raw connection data in the output', ('-i', '--include')),
422 silent=FlagArgument('Do not output anything', ('-s', '--silent')),
423 verbose=FlagArgument('More info at response', ('-v', '--verbose')),
424 version=VersionArgument('Print current version', ('-V', '--version')),
425 options=RuntimeConfigArgument(
426 _config_arg, 'Override a config value', ('-o', '--options'))
430 # Initial command line interface arguments
433 class ArgumentParseManager(object):
434 """Manage (initialize and update) an ArgumentParser object"""
438 arguments=None, required=None, syntax=None, description=None):
440 :param exe: (str) the basic command (e.g. 'kamaki')
442 :param arguments: (dict) if given, overrides the global _argument as
443 the parsers arguments specification
444 :param required: (list or tuple) an iterable of argument keys, denoting
445 which arguments are required. A tuple denoted an AND relation,
446 while a list denotes an OR relation e.g., ['a', 'b'] means that
447 either 'a' or 'b' is required, while ('a', 'b') means that both 'a'
449 Nesting is allowed e.g., ['a', ('b', 'c'), ['d', 'e']] means that
450 this command required either 'a', or both 'b' and 'c', or one of
452 Repeated arguments are also allowed e.g., [('a', 'b'), ('a', 'c'),
453 ['b', 'c']] means that the command required either 'a' and 'b' or
454 'a' and 'c' or at least one of 'b', 'c' and could be written as
455 [('a', ['b', 'c']), ['b', 'c']]
456 :param syntax: (str) The basic syntax of the arguments. Default:
457 exe <cmd_group> [<cmd_subbroup> ...] <cmd>
458 :param description: (str) The description of the commands or ''
460 self.parser = ArgumentParser(
461 add_help=False, formatter_class=RawDescriptionHelpFormatter)
463 self.syntax = syntax or (
464 '%s <cmd_group> [<cmd_subbroup> ...] <cmd>' % exe)
465 self.required = required
466 self.parser.description = description or ''
468 self.arguments = arguments
471 self.arguments = _arguments
472 self._parser_modified, self._parsed, self._unparsed = False, None, None
476 def required2list(required):
477 if isinstance(required, list) or isinstance(required, tuple):
480 terms.append(ArgumentParseManager.required2list(r))
481 return list(set(terms).union())
485 def required2str(required, arguments, tab=''):
486 if isinstance(required, list):
487 return ' %sat least one:\n%s' % (tab, ''.join(
488 [ArgumentParseManager.required2str(
489 r, arguments, tab + ' ') for r in required]))
490 elif isinstance(required, tuple):
491 return ' %sall:\n%s' % (tab, ''.join(
492 [ArgumentParseManager.required2str(
493 r, arguments, tab + ' ') for r in required]))
495 lt_pn, lt_all, arg = 23, 80, arguments[required]
497 ret = '%s%s' % (tab, ', '.join(arg.parsed_name))
499 ret += ' %s' % required.upper()
500 ret = ('{:<%s}' % lt_pn).format(ret)
501 prefix = ('\n%s' % tab2) if len(ret) > lt_pn else ' '
503 while arg.help[cur:]:
504 next = cur + lt_all - lt_pn
506 ret += ('{:<%s}' % (lt_all - lt_pn)).format(arg.help[cur:next])
507 cur, finish = next, '\n%s' % tab2
511 def _patch_with_required_args(arguments, required):
512 if isinstance(required, tuple):
513 return ' '.join([ArgumentParseManager._patch_with_required_args(
514 arguments, k) for k in required])
515 elif isinstance(required, list):
516 return '< %s >' % ' | '.join([
517 ArgumentParseManager._patch_with_required_args(
518 arguments, k) for k in required])
519 arg = arguments[required]
520 return '/'.join(arg.parsed_name) + (
521 ' %s [...]' % required.upper() if arg.arity < 0 else (
522 ' %s' % required.upper() if arg.arity else ''))
524 def print_help(self, out=stderr):
526 tmp_args = dict(self.arguments)
527 for term in self.required2list(self.required):
529 tmp_parser = ArgumentParseManager(self._exe, tmp_args)
530 tmp_parser.syntax = self.syntax + self._patch_with_required_args(
531 self.arguments, self.required)
532 tmp_parser.parser.description = '%s\n\nrequired arguments:\n%s' % (
533 self.parser.description,
534 self.required2str(self.required, self.arguments))
535 tmp_parser.update_parser()
536 tmp_parser.parser.print_help()
538 self.parser.print_help()
542 """The command syntax (useful for help messages, descriptions, etc)"""
543 return self.parser.prog
546 def syntax(self, new_syntax):
547 self.parser.prog = new_syntax
551 """:returns: (dict) arguments the parser should be aware of"""
552 return self._arguments
555 def arguments(self, new_arguments):
556 assert isinstance(new_arguments, dict), 'Arguments must be in a dict'
557 self._arguments = new_arguments
562 """(Namespace) parser-matched terms"""
563 if self._parser_modified:
569 """(list) parser-unmatched terms"""
570 if self._parser_modified:
572 return self._unparsed
574 def update_parser(self, arguments=None):
575 """Load argument specifications to parser
577 :param arguments: if not given, update self.arguments instead
579 arguments = arguments or self._arguments
581 for name, arg in arguments.items():
583 arg.update_parser(self.parser, name)
584 self._parser_modified = True
585 except ArgumentError:
588 def update_arguments(self, new_arguments):
589 """Add to / update existing arguments
591 :param new_arguments: (dict)
594 assert isinstance(new_arguments, dict), 'Arguments not in dict !!!'
595 self._arguments.update(new_arguments)
598 def _parse_required_arguments(self, required, parsed_args):
601 if isinstance(required, tuple):
602 for item in required:
603 if not self._parse_required_arguments(item, parsed_args):
606 if isinstance(required, list):
607 for item in required:
608 if self._parse_required_arguments(item, parsed_args):
611 return required in parsed_args
613 def parse(self, new_args=None):
614 """Parse user input"""
616 pkargs = (new_args,) if new_args else ()
617 self._parsed, unparsed = self.parser.parse_known_args(*pkargs)
619 k for k, v in vars(self._parsed).items() if v not in (None, )]
620 if not self._parse_required_arguments(self.required, parsed_args):
622 raise CLISyntaxError('Missing required arguments')
624 raiseCLIError(CLISyntaxError('Argument Syntax Error'))
625 for name, arg in self.arguments.items():
626 arg.value = getattr(self._parsed, name, arg.default)
628 for term in unparsed:
629 self._unparsed += split_input(' \'%s\' ' % term)
630 self._parser_modified = False