Statistics
| Branch: | Tag: | Revision:

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

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
        'admin_cli': 'astakos',
84
        'project_cli': 'astakos',
85
        'file_cli': 'pithos',
86
        'server_cli': 'cyclades',
87
        'flavor_cli': 'cyclades',
88
        'network_cli': 'network',
89
        'subnet_cli': 'network',
90
        'port_cli': 'network',
91
        'ip_cli': 'cyclades',
92
        'image_cli': 'image',
93
        'config_cli': 'config',
94
        'history_cli': 'history'
95
        #  Optional command specs:
96
        #  'livetest_cli': 'livetest',
97
    },
98
    CLOUD_PREFIX: {
99
        #'default': {
100
        #    'url': '',
101
        #    'token': ''
102
        #    'pithos_container': 'THIS IS DANGEROUS'
103
        #    'pithos_type': 'object-store',
104
        #    'pithos_version': 'v1',
105
        #    'cyclades_type': 'compute',
106
        #    'cyclades_version': 'v2.0',
107
        #    'plankton_type': 'image',
108
        #    'plankton_version': '',
109
        #    'astakos_type': 'identity',
110
        #    'astakos_version': 'v2.0'
111
        #}
112
    }
113
}
114

    
115

    
116
class Config(RawConfigParser):
117

    
118
    def __init__(self, path=None, with_defaults=True):
119
        RawConfigParser.__init__(self, dict_type=OrderedDict)
120
        self.path = path or os.environ.get(CONFIG_ENV, CONFIG_PATH)
121
        self._overrides = defaultdict(dict)
122
        if with_defaults:
123
            self._load_defaults()
124
        self.read(self.path)
125

    
126
        for section in self.sections():
127
            r = self._cloud_name(section)
128
            if r:
129
                for k, v in self.items(section):
130
                    self.set_cloud(r, k, v)
131
                self.remove_section(section)
132

    
133
    @staticmethod
134
    def _cloud_name(full_section_name):
135
        if not full_section_name.startswith(CLOUD_PREFIX + ' '):
136
            return None
137
        matcher = match(CLOUD_PREFIX + ' "([~@#$.:\-\w]+)"', full_section_name)
138
        if matcher:
139
            return matcher.groups()[0]
140
        else:
141
            icn = full_section_name[len(CLOUD_PREFIX) + 1:]
142
            raise InvalidCloudNameError('Invalid Cloud Name %s' % icn)
143

    
144
    def rescue_old_file(self, err=stderr):
145
        lost_terms = []
146
        global_terms = DEFAULTS['global'].keys()
147
        translations = dict(
148
            config=dict(serv='', cmd='config'),
149
            history=dict(serv='', cmd='history'),
150
            pithos=dict(serv='pithos', cmd='file'),
151
            file=dict(serv='pithos', cmd='file'),
152
            store=dict(serv='pithos', cmd='file'),
153
            storage=dict(serv='pithos', cmd='file'),
154
            image=dict(serv='plankton', cmd='image'),
155
            plankton=dict(serv='plankton', cmd='image'),
156
            compute=dict(serv='compute', cmd=''),
157
            cyclades=dict(serv='compute', cmd='server'),
158
            server=dict(serv='compute', cmd='server'),
159
            flavor=dict(serv='compute', cmd='flavor'),
160
            network=dict(serv='compute', cmd='network'),
161
            astakos=dict(serv='astakos', cmd='user'),
162
            user=dict(serv='astakos', cmd='user'),
163
        )
164

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

    
211
                if s in ('history',):
212
                    k = 'file'
213
                    v = self.get(s, k)
214
                    if v:
215
                        err.write(u'... rescue %s.%s => global.%s_%s\n' % (
216
                            s, k, s, k))
217
                        err.flush()
218
                        self.set('global', '%s_%s' % (s, k), v)
219
                        self.remove_option(s, k)
220

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

    
240
    def pretty_print(self, out=stdout):
241
        for s in self.sections():
242
            out.write(s)
243
            out.flush()
244
            for k, v in self.items(s):
245
                if isinstance(v, dict):
246
                    out.write(u'\t%s => {\n' % k)
247
                    out.flush()
248
                    for ki, vi in v.items():
249
                        out.write(u'\t\t%s => %s\n' % (ki, vi))
250
                        out.flush()
251
                    out.write(u'\t}\n')
252
                else:
253
                    out.write(u'\t %s => %s\n' % (k, v))
254
                out.flush()
255

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

    
277
    def get_cloud(self, cloud, option):
278
        """
279
        :param cloud: (str) cloud alias
280

281
        :param option: (str) option in cloud section
282

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

285
        :raises KeyError: if cloud or cloud's option does not exist
286
        """
287
        r = self.get(CLOUD_PREFIX, cloud)
288
        if not r:
289
            raise KeyError('Cloud "%s" does not exist' % cloud)
290
        return r[option]
291

    
292
    def set_cloud(self, cloud, option, value):
293
        try:
294
            d = self.get(CLOUD_PREFIX, cloud) or dict()
295
        except KeyError:
296
            d = dict()
297
        d[option] = value
298
        self.set(CLOUD_PREFIX, cloud, d)
299

    
300
    def _load_defaults(self):
301
        for section, options in DEFAULTS.items():
302
            for option, val in options.items():
303
                self.set(section, option, val)
304

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

    
316
    def reload(self):
317
        self = self.__init__(self.path)
318

    
319
    def get(self, section, option):
320
        """
321
        :param section: (str) HINT: for clouds, use cloud.<section>
322

323
        :param option: (str)
324

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

    
338
    def set(self, section, option, value):
339
        """
340
        :param section: (str) HINT: for remotes use cloud.<section>
341

342
        :param option: (str)
343

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

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

    
363
    def remove_from_cloud(self, cloud, option):
364
        d = self.get(CLOUD_PREFIX, cloud)
365
        if isinstance(d, dict):
366
            d.pop(option)
367

    
368
    def keys(self, section, include_defaults=True):
369
        d = self._get_dict(section, include_defaults)
370
        return d.keys()
371

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

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

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

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