Revision 9f5cbafe

/dev/null
1
# Copyright 2011-2013 GRNET S.A. All rights reserved.
2
#
3
# Redistribution and use in source and binary forms, with or
4
# without modification, are permitted provided that the following
5
# conditions are met:
6
#
7
#   1. Redistributions of source code must retain the above
8
#      copyright notice, this list of conditions and the following
9
#      disclaimer.
10
#
11
#   2. Redistributions in binary form must reproduce the above
12
#      copyright notice, this list of conditions and the following
13
#      disclaimer in the documentation and/or other materials
14
#      provided with the distribution.
15
#
16
# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS
17
# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
18
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
19
# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR
20
# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
23
# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
24
# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
25
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
26
# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
27
# POSSIBILITY OF SUCH DAMAGE.
28
#
29
# The views and conclusions contained in the software and
30
# documentation are those of the authors and should not be
31
# interpreted as representing official policies, either expressed
32
# or implied, of GRNET S.A.
33

  
34
import os
35
from logging import getLogger
36

  
37
from collections import defaultdict
38
from ConfigParser import RawConfigParser, NoOptionError, NoSectionError, Error
39
from re import match
40

  
41
from kamaki.cli.errors import CLISyntaxError
42
from kamaki import __version__
43

  
44
try:
45
    from collections import OrderedDict
46
except ImportError:
47
    from kamaki.clients.utils.ordereddict import OrderedDict
48

  
49

  
50
class InvalidCloudNameError(Error):
51
    """A valid cloud name is accepted by this regex: ([~@#$:-\w]+)"""
52

  
53

  
54
log = getLogger(__name__)
55

  
56
# Path to the file that stores the configuration
57
CONFIG_PATH = os.path.expanduser('~/.kamakirc')
58
HISTORY_PATH = os.path.expanduser('~/.kamaki.history')
59
CLOUD_PREFIX = 'cloud'
60

  
61
# Name of a shell variable to bypass the CONFIG_PATH value
62
CONFIG_ENV = 'KAMAKI_CONFIG'
63

  
64
version = ''
65
for c in '%s' % __version__:
66
    if c not in '0.123456789':
67
        break
68
    version += c
69
HEADER = '# Kamaki configuration file v%s\n' % version
70

  
71
DEFAULTS = {
72
    'global': {
73
        'default_cloud': '',
74
        'colors': 'off',
75
        'log_file': os.path.expanduser('~/.kamaki.log'),
76
        'log_token': 'off',
77
        'log_data': 'off',
78
        'log_pid': 'off',
79
        'max_threads': 7,
80
        'history_file': HISTORY_PATH,
81
        'user_cli': 'astakos',
82
        'file_cli': 'pithos',
83
        'server_cli': 'cyclades',
84
        'flavor_cli': 'cyclades',
85
        'network_cli': 'cyclades',
86
        'image_cli': 'image',
87
        'config_cli': 'config',
88
        'history_cli': 'history'
89
        #  Optional command specs:
90
        #  'livetest_cli': 'livetest',
91
        #  'astakos_cli': 'snf-astakos'
92
        #  'floating_cli': 'cyclades'
93
    },
94
    CLOUD_PREFIX:
95
    {
96
        #'default': {
97
        #    'url': '',
98
        #    'token': ''
99
        #    'pithos_type': 'object-store',
100
        #    'pithos_version': 'v1',
101
        #    'cyclades_type': 'compute',
102
        #    'cyclades_version': 'v2.0',
103
        #    'plankton_type': 'image',
104
        #    'plankton_version': '',
105
        #    'astakos_type': 'identity',
106
        #    'astakos_version': 'v2.0'
107
        #}
108
    }
109
}
110

  
111

  
112
try:
113
    import astakosclient
114
    DEFAULTS['global'].update(dict(astakos_cli='snf-astakos'))
115
except ImportError:
116
    pass
