Merge branch 'stable-2.16' into stable-2.17
[ganeti-github.git] / lib / qlang.py
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
52
53 OP_OR = constants.QLANG_OP_OR
54 OP_AND = constants.QLANG_OP_AND
55 OP_NOT = constants.QLANG_OP_NOT
56 OP_TRUE = constants.QLANG_OP_TRUE
57 OP_EQUAL = constants.QLANG_OP_EQUAL
58 OP_EQUAL_LEGACY = constants.QLANG_OP_EQUAL_LEGACY
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_EQUAL, # legacy support
166 "!=": OP_NOT_EQUAL, # legacy support
167 "<": OP_LT,
168 "<=": OP_LE,
169 ">": OP_GT,
170 ">=": OP_GE,
171 }
172
173 binary_cond = (field_name + pyp.oneOf(binopstbl.keys()) + rval)
174 binary_cond.setParseAction(lambda (lhs, op, rhs): [[binopstbl[op], lhs, rhs]])
175
176 # "in" condition
177 in_cond = (rval + pyp.Suppress("in") + field_name)
178 in_cond.setParseAction(lambda (value, field): [[OP_CONTAINS, field, value]])
179
180 # "not in" condition
181 not_in_cond = (rval + pyp.Suppress("not") + pyp.Suppress("in") + field_name)
182 not_in_cond.setParseAction(lambda (value, field): [[OP_NOT, [OP_CONTAINS,
183 field, value]]])
184
185 # Regular expression, e.g. m/foobar/i
186 regexp_val = pyp.Group(pyp.Optional("m").suppress() +
187 pyp.MatchFirst([pyp.QuotedString(i, escChar="\\")
188 for i in _KNOWN_REGEXP_DELIM]) +
189 pyp.Optional(pyp.Word(pyp.alphas), default=""))
190 regexp_val.setParseAction(_ConvertRegexpValue)
191 regexp_cond = (field_name + pyp.Suppress("=~") + regexp_val)
192 regexp_cond.setParseAction(lambda (field, value): [[OP_REGEXP, field, value]])
193
194 not_regexp_cond = (field_name + pyp.Suppress("!~") + regexp_val)
195 not_regexp_cond.setParseAction(lambda (field, value):
196 [[OP_NOT, [OP_REGEXP, field, value]]])
197
198 # Globbing, e.g. name =* "*.site"
199 glob_cond = (field_name + pyp.Suppress("=*") + quoted_string)
200 glob_cond.setParseAction(lambda (field, value):
201 [[OP_REGEXP, field,
202 utils.DnsNameGlobPattern(value)]])
203
204 not_glob_cond = (field_name + pyp.Suppress("!*") + quoted_string)
205 not_glob_cond.setParseAction(lambda (field, value):
206 [[OP_NOT, [OP_REGEXP, field,
207 utils.DnsNameGlobPattern(value)]]])
208
209 # All possible conditions
210 condition = (binary_cond ^ bool_cond ^
211 in_cond ^ not_in_cond ^
212 regexp_cond ^ not_regexp_cond ^
213 glob_cond ^ not_glob_cond)
214
215 # Associativity operators
216 filter_expr = pyp.operatorPrecedence(condition, [
217 (pyp.Keyword("not").suppress(), 1, pyp.opAssoc.RIGHT,
218 lambda toks: [[OP_NOT, toks[0][0]]]),
219 (pyp.Keyword("and").suppress(), 2, pyp.opAssoc.LEFT,
220 _ConvertLogicOp(OP_AND)),
221 (pyp.Keyword("or").suppress(), 2, pyp.opAssoc.LEFT,
222 _ConvertLogicOp(OP_OR)),
223 ])
224
225 parser = pyp.StringStart() + filter_expr + pyp.StringEnd()
226 parser.parseWithTabs()
227
228 # Originally C{parser.validate} was called here, but there seems to be some
229 # issue causing it to fail whenever the "not" operator is included above.
230
231 return parser
232
233
234 def ParseFilter(text, parser=None):
235 """Parses a query filter.
236
237 @type text: string
238 @param text: Query filter
239 @type parser: pyparsing.ParserElement
240 @param parser: Pyparsing object
241 @rtype: list
242
243 """
244 logging.debug("Parsing as query filter: %s", text)
245
246 if parser is None:
247 parser = BuildFilterParser()
248
249 try:
250 return parser.parseString(text)[0]
251 except pyp.ParseBaseException, err:
252 raise errors.QueryFilterParseError("Failed to parse query filter"
253 " '%s': %s" % (text, err), err)
254
255
256 def _CheckFilter(text):
257 """CHecks if a string could be a filter.
258
259 @rtype: bool
260
261 """
262 return bool(frozenset(text) & FILTER_DETECTION_CHARS)
263
264
265 def _CheckGlobbing(text):
266 """Checks if a string could be a globbing pattern.
267
268 @rtype: bool
269
270 """
271 return bool(frozenset(text) & GLOB_DETECTION_CHARS)
272
273
274 def _MakeFilterPart(namefield, text, isnumeric=False):
275 """Generates filter for one argument.
276
277 """
278 if isnumeric:
279 try:
280 number = int(text)
281 except (TypeError, ValueError), err:
282 raise errors.OpPrereqError("Invalid job ID passed: %s" % str(err),
283 errors.ECODE_INVAL)
284 return [OP_EQUAL, namefield, number]
285 elif _CheckGlobbing(text):
286 return [OP_REGEXP, namefield, utils.DnsNameGlobPattern(text)]
287 else:
288 return [OP_EQUAL, namefield, text]
289
290
291 def MakeFilter(args, force_filter, namefield=None, isnumeric=False):
292 """Try to make a filter from arguments to a command.
293
294 If the name could be a filter it is parsed as such. If it's just a globbing
295 pattern, e.g. "*.site", such a filter is constructed. As a last resort the
296 names are treated just as a plain name filter.
297
298 @type args: list of string
299 @param args: Arguments to command
300 @type force_filter: bool
301 @param force_filter: Whether to force treatment as a full-fledged filter
302 @type namefield: string
303 @param namefield: Name of field to use for simple filters (use L{None} for
304 a default of "name")
305 @type isnumeric: bool
306 @param isnumeric: Whether the namefield type is numeric, as opposed to
307 the default string type; this influences how the filter is built
308 @rtype: list
309 @return: Query filter
310
311 """
312 if namefield is None:
313 namefield = "name"
314
315 if (force_filter or
316 (args and len(args) == 1 and _CheckFilter(args[0]))):
317 try:
318 (filter_text, ) = args
319 except (TypeError, ValueError):
320 raise errors.OpPrereqError("Exactly one argument must be given as a"
321 " filter", errors.ECODE_INVAL)
322
323 result = ParseFilter(filter_text)
324 elif args:
325 result = [OP_OR] + [
326 _MakeFilterPart(namefield, arg, isnumeric=isnumeric)
327 for arg in args
328 ]
329 else:
330 result = None
331
332 return result