query.py 60 KB
Newer Older
1 2 3
#
#

4
# Copyright (C) 2010, 2011 Google Inc.
5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
# General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
# 02110-1301, USA.


Michael Hanselmann's avatar
Michael Hanselmann committed
22 23 24 25 26 27 28 29 30 31 32 33 34
"""Module for query operations

How it works:

  - Add field definitions
    - See how L{NODE_FIELDS} is built
    - Each field gets:
      - Query field definition (L{objects.QueryFieldDefinition}, use
        L{_MakeField} for creating), containing:
          - Name, must be lowercase and match L{FIELD_NAME_RE}
          - Title for tables, must not contain whitespace and match
            L{TITLE_RE}
          - Value data type, e.g. L{constants.QFT_NUMBER}
35 36
          - Human-readable description, must not end with punctuation or
            contain newlines
Michael Hanselmann's avatar
Michael Hanselmann committed
37
      - Data request type, see e.g. C{NQ_*}
Michael Hanselmann's avatar
Michael Hanselmann committed
38
      - OR-ed flags, see C{QFF_*}
Michael Hanselmann's avatar
Michael Hanselmann committed
39 40 41 42 43 44 45 46 47 48 49 50
      - A retrieval function, see L{Query.__init__} for description
    - Pass list of fields through L{_PrepareFieldList} for preparation and
      checks
  - Instantiate L{Query} with prepared field list definition and selected fields
  - Call L{Query.RequestedData} to determine what data to collect/compute
  - Call L{Query.Query} or L{Query.OldStyleQuery} with collected data and use
    result
      - Data container must support iteration using C{__iter__}
      - Items are passed to retrieval functions and can have any format
  - Call L{Query.GetFields} to get list of definitions for selected fields

@attention: Retrieval functions must be idempotent. They can be called multiple
51
  times, in any order and any number of times.
Michael Hanselmann's avatar
Michael Hanselmann committed
52 53

"""
54

Michael Hanselmann's avatar
Michael Hanselmann committed
55
import logging
56 57 58 59 60 61 62 63 64
import operator
import re

from ganeti import constants
from ganeti import errors
from ganeti import utils
from ganeti import compat
from ganeti import objects
from ganeti import ht
65
from ganeti import qlang
66

67 68
from ganeti.constants import (QFT_UNKNOWN, QFT_TEXT, QFT_BOOL, QFT_NUMBER,
                              QFT_UNIT, QFT_TIMESTAMP, QFT_OTHER,
René Nussbaumer's avatar
René Nussbaumer committed
69 70
                              RS_NORMAL, RS_UNKNOWN, RS_NODATA,
                              RS_UNAVAIL, RS_OFFLINE)
71

72

Michael Hanselmann's avatar
Michael Hanselmann committed
73 74 75 76
# Constants for requesting data from the caller/data provider. Each property
# collected/computed separately by the data provider should have its own to
# only collect the requested data and not more.

Michael Hanselmann's avatar
Michael Hanselmann committed
77 78 79
(NQ_CONFIG,
 NQ_INST,
 NQ_LIVE,
80 81
 NQ_GROUP,
 NQ_OOB) = range(1, 6)
Michael Hanselmann's avatar
Michael Hanselmann committed
82

83 84
(IQ_CONFIG,
 IQ_LIVE,
85
 IQ_DISKUSAGE,
86 87
 IQ_CONSOLE,
 IQ_NODES) = range(100, 105)
88

89 90 91
(LQ_MODE,
 LQ_OWNER,
 LQ_PENDING) = range(10, 13)
Michael Hanselmann's avatar
Michael Hanselmann committed
92

93 94 95 96
(GQ_CONFIG,
 GQ_NODE,
 GQ_INST) = range(200, 203)

Michael Hanselmann's avatar
Michael Hanselmann committed
97 98 99 100 101
# Query field flags
QFF_HOSTNAME = 0x01
QFF_IP_ADDRESS = 0x02
# Next values: 0x04, 0x08, 0x10, 0x20, 0x40, 0x80, 0x100, 0x200
QFF_ALL = (QFF_HOSTNAME | QFF_IP_ADDRESS)
102

103 104
FIELD_NAME_RE = re.compile(r"^[a-z0-9/._]+$")
TITLE_RE = re.compile(r"^[^\s]+$")
105
DOC_RE = re.compile(r"^[A-Z].*[^.,?!]$")
106 107 108

#: Verification function for each field type
_VERIFY_FN = {
109 110 111 112 113
  QFT_UNKNOWN: ht.TNone,
  QFT_TEXT: ht.TString,
  QFT_BOOL: ht.TBool,
  QFT_NUMBER: ht.TInt,
  QFT_UNIT: ht.TInt,
114
  QFT_TIMESTAMP: ht.TNumber,
115
  QFT_OTHER: lambda _: True,
116 117
  }

118 119 120 121 122 123
# Unique objects for special field statuses
_FS_UNKNOWN = object()
_FS_NODATA = object()
_FS_UNAVAIL = object()
_FS_OFFLINE = object()

124 125 126
#: List of all special status
_FS_ALL = frozenset([_FS_UNKNOWN, _FS_NODATA, _FS_UNAVAIL, _FS_OFFLINE])

127 128 129 130 131 132 133 134 135 136
#: VType to QFT mapping
_VTToQFT = {
  # TODO: fix validation of empty strings
  constants.VTYPE_STRING: QFT_OTHER, # since VTYPE_STRINGs can be empty
  constants.VTYPE_MAYBE_STRING: QFT_OTHER,
  constants.VTYPE_BOOL: QFT_BOOL,
  constants.VTYPE_SIZE: QFT_UNIT,
  constants.VTYPE_INT: QFT_NUMBER,
  }

137 138
_SERIAL_NO_DOC = "%s object serial number, incremented on each modification"

139