117

  
118

  
119
class Config(RawConfigParser):
120
    def __init__(self, path=None, with_defaults=True):
121
        RawConfigParser.__init__(self, dict_type=OrderedDict)
122
        self.path = path or os.environ.get(CONFIG_ENV, CONFIG_PATH)
123
        self._overrides = defaultdict(dict)
124
        if with_defaults:
125
            self._load_defaults()
126
        self.read(self.path)
127

  
128
        for section in self.sections():
129
            r = self._cloud_name(section)
130
            if r:
131
                for k, v in self.items(section):
132
                    self.set_cloud(r, k, v)
133
                self.remove_section(section)
134

  
135
    @staticmethod
136
    def _cloud_name(full_section_name):
137
        if not full_section_name.startswith(CLOUD_PREFIX + ' '):
138
            return None
139
        matcher = match(CLOUD_PREFIX + ' "([~@#$:\-\w]+)"', full_section_name)
140
        if matcher:
141
            return matcher.groups()[0]
142
        else:
143
            icn = full_section_name[len(CLOUD_PREFIX) + 1:]
144
            raise InvalidCloudNameError('Invalid Cloud Name %s' % icn)
145

  
146
    def rescue_old_file(self):
147
        lost_terms = []
148
        global_terms = DEFAULTS['global'].keys()
149
        translations = dict(
150
            config=dict(serv='', cmd='config'),
151
            history=dict(serv='', cmd='history'),
152
            pithos=dict(serv='pithos', cmd='file'),
153
            file=dict(serv='pithos', cmd='file'),
154
            store=dict(serv='pithos', cmd='file'),
155
            storage=dict(serv='pithos', cmd='file'),
156
            image=dict(serv='plankton', cmd='image'),
157
            plankton=dict(serv='plankton', cmd='image'),
158
            compute=dict(serv='compute', cmd=''),
159
            cyclades=dict(serv='compute', cmd='server'),
160
            server=dict(serv='compute', cmd='server'),
161
            flavor=dict(serv='compute', cmd='flavor'),
162
            network=dict(serv='compute', cmd='network'),
163
            astakos=dict(serv='astakos', cmd='user'),
164
            user=dict(serv='astakos', cmd='user'),
165
        )
166

  
167
        self.set('global', 'default_' + CLOUD_PREFIX, 'default')
168
        for s in self.sections():
169
            if s in ('global'):
170
                # global.url, global.token -->
171
                # cloud.default.url, cloud.default.token
172
                for term in set(self.keys(s)).difference(global_terms):
173
                    if term not in ('url', 'token'):
174
                        lost_terms.append('%s.%s = %s' % (
175
                            s, term, self.get(s, term)))
176
                        self.remove_option(s, term)
177
                        continue
178
                    gval = self.get(s, term)
179
                    try:
180
                        cval = self.get_cloud('default', term)
181
                    except KeyError:
182
                        cval = ''
183
                    if gval and cval and (
184
                        gval.lower().strip('/') != cval.lower().strip('/')):
185
                            raise CLISyntaxError(
186
                                'Conflicting values for default %s' % term,
187
                                importance=2, details=[
188
                                    ' global.%s:  %s' % (term, gval),
189
                                    ' %s.default.%s:  %s' % (
190
                                        CLOUD_PREFIX, term, cval),
191
                                    'Please remove one of them manually:',
192
                                    ' /config delete global.%s' % term,
193
                                    ' or'
194
                                    ' /config delete %s.default.%s' % (
195
                                        CLOUD_PREFIX, term),
196
                                    'and try again'])
197
                    elif gval:
198
                        print('... rescue %s.%s => %s.default.%s' % (
199
                            s, term, CLOUD_PREFIX, term))
200
                        self.set_cloud('default', term, gval)
201
                    self.remove_option(s, term)
202
            # translation for <service> or <command> settings
203
            # <service> or <command group> settings --> translation --> global
