Statistics
| Branch: | Tag: | Revision:

root / kamaki / cli.py @ 2f749e6e

History | View | Annotate | Download (11.6 kB)

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
#from progress.bar import IncrementalBar
58
#from requests.exceptions import ConnectionError
59

    
60
from . import clients
61
from .config import Config
62
#from .utils import print_list, print_dict, print_items, format_size
63

    
64
_commands = OrderedDict()
65

    
66
GROUPS = {}
67
CLI_LOCATIONS = ['', 'kamaki', 'kamaki.commands']
68

    
69
class CLIError(Exception):
70
    def __init__(self, message, status=0, details='', importance=0):
71
        """importance is set by the raiser
72
        0 is the lowest possible importance
73
        Suggested values: 0, 1, 2, 3
74
        """
75
        super(CLIError, self).__init__(message, status, details)
76
        self.message = message
77
        self.status = status
78
        self.details = details
79
        self.importance = importance
80

    
81
    def __unicode__(self):
82
        return unicode(self.message)
83

    
84
def command(group=None, name=None, syntax=None):
85
    """Class decorator that registers a class as a CLI command."""
86

    
87
    def decorator(cls):
88
        grp, sep, cmd = cls.__name__.partition('_')
89
        if not sep:
90
            grp, cmd = None, cls.__name__
91

    
92
        #cls.api = api
93
        cls.group = group or grp
94
        cls.name = name or cmd
95

    
96
        short_description, sep, long_description = cls.__doc__.partition('\n')
97
        cls.description = short_description
98
        cls.long_description = long_description or short_description
99

    
100
        cls.syntax = syntax
101
        if cls.syntax is None:
102
            # Generate a syntax string based on main's arguments
103
            spec = inspect.getargspec(cls.main.im_func)
104
            args = spec.args[1:]
105
            n = len(args) - len(spec.defaults or ())
106
            required = ' '.join('<%s>' % x.replace('____', '[:').replace('___', ':').replace('__',']').replace('_', ' ') for x in args[:n])
107
            optional = ' '.join('[%s]' % x.replace('____', '[:').replace('___', ':').replace('__', ']').replace('_', ' ') for x in args[n:])
108
            cls.syntax = ' '.join(x for x in [required, optional] if x)
109
            if spec.varargs:
110
                cls.syntax += ' <%s ...>' % spec.varargs
111

    
112
        if cls.group not in _commands:
113
            _commands[cls.group] = OrderedDict()
114
        _commands[cls.group][cls.name] = cls
115
        return cls
116
    return decorator
117

    
118
def set_api_description(api, description):
119
    """Method to be called by api CLIs
120
    Each CLI can set more than one api descriptions"""
121
    GROUPS[api] = description
122

    
123
def main():
124

    
125
    def print_groups():
126
        print('\nGroups:')
127
        for group in _commands:
128
            description = GROUPS.get(group, '')
129
            print(' ', group.ljust(12), description)
130

    
131
    def print_commands(group):
132
        description = GROUPS.get(group, '')
133
        if description:
134
            print('\n' + description)
135

    
136
        print('\nCommands:')
137
        for name, cls in _commands[group].items():
138
            print(' ', name.ljust(14), cls.description)
139

    
140
    def manage_logging_handlers(args):
141
        """This is mostly to handle logging for clients package"""
142

    
143
        def add_handler(name, level, prefix=''):
144
            h = logging.StreamHandler()
145
            fmt = logging.Formatter(prefix + '%(message)s')
146
            h.setFormatter(fmt)
147
            logger = logging.getLogger(name)
148
            logger.addHandler(h)
149
            logger.setLevel(level)
150

    
151
        if args.silent:
152
            add_handler('', logging.CRITICAL)
153
        elif args.debug:
154
            add_handler('requests', logging.INFO, prefix='* ')
155
            add_handler('clients.send', logging.DEBUG, prefix='> ')
156
            add_handler('clients.recv', logging.DEBUG, prefix='< ')
157
        elif args.verbose:
158
            add_handler('requests', logging.INFO, prefix='* ')
159
            add_handler('clients.send', logging.INFO, prefix='> ')
160
            add_handler('clients.recv', logging.INFO, prefix='< ')
161
        elif args.include:
162
            add_handler('clients.recv', logging.INFO)
163
        else:
164
            add_handler('', logging.WARNING)
165

    
166
    def load_groups(config):
167
        """load groups and import CLIs and Modules"""
168
        loaded_modules = {}
169
        for api in config.apis():
170
            api_cli = config.get(api, 'cli')
171
            if None == api_cli or len(api_cli)==0:
172
                print('Warnig: No Command Line Interface "%s" given for API "%s"'%(api_cli, api))
173
                print('\t(cli option in config file)')
174
                continue
175
            if not loaded_modules.has_key(api_cli):
176
                loaded_modules[api_cli] = False
177
                for location in CLI_LOCATIONS:
178
                    location += api_cli if location == '' else '.%s'%api_cli
179
                    try:
180
                        __import__(location)
181
                        loaded_modules[api_cli] = True
182
                        break
183
                    except ImportError:
184
                        pass