140
def _GetUnknownField(ctx, item): # pylint: disable=W0613
141 142 143
  """Gets the contents of an unknown field.

  """
144
  return _FS_UNKNOWN
145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163


def _GetQueryFields(fielddefs, selected):
  """Calculates the internal list of selected fields.

  Unknown fields are returned as L{constants.QFT_UNKNOWN}.

  @type fielddefs: dict
  @param fielddefs: Field definitions
  @type selected: list of strings
  @param selected: List of selected fields

  """
  result = []

  for name in selected:
    try:
      fdef = fielddefs[name]
    except KeyError:
164
      fdef = (_MakeField(name, name, QFT_UNKNOWN, "Unknown field '%s'" % name),
Michael Hanselmann's avatar
Michael Hanselmann committed
165
              None, 0, _GetUnknownField)
166

Michael Hanselmann's avatar
Michael Hanselmann committed
167
    assert len(fdef) == 4
168 169 170 171 172 173 174 175 176 177 178 179

    result.append(fdef)

  return result


def GetAllFields(fielddefs):
  """Extract L{objects.QueryFieldDefinition} from field definitions.

  @rtype: list of L{objects.QueryFieldDefinition}

  """
Michael Hanselmann's avatar
Michael Hanselmann committed
180
  return [fdef for (fdef, _, _, _) in fielddefs]
181 182


183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262
class _FilterHints:
  """Class for filter analytics.

  When filters are used, the user of the L{Query} class usually doesn't know
  exactly which items will be necessary for building the result. It therefore
  has to prepare and compute the input data for potentially returning
  everything.

  There are two ways to optimize this. The first, and simpler, is to assign
  each field a group of data, so that the caller can determine which
  computations are necessary depending on the data groups requested. The list
  of referenced groups must also be computed for fields referenced in the
  filter.

  The second is restricting the items based on a primary key. The primary key
  is usually a unique name (e.g. a node name). This class extracts all
  referenced names from a filter. If it encounters any filter condition which
  disallows such a list to be determined (e.g. a non-equality filter), all
  names will be requested.

  The end-effect is that any operation other than L{qlang.OP_OR} and
  L{qlang.OP_EQUAL} will make the query more expensive.

  """
  def __init__(self, namefield):
    """Initializes this class.

    @type namefield: string
    @param namefield: Field caller is interested in

    """
    self._namefield = namefield

    #: Whether all names need to be requested (e.g. if a non-equality operator
    #: has been used)
    self._allnames = False

    #: Which names to request
    self._names = None

    #: Data kinds referenced by the filter (used by L{Query.RequestedData})
    self._datakinds = set()

  def RequestedNames(self):
    """Returns all requested values.

    Returns C{None} if list of values can't be determined (e.g. encountered
    non-equality operators).

    @rtype: list

    """
    if self._allnames or self._names is None:
      return None

    return utils.UniqueSequence(self._names)

  def ReferencedData(self):
    """Returns all kinds of data referenced by the filter.

    """
    return frozenset(self._datakinds)

  def _NeedAllNames(self):
    """Changes internal state to request all names.

    """
    self._allnames = True
    self._names = None

  def NoteLogicOp(self, op):
    """Called when handling a logic operation.

    @type op: string
    @param op: Operator

    """
    if op != qlang.OP_OR:
      self._NeedAllNames()

263
  def NoteUnaryOp(self, op): # pylint: disable=W0613
264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325
    """Called when handling an unary operation.

    @type op: string
    @param op: Operator

    """
    self._NeedAllNames()

  def NoteBinaryOp(self, op, datakind, name, value):
    """Called when handling a binary operation.

    @type op: string
    @param op: Operator
    @type name: string
    @param name: Left-hand side of operator (field name)
    @param value: Right-hand side of operator

    """
    if datakind is not None:
      self._datakinds.add(datakind)

    if self._allnames:
      return

    # If any operator other than equality was used, all names need to be
    # retrieved
    if op == qlang.OP_EQUAL and name == self._namefield:
      if self._names is None:
        self._names = []
      self._names.append(value)
    else:
      self._NeedAllNames()


def _WrapLogicOp(op_fn, sentences, ctx, item):
  """Wrapper for logic operator functions.

  """
  return op_fn(fn(ctx, item) for fn in sentences)


def _WrapUnaryOp(op_fn, inner, ctx, item):
  """Wrapper for unary operator functions.

  """
  return op_fn(inner(ctx, item))


def _WrapBinaryOp(op_fn, retrieval_fn, value, ctx, item):
  """Wrapper for binary operator functions.

  """
  return op_fn(retrieval_fn(ctx, item), value)


def _WrapNot(fn, lhs, rhs):
  """Negates the result of a wrapped function.

  """
  return not fn(lhs, rhs)


326 327 328 329 330 331 332 333 334 335
def _PrepareRegex(pattern):
  """Compiles a regular expression.

  """
  try:
    return re.compile(pattern)
  except re.error, err:
    raise errors.ParameterError("Invalid regex pattern (%s)" % err)


336 337 338 339
class _FilterCompilerHelper:
  """Converts a query filter to a callable usable for filtering.

  """
340
  # String statement has no effect, pylint: disable=W0105
