diff --git a/doc/rapi.rst b/doc/rapi.rst
index d278b2975b028cc34cb5aaf40a87f2d704f59949..fbc9970c3c9bda2d77efe1b79f15e7efad725652 100644
--- a/doc/rapi.rst
+++ b/doc/rapi.rst
@@ -497,6 +497,46 @@ Body parameters:
    :exclude: group_name, force, dry_run
 
 
+``/2/groups/[group_name]/tags``
++++++++++++++++++++++++++++++++
+
+Manages per-nodegroup tags.
+
+Supports the following commands: ``GET``, ``PUT``, ``DELETE``.
+
+``GET``
+~~~~~~~
+
+Returns a list of tags.
+
+Example::
+
+    ["tag1", "tag2", "tag3"]
+
+``PUT``
+~~~~~~~
+
+Add a set of tags.
+
+The request as a list of strings should be ``PUT`` to this URI. The
+result will be a job id.
+
+It supports the ``dry-run`` argument.
+
+
+``DELETE``
+~~~~~~~~~~
+
+Delete a tag.
+
+In order to delete a set of tags, the DELETE request should be addressed
+to URI like::
+
+    /tags?tag=[tag]&tag=[tag]
+
+It supports the ``dry-run`` argument.
+
+
 ``/2/instances``
 ++++++++++++++++
 
diff --git a/lib/rapi/baserlib.py b/lib/rapi/baserlib.py
index 66f69b28bce55b678626d3661dffd91b77395f88..534ebae19e18687d2a1c8550ce61cfeaea46c0bc 100644
--- a/lib/rapi/baserlib.py
+++ b/lib/rapi/baserlib.py
@@ -92,12 +92,16 @@ def _Tags_GET(kind, name):
   """Helper function to retrieve tags.
 
   """
-  if kind == constants.TAG_INSTANCE or kind == constants.TAG_NODE:
+  if kind in (constants.TAG_INSTANCE,
+              constants.TAG_NODEGROUP,
+              constants.TAG_NODE):
     if not name:
       raise http.HttpBadRequest("Missing name on tag request")
     cl = GetClient()
     if kind == constants.TAG_INSTANCE:
       fn = cl.QueryInstances
+    elif kind == constants.TAG_NODEGROUP:
+      fn = cl.QueryGroups
     else:
       fn = cl.QueryNodes
     result = fn(names=[name], fields=["tags"], use_locking=False)
diff --git a/lib/rapi/client.py b/lib/rapi/client.py
index f1a157d3a1b78ac7e5577bf513a3d8607621426a..546b58cbdd1268d76bbe258f17b5952743adac3c 100644
--- a/lib/rapi/client.py
+++ b/lib/rapi/client.py
@@ -1620,6 +1620,63 @@ class GanetiRapiClient(object): # pylint: disable-msg=R0904
                              ("/%s/groups/%s/assign-nodes" %
                              (GANETI_RAPI_VERSION, group)), query, body)
 
