Adjust setup
[kamaki] / kamaki / cli / config / __init__.py
1 # Copyright 2011-2013 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 from sys import stdout, stderr
37
38 from collections import defaultdict
39 from ConfigParser import RawConfigParser, NoOptionError, NoSectionError, Error
40 from re import match
41
42 from kamaki.cli.errors import CLISyntaxError
43 from kamaki import __version__
44
45 try:
46     from collections import OrderedDict
47 except ImportError:
48     from kamaki.clients.utils.ordereddict import OrderedDict
49
50
51 class InvalidCloudNameError(Error):
52     """A valid cloud name must pass through this regex: ([~@#$:.-\w]+)"""
53
54
55 log = getLogger(__name__)
56
57 # Path to the file that stores the configuration
58 CONFIG_PATH = os.path.expanduser('~/.kamakirc')
59 HISTORY_PATH = os.path.expanduser('~/.kamaki.history')
60 CLOUD_PREFIX = 'cloud'
61
62 # Name of a shell variable to bypass the CONFIG_PATH value
63 CONFIG_ENV = 'KAMAKI_CONFIG'
64
65 version = ''
66 for c in '%s' % __version__:
67     if c not in '0.123456789':
68         break
69     version += c
70 HEADER = '# Kamaki configuration file v%s\n' % version
71
72 DEFAULTS = {
73     'global': {
74         'default_cloud': '',
75         'colors': 'off',
76         'log_file': os.path.expanduser('~/.kamaki.log'),
77         'log_token': 'off',
78         'log_data': 'off',
79         'log_pid': 'off',
80         'history_file': HISTORY_PATH,
81         'user_cli': 'astakos',
82         'quota_cli': 'astakos',
83         'resource_cli': 'astakos',
84         'project_cli': 'astakos',
85         'file_cli': 'pithos',
86         'container_cli': 'pithos',
87         'sharer_cli': 'pithos',
88         'group_cli': 'pithos',
89         'server_cli': 'cyclades',
90         'flavor_cli': 'cyclades',
91         'network_cli': 'network',
92         'subnet_cli': 'network',
93         'port_cli': 'network',
94         'ip_cli': 'network',
95         'image_cli': 'image',
96         'imagecompute_cli': 'image',
97         'config_cli': 'config',
98         'history_cli': 'history'
99         #  Optional command specs:
100         #  'livetest_cli': 'livetest',
101         #  'service_cli': 'astakos'
102         #  'endpoint_cli': 'astakos'
103         #  'commission_cli': 'astakos'
104     },
105     CLOUD_PREFIX: {
106         #'default': {
107         #    'url': '',
108         #    'token': ''
109         #    'pithos_container': 'THIS IS DANGEROUS'
110         #    'pithos_type': 'object-store',
111         #    'pithos_version': 'v1',
112         #    'cyclades_type': 'compute',
113         #    'cyclades_version': 'v2.0',
114         #    'plankton_type': 'image',
115         #    'plankton_version': '',
116         #    'astakos_type': 'identity',
117         #    'astakos_version': 'v2.0'
118         #}
119     }
120 }
121
122
123 class Config(RawConfigParser):
124
125     def __init__(self, path=None, with_defaults=True):
126         RawConfigParser.__init__(self, dict_type=OrderedDict)
127         self.path = path or os.environ.get(CONFIG_ENV, CONFIG_PATH)
128         self._overrides = defaultdict(dict)
129         if with_defaults:
130             self._load_defaults()
131         self.read(self.path)
132
133         for section in self.sections():
134             r = self._cloud_name(section)
135             if r:
136                 for k, v in self.items(section):
137                     self.set_cloud(r, k, v)
138                 self.remove_section(section)
139
140     @staticmethod
141     def _cloud_name(full_section_name):
142         if not full_section_name.startswith(CLOUD_PREFIX + ' '):
143             return None
144         matcher = match(CLOUD_PREFIX + ' "([~@#$.:\-\w]+)"', full_section_name)
145         if matcher:
146             return matcher.groups()[0]
147         else:
148             icn = full_section_name[len(CLOUD_PREFIX) + 1:]
149             raise InvalidCloudNameError('Invalid Cloud Name %s' % icn)
150
151     def rescue_old_file(self, err=stderr):
152         lost_terms = []
153         global_terms = DEFAULTS['global'].keys()
154         translations = dict(
155             config=dict(serv='', cmd='config'),
156             history=dict(serv='', cmd='history'),
157             pithos=dict(serv='pithos', cmd='file'),
158             file=dict(serv='pithos', cmd='file'),
159             store=dict(serv='pithos', cmd='file'),
160             storage=dict(serv='pithos', cmd='file'),
161             image=dict(serv='plankton', cmd='image'),
162             plankton=dict(serv='plankton', cmd='image'),
163             compute=dict(serv='compute', cmd=''),
164             cyclades=dict(serv='compute', cmd='server'),
165             server=dict(serv='compute', cmd='server'),
166             flavor=dict(serv='compute', cmd='flavor'),
167             network=dict(serv='network', cmd='network'),
168             astakos=dict(serv='astakos', cmd='user'),
169             user=dict(serv='astakos', cmd='user'),
170         )
171
172         dc = 'default_' + CLOUD_PREFIX
173         self.set('global', dc, self.get('global', dc) or 'default')
174         for s in self.sections():
175             if s in ('global', ):
176                 # global.url, global.token -->
177                 # cloud.default.url, cloud.default.token
178                 for term in set(self.keys(s)).difference(global_terms):
179                     if term not in ('url', 'token'):
180                         lost_terms.append('%s.%s = %s' % (
181                             s, term, self.get(s, term)))
182                         self.remove_option(s, term)
183                         continue
184                     gval = self.get(s, term)
185                     default_cloud = self.get(
186                         'global', 'default_cloud') or 'default'
187                     try:
188                         cval = self.get_cloud(default_cloud, term)
189                     except KeyError:
190                         cval = ''
191                     if gval and cval and (
192                         gval.lower().strip('/') != cval.lower().strip('/')):
193                             raise CLISyntaxError(
194                                 'Conflicting values for default %s' % (
195                                     term),
196                                 importance=2, details=[
197                                     ' global.%s:  %s' % (term, gval),
198                                     ' %s.%s.%s:  %s' % (
199                                         CLOUD_PREFIX,
200                                         default_cloud,
201                                         term,
202                                         cval),
203                                     'Please remove one of them manually:',
204                                     ' /config delete global.%s' % term,
205                                     ' or'
206                                     ' /config delete %s.%s.%s' % (
207                                         CLOUD_PREFIX, default_cloud, term),
208                                     'and try again'])
209                     elif gval:
210                         err.write(u'... rescue %s.%s => %s.%s.%s\n' % (
211                             s, term, CLOUD_PREFIX, default_cloud, term))
212                         err.flush()
213                         self.set_cloud('default', term, gval)
214                     self.remove_option(s, term)
215                 print 'CHECK'
216                 for term, wrong, right in (
217                         ('ip', 'cyclades', 'network'),
218                         ('network', 'cyclades', 'network'),):
219                     k = '%s_cli' % term
220                     v = self.get(s, k)
221                     if v in (wrong, ):
222                         err.write('... change %s.%s value: `%s` => `%s`\n' % (
223                             s, k, wrong, right))
224                         err.flush()
225                         self.set(s, k, right)
226             # translation for <service> or <command> settings
227             # <service> or <command group> settings --> translation --> global
228             elif s in translations:
229
230                 if s in ('history',):
231                     k = 'file'
232                     v = self.get(s, k)
233                     if v:
234                         err.write(u'... rescue %s.%s => global.%s_%s\n' % (
235                             s, k, s, k))
236                         err.flush()
237                         self.set('global', '%s_%s' % (s, k), v)
238                         self.remove_option(s, k)
239
240                 trn = translations[s]
241                 for k, v in self.items(s, False):
242                     if v and k in ('cli',):
243                         err.write(u'... rescue %s.%s => global.%s_cli\n' % (
244                             s, k, trn['cmd']))
245                         err.flush()
246                         self.set('global', '%s_cli' % trn['cmd'], v)
247                     elif k in ('container',) and trn['serv'] in ('pithos',):
248                         err.write(
249                             u'... rescue %s.%s => %s.default.pithos_%s\n' % (
250                                 s, k, CLOUD_PREFIX, k))
251                         err.flush()
252                         self.set_cloud('default', 'pithos_%s' % k, v)
253                     else:
254                         lost_terms.append('%s.%s = %s' % (s, k, v))
255                 self.remove_section(s)
256         #  self.pretty_print()
257         return lost_terms
258
259     def pretty_print(self, out=stdout):
260         for s in self.sections():
261             out.write(s)
262             out.flush()
263             for k, v in self.items(s):
264                 if isinstance(v, dict):
265                     out.write(u'\t%s => {\n' % k)
266                     out.flush()
267                     for ki, vi in v.items():
268                         out.write(u'\t\t%s => %s\n' % (ki, vi))
269                         out.flush()
270                     out.write(u'\t}\n')
271                 else:
272                     out.write(u'\t %s => %s\n' % (k, v))
273                 out.flush()
274
275     def guess_version(self):
276         """
277         :returns: (float) version of the config file or 0.9 if unrecognized
278         """
279         checker = Config(self.path, with_defaults=False)
280         sections = checker.sections()
281         log.debug('Config file heuristic 1: old global section ?')
282         if 'global' in sections:
283             if checker.get('global', 'url') or checker.get('global', 'token'):
284                 log.debug('..... config file has an old global section')
285                 return 0.8
286         log.debug('........ nope')
287         log.debug('Config file heuristic 2: Any cloud sections ?')
288         if CLOUD_PREFIX in sections:
289             for r in self.keys(CLOUD_PREFIX):
290                 log.debug('... found cloud "%s"' % r)
291             ipv = self.get('global', 'ip_cli')
292             if ipv in ('cyclades', ):
293                     return 0.11
294             netv = self.get('global', 'network_cli')
295             if netv in ('cyclades', ):
296                 return 0.10
297             return 0.12
298         log.debug('........ nope')
299         log.debug('All heuristics failed, cannot decide')
300         return 0.12
301
302     def get_cloud(self, cloud, option):
303         """
304         :param cloud: (str) cloud alias
305
306         :param option: (str) option in cloud section
307
308         :returns: (str) the value assigned on this option
309
310         :raises KeyError: if cloud or cloud's option does not exist
311         """
312         r = self.get(CLOUD_PREFIX, cloud) if cloud else None
313         if not r:
314             raise KeyError('Cloud "%s" does not exist' % cloud)
315         return r[option]
316
317     def set_cloud(self, cloud, option, value):
318         try:
319             d = self.get(CLOUD_PREFIX, cloud) or dict()
320         except KeyError:
321             d = dict()
322         d[option] = value
323         self.set(CLOUD_PREFIX, cloud, d)
324
325     def _load_defaults(self):
326         for section, options in DEFAULTS.items():
327             for option, val in options.items():
328                 self.set(section, option, val)
329
330     def _get_dict(self, section, include_defaults=True):
331         try:
332             d = dict(DEFAULTS[section]) if include_defaults else {}
333         except KeyError:
334             d = {}
335         try:
336             d.update(RawConfigParser.items(self, section))
337         except NoSectionError:
338             pass
339         return d
340
341     def reload(self):
342         self = self.__init__(self.path)
343
344     def get(self, section, option):
345         """
346         :param section: (str) HINT: for clouds, use cloud.<section>
347
348         :param option: (str)
349
350         :returns: (str) the value stored at section: {option: value}
351         """
352         value = self._overrides.get(section, {}).get(option)
353         if value is not None:
354             return value
355         prefix = CLOUD_PREFIX + '.'
356         if section.startswith(prefix):
357             return self.get_cloud(section[len(prefix):], option)
358         try:
359             return RawConfigParser.get(self, section, option)
360         except (NoSectionError, NoOptionError):
361             return DEFAULTS.get(section, {}).get(option)
362
363     def set(self, section, option, value):
364         """
365         :param section: (str) HINT: for remotes use cloud.<section>
366
367         :param option: (str)
368
369         :param value: str
370         """
371         prefix = CLOUD_PREFIX + '.'
372         if section.startswith(prefix):
373             cloud = self._cloud_name(
374                 CLOUD_PREFIX + ' "' + section[len(prefix):] + '"')
375             return self.set_cloud(cloud, option, value)
376         if section not in RawConfigParser.sections(self):
377             self.add_section(section)
378         return RawConfigParser.set(self, section, option, value)
379
380     def remove_option(self, section, option, also_remove_default=False):
381         try:
382             if also_remove_default:
383                 DEFAULTS[section].pop(option)
384             RawConfigParser.remove_option(self, section, option)
385         except (NoSectionError, KeyError):
386             pass
387
388     def remove_from_cloud(self, cloud, option):
389         d = self.get(CLOUD_PREFIX, cloud)
390         if isinstance(d, dict):
391             d.pop(option)
392
393     def keys(self, section, include_defaults=True):
394         d = self._get_dict(section, include_defaults)
395         return d.keys()
396
397     def items(self, section, include_defaults=True):
398         d = self._get_dict(section, include_defaults)
399         return d.items()
400
401     def override(self, section, option, value):
402         self._overrides[section][option] = value
403
404     def write(self):
405         cld_bu = self._get_dict(CLOUD_PREFIX)
406         try:
407             for r, d in self.items(CLOUD_PREFIX):
408                 for k, v in d.items():
409                     self.set(CLOUD_PREFIX + ' "%s"' % r, k, v)
410             self.remove_section(CLOUD_PREFIX)
411
412             with open(self.path, 'w') as f:
413                 os.chmod(self.path, 0600)
414                 f.write(HEADER.lstrip())
415                 f.flush()
416                 RawConfigParser.write(self, f)
417         finally:
418             if CLOUD_PREFIX not in self.sections():
419                 self.add_section(CLOUD_PREFIX)
420             for cloud, d in cld_bu.items():
421                 self.set(CLOUD_PREFIX, cloud, d)