Statistics
| Branch: | Tag: | Revision:

root / kamaki / cli / config / __init__.py @ 38a79780

History | View | Annotate | Download (15.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
        'history_file': HISTORY_PATH,
81
        'user_cli': 'astakos',
82
        'quota_cli': 'astakos',
83
        'resource_cli': 'astakos',
84
        'project_cli': 'astakos',
85
        'membership_cli': 'astakos',
86
        'file_cli': 'pithos',
87
        'container_cli': 'pithos',
88
        'sharer_cli': 'pithos',
89
        'group_cli': 'pithos',
90
        'server_cli': 'cyclades',
91
        'flavor_cli': 'cyclades',
92
        'network_cli': 'network',
93
        'subnet_cli': 'network',
94
        'port_cli': 'network',
95
        'ip_cli': 'network',
96
        'image_cli': 'image',
97
        'imagecompute_cli': 'image',
98
        'config_cli': 'config',
99
        'history_cli': 'history'
100
        #  Optional command specs:
101
        #  'livetest_cli': 'livetest',
102
        #  'service_cli': 'astakos'
103
        #  'endpoint_cli': 'astakos'
104
        #  'commission_cli': 'astakos'
105
    },
106
    CLOUD_PREFIX: {
107
        #'default': {
108
        #    'url': '',
109
        #    'token': ''
110
        #    'pithos_container': 'THIS IS DANGEROUS'
111
        #    'pithos_type': 'object-store',
112
        #    'pithos_version': 'v1',
113
        #    'cyclades_type': 'compute',
114
        #    'cyclades_version': 'v2.0',
115
        #    'plankton_type': 'image',
116
        #    'plankton_version': '',
117
        #    'astakos_type': 'identity',
118
        #    'astakos_version': 'v2.0'
119
        #}
120
    }
121
}
122

    
123

    
124
class Config(RawConfigParser):
125

    
126
    def __init__(self, path=None, with_defaults=True):
127
        RawConfigParser.__init__(self, dict_type=OrderedDict)
128
        self.path = path or os.environ.get(CONFIG_ENV, CONFIG_PATH)
129
        self._overrides = defaultdict(dict)
130
        if with_defaults:
131
            self._load_defaults()
132
        self.read(self.path)
133

    
134
        for section in self.sections():
135
            r = self._cloud_name(section)
136
            if r:
137
                for k, v in self.items(section):
138
                    self.set_cloud(r, k, v)
139
                self.remove_section(section)
140

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

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

    
173
        dc = 'default_' + CLOUD_PREFIX
174
        self.set('global', dc, self.get('global', dc) or 'default')
175
        for s in self.sections():
176
            if s in ('global', ):
177
                # global.url, global.token -->
178
                # cloud.default.url, cloud.default.token
179
                for term in set(self.keys(s)).difference(global_terms):
180
                    if term not in ('url', 'token'):
181
                        lost_terms.append('%s.%s = %s' % (
182
                            s, term, self.get(s, term)))
183
                        self.remove_option(s, term)
184
                        continue
185
                    gval = self.get(s, term)
186
                    default_cloud = self.get(
187
                        'global', 'default_cloud') or 'default'
188
                    try:
189
                        cval = self.get_cloud(default_cloud, term)
190
                    except KeyError:
191
                        cval = ''
192
                    if gval and cval and (
193
                        gval.lower().strip('/') != cval.lower().strip('/')):
194
                            raise CLISyntaxError(
195
                                'Conflicting values for default %s' % (
196
                                    term),
197
                                importance=2, details=[
198
                                    ' global.%s:  %s' % (term, gval),
199
                                    ' %s.%s.%s:  %s' % (
200
                                        CLOUD_PREFIX,
201
                                        default_cloud,
202
                                        term,
203
                                        cval),
204
                                    'Please remove one of them manually:',
205
                                    ' /config delete global.%s' % term,
206
                                    ' or'
207
                                    ' /config delete %s.%s.%s' % (
208
                                        CLOUD_PREFIX, default_cloud, term),
209
                                    'and try again'])
210
                    elif gval:
