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_EQUAL_LEGACY = constants.QLANG_OP_EQUAL_LEGACY 
 60  OP_NOT_EQUAL = constants.QLANG_OP_NOT_EQUAL 
 61  OP_LT = constants.QLANG_OP_LT 
 62  OP_LE = constants.QLANG_OP_LE 
 63  OP_GT = constants.QLANG_OP_GT 
 64  OP_GE = constants.QLANG_OP_GE 
 65  OP_REGEXP = constants.QLANG_OP_REGEXP 
 66  OP_CONTAINS = constants.QLANG_OP_CONTAINS 
 67  FILTER_DETECTION_CHARS = constants.QLANG_FILTER_DETECTION_CHARS 
 68  GLOB_DETECTION_CHARS = constants.QLANG_GLOB_DETECTION_CHARS 
 69   
 70   
71 -def MakeSimpleFilter(namefield, values):
72 """Builds simple a filter. 73 74 @param namefield: Name of field containing item name 75 @param values: List of names 76 77 """ 78 if values: 79 return [OP_OR] + [[OP_EQUAL, namefield, i] for i in values] 80 81 return None
82 83
84 -def _ConvertLogicOp(op):
85 """Creates parsing action function for logic operator. 86 87 @type op: string 88 @param op: Operator for data structure, e.g. L{OP_AND} 89 90 """ 91 def fn(toks): 92 """Converts parser tokens to query operator structure. 93 94 @rtype: list 95 @return: Query operator structure, e.g. C{[OP_AND, ["=", "foo", "bar"]]} 96 97 """ 98 operands = toks[0] 99 100 if len(operands) == 1: 101 return operands[0] 102 103 # Build query operator structure 104 return [[op] + operands.asList()]
105 106 return fn 107 108 109 _KNOWN_REGEXP_DELIM = "/#^|" 110 _KNOWN_REGEXP_FLAGS = frozenset("si") 111 112
113 -def _ConvertRegexpValue(_, loc, toks):
114 """Regular expression value for condition. 115 116 """ 117 (regexp, flags) = toks[0] 118 119 # Ensure only whitelisted flags are used 120 unknown_flags = (frozenset(flags) - _KNOWN_REGEXP_FLAGS) 121 if unknown_flags: 122 raise pyp.ParseFatalException("Unknown regular expression flags: '%s'" % 123 "".join(unknown_flags), loc) 124 125 if flags: 126 re_flags = "(?%s)" % "".join(sorted(flags)) 127 else: 128 re_flags = "" 129 130 re_cond = re_flags + regexp 131 132 # Test if valid 133 try: 134 re.compile(re_cond) 135 except re.error, err: 136 raise pyp.ParseFatalException("Invalid regular expression (%s)" % err, loc) 137 138 return [re_cond]
139 140
141 -def BuildFilterParser():
142 """Builds a parser for query filter strings. 143 144 @rtype: pyparsing.ParserElement 145 146 """ 147 field_name = pyp.Word(pyp.alphas, pyp.alphanums + "_/.") 148 149 # Integer 150 num_sign = pyp.Word("-+", exact=1) 151 number = pyp.Combine(pyp.Optional(num_sign) + pyp.Word(pyp.nums)) 152 number.setParseAction(lambda toks: int(toks[0])) 153 154 quoted_string = pyp.quotedString.copy().setParseAction(pyp.removeQuotes) 155 156 # Right-hand-side value 157 rval = (number | quoted_string) 158 159 # Boolean condition 160 bool_cond = field_name.copy() 161 bool_cond.setParseAction(lambda (fname, ): [[OP_TRUE, fname]]) 162 163 # Simple binary conditions 164 binopstbl = { 165 "==": OP_EQUAL, 166 "=": OP_EQUAL, # legacy support 167 "!=": OP_NOT_EQUAL, # legacy support 168 "<": OP_LT, 169 "<=": OP_LE, 170 ">": OP_GT, 171 ">=": OP_GE, 172 } 173 174 binary_cond = (field_name + pyp.oneOf(binopstbl.keys()) + rval) 175 binary_cond.setParseAction(lambda (lhs, op, rhs): [[binopstbl[op], lhs, rhs]]) 176 177 # "in" condition 178 in_cond = (rval + pyp.Suppress("in") + field_name) 179 in_cond.setParseAction(lambda (value, field): [[OP_CONTAINS, field, value]]) 180 181 # "not in" condition 182 not_in_cond = (rval + pyp.Suppress("not") + pyp.Suppress("in") + field_name) 183 not_in_cond.setParseAction(lambda (value, field): [[OP_NOT, [OP_CONTAINS, 184 field, value]]]) 185 186 # Regular expression, e.g. m/foobar/i 187 regexp_val = pyp.Group(pyp.Optional("m").suppress() + 188 pyp.MatchFirst([pyp.QuotedString(i, escChar="\\") 189 for i in _KNOWN_REGEXP_DELIM]) + 190 pyp.Optional(pyp.Word(pyp.alphas), default="")) 191 regexp_val.setParseAction(_ConvertRegexpValue) 192 regexp_cond = (field_name + pyp.Suppress("=~") + regexp_val) 193 regexp_cond.setParseAction(lambda (field, value): [[OP_REGEXP, field, value]]) 194 195 not_regexp_cond = (field_name + pyp.Suppress("!~") + regexp_val) 196 not_regexp_cond.setParseAction(lambda (field, value): 197 [[OP_NOT, [OP_REGEXP, field, value]]]) 198 199 # Globbing, e.g. name =* "*.site" 200 glob_cond = (field_name + pyp.Suppress("=*") + quoted_string) 201 glob_cond.setParseAction(lambda (field, value): 202 [[OP_REGEXP, field, 203 utils.DnsNameGlobPattern(value)]]) 204 205 not_glob_cond = (field_name + pyp.Suppress("!*") + quoted_string) 206 not_glob_cond.setParseAction(lambda (field, value): 207 [[OP_NOT, [OP_REGEXP, field, 208 utils.DnsNameGlobPattern(value)]]]) 209 210 # All possible conditions 211 condition = (binary_cond ^ bool_cond ^ 212 in_cond ^ not_in_cond ^ 213 regexp_cond ^ not_regexp_cond ^ 214 glob_cond ^ not_glob_cond) 215 216 # Associativity operators 217 filter_expr = pyp.operatorPrecedence(condition, [ 218 (pyp.Keyword("not").suppress(), 1, pyp.opAssoc.RIGHT, 219 lambda toks: [[OP_NOT, toks[0][0]]]), 220 (pyp.Keyword("and").suppress(), 2, pyp.opAssoc.LEFT, 221 _ConvertLogicOp(OP_AND)), 222 (pyp.Keyword("or").suppress(), 2, pyp.opAssoc.LEFT, 223 _ConvertLogicOp(OP_OR)), 224 ]) 225 226 parser = pyp.StringStart() + filter_expr + pyp.StringEnd() 227 parser.parseWithTabs() 228 229 # Originally C{parser.validate} was called here, but there seems to be some 230 # issue causing it to fail whenever the "not" operator is included above. 231 232 return parser
233 234
235 -def ParseFilter(text, parser=None):
236 """Parses a query filter. 237 238 @type text: string 239 @param text: Query filter 240 @type parser: pyparsing.ParserElement 241 @param parser: Pyparsing object 242 @rtype: list 243 244 """ 245 logging.debug("Parsing as query filter: %s", text) 246 247 if parser is None: 248 parser = BuildFilterParser() 249 250 try: 251 return parser.parseString(text)[0] 252 except pyp.ParseBaseException, err: 253 raise errors.QueryFilterParseError("Failed to parse query filter" 254 " '%s': %s" % (text, err), err)
255 256
257 -def _CheckFilter(text):
258 """CHecks if a string could be a filter. 259 260 @rtype: bool 261 262 """ 263 return bool(frozenset(text) & FILTER_DETECTION_CHARS)
264 265
266 -def _CheckGlobbing(text):
267 """Checks if a string could be a globbing pattern. 268 269 @rtype: bool 270 271 """ 272 return bool(frozenset(text) & GLOB_DETECTION_CHARS)
273 274
275 -def _MakeFilterPart(namefield, text, isnumeric=False):
276 """Generates filter for one argument. 277 278 """ 279 if isnumeric: 280 try: 281 number = int(text) 282 except (TypeError, ValueError), err: 283 raise errors.OpPrereqError("Invalid job ID passed: %s" % str(err), 284 errors.ECODE_INVAL) 285 return [OP_EQUAL, namefield, number] 286 elif _CheckGlobbing(text): 287 return [OP_REGEXP, namefield, utils.DnsNameGlobPattern(text)] 288 else: 289 return [OP_EQUAL, namefield, text]
290 291
292 -def MakeFilter(args, force_filter, namefield=None, isnumeric=False):
293 """Try to make a filter from arguments to a command. 294 295 If the name could be a filter it is parsed as such. If it's just a globbing 296 pattern, e.g. "*.site", such a filter is constructed. As a last resort the 297 names are treated just as a plain name filter. 298 299 @type args: list of string 300 @param args: Arguments to command 301 @type force_filter: bool 302 @param force_filter: Whether to force treatment as a full-fledged filter 303 @type namefield: string 304 @param namefield: Name of field to use for simple filters (use L{None} for 305 a default of "name") 306 @type isnumeric: bool 307 @param isnumeric: Whether the namefield type is numeric, as opposed to 308 the default string type; this influences how the filter is built 309 @rtype: list 310 @return: Query filter 311 312 """ 313 if namefield is None: 314 namefield = "name" 315 316 if (force_filter or 317 (args and len(args) == 1 and _CheckFilter(args[0]))): 318 try: 319 (filter_text, ) = args 320 except (TypeError, ValueError): 321 raise errors.OpPrereqError("Exactly one argument must be given as a" 322 " filter", errors.ECODE_INVAL) 323 324 result = ParseFilter(filter_text) 325 elif args: 326 result = [OP_OR] + map(compat.partial(_MakeFilterPart, namefield, 327 isnumeric=isnumeric), args) 328 else: 329 result = None 330 331 return result
332