1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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
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
46
47 OP_OR = "|"
48 OP_AND = "&"
49
50
51
52 OP_NOT = "!"
53 OP_TRUE = "?"
54
55
56
57
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
69 FILTER_DETECTION_CHARS = frozenset("()=/!~'\"\\<>" + string.whitespace)
70
71
72 GLOB_DETECTION_CHARS = frozenset("*?")
73
74
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
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
108 return [[op] + operands.asList()]
109
110 return fn
111
112
113 _KNOWN_REGEXP_DELIM = "/#^|"
114 _KNOWN_REGEXP_FLAGS = frozenset("si")
115
116
118 """Regular expression value for condition.
119
120 """
121 (regexp, flags) = toks[0]
122
123
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
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
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
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
161 rval = (number | quoted_string)
162
163
164 bool_cond = field_name.copy()
165 bool_cond.setParseAction(lambda (fname, ): [[OP_TRUE, fname]])
166
167
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
181 in_cond = (rval + pyp.Suppress("in") + field_name)
182 in_cond.setParseAction(lambda (value, field): [[OP_CONTAINS, field, value]])
183
184
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
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
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
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
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
233
234
235 return parser
236
237
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
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
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
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
335