Statistics
| Branch: | Tag: | Revision:

root / kamaki / cli / config.py @ d2d0dbdb

History | View | Annotate | Download (13.4 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
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
log = getLogger(__name__)
51

    
52
# Path to the file that stores the configuration
53
CONFIG_PATH = os.path.expanduser('~/.kamakirc')
54
HISTORY_PATH = os.path.expanduser('~/.kamaki.history')
55
CLOUD_PREFIX = 'cloud'
56

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

    
60
version = ''
61
for c in '%s' % __version__:
62
    if c not in '0.123456789':
63
        break
64
    version += c
65
HEADER = '# Kamaki configuration file v%s\n' % version
66

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

    
105

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

    
115
        for section in self.sections():
116
            r = self._cloud_name(section)
117
            if r:
118
                for k, v in self.items(section):
119
                    self.set_cloud(r, k, v)
120
                self.remove_section(section)
121

    
122
    @staticmethod
123
    def _cloud_name(full_section_name):
124
        matcher = match(CLOUD_PREFIX + ' "(\w+)"', full_section_name)
125
        return matcher.groups()[0] if matcher else None
126

    
127
    def rescue_old_file(self):
128
        lost_terms = []
129
        global_terms = DEFAULTS['global'].keys()
130
        translations = dict(
131
            config=dict(serv='', cmd='config'),
132
            history=dict(serv='', cmd='history'),
133
            pithos=dict(serv='pithos', cmd='file'),
134
            file=dict(serv='pithos', cmd='file'),
135
            store=dict(serv='pithos', cmd='file'),
136
            storage=dict(serv='pithos', cmd='file'),
137
            image=dict(serv='plankton', cmd='image'),
138
            plankton=dict(serv='plankton', cmd='image'),
139
            compute=dict(serv='compute', cmd=''),
140
            cyclades=dict(serv='compute', cmd='server'),
141
            server=dict(serv='compute', cmd='server'),
142
            flavor=dict(serv='compute', cmd='flavor'),
143
            network=dict(serv='compute', cmd='network'),
144
            astakos=dict(serv='astakos', cmd='user'),
145
            user=dict(serv='astakos', cmd='user'),
146
        )
147

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

    
187
                if s in ('history',):
188
                    k = 'file'
189
                    v = self.get(s, k)
190
                    if v:
191
                        print('... rescue %s.%s => global.%s_%s' % (
192
                            s, k, s, k))
193
                        self.set('global', '%s_%s' % (s, k), v)
194
                        self.remove_option(s, k)
195

    
196
                trn = translations[s]
197
                for k, v in self.items(s, False):
198
                    if v and k in ('cli',):
199
                        print('... rescue %s.%s => global.%s_cli' % (
200
                            s, k, trn['cmd']))
201
                        self.set('global', '%s_cli' % trn['cmd'], v)
202
                    elif k in ('container',) and trn['serv'] in ('pithos',):
203
                        print('... rescue %s.%s => %s.default.pithos_%s' % (
204
                                    s, k, CLOUD_PREFIX, k))
205
                        self.set_cloud('default', 'pithos_%s' % k, v)
206
                    else:
207
                        lost_terms.append('%s.%s = %s' % (s, k, v))
208
                self.remove_section(s)
209
        #  self.pretty_print()
210
        return lost_terms
211

    
212
    def pretty_print(self):
213
        for s in self.sections():
214
            print s
215
            for k, v in self.items(s):
216
                if isinstance(v, dict):
217
                    print '\t', k, '=> {'
218
                    for ki, vi in v.items():
219
                        print '\t\t', ki, '=>', vi
220
                    print('\t}')
221
                else:
222
                    print '\t', k, '=>', v
223

    
224
    def guess_version(self):
225
        """
226
        :returns: (float) version of the config file or 0.0 if unrecognized
227
        """
228
        checker = Config(self.path, with_defaults=False)
229
        sections = checker.sections()
230
        log.warning('Config file heuristic 1: old global section ?')
231
        if 'global' in sections:
232
            if checker.get('global', 'url') or checker.get('global', 'token'):
233
                log.warning('..... config file has an old global section')
234
                return 0.8
235
        log.warning('........ nope')
236
        log.warning('Config file heuristic 2: Any cloud sections ?')
237
        if CLOUD_PREFIX in sections:
238
            for r in self.keys(CLOUD_PREFIX):
239
                log.warning('... found cloud "%s"' % r)
240
                return 0.9
241
        log.warning('........ nope')
242
        log.warning('All heuristics failed, cannot decide')
243
        return 0.9
244

    
245
    def get_cloud(self, cloud, option):
246
        """
247
        :param cloud: (str) cloud alias
248

249
        :param option: (str) option in cloud section
250

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

253
        :raises KeyError: if cloud or cloud's option does not exist
254
        """
255
        r = self.get(CLOUD_PREFIX, cloud)
256
        if not r:
257
            raise KeyError('Cloud "%s" does not exist' % cloud)
258
        return r[option]
259

    
260
    def get_global(self, option):
261
        return self.get('global', option)
262

    
263
    def set_cloud(self, cloud, option, value):
264
        try:
265
            d = self.get(CLOUD_PREFIX, cloud) or dict()
266
        except KeyError:
267
            d = dict()
268
        d[option] = value
269
        self.set(CLOUD_PREFIX, cloud, d)
270

    
271
    def set_global(self, option, value):
272
        self.set('global', option, value)
273

    
274
    def _load_defaults(self):
275
        for section, options in DEFAULTS.items():
276
            for option, val in options.items():
277
                self.set(section, option, val)
278

    
279
    def _get_dict(self, section, include_defaults=True):
280
        try:
281
            d = dict(DEFAULTS[section]) if include_defaults else {}
282
        except KeyError:
283
            d = {}
284
        try:
285
            d.update(RawConfigParser.items(self, section))
286
        except NoSectionError:
287
            pass
288
        return d
289

    
290
    def reload(self):
291
        self = self.__init__(self.path)
292

    
293
    def get(self, section, option):
294
        """
295
        :param section: (str) HINT: for clouds, use cloud.<section>
296

297
        :param option: (str)
298

299
        :returns: (str) the value stored at section: {option: value}
300
        """
301
        value = self._overrides.get(section, {}).get(option)
302
        if value is not None:
303
            return value
304
        prefix = CLOUD_PREFIX + '.'
305
        if section.startswith(prefix):
306
            return self.get_cloud(section[len(prefix):], option)
307
        try:
308
            return RawConfigParser.get(self, section, option)
309
        except (NoSectionError, NoOptionError):
310
            return DEFAULTS.get(section, {}).get(option)
311

    
312
    def set(self, section, option, value):
313
        """
314
        :param section: (str) HINT: for remotes use cloud.<section>
315

316
        :param option: (str)
317

318
        :param value: str
319
        """
320
        prefix = CLOUD_PREFIX + '.'
321
        if section.startswith(prefix):
322
            return self.set_cloud(section[len(prefix)], option, value)
323
        if section not in RawConfigParser.sections(self):
324
            self.add_section(section)
325
        RawConfigParser.set(self, section, option, value)
326

    
327
    def remove_option(self, section, option, also_remove_default=False):
328
        try:
329
            if also_remove_default:
330
                DEFAULTS[section].pop(option)
331
            RawConfigParser.remove_option(self, section, option)
332
        except NoSectionError:
333
            pass
334

    
335
    def remote_from_cloud(self, cloud, option):
336
        d = self.get(CLOUD_PREFIX, cloud)
337
        if isinstance(d, dict):
338
            d.pop(option)
339

    
340
    def keys(self, section, include_defaults=True):
341
        d = self._get_dict(section, include_defaults)
342
        return d.keys()
343

    
344
    def items(self, section, include_defaults=True):
345
        d = self._get_dict(section, include_defaults)
346
        return d.items()
347

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

    
351
    def write(self):
352
        cld_bu = dict(self._get_dict(CLOUD_PREFIX))
353
        try:
354
            for r, d in self.items(CLOUD_PREFIX):
355
                for k, v in d.items():
356
                    self.set(CLOUD_PREFIX + ' "%s"' % r, k, v)
357
            self.remove_section(CLOUD_PREFIX)
358

    
359
            with open(self.path, 'w') as f:
360
                os.chmod(self.path, 0600)
361
                f.write(HEADER.lstrip())
362
                f.flush()
363
                RawConfigParser.write(self, f)
364
        finally:
365
            if CLOUD_PREFIX not in self.sections():
366
                self.add_section(CLOUD_PREFIX)
367
            self._get_dict(CLOUD_PREFIX).update(cld_bu)