Statistics
| Branch: | Tag: | Revision:

root / kamaki / cli / config.py @ 114e19da

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

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

    
48

    
49
log = getLogger(__name__)
50

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

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

    
58
HEADER = """
59
# Kamaki configuration file v3 (kamaki >= v0.9)
60
"""
61

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

    
99

    
100
class Config(RawConfigParser):
101
    def __init__(self, path=None, with_defaults=True):
102
        RawConfigParser.__init__(self, dict_type=OrderedDict)
103
        self.path = path or os.environ.get(CONFIG_ENV, CONFIG_PATH)
104
        self._overrides = defaultdict(dict)
105
        if with_defaults:
106
            self._load_defaults()
107
        self.read(self.path)
108

    
109
        for section in self.sections():
110
            r = self._remote_name(section)
111
            if r:
112
                for k, v in self.items(section):
113
                    self.set_remote(r, k, v)
114
                self.remove_section(section)
115

    
116
    @staticmethod
117
    def _remote_name(full_section_name):
118
        matcher = match('remote "(\w+)"', full_section_name)
119
        return matcher.groups()[0] if matcher else None
120

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

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

    
175
                if s in ('history',):
176
                    k = 'file'
177
                    v = self.get(s, k)
178
                    if v:
179
                        print('... rescue %s.%s => global.%s_%s' % (
180
                            s, k, s, k))
181
                        self.set('global', '%s_%s' % (s, k), v)
182
                        self.remove_option(s, k)
183

    
184
                trn = translations[s]
185
                for k, v in self.items(s, False):
186
                    if v and k in ('cli',):
187
                        print('... rescue %s.%s => global.%s_cli' % (
188
                            s, k, trn['cmd']))
189
                        self.set('global', 'file_cli', v)
190
                    elif v and k in ('url', 'token'):
191
                        print(
192
                            '... rescue %s.%s => remote.default.%s_%s' % (
193
                                s, k, trn['serv'], k))
194
                        self.set_remote('default', 'pithos_%s' % k, v)
195
                    elif (k in ('container', 'uuid')) and (
196
                            trn['serv'] in ('pithos',)):
197
                        print(
198
                            '... rescue %s.%s => remote.default.pithos_%s' % (
199
                                    s, k, k))
200
                        self.set_remote('default', 'pithos_%s' % k, v)
201
                    elif v:
202
                        lost_terms.append('%s.%s = %s' % (s, k, v))
203
                self.remove_section(s)
204
        #  self.pretty_print()
205
        return lost_terms
206

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

    
219
    def guess_version(self):
220
        checker = Config(self.path, with_defaults=False)
221
        sections = checker.sections()
222
        log.warning('Config file heuristic 1: global section ?')
223
        if 'global' in sections:
224
            if checker.get('global', 'url') or checker.get('global', 'token'):
225
                log.warning('..... config file has an old global section')
226
                return 2.0
227
        log.warning('........ nope')
228
        log.warning('Config file heuristic 2: at least 1 remote section ?')
229
        if 'remote' in sections:
230
            for r in self.keys('remote'):
231
                log.warning('... found remote "%s"' % r)
232
                return 3.0
233
        log.warning('........ nope')
234
        log.warning('All heuristics failed, cannot decide')
235
        return 0.0
236

    
237
    def get_remote(self, remote, option):
238
        """
239
        :param remote: (str) remote cloud alias
240

241
        :param option: (str) option in remote cloud section
242

243
        :returns: (str) the value assigned on this option
244

245
        :raises KeyError: if remote or remote's option does not exist
246
        """
247
        r = self.get('remote', remote)
248
        if not r:
249
            raise KeyError('Remote "%s" does not exist' % remote)
250
        return r[option]
251

    
252
    def get_global(self, option):
253
        return self.get('global', option)
254

    
255
    def set_remote(self, remote, option, value):
256
        try:
257
            d = self.get('remote', remote) or dict()
258
        except KeyError:
259
            d = dict()
260
        d[option] = value
261
        self.set('remote', remote, d)
262

    
263
    def set_global(self, option, value):
264
        self.set('global', option, value)
265

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

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

    
282
    def reload(self):
283
        self = self.__init__(self.path)
284

    
285
    def get(self, section, option):
286
        """
287
        :param section: (str) HINT: for remotes, use remote.<section>
288

289
        :param option: (str)
290

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

    
303
    def set(self, section, option, value):
304
        """
305
        :param section: (str) HINT: for remotes use remote.<section>
306

307
        :param option: (str)
308

309
        :param value: str
310
        """
311
        if section.startswith('remote.'):
312
            return self.set_remote(section[len('remote.')], option, value)
313
        if section not in RawConfigParser.sections(self):
314
            self.add_section(section)
315
        RawConfigParser.set(self, section, option, value)
316

    
317
    def remove_option(self, section, option, also_remove_default=False):
318
        try:
319
            if also_remove_default:
320
                DEFAULTS[section].pop(option)
321
            RawConfigParser.remove_option(self, section, option)
322
        except NoSectionError:
323
            pass
324

    
325
    def remove_from_remote(self, remote, option):
326
        d = self.get('remote', remote)
327
        if isinstance(d, dict):
328
            d.pop(option)
329

    
330
    def keys(self, section, include_defaults=True):
331
        d = self._get_dict(section, include_defaults)
332
        return d.keys()
333

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

    
338
    def override(self, section, option, value):
339
        self._overrides[section][option] = value
340

    
341
    def write(self):
342
        for r, d in self.items('remote'):
343
            for k, v in d.items():
344
                self.set('remote "%s"' % r, k, v)
345
        self.remove_section('remote')
346

    
347
        with open(self.path, 'w') as f:
348
            os.chmod(self.path, 0600)
349
            f.write(HEADER.lstrip())
350
            f.flush()
351
            RawConfigParser.write(self, f)