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