Statistics
| Branch: | Tag: | Revision:

root / kamaki / cli / config.py @ ca5b9261

History | View | Annotate | Download (12.3 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
        value = self._overrides.get(section, {}).get(option)
287
        if value is not None:
288
            return value
289

    
290
        try:
291
            return RawConfigParser.get(self, section, option)
292
        except (NoSectionError, NoOptionError):
293
            return DEFAULTS.get(section, {}).get(option)
294

    
295
    def set(self, section, option, value):
296
        if section not in RawConfigParser.sections(self):
297
            self.add_section(section)
298
        RawConfigParser.set(self, section, option, value)
299

    
300
    def remove_option(self, section, option, also_remove_default=False):
301
        try:
302
            if also_remove_default:
303
                DEFAULTS[section].pop(option)
304
            RawConfigParser.remove_option(self, section, option)
305
        except NoSectionError:
306
            pass
307

    
308
    def remove_from_remote(self, remote, option):
309
        d = self.get('remote', remote)
310
        if isinstance(d, dict):
311
            d.pop(option)
312

    
313
    def keys(self, section, include_defaults=True):
314
        d = self._get_dict(section, include_defaults)
315
        return d.keys()
316

    
317
    def items(self, section, include_defaults=True):
318
        d = self._get_dict(section, include_defaults)
319
        return d.items()
320

    
321
    def override(self, section, option, value):
322
        self._overrides[section][option] = value
323

    
324
    def write(self):
325
        for r, d in self.items('remote'):
326
            for k, v in d.items():
327
                self.set('remote "%s"' % r, k, v)
328
        self.remove_section('remote')
329

    
330
        with open(self.path, 'w') as f:
331
            os.chmod(self.path, 0600)
332
            f.write(HEADER.lstrip())
333
            f.flush()
334
            RawConfigParser.write(self, f)