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