341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362

  #: How deep filters can be nested
  _LEVELS_MAX = 10

  # Unique identifiers for operator groups
  (_OPTYPE_LOGIC,
   _OPTYPE_UNARY,
   _OPTYPE_BINARY) = range(1, 4)

  """Functions for equality checks depending on field flags.

  List of tuples containing flags and a callable receiving the left- and
  right-hand side of the operator. The flags are an OR-ed value of C{QFF_*}
  (e.g. L{QFF_HOSTNAME}).

  Order matters. The first item with flags will be used. Flags are checked
  using binary AND.

  """
  _EQUALITY_CHECKS = [
    (QFF_HOSTNAME,
     lambda lhs, rhs: utils.MatchNameComponent(rhs, [lhs],
363 364 365
                                               case_sensitive=False),
     None),
    (None, operator.eq, None),
366 367 368 369 370 371 372 373 374
    ]

  """Known operators

  Operator as key (C{qlang.OP_*}), value a tuple of operator group
  (C{_OPTYPE_*}) and a group-specific value:

    - C{_OPTYPE_LOGIC}: Callable taking any number of arguments; used by
      L{_HandleLogicOp}
375
    - C{_OPTYPE_UNARY}: Always C{None}; details handled by L{_HandleUnaryOp}
376 377 378 379 380 381 382 383 384 385
    - C{_OPTYPE_BINARY}: Callable taking exactly two parameters, the left- and
      right-hand side of the operator, used by L{_HandleBinaryOp}

  """
  _OPS = {
    # Logic operators
    qlang.OP_OR: (_OPTYPE_LOGIC, compat.any),
    qlang.OP_AND: (_OPTYPE_LOGIC, compat.all),

    # Unary operators
386 387
    qlang.OP_NOT: (_OPTYPE_UNARY, None),
    qlang.OP_TRUE: (_OPTYPE_UNARY, None),
388 389 390 391

    # Binary operators
    qlang.OP_EQUAL: (_OPTYPE_BINARY, _EQUALITY_CHECKS),
    qlang.OP_NOT_EQUAL:
392 393 394 395 396
      (_OPTYPE_BINARY, [(flags, compat.partial(_WrapNot, fn), valprepfn)
                        for (flags, fn, valprepfn) in _EQUALITY_CHECKS]),
    qlang.OP_REGEXP: (_OPTYPE_BINARY, [
      (None, lambda lhs, rhs: rhs.search(lhs), _PrepareRegex),
      ]),
397
    qlang.OP_CONTAINS: (_OPTYPE_BINARY, [
398
      (None, operator.contains, None),
399 400 401 402 403 404 405 406 407 408 409 410 411
      ]),
    }

  def __init__(self, fields):
    """Initializes this class.

    @param fields: Field definitions (return value of L{_PrepareFieldList})

    """
    self._fields = fields
    self._hints = None
    self._op_handler = None

412
  def __call__(self, hints, qfilter):
413 414 415 416
    """Converts a query filter into a callable function.

    @type hints: L{_FilterHints} or None
    @param hints: Callbacks doing analysis on filter
417 418
    @type qfilter: list
    @param qfilter: Filter structure
419 420 421 422 423 424 425 426 427 428 429 430 431 432 433
    @rtype: callable
    @return: Function receiving context and item as parameters, returning
             boolean as to whether item matches filter

    """
    self._op_handler = {
      self._OPTYPE_LOGIC:
        (self._HandleLogicOp, getattr(hints, "NoteLogicOp", None)),
      self._OPTYPE_UNARY:
        (self._HandleUnaryOp, getattr(hints, "NoteUnaryOp", None)),
      self._OPTYPE_BINARY:
        (self._HandleBinaryOp, getattr(hints, "NoteBinaryOp", None)),
      }

    try:
434
      filter_fn = self._Compile(qfilter, 0)
435 436 437 438 439
    finally:
      self._op_handler = None

    return filter_fn

440
  def _Compile(self, qfilter, level):
441 442 443 444 445 446
    """Inner function for converting filters.

    Calls the correct handler functions for the top-level operator. This
    function is called recursively (e.g. for logic operators).

    """
447
    if not (isinstance(qfilter, (list, tuple)) and qfilter):
448 449 450 451 452 453 454 455
      raise errors.ParameterError("Invalid filter on level %s" % level)

    # Limit recursion
    if level >= self._LEVELS_MAX:
      raise errors.ParameterError("Only up to %s levels are allowed (filter"
                                  " nested too deep)" % self._LEVELS_MAX)

    # Create copy to be modified
456
    operands = qfilter[:]
457 458 459 460 461 462 463 464 465 466 467
    op = operands.pop(0)

    try:
      (kind, op_data) = self._OPS[op]
    except KeyError:
      raise errors.ParameterError("Unknown operator '%s'" % op)

    (handler, hints_cb) = self._op_handler[kind]

    return handler(hints_cb, level, op, op_data, operands)

468 469 470 471 472 473 474 475 476
  def _LookupField(self, name):
    """Returns a field definition by name.

    """
    try:
      return self._fields[name]
    except KeyError:
      raise errors.ParameterError("Unknown field '%s'" % name)

477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512
  def _HandleLogicOp(self, hints_fn, level, op, op_fn, operands):
    """Handles logic operators.

    @type hints_fn: callable
    @param hints_fn: Callback doing some analysis on the filter
    @type level: integer
    @param level: Current depth
    @type op: string
    @param op: Operator
    @type op_fn: callable
    @param op_fn: Function implementing operator
    @type operands: list
    @param operands: List of operands

    """
    if hints_fn:
      hints_fn(op)

    return compat.partial(_WrapLogicOp, op_fn,
                          [self._Compile(op, level + 1) for op in operands])

  def _HandleUnaryOp(self, hints_fn, level, op, op_fn, operands):
    """Handles unary operators.

    @type hints_fn: callable
    @param hints_fn: Callback doing some analysis on the filter
    @type level: integer
    @param level: Current depth
    @type op: string
    @param op: Operator
    @type op_fn: callable
    @param op_fn: Function implementing operator
    @type operands: list
    @param operands: List of operands

    """
513 514
    assert op_fn is None

515 516 517 518 519 520 521
    if hints_fn:
      hints_fn(op)

    if len(operands) != 1:
      raise errors.ParameterError("Unary operator '%s' expects exactly one"
                                  " operand" % op)

