Revision 6a0b1658
b/kamaki/cli.py | ||
---|---|---|
77 | 77 |
from pwd import getpwuid |
78 | 78 |
from sys import argv, exit, stdout |
79 | 79 |
|
80 |
from clint.textui import puts, puts_err, indent |
|
80 |
from clint import args |
|
81 |
from clint.textui import puts, puts_err, indent, progress |
|
82 |
from clint.textui.colored import magenta, red, yellow |
|
81 | 83 |
from clint.textui.cols import columns |
82 | 84 |
|
85 |
from requests.exceptions import ConnectionError |
|
86 |
|
|
83 | 87 |
from kamaki import clients |
84 | 88 |
from kamaki.config import Config |
85 | 89 |
from kamaki.utils import OrderedDict, print_addresses, print_dict, print_items |
... | ... | |
116 | 120 |
cls.api = api |
117 | 121 |
cls.group = group or grp |
118 | 122 |
cls.name = name or cmd |
119 |
cls.description = description or cls.__doc__ |
|
120 |
cls.syntax = syntax |
|
121 | 123 |
|
122 | 124 |
short_description, sep, long_description = cls.__doc__.partition('\n') |
123 | 125 |
cls.description = short_description |
... | ... | |
194 | 196 |
|
195 | 197 |
@command(api='compute') |
196 | 198 |
class server_list(object): |
197 |
"""list servers"""
|
|
199 |
"""List servers"""
|
|
198 | 200 |
|
199 | 201 |
def update_parser(cls, parser): |
200 | 202 |
parser.add_option('-l', dest='detail', action='store_true', |
... | ... | |
207 | 209 |
|
208 | 210 |
@command(api='compute') |
209 | 211 |
class server_info(object): |
210 |
"""get server details"""
|
|
212 |
"""Get server details"""
|
|
211 | 213 |
|
212 | 214 |
def main(self, server_id): |
213 | 215 |
server = self.client.get_server_details(int(server_id)) |
... | ... | |
216 | 218 |
|
217 | 219 |
@command(api='compute') |
218 | 220 |
class server_create(object): |
219 |
"""create server"""
|
|
221 |
"""Create a server"""
|
|
220 | 222 |
|
221 | 223 |
def update_parser(cls, parser): |
222 | 224 |
parser.add_option('--personality', dest='personalities', |
223 |
action='append', default=[], |
|
224 |
metavar='PATH[,SERVER PATH[,OWNER[,GROUP,[MODE]]]]', |
|
225 |
help='add a personality file') |
|
225 |
action='append', default=[],
|
|
226 |
metavar='PATH[,SERVER PATH[,OWNER[,GROUP,[MODE]]]]',
|
|
227 |
help='add a personality file')
|
|
226 | 228 |
parser.epilog = "If missing, optional personality values will be " \ |
227 |
"filled based on the file at PATH if missing."
|
|
229 |
"filled based on the file at PATH."
|
|
228 | 230 |
|
229 | 231 |
def main(self, name, flavor_id, image_id): |
230 | 232 |
personalities = [] |
... | ... | |
259 | 261 |
|
260 | 262 |
@command(api='compute') |
261 | 263 |
class server_rename(object): |
262 |
"""update server name"""
|
|
264 |
"""Update a server's name"""
|
|
263 | 265 |
|
264 | 266 |
def main(self, server_id, new_name): |
265 | 267 |
self.client.update_server_name(int(server_id), new_name) |
... | ... | |
267 | 269 |
|
268 | 270 |
@command(api='compute') |
269 | 271 |
class server_delete(object): |
270 |
"""delete server"""
|
|
272 |
"""Delete a server"""
|
|
271 | 273 |
|
272 | 274 |
def main(self, server_id): |
273 | 275 |
self.client.delete_server(int(server_id)) |
... | ... | |
275 | 277 |
|
276 | 278 |
@command(api='compute') |
277 | 279 |
class server_reboot(object): |
278 |
"""reboot server"""
|
|
280 |
"""Reboot a server"""
|
|
279 | 281 |
|
280 | 282 |
def update_parser(cls, parser): |
281 | 283 |
parser.add_option('-f', dest='hard', action='store_true', |
... | ... | |
287 | 289 |
|
288 | 290 |
@command(api='cyclades') |
289 | 291 |
class server_start(object): |
290 |
"""start server"""
|
|
292 |
"""Start a server"""
|
|
291 | 293 |
|
292 | 294 |
def main(self, server_id): |
293 | 295 |
self.client.start_server(int(server_id)) |
... | ... | |
295 | 297 |
|
296 | 298 |
@command(api='cyclades') |
297 | 299 |
class server_shutdown(object): |
298 |
"""shutdown server"""
|
|
300 |
"""Shutdown a server"""
|
|
299 | 301 |
|
300 | 302 |
def main(self, server_id): |
301 | 303 |
self.client.shutdown_server(int(server_id)) |
... | ... | |
303 | 305 |
|
304 | 306 |
@command(api='cyclades') |
305 | 307 |
class server_console(object): |
306 |
"""get a VNC console"""
|
|
308 |
"""Get a VNC console"""
|
|
307 | 309 |
|
308 | 310 |
def main(self, server_id): |
309 | 311 |
reply = self.client.get_server_console(int(server_id)) |
... | ... | |
312 | 314 |
|
313 | 315 |
@command(api='cyclades') |
314 | 316 |
class server_firewall(object): |
315 |
"""set the firewall profile"""
|
|
317 |
"""Set the server's firewall profile"""
|
|
316 | 318 |
|
317 | 319 |
def main(self, server_id, profile): |
318 | 320 |
self.client.set_firewall_profile(int(server_id), profile) |
... | ... | |
320 | 322 |
|
321 | 323 |
@command(api='cyclades') |
322 | 324 |
class server_addr(object): |
323 |
"""list server addresses"""
|
|
325 |
"""List a server's addresses"""
|
|
324 | 326 |
|
325 | 327 |
def main(self, server_id, network=None): |
326 | 328 |
reply = self.client.list_server_addresses(int(server_id), network) |
... | ... | |
330 | 332 |
|
331 | 333 |
@command(api='compute') |
332 | 334 |
class server_meta(object): |
333 |
"""get server metadata"""
|
|
335 |
"""Get a server's metadata"""
|
|
334 | 336 |
|
335 | 337 |
def main(self, server_id, key=None): |
336 | 338 |
reply = self.client.get_server_metadata(int(server_id), key) |
... | ... | |
339 | 341 |
|
340 | 342 |
@command(api='compute') |
341 | 343 |
class server_addmeta(object): |
342 |
"""add server metadata"""
|
|
344 |
"""Add server metadata"""
|
|
343 | 345 |
|
344 | 346 |
def main(self, server_id, key, val): |
345 | 347 |
reply = self.client.create_server_metadata(int(server_id), key, val) |
... | ... | |
348 | 350 |
|
349 | 351 |
@command(api='compute') |
350 | 352 |
class server_setmeta(object): |
351 |
"""update server metadata"""
|
|
353 |
"""Update server's metadata"""
|
|
352 | 354 |
|
353 | 355 |
def main(self, server_id, key, val): |
354 | 356 |
metadata = {key: val} |
... | ... | |
358 | 360 |
|
359 | 361 |
@command(api='compute') |
360 | 362 |
class server_delmeta(object): |
361 |
"""delete server metadata"""
|
|
363 |
"""Delete server metadata"""
|
|
362 | 364 |
|
363 | 365 |
def main(self, server_id, key): |
364 | 366 |
self.client.delete_server_metadata(int(server_id), key) |
... | ... | |
366 | 368 |
|
367 | 369 |
@command(api='cyclades') |
368 | 370 |
class server_stats(object): |
369 |
"""get server statistics"""
|
|
371 |
"""Get server statistics"""
|
|
370 | 372 |
|
371 | 373 |
def main(self, server_id): |
372 | 374 |
reply = self.client.get_server_stats(int(server_id)) |
... | ... | |
375 | 377 |
|
376 | 378 |
@command(api='compute') |
377 | 379 |
class flavor_list(object): |
378 |
"""list flavors"""
|
|
380 |
"""List flavors"""
|
|
379 | 381 |
|
380 | 382 |
def update_parser(cls, parser): |
381 | 383 |
parser.add_option('-l', dest='detail', action='store_true', |
... | ... | |
388 | 390 |
|
389 | 391 |
@command(api='compute') |
390 | 392 |
class flavor_info(object): |
391 |
"""get flavor details"""
|
|
393 |
"""Get flavor details"""
|
|
392 | 394 |
|
393 | 395 |
def main(self, flavor_id): |
394 | 396 |
flavor = self.client.get_flavor_details(int(flavor_id)) |
... | ... | |
397 | 399 |
|
398 | 400 |
@command(api='compute') |
399 | 401 |
class image_list(object): |
400 |
"""list images"""
|
|
402 |
"""List images"""
|
|
401 | 403 |
|
402 | 404 |
def update_parser(cls, parser): |
403 | 405 |
parser.add_option('-l', dest='detail', action='store_true', |
... | ... | |
410 | 412 |
|
411 | 413 |
@command(api='compute') |
412 | 414 |
class image_info(object): |
413 |
"""get image details"""
|
|
415 |
"""Get image details"""
|
|
414 | 416 |
|
415 | 417 |
def main(self, image_id): |
416 | 418 |
image = self.client.get_image_details(image_id) |
... | ... | |
419 | 421 |
|
420 | 422 |
@command(api='compute') |
421 | 423 |
class image_delete(object): |
422 |
"""delete image"""
|
|
424 |
"""Delete image"""
|
|
423 | 425 |
|
424 | 426 |
def main(self, image_id): |
425 | 427 |
self.client.delete_image(image_id) |
... | ... | |
427 | 429 |
|
428 | 430 |
@command(api='compute') |
429 | 431 |
class image_meta(object): |
430 |
"""get image metadata"""
|
|
432 |
"""Get image metadata"""
|
|
431 | 433 |
|
432 | 434 |
def main(self, image_id, key=None): |
433 | 435 |
reply = self.client.get_image_metadata(image_id, key) |
... | ... | |
436 | 438 |
|
437 | 439 |
@command(api='compute') |
438 | 440 |
class image_addmeta(object): |
439 |
"""add image metadata"""
|
|
441 |
"""Add image metadata"""
|
|
440 | 442 |
|
441 | 443 |
def main(self, image_id, key, val): |
442 | 444 |
reply = self.client.create_image_metadata(image_id, key, val) |
... | ... | |
445 | 447 |
|
446 | 448 |
@command(api='compute') |
447 | 449 |
class image_setmeta(object): |
448 |
"""update image metadata"""
|
|
450 |
"""Update image metadata"""
|
|
449 | 451 |
|
450 | 452 |
def main(self, image_id, key, val): |
451 | 453 |
metadata = {key: val} |
... | ... | |
455 | 457 |
|
456 | 458 |
@command(api='compute') |
457 | 459 |
class image_delmeta(object): |
458 |
"""delete image metadata"""
|
|
460 |
"""Delete image metadata"""
|
|
459 | 461 |
|
460 | 462 |
def main(self, image_id, key): |
461 | 463 |
self.client.delete_image_metadata(image_id, key) |
... | ... | |
463 | 465 |
|
464 | 466 |
@command(api='cyclades') |
465 | 467 |
class network_list(object): |
466 |
"""list networks"""
|
|
468 |
"""List networks"""
|
|
467 | 469 |
|
468 | 470 |
def update_parser(cls, parser): |
469 | 471 |
parser.add_option('-l', dest='detail', action='store_true', |
... | ... | |
476 | 478 |
|
477 | 479 |
@command(api='cyclades') |
478 | 480 |
class network_create(object): |
479 |
"""create a network"""
|
|
481 |
"""Create a network"""
|
|
480 | 482 |
|
481 | 483 |
def main(self, name): |
482 | 484 |
reply = self.client.create_network(name) |
... | ... | |
485 | 487 |
|
486 | 488 |
@command(api='cyclades') |
487 | 489 |
class network_info(object): |
488 |
"""get network details"""
|
|
490 |
"""Get network details"""
|
|
489 | 491 |
|
490 | 492 |
def main(self, network_id): |
491 | 493 |
network = self.client.get_network_details(network_id) |
... | ... | |
494 | 496 |
|
495 | 497 |
@command(api='cyclades') |
496 | 498 |
class network_rename(object): |
497 |
"""update network name"""
|
|
499 |
"""Update network name"""
|
|
498 | 500 |
|
499 | 501 |
def main(self, network_id, new_name): |
500 | 502 |
self.client.update_network_name(network_id, new_name) |
... | ... | |
502 | 504 |
|
503 | 505 |
@command(api='cyclades') |
504 | 506 |
class network_delete(object): |
505 |
"""delete a network"""
|
|
507 |
"""Delete a network"""
|
|
506 | 508 |
|
507 | 509 |
def main(self, network_id): |
508 | 510 |
self.client.delete_network(network_id) |
... | ... | |
510 | 512 |
|
511 | 513 |
@command(api='cyclades') |
512 | 514 |
class network_connect(object): |
513 |
"""connect a server to a network"""
|
|
515 |
"""Connect a server to a network"""
|
|
514 | 516 |
|
515 | 517 |
def main(self, server_id, network_id): |
516 | 518 |
self.client.connect_server(server_id, network_id) |
... | ... | |
518 | 520 |
|
519 | 521 |
@command(api='cyclades') |
520 | 522 |
class network_disconnect(object): |
521 |
"""disconnect a server from a network"""
|
|
523 |
"""Disconnect a server from a network"""
|
|
522 | 524 |
|
523 | 525 |
def main(self, server_id, network_id): |
524 | 526 |
self.client.disconnect_server(server_id, network_id) |
... | ... | |
526 | 528 |
|
527 | 529 |
@command(api='image') |
528 | 530 |
class glance_list(object): |
529 |
"""list images"""
|
|
531 |
"""List images"""
|
|
530 | 532 |
|
531 | 533 |
def update_parser(cls, parser): |
532 | 534 |
parser.add_option('-l', dest='detail', action='store_true', |
... | ... | |
562 | 564 |
|
563 | 565 |
@command(api='image') |
564 | 566 |
class glance_meta(object): |
565 |
"""get image metadata"""
|
|
567 |
"""Get image metadata"""
|
|
566 | 568 |
|
567 | 569 |
def main(self, image_id): |
568 | 570 |
image = self.client.get_meta(image_id) |
... | ... | |
571 | 573 |
|
572 | 574 |
@command(api='image') |
573 | 575 |
class glance_register(object): |
574 |
"""register an image"""
|
|
576 |
"""Register an image"""
|
|
575 | 577 |
|
576 | 578 |
def update_parser(cls, parser): |
577 | 579 |
parser.add_option('--checksum', dest='checksum', metavar='CHECKSUM', |
... | ... | |
595 | 597 |
def main(self, name, location): |
596 | 598 |
params = {} |
597 | 599 |
for key in ('checksum', 'container_format', 'disk_format', 'id', |
598 |
'owner', 'is_public', 'size'):
|
|
600 |
'owner', 'size'): |
|
599 | 601 |
val = getattr(self.options, key) |
600 | 602 |
if val is not None: |
601 | 603 |
params[key] = val |
602 | 604 |
|
605 |
if self.options.is_public: |
|
606 |
params['is_public'] = 'true' |
|
607 |
|
|
603 | 608 |
properties = {} |
604 | 609 |
for property in self.options.properties or []: |
605 | 610 |
key, sep, val = property.partition('=') |
... | ... | |
613 | 618 |
|
614 | 619 |
@command(api='image') |
615 | 620 |
class glance_members(object): |
616 |
"""get image members"""
|
|
621 |
"""Get image members"""
|
|
617 | 622 |
|
618 | 623 |
def main(self, image_id): |
619 | 624 |
members = self.client.list_members(image_id) |
... | ... | |
623 | 628 |
|
624 | 629 |
@command(api='image') |
625 | 630 |
class glance_shared(object): |
626 |
"""list shared images"""
|
|
631 |
"""List shared images"""
|
|
627 | 632 |
|
628 | 633 |
def main(self, member): |
629 | 634 |
images = self.client.list_shared(member) |
... | ... | |
633 | 638 |
|
634 | 639 |
@command(api='image') |
635 | 640 |
class glance_addmember(object): |
636 |
"""add a member to an image"""
|
|
641 |
"""Add a member to an image"""
|
|
637 | 642 |
|
638 | 643 |
def main(self, image_id, member): |
639 | 644 |
self.client.add_member(image_id, member) |
... | ... | |
641 | 646 |
|
642 | 647 |
@command(api='image') |
643 | 648 |
class glance_delmember(object): |
644 |
"""remove a member from an image"""
|
|
649 |
"""Remove a member from an image"""
|
|
645 | 650 |
|
646 | 651 |
def main(self, image_id, member): |
647 | 652 |
self.client.remove_member(image_id, member) |
... | ... | |
649 | 654 |
|
650 | 655 |
@command(api='image') |
651 | 656 |
class glance_setmembers(object): |
652 |
"""set the members of an image"""
|
|
657 |
"""Set the members of an image"""
|
|
653 | 658 |
|
654 | 659 |
def main(self, image_id, *member): |
655 | 660 |
self.client.set_members(image_id, member) |
... | ... | |
660 | 665 |
|
661 | 666 |
def update_parser(cls, parser): |
662 | 667 |
parser.add_option('--account', dest='account', metavar='NAME', |
663 |
help='use account NAME')
|
|
668 |
help="Specify an account to use")
|
|
664 | 669 |
parser.add_option('--container', dest='container', metavar='NAME', |
665 |
help='use container NAME')
|
|
670 |
help="Specify a container to use")
|
|
666 | 671 |
|
667 |
def main(self): |
|
668 |
self.config.override('storage_account', self.options.account) |
|
669 |
self.config.override('storage_container', self.options.container) |
|
672 |
def progress(self, message): |
|
673 |
"""Return a generator function to be used for progress tracking""" |
|
674 |
|
|
675 |
MESSAGE_LENGTH = 25 |
|
676 |
MAX_PROGRESS_LENGTH = 32 |
|
677 |
|
|
678 |
def progress_gen(n): |
|
679 |
msg = message.ljust(MESSAGE_LENGTH) |
|
680 |
width = min(n, MAX_PROGRESS_LENGTH) |
|
681 |
hide = self.config.get('global', 'silent') or (n < 2) |
|
682 |
for i in progress.bar(range(n), msg, width, hide): |
|
683 |
yield |
|
684 |
yield |
|
670 | 685 |
|
671 |
# Use the more efficient Pithos client if available |
|
672 |
if 'pithos' in self.config.get('apis').split(): |
|
673 |
self.client = clients.PithosClient(self.config) |
|
686 |
return progress_gen |
|
687 |
|
|
688 |
def main(self): |
|
689 |
if self.options.account is not None: |
|
690 |
self.client.account = self.options.account |
|
691 |
if self.options.container is not None: |
|
692 |
self.client.container = self.options.container |
|
674 | 693 |
|
675 | 694 |
|
676 | 695 |
@command(api='storage') |
677 | 696 |
class store_create(object): |
678 |
"""create a container"""
|
|
697 |
"""Create a container"""
|
|
679 | 698 |
|
680 | 699 |
def update_parser(cls, parser): |
681 |
parser.add_option('--account', dest='account', metavar='ACCOUNT',
|
|
682 |
help='use account ACCOUNT')
|
|
700 |
parser.add_option('--account', dest='account', metavar='NAME',
|
|
701 |
help="Specify an account to use")
|
|
683 | 702 |
|
684 | 703 |
def main(self, container): |
685 |
self.config.override('storage_account', self.options.account) |
|
704 |
if self.options.account: |
|
705 |
self.client.account = self.options.account |
|
686 | 706 |
self.client.create_container(container) |
687 | 707 |
|
688 | 708 |
|
689 | 709 |
@command(api='storage') |
690 |
class store_container(store_command):
|
|
691 |
"""get container info"""
|
|
710 |
class store_container(object):
|
|
711 |
"""Get container info"""
|
|
692 | 712 |
|
693 |
def main(self): |
|
694 |
store_command.main(self) |
|
695 |
reply = self.client.get_container_meta() |
|
713 |
def update_parser(cls, parser): |
|
714 |
parser.add_option('--account', dest='account', metavar='NAME', |
|
715 |
help="Specify an account to use") |
|
716 |
|
|
717 |
def main(self, container): |
|
718 |
if self.options.account: |
|
719 |
self.client.account = self.options.account |
|
720 |
reply = self.client.get_container_meta(container) |
|
696 | 721 |
print_dict(reply) |
697 | 722 |
|
698 | 723 |
|
699 | 724 |
@command(api='storage') |
700 | 725 |
class store_upload(store_command): |
701 |
"""upload a file"""
|
|
726 |
"""Upload a file"""
|
|
702 | 727 |
|
703 | 728 |
def main(self, path, remote_path=None): |
704 |
store_command.main(self) |
|
729 |
super(store_upload, self).main() |
|
730 |
|
|
705 | 731 |
if remote_path is None: |
706 | 732 |
remote_path = basename(path) |
707 | 733 |
with open(path) as f: |
708 |
self.client.create_object(remote_path, f) |
|
734 |
hash_cb = self.progress('Calculating block hashes') |
|
735 |
upload_cb = self.progress('Uploading blocks') |
|
736 |
self.client.create_object(remote_path, f, hash_cb=hash_cb, |
|
737 |
upload_cb=upload_cb) |
|
709 | 738 |
|
710 | 739 |
|
711 | 740 |
@command(api='storage') |
712 | 741 |
class store_download(store_command): |
713 |
"""download a file""" |
|
714 |
|
|
715 |
def main(self, remote_path, local_path): |
|
716 |
store_command.main(self) |
|
717 |
f = self.client.get_object(remote_path) |
|
742 |
"""Download a file""" |
|
743 |
|
|
744 |
def main(self, remote_path, local_path='-'): |
|
745 |
super(store_download, self).main() |
|
746 |
|
|
747 |
f, size = self.client.get_object(remote_path) |
|
718 | 748 |
out = open(local_path, 'w') if local_path != '-' else stdout |
719 |
block = 4096 |
|
720 |
data = f.read(block) |
|
749 |
|
|
750 |
blocksize = 4 * 1024**2 |
|
751 |
nblocks = 1 + (size - 1) // blocksize |
|
752 |
|
|
753 |
cb = self.progress('Downloading blocks') if local_path != '-' else None |
|
754 |
if cb: |
|
755 |
gen = cb(nblocks) |
|
756 |
gen.next() |
|
757 |
|
|
758 |
data = f.read(blocksize) |
|
721 | 759 |
while data: |
722 | 760 |
out.write(data) |
723 |
data = f.read(block) |
|
761 |
data = f.read(blocksize) |
|
762 |
if cb: |
|
763 |
gen.next() |
|
724 | 764 |
|
725 | 765 |
|
726 | 766 |
@command(api='storage') |
727 | 767 |
class store_delete(store_command): |
728 |
"""delete a file"""
|
|
768 |
"""Delete a file"""
|
|
729 | 769 |
|
730 | 770 |
def main(self, path): |
731 | 771 |
store_command.main(self) |
... | ... | |
751 | 791 |
puts(columns([name, 12], [cls.description, 60])) |
752 | 792 |
|
753 | 793 |
|
794 |
def add_handler(name, level, prefix=''): |
|
795 |
h = logging.StreamHandler() |
|
796 |
fmt = logging.Formatter(prefix + '%(message)s') |
|
797 |
h.setFormatter(fmt) |
|
798 |
logger = logging.getLogger(name) |
|
799 |
logger.addHandler(h) |
|
800 |
logger.setLevel(level) |
|
801 |
|
|
802 |
|
|
754 | 803 |
def main(): |
755 | 804 |
parser = OptionParser(add_help_option=False) |
756 | 805 |
parser.usage = '%prog <group> <command> [options]' |
... | ... | |
759 | 808 |
help="Show this help message and exit") |
760 | 809 |
parser.add_option('--config', dest='config', metavar='PATH', |
761 | 810 |
help="Specify the path to the configuration file") |
811 |
parser.add_option('-d', '--debug', dest='debug', action='store_true', |
|
812 |
default=False, |
|
813 |
help="Include debug output") |
|
762 | 814 |
parser.add_option('-i', '--include', dest='include', action='store_true', |
763 | 815 |
default=False, |
764 | 816 |
help="Include protocol headers in the output") |
... | ... | |
780 | 832 |
print "kamaki %s" % kamaki.__version__ |
781 | 833 |
exit(0) |
782 | 834 |
|
783 |
if args.contains(['-s', '--silent']): |
|
784 |
level = logging.CRITICAL |
|
785 |
elif args.contains(['-v', '--verbose']): |
|
786 |
level = logging.INFO |
|
787 |
else: |
|
788 |
level = logging.WARNING |
|
789 |
|
|
790 |
logging.basicConfig(level=level, format='%(message)s') |
|
791 |
|
|
792 | 835 |
if '--config' in args: |
793 | 836 |
config_path = args.grouped['--config'].get(0) |
794 | 837 |
else: |
... | ... | |
859 | 902 |
if hasattr(cmd, 'update_parser'): |
860 | 903 |
cmd.update_parser(parser) |
861 | 904 |
|
862 |
if args.contains(['-h', '--help']): |
|
905 |
options, arguments = parser.parse_args(argv) |
|
906 |
|
|
907 |
if options.help: |
|
863 | 908 |
parser.print_help() |
864 | 909 |
exit(0) |
865 | 910 |
|
866 |
cmd.options, cmd.args = parser.parse_args(argv) |
|
911 |
if options.silent: |
|
912 |
add_handler('', logging.CRITICAL) |
|
913 |
elif options.debug: |
|
914 |
add_handler('requests', logging.INFO, prefix='* ') |
|
915 |
add_handler('clients.send', logging.DEBUG, prefix='> ') |
|
916 |
add_handler('clients.recv', logging.DEBUG, prefix='< ') |
|
917 |
elif options.verbose: |
|
918 |
add_handler('requests', logging.INFO, prefix='* ') |
|
919 |
add_handler('clients.send', logging.INFO, prefix='> ') |
|
920 |
add_handler('clients.recv', logging.INFO, prefix='< ') |
|
921 |
elif options.include: |
|
922 |
add_handler('clients.recv', logging.INFO) |
|
923 |
else: |
|
924 |
add_handler('', logging.WARNING) |
|
867 | 925 |
|
868 | 926 |
api = cmd.api |
869 |
if api == 'config': |
|
870 |
cmd.config = config |
|
871 |
elif api in ('compute', 'image', 'storage'): |
|
872 |
token = config.get(api, 'token') or config.get('gobal', 'token') |
|
873 |
url = config.get(api, 'url') |
|
874 |
client_cls = getattr(clients, api) |
|
875 |
kwargs = dict(base_url=url, token=token) |
|
876 |
|
|
877 |
# Special cases |
|
878 |
if api == 'compute' and config.getboolean(api, 'cyclades_extensions'): |
|
879 |
client_cls = clients.cyclades |
|
880 |
elif api == 'storage': |
|
881 |
kwargs['account'] = config.get(api, 'account') |
|
882 |
kwargs['container'] = config.get(api, 'container') |
|
883 |
if config.getboolean(api, 'pithos_extensions'): |
|
884 |
client_cls = clients.pithos |
|
885 |
|
|
886 |
cmd.client = client_cls(**kwargs) |
|
887 |
|
|
927 |
if api in ('compute', 'cyclades'): |
|
928 |
url = config.get('compute', 'url') |
|
929 |
token = config.get('compute', 'token') or config.get('global', 'token') |
|
930 |
if config.getboolean('compute', 'cyclades_extensions'): |
|
931 |
cmd.client = clients.cyclades(url, token) |
|
932 |
else: |
|
933 |
cmd.client = clients.compute(url, token) |
|
934 |
elif api in ('storage', 'pithos'): |
|
935 |
url = config.get('storage', 'url') |
|
936 |
token = config.get('storage', 'token') or config.get('global', 'token') |
|
937 |
account = config.get('storage', 'account') |
|
938 |
container = config.get('storage', 'container') |
|
939 |
if config.getboolean('storage', 'pithos_extensions'): |
|
940 |
cmd.client = clients.pithos(url, token, account, container) |
|
941 |
else: |
|
942 |
cmd.client = clients.storage(url, token, account, container) |
|
943 |
elif api == 'image': |
|
944 |
url = config.get('image', 'url') |
|
945 |
token = config.get('image', 'token') or config.get('global', 'token') |
|
946 |
cmd.client = clients.image(url, token) |
|
947 |
|
|
948 |
cmd.options = options |
|
949 |
cmd.config = config |
|
950 |
|
|
888 | 951 |
try: |
889 |
ret = cmd.main(*args.grouped['_'][2:])
|
|
952 |
ret = cmd.main(*arguments[3:])
|
|
890 | 953 |
exit(ret) |
891 | 954 |
except TypeError as e: |
892 | 955 |
if e.args and e.args[0].startswith('main()'): |
... | ... | |
894 | 957 |
exit(1) |
895 | 958 |
else: |
896 | 959 |
raise |
897 |
except clients.ClientError, err: |
|
898 |
log.error('%s', err.message) |
|
899 |
log.info('%s', err.details) |
|
960 |
except clients.ClientError as err: |
|
961 |
if err.status == 404: |
|
962 |
color = yellow |
|
963 |
elif 500 <= err.status < 600: |
|
964 |
color = magenta |
|
965 |
else: |
|
966 |
color = red |
|
967 |
|
|
968 |
puts_err(color(err.message)) |
|
969 |
if err.details and (options.verbose or options.debug): |
|
970 |
puts_err(err.details) |
|
900 | 971 |
exit(2) |
972 |
except ConnectionError as err: |
|
973 |
puts_err(red("Connection error")) |
|
974 |
exit(1) |
|
901 | 975 |
|
902 | 976 |
|
903 | 977 |
if __name__ == '__main__': |
b/kamaki/clients/__init__.py | ||
---|---|---|
31 | 31 |
# interpreted as representing official policies, either expressed |
32 | 32 |
# or implied, of GRNET S.A. |
33 | 33 |
|
34 |
import json |
|
35 |
import logging |
|
36 |
|
|
37 |
import requests |
|
38 |
|
|
39 |
from requests.auth import AuthBase |
|
40 |
|
|
41 |
|
|
42 |
sendlog = logging.getLogger('clients.send') |
|
43 |
recvlog = logging.getLogger('clients.recv') |
|
44 |
|
|
45 |
|
|
46 |
# Add a convenience json property to the responses |
|
47 |
def _json(self): |
|
48 |
try: |
|
49 |
return json.loads(self.content) |
|
50 |
except ValueError: |
|
51 |
raise ClientError("Invalid JSON reply", self.status_code) |
|
52 |
requests.Response.json = property(_json) |
|
53 |
|
|
54 |
# Add a convenience status property to the responses |
|
55 |
def _status(self): |
|
56 |
return requests.status_codes._codes[self.status_code][0].upper() |
|
57 |
requests.Response.status = property(_status) |
|
58 |
|
|
59 |
|
|
60 |
class XAuthTokenAuth(AuthBase): |
|
61 |
def __init__(self, token): |
|
62 |
self.token = token |
|
63 |
|
|
64 |
def __call__(self, r): |
|
65 |
r.headers['X-Auth-Token'] = self.token |
|
66 |
return r |
|
67 |
|
|
68 |
|
|
34 | 69 |
class ClientError(Exception): |
35 | 70 |
def __init__(self, message, status=0, details=''): |
36 | 71 |
self.message = message |
37 | 72 |
self.status = status |
38 | 73 |
self.details = details |
39 | 74 |
|
40 |
def __int__(self): |
|
41 |
return int(self.status) |
|
42 | 75 |
|
43 |
def __str__(self): |
|
44 |
r = self.message |
|
45 |
if self.status: |
|
46 |
r += "\nHTTP Status: %d" % self.status |
|
47 |
if self.details: |
|
48 |
r += "\nDetails: \n%s" % self.details |
|
76 |
class Client(object): |
|
77 |
def __init__(self, base_url, token, include=False, verbose=False): |
|
78 |
self.base_url = base_url |
|
79 |
self.auth = XAuthTokenAuth(token) |
|
80 |
self.include = include |
|
81 |
self.verbose = verbose |
|
82 |
|
|
83 |
def raise_for_status(self, r): |
|
84 |
if 400 <= r.status_code < 500: |
|
85 |
message, sep, details = r.text.partition('\n') |
|
86 |
elif 500 <= r.status_code < 600: |
|
87 |
message = '%d Server Error' % (r.status_code,) |
|
88 |
details = r.text |
|
89 |
else: |
|
90 |
message = '%d Unknown Error' % (r.status_code,) |
|
91 |
details = r.text |
|
92 |
|
|
93 |
message = message or "HTTP Error %d" % (r.status_code,) |
|
94 |
raise ClientError(message, r.status_code, details) |
|
95 |
|
|
96 |
def request(self, method, path, **kwargs): |
|
97 |
raw = kwargs.pop('raw', False) |
|
98 |
success = kwargs.pop('success', 200) |
|
99 |
if 'json' in kwargs: |
|
100 |
data = json.dumps(kwargs.pop('json')) |
|
101 |
kwargs['data'] = data |
|
102 |
headers = kwargs.setdefault('headers', {}) |
|
103 |
headers['content-type'] = 'application/json' |
|
104 |
|
|
105 |
url = self.base_url + path |
|
106 |
kwargs.setdefault('auth', self.auth) |
|
107 |
r = requests.request(method, url, **kwargs) |
|
108 |
|
|
109 |
req = r.request |
|
110 |
sendlog.info('%s %s', req.method, req.url) |
|
111 |
for key, val in req.headers.items(): |
|
112 |
sendlog.info('%s: %s', key, val) |
|
113 |
sendlog.info('') |
|
114 |
if req.data: |
|
115 |
sendlog.info('%s', req.data) |
|
116 |
|
|
117 |
recvlog.info('%d %s', r.status_code, r.status) |
|
118 |
for key, val in r.headers.items(): |
|
119 |
recvlog.info('%s: %s', key, val) |
|
120 |
recvlog.info('') |
|
121 |
if not raw and r.text: |
|
122 |
recvlog.debug(r.text) |
|
123 |
|
|
124 |
if success is not None: |
|
125 |
# Success can either be an in or a collection |
|
126 |
success = (success,) if isinstance(success, int) else success |
|
127 |
if r.status_code not in success: |
|
128 |
self.raise_for_status(r) |
|
129 |
|
|
49 | 130 |
return r |
50 | 131 |
|
132 |
def delete(self, path, **kwargs): |
|
133 |
return self.request('delete', path, **kwargs) |
|
134 |
|
|
135 |
def get(self, path, **kwargs): |
|
136 |
return self.request('get', path, **kwargs) |
|
137 |
|
|
138 |
def head(self, path, **kwargs): |
|
139 |
return self.request('head', path, **kwargs) |
|
140 |
|
|
141 |
def post(self, path, **kwargs): |
|
142 |
return self.request('post', path, **kwargs) |
|
143 |
|
|
144 |
def put(self, path, **kwargs): |
|
145 |
return self.request('put', path, **kwargs) |
|
146 |
|
|
51 | 147 |
|
52 |
from .compute import ComputeClient |
|
53 |
from .image import ImageClient |
|
54 |
from .storage import StorageClient |
|
55 |
from .cyclades import CycladesClient |
|
56 |
from .pithos import PithosClient |
|
148 |
from .compute import ComputeClient as compute |
|
149 |
from .image import ImageClient as image |
|
150 |
from .storage import StorageClient as storage |
|
151 |
from .cyclades import CycladesClient as cyclades |
|
152 |
from .pithos import PithosClient as pithos |
b/kamaki/clients/compute.py | ||
---|---|---|
31 | 31 |
# interpreted as representing official policies, either expressed |
32 | 32 |
# or implied, of GRNET S.A. |
33 | 33 |
|
34 |
import json
|
|
34 |
from . import Client, ClientError
|
|
35 | 35 |
|
36 |
from . import ClientError |
|
37 |
from .http import HTTPClient |
|
38 | 36 |
|
39 |
|
|
40 |
class ComputeClient(HTTPClient): |
|
37 |
class ComputeClient(Client): |
|
41 | 38 |
"""OpenStack Compute API 1.1 client""" |
42 | 39 |
|
43 |
@property |
|
44 |
def url(self): |
|
45 |
url = self.config.get('compute_url') or self.config.get('url') |
|
46 |
if not url: |
|
47 |
raise ClientError('No URL was given') |
|
48 |
return url |
|
49 |
|
|
50 |
@property |
|
51 |
def token(self): |
|
52 |
token = self.config.get('compute_token') or self.config.get('token') |
|
53 |
if not token: |
|
54 |
raise ClientError('No token was given') |
|
55 |
return token |
|
40 |
def raise_for_status(self, r): |
|
41 |
d = r.json |
|
42 |
key = d.keys()[0] |
|
43 |
val = d[key] |
|
44 |
message = '%s: %s' % (key, val.get('message', '')) |
|
45 |
details = val.get('details', '') |
|
46 |
raise ClientError(message, r.status_code, details) |
|
56 | 47 |
|
57 | 48 |
def list_servers(self, detail=False): |
58 | 49 |
"""List servers, returned detailed output if detailed is True""" |
50 |
|
|
59 | 51 |
path = '/servers/detail' if detail else '/servers' |
60 |
reply = self.http_get(path)
|
|
61 |
return reply['servers']['values']
|
|
52 |
r = self.get(path, success=200)
|
|
53 |
return r.json['servers']['values']
|
|
62 | 54 |
|
63 | 55 |
def get_server_details(self, server_id): |
64 | 56 |
"""Return detailed output on a server specified by its id""" |
65 |
path = '/servers/%d' % server_id |
|
66 |
reply = self.http_get(path) |
|
67 |
return reply['server'] |
|
57 |
|
|
58 |
path = '/servers/%s' % (server_id,) |
|
59 |
r = self.get(path, success=200) |
|
60 |
return r.json['server'] |
|
68 | 61 |
|
69 | 62 |
def create_server(self, name, flavor_id, image_id, personality=None): |
70 | 63 |
"""Submit request to create a new server |
... | ... | |
78 | 71 |
|
79 | 72 |
The call returns a dictionary describing the newly created server. |
80 | 73 |
""" |
81 |
req = {'name': name, 'flavorRef': flavor_id, 'imageRef': image_id} |
|
74 |
req = {'server': {'name': name, |
|
75 |
'flavorRef': flavor_id, |
|
76 |
'imageRef': image_id}} |
|
82 | 77 |
if personality: |
83 | 78 |
req['personality'] = personality |
84 | 79 |
|
85 |
body = json.dumps({'server': req}) |
|
86 |
reply = self.http_post('/servers', body) |
|
87 |
return reply['server'] |
|
80 |
r = self.post('/servers', json=req, success=202) |
|
81 |
return r.json['server'] |
|
88 | 82 |
|
89 | 83 |
def update_server_name(self, server_id, new_name): |
90 | 84 |
"""Update the name of the server as reported by the API. |
... | ... | |
92 | 86 |
This call does not modify the hostname actually used by the server |
93 | 87 |
internally. |
94 | 88 |
""" |
95 |
path = '/servers/%d' % server_id
|
|
96 |
body = json.dumps({'server': {'name': new_name}})
|
|
97 |
self.http_put(path, body)
|
|
89 |
path = '/servers/%s' % (server_id,)
|
|
90 |
req = {'server': {'name': new_name}}
|
|
91 |
self.put(path, json=req, success=204)
|
|
98 | 92 |
|
99 | 93 |
def delete_server(self, server_id): |
100 | 94 |
"""Submit a deletion request for a server specified by id""" |
101 |
path = '/servers/%d' % server_id |
|
102 |
self.http_delete(path) |
|
95 |
|
|
96 |
path = '/servers/%s' % (server_id,) |
|
97 |
self.delete(path, success=204) |
|
103 | 98 |
|
104 | 99 |
def reboot_server(self, server_id, hard=False): |
105 | 100 |
"""Submit a reboot request for a server specified by id""" |
106 |
path = '/servers/%d/action' % server_id |
|
107 |
type = 'HARD' if hard else 'SOFT' |
|
108 |
body = json.dumps({'reboot': {'type': type}}) |
|
109 |
self.http_post(path, body) |
|
110 | 101 |
|
102 |
path = '/servers/%s/action' % (server_id,) |
|
103 |
type = 'HARD' if hard else 'SOFT' |
|
104 |
req = {'reboot': {'type': type}} |
|
105 |
self.post(path, json=req, success=202) |
|
106 |
|
|
111 | 107 |
def get_server_metadata(self, server_id, key=None): |
112 |
path = '/servers/%d/meta' % server_id
|
|
108 |
path = '/servers/%s/meta' % (server_id,)
|
|
113 | 109 |
if key: |
114 | 110 |
path += '/%s' % key |
115 |
reply = self.http_get(path)
|
|
116 |
return reply['meta'] if key else reply['metadata']['values']
|
|
111 |
r = self.get(path, success=200)
|
|
112 |
return r.json['meta'] if key else r.json['metadata']['values']
|
|
117 | 113 |
|
118 | 114 |
def create_server_metadata(self, server_id, key, val): |
119 | 115 |
path = '/servers/%d/meta/%s' % (server_id, key) |
120 |
body = json.dumps({'meta': {key: val}})
|
|
121 |
reply = self.http_put(path, body, success=201)
|
|
122 |
return reply['meta']
|
|
116 |
req = {'meta': {key: val}}
|
|
117 |
r = self.put(path, json=req, success=201)
|
|
118 |
return r.json['meta']
|
|
123 | 119 |
|
124 | 120 |
def update_server_metadata(self, server_id, **metadata): |
125 |
path = '/servers/%d/meta' % server_id
|
|
126 |
body = json.dumps({'metadata': metadata})
|
|
127 |
reply = self.http_post(path, body, success=201)
|
|
128 |
return reply['metadata']
|
|
121 |
path = '/servers/%d/meta' % (server_id,)
|
|
122 |
req = {'metadata': metadata}
|
|
123 |
r = self.post(path, json=req, success=201)
|
|
124 |
return r.json['metadata']
|
|
129 | 125 |
|
130 | 126 |
def delete_server_metadata(self, server_id, key): |
131 | 127 |
path = '/servers/%d/meta/%s' % (server_id, key) |
132 |
reply = self.http_delete(path)
|
|
133 |
|
|
134 |
|
|
128 |
self.delete(path, success=204)
|
|
129 |
|
|
130 |
|
|
135 | 131 |
def list_flavors(self, detail=False): |
136 | 132 |
path = '/flavors/detail' if detail else '/flavors' |
137 |
reply = self.http_get(path)
|
|
138 |
return reply['flavors']['values']
|
|
133 |
r = self.get(path, success=200)
|
|
134 |
return r.json['flavors']['values']
|
|
139 | 135 |
|
140 | 136 |
def get_flavor_details(self, flavor_id): |
141 | 137 |
path = '/flavors/%d' % flavor_id |
142 |
reply = self.http_get(path)
|
|
143 |
return reply['flavor']
|
|
138 |
r = self.get(path, success=200)
|
|
139 |
return r.json['flavor']
|
|
144 | 140 |
|
145 | 141 |
|
146 | 142 |
def list_images(self, detail=False): |
147 | 143 |
path = '/images/detail' if detail else '/images' |
148 |
reply = self.http_get(path)
|
|
149 |
return reply['images']['values']
|
|
144 |
r = self.get(path, success=200)
|
|
145 |
return r.json['images']['values']
|
|
150 | 146 |
|
151 | 147 |
def get_image_details(self, image_id): |
152 |
path = '/images/%s' % image_id
|
|
153 |
reply = self.http_get(path)
|
|
154 |
return reply['image']
|
|
148 |
path = '/images/%s' % (image_id,)
|
|
149 |
r = self.get(path, success=200)
|
|
150 |
return r.json['image']
|
|
155 | 151 |
|
156 | 152 |
def delete_image(self, image_id): |
157 |
path = '/images/%s' % image_id
|
|
158 |
self.http_delete(path)
|
|
153 |
path = '/images/%s' % (image_id,)
|
|
154 |
self.delete(path, success=204)
|
|
159 | 155 |
|
160 | 156 |
def get_image_metadata(self, image_id, key=None): |
161 |
path = '/images/%s/meta' % image_id
|
|
157 |
path = '/images/%s/meta' % (image_id,)
|
|
162 | 158 |
if key: |
163 | 159 |
path += '/%s' % key |
164 |
reply = self.http_get(path)
|
|
165 |
return reply['meta'] if key else reply['metadata']['values']
|
|
160 |
r = self.get(path, success=200)
|
|
161 |
return r.json['meta'] if key else r.json['metadata']['values']
|
|
166 | 162 |
|
167 | 163 |
def create_image_metadata(self, image_id, key, val): |
168 | 164 |
path = '/images/%s/meta/%s' % (image_id, key) |
169 |
body = json.dumps({'meta': {key: val}})
|
|
170 |
reply = self.http_put(path, body, success=201)
|
|
171 |
return reply['meta']
|
|
165 |
req = {'meta': {key: val}}
|
|
166 |
r = self.put(path, json=req, success=201)
|
|
167 |
return r.json['meta']
|
|
172 | 168 |
|
173 | 169 |
def update_image_metadata(self, image_id, **metadata): |
174 |
path = '/images/%s/meta' % image_id
|
|
175 |
body = json.dumps({'metadata': metadata})
|
|
176 |
reply = self.http_post(path, body, success=201)
|
|
177 |
return reply['metadata']
|
|
170 |
path = '/images/%s/meta' % (image_id,)
|
|
171 |
req = {'metadata': metadata}
|
|
172 |
r = self.post(path, json=req, success=201)
|
|
173 |
return r.json['metadata']
|
|
178 | 174 |
|
179 | 175 |
def delete_image_metadata(self, image_id, key): |
180 | 176 |
path = '/images/%s/meta/%s' % (image_id, key) |
181 |
self.http_delete(path) |
|
177 |
self.delete(path, success=204) |
b/kamaki/clients/cyclades.py | ||
---|---|---|
31 | 31 |
# interpreted as representing official policies, either expressed |
32 | 32 |
# or implied, of GRNET S.A. |
33 | 33 |
|
34 |
import json |
|
35 |
|
|
36 | 34 |
from .compute import ComputeClient |
37 | 35 |
|
38 | 36 |
|
... | ... | |
41 | 39 |
|
42 | 40 |
def start_server(self, server_id): |
43 | 41 |
"""Submit a startup request for a server specified by id""" |
44 |
path = '/servers/%d/action' % server_id |
|
45 |
body = json.dumps({'start': {}}) |
|
46 |
self.http_post(path, body) |
|
42 |
|
|
43 |
path = '/servers/%s/action' % (server_id,) |
|
44 |
req = {'start': {}} |
|
45 |
self.post(path, json=req, success=202) |
|
47 | 46 |
|
48 | 47 |
def shutdown_server(self, server_id): |
49 | 48 |
"""Submit a shutdown request for a server specified by id""" |
50 |
path = '/servers/%d/action' % server_id |
|
51 |
body = json.dumps({'shutdown': {}}) |
|
52 |
self.http_post(path, body) |
|
49 |
|
|
50 |
path = '/servers/%s/action' % (server_id,) |
|
51 |
req = {'shutdown': {}} |
|
52 |
self.post(path, json=req, success=202) |
|
53 | 53 |
|
54 | 54 |
def get_server_console(self, server_id): |
55 | 55 |
"""Get a VNC connection to the console of a server specified by id""" |
56 |
path = '/servers/%d/action' % server_id |
|
57 |
body = json.dumps({'console': {'type': 'vnc'}}) |
|
58 |
reply = self.http_post(path, body, success=200) |
|
59 |
return reply['console'] |
|
56 |
|
|
57 |
path = '/servers/%s/action' % (server_id,) |
|
58 |
req = {'console': {'type': 'vnc'}} |
|
59 |
r = self.post(path, json=req, success=200) |
|
60 |
return r.json['console'] |
|
60 | 61 |
|
61 | 62 |
def set_firewall_profile(self, server_id, profile): |
62 | 63 |
"""Set the firewall profile for the public interface of a server |
... | ... | |
64 | 65 |
The server is specified by id, the profile argument |
65 | 66 |
is one of (ENABLED, DISABLED, PROTECTED). |
66 | 67 |
""" |
67 |
path = '/servers/%d/action' % server_id
|
|
68 |
body = json.dumps({'firewallProfile': {'profile': profile}})
|
|
69 |
self.http_post(path, body)
|
|
68 |
path = '/servers/%s/action' % (server_id,)
|
|
69 |
req = {'firewallProfile': {'profile': profile}}
|
|
70 |
self.post(path, json=req, success=202)
|
|
70 | 71 |
|
71 | 72 |
def list_server_addresses(self, server_id, network=None): |
72 |
path = '/servers/%d/ips' % server_id
|
|
73 |
path = '/servers/%s/ips' % (server_id,)
|
|
73 | 74 |
if network: |
74 | 75 |
path += '/%s' % network |
75 |
reply = self.http_get(path) |
|
76 |
return [reply['network']] if network else reply['addresses']['values'] |
|
76 |
r = self.get(path, success=200) |
|
77 |
if network: |
|
78 |
return [r.json['network']] |
|
79 |
else: |
|
80 |
return r.json['addresses']['values'] |
|
77 | 81 |
|
78 | 82 |
def get_server_stats(self, server_id): |
79 |
path = '/servers/%d/stats' % server_id
|
|
80 |
reply = self.http_get(path)
|
|
81 |
return reply['stats']
|
|
83 |
path = '/servers/%s/stats' % (server_id,)
|
|
84 |
r = self.get(path, success=200)
|
|
85 |
return r.json['stats']
|
|
82 | 86 |
|
83 | 87 |
|
84 | 88 |
def list_networks(self, detail=False): |
85 | 89 |
path = '/networks/detail' if detail else '/networks' |
86 |
reply = self.http_get(path)
|
|
87 |
return reply['networks']['values']
|
|
90 |
r = self.get(path, success=200)
|
|
91 |
return r.json['networks']['values']
|
|
88 | 92 |
|
89 | 93 |
def create_network(self, name): |
90 |
body = json.dumps({'network': {'name': name}})
|
|
91 |
reply = self.http_post('/networks', body)
|
|
92 |
return reply['network']
|
|
94 |
req = {'network': {'name': name}}
|
|
95 |
r = self.post('/networks', json=req, success=202)
|
|
96 |
return r.json['network']
|
|
93 | 97 |
|
94 | 98 |
def get_network_details(self, network_id): |
95 |
path = '/networks/%s' % network_id
|
|
96 |
reply = self.http_get(path)
|
|
97 |
return reply['network']
|
|
99 |
path = '/networks/%s' % (network_id,)
|
|
100 |
r = self.get(path, success=200)
|
|
101 |
return r.json['network']
|
|
98 | 102 |
|
99 | 103 |
def update_network_name(self, network_id, new_name): |
100 |
path = '/networks/%s' % network_id
|
|
101 |
body = json.dumps({'network': {'name': new_name}})
|
|
102 |
self.http_put(path, body)
|
|
104 |
path = '/networks/%s' % (network_id,)
|
|
105 |
req = {'network': {'name': new_name}}
|
|
106 |
self.put(path, json=req, success=204)
|
|
103 | 107 |
|
104 | 108 |
def delete_network(self, network_id): |
105 |
path = '/networks/%s' % network_id
|
|
106 |
self.http_delete(path)
|
|
109 |
path = '/networks/%s' % (network_id,)
|
|
110 |
self.delete(path, success=204)
|
|
107 | 111 |
|
108 | 112 |
def connect_server(self, server_id, network_id): |
109 |
path = '/networks/%s/action' % network_id
|
|
110 |
body = json.dumps({'add': {'serverRef': server_id}})
|
|
111 |
self.http_post(path, body)
|
|
113 |
path = '/networks/%s/action' % (network_id,)
|
|
114 |
req = {'add': {'serverRef': server_id}}
|
|
115 |
self.post(path, json=req, success=202)
|
|
112 | 116 |
|
113 | 117 |
def disconnect_server(self, server_id, network_id): |
114 |
path = '/networks/%s/action' % network_id |
|
115 |
body = json.dumps({'remove': {'serverRef': server_id}}) |
|
116 |
self.http_post(path, body) |
|
118 |
path = '/networks/%s/action' % (network_id,) |
|
119 |
req = {'remove': {'serverRef': server_id}} |
|
120 |
self.post(path, json=req, success=202) |
b/kamaki/clients/pithos.py | ||
---|---|---|
32 | 32 |
# or implied, of GRNET S.A. |
33 | 33 |
|
34 | 34 |
import hashlib |
35 |
import json
|
|
35 |
import os
|
|
36 | 36 |
|
37 |
from . import ClientError |
|
38 |
from .storage import StorageClient |
|
39 | 37 |
from ..utils import OrderedDict |
40 | 38 |
|
39 |
from .storage import StorageClient |
|
40 |
|
|
41 |
|
|
42 |
def pithos_hash(block, blockhash): |
|
43 |
h = hashlib.new(blockhash) |
|
44 |
h.update(block.rstrip('\x00')) |
|
45 |
return h.hexdigest() |
|
46 |
|
|
41 | 47 |
|
42 | 48 |
class PithosClient(StorageClient): |
43 | 49 |
"""GRNet Pithos API client""" |
44 | 50 |
|
45 | 51 |
def put_block(self, data, hash): |
46 |
path = '/%s/%s?update' % (self.account, self.container) |
|
52 |
path = '/%s/%s' % (self.account, self.container) |
|
53 |
params = {'update': ''} |
|
47 | 54 |
headers = {'Content-Type': 'application/octet-stream', |
48 |
'Content-Length': len(data)}
|
|
49 |
resp, reply = self.raw_http_cmd('POST', path, data, headers,
|
|
50 |
success=202)
|
|
51 |
assert reply.strip() == hash, 'Local hash does not match server'
|
|
55 |
'Content-Length': str(len(data))}
|
|
56 |
r = self.post(path, params=params, data=data, headers=headers,
|
|
57 |
success=202) |
|
58 |
assert r.text.strip() == hash, 'Local hash does not match server'
|
|
52 | 59 |
|
53 |
def create_object(self, object, f): |
|
54 |
meta = self.get_container_meta() |
|
60 |
def create_object(self, object, f, hash_cb=None, upload_cb=None): |
|
61 |
"""Create an object by uploading only the missing blocks |
|
62 |
|
|
63 |
hash_cb is a generator function taking the total number of blocks to |
|
64 |
be hashed as an argument. Its next() will be called every time a block |
|
65 |
is hashed. |
|
66 |
|
|
67 |
upload_cb is a generator function with the same properties that is |
|
68 |
called every time a block is uploaded. |
|
69 |
""" |
|
70 |
self.assert_container() |
|
71 |
|
|
72 |
meta = self.get_container_meta(self.container) |
|
55 | 73 |
blocksize = int(meta['block-size']) |
56 | 74 |
blockhash = meta['block-hash'] |
57 | 75 |
|
58 |
size = 0 |
|
76 |
file_size = os.fstat(f.fileno()).st_size |
|
77 |
nblocks = 1 + (file_size - 1) // blocksize |
|
59 | 78 |
hashes = OrderedDict() |
60 |
data = f.read(blocksize) |
|
61 |
while data: |
|
62 |
bytes = len(data) |
|
63 |
h = hashlib.new(blockhash) |
|
64 |
h.update(data.rstrip('\x00')) |
|
65 |
hash = h.hexdigest() |
|
79 |
|
|
80 |
size = 0 |
|
81 |
|
|
82 |
if hash_cb: |
|
83 |
hash_gen = hash_cb(nblocks) |
|
84 |
hash_gen.next() |
|
85 |
for i in range(nblocks): |
|
86 |
block = f.read(blocksize) |
|
87 |
bytes = len(block) |
|
88 |
hash = pithos_hash(block, blockhash) |
|
66 | 89 |
hashes[hash] = (size, bytes) |
67 | 90 |
size += bytes |
68 |
data = f.read(blocksize) |
|
91 |
if hash_cb: |
|
92 |
hash_gen.next() |
|
93 |
|
|
94 |
assert size == file_size |
|
69 | 95 |
|
70 |
path = '/%s/%s/%s?hashmap&format=json' % (self.account, self.container,
|
|
71 |
object)
|
|
96 |
path = '/%s/%s/%s' % (self.account, self.container, object)
|
|
97 |
params = {'hashmap': '', 'format': 'json'}
|
|
72 | 98 |
hashmap = dict(bytes=size, hashes=hashes.keys()) |
73 |
req = json.dumps(hashmap) |
|
74 |
resp, reply = self.raw_http_cmd('PUT', path, req, success=None) |
|
75 |
|
|
76 |
if resp.status not in (201, 409): |
|
77 |
raise ClientError('Invalid response from the server') |
|
99 |
r = self.put(path, params=params, json=hashmap, success=(201, 409)) |
|
78 | 100 |
|
79 |
if resp.status == 201:
|
|
101 |
if r.status_code == 201:
|
|
80 | 102 |
return |
81 | 103 |
|
82 |
missing = json.loads(reply)
|
|
104 |
missing = r.json
|
|
83 | 105 |
|
106 |
if upload_cb: |
|
107 |
upload_gen = upload_cb(len(missing)) |
|
108 |
upload_gen.next() |
|
84 | 109 |
for hash in missing: |
85 | 110 |
offset, bytes = hashes[hash] |
86 | 111 |
f.seek(offset) |
87 | 112 |
data = f.read(bytes) |
88 | 113 |
self.put_block(data, hash) |
114 |
if upload_cb: |
|
115 |
upload_gen.next() |
|
89 | 116 |
|
90 |
self.http_put(path, req, success=201) |
|
117 |
self.put(path, params=params, json=hashmap, success=201) |
b/kamaki/clients/storage.py | ||
---|---|---|
31 | 31 |
# interpreted as representing official policies, either expressed |
32 | 32 |
# or implied, of GRNET S.A. |
33 | 33 |
|
34 |
from . import ClientError |
|
35 |
from .http import HTTPClient |
|
34 |
from . import Client, ClientError |
|
36 | 35 |
|
37 | 36 |
|
38 |
class StorageClient(HTTPClient):
|
|
37 |
class StorageClient(Client): |
|
39 | 38 |
"""OpenStack Object Storage API 1.0 client""" |
40 | 39 |
|
41 |
@property |
|
42 |
def url(self): |
|
43 |
url = self.config.get('storage_url') or self.config.get('url') |
|
44 |
if not url: |
|
45 |
raise ClientError('No URL was given') |
|
46 |
return url |
|
47 |
|
|
48 |
@property |
|
49 |
def token(self): |
|
50 |
token = self.config.get('storage_token') or self.config.get('token') |
|
51 |
if not token: |
|
52 |
raise ClientError('No token was given') |
|
53 |
return token |
|
40 |
def __init__(self, base_url, token, account=None, container=None): |
|
41 |
super(StorageClient, self).__init__(base_url, token) |
|
42 |
self.account = account |
|
43 |
self.container = container |
|
54 | 44 |
|
55 |
@property |
|
56 |
def account(self): |
|
57 |
account = self.config.get('storage_account') |
|
58 |
if not account: |
|
59 |
raise ClientError('No account was given') |
|
60 |
return account |
|
45 |
def assert_account(self): |
|
46 |
if not self.account: |
|
47 |
raise ClientError("Please provide an account") |
|
61 | 48 |
|
62 |
@property |
|
63 |
def container(self): |
|
64 |
container = self.config.get('storage_container') |
|
65 |
if not container: |
|
66 |
raise ClientError('No container was given') |
|
67 |
return container |
|
49 |
def assert_container(self): |
|
50 |
self.assert_account() |
|
51 |
if not self.container: |
|
52 |
raise ClientError("Please provide a container") |
|
68 | 53 |
|
69 | 54 |
def create_container(self, container): |
55 |
self.assert_account() |
|
70 | 56 |
path = '/%s/%s' % (self.account, container) |
71 |
self.http_put(path, success=201) |
|
57 |
r = self.put(path, success=(201, 202)) |
|
58 |
if r.status_code == 202: |
|
59 |
raise ClientError("Container already exists") |
|
72 | 60 |
|
73 |
def get_container_meta(self): |
|
74 |
path = '/%s/%s' % (self.account, self.container) |
|
75 |
resp, reply = self.raw_http_cmd('HEAD', path, success=204) |
|
61 |
def get_container_meta(self, container): |
|
62 |
self.assert_account() |
|
63 |
path = '/%s/%s' % (self.account, container) |
|
64 |
r = self.head(path, success=(204, 404)) |
|
65 |
if r.status_code == 404: |
|
66 |
raise ClientError("Container does not exist", r.status_code) |
|
67 |
|
|
76 | 68 |
reply = {} |
77 | 69 |
prefix = 'x-container-' |
78 |
for key, val in resp.getheaders():
|
|
70 |
for key, val in r.headers.items():
|
|
79 | 71 |
key = key.lower() |
80 | 72 |
if key.startswith(prefix): |
81 | 73 |
reply[key[len(prefix):]] = val |
74 |
|
|
82 | 75 |
return reply |
83 | 76 |
|
84 |
def create_object(self, object, f): |
|
77 |
def create_object(self, object, f, hash_cb=None, upload_cb=None): |
|
78 |
# This is a naive implementation, it loads the whole file in memory |
|
79 |
self.assert_container() |
|
85 | 80 |
path = '/%s/%s/%s' % (self.account, self.container, object) |
86 | 81 |
data = f.read() |
87 |
self.http_put(path, data, success=201)
|
|
88 |
|
|
82 |
self.put(path, data=data, success=201)
|
|
83 |
|
|
89 | 84 |
def get_object(self, object): |
85 |
self.assert_container() |
|
90 | 86 |
path = '/%s/%s/%s' % (self.account, self.container, object) |
91 |
resp, reply = self.raw_http_cmd('GET', path, success=200,
|
|
92 |
skip_read=True)
|
|
93 |
return resp.fp
|
|
94 |
|
|
87 |
r = self.get(path, raw=True)
|
|
88 |
size = int(r.headers['content-length'])
|
|
89 |
return r.raw, size
|
|
90 |
|
|
95 | 91 |
def delete_object(self, object): |
92 |
self.assert_container() |
|
96 | 93 |
path = '/%s/%s/%s' % (self.account, self.container, object) |
97 |
self.http_delete(path) |
|
94 |
self.delete(path, success=204) |
b/setup.py | ||
---|---|---|
51 | 51 |
'console_scripts': ['kamaki = kamaki.cli:main'] |
52 | 52 |
}, |
53 | 53 |
install_requires=[ |
54 |
'requests>=0.10.2', |
|
54 | 55 |
'clint>=0.3' |
55 | 56 |
] |
56 | 57 |
) |
Also available in: Unified diff