Revision eb3ca8ca kamaki/kamaki.py
b/kamaki/kamaki.py | ||
---|---|---|
33 | 33 |
# interpreted as representing official policies, either expressed |
34 | 34 |
# or implied, of GRNET S.A. |
35 | 35 |
|
36 |
""" |
|
37 |
To add a command create a new class and add a 'command' decorator. The class |
|
38 |
must have a 'main' method which will contain the code to be executed. |
|
39 |
Optionally a command can implement an 'update_parser' class method in order |
|
40 |
to add command line arguments, or modify the OptionParser in any way. |
|
41 |
|
|
42 |
The name of the class is important and it will determine the name and grouping |
|
43 |
of the command. This behavior can be overriden with the 'group' and 'name' |
|
44 |
decorator arguments: |
|
45 |
|
|
46 |
@command(api='nova') |
|
47 |
class server_list(object): |
|
48 |
# This command will be named 'list' under group 'server' |
|
49 |
... |
|
50 |
|
|
51 |
@command(api='nova', name='ls') |
|
52 |
class server_list(object): |
|
53 |
# This command will be named 'ls' under group 'server' |
|
54 |
... |
|
55 |
|
|
56 |
The docstring of a command class will be used as the command description in |
|
57 |
help messages, unless overriden with the 'description' decorator argument. |
|
58 |
|
|
59 |
The syntax of a command will be generated dynamically based on the signature |
|
60 |
of the 'main' method, unless overriden with the 'syntax' decorator argument: |
|
61 |
|
|
62 |
def main(self, server_id, network=None): |
|
63 |
# This syntax of this command will be: '<server id> [network]' |
|
64 |
... |
|
65 |
|
|
66 |
The order of commands is important, it will be preserved in the help output. |
|
67 |
""" |
|
68 |
|
|
36 | 69 |
import inspect |
37 | 70 |
import logging |
38 | 71 |
import os |
39 | 72 |
import sys |
40 | 73 |
|
41 | 74 |
from base64 import b64encode |
42 |
from collections import defaultdict |
|
43 | 75 |
from grp import getgrgid |
44 | 76 |
from optparse import OptionParser |
45 | 77 |
from pwd import getpwuid |
46 | 78 |
|
47 |
from client import Client, ClientError |
|
79 |
from client import ComputeClient, ImagesClient, ClientError |
|
80 |
from config import Config, ConfigError |
|
81 |
from utils import OrderedDict, print_addresses, print_dict, print_items |
|
48 | 82 |
|
49 | 83 |
|
50 |
API_ENV = 'KAMAKI_API' |
|
51 |
URL_ENV = 'KAMAKI_URL' |
|
52 |
TOKEN_ENV = 'KAMAKI_TOKEN' |
|
53 |
RCFILE = '.kamakirc' |
|
84 |
# Path to the file that stores the configuration |
|
85 |
CONFIG_PATH = os.path.expanduser('~/.kamakirc') |
|
54 | 86 |
|
87 |
# Name of a shell variable to bypass the CONFIG_PATH value |
|
88 |
CONFIG_ENV = 'KAMAKI_CONFIG' |
|
89 |
|
|
90 |
# The defaults also determine the allowed keys |
|
91 |
CONFIG_DEFAULTS = { |
|
92 |
'apis': 'nova synnefo glance plankton', |
|
93 |
'token': '', |
|
94 |
'compute_url': 'https://okeanos.grnet.gr/api/v1', |
|
95 |
'images_url': 'https://okeanos.grnet.gr/plankton', |
|
96 |
} |
|
55 | 97 |
|
56 | 98 |
log = logging.getLogger('kamaki') |
57 | 99 |
|
100 |
_commands = OrderedDict() |
|
101 |
|
|
58 | 102 |
|
59 |
def print_addresses(addresses, margin):
|
|
60 |
for address in addresses:
|
|
61 |
if address['id'] == 'public': |
|
62 |
net = 'public'
|
|
63 |
else:
|
|
64 |
net = '%s/%s' % (address['id'], address['name'])
|
|
65 |
print '%s:' % net.rjust(margin + 4)
|
|
103 |
def command(api=None, group=None, name=None, description=None, syntax=None):
|
|
104 |
"""Class decorator that registers a class as a CLI command."""
|
|
105 |
|
|
106 |
def decorator(cls):
|
|
107 |
grp, sep, cmd = cls.__name__.partition('_')
|
|
108 |
if not sep:
|
|
109 |
grp, cmd = None, cls.__name__
|
|
66 | 110 |
|
67 |
ether = address.get('mac', None) |
|
68 |
if ether: |
|
69 |
print '%s: %s' % ('ether'.rjust(margin + 8), ether) |
|
111 |
cls.api = api |
|
112 |
cls.group = group or grp |
|
113 |
cls.name = name or cmd |
|
114 |
cls.description = description or cls.__doc__ |
|
115 |
cls.syntax = syntax |
|
70 | 116 |
|
71 |
firewall = address.get('firewallProfile', None) |
|
72 |
if firewall: |
|
73 |
print '%s: %s' % ('firewall'.rjust(margin + 8), firewall) |
|
117 |
if cls.syntax is None: |
|
118 |
# Generate a syntax string based on main's arguments |
|
119 |
spec = inspect.getargspec(cls.main.im_func) |
|
120 |
args = spec.args[1:] |
|
121 |
n = len(args) - len(spec.defaults or ()) |
|
122 |
required = ' '.join('<%s>' % x.replace('_', ' ') for x in args[:n]) |
|
123 |
optional = ' '.join('[%s]' % x.replace('_', ' ') for x in args[n:]) |
|
124 |
cls.syntax = ' '.join(x for x in [required, optional] if x) |
|
74 | 125 |
|
75 |
for ip in address.get('values', []): |
|
76 |
key = 'inet' if ip['version'] == 4 else 'inet6' |
|
77 |
print '%s: %s' % (key.rjust(margin + 8), ip['addr']) |
|
126 |
if cls.group not in _commands: |
|
127 |
_commands[cls.group] = OrderedDict() |
|
128 |
_commands[cls.group][cls.name] = cls |
|
129 |
return cls |
|
130 |
return decorator |
|
78 | 131 |
|
79 | 132 |
|
80 |
def print_metadata(metadata, margin): |
|
81 |
print '%s:' % 'metadata'.rjust(margin) |
|
82 |
for key, val in metadata.get('values', {}).items(): |
|
83 |
print '%s: %s' % (key.rjust(margin + 4), val) |
|
133 |
@command() |
|
134 |
class config_list(object): |
|
135 |
"""list configuration options""" |
|
136 |
|
|
137 |
def main(self): |
|
138 |
for key, val in sorted(self.config.items()): |
|
139 |
print '%s=%s' % (key, val) |
|
84 | 140 |
|
85 | 141 |
|
86 |
def print_dict(d, exclude=()): |
|
87 |
if not d: |
|
88 |
return |
|
89 |
margin = max(len(key) for key in d) + 1 |
|
142 |
@command() |
|
143 |
class config_get(object): |
|
144 |
"""get a configuration option""" |
|
90 | 145 |
|
91 |
for key, val in sorted(d.items()): |
|
92 |
if key in exclude: |
|
93 |
continue |
|
94 |
|
|
95 |
if key == 'addresses': |
|
96 |
print '%s:' % 'addresses'.rjust(margin) |
|
97 |
print_addresses(val.get('values', []), margin) |
|
98 |
continue |
|
99 |
elif key == 'metadata': |
|
100 |
print_metadata(val, margin) |
|
101 |
continue |
|
102 |
elif key == 'servers': |
|
103 |
val = ', '.join(str(x) for x in val['values']) |
|
104 |
|
|
105 |
print '%s: %s' % (key.rjust(margin), val) |
|
106 |
|
|
107 |
|
|
108 |
def print_items(items, detail=False): |
|
109 |
for item in items: |
|
110 |
print '%s %s' % (item['id'], item.get('name', '')) |
|
111 |
if detail: |
|
112 |
print_dict(item, exclude=('id', 'name')) |
|
113 |
|
|
114 |
|
|
115 |
|
|
116 |
class Command(object): |
|
117 |
"""Abstract class. |
|
118 |
|
|
119 |
All commands should subclass this class. |
|
120 |
""" |
|
121 |
|
|
122 |
api = 'openstack' |
|
123 |
group = '<group>' |
|
124 |
name = '<command>' |
|
125 |
syntax = '' |
|
126 |
description = '' |
|
127 |
|
|
128 |
def __init__(self, argv): |
|
129 |
self._init_parser(argv) |
|
130 |
self._init_logging() |
|
131 |
self._init_conf() |
|
132 |
if self.name != '<command>': |
|
133 |
self.client = Client(self.url, self.token) |
|
134 |
|
|
135 |
def _init_parser(self, argv): |
|
136 |
parser = OptionParser() |
|
137 |
parser.usage = '%%prog %s %s %s [options]' % (self.group, self.name, |
|
138 |
self.syntax) |
|
139 |
parser.add_option('--api', dest='api', metavar='API', |
|
140 |
help='API can be either openstack or synnefo') |
|
141 |
parser.add_option('--url', dest='url', metavar='URL', |
|
142 |
help='API URL') |
|
143 |
parser.add_option('--token', dest='token', metavar='TOKEN', |
|
144 |
help='use token TOKEN') |
|
145 |
parser.add_option('-v', action='store_true', dest='verbose', |
|
146 |
default=False, help='use verbose output') |
|
147 |
parser.add_option('-d', action='store_true', dest='debug', |
|
148 |
default=False, help='use debug output') |
|
149 |
|
|
150 |
self.add_options(parser) |
|
151 |
|
|
152 |
options, args = parser.parse_args(argv) |
|
153 |
|
|
154 |
# Add options to self |
|
155 |
for opt in parser.option_list: |
|
156 |
key = opt.dest |
|
157 |
if key: |
|
158 |
val = getattr(options, key) |
|
159 |
setattr(self, key, val) |
|
160 |
|
|
161 |
self.args = args |
|
162 |
self.parser = parser |
|
163 |
|
|
164 |
def _init_logging(self): |
|
165 |
if self.debug: |
|
166 |
log.setLevel(logging.DEBUG) |
|
167 |
elif self.verbose: |
|
168 |
log.setLevel(logging.INFO) |
|
169 |
else: |
|
170 |
log.setLevel(logging.WARNING) |
|
171 |
|
|
172 |
def _init_conf(self): |
|
173 |
if not self.api: |
|
174 |
self.api = os.environ.get(API_ENV, None) |
|
175 |
if not self.url: |
|
176 |
self.url = os.environ.get(URL_ENV, None) |
|
177 |
if not self.token: |
|
178 |
self.token = os.environ.get(TOKEN_ENV, None) |
|
179 |
|
|
180 |
path = os.path.join(os.path.expanduser('~'), RCFILE) |
|
181 |
if not os.path.exists(path): |
|
182 |
return |
|
183 |
|
|
184 |
for line in open(path): |
|
185 |
key, sep, val = line.partition('=') |
|
186 |
if not sep: |
|
187 |
continue |
|
188 |
key = key.strip().lower() |
|
189 |
val = val.strip() |
|
190 |
if key == 'api' and not self.api: |
|
191 |
self.api = val |
|
192 |
elif key == 'url' and not self.url: |
|
193 |
self.url = val |
|
194 |
elif key == 'token' and not self.token: |
|
195 |
self.token = val |
|
196 |
|
|
197 |
def add_options(self, parser): |
|
198 |
pass |
|
199 |
|
|
200 |
def main(self, *args): |
|
201 |
pass |
|
202 |
|
|
203 |
def execute(self): |
|
204 |
try: |
|
205 |
self.main(*self.args) |
|
206 |
except TypeError: |
|
207 |
self.parser.print_help() |
|
208 |
|
|
209 |
|
|
210 |
# Server Group |
|
211 |
|
|
212 |
class ListServers(Command): |
|
213 |
group = 'server' |
|
214 |
name = 'list' |
|
215 |
description = 'list servers' |
|
216 |
|
|
217 |
def add_options(self, parser): |
|
218 |
parser.add_option('-l', action='store_true', dest='detail', |
|
219 |
default=False, help='show detailed output') |
|
146 |
def main(self, key): |
|
147 |
val = self.config.get(key) |
|
148 |
if val is not None: |
|
149 |
print val |
|
150 |
|
|
151 |
|
|
152 |
@command() |
|
153 |
class config_set(object): |
|
154 |
"""set a configuration option""" |
|
155 |
|
|
156 |
def main(self, key, val): |
|
157 |
self.config.set(key, val) |
|
158 |
|
|
159 |
|
|
160 |
@command() |
|
161 |
class config_del(object): |
|
162 |
"""delete a configuration option""" |
|
163 |
|
|
164 |
def main(self, key): |
|
165 |
self.config.delete(key) |
|
166 |
|
|
167 |
|
|
168 |
@command(api='nova') |
|
169 |
class server_list(object): |
|
170 |
"""list servers""" |
|
171 |
|
|
172 |
@classmethod |
|
173 |
def update_parser(cls, parser): |
|
174 |
parser.add_option('-l', dest='detail', action='store_true', |
|
175 |
default=False, help='show detailed output') |
|
220 | 176 |
|
221 | 177 |
def main(self): |
222 |
servers = self.client.list_servers(self.detail) |
|
223 |
print_items(servers, self.detail) |
|
178 |
servers = self.client.list_servers(self.options.detail)
|
|
179 |
print_items(servers, self.options.detail)
|
|
224 | 180 |
|
225 | 181 |
|
226 |
class GetServerDetails(Command): |
|
227 |
group = 'server' |
|
228 |
name = 'info' |
|
229 |
syntax = '<server id>' |
|
230 |
description = 'get server details' |
|
182 |
@command(api='nova') |
|
183 |
class server_info(object): |
|
184 |
"""get server details""" |
|
231 | 185 |
|
232 | 186 |
def main(self, server_id): |
233 | 187 |
server = self.client.get_server_details(int(server_id)) |
234 | 188 |
print_dict(server) |
235 | 189 |
|
236 | 190 |
|
237 |
class CreateServer(Command): |
|
238 |
group = 'server' |
|
239 |
name = 'create' |
|
240 |
syntax = '<server name>' |
|
241 |
description = 'create server' |
|
242 |
|
|
243 |
def add_options(self, parser): |
|
244 |
parser.add_option('-f', dest='flavor', metavar='FLAVOR_ID', default=1, |
|
245 |
help='use flavor FLAVOR_ID') |
|
246 |
parser.add_option('-i', dest='image', metavar='IMAGE_ID', default=1, |
|
247 |
help='use image IMAGE_ID') |
|
248 |
parser.add_option('--personality', |
|
249 |
dest='personalities', |
|
250 |
action='append', |
|
251 |
default=[], |
|
252 |
metavar='PATH[,SERVER PATH[,OWNER[,GROUP,[MODE]]]]', |
|
253 |
help='add a personality file') |
|
191 |
@command(api='nova') |
|
192 |
class server_create(object): |
|
193 |
"""create server""" |
|
254 | 194 |
|
255 |
def main(self, name): |
|
256 |
flavor_id = int(self.flavor) |
|
257 |
image_id = int(self.image) |
|
195 |
@classmethod |
|
196 |
def update_parser(cls, parser): |
|
197 |
parser.add_option('--personality', dest='personalities', |
|
198 |
action='append', default=[], |
|
199 |
metavar='PATH[,SERVER PATH[,OWNER[,GROUP,[MODE]]]]', |
|
200 |
help='add a personality file') |
|
201 |
parser.epilog = "If missing, optional personality values will be " \ |
|
202 |
"filled based on the file at PATH if missing." |
|
203 |
|
|
204 |
def main(self, name, flavor_id, image_id): |
|
258 | 205 |
personalities = [] |
259 |
for personality in self.personalities: |
|
206 |
for personality in self.options.personalities:
|
|
260 | 207 |
p = personality.split(',') |
261 | 208 |
p.extend([None] * (5 - len(p))) # Fill missing fields with None |
262 | 209 |
|
... | ... | |
264 | 211 |
|
265 | 212 |
if not path: |
266 | 213 |
log.error("Invalid personality argument '%s'", p) |
267 |
return |
|
214 |
return 1
|
|
268 | 215 |
if not os.path.exists(path): |
269 | 216 |
log.error("File %s does not exist", path) |
270 |
return |
|
217 |
return 1
|
|
271 | 218 |
|
272 | 219 |
with open(path) as f: |
273 | 220 |
contents = b64encode(f.read()) |
... | ... | |
280 | 227 |
'mode': int(p[4]) if p[4] else 0x7777 & st.st_mode, |
281 | 228 |
'contents': contents}) |
282 | 229 |
|
283 |
reply = self.client.create_server(name, flavor_id, image_id,
|
|
284 |
personalities)
|
|
230 |
reply = self.client.create_server(name, int(flavor_id), image_id,
|
|
231 |
personalities) |
|
285 | 232 |
print_dict(reply) |
286 | 233 |
|
287 | 234 |
|
288 |
class UpdateServerName(Command): |
|
289 |
group = 'server' |
|
290 |
name = 'rename' |
|
291 |
syntax = '<server id> <new name>' |
|
292 |
description = 'update server name' |
|
235 |
@command(api='nova') |
|
236 |
class server_rename(object): |
|
237 |
"""update server name""" |
|
293 | 238 |
|
294 | 239 |
def main(self, server_id, new_name): |
295 | 240 |
self.client.update_server_name(int(server_id), new_name) |
296 | 241 |
|
297 | 242 |
|
298 |
class DeleteServer(Command): |
|
299 |
group = 'server' |
|
300 |
name = 'delete' |
|
301 |
syntax = '<server id>' |
|
302 |
description = 'delete server' |
|
243 |
@command(api='nova') |
|
244 |
class server_delete(object): |
|
245 |
"""delete server""" |
|
303 | 246 |
|
304 | 247 |
def main(self, server_id): |
305 | 248 |
self.client.delete_server(int(server_id)) |
306 | 249 |
|
307 | 250 |
|
308 |
class RebootServer(Command): |
|
309 |
group = 'server' |
|
310 |
name = 'reboot' |
|
311 |
syntax = '<server id>' |
|
312 |
description = 'reboot server' |
|
251 |
@command(api='nova') |
|
252 |
class server_reboot(object): |
|
253 |
"""reboot server""" |
|
313 | 254 |
|
314 |
def add_options(self, parser): |
|
315 |
parser.add_option('-f', action='store_true', dest='hard', |
|
316 |
default=False, help='perform a hard reboot') |
|
255 |
@classmethod |
|
256 |
def update_parser(cls, parser): |
|
257 |
parser.add_option('-f', dest='hard', action='store_true', |
|
258 |
default=False, help='perform a hard reboot') |
|
317 | 259 |
|
318 | 260 |
def main(self, server_id): |
319 |
self.client.reboot_server(int(server_id), self.hard) |
|
261 |
self.client.reboot_server(int(server_id), self.options.hard)
|
|
320 | 262 |
|
321 | 263 |
|
322 |
class StartServer(Command): |
|
323 |
api = 'synnefo' |
|
324 |
group = 'server' |
|
325 |
name = 'start' |
|
326 |
syntax = '<server id>' |
|
327 |
description = 'start server' |
|
264 |
@command(api='synnefo') |
|
265 |
class server_start(object): |
|
266 |
"""start server""" |
|
328 | 267 |
|
329 | 268 |
def main(self, server_id): |
330 | 269 |
self.client.start_server(int(server_id)) |
331 | 270 |
|
332 | 271 |
|
333 |
class StartServer(Command): |
|
334 |
api = 'synnefo' |
|
335 |
group = 'server' |
|
336 |
name = 'shutdown' |
|
337 |
syntax = '<server id>' |
|
338 |
description = 'shutdown server' |
|
272 |
@command(api='synnefo') |
|
273 |
class server_shutdown(object): |
|
274 |
"""shutdown server""" |
|
339 | 275 |
|
340 | 276 |
def main(self, server_id): |
341 | 277 |
self.client.shutdown_server(int(server_id)) |
342 | 278 |
|
343 | 279 |
|
344 |
class ServerConsole(Command): |
|
345 |
api = 'synnefo' |
|
346 |
group = 'server' |
|
347 |
name = 'console' |
|
348 |
syntax = '<server id>' |
|
349 |
description = 'get VNC console' |
|
350 |
|
|
280 |
@command(api='synnefo') |
|
281 |
class server_console(object): |
|
282 |
"""get a VNC console""" |
|
283 |
|
|
351 | 284 |
def main(self, server_id): |
352 | 285 |
reply = self.client.get_server_console(int(server_id)) |
353 | 286 |
print_dict(reply) |
354 | 287 |
|
355 | 288 |
|
356 |
class SetFirewallProfile(Command): |
|
357 |
api = 'synnefo' |
|
358 |
group = 'server' |
|
359 |
name = 'firewall' |
|
360 |
syntax = '<server id> <profile>' |
|
361 |
description = 'set the firewall profile' |
|
289 |
@command(api='synnefo') |
|
290 |
class server_firewall(object): |
|
291 |
"""set the firewall profile""" |
|
362 | 292 |
|
363 | 293 |
def main(self, server_id, profile): |
364 | 294 |
self.client.set_firewall_profile(int(server_id), profile) |
365 | 295 |
|
366 | 296 |
|
367 |
class ListAddresses(Command): |
|
368 |
group = 'server' |
|
369 |
name = 'addr' |
|
370 |
syntax = '<server id> [network]' |
|
371 |
description = 'list server addresses' |
|
297 |
@command(api='synnefo') |
|
298 |
class server_addr(object): |
|
299 |
"""list server addresses""" |
|
372 | 300 |
|
373 | 301 |
def main(self, server_id, network=None): |
374 | 302 |
reply = self.client.list_server_addresses(int(server_id), network) |
... | ... | |
376 | 304 |
print_addresses(reply, margin) |
377 | 305 |
|
378 | 306 |
|
379 |
class GetServerMeta(Command): |
|
380 |
group = 'server' |
|
381 |
name = 'meta' |
|
382 |
syntax = '<server id> [key]' |
|
383 |
description = 'get server metadata' |
|
307 |
@command(api='nova') |
|
308 |
class server_meta(object): |
|
309 |
"""get server metadata""" |
|
384 | 310 |
|
385 | 311 |
def main(self, server_id, key=None): |
386 | 312 |
reply = self.client.get_server_metadata(int(server_id), key) |
387 | 313 |
print_dict(reply) |
388 | 314 |
|
389 | 315 |
|
390 |
class CreateServerMetadata(Command): |
|
391 |
group = 'server' |
|
392 |
name = 'addmeta' |
|
393 |
syntax = '<server id> <key> <val>' |
|
394 |
description = 'add server metadata' |
|
316 |
@command(api='nova') |
|
317 |
class server_addmeta(object): |
|
318 |
"""add server metadata""" |
|
395 | 319 |
|
396 | 320 |
def main(self, server_id, key, val): |
397 | 321 |
reply = self.client.create_server_metadata(int(server_id), key, val) |
398 | 322 |
print_dict(reply) |
399 | 323 |
|
400 | 324 |
|
401 |
class UpdateServerMetadata(Command): |
|
402 |
group = 'server' |
|
403 |
name = 'setmeta' |
|
404 |
syntax = '<server id> <key> <val>' |
|
405 |
description = 'update server metadata' |
|
325 |
@command(api='nova') |
|
326 |
class server_setmeta(object): |
|
327 |
"""update server metadata""" |
|
406 | 328 |
|
407 | 329 |
def main(self, server_id, key, val): |
408 | 330 |
metadata = {key: val} |
... | ... | |
410 | 332 |
print_dict(reply) |
411 | 333 |
|
412 | 334 |
|
413 |
class DeleteServerMetadata(Command): |
|
414 |
group = 'server' |
|
415 |
name = 'delmeta' |
|
416 |
syntax = '<server id> <key>' |
|
417 |
description = 'delete server metadata' |
|
335 |
@command(api='nova') |
|
336 |
class server_delmeta(object): |
|
337 |
"""delete server metadata""" |
|
418 | 338 |
|
419 | 339 |
def main(self, server_id, key): |
420 | 340 |
self.client.delete_server_metadata(int(server_id), key) |
421 | 341 |
|
422 | 342 |
|
423 |
class GetServerStats(Command): |
|
424 |
api = 'synnefo' |
|
425 |
group = 'server' |
|
426 |
name = 'stats' |
|
427 |
syntax = '<server id>' |
|
428 |
description = 'get server statistics' |
|
343 |
@command(api='synnefo') |
|
344 |
class server_stats(object): |
|
345 |
"""get server statistics""" |
|
429 | 346 |
|
430 | 347 |
def main(self, server_id): |
431 | 348 |
reply = self.client.get_server_stats(int(server_id)) |
432 | 349 |
print_dict(reply, exclude=('serverRef',)) |
433 | 350 |
|
434 | 351 |
|
435 |
# Flavor Group |
|
436 |
|
|
437 |
class ListFlavors(Command): |
|
438 |
group = 'flavor' |
|
439 |
name = 'list' |
|
440 |
description = 'list flavors' |
|
352 |
@command(api='nova') |
|
353 |
class flavor_list(object): |
|
354 |
"""list flavors""" |
|
355 |
|
|
356 |
@classmethod |
|
357 |
def update_parser(cls, parser): |
|
358 |
parser.add_option('-l', dest='detail', action='store_true', |
|
359 |
default=False, help='show detailed output') |
|
441 | 360 |
|
442 |
def add_options(self, parser): |
|
443 |
parser.add_option('-l', action='store_true', dest='detail', |
|
444 |
default=False, help='show detailed output') |
|
445 |
|
|
446 | 361 |
def main(self): |
447 |
flavors = self.client.list_flavors(self.detail) |
|
448 |
print_items(flavors, self.detail) |
|
362 |
flavors = self.client.list_flavors(self.options.detail)
|
|
363 |
print_items(flavors, self.options.detail)
|
|
449 | 364 |
|
450 | 365 |
|
451 |
class GetFlavorDetails(Command): |
|
452 |
group = 'flavor' |
|
453 |
name = 'info' |
|
454 |
syntax = '<flavor id>' |
|
455 |
description = 'get flavor details' |
|
366 |
@command(api='nova') |
|
367 |
class flavor_info(object): |
|
368 |
"""get flavor details""" |
|
456 | 369 |
|
457 | 370 |
def main(self, flavor_id): |
458 | 371 |
flavor = self.client.get_flavor_details(int(flavor_id)) |
459 | 372 |
print_dict(flavor) |
460 | 373 |
|
461 | 374 |
|
462 |
class ListImages(Command):
|
|
463 |
group = 'image'
|
|
464 |
name = 'list'
|
|
465 |
description = 'list images' |
|
466 |
|
|
467 |
def add_options(self, parser):
|
|
468 |
parser.add_option('-l', action='store_true', dest='detail',
|
|
469 |
default=False, help='show detailed output')
|
|
470 |
|
|
375 |
@command(api='nova')
|
|
376 |
class image_list(object):
|
|
377 |
"""list images"""
|
|
378 |
|
|
379 |
@classmethod |
|
380 |
def update_parser(cls, parser):
|
|
381 |
parser.add_option('-l', dest='detail', action='store_true',
|
|
382 |
default=False, help='show detailed output') |
|
383 |
|
|
471 | 384 |
def main(self): |
472 |
images = self.client.list_images(self.detail) |
|
473 |
print_items(images, self.detail) |
|
385 |
images = self.client.list_images(self.options.detail)
|
|
386 |
print_items(images, self.options.detail)
|
|
474 | 387 |
|
475 | 388 |
|
476 |
class GetImageDetails(Command): |
|
477 |
group = 'image' |
|
478 |
name = 'info' |
|
479 |
syntax = '<image id>' |
|
480 |
description = 'get image details' |
|
389 |
@command(api='nova') |
|
390 |
class image_info(object): |
|
391 |
"""get image details""" |
|
481 | 392 |
|
482 | 393 |
def main(self, image_id): |
483 |
image = self.client.get_image_details(int(image_id))
|
|
394 |
image = self.client.get_image_details(image_id)
|
|
484 | 395 |
print_dict(image) |
485 | 396 |
|
486 | 397 |
|
487 |
class CreateImage(Command): |
|
488 |
group = 'image' |
|
489 |
name = 'create' |
|
490 |
syntax = '<server id> <image name>' |
|
491 |
description = 'create image' |
|
398 |
@command(api='nova') |
|
399 |
class image_create(object): |
|
400 |
"""create image""" |
|
492 | 401 |
|
493 | 402 |
def main(self, server_id, name): |
494 | 403 |
reply = self.client.create_image(int(server_id), name) |
495 | 404 |
print_dict(reply) |
496 | 405 |
|
497 | 406 |
|
498 |
class DeleteImage(Command): |
|
499 |
group = 'image' |
|
500 |
name = 'delete' |
|
501 |
syntax = '<image id>' |
|
502 |
description = 'delete image' |
|
407 |
@command(api='nova') |
|
408 |
class image_delete(object): |
|
409 |
"""delete image""" |
|
503 | 410 |
|
504 | 411 |
def main(self, image_id): |
505 |
self.client.delete_image(int(image_id))
|
|
412 |
self.client.delete_image(image_id)
|
|
506 | 413 |
|
507 | 414 |
|
508 |
class GetImageMetadata(Command): |
|
509 |
group = 'image' |
|
510 |
name = 'meta' |
|
511 |
syntax = '<image id> [key]' |
|
512 |
description = 'get image metadata' |
|
415 |
@command(api='nova') |
|
416 |
class image_meta(object): |
|
417 |
"""get image metadata""" |
|
513 | 418 |
|
514 | 419 |
def main(self, image_id, key=None): |
515 |
reply = self.client.get_image_metadata(int(image_id), key)
|
|
420 |
reply = self.client.get_image_metadata(image_id, key)
|
|
516 | 421 |
print_dict(reply) |
517 | 422 |
|
518 | 423 |
|
519 |
class CreateImageMetadata(Command): |
|
520 |
group = 'image' |
|
521 |
name = 'addmeta' |
|
522 |
syntax = '<image id> <key> <val>' |
|
523 |
description = 'add image metadata' |
|
424 |
@command(api='nova') |
|
425 |
class image_addmeta(object): |
|
426 |
"""add image metadata""" |
|
524 | 427 |
|
525 | 428 |
def main(self, image_id, key, val): |
526 |
reply = self.client.create_image_metadata(int(image_id), key, val)
|
|
429 |
reply = self.client.create_image_metadata(image_id, key, val)
|
|
527 | 430 |
print_dict(reply) |
528 | 431 |
|
529 | 432 |
|
530 |
class UpdateImageMetadata(Command): |
|
531 |
group = 'image' |
|
532 |
name = 'setmeta' |
|
533 |
syntax = '<image id> <key> <val>' |
|
534 |
description = 'update image metadata' |
|
433 |
@command(api='nova') |
|
434 |
class image_setmeta(object): |
|
435 |
"""update image metadata""" |
|
535 | 436 |
|
536 | 437 |
def main(self, image_id, key, val): |
537 | 438 |
metadata = {key: val} |
538 |
reply = self.client.update_image_metadata(int(image_id), **metadata)
|
|
439 |
reply = self.client.update_image_metadata(image_id, **metadata)
|
|
539 | 440 |
print_dict(reply) |
540 | 441 |
|
541 | 442 |
|
542 |
class DeleteImageMetadata(Command): |
|
543 |
group = 'image' |
|
544 |
name = 'delmeta' |
|
545 |
syntax = '<image id> <key>' |
|
546 |
description = 'delete image metadata' |
|
443 |
@command(api='nova') |
|
444 |
class image_delmeta(object): |
|
445 |
"""delete image metadata""" |
|
547 | 446 |
|
548 | 447 |
def main(self, image_id, key): |
549 |
self.client.delete_image_metadata(int(image_id), key)
|
|
448 |
self.client.delete_image_metadata(image_id, key)
|
|
550 | 449 |
|
551 | 450 |
|
552 |
class ListNetworks(Command): |
|
553 |
api = 'synnefo' |
|
554 |
group = 'network' |
|
555 |
name = 'list' |
|
556 |
description = 'list networks' |
|
451 |
@command(api='synnefo') |
|
452 |
class network_list(object): |
|
453 |
"""list networks""" |
|
557 | 454 |
|
558 |
def add_options(self, parser): |
|
559 |
parser.add_option('-l', action='store_true', dest='detail', |
|
560 |
default=False, help='show detailed output') |
|
455 |
@classmethod |
|
456 |
def update_parser(cls, parser): |
|
457 |
parser.add_option('-l', dest='detail', action='store_true', |
|
458 |
default=False, help='show detailed output') |
|
561 | 459 |
|
562 | 460 |
def main(self): |
563 |
networks = self.client.list_networks(self.detail) |
|
564 |
print_items(networks, self.detail) |
|
461 |
networks = self.client.list_networks(self.options.detail)
|
|
462 |
print_items(networks, self.options.detail)
|
|
565 | 463 |
|
566 | 464 |
|
567 |
class CreateNetwork(Command): |
|
568 |
api = 'synnefo' |
|
569 |
group = 'network' |
|
570 |
name = 'create' |
|
571 |
syntax = '<network name>' |
|
572 |
description = 'create a network' |
|
465 |
@command(api='synnefo') |
|
466 |
class network_create(object): |
|
467 |
"""create a network""" |
|
573 | 468 |
|
574 | 469 |
def main(self, name): |
575 | 470 |
reply = self.client.create_network(name) |
576 | 471 |
print_dict(reply) |
577 | 472 |
|
578 | 473 |
|
579 |
class GetNetworkDetails(Command): |
|
580 |
api = 'synnefo' |
|
581 |
group = 'network' |
|
582 |
name = 'info' |
|
583 |
syntax = '<network id>' |
|
584 |
description = 'get network details' |
|
585 |
|
|
474 |
@command(api='synnefo') |
|
475 |
class network_info(object): |
|
476 |
"""get network details""" |
|
477 |
|
|
586 | 478 |
def main(self, network_id): |
587 | 479 |
network = self.client.get_network_details(network_id) |
588 | 480 |
print_dict(network) |
589 | 481 |
|
590 | 482 |
|
591 |
class RenameNetwork(Command): |
|
592 |
api = 'synnefo' |
|
593 |
group = 'network' |
|
594 |
name = 'rename' |
|
595 |
syntax = '<network id> <new name>' |
|
596 |
description = 'update network name' |
|
483 |
@command(api='synnefo') |
|
484 |
class network_rename(object): |
|
485 |
"""update network name""" |
|
597 | 486 |
|
598 |
def main(self, network_id, name): |
|
599 |
self.client.update_network_name(network_id, name) |
|
487 |
def main(self, network_id, new_name):
|
|
488 |
self.client.update_network_name(network_id, new_name)
|
|
600 | 489 |
|
601 | 490 |
|
602 |
class DeleteNetwork(Command): |
|
603 |
api = 'synnefo' |
|
604 |
group = 'network' |
|
605 |
name = 'delete' |
|
606 |
syntax = '<network id>' |
|
607 |
description = 'delete a network' |
|
491 |
@command(api='synnefo') |
|
492 |
class network_delete(object): |
|
493 |
"""delete a network""" |
|
608 | 494 |
|
609 | 495 |
def main(self, network_id): |
610 | 496 |
self.client.delete_network(network_id) |
611 | 497 |
|
612 |
class ConnectServer(Command): |
|
613 |
api = 'synnefo' |
|
614 |
group = 'network' |
|
615 |
name = 'connect' |
|
616 |
syntax = '<server id> <network id>' |
|
617 |
description = 'connect a server to a network' |
|
498 |
|
|
499 |
@command(api='synnefo') |
|
500 |
class network_connect(object): |
|
501 |
"""connect a server to a network""" |
|
618 | 502 |
|
619 | 503 |
def main(self, server_id, network_id): |
620 | 504 |
self.client.connect_server(server_id, network_id) |
621 | 505 |
|
622 | 506 |
|
623 |
class DisconnectServer(Command): |
|
624 |
api = 'synnefo' |
|
625 |
group = 'network' |
|
626 |
name = 'disconnect' |
|
627 |
syntax = '<server id> <network id>' |
|
628 |
description = 'disconnect a server from a network' |
|
629 |
|
|
507 |
@command(api='synnefo') |
|
508 |
class network_disconnect(object): |
|
509 |
"""disconnect a server from a network""" |
|
510 |
|
|
630 | 511 |
def main(self, server_id, network_id): |
631 | 512 |
self.client.disconnect_server(server_id, network_id) |
632 | 513 |
|
633 | 514 |
|
634 |
|
|
635 |
def print_usage(exe, groups, group=None): |
|
636 |
nop = Command([]) |
|
637 |
nop.parser.print_help() |
|
515 |
@command(api='glance') |
|
516 |
class glance_list(object): |
|
517 |
"""list images""" |
|
638 | 518 |
|
519 |
def main(self): |
|
520 |
images = self.client.list_public() |
|
521 |
print images |
|
522 |
|
|
523 |
|
|
524 |
def print_groups(groups): |
|
525 |
|
|
526 |
print 'Groups:' |
|
527 |
for group in groups: |
|
528 |
print ' %s' % group |
|
529 |
|
|
530 |
|
|
531 |
def print_commands(group, commands): |
|
639 | 532 |
|
640 | 533 |
print 'Commands:' |
641 |
|
|
642 |
if group: |
|
643 |
items = [(group, groups[group])] |
|
644 |
else: |
|
645 |
items = sorted(groups.items()) |
|
646 |
|
|
647 |
for group, commands in items: |
|
648 |
for command, cls in sorted(commands.items()): |
|
649 |
name = ' %s %s' % (group, command) |
|
650 |
print '%s %s' % (name.ljust(22), cls.description) |
|
651 |
|
|
534 |
for name, cls in _commands[group].items(): |
|
535 |
if name in commands: |
|
536 |
print ' %s %s' % (name.ljust(10), cls.description) |
|
652 | 537 |
|
653 | 538 |
|
654 | 539 |
def main(): |
655 |
nop = Command([]) |
|
656 |
groups = defaultdict(dict) |
|
657 |
module = sys.modules[__name__] |
|
658 |
for name, cls in inspect.getmembers(module, inspect.isclass): |
|
659 |
if issubclass(cls, Command) and cls != Command: |
|
660 |
if nop.api == 'openstack' and nop.api != cls.api: |
|
661 |
continue # Ignore synnefo commands |
|
662 |
groups[cls.group][cls.name] = cls |
|
540 |
parser = OptionParser(add_help_option=False) |
|
541 |
parser.usage = '%prog <group> <command> [options]' |
|
542 |
parser.add_option('--help', dest='help', action='store_true', |
|
543 |
default=False, help='show this help message and exit') |
|
544 |
parser.add_option('--api', dest='apis', metavar='API', action='append', |
|
545 |
help='API to use (can be used multiple times)') |
|
546 |
parser.add_option('--compute-url', dest='compute_url', metavar='URL', |
|
547 |
help='URL for the compute API') |
|
548 |
parser.add_option('--images-url', dest='images_url', metavar='URL', |
|
549 |
help='URL for the images API') |
|
550 |
parser.add_option('--token', dest='token', metavar='TOKEN', |
|
551 |
help='use token TOKEN') |
|
552 |
parser.add_option('-v', dest='verbose', action='store_true', default=False, |
|
553 |
help='use verbose output') |
|
554 |
parser.add_option('-d', dest='debug', action='store_true', default=False, |
|
555 |
help='use debug output') |
|
556 |
|
|
557 |
# Do a preliminary parsing, ignore any errors since we will print help |
|
558 |
# anyway if we don't reach the main parsing. |
|
559 |
_error = parser.error |
|
560 |
parser.error = lambda msg: None |
|
561 |
options, args = parser.parse_args(sys.argv) |
|
562 |
parser.error = _error |
|
563 |
|
|
564 |
if options.debug: |
|
565 |
log.setLevel(logging.DEBUG) |
|
566 |
elif options.verbose: |
|
567 |
log.setLevel(logging.INFO) |
|
568 |
else: |
|
569 |
log.setLevel(logging.WARNING) |
|
663 | 570 |
|
664 |
argv = list(sys.argv) |
|
665 |
exe = os.path.basename(argv.pop(0)) |
|
666 |
group = argv.pop(0) if argv else None |
|
667 |
command = argv.pop(0) if argv else None |
|
571 |
try: |
|
572 |
config = Config(CONFIG_PATH, CONFIG_ENV, CONFIG_DEFAULTS) |
|
573 |
except ConfigError, e: |
|
574 |
log.error('%s', e.args[0]) |
|
575 |
return 1 |
|
576 |
|
|
577 |
for key in CONFIG_DEFAULTS: |
|
578 |
config.override(key, getattr(options, key)) |
|
579 |
|
|
580 |
apis = config.get('apis').split() |
|
581 |
|
|
582 |
# Find available groups based on the given APIs |
|
583 |
available_groups = [] |
|
584 |
for group, group_commands in _commands.items(): |
|
585 |
for name, cls in group_commands.items(): |
|
586 |
if cls.api is None or cls.api in apis: |
|
587 |
available_groups.append(group) |
|
588 |
break |
|
589 |
|
|
590 |
if len(args) < 2: |
|
591 |
parser.print_help() |
|
592 |
print_groups(available_groups) |
|
593 |
return 0 |
|
594 |
|
|
595 |
group = args[1] |
|
596 |
|
|
597 |
if group not in available_groups: |
|
598 |
parser.print_help() |
|
599 |
print_groups(available_groups) |
|
600 |
return 1 |
|
601 |
|
|
602 |
# Find available commands based on the given APIs |
|
603 |
available_commands = [] |
|
604 |
for name, cls in _commands[group].items(): |
|
605 |
if cls.api is None or cls.api in apis: |
|
606 |
available_commands.append(name) |
|
607 |
continue |
|
608 |
|
|
609 |
parser.usage = '%%prog %s <command> [options]' % group |
|
610 |
|
|
611 |
if len(args) < 3: |
|
612 |
parser.print_help() |
|
613 |
print_commands(group, available_commands) |
|
614 |
return 0 |
|
615 |
|
|
616 |
name = args[2] |
|
668 | 617 |
|
669 |
if group not in groups: |
|
670 |
group = None |
|
618 |
if name not in available_commands: |
|
619 |
parser.print_help() |
|
620 |
print_commands(group, available_commands) |
|
621 |
return 1 |
|
671 | 622 |
|
672 |
if not group or command not in groups[group]: |
|
673 |
print_usage(exe, groups, group) |
|
674 |
sys.exit(1) |
|
623 |
cls = _commands[group][name] |
|
624 |
cls.config = config |
|
675 | 625 |
|
676 |
cls = groups[group][command] |
|
626 |
syntax = '%s [options]' % cls.syntax if cls.syntax else '[options]' |
|
627 |
parser.usage = '%%prog %s %s %s' % (group, name, syntax) |
|
628 |
parser.epilog = '' |
|
629 |
if hasattr(cls, 'update_parser'): |
|
630 |
cls.update_parser(parser) |
|
631 |
|
|
632 |
options, args = parser.parse_args(sys.argv) |
|
633 |
if options.help: |
|
634 |
parser.print_help() |
|
635 |
return 0 |
|
636 |
|
|
637 |
cmd = cls() |
|
638 |
cmd.config = config |
|
639 |
cmd.options = options |
|
640 |
|
|
641 |
if cmd.api in ('nova', 'synnefo'): |
|
642 |
url = config.get('compute_url') |
|
643 |
token = config.get('token') |
|
644 |
cmd.client = ComputeClient(url, token) |
|
645 |
elif cmd.api in ('glance', 'plankton'): |
|
646 |
url = config.get('images_url') |
|
647 |
token = config.get('token') |
|
648 |
cmd.client = ImagesClient(url, token) |
|
677 | 649 |
|
678 | 650 |
try: |
679 |
cmd = cls(argv) |
|
680 |
cmd.execute() |
|
651 |
return cmd.main(*args[3:]) |
|
652 |
except TypeError: |
|
653 |
parser.print_help() |
|
654 |
return 1 |
|
681 | 655 |
except ClientError, err: |
682 | 656 |
log.error('%s', err.message) |
683 | 657 |
log.info('%s', err.details) |
658 |
return 2 |
|
684 | 659 |
|
685 | 660 |
|
686 | 661 |
if __name__ == '__main__': |
687 | 662 |
ch = logging.StreamHandler() |
688 | 663 |
ch.setFormatter(logging.Formatter('%(message)s')) |
689 | 664 |
log.addHandler(ch) |
690 |
|
|
691 |
main() |
|
665 |
err = main() or 0 |
|
666 |
sys.exit(err) |
Also available in: Unified diff