Source code for cumin.query

"""Query handling: factory and builder."""
from pyparsing import ParseException, ParseResults

from cumin import grammar
from cumin.backends import BaseQuery, BaseQueryAggregator, InvalidQueryError


[docs]class Query(BaseQueryAggregator): """Cumin main query class. It has multi-query capability and allow to use a default backend, if set, without additional syntax. If a ``default_backend`` is set in the configuration, it will try to execute the query string first with the default backend and only if the query is not parsable with that backend it will try to execute it with the multi-query grammar. When a query is executed, a :py:class:`ClusterShell.NodeSet.NodeSet` with the FQDN of the matched hosts is returned. Examples: >>> import cumin >>> from cumin.query import Query >>> config = cumin.Config() >>> hosts = Query(config).execute(query_string) """
[docs] def __init__(self, config): """Query constructor, initialize the registered backends. :Parameters: according to parent :py:meth:`cumin.backends.BaseQueryAggregator.__init__`. """ super().__init__(config) external = self.config.get('plugins', {}).get('backends', []) self.registered_backends = grammar.get_registered_backends(external=external) self.grammar = grammar.grammar(self.registered_backends.keys())
[docs] def execute(self, query_string): """Override parent class execute method to implement the multi-query capability. :Parameters: according to parent :py:meth:`cumin.backends.BaseQueryAggregator.execute`. Returns: ClusterShell.NodeSet.NodeSet: with the FQDNs of the matching hosts. Raises: cumin.backends.InvalidQueryError: if unable to parse the query. """ if 'default_backend' not in self.config: try: # No default backend set, using directly the global grammar return super().execute(query_string) except ParseException as e: raise InvalidQueryError(("Unable to parse the query '{query}' with the global grammar and no " "default backend is set:\n{error}").format(query=query_string, error=e)) try: # Default backend set, trying it first hosts = self._query_default_backend(query_string) except ParseException as e_default: try: # Trying global grammar as a fallback hosts = super().execute(query_string) except ParseException as e_global: raise InvalidQueryError( ("Unable to parse the query '{query}' neither with the default backend '{name}' nor with the " "global grammar:\n{name}: {e_def}\nglobal: {e_glob}").format( query=query_string, name=self.config['default_backend'], e_def=e_default, e_glob=e_global)) return hosts
[docs] def _query_default_backend(self, query_string): """Execute the query with the default backend, according to the configuration. Arguments: query_string (str): the query string to be parsed and executed with the default backend. Returns: ClusterShell.NodeSet.NodeSet: with the FQDNs of the matching hosts. Raises: cumin.backends.InvalidQueryError: if unable to get the default backend from the registered backends. """ for registered_backend in self.registered_backends.values(): if registered_backend.name == self.config['default_backend']: backend = registered_backend break else: raise InvalidQueryError("Default backend '{name}' is not registered: {backends}".format( name=self.config['default_backend'], backends=self.registered_backends)) query = backend.cls(self.config) return query.execute(query_string)
[docs] def _parse_token(self, token): """Concrete implementation of parent abstract method. :Parameters: according to parent :py:meth:`cumin.backends.BaseQueryAggregator._parse_token`. Raises: cumin.backends.InvalidQueryError: on internal parsing error. """ if not isinstance(token, ParseResults): # pragma: no cover - this should never happen raise InvalidQueryError('Expecting ParseResults object, got {type}: {token}'.format( type=type(token), token=token)) token_dict = token.asDict() self.logger.trace('Token is: %s', token_dict) if self._replace_alias(token_dict): return # This token was an alias and got replaced if 'backend' in token_dict and 'query' in token_dict: element = self._get_stack_element() query = self.registered_backends[token_dict['backend']].cls(self.config) element['hosts'] = query.execute(token_dict['query']) if 'bool' in token_dict: element['bool'] = token_dict['bool'] self.stack_pointer['children'].append(element) elif 'open_subgroup' in token_dict and 'close_subgroup' in token_dict: self._open_subgroup() if 'bool' in token_dict: self.stack_pointer['bool'] = token_dict['bool'] for subtoken in token: if isinstance(subtoken, str): continue self._parse_token(subtoken) self._close_subgroup() else: # pragma: no cover - this should never happen raise InvalidQueryError('Got unexpected token: {token}'.format(token=token))
[docs] def _replace_alias(self, token_dict): """Replace any alias in the query in a recursive way, alias can reference other aliases. Arguments: token_dict (dict): the dictionary of the parsed token returned by the grammar parsing. Returns: bool: :py:data:`True` if a replacement was made, :py:data`False` otherwise. Raises: cumin.backends.InvalidQueryError: if unable to replace an alias. """ if 'alias' not in token_dict: return False alias_name = token_dict['alias'] if alias_name not in self.config.get('aliases', {}): raise InvalidQueryError("Unable to find alias replacement for '{alias}' in the configuration".format( alias=alias_name)) self._open_subgroup() if 'bool' in token_dict: self.stack_pointer['bool'] = token_dict['bool'] # Calling BaseQuery._build() directly and not the parent's one to avoid resetting the stack BaseQuery._build(self, self.config['aliases'][alias_name]) # pylint: disable=protected-access self._close_subgroup() return True