1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31 """Module for a simple query language
32
33 A query filter is always a list. The first item in the list is the operator
34 (e.g. C{[OP_AND, ...]}), while the other items depend on the operator. For
35 logic operators (e.g. L{OP_AND}, L{OP_OR}), they are subfilters whose results
36 are combined. Unary operators take exactly one other item (e.g. a subfilter for
37 L{OP_NOT} and a field name for L{OP_TRUE}). Binary operators take exactly two
38 operands, usually a field name and a value to compare against. Filters are
39 converted to callable functions by L{query._CompileFilter}.
40
41 """
42
43 import re
44 import logging
45
46 import pyparsing as pyp
47
48 from ganeti import constants
49 from ganeti import errors
50 from ganeti import utils
51 from ganeti import compat
52
53
54 OP_OR = constants.QLANG_OP_OR
55 OP_AND = constants.QLANG_OP_AND
56 OP_NOT = constants.QLANG_OP_NOT
57 OP_TRUE = constants.QLANG_OP_TRUE
58 OP_EQUAL = constants.QLANG_OP_EQUAL
59 OP_NOT_EQUAL = constants.QLANG_OP_NOT_EQUAL
60 OP_LT = constants.QLANG_OP_LT
61 OP_LE = constants.QLANG_OP_LE
62 OP_GT = constants.QLANG_OP_GT
63 OP_GE = constants.QLANG_OP_GE
64 OP_REGEXP = constants.QLANG_OP_REGEXP
65 OP_CONTAINS = constants.QLANG_OP_CONTAINS
66 FILTER_DETECTION_CHARS = constants.QLANG_FILTER_DETECTION_CHARS
67 GLOB_DETECTION_CHARS = constants.QLANG_GLOB_DETECTION_CHARS
68
69
71 """Builds simple a filter.
72
73 @param namefield: Name of field containing item name
74 @param values: List of names
75
76 """
77 if values:
78 return [OP_OR] + [[OP_EQUAL, namefield, i] for i in values]
79
80 return None
81
82
84 """Creates parsing action function for logic operator.
85
86 @type op: string
87 @param op: Operator for data structure, e.g. L{OP_AND}
88
89 """
90 def fn(toks):
91 """Converts parser tokens to query operator structure.
92
93 @rtype: list
94 @return: Query operator structure, e.g. C{[OP_AND, ["=", "foo", "bar"]]}
95
96 """
97 operands = toks[0]
98
99 if len(operands) == 1:
100 return operands[0]
101
102
103 return [[op] + operands.asList()]
104
105 return fn
106
107
108 _KNOWN_REGEXP_DELIM = "/#^|"
109 _KNOWN_REGEXP_FLAGS = frozenset("si")
110
111
113 """Regular expression value for condition.
114
115 """
116 (regexp, flags) = toks[0]
117
118
119 unknown_flags = (frozenset(flags) - _KNOWN_REGEXP_FLAGS)
120 if unknown_flags:
121 raise pyp.ParseFatalException("Unknown regular expression flags: '%s'" %
122 "".join(unknown_flags), loc)
123
124 if flags:
125 re_flags = "(?%s)" % "".join(sorted(flags))
126 else:
127 re_flags = ""
128
129 re_cond = re_flags + regexp
130
131
132 try:
133 re.compile(re_cond)
134 except re.error, err:
135 raise pyp.ParseFatalException("Invalid regular expression (%s)" % err, loc)
136
137 return [re_cond]
138
139
141 """Builds a parser for query filter strings.
142
143 @rtype: pyparsing.ParserElement
144
145 """
146 field_name = pyp.Word(pyp.alphas, pyp.alphanums + "_/.")
147
148
149 num_sign = pyp.Word("-+", exact=1)
150 number = pyp.Combine(pyp.Optional(num_sign) + pyp.Word(pyp.nums))
151 number.setParseAction(lambda toks: int(toks[0]))
152
153 quoted_string = pyp.quotedString.copy().setParseAction(pyp.removeQuotes)
154
155
156 rval = (number | quoted_string)
157
158
159 bool_cond = field_name.copy()
160 bool_cond.setParseAction(lambda (fname, ): [[OP_TRUE, fname]])
161
162
163 binopstbl = {
164 "==": OP_EQUAL,
165 "!=": OP_NOT_EQUAL,
166 "<": OP_LT,
167 "<=": OP_LE,
168 ">": OP_GT,
169 ">=": OP_GE,
170 }
171
172 binary_cond = (field_name + pyp.oneOf(binopstbl.keys()) + rval)
173 binary_cond.setParseAction(lambda (lhs, op, rhs): [[binopstbl[op], lhs, rhs]])
174
175
176 in_cond = (rval + pyp.Suppress("in") + field_name)
177 in_cond.setParseAction(lambda (value, field): [[OP_CONTAINS, field, value]])
178
179
180 not_in_cond = (rval + pyp.Suppress("not") + pyp.Suppress("in") + field_name)
181 not_in_cond.setParseAction(lambda (value, field): [[OP_NOT, [OP_CONTAINS,
182 field, value]]])
183
184
185 regexp_val = pyp.Group(pyp.Optional("m").suppress() +
186 pyp.MatchFirst([pyp.QuotedString(i, escChar="\\")
187 for i in _KNOWN_REGEXP_DELIM]) +
188 pyp.Optional(pyp.Word(pyp.alphas), default=""))
189 regexp_val.setParseAction(_ConvertRegexpValue)
190 regexp_cond = (field_name + pyp.Suppress("=~") + regexp_val)
191 regexp_cond.setParseAction(lambda (field, value): [[OP_REGEXP, field, value]])
192
193 not_regexp_cond = (field_name + pyp.Suppress("!~") + regexp_val)
194 not_regexp_cond.setParseAction(lambda (field, value):
195 [[OP_NOT, [OP_REGEXP, field, value]]])
196
197
198 glob_cond = (field_name + pyp.Suppress("=*") + quoted_string)
199 glob_cond.setParseAction(lambda (field, value):
200 [[OP_REGEXP, field,
201 utils.DnsNameGlobPattern(value)]])
202
203 not_glob_cond = (field_name + pyp.Suppress("!*") + quoted_string)
204 not_glob_cond.setParseAction(lambda (field, value):
205 [[OP_NOT, [OP_REGEXP, field,
206 utils.DnsNameGlobPattern(value)]]])
207
208
209 condition = (binary_cond ^ bool_cond ^
210 in_cond ^ not_in_cond ^
211 regexp_cond ^ not_regexp_cond ^
212 glob_cond ^ not_glob_cond)
213
214
215 filter_expr = pyp.operatorPrecedence(condition, [
216 (pyp.Keyword("not").suppress(), 1, pyp.opAssoc.RIGHT,
217 lambda toks: [[OP_NOT, toks[0][0]]]),
218 (pyp.Keyword("and").suppress(), 2, pyp.opAssoc.LEFT,
219 _ConvertLogicOp(OP_AND)),
220 (pyp.Keyword("or").suppress(), 2, pyp.opAssoc.LEFT,
221 _ConvertLogicOp(OP_OR)),
222 ])
223
224 parser = pyp.StringStart() + filter_expr + pyp.StringEnd()
225 parser.parseWithTabs()
226
227
228
229
230 return parser
231
232
234 """Parses a query filter.
235
236 @type text: string
237 @param text: Query filter
238 @type parser: pyparsing.ParserElement
239 @param parser: Pyparsing object
240 @rtype: list
241
242 """
243 logging.debug("Parsing as query filter: %s", text)
244
245 if parser is None:
246 parser = BuildFilterParser()
247
248 try:
249 return parser.parseString(text)[0]
250 except pyp.ParseBaseException, err:
251 raise errors.QueryFilterParseError("Failed to parse query filter"
252 " '%s': %s" % (text, err), err)
253
254
256 """CHecks if a string could be a filter.
257
258 @rtype: bool
259
260 """
261 return bool(frozenset(text) & FILTER_DETECTION_CHARS)
262
263
265 """Checks if a string could be a globbing pattern.
266
267 @rtype: bool
268
269 """
270 return bool(frozenset(text) & GLOB_DETECTION_CHARS)
271
272
288
289
290 -def MakeFilter(args, force_filter, namefield=None, isnumeric=False):
291 """Try to make a filter from arguments to a command.
292
293 If the name could be a filter it is parsed as such. If it's just a globbing
294 pattern, e.g. "*.site", such a filter is constructed. As a last resort the
295 names are treated just as a plain name filter.
296
297 @type args: list of string
298 @param args: Arguments to command
299 @type force_filter: bool
300 @param force_filter: Whether to force treatment as a full-fledged filter
301 @type namefield: string
302 @param namefield: Name of field to use for simple filters (use L{None} for
303 a default of "name")
304 @type isnumeric: bool
305 @param isnumeric: Whether the namefield type is numeric, as opposed to
306 the default string type; this influences how the filter is built
307 @rtype: list
308 @return: Query filter
309
310 """
311 if namefield is None:
312 namefield = "name"
313
314 if (force_filter or
315 (args and len(args) == 1 and _CheckFilter(args[0]))):
316 try:
317 (filter_text, ) = args
318 except (TypeError, ValueError):
319 raise errors.OpPrereqError("Exactly one argument must be given as a"
320 " filter", errors.ECODE_INVAL)
321
322 result = ParseFilter(filter_text)
323 elif args:
324 result = [OP_OR] + map(compat.partial(_MakeFilterPart, namefield,
325 isnumeric=isnumeric), args)
326 else:
327 result = None
328
329 return result
330