diff --git a/qa/ganeti-qa.py b/qa/ganeti-qa.py index 34b3b3adc3a6b4d1807a60e96e78db0a237a41e9..8c3f4596d8bb5b24afd6ddbf58320660d0cf0f3e 100755 --- a/qa/ganeti-qa.py +++ b/qa/ganeti-qa.py @@ -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"): diff --git a/qa/qa-sample.json b/qa/qa-sample.json index 501da9dd8bbd133600a7493804dfdc9ccdf1471f..ef27e00185303696708e63dbc3729e77e6aa293f 100644 --- a/qa/qa-sample.json +++ b/qa/qa-sample.json @@ -119,6 +119,7 @@ "cluster-redist-conf": true, "cluster-repair-disk-sizes": true, "cluster-exclusive-storage": true, + "cluster-instance-policy": true, "haskell-confd": true, "htools": true, diff --git a/qa/qa_cluster.py b/qa/qa_cluster.py index 6ade74cf125b6fc384c05e028b20f38bc49b8dd8..543b77448ad1fa2ec87375aab55a5aa130612ba9 100644 --- a/qa/qa_cluster.py +++ b/qa/qa_cluster.py @@ -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) diff --git a/qa/qa_instance.py b/qa/qa_instance.py index bcc464a07b5f42ad94a062c57f06f0a6faa6b583..ea8a2a7bf43fa2a0caa6d7adfb677d0e2231e6c6 100644 --- a/qa/qa_instance.py +++ b/qa/qa_instance.py @@ -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