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',
88 'config_cli': 'config',
89 'history_cli': 'history'
90 # Optional command specs:
91 # 'livetest_cli': 'livetest',
92 # 'astakos_cli': 'snf-astakos'
98 # 'pithos_type': 'object-store',
99 # 'pithos_version': 'v1',
100 # 'cyclades_type': 'compute',
101 # 'cyclades_version': 'v2.0',
102 # 'plankton_type': 'image',
103 # 'plankton_version': '',
104 # 'astakos_type': 'identity',
105 # 'astakos_version': 'v2.0'
113 DEFAULTS['global'].update(dict(astakos_cli='snf-astakos'))
118 class Config(RawConfigParser):
120 def __init__(self, path=None, with_defaults=True):
121 RawConfigParser.__init__(self, dict_type=OrderedDict)
122 self.path = path or os.environ.get(CONFIG_ENV, CONFIG_PATH)
123 self._overrides = defaultdict(dict)
125 self._load_defaults()
128 for section in self.sections():
129 r = self._cloud_name(section)
131 for k, v in self.items(section):
132 self.set_cloud(r, k, v)
133 self.remove_section(section)
136 def _cloud_name(full_section_name):
137 if not full_section_name.startswith(CLOUD_PREFIX + ' '):
139 matcher = match(CLOUD_PREFIX + ' "([~@#$.:\-\w]+)"', full_section_name)
141 return matcher.groups()[0]
143 icn = full_section_name[len(CLOUD_PREFIX) + 1:]
144 raise InvalidCloudNameError('Invalid Cloud Name %s' % icn)
146 def rescue_old_file(self, err=stderr):
148 global_terms = DEFAULTS['global'].keys()
150 config=dict(serv='', cmd='config'),
151 history=dict(serv='', cmd='history'),
152 pithos=dict(serv='pithos', cmd='file'),
153 file=dict(serv='pithos', cmd='file'),
154 store=dict(serv='pithos', cmd='file'),
155 storage=dict(serv='pithos', cmd='file'),
156 image=dict(serv='plankton', cmd='image'),
157 plankton=dict(serv='plankton', cmd='image'),
158 compute=dict(serv='compute', cmd=''),
159 cyclades=dict(serv='compute', cmd='server'),
160 server=dict(serv='compute', cmd='server'),
161 flavor=dict(serv='compute', cmd='flavor'),
162 network=dict(serv='compute', cmd='network'),
163 astakos=dict(serv='astakos', cmd='user'),
164 user=dict(serv='astakos', cmd='user'),
167 self.set('global', 'default_' + CLOUD_PREFIX, 'default')
168 for s in self.sections():
169 if s in ('global', ):
170 # global.url, global.token -->
171 # cloud.default.url, cloud.default.token
172 for term in set(self.keys(s)).difference(global_terms):
173 if term not in ('url', 'token'):
174 lost_terms.append('%s.%s = %s' % (
175 s, term, self.get(s, term)))
176 self.remove_option(s, term)
178 gval = self.get(s, term)
179 default_cloud = self.get(
180 'global', 'default_cloud') or 'default'
182 cval = self.get_cloud(default_cloud, term)
185 if gval and cval and (
186 gval.lower().strip('/') != cval.lower().strip('/')):
187 raise CLISyntaxError(
188 'Conflicting values for default %s' % (
190 importance=2, details=[
191 ' global.%s: %s' % (term, gval),
197 'Please remove one of them manually:',
198 ' /config delete global.%s' % term,
200 ' /config delete %s.%s.%s' % (
201 CLOUD_PREFIX, default_cloud, term),
204 err.write(u'... rescue %s.%s => %s.%s.%s\n' % (
205 s, term, CLOUD_PREFIX, default_cloud, term))
207 self.set_cloud('default', term, gval)
208 self.remove_option(s, term)
209 # translation for <service> or <command> settings
210 # <service> or <command group> settings --> translation --> global
211 elif s in translations:
213 if s in ('history',):
217 err.write(u'... rescue %s.%s => global.%s_%s\n' % (
220 self.set('global', '%s_%s' % (s, k), v)
221 self.remove_option(s, k)
223 trn = translations[s]
224 for k, v in self.items(s, False):
225 if v and k in ('cli',):
226 err.write(u'... rescue %s.%s => global.%s_cli\n' % (
229 self.set('global', '%s_cli' % trn['cmd'], v)
230 elif k in ('container',) and trn['serv'] in ('pithos',):
232 u'... rescue %s.%s => %s.default.pithos_%s\n' % (
233 s, k, CLOUD_PREFIX, k))
235 self.set_cloud('default', 'pithos_%s' % k, v)
237 lost_terms.append('%s.%s = %s' % (s, k, v))
238 self.remove_section(s)
239 # self.pretty_print()
242 def pretty_print(self, out=stdout):
243 for s in self.sections():
246 for k, v in self.items(s):
247 if isinstance(v, dict):
248 out.write(u'\t%s => {\n' % k)
250 for ki, vi in v.items():
251 out.write(u'\t\t%s => %s\n' % (ki, vi))
255 out.write(u'\t %s => %s\n' % (k, v))
258 def guess_version(self):
260 :returns: (float) version of the config file or 0.0 if unrecognized
262 checker = Config(self.path, with_defaults=False)
263 sections = checker.sections()
264 log.debug('Config file heuristic 1: old global section ?')
265 if 'global' in sections:
266 if checker.get('global', 'url') or checker.get('global', 'token'):
267 log.debug('..... config file has an old global section')
269 log.debug('........ nope')
270 log.debug('Config file heuristic 2: Any cloud sections ?')
271 if CLOUD_PREFIX in sections:
272 for r in self.keys(CLOUD_PREFIX):
273 log.debug('... found cloud "%s"' % r)
275 log.debug('........ nope')
276 log.debug('All heuristics failed, cannot decide')
279 def get_cloud(self, cloud, option):
281 :param cloud: (str) cloud alias
283 :param option: (str) option in cloud section
285 :returns: (str) the value assigned on this option
287 :raises KeyError: if cloud or cloud's option does not exist
289 r = self.get(CLOUD_PREFIX, cloud)
291 raise KeyError('Cloud "%s" does not exist' % cloud)
294 def get_global(self, option):
295 return self.get('global', option)
297 def set_cloud(self, cloud, option, value):
299 d = self.get(CLOUD_PREFIX, cloud) or dict()
303 self.set(CLOUD_PREFIX, cloud, d)
305 def set_global(self, option, value):
306 self.set('global', option, value)
308 def _load_defaults(self):
309 for section, options in DEFAULTS.items():
310 for option, val in options.items():
311 self.set(section, option, val)
313 def _get_dict(self, section, include_defaults=True):
315 d = dict(DEFAULTS[section]) if include_defaults else {}
319 d.update(RawConfigParser.items(self, section))
320 except NoSectionError:
325 self = self.__init__(self.path)
327 def get(self, section, option):
329 :param section: (str) HINT: for clouds, use cloud.<section>
333 :returns: (str) the value stored at section: {option: value}
335 value = self._overrides.get(section, {}).get(option)
336 if value is not None:
338 prefix = CLOUD_PREFIX + '.'
339 if section.startswith(prefix):
340 return self.get_cloud(section[len(prefix):], option)
342 return RawConfigParser.get(self, section, option)
343 except (NoSectionError, NoOptionError):
344 return DEFAULTS.get(section, {}).get(option)
346 def set(self, section, option, value):
348 :param section: (str) HINT: for remotes use cloud.<section>
354 prefix = CLOUD_PREFIX + '.'
355 if section.startswith(prefix):
356 cloud = self._cloud_name(
357 CLOUD_PREFIX + ' "' + section[len(prefix):] + '"')
358 return self.set_cloud(cloud, option, value)
359 if section not in RawConfigParser.sections(self):
360 self.add_section(section)
361 RawConfigParser.set(self, section, option, value)
363 def remove_option(self, section, option, also_remove_default=False):
365 if also_remove_default:
366 DEFAULTS[section].pop(option)
367 RawConfigParser.remove_option(self, section, option)
368 except NoSectionError:
371 def remove_from_cloud(self, cloud, option):
372 d = self.get(CLOUD_PREFIX, cloud)
373 if isinstance(d, dict):
376 def keys(self, section, include_defaults=True):
377 d = self._get_dict(section, include_defaults)
380 def items(self, section, include_defaults=True):
381 d = self._get_dict(section, include_defaults)
384 def override(self, section, option, value):
385 self._overrides[section][option] = value
388 cld_bu = self._get_dict(CLOUD_PREFIX)
390 for r, d in self.items(CLOUD_PREFIX):
391 for k, v in d.items():
392 self.set(CLOUD_PREFIX + ' "%s"' % r, k, v)
393 self.remove_section(CLOUD_PREFIX)
395 with open(self.path, 'w') as f:
396 os.chmod(self.path, 0600)
397 f.write(HEADER.lstrip())
399 RawConfigParser.write(self, f)
401 if CLOUD_PREFIX not in self.sections():
402 self.add_section(CLOUD_PREFIX)
403 for cloud, d in cld_bu.items():
404 self.set(CLOUD_PREFIX, cloud, d)