diff --git a/lib/cmdlib.py b/lib/cmdlib.py
index 5ddecffcf5bb9a43af34ee1958c762df810202ff..622375fdde31eb0dd4917018c2f77917c13f2d4f 100644
--- a/lib/cmdlib.py
+++ b/lib/cmdlib.py
@@ -1020,18 +1020,32 @@ def _CheckOutputFields(static, dynamic, selected):
                                % ",".join(delta), errors.ECODE_INVAL)
 
 
-def _CheckGlobalHvParams(params):
-  """Validates that given hypervisor params are not global ones.
+def _CheckParamsNotGlobal(params, glob_pars, kind, bad_levels, good_levels):
+  """Make sure that none of the given paramters is global.
 
-  This will ensure that instances don't get customised versions of
-  global params.
+  If a global parameter is found, an L{errors.OpPrereqError} exception is
+  raised. This is used to avoid setting global parameters for individual nodes.
+
+  @type params: dictionary
+  @param params: Parameters to check
+  @type glob_pars: dictionary
+  @param glob_pars: Forbidden parameters
+  @type kind: string
+  @param kind: Kind of parameters (e.g. "node")
+  @type bad_levels: string
+  @param bad_levels: Level(s) at which the parameters are forbidden (e.g.
+      "instance")
+  @type good_levels: strings
+  @param good_levels: Level(s) at which the parameters are allowed (e.g.
+      "cluster or group")
 
   """
-  used_globals = constants.HVC_GLOBALS.intersection(params)
+  used_globals = glob_pars.intersection(params)
   if used_globals:
-    msg = ("The following hypervisor parameters are global and cannot"
-           " be customized at instance level, please modify them at"
-           " cluster level: %s" % utils.CommaJoin(used_globals))
+    msg = ("The following %s parameters are global and cannot"
+           " be customized at %s level, please modify them at"
+           " %s level: %s" %
+           (kind, bad_levels, good_levels, utils.CommaJoin(used_globals)))
     raise errors.OpPrereqError(msg, errors.ECODE_INVAL)
 
 
@@ -6129,6 +6143,8 @@ class LUNodeAdd(LogicalUnit):
 
     if self.op.ndparams:
       utils.ForceDictType(self.op.ndparams, constants.NDS_PARAMETER_TYPES)
+      _CheckParamsNotGlobal(self.op.ndparams, constants.NDC_GLOBALS, "node",
+                            "node", "cluster or group")
 
     if self.op.hv_state:
       self.new_hv_state = _MergeAndVerifyHvState(self.op.hv_state, None)
@@ -6154,9 +6170,6 @@ class LUNodeAdd(LogicalUnit):
     if vg_name is not None:
       vparams = {constants.NV_PVLIST: [vg_name]}
       excl_stor = _IsExclusiveStorageEnabledNode(cfg, self.new_node)
-      if self.op.ndparams:
-        excl_stor = self.op.ndparams.get(constants.ND_EXCLUSIVE_STORAGE,
-                                         excl_stor)
       cname = self.cfg.GetClusterName()
       result = rpcrunner.call_node_verify_light([node], vparams, cname)[node]
       (errmsgs, _) = _CheckNodePVs(result.payload, excl_stor)
@@ -6554,6 +6567,8 @@ class LUNodeSetParams(LogicalUnit):
     if self.op.ndparams:
       new_ndparams = _GetUpdatedParams(self.node.ndparams, self.op.ndparams)
       utils.ForceDictType(new_ndparams, constants.NDS_PARAMETER_TYPES)
+      _CheckParamsNotGlobal(self.op.ndparams, constants.NDC_GLOBALS, "node",
+                            "node", "cluster or group")
       self.new_ndparams = new_ndparams
 
     if self.op.hv_state:
@@ -10592,7 +10607,8 @@ class LUInstanceCreate(LogicalUnit):
     hv_type.CheckParameterSyntax(filled_hvp)
     self.hv_full = filled_hvp
     # check that we don't specify global parameters on an instance
-    _CheckGlobalHvParams(self.op.hvparams)
+    _CheckParamsNotGlobal(self.op.hvparams, constants.HVC_GLOBALS, "hypervisor",
+                          "instance", "cluster")
 
     # fill and remember the beparams dict
     self.be_full = _ComputeFullBeParams(self.op, cluster)
@@ -13290,7 +13306,8 @@ class LUInstanceSetParams(LogicalUnit):
       raise errors.OpPrereqError("No changes submitted", errors.ECODE_INVAL)
 
     if self.op.hvparams:
-      _CheckGlobalHvParams(self.op.hvparams)
+      _CheckParamsNotGlobal(self.op.hvparams, constants.HVC_GLOBALS,
+                            "hypervisor", "instance", "cluster")
 
     self.op.disks = self._UpgradeDiskNicMods(
       "disk", self.op.disks, opcodes.OpInstanceSetParams.TestDiskModifications)
diff --git a/lib/constants.py b/lib/constants.py
index b99b12c4d85edef39e44239880b22142e9ca0fdf..94c5080250a79e5c5f8d22d37ed78ae1ba9bf585 100644
--- a/lib/constants.py
+++ b/lib/constants.py
@@ -2023,6 +2023,10 @@ NDC_DEFAULTS = {
   ND_EXCLUSIVE_STORAGE: False,
   }
 
+NDC_GLOBALS = compat.UniqueFrozenset([
+  ND_EXCLUSIVE_STORAGE,
+  ])
+
 DISK_LD_DEFAULTS = {
   LD_DRBD8: {
     LDP_RESYNC_RATE: CLASSIC_DRBD_SYNC_SPEED,
diff --git a/lib/objects.py b/lib/objects.py
index 60f6afcc350a8295145d6a15bdc89d0a8dc5a595..f51c0ec7a4badd7d3749234974b6f05d6d8d796b 100644
--- a/lib/objects.py
+++ b/lib/objects.py
@@ -38,6 +38,7 @@ pass to and from external parties.
 import ConfigParser
 import re
 import copy
+import logging
 import time
 from cStringIO import StringIO
 
@@ -1334,6 +1335,12 @@ class Node(TaggableObject):
 
     if self.ndparams is None:
       self.ndparams = {}
+    # And remove any global parameter
+    for key in constants.NDC_GLOBALS:
+      if key in self.ndparams:
+        logging.warning("Ignoring %s node parameter for node %s",
+                        key, self.name)
+        del self.ndparams[key]
 
     if self.powered is None:
       self.powered = True
diff --git a/man/ganeti.rst b/man/ganeti.rst
index 03fe65272d05e0028d02db5487e7623f100497b3..16800d128253cb924441fda24b300d23b2a5e7dd 100644
--- a/man/ganeti.rst
+++ b/man/ganeti.rst
@@ -120,7 +120,9 @@ exclusive_storage
     When this Boolean flag is enabled, physical disks on the node are
     assigned to instance disks in an exclusive manner, so as to lower I/O
     interference between instances. See the `Partitioned Ganeti
-    <design-partitioned.rst>`_ design document for more details.
+    <design-partitioned.rst>`_ design document for more details. This
+    parameter cannot be set on individual nodes, as its value must be
+    the same within each node group.
 
 
 Hypervisor State Parameters
diff --git a/qa/ganeti-qa.py b/qa/ganeti-qa.py
index 5d6a73bea0eaac1710d17e36c065c17cabc6aa3e..41ad3217bbf7905c9e6f93471f973c33905c8edd 100755
--- a/qa/ganeti-qa.py
+++ b/qa/ganeti-qa.py
@@ -452,7 +452,7 @@ def RunExclusiveStorageTests():
   node = qa_config.AcquireNode()
   try:
     old_es = qa_cluster.TestSetExclStorCluster(False)
-    qa_cluster.TestExclStorSingleNode(node)
+    qa_node.TestExclStorSingleNode(node)
 
     qa_cluster.TestSetExclStorCluster(True)
     qa_cluster.TestExclStorSharedPv(node)
diff --git a/qa/qa_cluster.py b/qa/qa_cluster.py
index 09434a70d80274d877e79414ef6389d61cbcd53b..ffb0971702c296e34249b330b604f55f49593e10 100644
--- a/qa/qa_cluster.py
+++ b/qa/qa_cluster.py
@@ -667,24 +667,6 @@ def TestSetExclStorCluster(newvalue):
   return oldvalue
 
 
-def _BuildSetESCmd(value, node_name):
-  return ["gnt-node", "modify", "--node-parameters",
-          "exclusive_storage=%s" % value, node_name]
-
-
-def TestExclStorSingleNode(node):
-  """cluster-verify reports exclusive_storage set only on one node.
-
-  """
-  node_name = node["primary"]
-  es_val = _GetBoolClusterField("exclusive_storage")
-  assert not es_val
-  AssertCommand(_BuildSetESCmd(True, node_name))
-  AssertClusterVerify(fail=True, errors=[constants.CV_EGROUPMIXEDESFLAG])
-  AssertCommand(_BuildSetESCmd("default", node_name))
-  AssertClusterVerify()
-
-
 def TestExclStorSharedPv(node):
   """cluster-verify reports LVs that share the same PV with exclusive_storage.
 