522 523 524 525 526 527 528 529 530 531 532 533
    if op == qlang.OP_TRUE:
      (_, _, _, retrieval_fn) = self._LookupField(operands[0])

      op_fn = operator.truth
      arg = retrieval_fn
    elif op == qlang.OP_NOT:
      op_fn = operator.not_
      arg = self._Compile(operands[0], level + 1)
    else:
      raise errors.ProgrammerError("Can't handle operator '%s'" % op)

    return compat.partial(_WrapUnaryOp, op_fn, arg)
534 535 536 537 538 539 540 541 542 543 544 545 546 547 548

  def _HandleBinaryOp(self, hints_fn, level, op, op_data, operands):
    """Handles binary operators.

    @type hints_fn: callable
    @param hints_fn: Callback doing some analysis on the filter
    @type level: integer
    @param level: Current depth
    @type op: string
    @param op: Operator
    @param op_data: Functions implementing operators
    @type operands: list
    @param operands: List of operands

    """
549
    # Unused arguments, pylint: disable=W0613
550 551 552 553 554 555
    try:
      (name, value) = operands
    except (ValueError, TypeError):
      raise errors.ParameterError("Invalid binary operator, expected exactly"
                                  " two operands")

556
    (fdef, datakind, field_flags, retrieval_fn) = self._LookupField(name)
557 558 559 560 561 562 563 564 565 566 567 568 569 570 571

    assert fdef.kind != QFT_UNKNOWN

    # TODO: Type conversions?

    verify_fn = _VERIFY_FN[fdef.kind]
    if not verify_fn(value):
      raise errors.ParameterError("Unable to compare field '%s' (type '%s')"
                                  " with '%s', expected %s" %
                                  (name, fdef.kind, value.__class__.__name__,
                                   verify_fn))

    if hints_fn:
      hints_fn(op, datakind, name, value)

572
    for (fn_flags, fn, valprepfn) in op_data:
573
      if fn_flags is None or fn_flags & field_flags:
574 575 576 577
        # Prepare value if necessary (e.g. compile regular expression)
        if valprepfn:
          value = valprepfn(value)

578 579 580 581 582 583
        return compat.partial(_WrapBinaryOp, fn, retrieval_fn, value)

    raise errors.ProgrammerError("Unable to find operator implementation"
                                 " (op '%s', flags %s)" % (op, field_flags))


584
def _CompileFilter(fields, hints, qfilter):
585 586 587 588 589 590 591
  """Converts a query filter into a callable function.

  See L{_FilterCompilerHelper} for details.

  @rtype: callable

  """
592
  return _FilterCompilerHelper(fields)(hints, qfilter)
593 594


595
class Query:
596
  def __init__(self, fieldlist, selected, qfilter=None, namefield=None):
597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614
    """Initializes this class.

    The field definition is a dictionary with the field's name as a key and a
    tuple containing, in order, the field definition object
    (L{objects.QueryFieldDefinition}, the data kind to help calling code
    collect data and a retrieval function. The retrieval function is called
    with two parameters, in order, the data container and the item in container
    (see L{Query.Query}).

    Users of this class can call L{RequestedData} before preparing the data
    container to determine what data is needed.

    @type fieldlist: dictionary
    @param fieldlist: Field definitions
    @type selected: list of strings
    @param selected: List of selected fields

    """
615 616
    assert namefield is None or namefield in fieldlist

617 618
    self._fields = _GetQueryFields(fieldlist, selected)

619 620 621 622
    self._filter_fn = None
    self._requested_names = None
    self._filter_datakinds = frozenset()

623
    if qfilter is not None:
624 625 626 627 628 629 630
      # Collect requested names if wanted
      if namefield:
        hints = _FilterHints(namefield)
      else:
        hints = None

      # Build filter function
631
      self._filter_fn = _CompileFilter(fieldlist, hints, qfilter)
632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649
      if hints:
        self._requested_names = hints.RequestedNames()
        self._filter_datakinds = hints.ReferencedData()

    if namefield is None:
      self._name_fn = None
    else:
      (_, _, _, self._name_fn) = fieldlist[namefield]

  def RequestedNames(self):
    """Returns all names referenced in the filter.

    If there is no filter or operators are preventing determining the exact
    names, C{None} is returned.

    """
    return self._requested_names

650 651 652 653 654 655
  def RequestedData(self):
    """Gets requested kinds of data.

    @rtype: frozenset

    """
656 657 658
    return (self._filter_datakinds |
            frozenset(datakind for (_, datakind, _, _) in self._fields
                      if datakind is not None))
659 660 661 662 663 664 665 666 667 668 669

  def GetFields(self):
    """Returns the list of fields for this query.

    Includes unknown fields.

    @rtype: List of L{objects.QueryFieldDefinition}

    """
    return GetAllFields(self._fields)

670
  def Query(self, ctx, sort_by_name=True):
671 672 673 674
    """Execute a query.

    @param ctx: Data container passed to field retrieval functions, must
      support iteration using C{__iter__}
675 676 677
    @type sort_by_name: boolean
    @param sort_by_name: Whether to sort by name or keep the input data's
      ordering
678 679

    """
680 681
    sort = (self._name_fn and sort_by_name)

682 683 684 685 686
    result = []

    for idx, item in enumerate(ctx):
      if not (self._filter_fn is None or self._filter_fn(ctx, item)):
        continue
687

688 689 690 691
      row = [_ProcessResult(fn(ctx, item)) for (_, _, _, fn) in self._fields]

      # Verify result
      if __debug__:
692
        _VerifyResultRow(self._fields, row)
693

694
      if sort:
695 696 697
        (status, name) = _ProcessResult(self._name_fn(ctx, item))
        assert status == constants.RS_NORMAL
        # TODO: Are there cases where we wouldn't want to use NiceSort?
698
        result.append((utils.NiceSortKey(name), idx, row))
699
      else:
700
        result.append(row)
701

702 703
    if not sort:
      return result
