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