Statistics
| Branch: | Tag: | Revision:

root / kamaki / cli / config.py @ fa382f9e

History | View | Annotate | Download (13 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\n' % 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
try:
106
    import astakosclient
107
    DEFAULTS['global'].update(dict(astakos_cli='snf-astakos'))
108
except ImportError:
109
    pass
110

    
111

    
112
class Config(RawConfigParser):
113
    def __init__(self, path=None, with_defaults=True):
114
        RawConfigParser.__init__(self, dict_type=OrderedDict)
115
        self.path = path or os.environ.get(CONFIG_ENV, CONFIG_PATH)
116
        self._overrides = defaultdict(dict)
117
        if with_defaults:
118
            self._load_defaults()
119
        self.read(self.path)
120

    
121
        for section in self.sections():
122
            r = self._cloud_name(section)
123
            if r:
124
                for k, v in self.items(section):
125
                    self.set_cloud(r, k, v)
126
                self.remove_section(section)
127

    
128
    @staticmethod
129
    def _cloud_name(full_section_name):
130
        matcher = match('cloud "(\w+)"', full_section_name)
131
        return matcher.groups()[0] if matcher else None
132

    
133
    def rescue_old_file(self):
134
        lost_terms = []
135
        global_terms = DEFAULTS['global'].keys()
136
        translations = dict(
137
            config=dict(serv='', cmd='config'),
138
            history=dict(serv='', cmd='history'),
139
            pithos=dict(serv='pithos', cmd='file'),
140
            file=dict(serv='pithos', cmd='file'),
141
            store=dict(serv='pithos', cmd='file'),
142
            storage=dict(serv='pithos', cmd='file'),
143
            image=dict(serv='plankton', cmd='image'),
144
            plankton=dict(serv='plankton', cmd='image'),
145
            compute=dict(serv='compute', cmd=''),
146
            cyclades=dict(serv='compute', cmd='server'),
147
            server=dict(serv='compute', cmd='server'),
148
            flavor=dict(serv='compute', cmd='flavor'),
149
            network=dict(serv='compute', cmd='network'),
150
            astakos=dict(serv='astakos', cmd='user'),
151
            user=dict(serv='astakos', cmd='user'),
152
        )
153

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

    
191
                if s in ('history',):
192
                    k = 'file'
193
                    v = self.get(s, k)
194
                    if v:
195
                        print('... rescue %s.%s => global.%s_%s' % (
196
                            s, k, s, k))
197
                        self.set('global', '%s_%s' % (s, k), v)
198
                        self.remove_option(s, k)
199

    
200
                trn = translations[s]
201
                for k, v in self.items(s, False):
202
                    if v and k in ('cli',):
203
                        print('... rescue %s.%s => global.%s_cli' % (
204
                            s, k, trn['cmd']))
205
                        self.set('global', '%s_cli' % trn['cmd'], v)
206
                    elif k in ('container',) and trn['serv'] in ('pithos',):
207
                        print('... rescue %s.%s => cloud.default.pithos_%s' % (
208
                                    s, k, k))
209
                        self.set_cloud('default', 'pithos_%s' % k, v)
210
                    else:
211
                        lost_terms.append('%s.%s = %s' % (s, k, v))
212
                self.remove_section(s)
213
        #  self.pretty_print()
214
        return lost_terms
215

    
216
    def pretty_print(self):
217
        for s in self.sections():
218
            print s
219
            for k, v in self.items(s):
220
                if isinstance(v, dict):
221
                    print '\t', k, '=> {'
222
                    for ki, vi in v.items():
223
                        print '\t\t', ki, '=>', vi
224
                    print('\t}')
225
                else:
226
                    print '\t', k, '=>', v
227

    
228
    def guess_version(self):
229
        """
230
        :returns: (float) version of the config file or 0.0 if unrecognized
231
        """
232
        checker = Config(self.path, with_defaults=False)
233
        sections = checker.sections()
234
        log.warning('Config file heuristic 1: old global section ?')
235
        if 'global' in sections:
236
            if checker.get('global', 'url') or checker.get('global', 'token'):
237
                log.warning('..... config file has an old global section')
238
                return 0.8
239
        log.warning('........ nope')
240
        log.warning('Config file heuristic 2: Any cloud sections ?')
241
        if 'cloud' in sections:
242
            for r in self.keys('cloud'):
243
                log.warning('... found cloud "%s"' % r)
244
                return 0.9
245
        log.warning('........ nope')
246
        log.warning('All heuristics failed, cannot decide')
247
        return 0.9
248

    
249
    def get_cloud(self, cloud, option):
250
        """
251
        :param cloud: (str) cloud alias
252

253
        :param option: (str) option in cloud section
254

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

257
        :raises KeyError: if cloud or cloud's option does not exist
258
        """
259
        r = self.get('cloud', cloud)
260
        if not r:
261
            raise KeyError('Cloud "%s" does not exist' % cloud)
262
        return r[option]
263

    
264
    def get_global(self, option):
265
        return self.get('global', option)
266

    
267
    def set_cloud(self, cloud, option, value):
268
        try:
269
            d = self.get('cloud', cloud) or dict()
270
        except KeyError:
271
            d = dict()
272
        d[option] = value
273
        self.set('cloud', cloud, d)
274

    
275
    def set_global(self, option, value):
276
        self.set('global', option, value)
277

    
278
    def _load_defaults(self):
279
        for section, options in DEFAULTS.items():
280
            for option, val in options.items():
281
                self.set(section, option, val)
282

    
283
    def _get_dict(self, section, include_defaults=True):
284
        try:
285
            d = dict(DEFAULTS[section]) if include_defaults else {}
286
        except KeyError:
287
            d = {}
288
        try:
289
            d.update(RawConfigParser.items(self, section))
290
        except NoSectionError:
291
            pass
292
        return d
293

    
294
    def reload(self):
295
        self = self.__init__(self.path)
296

    
297
    def get(self, section, option):
298
        """
299
        :param section: (str) HINT: for clouds, use cloud.<section>
300

301
        :param option: (str)
302

303
        :returns: (str) the value stored at section: {option: value}
304
        """
305
        value = self._overrides.get(section, {}).get(option)
306
        if value is not None:
307
            return value
308
        prefix = 'cloud.'
309
        if section.startswith(prefix):
310
            return self.get_cloud(section[len(prefix):], option)
311
        try:
312
            return RawConfigParser.get(self, section, option)
313
        except (NoSectionError, NoOptionError):
314
            return DEFAULTS.get(section, {}).get(option)
315

    
316
    def set(self, section, option, value):
317
        """
318
        :param section: (str) HINT: for remotes use cloud.<section>
319

320
        :param option: (str)
321

322
        :param value: str
323
        """
324
        prefix = 'cloud.'
325
        if section.startswith(prefix):
326
            return self.set_cloud(section[len(prefix)], option, value)
327
        if section not in RawConfigParser.sections(self):
328
            self.add_section(section)
329
        RawConfigParser.set(self, section, option, value)
330

    
331
    def remove_option(self, section, option, also_remove_default=False):
332
        try:
333
            if also_remove_default:
334
                DEFAULTS[section].pop(option)
335
            RawConfigParser.remove_option(self, section, option)
336
        except NoSectionError:
337
            pass
338

    
339
    def remote_from_cloud(self, cloud, option):
340
        d = self.get('cloud', cloud)
341
        if isinstance(d, dict):
342
            d.pop(option)
343

    
344
    def keys(self, section, include_defaults=True):
345
        d = self._get_dict(section, include_defaults)
346
        return d.keys()
347

    
348
    def items(self, section, include_defaults=True):
349
        d = self._get_dict(section, include_defaults)
350
        return d.items()
351

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

    
355
    def write(self):
356
        for r, d in self.items('cloud'):
357
            for k, v in d.items():
358
                self.set('cloud "%s"' % r, k, v)
359
        self.remove_section('cloud')
360

    
361
        with open(self.path, 'w') as f:
362
            os.chmod(self.path, 0600)
363
            f.write(HEADER.lstrip())
364
            f.flush()
365
            RawConfigParser.write(self, f)