Merge branch 'stable-2.7' into stable-2.8
[ganeti-local] / qa / qa_cluster.py
index 77a78fb..f532826 100644 (file)
@@ -1,7 +1,7 @@
 #
 #
 
 #
 #
 
-# Copyright (C) 2007, 2010, 2011, 2012 Google Inc.
+# Copyright (C) 2007, 2010, 2011, 2012, 2013 Google Inc.
 #
 # This program is free software; you can redistribute it and/or modify
 # it under the terms of the GNU General Public License as published by
 #
 # This program is free software; you can redistribute it and/or modify
 # it under the terms of the GNU General Public License as published by
 
 """
 
 
 """
 
+import re
 import tempfile
 import os.path
 
 from ganeti import constants
 from ganeti import compat
 from ganeti import utils
 import tempfile
 import os.path
 
 from ganeti import constants
 from ganeti import compat
 from ganeti import utils
+from ganeti import pathutils
 
 import qa_config
 import qa_utils
 import qa_error
 
 import qa_config
 import qa_utils
 import qa_error
+import qa_instance
 
 from qa_utils import AssertEqual, AssertCommand, GetCommandOutput
 
 
 
 from qa_utils import AssertEqual, AssertCommand, GetCommandOutput
 
 
+# Prefix for LVM volumes created by QA code during tests
+_QA_LV_PREFIX = "qa-"
+
 #: cluster verify command
 _CLUSTER_VERIFY = ["gnt-cluster", "verify"]
 
 #: cluster verify command
 _CLUSTER_VERIFY = ["gnt-cluster", "verify"]
 
@@ -55,7 +61,79 @@ def _CheckFileOnAllNodes(filename, content):
   """
   cmd = utils.ShellQuoteArgs(["cat", filename])
   for node in qa_config.get("nodes"):
   """
   cmd = utils.ShellQuoteArgs(["cat", filename])
   for node in qa_config.get("nodes"):
-    AssertEqual(qa_utils.GetCommandOutput(node["primary"], cmd), content)
+    AssertEqual(qa_utils.GetCommandOutput(node.primary, cmd), content)
+
+
+def _GetClusterField(field_path):
+  """Get the value of a cluster field.
+
+  @type field_path: list of strings
+  @param field_path: Names of the groups/fields to navigate to get the desired
+      value, e.g. C{["Default node parameters", "oob_program"]}
+  @return: The effective value of the field (the actual type depends on the
+      chosen field)
+
+  """
+  assert isinstance(field_path, list)
+  assert field_path
+  ret = qa_utils.GetObjectInfo(["gnt-cluster", "info"])
+  for key in field_path:
+    ret = ret[key]
+  return ret
+
+
+# Cluster-verify errors (date, "ERROR", then error code)
+_CVERROR_RE = re.compile(r"^[\w\s:]+\s+- (ERROR|WARNING):([A-Z0-9_-]+):")
+
+
+def _GetCVErrorCodes(cvout):
+  errs = set()
+  warns = set()
+  for l in cvout.splitlines():
+    m = _CVERROR_RE.match(l)
+    if m:
+      etype = m.group(1)
+      ecode = m.group(2)
+      if etype == "ERROR":
+        errs.add(ecode)
+      elif etype == "WARNING":
+        warns.add(ecode)
+  return (errs, warns)
+
+
+def _CheckVerifyErrors(actual, expected, etype):
+  exp_codes = compat.UniqueFrozenset(e for (_, e, _) in expected)
+  if not actual.issuperset(exp_codes):
+    missing = exp_codes.difference(actual)
+    raise qa_error.Error("Cluster-verify didn't return these expected"
+                         " %ss: %s" % (etype, utils.CommaJoin(missing)))
+
+
+def AssertClusterVerify(fail=False, errors=None, warnings=None):
+  """Run cluster-verify and check the result
+
+  @type fail: bool
+  @param fail: if cluster-verify is expected to fail instead of succeeding
+  @type errors: list of tuples
+  @param errors: List of CV_XXX errors that are expected; if specified, all the
+      errors listed must appear in cluster-verify output. A non-empty value
+      implies C{fail=True}.
+  @type warnings: list of tuples
+  @param warnings: Same as C{errors} but for warnings.
+
+  """
+  cvcmd = "gnt-cluster verify"
+  mnode = qa_config.GetMasterNode()
+  if errors or warnings:
+    cvout = GetCommandOutput(mnode.primary, cvcmd + " --error-codes",
+                             fail=(fail or errors))
+    (act_errs, act_warns) = _GetCVErrorCodes(cvout)
+    if errors:
+      _CheckVerifyErrors(act_errs, errors, "error")
+    if warnings:
+      _CheckVerifyErrors(act_warns, warnings, "warning")
+  else:
+    AssertCommand(cvcmd, fail=fail, node=mnode)
 
 
 # data for testing failures due to bad keys/values for disk parameters
 
 
 # data for testing failures due to bad keys/values for disk parameters
