commit 0e79564a3c8fc5c4c0ad59e7e1760836218087c7
Author: Bernardo Dal Seno <bdalseno@google.com>
Date:   Mon Mar 25 01:58:16 2013 +0100

    gnt-cluster info uses a revised format
    
    The code is more modular and the output is YAML-compliant.
    
    Added function in QA that uses PyYAML to parse the command output, and QA
    is updated to take advantage of it (instead of using lots of complicated
    REs).
    
    Signed-off-by: Bernardo Dal Seno <bdalseno@google.com>
    Reviewed-by: Helga Velroyen <helgav@google.com>

diff --git a/lib/client/gnt_cluster.py b/lib/client/gnt_cluster.py
index b5e1b77..a332c2d 100644
--- a/lib/client/gnt_cluster.py
+++ b/lib/client/gnt_cluster.py
@@ -367,24 +367,24 @@ def ShowClusterMaster(opts, args):
   return 0
 
 
-def _PrintGroupedParams(paramsdict, level=1, roman=False):
-  """Print Grouped parameters (be, nic, disk) by group.
+def _FormatGroupedParams(paramsdict, roman=False):
+  """Format Grouped parameters (be, nic, disk) by group.
 
   @type paramsdict: dict of dicts
   @param paramsdict: {group: {param: value, ...}, ...}
