Hi! On Tue, Sep 30, 2014 at 4:14 PM, Petr Pudlak <pud...@google.com> wrote:
> On Tue, Sep 02, 2014 at 04:19:45PM +0200, 'Helga Velroyen' via > ganeti-devel wrote: > >> 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> >> >> Conflicts: >> src/Ganeti/OpParams.hs >> >> 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 b6c7975..2c0de7f 100644 >> --- a/lib/client/gnt_cluster.py >> +++ b/lib/client/gnt_cluster.py >> @@ -938,7 +938,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 >> @@ -962,8 +962,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: >> @@ -1058,18 +1060,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, >> @@ -1080,7 +1139,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): >> @@ -2312,7 +2372,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 5674b07..493ae87 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 >> > > I'm not sure if NODE_RES is needed here, AFAIK it should be used for > resource-intensive operations (but since nobody really knows where NODE_RES > should be used, my opinion isn't strong about it). Good point, I think I did not pay too much attention here,but I agree that it is most probably not justified as changing the SSH keys is not that IO-load-intensive. > > > + >> + 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 >> > > I'd suggest > > self._ssh_renewal_suppressed = \ > not self.cfg.GetClusterInfo().modify_ssh_setup and self.op.ssh_keys ACK > > + >> 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 4d6e09a..712d991 100644 >> --- a/qa/qa_cluster.py >> +++ b/qa/qa_cluster.py >> @@ -1220,12 +1220,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 4a2db43..6c9f75f 100644 >> --- a/src/Ganeti/Constants.hs >> +++ b/src/Ganeti/Constants.hs >> @@ -4487,6 +4487,9 @@ sshsNodeDaemonCertificate = >> "node_daemon_certificate" >> sshsAdd :: String >> sshsAdd = "add" >> >> +sshsReplaceOrAdd :: String >> +sshsReplaceOrAdd = "replace_or_add" >> + >> sshsRemove :: String >> sshsRemove = "remove" >> >> @@ -4496,8 +4499,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 4ebb222..66c5285 100644 >> --- a/src/Ganeti/OpCodes.hs >> +++ b/src/Ganeti/OpCodes.hs >> @@ -269,6 +269,7 @@ $(genOpCode "OpCode" >> [t| () |], >> OpDoc.opClusterRenewCrypto, >> [ pNodeSslCerts >> + , pSshKeys >> ], >> []) >> , ("OpQuery", >> diff --git a/src/Ganeti/OpParams.hs b/src/Ganeti/OpParams.hs >> index f277065..9cac511 100644 >> --- a/src/Ganeti/OpParams.hs >> +++ b/src/Ganeti/OpParams.hs >> @@ -285,6 +285,7 @@ module Ganeti.OpParams >> , pEnableDataCollectors >> , pDisableDataCollectors >> , pNodeSslCerts >> + , pSshKeys >> ) where >> >> import Control.Monad (liftM, mplus) >> @@ -1850,3 +1851,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 f881731..517b332 100644 >> --- a/test/hs/Test/Ganeti/OpCodes.hs >> +++ b/test/hs/Test/Ganeti/OpCodes.hs >> @@ -153,7 +153,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"I think the only thing I told him is >> that I am not the only one who is annoyh >> + _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() >> > > Just a minor point, again we could simplify these (and similar statements > in the module) with utils.WriteFile (as the module uses utils.ReadFile > already). ACK > > > + >> + 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) >> > > This just tests that "Pink" isn't in the filename, doesn't it? Probably it > should be similar to the previous test - read the file and check the > content. It tests whether the "Pink Bunny" key has disappeared. I think checking if "Pink" is not in there is sufficient. I deliberately did not check the entire key file, because I want to avoid too pedantic tests that are hard to maintain, because they would need to be change with any small change in the code. However, for better readability, I changed it to check for the entire line "Pink Bunny". > > > + >> + >> 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, >> > > +1! > > 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.1.0.rc2.206.gedb03e5 >> >> > > Rest LGTM, no need to resend. > Even though you said no need to resend, here the interdiff FYI: diff --git a/lib/cmdlib/cluster.py b/lib/cmdlib/cluster.py index 35f1d93..0defb61 100644 --- a/lib/cmdlib/cluster.py +++ b/lib/cmdlib/cluster.py @@ -113,12 +113,10 @@ class LUClusterRenewCrypto(NoHooksLU): 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): @@ -129,10 +127,8 @@ class LUClusterRenewCrypto(NoHooksLU): 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 + self._ssh_renewal_suppressed = \ + not self.cfg.GetClusterInfo().modify_ssh_setup and self.op.ssh_keys def _RenewNodeSslCertificates(self): """Renews the nodes' SSL certificates. diff --git a/test/py/ganeti.client.gnt_cluster_unittest.py b/test/py/ ganeti.client.gnt_cluster_unittest.py index 95b29fa..be28eb2 100755 --- a/test/py/ganeti.client.gnt_cluster_unittest.py +++ b/test/py/ganeti.client.gnt_cluster_unittest.py @@ -387,20 +387,14 @@ class TestBuildGanetiPubKeys(testutils.GanetiTestCase): 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() + utils.WriteFile(self.priv_filename, data=self._PRIV_KEY) 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() + utils.WriteFile(self.pub_filename, data=self._PUB_KEY) 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() + utils.WriteFile(self.auth_filename, data=self._AUTH_KEYS) def setUp(self): testutils.GanetiTestCase.setUp(self) @@ -468,7 +462,7 @@ class TestBuildGanetiPubKeys(testutils.GanetiTestCase): 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) + self.assertFalse("Pink Bunny" in self.pub_key_filename) if __name__ == "__main__": -- Helga Velroyen | Software Engineer | hel...@google.com | Google Germany GmbH Dienerstr. 12 80331 München Registergericht und -nummer: Hamburg, HRB 86891 Sitz der Gesellschaft: Hamburg Geschäftsführer: Graham Law, Christine Elizabeth Flores