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_REGEXP = "=~"
62 OP_CONTAINS = "=[]"
63
64
65
66 FILTER_DETECTION_CHARS = frozenset("()=/!~'\"\\" + string.whitespace)
67
68
69 GLOB_DETECTION_CHARS = frozenset("*?")
70
71
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
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
105 return [[op] + operands.asList()]
106
107 return fn
108
109
110 _KNOWN_REGEXP_DELIM = "/#^|"
111 _KNOWN_REGEXP_FLAGS = frozenset("si")
112
113
115 """Regular expression value for condition.
116
117 """
118 (regexp, flags) = toks[0]
119
120
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
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
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
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
158 rval = (number | quoted_string)
159
160
161 bool_cond = field_name.copy()
162 bool_cond.setParseAction(lambda (fname, ): [[OP_TRUE, fname]])
163
164
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
174 in_cond = (rval + pyp.Suppress("in") + field_name)
175 in_cond.setParseAction(lambda (value, field): [[OP_CONTAINS, field, value]])
176
177
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
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
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
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
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
226
227
228 return parser
229
230
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
265
266
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
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
293
294
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
325