root / tools / cloud @ f533f224
History | View | Annotate | Download (18 kB)
1 |
#!/usr/bin/env python |
---|---|
2 |
|
3 |
from httplib import HTTPConnection |
4 |
from optparse import OptionParser |
5 |
from os.path import basename |
6 |
from sys import argv, exit |
7 |
|
8 |
import json |
9 |
|
10 |
DEFAULT_HOST = '127.0.0.1:8000' |
11 |
DEFAULT_API = 'v1.1' |
12 |
|
13 |
TOKEN = '46e427d657b20defe352804f0eb6f8a2' |
14 |
|
15 |
|
16 |
commands = {} |
17 |
|
18 |
def command_name(name): |
19 |
def decorator(cls): |
20 |
commands[name] = cls |
21 |
return cls |
22 |
return decorator |
23 |
|
24 |
|
25 |
def print_addresses(networks): |
26 |
for i, net in enumerate(networks): |
27 |
key = 'addresses:'.rjust(13) if i == 0 else ' ' * 13 |
28 |
addr = '' |
29 |
if 'values' in net: |
30 |
addr = '[%s]' % ' '.join(ip['addr'] for ip in net['values']) |
31 |
|
32 |
val = '%s/%s %s %s' % (net['id'], net['name'], net['mac'], addr) |
33 |
print '%s %s' % (key, val) |
34 |
|
35 |
def print_dict(d, show_empty=True): |
36 |
for key, val in sorted(d.items()): |
37 |
if key == 'metadata': |
38 |
val = ', '.join('%s="%s"' % x for x in val['values'].items()) |
39 |
elif key == 'addresses': |
40 |
print_addresses(val['values']) |
41 |
continue |
42 |
elif key == 'servers': |
43 |
val = ', '.join(str(server_id) for server_id in val['values']) |
44 |
if val or show_empty: |
45 |
print '%s: %s' % (key.rjust(12), val) |
46 |
|
47 |
|
48 |
class Command(object): |
49 |
def __init__(self, argv): |
50 |
parser = OptionParser() |
51 |
parser.add_option('--host', dest='host', metavar='HOST', default=DEFAULT_HOST, |
52 |
help='use server HOST') |
53 |
parser.add_option('--api', dest='api', metavar='API', default=DEFAULT_API, |
54 |
help='use api API') |
55 |
parser.add_option('-v', action='store_true', dest='verbose', default=False, |
56 |
help='use verbose output') |
57 |
self.add_options(parser) |
58 |
options, args = parser.parse_args(argv) |
59 |
|
60 |
# Add options to self |
61 |
for opt in parser.option_list: |
62 |
key = opt.dest |
63 |
if key: |
64 |
val = getattr(options, key) |
65 |
setattr(self, key, val) |
66 |
|
67 |
self.execute(*args) |
68 |
|
69 |
def add_options(self, parser): |
70 |
pass |
71 |
|
72 |
def execute(self, *args): |
73 |
pass |
74 |
|
75 |
def http_cmd(self, method, path, body=None, expected_status=200): |
76 |
conn = HTTPConnection(self.host) |
77 |
|
78 |
kwargs = {} |
79 |
kwargs['headers'] = {'X-Auth-Token': TOKEN} |
80 |
if body: |
81 |
kwargs['headers']['Content-Type'] = 'application/json' |
82 |
kwargs['body'] = body |
83 |
conn.request(method, path, **kwargs) |
84 |
|
85 |
resp = conn.getresponse() |
86 |
if self.verbose: |
87 |
print '%d %s' % (resp.status, resp.reason) |
88 |
for key, val in resp.getheaders(): |
89 |
print '%s: %s' % (key.capitalize(), val) |
90 |
|
91 |
|
92 |
buf = resp.read() or '{}' |
93 |
try: |
94 |
reply = json.loads(buf) |
95 |
except ValueError: |
96 |
print 'Invalid response from the server.' |
97 |
if self.verbose: |
98 |
print buf |
99 |
exit(1) |
100 |
|
101 |
# If the response status is not the expected one, |
102 |
# assume an error has occured and treat the body |
103 |
# as a cloudfault. |
104 |
if resp.status != expected_status: |
105 |
if len(reply) == 1: |
106 |
key = reply.keys()[0] |
107 |
val = reply[key] |
108 |
print '%s: %s' % (key, val.get('message', '')) |
109 |
if self.verbose: |
110 |
print val.get('details', '') |
111 |
else: |
112 |
print 'Invalid response from the server.' |
113 |
exit(1) |
114 |
|
115 |
return reply |
116 |
|
117 |
def http_get(self, path, expected_status=200): |
118 |
return self.http_cmd('GET', path, None, expected_status) |
119 |
|
120 |
def http_post(self, path, body, expected_status=202): |
121 |
return self.http_cmd('POST', path, body, expected_status) |
122 |
|
123 |
def http_put(self, path, body, expected_status=204): |
124 |
return self.http_cmd('PUT', path, body, expected_status) |
125 |
|
126 |
def http_delete(self, path, expected_status=204): |
127 |
return self.http_cmd('DELETE', path, None, expected_status) |
128 |
|
129 |
|
130 |
@command_name('ls') |
131 |
class ListServers(Command): |
132 |
description = 'list servers' |
133 |
|
134 |
def add_options(self, parser): |
135 |
parser.add_option('-l', action='store_true', dest='detail', default=False, |
136 |
help='show detailed output') |
137 |
parser.add_option('-a', action='store_true', dest='show_empty', default=False, |
138 |
help='include empty values') |
139 |
|
140 |
def execute(self): |
141 |
path = '/api/%s/servers' % self.api |
142 |
if self.detail: |
143 |
path += '/detail' |
144 |
|
145 |
reply = self.http_get(path) |
146 |
|
147 |
for server in reply['servers']['values']: |
148 |
id = server.pop('id') |
149 |
name = server.pop('name') |
150 |
if self.detail: |
151 |
print '%d %s' % (id, name) |
152 |
print_dict(server, self.show_empty) |
153 |
|
154 |
else: |
155 |
print '%3d %s' % (id, name) |
156 |
|
157 |
|
158 |
@command_name('info') |
159 |
class GetServerDetails(Command): |
160 |
description = 'get server details' |
161 |
syntax = '<server id>' |
162 |
|
163 |
def add_options(self, parser): |
164 |
parser.add_option('-a', action='store_true', dest='show_empty', default=False, |
165 |
help='include empty values') |
166 |
|
167 |
def execute(self, server_id): |
168 |
path = '/api/%s/servers/%d' % (self.api, int(server_id)) |
169 |
reply = self.http_get(path) |
170 |
server = reply['server'] |
171 |
server.pop('id') |
172 |
print_dict(server, self.show_empty) |
173 |
|
174 |
|
175 |
@command_name('create') |
176 |
class CreateServer(Command): |
177 |
description = 'create server' |
178 |
syntax = '<server name>' |
179 |
|
180 |
def add_options(self, parser): |
181 |
parser.add_option('-f', dest='flavor', metavar='FLAVOR_ID', default=1, |
182 |
help='use flavor FLAVOR_ID') |
183 |
parser.add_option('-i', dest='image', metavar='IMAGE_ID', default=1, |
184 |
help='use image IMAGE_ID') |
185 |
|
186 |
def execute(self, name): |
187 |
path = '/api/%s/servers' % self.api |
188 |
server = {'name': name, 'flavorRef': self.flavor, 'imageRef': self.image} |
189 |
body = json.dumps({'server': server}) |
190 |
reply = self.http_post(path, body) |
191 |
server = reply['server'] |
192 |
print_dict(server) |
193 |
|
194 |
|
195 |
@command_name('rename') |
196 |
class UpdateServerName(Command): |
197 |
description = 'update server name' |
198 |
syntax = '<server id> <new name>' |
199 |
|
200 |
def execute(self, server_id, name): |
201 |
path = '/api/%s/servers/%d' % (self.api, int(server_id)) |
202 |
body = json.dumps({'server': {'name': name}}) |
203 |
self.http_put(path, body) |
204 |
|
205 |
|
206 |
@command_name('delete') |
207 |
class DeleteServer(Command): |
208 |
description = 'delete server' |
209 |
syntax = '<server id>' |
210 |
|
211 |
def execute(self, server_id): |
212 |
path = '/api/%s/servers/%d' % (self.api, int(server_id)) |
213 |
self.http_delete(path) |
214 |
|
215 |
|
216 |
@command_name('reboot') |
217 |
class RebootServer(Command): |
218 |
description = 'reboot server' |
219 |
syntax = '<server id>' |
220 |
|
221 |
def add_options(self, parser): |
222 |
parser.add_option('-f', action='store_true', dest='hard', default=False, |
223 |
help='perform a hard reboot') |
224 |
|
225 |
def execute(self, server_id): |
226 |
path = '/api/%s/servers/%d/action' % (self.api, int(server_id)) |
227 |
type = 'HARD' if self.hard else 'SOFT' |
228 |
body = json.dumps({'reboot': {'type': type}}) |
229 |
self.http_post(path, body) |
230 |
|
231 |
|
232 |
@command_name('start') |
233 |
class StartServer(Command): |
234 |
description = 'start server' |
235 |
syntax = '<server id>' |
236 |
|
237 |
def execute(self, server_id): |
238 |
path = '/api/%s/servers/%d/action' % (self.api, int(server_id)) |
239 |
body = json.dumps({'start': {}}) |
240 |
self.http_post(path, body) |
241 |
|
242 |
|
243 |
@command_name('shutdown') |
244 |
class StartServer(Command): |
245 |
description = 'shutdown server' |
246 |
syntax = '<server id>' |
247 |
|
248 |
def execute(self, server_id): |
249 |
path = '/api/%s/servers/%d/action' % (self.api, int(server_id)) |
250 |
body = json.dumps({'shutdown': {}}) |
251 |
self.http_post(path, body) |
252 |
|
253 |
|
254 |
@command_name('console') |
255 |
class ServerConsole(Command): |
256 |
description = 'get VNC console' |
257 |
syntax = '<server id>' |
258 |
|
259 |
def add_options(self, parser): |
260 |
pass |
261 |
|
262 |
def execute(self, server_id): |
263 |
path = '/api/%s/servers/%d/action' % (self.api, int(server_id)) |
264 |
body = json.dumps({'console': {'type': 'vnc'}}) |
265 |
reply = self.http_cmd('POST', path, body, 200) |
266 |
print_dict(reply['console']) |
267 |
|
268 |
|
269 |
@command_name('lsaddr') |
270 |
class ListAddresses(Command): |
271 |
description = 'list server addresses' |
272 |
syntax = '<server id> [network]' |
273 |
|
274 |
def execute(self, server_id, network=None): |
275 |
path = '/api/%s/servers/%d/ips' % (self.api, int(server_id)) |
276 |
if network: |
277 |
path += '/%s' % network |
278 |
reply = self.http_get(path) |
279 |
|
280 |
addresses = [reply['network']] if network else reply['addresses']['values'] |
281 |
print_addresses(addresses) |
282 |
|
283 |
|
284 |
@command_name('lsflv') |
285 |
class ListFlavors(Command): |
286 |
description = 'list flavors' |
287 |
|
288 |
def add_options(self, parser): |
289 |
parser.add_option('-l', action='store_true', dest='detail', default=False, |
290 |
help='show detailed output') |
291 |
|
292 |
def execute(self): |
293 |
path = '/api/%s/flavors' % self.api |
294 |
if self.detail: |
295 |
path += '/detail' |
296 |
reply = self.http_get(path) |
297 |
|
298 |
for flavor in reply['flavors']['values']: |
299 |
id = flavor.pop('id') |
300 |
name = flavor.pop('name') |
301 |
details = ' '.join('%s=%s' % item for item in sorted(flavor.items())) |
302 |
print '%3d %s %s' % (id, name, details) |
303 |
|
304 |
|
305 |
@command_name('flvinfo') |
306 |
class GetFlavorDetails(Command): |
307 |
description = 'get flavor details' |
308 |
syntax = '<flavor id>' |
309 |
|
310 |
def execute(self, flavor_id): |
311 |
path = '/api/%s/flavors/%d' % (self.api, int(flavor_id)) |
312 |
reply = self.http_get(path) |
313 |
|
314 |
flavor = reply['flavor'] |
315 |
id = flavor.pop('id') |
316 |
name = flavor.pop('name') |
317 |
details = ' '.join('%s=%s' % item for item in sorted(flavor.items())) |
318 |
print '%3d %s %s' % (id, name, details) |
319 |
|
320 |
|
321 |
@command_name('lsimg') |
322 |
class ListImages(Command): |
323 |
description = 'list images' |
324 |
|
325 |
def add_options(self, parser): |
326 |
parser.add_option('-l', action='store_true', dest='detail', default=False, |
327 |
help='show detailed output') |
328 |
|
329 |
def execute(self): |
330 |
path = '/api/%s/images' % self.api |
331 |
if self.detail: |
332 |
path += '/detail' |
333 |
reply = self.http_get(path) |
334 |
|
335 |
for image in reply['images']['values']: |
336 |
id = image.pop('id') |
337 |
name = image.pop('name') |
338 |
if self.detail: |
339 |
print '%d %s' % (id, name) |
340 |
print_dict(image) |
341 |
|
342 |
else: |
343 |
print '%3d %s' % (id, name) |
344 |
|
345 |
|
346 |
@command_name('imginfo') |
347 |
class GetImageDetails(Command): |
348 |
description = 'get image details' |
349 |
syntax = '<image id>' |
350 |
|
351 |
def execute(self, image_id): |
352 |
path = '/api/%s/images/%d' % (self.api, int(image_id)) |
353 |
reply = self.http_get(path) |
354 |
image = reply['image'] |
355 |
image.pop('id') |
356 |
print_dict(image) |
357 |
|
358 |
|
359 |
@command_name('createimg') |
360 |
class CreateImage(Command): |
361 |
description = 'create image' |
362 |
syntax = '<server id> <image name>' |
363 |
|
364 |
def execute(self, server_id, name): |
365 |
path = '/api/%s/images' % self.api |
366 |
image = {'name': name, 'serverRef': int(server_id)} |
367 |
body = json.dumps({'image': image}) |
368 |
reply = self.http_post(path, body) |
369 |
print_dict(reply['image']) |
370 |
|
371 |
@command_name('deleteimg') |
372 |
class DeleteImage(Command): |
373 |
description = 'delete image' |
374 |
syntax = '<image id>' |
375 |
|
376 |
def execute(self, image_id): |
377 |
path = '/api/%s/images/%d' % (self.api, int(image_id)) |
378 |
self.http_delete(path) |
379 |
|
380 |
@command_name('lsmeta') |
381 |
class ListServerMeta(Command): |
382 |
description = 'list server meta' |
383 |
syntax = '<server id> [key]' |
384 |
|
385 |
def execute(self, server_id, key=None): |
386 |
path = '/api/%s/servers/%d/meta' % (self.api, int(server_id)) |
387 |
if key: |
388 |
path += '/' + key |
389 |
reply = self.http_get(path) |
390 |
if key: |
391 |
print_dict(reply['meta']) |
392 |
else: |
393 |
print_dict(reply['metadata']['values']) |
394 |
|
395 |
@command_name('setmeta') |
396 |
class UpdateServerMeta(Command): |
397 |
description = 'update server meta' |
398 |
syntax = '<server id> <key> <val>' |
399 |
|
400 |
def execute(self, server_id, key, val): |
401 |
path = '/api/%s/servers/%d/meta' % (self.api, int(server_id)) |
402 |
metadata = {key: val} |
403 |
body = json.dumps({'metadata': metadata}) |
404 |
reply = self.http_post(path, body, expected_status=201) |
405 |
print_dict(reply['metadata']) |
406 |
|
407 |
@command_name('addmeta') |
408 |
class CreateServerMeta(Command): |
409 |
description = 'add server meta' |
410 |
syntax = '<server id> <key> <val>' |
411 |
|
412 |
def execute(self, server_id, key, val): |
413 |
path = '/api/%s/servers/%d/meta/%s' % (self.api, int(server_id), key) |
414 |
meta = {key: val} |
415 |
body = json.dumps({'meta': meta}) |
416 |
reply = self.http_put(path, body, expected_status=201) |
417 |
print_dict(reply['meta']) |
418 |
|
419 |
@command_name('delmeta') |
420 |
class DeleteServerMeta(Command): |
421 |
description = 'delete server meta' |
422 |
syntax = '<server id> <key>' |
423 |
|
424 |
def execute(self, server_id, key): |
425 |
path = '/api/%s/servers/%d/meta/%s' % (self.api, int(server_id), key) |
426 |
reply = self.http_delete(path) |
427 |
|
428 |
@command_name('lsimgmeta') |
429 |
class ListImageMeta(Command): |
430 |
description = 'list image meta' |
431 |
syntax = '<image id> [key]' |
432 |
|
433 |
def execute(self, image_id, key=None): |
434 |
path = '/api/%s/images/%d/meta' % (self.api, int(image_id)) |
435 |
if key: |
436 |
path += '/' + key |
437 |
reply = self.http_get(path) |
438 |
if key: |
439 |
print_dict(reply['meta']) |
440 |
else: |
441 |
print_dict(reply['metadata']['values']) |
442 |
|
443 |
@command_name('setimgmeta') |
444 |
class UpdateImageMeta(Command): |
445 |
description = 'update image meta' |
446 |
syntax = '<image id> <key> <val>' |
447 |
|
448 |
def execute(self, image_id, key, val): |
449 |
path = '/api/%s/images/%d/meta' % (self.api, int(image_id)) |
450 |
metadata = {key: val} |
451 |
body = json.dumps({'metadata': metadata}) |
452 |
reply = self.http_post(path, body, expected_status=201) |
453 |
print_dict(reply['metadata']) |
454 |
|
455 |
@command_name('addimgmeta') |
456 |
class CreateImageMeta(Command): |
457 |
description = 'add image meta' |
458 |
syntax = '<image id> <key> <val>' |
459 |
|
460 |
def execute(self, image_id, key, val): |
461 |
path = '/api/%s/images/%d/meta/%s' % (self.api, int(image_id), key) |
462 |
meta = {key: val} |
463 |
body = json.dumps({'meta': meta}) |
464 |
reply = self.http_put(path, body, expected_status=201) |
465 |
print_dict(reply['meta']) |
466 |
|
467 |
@command_name('delimgmeta') |
468 |
class DeleteImageMeta(Command): |
469 |
description = 'delete image meta' |
470 |
syntax = '<image id> <key>' |
471 |
|
472 |
def execute(self, image_id, key): |
473 |
path = '/api/%s/images/%d/meta/%s' % (self.api, int(image_id), key) |
474 |
reply = self.http_delete(path) |
475 |
|
476 |
|
477 |
@command_name('lsnet') |
478 |
class ListNetworks(Command): |
479 |
description = 'list networks' |
480 |
|
481 |
def add_options(self, parser): |
482 |
parser.add_option('-l', action='store_true', dest='detail', default=False, |
483 |
help='show detailed output') |
484 |
|
485 |
def execute(self): |
486 |
path = '/api/%s/networks' % self.api |
487 |
if self.detail: |
488 |
path += '/detail' |
489 |
reply = self.http_get(path) |
490 |
|
491 |
for network in reply['networks']['values']: |
492 |
id = network.pop('id') |
493 |
name = network.pop('name') |
494 |
if self.detail: |
495 |
print '%s %s' % (id, name) |
496 |
print_dict(network) |
497 |
|
498 |
else: |
499 |
print '%3s %s' % (id, name) |
500 |
|
501 |
|
502 |
@command_name('createnet') |
503 |
class CreateNetwork(Command): |
504 |
description = 'create network' |
505 |
syntax = '<network name>' |
506 |
|
507 |
def execute(self, name): |
508 |
path = '/api/%s/networks' % self.api |
509 |
network = {'name': name} |
510 |
body = json.dumps({'network': network}) |
511 |
reply = self.http_post(path, body) |
512 |
print_dict(reply['network']) |
513 |
|
514 |
|
515 |
@command_name('netinfo') |
516 |
class GetNetworkDetails(Command): |
517 |
description = 'get network details' |
518 |
syntax = '<network id>' |
519 |
|
520 |
def execute(self, network_id): |
521 |
path = '/api/%s/networks/%d' % (self.api, int(network_id)) |
522 |
reply = self.http_get(path) |
523 |
net = reply['network'] |
524 |
name = net.pop('id') |
525 |
print_dict(net) |
526 |
|
527 |
|
528 |
@command_name('renamenet') |
529 |
class UpdateNetworkName(Command): |
530 |
description = 'update network name' |
531 |
syntax = '<network_id> <new name>' |
532 |
|
533 |
def execute(self, network_id, name): |
534 |
path = '/api/%s/networks/%d' % (self.api, int(network_id)) |
535 |
body = json.dumps({'network': {'name': name}}) |
536 |
self.http_put(path, body) |
537 |
|
538 |
|
539 |
@command_name('deletenet') |
540 |
class DeleteNetwork(Command): |
541 |
description = 'delete network' |
542 |
syntax = '<network id>' |
543 |
|
544 |
def execute(self, network_id): |
545 |
path = '/api/%s/networks/%d' % (self.api, int(network_id)) |
546 |
self.http_delete(path) |
547 |
|
548 |
|
549 |
@command_name('connect') |
550 |
class AddNetwork(Command): |
551 |
description = 'connect a server to a network' |
552 |
syntax = '<server id> <network id>' |
553 |
|
554 |
def execute(self, server_id, network_id): |
555 |
path = '/api/%s/networks/%d/action' % (self.api, int(network_id)) |
556 |
body = json.dumps({'add': {'serverRef': server_id}}) |
557 |
self.http_post(path, body, expected_status=202) |
558 |
|
559 |
|
560 |
@command_name('disconnect') |
561 |
class RemoveNetwork(Command): |
562 |
description = 'disconnect a server from a network' |
563 |
syntax = '<server id> <network id>' |
564 |
|
565 |
def execute(self, server_id, network_id): |
566 |
path = '/api/%s/networks/%s/action' % (self.api, int(network_id)) |
567 |
body = json.dumps({'remove': {'serverRef': server_id}}) |
568 |
self.http_post(path, body, expected_status=202) |
569 |
|
570 |
|
571 |
def print_usage(): |
572 |
print 'Usage: %s <command>' % basename(argv[0]) |
573 |
|
574 |
print 'Commands:' |
575 |
for name, cls in sorted(commands.items()): |
576 |
description = getattr(cls, 'description', '') |
577 |
print ' %s %s' % (name.ljust(12), description) |
578 |
|
579 |
def main(): |
580 |
try: |
581 |
name = argv[1] |
582 |
cls = commands[name] |
583 |
except (IndexError, KeyError): |
584 |
print_usage() |
585 |
exit(1) |
586 |
|
587 |
try: |
588 |
cls(argv[2:]) |
589 |
except TypeError: |
590 |
syntax = getattr(cls, 'syntax', '') |
591 |
if syntax: |
592 |
print 'Syntax: %s %s' % (name, syntax) |
593 |
else: |
594 |
print 'Invalid syntax' |
595 |
exit(1) |
596 |
|
597 |
|
598 |
if __name__ == '__main__': |
599 |
main() |