Statistics
| Branch: | Tag: | Revision:

root / kamaki / cli / config.py @ 844a6bdb

History | View | Annotate | Download (11.9 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 v:
196
                        lost_terms.append('%s.%s = %s' % (s, k, v))
197
                self.remove_section(s)
198
        #  self.pretty_print()
199
        return lost_terms
200

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

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

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

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

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

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

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

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

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

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

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

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

    
279
    def get(self, section, option):
280
        value = self._overrides.get(section, {}).get(option)
281
        if value is not None:
282
            return value
283

    
284
        try:
285
            return RawConfigParser.get(self, section, option)
286
        except (NoSectionError, NoOptionError):
287
            return DEFAULTS.get(section, {}).get(option)
288

    
289
    def set(self, section, option, value):
290
        if section not in RawConfigParser.sections(self):
291
            self.add_section(section)
292
        RawConfigParser.set(self, section, option, value)
293

    
294
    def remove_option(self, section, option, also_remove_default=False):
295
        try:
296
            if also_remove_default:
297
                DEFAULTS[section].pop(option)
298
            RawConfigParser.remove_option(self, section, option)
299
        except NoSectionError:
300
            pass
301

    
302
    def remove_from_remote(self, remote, option):
303
        d = self.get('remote', remote)
304
        if isinstance(d, dict):
305
            d.pop(option)
306

    
307
    def keys(self, section, include_defaults=True):
308
        d = self._get_dict(section, include_defaults)
309
        return d.keys()
310

    
311
    def items(self, section, include_defaults=True):
312
        d = self._get_dict(section, include_defaults)
313
        return d.items()
314

    
315
    def override(self, section, option, value):
316
        self._overrides[section][option] = value
317

    
318
    def write(self):
319
        for r, d in self.items('remote'):
320
            for k, v in d.items():
321
                self.set('remote "%s"' % r, k, v)
322
        self.remove_section('remote')
323

    
324
        with open(self.path, 'w') as f:
325
            os.chmod(self.path, 0600)
326
            f.write(HEADER.lstrip())
327
            f.flush()
328
            RawConfigParser.write(self, f)