__init__.py 16 KB
Newer Older
1
# Copyright 2011-2014 GRNET S.A. All rights reserved.
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
#
# Redistribution and use in source and binary forms, with or
# without modification, are permitted provided that the following
# conditions are met:
#
#   1. Redistributions of source code must retain the above
#      copyright notice, this list of conditions and the following
#      disclaimer.
#
#   2. Redistributions in binary form must reproduce the above
#      copyright notice, this list of conditions and the following
#      disclaimer in the documentation and/or other materials
#      provided with the distribution.
#
# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS
# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR
# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
#
# The views and conclusions contained in the software and
# documentation are those of the authors and should not be
# interpreted as representing official policies, either expressed
# or implied, of GRNET S.A.

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
46
47
try:
    from collections import OrderedDict
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
56
log = getLogger(__name__)

57
58
# Path to the file that stores the configuration
CONFIG_PATH = os.path.expanduser('~/.kamakirc')
Stavros Sachtouris's avatar
Stavros Sachtouris committed
59
HISTORY_PATH = os.path.expanduser('~/.kamaki.history')
60
CLOUD_PREFIX = 'cloud'
61
62
63
64

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

65
66
67
68
69
70
71
72
# Get default CA Certifications file path - created while packaging
try:
    from kamaki import defaults
    CACERTS_DEFAULT_PATH = getattr(defaults, 'CACERTS_DEFAULT_PATH', '')
except ImportError as ie:
    log.debug('ImportError while loading default certs: %s' % ie)
    CACERTS_DEFAULT_PATH = ''

73
74
75
76
77
version = ''
for c in '%s' % __version__:
    if c not in '0.123456789':
        break
    version += c
78
HEADER = '# Kamaki configuration file v%s\n' % version
79
80
81

DEFAULTS = {
    'global': {
82
        'default_cloud': '',
Stavros Sachtouris's avatar
Stavros Sachtouris committed
83
        'colors': 'off',
84
        'log_file': os.path.expanduser('~/.kamaki.log'),
85
86
        'log_token': 'off',
        'log_data': 'off',
87
        'log_pid': 'off',
88
        'history_file': HISTORY_PATH,
89
        'history_limit': 0,
90
        'user_cli': 'astakos',
91
92
        'quota_cli': 'astakos',
        'resource_cli': 'astakos',
93
        'project_cli': 'astakos',
94
        'membership_cli': 'astakos',
95
        'file_cli': 'pithos',
96
        'container_cli': 'pithos',
97
98
        'sharer_cli': 'pithos',
        'group_cli': 'pithos',
99
100
        'server_cli': 'cyclades',
        'flavor_cli': 'cyclades',
101
102
103
        'network_cli': 'network',
        'subnet_cli': 'network',
        'port_cli': 'network',
104
        'ip_cli': 'network',
105
106
        'volume_cli': 'blockstorage',
        'snapshot_cli': 'blockstorage',
107
        'image_cli': 'image',
108
        'imagecompute_cli': 'image',
109
        'config_cli': 'config',
110
111
112
        'history_cli': 'history',
        'ignore_ssl': 'off',
        'ca_certs': CACERTS_DEFAULT_PATH,
113
        #  Optional command specs:
114
115
116
        #  'service_cli': 'astakos'
        #  'endpoint_cli': 'astakos'
        #  'commission_cli': 'astakos'
117
    },
118
    CLOUD_PREFIX: {
119
120
121
122
123
124
125
126
127
128
129
130
131
        # 'default': {
        #     'url': '',
        #     'token': ''
        #     'pithos_container': 'THIS IS DANGEROUS'
        #     'pithos_type': 'object-store',
        #     'pithos_version': 'v1',
        #     'cyclades_type': 'compute',
        #     'cyclades_version': 'v2.0',
        #     'plankton_type': 'image',
        #     'plankton_version': '',
        #     'astakos_type': 'identity',
        #     'astakos_version': 'v2.0'
        # }
132
133
134
135
136
    }
}


class Config(RawConfigParser):
137

138
    def __init__(self, path=None, with_defaults=True):
139
140
141
        RawConfigParser.__init__(self, dict_type=OrderedDict)
        self.path = path or os.environ.get(CONFIG_ENV, CONFIG_PATH)
        self._overrides = defaultdict(dict)
142
143
        if with_defaults:
            self._load_defaults()
144
145
        self.read(self.path)

146
        for section in self.sections():
147
            r = self.cloud_name(section)
