Argument object handles part of the functionality
[kamaki] / kamaki / cli / __init__.py
1 #!/usr/bin/env python
2
3 # Copyright 2011-2012 GRNET S.A. All rights reserved.
4 #
5 # Redistribution and use in source and binary forms, with or
6 # without modification, are permitted provided that the following
7 # conditions are met:
8 #
9 #   1. Redistributions of source code must retain the above
10 #      copyright notice, this list of conditions and the following
11 #      disclaimer.
12 #
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.
17 #
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.
30 #
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.
35
36 from __future__ import print_function
37
38 import gevent.monkey
39 #Monkey-patch everything for gevent early on
40 gevent.monkey.patch_all()
41
42 import inspect
43 import logging
44 import sys
45
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
50
51 try:
52     from collections import OrderedDict
53 except ImportError:
54     from ordereddict import OrderedDict
55
56 from colors import magenta, red, yellow, bold
57
58 from kamaki import clients
59 from .errors import CLIError
60 from .config import Config
61
62 _commands = OrderedDict()
63
64 GROUPS = {}
65 CLI_LOCATIONS = ['kamaki.cli.commands', 'kamaki.commands', 'kamaki.cli', 'kamaki', '']
66
67 def command(group=None, name=None, syntax=None):
68     """Class decorator that registers a class as a CLI command."""
69
70     def decorator(cls):
71         grp, sep, cmd = cls.__name__.partition('_')
72         if not sep:
73             grp, cmd = None, cls.__name__
74
75         #cls.api = api
76         cls.group = group or grp
77         cls.name = name or cmd
78
79         short_description, sep, long_description = cls.__doc__.partition('\n')
80         cls.description = short_description
81         cls.long_description = long_description or short_description
82
83         cls.syntax = syntax
84         if cls.syntax is None:
85             # Generate a syntax string based on main's arguments
86             spec = inspect.getargspec(cls.main.im_func)
87             args = spec.args[1:]
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)
92             if spec.varargs:
93                 cls.syntax += ' <%s ...>' % spec.varargs
94
95         if cls.group not in _commands:
96             _commands[cls.group] = OrderedDict()
97         _commands[cls.group][cls.name] = cls
98         return cls
99     return decorator
100
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
105
106 def main():
107
108     def print_groups():
109         print('\nGroups:')
110         for group in _commands:
111             description = GROUPS.get(group, '')
112             print(' ', group.ljust(12), description)
113
114     def print_commands(group):
115         description = GROUPS.get(group, '')
116         if description:
117             print('\n' + description)
118
119         print('\nCommands:')
120         for name, cls in _commands[group].items():
121             print(' ', name.ljust(14), cls.description)
122
123     def manage_logging_handlers(args):
124         """This is mostly to handle logging for clients package"""
125
126         def add_handler(name, level, prefix=''):
127             h = logging.StreamHandler()
128             fmt = logging.Formatter(prefix + '%(message)s')
129             h.setFormatter(fmt)
130             logger = logging.getLogger(name)
131             logger.addHandler(h)
132             logger.setLevel(level)
133
134         if args.silent:
135             add_handler('', logging.CRITICAL)
136         elif args.debug:
137             add_handler('requests', logging.INFO, prefix='* ')
138             add_handler('clients.send', logging.DEBUG, prefix='> ')
139             add_handler('clients.recv', logging.DEBUG, prefix='< ')
140         elif args.verbose:
141             add_handler('requests', logging.INFO, prefix='* ')
142             add_handler('clients.send', logging.INFO, prefix='> ')
143             add_handler('clients.recv', logging.INFO, prefix='< ')
144         elif args.include:
145             add_handler('clients.recv', logging.INFO)
146         else:
147             add_handler('', logging.WARNING)
148
149     def load_groups(config):
150         """load groups and import CLIs and Modules"""
151         loaded_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)')
157                 continue
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
162                     try:
163                         __import__(location)
164                         loaded_modules[api_cli] = True
165                         break
166                     except ImportError:
167                         pass
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)')
171                     continue
172             if not GROUPS.has_key(api):
173                 GROUPS[api] = 'No description (interface: %s)'%api_cli
174
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',
179                           default=False,
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',
184                           default=False,
185                           help="Include debug output")
186         parser.add_argument('-i', '--include', dest='include', action='store_true',
187                           default=False,
188                           help="Include protocol headers in the output")
189         parser.add_argument('-s', '--silent', dest='silent', action='store_true',
190                           default=False,
191                           help="Silent mode, don't output anything")
192         parser.add_argument('-v', '--verbose', dest='verbose', action='store_true',
193                           default=False,
194                           help="Make the operation more talkative")
195         parser.add_argument('-V', '--version', dest='version', action='store_true',
196                           default=False,
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")
201         return parser
202
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
206         """
207         arg_tail = []
208         while len(arg_list) > 0:
209             group = arg_list.pop(0)
210             if group not in term_list:
211                 arg_tail.append(group)
212             else:
213                 arg_list += arg_tail
214                 return group
215         return None
216
217     """Main Code"""
218     exe = basename(sys.argv[0])
219     parser = init_parser(exe)
220     args, argv = parser.parse_known_args()
221
222     #print version
223     if args.version:
224         import kamaki
225         print("kamaki %s" % kamaki.__version__)
226         exit(0)
227
228     config = Config(args.config) if args.config else Config()
229
230     #load config options from command line
231     for option in args.options:
232         keypath, sep, val = option.partition('=')
233         if not sep:
234             print("Invalid option '%s'" % option)
235             exit(1)
236         section, sep, key = keypath.partition('.')
237         if not sep:
238             print("Invalid option '%s'" % option)
239             exit(1)
240         config.override(section.strip(), key.strip(), val.strip())
241
242     load_groups(config)
243     group = find_term_in_args(argv, _commands)
244     if not group:
245         parser.print_help()
246         print_groups()
247         exit(0)
248
249     parser.prog = '%s %s <command>' % (exe, group)
250     command = find_term_in_args(argv, _commands[group])
251
252     if not command:
253         parser.print_help()
254         print_commands(group)
255         exit(0)
256
257     cmd = _commands[group][command]()
258
259     parser.prog = '%s %s %s' % (exe, group, command)
260     if cmd.syntax:
261         parser.prog += '  %s' % cmd.syntax
262     parser.description = cmd.description
263     parser.epilog = ''
264     if hasattr(cmd, 'update_parser'):
265         cmd.update_parser(parser)
266
267     #check other args
268     args, argv = parser.parse_known_args()
269     if group != argv[0]:
270         errmsg = red('Invalid command group '+argv[0])
271         print(errmsg, file=stderr)
272         exit(1)
273     if command != argv[1]:
274         errmsg = red('Invalid command "%s" in group "%s"'%(argv[1], argv[0]))
275         print(errmsg, file=stderr)
276         exit(1)
277
278     if args.help:
279         parser.print_help()
280         exit(0)
281
282     manage_logging_handlers(args)
283     cmd.args = args
284     cmd.config = config
285     try:
286         ret = cmd.main(*argv[2:])
287         exit(ret)
288     except TypeError as e:
289         if e.args and e.args[0].startswith('main()'):
290             parser.print_help()
291             exit(1)
292         else:
293             raise
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:
303             errmsg = red(errmsg)
304         print(errmsg, file=stderr)
305         exit(1)
306
307 if __name__ == '__main__':
308     main()