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()