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() |