Statistics
| Branch: | Tag: | Revision:

root / kamaki / cli / config.py @ 64a3c0de

History | View | Annotate | Download (14 kB)

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.warning('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.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
        if CLOUD_PREFIX in sections:
257
            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
        :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)