diff --git a/lib/cmdlib.py b/lib/cmdlib.py
index 75b230334b5b4d7642536c7135b3f6eeb9634799..d0b6a368e0608462e7dc8938652f0c05a448d773 100644
--- a/lib/cmdlib.py
+++ b/lib/cmdlib.py
@@ -10224,6 +10224,113 @@ class LURemoveExport(NoHooksLU):
                   " Domain Name.")
 
 
+class LUQueryGroups(NoHooksLU):
+  """Logical unit for querying node groups.
+
+  """
+  # pylint: disable-msg=W0142
+  _OP_PARAMS = [
+    _POutputFields,
+    ("names", ht.EmptyList, ht.TListOf(ht.TNonEmptyString)),
+    ]
+
+  REQ_BGL = False
+
+  _FIELDS_DYNAMIC = utils.FieldSet()
+
+  _SIMPLE_FIELDS = ["name", "uuid"]
+
+  _FIELDS_STATIC = utils.FieldSet(
+      "node_cnt", "node_list", "pinst_cnt", "pinst_list", *_SIMPLE_FIELDS)
+
+  def CheckArguments(self):
+    _CheckOutputFields(static=self._FIELDS_STATIC,
+                       dynamic=self._FIELDS_DYNAMIC,
+                       selected=self.op.output_fields)
+
+  def ExpandNames(self):
+    self.needed_locks = {}
+
+  def Exec(self, feedback_fn):
+    """Computes the list of groups and their attributes.
+
+    """
+    all_groups = self.cfg.GetAllNodeGroupsInfo()
+
+    if not self.op.names:
+      my_groups = utils.NiceSort(all_groups.keys())
+    else:
+      # Accept names to be either names or UUIDs.
+      all_uuid = frozenset(all_groups.keys())
+      name_to_uuid = dict((g.name, g.uuid) for g in all_groups.values())
+      my_groups = []
+      missing = []
+
+      for name in self.op.names:
+        if name in all_uuid:
+          my_groups.append(name)
+        elif name in name_to_uuid:
+          my_groups.append(name_to_uuid[name])
+        else:
+          missing.append(name)
+
+      if missing:
+        raise errors.OpPrereqError("Some groups do not exist: %s" % missing,
+                                   errors.ECODE_NOENT)
+
+    do_nodes = bool(frozenset(["node_cnt", "node_list"]).
+                    intersection(self.op.output_fields))
+
+    do_instances = bool(frozenset(["pinst_cnt", "pinst_list"]).
+                        intersection(self.op.output_fields))
+
+    # We need to map group->[nodes], and group->[instances]. The former is
+    # directly attainable, but the latter we have to do through instance->node,
+    # hence we need to process nodes even if we only need instance information.
+    if do_nodes or do_instances:
+      all_nodes = self.cfg.GetAllNodesInfo()
+      group_to_nodes = dict((all_groups[name].uuid, []) for name in my_groups)
+      node_to_group = {}
+
+      for node in all_nodes.values():
+        if node.group in group_to_nodes:
+          group_to_nodes[node.group].append(node.name)
+          node_to_group[node.name] = node.group
+
+      if do_instances:
+        all_instances = self.cfg.GetAllInstancesInfo()
+        group_to_instances = dict((all_groups[name].uuid, [])
+                                  for name in my_groups)
+        for instance in all_instances.values():
+          node = instance.primary_node
+          if node in node_to_group:
+            group_to_instances[node_to_group[node]].append(instance.name)
+
+    output = []
+
+    for name in my_groups:
+      group = all_groups[name]
+      group_output = []
+
+      for field in self.op.output_fields:
+        if field in self._SIMPLE_FIELDS:
+          val = getattr(group, field)
+        elif field == "node_list":
+          val = utils.NiceSort(group_to_nodes[group.uuid])
+        elif field == "node_cnt":
+          val = len(group_to_nodes[group.uuid])
+        elif field == "pinst_list":
+          val = utils.NiceSort(group_to_instances[group.uuid])
+        elif field == "pinst_cnt":
+          val = len(group_to_instances[group.uuid])
+        else:
+          raise errors.ParameterError(field)
+        group_output.append(val)
+      output.append(group_output)
+
+    return output
+
+
 class TagsLU(NoHooksLU): # pylint: disable-msg=W0223
   """Generic tags LU.
 
diff --git a/lib/mcpu.py b/lib/mcpu.py
index 1d88f712e083edeed20927888267f34563366fbb..a724eb9e9fe21856e808c0cc09aae1a8e3e3a173 100644
--- a/lib/mcpu.py
+++ b/lib/mcpu.py
@@ -188,6 +188,8 @@ class Processor(object):
     opcodes.OpQueryInstanceData: cmdlib.LUQueryInstanceData,
     opcodes.OpSetInstanceParams: cmdlib.LUSetInstanceParams,
     opcodes.OpGrowDisk: cmdlib.LUGrowDisk,
+    # node group lu
+    opcodes.OpQueryGroups: cmdlib.LUQueryGroups,
     # os lu
     opcodes.OpDiagnoseOS: cmdlib.LUDiagnoseOS,
     # exports lu
diff --git a/lib/opcodes.py b/lib/opcodes.py
index 6683425d25cc4d844f60e9797eb5ca68d435aa35..93138280819e60078abece7c42eaf5e03dd9e58d 100644
--- a/lib/opcodes.py
+++ b/lib/opcodes.py
@@ -700,6 +700,14 @@ class OpGrowDisk(OpCode):
     ]
 
 
+# Node group opcodes
+
+class OpQueryGroups(OpCode):
+  """Compute the list of node groups."""
+  OP_ID = "OP_GROUP_QUERY"
+  __slots__ = ["output_fields", "names"]
+
+
 # OS opcodes
 class OpDiagnoseOS(OpCode):
   """Compute the list of guest operating systems."""