diff --git a/lib/cli.py b/lib/cli.py
index 03ea3b48ed7d2f9dddd20edff3e144395336b0fd..eb2884f16eb40d53d1e58b55d19301ea705a6d1d 100644
--- a/lib/cli.py
+++ b/lib/cli.py
@@ -234,6 +234,8 @@ __all__ = [
   "FormatError",
   "FormatQueryResult",
   "FormatParameterDict",
+  "FormatParamsDictInfo",
+  "PrintGenericInfo",
   "GenerateTable",
   "AskUser",
   "FormatTimestamp",
@@ -3610,6 +3612,27 @@ def FormatParameterDict(buf, param_dict, actual, level=1):
       buf.write(" %s\n" % val)
 
 
+def FormatParamsDictInfo(param_dict, actual):
+  """Formats a parameter dictionary.
+
+  @type param_dict: dict
+  @param param_dict: the own parameters
+  @type actual: dict
+  @param actual: the current parameter set (including defaults)
+  @rtype: dict
+  @return: dictionary where the value of each parameter is either a fully
+      formatted string or a dictionary containing formatted strings
+
+  """
+  ret = {}
+  for (key, data) in actual.items():
+    if isinstance(data, dict) and data:
+      ret[key] = FormatParamsDictInfo(param_dict.get(key, {}), data)
+    else:
+      ret[key] = str(param_dict.get(key, "default (%s)" % data))
+  return ret
+
+
 def ConfirmOperation(names, list_type, text, extra=""):
   """Ask the user to confirm an operation on a list of list_type.
 
@@ -3736,3 +3759,93 @@ def CreateIPolicyFromOpts(ispecs_mem_size=None,
   assert not (frozenset(ipolicy_out.keys()) - constants.IPOLICY_ALL_KEYS)
 
   return ipolicy_out
+
+
+def _SerializeGenericInfo(buf, data, level, afterkey=False):
+  """Formatting core of L{PrintGenericInfo}.
+
+  @param buf: (string) stream to accumulate the result into
+  @param data: data to format
+  @type level: int
+  @param level: depth in the data hierarchy, used for indenting
+  @type afterkey: bool
+  @param afterkey: True when we are in the middle of a line after a key (used
+      to properly add newlines or indentation)
+
+  """
+  baseind = "  "
+  if isinstance(data, dict):
+    if not data:
+      buf.write("\n")
+    else:
+      if afterkey:
+        buf.write("\n")
+        doindent = True
+      else:
+        doindent = False
+      for key in sorted(data):
+        if doindent:
+          buf.write(baseind * level)
+        else:
+          doindent = True
+        buf.write(key)
+        buf.write(": ")
+        _SerializeGenericInfo(buf, data[key], level + 1, afterkey=True)
+  elif isinstance(data, list) and len(data) > 0 and isinstance(data[0], tuple):
+    # list of tuples (an ordered dictionary)
+    if afterkey:
+      buf.write("\n")
+      doindent = True
+    else:
+      doindent = False
+    for (key, val) in data:
+      if doindent:
+        buf.write(baseind * level)
+      else:
+        doindent = True
+      buf.write(key)
+      buf.write(": ")
+      _SerializeGenericInfo(buf, val, level + 1, afterkey=True)
+  elif isinstance(data, list):
+    if not data:
+      buf.write("\n")
+    else:
+      if afterkey:
+        buf.write("\n")
+        doindent = True
+      else:
+        doindent = False
+      for item in data:
+        if doindent:
+          buf.write(baseind * level)
+        else:
+          doindent = True
+        buf.write("-")
+        buf.write(baseind[1:])
+        _SerializeGenericInfo(buf, item, level + 1)
+  else:
+    # This branch should be only taken for strings, but it's practically
+    # impossible to guarantee that no other types are produced somewhere
+    buf.write(str(data))
+    buf.write("\n")
+
+
+def PrintGenericInfo(data):
+  """Print information formatted according to the hierarchy.
+
+  The output is a valid YAML string.
+
+  @param data: the data to print. It's a hierarchical structure whose elements
+      can be:
+        - dictionaries, where keys are strings and values are of any of the
+          types listed here
+        - lists of pairs (key, value), where key is a string and value is of
+          any of the types listed here; it's a way to encode ordered
+          dictionaries
+        - lists of any of the types listed here
+        - strings
+
+  """
+  buf = StringIO()
+  _SerializeGenericInfo(buf, data, 0)
+  ToStdout(buf.getvalue().rstrip("\n"))
diff --git a/test/py/ganeti.cli_unittest.py b/test/py/ganeti.cli_unittest.py
index 3ebde5fe052f089a14d86e39985acda62616e8b2..ce25e780270f2c87b61e176d86462e4334bdb08c 100755
--- a/test/py/ganeti.cli_unittest.py
+++ b/test/py/ganeti.cli_unittest.py
@@ -1044,5 +1044,83 @@ class TestFieldDescValues(unittest.TestCase):
                      ["zname", kind, "Ztitle", "zzz doc zzz"])
 
 
+class TestSerializeGenericInfo(unittest.TestCase):
+  """Test case for cli._SerializeGenericInfo"""
+  def _RunTest(self, data, expected):
+    buf = StringIO()
+    cli._SerializeGenericInfo(buf, data, 0)
+    self.assertEqual(buf.getvalue(), expected)
+
+  def testSimple(self):
+    test_samples = [
+      ("abc", "abc\n"),
+      ([], "\n"),
+      ({}, "\n"),
+      (["1", "2", "3"], "- 1\n- 2\n- 3\n"),
+      ([("z", "26")], "z: 26\n"),
+      ({"z": "26"}, "z: 26\n"),
+      ([("z", "26"), ("a", "1")], "z: 26\na: 1\n"),
+      ({"z": "26", "a": "1"}, "a: 1\nz: 26\n"),
+      ]
+    for (data, expected) in test_samples:
+      self._RunTest(data, expected)
+
+  def testLists(self):
+    adict = {
+      "aa": "11",
+      "bb": "22",
+      "cc": "33",
+      }
+    adict_exp = ("- aa: 11\n"
+                 "  bb: 22\n"
+                 "  cc: 33\n")
+    anobj = [
+      ("zz", "11"),
+      ("ww", "33"),
+      ("xx", "22"),
+      ]
+    anobj_exp = ("- zz: 11\n"
+                 "  ww: 33\n"
+                 "  xx: 22\n")
+    alist = ["aa", "cc", "bb"]
+    alist_exp = ("- - aa\n"
+                 "  - cc\n"
+                 "  - bb\n")
+    test_samples = [
+      (adict, adict_exp),
+      (anobj, anobj_exp),
+      (alist, alist_exp),
+      ]
+    for (base_data, base_expected) in test_samples:
+      for k in range(1, 4):
+        data = k * [base_data]
+        expected = k * base_expected
+        self._RunTest(data, expected)
+
+  def testDictionaries(self):
+    data = [
+      ("aaa", ["x", "y"]),
+      ("bbb", {
+          "w": "1",
+          "z": "2",
+          }),
+      ("ccc", [
+          ("xyz", "123"),
+          ("efg", "456"),
+          ]),
+      ]
+    expected = ("aaa: \n"
+                "  - x\n"
+                "  - y\n"
+                "bbb: \n"
+                "  w: 1\n"
+                "  z: 2\n"
+                "ccc: \n"
+                "  xyz: 123\n"
+                "  efg: 456\n")
+    self._RunTest(data, expected)
+    self._RunTest(dict(data), expected)
+
+
 if __name__ == "__main__":
   testutils.GanetiTestProgram()