148
149
            if r:
                for k, v in self.items(section):
150
                    self.set_cloud(r, k, v)
151
                self.remove_section(section)
152

153
    @staticmethod
154
    def cloud_name(full_section_name):
155
156
        if not full_section_name.startswith(CLOUD_PREFIX + ' '):
            return None
157
        matcher = match(CLOUD_PREFIX + ' "([~@#$.:\-\w]+)"', full_section_name)
158
159
160
161
162
        if matcher:
            return matcher.groups()[0]
        else:
            icn = full_section_name[len(CLOUD_PREFIX) + 1:]
            raise InvalidCloudNameError('Invalid Cloud Name %s' % icn)
163

164
    def rescue_old_file(self, err=stderr):
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
        lost_terms = []
        global_terms = DEFAULTS['global'].keys()
        translations = dict(
            config=dict(serv='', cmd='config'),
            history=dict(serv='', cmd='history'),
            pithos=dict(serv='pithos', cmd='file'),
            file=dict(serv='pithos', cmd='file'),
            store=dict(serv='pithos', cmd='file'),
            storage=dict(serv='pithos', cmd='file'),
            image=dict(serv='plankton', cmd='image'),
            plankton=dict(serv='plankton', cmd='image'),
            compute=dict(serv='compute', cmd=''),
            cyclades=dict(serv='compute', cmd='server'),
            server=dict(serv='compute', cmd='server'),
            flavor=dict(serv='compute', cmd='flavor'),
Stavros Sachtouris's avatar
Stavros Sachtouris committed
180
            network=dict(serv='network', cmd='network'),
181
182
183
184
            astakos=dict(serv='astakos', cmd='user'),
            user=dict(serv='astakos', cmd='user'),
        )

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

                if s in ('history',):
                    k = 'file'
                    v = self.get(s, k)
                    if v:
247
                        err.write(u'... rescue %s.%s => global.%s_%s\n' % (
248
                            s, k, s, k))
249
                        err.flush()
250
251
252
253
254
255
                        self.set('global', '%s_%s' % (s, k), v)
                        self.remove_option(s, k)

                trn = translations[s]
                for k, v in self.items(s, False):
                    if v and k in ('cli',):
256
                        err.write(u'... rescue %s.%s => global.%s_cli\n' % (
257
                            s, k, trn['cmd']))
258
                        err.flush()
259
260
                        self.set('global', '%s_cli' % trn['cmd'], v)
                    elif k in ('container',) and trn['serv'] in ('pithos',):
261
262
263
264
                        err.write(
                            u'... rescue %s.%s => %s.default.pithos_%s\n' % (
                                s, k, CLOUD_PREFIX, k))
                        err.flush()
265
266
                        self.set_cloud('default', 'pithos_%s' % k, v)
                    else:
267
268
269
270
271
                        lost_terms.append('%s.%s = %s' % (s, k, v))
                self.remove_section(s)
        #  self.pretty_print()
        return lost_terms

272
    def pretty_print(self, out=stdout):
273
        for s in self.sections():
274
275
            out.write(s)
            out.flush()
276
277
            for k, v in self.items(s):
                if isinstance(v, dict):
278
279
                    out.write(u'\t%s => {\n' % k)
                    out.flush()
280
                    for ki, vi in v.items():
281
282
283
                        out.write(u'\t\t%s => %s\n' % (ki, vi))
                        out.flush()
                    out.write(u'\t}\n')
284
                else:
285
286
                    out.write(u'\t %s => %s\n' % (k, v))
                out.flush()
287

288
    def guess_version(self):
289
        """
Stavros Sachtouris's avatar
Stavros Sachtouris committed
290
        :returns: (float) version of the config file or 0.9 if unrecognized
291
        """
292
293
        checker = Config(self.path, with_defaults=False)
        sections = checker.sections()
294
        #  log.debug('Config file heuristic 1: old global section ?')
295
296
        if 'global' in sections:
            if checker.get('global', 'url') or checker.get('global', 'token'):
297
                log.debug('config file has an old global section')
298
                return 0.8
299
        #  log.debug('Config file heuristic 2: Any cloud sections ?')
300
301
        if CLOUD_PREFIX in sections:
            for r in self.keys(CLOUD_PREFIX):
302
                log.debug('found cloud "%s"' % r)
