Revision cae67d7b

b/kamaki/__init__.py
31 31
# interpreted as representing official policies, either expressed
32 32
# or implied, of GRNET S.A.
33 33

  
34
__version__ = '0.3'
34
__version__ = '0.4'
b/kamaki/cli.py
73 73
from base64 import b64encode
74 74
from grp import getgrgid
75 75
from optparse import OptionParser
76
from os.path import abspath, basename, exists
76
from os.path import abspath, basename, exists, expanduser
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
81
from clint.textui.cols import columns
82

  
80 83
from kamaki import clients
81
from kamaki.config import Config, ConfigError
84
from kamaki.config import Config
82 85
from kamaki.utils import OrderedDict, print_addresses, print_dict, print_items
83 86

  
84 87

  
85
log = logging.getLogger('kamaki')
88
# Path to the file that stores the configuration
89
CONFIG_PATH = expanduser('~/.kamakirc')
90

  
91
# Name of a shell variable to bypass the CONFIG_PATH value
92
CONFIG_ENV = 'KAMAKI_CONFIG'
93

  
86 94

  
87 95
_commands = OrderedDict()
88 96

  
89 97

  
90
def command(api=None, group=None, name=None, description=None, syntax=None):
98
GROUPS = {
99
    'config': "Configuration commands",
100
    'server': "Compute API server commands",
101
    'flavor': "Compute API flavor commands",
102
    'image': "Compute API image commands",
103
    'network': "Compute API network commands (Cyclades extension)",
104
    'glance': "Image API commands",
105
    'store': "Storage API commands"}
106

  
107

  
108
def command(api=None, group=None, name=None, syntax=None):
91 109
    """Class decorator that registers a class as a CLI command."""
92 110
    
93 111
    def decorator(cls):
......
101 119
        cls.description = description or cls.__doc__
102 120
        cls.syntax = syntax
103 121
        
122
        short_description, sep, long_description = cls.__doc__.partition('\n')
123
        cls.description = short_description
124
        cls.long_description = long_description or short_description
125
        
126
        cls.syntax = syntax
104 127
        if cls.syntax is None:
105 128
            # Generate a syntax string based on main's arguments
106 129
            spec = inspect.getargspec(cls.main.im_func)
......
119 142
    return decorator
120 143

  
121 144

  
122
@command()
145
@command(api='config')
123 146
class config_list(object):
124
    """list configuration options"""
147
    """List configuration options"""
125 148
    
126
    @classmethod
127 149
    def update_parser(cls, parser):
128 150
        parser.add_option('-a', dest='all', action='store_true',
129
                default=False, help='include empty values')
151
                          default=False, help='include default values')
130 152
    
131 153
    def main(self):
132
        for key, val in sorted(self.config.items()):
133
            if not val and not self.options.all:
134
                continue
135
            print '%s=%s' % (key, val)
154
        include_defaults = self.options.all
155
        for section in sorted(self.config.sections()):
156
            items = self.config.items(section, include_defaults)
157
            for key, val in sorted(items):
158
                puts('%s.%s = %s' % (section, key, val))
136 159

  
137 160

  
138
@command()
161
@command(api='config')
139 162
class config_get(object):
140
    """get a configuration option"""
163
    """Show a configuration option"""
141 164
    
142
    def main(self, key):
143
        val = self.config.get(key)
144
        if val is not None:
145
            print val
165
    def main(self, option):
166
        section, sep, key = option.rpartition('.')
167
        section = section or 'global'
168
        value = self.config.get(section, key)
169
        if value is not None:
170
            print value
146 171

  
147 172

  
148
@command()
173
@command(api='config')
149 174
class config_set(object):
150
    """set a configuration option"""
175
    """Set a configuration option"""
151 176
    
152
    def main(self, key, val):
153
        self.config.set(key, val)
177
    def main(self, option, value):
178
        section, sep, key = option.rpartition('.')
179
        section = section or 'global'
180
        self.config.set(section, key, value)
181
        self.config.write()
154 182

  
155 183

  
156
@command()
157
class config_del(object):
158
    """delete a configuration option"""
184
@command(api='config')
185
class config_delete(object):
186
    """Delete a configuration option (and use the default value)"""
159 187
    
160
    def main(self, key):
161
        self.config.delete(key)
