diff --git a/lib/cli.py b/lib/cli.py index 9c83c419da851dabe72f6486eca66e105750a94d..2113308d146ce39600842314f0c48a231f1bfe2f 100644 --- a/lib/cli.py +++ b/lib/cli.py @@ -108,6 +108,7 @@ __all__ = [ "IGNORE_REMOVE_FAILURES_OPT", "IGNORE_SECONDARIES_OPT", "IGNORE_SIZE_OPT", + "INCLUDEDEFAULTS_OPT", "INTERVAL_OPT", "MAC_PREFIX_OPT", "MAINTAIN_NODE_HEALTH_OPT", @@ -237,6 +238,7 @@ __all__ = [ "FormatQueryResult", "FormatParamsDictInfo", "FormatPolicyInfo", + "PrintIPolicyCommand", "PrintGenericInfo", "GenerateTable", "AskUser", @@ -1621,6 +1623,10 @@ NOCONFLICTSCHECK_OPT = cli_option("--no-conflicts-check", action="store_false", help="Don't check for conflicting IPs") +INCLUDEDEFAULTS_OPT = cli_option("--include-defaults", dest="include_defaults", + default=False, action="store_true", + help="Include default values") + #: Options provided by all commands COMMON_OPTS = [DEBUG_OPT, REASON_OPT] @@ -3751,6 +3757,41 @@ def FormatPolicyInfo(custom_ipolicy, eff_ipolicy, iscluster): return ret +def _PrintSpecsParameters(buf, specs): + values = ("%s=%s" % (par, val) for (par, val) in sorted(specs.items())) + buf.write(",".join(values)) + + +def PrintIPolicyCommand(buf, ipolicy, isgroup): + """Print the command option used to generate the given instance policy. + + Currently only the parts dealing with specs are supported. + + @type buf: StringIO + @param buf: stream to write into + @type ipolicy: dict + @param ipolicy: instance policy + @type isgroup: bool + @param isgroup: whether the policy is at group level + + """ + if not isgroup: + stdspecs = ipolicy.get("std") + if stdspecs: + buf.write(" %s " % IPOLICY_STD_SPECS_STR) + _PrintSpecsParameters(buf, stdspecs) + minmax = ipolicy.get("minmax") + if minmax: + minspecs = minmax.get("min") + maxspecs = minmax.get("max") + if minspecs and maxspecs: + buf.write(" %s " % IPOLICY_BOUNDS_SPECS_STR) + buf.write("min:") + _PrintSpecsParameters(buf, minspecs) + buf.write("/max:") + _PrintSpecsParameters(buf, maxspecs) + + def ConfirmOperation(names, list_type, text, extra=""): """Ask the user to confirm an operation on a list of list_type. diff --git a/lib/client/gnt_cluster.py b/lib/client/gnt_cluster.py index e0e2d7fc23937d9c537a4ba3efb8cb211f5fe991..750d65510ea572decb0c2dd7d13fb4a9ab993c6f 100644 --- a/lib/client/gnt_cluster.py +++ b/lib/client/gnt_cluster.py @@ -26,6 +26,7 @@ # W0614: Unused import %s from wildcard import (since we need cli) # C0103: Invalid name gnt-cluster +from cStringIO import StringIO import os.path import time import OpenSSL @@ -1491,6 +1492,26 @@ def Epo(opts, args, cl=None, _on_fn=_EpoOn, _off_fn=_EpoOff, return _off_fn(opts, node_list, inst_map) +def _GetCreateCommand(info): + buf = StringIO() + buf.write("gnt-cluster init") + PrintIPolicyCommand(buf, info["ipolicy"], False) + buf.write(" ") + buf.write(info["name"]) + return buf.getvalue() + + +def ShowCreateCommand(opts, args): + """Shows the command that can be used to re-create the cluster. + + Currently it works only for ipolicy specs. + + """ + cl = GetClient(query=True) + result = cl.QueryClusterInfo() + ToStdout(_GetCreateCommand(result)) + + commands = { "init": ( InitCluster, [ArgHost(min=1, max=1)], @@ -1603,6 +1624,9 @@ commands = { "deactivate-master-ip": ( DeactivateMasterIp, ARGS_NONE, [CONFIRM_OPT], "", "Deactivates the master IP"), + "show-ispecs-cmd": ( + ShowCreateCommand, ARGS_NONE, [], "", + "Show the command line to re-create the cluster"), } diff --git a/lib/client/gnt_group.py b/lib/client/gnt_group.py index 2f8dcc9dc294ea98cc202d537ad3d31e297834ee..32e0e3a1493ddd13fe4b5cffadb9cda15c8c900a 100644 --- a/lib/client/gnt_group.py +++ b/lib/client/gnt_group.py @@ -24,6 +24,8 @@ # W0401: Wildcard import ganeti.cli # W0614: Unused import %s from wildcard import (since we need cli) +from cStringIO import StringIO + from ganeti.cli import * from ganeti import constants from ganeti import opcodes @@ -313,6 +315,35 @@ def GroupInfo(_, args): ]) +def _GetCreateCommand(group): + (name, ipolicy) = group + buf = StringIO() + buf.write("gnt-group add") + PrintIPolicyCommand(buf, ipolicy, True) + buf.write(" ") + buf.write(name) + return buf.getvalue() + + +def ShowCreateCommand(opts, args): + """Shows the command that can be used to re-create a node group. + + Currently it works only for ipolicy specs. + + """ + cl = GetClient(query=True) + selected_fields = ["name"] + if opts.include_defaults: + selected_fields += ["ipolicy"] + else: + selected_fields += ["custom_ipolicy"] + result = cl.QueryGroups(names=args, fields=selected_fields, + use_locking=False) + + for group in result: + ToStdout(_GetCreateCommand(group)) + + commands = { "add": ( AddGroup, ARGS_ONE_GROUP, @@ -366,6 +397,10 @@ commands = { "info": ( GroupInfo, ARGS_MANY_GROUPS, [], "[<group_name>...]", "Show group information"), + "show-ispecs-cmd": ( + ShowCreateCommand, ARGS_MANY_GROUPS, [INCLUDEDEFAULTS_OPT], + "[--include-defaults] [<group_name>...]", + "Show the command line to re-create a group"), } diff --git a/man/gnt-cluster.rst b/man/gnt-cluster.rst index c42c5fc5e72c73b93824dcbab9b1592b03cb51e4..1e837ae25e406f433676652f2e8470fde3f91f1c 100644 --- a/man/gnt-cluster.rst +++ b/man/gnt-cluster.rst @@ -152,6 +152,14 @@ Passing the ``--roman`` option gnt-cluster info will try to print its integer fields in a latin friendly way. This allows further diffusion of Ganeti among ancient cultures. +SHOW-ISPECS-CMD +~~~~~~~~~~~~~~~ + +**show-ispecs-cmd** + +Shows the command line that can be used to recreate the cluster with the +same options relative to specs in the instance policies. + INIT ~~~~ diff --git a/man/gnt-group.rst b/man/gnt-group.rst index a633dc98c1f1a8ca0212030eba9a540b52762f96..6786e698d1eb4d9ff94270e17576a52f9da06dfa 100644 --- a/man/gnt-group.rst +++ b/man/gnt-group.rst @@ -257,10 +257,23 @@ be interpreted as stdin. INFO ~~~~ -**info** [group...] +**info** [*group*...] Shows config information for all (or given) groups. +SHOW-ISPECS-CMD +~~~~~~~~~~~~~~~ + +**show-ispecs-cmd** [\--include-defaults] [*group*...] + +Shows the command line that can be used to recreate the given groups (or +all groups, if none is given) with the same options relative to specs in +the instance policies. + +If ``--include-defaults`` is specified, include also the default values +(i.e. the cluster-level settings), and not only the configuration items +that a group overrides. + .. vim: set textwidth=72 : .. Local Variables: diff --git a/test/py/ganeti.cli_unittest.py b/test/py/ganeti.cli_unittest.py index 96f84d60d61a1263358f89ef9d497d33952b6f8b..a90dd4abaeb3458c9b88f899bee3f6f181b3be92 100755 --- a/test/py/ganeti.cli_unittest.py +++ b/test/py/ganeti.cli_unittest.py @@ -1423,5 +1423,59 @@ class TestCreateIPolicyFromOpts(unittest.TestCase): self._TestFullISpecsInner(skel_ipolicy, exp_minmax1, None, False, fill_all) + +class TestPrintIPolicyCommand(unittest.TestCase): + """Test case for cli.PrintIPolicyCommand""" + _SPECS1 = { + "par1": 42, + "par2": "xyz", + } + _SPECS1_STR = "par1=42,par2=xyz" + _SPECS2 = { + "param": 10, + "another_param": 101, + } + _SPECS2_STR = "another_param=101,param=10" + + def _CheckPrintIPolicyCommand(self, ipolicy, isgroup, expected): + buf = StringIO() + cli.PrintIPolicyCommand(buf, ipolicy, isgroup) + self.assertEqual(buf.getvalue(), expected) + + def testIgnoreStdForGroup(self): + self._CheckPrintIPolicyCommand({"std": self._SPECS1}, True, "") + + def testIgnoreEmpty(self): + policies = [ + {}, + {"std": {}}, + {"minmax": {}}, + {"minmax": { + "min": {}, + "max": {}, + }}, + {"minmax": { + "min": self._SPECS1, + "max": {}, + }}, + ] + for pol in policies: + self._CheckPrintIPolicyCommand(pol, False, "") + + def testFullPolicies(self): + cases = [ + ({"std": self._SPECS1}, + " %s %s" % (cli.IPOLICY_STD_SPECS_STR, self._SPECS1_STR)), + ({"minmax": { + "min": self._SPECS1, + "max": self._SPECS2, + }}, + " %s min:%s/max:%s" % (cli.IPOLICY_BOUNDS_SPECS_STR, + self._SPECS1_STR, self._SPECS2_STR)), + ] + for (pol, exp) in cases: + self._CheckPrintIPolicyCommand(pol, False, exp) + + if __name__ == "__main__": testutils.GanetiTestProgram()