@@ -76,7 +154,8 @@ def TestClusterInit(rapi_user, rapi_secret):
   """gnt-cluster init"""
   master = qa_config.GetMasterNode()
 
   """gnt-cluster init"""
   master = qa_config.GetMasterNode()
 
-  rapi_dir = os.path.dirname(constants.RAPI_USERS_FILE)
+  rapi_users_path = qa_utils.MakeNodePath(master, pathutils.RAPI_USERS_FILE)
+  rapi_dir = os.path.dirname(rapi_users_path)
 
   # First create the RAPI credentials
   fh = tempfile.NamedTemporaryFile()
 
   # First create the RAPI credentials
   fh = tempfile.NamedTemporaryFile()
@@ -84,42 +163,62 @@ def TestClusterInit(rapi_user, rapi_secret):
     fh.write("%s %s write\n" % (rapi_user, rapi_secret))
     fh.flush()
 
     fh.write("%s %s write\n" % (rapi_user, rapi_secret))
     fh.flush()
 
-    tmpru = qa_utils.UploadFile(master["primary"], fh.name)
+    tmpru = qa_utils.UploadFile(master.primary, fh.name)
     try:
       AssertCommand(["mkdir", "-p", rapi_dir])
     try:
       AssertCommand(["mkdir", "-p", rapi_dir])
-      AssertCommand(["mv", tmpru, constants.RAPI_USERS_FILE])
+      AssertCommand(["mv", tmpru, rapi_users_path])
     finally:
       AssertCommand(["rm", "-f", tmpru])
   finally:
     fh.close()
 
   # Initialize cluster
     finally:
       AssertCommand(["rm", "-f", tmpru])
   finally:
     fh.close()
 
   # Initialize cluster
-  cmd = ["gnt-cluster", "init"]
-
-  cmd.append("--primary-ip-version=%d" %
-             qa_config.get("primary_ip_version", 4))
+  cmd = [
+    "gnt-cluster", "init",
+    "--primary-ip-version=%d" % qa_config.get("primary_ip_version", 4),
+    "--enabled-hypervisors=%s" % ",".join(qa_config.GetEnabledHypervisors()),
+    "--enabled-disk-templates=%s" %
+      ",".join(qa_config.GetEnabledDiskTemplates())
+    ]
 
   for spec_type in ("mem-size", "disk-size", "disk-count", "cpu-count",
                     "nic-count"):
     for spec_val in ("min", "max", "std"):
       spec = qa_config.get("ispec_%s_%s" %
 
   for spec_type in ("mem-size", "disk-size", "disk-count", "cpu-count",
                     "nic-count"):
     for spec_val in ("min", "max", "std"):
       spec = qa_config.get("ispec_%s_%s" %
-                           (spec_type.replace('-', '_'), spec_val), None)
-      if spec:
+                           (spec_type.replace("-", "_"), spec_val), None)
+      if spec is not None:
         cmd.append("--specs-%s=%s=%d" % (spec_type, spec_val, spec))
 
         cmd.append("--specs-%s=%s=%d" % (spec_type, spec_val, spec))
 