204
            elif s in translations:
205

  
206
                if s in ('history',):
207
                    k = 'file'
208
                    v = self.get(s, k)
209
                    if v:
210
                        print('... rescue %s.%s => global.%s_%s' % (
211
                            s, k, s, k))
212
                        self.set('global', '%s_%s' % (s, k), v)
213
                        self.remove_option(s, k)
214

  
215
                trn = translations[s]
216
                for k, v in self.items(s, False):
217
                    if v and k in ('cli',):
218
                        print('... rescue %s.%s => global.%s_cli' % (
219
                            s, k, trn['cmd']))
220
                        self.set('global', '%s_cli' % trn['cmd'], v)
221
                    elif k in ('container',) and trn['serv'] in ('pithos',):
222
                        print('... rescue %s.%s => %s.default.pithos_%s' % (
223
                                    s, k, CLOUD_PREFIX, k))
224
                        self.set_cloud('default', 'pithos_%s' % k, v)
225
                    else:
226
                        lost_terms.append('%s.%s = %s' % (s, k, v))
227
                self.remove_section(s)
228
        #  self.pretty_print()
229
        return lost_terms
230

  
231
    def pretty_print(self):
232
        for s in self.sections():
233
            print s
234
            for k, v in self.items(s):
235
                if isinstance(v, dict):
236
                    print '\t', k, '=> {'
237
                    for ki, vi in v.items():
238
                        print '\t\t', ki, '=>', vi
239
                    print('\t}')
240
                else:
241
                    print '\t', k, '=>', v
242

  
243
    def guess_version(self):
244
        """
245
        :returns: (float) version of the config file or 0.0 if unrecognized
246
        """
247
        checker = Config(self.path, with_defaults=False)
248
        sections = checker.sections()
249
        log.debug('Config file heuristic 1: old global section ?')
250
        if 'global' in sections:
251
            if checker.get('global', 'url') or checker.get('global', 'token'):
252
                log.debug('..... config file has an old global section')
253
                return 0.8
254
        log.debug('........ nope')
255
        log.debug('Config file heuristic 2: Any cloud sections ?')
256
        if CLOUD_PREFIX in sections:
257
            for r in self.keys(CLOUD_PREFIX):
258
                log.debug('... found cloud "%s"' % r)
259
                return 0.9
260
        log.debug('........ nope')
261
        log.debug('All heuristics failed, cannot decide')
262
        return 0.9
263

  
264
    def get_cloud(self, cloud, option):
265
        """
266
        :param cloud: (str) cloud alias
267

  
268
        :param option: (str) option in cloud section
269

  
270
        :returns: (str) the value assigned on this option
271

  
272
        :raises KeyError: if cloud or cloud's option does not exist
273
        """
274
        r = self.get(CLOUD_PREFIX, cloud)
275
        if not r:
276
            raise KeyError('Cloud "%s" does not exist' % cloud)
277
        return r[option]
278

  
279
    def get_global(self, option):
280
        return self.get('global', option)
281

  
282
    def set_cloud(self, cloud, option, value):
283
        try:
284
            d = self.get(CLOUD_PREFIX, cloud) or dict()
285
        except KeyError:
286
            d = dict()
287
        d[option] = value
288
        self.set(CLOUD_PREFIX, cloud, d)
289

  
290
    def set_global(self, option, value):
291
        self.set('global', option, value)
292

  
293
    def _load_defaults(self):
294
        for section, options in DEFAULTS.items():
295
            for option, val in options.items():
296
                self.set(section, option, val)
297

  
298
    def _get_dict(self, section, include_defaults=True):
299
        try:
300
            d = dict(DEFAULTS[section]) if include_defaults else {}
301
        except KeyError:
302
            d = {}
303
        try:
304
            d.update(RawConfigParser.items(self, section))
305
        except NoSectionError:
306
            pass
307
        return d
308

  
309
    def reload(self):
