Commit 6d9446fa authored by Petr Pudlak's avatar Petr Pudlak

Merge branch 'stable-2.13' into stable-2.14

* stable-2.13
  Describe --no-verify-disks option in watcher man page
  Make disk verification optional

* stable-2.12
  Handle SSL setup when downgrading
  Write SSH ports to ssconf files
  Noded: Consider certificate chain in callback
  Cluster-keys-replacement: update documentation
  Backend: Use timestamp as serial no for server cert
  UPGRADE: add note about 2.12.5
  NEWS: Mention issue 1094
  man: mention changes in renew-crypto
  Verify: warn about self-signed client certs
  Bootstrap: validate SSL setup before starting noded
  Clean up configuration of curl request
  Renew-crypto: remove superflous copying of node certs
  Renew-crypto: propagate verbose and debug option
  Noded: log the certificate and digest on noded startup
  QA: reload rapi cert after renew crypto
  Prepare-node-join: use common functions
  Renew-crypto: remove dead code
  Init: add master client certificate to configuration
  Renew-crypto: rebuild digest map of all nodes
  Noded: make "bootstrap" a constant
  node-daemon-setup: generate client certificate
  tools: Move (Re)GenerateClientCert to common
  Renew cluster and client certificates together
  Init: create the master's client cert in bootstrap
  Renew client certs using ssl_update tool
  Run functions while (some) daemons are stopped
  Back up old client.pem files
  Introduce ssl_update tool
  x509 function for creating signed certs
  Add tools/common.py from 2.13
  Consider ECDSA in SSH setup
  Update documentation of watcher and RAPI daemon
  Watcher: add option for setting RAPI IP
  When connecting to Metad fails, log the full stack trace
  Set up the Metad client with allow_non_master
  Set up the configuration client properly on non-masters
  Add the 'allow_non_master' option to the WConfd RPC client
  Add the option to disable master checks to the RPC client
  Add 'allow_non_master' to the Luxi test transport class too
  Add 'allow_non_master' to FdTransport for compatibility
  Properly document all constructor arguments of Transport
  Allow the Transport class to be used for non-master nodes
  Don't define the set of all daemons twice

Conflicts:
	Makefile.am
	lib/cmdlib/cluster/verify.py
	lib/config/__init__.py
	tools/cfgupgrade

Resolution:
	Makefile.am
          - keep newly added files from both branches
	lib/cmdlib/cluster/verify.py
          - propagate relevant changes from/lib/cmdlib/cluster.py to
            lib/cmdlib/cluster/__init__.py
	lib/config/__init__.py
          - include methods added in stable-2.13
          - temporarily disable the warning for too many lines
	tools/cfgupgrade
          - propagate changes to lib/tools/cfgupgrade.py
