X-Git-Url: https://code.grnet.gr/git/ganeti-local/blobdiff_plain/7578ab0a81db437f5340c9d574217beb525104b7..4b8b172db2da14e7bf51a6ef54afac8cf97c8c05:/lib/qlang.py diff --git a/lib/qlang.py b/lib/qlang.py index d16e736..2352391 100644 --- a/lib/qlang.py +++ b/lib/qlang.py @@ -1,7 +1,7 @@ # # -# Copyright (C) 2010, 2011 Google Inc. +# 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 @@ -32,10 +32,14 @@ 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 @@ -53,10 +57,21 @@ OP_TRUE = "?" # 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. @@ -140,8 +155,10 @@ def BuildFilterParser(): 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 | pyp.quotedString.setParseAction(pyp.removeQuotes)) + rval = (number | quoted_string) # Boolean condition bool_cond = field_name.copy() @@ -151,6 +168,10 @@ def BuildFilterParser(): binopstbl = { "==": OP_EQUAL, "!=": OP_NOT_EQUAL, + "<": OP_LT, + "<=": OP_LE, + ">": OP_GT, + ">=": OP_GE, } binary_cond = (field_name + pyp.oneOf(binopstbl.keys()) + rval) @@ -178,10 +199,22 @@ def BuildFilterParser(): 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) + regexp_cond ^ not_regexp_cond ^ + glob_cond ^ not_glob_cond) # Associativity operators filter_expr = pyp.operatorPrecedence(condition, [ @@ -212,6 +245,8 @@ def ParseFilter(text, parser=None): @rtype: list """ + logging.debug("Parsing as query filter: %s", text) + if parser is None: parser = BuildFilterParser() @@ -220,3 +255,80 @@ def ParseFilter(text, parser=None): 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