310
        self = self.__init__(self.path)
311

  
312
    def get(self, section, option):
313
        """
314
        :param section: (str) HINT: for clouds, use cloud.<section>
315

  
316
        :param option: (str)
317

  
318
        :returns: (str) the value stored at section: {option: value}
319
        """
320
        value = self._overrides.get(section, {}).get(option)
321
        if value is not None:
322
            return value
323
        prefix = CLOUD_PREFIX + '.'
324
        if section.startswith(prefix):
325
            return self.get_cloud(section[len(prefix):], option)
326
        try:
327
            return RawConfigParser.get(self, section, option)
328
        except (NoSectionError, NoOptionError):
329
            return DEFAULTS.get(section, {}).get(option)
330

  
331
    def set(self, section, option, value):
332
        """
333
        :param section: (str) HINT: for remotes use cloud.<section>
334

  
335
        :param option: (str)
336

  
337
        :param value: str
338
        """
339
        prefix = CLOUD_PREFIX + '.'
340
        if section.startswith(prefix):
341
            cloud = self._cloud_name(
342
                CLOUD_PREFIX + ' "' + section[len(prefix):] + '"')
343
            return self.set_cloud(cloud, option, value)
344
        if section not in RawConfigParser.sections(self):
345
            self.add_section(section)
346
        RawConfigParser.set(self, section, option, value)
347

  
348
    def remove_option(self, section, option, also_remove_default=False):
349
        try:
350
            if also_remove_default:
351
                DEFAULTS[section].pop(option)
352
            RawConfigParser.remove_option(self, section, option)
353
        except NoSectionError:
354
            pass
355

  
356
    def remove_from_cloud(self, cloud, option):
357
        d = self.get(CLOUD_PREFIX, cloud)
358
        if isinstance(d, dict):
359
            d.pop(option)
360

  
361
    def keys(self, section, include_defaults=True):
362
        d = self._get_dict(section, include_defaults)
363
        return d.keys()
364

  
365
    def items(self, section, include_defaults=True):
366
        d = self._get_dict(section, include_defaults)
367
        return d.items()
368

  
369
    def override(self, section, option, value):
370
        self._overrides[section][option] = value
371

  
372
    def write(self):
373
        cld_bu = self._get_dict(CLOUD_PREFIX)
374
        try:
375
            for r, d in self.items(CLOUD_PREFIX):
376
                for k, v in d.items():
377
                    self.set(CLOUD_PREFIX + ' "%s"' % r, k, v)
378
            self.remove_section(CLOUD_PREFIX)
379

  
380
            with open(self.path, 'w') as f:
381
                os.chmod(self.path, 0600)
382
                f.write(HEADER.lstrip())
383
                f.flush()
384
                RawConfigParser.write(self, f)
385
        finally:
386
            if CLOUD_PREFIX not in self.sections():
387
                self.add_section(CLOUD_PREFIX)
388
            for cloud, d in cld_bu.items():
389
                self.set(CLOUD_PREFIX, cloud, d)
b/kamaki/cli/config/__init__.py
1
# Copyright 2011-2013 GRNET S.A. All rights reserved.
2
#
3
# Redistribution and use in source and binary forms, with or
4
# without modification, are permitted provided that the following
5
# conditions are met:
6
#
7
#   1. Redistributions of source code must retain the above
8
#      copyright notice, this list of conditions and the following
9
#      disclaimer.
10
#
11
#   2. Redistributions in binary form must reproduce the above
12
#      copyright notice, this list of conditions and the following
13
#      disclaimer in the documentation and/or other materials
14
#      provided with the distribution.
15
#
16
# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS
17
# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
18
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
19
# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR
20
# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
23
# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
24
# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
25
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
26
# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
27
# POSSIBILITY OF SUCH DAMAGE.
28
#
29
# The views and conclusions contained in the software and
30
# documentation are those of the authors and should not be
31
# interpreted as representing official policies, either expressed
32
# or implied, of GRNET S.A.
33

  
34
import os
35
from logging import getLogger
36

  
37
from collections import defaultdict
38
from ConfigParser import RawConfigParser, NoOptionError, NoSectionError, Error
39
from re import match
40

  
41
from kamaki.cli.errors import CLISyntaxError
42
from kamaki import __version__
43

  
44
try:
45
    from collections import OrderedDict
