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

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
45
46
try:
    from collections import OrderedDict
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
55
log = getLogger(__name__)

56
57
# Path to the file that stores the configuration
CONFIG_PATH = os.path.expanduser('~/.kamakirc')
Stavros Sachtouris's avatar
Stavros Sachtouris committed
58
HISTORY_PATH = os.path.expanduser('~/.kamaki.history')
59
CLOUD_PREFIX = 'cloud'
60
61
62
63

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

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

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


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


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

128
        for section in self.sections():
129
            r = self._cloud_name(section)
130
131
            if r:
                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
138
        if not full_section_name.startswith(CLOUD_PREFIX + ' '):
            return None
139
        matcher = match(CLOUD_PREFIX + ' "([~@#$:\-\w]+)"', full_section_name)
140
141
142
143
144
        if matcher:
            return matcher.groups()[0]
        else:
            icn = full_section_name[len(CLOUD_PREFIX) + 1:]
            raise InvalidCloudNameError('Invalid Cloud Name %s' % icn)
145

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

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

                if s in ('history',):
                    k = 'file'
                    v = self.get(s, k)
                    if v:
                        print('... rescue %s.%s => global.%s_%s' % (
                            s, k, s, k))
                        self.set('global', '%s_%s' % (s, k), v)
                        self.remove_option(s, k)

                trn = translations[s]
                for k, v in self.items(s, False):
                    if v and k in ('cli',):
                        print('... rescue %s.%s => global.%s_cli' % (
                            s, k, trn['cmd']))
220
221
                        self.set('global', '%s_cli' % trn['cmd'], v)
                    elif k in ('container',) and trn['serv'] in ('pithos',):
222
223
                        print('... rescue %s.%s => %s.default.pithos_%s' % (
                                    s, k, CLOUD_PREFIX, k))
224
225
                        self.set_cloud('default', 'pithos_%s' % k, v)
                    else:
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
                        lost_terms.append('%s.%s = %s' % (s, k, v))
                self.remove_section(s)
        #  self.pretty_print()
        return lost_terms

    def pretty_print(self):
        for s in self.sections():
            print s
            for k, v in self.items(s):
                if isinstance(v, dict):
                    print '\t', k, '=> {'
                    for ki, vi in v.items():
                        print '\t\t', ki, '=>', vi
                    print('\t}')
                else:
                    print '\t', k, '=>', v
242

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

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

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
278
        return r[option]

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

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
291

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

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

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

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

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

        :param option: (str)

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

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

        :param option: (str)

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

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

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

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

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

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

    def write(self):
373
        cld_bu = self._get_dict(CLOUD_PREFIX)
374
375
376
377
378
379
380
381
382
383
384
385
386
387
        try:
            for r, d in self.items(CLOUD_PREFIX):
                for k, v in d.items():
                    self.set(CLOUD_PREFIX + ' "%s"' % r, k, v)
            self.remove_section(CLOUD_PREFIX)

            with open(self.path, 'w') as f:
                os.chmod(self.path, 0600)
                f.write(HEADER.lstrip())
                f.flush()
                RawConfigParser.write(self, f)
        finally:
            if CLOUD_PREFIX not in self.sections():
                self.add_section(CLOUD_PREFIX)
388
389
            for cloud, d in cld_bu.items():
                self.set(CLOUD_PREFIX, cloud, d)