Statistics
| Branch: | Tag: | Revision:

root / kamaki / cli / config.py @ a624a072

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
        'max_threads': 7,
79
        'history_file': HISTORY_PATH,
80
        'user_cli': 'astakos',
81
        'file_cli': 'pithos',
82
        'server_cli': 'cyclades',
83
        'flavor_cli': 'cyclades',
84
        'network_cli': 'cyclades',
85
        'image_cli': 'image',
86
        'config_cli': 'config',
87
        'history_cli': 'history'
88
        #  Optional command specs:
89
        #  'livetest_cli': 'livetest',
90
        #  'astakos_cli': 'snf-astakos'
91
        #  'floating_cli': 'cyclades'
92
    },
93
    CLOUD_PREFIX:
94
    {
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.warning('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.warning('..... config file has an old global section')
252
                return 0.8
253
        log.warning('........ nope')
254
        log.warning('Config file heuristic 2: Any cloud sections ?')
255
        if CLOUD_PREFIX in sections:
256
            for r in self.keys(CLOUD_PREFIX):
257
                log.warning('... found cloud "%s"' % r)
258
                return 0.9
259
        log.warning('........ nope')
260
        log.warning('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 remote_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)