Package ganeti :: Module qlang
[hide private]
[frames] | no frames]

Source Code for Module ganeti.qlang

  1  # 
  2  # 
  3   
  4  # Copyright (C) 2010, 2011, 2012 Google Inc. 
  5  # 
  6  # This program is free software; you can redistribute it and/or modify 
  7  # it under the terms of the GNU General Public License as published by 
  8  # the Free Software Foundation; either version 2 of the License, or 
  9  # (at your option) any later version. 
 10  # 
 11  # This program is distributed in the hope that it will be useful, but 
 12  # WITHOUT ANY WARRANTY; without even the implied warranty of 
 13  # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU 
 14  # General Public License for more details. 
 15  # 
 16  # You should have received a copy of the GNU General Public License 
 17  # along with this program; if not, write to the Free Software 
 18  # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 
 19  # 02110-1301, USA. 
 20   
 21   
 22  """Module for a simple query language 
 23   
 24  A query filter is always a list. The first item in the list is the operator 
 25  (e.g. C{[OP_AND, ...]}), while the other items depend on the operator. For 
 26  logic operators (e.g. L{OP_AND}, L{OP_OR}), they are subfilters whose results 
 27  are combined. Unary operators take exactly one other item (e.g. a subfilter for 
 28  L{OP_NOT} and a field name for L{OP_TRUE}). Binary operators take exactly two 
 29  operands, usually a field name and a value to compare against. Filters are 
 30  converted to callable functions by L{query._CompileFilter}. 
 31   
 32  """ 
 33   
 34  import re 
 35  import string # pylint: disable=W0402 
 36  import logging 
 37   
 38  import pyparsing as pyp 
 39   
 40  from ganeti import errors 
 41  from ganeti import utils 
 42  from ganeti import compat 
 43   
 44   
 45  # Logic operators with one or more operands, each of which is a filter on its 
 46  # own 
 47  OP_OR = "|" 
 48  OP_AND = "&" 
 49   
 50   
 51  # Unary operators with exactly one operand 
 52  OP_NOT = "!" 
 53  OP_TRUE = "?" 
 54   
 55   
 56  # Binary operators with exactly two operands, the field name and an 
 57  # operator-specific value 
 58  OP_EQUAL = "=" 
 59  OP_NOT_EQUAL = "!=" 
 60  OP_LT = "<" 
 61  OP_LE = "<=" 
 62  OP_GT = ">" 
 63  OP_GE = ">=" 
 64  OP_REGEXP = "=~" 
 65  OP_CONTAINS = "=[]" 
 66   
 67   
 68  #: Characters used for detecting user-written filters (see L{_CheckFilter}) 
 69  FILTER_DETECTION_CHARS = frozenset("()=/!~'\"\\<>" + string.whitespace) 
 70   
 71  #: Characters used to detect globbing filters (see L{_CheckGlobbing}) 
 72  GLOB_DETECTION_CHARS = frozenset("*?") 
 73   
 74   