188
    def main(self, option):
189
        section, sep, key = option.rpartition('.')
190
        section = section or 'global'
191
        self.config.remove_option(section, key)
192
        self.config.write()
162 193

  
163 194

  
164 195
@command(api='compute')
165 196
class server_list(object):
166 197
    """list servers"""
167 198
    
168
    @classmethod
169 199
    def update_parser(cls, parser):
170 200
        parser.add_option('-l', dest='detail', action='store_true',
171 201
                default=False, help='show detailed output')
......
188 218
class server_create(object):
189 219
    """create server"""
190 220
    
191
    @classmethod
192 221
    def update_parser(cls, parser):
193 222
        parser.add_option('--personality', dest='personalities',
194 223
                action='append', default=[],
......
248 277
class server_reboot(object):
249 278
    """reboot server"""
250 279
    
251
    @classmethod
252 280
    def update_parser(cls, parser):
253 281
        parser.add_option('-f', dest='hard', action='store_true',
254 282
                default=False, help='perform a hard reboot')
......
349 377
class flavor_list(object):
350 378
    """list flavors"""
351 379
    
352
    @classmethod
353 380
    def update_parser(cls, parser):
354 381
        parser.add_option('-l', dest='detail', action='store_true',
355 382
                default=False, help='show detailed output')
......
372 399
class image_list(object):
373 400
    """list images"""
374 401
    
375
    @classmethod
376 402
    def update_parser(cls, parser):
377 403
        parser.add_option('-l', dest='detail', action='store_true',
378 404
                default=False, help='show detailed output')
......
439 465
class network_list(object):
440 466
    """list networks"""
441 467
    
442
    @classmethod
443 468
    def update_parser(cls, parser):
444 469
        parser.add_option('-l', dest='detail', action='store_true',
445 470
                default=False, help='show detailed output')
......
503 528
class glance_list(object):
504 529
    """list images"""
505 530
    
506
    @classmethod
507 531
    def update_parser(cls, parser):
508 532
        parser.add_option('-l', dest='detail', action='store_true',
509 533
                default=False, help='show detailed output')
......
549 573
class glance_register(object):
550 574
    """register an image"""
551 575
    
552
    @classmethod
553 576
    def update_parser(cls, parser):
554 577
        parser.add_option('--checksum', dest='checksum', metavar='CHECKSUM',
555 578
                help='set image checksum')
......
635 658
class store_command(object):
636 659
    """base class for all store_* commands"""
637 660
    
638
    @classmethod
639 661
    def update_parser(cls, parser):
640 662
        parser.add_option('--account', dest='account', metavar='NAME',
641 663
                help='use account NAME')
......
655 677
class store_create(object):
656 678
    """create a container"""
657 679
    
658
    @classmethod
659 680
    def update_parser(cls, parser):
660 681
        parser.add_option('--account', dest='account', metavar='ACCOUNT',
661 682
                help='use account ACCOUNT')
......
711 732
        self.client.delete_object(path)
712 733

  
713 734

  
714
def print_groups(groups):
715
    print
716
    print 'Groups:'
717
    for group in groups:
718
        print '  %s' % group
735
def print_groups():
736
    puts('\nGroups:')
737
    with indent(2):
738
        for group in _commands:
739
            description = GROUPS.get(group, '')
740
            puts(columns([group, 12], [description, 60]))
719 741

  
720 742

  
721
def print_commands(group, commands):
722
    print
723
    print 'Commands:'
724
    for name, cls in _commands[group].items():
725
        if name in commands:
726
            print '  %s %s' % (name.ljust(10), cls.description)
743
def print_commands(group):
744
    description = GROUPS.get(group, '')
745
    if description:
746
        puts('\n' + description)
747
    
748
    puts('\nCommands:')
749
    with indent(2):
750
        for name, cls in _commands[group].items():
751
            puts(columns([name, 12], [cls.description, 60]))
727 752

  
728 753

  
729 754
def main():
730
    ch = logging.StreamHandler()
731
    ch.setFormatter(logging.Formatter('%(message)s'))
732
    log.addHandler(ch)
733
    
734 755
    parser = OptionParser(add_help_option=False)
735 756
    parser.usage = '%prog <group> <command> [options]'
736
    parser.add_option('--help', dest='help', action='store_true',
737
            default=False, help='show this help message and exit')
738
    parser.add_option('-v', dest='verbose', action='store_true', default=False,
739
            help='use verbose output')
740
    parser.add_option('-d', dest='debug', action='store_true', default=False,
741
            help='use debug output')
757
    parser.add_option('-h', '--help', dest='help', action='store_true',
758
                      default=False,
759
                      help="Show this help message and exit")
760
    parser.add_option('--config', dest='config', metavar='PATH',
761
                      help="Specify the path to the configuration file")
762
    parser.add_option('-i', '--include', dest='include', action='store_true',
763
                      default=False,
764
                      help="Include protocol headers in the output")
765
    parser.add_option('-s', '--silent', dest='silent', action='store_true',
766
                      default=False,
767
                      help="Silent mode, don't output anything")
768
    parser.add_option('-v', '--verbose', dest='verbose', action='store_true',
769
                      default=False,
770
                      help="Make the operation more talkative")
771
    parser.add_option('-V', '--version', dest='version', action='store_true',
772
                      default=False,
773
                      help="Show version number and quit")
742 774
    parser.add_option('-o', dest='options', action='append',
743
            metavar='KEY=VAL',
744
            help='override a config value (can be used multiple times)')
745
    
746
    # Do a preliminary parsing, ignore any errors since we will print help
747
    # anyway if we don't reach the main parsing.
748
    _error = parser.error
749
    parser.error = lambda msg: None
750
    options, args = parser.parse_args(argv)
751
    parser.error = _error
752
    
753
    if options.debug:
754
        log.setLevel(logging.DEBUG)
755
    elif options.verbose:
756
        log.setLevel(logging.INFO)
775
                      default=[], metavar="KEY=VAL",
776
                      help="Override a config values")
