Commit 6d4a1656 authored by Michael Hanselmann's avatar Michael Hanselmann
Browse files

Implement replacing cluster certs and keys via “gnt-cluster renew-crypto”

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: default avatarMichael Hanselmann <>
Reviewed-by: default avatarIustin Pop <>
parent 7e49b6ce
......@@ -79,6 +79,9 @@ __all__ = [
......@@ -102,6 +105,7 @@ __all__ = [
......@@ -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",
default=False, action="store_true",
help="Generate a new cluster certificate")
RAPI_CERT_OPT = cli_option("--rapi-certificate", dest="rapi_cert",
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.
......@@ -540,7 +540,7 @@
<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>
<arg>-C <replaceable>candidate_pool_size</replaceable></arg>
<arg choice="opt">-C <replaceable>candidate_pool_size</replaceable></arg>
......@@ -558,7 +558,7 @@
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 @@
<arg choice="opt">--new-cluster-certificate</arg>
<arg choice="opt">--new-hmac-key</arg>
<arg choice="opt">--new-rapi-certificate</arg>
<arg choice="opt">--rapi-certificate <replaceable>rapi-cert</replaceable></arg>
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>. To generate a new self-signed RAPI certificate (used
by <citerefentry>
</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
......@@ -88,6 +88,9 @@ def RunClusterTests():
"""Runs tests related to gnt-cluster.
if qa_config.TestEnabled("cluster-renew-crypto"):
if qa_config.TestEnabled('cluster-verify'):
......@@ -115,6 +118,7 @@ def RunClusterTests():
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.
......@@ -45,6 +45,7 @@
"cluster-command": true,
"cluster-copyfile": true,
"cluster-master-failover": true,
"cluster-renew-crypto": true,
"cluster-destroy": true,
"cluster-rename": true,
......@@ -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"]
utils.ShellQuoteArgs(cmd)).wait(), 0)
# Invalid RAPI certificate
cmd = ["gnt-cluster", "renew-crypto", "--force",
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(, validity=validity)
tmpcert = qa_utils.UploadFile(master["primary"],
cmd = ["gnt-cluster", "renew-crypto", "--force",
"--rapi-certificate=%s" % tmpcert]
utils.ShellQuoteArgs(cmd)).wait(), 0)
cmd = ["rm", "-f", tmpcert]
utils.ShellQuoteArgs(cmd)).wait(), 0)
# Normal case
cmd = ["gnt-cluster", "renew-crypto", "--force",
"--new-cluster-certificate", "--new-hmac-key",
utils.ShellQuoteArgs(cmd)).wait(), 0)
def TestClusterBurnin():
master = qa_config.GetMasterNode()
......@@ -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
rapi_cert_pem = utils.ReadFile(rapi_cert_filename)
except Exception, err: # pylint: disable-msg=W0703
ToStderr("Can't load new RAPI certificate from %s: %s" %
(rapi_cert_filename, str(err)))
return 1
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
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,
files_to_copy = []
if new_cluster_cert:
if new_rapi_cert or rapi_cert_pem:
if new_hmac_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,
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 = {
"Alters the parameters of the cluster"),
"renew-crypto": (
RenewCrypto, ARGS_NONE,
"Renews cluster certificates, keys and secrets"),
if __name__ == '__main__':
sys.exit(GenericMain(commands, override={"tag_type": constants.TAG_CLUSTER}))
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment