Statistics
| Branch: | Tag: | Revision:

root / kamaki / cli / config / __init__.py @ ab154b94

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

    
122

    
123
class Config(RawConfigParser):
124

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

348
        :param option: (str)
349

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

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

367
        :param option: (str)
368

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

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

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

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

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

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

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

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