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  # All rights reserved. 
  6  # 
  7  # Redistribution and use in source and binary forms, with or without 
  8  # modification, are permitted provided that the following conditions are 
  9  # met: 
 10  # 
 11  # 1. Redistributions of source code must retain the above copyright notice, 
 12  # this list of conditions and the following disclaimer. 
 13  # 
 14  # 2. Redistributions in binary form must reproduce the above copyright 
 15  # notice, this list of conditions and the following disclaimer in the 
 16  # documentation and/or other materials provided with the distribution. 
 17  # 
 18  # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS 
 19  # IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED 
 20  # TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 
 21  # PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR 
 22  # CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, 
 23  # EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, 
 24  # PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR 
 25  # PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF 
 26  # LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING 
 27  # NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 
 28  # SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 
 29   
 30   
 31  """Module for a simple query language 
 32   
 33  A query filter is always a list. The first item in the list is the operator 
 34  (e.g. C{[OP_AND, ...]}), while the other items depend on the operator. For 
 35  logic operators (e.g. L{OP_AND}, L{OP_OR}), they are subfilters whose results 
 36  are combined. Unary operators take exactly one other item (e.g. a subfilter for 
 37  L{OP_NOT} and a field name for L{OP_TRUE}). Binary operators take exactly two 
 38  operands, usually a field name and a value to compare against. Filters are 
 39  converted to callable functions by L{query._CompileFilter}. 
 40   
 41  """ 
 42   
 43  import re 
 44  import logging 
 45   
 46  import pyparsing as pyp 
 47   
 48  from ganeti import constants 
 49  from ganeti import errors 
 50  from ganeti import utils 
 51  from ganeti import compat 
 52   
 53   
 54  OP_OR = constants.QLANG_OP_OR 
 55  OP_AND = constants.QLANG_OP_AND 
 56  OP_NOT = constants.QLANG_OP_NOT 
 57  OP_TRUE = constants.QLANG_OP_TRUE 
 58  OP_EQUAL = constants.QLANG_OP_EQUAL 
 59  OP_NOT_EQUAL = constants.QLANG_OP_NOT_EQUAL 
 60  OP_LT = constants.QLANG_OP_LT 
 61  OP_LE = constants.QLANG_OP_LE 
 62  OP_GT = constants.QLANG_OP_GT 
 63  OP_GE = constants.QLANG_OP_GE 
 64  OP_REGEXP = constants.QLANG_OP_REGEXP 
 65  OP_CONTAINS = constants.QLANG_OP_CONTAINS 
 66  FILTER_DETECTION_CHARS = constants.QLANG_FILTER_DETECTION_CHARS 
 67  GLOB_DETECTION_CHARS = constants.QLANG_GLOB_DETECTION_CHARS 
 68   
 69   
