diff --git a/lib/bootstrap.py b/lib/bootstrap.py index ebfad81f9329cb0e420f112109d5decfc6ca31f6..0125b095a9116ed156a19310479ec602c0414495 100644 --- a/lib/bootstrap.py +++ b/lib/bootstrap.py @@ -77,7 +77,7 @@ def GenerateHmacKey(file_name): def GenerateClusterCrypto(new_cluster_cert, new_rapi_cert, new_confd_hmac_key, - rapi_cert_pem=None): + new_cds, rapi_cert_pem=None, cds=None): """Updates the cluster certificates, keys and secrets. @type new_cluster_cert: bool @@ -86,8 +86,12 @@ def GenerateClusterCrypto(new_cluster_cert, new_rapi_cert, new_confd_hmac_key, @param new_rapi_cert: Whether to generate a new RAPI certificate @type new_confd_hmac_key: bool @param new_confd_hmac_key: Whether to generate a new HMAC key + @type new_cds: bool + @param new_cds: Whether to generate a new cluster domain secret @type rapi_cert_pem: string @param rapi_cert_pem: New RAPI certificate in PEM format + @type cds: string + @param cds: New cluster domain secret """ # noded SSL certificate @@ -122,6 +126,18 @@ def GenerateClusterCrypto(new_cluster_cert, new_rapi_cert, new_confd_hmac_key, constants.RAPI_CERT_FILE) utils.GenerateSelfSignedSslCert(constants.RAPI_CERT_FILE) + # Cluster domain secret + if cds: + logging.debug("Writing cluster domain secret to %s", + constants.CLUSTER_DOMAIN_SECRET_FILE) + utils.WriteFile(constants.CLUSTER_DOMAIN_SECRET_FILE, + data=cds, backup=True) + + elif new_cds or not os.path.exists(constants.CLUSTER_DOMAIN_SECRET_FILE): + logging.debug("Generating new cluster domain secret at %s", + constants.CLUSTER_DOMAIN_SECRET_FILE) + GenerateHmacKey(constants.CLUSTER_DOMAIN_SECRET_FILE) + def _InitGanetiServerSetup(master_name): """Setup the necessary configuration for the initial node daemon. @@ -131,7 +147,7 @@ def _InitGanetiServerSetup(master_name): """ # Generate cluster secrets - GenerateClusterCrypto(True, False, False) + GenerateClusterCrypto(True, False, False, False) result = utils.RunCmd([constants.DAEMON_UTIL, "start", constants.NODED]) if result.failed: @@ -415,6 +431,7 @@ def SetupNodeDaemon(cluster_name, node, ssh_key_check): # and then connect with ssh to set password and start ganeti-noded # note that all the below variables are sanitized at this point, # either by being constants or by the checks above + # TODO: Could this command exceed a shell's maximum command length? mycommand = ("umask 077 && " "cat > '%s' << '!EOF.' && \n" "%s!EOF.\n" diff --git a/lib/cli.py b/lib/cli.py index bb49098b4ba9379eee89e83646db2f33f6423e14..c178bb62188cddda0cff1fc3622c3d606f28bf63 100644 --- a/lib/cli.py +++ b/lib/cli.py @@ -50,6 +50,7 @@ __all__ = [ "AUTO_REPLACE_OPT", "BACKEND_OPT", "CLEANUP_OPT", + "CLUSTER_DOMAIN_SECRET_OPT", "CONFIRM_OPT", "CP_SIZE_OPT", "DEBUG_OPT", @@ -81,6 +82,7 @@ __all__ = [ "MC_OPT", "NET_OPT", "NEW_CLUSTER_CERT_OPT", + "NEW_CLUSTER_DOMAIN_SECRET_OPT", "NEW_CONFD_HMAC_KEY_OPT", "NEW_RAPI_CERT_OPT", "NEW_SECONDARY_OPT", @@ -824,7 +826,6 @@ MASTER_NETDEV_OPT = cli_option("--master-netdev", dest="master_netdev", metavar="NETDEV", default=constants.DEFAULT_BRIDGE) - GLOBAL_FILEDIR_OPT = cli_option("--file-storage-dir", dest="file_storage_dir", help="Specify the default directory (cluster-" "wide) for storing the file-based disks [%s]" % @@ -898,6 +899,18 @@ NEW_CONFD_HMAC_KEY_OPT = cli_option("--new-confd-hmac-key", help=("Create a new HMAC key for %s" % constants.CONFD)) +CLUSTER_DOMAIN_SECRET_OPT = cli_option("--cluster-domain-secret", + dest="cluster_domain_secret", + default=None, + help=("Load new new cluster domain" + " secret from file")) + +NEW_CLUSTER_DOMAIN_SECRET_OPT = cli_option("--new-cluster-domain-secret", + dest="new_cluster_domain_secret", + default=False, action="store_true", + help=("Create a new cluster domain" + " secret")) + def _ParseArgs(argv, commands, aliases): """Parser for the command line arguments. diff --git a/lib/constants.py b/lib/constants.py index 3edbd6d1015112429e8d7a98583499ad219cf76e..f8b68c47c8a3b2ee534a9511acadf8a180e0e463 100644 --- a/lib/constants.py +++ b/lib/constants.py @@ -100,6 +100,7 @@ CLUSTER_CONF_FILE = DATA_DIR + "/config.data" NODED_CERT_FILE = DATA_DIR + "/server.pem" RAPI_CERT_FILE = DATA_DIR + "/rapi.pem" CONFD_HMAC_KEY = DATA_DIR + "/hmac.key" +CLUSTER_DOMAIN_SECRET_FILE = DATA_DIR + "/cluster-domain-secret" WATCHER_STATEFILE = DATA_DIR + "/watcher.data" WATCHER_PAUSEFILE = DATA_DIR + "/watcher.pause" INSTANCE_UPFILE = RUN_GANETI_DIR + "/instance-status" diff --git a/man/gnt-cluster.sgml b/man/gnt-cluster.sgml index 450f4048c875360c17ea921ac1ca9db46c6fc29a..6ee98c2d83bb4a777477529807442199c2228602 100644 --- a/man/gnt-cluster.sgml +++ b/man/gnt-cluster.sgml @@ -715,6 +715,9 @@ <sbr> <arg choice="opt">--new-rapi-certificate</arg> <arg choice="opt">--rapi-certificate <replaceable>rapi-cert</replaceable></arg> + <sbr> + <arg choice="opt">--new-cluster-domain-secret</arg> + <arg choice="opt">--cluster-domain-secret <replaceable>filename</replaceable></arg> </cmdsynopsis> <para> @@ -726,14 +729,24 @@ 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> + </citerefentry>. + </para> + + <para> + 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> + + <para> + <option>--new-cluster-domain-secret</option> generates a new, random + cluster domain secret. <option>--cluster-domain-secret</option> reads + the secret from a file. The cluster domain secret is used to sign + information exchanged between separate clusters via a third party. + </para> </refsect2> <refsect2> diff --git a/qa/qa_cluster.py b/qa/qa_cluster.py index 5ce6252f78a0b5f753322c72f3984d468d30961e..65c975126d7d5696a5c801a9bef89fb484a0a863 100644 --- a/qa/qa_cluster.py +++ b/qa/qa_cluster.py @@ -151,10 +151,14 @@ def TestClusterRenewCrypto(): # Conflicting options cmd = ["gnt-cluster", "renew-crypto", "--force", - "--new-cluster-certificate", "--new-confd-hmac-key", - "--new-rapi-certificate", "--rapi-certificate=/dev/null"] - AssertNotEqual(StartSSH(master["primary"], - utils.ShellQuoteArgs(cmd)).wait(), 0) + "--new-cluster-certificate", "--new-confd-hmac-key"] + conflicting = [ + ["--new-rapi-certificate", "--rapi-certificate=/dev/null"], + ["--new-cluster-domain-secret", "--cluster-domain-secret=/dev/null"], + ] + for i in conflicting: + AssertNotEqual(StartSSH(master["primary"], + utils.ShellQuoteArgs(cmd + i)).wait(), 0) # Invalid RAPI certificate cmd = ["gnt-cluster", "renew-crypto", "--force", @@ -181,10 +185,27 @@ def TestClusterRenewCrypto(): AssertEqual(StartSSH(master["primary"], utils.ShellQuoteArgs(cmd)).wait(), 0) + # Custom cluster domain secret + cds_fh = tempfile.NamedTemporaryFile() + cds_fh.write(utils.GenerateSecret()) + cds_fh.write("\n") + cds_fh.flush() + + tmpcds = qa_utils.UploadFile(master["primary"], cds_fh.name) + try: + cmd = ["gnt-cluster", "renew-crypto", "--force", + "--cluster-domain-secret=%s" % tmpcds] + AssertEqual(StartSSH(master["primary"], + utils.ShellQuoteArgs(cmd)).wait(), 0) + finally: + cmd = ["rm", "-f", tmpcds] + AssertEqual(StartSSH(master["primary"], + utils.ShellQuoteArgs(cmd)).wait(), 0) + # Normal case cmd = ["gnt-cluster", "renew-crypto", "--force", "--new-cluster-certificate", "--new-confd-hmac-key", - "--new-rapi-certificate"] + "--new-rapi-certificate", "--new-cluster-domain-secret"] AssertEqual(StartSSH(master["primary"], utils.ShellQuoteArgs(cmd)).wait(), 0) diff --git a/scripts/gnt-cluster b/scripts/gnt-cluster index 50e68fe4fafd52a6765c717113bfc667cb09a83a..c51b73dd13a7476635166fa86bdb1b26a2d92e23 100755 --- a/scripts/gnt-cluster +++ b/scripts/gnt-cluster @@ -495,7 +495,8 @@ def SearchTags(opts, args): def _RenewCrypto(new_cluster_cert, new_rapi_cert, rapi_cert_filename, - new_confd_hmac_key, force): + new_confd_hmac_key, new_cds, cds_filename, + force): """Renews cluster certificates, keys and secrets. @type new_cluster_cert: bool @@ -506,6 +507,10 @@ def _RenewCrypto(new_cluster_cert, new_rapi_cert, rapi_cert_filename, @param rapi_cert_filename: Path to file containing new RAPI certificate @type new_confd_hmac_key: bool @param new_confd_hmac_key: Whether to generate a new HMAC key + @type new_cds: bool + @param new_cds: Whether to generate a new cluster domain secret + @type cds_filename: string + @param cds_filename: Path to file containing new cluster domain secret @type force: bool @param force: Whether to ask user for confirmation @@ -515,6 +520,12 @@ def _RenewCrypto(new_cluster_cert, new_rapi_cert, rapi_cert_filename, " options can be specified at the same time.") return 1 + if new_cds and cds_filename: + ToStderr("Only one of the --new-cluster-domain-secret and" + " --cluster-domain-secret options can be specified at" + " the same time.") + return 1 + if rapi_cert_filename: # Read and verify new certificate try: @@ -537,6 +548,16 @@ def _RenewCrypto(new_cluster_cert, new_rapi_cert, rapi_cert_filename, else: rapi_cert_pem = None + if cds_filename: + try: + cds = utils.ReadFile(cds_filename) + except Exception, err: # pylint: disable-msg=W0703 + ToStderr("Can't load new cluster domain secret from %s: %s" % + (cds_filename, str(err))) + return 1 + else: + cds = None + if not force: usertext = ("This requires all daemons on all nodes to be restarted and" " may take some time. Continue?") @@ -547,7 +568,9 @@ def _RenewCrypto(new_cluster_cert, new_rapi_cert, rapi_cert_filename, ctx.feedback_fn("Updating certificates and keys") bootstrap.GenerateClusterCrypto(new_cluster_cert, new_rapi_cert, new_confd_hmac_key, - rapi_cert_pem=rapi_cert_pem) + new_cds, + rapi_cert_pem=rapi_cert_pem, + cds=cds) files_to_copy = [] @@ -560,6 +583,9 @@ def _RenewCrypto(new_cluster_cert, new_rapi_cert, rapi_cert_filename, if new_confd_hmac_key: files_to_copy.append(constants.CONFD_HMAC_KEY) + if new_cds or cds: + files_to_copy.append(constants.CLUSTER_DOMAIN_SECRET_FILE) + if files_to_copy: for node_name in ctx.nonmaster_nodes: ctx.feedback_fn("Copying %s to %s" % @@ -583,6 +609,8 @@ def RenewCrypto(opts, args): opts.new_rapi_cert, opts.rapi_cert, opts.new_confd_hmac_key, + opts.new_cluster_domain_secret, + opts.cluster_domain_secret, opts.force) @@ -789,7 +817,8 @@ commands = { "renew-crypto": ( RenewCrypto, ARGS_NONE, [NEW_CLUSTER_CERT_OPT, NEW_RAPI_CERT_OPT, RAPI_CERT_OPT, - NEW_CONFD_HMAC_KEY_OPT, FORCE_OPT], + NEW_CONFD_HMAC_KEY_OPT, FORCE_OPT, + NEW_CLUSTER_DOMAIN_SECRET_OPT, CLUSTER_DOMAIN_SECRET_OPT], "[opts...]", "Renews cluster certificates, keys and secrets"), } diff --git a/tools/cfgupgrade b/tools/cfgupgrade index fa6a8194dfc69b0121901dbba8867edbbe403b1c..c9db1ce15d5abe9147ae60945e80266d820f9f18 100755 --- a/tools/cfgupgrade +++ b/tools/cfgupgrade @@ -174,7 +174,7 @@ def main(): backup=True) if not options.dry_run: - bootstrap.GenerateClusterCrypto(False, False, False) + bootstrap.GenerateClusterCrypto(False, False, False, False) except: logging.critical("Writing configuration failed. It is probably in an"