Revision df79206f kamaki/cli.py

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__':

Also available in: Unified diff