diff --git a/Makefile.am b/Makefile.am
index 8f39bd31ad4c4f0bad0d0fc680c372d620ca07e3..b51685a30f80f879d07cee29bfbd492fb89c1f20 100644
--- a/Makefile.am
+++ b/Makefile.am
@@ -138,6 +138,7 @@ pkgpython_PYTHON = \
 	lib/netutils.py \
 	lib/objects.py \
 	lib/opcodes.py \
+	lib/query.py \
 	lib/rpc.py \
 	lib/runtime.py \
 	lib/serializer.py \
@@ -442,6 +443,7 @@ python_tests = \
 	test/ganeti.netutils_unittest.py \
 	test/ganeti.objects_unittest.py \
 	test/ganeti.opcodes_unittest.py \
+	test/ganeti.query_unittest.py \
 	test/ganeti.rapi.client_unittest.py \
 	test/ganeti.rapi.resources_unittest.py \
 	test/ganeti.rapi.rlib2_unittest.py \
diff --git a/lib/query.py b/lib/query.py
new file mode 100644
index 0000000000000000000000000000000000000000..9be8c545290b0d7186fc4fc220fa97ff406e588e
--- /dev/null
+++ b/lib/query.py
@@ -0,0 +1,233 @@
+#
+#
+
+# Copyright (C) 2010 Google Inc.
+#
+# 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.
+
+
+"""Module for query operations"""
+
+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
+
+
+FIELD_NAME_RE = re.compile(r"^[a-z0-9/._]+$")
+TITLE_RE = re.compile(r"^[^\s]+$")
+
+#: Verification function for each field type
+_VERIFY_FN = {
+  constants.QFT_UNKNOWN: ht.TNone,
+  constants.QFT_TEXT: ht.TString,
+  constants.QFT_BOOL: ht.TBool,
+  constants.QFT_NUMBER: ht.TInt,
+  constants.QFT_UNIT: ht.TInt,
+  constants.QFT_TIMESTAMP: ht.TOr(ht.TInt, ht.TFloat),
+  constants.QFT_OTHER: lambda _: True,
+  }
+
+
+def _GetUnknownField(ctx, item): # pylint: disable-msg=W0613
+  """Gets the contents of an unknown field.
+
+  """
+  return (constants.QRFS_UNKNOWN, None)
+
+
+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:
+      fdef = (_MakeField(name, name, constants.QFT_UNKNOWN),
+              None, _GetUnknownField)
+
+    assert len(fdef) == 3
+
+    result.append(fdef)
+
+  return result
+
+
+def GetAllFields(fielddefs):
+  """Extract L{objects.QueryFieldDefinition} from field definitions.
+
+  @rtype: list of L{objects.QueryFieldDefinition}
+
+  """
+  return [fdef for (fdef, _, _) in fielddefs]
+
+
+class Query:
+  def __init__(self, fieldlist, selected):
+    """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
+
+    """
+    self._fields = _GetQueryFields(fieldlist, selected)
+
+  def RequestedData(self):
+    """Gets requested kinds of data.
+
+    @rtype: frozenset
+
+    """
+    return frozenset(datakind
+                     for (_, datakind, _) in self._fields
+                     if datakind is not None)
+
+  def GetFields(self):
+    """Returns the list of fields for this query.
+
+    Includes unknown fields.
+
+    @rtype: List of L{objects.QueryFieldDefinition}
+
+    """
+    return GetAllFields(self._fields)
+
+  def Query(self, ctx):
+    """Execute a query.
+
+    @param ctx: Data container passed to field retrieval functions, must
+      support iteration using C{__iter__}
+
+    """
+    result = [[fn(ctx, item) for (_, _, fn) in self._fields]
+              for item in ctx]
+
+    # Verify result
+    if __debug__:
+      for (idx, row) in enumerate(result):
+        assert _VerifyResultRow(self._fields, row), \
+               ("Inconsistent result for fields %s in row %s: %r" %
+                (self._fields, idx, row))
+
+    return result
+
+  def OldStyleQuery(self, ctx):
+    """Query with "old" query result format.
+
+    See L{Query.Query} for arguments.
+
+    """
+    unknown = set(fdef.name
+                  for (fdef, _, _) in self._fields
+                  if fdef.kind == constants.QFT_UNKNOWN)
+    if unknown:
+      raise errors.OpPrereqError("Unknown output fields selected: %s" %
+                                 (utils.CommaJoin(unknown), ),
+                                 errors.ECODE_INVAL)
+
+    return [[value for (_, value) in row]
+            for row in self.Query(ctx)]
+
+
+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
+
+  """
+  return (len(row) == len(fields) and
+          compat.all((status == constants.QRFS_NORMAL and
+                      _VERIFY_FN[fdef.kind](value)) or
+                     # Value for an abnormal status must be None
+                     (status != constants.QRFS_NORMAL and value is None)
+                     for ((status, value), (fdef, _, _)) in zip(row, fields)))
+
+
+def _PrepareFieldList(fields):
+  """Prepares field list for use by L{Query}.
+
+  Converts the list to a dictionary and does some verification.
+
+  @type fields: list of tuples; (L{objects.QueryFieldDefinition}, data kind,
+    retrieval function)
+  @param fields: List of fields
+  @rtype: dict
+  @return: Field dictionary for L{Query}
+
+  """
+  assert len(set(fdef.title.lower()
+                 for (fdef, _, _) in fields)) == len(fields), \
+         "Duplicate title found"
+
+  result = {}
+
+  for field in fields:
+    (fdef, _, fn) = field
+
+    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 callable(fn)
+    assert fdef.name not in result, "Duplicate field name found"
+
+    result[fdef.name] = field
+
+  assert len(result) == len(fields)
+  assert compat.all(name == fdef.name
+                    for (name, (fdef, _, _)) in result.items())
+
+  return result
+
+
+def _MakeField(name, title, kind):
+  """Wrapper for creating L{objects.QueryFieldDefinition} instances.
+
+  @param name: Field name as a regular expression
+  @param title: Human-readable title
+  @param kind: Field type
+
+  """
+  return objects.QueryFieldDefinition(name=name, title=title, kind=kind)
diff --git a/test/ganeti.query_unittest.py b/test/ganeti.query_unittest.py
new file mode 100755
index 0000000000000000000000000000000000000000..098645756095e8932fe75a5dc8722ee704debd95
--- /dev/null
+++ b/test/ganeti.query_unittest.py
@@ -0,0 +1,258 @@
+#!/usr/bin/python
+#
+
+# Copyright (C) 2010 Google Inc.
+#
+# 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.
+
+
+"""Script for testing ganeti.query"""
+
+import re
+import unittest
+
+from ganeti import constants
+from ganeti import utils
+from ganeti import compat
+from ganeti import errors
+from ganeti import query
+from ganeti import objects
+
+import testutils
+
+
+class TestConstants(unittest.TestCase):
+  def test(self):
+    self.assertEqual(set(query._VERIFY_FN.keys()),
+                     constants.QFT_ALL)
+
+
+class _QueryData:
+  def __init__(self, data, **kwargs):
+    self.data = data
+
+    for name, value in kwargs.items():
+      setattr(self, name, value)
+
+  def __iter__(self):
+    return iter(self.data)
+
+
+def _GetDiskSize(nr, ctx, item):
+  disks = item["disks"]
+  try:
+    return (constants.QRFS_NORMAL, disks[nr])
+  except IndexError:
+    return (constants.QRFS_UNAVAIL, None)
+
+
+class TestQuery(unittest.TestCase):
+  def test(self):
+    (STATIC, DISK) = range(10, 12)
+
+    fielddef = query._PrepareFieldList([
+      (query._MakeField("name", "Name", constants.QFT_TEXT),
+       STATIC, lambda ctx, item: (constants.QRFS_NORMAL, item["name"])),
+      (query._MakeField("master", "Master", constants.QFT_BOOL),
+       STATIC, lambda ctx, item: (constants.QRFS_NORMAL,
+                                  ctx.mastername == item["name"])),
+      ] +
+      [(query._MakeField("disk%s.size" % i, "DiskSize%s" % i,
+                         constants.QFT_UNIT),
+        DISK, compat.partial(_GetDiskSize, i))
+       for i in range(4)])
+
+    q = query.Query(fielddef, ["name"])
+    self.assertEqual(q.RequestedData(), set([STATIC]))
+    self.assertEqual(len(q._fields), 1)
+    self.assertEqual(len(q.GetFields()), 1)
+    self.assertEqual(q.GetFields()[0].ToDict(),
+      objects.QueryFieldDefinition(name="name",
+                                   title="Name",
+                                   kind=constants.QFT_TEXT).ToDict())
+
+    # Create data only once query has been prepared
+    data = [
+      { "name": "node1", "disks": [0, 1, 2], },
+      { "name": "node2", "disks": [3, 4], },
+      { "name": "node3", "disks": [5, 6, 7], },
+      ]
+
+    self.assertEqual(q.Query(_QueryData(data, mastername="node3")),
+                     [[(constants.QRFS_NORMAL, "node1")],
+                      [(constants.QRFS_NORMAL, "node2")],
+                      [(constants.QRFS_NORMAL, "node3")]])
+    self.assertEqual(q.OldStyleQuery(_QueryData(data, mastername="node3")),
+                     [["node1"], ["node2"], ["node3"]])
+
+    q = query.Query(fielddef, ["name", "master"])
+    self.assertEqual(q.RequestedData(), set([STATIC]))
+    self.assertEqual(len(q._fields), 2)
+    self.assertEqual(q.Query(_QueryData(data, mastername="node3")),
+                     [[(constants.QRFS_NORMAL, "node1"),
+                       (constants.QRFS_NORMAL, False)],
+                      [(constants.QRFS_NORMAL, "node2"),
+                       (constants.QRFS_NORMAL, False)],
+                      [(constants.QRFS_NORMAL, "node3"),
+                       (constants.QRFS_NORMAL, True)],
+                     ])
+
+    q = query.Query(fielddef, ["name", "master", "disk0.size"])
+    self.assertEqual(q.RequestedData(), set([STATIC, DISK]))
+    self.assertEqual(len(q._fields), 3)
+    self.assertEqual(q.Query(_QueryData(data, mastername="node2")),
+                     [[(constants.QRFS_NORMAL, "node1"),
+                       (constants.QRFS_NORMAL, False),
+                       (constants.QRFS_NORMAL, 0)],
+                      [(constants.QRFS_NORMAL, "node2"),
+                       (constants.QRFS_NORMAL, True),
+                       (constants.QRFS_NORMAL, 3)],
+                      [(constants.QRFS_NORMAL, "node3"),
+                       (constants.QRFS_NORMAL, False),
+                       (constants.QRFS_NORMAL, 5)],
+                     ])
+
+    # With unknown column
+    q = query.Query(fielddef, ["disk2.size", "disk1.size", "disk99.size",
+                               "disk0.size"])
+    self.assertEqual(q.RequestedData(), set([DISK]))
+    self.assertEqual(len(q._fields), 4)
+    self.assertEqual(q.Query(_QueryData(data, mastername="node2")),
+                     [[(constants.QRFS_NORMAL, 2),
+                       (constants.QRFS_NORMAL, 1),
+                       (constants.QRFS_UNKNOWN, None),
+                       (constants.QRFS_NORMAL, 0)],
+                      [(constants.QRFS_UNAVAIL, None),
+                       (constants.QRFS_NORMAL, 4),
+                       (constants.QRFS_UNKNOWN, None),
+                       (constants.QRFS_NORMAL, 3)],
+                      [(constants.QRFS_NORMAL, 7),
+                       (constants.QRFS_NORMAL, 6),
+                       (constants.QRFS_UNKNOWN, None),
+                       (constants.QRFS_NORMAL, 5)],
+                     ])
+    self.assertRaises(errors.OpPrereqError, q.OldStyleQuery,
+                      _QueryData(data, mastername="node2"))
+    self.assertEqual([fdef.ToDict() for fdef in q.GetFields()], [
+                     { "name": "disk2.size", "title": "DiskSize2",
+                       "kind": constants.QFT_UNIT, },
+                     { "name": "disk1.size", "title": "DiskSize1",
+                       "kind": constants.QFT_UNIT, },
+                     { "name": "disk99.size", "title": "disk99.size",
+                       "kind": constants.QFT_UNKNOWN, },
+                     { "name": "disk0.size", "title": "DiskSize0",
+                       "kind": constants.QFT_UNIT, },
+                     ])
+
+    # Empty query
+    q = query.Query(fielddef, [])
+    self.assertEqual(q.RequestedData(), set([]))
+    self.assertEqual(len(q._fields), 0)
+    self.assertEqual(q.Query(_QueryData(data, mastername="node2")),
+                     [[], [], []])
+    self.assertEqual(q.OldStyleQuery(_QueryData(data, mastername="node2")),
+                     [[], [], []])
+    self.assertEqual(q.GetFields(), [])
+
+  def testPrepareFieldList(self):
+    # Duplicate titles
+    for (a, b) in [("name", "name"), ("NAME", "name")]:
+      self.assertRaises(AssertionError, query._PrepareFieldList, [
+        (query._MakeField("name", b, constants.QFT_TEXT), None,
+         lambda *args: None),
+        (query._MakeField("other", a, constants.QFT_TEXT), None,
+         lambda *args: None),
+        ])
+
+    # Non-lowercase names
+    self.assertRaises(AssertionError, query._PrepareFieldList, [
+      (query._MakeField("NAME", "Name", constants.QFT_TEXT), None,
+       lambda *args: None),
+      ])
+    self.assertRaises(AssertionError, query._PrepareFieldList, [
+      (query._MakeField("Name", "Name", constants.QFT_TEXT), None,
+       lambda *args: None),
+      ])
+
+    # Empty name
+    self.assertRaises(AssertionError, query._PrepareFieldList, [
+      (query._MakeField("", "Name", constants.QFT_TEXT), None,
+       lambda *args: None),
+      ])
+
+    # Empty title
+    self.assertRaises(AssertionError, query._PrepareFieldList, [
+      (query._MakeField("name", "", constants.QFT_TEXT), None,
+       lambda *args: None),
+      ])
+
+    # Whitespace in title
+    self.assertRaises(AssertionError, query._PrepareFieldList, [
+      (query._MakeField("name", "Co lu mn", constants.QFT_TEXT), None,
+       lambda *args: None),
+      ])
+
+    # No callable function
+    self.assertRaises(AssertionError, query._PrepareFieldList, [
+      (query._MakeField("name", "Name", constants.QFT_TEXT), None, None),
+      ])
+
+  def testUnknown(self):
+    fielddef = query._PrepareFieldList([
+      (query._MakeField("name", "Name", constants.QFT_TEXT),
+       None, lambda _, item: (constants.QRFS_NORMAL, "name%s" % item)),
+      (query._MakeField("other0", "Other0", constants.QFT_TIMESTAMP),
+       None, lambda *args: (constants.QRFS_NORMAL, 1234)),
+      (query._MakeField("nodata", "NoData", constants.QFT_NUMBER),
+       None, lambda *args: (constants.QRFS_NODATA, None)),
+      (query._MakeField("unavail", "Unavail", constants.QFT_BOOL),
+       None, lambda *args: (constants.QRFS_UNAVAIL, None)),
+      ])
+
+    for selected in [["foo"], ["Hello", "World"],
+                     ["name1", "other", "foo"]]:
+      q = query.Query(fielddef, selected)
+      self.assertEqual(len(q._fields), len(selected))
+      self.assert_(compat.all(len(row) == len(selected)
+                              for row in q.Query(_QueryData(range(1, 10)))))
+      self.assertEqual(q.Query(_QueryData(range(1, 10))),
+                       [[(constants.QRFS_UNKNOWN, None)] * len(selected)
+                        for i in range(1, 10)])
+      self.assertEqual([fdef.ToDict() for fdef in q.GetFields()],
+                       [{ "name": name, "title": name,
+                          "kind": constants.QFT_UNKNOWN, }
+                        for name in selected])
+
+    q = query.Query(fielddef, ["name", "other0", "nodata", "unavail"])
+    self.assertEqual(len(q._fields), 4)
+    self.assertEqual(q.OldStyleQuery(_QueryData(range(1, 10))), [
+                     ["name%s" % i, 1234, None, None]
+                     for i in range(1, 10)
+                     ])
+
+    q = query.Query(fielddef, ["name", "other0", "nodata", "unavail", "unk"])
+    self.assertEqual(len(q._fields), 5)
+    self.assertEqual(q.Query(_QueryData(range(1, 10))),
+                     [[(constants.QRFS_NORMAL, "name%s" % i),
+                       (constants.QRFS_NORMAL, 1234),
+                       (constants.QRFS_NODATA, None),
+                       (constants.QRFS_UNAVAIL, None),
+                       (constants.QRFS_UNKNOWN, None)]
+                      for i in range(1, 10)])
+
+
+if __name__ == "__main__":
+  testutils.GanetiTestProgram()