777
    
778
    if args.contains(['-V', '--version']):
779
        import kamaki
780
        print "kamaki %s" % kamaki.__version__
781
        exit(0)
782
    
783
    if args.contains(['-s', '--silent']):
784
        level = logging.CRITICAL
785
    elif args.contains(['-v', '--verbose']):
786
        level = logging.INFO
757 787
    else:
758
        log.setLevel(logging.WARNING)
788
        level = logging.WARNING
759 789
    
760
    try:
761
        config = Config()
762
    except ConfigError, e:
763
        log.error('%s', e.args[0])
764
        exit(1)
790
    logging.basicConfig(level=level, format='%(message)s')
765 791
    
766
    for option in options.options or []:
767
        key, sep, val = option.partition('=')
768
        if not sep:
769
            log.error('Invalid option "%s"', option)
770
            exit(1)
771
        config.override(key.strip(), val.strip())
792
    if '--config' in args:
793
        config_path = args.grouped['--config'].get(0)
794
    else:
795
        config_path = os.environ.get(CONFIG_ENV, CONFIG_PATH)
772 796
    
773
    apis = config.get('apis').split()
797
    config = Config(config_path)
774 798
    
775
    # Find available groups based on the given APIs
776
    available_groups = []
799
    for option in args.grouped.get('-o', []):
800
        keypath, sep, val = option.partition('=')
801
        if not sep:
802
            log.error("Invalid option '%s'", option)
803
            exit(1)
804
        section, sep, key = keypath.partition('.')
805
        if not sep:
806
            log.error("Invalid option '%s'", option)
807
            exit(1)
808
        config.override(section.strip(), key.strip(), val.strip())
809
    
810
    apis = set(['config'])
811
    for api in ('compute', 'image', 'storage'):
812
        if config.getboolean(api, 'enable'):
813
            apis.add(api)
814
    if config.getboolean('compute', 'cyclades_extensions'):
815
        apis.add('cyclades')
816
    if config.getboolean('storage', 'pithos_extensions'):
817
        apis.add('pithos')
818
    
819
    # Remove commands that belong to APIs that are not included
777 820
    for group, group_commands in _commands.items():
778 821
        for name, cls in group_commands.items():
779
            if cls.api is None or cls.api in apis:
780
                available_groups.append(group)
781
                break
822
            if cls.api not in apis:
823
                del group_commands[name]
824
        if not group_commands:
825
            del _commands[group]
782 826
    
783
    if len(args) < 2:
827
    if not args.grouped['_']:
