Statistics
| Branch: | Tag: | Revision:

root / kamaki / cli / config.py @ 668068af

History | View | Annotate | Download (13.8 kB)

1
# Copyright 2011-2012 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
    },
92
    CLOUD_PREFIX:
93
    {
94
        #'default': {
95
        #    'url': '',
96
        #    'token': ''
97
        #    'pithos_type': 'object-store',
98
        #    'pithos_version': 'v1',
99
        #    'cyclades_type': 'compute',
100
        #    'cyclades_version': 'v2.0',
101
        #    'plankton_type': 'image',
102
        #    'plankton_version': '',
103
        #    'astakos_type': 'identity',
104
        #    'astakos_version': 'v2.0'
105
        #}
106
    }
107
}
108

    
109

    
110
class Config(RawConfigParser):
111
    def __init__(self, path=None, with_defaults=True):
112
        RawConfigParser.__init__(self, dict_type=OrderedDict)
113
        self.path = path or os.environ.get(CONFIG_ENV, CONFIG_PATH)
114
        self._overrides = defaultdict(dict)
115
        if with_defaults:
116
            self._load_defaults()
117
        self.read(self.path)
118

    
119
        for section in self.sections():
120
            r = self._cloud_name(section)
121
            if r:
122
                for k, v in self.items(section):
123
                    self.set_cloud(r, k, v)
124
                self.remove_section(section)
125

    
126
    @staticmethod
127
    def _cloud_name(full_section_name):
128
        if not full_section_name.startswith(CLOUD_PREFIX + ' '):
129
            return None
130
        matcher = match(CLOUD_PREFIX + ' "([@#$:\-\w]+)"', full_section_name)
131
        if matcher:
132
            return matcher.groups()[0]
133
        else:
134
            icn = full_section_name[len(CLOUD_PREFIX) + 1:]
135
            raise InvalidCloudNameError('Invalid Cloud Name %s' % icn)
136

    
137
    def rescue_old_file(self):
138
        lost_terms = []
139
        global_terms = DEFAULTS['global'].keys()
140
        translations = dict(
141
            config=dict(serv='', cmd='config'),
142
            history=dict(serv='', cmd='history'),
143
            pithos=dict(serv='pithos', cmd='file'),
144
            file=dict(serv='pithos', cmd='file'),
145
            store=dict(serv='pithos', cmd='file'),
146
            storage=dict(serv='pithos', cmd='file'),
147
            image=dict(serv='plankton', cmd='image'),
148
            plankton=dict(serv='plankton', cmd='image'),
149
            compute=dict(serv='compute', cmd=''),
150
            cyclades=dict(serv='compute', cmd='server'),
151
            server=dict(serv='compute', cmd='server'),
152
            flavor=dict(serv='compute', cmd='flavor'),
153
            network=dict(serv='compute', cmd='network'),
154
            astakos=dict(serv='astakos', cmd='user'),
155
            user=dict(serv='astakos', cmd='user'),
156
        )
157

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

    
197
                if s in ('history',):
198
                    k = 'file'
199
                    v = self.get(s, k)
200
                    if v:
201
                        print('... rescue %s.%s => global.%s_%s' % (
202
                            s, k, s, k))
203
                        self.set('global', '%s_%s' % (s, k), v)
204
                        self.remove_option(s, k)
205

    
206
                trn = translations[s]
207
                for k, v in self.items(s, False):
208
                    if v and k in ('cli',):
209
                        print('... rescue %s.%s => global.%s_cli' % (
210
                            s, k, trn['cmd']))
211
                        self.set('global', '%s_cli' % trn['cmd'], v)
212
                    elif k in ('container',) and trn['serv'] in ('pithos',):
213
                        print('... rescue %s.%s => %s.default.pithos_%s' % (
214
                                    s, k, CLOUD_PREFIX, k))
215
                        self.set_cloud('default', 'pithos_%s' % k, v)
216
                    else:
217
                        lost_terms.append('%s.%s = %s' % (s, k, v))
218
                self.remove_section(s)
219
        #  self.pretty_print()
220
        return lost_terms
221

    
222
    def pretty_print(self):
223
        for s in self.sections():
224
            print s
225
            for k, v in self.items(s):
226
                if isinstance(v, dict):
227
                    print '\t', k, '=> {'
228
                    for ki, vi in v.items():
229
                        print '\t\t', ki, '=>', vi
