Add QA for policy-instance interactions
authorBernardo Dal Seno <bdalseno@google.com>
Fri, 22 Feb 2013 18:07:33 +0000 (19:07 +0100)
committerBernardo Dal Seno <bdalseno@google.com>
Mon, 11 Mar 2013 18:57:37 +0000 (19:57 +0100)
Violations on policy changes are checked.

Signed-off-by: Bernardo Dal Seno <bdalseno@google.com>
Reviewed-by: Guido Trotter <ultrotter@google.com>

qa/ganeti-qa.py
qa/qa-sample.json
qa/qa_cluster.py
qa/qa_instance.py

index 34b3b3a..8c3f459 100755 (executable)
@@ -484,6 +484,48 @@ def RunExclusiveStorageTests():
     qa_config.ReleaseNode(node)
 
 
+def _BuildSpecDict(par, mn, st, mx):
+  return {par: {"min": mn, "std": st, "max": mx}}
+
+
+def TestIPolicyPlainInstance():
+  """Test instance policy interaction with instances"""
+  params = ["mem-size", "cpu-count", "disk-count", "disk-size", "nic-count"]
+  if not qa_config.IsTemplateSupported(constants.DT_PLAIN):
+    print "Template %s not supported" % constants.DT_PLAIN
+    return
+
+  # This test assumes that the group policy is empty
+  (_, old_specs) = qa_cluster.TestClusterSetISpecs({})
+  node = qa_config.AcquireNode()
+  try:
+    instance = qa_instance.TestInstanceAddWithPlainDisk([node])
+    try:
+      policyerror = [constants.CV_EINSTANCEPOLICY]
+      for par in params:
+        qa_cluster.AssertClusterVerify()
+        (iminval, imaxval) = qa_instance.GetInstanceSpec(instance["name"], par)
+        # Some specs must be multiple of 4
+        new_spec = _BuildSpecDict(par, imaxval + 4, imaxval + 4, imaxval + 4)
+        qa_cluster.TestClusterSetISpecs(new_spec)
+        qa_cluster.AssertClusterVerify(warnings=policyerror)
+        if iminval > 0:
+          # Some specs must be multiple of 4
+          if iminval >= 4:
+            upper = iminval - 4
+          else:
+            upper = iminval - 1
+          new_spec = _BuildSpecDict(par, 0, upper, upper)
+          qa_cluster.TestClusterSetISpecs(new_spec)
+          qa_cluster.AssertClusterVerify(warnings=policyerror)
+        qa_cluster.TestClusterSetISpecs(old_specs)
+      qa_instance.TestInstanceRemove(instance)
+    finally:
+      qa_config.ReleaseInstance(instance)
+  finally:
+    qa_config.ReleaseNode(node)
+
+
 def RunInstanceTests():
   """Create and exercise instances."""
   instance_tests = [
@@ -612,6 +654,8 @@ def RunQa():
     qa_config.ReleaseNode(pnode)
 
   RunExclusiveStorageTests()
+  RunTestIf(["cluster-instance-policy", "instance-add-plain-disk"],
+            TestIPolicyPlainInstance)
 
   # Test removing instance with offline drbd secondary
   if qa_config.TestEnabled("instance-remove-drbd-offline"):
index 501da9d..ef27e00 100644 (file)
     "cluster-redist-conf": true,
     "cluster-repair-disk-sizes": true,
     "cluster-exclusive-storage": true,
+    "cluster-instance-policy": true,
 
     "haskell-confd": true,
     "htools": true,
index 6ade74c..543b774 100644 (file)
@@ -96,20 +96,33 @@ def _GetBoolClusterField(field):
 
 
 # Cluster-verify errors (date, "ERROR", then error code)
-_CVERROR_RE = re.compile(r"^[\w\s:]+\s+- ERROR:([A-Z0-9_-]+):")
+_CVERROR_RE = re.compile(r"^[\w\s:]+\s+- (ERROR|WARNING):([A-Z0-9_-]+):")
 
 
 def _GetCVErrorCodes(cvout):
-  ret = set()
+  errs = set()
+  warns = set()
   for l in cvout.splitlines():
     m = _CVERROR_RE.match(l)
     if m:
-      ecode = m.group(1)
-      ret.add(ecode)
-  return ret
+      etype = m.group(1)
+      ecode = m.group(2)
+      if etype == "ERROR":
+        errs.add(ecode)
+      elif etype == "WARNING":
+        warns.add(ecode)
+  return (errs, warns)
 
 
-def AssertClusterVerify(fail=False, errors=None):
+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
@@ -118,19 +131,20 @@ def AssertClusterVerify(fail=False, errors=None):
   @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:
+  if errors or warnings:
     cvout = GetCommandOutput(mnode["primary"], cvcmd + " --error-codes",
-                             fail=True)
-    actual = _GetCVErrorCodes(cvout)
-    expected = compat.UniqueFrozenset(e for (_, e, _) in errors)
-    if not actual.issuperset(expected):
-      missing = expected.difference(actual)
-      raise qa_error.Error("Cluster-verify didn't return these expected"
-                           " errors: %s" % utils.CommaJoin(missing))
+                             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)
 
