Statistics
| Branch: | Tag: | Revision:

root / kamaki / cli / config.py @ fa479dc3

History | View | Annotate | Download (12.5 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 (k in ('container', 'uuid')) and (
191
                            trn['serv'] in ('pithos',)):
192
                        print(
193
                            '... rescue %s.%s => remote.default.pithos_%s' % (
194
                                    s, k, k))
195
                        self.set_remote('default', 'pithos_%s' % k, v)
196
                    elif v:
197
                        lost_terms.append('%s.%s = %s' % (s, k, v))
198
                self.remove_section(s)
199
        #  self.pretty_print()
200
        return lost_terms
201

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

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

    
232
    def get_remote(self, remote, option):
233
        """
234
        :param remote: (str) remote cloud alias
235

236
        :param option: (str) option in remote cloud section
237

238
        :returns: (str) the value assigned on this option
239

240
        :raises KeyError: if remote or remote's option does not exist
241
        """
242
        r = self.get('remote', remote)
243
        if not r:
244
            raise KeyError('Remote "%s" does not exist' % remote)
245
        return r[option]
246

    
247
    def get_global(self, option):
248
        return self.get('global', option)
249

    
250
    def set_remote(self, remote, option, value):
251
        try:
252
            d = self.get('remote', remote) or dict()
253
        except KeyError:
254
            d = dict()
255
        d[option] = value
256
        self.set('remote', remote, d)
257

    
258
    def set_global(self, option, value):
259
        self.set('global', option, value)
260

    
261
    def _load_defaults(self):
262
        for section, options in DEFAULTS.items():
263
            for option, val in options.items():
264
                self.set(section, option, val)
265

    
266
    def _get_dict(self, section, include_defaults=True):
267
        try:
268
            d = dict(DEFAULTS[section]) if include_defaults else {}
269
        except KeyError:
270
            d = {}
271
        try:
272
            d.update(RawConfigParser.items(self, section))
273
        except NoSectionError:
274
            pass
275
        return d
276

    
277
    def reload(self):
278
        self = self.__init__(self.path)
279

    
280
    def get(self, section, option):
281
        """
282
        :param section: (str) HINT: for remotes, use remote.<section>
283

284
        :param option: (str)
285

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

    
298
    def set(self, section, option, value):
299
        """
300
        :param section: (str) HINT: for remotes use remote.<section>
301

302
        :param option: (str)
303

304
        :param value: str
305
        """
306
        if section.startswith('remote.'):
307
            return self.set_remote(section[len('remote.')], option, value)
308
        if section not in RawConfigParser.sections(self):
309
            self.add_section(section)
310
        RawConfigParser.set(self, section, option, value)
311

    
312
    def remove_option(self, section, option, also_remove_default=False):
313
        try:
314
            if also_remove_default:
315
                DEFAULTS[section].pop(option)
316
            RawConfigParser.remove_option(self, section, option)
317
        except NoSectionError:
318
            pass
319

    
320
    def remove_from_remote(self, remote, option):
321
        d = self.get('remote', remote)
322
        if isinstance(d, dict):
323
            d.pop(option)
324

    
325
    def keys(self, section, include_defaults=True):
326
        d = self._get_dict(section, include_defaults)
327
        return d.keys()
328

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

    
333
    def override(self, section, option, value):
334
        self._overrides[section][option] = value
335

    
336
    def write(self):
337
        for r, d in self.items('remote'):
338
            for k, v in d.items():
339
                self.set('remote "%s"' % r, k, v)
340
        self.remove_section('remote')
341

    
342
        with open(self.path, 'w') as f:
343
            os.chmod(self.path, 0600)
344
            f.write(HEADER.lstrip())
345
            f.flush()
346
            RawConfigParser.write(self, f)