Stavros Sachtouris's avatar
Stavros Sachtouris committed
303
304
305
306
307
308
309
            ipv = self.get('global', 'ip_cli')
            if ipv in ('cyclades', ):
                    return 0.11
            netv = self.get('global', 'network_cli')
            if netv in ('cyclades', ):
                return 0.10
            return 0.12
310
        log.debug('All heuristics failed, cannot decide')
Stavros Sachtouris's avatar
Stavros Sachtouris committed
311
        return 0.12
312

313
    def get_cloud(self, cloud, option):
314
        """
315
        :param cloud: (str) cloud alias
316

317
        :param option: (str) option in cloud section
318
319
320

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

321
        :raises KeyError: if cloud or cloud's option does not exist
322
        """
323
        r = self.get(CLOUD_PREFIX, cloud) if cloud else None
324
        if not r:
325
            raise KeyError('Cloud "%s" does not exist' % cloud)
326
327
        return r[option]

328
    def set_cloud(self, cloud, option, value):
329
        try:
330
            d = self.get(CLOUD_PREFIX, cloud) or dict()
331
        except KeyError:
332
            d = dict()
333
        d[option] = value
334
        self.set(CLOUD_PREFIX, cloud, d)
335

336
337
338
339
340
    def _load_defaults(self):
        for section, options in DEFAULTS.items():
            for option, val in options.items():
                self.set(section, option, val)

341
342
343
344
345
346
347
348
349
350
351
    def _get_dict(self, section, include_defaults=True):
        try:
            d = dict(DEFAULTS[section]) if include_defaults else {}
        except KeyError:
            d = {}
        try:
            d.update(RawConfigParser.items(self, section))
        except NoSectionError:
            pass
        return d

352
353
354
    def reload(self):
        self = self.__init__(self.path)

355
    def get(self, section, option):
356
        """
357
        :param section: (str) HINT: for clouds, use cloud.<section>
358
359
360
361
362

        :param option: (str)

        :returns: (str) the value stored at section: {option: value}
        """
363
364
365
        value = self._overrides.get(section, {}).get(option)
        if value is not None:
            return value
366
        prefix = CLOUD_PREFIX + '.'
367
368
        if section.startswith(prefix):
            return self.get_cloud(section[len(prefix):], option)
369
370
371
372
373
374
        try:
            return RawConfigParser.get(self, section, option)
        except (NoSectionError, NoOptionError):
            return DEFAULTS.get(section, {}).get(option)

    def set(self, section, option, value):
375
        """
376
        :param section: (str) HINT: for remotes use cloud.<section>
377
378
379
380
381

        :param option: (str)

        :param value: str
        """
382
        prefix = CLOUD_PREFIX + '.'
383
        if section.startswith(prefix):
384
            cloud = self.cloud_name(
385
386
                CLOUD_PREFIX + ' "' + section[len(prefix):] + '"')
            return self.set_cloud(cloud, option, value)
387
388
        if section not in RawConfigParser.sections(self):
            self.add_section(section)
389
        return RawConfigParser.set(self, section, option, value)
390

391
    def remove_option(self, section, option, also_remove_default=False):
392
        try:
393
394
            if also_remove_default:
                DEFAULTS[section].pop(option)
395
            RawConfigParser.remove_option(self, section, option)
396
        except (NoSectionError, KeyError):
397
398
            pass

399
    def remove_from_cloud(self, cloud, option):
400
        d = self.get(CLOUD_PREFIX, cloud)
401
402
403
        if isinstance(d, dict):
            d.pop(option)

404
405
406
407
    def keys(self, section, include_defaults=True):
        d = self._get_dict(section, include_defaults)
        return d.keys()

408
    def items(self, section, include_defaults=True):
409
        d = self._get_dict(section, include_defaults)
410
411
412
413
414
415
        return d.items()

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

    def write(self):
416
        cld_bu = self._get_dict(CLOUD_PREFIX)
417
418
419
420
421
422
423
424
425
426
427
428
429
430
        try:
            for r, d in self.items(CLOUD_PREFIX):
                for k, v in d.items():
                    self.set(CLOUD_PREFIX + ' "%s"' % r, k, v)
            self.remove_section(CLOUD_PREFIX)

            with open(self.path, 'w') as f:
                os.chmod(self.path, 0600)
                f.write(HEADER.lstrip())
                f.flush()
                RawConfigParser.write(self, f)
        finally:
            if CLOUD_PREFIX not in self.sections():
                self.add_section(CLOUD_PREFIX)
431
432
            for cloud, d in cld_bu.items():
                self.set(CLOUD_PREFIX, cloud, d)