index bcc464a..ea8a2a7 100644 (file)
@@ -153,19 +153,33 @@ def _DestroyInstanceVolumes(instance):
     AssertCommand(["lvremove", "-f"] + vols, node=node)
 
 
-def _GetBoolInstanceField(instance, field):
-  """Get the Boolean value of a field of an instance.
+def _GetInstanceField(instance, field):
+  """Get the value of a field of an instance.
 
   @type instance: string
   @param instance: Instance name
   @type field: string
   @param field: Name of the field
+  @rtype: string
 
   """
   master = qa_config.GetMasterNode()
   infocmd = utils.ShellQuoteArgs(["gnt-instance", "list", "--no-headers",
-                                  "-o", field, instance])
-  info_out = qa_utils.GetCommandOutput(master["primary"], infocmd).strip()
+                                  "--units", "m", "-o", field, instance])
+  return qa_utils.GetCommandOutput(master["primary"], infocmd).strip()
+
+
+def _GetBoolInstanceField(instance, field):
+  """Get the Boolean value of a field of an instance.
+
+  @type instance: string
+  @param instance: Instance name
+  @type field: string
+  @param field: Name of the field
+  @rtype: bool
+
+  """
+  info_out = _GetInstanceField(instance, field)
   if info_out == "Y":
     return True
   elif info_out == "N":
@@ -175,6 +189,59 @@ def _GetBoolInstanceField(instance, field):
                          " %s" % (field, instance, info_out))
 
 
+def _GetNumInstanceField(instance, field):
+  """Get a numeric value of a field of an instance.
+
+  @type instance: string
+  @param instance: Instance name
+  @type field: string
+  @param field: Name of the field
+  @rtype: int or float
+
+  """
+  info_out = _GetInstanceField(instance, field)
+  try:
+    ret = int(info_out)
+  except ValueError:
+    try:
+      ret = float(info_out)
+    except ValueError:
+      raise qa_error.Error("Field %s of instance %s has a non-numeric value:"
+                           " %s" % (field, instance, info_out))
+  return ret
+
+
+def GetInstanceSpec(instance, spec):
+  """Return the current spec for the given parameter.
+
+  @type instance: string
+  @param instance: Instance name
+  @type spec: string
+  @param spec: one of the supported parameters: "mem-size", "cpu-count",
+      "disk-count", "disk-size", "nic-count"
+  @rtype: tuple
+  @return: (minspec, maxspec); minspec and maxspec can be different only for
+      memory and disk size
+
+  """
+  specmap = {
+    "mem-size": ["be/minmem", "be/maxmem"],
+    "cpu-count": ["vcpus"],
+    "disk-count": ["disk.count"],
+    "disk-size": ["disk.size/ "],
+    "nic-count": ["nic.count"],
+    }
+  # For disks, first we need the number of disks
+  if spec == "disk-size":
+    (numdisk, _) = GetInstanceSpec(instance, "disk-count")
+    fields = ["disk.size/%s" % k for k in range(0, numdisk)]
+  else:
+    assert spec in specmap, "%s not in %s" % (spec, specmap)
+    fields = specmap[spec]
+  values = [_GetNumInstanceField(instance, f) for f in fields]
+  return (min(values), max(values))
+
+
 def IsFailoverSupported(instance):
   templ = qa_config.GetInstanceTemplate(instance)
   return templ in constants.DTS_MIRRORED