784 828
        parser.print_help()
785
        print_groups(available_groups)
829
        print_groups()
786 830
        exit(0)
787 831
    
788
    group = args[1]
832
    group = args.grouped['_'][0]
789 833
    
790
    if group not in available_groups:
834
    if group not in _commands:
791 835
        parser.print_help()
792
        print_groups(available_groups)
836
        print_groups()
793 837
        exit(1)
794 838
    
795
    # Find available commands based on the given APIs
796
    available_commands = []
797
    for name, cls in _commands[group].items():
798
        if cls.api is None or cls.api in apis:
799
            available_commands.append(name)
800
            continue
801
    
802 839
    parser.usage = '%%prog %s <command> [options]' % group
803 840
    
804
    if len(args) < 3:
841
    if len(args.grouped['_']) == 1:
805 842
        parser.print_help()
806
        print_commands(group, available_commands)
843
        print_commands(group)
807 844
        exit(0)
808 845
    
809
    name = args[2]
846
    name = args.grouped['_'][1]
810 847
    
811
    if name not in available_commands:
848
    if name not in _commands[group]:
812 849
        parser.print_help()
813
        print_commands(group, available_commands)
850
        print_commands(group)
814 851
        exit(1)
815 852
    
816
    cls = _commands[group][name]
853
    cmd = _commands[group][name]()
817 854
    
818
    syntax = '%s [options]' % cls.syntax if cls.syntax else '[options]'
855
    syntax = '%s [options]' % cmd.syntax if cmd.syntax else '[options]'
819 856
    parser.usage = '%%prog %s %s %s' % (group, name, syntax)
857
    parser.description = cmd.description
820 858
    parser.epilog = ''
821
    if hasattr(cls, 'update_parser'):
822
        cls.update_parser(parser)
859
    if hasattr(cmd, 'update_parser'):
860
        cmd.update_parser(parser)
823 861
    
824
    options, args = parser.parse_args(argv)
825
    if options.help:
862
    if args.contains(['-h', '--help']):
826 863
        parser.print_help()
827 864
        exit(0)
828 865
    
829
    cmd = cls()
830
    cmd.config = config
831
    cmd.options = options
832
    
833
    if cmd.api:
834
        client_name = cmd.api.capitalize() + 'Client'
835
        client = getattr(clients, client_name, None)
836
        if client:
837
            cmd.client = client(config)
866
    cmd.options, cmd.args = parser.parse_args(argv)
838 867
    
868
    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
        
839 888
    try:
840
        ret = cmd.main(*args[3:])
889
        ret = cmd.main(*args.grouped['_'][2:])
841 890
        exit(ret)
842 891
    except TypeError as e:
843 892
        if e.args and e.args[0].startswith('main()'):
