diff --git a/doc/hooks.rst b/doc/hooks.rst
index 2effa481c688b886edc88b01ed836ac3f04034c6..3cfd40498c3567edb1060f4fc776d9b38e3536e8 100644
--- a/doc/hooks.rst
+++ b/doc/hooks.rst
@@ -168,6 +168,20 @@ Relocate secondary instances from a node.
 :post-execution: master node
 
 
+Node group operations
+~~~~~~~~~~~~~~~~~~~~~
+
+OP_ADD_GROUP
+++++++++++++
+
+Adds a node group to the cluster.
+
+:directory: group-add
+:env. vars: GROUP_NAME
+:pre-execution: master node
+:post-execution: master node
+
+
 Instance operations
 ~~~~~~~~~~~~~~~~~~~
 
diff --git a/lib/cmdlib.py b/lib/cmdlib.py
index 9505e19a2b3c6e9e8274468a1130b3bcc888975c..24142ece9f347616931f0bb93e1fd4cac03d4173 100644
--- a/lib/cmdlib.py
+++ b/lib/cmdlib.py
@@ -81,6 +81,9 @@ _PIgnoreOfflineNodes = ("ignore_offline_nodes", False, ht.TBool)
 #: a required node name (for single-node LUs)
 _PNodeName = ("node_name", ht.NoDefault, ht.TNonEmptyString)
 
+#: a required node group name (for single-group LUs)
+_PGroupName = ("group_name", ht.NoDefault, ht.TNonEmptyString)
+
 #: the migration type (live/non-live)
 _PMigrationMode = ("mode", None,
                    ht.TOr(ht.TNone, ht.TElemOf(constants.HT_MIGRATION_MODES)))
@@ -10297,6 +10300,65 @@ class LURemoveExport(NoHooksLU):
                   " Domain Name.")
 
 
+class LUAddGroup(LogicalUnit):
+  """Logical unit for creating node groups.
+
+  """
+  HPATH = "group-add"
+  HTYPE = constants.HTYPE_GROUP
+
+  _OP_PARAMS = [
+    _PGroupName,
+    ]
+
+  REQ_BGL = False
+
+  def ExpandNames(self):
+    # We need the new group's UUID here so that we can create and acquire the
+    # corresponding lock. Later, in Exec(), we'll indicate to cfg.AddNodeGroup
+    # that it should not check whether the UUID exists in the configuration.
+    self.group_uuid = self.cfg.GenerateUniqueID(self.proc.GetECId())
+    self.needed_locks = {}
+    self.add_locks[locking.LEVEL_NODEGROUP] = self.group_uuid
+
+  def CheckPrereq(self):
+    """Check prerequisites.
+
+    This checks that the given group name is not an existing node group
+    already.
+
+    """
+    try:
+      existing_uuid = self.cfg.LookupNodeGroup(self.op.group_name)
+    except errors.OpPrereqError:
+      pass
+    else:
+      raise errors.OpPrereqError("Desired group name '%s' already exists as a"
+                                 " node group (UUID: %s)" %
+                                 (self.op.group_name, existing_uuid),
+                                 errors.ECODE_EXISTS)
+
+  def BuildHooksEnv(self):
+    """Build hooks env.
+
+    """
+    env = {
+      "GROUP_NAME": self.op.group_name,
+      }
+    mn = self.cfg.GetMasterNode()
+    return env, [mn], [mn]
+
+  def Exec(self, feedback_fn):
+    """Add the node group to the cluster.
+
+    """
+    group_obj = objects.NodeGroup(name=self.op.group_name, members=[],
+                                  uuid=self.group_uuid)
+
+    self.cfg.AddNodeGroup(group_obj, self.proc.GetECId(), check_uuid=False)
+    del self.remove_locks[locking.LEVEL_NODEGROUP]
+
+
 class LUQueryGroups(NoHooksLU):
   """Logical unit for querying node groups.
 
diff --git a/lib/constants.py b/lib/constants.py
index 083a026ea78f1ad4f3e79ca19e469fe420c4c5c6..7716d8e9808a03b1bdfcf6b656f82e30f75d3463 100644
--- a/lib/constants.py
+++ b/lib/constants.py
@@ -294,6 +294,7 @@ HOOKS_VERSION = 2
 # hooks subject type (what object type does the LU deal with)
 HTYPE_CLUSTER = "CLUSTER"
 HTYPE_NODE = "NODE"
+HTYPE_GROUP = "GROUP"
 HTYPE_INSTANCE = "INSTANCE"
 
 HKR_SKIP = 0
diff --git a/lib/mcpu.py b/lib/mcpu.py
index 65e52eeac034b93ea138fd9126cfafe5b93c1182..436ce28ca09c53bdbab155b086e8d03c201eb6d7 100644
--- a/lib/mcpu.py
+++ b/lib/mcpu.py
@@ -189,6 +189,7 @@ class Processor(object):
     opcodes.OpSetInstanceParams: cmdlib.LUSetInstanceParams,
     opcodes.OpGrowDisk: cmdlib.LUGrowDisk,
     # node group lu
+    opcodes.OpAddGroup: cmdlib.LUAddGroup,
     opcodes.OpQueryGroups: cmdlib.LUQueryGroups,
     # os lu
     opcodes.OpDiagnoseOS: cmdlib.LUDiagnoseOS,
diff --git a/lib/opcodes.py b/lib/opcodes.py
index f061c5ab4591492e2009335daad03994eea78732..6d0e8aaf0c173151628532ea4bd76f31f0203931 100644
--- a/lib/opcodes.py
+++ b/lib/opcodes.py
@@ -720,6 +720,13 @@ class OpGrowDisk(OpCode):
 
 # Node group opcodes
 
+class OpAddGroup(OpCode):
+  """Add a node group to the cluster."""
+  OP_ID = "OP_GROUP_ADD"
+  OP_DSC_FIELD = "group_name"
+  __slots__ = ["group_name"]
+
+
 class OpQueryGroups(OpCode):
   """Compute the list of node groups."""
   OP_ID = "OP_GROUP_QUERY"