Revision fb0be379 lib/query.py
b/lib/query.py | ||
---|---|---|
48 | 48 |
- Call L{Query.GetFields} to get list of definitions for selected fields |
49 | 49 |
|
50 | 50 |
@attention: Retrieval functions must be idempotent. They can be called multiple |
51 |
times, in any order and any number of times. This is important to keep in |
|
52 |
mind for implementing filters in the future. |
|
51 |
times, in any order and any number of times. |
|
53 | 52 |
|
54 | 53 |
""" |
55 | 54 |
|
... | ... | |
63 | 62 |
from ganeti import compat |
64 | 63 |
from ganeti import objects |
65 | 64 |
from ganeti import ht |
65 |
from ganeti import qlang |
|
66 | 66 |
|
67 | 67 |
from ganeti.constants import (QFT_UNKNOWN, QFT_TEXT, QFT_BOOL, QFT_NUMBER, |
68 | 68 |
QFT_UNIT, QFT_TIMESTAMP, QFT_OTHER, |
... | ... | |
176 | 176 |
return [fdef for (fdef, _, _, _) in fielddefs] |
177 | 177 |
|
178 | 178 |
|
179 |
class _FilterHints: |
|
180 |
"""Class for filter analytics. |
|
181 |
|
|
182 |
When filters are used, the user of the L{Query} class usually doesn't know |
|
183 |
exactly which items will be necessary for building the result. It therefore |
|
184 |
has to prepare and compute the input data for potentially returning |
|
185 |
everything. |
|
186 |
|
|
187 |
There are two ways to optimize this. The first, and simpler, is to assign |
|
188 |
each field a group of data, so that the caller can determine which |
|
189 |
computations are necessary depending on the data groups requested. The list |
|
190 |
of referenced groups must also be computed for fields referenced in the |
|
191 |
filter. |
|
192 |
|
|
193 |
The second is restricting the items based on a primary key. The primary key |
|
194 |
is usually a unique name (e.g. a node name). This class extracts all |
|
195 |
referenced names from a filter. If it encounters any filter condition which |
|
196 |
disallows such a list to be determined (e.g. a non-equality filter), all |
|
197 |
names will be requested. |
|
198 |
|
|
199 |
The end-effect is that any operation other than L{qlang.OP_OR} and |
|
200 |
L{qlang.OP_EQUAL} will make the query more expensive. |
|
201 |
|
|
202 |
""" |
|
203 |
def __init__(self, namefield): |
|
204 |
"""Initializes this class. |
|
205 |
|
|
206 |
@type namefield: string |
|
207 |
@param namefield: Field caller is interested in |
|
208 |
|
|
209 |
""" |
|
210 |
self._namefield = namefield |
|
211 |
|
|
212 |
#: Whether all names need to be requested (e.g. if a non-equality operator |
|
213 |
#: has been used) |
|
214 |
self._allnames = False |
|
215 |
|
|
216 |
#: Which names to request |
|
217 |
self._names = None |
|
218 |
|
|
219 |
#: Data kinds referenced by the filter (used by L{Query.RequestedData}) |
|
220 |
self._datakinds = set() |
|
221 |
|
|
222 |
def RequestedNames(self): |
|
223 |
"""Returns all requested values. |
|
224 |
|
|
225 |
Returns C{None} if list of values can't be determined (e.g. encountered |
|
226 |
non-equality operators). |
|
227 |
|
|
228 |
@rtype: list |
|
229 |
|
|
230 |
""" |
|
231 |
if self._allnames or self._names is None: |
|
232 |
return None |
|
233 |
|
|
234 |
return utils.UniqueSequence(self._names) |
|
235 |
|
|
236 |
def ReferencedData(self): |
|
237 |
"""Returns all kinds of data referenced by the filter. |
|
238 |
|
|
239 |
""" |
|
240 |
return frozenset(self._datakinds) |
|
241 |
|
|
242 |
def _NeedAllNames(self): |
|
243 |
"""Changes internal state to request all names. |
|
244 |
|
|
245 |
""" |
|
246 |
self._allnames = True |
|
247 |
self._names = None |
|
248 |
|
|
249 |
def NoteLogicOp(self, op): |
|
250 |
"""Called when handling a logic operation. |
|
251 |
|
|
252 |
@type op: string |
|
253 |
@param op: Operator |
|
254 |
|
|
255 |
""" |
|
256 |
if op != qlang.OP_OR: |
|
257 |
self._NeedAllNames() |
|
258 |
|
|
259 |
def NoteUnaryOp(self, op): # pylint: disable-msg=W0613 |
|
260 |
"""Called when handling an unary operation. |
|
261 |
|
|
262 |
@type op: string |
|
263 |
@param op: Operator |
|
264 |
|
|
265 |
""" |
|
266 |
self._NeedAllNames() |
|
267 |
|
|
268 |
def NoteBinaryOp(self, op, datakind, name, value): |
|
269 |
"""Called when handling a binary operation. |
|
270 |
|
|
271 |
@type op: string |
|
272 |
@param op: Operator |
|
273 |
@type name: string |
|
274 |
@param name: Left-hand side of operator (field name) |
|
275 |
@param value: Right-hand side of operator |
|
276 |
|
|
277 |
""" |
|
278 |
if datakind is not None: |
|
279 |
self._datakinds.add(datakind) |
|
280 |
|
|
281 |
if self._allnames: |
|
282 |
return |
|
283 |
|
|
284 |
# If any operator other than equality was used, all names need to be |
|
285 |
# retrieved |
|
286 |
if op == qlang.OP_EQUAL and name == self._namefield: |
|
287 |
if self._names is None: |
|
288 |
self._names = [] |
|
289 |
self._names.append(value) |
|
290 |
else: |
|
291 |
self._NeedAllNames() |
|
292 |
|
|
293 |
|
|
294 |
def _WrapLogicOp(op_fn, sentences, ctx, item): |
|
295 |
"""Wrapper for logic operator functions. |
|
296 |
|
|
297 |
""" |
|
298 |
return op_fn(fn(ctx, item) for fn in sentences) |
|
299 |
|
|
300 |
|
|
301 |
def _WrapUnaryOp(op_fn, inner, ctx, item): |
|
302 |
"""Wrapper for unary operator functions. |
|
303 |
|
|
304 |
""" |
|
305 |
return op_fn(inner(ctx, item)) |
|
306 |
|
|
307 |
|
|
308 |
def _WrapBinaryOp(op_fn, retrieval_fn, value, ctx, item): |
|
309 |
"""Wrapper for binary operator functions. |
|
310 |
|
|
311 |
""" |
|
312 |
return op_fn(retrieval_fn(ctx, item), value) |
|
313 |
|
|
314 |
|
|
315 |
def _WrapNot(fn, lhs, rhs): |
|
316 |
"""Negates the result of a wrapped function. |
|
317 |
|
|
318 |
""" |
|
319 |
return not fn(lhs, rhs) |
|
320 |
|
|
321 |
|
|
322 |
class _FilterCompilerHelper: |
|
323 |
"""Converts a query filter to a callable usable for filtering. |
|
324 |
|
|
325 |
""" |
|
326 |
# String statement has no effect, pylint: disable-msg=W0105 |
|
327 |
|
|
328 |
#: How deep filters can be nested |
|
329 |
_LEVELS_MAX = 10 |
|
330 |
|
|
331 |
# Unique identifiers for operator groups |
|
332 |
(_OPTYPE_LOGIC, |
|
333 |
_OPTYPE_UNARY, |
|
334 |
_OPTYPE_BINARY) = range(1, 4) |
|
335 |
|
|
336 |
"""Functions for equality checks depending on field flags. |
|
337 |
|
|
338 |
List of tuples containing flags and a callable receiving the left- and |
|
339 |
right-hand side of the operator. The flags are an OR-ed value of C{QFF_*} |
|
340 |
(e.g. L{QFF_HOSTNAME}). |
|
341 |
|
|
342 |
Order matters. The first item with flags will be used. Flags are checked |
|
343 |
using binary AND. |
|
344 |
|
|
345 |
""" |
|
346 |
_EQUALITY_CHECKS = [ |
|
347 |
(QFF_HOSTNAME, |
|
348 |
lambda lhs, rhs: utils.MatchNameComponent(rhs, [lhs], |
|
349 |
case_sensitive=False)), |
|
350 |
(None, operator.eq), |
|
351 |
] |
|
352 |
|
|
353 |
"""Known operators |
|
354 |
|
|
355 |
Operator as key (C{qlang.OP_*}), value a tuple of operator group |
|
356 |
(C{_OPTYPE_*}) and a group-specific value: |
|
357 |
|
|
358 |
- C{_OPTYPE_LOGIC}: Callable taking any number of arguments; used by |
|
359 |
L{_HandleLogicOp} |
|
360 |
- C{_OPTYPE_UNARY}: Callable taking exactly one parameter; used by |
|
361 |
L{_HandleUnaryOp} |
|
362 |
- C{_OPTYPE_BINARY}: Callable taking exactly two parameters, the left- and |
|
363 |
right-hand side of the operator, used by L{_HandleBinaryOp} |
|
364 |
|
|
365 |
""" |
|
366 |
_OPS = { |
|
367 |
# Logic operators |
|
368 |
qlang.OP_OR: (_OPTYPE_LOGIC, compat.any), |
|
369 |
qlang.OP_AND: (_OPTYPE_LOGIC, compat.all), |
|
370 |
|
|
371 |
# Unary operators |
|
372 |
qlang.OP_NOT: (_OPTYPE_UNARY, operator.not_), |
|
373 |
|
|
374 |
# Binary operators |
|
375 |
qlang.OP_EQUAL: (_OPTYPE_BINARY, _EQUALITY_CHECKS), |
|
376 |
qlang.OP_NOT_EQUAL: |
|
377 |
(_OPTYPE_BINARY, [(flags, compat.partial(_WrapNot, fn)) |
|
378 |
for (flags, fn) in _EQUALITY_CHECKS]), |
|
379 |
qlang.OP_GLOB: (_OPTYPE_BINARY, NotImplemented), |
|
380 |
qlang.OP_REGEXP: (_OPTYPE_BINARY, NotImplemented), |
|
381 |
qlang.OP_CONTAINS: (_OPTYPE_BINARY, [ |
|
382 |
(None, operator.contains), |
|
383 |
]), |
|
384 |
} |
|
385 |
|
|
386 |
def __init__(self, fields): |
|
387 |
"""Initializes this class. |
|
388 |
|
|
389 |
@param fields: Field definitions (return value of L{_PrepareFieldList}) |
|
390 |
|
|
391 |
""" |
|
392 |
self._fields = fields |
|
393 |
self._hints = None |
|
394 |
self._op_handler = None |
|
395 |
|
|
396 |
def __call__(self, hints, filter_): |
|
397 |
"""Converts a query filter into a callable function. |
|
398 |
|
|
399 |
@type hints: L{_FilterHints} or None |
|
400 |
@param hints: Callbacks doing analysis on filter |
|
401 |
@type filter_: list |
|
402 |
@param filter_: Filter structure |
|
403 |
@rtype: callable |
|
404 |
@return: Function receiving context and item as parameters, returning |
|
405 |
boolean as to whether item matches filter |
|
406 |
|
|
407 |
""" |
|
408 |
self._op_handler = { |
|
409 |
self._OPTYPE_LOGIC: |
|
410 |
(self._HandleLogicOp, getattr(hints, "NoteLogicOp", None)), |
|
411 |
self._OPTYPE_UNARY: |
|
412 |
(self._HandleUnaryOp, getattr(hints, "NoteUnaryOp", None)), |
|
413 |
self._OPTYPE_BINARY: |
|
414 |
(self._HandleBinaryOp, getattr(hints, "NoteBinaryOp", None)), |
|
415 |
} |
|
416 |
|
|
417 |
try: |
|
418 |
filter_fn = self._Compile(filter_, 0) |
|
419 |
finally: |
|
420 |
self._op_handler = None |
|
421 |
|
|
422 |
return filter_fn |
|
423 |
|
|
424 |
def _Compile(self, filter_, level): |
|
425 |
"""Inner function for converting filters. |
|
426 |
|
|
427 |
Calls the correct handler functions for the top-level operator. This |
|
428 |
function is called recursively (e.g. for logic operators). |
|
429 |
|
|
430 |
""" |
|
431 |
if not (isinstance(filter_, (list, tuple)) and filter_): |
|
432 |
raise errors.ParameterError("Invalid filter on level %s" % level) |
|
433 |
|
|
434 |
# Limit recursion |
|
435 |
if level >= self._LEVELS_MAX: |
|
436 |
raise errors.ParameterError("Only up to %s levels are allowed (filter" |
|
437 |
" nested too deep)" % self._LEVELS_MAX) |
|
438 |
|
|
439 |
# Create copy to be modified |
|
440 |
operands = filter_[:] |
|
441 |
op = operands.pop(0) |
|
442 |
|
|
443 |
try: |
|
444 |
(kind, op_data) = self._OPS[op] |
|
445 |
except KeyError: |
|
446 |
raise errors.ParameterError("Unknown operator '%s'" % op) |
|
447 |
|
|
448 |
(handler, hints_cb) = self._op_handler[kind] |
|
449 |
|
|
450 |
return handler(hints_cb, level, op, op_data, operands) |
|
451 |
|
|
452 |
def _HandleLogicOp(self, hints_fn, level, op, op_fn, operands): |
|
453 |
"""Handles logic operators. |
|
454 |
|
|
455 |
@type hints_fn: callable |
|
456 |
@param hints_fn: Callback doing some analysis on the filter |
|
457 |
@type level: integer |
|
458 |
@param level: Current depth |
|
459 |
@type op: string |
|
460 |
@param op: Operator |
|
461 |
@type op_fn: callable |
|
462 |
@param op_fn: Function implementing operator |
|
463 |
@type operands: list |
|
464 |
@param operands: List of operands |
|
465 |
|
|
466 |
""" |
|
467 |
if hints_fn: |
|
468 |
hints_fn(op) |
|
469 |
|
|
470 |
return compat.partial(_WrapLogicOp, op_fn, |
|
471 |
[self._Compile(op, level + 1) for op in operands]) |
|
472 |
|
|
473 |
def _HandleUnaryOp(self, hints_fn, level, op, op_fn, operands): |
|
474 |
"""Handles unary operators. |
|
475 |
|
|
476 |
@type hints_fn: callable |
|
477 |
@param hints_fn: Callback doing some analysis on the filter |
|
478 |
@type level: integer |
|
479 |
@param level: Current depth |
|
480 |
@type op: string |
|
481 |
@param op: Operator |
|
482 |
@type op_fn: callable |
|
483 |
@param op_fn: Function implementing operator |
|
484 |
@type operands: list |
|
485 |
@param operands: List of operands |
|
486 |
|
|
487 |
""" |
|
488 |
if hints_fn: |
|
489 |
hints_fn(op) |
|
490 |
|
|
491 |
if len(operands) != 1: |
|
492 |
raise errors.ParameterError("Unary operator '%s' expects exactly one" |
|
493 |
" operand" % op) |
|
494 |
|
|
495 |
return compat.partial(_WrapUnaryOp, op_fn, |
|
496 |
self._Compile(operands[0], level + 1)) |
|
497 |
|
|
498 |
def _HandleBinaryOp(self, hints_fn, level, op, op_data, operands): |
|
499 |
"""Handles binary operators. |
|
500 |
|
|
501 |
@type hints_fn: callable |
|
502 |
@param hints_fn: Callback doing some analysis on the filter |
|
503 |
@type level: integer |
|
504 |
@param level: Current depth |
|
505 |
@type op: string |
|
506 |
@param op: Operator |
|
507 |
@param op_data: Functions implementing operators |
|
508 |
@type operands: list |
|
509 |
@param operands: List of operands |
|
510 |
|
|
511 |
""" |
|
512 |
# Unused arguments, pylint: disable-msg=W0613 |
|
513 |
try: |
|
514 |
(name, value) = operands |
|
515 |
except (ValueError, TypeError): |
|
516 |
raise errors.ParameterError("Invalid binary operator, expected exactly" |
|
517 |
" two operands") |
|
518 |
|
|
519 |
try: |
|
520 |
(fdef, datakind, field_flags, retrieval_fn) = self._fields[name] |
|
521 |
except KeyError: |
|
522 |
raise errors.ParameterError("Unknown field '%s'" % name) |
|
523 |
|
|
524 |
assert fdef.kind != QFT_UNKNOWN |
|
525 |
|
|
526 |
# TODO: Type conversions? |
|
527 |
|
|
528 |
verify_fn = _VERIFY_FN[fdef.kind] |
|
529 |
if not verify_fn(value): |
|
530 |
raise errors.ParameterError("Unable to compare field '%s' (type '%s')" |
|
531 |
" with '%s', expected %s" % |
|
532 |
(name, fdef.kind, value.__class__.__name__, |
|
533 |
verify_fn)) |
|
534 |
|
|
535 |
if hints_fn: |
|
536 |
hints_fn(op, datakind, name, value) |
|
537 |
|
|
538 |
for (fn_flags, fn) in op_data: |
|
539 |
if fn_flags is None or fn_flags & field_flags: |
|
540 |
return compat.partial(_WrapBinaryOp, fn, retrieval_fn, value) |
|
541 |
|
|
542 |
raise errors.ProgrammerError("Unable to find operator implementation" |
|
543 |
" (op '%s', flags %s)" % (op, field_flags)) |
|
544 |
|
|
545 |
|
|
546 |
def _CompileFilter(fields, hints, filter_): |
|
547 |
"""Converts a query filter into a callable function. |
|
548 |
|
|
549 |
See L{_FilterCompilerHelper} for details. |
|
550 |
|
|
551 |
@rtype: callable |
|
552 |
|
|
553 |
""" |
|
554 |
return _FilterCompilerHelper(fields)(hints, filter_) |
|
555 |
|
|
556 |
|
|
179 | 557 |
class Query: |
180 |
def __init__(self, fieldlist, selected): |
|
558 |
def __init__(self, fieldlist, selected, filter_=None, namefield=None):
|
|
181 | 559 |
"""Initializes this class. |
182 | 560 |
|
183 | 561 |
The field definition is a dictionary with the field's name as a key and a |
... | ... | |
196 | 574 |
@param selected: List of selected fields |
197 | 575 |
|
198 | 576 |
""" |
577 |
assert namefield is None or namefield in fieldlist |
|
578 |
|
|
199 | 579 |
self._fields = _GetQueryFields(fieldlist, selected) |
200 | 580 |
|
581 |
self._filter_fn = None |
|
582 |
self._requested_names = None |
|
583 |
self._filter_datakinds = frozenset() |
|
584 |
|
|
585 |
if filter_ is not None: |
|
586 |
# Collect requested names if wanted |
|
587 |
if namefield: |
|
588 |
hints = _FilterHints(namefield) |
|
589 |
else: |
|
590 |
hints = None |
|
591 |
|
|
592 |
# Build filter function |
|
593 |
self._filter_fn = _CompileFilter(fieldlist, hints, filter_) |
|
594 |
if hints: |
|
595 |
self._requested_names = hints.RequestedNames() |
|
596 |
self._filter_datakinds = hints.ReferencedData() |
|
597 |
|
|
598 |
if namefield is None: |
|
599 |
self._name_fn = None |
|
600 |
else: |
|
601 |
(_, _, _, self._name_fn) = fieldlist[namefield] |
|
602 |
|
|
603 |
def RequestedNames(self): |
|
604 |
"""Returns all names referenced in the filter. |
|
605 |
|
|
606 |
If there is no filter or operators are preventing determining the exact |
|
607 |
names, C{None} is returned. |
|
608 |
|
|
609 |
""" |
|
610 |
return self._requested_names |
|
611 |
|
|
201 | 612 |
def RequestedData(self): |
202 | 613 |
"""Gets requested kinds of data. |
203 | 614 |
|
204 | 615 |
@rtype: frozenset |
205 | 616 |
|
206 | 617 |
""" |
207 |
return frozenset(datakind
|
|
208 |
for (_, datakind, _, _) in self._fields
|
|
209 |
if datakind is not None)
|
|
618 |
return (self._filter_datakinds |
|
|
619 |
frozenset(datakind for (_, datakind, _, _) in self._fields
|
|
620 |
if datakind is not None))
|
|
210 | 621 |
|
211 | 622 |
def GetFields(self): |
212 | 623 |
"""Returns the list of fields for this query. |
... | ... | |
225 | 636 |
support iteration using C{__iter__} |
226 | 637 |
|
227 | 638 |
""" |
228 |
result = [[_ProcessResult(fn(ctx, item)) for (_, _, _, fn) in self._fields] |
|
229 |
for item in ctx] |
|
639 |
result = [] |
|
640 |
|
|
641 |
for idx, item in enumerate(ctx): |
|
642 |
if not (self._filter_fn is None or self._filter_fn(ctx, item)): |
|
643 |
continue |
|
644 |
|
|
645 |
row = [_ProcessResult(fn(ctx, item)) for (_, _, _, fn) in self._fields] |
|
230 | 646 |
|
231 |
# Verify result |
|
232 |
if __debug__: |
|
233 |
for row in result: |
|
647 |
# Verify result |
|
648 |
if __debug__: |
|
234 | 649 |
_VerifyResultRow(self._fields, row) |
235 | 650 |
|
236 |
return result |
|
651 |
if self._name_fn: |
|
652 |
(status, name) = _ProcessResult(self._name_fn(ctx, item)) |
|
653 |
assert status == constants.RS_NORMAL |
|
654 |
# TODO: Are there cases where we wouldn't want to use NiceSort? |
|
655 |
sortname = utils.NiceSortKey(name) |
|
656 |
else: |
|
657 |
sortname = None |
|
658 |
|
|
659 |
result.append((sortname, idx, row)) |
|
660 |
|
|
661 |
# TODO: Would "heapq" be more efficient than sorting? |
|
662 |
|
|
663 |
# Sorting in-place instead of using "sorted()" |
|
664 |
result.sort() |
|
665 |
|
|
666 |
assert not result or (len(result[0]) == 3 and len(result[-1]) == 3) |
|
667 |
|
|
668 |
return map(operator.itemgetter(2), result) |
|
237 | 669 |
|
238 | 670 |
def OldStyleQuery(self, ctx): |
239 | 671 |
"""Query with "old" query result format. |
Also available in: Unified diff