-  @type level: int
-  @param level: Level of indention
+  @rtype: dict of dicts
+  @return: copy of the input dictionaries with strings as values
 
   """
-  indent = "  " * level
-  for item, val in sorted(paramsdict.items()):
+  ret = {}
+  for (item, val) in paramsdict.items():
     if isinstance(val, dict):
-      ToStdout("%s- %s:", indent, item)
-      _PrintGroupedParams(val, level=level + 1, roman=roman)
+      ret[item] = _FormatGroupedParams(val, roman=roman)
     elif roman and isinstance(val, int):
-      ToStdout("%s  %s: %s", indent, item, compat.TryToRoman(val))
+      ret[item] = compat.TryToRoman(val)
     else:
-      ToStdout("%s  %s: %s", indent, item, val)
+      ret[item] = str(val)
+  return ret
 
 
 def ShowClusterConfig(opts, args):
@@ -400,91 +400,97 @@ def ShowClusterConfig(opts, args):
   cl = GetClient(query=True)
   result = cl.QueryClusterInfo()
 
-  ToStdout("Cluster name: %s", result["name"])
-  ToStdout("Cluster UUID: %s", result["uuid"])
-
-  ToStdout("Creation time: %s", utils.FormatTime(result["ctime"]))
-  ToStdout("Modification time: %s", utils.FormatTime(result["mtime"]))
-
-  ToStdout("Master node: %s", result["master"])
-
-  ToStdout("Architecture (this node): %s (%s)",
-           result["architecture"][0], result["architecture"][1])
-
   if result["tags"]:
     tags = utils.CommaJoin(utils.NiceSort(result["tags"]))
   else:
     tags = "(none)"
-
-  ToStdout("Tags: %s", tags)
-
-  ToStdout("Default hypervisor: %s", result["default_hypervisor"])
-  ToStdout("Enabled hypervisors: %s",
-           utils.CommaJoin(result["enabled_hypervisors"]))
-
-  ToStdout("Hypervisor parameters:")
-  _PrintGroupedParams(result["hvparams"])
-
-  ToStdout("OS-specific hypervisor parameters:")
-  _PrintGroupedParams(result["os_hvp"])
-
-  ToStdout("OS parameters:")
-  _PrintGroupedParams(result["osparams"])
-
-  ToStdout("Hidden OSes: %s", utils.CommaJoin(result["hidden_os"]))
-  ToStdout("Blacklisted OSes: %s", utils.CommaJoin(result["blacklisted_os"]))
-
-  ToStdout("Cluster parameters:")
-  ToStdout("  - candidate pool size: %s",
-            compat.TryToRoman(result["candidate_pool_size"],
-                              convert=opts.roman_integers))
-  ToStdout("  - master netdev: %s", result["master_netdev"])
-  ToStdout("  - master netmask: %s", result["master_netmask"])
-  ToStdout("  - use external master IP address setup script: %s",
-           result["use_external_mip_script"])
-  ToStdout("  - lvm volume group: %s", result["volume_group_name"])
   if result["reserved_lvs"]:
     reserved_lvs = utils.CommaJoin(result["reserved_lvs"])
   else:
     reserved_lvs = "(none)"
-  ToStdout("  - lvm reserved volumes: %s", reserved_lvs)
-  ToStdout("  - drbd usermode helper: %s", result["drbd_usermode_helper"])
-  ToStdout("  - file storage path: %s", result["file_storage_dir"])
-  ToStdout("  - shared file storage path: %s",
-           result["shared_file_storage_dir"])
-  ToStdout("  - maintenance of node health: %s",
-           result["maintain_node_health"])
-  ToStdout("  - uid pool: %s", uidpool.FormatUidPool(result["uid_pool"]))
-  ToStdout("  - default instance allocator: %s", result["default_iallocator"])
-  ToStdout("  - primary ip version: %d", result["primary_ip_version"])
-  ToStdout("  - preallocation wipe disks: %s", result["prealloc_wipe_disks"])
-  ToStdout("  - OS search path: %s", utils.CommaJoin(pathutils.OS_SEARCH_PATH))
-  ToStdout("  - ExtStorage Providers search path: %s",
-           utils.CommaJoin(pathutils.ES_SEARCH_PATH))
-  ToStdout("  - enabled storage types: %s",
-           utils.CommaJoin(result["enabled_storage_types"]))
-
-  ToStdout("Default node parameters:")
-  _PrintGroupedParams(result["ndparams"], roman=opts.roman_integers)
-
-  ToStdout("Default instance parameters:")
-  _PrintGroupedParams(result["beparams"], roman=opts.roman_integers)
-
-  ToStdout("Default nic parameters:")
-  _PrintGroupedParams(result["nicparams"], roman=opts.roman_integers)
-
-  ToStdout("Default disk parameters:")
-  _PrintGroupedParams(result["diskparams"], roman=opts.roman_integers)
-
-  ToStdout("Instance policy - limits for instances:")
-  for key in constants.IPOLICY_ISPECS:
-    ToStdout("  - %s", key)
-    _PrintGroupedParams(result["ipolicy"][key], roman=opts.roman_integers)
-  ToStdout("  - enabled disk templates: %s",
-           utils.CommaJoin(result["ipolicy"][constants.IPOLICY_DTS]))
-  for key in constants.IPOLICY_PARAMETERS:
-    ToStdout("  - %s: %s", key, result["ipolicy"][key])
 
+  info = [
+    ("Cluster name", result["name"]),
+    ("Cluster UUID", result["uuid"]),
+
+    ("Creation time", utils.FormatTime(result["ctime"])),
+    ("Modification time", utils.FormatTime(result["mtime"])),
+
+    ("Master node", result["master"]),
+
+    ("Architecture (this node)",
+     "%s (%s)" % (result["architecture"][0], result["architecture"][1])),
+
+    ("Tags", tags),
+
+    ("Default hypervisor", result["default_hypervisor"]),
+    ("Enabled hypervisors",
+     utils.CommaJoin(result["enabled_hypervisors"])),
+
+    ("Hypervisor parameters", _FormatGroupedParams(result["hvparams"])),
+
+    ("OS-specific hypervisor parameters",
+     _FormatGroupedParams(result["os_hvp"])),
+
+    ("OS parameters", _FormatGroupedParams(result["osparams"])),
+
+    ("Hidden OSes", utils.CommaJoin(result["hidden_os"])),
+    ("Blacklisted OSes", utils.CommaJoin(result["blacklisted_os"])),
+
+    ("Cluster parameters", [
+      ("candidate pool size",
+       compat.TryToRoman(result["candidate_pool_size"],
+                         convert=opts.roman_integers)),
+      ("master netdev", result["master_netdev"]),
+      ("master netmask", result["master_netmask"]),
+      ("use external master IP address setup script",
+       result["use_external_mip_script"]),
+      ("lvm volume group", result["volume_group_name"]),
+      ("lvm reserved volumes", reserved_lvs),
+      ("drbd usermode helper", result["drbd_usermode_helper"]),
+      ("file storage path", result["file_storage_dir"]),
+      ("shared file storage path", result["shared_file_storage_dir"]),
+      ("maintenance of node health", result["maintain_node_health"]),
+      ("uid pool", uidpool.FormatUidPool(result["uid_pool"])),
+      ("default instance allocator", result["default_iallocator"]),
+      ("primary ip version", result["primary_ip_version"]),
+      ("preallocation wipe disks", result["prealloc_wipe_disks"]),
+      ("OS search path", utils.CommaJoin(pathutils.OS_SEARCH_PATH)),
+      ("ExtStorage Providers search path",
+       utils.CommaJoin(pathutils.ES_SEARCH_PATH)),
+      ("enabled storage types",
+       utils.CommaJoin(result["enabled_storage_types"])),
+      ]),
+
+    ("Default node parameters",
+     _FormatGroupedParams(result["ndparams"], roman=opts.roman_integers)),
+
+    ("Default instance parameters",
+     _FormatGroupedParams(result["beparams"], roman=opts.roman_integers)),
+
+    ("Default nic parameters",
+     _FormatGroupedParams(result["nicparams"], roman=opts.roman_integers)),
+
+    ("Default disk parameters",
+     _FormatGroupedParams(result["diskparams"], roman=opts.roman_integers)),
+
+    ("Instance policy - limits for instances",
+     [
+       (key,
+        _FormatGroupedParams(result["ipolicy"][key], roman=opts.roman_integers))
+       for key in constants.IPOLICY_ISPECS
+       ] +
+     [
+       ("enabled disk templates",
+        utils.CommaJoin(result["ipolicy"][constants.IPOLICY_DTS])),
+       ] +
+     [
+       (key, result["ipolicy"][key])
+       for key in constants.IPOLICY_PARAMETERS
+       ]),
+    ]
+
+  PrintGenericInfo(info)
   return 0
 
 
diff --git a/qa/qa_cluster.py b/qa/qa_cluster.py
index 8d4ff49..187ad23 100644
--- a/qa/qa_cluster.py
+++ b/qa/qa_cluster.py
@@ -63,36 +63,22 @@ def _CheckFileOnAllNodes(filename, content):
     AssertEqual(qa_utils.GetCommandOutput(node.primary, cmd), content)
 
 
-# "gnt-cluster info" fields
-_CIFIELD_RE = re.compile(r"^[-\s]*(?P<field>[^\s:]+):\s*(?P<value>\S.*)$")
+def _GetClusterField(field_path):
+  """Get the value of a cluster field.
 
