Statistics
| Branch: | Tag: | Revision:

root / kamaki / cli / config.py @ b91111b9

History | View | Annotate | Download (14 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

    
37
from collections import defaultdict
38
from ConfigParser import RawConfigParser, NoOptionError, NoSectionError, Error
39
from re import match
40

    
41
from kamaki.cli.errors import CLISyntaxError
42
from kamaki import __version__
43

    
44
try:
45
    from collections import OrderedDict
46
except ImportError:
47
    from kamaki.clients.utils.ordereddict import OrderedDict
48

    
49

    
50
class InvalidCloudNameError(Error):
51
    """A valid cloud name is accepted by this regex: ([@#$:-\w]+)"""
52

    
53

    
54
log = getLogger(__name__)
55

    
56
# Path to the file that stores the configuration
57
CONFIG_PATH = os.path.expanduser('~/.kamakirc')
58
HISTORY_PATH = os.path.expanduser('~/.kamaki.history')
59
CLOUD_PREFIX = 'cloud'
60

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

    
64
version = ''
65
for c in '%s' % __version__:
66
    if c not in '0.123456789':
67
        break
68
    version += c
69
HEADER = '# Kamaki configuration file v%s\n' % version
70

    
71
DEFAULTS = {
72
    'global': {
73
        'default_cloud': '',
74
        'colors': 'off',
75
        'log_file': os.path.expanduser('~/.kamaki.log'),
76
        'log_token': 'off',
77
        'log_data': 'off',
78
        'max_threads': 7,
79
        'history_file': HISTORY_PATH,
80
        'user_cli': 'astakos',
81
        'file_cli': 'pithos',
82
        'server_cli': 'cyclades',
83
        'flavor_cli': 'cyclades',
84
        'network_cli': 'cyclades',
85
        'image_cli': 'image',
86
        'config_cli': 'config',
87
        'history_cli': 'history'
88
        #  Optional command specs:
89
        #  'livetest_cli': 'livetest',
90
        #  'astakos_cli': 'snf-astakos'
91
    },
92
    CLOUD_PREFIX:
93
    {
94
        #'default': {
95
        #    'url': '',
96
        #    'token': ''
97
        #    'pithos_type': 'object-store',
98
        #    'pithos_version': 'v1',
99
        #    'cyclades_type': 'compute',
100
        #    'cyclades_version': 'v2.0',
101
        #    'plankton_type': 'image',
102
        #    'plankton_version': '',
103
        #    'astakos_type': 'identity',
104
        #    'astakos_version': 'v2.0'
105
        #}
106
    }
107
}
108

    
109

    
110
try:
111
    import astakosclient
112
    DEFAULTS['global'].update(dict(astakos_cli='snf-astakos'))
113
except ImportError:
114
    pass
115

    
116

    
117
class Config(RawConfigParser):
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):
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
                    try:
178
                        cval = self.get_cloud('default', term)
179
                    except KeyError:
180
                        cval = ''
181
                    if gval and cval and (
182
                        gval.lower().strip('/') != cval.lower().strip('/')):
183
                            raise CLISyntaxError(
184
                                'Conflicting values for default %s' % term,
185
                                importance=2, details=[
186
                                    ' global.%s:  %s' % (term, gval),
187
                                    ' %s.default.%s:  %s' % (
188
                                        CLOUD_PREFIX, term, cval),
189
                                    'Please remove one of them manually:',
190
                                    ' /config delete global.%s' % term,
191
                                    ' or'
192
                                    ' /config delete %s.default.%s' % (
193
                                        CLOUD_PREFIX, term),
194
                                    'and try again'])
195
                    elif gval:
196
                        print('... rescue %s.%s => %s.default.%s' % (
197
                            s, term, CLOUD_PREFIX, term))
198
                        self.set_cloud('default', term, gval)
199
                    self.remove_option(s, term)
200
            # translation for <service> or <command> settings
201
            # <service> or <command group> settings --> translation --> global
202
            elif s in translations:
203

    
204
                if s in ('history',):
205
                    k = 'file'
206
                    v = self.get(s, k)
207
                    if v:
208
                        print('... rescue %s.%s => global.%s_%s' % (
209
                            s, k, s, k))
210
                        self.set('global', '%s_%s' % (s, k), v)
211
                        self.remove_option(s, k)
212

    
213
                trn = translations[s]
214
                for k, v in self.items(s, False):
215
                    if v and k in ('cli',):
216
                        print('... rescue %s.%s => global.%s_cli' % (
217
                            s, k, trn['cmd']))
218
                        self.set('global', '%s_cli' % trn['cmd'], v)
219
                    elif k in ('container',) and trn['serv'] in ('pithos',):
220
                        print('... rescue %s.%s => %s.default.pithos_%s' % (
221
                                    s, k, CLOUD_PREFIX, k))
222
                        self.set_cloud('default', 'pithos_%s' % k, v)
223
                    else:
224
                        lost_terms.append('%s.%s = %s' % (s, k, v))
225
                self.remove_section(s)
226
        #  self.pretty_print()
227
        return lost_terms
228

    
229
    def pretty_print(self):
230
        for s in self.sections():
231
            print s
232
            for k, v in self.items(s):
233
                if isinstance(v, dict):
234
                    print '\t', k, '=> {'
235
                    for ki, vi in v.items():
