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(
318 self.help, self.parsed_name, self.default)
319 newarg._value = self._value
322 def get_generator(self, message, message_len=25):
323 """Get a generator to handle progress of the bar (gen.next())"""
327 self.bar = KamakiProgressBar()
331 self.bar.message = message.ljust(message_len)
332 self.bar.suffix = '%(percent)d%% - %(eta)ds'
336 for i in self.bar.iter(range(int(n))):
342 """Stop progress bar, return terminal cursor to user"""
345 mybar = getattr(self, 'bar', None)
352 cloud=ValueArgument('Chose a cloud to connect to', ('--cloud')),
353 help=Argument(0, 'Show help message', ('-h', '--help')),
354 debug=FlagArgument('Include debug output', ('-d', '--debug')),
355 include=FlagArgument(
356 'Include raw connection data in the output', ('-i', '--include')),
357 silent=FlagArgument('Do not output anything', ('-s', '--silent')),
358 verbose=FlagArgument('More info at response', ('-v', '--verbose')),
359 version=VersionArgument('Print current version', ('-V', '--version')),
360 options=RuntimeConfigArgument(
361 _config_arg, 'Override a config value', ('-o', '--options'))
365 # Initial command line interface arguments
368 class ArgumentParseManager(object):
369 """Manage (initialize and update) an ArgumentParser object"""
371 def __init__(self, exe, arguments=None):
373 :param exe: (str) the basic command (e.g. 'kamaki')
375 :param arguments: (dict) if given, overrides the global _argument as
376 the parsers arguments specification
378 self.parser = ArgumentParser(
379 add_help=False, formatter_class=RawDescriptionHelpFormatter)
380 self.syntax = '%s <cmd_group> [<cmd_subbroup> ...] <cmd>' % exe
382 self.arguments = arguments
385 self.arguments = _arguments
386 self._parser_modified, self._parsed, self._unparsed = False, None, None
391 """The command syntax (useful for help messages, descriptions, etc)"""
392 return self.parser.prog
395 def syntax(self, new_syntax):
396 self.parser.prog = new_syntax
400 """:returns: (dict) arguments the parser should be aware of"""
401 return self._arguments
404 def arguments(self, new_arguments):
405 assert isinstance(new_arguments, dict), 'Arguments must be in a dict'
406 self._arguments = new_arguments
411 """(Namespace) parser-matched terms"""
412 if self._parser_modified:
418 """(list) parser-unmatched terms"""
419 if self._parser_modified:
421 return self._unparsed
423 def update_parser(self, arguments=None):
424 """Load argument specifications to parser
426 :param arguments: if not given, update self.arguments instead
428 arguments = arguments or self._arguments
430 for name, arg in arguments.items():
432 arg.update_parser(self.parser, name)
433 self._parser_modified = True
434 except ArgumentError:
437 def update_arguments(self, new_arguments):
438 """Add to / update existing arguments
440 :param new_arguments: (dict)
443 assert isinstance(new_arguments, dict)
444 self._arguments.update(new_arguments)
447 def parse(self, new_args=None):
448 """Parse user input"""
450 pkargs = (new_args,) if new_args else ()
451 self._parsed, unparsed = self.parser.parse_known_args(*pkargs)
453 raiseCLIError(CLISyntaxError('Argument Syntax Error'))
454 for name, arg in self.arguments.items():
455 arg.value = getattr(self._parsed, name, arg.default)
457 for term in unparsed:
458 self._unparsed += split_input(' \'%s\' ' % term)
459 self._parser_modified = False