diff --git a/qa/qa_node.py b/qa/qa_node.py
index 647af19078e7cd59a2a274c5dfcebaf6cff558d5..ac7fc90f93399eb00c1e1e3bf36a245a7d399c17 100644
--- a/qa/qa_node.py
+++ b/qa/qa_node.py
@@ -422,3 +422,22 @@ def TestNodeListFields():
 def TestNodeListDrbd(node):
   """gnt-node list-drbd"""
   AssertCommand(["gnt-node", "list-drbd", node["primary"]])
+
+
+def _BuildSetESCmd(action, value, node_name):
+  cmd = ["gnt-node"]
+  if action == "add":
+    cmd.extend(["add", "--readd"])
+  else:
+    cmd.append("modify")
+  cmd.extend(["--node-parameters", "exclusive_storage=%s" % value, node_name])
+  return cmd
+
+
+def TestExclStorSingleNode(node):
+  """gnt-node add/modify cannot change the exclusive_storage flag.
+
+  """
+  for action in ["add", "modify"]:
+    for value in (True, False, "default"):
+      AssertCommand(_BuildSetESCmd(action, value, node["primary"]), fail=True)
diff --git a/test/py/ganeti.objects_unittest.py b/test/py/ganeti.objects_unittest.py
index c29f83eec7c23feca027f88a2c00bdf3008246e5..9c4595d029bbe154e855f5db623daa805c510805 100755
--- a/test/py/ganeti.objects_unittest.py
+++ b/test/py/ganeti.objects_unittest.py
@@ -351,6 +351,23 @@ class TestNode(unittest.TestCase):
     self.assertEqual(node2.disk_state[constants.LD_LV]["lv2082"].total, 512)
     self.assertEqual(node2.disk_state[constants.LD_LV]["lv32352"].total, 128)
 
+  def testFilterEsNdp(self):
+    node1 = objects.Node(name="node11673.example.com", ndparams={
+      constants.ND_EXCLUSIVE_STORAGE: True,
+      })
+    node2 = objects.Node(name="node11674.example.com", ndparams={
+      constants.ND_SPINDLE_COUNT: 3,
+      constants.ND_EXCLUSIVE_STORAGE: False,
+      })
+    self.assertTrue(constants.ND_EXCLUSIVE_STORAGE in node1.ndparams)
+    node1.UpgradeConfig()
+    self.assertFalse(constants.ND_EXCLUSIVE_STORAGE in node1.ndparams)
+    self.assertTrue(constants.ND_EXCLUSIVE_STORAGE in node2.ndparams)
+    self.assertTrue(constants.ND_SPINDLE_COUNT in node2.ndparams)
+    node2.UpgradeConfig()
+    self.assertFalse(constants.ND_EXCLUSIVE_STORAGE in node2.ndparams)
+    self.assertTrue(constants.ND_SPINDLE_COUNT in node2.ndparams)
+
 
 if __name__ == "__main__":
   testutils.GanetiTestProgram()