230
                    print('\t}')
231
                else:
232
                    print '\t', k, '=>', v
233

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

    
255
    def get_cloud(self, cloud, option):
256
        """
257
        :param cloud: (str) cloud alias
258

259
        :param option: (str) option in cloud section
260

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

263
        :raises KeyError: if cloud or cloud's option does not exist
264
        """
265
        r = self.get(CLOUD_PREFIX, cloud)
266
        if not r:
267
            raise KeyError('Cloud "%s" does not exist' % cloud)
268
        return r[option]
269

    
270
    def get_global(self, option):
271
        return self.get('global', option)
272

    
273
    def set_cloud(self, cloud, option, value):
274
        try:
275
            d = self.get(CLOUD_PREFIX, cloud) or dict()
276
        except KeyError:
277
            d = dict()
278
        d[option] = value
279
        self.set(CLOUD_PREFIX, cloud, d)
280

    
281
    def set_global(self, option, value):
282
        self.set('global', option, value)
283

    
284
    def _load_defaults(self):
285
        for section, options in DEFAULTS.items():
286
            for option, val in options.items():
287
                self.set(section, option, val)
288

    
289
    def _get_dict(self, section, include_defaults=True):
290
        try:
291
            d = dict(DEFAULTS[section]) if include_defaults else {}
292
        except KeyError:
293
            d = {}
294
        try:
295
            d.update(RawConfigParser.items(self, section))
296
        except NoSectionError:
297
            pass
298
        return d
299

    
300
    def reload(self):
301
        self = self.__init__(self.path)
302

    
303
    def get(self, section, option):
304
        """
305
        :param section: (str) HINT: for clouds, use cloud.<section>
306

307
        :param option: (str)
308

309
        :returns: (str) the value stored at section: {option: value}
310
        """
311
        value = self._overrides.get(section, {}).get(option)
312
        if value is not None:
313
            return value
314
        prefix = CLOUD_PREFIX + '.'
315
        if section.startswith(prefix):
316
            return self.get_cloud(section[len(prefix):], option)
317
        try:
318
            return RawConfigParser.get(self, section, option)
319
        except (NoSectionError, NoOptionError):
320
            return DEFAULTS.get(section, {}).get(option)
321

    
322
    def set(self, section, option, value):
323
        """
324
        :param section: (str) HINT: for remotes use cloud.<section>
325

326
        :param option: (str)
327

328
        :param value: str
329
        """
330
        prefix = CLOUD_PREFIX + '.'
331
        if section.startswith(prefix):
332
            cloud = self._cloud_name(
333
                CLOUD_PREFIX + ' "' + section[len(prefix):] + '"')
334
            return self.set_cloud(cloud, option, value)
335
        if section not in RawConfigParser.sections(self):
336
            self.add_section(section)
337
        RawConfigParser.set(self, section, option, value)
338

    
339
    def remove_option(self, section, option, also_remove_default=False):
340
        try:
341
            if also_remove_default:
342
                DEFAULTS[section].pop(option)
343
            RawConfigParser.remove_option(self, section, option)
344
        except NoSectionError:
345
            pass
346

    
347
    def remote_from_cloud(self, cloud, option):
348
        d = self.get(CLOUD_PREFIX, cloud)
349
        if isinstance(d, dict):
350
            d.pop(option)
351

    
352
    def keys(self, section, include_defaults=True):
353
        d = self._get_dict(section, include_defaults)
354
        return d.keys()
355

    
356
    def items(self, section, include_defaults=True):
357
        d = self._get_dict(section, include_defaults)
358
        return d.items()
359

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

    
363
    def write(self):
364
        cld_bu = self._get_dict(CLOUD_PREFIX)
365
        try:
366
            for r, d in self.items(CLOUD_PREFIX):
367
                for k, v in d.items():
368
                    self.set(CLOUD_PREFIX + ' "%s"' % r, k, v)
369
            self.remove_section(CLOUD_PREFIX)
370

    
371
            with open(self.path, 'w') as f:
372
                os.chmod(self.path, 0600)
373
                f.write(HEADER.lstrip())
374
                f.flush()
375
                RawConfigParser.write(self, f)
376
        finally:
377
            if CLOUD_PREFIX not in self.sections():
378
                self.add_section(CLOUD_PREFIX)
379
            for cloud, d in cld_bu.items():
380
                self.set(CLOUD_PREFIX, cloud, d)