70 -def MakeSimpleFilter(namefield, values):
71 """Builds simple a filter. 72 73 @param namefield: Name of field containing item name 74 @param values: List of names 75 76 """ 77 if values: 78 return [OP_OR] + [[OP_EQUAL, namefield, i] for i in values] 79 80 return None
81 82
83 -def _ConvertLogicOp(op):
84 """Creates parsing action function for logic operator. 85 86 @type op: string 87 @param op: Operator for data structure, e.g. L{OP_AND} 88 89 """ 90 def fn(toks): 91 """Converts parser tokens to query operator structure. 92 93 @rtype: list 94 @return: Query operator structure, e.g. C{[OP_AND, ["=", "foo", "bar"]]} 95 96 """ 97 operands = toks[0] 98 99 if len(operands) == 1: 100 return operands[0] 101 102 # Build query operator structure 103 return [[op] + operands.asList()]
104 105 return fn 106 107 108 _KNOWN_REGEXP_DELIM = "/#^|" 109 _KNOWN_REGEXP_FLAGS = frozenset("si") 110 111
112 -def _ConvertRegexpValue(_, loc, toks):
113 """Regular expression value for condition. 114 115 """ 116 (regexp, flags) = toks[0] 117 118 # Ensure only whitelisted flags are used 119 unknown_flags = (frozenset(flags) - _KNOWN_REGEXP_FLAGS) 120 if unknown_flags: 121 raise pyp.ParseFatalException("Unknown regular expression flags: '%s'" % 122 "".join(unknown_flags), loc) 123 124 if flags: 125 re_flags = "(?%s)" % "".join(sorted(flags)) 126 else: 127 re_flags = "" 128 129 re_cond = re_flags + regexp 130 131 # Test if valid 132 try: 133 re.compile(re_cond) 134 except re.error, err: 135 raise pyp.ParseFatalException("Invalid regular expression (%s)" % err, loc) 136 137 return [re_cond]
138 139
140 -def BuildFilterParser():
141 """Builds a parser for query filter strings. 142 143 @rtype: pyparsing.ParserElement 144 145 """ 146 field_name = pyp.Word(pyp.alphas, pyp.alphanums + "_/.") 147 148 # Integer 149 num_sign = pyp.Word("-+", exact=1) 150 number = pyp.Combine(pyp.Optional(num_sign) + pyp.Word(pyp.nums)) 151 number.setParseAction(lambda toks: int(toks[0])) 152 153 quoted_string = pyp.quotedString.copy().setParseAction(pyp.removeQuotes) 154 155 # Right-hand-side value 156 rval = (number | quoted_string) 157 158 # Boolean condition 159 bool_cond = field_name.copy() 160 bool_cond.setParseAction(lambda (fname, ): [[OP_TRUE, fname]]) 161 162 # Simple binary conditions 163 binopstbl = { 164 "==": OP_EQUAL, 165 "!=": OP_NOT_EQUAL, 166 "<": OP_LT, 167 "<=": OP_LE, 168 ">": OP_GT, 169 ">=": OP_GE, 170 } 171 172 binary_cond = (field_name + pyp.oneOf(binopstbl.keys()) + rval) 173 binary_cond.setParseAction(lambda (lhs, op, rhs): [[binopstbl[op], lhs, rhs]]) 174 175 # "in" condition 176 in_cond = (rval + pyp.Suppress("in") + field_name) 177 in_cond.setParseAction(lambda (value, field): [[OP_CONTAINS, field, value]]) 178 179 # "not in" condition 180 not_in_cond = (rval + pyp.Suppress("not") + pyp.Suppress("in") + field_name) 181 not_in_cond.setParseAction(lambda (value, field): [[OP_NOT, [OP_CONTAINS, 182 field, value]]]) 183 184 # Regular expression, e.g. m/foobar/i 185 regexp_val = pyp.Group(pyp.Optional("m").suppress() + 186 pyp.MatchFirst([pyp.QuotedString(i, escChar="\\") 187 for i in _KNOWN_REGEXP_DELIM]) + 188 pyp.Optional(pyp.Word(pyp.alphas), default="")) 189 regexp_val.setParseAction(_ConvertRegexpValue) 190 regexp_cond = (field_name + pyp.Suppress("=~") + regexp_val) 191 regexp_cond.setParseAction(lambda (field, value): [[OP_REGEXP, field, value]]) 192 193 not_regexp_cond = (field_name + pyp.Suppress("!~") + regexp_val) 194 not_regexp_cond.setParseAction(lambda (field, value): 195 [[OP_NOT, [OP_REGEXP, field, value]]]) 196 197 # Globbing, e.g. name =* "*.site" 198 glob_cond = (field_name + pyp.Suppress("=*") + quoted_string) 199 glob_cond.setParseAction(lambda (field, value): 200 [[OP_REGEXP, field, 201 utils.DnsNameGlobPattern(value)]]) 202 203 not_glob_cond = (field_name + pyp.Suppress("!*") + quoted_string) 204 not_glob_cond.setParseAction(lambda (field, value): 205 [[OP_NOT, [OP_REGEXP, field, 206 utils.DnsNameGlobPattern(value)]]]) 207 208 # All possible conditions 209 condition = (binary_cond ^ bool_cond ^ 210 in_cond ^ not_in_cond ^ 211 regexp_cond ^ not_regexp_cond ^ 212 glob_cond ^ not_glob_cond) 213 214 # Associativity operators 215 filter_expr = pyp.operatorPrecedence(condition, [ 216 (pyp.Keyword("not").suppress(), 1, pyp.opAssoc.RIGHT, 217 lambda toks: [[OP_NOT, toks[0][0]]]), 218 (pyp.Keyword("and").suppress(), 2, pyp.opAssoc.LEFT, 219 _ConvertLogicOp(OP_AND)), 220 (pyp.Keyword("or").suppress(), 2, pyp.opAssoc.LEFT, 221 _ConvertLogicOp(OP_OR)), 222 ]) 223 224 parser = pyp.StringStart() + filter_expr + pyp.StringEnd() 225 parser.parseWithTabs() 226 227 # Originally C{parser.validate} was called here, but there seems to be some 228 # issue causing it to fail whenever the "not" operator is included above. 229 230 return parser
231 232
233 -def ParseFilter(text, parser=None):
234 """Parses a query filter. 235 236 @type text: string 237 @param text: Query filter 238 @type parser: pyparsing.ParserElement 239 @param parser: Pyparsing object 240 @rtype: list 241 242 """ 243 logging.debug("Parsing as query filter: %s", text) 244 245 if parser is None: 246 parser = BuildFilterParser() 247 248 try: 249 return parser.parseString(text)[0] 250 except pyp.ParseBaseException, err: 251 raise errors.QueryFilterParseError("Failed to parse query filter" 252 " '%s': %s" % (text, err), err)
253 254
255 -def _CheckFilter(text):
256 """CHecks if a string could be a filter. 257 258 @rtype: bool 259 260 """ 261 return bool(frozenset(text) & FILTER_DETECTION_CHARS)
262 263
264 -def _CheckGlobbing(text):
265 """Checks if a string could be a globbing pattern. 266 267 @rtype: bool 268 269 """ 270 return bool(frozenset(text) & GLOB_DETECTION_CHARS)
271 272
273 -def _MakeFilterPart(namefield, text, isnumeric=False):
274 """Generates filter for one argument. 275 276 """ 277 if isnumeric: 278 try: 279 number = int(text) 280 except (TypeError, ValueError), err: 281 raise errors.OpPrereqError("Invalid job ID passed: %s" % str(err), 282 errors.ECODE_INVAL) 283 return [OP_EQUAL, namefield, number] 284 elif _CheckGlobbing(text): 285 return [OP_REGEXP, namefield, utils.DnsNameGlobPattern(text)] 286 else: 287 return [OP_EQUAL, namefield, text]
288 289
290 -def MakeFilter(args, force_filter, namefield=None, isnumeric=False):
291 """Try to make a filter from arguments to a command. 292 293 If the name could be a filter it is parsed as such. If it's just a globbing 294 pattern, e.g. "*.site", such a filter is constructed. As a last resort the 295 names are treated just as a plain name filter. 296 297 @type args: list of string 298 @param args: Arguments to command 299 @type force_filter: bool 300 @param force_filter: Whether to force treatment as a full-fledged filter 301 @type namefield: string 302 @param namefield: Name of field to use for simple filters (use L{None} for 303 a default of "name") 304 @type isnumeric: bool 305 @param isnumeric: Whether the namefield type is numeric, as opposed to 306 the default string type; this influences how the filter is built 307 @rtype: list 308 @return: Query filter 309 310 """ 311 if namefield is None: 312 namefield = "name" 313 314 if (force_filter or 315 (args and len(args) == 1 and _CheckFilter(args[0]))): 316 try: 317 (filter_text, ) = args 318 except (TypeError, ValueError): 319 raise errors.OpPrereqError("Exactly one argument must be given as a" 320 " filter", errors.ECODE_INVAL) 321 322 result = ParseFilter(filter_text) 323 elif args: 324 result = [OP_OR] + map(compat.partial(_MakeFilterPart, namefield, 325 isnumeric=isnumeric), args) 326 else: 327 result = None 328 329 return result
330