236
                        print '\t\t', ki, '=>', vi
237
                    print('\t}')
238
                else:
239
                    print '\t', k, '=>', v
240

    
241
    def guess_version(self):
242
        """
243
        :returns: (float) version of the config file or 0.0 if unrecognized
244
        """
245
        checker = Config(self.path, with_defaults=False)
246
        sections = checker.sections()
247
        log.warning('Config file heuristic 1: old global section ?')
248
        if 'global' in sections:
249
            if checker.get('global', 'url') or checker.get('global', 'token'):
250
                log.warning('..... config file has an old global section')
251
                return 0.8
252
        log.warning('........ nope')
253
        log.warning('Config file heuristic 2: Any cloud sections ?')
254
        if CLOUD_PREFIX in sections:
255
            for r in self.keys(CLOUD_PREFIX):
256
                log.warning('... found cloud "%s"' % r)
257
                return 0.9
258
        log.warning('........ nope')
259
        log.warning('All heuristics failed, cannot decide')
260
        return 0.9
261

    
262
    def get_cloud(self, cloud, option):
263
        """
264
        :param cloud: (str) cloud alias
265

266
        :param option: (str) option in cloud section
267

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

270
        :raises KeyError: if cloud or cloud's option does not exist
271
        """
272
        r = self.get(CLOUD_PREFIX, cloud)
273
        if not r:
274
            raise KeyError('Cloud "%s" does not exist' % cloud)
275
        return r[option]
276

    
277
    def get_global(self, option):
278
        return self.get('global', option)
279

    
280
    def set_cloud(self, cloud, option, value):
281
        try:
282
            d = self.get(CLOUD_PREFIX, cloud) or dict()
283
        except KeyError:
284
            d = dict()
285
        d[option] = value
286
        self.set(CLOUD_PREFIX, cloud, d)
287

    
288
    def set_global(self, option, value):
289
        self.set('global', option, value)
290

    
291
    def _load_defaults(self):
292
        for section, options in DEFAULTS.items():
293
            for option, val in options.items():
294
                self.set(section, option, val)
295

    
296
    def _get_dict(self, section, include_defaults=True):
297
        try:
298
            d = dict(DEFAULTS[section]) if include_defaults else {}
299
        except KeyError:
300
            d = {}
301
        try:
302
            d.update(RawConfigParser.items(self, section))
303
        except NoSectionError:
304
            pass
305
        return d
306

    
307
    def reload(self):
308
        self = self.__init__(self.path)
309

    
310
    def get(self, section, option):
311
        """
312
        :param section: (str) HINT: for clouds, use cloud.<section>
313

314
        :param option: (str)
315

316
        :returns: (str) the value stored at section: {option: value}
317
        """
318
        value = self._overrides.get(section, {}).get(option)
319
        if value is not None:
320
            return value
321
        prefix = CLOUD_PREFIX + '.'
322
        if section.startswith(prefix):
323
            return self.get_cloud(section[len(prefix):], option)
324
        try:
325
            return RawConfigParser.get(self, section, option)
326
        except (NoSectionError, NoOptionError):
327
            return DEFAULTS.get(section, {}).get(option)
328

    
329
    def set(self, section, option, value):
330
        """
331
        :param section: (str) HINT: for remotes use cloud.<section>
332

333
        :param option: (str)
334

335
        :param value: str
336
        """
337
        prefix = CLOUD_PREFIX + '.'
338
        if section.startswith(prefix):
339
            cloud = self._cloud_name(
340
                CLOUD_PREFIX + ' "' + section[len(prefix):] + '"')
341
            return self.set_cloud(cloud, option, value)
342
        if section not in RawConfigParser.sections(self):
343
            self.add_section(section)
344
        RawConfigParser.set(self, section, option, value)
345

    
346
    def remove_option(self, section, option, also_remove_default=False):
347
        try:
348
            if also_remove_default:
349
                DEFAULTS[section].pop(option)
350
            RawConfigParser.remove_option(self, section, option)
351
        except NoSectionError:
352
            pass
353

    
354
    def remote_from_cloud(self, cloud, option):
355
        d = self.get(CLOUD_PREFIX, cloud)
356
        if isinstance(d, dict):
357
            d.pop(option)
358

    
359
    def keys(self, section, include_defaults=True):
360
        d = self._get_dict(section, include_defaults)
361
        return d.keys()
362

    
363
    def items(self, section, include_defaults=True):
364
        d = self._get_dict(section, include_defaults)
365
        return d.items()
366

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

    
370
    def write(self):
371
        cld_bu = self._get_dict(CLOUD_PREFIX)
372
        try:
373
            for r, d in self.items(CLOUD_PREFIX):
374
                for k, v in d.items():
375
                    self.set(CLOUD_PREFIX + ' "%s"' % r, k, v)
376
            self.remove_section(CLOUD_PREFIX)
377

    
378
            with open(self.path, 'w') as f:
379
                os.chmod(self.path, 0600)
380
                f.write(HEADER.lstrip())
381
                f.flush()
382
                RawConfigParser.write(self, f)
383
        finally:
384
            if CLOUD_PREFIX not in self.sections():
385
                self.add_section(CLOUD_PREFIX)
386
            for cloud, d in cld_bu.items():
387
                self.set(CLOUD_PREFIX, cloud, d)