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 .errors import CLIError
60 from .config import Config
62 _commands = OrderedDict()
65 CLI_LOCATIONS = ['kamaki.cli.commands', 'kamaki.commands', 'kamaki.cli', 'kamaki', '']
67 def command(group=None, name=None, syntax=None):
68 """Class decorator that registers a class as a CLI command."""
71 grp, sep, cmd = cls.__name__.partition('_')
73 grp, cmd = None, cls.__name__
76 cls.group = group or grp
77 cls.name = name or cmd
79 short_description, sep, long_description = cls.__doc__.partition('\n')
80 cls.description = short_description
81 cls.long_description = long_description or short_description
84 if cls.syntax is None:
85 # Generate a syntax string based on main's arguments
86 spec = inspect.getargspec(cls.main.im_func)
88 n = len(args) - len(spec.defaults or ())
89 required = ' '.join('<%s>' % x.replace('____', '[:').replace('___', ':').replace('__',']').replace('_', ' ') for x in args[:n])
90 optional = ' '.join('[%s]' % x.replace('____', '[:').replace('___', ':').replace('__', ']').replace('_', ' ') for x in args[n:])
91 cls.syntax = ' '.join(x for x in [required, optional] if x)
93 cls.syntax += ' <%s ...>' % spec.varargs
95 if cls.group not in _commands:
96 _commands[cls.group] = OrderedDict()
97 _commands[cls.group][cls.name] = cls
101 def set_api_description(api, description):
102 """Method to be called by api CLIs
103 Each CLI can set more than one api descriptions"""
104 GROUPS[api] = description
110 for group in _commands:
111 description = GROUPS.get(group, '')
112 print(' ', group.ljust(12), description)
114 def print_commands(group):
115 description = GROUPS.get(group, '')
117 print('\n' + description)
120 for name, cls in _commands[group].items():
121 print(' ', name.ljust(14), cls.description)
123 def manage_logging_handlers(args):
124 """This is mostly to handle logging for clients package"""
126 def add_handler(name, level, prefix=''):
127 h = logging.StreamHandler()
128 fmt = logging.Formatter(prefix + '%(message)s')
130 logger = logging.getLogger(name)
132 logger.setLevel(level)
135 add_handler('', logging.CRITICAL)
137 add_handler('requests', logging.INFO, prefix='* ')
138 add_handler('clients.send', logging.DEBUG, prefix='> ')
139 add_handler('clients.recv', logging.DEBUG, prefix='< ')
141 add_handler('requests', logging.INFO, prefix='* ')
142 add_handler('clients.send', logging.INFO, prefix='> ')
143 add_handler('clients.recv', logging.INFO, prefix='< ')
145 add_handler('clients.recv', logging.INFO)
147 add_handler('', logging.WARNING)
149 def load_groups(config):
150 """load groups and import CLIs and Modules"""
152 for api in config.apis():
153 api_cli = config.get(api, 'cli')
154 if None == api_cli or len(api_cli)==0:
155 print('Warnig: No Command Line Interface "%s" given for API "%s"'%(api_cli, api))
156 print('\t(cli option in config file)')
158 if not loaded_modules.has_key(api_cli):
159 loaded_modules[api_cli] = False
160 for location in CLI_LOCATIONS:
161 location += api_cli if location == '' else '.%s'%api_cli
164 loaded_modules[api_cli] = True
168 if not loaded_modules[api_cli]:
169 print('Warning: failed to load Command Line Interface "%s" for API "%s"'%(api_cli, api))
170 print('\t(No suitable cli in known paths)')
172 if not GROUPS.has_key(api):
173 GROUPS[api] = 'No description (interface: %s)'%api_cli
175 def init_parser(exe):
176 parser = ArgumentParser(add_help=False)
177 parser.prog = '%s <group> <command>' % exe
178 parser.add_argument('-h', '--help', dest='help', action='store_true',
180 help="Show this help message and exit")
181 parser.add_argument('--config', dest='config', metavar='PATH',
182 help="Specify the path to the configuration file")
183 parser.add_argument('-d', '--debug', dest='debug', action='store_true',
185 help="Include debug output")
186 parser.add_argument('-i', '--include', dest='include', action='store_true',
188 help="Include protocol headers in the output")
189 parser.add_argument('-s', '--silent', dest='silent', action='store_true',
191 help="Silent mode, don't output anything")
192 parser.add_argument('-v', '--verbose', dest='verbose', action='store_true',
194 help="Make the operation more talkative")
195 parser.add_argument('-V', '--version', dest='version', action='store_true',
197 help="Show version number and quit")
198 parser.add_argument('-o', dest='options', action='append',
199 default=[], metavar="KEY=VAL",
200 help="Override a config value")
203 def find_term_in_args(arg_list, term_list):
204 """find an arg_list term in term_list. All other terms up to found
205 term are rearanged at the end of arg_list, preserving relative order
208 while len(arg_list) > 0:
209 group = arg_list.pop(0)
210 if group not in term_list:
211 arg_tail.append(group)
218 exe = basename(sys.argv[0])
219 parser = init_parser(exe)
220 args, argv = parser.parse_known_args()
225 print("kamaki %s" % kamaki.__version__)
228 config = Config(args.config) if args.config else Config()
230 #load config options from command line
231 for option in args.options:
232 keypath, sep, val = option.partition('=')
234 print("Invalid option '%s'" % option)
236 section, sep, key = keypath.partition('.')
238 print("Invalid option '%s'" % option)
240 config.override(section.strip(), key.strip(), val.strip())
243 group = find_term_in_args(argv, _commands)
249 parser.prog = '%s %s <command>' % (exe, group)
250 command = find_term_in_args(argv, _commands[group])
254 print_commands(group)
257 cmd = _commands[group][command]()
259 parser.prog = '%s %s %s' % (exe, group, command)
261 parser.prog += ' %s' % cmd.syntax
262 parser.description = cmd.description
264 if hasattr(cmd, 'update_parser'):
265 cmd.update_parser(parser)
268 args, argv = parser.parse_known_args()
270 errmsg = red('Invalid command group '+argv[0])
271 print(errmsg, file=stderr)
273 if command != argv[1]:
274 errmsg = red('Invalid command "%s" in group "%s"'%(argv[1], argv[0]))
275 print(errmsg, file=stderr)
282 manage_logging_handlers(args)
286 ret = cmd.main(*argv[2:])
288 except TypeError as e:
289 if e.args and e.args[0].startswith('main()'):
294 except CLIError as err:
295 errmsg = 'CLI Error '
296 errmsg += '(%s): '%err.status if err.status else ': '
297 errmsg += unicode(err.message) if err.message else ''
298 if err.importance == 1:
299 errmsg = yellow(errmsg)
300 elif err.importance == 2:
301 errmsg = magenta(errmsg)
302 elif err.importance > 2:
304 print(errmsg, file=stderr)
307 if __name__ == '__main__':