704 705 706 707 708 709 710 711 712

    # TODO: Would "heapq" be more efficient than sorting?

    # Sorting in-place instead of using "sorted()"
    result.sort()

    assert not result or (len(result[0]) == 3 and len(result[-1]) == 3)

    return map(operator.itemgetter(2), result)
713

714
  def OldStyleQuery(self, ctx, sort_by_name=True):
715 716 717 718 719
    """Query with "old" query result format.

    See L{Query.Query} for arguments.

    """
Michael Hanselmann's avatar
Michael Hanselmann committed
720 721
    unknown = set(fdef.name for (fdef, _, _, _) in self._fields
                  if fdef.kind == QFT_UNKNOWN)
722 723 724 725 726 727
    if unknown:
      raise errors.OpPrereqError("Unknown output fields selected: %s" %
                                 (utils.CommaJoin(unknown), ),
                                 errors.ECODE_INVAL)

    return [[value for (_, value) in row]
728
            for row in self.Query(ctx, sort_by_name=sort_by_name)]
729 730


731 732 733 734 735
def _ProcessResult(value):
  """Converts result values into externally-visible ones.

  """
  if value is _FS_UNKNOWN:
René Nussbaumer's avatar
René Nussbaumer committed
736
    return (RS_UNKNOWN, None)
737
  elif value is _FS_NODATA:
René Nussbaumer's avatar
René Nussbaumer committed
738
    return (RS_NODATA, None)
739
  elif value is _FS_UNAVAIL:
René Nussbaumer's avatar
René Nussbaumer committed
740
    return (RS_UNAVAIL, None)
741
  elif value is _FS_OFFLINE:
René Nussbaumer's avatar
René Nussbaumer committed
742
    return (RS_OFFLINE, None)
743
  else:
René Nussbaumer's avatar
René Nussbaumer committed
744
    return (RS_NORMAL, value)
745 746


747 748 749 750 751 752 753 754 755
def _VerifyResultRow(fields, row):
  """Verifies the contents of a query result row.

  @type fields: list
  @param fields: Field definitions for result
  @type row: list of tuples
  @param row: Row data

  """
756 757
  assert len(row) == len(fields)
  errs = []
Michael Hanselmann's avatar
Michael Hanselmann committed
758
  for ((status, value), (fdef, _, _, _)) in zip(row, fields):
René Nussbaumer's avatar
René Nussbaumer committed
759
    if status == RS_NORMAL:
760 761 762 763 764 765
      if not _VERIFY_FN[fdef.kind](value):
        errs.append("normal field %s fails validation (value is %s)" %
                    (fdef.name, value))
    elif value is not None:
      errs.append("abnormal field %s has a non-None value" % fdef.name)
  assert not errs, ("Failed validation: %s in row %s" %
766
                    (utils.CommaJoin(errs), row))
767 768


769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784
def _FieldDictKey((fdef, _, flags, fn)):
  """Generates key for field dictionary.

  """
  assert fdef.name and fdef.title, "Name and title are required"
  assert FIELD_NAME_RE.match(fdef.name)
  assert TITLE_RE.match(fdef.title)
  assert (DOC_RE.match(fdef.doc) and len(fdef.doc.splitlines()) == 1 and
          fdef.doc.strip() == fdef.doc), \
         "Invalid description for field '%s'" % fdef.name
  assert callable(fn)
  assert (flags & ~QFF_ALL) == 0, "Unknown flags for field '%s'" % fdef.name

  return fdef.name


785
def _PrepareFieldList(fields, aliases):
786 787 788 789
  """Prepares field list for use by L{Query}.

  Converts the list to a dictionary and does some verification.

790 791 792 793 794 795 796
  @type fields: list of tuples; (L{objects.QueryFieldDefinition}, data
      kind, retrieval function)
  @param fields: List of fields, see L{Query.__init__} for a better
      description
  @type aliases: list of tuples; (alias, target)
  @param aliases: list of tuples containing aliases; for each
      alias/target pair, a duplicate will be created in the field list
797 798 799 800
  @rtype: dict
  @return: Field dictionary for L{Query}

  """
801 802
  if __debug__:
    duplicates = utils.FindDuplicates(fdef.title.lower()
Michael Hanselmann's avatar
Michael Hanselmann committed
803
                                      for (fdef, _, _, _) in fields)
804
    assert not duplicates, "Duplicate title(s) found: %r" % duplicates
805

806
  result = utils.SequenceToDict(fields, key=_FieldDictKey)
807

808 809 810
  for alias, target in aliases:
    assert alias not in result, "Alias %s overrides an existing field" % alias
    assert target in result, "Missing target %s for alias %s" % (target, alias)
Michael Hanselmann's avatar
Michael Hanselmann committed
811
    (fdef, k, flags, fn) = result[target]
812 813
    fdef = fdef.Copy()
    fdef.name = alias
Michael Hanselmann's avatar
Michael Hanselmann committed
814
    result[alias] = (fdef, k, flags, fn)
815 816

  assert len(result) == len(fields) + len(aliases)
817
  assert compat.all(name == fdef.name
Michael Hanselmann's avatar
Michael Hanselmann committed
818
                    for (name, (fdef, _, _, _)) in result.items())
819 820 821 822

  return result


823
def GetQueryResponse(query, ctx, sort_by_name=True):
824 825 826 827
  """Prepares the response for a query.

  @type query: L{Query}
  @param ctx: Data container, see L{Query.Query}
828 829 830
  @type sort_by_name: boolean
  @param sort_by_name: Whether to sort by name or keep the input data's
    ordering
831 832

  """
833
  return objects.QueryResponse(data=query.Query(ctx, sort_by_name=sort_by_name),
834 835 836
                               fields=query.GetFields()).ToDict()


