Unitest purge, create_by_manifestation, versionlst
[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 .config import Config
60
61 _commands = OrderedDict()
62
63 GROUPS = {}
64 CLI_LOCATIONS = ['kamaki.cli.commands', 'kamaki.commands', 'kamaki.cli', 'kamaki', '']
65
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
71         """
72         super(CLIError, self).__init__(message, status, details)
73         self.message = message
74         self.status = status
75         self.details = details
76         self.importance = importance
77
78     def __unicode__(self):
79         return unicode(self.message)
80
81 def command(group=None, name=None, syntax=None):
82     """Class decorator that registers a class as a CLI command."""
83
84     def decorator(cls):
85         grp, sep, cmd = cls.__name__.partition('_')
86         if not sep:
87             grp, cmd = None, cls.__name__
88
89         #cls.api = api
90         cls.group = group or grp
91         cls.name = name or cmd
92
93         short_description, sep, long_description = cls.__doc__.partition('\n')
94         cls.description = short_description
95         cls.long_description = long_description or short_description
96
97         cls.syntax = syntax
98         if cls.syntax is None:
99             # Generate a syntax string based on main's arguments
100             spec = inspect.getargspec(cls.main.im_func)
101             args = spec.args[1:]
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)
106             if spec.varargs:
107                 cls.syntax += ' <%s ...>' % spec.varargs
108
109         if cls.group not in _commands:
110             _commands[cls.group] = OrderedDict()
111         _commands[cls.group][cls.name] = cls
112         return cls
113     return decorator
114
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
119
120 def main():
121
122     def print_groups():
123         print('\nGroups:')
124         for group in _commands:
125             description = GROUPS.get(group, '')
126             print(' ', group.ljust(12), description)
127
128     def print_commands(group):
129         description = GROUPS.get(group, '')
130         if description:
131             print('\n' + description)
132
133         print('\nCommands:')
134         for name, cls in _commands[group].items():
135             print(' ', name.ljust(14), cls.description)
136
137     def manage_logging_handlers(args):
138         """This is mostly to handle logging for clients package"""
139
140         def add_handler(name, level, prefix=''):
141             h = logging.StreamHandler()
142             fmt = logging.Formatter(prefix + '%(message)s')
143             h.setFormatter(fmt)
144             logger = logging.getLogger(name)
145             logger.addHandler(h)
146             logger.setLevel(level)
147
148         if args.silent:
149             add_handler('', logging.CRITICAL)
150         elif args.debug:
151             add_handler('requests', logging.INFO, prefix='* ')
152             add_handler('clients.send', logging.DEBUG, prefix='> ')
153             add_handler('clients.recv', logging.DEBUG, prefix='< ')
154         elif args.verbose:
155             add_handler('requests', logging.INFO, prefix='* ')
156             add_handler('clients.send', logging.INFO, prefix='> ')
157             add_handler('clients.recv', logging.INFO, prefix='< ')
158         elif args.include:
159             add_handler('clients.recv', logging.INFO)
160         else:
161             add_handler('', logging.WARNING)
162
163     def load_groups(config):
164         """load groups and import CLIs and Modules"""
165         loaded_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)')
171                 continue
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
176                     try:
177                         __import__(location)
178                         loaded_modules[api_cli] = True
179                         break
180                     except ImportError:
181                         pass
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)')
185                     continue
186             if not GROUPS.has_key(api):
187                 GROUPS[api] = 'No description (interface: %s)'%api_cli
188
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',
193                           default=False,
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',
198                           default=False,
199                           help="Include debug output")
200         parser.add_argument('-i', '--include', dest='include', action='store_true',
201                           default=False,
202                           help="Include protocol headers in the output")
203         parser.add_argument('-s', '--silent', dest='silent', action='store_true',
204                           default=False,
205                           help="Silent mode, don't output anything")
206         parser.add_argument('-v', '--verbose', dest='verbose', action='store_true',
207                           default=False,
208                           help="Make the operation more talkative")
209         parser.add_argument('-V', '--version', dest='version', action='store_true',
210                           default=False,
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")
215         return parser
216
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
220         """
221         arg_tail = []
222         while len(arg_list) > 0:
223             group = arg_list.pop(0)
224             if group not in term_list:
225                 arg_tail.append(group)
226             else:
227                 arg_list += arg_tail
228                 return group
229         return None
230
231     """Main Code"""
232     exe = basename(sys.argv[0])
233     parser = init_parser(exe)
234     args, argv = parser.parse_known_args()
235
236     #print version
237     if args.version:
238         import kamaki
239         print("kamaki %s" % kamaki.__version__)
240         exit(0)
241
242     config = Config(args.config) if args.config else Config()
243
244     #load config options from command line
245     for option in args.options:
246         keypath, sep, val = option.partition('=')
247         if not sep:
248             print("Invalid option '%s'" % option)
249             exit(1)
250         section, sep, key = keypath.partition('.')
251         if not sep:
252             print("Invalid option '%s'" % option)
253             exit(1)
254         config.override(section.strip(), key.strip(), val.strip())
255
256     load_groups(config)
257     group = find_term_in_args(argv, _commands)
258     if not group:
259         parser.print_help()
260         print_groups()
261         exit(0)
262
263     parser.prog = '%s %s <command>' % (exe, group)
264     command = find_term_in_args(argv, _commands[group])
265
266     if not command:
267         parser.print_help()
268         print_commands(group)
269         exit(0)
270
271     cmd = _commands[group][command]()
272
273     parser.prog = '%s %s %s' % (exe, group, command)
274     if cmd.syntax:
275         parser.prog += '  %s' % cmd.syntax
276     parser.description = cmd.description
277     parser.epilog = ''
278     if hasattr(cmd, 'update_parser'):
279         cmd.update_parser(parser)
280
281     #check other args
282     args, argv = parser.parse_known_args()
283     if group != argv[0]:
284         errmsg = red('Invalid command group '+argv[0])
285         print(errmsg, file=stderr)
286         exit(1)
287     if command != argv[1]:
288         errmsg = red('Invalid command "%s" in group "%s"'%(argv[1], argv[0]))
289         print(errmsg, file=stderr)
290         exit(1)
291
292     if args.help:
293         parser.print_help()
294         exit(0)
295
296     manage_logging_handlers(args)
297     cmd.args = args
298     cmd.config = config
299     try:
300         ret = cmd.main(*argv[2:])
301         exit(ret)
302     except TypeError as e:
303         if e.args and e.args[0].startswith('main()'):
304             parser.print_help()
305             exit(1)
306         else:
307             raise
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:
317             errmsg = red(errmsg)
318         print(errmsg, file=stderr)
319         exit(1)
320
321 if __name__ == '__main__':
322     main()