-  if master.get("secondary", None):
-    cmd.append("--secondary-ip=%s" % master["secondary"])
+  if master.secondary:
+    cmd.append("--secondary-ip=%s" % master.secondary)
+
+  vgname = qa_config.get("vg-name", None)
+  if vgname:
+    cmd.append("--vg-name=%s" % vgname)
+
+  master_netdev = qa_config.get("master-netdev", None)
+  if master_netdev:
+    cmd.append("--master-netdev=%s" % master_netdev)
 
 
-  bridge = qa_config.get("bridge", None)
-  if bridge:
-    cmd.append("--bridge=%s" % bridge)
-    cmd.append("--master-netdev=%s" % bridge)
+  nicparams = qa_config.get("default-nicparams", None)
+  if nicparams:
+    cmd.append("--nic-parameters=%s" %
+               ",".join(utils.FormatKeyValue(nicparams)))
 
 
-  htype = qa_config.get("enabled-hypervisors", None)
-  if htype:
-    cmd.append("--enabled-hypervisors=%s" % htype)
+  # Cluster value of the exclusive-storage node parameter
+  e_s = qa_config.get("exclusive-storage")
+  if e_s is not None:
+    cmd.extend(["--node-parameters", "exclusive_storage=%s" % e_s])
+  else:
+    e_s = False
+  qa_config.SetExclusiveStorage(e_s)
+
+  extra_args = qa_config.get("cluster-init-args")
+  if extra_args:
+    cmd.extend(extra_args)
 
   cmd.append(qa_config.get("name"))
 
   cmd.append(qa_config.get("name"))
+
   AssertCommand(cmd)
 
   cmd = ["gnt-cluster", "modify"]
   AssertCommand(cmd)
 
   cmd = ["gnt-cluster", "modify"]
@@ -204,7 +303,7 @@ def TestClusterEpo():
   master = qa_config.GetMasterNode()
 
   # Assert that OOB is unavailable for all nodes
   master = qa_config.GetMasterNode()
 
   # Assert that OOB is unavailable for all nodes