837 838 839 840 841 842 843 844 845 846 847 848 849 850 851 852 853 854 855 856 857
def QueryFields(fielddefs, selected):
  """Returns list of available fields.

  @type fielddefs: dict
  @param fielddefs: Field definitions
  @type selected: list of strings
  @param selected: List of selected fields
  @return: List of L{objects.QueryFieldDefinition}

  """
  if selected is None:
    # Client requests all fields, sort by name
    fdefs = utils.NiceSort(GetAllFields(fielddefs.values()),
                           key=operator.attrgetter("name"))
  else:
    # Keep order as requested by client
    fdefs = Query(fielddefs, selected).GetFields()

  return objects.QueryFieldsResponse(fields=fdefs).ToDict()


858
def _MakeField(name, title, kind, doc):
859 860 861 862 863
  """Wrapper for creating L{objects.QueryFieldDefinition} instances.

  @param name: Field name as a regular expression
  @param title: Human-readable title
  @param kind: Field type
864
  @param doc: Human-readable description
865 866

  """
867 868
  return objects.QueryFieldDefinition(name=name, title=title, kind=kind,
                                      doc=doc)
Michael Hanselmann's avatar
Michael Hanselmann committed
869 870 871 872 873 874 875 876 877 878 879 880


def _GetNodeRole(node, master_name):
  """Determine node role.

  @type node: L{objects.Node}
  @param node: Node object
  @type master_name: string
  @param master_name: Master node name

  """
  if node.name == master_name:
881
    return constants.NR_MASTER
Michael Hanselmann's avatar
Michael Hanselmann committed
882
  elif node.master_candidate:
883
    return constants.NR_MCANDIDATE
Michael Hanselmann's avatar
Michael Hanselmann committed
884
  elif node.drained:
885
    return constants.NR_DRAINED
Michael Hanselmann's avatar
Michael Hanselmann committed
886
  elif node.offline:
887
    return constants.NR_OFFLINE
Michael Hanselmann's avatar
Michael Hanselmann committed
888
  else:
889
    return constants.NR_REGULAR
Michael Hanselmann's avatar
Michael Hanselmann committed
890 891 892 893 894 895 896 897 898


def _GetItemAttr(attr):
  """Returns a field function to return an attribute of the item.

  @param attr: Attribute name

  """
  getter = operator.attrgetter(attr)
899
  return lambda _, item: getter(item)
Michael Hanselmann's avatar
Michael Hanselmann committed
900 901


902 903 904 905 906 907 908 909 910 911 912 913 914 915 916 917 918 919 920 921 922 923 924 925 926 927 928 929
def _ConvWrapInner(convert, fn, ctx, item):
  """Wrapper for converting values.

  @param convert: Conversion function receiving value as single parameter
  @param fn: Retrieval function

  """
  value = fn(ctx, item)

  # Is the value an abnormal status?
  if compat.any(value is fs for fs in _FS_ALL):
    # Return right away
    return value

  # TODO: Should conversion function also receive context, item or both?
  return convert(value)


def _ConvWrap(convert, fn):
  """Convenience wrapper for L{_ConvWrapInner}.

  @param convert: Conversion function receiving value as single parameter
  @param fn: Retrieval function

  """
  return compat.partial(_ConvWrapInner, convert, fn)


930 931 932 933 934 935 936 937 938 939 940 941 942 943
def _GetItemTimestamp(getter):
  """Returns function for getting timestamp of item.

  @type getter: callable
  @param getter: Function to retrieve timestamp attribute

  """
  def fn(_, item):
    """Returns a timestamp of item.

    """
    timestamp = getter(item)
    if timestamp is None:
      # Old configs might not have all timestamps
944
      return _FS_UNAVAIL
945
    else:
946
      return timestamp
947 948 949 950 951 952 953 954 955 956 957

  return fn


def _GetItemTimestampFields(datatype):
  """Returns common timestamp fields.

  @param datatype: Field data type for use by L{Query.RequestedData}

  """
  return [
958
    (_MakeField("ctime", "CTime", QFT_TIMESTAMP, "Creation timestamp"),
Michael Hanselmann's avatar
Michael Hanselmann committed
959
     datatype, 0, _GetItemTimestamp(operator.attrgetter("ctime"))),
960
    (_MakeField("mtime", "MTime", QFT_TIMESTAMP, "Modification timestamp"),
Michael Hanselmann's avatar
Michael Hanselmann committed
961
     datatype, 0, _GetItemTimestamp(operator.attrgetter("mtime"))),
962 963 964
    ]


Michael Hanselmann's avatar
Michael Hanselmann committed
965 966 967 968 969
class NodeQueryData:
  """Data container for node data queries.

  """
  def __init__(self, nodes, live_data, master_name, node_to_primary,
970
               node_to_secondary, groups, oob_support, cluster):
Michael Hanselmann's avatar
Michael Hanselmann committed
971 972 973 974 975 976 977 978 979
    """Initializes this class.

    """
    self.nodes = nodes
    self.live_data = live_data
    self.master_name = master_name
    self.node_to_primary = node_to_primary
    self.node_to_secondary = node_to_secondary
    self.groups = groups
980
    self.oob_support = oob_support
981
    self.cluster = cluster
Michael Hanselmann's avatar
Michael Hanselmann committed
982 983 984 985 986 987 988 989 990 991 992 993 994 995 996 997 998 999 1000 1001 1002

    # Used for individual rows
    self.curlive_data = None

  def __iter__(self):
    """Iterate over all nodes.

    This function has side-effects and only one instance of the resulting
    generator should be used at a time.

    """
    for node in self.nodes:
      if self.live_data:
        self.curlive_data = self.live_data.get(node.name, None)
      else:
        self.curlive_data = None
      yield node


