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
38 from datetime import datetime as dtm
39 from time import mktime
41 from logging import getLogger
42 from argparse import ArgumentParser, ArgumentError
43 from argparse import RawDescriptionHelpFormatter
44 from progress.bar import ShadyBar as KamakiProgressBar
46 log = getLogger(__name__)
49 class Argument(object):
50 """An argument that can be parsed from command line or otherwise.
51 This is the top-level Argument class. It is suggested to extent this
52 class into more specific argument types.
55 def __init__(self, arity, help=None, parsed_name=None, default=None):
56 self.arity = int(arity)
57 self.help = '%s' % help or ''
59 assert parsed_name, 'No parsed name for argument %s' % self
60 self.parsed_name = list(parsed_name) if isinstance(
61 parsed_name, list) or isinstance(parsed_name, tuple) else (
62 '%s' % parsed_name).split()
63 for name in self.parsed_name:
64 assert name.count(' ') == 0, '%s: Invalid parse name "%s"' % (
66 msg = '%s: Invalid parse name "%s" should start with a "-"' % (
68 assert name.startswith('-'), msg
70 self.default = default if (default or self.arity) else False
74 return getattr(self, '_value', self.default)
77 def value(self, newvalue):
78 self._value = newvalue
80 def update_parser(self, parser, name):
81 """Update argument parser with self info"""
82 action = 'append' if self.arity < 0 else (
83 'store' if self.arity else 'store_true')
86 dest=name, action=action, default=self.default, help=self.help)
89 class ConfigArgument(Argument):
90 """Manage a kamaki configuration (file)"""
92 def __init__(self, help, parsed_name=('-c', '--config')):
93 super(ConfigArgument, self).__init__(1, help, parsed_name, None)
98 return super(ConfigArgument, self).value
101 def value(self, config_file):
103 self._value = Config(config_file)
104 self.file_path = config_file
106 self._value = Config(self.file_path)
108 self._value = Config()
110 def get(self, group, term):
111 """Get a configuration setting from the Config object"""
112 return self.value.get(group, term)
118 return [term[:-slen] for term in self.value.keys('global') if (
119 term.endswith(suffix))]
125 return [(k[:-slen], v) for k, v in self.value.items('global') if (
128 def get_global(self, option):
129 return self.value.get_global(option)
131 def get_cloud(self, cloud, option):
132 return self.value.get_cloud(cloud, option)
135 _config_arg = ConfigArgument('Path to config file')
138 class RuntimeConfigArgument(Argument):
139 """Set a run-time setting option (not persistent)"""
141 def __init__(self, config_arg, help='', parsed_name=None, default=None):
142 super(self.__class__, self).__init__(1, help, parsed_name, default)
143 self._config_arg = config_arg
147 return super(RuntimeConfigArgument, self).value
150 def value(self, options):
151 if options == self.default:
153 if not isinstance(options, list):
154 options = ['%s' % options]
155 for option in options:
156 keypath, sep, val = option.partition('=')
159 CLISyntaxError('Argument Syntax Error '),
161 '%s is missing a "="',
162 ' (usage: -o section.key=val)' % option])
163 section, sep, key = keypath.partition('.')
167 self._config_arg.value.override(
173 class FlagArgument(Argument):
175 :value: true if set, false otherwise
178 def __init__(self, help='', parsed_name=None, default=False):
179 super(FlagArgument, self).__init__(0, help, parsed_name, default)
182 class ValueArgument(Argument):
185 :value returns: given value or default
188 def __init__(self, help='', parsed_name=None, default=None):
189 super(ValueArgument, self).__init__(1, help, parsed_name, default)
192 class CommaSeparatedListArgument(ValueArgument):
195 :value returns: list of the comma separated values
200 return self._value or list()
203 def value(self, newvalue):
204 self._value = newvalue.split(',') if newvalue else list()
207 class IntArgument(ValueArgument):
211 """integer (type checking)"""
212 return getattr(self, '_value', self.default)
215 def value(self, newvalue):
217 self._value = self.default if (
218 newvalue == self.default) else int(newvalue)
220 raiseCLIError(CLISyntaxError(
222 details=['Value %s not an int' % newvalue]))
225 class DateArgument(ValueArgument):
227 DATE_FORMAT = '%a %b %d %H:%M:%S %Y'
229 INPUT_FORMATS = [DATE_FORMAT, '%d-%m-%Y', '%H:%M:%S %d-%m-%Y']
233 v = getattr(self, '_value', self.default)
234 return mktime(v.timetuple()) if v else None
238 v = getattr(self, '_value', self.default)
239 return v.strftime(self.DATE_FORMAT) if v else None
243 return self.timestamp
246 def value(self, newvalue):
247 self._value = self.format_date(newvalue) if newvalue else self.default
249 def format_date(self, datestr):
250 for format in self.INPUT_FORMATS:
252 t = dtm.strptime(datestr, format)
255 return t # .strftime(self.DATE_FORMAT)
256 raiseCLIError(None, 'Date Argument Error', details=[
257 '%s not a valid date' % datestr,
258 'Correct formats:\n\t%s' % self.INPUT_FORMATS])
261 class VersionArgument(FlagArgument):
262 """A flag argument with that prints current version"""
267 return super(self.__class__, self).value
270 def value(self, newvalue):
271 self._value = newvalue
274 print('kamaki %s' % kamaki.__version__)
277 class RepeatableArgument(Argument):
278 """A value argument that can be repeated"""
280 def __init__(self, help='', parsed_name=None, default=[]):
281 super(RepeatableArgument, self).__init__(
282 -1, help, parsed_name, default)
285 class KeyValueArgument(Argument):
286 """A Key=Value Argument that can be repeated
288 :syntax: --<arg> key1=value1 --<arg> key2=value2 ...
291 def __init__(self, help='', parsed_name=None, default=[]):
292 super(KeyValueArgument, self).__init__(-1, help, parsed_name, default)
297 :returns: (dict) {key1: val1, key2: val2, ...}
299 return super(KeyValueArgument, self).value
302 def value(self, keyvalue_pairs):
304 :param keyvalue_pairs: (str) ['key1=val1', 'key2=val2', ...]
306 self._value = getattr(self, '_value', self.value) or {}
308 for pair in keyvalue_pairs:
309 key, sep, val = pair.partition('=')
310 assert sep, ' %s misses a "=" (usage: key1=val1 )\n' % (pair)
311 self._value[key] = val
312 except Exception as e:
313 raiseCLIError(e, 'KeyValueArgument Syntax Error')
316 class ProgressBarArgument(FlagArgument):
317 """Manage a progress bar"""
319 def __init__(self, help='', parsed_name='', default=True):
320 self.suffix = '%(percent)d%%'
321 super(ProgressBarArgument, self).__init__(help, parsed_name, default)
324 """Get a modifiable copy of this bar"""
325 newarg = ProgressBarArgument(
326 self.help, self.parsed_name, self.default)
327 newarg._value = self._value
330 def get_generator(self, message, message_len=25):
331 """Get a generator to handle progress of the bar (gen.next())"""
335 self.bar = KamakiProgressBar()
339 self.bar.message = message.ljust(message_len)
340 self.bar.suffix = '%(percent)d%% - %(eta)ds'
344 for i in self.bar.iter(range(int(n))):
350 """Stop progress bar, return terminal cursor to user"""
353 mybar = getattr(self, 'bar', None)
360 cloud=ValueArgument('Chose a cloud to connect to', ('--cloud')),
361 help=Argument(0, 'Show help message', ('-h', '--help')),
362 debug=FlagArgument('Include debug output', ('-d', '--debug')),
363 include=FlagArgument(
364 'Include raw connection data in the output', ('-i', '--include')),
365 silent=FlagArgument('Do not output anything', ('-s', '--silent')),
366 verbose=FlagArgument('More info at response', ('-v', '--verbose')),
367 version=VersionArgument('Print current version', ('-V', '--version')),
368 options=RuntimeConfigArgument(
369 _config_arg, 'Override a config value', ('-o', '--options'))
373 # Initial command line interface arguments
376 class ArgumentParseManager(object):
377 """Manage (initialize and update) an ArgumentParser object"""
379 def __init__(self, exe, arguments=None):
381 :param exe: (str) the basic command (e.g. 'kamaki')
383 :param arguments: (dict) if given, overrides the global _argument as
384 the parsers arguments specification
386 self.parser = ArgumentParser(
387 add_help=False, formatter_class=RawDescriptionHelpFormatter)
388 self.syntax = '%s <cmd_group> [<cmd_subbroup> ...] <cmd>' % exe
390 self.arguments = arguments
393 self.arguments = _arguments
394 self._parser_modified, self._parsed, self._unparsed = False, None, None
399 """The command syntax (useful for help messages, descriptions, etc)"""
400 return self.parser.prog
403 def syntax(self, new_syntax):
404 self.parser.prog = new_syntax
408 """:returns: (dict) arguments the parser should be aware of"""
409 return self._arguments
412 def arguments(self, new_arguments):
413 assert isinstance(new_arguments, dict), 'Arguments must be in a dict'
414 self._arguments = new_arguments
419 """(Namespace) parser-matched terms"""
420 if self._parser_modified:
426 """(list) parser-unmatched terms"""
427 if self._parser_modified:
429 return self._unparsed
431 def update_parser(self, arguments=None):
432 """Load argument specifications to parser
434 :param arguments: if not given, update self.arguments instead
436 arguments = arguments or self._arguments
438 for name, arg in arguments.items():
440 arg.update_parser(self.parser, name)
441 self._parser_modified = True
442 except ArgumentError:
445 def update_arguments(self, new_arguments):
446 """Add to / update existing arguments
448 :param new_arguments: (dict)
451 assert isinstance(new_arguments, dict)
452 self._arguments.update(new_arguments)
455 def parse(self, new_args=None):
456 """Parse user input"""
458 pkargs = (new_args,) if new_args else ()
459 self._parsed, unparsed = self.parser.parse_known_args(*pkargs)
461 raiseCLIError(CLISyntaxError('Argument Syntax Error'))
462 for name, arg in self.arguments.items():
463 arg.value = getattr(self._parsed, name, arg.default)
465 for term in unparsed:
466 self._unparsed += split_input(' \'%s\' ' % term)
467 self._parser_modified = False