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

Source Code for Module ganeti.qlang

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