query2: Add <, >, <=, >= comparison operators
[ganeti-local] / lib / qlang.py
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