#: Fields that are direct attributes of an L{objects.Node} object
_NODE_SIMPLE_FIELDS = {
Michael Hanselmann's avatar
Michael Hanselmann committed
1003 1004
  "drained": ("Drained", QFT_BOOL, 0, "Whether node is drained"),
  "master_candidate": ("MasterC", QFT_BOOL, 0,
1005
                       "Whether node is a master candidate"),
Michael Hanselmann's avatar
Michael Hanselmann committed
1006
  "master_capable": ("MasterCapable", QFT_BOOL, 0,
1007
                     "Whether node can become a master candidate"),
Michael Hanselmann's avatar
Michael Hanselmann committed
1008 1009 1010 1011 1012
  "name": ("Node", QFT_TEXT, QFF_HOSTNAME, "Node name"),
  "offline": ("Offline", QFT_BOOL, 0, "Whether node is marked offline"),
  "serial_no": ("SerialNo", QFT_NUMBER, 0, _SERIAL_NO_DOC % "Node"),
  "uuid": ("UUID", QFT_TEXT, 0, "Node UUID"),
  "vm_capable": ("VMCapable", QFT_BOOL, 0, "Whether node can host instances"),
Michael Hanselmann's avatar
Michael Hanselmann committed
1013 1014 1015 1016
  }


#: Fields requiring talking to the node
1017
# Note that none of these are available for non-vm_capable nodes
Michael Hanselmann's avatar
Michael Hanselmann committed
1018
_NODE_LIVE_FIELDS = {
1019 1020 1021 1022 1023 1024 1025 1026 1027 1028 1029 1030 1031 1032 1033 1034 1035 1036 1037
  "bootid": ("BootID", QFT_TEXT, "bootid",
             "Random UUID renewed for each system reboot, can be used"
             " for detecting reboots by tracking changes"),
  "cnodes": ("CNodes", QFT_NUMBER, "cpu_nodes",
             "Number of NUMA domains on node (if exported by hypervisor)"),
  "csockets": ("CSockets", QFT_NUMBER, "cpu_sockets",
               "Number of physical CPU sockets (if exported by hypervisor)"),
  "ctotal": ("CTotal", QFT_NUMBER, "cpu_total", "Number of logical processors"),
  "dfree": ("DFree", QFT_UNIT, "vg_free",
            "Available disk space in volume group"),
  "dtotal": ("DTotal", QFT_UNIT, "vg_size",
             "Total disk space in volume group used for instance disk"
             " allocation"),
  "mfree": ("MFree", QFT_UNIT, "memory_free",
            "Memory available for instance allocations"),
  "mnode": ("MNode", QFT_UNIT, "memory_dom0",
            "Amount of memory used by node (dom0 for Xen)"),
  "mtotal": ("MTotal", QFT_UNIT, "memory_total",
             "Total amount of memory of physical machine"),
Michael Hanselmann's avatar
Michael Hanselmann committed
1038 1039 1040
  }


1041 1042 1043 1044 1045 1046 1047 1048 1049 1050 1051 1052 1053 1054 1055 1056 1057
def _GetGroup(cb):
  """Build function for calling another function with an node group.

  @param cb: The callback to be called with the nodegroup

  """
  def fn(ctx, node):
    """Get group data for a node.

    @type ctx: L{NodeQueryData}
    @type inst: L{objects.Node}
    @param inst: Node object

    """
    ng = ctx.groups.get(node.group, None)
    if ng is None:
      # Nodes always have a group, or the configuration is corrupt
1058
      return _FS_UNAVAIL
1059 1060 1061 1062 1063 1064

    return cb(ctx, node, ng)

  return fn


1065
def _GetNodeGroup(ctx, node, ng): # pylint: disable=W0613
Michael Hanselmann's avatar
Michael Hanselmann committed
1066 1067 1068 1069 1070
  """Returns the name of a node's group.

  @type ctx: L{NodeQueryData}
  @type node: L{objects.Node}
  @param node: Node object
1071 1072
  @type ng: L{objects.NodeGroup}
  @param ng: The node group this node belongs to
Michael Hanselmann's avatar
Michael Hanselmann committed
1073 1074

  """
1075
  return ng.name
Michael Hanselmann's avatar
Michael Hanselmann committed
1076 1077


1078 1079 1080 1081 1082 1083 1084 1085 1086
def _GetNodePower(ctx, node):
  """Returns the node powered state

  @type ctx: L{NodeQueryData}
  @type node: L{objects.Node}
  @param node: Node object

  """
  if ctx.oob_support[node.name]:
1087
    return node.powered
1088

1089
  return _FS_UNAVAIL
1090 1091


1092 1093 1094 1095 1096 1097 1098 1099 1100 1101
def _GetNdParams(ctx, node, ng):
  """Returns the ndparams for this node.

  @type ctx: L{NodeQueryData}
  @type node: L{objects.Node}
  @param node: Node object
  @type ng: L{objects.NodeGroup}
  @param ng: The node group this node belongs to

  """
1102
  return ctx.cluster.SimpleFillND(ng.FillND(node))
1103 1104


1105
def _GetLiveNodeField(field, kind, ctx, node):
Michael Hanselmann's avatar
Michael Hanselmann committed
1106 1107 1108 1109 1110
  """Gets the value of a "live" field from L{NodeQueryData}.

  @param field: Live field name
  @param kind: Data kind, one of L{constants.QFT_ALL}
  @type ctx: L{NodeQueryData}
1111 1112
  @type node: L{objects.Node}
  @param node: Node object
Michael Hanselmann's avatar
Michael Hanselmann committed
1113 1114

  """
1115
  if node.offline:
1116
    return _FS_OFFLINE
1117

1118 1119 1120
  if not node.vm_capable:
    return _FS_UNAVAIL

Michael Hanselmann's avatar
Michael Hanselmann committed
1121
  if not ctx.curlive_data:
1122
    return _FS_NODATA
Michael Hanselmann's avatar
Michael Hanselmann committed
1123 1124 1125 1126

  try:
    value = ctx.curlive_data[field]
  except KeyError:
