Statistics
| Branch: | Tag: | Revision:

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

History | View | Annotate | Download (14.9 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='compute', cmd='network'),
168
            astakos=dict(serv='astakos', cmd='user'),
169
            user=dict(serv='astakos', cmd='user'),
170
        )
171

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

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

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

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

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

    
284
    def get_cloud(self, cloud, option):
285
        """
286
        :param cloud: (str) cloud alias
287

288
        :param option: (str) option in cloud section
289

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

292
        :raises KeyError: if cloud or cloud's option does not exist
293
        """
294
        r = self.get(CLOUD_PREFIX, cloud) if cloud else None
295
        if not r:
296
            raise KeyError('Cloud "%s" does not exist' % cloud)
297
        return r[option]
298

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

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

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

    
323
    def reload(self):
324
        self = self.__init__(self.path)
325

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

330
        :param option: (str)
331

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

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

349
        :param option: (str)
350

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

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

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

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

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

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

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

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