QA: Update assertion for diskless instances
[ganeti-local] / qa / qa_utils.py
index 9a67a0a..e842b15 100644 (file)
@@ -29,6 +29,7 @@ import sys
 import subprocess
 import random
 import tempfile
+import operator
 
 try:
   import functools
@@ -40,6 +41,7 @@ from ganeti import compat
 from ganeti import constants
 from ganeti import ht
 from ganeti import pathutils
+from ganeti import vcluster
 
 import qa_config
 import qa_error
@@ -55,6 +57,9 @@ _MULTIPLEXERS = {}
 #: Unique ID per QA run
 _RUN_UUID = utils.NewUUID()
 
+#: Path to the QA query output log file
+_QA_OUTPUT = pathutils.GetLogFilename("qa-output")
+
 
 (INST_DOWN,
  INST_UP) = range(500, 502)
@@ -126,21 +131,17 @@ def AssertMatch(string, pattern):
     raise qa_error.Error("%r doesn't match /%r/" % (string, pattern))
 
 
-def _GetName(entity, key):
+def _GetName(entity, fn):
   """Tries to get name of an entity.
 
   @type entity: string or dict
-  @type key: string
-  @param key: Dictionary key containing name
+  @param fn: Function retrieving name from entity
 
   """
   if isinstance(entity, basestring):
     result = entity
-  elif isinstance(entity, dict):
-    result = entity[key]
   else:
-    raise qa_error.Error("Expected string or dictionary, got %s: %s" %
-                         (type(entity), entity))
+    result = fn(entity)
 
   if not ht.TNonEmptyString(result):
     raise Exception("Invalid name '%s'" % result)
@@ -160,7 +161,7 @@ def _AssertRetCode(rcode, fail, cmdstr, nodename):
                          (cmdstr, nodename, rcode))
 
 
-def AssertCommand(cmd, fail=False, node=None):
+def AssertCommand(cmd, fail=False, node=None, log_cmd=True):
   """Checks that a remote command succeeds.
 
   @param cmd: either a string (the command to execute) or a list (to
@@ -170,6 +171,8 @@ def AssertCommand(cmd, fail=False, node=None):
   @param node: if passed, it should be the node on which the command
       should be executed, instead of the master node (can be either a
       dict or a string)
+  @param log_cmd: if False, the command won't be logged (simply passed to
+      StartSSH)
   @return: the return code of the command
   @raise qa_error.Error: if the command fails when it shouldn't or vice versa
 
@@ -177,19 +180,40 @@ def AssertCommand(cmd, fail=False, node=None):
   if node is None:
     node = qa_config.GetMasterNode()
 
-  nodename = _GetName(node, "primary")
+  nodename = _GetName(node, operator.attrgetter("primary"))
 
   if isinstance(cmd, basestring):
     cmdstr = cmd
   else:
     cmdstr = utils.ShellQuoteArgs(cmd)
 
-  rcode = StartSSH(nodename, cmdstr).wait()
+  rcode = StartSSH(nodename, cmdstr, log_cmd=log_cmd).wait()
   _AssertRetCode(rcode, fail, cmdstr, nodename)
 
   return rcode
 
 
+def AssertRedirectedCommand(cmd, fail=False, node=None, log_cmd=True):
+  """Executes a command with redirected output.
+
+  The log will go to the qa-output log file in the ganeti log
+  directory on the node where the command is executed. The fail and
+  node parameters are passed unchanged to AssertCommand.
+
+  @param cmd: the command to be executed, as a list; a string is not
+      supported
+
+  """
+  if not isinstance(cmd, list):
+    raise qa_error.Error("Non-list passed to AssertRedirectedCommand")
+  ofile = utils.ShellQuote(_QA_OUTPUT)
+  cmdstr = utils.ShellQuoteArgs(cmd)
+  AssertCommand("echo ---- $(date) %s ---- >> %s" % (cmdstr, ofile),
+                fail=False, node=node, log_cmd=False)
+  return AssertCommand(cmdstr + " >> %s" % ofile,
+                       fail=fail, node=node, log_cmd=log_cmd)
+
+
 def GetSSHCommand(node, cmd, strict=True, opts=None, tty=None):
   """Builds SSH command to be executed.
 
@@ -227,31 +251,48 @@ def GetSSHCommand(node, cmd, strict=True, opts=None, tty=None):
     spath = _MULTIPLEXERS[node][0]
     args.append("-oControlPath=%s" % spath)
     args.append("-oControlMaster=no")
