diff --git a/NEWS b/NEWS index b14e9923212324dd75503295047a9ec1483062b8..37527ebe1933875d9f13b495aec4607e44403bef 100644 --- a/NEWS +++ b/NEWS @@ -23,6 +23,7 @@ Version 2.6.0 beta1 - Deprecation warnings due to pycrypto/paramiko import in tools/setup-ssh have been silenced, as usually they are safe; please make sure to run an up-to-date paramiko version +- The QA scripts now depend on Python 2.5 or above Version 2.5.0 diff --git a/qa/qa-sample.json b/qa/qa-sample.json index 3deeda37d6dd1aff4a85e5f285065de66ccaf589..d2c5f1a9a33bec100404f210b0498d88db085316 100644 --- a/qa/qa-sample.json +++ b/qa/qa-sample.json @@ -24,6 +24,9 @@ "disk": ["1G", "512M"], "disk-growth": ["2G", "768M"], + "# Script to check instance status": null, + "instance-check": null, + "nodes": [ { "# Master node": null, diff --git a/qa/qa_config.py b/qa/qa_config.py index 5e19c5eab60094ebf2b4998965d1fb67df793977..48a8a11fa4ccf6128ace5d88a5b141e28b50d501 100644 --- a/qa/qa_config.py +++ b/qa/qa_config.py @@ -23,6 +23,7 @@ """ +import os from ganeti import utils from ganeti import serializer @@ -31,6 +32,9 @@ from ganeti import compat import qa_error +_INSTANCE_CHECK_KEY = "instance-check" + + cfg = None options = None @@ -55,6 +59,14 @@ def Validate(): raise qa_error.Error("Config options 'disk' and 'disk-growth' must have" " the same number of items") + check = GetInstanceCheckScript() + if check: + try: + os.stat(check) + except EnvironmentError, err: + raise qa_error.Error("Can't find instance check script '%s': %s" % + (check, err)) + def get(name, default=None): return cfg.get(name, default) @@ -135,6 +147,13 @@ def TestEnabled(tests, _cfg=None): tests, compat.all) +def GetInstanceCheckScript(): + """Returns path to instance check script or C{None}. + + """ + return cfg.get(_INSTANCE_CHECK_KEY, None) + + def GetMasterNode(): return cfg["nodes"][0] diff --git a/qa/qa_utils.py b/qa/qa_utils.py index f0ef63cc2b7ed78defaf2f9f623f42645ed0a127..d772302c6825b8d23f23c7559386650d07c4ebf9 100644 --- a/qa/qa_utils.py +++ b/qa/qa_utils.py @@ -30,9 +30,15 @@ import subprocess import random import tempfile +try: + import functools +except ImportError, err: + raise ImportError("Python 2.5 or higher is required: %s" % err) + from ganeti import utils from ganeti import compat from ganeti import constants +from ganeti import ht import qa_config import qa_error @@ -45,6 +51,16 @@ _RESET_SEQ = None _MULTIPLEXERS = {} +#: Unique ID per QA run +_RUN_UUID = utils.NewUUID() + + +(INST_DOWN, + INST_UP) = range(500, 502) + +(FIRST_ARG, + RETURN_VALUE) = range(1000, 1002) + def _SetupColours(): """Initializes the colour constants. @@ -522,3 +538,91 @@ def RemoveFromEtcHosts(hostnames): quoted_tmp_hosts)) except qa_error.Error: AssertCommand(["rm", tmp_hosts]) + + +def RunInstanceCheck(instance, running): + """Check if instance is running or not. + + """ + script = qa_config.GetInstanceCheckScript() + if not script: + return + + master_node = qa_config.GetMasterNode() + instance_name = instance["name"] + + # Build command to connect to master node + master_ssh = GetSSHCommand(master_node["primary"], "--") + + if running: + running_shellval = "1" + running_text = "" + else: + running_shellval = "" + running_text = "not " + + print FormatInfo("Checking if instance '%s' is %srunning" % + (instance_name, running_text)) + + args = [script, instance_name] + env = { + "PATH": constants.HOOKS_PATH, + "RUN_UUID": _RUN_UUID, + "MASTER_SSH": utils.ShellQuoteArgs(master_ssh), + "INSTANCE_NAME": instance_name, + "INSTANCE_RUNNING": running_shellval, + } + + result = os.spawnve(os.P_WAIT, script, args, env) + if result != 0: + raise qa_error.Error("Instance check failed with result %s" % result) + + +_TInstCheck = ht.TStrictDict(False, False, { + "name": ht.TNonEmptyString, + }) + + +def _InstanceCheckInner(expected, instarg, args, result): + """Helper function used by L{InstanceCheck}. + + """ + if instarg == FIRST_ARG: + instance = args[0] + elif instarg == RETURN_VALUE: + instance = result + else: + raise Exception("Invalid value '%s' for instance argument" % instarg) + + if expected in (INST_DOWN, INST_UP): + if not _TInstCheck(instance): + raise Exception("Invalid instance: %s" % instance) + + RunInstanceCheck(instance, (expected == INST_UP)) + elif expected is not None: + raise Exception("Invalid value '%s'" % expected) + + +def InstanceCheck(before, after, instarg): + """Decorator to check instance status before and after test. + + @param before: L{INST_DOWN} if instance must be stopped before test, + L{INST_UP} if instance must be running before test, L{None} to not check. + @param after: L{INST_DOWN} if instance must be stopped after test, + L{INST_UP} if instance must be running after test, L{None} to not check. + @param instarg: L{FIRST_ARG} to use first argument to test as instance (a + dictionary), L{RETURN_VALUE} to use return value (disallows pre-checks) + + """ + def decorator(fn): + @functools.wraps(fn) + def wrapper(*args, **kwargs): + _InstanceCheckInner(before, instarg, args, NotImplemented) + + result = fn(*args, **kwargs) + + _InstanceCheckInner(after, instarg, args, result) + + return result + return wrapper + return decorator