#!/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 # # 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_s`` - one to get one comoponent item defined for all containers named ``get_`` :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 ``:`` for each prefix of the form ``.:`` If list is numerically ordered by ```` 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`]:`` 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))