On Fri, 27 Nov 2015 at 16:24 Lisa Velden <[email protected]> wrote:
> On Tue, Nov 24, 2015 at 3:15 PM, 'Helga Velroyen' via ganeti-devel <
> [email protected]> wrote:
>
>> In order to improve the runtime complexity of
>> 'renew-crypto', this patch adds a function to
>> bulk-remove SSH keys of nodes (in contrast to
>> the function that only removes one key at a time).
>>
>> Within this patch, it is only called in a unit
>> test. Further patches will integrate and test it
>> further.
>>
>> Signed-off-by: Helga Velroyen <[email protected]>
>> ---
>> lib/backend.py | 254
>> +++++++++++++++++++++++++++++++++++++
>> test/py/ganeti.backend_unittest.py | 31 +++++
>> 2 files changed, 285 insertions(+)
>>
>> diff --git a/lib/backend.py b/lib/backend.py
>> index d470060..3d0443c 100644
>> --- a/lib/backend.py
>> +++ b/lib/backend.py
>> @@ -1857,6 +1857,260 @@ def RemoveNodeSshKey(node_uuid, node_name,
>> return result_msgs
>>
>>
>> +# Node info named tuple specifically for the use with
>> RemoveNodeSshKeyBulk
>> +SshRemoveNodeInfo = collections.namedtuple(
>> + "SshRemoveNodeInfo",
>> + ["uuid",
>> + "name",
>> + "from_authorized_keys",
>> + "from_public_keys",
>> + "clear_authorized_keys",
>> + "clear_public_keys"])
>> +
>> +
>> +def RemoveNodeSshKeyBulk(node_list,
>> + master_candidate_uuids,
>> + potential_master_candidates,
>> + master_uuid=None,
>> + keys_to_remove=None,
>> + pub_key_file=pathutils.SSH_PUB_KEYS,
>> + ssconf_store=None,
>> + noded_cert_file=pathutils.NODED_CERT_FILE,
>> + readd=False,
>> + run_cmd_fn=ssh.RunSshCmdWithStdin):
>> + """Removes the node's SSH keys from the key files and distributes
>> those.
>> +
>> + Note that at least one of the flags C{from_authorized_keys},
>> + C{from_public_keys}, C{clear_authorized_keys}, and C{clear_public_keys}
>> + of at least one node has to be set to C{True} for the function to
>> perform any
>> + action at all. Not doing so will trigger an assertion in the function.
>
> +
>> + @type node_list: list of C{SshRemoveNodeInfo}.
>> + @param node_list: list of information about nodes whose keys are being
>> removed
>> + @type master_candidate_uuids: list of str
>> + @param master_candidate_uuids: list of UUIDs of the current master
>> candidates
>> + @type potential_master_candidates: list of str
>> + @param potential_master_candidates: list of names of potential master
>> + candidates
>> + @type keys_to_remove: dict of str to list of str
>> + @param keys_to_remove: a dictionary mapping node UUIDS to lists of SSH
>> keys
>> + to be removed. This list is supposed to be used only if the keys are
>> not
>> + in the public keys file. This is for example the case when removing a
>> + master node's key.
>> + @type readd: boolean
>> + @param readd: whether this is called during a readd operation.
>> + @rtype: list of string
>> + @returns: list of feedback messages
>> +
>> + """
>> + # Non-disruptive error messages, list of (node, msg) pairs
>> + result_msgs = []
>> +
>> + # whether there are any keys to be added or retrieved at all
>> + from_authorized_keys = any([node_info.from_authorized_keys for
>> node_info in
>> + node_list])
>> + from_public_keys = any([node_info.from_public_keys for node_info in
>> + node_list])
>> + clear_authorized_keys = any([node_info.clear_authorized_keys for
>> node_info in
>> + node_list])
>> + clear_public_keys = any([node_info.clear_public_keys for node_info in
>> + node_list])
>> +
>> + # Make sure at least one of these flags is true.
>> + if not (from_authorized_keys or from_public_keys or
>> clear_authorized_keys
>> + or clear_public_keys):
>> + raise errors.SshUpdateError("No removal from any key file was
>> requested.")
>> +
>> + if not ssconf_store:
>> + ssconf_store = ssconf.SimpleStore()
>> +
>> + master_node = ssconf_store.GetMasterNode()
>> + ssh_port_map = ssconf_store.GetSshPortMap()
>> +
>> + if from_authorized_keys or from_public_keys:
>> + all_keys_to_remove = {}
>> + for node_info in node_list:
>> +
>> + if keys_to_remove:
>> + keys = keys_to_remove
>>
>
> As you only have one dict of keys_to_remove, you could already initialize
> all_keys_to_remove with that.
>
I don't think I should. If 'keys_to_remove' is not set, it is actually
None. If I initialize all_keys_to_remove with that instead of the empty
dictionary {}, it will fail as soon as I try to run .update() on it.
>
>
>> + else:
>> + keys = ssh.QueryPubKeyFile([node_info.uuid],
>> key_file=pub_key_file)
>> + if (not keys or node_info.uuid not in keys) and not readd:
>> + raise errors.SshUpdateError("Node '%s' not found in the list
>> of"
>> + " public SSH keys. It seems
>> someone"
>> + " tries to remove a key from
>> outside"
>> + " the cluster!" % node_info.uuid)
>> + # During an upgrade all nodes have the master key. In this case
>> we
>> + # should not remove it to avoid accidentally shutting down
>> cluster
>> + # SSH communication
>> + master_keys = None
>> + if master_uuid:
>> + master_keys = ssh.QueryPubKeyFile([master_uuid],
>> + key_file=pub_key_file)
>> + for master_key in master_keys:
>> + if master_key in keys[node_info.uuid]:
>> + keys[node_info.uuid].remove(master_key)
>> +
>> + if node_info.name == master_node and not keys_to_remove:
>> + raise errors.SshUpdateError("Cannot remove the master node's
>> keys.")
>>
>
> I would move this at the beginning of the for loop to fail early.
>
ACK, see interdiff below.
>
>
>> +
>> + all_keys_to_remove.update(keys)
>> +
>> + if all_keys_to_remove:
>> + base_data = {}
>> + _InitSshUpdateData(base_data, noded_cert_file, ssconf_store)
>> + cluster_name = base_data[constants.SSHS_CLUSTER_NAME]
>> +
>> + if from_authorized_keys:
>> + # UUIDs of nodes that are supposed to be removed from the
>> + # authorized_keys files.
>> + nodes_remove_from_authorized_keys = [
>> + node_info.uuid for node_info in node_list
>> + if node_info.from_authorized_keys]
>> + keys_to_remove_from_authorized_keys = dict([
>> + (uuid, keys) for (uuid, keys) in all_keys_to_remove.items()
>> + if uuid in nodes_remove_from_authorized_keys])
>> + base_data[constants.SSHS_SSH_AUTHORIZED_KEYS] = \
>> + (constants.SSHS_REMOVE, keys_to_remove_from_authorized_keys)
>> + (auth_key_file, _) = \
>> + ssh.GetAllUserFiles(constants.SSH_LOGIN_USER, mkdir=False,
>> + dircheck=False)
>> +
>> + for uuid in nodes_remove_from_authorized_keys:
>> + ssh.RemoveAuthorizedKeys(auth_key_file,
>> +
>> keys_to_remove_from_authorized_keys[uuid])
>> +
>> + pot_mc_data = base_data.copy()
>> +
>> + if from_public_keys:
>> + nodes_remove_from_public_keys = [
>> + node_info.uuid for node_info in node_list
>> + if node_info.from_public_keys]
>> + keys_to_remove_from_public_keys = dict([
>> + (uuid, keys) for (uuid, keys) in all_keys_to_remove.items()
>> + if uuid in nodes_remove_from_public_keys])
>> + pot_mc_data[constants.SSHS_SSH_PUBLIC_KEYS] = \
>> + (constants.SSHS_REMOVE, keys_to_remove_from_public_keys)
>> +
>> + all_nodes = ssconf_store.GetNodeList()
>> + online_nodes = ssconf_store.GetOnlineNodeList()
>> + all_nodes_to_remove = [node_info.name for node_info in node_list]
>> + logging.debug("Removing keys of nodes '%s' from all nodes but
>> itself and"
>> + " master.", ", ".join(all_nodes_to_remove))
>> + for node in all_nodes:
>> + if node == master_node:
>> + logging.debug("Skipping master node '%s'.", master_node)
>> + continue
>> + if node not in online_nodes:
>> + logging.debug("Skipping offline node '%s'.", node)
>> + continue
>> + if node in all_nodes_to_remove:
>> + logging.debug("Skipping node whose key is removed itself
>> '%s'.", node)
>
> + continue
>> + ssh_port = ssh_port_map.get(node)
>> + if not ssh_port:
>> + raise errors.OpExecError("No SSH port information available
>> for"
>> + " node '%s', map: %s." %
>> + (node, ssh_port_map))
>> + error_msg_final = ("When removing the key of node '%s', updating
>> the"
>> + " SSH key files of node '%s' failed. Last
>> error"
>> + " was: %s.")
>> + if node in potential_master_candidates:
>> + logging.debug("Updating key setup of potential master
>> candidate node"
>> + " %s.", node)
>> + try:
>> + utils.RetryByNumberOfTimes(
>> + constants.SSHS_MAX_RETRIES,
>> + errors.SshUpdateError,
>> + run_cmd_fn, cluster_name, node, pathutils.SSH_UPDATE,
>> + ssh_port, pot_mc_data,
>> + debug=False, verbose=False, use_cluster_key=False,
>> + ask_key=False, strict_host_check=False)
>> + except errors.SshUpdateError as last_exception:
>> + error_msg = error_msg_final % (
>> + node_info.name, node, last_exception)
>> + result_msgs.append((node, error_msg))
>> + logging.error(error_msg)
>> +
>> + else:
>> + if from_authorized_keys:
>> + logging.debug("Updating key setup of normal node %s.", node)
>> + try:
>> + utils.RetryByNumberOfTimes(
>> + constants.SSHS_MAX_RETRIES,
>> + errors.SshUpdateError,
>> + run_cmd_fn, cluster_name, node, pathutils.SSH_UPDATE,
>> + ssh_port, base_data,
>> + debug=False, verbose=False, use_cluster_key=False,
>> + ask_key=False, strict_host_check=False)
>> + except errors.SshUpdateError as last_exception:
>> + error_msg = error_msg_final % (
>> + node_info.name, node, last_exception)
>> + result_msgs.append((node, error_msg))
>> + logging.error(error_msg)
>> +
>> + for node_info in node_list:
>> + if node_info.clear_authorized_keys or node_info.from_public_keys or \
>> + node_info.clear_public_keys:
>> + data = {}
>> + _InitSshUpdateData(data, noded_cert_file, ssconf_store)
>> + cluster_name = data[constants.SSHS_CLUSTER_NAME]
>> + ssh_port = ssh_port_map.get(node_info.name)
>> + if not ssh_port:
>> + raise errors.OpExecError("No SSH port information available for"
>> + " node '%s', which is leaving the
>> cluster.")
>> +
>> + if node_info.clear_authorized_keys:
>> + # The 'authorized_keys' file is not solely managed by Ganeti.
>> Therefore,
>> + # we have to specify exactly which keys to clear to leave keys
>> untouched
>> + # that were not added by Ganeti.
>> + other_master_candidate_uuids = [uuid for uuid in
>> master_candidate_uuids
>> + if uuid != node_info.uuid]
>> + candidate_keys =
>> ssh.QueryPubKeyFile(other_master_candidate_uuids,
>> + key_file=pub_key_file)
>> + data[constants.SSHS_SSH_AUTHORIZED_KEYS] = \
>> + (constants.SSHS_REMOVE, candidate_keys)
>> +
>> + if node_info.clear_public_keys:
>> + data[constants.SSHS_SSH_PUBLIC_KEYS] = \
>> + (constants.SSHS_CLEAR, {})
>> + elif node_info.from_public_keys:
>> + # Since clearing the public keys subsumes removing just a single
>> key,
>> + # we only do it of clear_public_keys is 'False'.
>>
>
> s/of/if
>
>
>> +
>> + if all_keys_to_remove:
>> + data[constants.SSHS_SSH_PUBLIC_KEYS] = \
>> + (constants.SSHS_REMOVE, all_keys_to_remove)
>> +
>> + # If we have no changes to any keyfile, just return
>> + if not (constants.SSHS_SSH_PUBLIC_KEYS in data or
>> + constants.SSHS_SSH_AUTHORIZED_KEYS in data):
>> + return
>> +
>> + logging.debug("Updating SSH key setup of target node '%s'.",
>> + node_info.name)
>> + try:
>> + utils.RetryByNumberOfTimes(
>> + constants.SSHS_MAX_RETRIES,
>> + errors.SshUpdateError,
>> + run_cmd_fn, cluster_name, node_info.name,
>> pathutils.SSH_UPDATE,
>> + ssh_port, data,
>> + debug=False, verbose=False, use_cluster_key=False,
>> + ask_key=False, strict_host_check=False)
>> + except errors.SshUpdateError as last_exception:
>> + result_msgs.append(
>> + (node_info.name,
>> + ("Removing SSH keys from node '%s' failed."
>> + " This can happen when the node is already unreachable."
>> + " Error: %s" % (node_info.name, last_exception))))
>> +
>> + if all_keys_to_remove and from_public_keys:
>> + for node_uuid in nodes_remove_from_public_keys:
>> + ssh.RemovePublicKey(node_uuid, key_file=pub_key_file)
>> +
>> + return result_msgs
>> +
>> +
>> def _GenerateNodeSshKey(node_uuid, node_name, ssh_port_map,
>> pub_key_file=pathutils.SSH_PUB_KEYS,
>> ssconf_store=None,
>> diff --git a/test/py/ganeti.backend_unittest.py b/test/py/
>> ganeti.backend_unittest.py
>> index ab3de9e..484348f 100755
>> --- a/test/py/ganeti.backend_unittest.py
>> +++ b/test/py/ganeti.backend_unittest.py
>> @@ -1367,6 +1367,37 @@ class
>> TestAddRemoveGenerateNodeSshKey(testutils.GanetiTestCase):
>> self.assertEqual(1,
>> len(self._ssh_file_manager.GetAuthorizedKeysOfNode(node_name)))
>>
>> + def testRemoveMasterCandidateBulk(self):
>> + node_list = []
>> + key_map = {}
>> + for node_name, (node_uuid, node_key, _, _, _) in \
>> + self._ssh_file_manager.GetAllMasterCandidates()[:3]:
>> + node_list.append(backend.SshRemoveNodeInfo(uuid=node_uuid,
>> + name=node_name,
>> +
>> from_authorized_keys=True,
>> + from_public_keys=True,
>> +
>> clear_authorized_keys=True,
>> + clear_public_keys=True))
>> + key_map[node_name] = node_key
>> +
>> + backend.RemoveNodeSshKeyBulk(node_list,
>> + self._master_candidate_uuids,
>> + self._potential_master_candidates,
>> + 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)
>> +
>> + for node_info in node_list:
>> + self._ssh_file_manager.AssertNoNodeHasPublicKey(
>> + node_info.uuid, key_map[node_info.name])
>> + self._ssh_file_manager.AssertNodeSetOnlyHasAuthorizedKey(
>> + [node_info.name], key_map[node_info.name])
>> + self.assertEqual(0,
>> + len(self._ssh_file_manager.GetPublicKeysOfNode(node_info.name
>> )))
>> + self.assertEqual(1,
>> + len(self._ssh_file_manager.GetAuthorizedKeysOfNode(
>> node_info.name)))
>> +
>> def testRemovePotentialMasterCandidate(self):
>> (node_name, node_info) = \
>> self._ssh_file_manager.GetAllPurePotentialMasterCandidates()[0]
>> --
>> 2.6.0.rc2.230.g3dd15c0
>>
>>
>
>
See interdiff:
diff --git a/lib/backend.py b/lib/backend.py
index 3d0443c..16d3146 100644
--- a/lib/backend.py
+++ b/lib/backend.py
@@ -1930,7 +1930,8 @@ def RemoveNodeSshKeyBulk(node_list,
if from_authorized_keys or from_public_keys:
all_keys_to_remove = {}
for node_info in node_list:
-
+ if node_info.name == master_node and not keys_to_remove:
+ raise errors.SshUpdateError("Cannot remove the master node's
keys.")
if keys_to_remove:
keys = keys_to_remove
else:
@@ -1951,9 +1952,6 @@ def RemoveNodeSshKeyBulk(node_list,
if master_key in keys[node_info.uuid]:
keys[node_info.uuid].remove(master_key)
- if node_info.name == master_node and not keys_to_remove:
- raise errors.SshUpdateError("Cannot remove the master node's
keys.")
-
all_keys_to_remove.update(keys)
if all_keys_to_remove:
@@ -2076,7 +2074,7 @@ def RemoveNodeSshKeyBulk(node_list,
(constants.SSHS_CLEAR, {})
elif node_info.from_public_keys:
# Since clearing the public keys subsumes removing just a single
key,
- # we only do it of clear_public_keys is 'False'.
+ # we only do it if clear_public_keys is 'False'.
if all_keys_to_remove:
data[constants.SSHS_SSH_PUBLIC_KEYS] = \
>
> --
> Lisa Velden
> Software Engineer
> [email protected]
>
> Google Germany GmbH
> Dienerstraße 12
> 80331 München
>
> Geschäftsführer: Matthew Scott Sucherman, Paul Terence Manicle
> Registergericht und -nummer: Hamburg, HRB 86891
> Sitz der Gesellschaft: Hamburg
>
--
Helga Velroyen
Software Engineer
[email protected]
Google Germany GmbH
Dienerstraße 12
80331 München
Geschäftsführer: Matthew Scott Sucherman, Paul Terence Manicle
Registergericht und -nummer: Hamburg, HRB 86891
Sitz der Gesellschaft: Hamburg
Diese E-Mail ist vertraulich. Wenn Sie nicht der richtige Adressat sind,
leiten Sie diese bitte nicht weiter, informieren Sie den Absender und
löschen Sie die E-Mail und alle Anhänge. Vielen Dank.
This e-mail is confidential. If you are not the right addressee please do
not forward it, please inform the sender, and please erase this e-mail
including any attachments. Thanks.