46
except ImportError:
47
    from kamaki.clients.utils.ordereddict import OrderedDict
48

  
49

  
50
class InvalidCloudNameError(Error):
51
    """A valid cloud name must pass through this regex: ([~@#$:-\w]+)"""
52

  
53

  
54
log = getLogger(__name__)
55

  
56
# Path to the file that stores the configuration
57
CONFIG_PATH = os.path.expanduser('~/.kamakirc')
58
HISTORY_PATH = os.path.expanduser('~/.kamaki.history')
59
CLOUD_PREFIX = 'cloud'
60

  
61
# Name of a shell variable to bypass the CONFIG_PATH value
62
CONFIG_ENV = 'KAMAKI_CONFIG'
63

  
64
version = ''
65
for c in '%s' % __version__:
66
    if c not in '0.123456789':
67
        break
68
    version += c
69
HEADER = '# Kamaki configuration file v%s\n' % version
70

  
71
DEFAULTS = {
72
    'global': {
73
        'default_cloud': '',
74
        'colors': 'off',
75
        'log_file': os.path.expanduser('~/.kamaki.log'),
76
        'log_token': 'off',
77
        'log_data': 'off',
78
        'log_pid': 'off',
79
        'max_threads': 7,
80
        'history_file': HISTORY_PATH,
81
        'user_cli': 'astakos',
82
        'file_cli': 'pithos',
83
        'server_cli': 'cyclades',
84
        'flavor_cli': 'cyclades',
85
        'network_cli': 'cyclades',
86
        'image_cli': 'image',
87
        'config_cli': 'config',
88
        'history_cli': 'history'
89
        #  Optional command specs:
90
        #  'livetest_cli': 'livetest',
91
        #  'astakos_cli': 'snf-astakos'
92
        #  'floating_cli': 'cyclades'
93
    },
94
    CLOUD_PREFIX: {
95
        #'default': {
96
        #    'url': '',
97
        #    'token': ''
98
        #    'pithos_type': 'object-store',
99
        #    'pithos_version': 'v1',
100
        #    'cyclades_type': 'compute',
101
        #    'cyclades_version': 'v2.0',
102
        #    'plankton_type': 'image',
103
        #    'plankton_version': '',
104
        #    'astakos_type': 'identity',
105
        #    'astakos_version': 'v2.0'
106
        #}
107
    }
108
}
109

  
110

  
111
try:
112
    import astakosclient
113
    DEFAULTS['global'].update(dict(astakos_cli='snf-astakos'))
114
except ImportError:
115
    pass
116

  
117

  
118
class Config(RawConfigParser):
119
    def __init__(self, path=None, with_defaults=True):
120
        RawConfigParser.__init__(self, dict_type=OrderedDict)
121
        self.path = path or os.environ.get(CONFIG_ENV, CONFIG_PATH)
122
        self._overrides = defaultdict(dict)
123
        if with_defaults:
124
            self._load_defaults()
125
        self.read(self.path)
126

  
127
        for section in self.sections():
128
            r = self._cloud_name(section)
129
            if r:
130
                for k, v in self.items(section):
131
                    self.set_cloud(r, k, v)
132
                self.remove_section(section)
133

  
134
    @staticmethod
135
    def _cloud_name(full_section_name):
136
        if not full_section_name.startswith(CLOUD_PREFIX + ' '):
137
            return None
138
        matcher = match(CLOUD_PREFIX + ' "([~@#$:\-\w]+)"', full_section_name)
