Statistics
| Branch: | Tag: | Revision:

root / kamaki / cli / config / __init__.py @ 8556d269

History | View | Annotate | Download (14.7 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
from sys import stdout, stderr
37

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

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

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

    
50

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

    
54

    
55
log = getLogger(__name__)
56

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

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

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

    
72
DEFAULTS = {
73
    'global': {
74
        'default_cloud': '',
75
        'colors': 'off',
76
        'log_file': os.path.expanduser('~/.kamaki.log'),
77
        'log_token': 'off',
78
        'log_data': 'off',
79
        'log_pid': 'off',
80
        'max_threads': 7,
81
        'history_file': HISTORY_PATH,
82
        'user_cli': 'astakos',
83
        'file_cli': 'pithos',
84
        'server_cli': 'cyclades',
85
        'flavor_cli': 'cyclades',
86
        'network_cli': 'cyclades',
87
        'image_cli': 'image',
88
        'config_cli': 'config',
89
        'history_cli': 'history'
90
        #  Optional command specs:
91
        #  'livetest_cli': 'livetest',
92
        #  'astakos_cli': 'snf-astakos'
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

    
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, err=stderr):
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
                    default_cloud = self.get(
180
                        'global', 'default_cloud') or 'default'
181
                    try:
182
                        cval = self.get_cloud(default_cloud, term)
183
                    except KeyError:
184
                        cval = ''
185
                    if gval and cval and (
186
                        gval.lower().strip('/') != cval.lower().strip('/')):
187
                            raise CLISyntaxError(
188
                                'Conflicting values for default %s' % (
189
                                    term),
190
                                importance=2, details=[
191
                                    ' global.%s:  %s' % (term, gval),
192
                                    ' %s.%s.%s:  %s' % (
193
                                        CLOUD_PREFIX,
194
                                        default_cloud,
195
                                        term,
196
                                        cval),
197
                                    'Please remove one of them manually:',
198
                                    ' /config delete global.%s' % term,
199
                                    ' or'
200
                                    ' /config delete %s.%s.%s' % (
201
                                        CLOUD_PREFIX, default_cloud, term),
202
                                    'and try again'])
203
                    elif gval:
204
                        err.write(u'... rescue %s.%s => %s.%s.%s\n' % (
205
                            s, term, CLOUD_PREFIX, default_cloud, term))
206
                        err.flush()
207
                        self.set_cloud('default', term, gval)
208
                    self.remove_option(s, term)
209
            # translation for <service> or <command> settings
210
            # <service> or <command group> settings --> translation --> global
211
            elif s in translations:
212

    
213
                if s in ('history',):
214
                    k = 'file'
215
                    v = self.get(s, k)
216
                    if v:
217
                        err.write(u'... rescue %s.%s => global.%s_%s\n' % (
218
                            s, k, s, k))
219
                        err.flush()
220
                        self.set('global', '%s_%s' % (s, k), v)
221
                        self.remove_option(s, k)
222

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

    
242
    def pretty_print(self, out=stdout):
243
        for s in self.sections():
244
            out.write(s)
245
            out.flush()
246
            for k, v in self.items(s):
247
                if isinstance(v, dict):
248
                    out.write(u'\t%s => {\n' % k)
249
                    out.flush()
250
                    for ki, vi in v.items():
251
                        out.write(u'\t\t%s => %s\n' % (ki, vi))
252
                        out.flush()
253
                    out.write(u'\t}\n')
254
                else:
255
                    out.write(u'\t %s => %s\n' % (k, v))
256
                out.flush()
257

    
258
    def guess_version(self):
259
        """
260
        :returns: (float) version of the config file or 0.9 if unrecognized
261
        """
262
        checker = Config(self.path, with_defaults=False)
263
        sections = checker.sections()
264
        log.debug('Config file heuristic 1: old global section ?')
265
        if 'global' in sections:
266
            if checker.get('global', 'url') or checker.get('global', 'token'):
267
                log.debug('..... config file has an old global section')
268
                return 0.8
269
        log.debug('........ nope')
270
        log.debug('Config file heuristic 2: Any cloud sections ?')
271
        if CLOUD_PREFIX in sections:
272
            for r in self.keys(CLOUD_PREFIX):
273
                log.debug('... found cloud "%s"' % r)
274
                return 0.9
275
        log.debug('........ nope')
276
        log.debug('All heuristics failed, cannot decide')
277
        return 0.9
278

    
279
    def get_cloud(self, cloud, option):
280
        """
281
        :param cloud: (str) cloud alias
282

283
        :param option: (str) option in cloud section
284

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

287
        :raises KeyError: if cloud or cloud's option does not exist
288
        """
289
        r = self.get(CLOUD_PREFIX, cloud)
290
        if not r:
291
            raise KeyError('Cloud "%s" does not exist' % cloud)
292
        return r[option]
293

    
294
    def get_global(self, option):
295
        return self.get('global', option)
296

    
297
    def set_cloud(self, cloud, option, value):
298
        try:
299
            d = self.get(CLOUD_PREFIX, cloud) or dict()
300
        except KeyError:
301
            d = dict()
302
        d[option] = value
303
        self.set(CLOUD_PREFIX, cloud, d)
304

    
305
    def set_global(self, option, value):
306
        self.set('global', option, value)
307

    
308
    def _load_defaults(self):
309
        for section, options in DEFAULTS.items():
310
            for option, val in options.items():
311
                self.set(section, option, val)
312

    
313
    def _get_dict(self, section, include_defaults=True):
314
        try:
315
            d = dict(DEFAULTS[section]) if include_defaults else {}
316
        except KeyError:
317
            d = {}
318
        try:
319
            d.update(RawConfigParser.items(self, section))
320
        except NoSectionError:
321
            pass
322
        return d
323

    
324
    def reload(self):
325
        self = self.__init__(self.path)
326

    
327
    def get(self, section, option):
328
        """
329
        :param section: (str) HINT: for clouds, use cloud.<section>
330

331
        :param option: (str)
332

333
        :returns: (str) the value stored at section: {option: value}
334
        """
335
        value = self._overrides.get(section, {}).get(option)
336
        if value is not None:
337
            return value
338
        prefix = CLOUD_PREFIX + '.'
339
        if section.startswith(prefix):
340
            return self.get_cloud(section[len(prefix):], option)
341
        try:
342
            return RawConfigParser.get(self, section, option)
343
        except (NoSectionError, NoOptionError):
344
            return DEFAULTS.get(section, {}).get(option)
345

    
346
    def set(self, section, option, value):
347
        """
348
        :param section: (str) HINT: for remotes use cloud.<section>
349

350
        :param option: (str)
351

352
        :param value: str
353
        """
354
        prefix = CLOUD_PREFIX + '.'
355
        if section.startswith(prefix):
356
            cloud = self._cloud_name(
357
                CLOUD_PREFIX + ' "' + section[len(prefix):] + '"')
358
            return self.set_cloud(cloud, option, value)
359
        if section not in RawConfigParser.sections(self):
360
            self.add_section(section)
361
        return RawConfigParser.set(self, section, option, value)
362

    
363
    def remove_option(self, section, option, also_remove_default=False):
364
        try:
365
            if also_remove_default:
366
                DEFAULTS[section].pop(option)
367
            RawConfigParser.remove_option(self, section, option)
368
        except (NoSectionError, KeyError):
369
            pass
370

    
371
    def remove_from_cloud(self, cloud, option):
372
        d = self.get(CLOUD_PREFIX, cloud)
373
        if isinstance(d, dict):
374
            d.pop(option)
375

    
376
    def keys(self, section, include_defaults=True):
377
        d = self._get_dict(section, include_defaults)
378
        return d.keys()
379

    
380
    def items(self, section, include_defaults=True):
381
        d = self._get_dict(section, include_defaults)
382
        return d.items()
383

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

    
387
    def write(self):
388
        cld_bu = self._get_dict(CLOUD_PREFIX)
389
        try:
390
            for r, d in self.items(CLOUD_PREFIX):
391
                for k, v in d.items():
392
                    self.set(CLOUD_PREFIX + ' "%s"' % r, k, v)
393
            self.remove_section(CLOUD_PREFIX)
394

    
395
            with open(self.path, 'w') as f:
396
                os.chmod(self.path, 0600)
397
                f.write(HEADER.lstrip())
398
                f.flush()
399
                RawConfigParser.write(self, f)
400
        finally:
401
            if CLOUD_PREFIX not in self.sections():
402
                self.add_section(CLOUD_PREFIX)
403
            for cloud, d in cld_bu.items():
404
                self.set(CLOUD_PREFIX, cloud, d)