1 # Copyright 2011-2013 GRNET S.A. All rights reserved.
3 # Redistribution and use in source and binary forms, with or
4 # without modification, are permitted provided that the following
7 # 1. Redistributions of source code must retain the above
8 # copyright notice, this list of conditions and the following
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.
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.
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.
35 from logging import getLogger
36 from sys import stdout, stderr
38 from collections import defaultdict
39 from ConfigParser import RawConfigParser, NoOptionError, NoSectionError, Error
42 from kamaki.cli.errors import CLISyntaxError
43 from kamaki import __version__
46 from collections import OrderedDict
48 from kamaki.clients.utils.ordereddict import OrderedDict
51 class InvalidCloudNameError(Error):
52 """A valid cloud name must pass through this regex: ([~@#$:.-\w]+)"""
55 log = getLogger(__name__)
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'
62 # Name of a shell variable to bypass the CONFIG_PATH value
63 CONFIG_ENV = 'KAMAKI_CONFIG'
66 for c in '%s' % __version__:
67 if c not in '0.123456789':
70 HEADER = '# Kamaki configuration file v%s\n' % version
76 'log_file': os.path.expanduser('~/.kamaki.log'),
81 'history_file': HISTORY_PATH,
82 'user_cli': 'astakos',
84 'server_cli': 'cyclades',
85 'flavor_cli': 'cyclades',
86 'network_cli': 'cyclades',
89 'config_cli': 'config',
90 'history_cli': 'history'
91 # Optional command specs:
92 # 'livetest_cli': 'livetest',
93 # 'astakos_cli': 'snf-astakos'
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'
115 DEFAULTS['global'].update(dict(astakos_cli='snf-astakos'))
120 class Config(RawConfigParser):
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)
127 self._load_defaults()
130 for section in self.sections():
131 r = self._cloud_name(section)
133 for k, v in self.items(section):
134 self.set_cloud(r, k, v)
135 self.remove_section(section)
138 def _cloud_name(full_section_name):
139 if not full_section_name.startswith(CLOUD_PREFIX + ' '):
141 matcher = match(CLOUD_PREFIX + ' "([~@#$.:\-\w]+)"', full_section_name)
143 return matcher.groups()[0]
145 icn = full_section_name[len(CLOUD_PREFIX) + 1:]
146 raise InvalidCloudNameError('Invalid Cloud Name %s' % icn)
148 def rescue_old_file(self, err=stderr):
150 global_terms = DEFAULTS['global'].keys()
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'),
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)
180 gval = self.get(s, term)
181 default_cloud = self.get(
182 'global', 'default_cloud') or 'default'
184 cval = self.get_cloud(default_cloud, term)
187 if gval and cval and (
188 gval.lower().strip('/') != cval.lower().strip('/')):
189 raise CLISyntaxError(
190 'Conflicting values for default %s' % (
192 importance=2, details=[
193 ' global.%s: %s' % (term, gval),
199 'Please remove one of them manually:',
200 ' /config delete global.%s' % term,
202 ' /config delete %s.%s.%s' % (
203 CLOUD_PREFIX, default_cloud, term),
206 err.write(u'... rescue %s.%s => %s.%s.%s\n' % (
207 s, term, CLOUD_PREFIX, default_cloud, term))
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:
215 if s in ('history',):
219 err.write(u'... rescue %s.%s => global.%s_%s\n' % (
222 self.set('global', '%s_%s' % (s, k), v)
223 self.remove_option(s, k)
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' % (
231 self.set('global', '%s_cli' % trn['cmd'], v)
232 elif k in ('container',) and trn['serv'] in ('pithos',):
234 u'... rescue %s.%s => %s.default.pithos_%s\n' % (
235 s, k, CLOUD_PREFIX, k))
237 self.set_cloud('default', 'pithos_%s' % k, v)
239 lost_terms.append('%s.%s = %s' % (s, k, v))
240 self.remove_section(s)
241 # self.pretty_print()
244 def pretty_print(self, out=stdout):
245 for s in self.sections():
248 for k, v in self.items(s):
249 if isinstance(v, dict):
250 out.write(u'\t%s => {\n' % k)
252 for ki, vi in v.items():
253 out.write(u'\t\t%s => %s\n' % (ki, vi))
257 out.write(u'\t %s => %s\n' % (k, v))
260 def guess_version(self):
262 :returns: (float) version of the config file or 0.9 if unrecognized
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')
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)
277 log.debug('........ nope')
278 log.debug('All heuristics failed, cannot decide')
281 def get_cloud(self, cloud, option):
283 :param cloud: (str) cloud alias
285 :param option: (str) option in cloud section
287 :returns: (str) the value assigned on this option
289 :raises KeyError: if cloud or cloud's option does not exist
291 r = self.get(CLOUD_PREFIX, cloud)
293 raise KeyError('Cloud "%s" does not exist' % cloud)
296 def set_cloud(self, cloud, option, value):
298 d = self.get(CLOUD_PREFIX, cloud) or dict()
302 self.set(CLOUD_PREFIX, cloud, d)
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)
309 def _get_dict(self, section, include_defaults=True):
311 d = dict(DEFAULTS[section]) if include_defaults else {}
315 d.update(RawConfigParser.items(self, section))
316 except NoSectionError:
321 self = self.__init__(self.path)
323 def get(self, section, option):
325 :param section: (str) HINT: for clouds, use cloud.<section>
329 :returns: (str) the value stored at section: {option: value}
331 value = self._overrides.get(section, {}).get(option)
332 if value is not None:
334 prefix = CLOUD_PREFIX + '.'
335 if section.startswith(prefix):
336 return self.get_cloud(section[len(prefix):], option)
338 return RawConfigParser.get(self, section, option)
339 except (NoSectionError, NoOptionError):
340 return DEFAULTS.get(section, {}).get(option)
342 def set(self, section, option, value):
344 :param section: (str) HINT: for remotes use cloud.<section>
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)
359 def remove_option(self, section, option, also_remove_default=False):
361 if also_remove_default:
362 DEFAULTS[section].pop(option)
363 RawConfigParser.remove_option(self, section, option)
364 except (NoSectionError, KeyError):
367 def remove_from_cloud(self, cloud, option):
368 d = self.get(CLOUD_PREFIX, cloud)
369 if isinstance(d, dict):
372 def keys(self, section, include_defaults=True):
373 d = self._get_dict(section, include_defaults)
376 def items(self, section, include_defaults=True):
377 d = self._get_dict(section, include_defaults)
380 def override(self, section, option, value):
381 self._overrides[section][option] = value
384 cld_bu = self._get_dict(CLOUD_PREFIX)
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)
391 with open(self.path, 'w') as f:
392 os.chmod(self.path, 0600)
393 f.write(HEADER.lstrip())
395 RawConfigParser.write(self, f)
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)