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'),
80 'history_file': HISTORY_PATH,
81 'user_cli': 'astakos',
82 'quota_cli': 'astakos',
83 'resource_cli': 'astakos',
84 'project_cli': 'astakos',
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',
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'
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'
123 class Config(RawConfigParser):
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)
130 self._load_defaults()
133 for section in self.sections():
134 r = self._cloud_name(section)
136 for k, v in self.items(section):
137 self.set_cloud(r, k, v)
138 self.remove_section(section)
141 def _cloud_name(full_section_name):
142 if not full_section_name.startswith(CLOUD_PREFIX + ' '):
144 matcher = match(CLOUD_PREFIX + ' "([~@#$.:\-\w]+)"', full_section_name)
146 return matcher.groups()[0]
148 icn = full_section_name[len(CLOUD_PREFIX) + 1:]
149 raise InvalidCloudNameError('Invalid Cloud Name %s' % icn)
151 def rescue_old_file(self, err=stderr):
153 global_terms = DEFAULTS['global'].keys()
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'),
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)
184 gval = self.get(s, term)
185 default_cloud = self.get(
186 'global', 'default_cloud') or 'default'
188 cval = self.get_cloud(default_cloud, term)
191 if gval and cval and (
192 gval.lower().strip('/') != cval.lower().strip('/')):
193 raise CLISyntaxError(
194 'Conflicting values for default %s' % (
196 importance=2, details=[
197 ' global.%s: %s' % (term, gval),
203 'Please remove one of them manually:',
204 ' /config delete global.%s' % term,
206 ' /config delete %s.%s.%s' % (
207 CLOUD_PREFIX, default_cloud, term),
210 err.write(u'... rescue %s.%s => %s.%s.%s\n' % (
211 s, term, CLOUD_PREFIX, default_cloud, term))
213 self.set_cloud('default', term, gval)
214 self.remove_option(s, term)
216 for term, wrong, right in (
217 ('ip', 'cyclades', 'network'),
218 ('network', 'cyclades', 'network'),):
222 err.write('... change %s.%s value: `%s` => `%s`\n' % (
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:
230 if s in ('history',):
234 err.write(u'... rescue %s.%s => global.%s_%s\n' % (
237 self.set('global', '%s_%s' % (s, k), v)
238 self.remove_option(s, k)
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' % (
246 self.set('global', '%s_cli' % trn['cmd'], v)
247 elif k in ('container',) and trn['serv'] in ('pithos',):
249 u'... rescue %s.%s => %s.default.pithos_%s\n' % (
250 s, k, CLOUD_PREFIX, k))
252 self.set_cloud('default', 'pithos_%s' % k, v)
254 lost_terms.append('%s.%s = %s' % (s, k, v))
255 self.remove_section(s)
256 # self.pretty_print()
259 def pretty_print(self, out=stdout):
260 for s in self.sections():
263 for k, v in self.items(s):
264 if isinstance(v, dict):
265 out.write(u'\t%s => {\n' % k)
267 for ki, vi in v.items():
268 out.write(u'\t\t%s => %s\n' % (ki, vi))
272 out.write(u'\t %s => %s\n' % (k, v))
275 def guess_version(self):
277 :returns: (float) version of the config file or 0.9 if unrecognized
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')
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', ):
294 netv = self.get('global', 'network_cli')
295 if netv in ('cyclades', ):
298 log.debug('........ nope')
299 log.debug('All heuristics failed, cannot decide')
302 def get_cloud(self, cloud, option):
304 :param cloud: (str) cloud alias
306 :param option: (str) option in cloud section
308 :returns: (str) the value assigned on this option
310 :raises KeyError: if cloud or cloud's option does not exist
312 r = self.get(CLOUD_PREFIX, cloud) if cloud else None
314 raise KeyError('Cloud "%s" does not exist' % cloud)
317 def set_cloud(self, cloud, option, value):
319 d = self.get(CLOUD_PREFIX, cloud) or dict()
323 self.set(CLOUD_PREFIX, cloud, d)
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)
330 def _get_dict(self, section, include_defaults=True):
332 d = dict(DEFAULTS[section]) if include_defaults else {}
336 d.update(RawConfigParser.items(self, section))
337 except NoSectionError:
342 self = self.__init__(self.path)
344 def get(self, section, option):
346 :param section: (str) HINT: for clouds, use cloud.<section>
350 :returns: (str) the value stored at section: {option: value}
352 value = self._overrides.get(section, {}).get(option)
353 if value is not None:
355 prefix = CLOUD_PREFIX + '.'
356 if section.startswith(prefix):
357 return self.get_cloud(section[len(prefix):], option)
359 return RawConfigParser.get(self, section, option)
360 except (NoSectionError, NoOptionError):
361 return DEFAULTS.get(section, {}).get(option)
363 def set(self, section, option, value):
365 :param section: (str) HINT: for remotes use cloud.<section>
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)
380 def remove_option(self, section, option, also_remove_default=False):
382 if also_remove_default:
383 DEFAULTS[section].pop(option)
384 RawConfigParser.remove_option(self, section, option)
385 except (NoSectionError, KeyError):
388 def remove_from_cloud(self, cloud, option):
389 d = self.get(CLOUD_PREFIX, cloud)
390 if isinstance(d, dict):
393 def keys(self, section, include_defaults=True):
394 d = self._get_dict(section, include_defaults)
397 def items(self, section, include_defaults=True):
398 d = self._get_dict(section, include_defaults)
401 def override(self, section, option, value):
402 self._overrides[section][option] = value
405 cld_bu = self._get_dict(CLOUD_PREFIX)
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)
412 with open(self.path, 'w') as f:
413 os.chmod(self.path, 0600)
414 f.write(HEADER.lstrip())
416 RawConfigParser.write(self, f)
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)