-
-def _GetBoolClusterField(field):
-  """Get the Boolean value of a cluster field.
-
-  This function currently assumes that the field name is unique in the cluster
-  configuration. An assertion checks this assumption.
-
-  @type field: string
-  @param field: Name of the field
-  @rtype: bool
-  @return: The effective value of the field
+  @type field_path: list of strings
+  @param field_path: Names of the groups/fields to navigate to get the desired
+      value, e.g. C{["Default node parameters", "oob_program"]}
+  @return: The effective value of the field (the actual type depends on the
+      chosen field)
 
   """
-  master = qa_config.GetMasterNode()
-  infocmd = "gnt-cluster info"
-  info_out = qa_utils.GetCommandOutput(master.primary, infocmd)
-  ret = None
-  for l in info_out.splitlines():
-    m = _CIFIELD_RE.match(l)
-    # FIXME: There should be a way to specify a field through a hierarchy
-    if m and m.group("field") == field:
-      # Make sure that ignoring the hierarchy doesn't cause a double match
-      assert ret is None
-      ret = (m.group("value").lower() == "true")
-  if ret is not None:
-    return ret
-  raise qa_error.Error("Field not found in cluster configuration: %s" % field)
+  assert isinstance(field_path, list)
+  assert field_path
+  ret = qa_utils.GetObjectInfo(["gnt-cluster", "info"])
+  for key in field_path:
+    ret = ret[key]
+  return ret
 
 
 # Cluster-verify errors (date, "ERROR", then error code)
@@ -467,13 +453,6 @@ def TestClusterModifyBe():
     AssertCommand(["gnt-cluster", "modify", "-B", bep])
 
 