+  def GetGroupTags(self, group):
+    """Gets tags for a node group.
+
+    @type group: string
+    @param group: Node group whose tags to return
+
+    @rtype: list of strings
+    @return: tags for the group
+
+    """
+    return self._SendRequest(HTTP_GET,
+                             ("/%s/groups/%s/tags" %
+                              (GANETI_RAPI_VERSION, group)), None, None)
+
+  def AddGroupTags(self, group, tags, dry_run=False):
+    """Adds tags to a node group.
+
+    @type group: str
+    @param group: group to add tags to
+    @type tags: list of string
+    @param tags: tags to add to the group
+    @type dry_run: bool
+    @param dry_run: whether to perform a dry run
+
+    @rtype: string
+    @return: job id
+
+    """
+    query = [("tag", t) for t in tags]
+    if dry_run:
+      query.append(("dry-run", 1))
+
+    return self._SendRequest(HTTP_PUT,
+                             ("/%s/groups/%s/tags" %
+                              (GANETI_RAPI_VERSION, group)), query, None)
+
+  def DeleteGroupTags(self, group, tags, dry_run=False):
+    """Deletes tags from a node group.
+
+    @type group: str
+    @param group: group to delete tags from
+    @type tags: list of string
+    @param tags: tags to delete
+    @type dry_run: bool
+    @param dry_run: whether to perform a dry run
+    @rtype: string
+    @return: job id
+
+    """
+    query = [("tag", t) for t in tags]
+    if dry_run:
+      query.append(("dry-run", 1))
+
+    return self._SendRequest(HTTP_DELETE,
+                             ("/%s/groups/%s/tags" %
+                              (GANETI_RAPI_VERSION, group)), query, None)
+
   def Query(self, what, fields, filter_=None):
     """Retrieves information about resources.
 
diff --git a/lib/rapi/connector.py b/lib/rapi/connector.py
index 6243f4a1bf2ff12a9f1d70ebc521897ce0c7bc50..cb4bb296cc445f965e59a1869ee0d7828859ab30 100644
--- a/lib/rapi/connector.py
+++ b/lib/rapi/connector.py
@@ -230,6 +230,8 @@ def GetHandlers(node_name_pattern, instance_name_pattern,
       rlib2.R_2_groups_name_rename,
     re.compile(r'^/2/groups/(%s)/assign-nodes$' % group_name_pattern):
       rlib2.R_2_groups_name_assign_nodes,
+    re.compile(r'^/2/groups/(%s)/tags$' % group_name_pattern):
+      rlib2.R_2_groups_name_tags,
 
     "/2/jobs": rlib2.R_2_jobs,
     re.compile(r"^/2/jobs/(%s)$" % job_id_pattern):
diff --git a/lib/rapi/rlib2.py b/lib/rapi/rlib2.py
index 66b0ecec82bc18eb36b6fd9f807aab773afe5faf..1e8c50cfd1fbf439af80325ae0994f3dd63196bf 100644
--- a/lib/rapi/rlib2.py
+++ b/lib/rapi/rlib2.py
@@ -1375,6 +1375,15 @@ class R_2_nodes_name_tags(_R_Tags):
   TAG_LEVEL = constants.TAG_NODE
 
 
+class R_2_groups_name_tags(_R_Tags):
+  """ /2/groups/[group_name]/tags resource.
+
+  Manages per-nodegroup tags.
+
+  """
+  TAG_LEVEL = constants.TAG_NODEGROUP
+
+
 class R_2_tags(_R_Tags):
   """ /2/tags resource.
 
diff --git a/test/ganeti.rapi.client_unittest.py b/test/ganeti.rapi.client_unittest.py
index 3f9f2b8f6208e9c154a6c3056b1373f70e587a11..464d451780f1987cd0c2231e81e835f1bbd7cc38 100755
--- a/test/ganeti.rapi.client_unittest.py
+++ b/test/ganeti.rapi.client_unittest.py
@@ -1087,6 +1087,30 @@ class GanetiRapiClientTests(testutils.GanetiTestCase):
       self.assertEqual(data["amount"], amount)
       self.assertEqual(self.rapi.CountPending(), 0)
 
+  def testGetGroupTags(self):
+    self.rapi.AddResponse("[]")
+    self.assertEqual([], self.client.GetGroupTags("fooGroup"))
+    self.assertHandler(rlib2.R_2_groups_name_tags)
+    self.assertItems(["fooGroup"])
+
+  def testAddGroupTags(self):
+    self.rapi.AddResponse("1234")
+    self.assertEqual(1234,
+        self.client.AddGroupTags("fooGroup", ["awesome"], dry_run=True))
+    self.assertHandler(rlib2.R_2_groups_name_tags)
+    self.assertItems(["fooGroup"])
+    self.assertDryRun()
+    self.assertQuery("tag", ["awesome"])
+
+  def testDeleteGroupTags(self):
+    self.rapi.AddResponse("25826")
+    self.assertEqual(25826, self.client.DeleteGroupTags("foo", ["awesome"],
+                                                        dry_run=True))
+    self.assertHandler(rlib2.R_2_groups_name_tags)
+    self.assertItems(["foo"])
+    self.assertDryRun()
+    self.assertQuery("tag", ["awesome"])
+
   def testQuery(self):
     for idx, what in enumerate(constants.QR_VIA_RAPI):
       for idx2, filter_ in enumerate([None, ["?", "name"]]):