diff --git a/lib/query.py b/lib/query.py
index 2091190c4a4a5a6f39d1cb685ee0598af8c57f37..89681b82eb4fcddce8da825304f5708b290fa512 100644
--- a/lib/query.py
+++ b/lib/query.py
@@ -1,7 +1,7 @@
 #
 #
 
-# Copyright (C) 2010, 2011 Google Inc.
+# Copyright (C) 2010, 2011, 2012 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
@@ -136,6 +136,12 @@ _VTToQFT = {
 
 _SERIAL_NO_DOC = "%s object serial number, incremented on each modification"
 
+# TODO: Consider moving titles closer to constants
+NDP_TITLE = {
+  constants.ND_OOB_PROGRAM: "OutOfBandProgram",
+  constants.ND_SPINDLE_COUNT: "SpindleCount",
+  }
+
 
 def _GetUnknownField(ctx, item): # pylint: disable=W0613
   """Gets the contents of an unknown field.
@@ -899,6 +905,34 @@ def _GetItemAttr(attr):
   return lambda _, item: getter(item)
 
 
+def _GetNDParam(name):
+  """Return a field function to return an ND parameter out of the context.
+
+  """
+  def _helper(ctx, _):
+    if ctx.ndparams is None:
+      return _FS_UNAVAIL
+    else:
+      return ctx.ndparams.get(name, None)
+  return _helper
+
+
+def _BuildNDFields(is_group):
+  """Builds all the ndparam fields.
+
+  @param is_group: whether this is called at group or node level
+
+  """
+  if is_group:
+    field_kind = GQ_CONFIG
+  else:
+    field_kind = NQ_GROUP
+  return [(_MakeField("ndp/%s" % name, NDP_TITLE.get(name, "ndp/%s" % name),
+                      _VTToQFT[kind], "The \"%s\" node parameter" % name),
+           field_kind, 0, _GetNDParam(name))
+          for name, kind in constants.NDS_PARAMETER_TYPES.items()]
+
+
 def _ConvWrapInner(convert, fn, ctx, item):
   """Wrapper for converting values.
 
@@ -982,6 +1016,7 @@ class NodeQueryData:
 
     # Used for individual rows
     self.curlive_data = None
+    self.ndparams = None
 
   def __iter__(self):
     """Iterate over all nodes.
@@ -991,6 +1026,11 @@ class NodeQueryData:
 
     """
     for node in self.nodes:
+      group = self.groups.get(node.group, None)
+      if group is None:
+        self.ndparams = None
+      else:
+        self.ndparams = self.cluster.FillND(node, group)
       if self.live_data:
         self.curlive_data = self.live_data.get(node.name, None)
       else:
@@ -1198,6 +1238,8 @@ def _BuildNodeFields():
      NQ_CONFIG, 0, _GetNodeDiskState),
     ]
 
+  fields.extend(_BuildNDFields(False))
+
   # Node role
   role_values = (constants.NR_MASTER, constants.NR_MCANDIDATE,
                  constants.NR_REGULAR, constants.NR_DRAINED,
@@ -1959,6 +2001,7 @@ class GroupQueryData:
 
     # Used for individual rows
     self.group_ipolicy = None
+    self.ndparams = None
 
   def __iter__(self):
     """Iterate over all node groups.
@@ -1969,6 +2012,7 @@ class GroupQueryData:
     """
     for group in self.groups:
       self.group_ipolicy = self.cluster.SimpleFillIPolicy(group.ipolicy)
+      self.ndparams = self.cluster.SimpleFillND(group.ndparams)
       yield group
 
 
@@ -1977,7 +2021,6 @@ _GROUP_SIMPLE_FIELDS = {
   "name": ("Group", QFT_TEXT, "Group name"),
   "serial_no": ("SerialNo", QFT_NUMBER, _SERIAL_NO_DOC % "Group"),
   "uuid": ("UUID", QFT_TEXT, "Group UUID"),
-  "ndparams": ("NDParams", QFT_OTHER, "Node parameters"),
   }
 
 
@@ -2027,8 +2070,17 @@ def _BuildGroupFields():
     (_MakeField("custom_ipolicy", "CustomInstancePolicy", QFT_OTHER,
                 "Custom instance policy limitations"),
      GQ_CONFIG, 0, _GetItemAttr("ipolicy")),
+    (_MakeField("custom_ndparams", "CustomNDParams", QFT_OTHER,
+                "Custom node parameters"),
+     GQ_CONFIG, 0, _GetItemAttr("ndparams")),
+    (_MakeField("ndparams", "NDParams", QFT_OTHER,
+                "Node parameters"),
+     GQ_CONFIG, 0, lambda ctx, _: ctx.ndparams),
     ])
 
