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