3 # Copyright 2011-2012 GRNET S.A. All rights reserved.
5 # Redistribution and use in source and binary forms, with or
6 # without modification, are permitted provided that the following
9 # 1. Redistributions of source code must retain the above
10 # copyright notice, this list of conditions and the following
13 # 2. Redistributions in binary form must reproduce the above
14 # copyright notice, this list of conditions and the following
15 # disclaimer in the documentation and/or other materials
16 # provided with the distribution.
18 # THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS
19 # OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
20 # WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
21 # PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR
22 # CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
23 # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
24 # LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
25 # USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
26 # AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
27 # LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
28 # ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
29 # POSSIBILITY OF SUCH DAMAGE.
31 # The views and conclusions contained in the software and
32 # documentation are those of the authors and should not be
33 # interpreted as representing official policies, either expressed
34 # or implied, of GRNET S.A.
36 from __future__ import print_function
39 #Monkey-patch everything for gevent early on
40 gevent.monkey.patch_all()
46 from argparse import ArgumentParser
47 from base64 import b64encode
48 from os.path import abspath, basename, exists
49 from sys import exit, stdout, stderr
52 from collections import OrderedDict
54 from ordereddict import OrderedDict
56 from colors import magenta, red, yellow, bold
58 from kamaki import clients
59 from .config import Config
61 _commands = OrderedDict()
64 CLI_LOCATIONS = ['kamaki.cli.commands', 'kamaki.commands', 'kamaki.cli', 'kamaki', '']
66 class CLIError(Exception):
67 def __init__(self, message, status=0, details='', importance=0):
68 """importance is set by the raiser
69 0 is the lowest possible importance
70 Suggested values: 0, 1, 2, 3
72 super(CLIError, self).__init__(message, status, details)
73 self.message = message
75 self.details = details
76 self.importance = importance
78 def __unicode__(self):
79 return unicode(self.message)
81 def command(group=None, name=None, syntax=None):
82 """Class decorator that registers a class as a CLI command."""
85 grp, sep, cmd = cls.__name__.partition('_')
87 grp, cmd = None, cls.__name__
90 cls.group = group or grp
91 cls.name = name or cmd
93 short_description, sep, long_description = cls.__doc__.partition('\n')
94 cls.description = short_description
95 cls.long_description = long_description or short_description
98 if cls.syntax is None:
99 # Generate a syntax string based on main's arguments
100 spec = inspect.getargspec(cls.main.im_func)
102 n = len(args) - len(spec.defaults or ())
103 required = ' '.join('<%s>' % x.replace('____', '[:').replace('___', ':').replace('__',']').replace('_', ' ') for x in args[:n])
104 optional = ' '.join('[%s]' % x.replace('____', '[:').replace('___', ':').replace('__', ']').replace('_', ' ') for x in args[n:])
105 cls.syntax = ' '.join(x for x in [required, optional] if x)
107 cls.syntax += ' <%s ...>' % spec.varargs
109 if cls.group not in _commands:
110 _commands[cls.group] = OrderedDict()
111 _commands[cls.group][cls.name] = cls
115 def set_api_description(api, description):
116 """Method to be called by api CLIs
117 Each CLI can set more than one api descriptions"""
118 GROUPS[api] = description
124 for group in _commands:
125 description = GROUPS.get(group, '')
126 print(' ', group.ljust(12), description)
128 def print_commands(group):
129 description = GROUPS.get(group, '')
131 print('\n' + description)
134 for name, cls in _commands[group].items():
135 print(' ', name.ljust(14), cls.description)
137 def manage_logging_handlers(args):
138 """This is mostly to handle logging for clients package"""
140 def add_handler(name, level, prefix=''):
141 h = logging.StreamHandler()
142 fmt = logging.Formatter(prefix + '%(message)s')
144 logger = logging.getLogger(name)
146 logger.setLevel(level)
149 add_handler('', logging.CRITICAL)
151 add_handler('requests', logging.INFO, prefix='* ')
152 add_handler('clients.send', logging.DEBUG, prefix='> ')
153 add_handler('clients.recv', logging.DEBUG, prefix='< ')
155 add_handler('requests', logging.INFO, prefix='* ')
156 add_handler('clients.send', logging.INFO, prefix='> ')
157 add_handler('clients.recv', logging.INFO, prefix='< ')
159 add_handler('clients.recv', logging.INFO)
161 add_handler('', logging.WARNING)
163 def load_groups(config):
164 """load groups and import CLIs and Modules"""
166 for api in config.apis():
167 api_cli = config.get(api, 'cli')
168 if None == api_cli or len(api_cli)==0:
169 print('Warnig: No Command Line Interface "%s" given for API "%s"'%(api_cli, api))
170 print('\t(cli option in config file)')
172 if not loaded_modules.has_key(api_cli):
173 loaded_modules[api_cli] = False
174 for location in CLI_LOCATIONS:
175 location += api_cli if location == '' else '.%s'%api_cli
178 loaded_modules[api_cli] = True
182 if not loaded_modules[api_cli]:
183 print('Warning: failed to load Command Line Interface "%s" for API "%s"'%(api_cli, api))
184 print('\t(No suitable cli in known paths)')
186 if not GROUPS.has_key(api):
187 GROUPS[api] = 'No description (interface: %s)'%api_cli
189 def init_parser(exe):
190 parser = ArgumentParser(add_help=False)
191 parser.prog = '%s <group> <command>' % exe
192 parser.add_argument('-h', '--help', dest='help', action='store_true',
194 help="Show this help message and exit")
195 parser.add_argument('--config', dest='config', metavar='PATH',
196 help="Specify the path to the configuration file")
197 parser.add_argument('-d', '--debug', dest='debug', action='store_true',
199 help="Include debug output")
200 parser.add_argument('-i', '--include', dest='include', action='store_true',
202 help="Include protocol headers in the output")
203 parser.add_argument('-s', '--silent', dest='silent', action='store_true',
205 help="Silent mode, don't output anything")
206 parser.add_argument('-v', '--verbose', dest='verbose', action='store_true',
208 help="Make the operation more talkative")
209 parser.add_argument('-V', '--version', dest='version', action='store_true',
211 help="Show version number and quit")
212 parser.add_argument('-o', dest='options', action='append',
213 default=[], metavar="KEY=VAL",
214 help="Override a config value")
217 def find_term_in_args(arg_list, term_list):
218 """find an arg_list term in term_list. All other terms up to found
219 term are rearanged at the end of arg_list, preserving relative order
222 while len(arg_list) > 0:
223 group = arg_list.pop(0)
224 if group not in term_list:
225 arg_tail.append(group)
232 exe = basename(sys.argv[0])
233 parser = init_parser(exe)
234 args, argv = parser.parse_known_args()
239 print("kamaki %s" % kamaki.__version__)
242 config = Config(args.config) if args.config else Config()
244 #load config options from command line
245 for option in args.options:
246 keypath, sep, val = option.partition('=')
248 print("Invalid option '%s'" % option)
250 section, sep, key = keypath.partition('.')
252 print("Invalid option '%s'" % option)
254 config.override(section.strip(), key.strip(), val.strip())
257 group = find_term_in_args(argv, _commands)
263 parser.prog = '%s %s <command>' % (exe, group)
264 command = find_term_in_args(argv, _commands[group])
268 print_commands(group)
271 cmd = _commands[group][command]()
273 parser.prog = '%s %s %s' % (exe, group, command)
275 parser.prog += ' %s' % cmd.syntax
276 parser.description = cmd.description
278 if hasattr(cmd, 'update_parser'):
279 cmd.update_parser(parser)
282 args, argv = parser.parse_known_args()
284 errmsg = red('Invalid command group '+argv[0])
285 print(errmsg, file=stderr)
287 if command != argv[1]:
288 errmsg = red('Invalid command "%s" in group "%s"'%(argv[1], argv[0]))
289 print(errmsg, file=stderr)
296 manage_logging_handlers(args)
300 ret = cmd.main(*argv[2:])
302 except TypeError as e:
303 if e.args and e.args[0].startswith('main()'):
308 except CLIError as err:
309 errmsg = 'CLI Error '
310 errmsg += '(%s): '%err.status if err.status else ': '
311 errmsg += unicode(err.message) if err.message else ''
312 if err.importance == 1:
313 errmsg = yellow(errmsg)
314 elif err.importance == 2:
315 errmsg = magenta(errmsg)
316 elif err.importance > 2:
318 print(errmsg, file=stderr)
321 if __name__ == '__main__':