-_START_IPOLICY_RE = re.compile(r"^(\s*)Instance policy")
-_START_ISPEC_RE = re.compile(r"^\s+-\s+(std|min|max)")
-_VALUE_RE = r"([^\s:][^:]*):\s+(\S.*)$"
-_IPOLICY_PARAM_RE = re.compile(r"^\s+-\s+" + _VALUE_RE)
-_ISPEC_VALUE_RE = re.compile(r"^\s+" + _VALUE_RE)
-
-
 def _GetClusterIPolicy():
   """Return the run-time values of the cluster-level instance policy.
 
@@ -484,49 +463,26 @@ def _GetClusterIPolicy():
         "min", "max", or "std"
 
   """
-  mnode = qa_config.GetMasterNode()
-  info = GetCommandOutput(mnode.primary, "gnt-cluster info")
-  inside_policy = False
-  end_ispec_re = None
-  curr_spec = ""
-  specs = {}
-  policy = {}
-  for line in info.splitlines():
-    if inside_policy:
-      # The order of the matching is important, as some REs overlap
-      m = _START_ISPEC_RE.match(line)
-      if m:
-        curr_spec = m.group(1)
-        continue
-      m = _IPOLICY_PARAM_RE.match(line)
-      if m:
-        policy[m.group(1)] = m.group(2).strip()
-        continue
-      m = _ISPEC_VALUE_RE.match(line)
-      if m:
-        assert curr_spec
-        par = m.group(1)
+  info = qa_utils.GetObjectInfo(["gnt-cluster", "info"])
+  policy = info["Instance policy - limits for instances"]
+  ret_specs = {}
+  ret_policy = {}
+  for (key, val) in policy.items():
+    if key in constants.IPOLICY_ISPECS:
+      for (par, pval) in val.items():
         if par == "memory-size":
           par = "mem-size"
-        d = specs.setdefault(par, {})
-        d[curr_spec] = m.group(2).strip()
-        continue
-      assert end_ispec_re is not None
-      if end_ispec_re.match(line):
-        inside_policy = False
+        d = ret_specs.setdefault(par, {})
+        d[key] = pval
     else:
-      m = _START_IPOLICY_RE.match(line)
-      if m:
-        inside_policy = True
-        # We stop parsing when we find the same indentation level
-        re_str = r"^\s{%s}\S" % len(m.group(1))
-        end_ispec_re = re.compile(re_str)
+      ret_policy[key] = val
+
   # Sanity checks
-  assert len(specs) > 0
-  good = ("min" in d and "std" in d and "max" in d for d in specs)
-  assert good, "Missing item in specs: %s" % specs
-  assert len(policy) > 0
-  return (policy, specs)
+  assert len(ret_specs) > 0
+  good = ("min" in d and "std" in d and "max" in d for d in ret_specs)
+  assert good, "Missing item in specs: %s" % ret_specs
+  assert len(ret_policy) > 0
+  return (ret_policy, ret_specs)
 
 
 def TestClusterModifyIPolicy():
@@ -912,10 +868,11 @@ def TestSetExclStorCluster(newvalue):
   @return: The old value of exclusive_storage
 
   """
-  oldvalue = _GetBoolClusterField("exclusive_storage")
+  es_path = ["Default node parameters", "exclusive_storage"]
+  oldvalue = _GetClusterField(es_path)
   AssertCommand(["gnt-cluster", "modify", "--node-parameters",
                  "exclusive_storage=%s" % newvalue])
-  effvalue = _GetBoolClusterField("exclusive_storage")
+  effvalue = _GetClusterField(es_path)
   if effvalue != newvalue:
     raise qa_error.Error("exclusive_storage has the wrong value: %s instead"
                          " of %s" % (effvalue, newvalue))
diff --git a/qa/qa_utils.py b/qa/qa_utils.py
index e842b15..a61bfbf 100644
--- a/qa/qa_utils.py
+++ b/qa/qa_utils.py
@@ -23,13 +23,14 @@
 
 """
 
+import operator
 import os
+import random
 import re
-import sys
 import subprocess
-import random
+import sys
 import tempfile
-import operator
+import yaml
 
 try:
   import functools
@@ -343,6 +344,21 @@ def GetCommandOutput(node, cmd, tty=None, fail=False):
   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.
 
