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