creole/creole/client.py

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))