Signed-off-by: default avatarPetr Pudlak <pudlak@google.com>
Reviewed-by: default avatarHelga Velroyen <helgav@google.com>
parents e86e6b3e 7f1ac87a
......@@ -335,6 +335,7 @@ CLEANFILES = \
tools/vif-ganeti-metad \
tools/net-common \
tools/users-setup \
tools/ssl-update \
tools/vcluster-setup \
tools/prepare-node-join \
tools/ssh-update \
......@@ -593,12 +594,13 @@ rpc_stub_PYTHON = \
pytools_PYTHON = \
lib/tools/__init__.py \
lib/tools/burnin.py \
lib/tools/common.py \
lib/tools/ensure_dirs.py \
lib/tools/node_cleanup.py \
lib/tools/node_daemon_setup.py \
lib/tools/prepare_node_join.py \
lib/tools/common.py \
lib/tools/ssh_update.py \
lib/tools/ssl_update.py \
lib/tools/cfgupgrade.py
utils_PYTHON = \
......@@ -1259,7 +1261,8 @@ PYTHON_BOOTSTRAP = \
tools/node-cleanup \
tools/node-daemon-setup \
tools/prepare-node-join \
tools/ssh-update
tools/ssh-update \
tools/ssl-update
qa_scripts = \
qa/__init__.py \
......@@ -1500,7 +1503,8 @@ nodist_pkglib_python_scripts = \
tools/ensure-dirs \
tools/node-daemon-setup \
tools/prepare-node-join \
tools/ssh-update
tools/ssh-update \
tools/ssl-update
pkglib_python_basenames = \
$(patsubst daemons/%,%,$(patsubst tools/%,%,\
......@@ -2438,6 +2442,7 @@ tools/node-daemon-setup: MODULE = ganeti.tools.node_daemon_setup
tools/prepare-node-join: MODULE = ganeti.tools.prepare_node_join
tools/ssh-update: MODULE = ganeti.tools.ssh_update
tools/node-cleanup: MODULE = ganeti.tools.node_cleanup
tools/ssl-update: MODULE = ganeti.tools.ssl_update
$(HS_BUILT_TEST_HELPERS): TESTROLE = $(patsubst test/hs/%,%,$@)
$(PYTHON_BOOTSTRAP) $(gnt_scripts) $(gnt_python_sbin_SCRIPTS): Makefile | stamp-directories
......
......@@ -67,6 +67,14 @@ to replace all SSH key pairs of non-master nodes' with the master node's SSH
key pair.
2.12
----
Due to issue #1094 in Ganeti 2.11 and 2.12 up to version 2.12.4, we
advise to rerun 'gnt-cluster renew-crypto --new-node-certificates'
after an upgrade to 2.12.5 or higher.
2.11
----
......
......@@ -24,13 +24,30 @@ don't forget to use "shred" to remove files securely afterwards).
Replacing SSL keys
==================
The cluster SSL key is stored in ``/var/lib/ganeti/server.pem``.
The cluster-wide SSL key is stored in ``/var/lib/ganeti/server.pem``.
Besides that, since Ganeti 2.11, each node has an individual node
SSL key, which is stored in ``/var/lib/ganeti/client.pem``. This
client certificate is signed by the cluster-wide SSL certficate.
Run the following command to generate a new key::
To renew the individual node certificates, run this command::
gnt-cluster renew-crypto --new-node-certificates
Run the following command to generate a new cluster-wide certificate::
gnt-cluster renew-crypto --new-cluster-certificate
# Older version, which don't have this command, can instead use:
Note that this triggers both, the renewal of the cluster certificate
as well as the renewal of the individual node certificate. The reason
for this is that the node certificates are signed by the cluster
certificate and thus they need to be renewed and signed as soon as
the changes certificate changes. Therefore, the command above is
equivalent to::
gnt-cluster renew-crypto --new-cluster-certificate --new-node-certificates
On older versions, which don't have this command, use this instead::
chmod 0600 /var/lib/ganeti/server.pem &&
openssl req -new -newkey rsa:1024 -days 1825 -nodes \
-x509 -keyout /var/lib/ganeti/server.pem \
......@@ -42,6 +59,10 @@ Run the following command to generate a new key::
gnt-cluster command /etc/init.d/ganeti restart
Note that older versions don't have individual node certificates and thus
one does not have to handle the creation and distribution of them.
Replacing SSH keys
==================
......
......@@ -943,6 +943,12 @@ def _VerifyNodeInfo(what, vm_capable, result, all_hvparams):
def _VerifyClientCertificate(cert_file=pathutils.NODED_CLIENT_CERT_FILE):
"""Verify the existance and validity of the client SSL certificate.
Also, verify that the client certificate is not self-signed. Self-
signed client certificates stem from Ganeti versions 2.12.0 - 2.12.4
and should be replaced by client certificates signed by the server
certificate. Hence we output a warning when we encounter a self-signed
one.
"""
create_cert_cmd = "gnt-cluster renew-crypto --new-node-certificates"
if not os.path.exists(cert_file):
......@@ -953,9 +959,13 @@ def _VerifyClientCertificate(cert_file=pathutils.NODED_CLIENT_CERT_FILE):
(errcode, msg) = utils.VerifyCertificate(cert_file)
if errcode is not None:
return (errcode, msg)
else:
# if everything is fine, we return the digest to be compared to the config
return (None, utils.GetCertificateDigest(cert_filename=cert_file))
(errcode, msg) = utils.IsCertificateSelfSigned(cert_file)
if errcode is not None:
return (errcode, msg)
# if everything is fine, we return the digest to be compared to the config
return (None, utils.GetCertificateDigest(cert_filename=cert_file))
def _VerifySshSetup(node_status_list, my_name,
......@@ -1354,13 +1364,8 @@ def GetCryptoTokens(token_requests):
@return: list of tuples of the token type and the public crypto token
"""
getents = runtime.GetEnts()
_VALID_CERT_FILES = [pathutils.NODED_CERT_FILE,
pathutils.NODED_CLIENT_CERT_FILE,
pathutils.NODED_CLIENT_CERT_FILE_TMP]
_DEFAULT_CERT_FILE = pathutils.NODED_CLIENT_CERT_FILE
tokens = []
for (token_type, action, options) in token_requests:
for (token_type, action, _) in token_requests:
if token_type not in constants.CRYPTO_TYPES:
raise errors.ProgrammerError("Token type '%s' not supported." %
token_type)
......@@ -1368,46 +1373,8 @@ def GetCryptoTokens(token_requests):
raise errors.ProgrammerError("Action '%s' is not supported." %
action)
if token_type == constants.CRYPTO_TYPE_SSL_DIGEST:
if action == constants.CRYPTO_ACTION_CREATE:
# extract file name from options
cert_filename = None
if options:
cert_filename = options.get(constants.CRYPTO_OPTION_CERT_FILE)
if not cert_filename:
cert_filename = _DEFAULT_CERT_FILE
# For security reason, we don't allow arbitrary filenames
if not cert_filename in _VALID_CERT_FILES:
raise errors.ProgrammerError(
"The certificate file name path '%s' is not allowed." %
cert_filename)
# extract serial number from options
serial_no = None
if options:
try:
serial_no = int(options[constants.CRYPTO_OPTION_SERIAL_NO])
except ValueError:
raise errors.ProgrammerError(
"The given serial number is not an intenger: %s." %
options.get(constants.CRYPTO_OPTION_SERIAL_NO))
except KeyError:
raise errors.ProgrammerError("No serial number was provided.")
if not serial_no:
raise errors.ProgrammerError(
"Cannot create an SSL certificate without a serial no.")
utils.GenerateNewSslCert(
True, cert_filename, serial_no,
"Create new client SSL certificate in %s." % cert_filename,
uid=getents.masterd_uid, gid=getents.masterd_gid)
tokens.append((token_type,
utils.GetCertificateDigest(
cert_filename=cert_filename)))
elif action == constants.CRYPTO_ACTION_GET:
tokens.append((token_type,
utils.GetCertificateDigest()))
tokens.append((token_type,
utils.GetCertificateDigest()))
return tokens
......@@ -3028,7 +2995,8 @@ def ModifyInstanceMetadata(metadata):
raise errors.HypervisorError("Failed to start metadata daemon")
def _Connect():
return transport.Transport(pathutils.SOCKET_DIR + "/ganeti-metad")
return transport.Transport(pathutils.SOCKET_DIR + "/ganeti-metad",
allow_non_master=True)
retries = 5
......@@ -3040,6 +3008,8 @@ def ModifyInstanceMetadata(metadata):
raise TimeoutError("Connection to metadata daemon timed out")
except (socket.error, NoMasterError), err:
if retries == 0:
logging.error("Failed to connect to the metadata daemon",
exc_info=True)
raise TimeoutError("Failed to connect to metadata daemon: %s" % err)
else:
retries -= 1
......@@ -4880,9 +4850,11 @@ def CreateX509Certificate(validity, cryptodir=pathutils.CRYPTO_KEYS_DIR):
@return: Certificate name and public part
"""
serial_no = int(time.time())
(key_pem, cert_pem) = \
utils.GenerateSelfSignedX509Cert(netutils.Hostname.GetSysName(),
min(validity, _MAX_SSL_CERT_VALIDITY), 1)
min(validity, _MAX_SSL_CERT_VALIDITY),
serial_no)
cert_dir = tempfile.mkdtemp(dir=cryptodir,
prefix="x509-%s-" % utils.TimestampForFilename())
......
......@@ -79,10 +79,12 @@ def GenerateHmacKey(file_name):
# pylint: disable=R0913
def GenerateClusterCrypto(new_cluster_cert, new_rapi_cert, new_spice_cert,
new_confd_hmac_key, new_cds,
new_confd_hmac_key, new_cds, new_client_cert,
master_name,
rapi_cert_pem=None, spice_cert_pem=None,
spice_cacert_pem=None, cds=None,
nodecert_file=pathutils.NODED_CERT_FILE,
clientcert_file=pathutils.NODED_CLIENT_CERT_FILE,
rapicert_file=pathutils.RAPI_CERT_FILE,
spicecert_file=pathutils.SPICE_CERT_FILE,
spicecacert_file=pathutils.SPICE_CACERT_FILE,
......@@ -100,6 +102,10 @@ def GenerateClusterCrypto(new_cluster_cert, new_rapi_cert, new_spice_cert,
@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 new_client_cert: bool
@param new_client_cert: Whether to generate a new client certificate
@type master_name: string
@param master_name: FQDN of the master node
@type rapi_cert_pem: string
@param rapi_cert_pem: New RAPI certificate in PEM format
@type spice_cert_pem: string
......@@ -127,6 +133,12 @@ def GenerateClusterCrypto(new_cluster_cert, new_rapi_cert, new_spice_cert,
new_cluster_cert, nodecert_file, 1,
"Generating new cluster certificate at %s" % nodecert_file)
# If the cluster certificate was renewed, the client cert has to be
# renewed and resigned.
if new_cluster_cert or new_client_cert:
utils.GenerateNewClientSslCert(clientcert_file, nodecert_file,
master_name)
# confd HMAC key
if new_confd_hmac_key or not os.path.exists(hmackey_file):
logging.debug("Writing new confd HMAC key to %s", hmackey_file)
......@@ -177,7 +189,7 @@ def GenerateClusterCrypto(new_cluster_cert, new_rapi_cert, new_spice_cert,
GenerateHmacKey(cds_file)
def _InitGanetiServerSetup(master_name):
def _InitGanetiServerSetup(master_name, cfg):
"""Setup the necessary configuration for the initial node daemon.
This creates the nodepass file containing the shared password for
......@@ -185,11 +197,34 @@ def _InitGanetiServerSetup(master_name):
@type master_name: str
@param master_name: Name of the master node
@type cfg: ConfigWriter
@param cfg: the configuration writer
"""
# Generate cluster secrets
GenerateClusterCrypto(True, False, False, False, False)
GenerateClusterCrypto(True, False, False, False, False, False, master_name)
# Add the master's SSL certificate digest to the configuration.
master_uuid = cfg.GetMasterNode()
master_digest = utils.GetCertificateDigest()
cfg.AddNodeToCandidateCerts(master_uuid, master_digest)
cfg.Update(cfg.GetClusterInfo(), logging.error)
ssconf.WriteSsconfFiles(cfg.GetSsconfValues())
if not os.path.exists(os.path.join(pathutils.DATA_DIR,
"%s%s" % (constants.SSCONF_FILEPREFIX,
constants.SS_MASTER_CANDIDATES_CERTS))):
raise errors.OpExecError("Ssconf file for master candidate certificates"
" was not written.")
if not os.path.exists(pathutils.NODED_CERT_FILE):
raise errors.OpExecError("The server certficate was not created properly.")
if not os.path.exists(pathutils.NODED_CLIENT_CERT_FILE):
raise errors.OpExecError("The client certificate was not created"
" properly.")
# set up the inter-node password and certificate
result = utils.RunCmd([pathutils.DAEMON_UTIL, "start", constants.NODED])
if result.failed:
raise errors.OpExecError("Could not start the node daemon, command %s"
......@@ -780,7 +815,7 @@ def InitCluster(cluster_name, mac_prefix, # pylint: disable=R0913, R0914
if modify_ssh_setup:
ssh.InitPubKeyFile(master_uuid)
# set up the inter-node password and certificate
_InitGanetiServerSetup(hostname.name)
_InitGanetiServerSetup(hostname.name, cfg)
logging.debug("Starting daemons")
result = utils.RunCmd([pathutils.DAEMON_UTIL, "start-all"])
......@@ -897,6 +932,7 @@ def SetupNodeDaemon(opts, cluster_name, node, ssh_port):
utils.ReadFile(pathutils.NODED_CERT_FILE),
constants.NDS_SSCONF: ssconf.SimpleStore().ReadAll(),
constants.NDS_START_NODE_DAEMON: True,
constants.NDS_NODE_NAME: node,
}
ssh.RunSshCmdWithStdin(cluster_name, node, pathutils.NODE_DAEMON_SETUP,
......
......@@ -80,6 +80,7 @@ __all__ = [
"JobSubmittedException",
"ParseTimespec",
"RunWhileClusterStopped",
"RunWhileDaemonsStopped",
"SubmitOpCode",
"SubmitOpCodeToDrainedQueue",
"SubmitOrSend",
......@@ -1460,12 +1461,13 @@ def GenericInstanceCreate(mode, opts, args):
return 0
class _RunWhileClusterStoppedHelper(object):
"""Helper class for L{RunWhileClusterStopped} to simplify state management
class _RunWhileDaemonsStoppedHelper(object):
"""Helper class for L{RunWhileDaemonsStopped} to simplify state management
"""
def __init__(self, feedback_fn, cluster_name, master_node,
online_nodes, ssh_ports):
online_nodes, ssh_ports, exclude_daemons, debug,
verbose):
"""Initializes this class.
@type feedback_fn: callable
......@@ -1478,6 +1480,14 @@ class _RunWhileClusterStoppedHelper(object):
@param online_nodes: List of names of online nodes
@type ssh_ports: list
@param ssh_ports: List of SSH ports of online nodes
@type exclude_daemons: list of string
@param exclude_daemons: list of daemons to shutdown
@param exclude_daemons: list of daemons that will be restarted after
all others are shutdown
@type debug: boolean
@param debug: show debug output
@type verbose: boolesn
@param verbose: show verbose output
"""
self.feedback_fn = feedback_fn
......@@ -1491,6 +1501,10 @@ class _RunWhileClusterStoppedHelper(object):
self.nonmaster_nodes = [name for name in online_nodes
if name != master_node]
self.exclude_daemons = exclude_daemons
self.debug = debug
self.verbose = verbose
assert self.master_node not in self.nonmaster_nodes
def _RunCmd(self, node_name, cmd):
......@@ -1542,6 +1556,13 @@ class _RunWhileClusterStoppedHelper(object):
for node_name in self.online_nodes:
self.feedback_fn("Stopping daemons on %s" % node_name)
self._RunCmd(node_name, [pathutils.DAEMON_UTIL, "stop-all"])
# Starting any daemons listed as exception
for daemon in self.exclude_daemons:
if (daemon in constants.DAEMONS_MASTER and
node_name != self.master_node):
continue
self.feedback_fn("Starting daemon '%s' on %s" % (daemon, node_name))
self._RunCmd(node_name, [pathutils.DAEMON_UTIL, "start", daemon])
# All daemons are shut down now
try:
......@@ -1554,18 +1575,31 @@ class _RunWhileClusterStoppedHelper(object):
finally:
# Start cluster again, master node last
for node_name in self.nonmaster_nodes + [self.master_node]:
# Stopping any daemons listed as exception.
# This might look unnecessary, but it makes sure that daemon-util
# starts all daemons in the right order.
for daemon in self.exclude_daemons:
if (daemon in constants.DAEMONS_MASTER and
node_name != self.master_node):
continue
self.feedback_fn("Stopping daemon '%s' on %s" % (daemon, node_name))
self._RunCmd(node_name, [pathutils.DAEMON_UTIL, "stop", daemon])
self.feedback_fn("Starting daemons on %s" % node_name)
self._RunCmd(node_name, [pathutils.DAEMON_UTIL, "start-all"])
finally:
# Resume watcher
watcher_block.Close()
def RunWhileClusterStopped(feedback_fn, fn, *args):
def RunWhileDaemonsStopped(feedback_fn, exclude_daemons, fn, *args, **kwargs):
"""Calls a function while all cluster daemons are stopped.
@type feedback_fn: callable
@param feedback_fn: Feedback function
@type exclude_daemons: list of string
@param exclude_daemons: list of daemons that are NOT stopped. If None,
all daemons will be stopped.
@type fn: callable
@param fn: Function to be called when daemons are stopped
......@@ -1585,9 +1619,27 @@ def RunWhileClusterStopped(feedback_fn, fn, *args):
del cl
assert master_node in online_nodes
if exclude_daemons is None:
exclude_daemons = []
debug = kwargs.get("debug", False)
verbose = kwargs.get("verbose", False)
return _RunWhileDaemonsStoppedHelper(
feedback_fn, cluster_name, master_node, online_nodes, ssh_ports,
exclude_daemons, debug, verbose).Call(fn, *args)
def RunWhileClusterStopped(feedback_fn, fn, *args):
"""Calls a function while all cluster daemons are stopped.
@type feedback_fn: callable
@param feedback_fn: Feedback function
@type fn: callable
@param fn: Function to be called when daemons are stopped
return _RunWhileClusterStoppedHelper(feedback_fn, cluster_name, master_node,
online_nodes, ssh_ports).Call(fn, *args)
"""
RunWhileDaemonsStopped(feedback_fn, None, fn, *args)
def GenerateTable(headers, fields, separator, data,
......
......@@ -46,6 +46,7 @@ from ganeti.cli import *
from ganeti import bootstrap
from ganeti import compat
from ganeti import constants
from ganeti import config
from ganeti import errors
from ganeti import netutils
from ganeti import objects
......@@ -966,7 +967,8 @@ def _ReadAndVerifyCert(cert_filename, verify_private_key=False):
def _RenewCrypto(new_cluster_cert, new_rapi_cert, # pylint: disable=R0911
rapi_cert_filename, new_spice_cert, spice_cert_filename,
spice_cacert_filename, new_confd_hmac_key, new_cds,
cds_filename, force, new_node_cert, new_ssh_keys):
cds_filename, force, new_node_cert, new_ssh_keys,
verbose, debug):
"""Renews cluster certificates, keys and secrets.
@type new_cluster_cert: bool
......@@ -994,6 +996,10 @@ def _RenewCrypto(new_cluster_cert, new_rapi_cert, # pylint: disable=R0911
@param new_node_cert: Whether to generate new node certificates
@type new_ssh_keys: bool
@param new_ssh_keys: Whether to generate new node SSH keys
@type verbose: boolean
@param verbose: show verbose output
@type debug: boolean
@param debug: show debug output
"""
ToStdout("Updating certificates now. Running \"gnt-cluster verify\" "
......@@ -1048,13 +1054,15 @@ def _RenewCrypto(new_cluster_cert, new_rapi_cert, # pylint: disable=R0911
return 1
def _RenewCryptoInner(ctx):
ctx.feedback_fn("Updating cluster-wide certificates and keys")
# Note: the node certificate will be generated in the LU
bootstrap.GenerateClusterCrypto(new_cluster_cert,
ctx.feedback_fn("Updating certificates and keys")
bootstrap.GenerateClusterCrypto(False,
new_rapi_cert,
new_spice_cert,
new_confd_hmac_key,
new_cds,
False,
None,
rapi_cert_pem=rapi_cert_pem,
spice_cert_pem=spice_cert_pem,
spice_cacert_pem=spice_cacert_pem,
......@@ -1062,9 +1070,6 @@ def _RenewCrypto(new_cluster_cert, new_rapi_cert, # pylint: disable=R0911
files_to_copy = []
if new_cluster_cert:
files_to_copy.append(pathutils.NODED_CERT_FILE)
if new_rapi_cert or rapi_cert_pem:
files_to_copy.append(pathutils.RAPI_CERT_FILE)
......@@ -1086,14 +1091,102 @@ def _RenewCrypto(new_cluster_cert, new_rapi_cert, # pylint: disable=R0911
for file_name in files_to_copy:
ctx.ssh.CopyFileToNode(node_name, port, file_name)
RunWhileClusterStopped(ToStdout, _RenewCryptoInner)
if new_node_cert or new_ssh_keys:
def _RenewClientCerts(ctx):
ctx.feedback_fn("Updating client SSL certificates.")
cluster_name = ssconf.SimpleStore().GetClusterName()
for node_name in ctx.nonmaster_nodes + [ctx.master_node]:
ssh_port = ctx.ssh_ports[node_name]
data = {
constants.NDS_CLUSTER_NAME: cluster_name,
constants.NDS_NODE_DAEMON_CERTIFICATE:
utils.ReadFile(pathutils.NODED_CERT_FILE),
constants.NDS_NODE_NAME: node_name,
constants.NDS_ACTION: constants.CRYPTO_ACTION_CREATE,
}
ssh.RunSshCmdWithStdin(
cluster_name,
node_name,
pathutils.SSL_UPDATE,
ssh_port,
data,
debug=ctx.debug,
verbose=ctx.verbose,
use_cluster_key=True,
ask_key=False,
strict_host_check=True)
# Create a temporary ssconf file using the master's client cert digest
# and the 'bootstrap' keyword to enable distribution of all nodes' digests.
master_digest = utils.GetCertificateDigest()
ssconf_master_candidate_certs_filename = os.path.join(
pathutils.DATA_DIR, "%s%s" %
(constants.SSCONF_FILEPREFIX, constants.SS_MASTER_CANDIDATES_CERTS))
utils.WriteFile(
ssconf_master_candidate_certs_filename,
data="%s=%s" % (constants.CRYPTO_BOOTSTRAP, master_digest))
for node_name in ctx.nonmaster_nodes:
port = ctx.ssh_ports[node_name]
ctx.feedback_fn("Copying %s to %s:%d" %
(ssconf_master_candidate_certs_filename, node_name, port))
ctx.ssh.CopyFileToNode(node_name, port,
ssconf_master_candidate_certs_filename)
# Write the boostrap entry to the config using wconfd.
config_live_lock = utils.livelock.LiveLock("renew_crypto")
cfg = config.GetConfig(None, config_live_lock)
cfg.AddNodeToCandidateCerts(constants.CRYPTO_BOOTSTRAP, master_digest)
cfg.Update(cfg.GetClusterInfo(), ctx.feedback_fn)
def _RenewServerAndClientCerts(ctx):
ctx.feedback_fn("Updating the cluster SSL certificate.")
master_name = ssconf.SimpleStore().GetMasterNode()
bootstrap.GenerateClusterCrypto(True, # cluster cert
False, # rapi cert
False, # spice cert
False, # confd hmac key
False, # cds
True, # client cert
master_name)
for node_name in ctx.nonmaster_nodes:
port = ctx.ssh_ports[node_name]
server_cert = pathutils.NODED_CERT_FILE
ctx.feedback_fn("Copying %s to %s:%d" %
(server_cert, node_name, port))
ctx.ssh.CopyFileToNode(node_name, port, server_cert)
_RenewClientCerts(ctx)
if new_cluster_cert or new_rapi_cert or new_spice_cert \
or new_confd_hmac_key or new_cds:
RunWhileClusterStopped(ToStdout, _RenewCryptoInner)
# If only node certficates are recreated, call _RenewClientCerts only.
if new_node_cert and not new_cluster_cert:
RunWhileDaemonsStopped(ToStdout, [constants.NODED, constants.WCONFD],
_RenewClientCerts, verbose=verbose, debug=debug)
# If the cluster certificate are renewed, the client certificates need
# to be renewed too.
if new_cluster_cert:
RunWhileDaemonsStopped(ToStdout, [constants.NODED, constants.WCONFD],
_RenewServerAndClientCerts, verbose=verbose,
debug=debug)
if new_node_cert or new_cluster_cert or new_ssh_keys:
cl = GetClient()
renew_op = opcodes.OpClusterRenewCrypto(node_certificates=new_node_cert,
ssh_keys=new_ssh_keys)
renew_op = opcodes.OpClusterRenewCrypto(
node_certificates=new_node_cert or new_cluster_cert,
ssh_keys=new_ssh_keys)
SubmitOpCode(renew_op, cl=cl)
ToStdout("All requested certificates and keys have been replaced."
" Running \"gnt-cluster verify\" now is recommended.")
return 0
......@@ -1162,7 +1255,9 @@ def RenewCrypto(opts, args):
opts.cluster_domain_secret,
opts.force,
opts.new_node_cert,
opts.new_ssh_keys)
opts.new_ssh_keys,
opts.verbose,
opts.debug > 0)
def _GetEnabledDiskTemplates(opts):
......@@ -2086,6 +2181,7 @@ def _VersionSpecificDowngrade():
@return: True upon success
"""
ToStdout("Performing version-specific downgrade tasks.")
return True
......@@ -2409,7 +2505,8 @@ commands = {
NEW_CONFD_HMAC_KEY_OPT, FORCE_OPT,
NEW_CLUSTER_DOMAIN_SECRET_OPT, CLUSTER_DOMAIN_SECRET_OPT,
NEW_SPICE_CERT_OPT, SPICE_CERT_OPT, SPICE_CACERT_OPT,
NEW_NODE_CERT_OPT, NEW_SSH_KEY_OPT, NOSSH_KEYCHECK_OPT],
NEW_NODE_CERT_OPT, NEW_SSH_KEY_OPT, NOSSH_KEYCHECK_OPT,
VERBOSE_OPT],
"[opts...]",
"Renews cluster certificates, keys and secrets"),
"epo": (
......
......@@ -65,44 +65,13 @@ from ganeti.cmdlib.common import ShareAll, RunPostHook, \
CheckOSParams, CheckHVParams, AdjustCandidatePool, CheckNodePVs, \
ComputeIPolicyInstanceViolation, AnnotateDiskParams, SupportsOob, \
CheckIpolicyVsDiskTemplates, CheckDiskAccessModeValidity, \
CheckDiskAccessModeConsistency, CreateNewClientCert, \
CheckDiskAccessModeConsistency, GetClientCertDigest, \
AddInstanceCommunicationNetworkOp, ConnectInstanceCommunicationNetworkOp, \
CheckImageValidity, \
CheckDiskAccessModeConsistency, CreateNewClientCert, EnsureKvmdOnNodes
CheckImageValidity, CheckDiskAccessModeConsistency, EnsureKvmdOnNodes
import ganeti.masterd.instance
def _UpdateMasterClientCert(
lu, cfg, master_uuid,
client_cert=pathutils.NODED_CLIENT_CERT_FILE,
client_cert_tmp=pathutils.NODED_CLIENT_CERT_FILE_TMP):
"""Renews the master's client certificate and propagates the config.
@type lu: C{LogicalUnit}
@param lu: the logical unit holding the config
@type cfg: C{config.ConfigWriter}
@param cfg: the cluster's configuration
@type master_uuid: string
@param master_uuid: the master node's UUID
@type client_cert: string
@param client_cert: the path of the client certificate
@type client_cert_tmp: string
@param client_cert_tmp: the temporary path of the client certificate
@rtype: string
@return: the digest of the newly created client certificate
"""
client_digest = CreateNewClientCert(lu, master_uuid, filename=client_cert_tmp)
cfg.AddNodeToCandidateCerts(master_uuid, client_digest)
# This triggers an update of the config and distribution of it with the old
# SSL certificate
utils.RemoveFile(client_cert)
utils.RenameFile(client_cert_tmp, client_cert)
return client_digest
class LUClusterRenewCrypto(NoHooksLU):
"""Renew the cluster's crypto tokens.
......@@ -137,76 +106,58 @@ class LUClusterRenewCrypto(NoHooksLU):
"""
master_uuid = self.cfg.GetMasterNode()
cluster = self.cfg.GetClusterInfo()
server_digest = utils.GetCertificateDigest(
cert_filename=pathutils.NODED_CERT_FILE)
self.cfg.AddNodeToCandidateCerts("%s-SERVER" % master_uuid,
server_digest)
try:
old_master_digest = utils.GetCertificateDigest(
cert_filename=pathutils.NODED_CLIENT_CERT_FILE)
self.cfg.AddNodeToCandidateCerts("%s-OLDMASTER" % master_uuid,
old_master_digest)
except IOError:
logging.info("No old certificate available.")
logging.debug("Renewing the master's SSL node certificate."
" Master's UUID: %s.", master_uuid)
last_exception = None
for _ in range(self._MAX_NUM_RETRIES):
try:
# Technically it should not be necessary to set the cert
# paths. However, due to a bug in the mock library, we
# have to do this to be able to test the function properly.
_UpdateMasterClientCert(
self, self.cfg, master_uuid,
client_cert=pathutils.NODED_CLIENT_CERT_FILE,
client_cert_tmp=pathutils.NODED_CLIENT_CERT_FILE_TMP)
break