-  args.append(node)
-  if cmd:
-    args.append(cmd)
+
+  (vcluster_master, vcluster_basedir) = \
+    qa_config.GetVclusterSettings()
+
+  if vcluster_master:
+    args.append(vcluster_master)
+    args.append("%s/%s/cmd" % (vcluster_basedir, node))
+
+    if cmd:
+      # For virtual clusters the whole command must be wrapped using the "cmd"
+      # script, as that script sets a number of environment variables. If the
+      # command contains shell meta characters the whole command needs to be
+      # quoted.
+      args.append(utils.ShellQuote(cmd))
+  else:
+    args.append(node)
+
+    if cmd:
+      args.append(cmd)
 
   return args
 
 
-def StartLocalCommand(cmd, _nolog_opts=False, **kwargs):
+def StartLocalCommand(cmd, _nolog_opts=False, log_cmd=True, **kwargs):
   """Starts a local command.
 
   """
-  if _nolog_opts:
-    pcmd = [i for i in cmd if not i.startswith("-")]
-  else:
-    pcmd = cmd
-  print "Command: %s" % utils.ShellQuoteArgs(pcmd)
+  if log_cmd:
+    if _nolog_opts:
+      pcmd = [i for i in cmd if not i.startswith("-")]
+    else:
+      pcmd = cmd
+    print "Command: %s" % utils.ShellQuoteArgs(pcmd)
   return subprocess.Popen(cmd, shell=False, **kwargs)
 
 
-def StartSSH(node, cmd, strict=True):
+def StartSSH(node, cmd, strict=True, log_cmd=True):
   """Starts SSH.
 
   """
   return StartLocalCommand(GetSSHCommand(node, cmd, strict=strict),
-                           _nolog_opts=True)
+                           _nolog_opts=True, log_cmd=log_cmd)
 
 
 def StartMultiplexer(node):
@@ -298,7 +339,7 @@ def GetCommandOutput(node, cmd, tty=None, fail=False):
   p = StartLocalCommand(GetSSHCommand(node, cmd, tty=tty),
                         stdout=subprocess.PIPE)
   rcode = p.wait()
-  _AssertRetCode(rcode, fail, node, cmd)
+  _AssertRetCode(rcode, fail, cmd, node)
   return p.stdout.read()
 
 
@@ -362,13 +403,19 @@ def BackupFile(node, path):
   anymore.
 
   """
+  vpath = MakeNodePath(node, path)
+
   cmd = ("tmp=$(tempfile --prefix .gnt --directory=$(dirname %s)) && "
          "[[ -f \"$tmp\" ]] && "
          "cp %s $tmp && "
-         "echo $tmp") % (utils.ShellQuote(path), utils.ShellQuote(path))
+         "echo $tmp") % (utils.ShellQuote(vpath), utils.ShellQuote(vpath))
 
   # Return temporary filename
-  return GetCommandOutput(node, cmd).strip()
+  result = GetCommandOutput(node, cmd).strip()
+
+  print "Backup filename: %s" % result
+
+  return result
 
 
 def _ResolveName(cmd, key):
@@ -377,7 +424,7 @@ def _ResolveName(cmd, key):
   """
   master = qa_config.GetMasterNode()
 
-  output = GetCommandOutput(master["primary"], utils.ShellQuoteArgs(cmd))
+  output = GetCommandOutput(master.primary, utils.ShellQuoteArgs(cmd))
   for line in output.splitlines():
     (lkey, lvalue) = line.split(":", 1)
     if lkey == key:
@@ -400,7 +447,7 @@ def ResolveNodeName(node):
   """Gets the full name of a node.
 
   """
