Statistics
| Branch: | Tag: | Revision:

root / kamaki / cli / config.py @ 144b3551

History | View | Annotate | Download (12.8 kB)

1
# Copyright 2011-2012 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
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
log = getLogger(__name__)
51

    
52
# Path to the file that stores the configuration
53
CONFIG_PATH = os.path.expanduser('~/.kamakirc')
54
HISTORY_PATH = os.path.expanduser('~/.kamaki.history')
55

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

    
59
version = ''
60
for c in '%s' % __version__:
61
    if c not in '0.123456789':
62
        break
63
    version += c
64
HEADER = '# Kamaki configuration file v%s' % version
65

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

    
104

    
105
class Config(RawConfigParser):
106
    def __init__(self, path=None, with_defaults=True):
107
        RawConfigParser.__init__(self, dict_type=OrderedDict)
108
        self.path = path or os.environ.get(CONFIG_ENV, CONFIG_PATH)
109
        self._overrides = defaultdict(dict)
110
        if with_defaults:
111
            self._load_defaults()
112
        self.read(self.path)
113

    
114
        for section in self.sections():
115
            r = self._cloud_name(section)
116
            if r:
117
                for k, v in self.items(section):
118
                    self.set_cloud(r, k, v)
119
                self.remove_section(section)
120

    
121
    @staticmethod
122
    def _cloud_name(full_section_name):
123
        matcher = match('cloud "(\w+)"', full_section_name)
124
        return matcher.groups()[0] if matcher else None
125

    
126
    def rescue_old_file(self):
127
        lost_terms = []
128
        global_terms = DEFAULTS['global'].keys()
129
        translations = dict(
130
            config=dict(serv='', cmd='config'),
131
            history=dict(serv='', cmd='history'),
132
            pithos=dict(serv='pithos', cmd='file'),
133
            file=dict(serv='pithos', cmd='file'),
134
            store=dict(serv='pithos', cmd='file'),
135
            storage=dict(serv='pithos', cmd='file'),
136
            image=dict(serv='plankton', cmd='image'),
137
            plankton=dict(serv='plankton', cmd='image'),
138
            compute=dict(serv='compute', cmd=''),
139
            cyclades=dict(serv='compute', cmd='server'),
140
            server=dict(serv='compute', cmd='server'),
141
            flavor=dict(serv='compute', cmd='flavor'),
142
            network=dict(serv='compute', cmd='network'),
143
            astakos=dict(serv='astakos', cmd='user'),
144
            user=dict(serv='astakos', cmd='user'),
145
        )
146

    
147
        self.set('global', 'default_cloud', 'default')
148
        for s in self.sections():
149
            if s in ('global'):
150
                # global.url, global.token -->
151
                # cloud.default.url, cloud.default.token
152
                for term in set(self.keys(s)).difference(global_terms):
153
                    if term not in ('url', 'token'):
154
                        lost_terms.append('%s.%s = %s' % (
155
                            s, term, self.get(s, term)))
156
                        self.remove_option(s, term)
157
                        continue
158
                    gval = self.get(s, term)
159
                    cval = self.get_cloud('default', term)
160
                    if gval and cval and (
161
                        gval.lower().strip('/') != cval.lower().strip('/')):
162
                            raise CLISyntaxError(
163
                                'Conflicting values for default %s' % term,
164
                                importance=2, details=[
165
                                    ' global.%s:  %s' % (term, gval),
166
                                    ' cloud.default.%s:  %s' % (term, cval),
167
                                    'Please remove one of them manually:',
168
                                    ' /config delete global.%s' % term,
169
                                    ' or'
170
                                    ' /config delete cloud.default.%s' % term,
171
                                    'and try again'])
172
                    elif gval:
173
                        print('... rescue %s.%s => cloud.default.%s' % (
174
                            s, term, term))
175
                        self.set_cloud('default', term, gval)
176
                    self.remove_option(s, term)
177
            # translation for <service> or <command> settings
178
            # <service> or <command group> settings --> translation --> global
179
            elif s in translations:
180

    
181
                if s in ('history',):
182
                    k = 'file'
183
                    v = self.get(s, k)
184
                    if v:
185
                        print('... rescue %s.%s => global.%s_%s' % (
186
                            s, k, s, k))
187
                        self.set('global', '%s_%s' % (s, k), v)
188
                        self.remove_option(s, k)
189

    
190
                trn = translations[s]
191
                for k, v in self.items(s, False):
192
                    if v and k in ('cli',):
193
                        print('... rescue %s.%s => global.%s_cli' % (
194
                            s, k, trn['cmd']))
195
                        self.set('global', '%s_cli' % trn['cmd'], v)
196
                    elif k in ('container',) and trn['serv'] in ('pithos',):
197
                        print('... rescue %s.%s => cloud.default.pithos_%s' % (
198
                                    s, k, k))
199
                        self.set_cloud('default', 'pithos_%s' % k, v)
200
                    else:
201
                        lost_terms.append('%s.%s = %s' % (s, k, v))
