From c9e05005c62805eed688dd7024a4b19983b700e3 Mon Sep 17 00:00:00 2001
From: Michael Hanselmann <hansmi@google.com>
Date: Thu, 10 May 2012 11:44:02 +0200
Subject: [PATCH] QA: Infrastructure for hook script to check instance status

This script can be used to check if an instance is running or stopped at
various points during a QA run. Environment variables are used to pass
the most essential information.

Signed-off-by: Michael Hanselmann <hansmi@google.com>
Reviewed-by: Iustin Pop <iustin@google.com>
---
 NEWS              |   1 +
 qa/qa-sample.json |   3 ++
 qa/qa_config.py   |  19 +++++++++
 qa/qa_utils.py    | 104 ++++++++++++++++++++++++++++++++++++++++++++++
 4 files changed, 127 insertions(+)

diff --git a/NEWS b/NEWS
index b14e99232..37527ebe1 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 3deeda37d..d2c5f1a9a 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 5e19c5eab..48a8a11fa 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 f0ef63cc2..d772302c6 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
-- 
GitLab