839 lines
26 KiB
Python
839 lines
26 KiB
Python
#!/usr/bin/python
|
|
# -*- coding: utf-8 -*-
|
|
#
|
|
##########################################################################
|
|
# creole.client - client to request creole.server through REST API
|
|
# Copyright © 2012,2013 Pôle de compétences EOLE <eole@ac-dijon.fr>
|
|
#
|
|
# License CeCILL:
|
|
# * in french: http://www.cecill.info/licences/Licence_CeCILL_V2-fr.html
|
|
# * in english http://www.cecill.info/licences/Licence_CeCILL_V2-en.html
|
|
##########################################################################
|
|
|
|
"""Request informations from :class:`creole.CreoleServer`
|
|
|
|
Simple http :mod:`restkit.request` client to request and manipulate
|
|
informations from :class:`creole.CreoleServer`.
|
|
|
|
"""
|
|
|
|
from http_parser.http import NoMoreData
|
|
import restkit
|
|
import eventlet
|
|
from restkit.errors import ResourceError, RequestError, ParseException, RequestTimeout
|
|
from eventlet.timeout import Timeout as EventletTimeout
|
|
|
|
from collections import OrderedDict
|
|
|
|
import json
|
|
import logging
|
|
from time import sleep
|
|
|
|
from .dtd_parser import parse_dtd
|
|
from .config import dtdfilename
|
|
|
|
from .i18n import _
|
|
from pyeole.encode import normalize
|
|
|
|
import re
|
|
|
|
# Stat filesystem
|
|
import os
|
|
|
|
# Create instance method on the fly
|
|
import types
|
|
|
|
log = logging.getLogger(__name__)
|
|
|
|
_CONTAINER_COMPONENTS = ['container'] + parse_dtd(dtdfilename)['container']['options']
|
|
"""List of components used to define an LXC container.
|
|
|
|
They are extracted from the ``creole.dtd``.
|
|
|
|
Each of them are use to fabric two accessor methods bound to
|
|
:class:`CreoleClient`.
|
|
|
|
"""
|
|
LOCAL_URL = 'http://127.0.0.1:8000'
|
|
#Si on veut garder les threads, on peut désactiver les reap_connections pour éviter les tracebacks
|
|
#restkit.session.get_session('thread', reap_connections=False)
|
|
|
|
|
|
def _merge_entries(old, new):
|
|
"""Merge component informations
|
|
|
|
This merge keep information from :data:`old` when the :data:`new`
|
|
is ``None``.
|
|
|
|
The boolean information are ored between :data:`old` and
|
|
:data:`new`.
|
|
|
|
:param old: previous component informations
|
|
:type old: `dict`
|
|
:param new: new component informations
|
|
:type new: `dict`
|
|
:return: merged informations
|
|
:rtype: `dict`
|
|
|
|
"""
|
|
for key, val in new.items():
|
|
if val is None:
|
|
# Do not override previous value
|
|
continue
|
|
elif isinstance(val, bool):
|
|
# Switch on first True
|
|
# old[key] may not exists
|
|
old[key] = val | old.get(key, False)
|
|
else:
|
|
old[key] = val
|
|
|
|
return old
|
|
|
|
|
|
def _merge_duplicates_in_components(container_info, keys_to_strip=None):
|
|
"""Merge duplicates entries
|
|
|
|
:param container_info: information on a container or group of
|
|
containers
|
|
:type container_info: `dict`
|
|
:param keys_to_strip: keys for which to remove duplicated entries
|
|
:type keys_to_strip: `list`
|
|
|
|
"""
|
|
# Do not work in-place
|
|
info = container_info.copy()
|
|
|
|
if keys_to_strip is None:
|
|
# Run on all keys
|
|
keys_to_strip = info.keys()
|
|
|
|
for key in keys_to_strip:
|
|
if not isinstance(info[key], list):
|
|
# Do not work on single values
|
|
continue
|
|
|
|
result = OrderedDict()
|
|
for entry in info[key]:
|
|
if 'name' in entry:
|
|
name = repr(entry['name'])
|
|
if name in result and not entry.get(u'activate', False):
|
|
# Duplicate found but inactive
|
|
continue
|
|
elif name in result:
|
|
# Merge old and new informations
|
|
old_entry = result[name]
|
|
# Make sure entry appears at right place
|
|
del(result[name])
|
|
result[name] = _merge_entries(old=old_entry,
|
|
new=entry)
|
|
else:
|
|
# New entry
|
|
result[name] = entry
|
|
|
|
if result:
|
|
# Store stripped information
|
|
info[key] = [ item for item in result.values() ]
|
|
|
|
return info
|
|
|
|
|
|
def _build_component_accessors(component):
|
|
"""Fabric of accessors for container components
|
|
|
|
It build two accessors:
|
|
|
|
- one to get all components for all containers named
|
|
``get_<component>s``
|
|
|
|
- one to get one comoponent item defined for all containers
|
|
named ``get_<component>``
|
|
|
|
:param name: type of container variable
|
|
:type name: `str`
|
|
:return: component accessors
|
|
:rtype: `tuple` of `function`
|
|
|
|
"""
|
|
def all_components(self, container=None):
|
|
"""Return all components
|
|
"""
|
|
return self.get_components('{0}s'.format(component),
|
|
container=container)
|
|
|
|
all_components.__name__ = 'get_{0}s'.format(component)
|
|
all_components.__doc__ = """Get {0}s for all containers
|
|
|
|
:param container: limit search to a container
|
|
:type container: `str`
|
|
:returns: {0}s informations
|
|
:rtype: `list`
|
|
|
|
""".format(component)
|
|
|
|
def single_component(self, name, container=None):
|
|
"""Return single component
|
|
"""
|
|
components = []
|
|
ret = self.get_components('{0}s'.format(component),
|
|
container=container)
|
|
for item in ret:
|
|
if item['name'] == name:
|
|
components.append(item)
|
|
return components
|
|
single_component.__doc__ = """Get one {0} for all containers
|
|
|
|
:param name: name of {0} to return
|
|
:type name: `str`
|
|
:param container: limit search to a container
|
|
:type container: `str`
|
|
:returns: {0} informations for all containers
|
|
:rtype: `list`
|
|
|
|
""".format(component)
|
|
|
|
single_component.__name__ = 'get_{0}'.format(component)
|
|
|
|
return all_components, single_component
|
|
|
|
|
|
class CreoleClient(object):
|
|
"""Request informations from :class:`creole.CreoleServer`.
|
|
|
|
In addition, this class provides some utilities to manipulate
|
|
returned data.
|
|
|
|
"""
|
|
|
|
def __init__(self, url=None):
|
|
"""Initialize client.
|
|
|
|
:param url: HTTP URL to the :class:`creole.CreoleServer`
|
|
:type url: `str`
|
|
|
|
"""
|
|
if url is None:
|
|
if self.is_in_lxc():
|
|
url = 'http://192.0.2.1:8000'
|
|
else:
|
|
url = LOCAL_URL
|
|
|
|
self.url = url
|
|
comp_list = _CONTAINER_COMPONENTS[:]
|
|
comp_list.remove('container')
|
|
# Disable logging of restkit
|
|
restkit.set_logging('critical', logging.NullHandler())
|
|
self._is_container_actif = None
|
|
self._restkit_request = None
|
|
for component in comp_list:
|
|
get_all, get_single = _build_component_accessors(component)
|
|
setattr(self, get_all.__name__,
|
|
types.MethodType(get_all, self, CreoleClient))
|
|
setattr(self, get_single.__name__,
|
|
types.MethodType(get_single, self, CreoleClient))
|
|
|
|
@staticmethod
|
|
def is_in_lxc():
|
|
"""Check if we are in LXC.
|
|
|
|
We are under LXC if /proc/1/cgroup contains ``/lxc``.
|
|
|
|
:return: if we are under LXC.
|
|
:rtype: `bool`
|
|
|
|
"""
|
|
if not os.path.isdir('/proc/self'):
|
|
# when launch in chroot
|
|
return True
|
|
else:
|
|
return os.access('/dev/lxc/console', os.F_OK)
|
|
|
|
|
|
def close(self):
|
|
if self._restkit_request is not None:
|
|
self._restkit_request.close()
|
|
|
|
|
|
def _request(self, path, **kwargs):
|
|
"""Send HTTP request to Creole server.
|
|
|
|
If ConnectionError, try three time before leave.
|
|
|
|
:param path: path to the creole resource
|
|
:type path: `str`
|
|
:return: response of the request
|
|
:rtype: :class:`restkit.wrappers.Response`
|
|
:raise CreoleClientError: on HTTP errors
|
|
|
|
"""
|
|
timeout = 5
|
|
max_try = 3
|
|
tried = 0
|
|
|
|
method = 'GET'
|
|
if 'method' in kwargs:
|
|
method = kwargs['method']
|
|
del(kwargs['method'])
|
|
|
|
uri = restkit.util.make_uri(path, **kwargs)
|
|
|
|
while tried < max_try:
|
|
tried += 1
|
|
try:
|
|
# use eventlet backend (#13194, #21388)
|
|
with eventlet.Timeout(timeout):
|
|
self._restkit_request = restkit.request(uri, method=method, backend='eventlet')
|
|
return self._restkit_request
|
|
except (ResourceError, RequestError, ParseException, NoMoreData, RequestTimeout, EventletTimeout) as err:
|
|
log.debug(_(u"Connexion error '{0}',"
|
|
u" retry {1}/{2}").format(err, tried, max_try))
|
|
sleep(1)
|
|
|
|
if isinstance(err, RequestError):
|
|
msg = _(u"HTTP error: {0}\nPlease check creoled's log (/var/log/rsyslog/local/creoled/creoled.info.log)\nand restart service with command 'service creoled start'")
|
|
else:
|
|
msg = _(u"HTTP error: {0}")
|
|
if isinstance(err, RequestTimeout) or isinstance(err, EventletTimeout):
|
|
err = _(u"creoled service didn't respond in time")
|
|
|
|
raise TimeoutCreoleClientError(msg.format(err))
|
|
|
|
def is_container_actif(self):
|
|
if self._is_container_actif is None:
|
|
self._is_container_actif = self.get_creole('mode_conteneur_actif', 'non') == 'oui'
|
|
return self._is_container_actif
|
|
|
|
def request(self, command, path=None, **kwargs):
|
|
"""Send HTTP request to creole server.
|
|
|
|
:param command: action to perform for the creole resource
|
|
:type command: `str`
|
|
:param path: path to the creole resource
|
|
:type path: `str`
|
|
:return: dictionary of variable:value
|
|
:rtype: `dict`
|
|
:raise CreoleClientError: on bad response status or HTTP error
|
|
|
|
"""
|
|
if path is not None:
|
|
path = self.validate_path(path)
|
|
ret = self._request(self.url + command + path, **kwargs)
|
|
else:
|
|
ret = self._request(self.url + command, **kwargs)
|
|
if ret.status_int != 200:
|
|
log.debug(_(u'HTML content: {0}').format(ret.body_string()))
|
|
raise CreoleClientError(_(u"HTML error {0}, please consult creoled events log (/var/log/rsyslog/local/creoled/creoled.info.log) to have more informations").format(ret.status_int))
|
|
reply = json.loads(ret.body_string())
|
|
|
|
# Previous fix for NoMoreData exception #7218 :
|
|
#ret.connection.close()
|
|
|
|
if reply['status'] != 0:
|
|
if reply['status'] == 4:
|
|
raise NotFoundError(u"{0}".format(reply['response']))
|
|
else:
|
|
raise CreoleClientError(normalize(_("Creole error {0}: {1}")).format(
|
|
reply['status'], reply['response']))
|
|
|
|
return reply['response']
|
|
|
|
@staticmethod
|
|
def validate_path(path):
|
|
"""Validate the path for http request.
|
|
|
|
:data:`path` must use ``/`` as separator with a leading one or
|
|
use ``.`` as separator.
|
|
|
|
:param path: path to the creole resource
|
|
:type path: `str`
|
|
:return: slash separated path to the resource
|
|
:rtype: `str`
|
|
:raise CreoleClientError: when path does not validate
|
|
|
|
"""
|
|
ret = path
|
|
if not ret.startswith('/'):
|
|
if ret.find('.') != -1 and ret.find('/') != -1:
|
|
raise CreoleClientError(_(u"Path must not mix dotted and" +
|
|
u" slash notation: '{0}'").format(path))
|
|
elif ret.find('.') != -1:
|
|
ret = '/{0}'.format( ret.replace('.', '/') )
|
|
else:
|
|
raise CreoleClientError(_(u"Path must start" +
|
|
u" with '/': '{0}'").format(path))
|
|
return ret
|
|
|
|
def get(self, path='/creole', *args, **kwargs):
|
|
"""Get the values from part of the tree.
|
|
|
|
If :data:`path` is a variable, it returns it's value.
|
|
|
|
If :data:`path` is a tree node, it returns the whole tree
|
|
of ``variable:value`` as flat dictionary.
|
|
|
|
:param path: path to the creole resource
|
|
:type path: `str`
|
|
:param default: default value if any error occurs
|
|
:return: slash separated path to the resource
|
|
:rtype: `str`
|
|
|
|
"""
|
|
# Use a dictionary to test existence
|
|
default = {}
|
|
if len(args) > 1:
|
|
raise ValueError(_("Too many positional parameters {0}.").format(args))
|
|
|
|
if kwargs.has_key('default'):
|
|
default['value'] = kwargs['default']
|
|
del(kwargs['default'])
|
|
elif len(args) == 1:
|
|
default['value'] = args[0]
|
|
|
|
try:
|
|
ret = self.request('/get', path, **kwargs)
|
|
except (NotFoundError, CreoleClientError) as err:
|
|
if default.has_key('value'):
|
|
ret = default['value']
|
|
else:
|
|
raise err
|
|
|
|
return ret
|
|
|
|
def list(self, path='/creole'):
|
|
"""List content of a path.
|
|
|
|
If :data:`path` is a variable, it returns it's name.
|
|
|
|
If :data:`path` is a tree node, it returns the list of items
|
|
under it.
|
|
|
|
:param path: path to the creole resource
|
|
:type path: `str`
|
|
:return: items present under a path
|
|
:rtype: `list`
|
|
|
|
"""
|
|
return self.request('/list', path)
|
|
|
|
def get_creole(self, name=None, *args, **kwargs):
|
|
"""Get variables under ``/creole``.
|
|
|
|
The full path of variable names is stripped in key names.
|
|
|
|
:param path: path to the creole resource
|
|
:type path: `str`
|
|
:param default: default value to return if the variable named
|
|
:data:`name` does not exist or any error occurs
|
|
:return: variables and their value
|
|
:rtype: `dict`
|
|
|
|
"""
|
|
if name is not None:
|
|
# Tiramisu has no any meaningful message
|
|
try:
|
|
ret = self.get('/creole', *args, variable=name, **kwargs)
|
|
except NotFoundError:
|
|
msg = _(u'Unknown variable {0}')
|
|
raise NotFoundError(msg.format(name))
|
|
else:
|
|
ret = self.strip_full_path(self.get('/creole', *args, **kwargs))
|
|
|
|
return ret
|
|
|
|
def reload_config(self):
|
|
"""Reload Tiramisu's config
|
|
"""
|
|
return self.request('/reload_config')
|
|
|
|
def reload_eol(self):
|
|
"""Reload Tiramisu's partial config
|
|
"""
|
|
return self.request('/reload_eol')
|
|
|
|
def valid_mandatory(self):
|
|
return self.request('/valid_mandatory')
|
|
|
|
def get_containers(self, group=None):
|
|
"""Get basic informations of all containers
|
|
|
|
:param group: limit search to a group of containers
|
|
:type group: `str`
|
|
:return: containers informations
|
|
:rtype: `list`
|
|
"""
|
|
mode_container = self.is_container_actif()
|
|
if group is None or (not mode_container and group == 'root'):
|
|
args = {}
|
|
else:
|
|
args = {'withoption':'group',
|
|
'withvalue':group}
|
|
|
|
try:
|
|
ret = self.get('/containers/containers', **args)
|
|
except NotFoundError:
|
|
# Tiramisu has no any meaningful message
|
|
if group is not None:
|
|
msg = _(u'No container found for group {0}')
|
|
else:
|
|
msg = _(u'No container found! Is that possible?')
|
|
raise NotFoundError(msg.format(group))
|
|
|
|
ret = self.to_list_of_dict(ret, prefix='container')
|
|
return ret
|
|
|
|
|
|
def get_container(self, name):
|
|
"""Get informations of one container
|
|
|
|
:param name: type of container variable
|
|
:type name: `str`
|
|
:return: component for all containers
|
|
:rtype: `list`
|
|
"""
|
|
try:
|
|
ret = self.get('/containers/containers',
|
|
withoption='name',
|
|
withvalue=name)
|
|
except NotFoundError:
|
|
# Tiramisu has no any meaningful message
|
|
raise NotFoundError(_(u'Unknown container {0}').format(name))
|
|
|
|
ret = self.to_list_of_dict(ret, prefix='container')
|
|
return ret[0]
|
|
|
|
|
|
def get_groups(self):
|
|
"""Get list of container groups
|
|
|
|
All groups are a container, but all containers are not a
|
|
group.
|
|
|
|
:return: container groups names
|
|
:rtype: `list`
|
|
|
|
"""
|
|
mode_container = self.is_container_actif()
|
|
containers = self.get_containers()
|
|
if not mode_container:
|
|
groups = ['root']
|
|
else:
|
|
groups = []
|
|
for container in containers:
|
|
if container['name'] == container['group']:
|
|
groups.append(container['name'])
|
|
if 'all' in groups:
|
|
groups.remove('all')
|
|
|
|
return groups
|
|
|
|
|
|
def is_group(self, name):
|
|
"""Verify is a container is a group of containers.
|
|
|
|
:param name: name of the container
|
|
:type name: `str`
|
|
:return: is the container a group of containers?
|
|
:rtype: `bool`
|
|
|
|
"""
|
|
mode_container = self.is_container_actif()
|
|
if not mode_container:
|
|
return name == 'root'
|
|
|
|
container = self.get_container(name)
|
|
return name == container['group']
|
|
|
|
|
|
def get_containers_components(self, containers, group=False, merge_duplicates=False):
|
|
"""Get all components of a list of containers or group of containers.
|
|
|
|
:param containers: container names
|
|
:type containers: `list` of `str`
|
|
:param group: containers are names of groups of containers
|
|
:type group: `bool`
|
|
:param merge_duplicates: merge duplicate entries
|
|
:type merge_duplicates: `bool`
|
|
:return: components of the containers
|
|
:rtype: `dict`
|
|
|
|
"""
|
|
comp_list = [ '{0}s'.format(name) for name in _CONTAINER_COMPONENTS[:] ]
|
|
component = {}
|
|
|
|
if not group:
|
|
if 'all' in containers:
|
|
# make sure all is first
|
|
containers.remove('all')
|
|
|
|
# Remove duplicates
|
|
containers = list(set(containers))
|
|
containers.insert(0, 'all')
|
|
|
|
for comp in comp_list:
|
|
component[comp] = []
|
|
for container in containers:
|
|
by_cont = self.get_components(None, container=container, group=group)
|
|
|
|
for comp, items in by_cont.items():
|
|
if comp + 's' in comp_list:
|
|
component[comp + 's'].extend(items)
|
|
|
|
if merge_duplicates:
|
|
component = _merge_duplicates_in_components(component, comp_list)
|
|
|
|
if 'interfaces' in component:
|
|
for interface in component['interfaces']:
|
|
if 'gateway' in interface and interface['gateway']:
|
|
component['gateway'] = {u'interface': interface['name'],
|
|
u'ip': interface['gateway']}
|
|
|
|
return component
|
|
|
|
|
|
def get_container_infos(self, container):
|
|
"""Get all components of a container or its group
|
|
|
|
:param container: container name
|
|
:type container: `str`
|
|
:return: components of the container or its group
|
|
:rtype: `dict`
|
|
|
|
"""
|
|
container_info = self.get_container(container)
|
|
group_name = container_info[u'real_container']
|
|
container_info = self.get_group_infos(group_name)
|
|
|
|
return container_info
|
|
|
|
|
|
def get_group_infos(self, group):
|
|
"""Get all components of a group of container
|
|
|
|
:param group: container group name
|
|
:type group: `str`
|
|
:return: components of the container
|
|
:rtype: `dict`
|
|
|
|
"""
|
|
group_info = self.get_containers_components(containers=[group],
|
|
group=True,
|
|
merge_duplicates=True)
|
|
|
|
# If we need to do thing in the name of all containers in the group
|
|
names = []
|
|
found = False
|
|
for container in group_info['containers']:
|
|
name = container['name']
|
|
names.append(name)
|
|
if name == group:
|
|
found = True
|
|
group_info.update(container)
|
|
if not found:
|
|
group_info.update(self.get_container(group))
|
|
group_info['containers'] = names
|
|
|
|
return group_info
|
|
|
|
|
|
def get_components(self, name, container=None, group=False):
|
|
"""Get component for containers
|
|
|
|
:param name: type of container variable
|
|
:type name: `str`
|
|
:param container: limit search to a container
|
|
:type container: `str`
|
|
:return: component for all containers
|
|
:rtype: `list`
|
|
"""
|
|
if container is not None:
|
|
if group:
|
|
option_name = 'real_container'
|
|
else:
|
|
option_name = 'container'
|
|
|
|
args = {'withoption': option_name,
|
|
'withvalue': container}
|
|
else:
|
|
args = {}
|
|
|
|
ret = None
|
|
if name is None:
|
|
path = '/containers'
|
|
else:
|
|
path = '/containers/{0}'.format(name)
|
|
try:
|
|
ret = self.get(path, **args)
|
|
except NotFoundError:
|
|
# Tiramisu has no any meaningful message
|
|
msg = _(u'Unknown container components {0} for container {1}')
|
|
if container is None:
|
|
msg = _(u'Unknown container components {0}')
|
|
else:
|
|
args = {'withoption':'container_group',
|
|
'withvalue':container}
|
|
try:
|
|
ret = self.get(path, **args)
|
|
except NotFoundError:
|
|
msg = _(u'Unknown container components {0} for container {1}')
|
|
# If not a container, maybe a container's group
|
|
if ret is None:
|
|
raise NotFoundError(msg.format(str(name), container))
|
|
if name is None:
|
|
comp_list = _CONTAINER_COMPONENTS[:]
|
|
dico = {}
|
|
ret_comp = {}
|
|
for comp in comp_list:
|
|
dico[comp] = {}
|
|
for path, item in ret.items():
|
|
spath = path.split('.')
|
|
#without 's'
|
|
comp = spath[0][:-1]
|
|
dico[comp]['.'.join(spath[1:])] = item
|
|
for comp in comp_list:
|
|
ret_comp[comp] = self.to_list_of_dict(dico[comp], prefix=comp)
|
|
|
|
else:
|
|
ret_comp = self.to_list_of_dict(ret, prefix=name)
|
|
return ret_comp
|
|
|
|
@classmethod
|
|
def to_list_of_dict(cls, flat, prefix=None):
|
|
"""Convert a flat dictionary to a list of dictionaries.
|
|
|
|
Build a list of dictionary ``<name>:<value>`` for each
|
|
prefix of the form ``<prefix><integer index>.<name>:<value>``
|
|
|
|
If list is numerically ordered by ``<integer index>``
|
|
extracted from each key accordingly to :data:`prefix`.
|
|
|
|
If the :data:`prefix` is not specified, a random element of
|
|
:data:`flat` is extracted to compute it.
|
|
|
|
:param flat: absolute attribute variable names and their
|
|
values
|
|
:type flat: `dict`
|
|
:param prefix: alphabetic prefix to extract integer index
|
|
:type prefix: `str`
|
|
:return: variables and their attributes values
|
|
:rtype: `list` of `dict`
|
|
|
|
"""
|
|
reply = {}
|
|
sorted_items = []
|
|
sort_key = None
|
|
|
|
if prefix is None:
|
|
# Extract prefix name
|
|
random_key = flat.iterkeys().next()
|
|
indexed_prefix = random_key.split('.')[0]
|
|
re_match = re.match(r'(\D+)\d+', indexed_prefix)
|
|
prefix = re_match.group(1)
|
|
|
|
if prefix is not None:
|
|
# check for none because maybe regexp match did not work
|
|
# Extract component index as integer for comparaison
|
|
sort_key = lambda string: int(string.split('.')[0].lstrip(prefix))
|
|
|
|
for key in sorted(flat.keys(), key=sort_key):
|
|
sid, sattr = cls._split_path_leaf(key)
|
|
if sid not in reply:
|
|
sorted_items.append(sid)
|
|
reply[sid] = {}
|
|
reply[sid][sattr] = flat[key]
|
|
return [ reply[item] for item in sorted_items ]
|
|
|
|
@staticmethod
|
|
def strip_full_path(flat):
|
|
"""Strip full path of flat dictionary keys.
|
|
|
|
:param flat: absolute variable names and their value
|
|
:type flat: `dict`
|
|
:return: short variable names and their value
|
|
:rtype: `dict`
|
|
"""
|
|
ret = {}
|
|
for path in flat:
|
|
parts = path.split('.')[1:]
|
|
if len(parts) == 1:
|
|
# Single variable
|
|
ret[ parts[0] ] = flat[path]
|
|
elif len(parts) == 2 and parts[0] == parts[1]:
|
|
# Master variable
|
|
ret[ parts[0] ] = flat[path]
|
|
else:
|
|
# slave variable
|
|
ret[ '.'.join(parts) ] = flat[path]
|
|
return ret
|
|
|
|
@staticmethod
|
|
def to_grouped_lists(dict_list, keyname, keyvalue=None):
|
|
"""Convert a `list` of `dict` to a `dict` :data:`keyvalue`:`list`.
|
|
|
|
Build dictionary of ``dictionary[:data:`keyvalue`]:<list of
|
|
dict>`` to group all items with the same value of a key.
|
|
|
|
:param dict_list: dictionaries
|
|
:type dict_list: `list`
|
|
:param keyname: name of the key to test
|
|
:type keyname: `str`
|
|
:param keyvalue: value to match :data:`keyname`
|
|
:return: dictionary grouped by a key value
|
|
:rtype: `dict`
|
|
|
|
"""
|
|
reply = {}
|
|
for key in dict_list:
|
|
if keyname in key and keyvalue and keyvalue != key[keyname]:
|
|
continue
|
|
if keyname not in key:
|
|
if None not in reply:
|
|
reply[None] = []
|
|
reply[None].append(key)
|
|
else:
|
|
if key[keyname] not in reply:
|
|
reply[ key[keyname] ] = []
|
|
reply[ key[keyname] ].append(key)
|
|
return reply
|
|
|
|
@staticmethod
|
|
def _split_path_leaf(path, separator='.'):
|
|
"""Split path in two parts: dirname and basename.
|
|
|
|
If :data:`path` does not contains the :data:`separator`, it's
|
|
considered as leaf and the dirname of :data:`path` is set to
|
|
`None`.
|
|
|
|
:param path: path to the creole resource
|
|
:type path: `str`
|
|
:return: dirname and basename of :data:`path`
|
|
:rtype: `list`
|
|
|
|
"""
|
|
if path.find(separator) == -1:
|
|
return (None, path)
|
|
|
|
splited = path.split(separator)
|
|
return ( '.'.join(splited[:-1]), splited[-1] )
|
|
|
|
|
|
class TimeoutCreoleClientError(StandardError):
|
|
pass
|
|
|
|
|
|
class CreoleClientError(StandardError):
|
|
"""Bad use of :class:`CreoleClient`
|
|
"""
|
|
pass
|
|
|
|
|
|
class NotFoundError(CreoleClientError):
|
|
"""Requested variable not found
|
|
"""
|
|
pass
|
|
|
|
|
|
if __name__ == '__main__':
|
|
try:
|
|
print(CreoleClient().get('/'))
|
|
except Exception as err:
|
|
print(_(u"Error: {0}").format(err))
|