-  result_output = GetCommandOutput(master["primary"],
+  result_output = GetCommandOutput(master.primary,
                                    "gnt-node list --verbose --no-headers -o"
                                    " powered")
   AssertEqual(compat.all(powered == "(unavail)"
                                    "gnt-node list --verbose --no-headers -o"
                                    " powered")
   AssertEqual(compat.all(powered == "(unavail)"
@@ -216,22 +315,23 @@ def TestClusterEpo():
   AssertCommand(["gnt-cluster", "epo", "--all", "some_arg"], fail=True)
 
   # Unless --all is given master is not allowed to be in the list
   AssertCommand(["gnt-cluster", "epo", "--all", "some_arg"], fail=True)
 
   # Unless --all is given master is not allowed to be in the list
-  AssertCommand(["gnt-cluster", "epo", "-f", master["primary"]], fail=True)
+  AssertCommand(["gnt-cluster", "epo", "-f", master.primary], fail=True)
 
   # This shouldn't fail
   AssertCommand(["gnt-cluster", "epo", "-f", "--all"])
 
   # All instances should have been stopped now
 
   # This shouldn't fail
   AssertCommand(["gnt-cluster", "epo", "-f", "--all"])
 
   # All instances should have been stopped now
-  result_output = GetCommandOutput(master["primary"],
+  result_output = GetCommandOutput(master.primary,
                                    "gnt-instance list --no-headers -o status")
                                    "gnt-instance list --no-headers -o status")
-  AssertEqual(compat.all(status == "ADMIN_down"
+  # ERROR_down because the instance is stopped but not recorded as such
+  AssertEqual(compat.all(status == "ERROR_down"
                          for status in result_output.splitlines()), True)
 
   # Now start everything again
   AssertCommand(["gnt-cluster", "epo", "--on", "-f", "--all"])
 
   # All instances should have been started now
                          for status in result_output.splitlines()), True)
 
   # Now start everything again
   AssertCommand(["gnt-cluster", "epo", "--on", "-f", "--all"])
 
   # All instances should have been started now
-  result_output = GetCommandOutput(master["primary"],
+  result_output = GetCommandOutput(master.primary,
                                    "gnt-instance list --no-headers -o status")
   AssertEqual(compat.all(status == "running"
                          for status in result_output.splitlines()), True)
                                    "gnt-instance list --no-headers -o status")
   AssertEqual(compat.all(status == "running"
                          for status in result_output.splitlines()), True)
@@ -253,24 +353,28 @@ def TestDelay(node):
   AssertCommand(["gnt-debug", "delay", "1"])
   AssertCommand(["gnt-debug", "delay", "--no-master", "1"])
   AssertCommand(["gnt-debug", "delay", "--no-master",
   AssertCommand(["gnt-debug", "delay", "1"])
   AssertCommand(["gnt-debug", "delay", "--no-master", "1"])
   AssertCommand(["gnt-debug", "delay", "--no-master",
-                 "-n", node["primary"], "1"])
+                 "-n", node.primary, "1"])
 
 
 def TestClusterReservedLvs():
   """gnt-cluster reserved lvs"""
 
 
 def TestClusterReservedLvs():
   """gnt-cluster reserved lvs"""
+  vgname = qa_config.get("vg-name", constants.DEFAULT_VG)
+  lvname = _QA_LV_PREFIX + "test"
+  lvfullname = "/".join([vgname, lvname])
   for fail, cmd in [
     (False, _CLUSTER_VERIFY),
     (False, ["gnt-cluster", "modify", "--reserved-lvs", ""]),
   for fail, cmd in [
     (False, _CLUSTER_VERIFY),
     (False, ["gnt-cluster", "modify", "--reserved-lvs", ""]),
-    (False, ["lvcreate", "-L1G", "-nqa-test", "xenvg"]),
+    (False, ["lvcreate", "-L1G", "-n", lvname, vgname]),
     (True, _CLUSTER_VERIFY),
     (False, ["gnt-cluster", "modify", "--reserved-lvs",
     (True, _CLUSTER_VERIFY),
     (False, ["gnt-cluster", "modify", "--reserved-lvs",
-             "xenvg/qa-test,.*/other-test"]),
+             "%s,.*/other-test" % lvfullname]),
     (False, _CLUSTER_VERIFY),
     (False, _CLUSTER_VERIFY),
-    (False, ["gnt-cluster", "modify", "--reserved-lvs", ".*/qa-.*"]),
+    (False, ["gnt-cluster", "modify", "--reserved-lvs",
+             ".*/%s.*" % _QA_LV_PREFIX]),
     (False, _CLUSTER_VERIFY),
     (False, ["gnt-cluster", "modify", "--reserved-lvs", ""]),
     (True, _CLUSTER_VERIFY),
     (False, _CLUSTER_VERIFY),
     (False, ["gnt-cluster", "modify", "--reserved-lvs", ""]),
     (True, _CLUSTER_VERIFY),
-    (False, ["lvremove", "-f", "xenvg/qa-test"]),
+    (False, ["lvremove", "-f", lvfullname]),
     (False, _CLUSTER_VERIFY),
     ]:
     AssertCommand(cmd, fail=fail)
     (False, _CLUSTER_VERIFY),
     ]:
     AssertCommand(cmd, fail=fail)
@@ -287,6 +391,105 @@ def TestClusterModifyDisk():
     AssertCommand(["gnt-cluster", "modify", "-D", param], fail=True)
 
 
     AssertCommand(["gnt-cluster", "modify", "-D", param], fail=True)
 
 
+def TestClusterModifyDiskTemplates():
+  """gnt-cluster modify --enabled-disk-templates=..."""
+  enabled_disk_templates = qa_config.GetEnabledDiskTemplates()
+  default_disk_template = qa_config.GetDefaultDiskTemplate()
+
+  _TestClusterModifyDiskTemplatesArguments(default_disk_template,
+                                           enabled_disk_templates)
+
+  _RestoreEnabledDiskTemplates()
+  nodes = qa_config.AcquireManyNodes(2)
+
+  instance_template = enabled_disk_templates[0]
+  instance = qa_instance.CreateInstanceByDiskTemplate(nodes, instance_template)
+
+  _TestClusterModifyUnusedDiskTemplate(instance_template)
+  _TestClusterModifyUsedDiskTemplate(instance_template,
+                                     enabled_disk_templates)
+
+  qa_instance.TestInstanceRemove(instance)
+  _RestoreEnabledDiskTemplates()
+
+
+def _RestoreEnabledDiskTemplates():
+  """Sets the list of enabled disk templates back to the list of enabled disk
+     templates from the QA configuration. This can be used to make sure that
+     the tests that modify the list of disk templates do not interfere with
+     other tests.
+
+  """
+  AssertCommand(
+    ["gnt-cluster", "modify",
+     "--enabled-disk-template=%s" %
+       ",".join(qa_config.GetEnabledDiskTemplates())],
+    fail=False)
+
+
+def _TestClusterModifyDiskTemplatesArguments(default_disk_template,
+                                             enabled_disk_templates):
+  """Tests argument handling of 'gnt-cluster modify' with respect to
+     the parameter '--enabled-disk-templates'. This test is independent
+     of instances.
+
+  """
+  AssertCommand(
+    ["gnt-cluster", "modify",
+     "--enabled-disk-template=%s" %
+       ",".join(enabled_disk_templates)],
+    fail=False)
+
+  # bogus templates
+  AssertCommand(["gnt-cluster", "modify",
+                 "--enabled-disk-templates=pinkbunny"],
+                fail=True)
+
+  # duplicate entries do no harm
+  AssertCommand(
+    ["gnt-cluster", "modify",
+     "--enabled-disk-templates=%s,%s" %
+      (default_disk_template, default_disk_template)],
+    fail=False)
+
+
+def _TestClusterModifyUsedDiskTemplate(instance_template,
+                                       enabled_disk_templates):
+  """Tests that disk templates that are currently in use by instances cannot
+     be disabled on the cluster.
+
+  """
+  # If the list of enabled disk templates contains only one template
+  # we need to add some other templates, because the list of enabled disk
+  # templates can only be set to a non-empty list.
+  new_disk_templates = list(set(enabled_disk_templates)
+                              - set([instance_template]))
+  if not new_disk_templates:
+    new_disk_templates = list(set(constants.DISK_TEMPLATES)
+                                - set([instance_template]))
+  AssertCommand(
+    ["gnt-cluster", "modify",
+     "--enabled-disk-templates=%s" %
+       ",".join(new_disk_templates)],
+    fail=True)
+
+
+def _TestClusterModifyUnusedDiskTemplate(instance_template):
+  """Tests that unused disk templates can be disabled safely."""
+  all_disk_templates = constants.DISK_TEMPLATES
+  AssertCommand(
+    ["gnt-cluster", "modify",
+     "--enabled-disk-templates=%s" %
+       ",".join(all_disk_templates)],
+    fail=False)
+  new_disk_templates = [instance_template]
+  AssertCommand(
+    ["gnt-cluster", "modify",
+     "--enabled-disk-templates=%s" %
+       ",".join(new_disk_templates)],
+    fail=False)
+
+
 def TestClusterModifyBe():
   """gnt-cluster modify -B"""
   for fail, cmd in [
 def TestClusterModifyBe():
   """gnt-cluster modify -B"""
   for fail, cmd in [
@@ -323,6 +526,151 @@ def TestClusterModifyBe():
     AssertCommand(["gnt-cluster", "modify", "-B", bep])
 
 
     AssertCommand(["gnt-cluster", "modify", "-B", bep])
 
 
+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"
+
+  """
+  info = qa_utils.GetObjectInfo(["gnt-cluster", "info"])
+  policy = info["Instance policy - limits for instances"]
+  (ret_policy, ret_specs) = qa_utils.ParseIPolicy(policy)
+
+  # Sanity checks
+  assert len(ret_specs) > 0
+  good = all("min" in d and "std" in d and "max" in d
+             for d in ret_specs.values())
+  assert good, "Missing item in specs: %s" % ret_specs
+  assert len(ret_policy) > 0
+  return (ret_policy, ret_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}
+
+  """
+  build_cmd = lambda opts: ["gnt-cluster", "modify"] + opts
+  return qa_utils.TestSetISpecs(new_specs, get_policy_fn=_GetClusterIPolicy,
+                                build_cmd_fn=build_cmd, fail=fail,
+                                old_values=old_values)
+
+
+def TestClusterModifyISpecs():
+  """gnt-cluster modify --specs-*"""
+  params = ["memory-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)
+
+    # Get the ipolicy command
+    mnode = qa_config.GetMasterNode()
+    initcmd = GetCommandOutput(mnode.primary, "gnt-cluster show-ispecs-cmd")
+    modcmd = ["gnt-cluster", "modify"]
+    opts = initcmd.split()
+    assert opts[0:2] == ["gnt-cluster", "init"]
+    for k in range(2, len(opts) - 1):
+      if opts[k].startswith("--ipolicy-"):
+        assert k + 2 <= len(opts)
+        modcmd.extend(opts[k:k + 2])
+    # Re-apply the ipolicy (this should be a no-op)
+    AssertCommand(modcmd)
+    new_initcmd = GetCommandOutput(mnode.primary, "gnt-cluster show-ispecs-cmd")
+    AssertEqual(initcmd, new_initcmd)
+
+
 def TestClusterInfo():
   """gnt-cluster info"""
   AssertCommand(["gnt-cluster", "info"])
 def TestClusterInfo():
   """gnt-cluster info"""
   AssertCommand(["gnt-cluster", "info"])
@@ -362,8 +710,8 @@ def TestClusterRenewCrypto():
          "--rapi-certificate=/dev/null"]
   AssertCommand(cmd, fail=True)
 
          "--rapi-certificate=/dev/null"]
   AssertCommand(cmd, fail=True)
 
-  rapi_cert_backup = qa_utils.BackupFile(master["primary"],
-                                         constants.RAPI_CERT_FILE)
+  rapi_cert_backup = qa_utils.BackupFile(master.primary,
+                                         pathutils.RAPI_CERT_FILE)
   try:
     # Custom RAPI certificate
     fh = tempfile.NamedTemporaryFile()
   try:
     # Custom RAPI certificate
     fh = tempfile.NamedTemporaryFile()
@@ -373,7 +721,7 @@ def TestClusterRenewCrypto():
 
     utils.GenerateSelfSignedSslCert(fh.name, validity=validity)
 
 
     utils.GenerateSelfSignedSslCert(fh.name, validity=validity)
 
-    tmpcert = qa_utils.UploadFile(master["primary"], fh.name)
+    tmpcert = qa_utils.UploadFile(master.primary, fh.name)
     try:
       AssertCommand(["gnt-cluster", "renew-crypto", "--force",
                      "--rapi-certificate=%s" % tmpcert])
     try:
       AssertCommand(["gnt-cluster", "renew-crypto", "--force",
                      "--rapi-certificate=%s" % tmpcert])
@@ -386,7 +734,7 @@ def TestClusterRenewCrypto():
     cds_fh.write("\n")
     cds_fh.flush()
 
     cds_fh.write("\n")
     cds_fh.flush()
 
-    tmpcds = qa_utils.UploadFile(master["primary"], cds_fh.name)
+    tmpcds = qa_utils.UploadFile(master.primary, cds_fh.name)
     try:
       AssertCommand(["gnt-cluster", "renew-crypto", "--force",
                      "--cluster-domain-secret=%s" % tmpcds])
     try:
       AssertCommand(["gnt-cluster", "renew-crypto", "--force",
                      "--cluster-domain-secret=%s" % tmpcds])
@@ -410,7 +758,7 @@ def TestClusterBurnin():
   master = qa_config.GetMasterNode()
 
   options = qa_config.get("options", {})
   master = qa_config.GetMasterNode()
 
   options = qa_config.get("options", {})
-  disk_template = options.get("burnin-disk-template", "drbd")
+  disk_template = options.get("burnin-disk-template", constants.DT_DRBD8)
   parallel = options.get("burnin-in-parallel", False)
   check_inst = options.get("burnin-check-instances", False)
   do_rename = options.get("burnin-rename", "")
   parallel = options.get("burnin-in-parallel", False)
   check_inst = options.get("burnin-check-instances", False)
   do_rename = options.get("burnin-rename", "")
@@ -430,15 +778,16 @@ def TestClusterBurnin():
     if len(instances) < 1:
       raise qa_error.Error("Burnin needs at least one instance")
 
     if len(instances) < 1:
       raise qa_error.Error("Burnin needs at least one instance")
 
-    script = qa_utils.UploadFile(master["primary"], "../tools/burnin")
+    script = qa_utils.UploadFile(master.primary, "../tools/burnin")
     try:
     try:
+      disks = qa_config.GetDiskOptions()
       # Run burnin
       cmd = [script,
              "--os=%s" % qa_config.get("os"),
              "--minmem-size=%s" % qa_config.get(constants.BE_MINMEM),
              "--maxmem-size=%s" % qa_config.get(constants.BE_MAXMEM),
       # Run burnin
       cmd = [script,
              "--os=%s" % qa_config.get("os"),
              "--minmem-size=%s" % qa_config.get(constants.BE_MINMEM),
              "--maxmem-size=%s" % qa_config.get(constants.BE_MAXMEM),
-             "--disk-size=%s" % ",".join(qa_config.get("disk")),
-             "--disk-growth=%s" % ",".join(qa_config.get("disk-growth")),
+             "--disk-size=%s" % ",".join([d.get("size") for d in disks]),
+             "--disk-growth=%s" % ",".join([d.get("growth") for d in disks]),
              "--disk-template=%s" % disk_template]
       if parallel:
         cmd.append("--parallel")
              "--disk-template=%s" % disk_template]
       if parallel:
         cmd.append("--parallel")
@@ -451,14 +800,14 @@ def TestClusterBurnin():
         cmd.append("--no-reboot")
       else:
         cmd.append("--reboot-types=%s" % ",".join(reboot_types))
         cmd.append("--no-reboot")
       else:
         cmd.append("--reboot-types=%s" % ",".join(reboot_types))
-      cmd += [inst["name"] for inst in instances]
+      cmd += [inst.name for inst in instances]
       AssertCommand(cmd)
     finally:
       AssertCommand(["rm", "-f", script])
 
   finally:
     for inst in instances:
       AssertCommand(cmd)
     finally:
       AssertCommand(["rm", "-f", script])
 
   finally:
     for inst in instances:
-      qa_config.ReleaseInstance(inst)
+      inst.Release()
 
 
 def TestClusterMasterFailover():
 
 
 def TestClusterMasterFailover():
@@ -472,37 +821,51 @@ def TestClusterMasterFailover():
     # Back to original master node
     AssertCommand(cmd, node=master)
   finally:
     # Back to original master node
     AssertCommand(cmd, node=master)
   finally:
-    qa_config.ReleaseNode(failovermaster)
+    failovermaster.Release()
+
+
+def _NodeQueueDrainFile(node):
+  """Returns path to queue drain file for a node.
+
+  """
+  return qa_utils.MakeNodePath(node, pathutils.JOB_QUEUE_DRAIN_FILE)
+
+
+def _AssertDrainFile(node, **kwargs):
+  """Checks for the queue drain file.
+
+  """
+  AssertCommand(["test", "-f", _NodeQueueDrainFile(node)], node=node, **kwargs)
 
 
 def TestClusterMasterFailoverWithDrainedQueue():
   """gnt-cluster master-failover with drained queue"""
 
 
 def TestClusterMasterFailoverWithDrainedQueue():
   """gnt-cluster master-failover with drained queue"""
-  drain_check = ["test", "-f", constants.JOB_QUEUE_DRAIN_FILE]
-
   master = qa_config.GetMasterNode()
   failovermaster = qa_config.AcquireNode(exclude=master)
 
   # Ensure queue is not drained
   for node in [master, failovermaster]:
   master = qa_config.GetMasterNode()
   failovermaster = qa_config.AcquireNode(exclude=master)
 
   # Ensure queue is not drained
   for node in [master, failovermaster]:
-    AssertCommand(drain_check, node=node, fail=True)
+    _AssertDrainFile(node, fail=True)
 
   # Drain queue on failover master
 
   # Drain queue on failover master
-  AssertCommand(["touch", constants.JOB_QUEUE_DRAIN_FILE], node=failovermaster)
+  AssertCommand(["touch", _NodeQueueDrainFile(failovermaster)],
+                node=failovermaster)
 
   cmd = ["gnt-cluster", "master-failover"]
   try:
 
   cmd = ["gnt-cluster", "master-failover"]
   try:
-    AssertCommand(drain_check, node=failovermaster)
+    _AssertDrainFile(failovermaster)
     AssertCommand(cmd, node=failovermaster)
     AssertCommand(cmd, node=failovermaster)
-    AssertCommand(drain_check, fail=True)
-    AssertCommand(drain_check, node=failovermaster, fail=True)
+    _AssertDrainFile(master, fail=True)
+    _AssertDrainFile(failovermaster, fail=True)
 
     # Back to original master node
     AssertCommand(cmd, node=master)
   finally:
 
     # Back to original master node
     AssertCommand(cmd, node=master)
   finally:
-    qa_config.ReleaseNode(failovermaster)
+    failovermaster.Release()
 
 
-  AssertCommand(drain_check, fail=True)
-  AssertCommand(drain_check, node=failovermaster, fail=True)
+  # Ensure queue is not drained
+  for node in [master, failovermaster]:
+    _AssertDrainFile(node, fail=True)
 
 
 def TestClusterCopyfile():
 
 
 def TestClusterCopyfile():
@@ -518,7 +881,7 @@ def TestClusterCopyfile():
   f.seek(0)
 
   # Upload file to master node
   f.seek(0)
 
   # Upload file to master node
-  testname = qa_utils.UploadFile(master["primary"], f.name)
+  testname = qa_utils.UploadFile(master.primary, f.name)
   try:
     # Copy file to all nodes
     AssertCommand(["gnt-cluster", "copyfile", testname])
   try:
     # Copy file to all nodes
     AssertCommand(["gnt-cluster", "copyfile", testname])
@@ -550,3 +913,42 @@ def TestClusterDestroy():
 def TestClusterRepairDiskSizes():
   """gnt-cluster repair-disk-sizes"""
   AssertCommand(["gnt-cluster", "repair-disk-sizes"])
 def TestClusterRepairDiskSizes():
   """gnt-cluster repair-disk-sizes"""
   AssertCommand(["gnt-cluster", "repair-disk-sizes"])
+
+
+def TestSetExclStorCluster(newvalue):
+  """Set the exclusive_storage node parameter at the cluster level.
+
+  @type newvalue: bool
+  @param newvalue: New value of exclusive_storage
+  @rtype: bool
+  @return: The old value of exclusive_storage
+
+  """
+  es_path = ["Default node parameters", "exclusive_storage"]
+  oldvalue = _GetClusterField(es_path)
+  AssertCommand(["gnt-cluster", "modify", "--node-parameters",
+                 "exclusive_storage=%s" % newvalue])
+  effvalue = _GetClusterField(es_path)
+  if effvalue != newvalue:
+    raise qa_error.Error("exclusive_storage has the wrong value: %s instead"
+                         " of %s" % (effvalue, newvalue))
+  qa_config.SetExclusiveStorage(newvalue)
+  return oldvalue
+
+
+def TestExclStorSharedPv(node):
+  """cluster-verify reports LVs that share the same PV with exclusive_storage.
+
+  """
+  vgname = qa_config.get("vg-name", constants.DEFAULT_VG)
+  lvname1 = _QA_LV_PREFIX + "vol1"
+  lvname2 = _QA_LV_PREFIX + "vol2"
+  node_name = node.primary
+  AssertCommand(["lvcreate", "-L1G", "-n", lvname1, vgname], node=node_name)
+  AssertClusterVerify(fail=True, errors=[constants.CV_ENODEORPHANLV])
+  AssertCommand(["lvcreate", "-L1G", "-n", lvname2, vgname], node=node_name)
+  AssertClusterVerify(fail=True, errors=[constants.CV_ENODELVM,
+                                         constants.CV_ENODEORPHANLV])
+  AssertCommand(["lvremove", "-f", "/".join([vgname, lvname1])], node=node_name)
+  AssertCommand(["lvremove", "-f", "/".join([vgname, lvname2])], node=node_name)
+  AssertClusterVerify()