75 -def MakeSimpleFilter(namefield, values):
76 """Builds simple a filter. 77 78 @param namefield: Name of field containing item name 79 @param values: List of names 80 81 """ 82 if values: 83 return [OP_OR] + [[OP_EQUAL, namefield, i] for i in values] 84 85 return None
86 87
88 -def _ConvertLogicOp(op):
89 """Creates parsing action function for logic operator. 90 91 @type op: string 92 @param op: Operator for data structure, e.g. L{OP_AND} 93 94 """ 95 def fn(toks): 96 """Converts parser tokens to query operator structure. 97 98 @rtype: list 99 @return: Query operator structure, e.g. C{[OP_AND, ["=", "foo", "bar"]]} 100 101 """ 102 operands = toks[0] 103 104 if len(operands) == 1: 105 return operands[0] 106 107 # Build query operator structure 108 return [[op] + operands.asList()]
109 110 return fn 111 112 113 _KNOWN_REGEXP_DELIM = "/#^|" 114 _KNOWN_REGEXP_FLAGS = frozenset("si") 115 116
117 -def _ConvertRegexpValue(_, loc, toks):
118 """Regular expression value for condition. 119 120 """ 121 (regexp, flags) = toks[0] 122 123 # Ensure only whitelisted flags are used 124 unknown_flags = (frozenset(flags) - _KNOWN_REGEXP_FLAGS) 125 if unknown_flags: 126 raise pyp.ParseFatalException("Unknown regular expression flags: '%s'" % 127 "".join(unknown_flags), loc) 128 129 if flags: 130 re_flags = "(?%s)" % "".join(sorted(flags)) 131 else: 132 re_flags = "" 133 134 re_cond = re_flags + regexp 135 136 # Test if valid 137 try: 138 re.compile(re_cond) 139 except re.error, err: 140 raise pyp.ParseFatalException("Invalid regular expression (%s)" % err, loc) 141 142 return [re_cond]
143 144
145 -def BuildFilterParser():
146 """Builds a parser for query filter strings. 147 148 @rtype: pyparsing.ParserElement 149 150 """ 151 field_name = pyp.Word(pyp.alphas, pyp.alphanums + "_/.") 152 153 # Integer 154 num_sign = pyp.Word("-+", exact=1) 155 number = pyp.Combine(pyp.Optional(num_sign) + pyp.Word(pyp.nums)) 156 number.setParseAction(lambda toks: int(toks[0])) 157 158 quoted_string = pyp.quotedString.copy().setParseAction(pyp.removeQuotes) 159 160 # Right-hand-side value 161 rval = (number | quoted_string) 162 163 # Boolean condition 164 bool_cond = field_name.copy() 165 bool_cond.setParseAction(lambda (fname, ): [[OP_TRUE, fname]]) 166 167 # Simple binary conditions 168 binopstbl = { 169 "==": OP_EQUAL, 170 "!=": OP_NOT_EQUAL, 171 "<": OP_LT, 172 "<=": OP_LE, 173 ">": OP_GT, 174 ">=": OP_GE, 175 } 176 177 binary_cond = (field_name + pyp.oneOf(binopstbl.keys()) + rval) 178 binary_cond.setParseAction(lambda (lhs, op, rhs): [[binopstbl[op], lhs, rhs]]) 179 180 # "in" condition 181 in_cond = (rval + pyp.Suppress("in") + field_name) 182 in_cond.setParseAction(lambda (value, field): [[OP_CONTAINS, field, value]]) 183 184 # "not in" condition 185 not_in_cond = (rval + pyp.Suppress("not") + pyp.Suppress("in") + field_name) 186 not_in_cond.setParseAction(lambda (value, field): [[OP_NOT, [OP_CONTAINS, 187 field, value]]]) 188 189 # Regular expression, e.g. m/foobar/i 190 regexp_val = pyp.Group(pyp.Optional("m").suppress() + 191 pyp.MatchFirst([pyp.QuotedString(i, escChar="\\") 192 for i in _KNOWN_REGEXP_DELIM]) + 193 pyp.Optional(pyp.Word(pyp.alphas), default="")) 194 regexp_val.setParseAction(_ConvertRegexpValue) 195 regexp_cond = (field_name + pyp.Suppress("=~") + regexp_val) 196 regexp_cond.setParseAction(lambda (field, value): [[OP_REGEXP, field, value]]) 197 198 not_regexp_cond = (field_name + pyp.Suppress("!~") + regexp_val) 199 not_regexp_cond.setParseAction(lambda (field, value): 200 [[OP_NOT, [OP_REGEXP, field, value]]]) 201 202 # Globbing, e.g. name =* "*.site" 203 glob_cond = (field_name + pyp.Suppress("=*") + quoted_string) 204 glob_cond.setParseAction(lambda (field, value): 205 [[OP_REGEXP, field, 206 utils.DnsNameGlobPattern(value)]]) 207 208 not_glob_cond = (field_name + pyp.Suppress("!*") + quoted_string) 209 not_glob_cond.setParseAction(lambda (field, value): 210 [[OP_NOT, [OP_REGEXP, field, 211 utils.DnsNameGlobPattern(value)]]]) 212 213 # All possible conditions 214 condition = (binary_cond ^ bool_cond ^ 215 in_cond ^ not_in_cond ^ 216 regexp_cond ^ not_regexp_cond ^ 217 glob_cond ^ not_glob_cond) 218 219 # Associativity operators 220 filter_expr = pyp.operatorPrecedence(condition, [ 221 (pyp.Keyword("not").suppress(), 1, pyp.opAssoc.RIGHT, 222 lambda toks: [[OP_NOT, toks[0][0]]]), 223 (pyp.Keyword("and").suppress(), 2, pyp.opAssoc.LEFT, 224 _ConvertLogicOp(OP_AND)), 225 (pyp.Keyword("or").suppress(), 2, pyp.opAssoc.LEFT, 226 _ConvertLogicOp(OP_OR)), 227 ]) 228 229 parser = pyp.StringStart() + filter_expr + pyp.StringEnd() 230 parser.parseWithTabs() 231 232 # Originally C{parser.validate} was called here, but there seems to be some 233 # issue causing it to fail whenever the "not" operator is included above. 234 235 return parser
236 237
238 -def ParseFilter(text, parser=None):
239 """Parses a query filter. 240 241 @type text: string 242 @param text: Query filter 243 @type parser: pyparsing.ParserElement 244 @param parser: Pyparsing object 245 @rtype: list 246 247 """ 248 logging.debug("Parsing as query filter: %s", text) 249 250 if parser is None: 251 parser = BuildFilterParser() 252 253 try: 254 return parser.parseString(text)[0] 255 except pyp.ParseBaseException, err: 256 raise errors.QueryFilterParseError("Failed to parse query filter" 257 " '%s': %s" % (text, err), err)
258 259
260 -def _CheckFilter(text):
261 """CHecks if a string could be a filter. 262 263 @rtype: bool 264 265 """ 266 return bool(frozenset(text) & FILTER_DETECTION_CHARS)
267 268
269 -def _CheckGlobbing(text):
270 """Checks if a string could be a globbing pattern. 271 272 @rtype: bool 273 274 """ 275 return bool(frozenset(text) & GLOB_DETECTION_CHARS)
276 277
278 -def _MakeFilterPart(namefield, text, isnumeric=False):
279 """Generates filter for one argument. 280 281 """ 282 if isnumeric: 283 try: 284 number = int(text) 285 except (TypeError, ValueError), err: 286 raise errors.OpPrereqError("Invalid job ID passed: %s" % str(err), 287 errors.ECODE_INVAL) 288 return [OP_EQUAL, namefield, number] 289 elif _CheckGlobbing(text): 290 return [OP_REGEXP, namefield, utils.DnsNameGlobPattern(text)] 291 else: 292 return [OP_EQUAL, namefield, text]
293 294
295 -def MakeFilter(args, force_filter, namefield=None, isnumeric=False):
296 """Try to make a filter from arguments to a command. 297 298 If the name could be a filter it is parsed as such. If it's just a globbing 299 pattern, e.g. "*.site", such a filter is constructed. As a last resort the 300 names are treated just as a plain name filter. 301 302 @type args: list of string 303 @param args: Arguments to command 304 @type force_filter: bool 305 @param force_filter: Whether to force treatment as a full-fledged filter 306 @type namefield: string 307 @param namefield: Name of field to use for simple filters (use L{None} for 308 a default of "name") 309 @type isnumeric: bool 310 @param isnumeric: Whether the namefield type is numeric, as opposed to 311 the default string type; this influences how the filter is built 312 @rtype: list 313 @return: Query filter 314 315 """ 316 if namefield is None: 317 namefield = "name" 318 319 if (force_filter or 320 (args and len(args) == 1 and _CheckFilter(args[0]))): 321 try: 322 (filter_text, ) = args 323 except (TypeError, ValueError): 324 raise errors.OpPrereqError("Exactly one argument must be given as a" 325 " filter", errors.ECODE_INVAL) 326 327 result = ParseFilter(filter_text) 328 elif args: 329 result = [OP_OR] + map(compat.partial(_MakeFilterPart, namefield, 330 isnumeric=isnumeric), args) 331 else: 332 result = None 333 334 return result
335