1127
    return _FS_UNAVAIL
Michael Hanselmann's avatar
Michael Hanselmann committed
1128

1129
  if kind == QFT_TEXT:
1130
    return value
Michael Hanselmann's avatar
Michael Hanselmann committed
1131

1132
  assert kind in (QFT_NUMBER, QFT_UNIT)
Michael Hanselmann's avatar
Michael Hanselmann committed
1133 1134 1135

  # Try to convert into number
  try:
1136
    return int(value)
Michael Hanselmann's avatar
Michael Hanselmann committed
1137 1138 1139
  except (ValueError, TypeError):
    logging.exception("Failed to convert node field '%s' (value %r) to int",
                      value, field)
1140
    return _FS_UNAVAIL
Michael Hanselmann's avatar
Michael Hanselmann committed
1141 1142 1143 1144 1145 1146 1147


def _BuildNodeFields():
  """Builds list of fields for node queries.

  """
  fields = [
1148
    (_MakeField("pip", "PrimaryIP", QFT_TEXT, "Primary IP address"),
Michael Hanselmann's avatar
Michael Hanselmann committed
1149
     NQ_CONFIG, 0, _GetItemAttr("primary_ip")),
1150
    (_MakeField("sip", "SecondaryIP", QFT_TEXT, "Secondary IP address"),
Michael Hanselmann's avatar
Michael Hanselmann committed
1151 1152
     NQ_CONFIG, 0, _GetItemAttr("secondary_ip")),
    (_MakeField("tags", "Tags", QFT_OTHER, "Tags"), NQ_CONFIG, 0,
1153
     lambda ctx, node: list(node.GetTags())),
1154
    (_MakeField("master", "IsMaster", QFT_BOOL, "Whether node is master"),
Michael Hanselmann's avatar
Michael Hanselmann committed
1155 1156
     NQ_CONFIG, 0, lambda ctx, node: node.name == ctx.master_name),
    (_MakeField("group", "Group", QFT_TEXT, "Node group"), NQ_GROUP, 0,
1157
     _GetGroup(_GetNodeGroup)),
1158
    (_MakeField("group.uuid", "GroupUUID", QFT_TEXT, "UUID of node group"),
Michael Hanselmann's avatar
Michael Hanselmann committed
1159
     NQ_CONFIG, 0, _GetItemAttr("group")),
1160 1161
    (_MakeField("powered", "Powered", QFT_BOOL,
                "Whether node is thought to be powered on"),
Michael Hanselmann's avatar
Michael Hanselmann committed
1162
     NQ_OOB, 0, _GetNodePower),
1163 1164
    (_MakeField("ndparams", "NodeParameters", QFT_OTHER,
                "Merged node parameters"),
Michael Hanselmann's avatar
Michael Hanselmann committed
1165
     NQ_GROUP, 0, _GetGroup(_GetNdParams)),
1166 1167
    (_MakeField("custom_ndparams", "CustomNodeParameters", QFT_OTHER,
                "Custom node parameters"),
Michael Hanselmann's avatar
Michael Hanselmann committed
1168
      NQ_GROUP, 0, _GetItemAttr("ndparams")),
Michael Hanselmann's avatar
Michael Hanselmann committed
1169 1170
    ]

1171 1172 1173 1174 1175 1176 1177
  # Node role
  role_values = (constants.NR_MASTER, constants.NR_MCANDIDATE,
                 constants.NR_REGULAR, constants.NR_DRAINED,
                 constants.NR_OFFLINE)
  role_doc = ("Node role; \"%s\" for master, \"%s\" for master candidate,"
              " \"%s\" for regular, \"%s\" for a drained, \"%s\" for offline" %
              role_values)
Michael Hanselmann's avatar
Michael Hanselmann committed
1178
  fields.append((_MakeField("role", "Role", QFT_TEXT, role_doc), NQ_CONFIG, 0,
1179 1180 1181
                 lambda ctx, node: _GetNodeRole(node, ctx.master_name)))
  assert set(role_values) == constants.NR_ALL

Michael Hanselmann's avatar
Michael Hanselmann committed
1182
  def _GetLength(getter):
1183
    return lambda ctx, node: len(getter(ctx)[node.name])
Michael Hanselmann's avatar
Michael Hanselmann committed
1184 1185

  def _GetList(getter):
1186
    return lambda ctx, node: list(getter(ctx)[node.name])
Michael Hanselmann's avatar
Michael Hanselmann committed
1187 1188

  # Add fields operating on instance lists
1189 1190 1191
  for prefix, titleprefix, docword, getter in \
      [("p", "Pri", "primary", operator.attrgetter("node_to_primary")),
       ("s", "Sec", "secondary", operator.attrgetter("node_to_secondary"))]:
Michael Hanselmann's avatar
Michael Hanselmann committed
1192
    # TODO: Allow filterting by hostname in list
Michael Hanselmann's avatar
Michael Hanselmann committed
1193
    fields.extend([
1194 1195
      (_MakeField("%sinst_cnt" % prefix, "%sinst" % prefix.upper(), QFT_NUMBER,
                  "Number of instances with this node as %s" % docword),
Michael Hanselmann's avatar
Michael Hanselmann committed
1196
       NQ_INST, 0, _GetLength(getter)),
Michael Hanselmann's avatar
Michael Hanselmann committed
1197
      (_MakeField("%sinst_list" % prefix, "%sInstances" % titleprefix,
1198 1199
                  QFT_OTHER,
                  "List of instances with this node as %s" % docword),
Michael Hanselmann's avatar
Michael Hanselmann committed
1200
       NQ_INST, 0, _GetList(getter)),
Michael Hanselmann's avatar
Michael Hanselmann committed
1201 1202 1203
      ])

  # Add simple fields
Michael Hanselmann's avatar
Michael Hanselmann committed
1204 1205 1206 1207