139
        if matcher:
140
            return matcher.groups()[0]
141
        else:
142
            icn = full_section_name[len(CLOUD_PREFIX) + 1:]
143
            raise InvalidCloudNameError('Invalid Cloud Name %s' % icn)
144

  
145
    def rescue_old_file(self):
146
        lost_terms = []
147
        global_terms = DEFAULTS['global'].keys()
148
        translations = dict(
149
            config=dict(serv='', cmd='config'),
150
            history=dict(serv='', cmd='history'),
151
            pithos=dict(serv='pithos', cmd='file'),
152
            file=dict(serv='pithos', cmd='file'),
153
            store=dict(serv='pithos', cmd='file'),
154
            storage=dict(serv='pithos', cmd='file'),
155
            image=dict(serv='plankton', cmd='image'),
156
            plankton=dict(serv='plankton', cmd='image'),
157
            compute=dict(serv='compute', cmd=''),
158
            cyclades=dict(serv='compute', cmd='server'),
159
            server=dict(serv='compute', cmd='server'),
160
            flavor=dict(serv='compute', cmd='flavor'),
161
            network=dict(serv='compute', cmd='network'),
162
            astakos=dict(serv='astakos', cmd='user'),
163
            user=dict(serv='astakos', cmd='user'),
164
        )
165

  
166
        self.set('global', 'default_' + CLOUD_PREFIX, 'default')
167
        for s in self.sections():
168
            if s in ('global'):
169
                # global.url, global.token -->
170
                # cloud.default.url, cloud.default.token
171
                for term in set(self.keys(s)).difference(global_terms):
172
                    if term not in ('url', 'token'):
173
                        lost_terms.append('%s.%s = %s' % (
174
                            s, term, self.get(s, term)))
175
                        self.remove_option(s, term)
176
                        continue
177
                    gval = self.get(s, term)
178
                    try:
179
                        cval = self.get_cloud('default', term)
180
                    except KeyError:
181
                        cval = ''
182
                    if gval and cval and (
183
                        gval.lower().strip('/') != cval.lower().strip('/')):
184
                            raise CLISyntaxError(
185
                                'Conflicting values for default %s' % term,
186
                                importance=2, details=[
187
                                    ' global.%s:  %s' % (term, gval),
188
                                    ' %s.default.%s:  %s' % (
189
                                        CLOUD_PREFIX, term, cval),
190
                                    'Please remove one of them manually:',
191
                                    ' /config delete global.%s' % term,
192
                                    ' or'
193
                                    ' /config delete %s.default.%s' % (
194
                                        CLOUD_PREFIX, term),
195
                                    'and try again'])
196
                    elif gval:
197
                        print('... rescue %s.%s => %s.default.%s' % (
198
                            s, term, CLOUD_PREFIX, term))
199
                        self.set_cloud('default', term, gval)
200
                    self.remove_option(s, term)
201
            # translation for <service> or <command> settings
202
            # <service> or <command group> settings --> translation --> global
203
            elif s in translations:
204

  
205
                if s in ('history',):
206
                    k = 'file'
207
                    v = self.get(s, k)
208
                    if v:
209
                        print('... rescue %s.%s => global.%s_%s' % (
210
                            s, k, s, k))
211
                        self.set('global', '%s_%s' % (s, k), v)
212
                        self.remove_option(s, k)
213

  
214
                trn = translations[s]
215
                for k, v in self.items(s, False):
216
                    if v and k in ('cli',):
217
                        print('... rescue %s.%s => global.%s_cli' % (
218
                            s, k, trn['cmd']))
219
                        self.set('global', '%s_cli' % trn['cmd'], v)
220
                    elif k in ('container',) and trn['serv'] in ('pithos',):
221
                        print('... rescue %s.%s => %s.default.pithos_%s' % (
222
                                    s, k, CLOUD_PREFIX, k))
