Statistics
| Branch: | Tag: | Revision:

root / kamaki / cli / config.py @ c825ddc9

History | View | Annotate | Download (12.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
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
class Config(RawConfigParser):
106
    def __init__(self, path=None, with_defaults=True):
107
        RawConfigParser.__init__(self, dict_type=OrderedDict)
108
        self.path = path or os.environ.get(CONFIG_ENV, CONFIG_PATH)
109
        self._overrides = defaultdict(dict)
110
        if with_defaults:
111
            self._load_defaults()
112
        self.read(self.path)
113

    
114
        for section in self.sections():
115
            r = self._cloud_name(section)
116
            if r:
117
                for k, v in self.items(section):
118
                    self.set_cloud(r, k, v)
119
                self.remove_section(section)
120

    
121
    @staticmethod
122
    def _cloud_name(full_section_name):
123
        matcher = match('cloud "(\w+)"', full_section_name)
124
        return matcher.groups()[0] if matcher else None
125

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

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

    
184
                if s in ('history',):
185
                    k = 'file'
186
                    v = self.get(s, k)
187
                    if v:
188
                        print('... rescue %s.%s => global.%s_%s' % (
189
                            s, k, s, k))
190
                        self.set('global', '%s_%s' % (s, k), v)
191
                        self.remove_option(s, k)
192

    
193
                trn = translations[s]
194
                for k, v in self.items(s, False):
195
                    if v and k in ('cli',):
196
                        print('... rescue %s.%s => global.%s_cli' % (
197
                            s, k, trn['cmd']))
198
                        self.set('global', '%s_cli' % trn['cmd'], v)
199
                    elif k in ('container',) and trn['serv'] in ('pithos',):
200
                        print('... rescue %s.%s => cloud.default.pithos_%s' % (
201
                                    s, k, k))
202
                        self.set_cloud('default', 'pithos_%s' % k, v)
203
                    else:
204
                        lost_terms.append('%s.%s = %s' % (s, k, v))
205
                self.remove_section(s)
206
        #  self.pretty_print()
207
        return lost_terms
208

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

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

    
242
    def get_cloud(self, cloud, option):
243
        """
244
        :param cloud: (str) cloud alias
245

246
        :param option: (str) option in cloud section
247

248
        :returns: (str) the value assigned on this option
249

250
        :raises KeyError: if cloud or cloud's option does not exist
251
        """
252
        r = self.get('cloud', cloud)
253
        if not r:
254
            raise KeyError('Cloud "%s" does not exist' % cloud)
255
        return r[option]
256

    
257
    def get_global(self, option):
258
        return self.get('global', option)
259

    
260
    def set_cloud(self, cloud, option, value):
261
        try:
262
            d = self.get('cloud', cloud) or dict()
263
        except KeyError:
264
            d = dict()
265
        d[option] = value
266
        self.set('cloud', cloud, d)
267

    
268
    def set_global(self, option, value):
269
        self.set('global', option, value)
270

    
271
    def _load_defaults(self):
272
        for section, options in DEFAULTS.items():
273
            for option, val in options.items():
274
                self.set(section, option, val)
275

    
276
    def _get_dict(self, section, include_defaults=True):
277
        try:
278
            d = dict(DEFAULTS[section]) if include_defaults else {}
279
        except KeyError:
280
            d = {}
281
        try:
282
            d.update(RawConfigParser.items(self, section))
283
        except NoSectionError:
284
            pass
285
        return d
286

    
287
    def reload(self):
288
        self = self.__init__(self.path)
289

    
290
    def get(self, section, option):
291
        """
292
        :param section: (str) HINT: for clouds, use cloud.<section>
293

294
        :param option: (str)
295

296
        :returns: (str) the value stored at section: {option: value}
297
        """
298
        value = self._overrides.get(section, {}).get(option)
299
        if value is not None:
300
            return value
301
        prefix = 'cloud.'
302
        if section.startswith(prefix):
303
            return self.get_cloud(section[len(prefix):], option)
304
        try:
305
            return RawConfigParser.get(self, section, option)
306
        except (NoSectionError, NoOptionError):
307
            return DEFAULTS.get(section, {}).get(option)
308

    
309
    def set(self, section, option, value):
310
        """
311
        :param section: (str) HINT: for remotes use cloud.<section>
312

313
        :param option: (str)
314

315
        :param value: str
316
        """
317
        prefix = 'cloud.'
318
        if section.startswith(prefix):
319
            return self.set_cloud(section[len(prefix)], option, value)
320
        if section not in RawConfigParser.sections(self):
321
            self.add_section(section)
322
        RawConfigParser.set(self, section, option, value)
323

    
324
    def remove_option(self, section, option, also_remove_default=False):
325
        try:
326
            if also_remove_default:
327
                DEFAULTS[section].pop(option)
328
            RawConfigParser.remove_option(self, section, option)
329
        except NoSectionError:
330
            pass
331

    
332
    def remote_from_cloud(self, cloud, option):
333
        d = self.get('cloud', cloud)
334
        if isinstance(d, dict):
335
            d.pop(option)
336

    
337
    def keys(self, section, include_defaults=True):
338
        d = self._get_dict(section, include_defaults)
339
        return d.keys()
340

    
341
    def items(self, section, include_defaults=True):
342
        d = self._get_dict(section, include_defaults)
343
        return d.items()
344

    
345
    def override(self, section, option, value):
346
        self._overrides[section][option] = value
347

    
348
    def write(self):
349
        for r, d in self.items('cloud'):
350
            for k, v in d.items():
351
                self.set('cloud "%s"' % r, k, v)
352
        self.remove_section('cloud')
353

    
354
        with open(self.path, 'w') as f:
355
            os.chmod(self.path, 0600)
356
            f.write(HEADER.lstrip())
357
            f.flush()
358
            RawConfigParser.write(self, f)