211
                        err.write(u'... rescue %s.%s => %s.%s.%s\n' % (
212
                            s, term, CLOUD_PREFIX, default_cloud, term))
213
                        err.flush()
214
                        self.set_cloud('default', term, gval)
215
                    self.remove_option(s, term)
216
                print 'CHECK'
217
                for term, wrong, right in (
218
                        ('ip', 'cyclades', 'network'),
219
                        ('network', 'cyclades', 'network'),):
220
                    k = '%s_cli' % term
221
                    v = self.get(s, k)
222
                    if v in (wrong, ):
223
                        err.write('... change %s.%s value: `%s` => `%s`\n' % (
224
                            s, k, wrong, right))
225
                        err.flush()
226
                        self.set(s, k, right)
227
            # translation for <service> or <command> settings
228
            # <service> or <command group> settings --> translation --> global
229
            elif s in translations:
230

    
231
                if s in ('history',):
232
                    k = 'file'
233
                    v = self.get(s, k)
234
                    if v:
235
                        err.write(u'... rescue %s.%s => global.%s_%s\n' % (
236
                            s, k, s, k))
237
                        err.flush()
238
                        self.set('global', '%s_%s' % (s, k), v)
239
                        self.remove_option(s, k)
240

    
241
                trn = translations[s]
242
                for k, v in self.items(s, False):
243
                    if v and k in ('cli',):
244
                        err.write(u'... rescue %s.%s => global.%s_cli\n' % (
245
                            s, k, trn['cmd']))
246
                        err.flush()
247
                        self.set('global', '%s_cli' % trn['cmd'], v)
248
                    elif k in ('container',) and trn['serv'] in ('pithos',):
249
                        err.write(
250
                            u'... rescue %s.%s => %s.default.pithos_%s\n' % (
251
                                s, k, CLOUD_PREFIX, k))
252
                        err.flush()
253
                        self.set_cloud('default', 'pithos_%s' % k, v)
254
                    else:
255
                        lost_terms.append('%s.%s = %s' % (s, k, v))
256
                self.remove_section(s)
257
        #  self.pretty_print()
258
        return lost_terms
259

    
260
    def pretty_print(self, out=stdout):
261
        for s in self.sections():
262
            out.write(s)
263
            out.flush()
264
            for k, v in self.items(s):
265
                if isinstance(v, dict):
266
                    out.write(u'\t%s => {\n' % k)
267
                    out.flush()
268
                    for ki, vi in v.items():
269
                        out.write(u'\t\t%s => %s\n' % (ki, vi))
270
                        out.flush()
271
                    out.write(u'\t}\n')
272
                else:
273
                    out.write(u'\t %s => %s\n' % (k, v))
274
                out.flush()
275

    
276
    def guess_version(self):
277
        """
278
        :returns: (float) version of the config file or 0.9 if unrecognized
279
        """
280
        checker = Config(self.path, with_defaults=False)
281
        sections = checker.sections()
282
        log.debug('Config file heuristic 1: old global section ?')
283
        if 'global' in sections:
284
            if checker.get('global', 'url') or checker.get('global', 'token'):
285
                log.debug('..... config file has an old global section')
286
                return 0.8
287
        log.debug('........ nope')
288
        log.debug('Config file heuristic 2: Any cloud sections ?')
289
        if CLOUD_PREFIX in sections:
290
            for r in self.keys(CLOUD_PREFIX):
291
                log.debug('... found cloud "%s"' % r)
292
            ipv = self.get('global', 'ip_cli')
293
            if ipv in ('cyclades', ):
294
                    return 0.11
295
            netv = self.get('global', 'network_cli')
296
            if netv in ('cyclades', ):
297
                return 0.10
298
            return 0.12
299
        log.debug('........ nope')
300
        log.debug('All heuristics failed, cannot decide')
301
        return 0.12
302

    
303
    def get_cloud(self, cloud, option):
304
        """
305
        :param cloud: (str) cloud alias
306

307
        :param option: (str) option in cloud section
308

309
        :returns: (str) the value assigned on this option
310

311
        :raises KeyError: if cloud or cloud's option does not exist
312
        """