185
                if not loaded_modules[api_cli]:
186
                    print('Warning: failed to load Command Line Interface "%s" for API "%s"'%(api_cli, api))
187
                    print('\t(No suitable cli in known paths)')
188
                    continue
189
            if not GROUPS.has_key(api):
190
                GROUPS[api] = 'No description (interface: %s)'%api_cli
191

    
192
    def init_parser(exe):
193
        parser = ArgumentParser(add_help=False)
194
        parser.prog = '%s <group> <command>' % exe
195
        parser.add_argument('-h', '--help', dest='help', action='store_true',
196
                          default=False,
197
                          help="Show this help message and exit")
198
        parser.add_argument('--config', dest='config', metavar='PATH',
199
                          help="Specify the path to the configuration file")
200
        parser.add_argument('-d', '--debug', dest='debug', action='store_true',
201
                          default=False,
202
                          help="Include debug output")
203
        parser.add_argument('-i', '--include', dest='include', action='store_true',
204
                          default=False,
205
                          help="Include protocol headers in the output")
206
        parser.add_argument('-s', '--silent', dest='silent', action='store_true',
207
                          default=False,
208
                          help="Silent mode, don't output anything")
209
        parser.add_argument('-v', '--verbose', dest='verbose', action='store_true',
210
                          default=False,
211
                          help="Make the operation more talkative")
212
        parser.add_argument('-V', '--version', dest='version', action='store_true',
213
                          default=False,
214
                          help="Show version number and quit")
215
        parser.add_argument('-o', dest='options', action='append',
216
                          default=[], metavar="KEY=VAL",
217
                          help="Override a config value")
218
        return parser
219

    
220
    def find_term_in_args(arg_list, term_list):
221
        """find an arg_list term in term_list. All other terms up to found
222
        term are rearanged at the end of arg_list, preserving relative order
223
        """
224
        arg_tail = []
225
        while len(arg_list) > 0:
226
            group = arg_list.pop(0)
227
            if group not in term_list:
228
                arg_tail.append(group)
229
            else:
230
                arg_list += arg_tail
231
                return group
232
        return None
233

    
234
    """Main Code"""
235
    exe = basename(sys.argv[0])
236
    parser = init_parser(exe)
237
    args, argv = parser.parse_known_args()
238

    
239
    #print version
240
    if args.version:
241
        import kamaki
242
        print("kamaki %s" % kamaki.__version__)
243
        exit(0)
244

    
245
    config = Config(args.config) if args.config else Config()
246

    
247
    #load config options from command line
248
    for option in args.options:
249
        keypath, sep, val = option.partition('=')
250
        if not sep:
251
            print("Invalid option '%s'" % option)
252
            exit(1)
253
        section, sep, key = keypath.partition('.')
254
        if not sep:
255
            print("Invalid option '%s'" % option)
256
            exit(1)
257
        config.override(section.strip(), key.strip(), val.strip())
258

    
259
    load_groups(config)
260
    group = find_term_in_args(argv, _commands)
261
    if not group:
262
        parser.print_help()
263
        print_groups()
264
        exit(0)
265

    
266
    parser.prog = '%s %s <command>' % (exe, group)
267
    command = find_term_in_args(argv, _commands[group])
268

    
269
    if not command:
270
        parser.print_help()
271
        print_commands(group)
272
        exit(0)
273

    
274
    cmd = _commands[group][command]()
275

    
276
    parser.prog = '%s %s %s' % (exe, group, command)
277
    if cmd.syntax:
278
        parser.prog += '  %s' % cmd.syntax
279
    parser.description = cmd.description
280
    parser.epilog = ''
281
    if hasattr(cmd, 'update_parser'):
282
        cmd.update_parser(parser)
283

    
284
    #check other args
285
    args, argv = parser.parse_known_args()
286
    if group != argv[0]:
287
        errmsg = red('Invalid command group '+argv[0])
288
        print(errmsg, file=stderr)
289
        exit(1)
290
    if command != argv[1]:
291
        errmsg = red('Invalid command "%s" in group "%s"'%(argv[1], argv[0]))
292
        print(errmsg, file=stderr)
293
        exit(1)
294

    
295
    if args.help:
296
        parser.print_help()
297
        exit(0)
298

    
299
    manage_logging_handlers(args)
300
    cmd.args = args
301
    cmd.config = config
302
    try:
303
        ret = cmd.main(*argv[2:])
304
        exit(ret)
305
    except TypeError as e:
306
        if e.args and e.args[0].startswith('main()'):
307
            parser.print_help()
308
            exit(1)
309
        else:
310
            raise
311
    except CLIError as err:
312
        errmsg = 'CLI Error '
313
        errmsg += '(%s): '%err.status if err.status else ': '
314
        errmsg += err.message if err.message else ''
315
        if err.importance == 1:
316
            errmsg = yellow(errmsg)
317
        elif err.importance == 2:
318
            errmsg = magenta(errmsg)
319
        elif err.importance > 2:
320
            errmsg = red(errmsg)
321
        print(errmsg, file=stderr)
322
        exit(1)
323

    
324
if __name__ == '__main__':
325
    main()