Revision 7493ccb6
b/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 += 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() |
b/kamaki/cli/commands/astakos_cli.py | ||
---|---|---|
1 |
# Copyright 2011-2012 GRNET S.A. All rights reserved. |
|
2 |
# |
|
3 |
# Redistribution and use in source and binary forms, with or |
|
4 |
# without modification, are permitted provided that the following |
|
5 |
# conditions are met: |
|
6 |
# |
|
7 |
# 1. Redistributions of source code must retain the above |
|
8 |
# copyright notice, this list of conditions and the following |
|
9 |
# disclaimer. |
|
10 |
# |
|
11 |
# 2. Redistributions in binary form must reproduce the above |
|
12 |
# copyright notice, this list of conditions and the following |
|
13 |
# disclaimer in the documentation and/or other materials |
|
14 |
# provided with the distribution. |
|
15 |
# |
|
16 |
# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS |
|
17 |
# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED |
|
18 |
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR |
|
19 |
# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR |
|
20 |
# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, |
|
21 |
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT |
|
22 |
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF |
|
23 |
# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED |
|
24 |
# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT |
|
25 |
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN |
|
26 |
# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE |
|
27 |
# POSSIBILITY OF SUCH DAMAGE. |
|
28 |
# |
|
29 |
# The views and conclusions contained in the software and |
|
30 |
# documentation are those of the authors and should not be |
|
31 |
# interpreted as representing official policies, either expressed |
|
32 |
# or implied, of GRNET S.A.command |
|
33 |
|
|
34 |
|
|
35 |
from kamaki.cli import command, set_api_description |
|
36 |
set_api_description('astakos', 'Astakos API commands') |
|
37 |
from kamaki.clients.astakos import AstakosClient, ClientError |
|
38 |
from kamaki.cli.utils import raiseCLIError, print_dict |
|
39 |
|
|
40 |
class _astakos_init(object): |
|
41 |
def main(self): |
|
42 |
token = self.config.get('astakos', 'token') or self.config.get('global', 'token') |
|
43 |
base_url = self.config.get('astakos', 'url') or self.config.get('global', 'url') |
|
44 |
if base_url is None: |
|
45 |
raise ClientError('no URL for astakos') |
|
46 |
self.client = AstakosClient(base_url=base_url, token=token) |
|
47 |
|
|
48 |
@command() |
|
49 |
class astakos_authenticate(_astakos_init): |
|
50 |
"""Authenticate a user""" |
|
51 |
|
|
52 |
def main(self): |
|
53 |
super(astakos_authenticate, self).main() |
|
54 |
try: |
|
55 |
reply = self.client.authenticate() |
|
56 |
except ClientError as err: |
|
57 |
raiseCLIError(err) |
|
58 |
print_dict(reply) |
b/kamaki/cli/commands/config_cli.py | ||
---|---|---|
1 |
# Copyright 2011-2012 GRNET S.A. All rights reserved. |
|
2 |
# |
|
3 |
# Redistribution and use in source and binary forms, with or |
|
4 |
# without modification, are permitted provided that the following |
|
5 |
# conditions are met: |
|
6 |
# |
|
7 |
# 1. Redistributions of source code must retain the above |
|
8 |
# copyright notice, this list of conditions and the following |
|
9 |
# disclaimer. |
|
10 |
# |
|
11 |
# 2. Redistributions in binary form must reproduce the above |
|
12 |
# copyright notice, this list of conditions and the following |
|
13 |
# disclaimer in the documentation and/or other materials |
|
14 |
# provided with the distribution. |
|
15 |
# |
|
16 |
# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS |
|
17 |
# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED |
|
18 |
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR |
|
19 |
# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR |
|
20 |
# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, |
|
21 |
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT |
|
22 |
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF |
|
23 |
# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED |
|
24 |
# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT |
|
25 |
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN |
|
26 |
# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE |
|
27 |
# POSSIBILITY OF SUCH DAMAGE. |
|
28 |
# |
|
29 |
# The views and conclusions contained in the software and |
|
30 |
# documentation are those of the authors and should not be |
|
31 |
# interpreted as representing official policies, either expressed |
|
32 |
# or implied, of GRNET S.A. |
|
33 |
|
|
34 |
from kamaki.cli import command, set_api_description |
|
35 |
set_api_description('config', 'Configuration commands') |
|
36 |
|
|
37 |
@command() |
|
38 |
class config_list(object): |
|
39 |
"""List configuration options""" |
|
40 |
|
|
41 |
def update_parser(self, parser): |
|
42 |
parser.add_argument('-a', dest='all', action='store_true', |
|
43 |
default=False, help='include default values') |
|
44 |
|
|
45 |
def main(self): |
|
46 |
include_defaults = self.args.all |
|
47 |
for section in sorted(self.config.sections()): |
|
48 |
items = self.config.items(section, include_defaults) |
|
49 |
for key, val in sorted(items): |
|
50 |
print('%s.%s = %s' % (section, key, val)) |
|
51 |
|
|
52 |
@command() |
|
53 |
class config_get(object): |
|
54 |
"""Show a configuration option""" |
|
55 |
|
|
56 |
def main(self, option): |
|
57 |
section, sep, key = option.rpartition('.') |
|
58 |
section = section or 'global' |
|
59 |
value = self.config.get(section, key) |
|
60 |
if value is not None: |
|
61 |
print(value) |
|
62 |
|
|
63 |
@command() |
|
64 |
class config_set(object): |
|
65 |
"""Set a configuration option""" |
|
66 |
|
|
67 |
def main(self, option, value): |
|
68 |
section, sep, key = option.rpartition('.') |
|
69 |
section = section or 'globail' |
|
70 |
self.config.set(section, key, value) |
|
71 |
self.config.write() |
|
72 |
|
|
73 |
@command() |
|
74 |
class config_delete(object): |
|
75 |
"""Delete a configuration option (and use the default value)""" |
|
76 |
|
|
77 |
def main(self, option): |
|
78 |
section, sep, key = option.rpartition('.') |
|
79 |
section = section or 'global' |
|
80 |
self.config.remove_option(section, key) |
|
81 |
self.config.write() |
b/kamaki/cli/commands/cyclades_cli.py | ||
---|---|---|
1 |
# Copyright 2012 GRNET S.A. All rights reserved. |
|
2 |
# |
|
3 |
# Redistribution and use in source and binary forms, with or |
|
4 |
# without modification, are permitted provided that the following |
|
5 |
# conditions are met: |
|
6 |
# |
|
7 |
# 1. Redistributions of source code must retain the above |
|
8 |
# copyright notice, this list of conditions and the following |
|
9 |
# disclaimer. |
|
10 |
# |
|
11 |
# 2. Redistributions in binary form must reproduce the above |
|
12 |
# copyright notice, this list of conditions and the following |
|
13 |
# disclaimer in the documentation and/or other materials |
|
14 |
# provided with the distribution. |
|
15 |
# |
|
16 |
# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS |
|
17 |
# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED |
|
18 |
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR |
|
19 |
# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR |
|
20 |
# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, |
|
21 |
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT |
|
22 |
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF |
|
23 |
# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED |
|
24 |
# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT |
|
25 |
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN |
|
26 |
# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE |
|
27 |
# POSSIBILITY OF SUCH DAMAGE. |
|
28 |
# |
|
29 |
# The views and conclusions contained in the software and |
|
30 |
# documentation are those of the authors and should not be |
|
31 |
# interpreted as representing official policies, either expressed |
|
32 |
# or implied, of GRNET S.A. |
|
33 |
|
|
34 |
from kamaki.cli import command, set_api_description, CLIError |
|
35 |
from kamaki.cli.utils import print_dict, print_items, print_list, format_size, raiseCLIError |
|
36 |
from colors import bold |
|
37 |
set_api_description('server', "Compute/Cyclades API server commands") |
|
38 |
set_api_description('flavor', "Compute/Cyclades API flavor commands") |
|
39 |
set_api_description('image', "Compute/Cyclades or Glance API image commands") |
|
40 |
set_api_description('network', "Compute/Cyclades API network commands") |
|
41 |
from kamaki.clients.cyclades import CycladesClient, ClientError |
|
42 |
|
|
43 |
class _init_cyclades(object): |
|
44 |
def main(self): |
|
45 |
token = self.config.get('compute', 'token') or self.config.get('global', 'token') |
|
46 |
base_url = self.config.get('compute', 'url') or self.config.get('global', 'url') |
|
47 |
self.client = CycladesClient(base_url=base_url, token=token) |
|
48 |
|
|
49 |
@command() |
|
50 |
class server_list(_init_cyclades): |
|
51 |
"""List servers""" |
|
52 |
|
|
53 |
def _print(self, servers): |
|
54 |
for server in servers: |
|
55 |
sname = server.pop('name') |
|
56 |
sid = server.pop('id') |
|
57 |
print('%s (%s)'%(bold(sname), bold(unicode(sid)))) |
|
58 |
if getattr(self.args, 'detail'): |
|
59 |
server_info._print(server) |
|
60 |
print('- - -') |
|
61 |
|
|
62 |
def update_parser(self, parser): |
|
63 |
parser.add_argument('-l', dest='detail', action='store_true', |
|
64 |
default=False, help='show detailed output') |
|
65 |
|
|
66 |
def main(self): |
|
67 |
super(self.__class__, self).main() |
|
68 |
try: |
|
69 |
servers = self.client.list_servers(self.args.detail) |
|
70 |
self._print(servers) |
|
71 |
#print_items(servers) |
|
72 |
except ClientError as err: |
|
73 |
raiseCLIError(err) |
|
74 |
|
|
75 |
@command() |
|
76 |
class server_info(_init_cyclades): |
|
77 |
"""Get server details""" |
|
78 |
|
|
79 |
@classmethod |
|
80 |
def _print(self,server): |
|
81 |
addr_dict = {} |
|
82 |
if server.has_key('attachments'): |
|
83 |
for addr in server['attachments']['values']: |
|
84 |
ips = addr.pop('values', []) |
|
85 |
for ip in ips: |
|
86 |
addr['IPv%s'%ip['version']] = ip['addr'] |
|
87 |
if addr.has_key('firewallProfile'): |
|
88 |
addr['firewall'] = addr.pop('firewallProfile') |
|
89 |
addr_dict[addr.pop('id')] = addr |
|
90 |
server['attachments'] = addr_dict if addr_dict is not {} else None |
|
91 |
if server.has_key('metadata'): |
|
92 |
server['metadata'] = server['metadata']['values'] |
|
93 |
print_dict(server, ident=14) |
|
94 |
|
|
95 |
def main(self, server_id): |
|
96 |
super(self.__class__, self).main() |
|
97 |
try: |
|
98 |
server = self.client.get_server_details(int(server_id)) |
|
99 |
except ClientError as err: |
|
100 |
raiseCLIError(err) |
|
101 |
except ValueError as err: |
|
102 |
raise CLIError(message='Server id must be positive integer', |
|
103 |
importance=1) |
|
104 |
self._print(server) |
|
105 |
|
|
106 |
@command() |
|
107 |
class server_create(_init_cyclades): |
|
108 |
"""Create a server""" |
|
109 |
|
|
110 |
def update_parser(self, parser): |
|
111 |
parser.add_argument('--personality', dest='personalities', |
|
112 |
action='append', default=[], |
|
113 |
metavar='PATH[,SERVER PATH[,OWNER[,GROUP,[MODE]]]]', |
|
114 |
help='add a personality file') |
|
115 |
|
|
116 |
def main(self, name, flavor_id, image_id): |
|
117 |
super(self.__class__, self).main() |
|
118 |
personalities = [] |
|
119 |
for personality in self.args.personalities: |
|
120 |
p = personality.split(',') |
|
121 |
p.extend([None] * (5 - len(p))) # Fill missing fields with None |
|
122 |
|
|
123 |
path = p[0] |
|
124 |
|
|
125 |
if not path: |
|
126 |
raise CLIError(message='Invalid personality argument %s'%p, importance=1) |
|
127 |
if not exists(path): |
|
128 |
raise CLIError(message="File %s does not exist" % path, importance=1) |
|
129 |
|
|
130 |
with open(path) as f: |
|
131 |
contents = b64encode(f.read()) |
|
132 |
|
|
133 |
d = {'path': p[1] or abspath(path), 'contents': contents} |
|
134 |
if p[2]: |
|
135 |
d['owner'] = p[2] |
|
136 |
if p[3]: |
|
137 |
d['group'] = p[3] |
|
138 |
if p[4]: |
|
139 |
d['mode'] = int(p[4]) |
|
140 |
personalities.append(d) |
|
141 |
|
|
142 |
try: |
|
143 |
reply = self.client.create_server(name, int(flavor_id), image_id, |
|
144 |
personalities) |
|
145 |
except ClientError as err: |
|
146 |
raiseCLIError(err) |
|
147 |
print_dict(reply) |
|
148 |
|
|
149 |
@command() |
|
150 |
class server_rename(_init_cyclades): |
|
151 |
"""Update a server's name""" |
|
152 |
|
|
153 |
def main(self, server_id, new_name): |
|
154 |
super(self.__class__, self).main() |
|
155 |
try: |
|
156 |
self.client.update_server_name(int(server_id), new_name) |
|
157 |
except ClientError as err: |
|
158 |
raiseCLIError(err) |
|
159 |
except ValueError: |
|
160 |
raise CLIError(message='Server id must be positive integer', importance=1) |
|
161 |
|
|
162 |
@command() |
|
163 |
class server_delete(_init_cyclades): |
|
164 |
"""Delete a server""" |
|
165 |
|
|
166 |
def main(self, server_id): |
|
167 |
super(self.__class__, self).main() |
|
168 |
try: |
|
169 |
self.client.delete_server(int(server_id)) |
|
170 |
except ClientError as err: |
|
171 |
raiseCLIError(err) |
|
172 |
except ValueError: |
|
173 |
raise CLIError(message='Server id must be positive integer', importance=1) |
|
174 |
|
|
175 |
@command() |
|
176 |
class server_reboot(_init_cyclades): |
|
177 |
"""Reboot a server""" |
|
178 |
|
|
179 |
def update_parser(self, parser): |
|
180 |
parser.add_argument('-f', dest='hard', action='store_true', |
|
181 |
default=False, help='perform a hard reboot') |
|
182 |
|
|
183 |
def main(self, server_id): |
|
184 |
super(self.__class__, self).main() |
|
185 |
try: |
|
186 |
self.client.reboot_server(int(server_id), self.args.hard) |
|
187 |
except ClientError as err: |
|
188 |
raiseCLIError(err) |
|
189 |
except ValueError: |
|
190 |
raise CLIError(message='Server id must be positive integer', importance=1) |
|
191 |
|
|
192 |
@command() |
|
193 |
class server_start(_init_cyclades): |
|
194 |
"""Start a server""" |
|
195 |
|
|
196 |
def main(self, server_id): |
|
197 |
super(self.__class__, self).main() |
|
198 |
try: |
|
199 |
self.client.start_server(int(server_id)) |
|
200 |
except ClientError as err: |
|
201 |
raiseCLIError(err) |
|
202 |
except ValueError: |
|
203 |
raise CLIError(message='Server id must be positive integer', importance=1) |
|
204 |
|
|
205 |
@command() |
|
206 |
class server_shutdown(_init_cyclades): |
|
207 |
"""Shutdown a server""" |
|
208 |
|
|
209 |
def main(self, server_id): |
|
210 |
super(self.__class__, self).main() |
|
211 |
try: |
|
212 |
self.client.shutdown_server(int(server_id)) |
|
213 |
except ClientError as err: |
|
214 |
raiseCLIError(err) |
|
215 |
except ValueError: |
|
216 |
raise CLIError(message='Server id must be positive integer', importance=1) |
|
217 |
|
|
218 |
@command() |
|
219 |
class server_console(_init_cyclades): |
|
220 |
"""Get a VNC console""" |
|
221 |
|
|
222 |
def main(self, server_id): |
|
223 |
super(self.__class__, self).main() |
|
224 |
try: |
|
225 |
reply = self.client.get_server_console(int(server_id)) |
|
226 |
except ClientError as err: |
|
227 |
raiseCLIError(err) |
|
228 |
except ValueError: |
|
229 |
raise CLIError(message='Server id must be positive integer', importance=1) |
|
230 |
print_dict(reply) |
|
231 |
|
|
232 |
@command() |
|
233 |
class server_firewall(_init_cyclades): |
|
234 |
"""Set the server's firewall profile""" |
|
235 |
|
|
236 |
def main(self, server_id, profile): |
|
237 |
super(self.__class__, self).main() |
|
238 |
try: |
|
239 |
self.client.set_firewall_profile(int(server_id), profile) |
|
240 |
except ClientError as err: |
|
241 |
raiseCLIError(err) |
|
242 |
except ValueError: |
|
243 |
raise CLIError(message='Server id must be positive integer', importance=1) |
|
244 |
@command() |
|
245 |
class server_addr(_init_cyclades): |
|
246 |
"""List a server's nic address""" |
|
247 |
|
|
248 |
def main(self, server_id): |
|
249 |
super(self.__class__, self).main() |
|
250 |
try: |
|
251 |
reply = self.client.list_server_nics(int(server_id)) |
|
252 |
except ClientError as err: |
|
253 |
raiseCLIError(err) |
|
254 |
except ValueError: |
|
255 |
raise CLIError(message='Server id must be positive integer', importance=1) |
|
256 |
print_list(reply) |
|
257 |
|
|
258 |
@command() |
|
259 |
class server_meta(_init_cyclades): |
|
260 |
"""Get a server's metadata""" |
|
261 |
|
|
262 |
def main(self, server_id, key=None): |
|
263 |
super(self.__class__, self).main() |
|
264 |
try: |
|
265 |
reply = self.client.get_server_metadata(int(server_id), key) |
|
266 |
except ClientError as err: |
|
267 |
raiseCLIError(err) |
|
268 |
except ValueError: |
|
269 |
raise CLIError(message='Server id must be positive integer', importance=1) |
|
270 |
print_dict(reply) |
|
271 |
|
|
272 |
@command() |
|
273 |
class server_addmeta(_init_cyclades): |
|
274 |
"""Add server metadata""" |
|
275 |
|
|
276 |
def main(self, server_id, key, val): |
|
277 |
super(self.__class__, self).main() |
|
278 |
try: |
|
279 |
reply = self.client.create_server_metadata(int(server_id), key, val) |
|
280 |
except ClientError as err: |
|
281 |
raiseCLIError(err) |
|
282 |
except ValueError: |
|
283 |
raise CLIError(message='Server id must be positive integer', importance=1) |
|
284 |
print_dict(reply) |
|
285 |
|
|
286 |
@command() |
|
287 |
class server_setmeta(_init_cyclades): |
|
288 |
"""Update server's metadata""" |
|
289 |
|
|
290 |
def main(self, server_id, key, val): |
|
291 |
super(self.__class__, self).main() |
|
292 |
metadata = {key: val} |
|
293 |
try: |
|
294 |
reply = self.client.update_server_metadata(int(server_id), **metadata) |
|
295 |
except ClientError as err: |
|
296 |
raiseCLIError(err) |
|
297 |
except ValueError: |
|
298 |
raise CLIError(message='Server id must be positive integer', importance=1) |
|
299 |
print_dict(reply) |
|
300 |
|
|
301 |
@command() |
|
302 |
class server_delmeta(_init_cyclades): |
|
303 |
"""Delete server metadata""" |
|
304 |
|
|
305 |
def main(self, server_id, key): |
|
306 |
super(self.__class__, self).main() |
|
307 |
try: |
|
308 |
self.client.delete_server_metadata(int(server_id), key) |
|
309 |
except ClientError as err: |
|
310 |
raiseCLIError(err) |
|
311 |
except ValueError: |
|
312 |
raise CLIError(message='Server id must be positive integer', importance=1) |
|
313 |
|
|
314 |
@command() |
|
315 |
class server_stats(_init_cyclades): |
|
316 |
"""Get server statistics""" |
|
317 |
|
|
318 |
def main(self, server_id): |
|
319 |
super(self.__class__, self).main() |
|
320 |
try: |
|
321 |
reply = self.client.get_server_stats(int(server_id)) |
|
322 |
except ClientError as err: |
|
323 |
raiseCLIError(err) |
|
324 |
except ValueError: |
|
325 |
raise CLIError(message='Server id must be positive integer', importance=1) |
|
326 |
print_dict(reply, exclude=('serverRef',)) |
|
327 |
|
|
328 |
@command() |
|
329 |
class flavor_list(_init_cyclades): |
|
330 |
"""List flavors""" |
|
331 |
|
|
332 |
def update_parser(self, parser): |
|
333 |
parser.add_argument('-l', dest='detail', action='store_true', |
|
334 |
default=False, help='show detailed output') |
|
335 |
|
|
336 |
def main(self): |
|
337 |
super(self.__class__, self).main() |
|
338 |
try: |
|
339 |
flavors = self.client.list_flavors(self.args.detail) |
|
340 |
except ClientError as err: |
|
341 |
raiseCLIError(err) |
|
342 |
print_items(flavors) |
|
343 |
|
|
344 |
@command() |
|
345 |
class flavor_info(_init_cyclades): |
|
346 |
"""Get flavor details""" |
|
347 |
|
|
348 |
def main(self, flavor_id): |
|
349 |
super(self.__class__, self).main() |
|
350 |
try: |
|
351 |
flavor = self.client.get_flavor_details(int(flavor_id)) |
|
352 |
except ClientError as err: |
|
353 |
raiseCLIError(err) |
|
354 |
except ValueError: |
|
355 |
raise CLIError(message='Server id must be positive integer', importance=1) |
|
356 |
print_dict(flavor) |
|
357 |
|
|
358 |
@command() |
|
359 |
class image_list(_init_cyclades): |
|
360 |
"""List images""" |
|
361 |
|
|
362 |
def update_parser(self, parser): |
|
363 |
parser.add_argument('-l', dest='detail', action='store_true', |
|
364 |
default=False, help='show detailed output') |
|
365 |
|
|
366 |
def _print(self, images): |
|
367 |
for img in images: |
|
368 |
iname = img.pop('name') |
|
369 |
iid = img.pop('id') |
|
370 |
print('%s (%s)'%(bold(iname), bold(unicode(iid)))) |
|
371 |
if getattr(self.args, 'detail'): |
|
372 |
image_info._print(img) |
|
373 |
print('- - -') |
|
374 |
|
|
375 |
def main(self): |
|
376 |
super(self.__class__, self).main() |
|
377 |
try: |
|
378 |
images = self.client.list_images(self.args.detail) |
|
379 |
except ClientError as err: |
|
380 |
raiseCLIError(err) |
|
381 |
#print_items(images) |
|
382 |
self._print(images) |
|
383 |
|
|
384 |
@command() |
|
385 |
class image_info(_init_cyclades): |
|
386 |
"""Get image details""" |
|
387 |
|
|
388 |
@classmethod |
|
389 |
def _print(self, image): |
|
390 |
if image.has_key('metadata'): |
|
391 |
image['metadata'] = image['metadata']['values'] |
|
392 |
print_dict(image, ident=14) |
|
393 |
|
|
394 |
def main(self, image_id): |
|
395 |
super(self.__class__, self).main() |
|
396 |
try: |
|
397 |
image = self.client.get_image_details(image_id) |
|
398 |
except ClientError as err: |
|
399 |
raiseCLIError(err) |
|
400 |
self._print(image) |
|
401 |
|
|
402 |
@command() |
|
403 |
class image_delete(_init_cyclades): |
|
404 |
"""Delete image""" |
|
405 |
|
|
406 |
def main(self, image_id): |
|
407 |
super(self.__class__, self).main() |
|
408 |
try: |
|
409 |
self.client.delete_image(image_id) |
|
410 |
except ClientError as err: |
|
411 |
raiseCLIError(err) |
|
412 |
|
|
413 |
@command() |
|
414 |
class image_properties(_init_cyclades): |
|
415 |
"""Get image properties""" |
|
416 |
|
|
417 |
def main(self, image_id, key=None): |
|
418 |
super(self.__class__, self).main() |
|
419 |
try: |
|
420 |
reply = self.client.get_image_metadata(image_id, key) |
|
421 |
except ClientError as err: |
|
422 |
raiseCLIError(err) |
|
423 |
print_dict(reply) |
|
424 |
|
|
425 |
@command() |
|
426 |
class image_addproperty(_init_cyclades): |
|
427 |
"""Add an image property""" |
|
428 |
|
|
429 |
def main(self, image_id, key, val): |
|
430 |
super(self.__class__, self).main() |
|
431 |
try: |
|
432 |
reply = self.client.create_image_metadata(image_id, key, val) |
|
433 |
except ClientError as err: |
|
434 |
raiseCLIError(err) |
|
435 |
print_dict(reply) |
|
436 |
|
|
437 |
@command() |
|
438 |
class image_setproperty(_init_cyclades): |
|
439 |
"""Update an image property""" |
|
440 |
|
|
441 |
def main(self, image_id, key, val): |
|
442 |
super(self.__class__, self).main() |
|
443 |
metadata = {key: val} |
|
444 |
try: |
|
445 |
reply = self.client.update_image_metadata(image_id, **metadata) |
|
446 |
except ClientError as err: |
|
447 |
raiseCLIError(err) |
|
448 |
print_dict(reply) |
|
449 |
|
|
450 |
@command() |
|
451 |
class image_delproperty(_init_cyclades): |
|
452 |
"""Delete an image property""" |
|
453 |
|
|
454 |
def main(self, image_id, key): |
|
455 |
super(self.__class__, self).main() |
|
456 |
try: |
|
457 |
self.client.delete_image_metadata(image_id, key) |
|
458 |
except ClientError as err: |
|
459 |
raiseCLIError(err) |
|
460 |
|
|
461 |
@command() |
|
462 |
class network_list(_init_cyclades): |
|
463 |
"""List networks""" |
|
464 |
|
|
465 |
def update_parser(self, parser): |
|
466 |
parser.add_argument('-l', dest='detail', action='store_true', |
|
467 |
default=False, help='show detailed output') |
|
468 |
|
|
469 |
def print_networks(self, nets): |
|
470 |
for net in nets: |
|
471 |
netname = bold(net.pop('name')) |
|
472 |
netid = bold(unicode(net.pop('id'))) |
|
473 |
print('%s (%s)'%(netname, netid)) |
|
474 |
if getattr(self.args, 'detail'): |
|
475 |
network_info.print_network(net) |
|
476 |
print('- - -') |
|
477 |
|
|
478 |
def main(self): |
|
479 |
super(self.__class__, self).main() |
|
480 |
try: |
|
481 |
networks = self.client.list_networks(self.args.detail) |
|
482 |
except ClientError as err: |
|
483 |
raiseCLIError(err) |
|
484 |
self.print_networks(networks) |
|
485 |
|
|
486 |
@command() |
|
487 |
class network_create(_init_cyclades): |
|
488 |
"""Create a network""" |
|
489 |
|
|
490 |
def update_parser(self, parser): |
|
491 |
try: |
|
492 |
super(self.__class__, self).update_parser(parser) |
|
493 |
except AttributeError: |
|
494 |
pass |
|
495 |
parser.add_argument('--with-cidr', action='store', dest='cidr', default=False, |
|
496 |
help='specific cidr for new network') |
|
497 |
parser.add_argument('--with-gateway', action='store', dest='gateway', default=False, |
|
498 |
help='specific getaway for new network') |
|
499 |
parser.add_argument('--with-dhcp', action='store', dest='dhcp', default=False, |
|
500 |
help='specific dhcp for new network') |
|
501 |
parser.add_argument('--with-type', action='store', dest='type', default=False, |
|
502 |
help='specific type for new network') |
|
503 |
|
|
504 |
def main(self, name): |
|
505 |
super(self.__class__, self).main() |
|
506 |
try: |
|
507 |
reply = self.client.create_network(name, cidr=getattr(self.args, 'cidr'), |
|
508 |
gateway=getattr(self.args, 'gateway'), dhcp=getattr(self.args, 'gateway'), |
|
509 |
type=getattr(self.args, 'type')) |
|
510 |
except ClientError as err: |
|
511 |
raiseCLIError(err) |
|
512 |
print_dict(reply) |
|
513 |
|
|
514 |
@command() |
|
515 |
class network_info(_init_cyclades): |
|
516 |
"""Get network details""" |
|
517 |
|
|
518 |
@classmethod |
|
519 |
def print_network(self, net): |
|
520 |
if net.has_key('attachments'): |
|
521 |
att = net['attachments']['values'] |
|
522 |
net['attachments'] = att if len(att) > 0 else None |
|
523 |
print_dict(net, ident=14) |
|
524 |
|
|
525 |
def main(self, network_id): |
|
526 |
super(self.__class__, self).main() |
|
527 |
try: |
|
528 |
network = self.client.get_network_details(network_id) |
|
529 |
except ClientError as err: |
|
530 |
raiseCLIError(err) |
|
531 |
network_info.print_network(network) |
|
532 |
|
|
533 |
@command() |
|
534 |
class network_rename(_init_cyclades): |
|
535 |
"""Update network name""" |
|
536 |
|
|
537 |
def main(self, network_id, new_name): |
|
538 |
super(self.__class__, self).main() |
|
539 |
try: |
|
540 |
self.client.update_network_name(network_id, new_name) |
|
541 |
except ClientError as err: |
|
542 |
raiseCLIError(err) |
|
543 |
|
|
544 |
@command() |
|
545 |
class network_delete(_init_cyclades): |
|
546 |
"""Delete a network""" |
|
547 |
|
|
548 |
def main(self, network_id): |
|
549 |
super(self.__class__, self).main() |
|
550 |
try: |
|
551 |
self.client.delete_network(network_id) |
|
552 |
except ClientError as err: |
|
553 |
raiseCLIError(err) |
|
554 |
|
|
555 |
@command() |
|
556 |
class network_connect(_init_cyclades): |
|
557 |
"""Connect a server to a network""" |
|
558 |
|
|
559 |
def main(self, server_id, network_id): |
|
560 |
super(self.__class__, self).main() |
|
561 |
try: |
|
562 |
self.client.connect_server(server_id, network_id) |
|
563 |
except ClientError as err: |
|
564 |
raiseCLIError(err) |
|
565 |
|
|
566 |
@command() |
|
567 |
class network_disconnect(_init_cyclades): |
|
568 |
"""Disconnect a nic that connects a server to a network""" |
|
569 |
|
|
570 |
def main(self, nic_id): |
|
571 |
super(self.__class__, self).main() |
|
572 |
try: |
|
573 |
server_id = nic_id.split('-')[1] |
|
574 |
self.client.disconnect_server(server_id, nic_id) |
|
575 |
except IndexError: |
|
576 |
raise CLIError(message='Incorrect nic format', importance=1, |
|
577 |
details='nid_id format: nic-<server_id>-<nic_index>') |
|
578 |
except ClientError as err: |
|
579 |
raiseCLIError(err) |
b/kamaki/cli/commands/image_cli.py | ||
---|---|---|
1 |
# Copyright 2011-2012 GRNET S.A. All rights reserved. |
|
2 |
# |
|
3 |
# Redistribution and use in source and binary forms, with or |
|
4 |
# without modification, are permitted provided that the following |
|
5 |
# conditions are met: |
|
6 |
# |
|
7 |
# 1. Redistributions of source code must retain the above |
|
8 |
# copyright notice, this list of conditions and the following |
|
9 |
# disclaimer. |
|
10 |
# |
|
11 |
# 2. Redistributions in binary form must reproduce the above |
|
12 |
# copyright notice, this list of conditions and the following |
|
13 |
# disclaimer in the documentation and/or other materials |
|
14 |
# provided with the distribution. |
|
15 |
# |
|
16 |
# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS |
|
17 |
# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED |
|
18 |
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR |
|
19 |
# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR |
|
20 |
# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, |
|
21 |
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT |
|
22 |
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF |
|
23 |
# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED |
|
24 |
# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT |
|
25 |
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN |
|
26 |
# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE |
|
27 |
# POSSIBILITY OF SUCH DAMAGE. |
|
28 |
# |
|
29 |
# The views and conclusions contained in the software and |
|
30 |
# documentation are those of the authors and should not be |
|
31 |
# interpreted as representing official policies, either expressed |
|
32 |
# or implied, of GRNET S.A.command |
|
33 |
|
|
34 |
from kamaki.cli import command, set_api_description |
|
35 |
from kamaki.cli.utils import print_dict, print_items, raiseCLIError |
|
36 |
set_api_description('image', "Compute/Cyclades or Glance API image commands") |
|
37 |
from kamaki.clients.image import ImageClient, ClientError |
|
38 |
|
|
39 |
class _init_image(object): |
|
40 |
def main(self): |
|
41 |
try: |
|
42 |
token = self.config.get('image', 'token') or self.config.get('global', 'token') |
|
43 |
base_url = self.config.get('image', 'url') or self.config.get('global', 'url') |
|
44 |
self.client = ImageClient(base_url=base_url, token=token) |
|
45 |
except ClientError as err: |
|
46 |
raiseCLIError(err) |
|
47 |
|
|
48 |
@command() |
|
49 |
class image_public(_init_image): |
|
50 |
"""List public images""" |
|
51 |
|
|
52 |
def update_parser(self, parser): |
|
53 |
parser.add_argument('-l', dest='detail', action='store_true', |
|
54 |
default=False, help='show detailed output') |
|
55 |
parser.add_argument('--container-format', dest='container_format', |
|
56 |
metavar='FORMAT', help='filter by container format') |
|
57 |
parser.add_argument('--disk-format', dest='disk_format', |
|
58 |
metavar='FORMAT', help='filter by disk format') |
|
59 |
parser.add_argument('--name', dest='name', metavar='NAME', |
|
60 |
help='filter by name') |
|
61 |
parser.add_argument('--size-min', dest='size_min', metavar='BYTES', |
|
62 |
help='filter by minimum size') |
|
63 |
parser.add_argument('--size-max', dest='size_max', metavar='BYTES', |
|
64 |
help='filter by maximum size') |
|
65 |
parser.add_argument('--status', dest='status', metavar='STATUS', |
|
66 |
help='filter by status') |
|
67 |
parser.add_argument('--order', dest='order', metavar='FIELD', |
|
68 |
help='order by FIELD (use a - prefix to reverse order)') |
|
69 |
|
|
70 |
def main(self): |
|
71 |
super(self.__class__, self).main() |
|
72 |
filters = {} |
|
73 |
for filter in ('container_format', 'disk_format', 'name', 'size_min', |
|
74 |
'size_max', 'status'): |
|
75 |
val = getattr(self.args, filter, None) |
|
76 |
if val is not None: |
|
77 |
filters[filter] = val |
|
78 |
|
|
79 |
order = self.args.order or '' |
|
80 |
try: |
|
81 |
images = self.client.list_public(self.args.detail, |
|
82 |
filters=filters, order=order) |
|
83 |
except ClientError as err: |
|
84 |
raiseCLIError(err) |
|
85 |
print_items(images, title=('name',)) |
|
86 |
|
|
87 |
@command() |
|
88 |
class image_meta(_init_image): |
|
89 |
"""Get image metadata""" |
|
90 |
|
|
91 |
def main(self, image_id): |
|
92 |
super(self.__class__, self).main() |
|
93 |
try: |
|
94 |
image = self.client.get_meta(image_id) |
|
95 |
except ClientError as err: |
|
96 |
raiseCLIError(err) |
|
97 |
print_dict(image) |
|
98 |
|
|
99 |
@command() |
|
100 |
class image_register(_init_image): |
|
101 |
"""Register an image""" |
|
102 |
|
|
103 |
def update_parser(self, parser): |
|
104 |
parser.add_argument('--checksum', dest='checksum', metavar='CHECKSUM', |
|
105 |
help='set image checksum') |
|
106 |
parser.add_argument('--container-format', dest='container_format', |
|
107 |
metavar='FORMAT', help='set container format') |
|
108 |
parser.add_argument('--disk-format', dest='disk_format', |
|
109 |
metavar='FORMAT', help='set disk format') |
|
110 |
parser.add_argument('--id', dest='id', |
|
111 |
metavar='ID', help='set image ID') |
|
112 |
parser.add_argument('--owner', dest='owner', |
|
113 |
metavar='USER', help='set image owner (admin only)') |
|
114 |
parser.add_argument('--property', dest='properties', action='append', |
|
115 |
metavar='KEY=VAL', |
|
116 |
help='add a property (can be used multiple times)') |
|
117 |
parser.add_argument('--public', dest='is_public', action='store_true', |
|
118 |
help='mark image as public') |
|
119 |
parser.add_argument('--size', dest='size', metavar='SIZE', |
|
120 |
help='set image size') |
|
121 |
|
|
122 |
def main(self, name, location): |
|
123 |
super(self.__class__, self).main() |
|
124 |
if not location.startswith('pithos://'): |
|
125 |
account = self.config.get('storage', 'account').split()[0] |
|
126 |
if account[-1] == '/': |
|
127 |
account = account[:-1] |
|
128 |
container = self.config.get('storage', 'container') |
|
129 |
location = 'pithos://%s/%s'%(account, location) \ |
|
130 |
if container is None or len(container) == 0 \ |
|
131 |
else 'pithos://%s/%s/%s' % (account, container, location) |
|
132 |
|
|
133 |
params = {} |
|
134 |
for key in ('checksum', 'container_format', 'disk_format', 'id', |
|
135 |
'owner', 'size'): |
|
136 |
val = getattr(self.args, key) |
|
137 |
if val is not None: |
|
138 |
params[key] = val |
|
139 |
|
|
140 |
if self.args.is_public: |
|
141 |
params['is_public'] = 'true' |
|
142 |
|
|
143 |
properties = {} |
|
144 |
for property in self.args.properties or []: |
|
145 |
key, sep, val = property.partition('=') |
|
146 |
if not sep: |
|
147 |
raise CLIError(message="Invalid property '%s'" % property, importance=1) |
|
148 |
properties[key.strip()] = val.strip() |
|
149 |
|
|
150 |
try: |
|
151 |
self.client.register(name, location, params, properties) |
|
152 |
except ClientError as err: |
|
153 |
raiseCLIError(err) |
|
154 |
|
|
155 |
@command() |
|
156 |
class image_members(_init_image): |
|
157 |
"""Get image members""" |
|
158 |
|
|
159 |
def main(self, image_id): |
|
160 |
super(self.__class__, self).main() |
|
161 |
try: |
|
162 |
members = self.client.list_members(image_id) |
|
163 |
except ClientError as err: |
|
164 |
raiseCLIError(err) |
|
165 |
for member in members: |
|
166 |
print(member['member_id']) |
|
167 |
|
|
168 |
@command() |
|
169 |
class image_shared(_init_image): |
|
170 |
"""List shared images""" |
|
171 |
|
|
172 |
def main(self, member): |
|
173 |
super(self.__class__, self).main() |
|
174 |
try: |
|
175 |
images = self.client.list_shared(member) |
|
176 |
except ClientError as err: |
|
177 |
raiseCLIError(err) |
|
178 |
for image in images: |
|
179 |
print(image['image_id']) |
|
180 |
|
|
181 |
@command() |
|
182 |
class image_addmember(_init_image): |
|
183 |
"""Add a member to an image""" |
|
184 |
|
|
185 |
def main(self, image_id, member): |
|
186 |
super(self.__class__, self).main() |
|
187 |
try: |
|
188 |
self.client.add_member(image_id, member) |
|
189 |
except ClientError as err: |
|
190 |
raiseCLIError(err) |
|
191 |
|
|
192 |
@command() |
|
193 |
class image_delmember(_init_image): |
|
194 |
"""Remove a member from an image""" |
|
195 |
|
|
196 |
def main(self, image_id, member): |
|
197 |
super(self.__class__, self).main() |
|
198 |
try: |
|
199 |
self.client.remove_member(image_id, member) |
|
200 |
except ClientError as err: |
|
201 |
raiseCLIError(err) |
|
202 |
|
|
203 |
@command() |
|
204 |
class image_setmembers(_init_image): |
|
205 |
"""Set the members of an image""" |
|
206 |
|
|
207 |
def main(self, image_id, *member): |
|
208 |
super(self.__class__, self).main() |
|
209 |
try: |
|
210 |
self.client.set_members(image_id, member) |
|
211 |
except ClientError as err: |
|
212 |
raiseCLIError(err) |
b/kamaki/cli/commands/pithos_cli.py | ||
---|---|---|
1 |
# Copyright 2011-2012 GRNET S.A. All rights reserved. |
|
2 |
# |
|
3 |
# Redistribution and use in source and binary forms, with or |
|
4 |
# without modification, are permitted provided that the following |
|
5 |
# conditions are met: |
|
6 |
# |
|
7 |
# 1. Redistributions of source code must retain the above |
|
8 |
# copyright notice, this list of conditions and the following |
|
9 |
# disclaimer. |
|
10 |
# |
|
11 |
# 2. Redistributions in binary form must reproduce the above |
|
12 |
# copyright notice, this list of conditions and the following |
|
13 |
# disclaimer in the documentation and/or other materials |
|
14 |
# provided with the distribution. |
|
15 |
# |
|
16 |
# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS |
|
17 |
# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED |
|
18 |
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR |
|
19 |
# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR |
|
20 |
# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, |
|
21 |
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT |
|
22 |
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF |
|
23 |
# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED |
|
24 |
# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT |
|
25 |
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN |
|
26 |
# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE |
|
27 |
# POSSIBILITY OF SUCH DAMAGE. |
|
28 |
# |
|
29 |
# The views and conclusions contained in the software and |
|
30 |
# documentation are those of the authors and should not be |
|
31 |
# interpreted as representing official policies, either expressed |
|
32 |
# or implied, of GRNET S.A.command |
|
33 |
|
|
34 |
from kamaki.cli import command, set_api_description, CLIError |
|
35 |
from kamaki.clients.utils import filter_in |
|
36 |
from kamaki.cli.utils import format_size, raiseCLIError, print_dict, pretty_keys, print_list |
|
37 |
set_api_description('store', 'Pithos+ storage commands') |
|
38 |
from kamaki.clients.pithos import PithosClient, ClientError |
|
39 |
from colors import bold |
|
40 |
from sys import stdout, exit |
|
41 |
import signal |
|
42 |
|
|
43 |
from progress.bar import IncrementalBar |
|
44 |
|
|
45 |
|
|
46 |
class ProgressBar(IncrementalBar): |
|
47 |
#suffix = '%(percent)d%% - %(eta)ds' |
|
48 |
suffix = '%(percent)d%%' |
|
49 |
|
|
50 |
class _pithos_init(object): |
|
51 |
def main(self): |
|
52 |
self.token = self.config.get('store', 'token') or self.config.get('global', 'token') |
|
53 |
self.base_url = self.config.get('store', 'url') or self.config.get('global', 'url') |
|
54 |
self.account = self.config.get('store', 'account') or self.config.get('global', 'account') |
|
55 |
self.container = self.config.get('store', 'container') or self.config.get('global', 'container') |
|
56 |
self.client = PithosClient(base_url=self.base_url, token=self.token, account=self.account, |
|
57 |
container=self.container) |
|
58 |
|
|
59 |
class _store_account_command(_pithos_init): |
|
60 |
"""Base class for account level storage commands""" |
|
61 |
|
|
62 |
def update_parser(self, parser): |
|
63 |
parser.add_argument('--account', dest='account', metavar='NAME', |
|
64 |
help="Specify an account to use") |
|
65 |
|
|
66 |
def progress(self, message): |
|
67 |
"""Return a generator function to be used for progress tracking""" |
|
68 |
|
|
69 |
MESSAGE_LENGTH = 25 |
|
70 |
|
|
71 |
def progress_gen(n): |
|
72 |
msg = message.ljust(MESSAGE_LENGTH) |
|
73 |
for i in ProgressBar(msg).iter(range(n)): |
|
74 |
yield |
|
75 |
yield |
|
76 |
|
|
77 |
return progress_gen |
|
78 |
|
|
79 |
def main(self): |
|
80 |
super(_store_account_command, self).main() |
|
81 |
if hasattr(self.args, 'account') and self.args.account is not None: |
|
82 |
self.client.account = self.args.account |
|
83 |
|
|
84 |
class _store_container_command(_store_account_command): |
|
85 |
"""Base class for container level storage commands""" |
|
86 |
|
|
87 |
def __init__(self): |
|
88 |
self.container = None |
|
89 |
self.path = None |
|
90 |
|
|
91 |
def update_parser(self, parser): |
|
92 |
super(_store_container_command, self).update_parser(parser) |
|
93 |
parser.add_argument('--container', dest='container', metavar='NAME', default=None, |
|
94 |
help="Specify a container to use") |
|
95 |
|
|
96 |
def extract_container_and_path(self, container_with_path, path_is_optional=True): |
|
97 |
assert isinstance(container_with_path, str) |
|
98 |
if ':' not in container_with_path: |
|
99 |
if hasattr(self.args, 'container'): |
|
100 |
self.container = getattr(self.args, 'container') |
|
101 |
else: |
|
102 |
self.container = self.client.container |
|
103 |
if self.container is None: |
|
104 |
self.container = container_with_path |
|
105 |
else: |
|
106 |
self.path = container_with_path |
|
107 |
if not path_is_optional and self.path is None: |
|
108 |
raise CLIError(message="Object path is missing", status=11) |
|
109 |
return |
|
110 |
cnp = container_with_path.split(':') |
|
111 |
self.container = cnp[0] |
|
112 |
try: |
|
113 |
self.path = cnp[1] |
|
114 |
except IndexError: |
|
115 |
if path_is_optional: |
|
116 |
self.path = None |
|
117 |
else: |
|
118 |
raise CLIError(message="Object path is missing", status=11) |
|
119 |
|
|
120 |
def main(self, container_with_path=None, path_is_optional=True): |
|
121 |
super(_store_container_command, self).main() |
|
122 |
if container_with_path is not None: |
|
123 |
self.extract_container_and_path(container_with_path, path_is_optional) |
|
124 |
self.client.container = self.container |
|
125 |
elif hasattr(self.args, 'container'): |
|
126 |
self.client.container = getattr(self.args,'container') |
|
127 |
self.container = self.client.container |
|
128 |
|
|
129 |
""" |
|
130 |
@command() |
|
131 |
class store_test(_store_container_command): |
|
132 |
""Test various stuff"" |
|
133 |
|
|
134 |
def main(self): |
|
135 |
super(self.__class__, self).main('pithos') |
|
136 |
r = self.client.list_containers() |
|
137 |
for item in r: |
|
138 |
if item['name'].startswith('c1_') or item['name'].startswith('c2_') \ |
|
139 |
or item['name'].startswith('c3_'): |
|
140 |
self.client.container = item['name'] |
|
141 |
self.client.del_container(delimiter='/') |
|
142 |
self.client.del_container() |
|
143 |
""" |
|
144 |
|
|
145 |
@command() |
|
146 |
class store_list(_store_container_command): |
|
147 |
"""List containers, object trees or objects in a directory |
|
148 |
""" |
|
149 |
|
|
150 |
def update_parser(self, parser): |
|
151 |
super(self.__class__, self).update_parser(parser) |
|
152 |
parser.add_argument('-l', action='store_true', dest='detail', default=False, |
|
153 |
help='show detailed output') |
|
154 |
parser.add_argument('-N', action='store', dest='show_size', default=1000, |
|
155 |
help='print output in chunks of size N') |
|
156 |
parser.add_argument('-n', action='store', dest='limit', default=None, |
|
157 |
help='show limited output') |
|
158 |
parser.add_argument('--marker', action='store', dest='marker', default=None, |
|
159 |
help='show output greater then marker') |
|
160 |
parser.add_argument('--prefix', action='store', dest='prefix', default=None, |
|
161 |
help='show output starting with prefix') |
|
162 |
parser.add_argument('--delimiter', action='store', dest='delimiter', default=None, |
|
163 |
help='show output up to the delimiter') |
|
164 |
parser.add_argument('--path', action='store', dest='path', default=None, |
|
165 |
help='show output starting with prefix up to /') |
|
166 |
parser.add_argument('--meta', action='store', dest='meta', default=None, |
|
167 |
help='show output having the specified meta keys (e.g. --meta "meta1 meta2 ..."') |
|
168 |
parser.add_argument('--if-modified-since', action='store', dest='if_modified_since', |
|
169 |
default=None, help='show output if modified since then') |
|
170 |
parser.add_argument('--if-unmodified-since', action='store', dest='if_unmodified_since', |
|
171 |
default=None, help='show output if not modified since then') |
|
172 |
parser.add_argument('--until', action='store', dest='until', default=None, |
|
173 |
help='show metadata until that date') |
|
174 |
dateformat = '%d/%m/%Y %H:%M:%S' |
|
175 |
parser.add_argument('--format', action='store', dest='format', default=dateformat, |
|
176 |
help='format to parse until date (default: d/m/Y H:M:S)') |
|
177 |
parser.add_argument('--shared', action='store_true', dest='shared', default=False, |
|
178 |
help='show only shared') |
|
179 |
parser.add_argument('--public', action='store_true', dest='public', default=False, |
|
180 |
help='show only public') |
|
181 |
|
|
182 |
def print_objects(self, object_list): |
|
183 |
import sys |
|
184 |
try: |
|
185 |
limit = getattr(self.args, 'show_size') |
|
186 |
limit = int(limit) |
|
187 |
except AttributeError: |
|
188 |
pass |
|
189 |
#index = 0 |
|
190 |
for index,obj in enumerate(object_list): |
|
191 |
if not obj.has_key('content_type'): |
|
192 |
continue |
|
193 |
pretty_obj = obj.copy() |
|
194 |
index += 1 |
|
195 |
empty_space = ' '*(len(str(len(object_list))) - len(str(index))) |
|
196 |
if obj['content_type'] == 'application/directory': |
|
197 |
isDir = True |
|
198 |
size = 'D' |
|
199 |
else: |
|
200 |
isDir = False |
|
201 |
size = format_size(obj['bytes']) |
|
202 |
pretty_obj['bytes'] = '%s (%s)'%(obj['bytes'],size) |
|
203 |
oname = bold(obj['name']) |
Also available in: Unified diff