202
                self.remove_section(s)
203
        #  self.pretty_print()
204
        return lost_terms
205

    
206
    def pretty_print(self):
207
        for s in self.sections():
208
            print s
209
            for k, v in self.items(s):
210
                if isinstance(v, dict):
211
                    print '\t', k, '=> {'
212
                    for ki, vi in v.items():
213
                        print '\t\t', ki, '=>', vi
214
                    print('\t}')
215
                else:
216
                    print '\t', k, '=>', v
217

    
218
    def guess_version(self):
219
        """
220
        :returns: (float) version of the config file or 0.0 if unrecognized
221
        """
222
        checker = Config(self.path, with_defaults=False)
223
        sections = checker.sections()
224
        log.warning('Config file heuristic 1: old global section ?')
225
        if 'global' in sections:
226
            if checker.get('global', 'url') or checker.get('global', 'token'):
227
                log.warning('..... config file has an old global section')
228
                return 8.0
229
        log.warning('........ nope')
230
        log.warning('Config file heuristic 2: missing all cloud sections ?')
231
        if 'cloud' in sections:
232
            for r in self.keys('cloud'):
233
                log.warning('... found cloud "%s"' % r)
234
                return 9.0
235
        log.warning('........ yep')
236
        log.warning('All heuristics failed, cannot decide')
237
        return 0.0
238

    
239
    def get_cloud(self, cloud, option):
240
        """
241
        :param cloud: (str) cloud alias
242

243
        :param option: (str) option in cloud section
244

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

247
        :raises KeyError: if cloud or cloud's option does not exist
248
        """
249
        r = self.get('cloud', cloud)
250
        if not r:
251
            raise KeyError('Cloud "%s" does not exist' % cloud)
252
        return r[option]
253

    
254
    def get_global(self, option):
255
        return self.get('global', option)
256

    
257
    def set_cloud(self, cloud, option, value):
258
        try:
259
            d = self.get('cloud', cloud) or dict()
260
        except KeyError:
261
            d = dict()
262
        d[option] = value
263
        self.set('cloud', cloud, d)
264

    
265
    def set_global(self, option, value):
266
        self.set('global', option, value)
267

    
268
    def _load_defaults(self):
269
        for section, options in DEFAULTS.items():
270
            for option, val in options.items():
271
                self.set(section, option, val)
272

    
273
    def _get_dict(self, section, include_defaults=True):
274
        try:
275
            d = dict(DEFAULTS[section]) if include_defaults else {}
276
        except KeyError:
277
            d = {}
278
        try:
279
            d.update(RawConfigParser.items(self, section))
280
        except NoSectionError:
281
            pass
282
        return d
283

    
284
    def reload(self):
285
        self = self.__init__(self.path)
286

    
287
    def get(self, section, option):
288
        """
289
        :param section: (str) HINT: for clouds, use cloud.<section>
290

291
        :param option: (str)
292

293
        :returns: (str) the value stored at section: {option: value}
294
        """
295
        value = self._overrides.get(section, {}).get(option)
296
        if value is not None:
297
            return value
298
        prefix = 'cloud.'
299
        if section.startswith(prefix):
300
            return self.get_cloud(section[len(prefix):], option)
301
        try:
302
            return RawConfigParser.get(self, section, option)
303
        except (NoSectionError, NoOptionError):
304
            return DEFAULTS.get(section, {}).get(option)
305

    
306
    def set(self, section, option, value):
307
        """
308
        :param section: (str) HINT: for remotes use cloud.<section>
309

310
        :param option: (str)
311

312
        :param value: str
313
        """
314
        prefix = 'cloud.'
315
        if section.startswith(prefix):
316
            return self.set_cloud(section[len(prefix)], option, value)
317
        if section not in RawConfigParser.sections(self):
318
            self.add_section(section)
319
        RawConfigParser.set(self, section, option, value)
320

    
321
    def remove_option(self, section, option, also_remove_default=False):
322
        try:
323
            if also_remove_default:
324
                DEFAULTS[section].pop(option)
325
            RawConfigParser.remove_option(self, section, option)
326
        except NoSectionError:
327
            pass
328

    
329
    def remote_from_cloud(self, cloud, option):
330
        d = self.get('cloud', cloud)
331
        if isinstance(d, dict):
332
            d.pop(option)
333

    
334
    def keys(self, section, include_defaults=True):
335
        d = self._get_dict(section, include_defaults)
336
        return d.keys()
337

    
338
    def items(self, section, include_defaults=True):
339
        d = self._get_dict(section, include_defaults)
340
        return d.items()
341

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

    
345
    def write(self):
346
        for r, d in self.items('cloud'):
347
            for k, v in d.items():
348
                self.set('cloud "%s"' % r, k, v)
349
        self.remove_section('cloud')
350

    
351
        with open(self.path, 'w') as f:
352
            os.chmod(self.path, 0600)
353
            f.write(HEADER.lstrip())
354
            f.flush()
355
            RawConfigParser.write(self, f)