Statistics
| Branch: | Tag: | Revision:

root / kamaki / cli / config / __init__.py @ 534e7bbb

History | View | Annotate | Download (14.6 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 set_cloud(self, cloud, option, value):
296
        try:
297
            d = self.get(CLOUD_PREFIX, cloud) or dict()
298
        except KeyError:
299
            d = dict()
300
        d[option] = value
301
        self.set(CLOUD_PREFIX, cloud, d)
302

    
303
    def _load_defaults(self):
304
        for section, options in DEFAULTS.items():
305
            for option, val in options.items():
306
                self.set(section, option, val)
307

    
308
    def _get_dict(self, section, include_defaults=True):
309
        try:
310
            d = dict(DEFAULTS[section]) if include_defaults else {}
311
        except KeyError:
312
            d = {}
313
        try:
314
            d.update(RawConfigParser.items(self, section))
315
        except NoSectionError:
316
            pass
317
        return d
318

    
319
    def reload(self):
320
        self = self.__init__(self.path)
321

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

326
        :param option: (str)
327

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

    
341
    def set(self, section, option, value):
342
        """
343
        :param section: (str) HINT: for remotes use cloud.<section>
344

345
        :param option: (str)
346

347
        :param value: str
348
        """
349
        prefix = CLOUD_PREFIX + '.'
350
        if section.startswith(prefix):
351
            cloud = self._cloud_name(
352
                CLOUD_PREFIX + ' "' + section[len(prefix):] + '"')
353
            return self.set_cloud(cloud, option, value)
354
        if section not in RawConfigParser.sections(self):
355
            self.add_section(section)
356
        return RawConfigParser.set(self, section, option, value)
357

    
358
    def remove_option(self, section, option, also_remove_default=False):
359
        try:
360
            if also_remove_default:
361
                DEFAULTS[section].pop(option)
362
            RawConfigParser.remove_option(self, section, option)
363
        except (NoSectionError, KeyError):
364
            pass
365

    
366
    def remove_from_cloud(self, cloud, option):
367
        d = self.get(CLOUD_PREFIX, cloud)
368
        if isinstance(d, dict):
369
            d.pop(option)
370

    
371
    def keys(self, section, include_defaults=True):
372
        d = self._get_dict(section, include_defaults)
373
        return d.keys()
374

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

    
379
    def override(self, section, option, value):
380
        self._overrides[section][option] = value
381

    
382
    def write(self):
383
        cld_bu = self._get_dict(CLOUD_PREFIX)
384
        try:
385
            for r, d in self.items(CLOUD_PREFIX):
386
                for k, v in d.items():
387
                    self.set(CLOUD_PREFIX + ' "%s"' % r, k, v)
388
            self.remove_section(CLOUD_PREFIX)
389

    
390
            with open(self.path, 'w') as f:
391
                os.chmod(self.path, 0600)
392
                f.write(HEADER.lstrip())
393
                f.flush()
394
                RawConfigParser.write(self, f)
395
        finally:
396
            if CLOUD_PREFIX not in self.sections():
397
                self.add_section(CLOUD_PREFIX)
398
            for cloud, d in cld_bu.items():
399
                self.set(CLOUD_PREFIX, cloud, d)