223
                        self.set_cloud('default', 'pithos_%s' % k, v)
224
                    else:
225
                        lost_terms.append('%s.%s = %s' % (s, k, v))
226
                self.remove_section(s)
227
        #  self.pretty_print()
228
        return lost_terms
229

  
230
    def pretty_print(self):
231
        for s in self.sections():
232
            print s
233
            for k, v in self.items(s):
234
                if isinstance(v, dict):
235
                    print '\t', k, '=> {'
236
                    for ki, vi in v.items():
237
                        print '\t\t', ki, '=>', vi
238
                    print('\t}')
239
                else:
240
                    print '\t', k, '=>', v
241

  
242
    def guess_version(self):
243
        """
244
        :returns: (float) version of the config file or 0.0 if unrecognized
245
        """
246
        checker = Config(self.path, with_defaults=False)
247
        sections = checker.sections()
248
        log.debug('Config file heuristic 1: old global section ?')
249
        if 'global' in sections:
250
            if checker.get('global', 'url') or checker.get('global', 'token'):
251
                log.debug('..... config file has an old global section')
252
                return 0.8
253
        log.debug('........ nope')
254
        log.debug('Config file heuristic 2: Any cloud sections ?')
255
        if CLOUD_PREFIX in sections:
256
            for r in self.keys(CLOUD_PREFIX):
257
                log.debug('... found cloud "%s"' % r)
258
                return 0.9
259
        log.debug('........ nope')
260
        log.debug('All heuristics failed, cannot decide')
261
        return 0.9
262

  
263
    def get_cloud(self, cloud, option):
264
        """
265
        :param cloud: (str) cloud alias
266

  
267
        :param option: (str) option in cloud section
268

  
269
        :returns: (str) the value assigned on this option
270

  
271
        :raises KeyError: if cloud or cloud's option does not exist
272
        """
273
        r = self.get(CLOUD_PREFIX, cloud)
274
        if not r:
275
            raise KeyError('Cloud "%s" does not exist' % cloud)
276
        return r[option]
277

  
278
    def get_global(self, option):
279
        return self.get('global', option)
280

  
281
    def set_cloud(self, cloud, option, value):
282
        try:
283
            d = self.get(CLOUD_PREFIX, cloud) or dict()
284
        except KeyError:
285
            d = dict()
286
        d[option] = value
287
        self.set(CLOUD_PREFIX, cloud, d)
288

  
289
    def set_global(self, option, value):
290
        self.set('global', option, value)
291

  
292
    def _load_defaults(self):
293
        for section, options in DEFAULTS.items():
294
            for option, val in options.items():
295
                self.set(section, option, val)
296

  
297
    def _get_dict(self, section, include_defaults=True):
298
        try:
299
            d = dict(DEFAULTS[section]) if include_defaults else {}
300
        except KeyError:
301
            d = {}
302
        try:
303
            d.update(RawConfigParser.items(self, section))
304
        except NoSectionError:
305
            pass
306
        return d
307

  
308
    def reload(self):
309
        self = self.__init__(self.path)
310

  
311
    def get(self, section, option):
312
        """
313
        :param section: (str) HINT: for clouds, use cloud.<section>
314

  
315
        :param option: (str)
316

  
317
        :returns: (str) the value stored at section: {option: value}
318
        """
319
        value = self._overrides.get(section, {}).get(option)
320
        if value is not None:
321
            return value
322
        prefix = CLOUD_PREFIX + '.'
323
        if section.startswith(prefix):
324
            return self.get_cloud(section[len(prefix):], option)
325
        try:
326
            return RawConfigParser.get(self, section, option)
327
        except (NoSectionError, NoOptionError):
328
            return DEFAULTS.get(section, {}).get(option)
329

  
330
    def set(self, section, option, value):
331
        """
332
        :param section: (str) HINT: for remotes use cloud.<section>
333

  
334
        :param option: (str)
335

  
336
        :param value: str
337
        """
