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 or (None if 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 KeyValueArgument(Argument):
278 """A Value Argument that can be repeated
280 :syntax: --<arg> key1=value1 --<arg> key2=value2 ...
283 def __init__(self, help='', parsed_name=None, default={}):
284 super(KeyValueArgument, self).__init__(-1, help, parsed_name, default)
289 :returns: (dict) {key1: val1, key2: val2, ...}
291 return super(KeyValueArgument, self).value
294 def value(self, keyvalue_pairs):
296 :param keyvalue_pairs: (str) ['key1=val1', 'key2=val2', ...]
299 for pair in keyvalue_pairs:
300 key, sep, val = pair.partition('=')
303 CLISyntaxError('Argument syntax error '),
304 details='%s is missing a "=" (usage: key1=val1 )\n' % pair)
305 self._value[key] = val
308 class ProgressBarArgument(FlagArgument):
309 """Manage a progress bar"""
311 def __init__(self, help='', parsed_name='', default=True):
312 self.suffix = '%(percent)d%%'
313 super(ProgressBarArgument, self).__init__(help, parsed_name, default)
316 """Get a modifiable copy of this bar"""
317 newarg = ProgressBarArgument(
321 newarg._value = self._value
324 def get_generator(self, message, message_len=25):
325 """Get a generator to handle progress of the bar (gen.next())"""
329 self.bar = KamakiProgressBar()
333 self.bar.message = message.ljust(message_len)
334 self.bar.suffix = '%(percent)d%% - %(eta)ds'
338 for i in self.bar.iter(range(int(n))):
344 """Stop progress bar, return terminal cursor to user"""
347 mybar = getattr(self, 'bar', None)
354 cloud=ValueArgument('Chose a cloud to connect to', ('--cloud')),
355 help=Argument(0, 'Show help message', ('-h', '--help')),
356 debug=FlagArgument('Include debug output', ('-d', '--debug')),
357 include=FlagArgument(
358 'Include raw connection data in the output', ('-i', '--include')),
359 silent=FlagArgument('Do not output anything', ('-s', '--silent')),
360 verbose=FlagArgument('More info at response', ('-v', '--verbose')),
361 version=VersionArgument('Print current version', ('-V', '--version')),
362 options=RuntimeConfigArgument(
363 _config_arg, 'Override a config value', ('-o', '--options'))
367 # Initial command line interface arguments
370 class ArgumentParseManager(object):
371 """Manage (initialize and update) an ArgumentParser object"""
375 _parser_modified = False
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
398 """The command syntax (useful for help messages, descriptions, etc)"""
399 return self.parser.prog
402 def syntax(self, new_syntax):
403 self.parser.prog = new_syntax
407 """(dict) arguments the parser should be aware of"""
408 return self._arguments
411 def arguments(self, new_arguments):
413 assert isinstance(new_arguments, 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
437 arguments = self._arguments
439 for name, arg in arguments.items():
441 arg.update_parser(self.parser, name)
442 self._parser_modified = True
443 except ArgumentError:
446 def update_arguments(self, new_arguments):
447 """Add to / update existing arguments
449 :param new_arguments: (dict)
452 assert isinstance(new_arguments, dict)
453 self._arguments.update(new_arguments)
456 def parse(self, new_args=None):
457 """Parse user input"""
459 pkargs = (new_args,) if new_args else ()
460 self._parsed, unparsed = self.parser.parse_known_args(*pkargs)
462 raiseCLIError(CLISyntaxError('Argument Syntax Error'))
463 for name, arg in self.arguments.items():
464 arg.value = getattr(self._parsed, name, arg.default)
466 for term in unparsed:
467 self._unparsed += split_input(' \'%s\' ' % term)
468 self._parser_modified = False