QA: Split function to set and parse instance policies
[ganeti-local] / qa / qa_utils.py
index baacdeb..9cea4b2 100644 (file)
 
 """
 
+import operator
 import os
+import random
 import re
-import sys
 import subprocess
-import random
+import sys
 import tempfile
+import yaml
 
 try:
   import functools
@@ -40,6 +42,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
@@ -129,21 +132,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)
@@ -182,7 +181,7 @@ def AssertCommand(cmd, fail=False, node=None, log_cmd=True):
   if node is None:
     node = qa_config.GetMasterNode()
 
-  nodename = _GetName(node, "primary")
+  nodename = _GetName(node, operator.attrgetter("primary"))
 
   if isinstance(cmd, basestring):
     cmdstr = cmd
@@ -253,9 +252,25 @@ 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
 
@@ -325,10 +340,25 @@ 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()
 
 
+def GetObjectInfo(infocmd):
+  """Get and parse information about a Ganeti object.
+
+  @type infocmd: list of strings
+  @param infocmd: command to be executed, e.g. ["gnt-cluster", "info"]
+  @return: the information parsed, appropriately stored in dictionaries,
+      lists...
+
+  """
+  master = qa_config.GetMasterNode()
+  cmdline = utils.ShellQuoteArgs(infocmd)
+  info_out = GetCommandOutput(master.primary, cmdline)
+  return yaml.load(info_out)
+
+
 def UploadFile(node, src):
   """Uploads a file to a node and returns the filename.
 
@@ -389,27 +419,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()
 
-def _ResolveName(cmd, key):
-  """Helper function.
-
-  """
-  master = qa_config.GetMasterNode()
+  print "Backup filename: %s" % result
 
-  output = GetCommandOutput(master["primary"], utils.ShellQuoteArgs(cmd))
-  for line in output.splitlines():
-    (lkey, lvalue) = line.split(":", 1)
-    if lkey == key:
-      return lvalue.lstrip()
-  raise KeyError("Key not found")
+  return result
 
 
 def ResolveInstanceName(instance):
@@ -419,16 +441,16 @@ def ResolveInstanceName(instance):
   @param instance: Instance name
 
   """
-  return _ResolveName(["gnt-instance", "info", instance],
-                      "Instance name")
+  info = GetObjectInfo(["gnt-instance", "info", instance])
+  return info[0]["Instance name"]
 
 
 def ResolveNodeName(node):
   """Gets the full name of a node.
 
   """
-  return _ResolveName(["gnt-node", "info", node["primary"]],
-                      "Node name")
+  info = GetObjectInfo(["gnt-node", "info", node.primary])
+  return info[0]["Node name"]
 
 
 def GetNodeInstances(node, secondaries=False):
@@ -441,7 +463,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():
@@ -485,7 +507,7 @@ def _List(listcmd, fields, names):
   if names:
     cmd.extend(names)
 
-  return GetCommandOutput(master["primary"],
+  return GetCommandOutput(master.primary,
                           utils.ShellQuoteArgs(cmd)).splitlines()
 
 
@@ -541,7 +563,7 @@ def GenericQueryFieldsTest(cmd, 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))
@@ -570,7 +592,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"):
@@ -595,7 +617,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)
@@ -614,7 +636,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:
@@ -623,7 +645,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"
@@ -732,3 +754,125 @@ def GetNonexistentEntityNames(count, name_config, name_prefix):
                     (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
+
+
+def _GetParameterOptions(key, specs, old_specs):
+  """Helper to build policy options."""
+  values = ["%s=%s" % (par, keyvals[key])
+            for (par, keyvals) in specs.items()
+            if key in keyvals]
+  if old_specs:
+    present_pars = frozenset(par
+                             for (par, keyvals) in specs.items()
+                             if key in keyvals)
+    values.extend("%s=%s" % (par, keyvals[key])
+                  for (par, keyvals) in old_specs.items()
+                  if key in keyvals and par not in present_pars)
+  return ",".join(values)
+
+
+def TestSetISpecs(new_specs, get_policy_fn=None, build_cmd_fn=None,
+                  fail=False, old_values=None):
+  """Change instance specs for an object.
+
+  @type new_specs: dict of dict
+  @param new_specs: new_specs[par][key], where key is "min", "max", "std". It
+      can be an empty dictionary.
+  @type get_policy_fn: function
+  @param get_policy_fn: function that returns the current policy as in
+      L{qa_cluster._GetClusterIPolicy}
+  @type build_cmd_fn: function
+  @param build_cmd_fn: function that return the full command line from the
+      options alone
+  @type fail: bool
+  @param fail: if the change is expected to fail
+  @type old_values: tuple
+  @param old_values: (old_policy, old_specs), as returned by
+     L{qa_cluster._GetClusterIPolicy}
+  @return: same as L{qa_cluster._GetClusterIPolicy}
+
+  """
+  assert get_policy_fn is not None
+  assert build_cmd_fn is not None
+
+  if old_values:
+    (old_policy, old_specs) = old_values
+  else:
+    (old_policy, old_specs) = get_policy_fn()
+  if new_specs:
+    cmd = []
+    if any(("min" in val or "max" in val) for val in new_specs.values()):
+      minmax_opt_items = []
+      for key in ["min", "max"]:
+        keyopt = _GetParameterOptions(key, new_specs, old_specs)
+        minmax_opt_items.append("%s:%s" % (key, keyopt))
+      cmd.extend([
+        "--ipolicy-bounds-specs",
+        "/".join(minmax_opt_items)
+        ])
+    std_opt = _GetParameterOptions("std", new_specs, {})
+    if std_opt:
+      cmd.extend(["--ipolicy-std-specs", std_opt])
+    AssertCommand(build_cmd_fn(cmd), fail=fail)
+
+  # Check the new state
+  (eff_policy, eff_specs) = get_policy_fn()
+  AssertEqual(eff_policy, old_policy)
+  if fail:
+    AssertEqual(eff_specs, old_specs)
+  else:
+    for par in eff_specs:
+      for key in eff_specs[par]:
+        if par in new_specs and key in new_specs[par]:
+          AssertEqual(int(eff_specs[par][key]), int(new_specs[par][key]))
+        else:
+          AssertEqual(int(eff_specs[par][key]), int(old_specs[par][key]))
+  return (eff_policy, eff_specs)
+
+
+def ParseIPolicy(policy):
+  """Parse and split instance an instance policy.
+
+  @type policy: dict
+  @param policy: policy, as returned by L{GetObjectInfo}
+  @rtype: tuple
+  @return: (policy, specs), where:
+      - policy is a dictionary of the policy values, instance specs excluded
+      - specs is dict of dict, specs[par][key] is a spec value, where key is
+        "min", "max", or "std"
+
+  """
+  ret_specs = {}
+  ret_policy = {}
+  ispec_keys = constants.ISPECS_MINMAX_KEYS | frozenset([constants.ISPECS_STD])
+  for (key, val) in policy.items():
+    if key in ispec_keys:
+      for (par, pval) in val.items():
+        d = ret_specs.setdefault(par, {})
+        d[key] = pval
+    else:
+      ret_policy[key] = val
+  return (ret_policy, ret_specs)