+  # ND parameters
+  fields.extend(_BuildNDFields(True))
+
   fields.extend(_GetItemTimestampFields(GQ_CONFIG))
 
   return _PrepareFieldList(fields, [])
diff --git a/test/ganeti.query_unittest.py b/test/ganeti.query_unittest.py
index 666eed40516b902dbd89ab81fa8f21c08d67e45b..caaa62680303c8d445532ae36118917386bda88e 100755
--- a/test/ganeti.query_unittest.py
+++ b/test/ganeti.query_unittest.py
@@ -1,7 +1,7 @@
 #!/usr/bin/python
 #
 
-# Copyright (C) 2010, 2011 Google Inc.
+# Copyright (C) 2010, 2011, 2012 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
@@ -322,14 +322,30 @@ class TestNodeQuery(unittest.TestCase):
     return query.Query(query.NODE_FIELDS, selected)
 
   def testSimple(self):
+    cluster = objects.Cluster(cluster_name="testcluster",
+                              ndparams=constants.NDC_DEFAULTS.copy())
+    grp1 = objects.NodeGroup(name="default",
+                             uuid="c0e89160-18e7-11e0-a46e-001d0904baeb",
+                             alloc_policy=constants.ALLOC_POLICY_PREFERRED,
+                             ipolicy=objects.MakeEmptyIPolicy(),
+                             ndparams={},
+                             )
+    grp2 = objects.NodeGroup(name="group2",
+                             uuid="c0e89160-18e7-11e0-a46e-001d0904babe",
+                             alloc_policy=constants.ALLOC_POLICY_PREFERRED,
+                             ipolicy=objects.MakeEmptyIPolicy(),
+                             ndparams={constants.ND_SPINDLE_COUNT: 2},
+                             )
+    groups = {grp1.uuid: grp1, grp2.uuid: grp2}
     nodes = [
-      objects.Node(name="node1", drained=False),
-      objects.Node(name="node2", drained=True),
-      objects.Node(name="node3", drained=False),
+      objects.Node(name="node1", drained=False, group=grp1.uuid, ndparams={}),
+      objects.Node(name="node2", drained=True, group=grp2.uuid, ndparams={}),
+      objects.Node(name="node3", drained=False, group=grp1.uuid,
+                   ndparams={constants.ND_SPINDLE_COUNT: 4}),
       ]
     for live_data in [None, dict.fromkeys([node.name for node in nodes], {})]:
-      nqd = query.NodeQueryData(nodes, live_data, None, None, None, None, None,
-                                None)
+      nqd = query.NodeQueryData(nodes, live_data, None, None, None,
+                                groups, None, cluster)
 
       q = self._Create(["name", "drained"])
       self.assertEqual(q.RequestedData(), set([query.NQ_CONFIG]))
@@ -345,6 +361,16 @@ class TestNodeQuery(unittest.TestCase):
                        [["node1", False],
                         ["node2", True],
                         ["node3", False]])
+      q = self._Create(["ndp/spindle_count"])
+      self.assertEqual(q.RequestedData(), set([query.NQ_GROUP]))
+      self.assertEqual(q.Query(nqd),
+                       [[(constants.RS_NORMAL,
+                          constants.NDC_DEFAULTS[constants.ND_SPINDLE_COUNT])],
+                        [(constants.RS_NORMAL,
+                          grp2.ndparams[constants.ND_SPINDLE_COUNT])],
+                        [(constants.RS_NORMAL,
+                          nodes[2].ndparams[constants.ND_SPINDLE_COUNT])],
+                       ])
 
   def test(self):
     selected = query.NODE_FIELDS.keys()
@@ -933,11 +959,15 @@ class TestGroupQuery(unittest.TestCase):
       objects.NodeGroup(name="default",
                         uuid="c0e89160-18e7-11e0-a46e-001d0904baeb",
                         alloc_policy=constants.ALLOC_POLICY_PREFERRED,
-                        ipolicy=objects.MakeEmptyIPolicy()),
+                        ipolicy=objects.MakeEmptyIPolicy(),
+                        ndparams={},
+                        ),
       objects.NodeGroup(name="restricted",
                         uuid="d2a40a74-18e7-11e0-9143-001d0904baeb",
                         alloc_policy=constants.ALLOC_POLICY_LAST_RESORT,
-                        ipolicy=objects.MakeEmptyIPolicy()),
+                        ipolicy=objects.MakeEmptyIPolicy(),
+                        ndparams={}
+                        ),
       ]
     self.cluster = objects.Cluster(cluster_name="testcluster",
       hvparams=constants.HVC_DEFAULTS,