| # |
| # |
| |
| # Copyright (C) 2010, 2011, 2012 Google Inc. |
| # |
| # This program is free software; you can redistribute it and/or modify |
| # it under the terms of the GNU General Public License as published by |
| # the Free Software Foundation; either version 2 of the License, or |
| # (at your option) any later version. |
| # |
| # This program is distributed in the hope that it will be useful, but |
| # WITHOUT ANY WARRANTY; without even the implied warranty of |
| # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU |
| # General Public License for more details. |
| # |
| # You should have received a copy of the GNU General Public License |
| # along with this program; if not, write to the Free Software |
| # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA |
| # 02110-1301, USA. |
| |
| |
| """Module for a simple query language |
| |
| A query filter is always a list. The first item in the list is the operator |
| (e.g. C{[OP_AND, ...]}), while the other items depend on the operator. For |
| logic operators (e.g. L{OP_AND}, L{OP_OR}), they are subfilters whose results |
| are combined. Unary operators take exactly one other item (e.g. a subfilter for |
| L{OP_NOT} and a field name for L{OP_TRUE}). Binary operators take exactly two |
| operands, usually a field name and a value to compare against. Filters are |
| converted to callable functions by L{query._CompileFilter}. |
| |
| """ |
| |
| import re |
| import string # pylint: disable=W0402 |
| import logging |
| |
| import pyparsing as pyp |
| |
| from ganeti import errors |
| from ganeti import utils |
| from ganeti import compat |
| |
| |
| # Logic operators with one or more operands, each of which is a filter on its |
| # own |
| OP_OR = "|" |
| OP_AND = "&" |
| |
| |
| # Unary operators with exactly one operand |
| OP_NOT = "!" |
| OP_TRUE = "?" |
| |
| |
| # Binary operators with exactly two operands, the field name and an |
| # operator-specific value |
| OP_EQUAL = "=" |
| OP_NOT_EQUAL = "!=" |
| OP_LT = "<" |
| OP_LE = "<=" |
| OP_GT = ">" |
| OP_GE = ">=" |
| OP_REGEXP = "=~" |
| OP_CONTAINS = "=[]" |
| |
| |
| #: Characters used for detecting user-written filters (see L{_CheckFilter}) |
| FILTER_DETECTION_CHARS = frozenset("()=/!~'\"\\<>" + string.whitespace) |
| |
| #: Characters used to detect globbing filters (see L{_CheckGlobbing}) |
| GLOB_DETECTION_CHARS = frozenset("*?") |
| |
| |
| def MakeSimpleFilter(namefield, values): |
| """Builds simple a filter. |
| |
| @param namefield: Name of field containing item name |
| @param values: List of names |
| |
| """ |
| if values: |
| return [OP_OR] + [[OP_EQUAL, namefield, i] for i in values] |
| |
| return None |
| |
| |
| def _ConvertLogicOp(op): |
| """Creates parsing action function for logic operator. |
| |
| @type op: string |
| @param op: Operator for data structure, e.g. L{OP_AND} |
| |
| """ |
| def fn(toks): |
| """Converts parser tokens to query operator structure. |
| |
| @rtype: list |
| @return: Query operator structure, e.g. C{[OP_AND, ["=", "foo", "bar"]]} |
| |
| """ |
| operands = toks[0] |
| |
| if len(operands) == 1: |
| return operands[0] |
| |
| # Build query operator structure |
| return [[op] + operands.asList()] |
| |
| return fn |
| |
| |
| _KNOWN_REGEXP_DELIM = "/#^|" |
| _KNOWN_REGEXP_FLAGS = frozenset("si") |
| |
| |
| def _ConvertRegexpValue(_, loc, toks): |
| """Regular expression value for condition. |
| |
| """ |
| (regexp, flags) = toks[0] |
| |
| # Ensure only whitelisted flags are used |
| unknown_flags = (frozenset(flags) - _KNOWN_REGEXP_FLAGS) |
| if unknown_flags: |
| raise pyp.ParseFatalException("Unknown regular expression flags: '%s'" % |
| "".join(unknown_flags), loc) |
| |
| if flags: |
| re_flags = "(?%s)" % "".join(sorted(flags)) |
| else: |
| re_flags = "" |
| |
| re_cond = re_flags + regexp |
| |
| # Test if valid |
| try: |
| re.compile(re_cond) |
| except re.error, err: |
| raise pyp.ParseFatalException("Invalid regular expression (%s)" % err, loc) |
| |
| return [re_cond] |
| |
| |
| def BuildFilterParser(): |
| """Builds a parser for query filter strings. |
| |
| @rtype: pyparsing.ParserElement |
| |
| """ |
| field_name = pyp.Word(pyp.alphas, pyp.alphanums + "_/.") |
| |
| # Integer |
| num_sign = pyp.Word("-+", exact=1) |
| number = pyp.Combine(pyp.Optional(num_sign) + pyp.Word(pyp.nums)) |
| number.setParseAction(lambda toks: int(toks[0])) |
| |
| quoted_string = pyp.quotedString.copy().setParseAction(pyp.removeQuotes) |
| |
| # Right-hand-side value |
| rval = (number | quoted_string) |
| |
| # Boolean condition |
| bool_cond = field_name.copy() |
| bool_cond.setParseAction(lambda (fname, ): [[OP_TRUE, fname]]) |
| |
| # Simple binary conditions |
| binopstbl = { |
| "==": OP_EQUAL, |
| "!=": OP_NOT_EQUAL, |
| "<": OP_LT, |
| "<=": OP_LE, |
| ">": OP_GT, |
| ">=": OP_GE, |
| } |
| |
| binary_cond = (field_name + pyp.oneOf(binopstbl.keys()) + rval) |
| binary_cond.setParseAction(lambda (lhs, op, rhs): [[binopstbl[op], lhs, rhs]]) |
| |
| # "in" condition |
| in_cond = (rval + pyp.Suppress("in") + field_name) |
| in_cond.setParseAction(lambda (value, field): [[OP_CONTAINS, field, value]]) |
| |
| # "not in" condition |
| not_in_cond = (rval + pyp.Suppress("not") + pyp.Suppress("in") + field_name) |
| not_in_cond.setParseAction(lambda (value, field): [[OP_NOT, [OP_CONTAINS, |
| field, value]]]) |
| |
| # Regular expression, e.g. m/foobar/i |
| regexp_val = pyp.Group(pyp.Optional("m").suppress() + |
| pyp.MatchFirst([pyp.QuotedString(i, escChar="\\") |
| for i in _KNOWN_REGEXP_DELIM]) + |
| pyp.Optional(pyp.Word(pyp.alphas), default="")) |
| regexp_val.setParseAction(_ConvertRegexpValue) |
| regexp_cond = (field_name + pyp.Suppress("=~") + regexp_val) |
| regexp_cond.setParseAction(lambda (field, value): [[OP_REGEXP, field, value]]) |
| |
| not_regexp_cond = (field_name + pyp.Suppress("!~") + regexp_val) |
| not_regexp_cond.setParseAction(lambda (field, value): |
| [[OP_NOT, [OP_REGEXP, field, value]]]) |
| |
| # Globbing, e.g. name =* "*.site" |
| glob_cond = (field_name + pyp.Suppress("=*") + quoted_string) |
| glob_cond.setParseAction(lambda (field, value): |
| [[OP_REGEXP, field, |
| utils.DnsNameGlobPattern(value)]]) |
| |
| not_glob_cond = (field_name + pyp.Suppress("!*") + quoted_string) |
| not_glob_cond.setParseAction(lambda (field, value): |
| [[OP_NOT, [OP_REGEXP, field, |
| utils.DnsNameGlobPattern(value)]]]) |
| |
| # All possible conditions |
| condition = (binary_cond ^ bool_cond ^ |
| in_cond ^ not_in_cond ^ |
| regexp_cond ^ not_regexp_cond ^ |
| glob_cond ^ not_glob_cond) |
| |
| # Associativity operators |
| filter_expr = pyp.operatorPrecedence(condition, [ |
| (pyp.Keyword("not").suppress(), 1, pyp.opAssoc.RIGHT, |
| lambda toks: [[OP_NOT, toks[0][0]]]), |
| (pyp.Keyword("and").suppress(), 2, pyp.opAssoc.LEFT, |
| _ConvertLogicOp(OP_AND)), |
| (pyp.Keyword("or").suppress(), 2, pyp.opAssoc.LEFT, |
| _ConvertLogicOp(OP_OR)), |
| ]) |
| |
| parser = pyp.StringStart() + filter_expr + pyp.StringEnd() |
| parser.parseWithTabs() |
| |
| # Originally C{parser.validate} was called here, but there seems to be some |
| # issue causing it to fail whenever the "not" operator is included above. |
| |
| return parser |
| |
| |
| def ParseFilter(text, parser=None): |
| """Parses a query filter. |
| |
| @type text: string |
| @param text: Query filter |
| @type parser: pyparsing.ParserElement |
| @param parser: Pyparsing object |
| @rtype: list |
| |
| """ |
| logging.debug("Parsing as query filter: %s", text) |
| |
| if parser is None: |
| parser = BuildFilterParser() |
| |
| try: |
| return parser.parseString(text)[0] |
| except pyp.ParseBaseException, err: |
| raise errors.QueryFilterParseError("Failed to parse query filter" |
| " '%s': %s" % (text, err), err) |
| |
| |
| def _CheckFilter(text): |
| """CHecks if a string could be a filter. |
| |
| @rtype: bool |
| |
| """ |
| return bool(frozenset(text) & FILTER_DETECTION_CHARS) |
| |
| |
| def _CheckGlobbing(text): |
| """Checks if a string could be a globbing pattern. |
| |
| @rtype: bool |
| |
| """ |
| return bool(frozenset(text) & GLOB_DETECTION_CHARS) |
| |
| |
| def _MakeFilterPart(namefield, text, isnumeric=False): |
| """Generates filter for one argument. |
| |
| """ |
| if isnumeric: |
| try: |
| number = int(text) |
| except (TypeError, ValueError), err: |
| raise errors.OpPrereqError("Invalid job ID passed: %s" % str(err), |
| errors.ECODE_INVAL) |
| return [OP_EQUAL, namefield, number] |
| elif _CheckGlobbing(text): |
| return [OP_REGEXP, namefield, utils.DnsNameGlobPattern(text)] |
| else: |
| return [OP_EQUAL, namefield, text] |
| |
| |
| def MakeFilter(args, force_filter, namefield=None, isnumeric=False): |
| """Try to make a filter from arguments to a command. |
| |
| If the name could be a filter it is parsed as such. If it's just a globbing |
| pattern, e.g. "*.site", such a filter is constructed. As a last resort the |
| names are treated just as a plain name filter. |
| |
| @type args: list of string |
| @param args: Arguments to command |
| @type force_filter: bool |
| @param force_filter: Whether to force treatment as a full-fledged filter |
| @type namefield: string |
| @param namefield: Name of field to use for simple filters (use L{None} for |
| a default of "name") |
| @type isnumeric: bool |
| @param isnumeric: Whether the namefield type is numeric, as opposed to |
| the default string type; this influences how the filter is built |
| @rtype: list |
| @return: Query filter |
| |
| """ |
| if namefield is None: |
| namefield = "name" |
| |
| if (force_filter or |
| (args and len(args) == 1 and _CheckFilter(args[0]))): |
| try: |
| (filter_text, ) = args |
| except (TypeError, ValueError): |
| raise errors.OpPrereqError("Exactly one argument must be given as a" |
| " filter", errors.ECODE_INVAL) |
| |
| result = ParseFilter(filter_text) |
| elif args: |
| result = [OP_OR] + map(compat.partial(_MakeFilterPart, namefield, |
| isnumeric=isnumeric), args) |
| else: |
| result = None |
| |
| return result |