This patch adds the '--new-ssh-keys' option to 'gnt-cluster renew-crypto'. In the client, it retrieves all current ssh keys and (re-)writes the 'ganeti_pub_key' file with it, then in the backend, the new keys are generated and distributed.
Signed-off-by: Helga Velroyen <hel...@google.com> --- UPGRADE | 21 +++++ lib/backend.py | 113 +++++++++++++++++++++++++- lib/cli.py | 24 ++++++ lib/client/gnt_cluster.py | 72 ++++++++++++++-- lib/cmdlib/cluster.py | 52 ++++++++++++ lib/rpc_defs.py | 7 ++ lib/server/noded.py | 11 +++ lib/tools/ssh_update.py | 28 ++++++- qa/qa_cluster.py | 7 +- src/Ganeti/Constants.hs | 12 ++- src/Ganeti/OpCodes.hs | 1 + src/Ganeti/OpParams.hs | 7 ++ test/hs/Test/Ganeti/OpCodes.hs | 3 +- test/py/ganeti.backend_unittest.py | 24 +++++- test/py/ganeti.client.gnt_cluster_unittest.py | 108 ++++++++++++++++++++++++ test/py/ganeti.mcpu_unittest.py | 1 - tools/post-upgrade | 8 ++ 17 files changed, 483 insertions(+), 16 deletions(-) diff --git a/UPGRADE b/UPGRADE index 62624ff..2186298 100644 --- a/UPGRADE +++ b/UPGRADE @@ -39,6 +39,27 @@ the Ganeti binaries should happen in the same way as for all other binaries on your system. +2.13 +---- + +When upgrading to 2.13, first apply the instructions of ``2.11 and +above``. 2.13 comes with the new feature of enhanced SSH security +through individual SSH keys. This features needs to be enabled +after the upgrade by:: + + $ gnt-cluster renew-crypt --new-ssh-keys --no-ssh-key-check + +Note that new SSH keys are generated automatically without warning when +upgrading with ``gnt-cluster upgrade``. + +If you instructed Ganeti to not touch the SSH setup (by using the +``--no-ssh-init`` option of ``gnt-cluster init``, the changes in the +handling of SSH keys will not affect your cluster. + +If you want to be prompted for each newly created SSH key, leave out +the ``--no-ssh-key-check`` option in the command listed above. + + 2.11 ---- diff --git a/lib/backend.py b/lib/backend.py index efdc00a..4a6ad04 100644 --- a/lib/backend.py +++ b/lib/backend.py @@ -1471,7 +1471,7 @@ def AddNodeSshKey(node_uuid, node_name, pot_mc_data = copy.deepcopy(base_data) if to_public_keys: pot_mc_data[constants.SSHS_SSH_PUBLIC_KEYS] = \ - (constants.SSHS_ADD, keys_by_uuid) + (constants.SSHS_REPLACE_OR_ADD, keys_by_uuid) all_nodes = ssconf_store.GetNodeList() master_node = ssconf_store.GetMasterNode() @@ -1626,6 +1626,117 @@ def RemoveNodeSshKey(node_uuid, node_name, from_authorized_keys, node_name, e) +def _GenerateNodeSshKey(node_uuid, node_name, ssh_port_map, + pub_key_file=pathutils.SSH_PUB_KEYS, + ssconf_store=None, + noded_cert_file=pathutils.NODED_CERT_FILE, + run_cmd_fn=ssh.RunSshCmdWithStdin): + """Generates the root SSH key pair on the node. + + @type node_uuid: str + @param node_uuid: UUID of the node whose key is removed + @type node_name: str + @param node_name: name of the node whose key is remove + @type ssh_port_map: dict of str to int + @param ssh_port_map: mapping of node names to their SSH port + + """ + if not ssconf_store: + ssconf_store = ssconf.SimpleStore() + + keys_by_uuid = ssh.QueryPubKeyFile([node_uuid], key_file=pub_key_file) + if not keys_by_uuid or node_uuid not in keys_by_uuid: + raise errors.SshUpdateError("Node %s (UUID: %s) whose key is requested to" + " be regenerated is not registered in the" + " public keys file." % (node_name, node_uuid)) + + data = {} + _InitSshUpdateData(data, noded_cert_file, ssconf_store) + cluster_name = data[constants.SSHS_CLUSTER_NAME] + data[constants.SSHS_GENERATE] = True + + run_cmd_fn(cluster_name, node_name, pathutils.SSH_UPDATE, + True, True, False, False, False, + ssh_port_map.get(node_name), data, ssconf_store) + + +def RenewSshKeys(node_uuids, node_names, ssh_port_map, + master_candidate_uuids, + potential_master_candidates, + pub_key_file=pathutils.SSH_PUB_KEYS, + ssconf_store=None, + noded_cert_file=pathutils.NODED_CERT_FILE, + run_cmd_fn=ssh.RunSshCmdWithStdin): + """Renews all SSH keys and updates authorized_keys and ganeti_pub_keys. + + """ + if not ssconf_store: + ssconf_store = ssconf.SimpleStore() + cluster_name = ssconf_store.GetClusterName() + + if not len(node_uuids) == len(node_names): + raise errors.ProgrammerError("List of nodes UUIDs and node names" + " does not match in length.") + + (_, root_keyfiles) = \ + ssh.GetAllUserFiles(constants.SSH_LOGIN_USER, mkdir=False, dircheck=False) + + node_uuid_name_map = zip(node_uuids, node_names) + + master_node_name = ssconf_store.GetMasterNode() + # process non-master nodes + for node_uuid, node_name in node_uuid_name_map: + if node_name == master_node_name: + continue + master_candidate = node_uuid in master_candidate_uuids + potential_master_candidate = node_name in potential_master_candidates + + keys_by_uuid = ssh.QueryPubKeyFile([node_uuid], key_file=pub_key_file) + if not keys_by_uuid: + raise errors.SshUpdateError("No public key of node %s (UUID %s) found," + " not generating a new key." + % (node_name, node_uuid)) + + RemoveNodeSshKey(node_uuid, node_name, + master_candidate, # from authorized keys + False, # Don't remove (yet) from public keys + False, # Don't clear authorized_keys + ssh_port_map, + master_candidate_uuids, + potential_master_candidates) + + _GenerateNodeSshKey(node_uuid, node_name, ssh_port_map, + pub_key_file=pub_key_file, + ssconf_store=ssconf_store, + noded_cert_file=noded_cert_file, + run_cmd_fn=run_cmd_fn) + + fetched_keys = ssh.ReadRemoteSshPubKeys(root_keyfiles, node_name, + cluster_name, + ssh_port_map[node_name], + False, # ask_key + False) # key_check + if not fetched_keys: + raise errors.SshUpdateError("Could not fetch key of node %s" + " (UUID %s)" % (node_name, node_uuid)) + + if potential_master_candidate: + ssh.RemovePublicKey(node_uuid, key_file=pub_key_file) + for pub_key in fetched_keys.values(): + ssh.AddPublicKey(node_uuid, pub_key, key_file=pub_key_file) + + AddNodeSshKey(node_uuid, node_name, + master_candidate, # Add to authorized_keys file + potential_master_candidate, # Add to public_keys + True, # Get public keys + ssh_port_map, potential_master_candidates, + pub_key_file=pub_key_file, ssconf_store=ssconf_store, + noded_cert_file=noded_cert_file, + run_cmd_fn=run_cmd_fn) + + # FIXME: Update master key as well + + def GetBlockDevSizes(devices): """Return the size of the given block devices diff --git a/lib/cli.py b/lib/cli.py index e939b5c..77c9323 100644 --- a/lib/cli.py +++ b/lib/cli.py @@ -136,6 +136,7 @@ __all__ = [ "NETWORK6_OPT", "NEW_CLUSTER_CERT_OPT", "NEW_NODE_CERT_OPT", + "NEW_SSH_KEY_OPT", "NEW_CLUSTER_DOMAIN_SECRET_OPT", "NEW_CONFD_HMAC_KEY_OPT", "NEW_RAPI_CERT_OPT", @@ -255,6 +256,7 @@ __all__ = [ "GetClient", "GetOnlineNodes", "GetNodesSshPorts", + "GetNodeUUIDs", "JobExecutor", "JobSubmittedException", "ParseTimespec", @@ -1491,6 +1493,10 @@ NEW_NODE_CERT_OPT = cli_option( "--new-node-certificates", dest="new_node_cert", default=False, action="store_true", help="Generate new node certificates (for all nodes)") +NEW_SSH_KEY_OPT = cli_option( + "--new-ssh-keys", dest="new_ssh_keys", default=False, + action="store_true", help="Generate new node SSH keys (for all nodes)") + RAPI_CERT_OPT = cli_option("--rapi-certificate", dest="rapi_cert", default=None, help="File containing new RAPI certificate") @@ -3729,6 +3735,7 @@ def GetNodesSshPorts(nodes, cl): @type cl: L{ganeti.luxi.Client} @return: the list of SSH ports corresponding to the nodes @rtype: a list of tuples + """ return map(lambda t: t[0], cl.QueryNodes(names=nodes, @@ -3736,6 +3743,23 @@ def GetNodesSshPorts(nodes, cl): use_locking=False)) +def GetNodeUUIDs(nodes, cl): + """Retrieves the UUIDs of given nodes. + + @param nodes: the names of nodes + @type nodes: a list of string + @param cl: a client to use for the query + @type cl: L{ganeti.luxi.Client} + @return: the list of UUIDs corresponding to the nodes + @rtype: a list of tuples + + """ + return map(lambda t: t[0], + cl.QueryNodes(names=nodes, + fields=["uuid"], + use_locking=False)) + + def _ToStream(stream, txt, *args): """Write a message to a stream, bypassing the logging system diff --git a/lib/client/gnt_cluster.py b/lib/client/gnt_cluster.py index daa64d6..6c8c28e 100644 --- a/lib/client/gnt_cluster.py +++ b/lib/client/gnt_cluster.py @@ -927,7 +927,7 @@ 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): + cds_filename, force, new_node_cert, new_ssh_keys): """Renews cluster certificates, keys and secrets. @type new_cluster_cert: bool @@ -951,8 +951,10 @@ def _RenewCrypto(new_cluster_cert, new_rapi_cert, # pylint: disable=R0911 @param cds_filename: Path to file containing new cluster domain secret @type force: bool @param force: Whether to ask user for confirmation - @type new_node_cert: string + @type new_node_cert: bool @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 """ if new_rapi_cert and rapi_cert_filename: @@ -1047,18 +1049,75 @@ def _RenewCrypto(new_cluster_cert, new_rapi_cert, # pylint: disable=R0911 ToStdout("All requested certificates and keys have been replaced." " Running \"gnt-cluster verify\" now is recommended.") - if new_node_cert: + if new_node_cert or new_ssh_keys: cl = GetClient() - renew_op = opcodes.OpClusterRenewCrypto(node_certificates=new_node_cert) + renew_op = opcodes.OpClusterRenewCrypto(node_certificates=new_node_cert, + ssh_keys=new_ssh_keys) SubmitOpCode(renew_op, cl=cl) return 0 +def _BuildGanetiPubKeys(options, pub_key_file=pathutils.SSH_PUB_KEYS, cl=None, + get_online_nodes_fn=GetOnlineNodes, + get_nodes_ssh_ports_fn=GetNodesSshPorts, + get_node_uuids_fn=GetNodeUUIDs, + homedir_fn=None): + """Recreates the 'ganeti_pub_key' file by polling all nodes. + + """ + if os.path.exists(pub_key_file): + utils.CreateBackup(pub_key_file) + utils.RemoveFile(pub_key_file) + + ssh.ClearPubKeyFile(pub_key_file) + + if not cl: + cl = GetClient() + + (cluster_name, master_node) = \ + cl.QueryConfigValues(["cluster_name", "master_node"]) + + online_nodes = get_online_nodes_fn([], cl=cl) + ssh_ports = get_nodes_ssh_ports_fn(online_nodes + [master_node], cl) + ssh_port_map = dict(zip(online_nodes + [master_node], ssh_ports)) + + node_uuids = get_node_uuids_fn(online_nodes + [master_node], cl) + node_uuid_map = dict(zip(online_nodes + [master_node], node_uuids)) + + nonmaster_nodes = [name for name in online_nodes + if name != master_node] + + (_, root_keyfiles) = \ + ssh.GetAllUserFiles(constants.SSH_LOGIN_USER, mkdir=False, dircheck=False, + _homedir_fn=homedir_fn) + + # get the key file of the master node + for (_, (_, public_key_file)) in root_keyfiles.items(): + try: + pub_key = utils.ReadFile(public_key_file) + ssh.AddPublicKey(node_uuid_map[master_node], pub_key, + key_file=pub_key_file) + except IOError: + # Not all types of keys might be existing + pass + + # get the key files of all non-master nodes + for node in nonmaster_nodes: + fetched_keys = ssh.ReadRemoteSshPubKeys(root_keyfiles, node, cluster_name, + ssh_port_map[node], + options.ssh_key_check, + options.ssh_key_check) + for pub_key in fetched_keys.values(): + ssh.AddPublicKey(node_uuid_map[node], pub_key, key_file=pub_key_file) + + def RenewCrypto(opts, args): """Renews cluster certificates, keys and secrets. """ + if opts.new_ssh_keys: + _BuildGanetiPubKeys(opts) return _RenewCrypto(opts.new_cluster_cert, opts.new_rapi_cert, opts.rapi_cert, @@ -1069,7 +1128,8 @@ def RenewCrypto(opts, args): opts.new_cluster_domain_secret, opts.cluster_domain_secret, opts.force, - opts.new_node_cert) + opts.new_node_cert, + opts.new_ssh_keys) def _GetEnabledDiskTemplates(opts): @@ -2258,7 +2318,7 @@ 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_NODE_CERT_OPT, NEW_SSH_KEY_OPT, NOSSH_KEYCHECK_OPT], "[opts...]", "Renews cluster certificates, keys and secrets"), "epo": ( diff --git a/lib/cmdlib/cluster.py b/lib/cmdlib/cluster.py index 9b2e155..3a5508f 100644 --- a/lib/cmdlib/cluster.py +++ b/lib/cmdlib/cluster.py @@ -99,6 +99,32 @@ class LUClusterRenewCrypto(NoHooksLU): """ + REQ_BGL = False + + def ExpandNames(self): + self.needed_locks = { + locking.LEVEL_NODE: locking.ALL_SET, + locking.LEVEL_NODE_RES: locking.ALL_SET, + locking.LEVEL_NODE_ALLOC: locking.ALL_SET, + } + self.share_locks = ShareAll() + self.share_locks[locking.LEVEL_NODE] = 0 + self.share_locks[locking.LEVEL_NODE_RES] = 0 + self.share_locks[locking.LEVEL_NODE_ALLOC] = 0 + + def CheckPrereq(self): + """Check prerequisites. + + This checks whether the cluster is empty. + + Any errors are signaled by raising errors.OpPrereqError. + + """ + self._ssh_renewal_suppressed = False + if not self.cfg.GetClusterInfo().modify_ssh_setup: + if self.op.ssh_keys: + self._ssh_renewal_suppressed = True + def _RenewNodeSslCertificates(self): """Renews the nodes' SSL certificates. @@ -132,9 +158,35 @@ class LUClusterRenewCrypto(NoHooksLU): self.cfg.RemoveNodeFromCandidateCerts("%s-SERVER" % master_uuid) self.cfg.RemoveNodeFromCandidateCerts("%s-OLDMASTER" % master_uuid) + def _RenewSshKeys(self): + """Renew all nodes' SSH keys. + + """ + master_uuid = self.cfg.GetMasterNode() + + nodes = self.cfg.GetAllNodesInfo() + nodes_uuid_names = [(node_uuid, node_info.name) for (node_uuid, node_info) + in nodes.items() if not node_info.offline] + node_names = [name for (_, name) in nodes_uuid_names] + node_uuids = [uuid for (uuid, _) in nodes_uuid_names] + port_map = ssh.GetSshPortMap(node_names, self.cfg) + potential_master_candidates = self.cfg.GetPotentialMasterCandidates() + master_candidate_uuids = self.cfg.GetMasterCandidateUuids() + result = self.rpc.call_node_ssh_keys_renew( + [master_uuid], + node_uuids, node_names, port_map, + master_candidate_uuids, + potential_master_candidates) + result[master_uuid].Raise("Could not renew the SSH keys of all nodes") + def Exec(self, feedback_fn): if self.op.node_certificates: self._RenewNodeSslCertificates() + if self.op.ssh_keys and not self._ssh_renewal_suppressed: + self._RenewSshKeys() + elif self._ssh_renewal_suppressed: + feedback_fn("Cannot renew SSH keys if the cluster is configured to not" + " modify the SSH setup.") class LUClusterActivateMasterIp(NoHooksLU): diff --git a/lib/rpc_defs.py b/lib/rpc_defs.py index ee66da0..b944373 100644 --- a/lib/rpc_defs.py +++ b/lib/rpc_defs.py @@ -549,6 +549,13 @@ _NODE_CALLS = [ ("master_candidate_uuids", None, "List of UUIDs of master candidates."), ("potential_master_candidates", None, "Potential master candidates")], None, None, "Remove a node's SSH key from the other nodes' key files."), + ("node_ssh_keys_renew", MULTI, None, constants.RPC_TMO_URGENT, [ + ("node_uuids", None, "UUIDs of the nodes whose key is renewed"), + ("node_names", None, "Names of the nodes whose key is renewed"), + ("ssh_port_map", None, "Map of nodes' SSH ports to be used for transfers"), + ("master_candidate_uuids", None, "List of UUIDs of master candidates."), + ("potential_master_candidates", None, "Potential master candidates")], + None, None, "Renew all SSH key pairs of all nodes nodes."), ] _MISC_CALLS = [ diff --git a/lib/server/noded.py b/lib/server/noded.py index 05af067..05bbc04 100644 --- a/lib/server/noded.py +++ b/lib/server/noded.py @@ -922,6 +922,17 @@ class NodeRequestHandler(http.server.HttpServerHandler): ssh_port_map, potential_master_candidates) @staticmethod + def perspective_node_ssh_keys_renew(params): + """Generates a new root SSH key pair on the node. + + """ + (node_uuids, node_names, ssh_port_map, + master_candidate_uuids, potential_master_candidates) = params + return backend.RenewSshKeys(node_uuids, node_names, ssh_port_map, + master_candidate_uuids, + potential_master_candidates) + + @staticmethod def perspective_node_ssh_key_remove(params): """Removes a node's SSH key from the other nodes' SSH files. diff --git a/lib/tools/ssh_update.py b/lib/tools/ssh_update.py index 41a9de8..052a2e4 100644 --- a/lib/tools/ssh_update.py +++ b/lib/tools/ssh_update.py @@ -53,6 +53,7 @@ _DATA_CHECK = ht.TStrictDict(False, True, { ht.TItems( [ht.TElemOf(constants.SSHS_ACTIONS), ht.TDictOf(ht.TNonEmptyString, ht.TListOf(ht.TNonEmptyString))]), + constants.SSHS_GENERATE: ht.TBool, }) @@ -143,11 +144,14 @@ def UpdatePubKeyFile(data, dry_run, key_file=pathutils.SSH_PUB_KEYS): logging.info("This is a dry run, not overriding %s", key_file) else: ssh.OverridePubKeyFile(public_keys, key_file=key_file) - elif action == constants.SSHS_ADD: + elif action in [constants.SSHS_ADD, constants.SSHS_REPLACE_OR_ADD]: if dry_run: - logging.info("This is a dry run, not adding a key to %s", key_file) + logging.info("This is a dry run, not adding or replacing a key to %s", + key_file) else: for uuid, keys in public_keys.items(): + if action == constants.SSHS_REPLACE_OR_ADD: + ssh.RemovePublicKey(uuid, key_file=key_file) for key in keys: ssh.AddPublicKey(uuid, key, key_file=key_file) elif action == constants.SSHS_REMOVE: @@ -166,6 +170,23 @@ def UpdatePubKeyFile(data, dry_run, key_file=pathutils.SSH_PUB_KEYS): % action) +def GenerateRootSshKeys(data, dry_run): + """(Re-)generates the root SSH keys. + + @type data: dict + @param data: Input data + @type dry_run: boolean + @param dry_run: Whether to perform a dry run + + """ + generate = data.get(constants.SSHS_GENERATE) + if generate: + if dry_run: + logging.info("This is a dry run, not generating any files.") + else: + common.GenerateRootSshKeys(SshUpdateError) + + def Main(): """Main routine. @@ -181,9 +202,10 @@ def Main(): common.VerifyClusterName(data, SshUpdateError) common.VerifyCertificate(data, SshUpdateError) - # Update SSH files + # Update / Generate SSH files UpdateAuthorizedKeys(data, opts.dry_run) UpdatePubKeyFile(data, opts.dry_run) + GenerateRootSshKeys(data, opts.dry_run) logging.info("Setup finished successfully") except Exception, err: # pylint: disable=W0703 diff --git a/qa/qa_cluster.py b/qa/qa_cluster.py index ff59eec..408fdb0 100644 --- a/qa/qa_cluster.py +++ b/qa/qa_cluster.py @@ -1219,12 +1219,17 @@ def TestClusterRenewCrypto(): AssertCommand(["gnt-cluster", "renew-crypto", "--force", "--new-cluster-certificate", "--new-confd-hmac-key", "--new-rapi-certificate", "--new-cluster-domain-secret", - "--new-node-certificates"]) + "--new-node-certificates", "--new-ssh-keys", + "--no-ssh-key-check"]) # Only renew node certificates AssertCommand(["gnt-cluster", "renew-crypto", "--force", "--new-node-certificates"]) + # Only renew SSH keys + AssertCommand(["gnt-cluster", "renew-crypto", "--force", + "--new-ssh-keys", "--no-ssh-key-check"]) + # Restore RAPI certificate AssertCommand(["gnt-cluster", "renew-crypto", "--force", "--rapi-certificate=%s" % rapi_cert_backup]) diff --git a/src/Ganeti/Constants.hs b/src/Ganeti/Constants.hs index 0557cb4..b6d9833 100644 --- a/src/Ganeti/Constants.hs +++ b/src/Ganeti/Constants.hs @@ -4465,6 +4465,9 @@ sshsNodeDaemonCertificate = "node_daemon_certificate" sshsAdd :: String sshsAdd = "add" +sshsReplaceOrAdd :: String +sshsReplaceOrAdd = "replace_or_add" + sshsRemove :: String sshsRemove = "remove" @@ -4474,8 +4477,15 @@ sshsOverride = "override" sshsClear :: String sshsClear = "clear" +sshsGenerate :: String +sshsGenerate = "generate" + sshsActions :: FrozenSet String -sshsActions = ConstantUtils.mkSet [sshsAdd, sshsRemove, sshsOverride, sshsClear] +sshsActions = ConstantUtils.mkSet [ sshsAdd + , sshsRemove + , sshsOverride + , sshsClear + , sshsReplaceOrAdd] -- * Key files for SSH daemon diff --git a/src/Ganeti/OpCodes.hs b/src/Ganeti/OpCodes.hs index 0fdcc05..91cc0b4 100644 --- a/src/Ganeti/OpCodes.hs +++ b/src/Ganeti/OpCodes.hs @@ -267,6 +267,7 @@ $(genOpCode "OpCode" [t| () |], OpDoc.opClusterRenewCrypto, [ pNodeSslCerts + , pSshKeys ], []) , ("OpQuery", diff --git a/src/Ganeti/OpParams.hs b/src/Ganeti/OpParams.hs index 8bb7f70..ae5f03b 100644 --- a/src/Ganeti/OpParams.hs +++ b/src/Ganeti/OpParams.hs @@ -283,6 +283,7 @@ module Ganeti.OpParams , pEnabledUserShutdown , pAdminStateSource , pNodeSslCerts + , pSshKeys ) where import Control.Monad (liftM, mplus) @@ -1832,3 +1833,9 @@ pNodeSslCerts = withDoc "Whether to renew node SSL certificates" . defaultField [| False |] $ simpleField "node_certificates" [t| Bool |] + +pSshKeys :: Field +pSshKeys = + withDoc "Whether to renew SSH keys" . + defaultField [| False |] $ + simpleField "ssh_keys" [t| Bool |] diff --git a/test/hs/Test/Ganeti/OpCodes.hs b/test/hs/Test/Ganeti/OpCodes.hs index 2594ae0..a78d53c 100644 --- a/test/hs/Test/Ganeti/OpCodes.hs +++ b/test/hs/Test/Ganeti/OpCodes.hs @@ -149,7 +149,8 @@ instance Arbitrary OpCodes.OpCode where "OP_TAGS_DEL" -> arbitraryOpTagsDel "OP_CLUSTER_POST_INIT" -> pure OpCodes.OpClusterPostInit - "OP_CLUSTER_RENEW_CRYPTO" -> pure OpCodes.OpClusterRenewCrypto + "OP_CLUSTER_RENEW_CRYPTO" -> + OpCodes.OpClusterRenewCrypto <$> arbitrary <*> arbitrary "OP_CLUSTER_DESTROY" -> pure OpCodes.OpClusterDestroy "OP_CLUSTER_QUERY" -> pure OpCodes.OpClusterQuery "OP_CLUSTER_VERIFY" -> diff --git a/test/py/ganeti.backend_unittest.py b/test/py/ganeti.backend_unittest.py index 9b18763..5fcc6b5 100755 --- a/test/py/ganeti.backend_unittest.py +++ b/test/py/ganeti.backend_unittest.py @@ -941,7 +941,7 @@ class TestSpaceReportingConstants(unittest.TestCase): self.assertEqual(None, backend._STORAGE_TYPE_INFO_FN[storage_type]) -class TestAddAndRemoveNodeSshKey(testutils.GanetiTestCase): +class TestAddRemoveGenerateNodeSshKey(testutils.GanetiTestCase): _CLUSTER_NAME = "mycluster" _SSH_PORT = 22 @@ -1024,7 +1024,8 @@ class TestAddAndRemoveNodeSshKey(testutils.GanetiTestCase): expected_key): return self._KeyOperationExecuted( key_data, node_name, expected_type, expected_key, - [constants.SSHS_ADD, constants.SSHS_OVERRIDE]) + [constants.SSHS_ADD, constants.SSHS_OVERRIDE, + constants.SSHS_REPLACE_OR_ADD]) def _KeyRemoved(self, key_data, node_name, expected_type, expected_key): @@ -1051,6 +1052,25 @@ class TestAddAndRemoveNodeSshKey(testutils.GanetiTestCase): calls_per_node[node].append(data) return calls_per_node + def testGenerateKey(self): + test_node_name = "node_name_7" + test_node_uuid = "node_uuid_7" + + self._SetupTestData() + + backend._GenerateNodeSshKey(test_node_uuid, test_node_name, + self._ssh_port_map, + pub_key_file=self._pub_key_file, + ssconf_store=self._ssconf_mock, + noded_cert_file=self.noded_cert_file, + run_cmd_fn=self._run_cmd_mock) + + calls_per_node = self._GetCallsPerNode() + for node, calls in calls_per_node.items(): + self.assertEquals(node, test_node_name) + for call in calls: + self.assertTrue(constants.SSHS_GENERATE in call) + def testAddNodeSshKeyValid(self): new_node_name = "new_node_name" new_node_uuid = "new_node_uuid" diff --git a/test/py/ganeti.client.gnt_cluster_unittest.py b/test/py/ganeti.client.gnt_cluster_unittest.py index b2ce012..7589baa 100755 --- a/test/py/ganeti.client.gnt_cluster_unittest.py +++ b/test/py/ganeti.client.gnt_cluster_unittest.py @@ -23,12 +23,17 @@ import unittest import optparse +import os +import shutil +import tempfile from ganeti import errors from ganeti.client import gnt_cluster from ganeti import utils from ganeti import compat from ganeti import constants +from ganeti import ssh +from ganeti import cli import mock import testutils @@ -354,5 +359,108 @@ class GetDrbdHelper(DrbdHelperTestCase): self.assertEquals(opts.drbd_helper, helper) +class TestBuildGanetiPubKeys(testutils.GanetiTestCase): + + _SOME_KEY_DICT = {"rsa": "key_rsa", + "dsa": "key_dsa"} + _MASTER_NODE_NAME = "master_node" + _MASTER_NODE_UUID = "master_uuid" + _NUM_NODES = 2 # excluding master node + _ONLINE_NODE_NAMES = ["node%s_name" % i for i in range(_NUM_NODES)] + _ONLINE_NODE_UUIDS = ["node%s_uuid" % i for i in range(_NUM_NODES)] + _CLUSTER_NAME = "cluster_name" + _PRIV_KEY = "master_private_key" + _PUB_KEY = "master_public_key" + _AUTH_KEYS = "a\nb\nc" + + def _setUpFakeKeys(self): + os.makedirs(os.path.join(self.tmpdir, ".ssh")) + + for key_type in ["rsa", "dsa"]: + self.priv_filename = os.path.join(self.tmpdir, ".ssh", "id_%s" % key_type) + priv_fd = open(self.priv_filename, 'w') + priv_fd.write(self._PRIV_KEY) + priv_fd.close() + + self.pub_filename = os.path.join( + self.tmpdir, ".ssh", "id_%s.pub" % key_type) + pub_fd = open(self.pub_filename, 'w') + pub_fd.write(self._PUB_KEY) + pub_fd.close() + + self.auth_filename = os.path.join(self.tmpdir, ".ssh", "authorized_keys") + auth_fd = open(self.auth_filename, 'w') + auth_fd.write(self._AUTH_KEYS) + auth_fd.close() + + def setUp(self): + testutils.GanetiTestCase.setUp(self) + self.tmpdir = tempfile.mkdtemp() + self.pub_key_filename = os.path.join(self.tmpdir, "ganeti_test_pub_keys") + self._setUpFakeKeys() + + self._ssh_read_remote_ssh_pub_keys_patcher = testutils \ + .patch_object(ssh, "ReadRemoteSshPubKeys") + self._ssh_read_remote_ssh_pub_keys_mock = \ + self._ssh_read_remote_ssh_pub_keys_patcher.start() + self._ssh_read_remote_ssh_pub_keys_mock.return_value = self._SOME_KEY_DICT + + self.mock_cl = mock.Mock() + self.mock_cl.QueryConfigValues = mock.Mock() + self.mock_cl.QueryConfigValues.return_value = \ + (self._CLUSTER_NAME, self._MASTER_NODE_NAME) + + self._get_online_nodes_mock = mock.Mock() + self._get_online_nodes_mock.return_value = \ + self._ONLINE_NODE_NAMES + + self._get_nodes_ssh_ports_mock = mock.Mock() + self._get_nodes_ssh_ports_mock.return_value = \ + [22 for i in range(self._NUM_NODES + 1)] + + self._get_node_uuids_mock = mock.Mock() + self._get_node_uuids_mock.return_value = \ + self._ONLINE_NODE_UUIDS + [self._MASTER_NODE_UUID] + + self._options = mock.Mock() + self._options.ssh_key_check = False + + def _GetTempHomedir(self, _): + return self.tmpdir + + def tearDown(self): + super(testutils.GanetiTestCase, self).tearDown() + shutil.rmtree(self.tmpdir) + self._ssh_read_remote_ssh_pub_keys_patcher.stop() + + def testNewPubKeyFile(self): + gnt_cluster._BuildGanetiPubKeys( + self._options, + pub_key_file=self.pub_key_filename, + cl=self.mock_cl, + get_online_nodes_fn=self._get_online_nodes_mock, + get_nodes_ssh_ports_fn=self._get_nodes_ssh_ports_mock, + get_node_uuids_fn=self._get_node_uuids_mock, + homedir_fn=self._GetTempHomedir) + key_file_result = utils.ReadFile(self.pub_key_filename) + for node_uuid in self._ONLINE_NODE_UUIDS + [self._MASTER_NODE_UUID]: + self.assertTrue(node_uuid in key_file_result) + self.assertTrue(self._PUB_KEY in key_file_result) + + def testOverridePubKeyFile(self): + fd = open(self.pub_key_filename, "w") + fd.write("Pink Bunny") + fd.close() + gnt_cluster._BuildGanetiPubKeys( + self._options, + pub_key_file=self.pub_key_filename, + cl=self.mock_cl, + get_online_nodes_fn=self._get_online_nodes_mock, + get_nodes_ssh_ports_fn=self._get_nodes_ssh_ports_mock, + get_node_uuids_fn=self._get_node_uuids_mock, + homedir_fn=self._GetTempHomedir) + self.assertFalse("Pink" in self.pub_key_filename) + + if __name__ == "__main__": testutils.GanetiTestProgram() diff --git a/test/py/ganeti.mcpu_unittest.py b/test/py/ganeti.mcpu_unittest.py index 02a97ef..27549d8 100755 --- a/test/py/ganeti.mcpu_unittest.py +++ b/test/py/ganeti.mcpu_unittest.py @@ -46,7 +46,6 @@ REQ_BGL_WHITELIST = compat.UniqueFrozenset([ opcodes.OpClusterDestroy, opcodes.OpClusterPostInit, opcodes.OpClusterRename, - opcodes.OpClusterRenewCrypto, opcodes.OpNodeAdd, opcodes.OpNodeRemove, opcodes.OpTestAllocator, diff --git a/tools/post-upgrade b/tools/post-upgrade index 6aec3ff..92ad7b9 100644 --- a/tools/post-upgrade +++ b/tools/post-upgrade @@ -48,6 +48,14 @@ def main(): if result.failed: cli.ToStderr("Failed to create node certificates: %s; Output %s" % (result.fail_reason, result.output)) + + if utils.version.IsBefore(version, 2, 13, 0): + result = utils.RunCmd(["gnt-cluster", "renew-crypto", + "--new-ssh-keys", "--no-ssh-key-check", "-f"]) + if result.failed: + cli.ToStderr("Failed to create node certificates: %s; Output %s" % + (result.fail_reason, result.output)) + return 0 if __name__ == "__main__": -- 2.0.0.526.g5318336