diff --git a/qa/ganeti-qa.py b/qa/ganeti-qa.py
index 41ad3217bbf7905c9e6f93471f973c33905c8edd..34b3b3adc3a6b4d1807a60e96e78db0a237a41e9 100755
--- a/qa/ganeti-qa.py
+++ b/qa/ganeti-qa.py
@@ -171,6 +171,8 @@ def RunClusterTests():
     ("cluster-reserved-lvs", qa_cluster.TestClusterReservedLvs),
     # TODO: add more cluster modify tests
     ("cluster-modify", qa_cluster.TestClusterModifyEmpty),
+    ("cluster-modify", qa_cluster.TestClusterModifyIPolicy),
+    ("cluster-modify", qa_cluster.TestClusterModifyISpecs),
     ("cluster-modify", qa_cluster.TestClusterModifyBe),
     ("cluster-modify", qa_cluster.TestClusterModifyDisk),
     ("cluster-rename", qa_cluster.TestClusterRename),
diff --git a/qa/qa_cluster.py b/qa/qa_cluster.py
index 83a713e12b47d569473991fb2ef7a1a5f8358013..6ade74cf125b6fc384c05e028b20f38bc49b8dd8 100644
--- a/qa/qa_cluster.py
+++ b/qa/qa_cluster.py
@@ -423,6 +423,199 @@ def TestClusterModifyBe():
     AssertCommand(["gnt-cluster", "modify", "-B", bep])
 
 
+_START_IPOLICY_RE = re.compile(r"^(\s*)Instance policy")
+_START_ISPEC_RE = re.compile(r"^\s+-\s+(std|min|max)")
+_VALUE_RE = r"([^\s:][^:]*):\s+(\S.*)$"
+_IPOLICY_PARAM_RE = re.compile(r"^\s+-\s+" + _VALUE_RE)
+_ISPEC_VALUE_RE = re.compile(r"^\s+" + _VALUE_RE)
+
+
+def _GetClusterIPolicy():
+  """Return the run-time values of the cluster-level instance policy.
+
+  @rtype: tuple
+  @return: (policy, specs), where:
+      - policy is a dictionary of the policy values, instance specs excluded
+      - specs is dict of dict, specs[par][key] is a spec value, where key is
+        "min", "max", or "std"
+
+  """
+  mnode = qa_config.GetMasterNode()
+  info = GetCommandOutput(mnode["primary"], "gnt-cluster info")
+  inside_policy = False
+  end_ispec_re = None
+  curr_spec = ""
+  specs = {}
+  policy = {}
+  for line in info.splitlines():
+    if inside_policy:
+      # The order of the matching is important, as some REs overlap
+      m = _START_ISPEC_RE.match(line)
+      if m:
+        curr_spec = m.group(1)
+        continue
+      m = _IPOLICY_PARAM_RE.match(line)
+      if m:
+        policy[m.group(1)] = m.group(2).strip()
+        continue
+      m = _ISPEC_VALUE_RE.match(line)
+      if m:
+        assert curr_spec
+        par = m.group(1)
+        if par == "memory-size":
+          par = "mem-size"
+        d = specs.setdefault(par, {})
+        d[curr_spec] = m.group(2).strip()
+        continue
+      assert end_ispec_re is not None
+      if end_ispec_re.match(line):
+        inside_policy = False
+    else:
+      m = _START_IPOLICY_RE.match(line)
+      if m:
+        inside_policy = True
+        # We stop parsing when we find the same indentation level
+        re_str = r"^\s{%s}\S" % len(m.group(1))
+        end_ispec_re = re.compile(re_str)
+  # Sanity checks
+  assert len(specs) > 0
+  good = ("min" in d and "std" in d and "max" in d for d in specs)
+  assert good, "Missing item in specs: %s" % specs
+  assert len(policy) > 0
+  return (policy, specs)
+
+
+def TestClusterModifyIPolicy():
+  """gnt-cluster modify --ipolicy-*"""
+  basecmd = ["gnt-cluster", "modify"]
+  (old_policy, old_specs) = _GetClusterIPolicy()
+  for par in ["vcpu-ratio", "spindle-ratio"]:
+    curr_val = float(old_policy[par])
+    test_values = [
+      (True, 1.0),
+      (True, 1.5),
+      (True, 2),
+      (False, "a"),
+      # Restore the old value
+      (True, curr_val),
+      ]
+    for (good, val) in test_values:
+      cmd = basecmd + ["--ipolicy-%s=%s" % (par, val)]
+      AssertCommand(cmd, fail=not good)
+      if good:
+        curr_val = val
+      # Check the affected parameter
+      (eff_policy, eff_specs) = _GetClusterIPolicy()
+      AssertEqual(float(eff_policy[par]), curr_val)
+      # Check everything else
+      AssertEqual(eff_specs, old_specs)
+      for p in eff_policy.keys():
+        if p == par:
+          continue
+        AssertEqual(eff_policy[p], old_policy[p])
+
+  # Disk templates are treated slightly differently
+  par = "disk-templates"
+  disp_str = "enabled disk templates"
+  curr_val = old_policy[disp_str]
+  test_values = [
+    (True, constants.DT_PLAIN),
+    (True, "%s,%s" % (constants.DT_PLAIN, constants.DT_DRBD8)),
+    (False, "thisisnotadisktemplate"),
+    (False, ""),
+    # Restore the old value
+    (True, curr_val.replace(" ", "")),
+    ]
+  for (good, val) in test_values:
+    cmd = basecmd + ["--ipolicy-%s=%s" % (par, val)]
+    AssertCommand(cmd, fail=not good)
+    if good:
+      curr_val = val
+    # Check the affected parameter
+    (eff_policy, eff_specs) = _GetClusterIPolicy()
+    AssertEqual(eff_policy[disp_str].replace(" ", ""), curr_val)
+    # Check everything else
+    AssertEqual(eff_specs, old_specs)
+    for p in eff_policy.keys():
+      if p == disp_str:
+        continue
+      AssertEqual(eff_policy[p], old_policy[p])
+
+
+def TestClusterSetISpecs(new_specs, fail=False, old_values=None):
+  """Change instance specs.
+
+  @type new_specs: dict of dict
+  @param new_specs: new_specs[par][key], where key is "min", "max", "std". It
+      can be an empty dictionary.
+  @type fail: bool
+  @param fail: if the change is expected to fail
+  @type old_values: tuple
+  @param old_values: (old_policy, old_specs), as returned by
+     L{_GetClusterIPolicy}
+  @return: same as L{_GetClusterIPolicy}
+
+  """
+  if old_values:
+    (old_policy, old_specs) = old_values
+  else:
+    (old_policy, old_specs) = _GetClusterIPolicy()
+  if new_specs:
+    cmd = ["gnt-cluster", "modify"]
+    for (par, keyvals) in new_specs.items():
+      if par == "spindle-use":
+        # ignore spindle-use, which is not settable
+        continue
+      cmd += [
+        "--specs-%s" % par,
+        ",".join(["%s=%s" % (k, v) for (k, v) in keyvals.items()]),
+        ]
+    AssertCommand(cmd, fail=fail)
+  # Check the new state
+  (eff_policy, eff_specs) = _GetClusterIPolicy()
+  AssertEqual(eff_policy, old_policy)
+  if fail:
+    AssertEqual(eff_specs, old_specs)
+  else:
+    for par in eff_specs:
+      for key in eff_specs[par]:
+        if par in new_specs and key in new_specs[par]:
+          AssertEqual(int(eff_specs[par][key]), int(new_specs[par][key]))
+        else:
+          AssertEqual(int(eff_specs[par][key]), int(old_specs[par][key]))
+  return (eff_policy, eff_specs)
+
+
+def TestClusterModifyISpecs():
+  """gnt-cluster modify --specs-*"""
+  params = ["mem-size", "disk-size", "disk-count", "cpu-count", "nic-count"]
+  (cur_policy, cur_specs) = _GetClusterIPolicy()
+  for par in params:
+    test_values = [
+      (True, 0, 4, 12),
+      (True, 4, 4, 12),
+      (True, 4, 12, 12),
+      (True, 4, 4, 4),
+      (False, 4, 0, 12),
+      (False, 4, 16, 12),
+      (False, 4, 4, 0),
+      (False, 12, 4, 4),
+      (False, 12, 4, 0),
+      (False, "a", 4, 12),
+      (False, 0, "a", 12),
+      (False, 0, 4, "a"),
+      # This is to restore the old values
+      (True,
+       cur_specs[par]["min"], cur_specs[par]["std"], cur_specs[par]["max"])
+      ]
+    for (good, mn, st, mx) in test_values:
+      new_vals = {par: {"min": str(mn), "std": str(st), "max": str(mx)}}
+      cur_state = (cur_policy, cur_specs)
+      # We update cur_specs, as we've copied the values to restore already
+      (cur_policy, cur_specs) = TestClusterSetISpecs(new_vals, fail=not good,
+                                                     old_values=cur_state)
+
+
 def TestClusterInfo():
   """gnt-cluster info"""
   AssertCommand(["gnt-cluster", "info"])