Fix wrong option names in QA and cluster-merge
[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_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