b/kamaki/config.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
import os
34
from collections import defaultdict
35
from ConfigParser import RawConfigParser, NoOptionError, NoSectionError
37 36

  
38
from os.path import exists, expanduser
37
from .utils import OrderedDict
39 38

  
40 39

  
41
# Path to the file that stores the configuration
42
CONFIG_PATH = expanduser('~/.kamakirc')
40
HEADER = """
41
# Kamaki configuration file
42
"""
43 43

  
44
# Name of a shell variable to bypass the CONFIG_PATH value
45
CONFIG_ENV = 'KAMAKI_CONFIG'
46

  
47
# The defaults also determine the allowed keys
48
CONFIG_DEFAULTS = {
49
    'apis': 'compute image storage cyclades pithos',
50
    'token': '',
51
    'url': '',
52
    'compute_token': '',
53
    'compute_url': 'https://okeanos.grnet.gr/api/v1',
54
    'image_token': '',
55
    'image_url': 'https://okeanos.grnet.gr/plankton',
56
    'storage_account': '',
57
    'storage_container': '',
58
    'storage_token': '',
59
    'storage_url': 'https://plus.pithos.grnet.gr/v1'
44
DEFAULTS = {
45
    'global': {
46
        'colors': 'on',
47
        'token': ''
48
    },
49
    'compute': {
50
        'enable': 'on',
51
        'cyclades_extensions': 'on',
52
        'url': 'https://okeanos.grnet.gr/api/v1.1',
53
        'token': ''
54
    },
55
    'image': {
56
        'enable': 'on',
57
        'url': 'https://okeanos.grnet.gr/plankton',
58
        'token': ''
59
    },
60
    'storage': {
61
        'enable': 'on',
62
        'pithos_extensions': 'on',
63
        'url': 'https://plus.pithos.grnet.gr/v1',
64
        'account': '',
65
        'container': '',
66
        'token': ''
67
    }
60 68
}
61 69

  
62 70

  
63
log = logging.getLogger('kamaki.config')
64

  
65

  
66
class ConfigError(Exception):
67
    pass
68

  
69

  
70
class Config(object):
71
    def __init__(self):
72
        self.path = os.environ.get(CONFIG_ENV, CONFIG_PATH)
73
        self.defaults = CONFIG_DEFAULTS
74
        
75
        d = self.read()
76
        for key, val in d.items():
77
            if key not in self.defaults:
78
                log.warning('Ignoring unknown config key "%s".', key)
79
        
80
        self.d = d
81
        self.overrides = {}
71
class Config(RawConfigParser):
72
    def __init__(self, path=None):
73
        RawConfigParser.__init__(self, dict_type=OrderedDict)
74
        self.path = path
75
        self._overrides = defaultdict(dict)
76
        self.read(path)
82 77
    
83
    def read(self):
84
        if not exists(self.path):
85
            return {}
86
        
87
        with open(self.path) as f:
88
            data = f.read()
78
    def sections(self):
79
        return DEFAULTS.keys()
80
    
81
    def get(self, section, option):
82
        value = self._overrides.get(section, {}).get(option)
83
        if value is not None:
84
            return value
89 85
        
90 86
        try:
91
            d = json.loads(data)
92
            assert isinstance(d, dict)
93
            return d
94
        except (ValueError, AssertionError):
95
            msg = '"%s" does not look like a kamaki config file.' % self.path
96
            raise ConfigError(msg)
97
    
98
    def write(self):
99
        self.read()     # Make sure we don't overwrite anything wrong
100
        with open(self.path, 'w') as f:
101
            data = json.dumps(self.d, indent=True)
102
            f.write(data)
87
            return RawConfigParser.get(self, section, option)
88
        except (NoSectionError, NoOptionError) as e:
89
            return DEFAULTS.get(section, {}).get(option)
103 90
    
104
    def items(self):
105
        for key, val in self.defaults.items():
106
            yield key, self.get(key)
91
    def set(self, section, option, value):
92
        if section not in RawConfigParser.sections(self):
93
            self.add_section(section)
94
        RawConfigParser.set(self, section, option, value)
107 95
    
108
    def get(self, key):
109
        if key in self.overrides:
110
            return self.overrides[key]
111
        if key in self.d:
112
            return self.d[key]
113
        return self.defaults.get(key, '')
96
    def remove_option(self, section, option):
97
        try:
98
            RawConfigParser.remove_option(self, section, option)
99
        except NoSectionError:
100
            pass
114 101
    
115
    def set(self, key, val):
116
        if key not in self.defaults:
117
            log.warning('Ignoring unknown config key "%s".', key)
118
            return
119
        self.d[key] = val
120
        self.write()
102
    def items(self, section, include_defaults=False):
103
        d = dict(DEFAULTS[section]) if include_defaults else {}
104
        try:
105
            d.update(RawConfigParser.items(self, section))
106
        except NoSectionError:
107
            pass
108
        return d.items()
121 109
    
122
    def delete(self, key):
123
        if key not in self.defaults:
124
            log.warning('Ignoring unknown config key "%s".', key)
125
            return
126
        self.d.pop(key, None)
127
        self.write()
110
    def override(self, section, option, value):
111
        self._overrides[section][option] = value
128 112
    
129
    def override(self, key, val):
130
        assert key in self.defaults
131
        if val is not None:
132
            self.overrides[key] = val
113
    def write(self):
114
        with open(self.path, 'w') as f:
115
            f.write(HEADER.lstrip())
116
            RawConfigParser.write(self, f)
b/setup.py
49 49
    include_package_data=True,
50 50
    entry_points={
51 51
        'console_scripts': ['kamaki = kamaki.cli:main']
52
    }
52
    },
53
    install_requires=[
54
        'clint>=0.3'
55
    ]
53 56
)

Also available in: Unified diff