313
        r = self.get(CLOUD_PREFIX, cloud) if cloud else None
314
        if not r:
315
            raise KeyError('Cloud "%s" does not exist' % cloud)
316
        return r[option]
317

    
318
    def set_cloud(self, cloud, option, value):
319
        try:
320
            d = self.get(CLOUD_PREFIX, cloud) or dict()
321
        except KeyError:
322
            d = dict()
323
        d[option] = value
324
        self.set(CLOUD_PREFIX, cloud, d)
325

    
326
    def _load_defaults(self):
327
        for section, options in DEFAULTS.items():
328
            for option, val in options.items():
329
                self.set(section, option, val)
330

    
331
    def _get_dict(self, section, include_defaults=True):
332
        try:
333
            d = dict(DEFAULTS[section]) if include_defaults else {}
334
        except KeyError:
335
            d = {}
336
        try:
337
            d.update(RawConfigParser.items(self, section))
338
        except NoSectionError:
339
            pass
340
        return d
341

    
342
    def reload(self):
343
        self = self.__init__(self.path)
344

    
345
    def get(self, section, option):
346
        """
347
        :param section: (str) HINT: for clouds, use cloud.<section>
348

349
        :param option: (str)
350

351
        :returns: (str) the value stored at section: {option: value}
352
        """
353
        value = self._overrides.get(section, {}).get(option)
354
        if value is not None:
355
            return value
356
        prefix = CLOUD_PREFIX + '.'
357
        if section.startswith(prefix):
358
            return self.get_cloud(section[len(prefix):], option)
359
        try:
360
            return RawConfigParser.get(self, section, option)
361
        except (NoSectionError, NoOptionError):
362
            return DEFAULTS.get(section, {}).get(option)
363

    
364
    def set(self, section, option, value):
365
        """
366
        :param section: (str) HINT: for remotes use cloud.<section>
367

368
        :param option: (str)
369

370
        :param value: str
371
        """
372
        prefix = CLOUD_PREFIX + '.'
373
        if section.startswith(prefix):
374
            cloud = self._cloud_name(
375
                CLOUD_PREFIX + ' "' + section[len(prefix):] + '"')
376
            return self.set_cloud(cloud, option, value)
377
        if section not in RawConfigParser.sections(self):
378
            self.add_section(section)
379
        return RawConfigParser.set(self, section, option, value)
380

    
381
    def remove_option(self, section, option, also_remove_default=False):
382
        try:
383
            if also_remove_default:
384
                DEFAULTS[section].pop(option)
385
            RawConfigParser.remove_option(self, section, option)
386
        except (NoSectionError, KeyError):
387
            pass
388

    
389
    def remove_from_cloud(self, cloud, option):
390
        d = self.get(CLOUD_PREFIX, cloud)
391
        if isinstance(d, dict):
392
            d.pop(option)
393

    
394
    def keys(self, section, include_defaults=True):
395
        d = self._get_dict(section, include_defaults)
396
        return d.keys()
397

    
398
    def items(self, section, include_defaults=True):
399
        d = self._get_dict(section, include_defaults)
400
        return d.items()
401

    
402
    def override(self, section, option, value):
403
        self._overrides[section][option] = value
404

    
405
    def write(self):
406
        cld_bu = self._get_dict(CLOUD_PREFIX)
407
        try:
408
            for r, d in self.items(CLOUD_PREFIX):
409
                for k, v in d.items():
410
                    self.set(CLOUD_PREFIX + ' "%s"' % r, k, v)
411
            self.remove_section(CLOUD_PREFIX)
412

    
413
            with open(self.path, 'w') as f:
414
                os.chmod(self.path, 0600)
415
                f.write(HEADER.lstrip())
416
                f.flush()
417
                RawConfigParser.write(self, f)
418
        finally:
419
            if CLOUD_PREFIX not in self.sections():
420
                self.add_section(CLOUD_PREFIX)
421
            for cloud, d in cld_bu.items():
422
                self.set(CLOUD_PREFIX, cloud, d)