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 netutils
42 from ganeti import utils
43 from ganeti import compat
44
45
46
47
48 OP_OR = "|"
49 OP_AND = "&"
50
51
52
53 OP_NOT = "!"
54 OP_TRUE = "?"
55
56
57
58
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
70 FILTER_DETECTION_CHARS = frozenset("()=/!~'\"\\<>" + string.whitespace)
71
72
73 GLOB_DETECTION_CHARS = frozenset("*?")
74
75
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
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
109 return [[op] + operands.asList()]
110
111 return fn
112
113
114 _KNOWN_REGEXP_DELIM = "/#^|"
115 _KNOWN_REGEXP_FLAGS = frozenset("si")
116
117
119 """Regular expression value for condition.
120
121 """
122 (regexp, flags) = toks[0]
123
124
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
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
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
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
162 rval = (number | quoted_string)
163
164
165 bool_cond = field_name.copy()
166 bool_cond.setParseAction(lambda (fname, ): [[OP_TRUE, fname]])
167
168
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
182 in_cond = (rval + pyp.Suppress("in") + field_name)
183 in_cond.setParseAction(lambda (value, field): [[OP_CONTAINS, field, value]])
184
185
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
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
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
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
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
234
235
236 return parser
237
238
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
273
274
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
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
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
339