Statistics
| Branch: | Tag: | Revision:

root / kamaki / cli / config / __init__.py @ 6489c38b

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
        'ip_cli': 'cyclades',
88
        'image_cli': 'image',
89
        'config_cli': 'config',
90
        'history_cli': 'history'
91
        #  Optional command specs:
92
        #  'livetest_cli': 'livetest',
93
        #  'astakos_cli': 'snf-astakos'
94
    },
95
    CLOUD_PREFIX: {
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

    
121
    def __init__(self, path=None, with_defaults=True):
122
        RawConfigParser.__init__(self, dict_type=OrderedDict)
123
        self.path = path or os.environ.get(CONFIG_ENV, CONFIG_PATH)
124
        self._overrides = defaultdict(dict)
125
        if with_defaults:
126
            self._load_defaults()
127
        self.read(self.path)
128

    
129
        for section in self.sections():
130
            r = self._cloud_name(section)
131
            if r:
132
                for k, v in self.items(section):
133
                    self.set_cloud(r, k, v)
134
                self.remove_section(section)
135

    
136
    @staticmethod
137
    def _cloud_name(full_section_name):
138
        if not full_section_name.startswith(CLOUD_PREFIX + ' '):
139
            return None
140
        matcher = match(CLOUD_PREFIX + ' "([~@#$.:\-\w]+)"', full_section_name)
141
        if matcher:
142
            return matcher.groups()[0]
143
        else:
144
            icn = full_section_name[len(CLOUD_PREFIX) + 1:]
145
            raise InvalidCloudNameError('Invalid Cloud Name %s' % icn)
146

    
147
    def rescue_old_file(self, err=stderr):
148
        lost_terms = []
149
        global_terms = DEFAULTS['global'].keys()
150
        translations = dict(
151
            config=dict(serv='', cmd='config'),
152
            history=dict(serv='', cmd='history'),
153
            pithos=dict(serv='pithos', cmd='file'),
154
            file=dict(serv='pithos', cmd='file'),
155
            store=dict(serv='pithos', cmd='file'),
156
            storage=dict(serv='pithos', cmd='file'),
157
            image=dict(serv='plankton', cmd='image'),
158
            plankton=dict(serv='plankton', cmd='image'),
159
            compute=dict(serv='compute', cmd=''),
160
            cyclades=dict(serv='compute', cmd='server'),
161
            server=dict(serv='compute', cmd='server'),
162
            flavor=dict(serv='compute', cmd='flavor'),
163
            network=dict(serv='compute', cmd='network'),
164
            astakos=dict(serv='astakos', cmd='user'),
165
            user=dict(serv='astakos', cmd='user'),
166
        )
167

    
168
        self.set('global', 'default_' + CLOUD_PREFIX, 'default')
169
        for s in self.sections():
170
            if s in ('global', ):
171
                # global.url, global.token -->
172
                # cloud.default.url, cloud.default.token
173
                for term in set(self.keys(s)).difference(global_terms):
174
                    if term not in ('url', 'token'):
175
                        lost_terms.append('%s.%s = %s' % (
176
                            s, term, self.get(s, term)))
177
                        self.remove_option(s, term)
178
                        continue
179
                    gval = self.get(s, term)
180
                    default_cloud = self.get(
181
                        'global', 'default_cloud') or 'default'
182
                    try:
183
                        cval = self.get_cloud(default_cloud, term)
184
                    except KeyError:
185
                        cval = ''
186
                    if gval and cval and (
187
                        gval.lower().strip('/') != cval.lower().strip('/')):
188
                            raise CLISyntaxError(
189
                                'Conflicting values for default %s' % (
190
                                    term),
191
                                importance=2, details=[
192
                                    ' global.%s:  %s' % (term, gval),
193
                                    ' %s.%s.%s:  %s' % (
194
                                        CLOUD_PREFIX,
195
                                        default_cloud,
196
                                        term,
197
                                        cval),
198
                                    'Please remove one of them manually:',
199
                                    ' /config delete global.%s' % term,
200
                                    ' or'
201
                                    ' /config delete %s.%s.%s' % (
202
                                        CLOUD_PREFIX, default_cloud, term),
203
                                    'and try again'])
204
                    elif gval:
205
                        err.write(u'... rescue %s.%s => %s.%s.%s\n' % (
206
                            s, term, CLOUD_PREFIX, default_cloud, term))
207
                        err.flush()
208
                        self.set_cloud('default', term, gval)
209
                    self.remove_option(s, term)
210
            # translation for <service> or <command> settings
211
            # <service> or <command group> settings --> translation --> global
212
            elif s in translations:
213

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

332
        :param option: (str)
333

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

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

351
        :param option: (str)
352

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

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

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

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

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

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

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

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