From 6d4a16569d3bb53641b77f1232689e15c7cf7a83 Mon Sep 17 00:00:00 2001 From: Michael Hanselmann <hansmi@google.com> Date: Fri, 12 Mar 2010 16:16:08 +0100 Subject: [PATCH] =?UTF-8?q?Implement=20replacing=20cluster=20certs=20and?= =?UTF-8?q?=20keys=20via=20=E2=80=9Cgnt-cluster=20renew-crypto=E2=80=9D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Recent changes to βgnt-cluster verifyβ made it complain on expiring SSL certificates. While it was possible to replace the SSL certificates and other cluster secrets manually before, doing so was cumbersome. Cluster certificates, keys and secrets can now be replaced easily. Signed-off-by: Michael Hanselmann <hansmi@google.com> Reviewed-by: Iustin Pop <iustin@google.com> --- lib/cli.py | 22 +++++++++ man/gnt-cluster.sgml | 37 ++++++++++++++- qa/ganeti-qa.py | 5 ++ qa/qa-sample.json | 1 + qa/qa_cluster.py | 48 ++++++++++++++++++- scripts/gnt-cluster | 109 +++++++++++++++++++++++++++++++++++++++++-- 6 files changed, 216 insertions(+), 6 deletions(-) diff --git a/lib/cli.py b/lib/cli.py index 558feacf6..5d0a66a9b 100644 --- a/lib/cli.py +++ b/lib/cli.py @@ -79,6 +79,9 @@ __all__ = [ "MASTER_NETDEV_OPT", "MC_OPT", "NET_OPT", + "NEW_CLUSTER_CERT_OPT", + "NEW_HMAC_KEY_OPT", + "NEW_RAPI_CERT_OPT", "NEW_SECONDARY_OPT", "NIC_PARAMS_OPT", "NODE_LIST_OPT", @@ -102,6 +105,7 @@ __all__ = [ "OFFLINE_OPT", "OS_OPT", "OS_SIZE_OPT", + "RAPI_CERT_OPT", "READD_OPT", "REBOOT_TYPE_OPT", "SECONDARY_IP_OPT", @@ -860,6 +864,24 @@ EARLY_RELEASE_OPT = cli_option("--early-release", help="Release the locks on the secondary" " node(s) early") +NEW_CLUSTER_CERT_OPT = cli_option("--new-cluster-certificate", + dest="new_cluster_cert", + default=False, action="store_true", + help="Generate a new cluster certificate") + +RAPI_CERT_OPT = cli_option("--rapi-certificate", dest="rapi_cert", + default=None, + help="File containing new RAPI certificate") + +NEW_RAPI_CERT_OPT = cli_option("--new-rapi-certificate", dest="new_rapi_cert", + default=None, action="store_true", + help=("Generate a new self-signed RAPI" + " certificate")) + +NEW_HMAC_KEY_OPT = cli_option("--new-hmac-key", dest="new_hmac_key", + default=False, action="store_true", + help="Create a new HMAC key") + def _ParseArgs(argv, commands, aliases): """Parser for the command line arguments. diff --git a/man/gnt-cluster.sgml b/man/gnt-cluster.sgml index 4b3b8223f..e7947d049 100644 --- a/man/gnt-cluster.sgml +++ b/man/gnt-cluster.sgml @@ -540,7 +540,7 @@ <sbr> <arg choice="opt">--nic-parameters <replaceable>nic-param</replaceable>=<replaceable>value</replaceable><arg rep="repeat" choice="opt">,<replaceable>nic-param</replaceable>=<replaceable>value</replaceable></arg></arg> <sbr> - <arg>-C <replaceable>candidate_pool_size</replaceable></arg> + <arg choice="opt">-C <replaceable>candidate_pool_size</replaceable></arg> </cmdsynopsis> @@ -558,7 +558,7 @@ </para> <para> - The <option>-C</option> options specifies the + The <option>-C</option> option specifies the <varname>candidate_pool_size</varname> cluster parameter. This is the number of nodes that the master will try to keep as <literal>master_candidates</literal>. For more details about @@ -703,6 +703,39 @@ </para> </refsect2> + <refsect2> + <title>RENEW-CRYPTO</title> + + <cmdsynopsis> + <command>renew-crypto</command> + <arg>-f</arg> + <sbr> + <arg choice="opt">--new-cluster-certificate</arg> + <arg choice="opt">--new-hmac-key</arg> + <sbr> + <arg choice="opt">--new-rapi-certificate</arg> + <arg choice="opt">--rapi-certificate <replaceable>rapi-cert</replaceable></arg> + </cmdsynopsis> + + <para> + This command will stop all + Ganeti daemons in the cluster and start them again once the new + certificates and keys are replicated. The options + <option>--new-cluster-certificate</option> and + <option>--new-hmac-key</option> can be used to regenerate the + cluster-internal SSL certificate respective the HMAC key used by + <citerefentry> + <refentrytitle>ganeti-confd</refentrytitle><manvolnum>8</manvolnum> + </citerefentry>. To generate a new self-signed RAPI certificate (used + by <citerefentry> + <refentrytitle>ganeti-rapi</refentrytitle><manvolnum>8</manvolnum> + </citerefentry>) specify <option>--new-rapi-certificate</option>. If + you want to use your own certificate, e.g. one signed by a certificate + authority (CA), pass its filename to + <option>--rapi-certificate</option>. + </para> + </refsect2> + <refsect2> <title>REPAIR-DISK-SIZES</title> diff --git a/qa/ganeti-qa.py b/qa/ganeti-qa.py index 73c01aee4..d90ff6b5b 100755 --- a/qa/ganeti-qa.py +++ b/qa/ganeti-qa.py @@ -88,6 +88,9 @@ def RunClusterTests(): """Runs tests related to gnt-cluster. """ + if qa_config.TestEnabled("cluster-renew-crypto"): + RunTest(qa_cluster.TestClusterRenewCrypto) + if qa_config.TestEnabled('cluster-verify'): RunTest(qa_cluster.TestClusterVerify) @@ -115,6 +118,7 @@ def RunClusterTests(): RunTest(qa_rapi.TestVersion) RunTest(qa_rapi.TestEmptyCluster) + def RunOsTests(): """Runs all tests related to gnt-os. @@ -176,6 +180,7 @@ def RunCommonInstanceTests(instance): if qa_rapi.Enabled(): RunTest(qa_rapi.TestInstance, instance) + def RunExportImportTests(instance, pnode): """Tries to export and import the instance. diff --git a/qa/qa-sample.json b/qa/qa-sample.json index d167e06e9..a150b37d7 100644 --- a/qa/qa-sample.json +++ b/qa/qa-sample.json @@ -45,6 +45,7 @@ "cluster-command": true, "cluster-copyfile": true, "cluster-master-failover": true, + "cluster-renew-crypto": true, "cluster-destroy": true, "cluster-rename": true, diff --git a/qa/qa_cluster.py b/qa/qa_cluster.py index 9ca548337..d4efc50de 100644 --- a/qa/qa_cluster.py +++ b/qa/qa_cluster.py @@ -25,13 +25,15 @@ import tempfile +from ganeti import constants +from ganeti import bootstrap from ganeti import utils import qa_config import qa_utils import qa_error -from qa_utils import AssertEqual, StartSSH +from qa_utils import AssertEqual, AssertNotEqual, StartSSH def _RemoveFileFromAllNodes(filename): @@ -144,6 +146,50 @@ def TestClusterVersion(): utils.ShellQuoteArgs(cmd)).wait(), 0) +def TestClusterRenewCrypto(): + """gnt-cluster renew-crypto""" + master = qa_config.GetMasterNode() + + # Conflicting options + cmd = ["gnt-cluster", "renew-crypto", "--force", + "--new-cluster-certificate", "--new-hmac-key", + "--new-rapi-certificate", "--rapi-certificate=/dev/null"] + AssertNotEqual(StartSSH(master["primary"], + utils.ShellQuoteArgs(cmd)).wait(), 0) + + # Invalid RAPI certificate + cmd = ["gnt-cluster", "renew-crypto", "--force", + "--rapi-certificate=/dev/null"] + AssertNotEqual(StartSSH(master["primary"], + utils.ShellQuoteArgs(cmd)).wait(), 0) + + # Custom RAPI certificate + fh = tempfile.NamedTemporaryFile() + + # Ensure certificate doesn't cause "gnt-cluster verify" to complain + validity = constants.SSL_CERT_EXPIRATION_WARN * 3 + + bootstrap.GenerateSelfSignedSslCert(fh.name, validity=validity) + + tmpcert = qa_utils.UploadFile(master["primary"], fh.name) + try: + cmd = ["gnt-cluster", "renew-crypto", "--force", + "--rapi-certificate=%s" % tmpcert] + AssertEqual(StartSSH(master["primary"], + utils.ShellQuoteArgs(cmd)).wait(), 0) + finally: + cmd = ["rm", "-f", tmpcert] + AssertEqual(StartSSH(master["primary"], + utils.ShellQuoteArgs(cmd)).wait(), 0) + + # Normal case + cmd = ["gnt-cluster", "renew-crypto", "--force", + "--new-cluster-certificate", "--new-hmac-key", + "--new-rapi-certificate"] + AssertEqual(StartSSH(master["primary"], + utils.ShellQuoteArgs(cmd)).wait(), 0) + + def TestClusterBurnin(): """Burnin""" master = qa_config.GetMasterNode() diff --git a/scripts/gnt-cluster b/scripts/gnt-cluster index 59b73a7b0..01ecedbc9 100755 --- a/scripts/gnt-cluster +++ b/scripts/gnt-cluster @@ -29,6 +29,7 @@ import sys import os.path import time +import OpenSSL from ganeti.cli import * from ganeti import opcodes @@ -493,6 +494,100 @@ def SearchTags(opts, args): ToStdout("%s %s", path, tag) +def _RenewCrypto(new_cluster_cert, new_rapi_cert, rapi_cert_filename, + new_hmac_key, force): + """Renews cluster certificates, keys and secrets. + + @type new_cluster_cert: bool + @param new_cluster_cert: Whether to generate a new cluster certificate + @type new_rapi_cert: bool + @param new_rapi_cert: Whether to generate a new RAPI certificate + @type rapi_cert_filename: string + @param rapi_cert_filename: Path to file containing new RAPI certificate + @type new_hmac_key: bool + @param new_hmac_key: Whether to generate a new HMAC key + @type force: bool + @param force: Whether to ask user for confirmation + + """ + assert new_cluster_cert or new_rapi_cert or rapi_cert_filename or new_hmac_key + + if new_rapi_cert and rapi_cert_filename: + ToStderr("Only one of the --new-rapi-certficate and --rapi-certificate" + " options can be specified at the same time.") + return 1 + + if rapi_cert_filename: + # Read and verify new certificate + try: + rapi_cert_pem = utils.ReadFile(rapi_cert_filename) + + OpenSSL.crypto.load_certificate(OpenSSL.crypto.FILETYPE_PEM, + rapi_cert_pem) + except Exception, err: # pylint: disable-msg=W0703 + ToStderr("Can't load new RAPI certificate from %s: %s" % + (rapi_cert_filename, str(err))) + return 1 + + try: + OpenSSL.crypto.load_privatekey(OpenSSL.crypto.FILETYPE_PEM, rapi_cert_pem) + except Exception, err: # pylint: disable-msg=W0703 + ToStderr("Can't load new RAPI private key from %s: %s" % + (rapi_cert_filename, str(err))) + return 1 + + else: + rapi_cert_pem = None + + if not force: + usertext = ("This requires all daemons on all nodes to be restarted and" + " may take some time. Continue?") + if not AskUser(usertext): + return 1 + + def _RenewCryptoInner(ctx): + ctx.feedback_fn("Updating certificates and keys") + bootstrap.GenerateClusterCrypto(new_cluster_cert, new_rapi_cert, + new_hmac_key, + rapi_cert_pem=rapi_cert_pem) + + files_to_copy = [] + + if new_cluster_cert: + files_to_copy.append(constants.SSL_CERT_FILE) + + if new_rapi_cert or rapi_cert_pem: + files_to_copy.append(constants.RAPI_CERT_FILE) + + if new_hmac_key: + files_to_copy.append(constants.HMAC_CLUSTER_KEY) + + if files_to_copy: + for node_name in ctx.nonmaster_nodes: + ctx.feedback_fn("Copying %s to %s" % + (", ".join(files_to_copy), node_name)) + for file_name in files_to_copy: + ctx.ssh.CopyFileToNode(node_name, file_name) + + RunWhileClusterStopped(ToStdout, _RenewCryptoInner) + + ToStdout("All requested certificates and keys have been replaced." + " Running \"gnt-cluster verify\" now is recommended.") + + return 0 + + +def RenewCrypto(opts, args): + """Renews cluster certificates, keys and secrets. + + """ + return _RenewCrypto(opts.new_cluster_cert, + opts.new_rapi_cert, + opts.rapi_cert, + opts.new_hmac_key, + opts.force) + + def SetClusterParams(opts, args): """Modify the cluster. @@ -512,10 +607,11 @@ def SetClusterParams(opts, args): vg_name = opts.vg_name if not opts.lvm_storage and opts.vg_name: - ToStdout("Options --no-lvm-storage and --vg-name conflict.") + ToStderr("Options --no-lvm-storage and --vg-name conflict.") return 1 - elif not opts.lvm_storage: - vg_name = '' + + if not opts.lvm_storage: + vg_name = "" hvlist = opts.enabled_hypervisors if hvlist is not None: @@ -692,7 +788,14 @@ commands = { NIC_PARAMS_OPT, NOLVM_STORAGE_OPT, VG_NAME_OPT], "[opts...]", "Alters the parameters of the cluster"), + "renew-crypto": ( + RenewCrypto, ARGS_NONE, + [NEW_CLUSTER_CERT_OPT, NEW_RAPI_CERT_OPT, RAPI_CERT_OPT, NEW_HMAC_KEY_OPT, + FORCE_OPT], + "[opts...]", + "Renews cluster certificates, keys and secrets"), } + if __name__ == '__main__': sys.exit(GenericMain(commands, override={"tag_type": constants.TAG_CLUSTER})) -- GitLab