-  return _ResolveName(["gnt-node", "info", node["primary"]],
+  return _ResolveName(["gnt-node", "info", node.primary],
                       "Node name")
 
 
@@ -414,7 +461,7 @@ def GetNodeInstances(node, secondaries=False):
   # Get list of all instances
   cmd = ["gnt-instance", "list", "--separator=:", "--no-headers",
          "--output=name,pnode,snodes"]
-  output = GetCommandOutput(master["primary"], utils.ShellQuoteArgs(cmd))
+  output = GetCommandOutput(master.primary, utils.ShellQuoteArgs(cmd))
 
   instances = []
   for line in output.splitlines():
@@ -458,7 +505,7 @@ def _List(listcmd, fields, names):
   if names:
     cmd.extend(names)
 
-  return GetCommandOutput(master["primary"],
+  return GetCommandOutput(master.primary,
                           utils.ShellQuoteArgs(cmd)).splitlines()
 
 
@@ -476,7 +523,7 @@ def GenericQueryTest(cmd, fields, namefield="name", test_unknown=True):
 
   # Test a number of field combinations
   for testfields in _SelectQueryFields(rnd, fields):
-    AssertCommand([cmd, "list", "--output", ",".join(testfields)])
+    AssertRedirectedCommand([cmd, "list", "--output", ",".join(testfields)])
 
   if namefield is not None:
     namelist_fn = compat.partial(_List, cmd, [namefield])
@@ -499,8 +546,9 @@ def GenericQueryTest(cmd, fields, namefield="name", test_unknown=True):
                   fail=True)
 
   # Check exit code for listing unknown field
-  AssertEqual(AssertCommand([cmd, "list", "--output=field/does/not/exist"],
-                            fail=True),
+  AssertEqual(AssertRedirectedCommand([cmd, "list",
+                                       "--output=field/does/not/exist"],
+                                      fail=True),
               constants.EXIT_UNKNOWN_FIELD)
 
 
@@ -508,12 +556,12 @@ def GenericQueryFieldsTest(cmd, fields):
   master = qa_config.GetMasterNode()
 
   # Listing fields
-  AssertCommand([cmd, "list-fields"])
-  AssertCommand([cmd, "list-fields"] + fields)
+  AssertRedirectedCommand([cmd, "list-fields"])
+  AssertRedirectedCommand([cmd, "list-fields"] + fields)
 
   # Check listed fields (all, must be sorted)
   realcmd = [cmd, "list-fields", "--separator=|", "--no-headers"]
-  output = GetCommandOutput(master["primary"],
+  output = GetCommandOutput(master.primary,
                             utils.ShellQuoteArgs(realcmd)).splitlines()
   AssertEqual([line.split("|", 1)[0] for line in output],
               utils.NiceSort(fields))
@@ -542,7 +590,7 @@ def AddToEtcHosts(hostnames):
 
   """
   master = qa_config.GetMasterNode()
-  tmp_hosts = UploadData(master["primary"], "", mode=0644)
+  tmp_hosts = UploadData(master.primary, "", mode=0644)
 
   data = []
   for localhost in ("::1", "127.0.0.1"):
@@ -567,7 +615,7 @@ def RemoveFromEtcHosts(hostnames):
 
   """
   master = qa_config.GetMasterNode()
-  tmp_hosts = UploadData(master["primary"], "", mode=0644)
+  tmp_hosts = UploadData(master.primary, "", mode=0644)
   quoted_tmp_hosts = utils.ShellQuote(tmp_hosts)
 
   sed_data = " ".join(hostnames)
@@ -586,7 +634,7 @@ def RunInstanceCheck(instance, running):
   """Check if instance is running or not.
 
   """
-  instance_name = _GetName(instance, "name")
+  instance_name = _GetName(instance, operator.attrgetter("name"))
 
   script = qa_config.GetInstanceCheckScript()
   if not script:
@@ -595,7 +643,7 @@ def RunInstanceCheck(instance, running):
   master_node = qa_config.GetMasterNode()
 
   # Build command to connect to master node
-  master_ssh = GetSSHCommand(master_node["primary"], "--")
+  master_ssh = GetSSHCommand(master_node.primary, "--")
 
   if running:
     running_shellval = "1"
@@ -667,17 +715,63 @@ def GetNonexistentGroups(count):
   """Gets group names which shouldn't exist on the cluster.
 
   @param count: Number of groups to get
-  @rtype: list
+  @rtype: integer
+
+  """
+  return GetNonexistentEntityNames(count, "groups", "group")
+
+
+def GetNonexistentEntityNames(count, name_config, name_prefix):
+  """Gets entity names which shouldn't exist on the cluster.
+
+  The actualy names can refer to arbitrary entities (for example
+  groups, networks).
+
+  @param count: Number of names to get
+  @rtype: integer
+  @param name_config: name of the leaf in the config containing
+    this entity's configuration, including a 'inexistent-'
+    element
+  @rtype: string
+  @param name_prefix: prefix of the entity's names, used to compose
+    the default values; for example for groups, the prefix is
+    'group' and the generated names are then group1, group2, ...
+  @rtype: string
 
   """
-  groups = qa_config.get("groups", {})
+  entities = qa_config.get(name_config, {})
 
-  default = ["group1", "group2", "group3"]
+  default = [name_prefix + str(i) for i in range(count)]
   assert count <= len(default)
 
-  candidates = groups.get("inexistent-groups", default)[:count]
+  name_config_inexistent = "inexistent-" + name_config
+  candidates = entities.get(name_config_inexistent, default)[:count]
 
   if len(candidates) < count:
-    raise Exception("At least %s non-existent groups are needed" % count)
+    raise Exception("At least %s non-existent %s are needed" %
+                    (count, name_config))
 
   return candidates
+
+
+def MakeNodePath(node, path):
+  """Builds an absolute path for a virtual node.
+
+  @type node: string or L{qa_config._QaNode}
+  @param node: Node
+  @type path: string
+  @param path: Path without node-specific prefix
+
+  """
+  (_, basedir) = qa_config.GetVclusterSettings()
+
+  if isinstance(node, basestring):
+    name = node
+  else:
+    name = node.primary
+
+  if basedir:
+    assert path.startswith("/")
+    return "%s%s" % (vcluster.MakeNodeRoot(basedir, name), path)
+  else:
+    return path