338
        prefix = CLOUD_PREFIX + '.'
339
        if section.startswith(prefix):
340
            cloud = self._cloud_name(
341
                CLOUD_PREFIX + ' "' + section[len(prefix):] + '"')
342
            return self.set_cloud(cloud, option, value)
343
        if section not in RawConfigParser.sections(self):
344
            self.add_section(section)
345
        RawConfigParser.set(self, section, option, value)
346

  
347
    def remove_option(self, section, option, also_remove_default=False):
348
        try:
349
            if also_remove_default:
350
                DEFAULTS[section].pop(option)
351
            RawConfigParser.remove_option(self, section, option)
352
        except NoSectionError:
353
            pass
354

  
355
    def remove_from_cloud(self, cloud, option):
356
        d = self.get(CLOUD_PREFIX, cloud)
357
        if isinstance(d, dict):
358
            d.pop(option)
359

  
360
    def keys(self, section, include_defaults=True):
361
        d = self._get_dict(section, include_defaults)
362
        return d.keys()
363

  
364
    def items(self, section, include_defaults=True):
365
        d = self._get_dict(section, include_defaults)
366
        return d.items()
367

  
368
    def override(self, section, option, value):
369
        self._overrides[section][option] = value
370

  
371
    def write(self):
372
        cld_bu = self._get_dict(CLOUD_PREFIX)
373
        try:
374
            for r, d in self.items(CLOUD_PREFIX):
375
                for k, v in d.items():
376
                    self.set(CLOUD_PREFIX + ' "%s"' % r, k, v)
377
            self.remove_section(CLOUD_PREFIX)
378

  
379
            with open(self.path, 'w') as f:
380
                os.chmod(self.path, 0600)
381
                f.write(HEADER.lstrip())
382
                f.flush()
383
                RawConfigParser.write(self, f)
384
        finally:
385
            if CLOUD_PREFIX not in self.sections():
386
                self.add_section(CLOUD_PREFIX)
387
            for cloud, d in cld_bu.items():
388
                self.set(CLOUD_PREFIX, cloud, d)
b/kamaki/cli/config/test.py
1
# Copyright 2013 GRNET S.A. All rights reserved.
2
#
3
# Redistribution and use in source and binary forms, with or
4
# without modification, are permitted provided that the following
5
# conditions are met:
6
#
7
#   1. Redistributions of source code must retain the above
8
#      copyright notice, this list of conditions and the following
9
#      disclaimer.
10
#
11
#   2. Redistributions in binary form must reproduce the above
12
#      copyright notice, this list of conditions and the following
13
#      disclaimer in the documentation and/or other materials
14
#      provided with the distribution.
15
#
16
# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS
17
# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
18
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
19
# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR
20
# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
23
# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
24
# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
25
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
26
# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
27
# POSSIBILITY OF SUCH DAMAGE.
28
#
29
# The views and conclusions contained in the software and
30
# documentation are those of the authors and should not be
31
# interpreted as representing official policies, either expressed
32
# or implied, of GRNET S.A.
33

  
34
from mock import patch, call, MagicMock
35
from unittest import TestCase
36
from StringIO import StringIO
37
from datetime import datetime
38
#from itertools import product
39

  
40
from kamaki.cli import argument, errors
41
from kamaki.cli.config import Config
42

  
43

  
44
if __name__ == '__main__':
45
    from sys import argv
46
    from kamaki.cli.test import runTestCase
47
    #runTestCase(Argument, 'Argument', argv[1:])
b/setup.py
64 64
        'kamaki.cli',
65 65
        'kamaki.cli.command_tree',
66 66
        'kamaki.cli.argument',
67
        'kamaki.cli.config',
67 68
        'kamaki.cli.utils',
68 69
        'kamaki.cli.commands',
69 70
        'kamaki.clients',

Also available in: Unified diff