Merge branch 'stable-2.14' into stable-2.15
authorKlaus Aehlig <aehlig@google.com>
Fri, 15 Jan 2016 10:17:06 +0000 (11:17 +0100)
committerKlaus Aehlig <aehlig@google.com>
Fri, 15 Jan 2016 10:26:19 +0000 (11:26 +0100)
* stable-2.14
  Test disk attachment with different primary nodes
  Check for same primary node before disk attachment
  Add detach/attach sequence test
  Allow disk attachment with external storage

* stable-2.13
  Run ssh-key renewal in debug mode during upgrade

* stable-2.12
  Increase minimal sizes of test online nodes
  Also log the high-level upgrade steps
  Add function to provide logged user feedback
  Run renew-crypto in upgrades in debug mode
  Unconditionally log upgrades at debug level
  Document healthy-majority restriction on master-failover
  Check for healthy majority on master failover with voting
  Add a predicate testing that a majority of nodes is healthy
  Fix outdated comment
  Pass arguments to correct daemons during master-failover
  Fix documentation for master-failover

* stable-2.11
  (no changes)

* stable-2.10
  KVM: explicitly configure routed NICs late

Signed-off-by: Klaus Aehlig <aehlig@google.com>
Reviewed-by: Lisa Velden <velden@google.com>

1  2 
lib/backend.py

diff --combined lib/backend.py
@@@ -56,12 -56,12 +56,12 @@@ import rando
  import re
  import shutil
  import signal
 -import socket
  import stat
  import tempfile
  import time
  import zlib
 -import copy
 +import contextlib
 +import collections
  
  from ganeti import errors
  from ganeti import http
@@@ -86,7 -86,8 +86,7 @@@ from ganeti import h
  from ganeti.storage.base import BlockDev
  from ganeti.storage.drbd import DRBD8
  from ganeti import hooksmaster
 -from ganeti.rpc import transport
 -from ganeti.rpc.errors import NoMasterError, TimeoutError
 +import ganeti.metad as metad
  
  
  _BOOT_ID_PATH = "/proc/sys/kernel/random/boot_id"
@@@ -435,12 -436,13 +435,13 @@@ def StartMasterDaemons(no_voting)
    """
  
    if no_voting:
-     masterd_args = "--no-voting --yes-do-it"
+     daemon_args = "--no-voting --yes-do-it"
    else:
-     masterd_args = ""
+     daemon_args = ""
  
    env = {
-     "EXTRA_MASTERD_ARGS": masterd_args,
+     "EXTRA_LUXID_ARGS": daemon_args,
+     "EXTRA_WCONFD_ARGS": daemon_args,
      }
  
    result = utils.RunCmd([pathutils.DAEMON_UTIL, "start-master"], env=env)
@@@ -565,8 -567,6 +566,8 @@@ def LeaveCluster(modify_ssh_setup)
        utils.RemoveFile(pub_key)
      except errors.OpExecError:
        logging.exception("Error while processing ssh files")
 +    except IOError:
 +      logging.exception("At least one SSH file was not accessible.")
  
    try:
      utils.RemoveFile(pathutils.CONFD_HMAC_KEY)
@@@ -1454,93 -1454,37 +1455,93 @@@ def AddNodeSshKey(node_uuid, node_name
      to its {ganeti_pub_keys} file
  
    """
 -  # assure that at least one of those flags is true, as the function would
 -  # not do anything otherwise
 -  assert (to_authorized_keys or to_public_keys or get_public_keys)
 +  node_list = [SshAddNodeInfo(name=node_name, uuid=node_uuid,
 +                              to_authorized_keys=to_authorized_keys,
 +                              to_public_keys=to_public_keys,
 +                              get_public_keys=get_public_keys)]
 +  return AddNodeSshKeyBulk(node_list,
 +                           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)
 +
 +
 +# Node info named tuple specifically for the use with AddNodeSshKeyBulk
 +SshAddNodeInfo = collections.namedtuple(
 +  "SshAddNodeInfo",
 +  ["uuid",
 +   "name",
 +   "to_authorized_keys",
 +   "to_public_keys",
 +   "get_public_keys"])
 +
 +
 +def AddNodeSshKeyBulk(node_list,
 +                      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):
 +  """Distributes a node's public SSH key across the cluster.
 +
 +  Note that this function should only be executed on the master node, which
 +  then will copy the new node's key to all nodes in the cluster via SSH.
 +
 +  Also note: at least one of the flags C{to_authorized_keys},
 +  C{to_public_keys}, and C{get_public_keys} has to be set to C{True} for
 +  the function to actually perform any actions.
 +
 +  @type node_list: list of SshAddNodeInfo tuples
 +  @param node_list: list of tuples containing the necessary node information for
 +    adding their keys
 +  @type potential_master_candidates: list of str
 +  @param potential_master_candidates: list of node names of potential master
 +    candidates; this should match the list of uuids in the public key file
 +
 +  """
 +  # whether there are any keys to be added or retrieved at all
 +  to_authorized_keys = any([node_info.to_authorized_keys for node_info in
 +                            node_list])
 +  to_public_keys = any([node_info.to_public_keys for node_info in
 +                        node_list])
  
    if not ssconf_store:
      ssconf_store = ssconf.SimpleStore()
  
 -  # Check and fix sanity of key file
 -  keys_by_name = ssh.QueryPubKeyFile([node_name], key_file=pub_key_file)
 -  keys_by_uuid = ssh.QueryPubKeyFile([node_uuid], key_file=pub_key_file)
 +  for node_info in node_list:
 +    # replacement not necessary for keys that are not supposed to be in the
 +    # list of public keys
 +    if not node_info.to_public_keys:
 +      continue
 +    # Check and fix sanity of key file
 +    keys_by_name = ssh.QueryPubKeyFile([node_info.name], key_file=pub_key_file)
 +    keys_by_uuid = ssh.QueryPubKeyFile([node_info.uuid], key_file=pub_key_file)
 +
 +    if (not keys_by_name or node_info.name not in keys_by_name) \
 +        and (not keys_by_uuid or node_info.uuid not in keys_by_uuid):
 +      raise errors.SshUpdateError(
 +        "No keys found for the new node '%s' (UUID %s) in the list of public"
 +        " SSH keys, neither for the name or the UUID" %
 +        (node_info.name, node_info.uuid))
 +    else:
 +      if node_info.name in keys_by_name:
 +        # Replace the name by UUID in the file as the name should only be used
 +        # temporarily
 +        ssh.ReplaceNameByUuid(node_info.uuid, node_info.name,
 +                              error_fn=errors.SshUpdateError,
 +                              key_file=pub_key_file)
  
 -  if (not keys_by_name or node_name not in keys_by_name) \
 -      and (not keys_by_uuid or node_uuid not in keys_by_uuid):
 -    raise errors.SshUpdateError(
 -      "No keys found for the new node '%s' (UUID %s) in the list of public"
 -      " SSH keys, neither for the name or the UUID" % (node_name, node_uuid))
 -  else:
 -    if node_name in keys_by_name:
 -      keys_by_uuid = {}
 -      # Replace the name by UUID in the file as the name should only be used
 -      # temporarily
 -      ssh.ReplaceNameByUuid(node_uuid, node_name,
 -                            error_fn=errors.SshUpdateError,
 -                            key_file=pub_key_file)
 -      keys_by_uuid[node_uuid] = keys_by_name[node_name]
 +  # Retrieve updated map of UUIDs to keys
 +  keys_by_uuid = ssh.QueryPubKeyFile(
 +      [node_info.uuid for node_info in node_list], key_file=pub_key_file)
  
    # Update the master node's key files
 -  if to_authorized_keys:
 -    (auth_key_file, _) = \
 -      ssh.GetAllUserFiles(constants.SSH_LOGIN_USER, mkdir=False, dircheck=False)
 -    ssh.AddAuthorizedKeys(auth_key_file, keys_by_uuid[node_uuid])
 +  (auth_key_file, _) = \
 +    ssh.GetAllUserFiles(constants.SSH_LOGIN_USER, mkdir=False, dircheck=False)
 +  for node_info in node_list:
 +    if node_info.to_authorized_keys:
 +      ssh.AddAuthorizedKeys(auth_key_file, keys_by_uuid[node_info.uuid])
  
    base_data = {}
    _InitSshUpdateData(base_data, noded_cert_file, ssconf_store)
  
    ssh_port_map = ssconf_store.GetSshPortMap()
  
 -  # Update the target node itself
 -  logging.debug("Updating SSH key files of target node '%s'.", node_name)
 -  if get_public_keys:
 -    node_data = {}
 -    _InitSshUpdateData(node_data, noded_cert_file, ssconf_store)
 -    all_keys = ssh.QueryPubKeyFile(None, key_file=pub_key_file)
 -    node_data[constants.SSHS_SSH_PUBLIC_KEYS] = \
 -      (constants.SSHS_OVERRIDE, all_keys)
 +  # Update the target nodes themselves
 +  for node_info in node_list:
 +    logging.debug("Updating SSH key files of target node '%s'.", node_info.name)
 +    if node_info.get_public_keys:
 +      node_data = {}
 +      _InitSshUpdateData(node_data, noded_cert_file, ssconf_store)
 +      all_keys = ssh.QueryPubKeyFile(None, key_file=pub_key_file)
 +      node_data[constants.SSHS_SSH_PUBLIC_KEYS] = \
 +        (constants.SSHS_OVERRIDE, all_keys)
  
 -    try:
 -      utils.RetryByNumberOfTimes(
 -          constants.SSHS_MAX_RETRIES,
 -          errors.SshUpdateError,
 -          run_cmd_fn, cluster_name, node_name, pathutils.SSH_UPDATE,
 -          ssh_port_map.get(node_name), node_data,
 -          debug=False, verbose=False, use_cluster_key=False,
 -          ask_key=False, strict_host_check=False)
 -    except errors.SshUpdateError as e:
 -      # Clean up the master's public key file if adding key fails
 -      if to_public_keys:
 -        ssh.RemovePublicKey(node_uuid)
 -      raise e
 -
 -  # Update all nodes except master and the target node
 +      try:
 +        utils.RetryByNumberOfTimes(
 +            constants.SSHS_MAX_RETRIES,
 +            errors.SshUpdateError,
 +            run_cmd_fn, cluster_name, node_info.name, pathutils.SSH_UPDATE,
 +            ssh_port_map.get(node_info.name), node_data,
 +            debug=False, verbose=False, use_cluster_key=False,
 +            ask_key=False, strict_host_check=False)
 +      except errors.SshUpdateError as e:
 +        # Clean up the master's public key file if adding key fails
 +        if node_info.to_public_keys:
 +          ssh.RemovePublicKey(node_info.uuid)
 +        raise e
 +
 +  # Update all nodes except master and the target nodes
 +  keys_by_uuid_auth = ssh.QueryPubKeyFile(
 +      [node_info.uuid for node_info in node_list
 +       if node_info.to_authorized_keys],
 +      key_file=pub_key_file)
    if to_authorized_keys:
      base_data[constants.SSHS_SSH_AUTHORIZED_KEYS] = \
 -      (constants.SSHS_ADD, keys_by_uuid)
 +      (constants.SSHS_ADD, keys_by_uuid_auth)
  
 -  pot_mc_data = copy.deepcopy(base_data)
 +  pot_mc_data = base_data.copy()
 +  keys_by_uuid_pub = ssh.QueryPubKeyFile(
 +      [node_info.uuid for node_info in node_list
 +       if node_info.to_public_keys],
 +      key_file=pub_key_file)
    if to_public_keys:
      pot_mc_data[constants.SSHS_SSH_PUBLIC_KEYS] = \
 -      (constants.SSHS_REPLACE_OR_ADD, keys_by_uuid)
 +      (constants.SSHS_REPLACE_OR_ADD, keys_by_uuid_pub)
  
    all_nodes = ssconf_store.GetNodeList()
    master_node = ssconf_store.GetMasterNode()
          error_msg = ("When adding the key of node '%s', updating SSH key"
                       " files of node '%s' failed after %s retries."
                       " Not trying again. Last error was: %s." %
 -                     (node, node_name, constants.SSHS_MAX_RETRIES,
 +                     (node, node_info.name, constants.SSHS_MAX_RETRIES,
                        last_exception))
          node_errors.append((node, error_msg))
          # We only log the error and don't throw an exception, because
@@@ -1685,83 -1620,9 +1686,83 @@@ def RemoveNodeSshKey(node_uuid, node_na
    @returns: list of feedback messages
  
    """
 +  node_list = [SshRemoveNodeInfo(uuid=node_uuid,
 +                                 name=node_name,
 +                                 from_authorized_keys=from_authorized_keys,
 +                                 from_public_keys=from_public_keys,
 +                                 clear_authorized_keys=clear_authorized_keys,
 +                                 clear_public_keys=clear_public_keys)]
 +  return RemoveNodeSshKeyBulk(node_list,
 +                              master_candidate_uuids,
 +                              potential_master_candidates,
 +                              master_uuid=master_uuid,
 +                              keys_to_remove=keys_to_remove,
 +                              pub_key_file=pub_key_file,
 +                              ssconf_store=ssconf_store,
 +                              noded_cert_file=noded_cert_file,
 +                              readd=readd,
 +                              run_cmd_fn=run_cmd_fn)
 +
 +
 +# 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):
    master_node = ssconf_store.GetMasterNode()
    ssh_port_map = ssconf_store.GetSshPortMap()
  
 +  all_keys_to_remove = {}
    if from_authorized_keys or from_public_keys:
 -    if keys_to_remove:
 -      keys = keys_to_remove
 -    else:
 -      keys = ssh.QueryPubKeyFile([node_uuid], key_file=pub_key_file)
 -      if (not keys or node_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_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_uuid]:
 -            keys[node_uuid].remove(master_key)
 -
 -    if node_name == master_node and not keys_to_remove:
 -      raise errors.SshUpdateError("Cannot remove the master node's keys.")
 -
 -    if node_uuid in keys:
 +    for node_info in node_list:
 +      # Skip nodes that don't actually need any keys to be removed.
 +      if not (node_info.from_authorized_keys or node_info.from_public_keys):
 +        continue
 +      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:
 +        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)
 +
 +      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)
 +          (constants.SSHS_REMOVE, keys_to_remove_from_authorized_keys)
          (auth_key_file, _) = \
            ssh.GetAllUserFiles(constants.SSH_LOGIN_USER, mkdir=False,
                                dircheck=False)
 -        ssh.RemoveAuthorizedKeys(auth_key_file, keys[node_uuid])
  
 -      pot_mc_data = copy.deepcopy(base_data)
 +        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)
 -        ssh.RemovePublicKey(node_uuid, key_file=pub_key_file)
 +          (constants.SSHS_REMOVE, keys_to_remove_from_public_keys)
  
        all_nodes = ssconf_store.GetNodeList()
        online_nodes = ssconf_store.GetOnlineNodeList()
 -      logging.debug("Removing key of node '%s' from all nodes but itself and"
 -                    " master.", node_name)
 +      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)
          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"
                  ask_key=False, strict_host_check=False)
            except errors.SshUpdateError as last_exception:
              error_msg = error_msg_final % (
 -                node_name, node, last_exception)
 +                node_info.name, node, last_exception)
              result_msgs.append((node, error_msg))
              logging.error(error_msg)
  
                    ask_key=False, strict_host_check=False)
              except errors.SshUpdateError as last_exception:
                error_msg = error_msg_final % (
 -                  node_name, node, last_exception)
 +                  node_info.name, node, last_exception)
                result_msgs.append((node, error_msg))
                logging.error(error_msg)
  
 -  if clear_authorized_keys or from_public_keys or 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_name)
 -    if not ssh_port:
 -      raise errors.OpExecError("No SSH port information available for"
 -                               " node '%s', which is leaving the cluster.")
 -
 -    if 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_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 clear_public_keys:
 -      data[constants.SSHS_SSH_PUBLIC_KEYS] = \
 -        (constants.SSHS_CLEAR, {})
 -    elif from_public_keys:
 -      # Since clearing the public keys subsumes removing just a single key,
 -      # we only do it of clear_public_keys is 'False'.
 -
 -      if keys[node_uuid]:
 +  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_REMOVE, keys)
 -
 -    # 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
 +          (constants.SSHS_CLEAR, {})
 +      elif node_info.from_public_keys:
 +        # Since clearing the public keys subsumes removing just a single key,
 +        # we only do it if clear_public_keys is 'False'.
 +
 +        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_name)
 -    try:
 -      utils.RetryByNumberOfTimes(
 -          constants.SSHS_MAX_RETRIES,
 -          errors.SshUpdateError,
 -          run_cmd_fn, cluster_name, node_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_name,
 -           ("Removing SSH keys from node '%s' failed."
 -            " This can happen when the node is already unreachable."
 -            " Error: %s" % (node_name, last_exception))))
 +      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
  
@@@ -2113,23 -1940,11 +2114,23 @@@ def RenewSshKeys(node_uuids, node_names
    all_node_errors = []
  
    # process non-master nodes
 +
 +  # keys to add in bulk at the end
 +  node_keys_to_add = []
 +
 +  # list of all nodes
 +  node_list = []
 +
 +  # list of keys to be removed before generating new keys
 +  node_info_to_remove = []
 +
    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
 +    node_list.append((node_uuid, node_name, master_candidate,
 +                      potential_master_candidate))
  
      keys_by_uuid = ssh.QueryPubKeyFile([node_uuid], key_file=pub_key_file)
      if not keys_by_uuid:
          # remove that node's key, because it is also the master node's key
          # and that would terminate all communication from the master to the
          # node.
 -        logging.debug("Removing SSH key of node '%s'.", node_name)
 -        node_errors = RemoveNodeSshKey(
 -           node_uuid, node_name, master_candidate_uuids,
 -           potential_master_candidates,
 -           master_uuid=master_node_uuid, from_authorized_keys=master_candidate,
 -           from_public_keys=False, clear_authorized_keys=False,
 -           clear_public_keys=False)
 -        if node_errors:
 -          all_node_errors = all_node_errors + node_errors
 +        node_info_to_remove.append(SshRemoveNodeInfo(
 +            uuid=node_uuid,
 +            name=node_name,
 +            from_authorized_keys=master_candidate,
 +            from_public_keys=False,
 +            clear_authorized_keys=False,
 +            clear_public_keys=False))
        else:
          logging.debug("Old key of node '%s' is the same as the current master"
                        " key. Not deleting that key on the node.", node_name)
  
 +  logging.debug("Removing old SSH keys of all master candidates.")
 +  if node_info_to_remove:
 +    node_errors = RemoveNodeSshKeyBulk(
 +        node_info_to_remove,
 +        master_candidate_uuids,
 +        potential_master_candidates,
 +        master_uuid=master_node_uuid)
 +    if node_errors:
 +      all_node_errors = all_node_errors + node_errors
 +
 +  for (node_uuid, node_name, master_candidate, potential_master_candidate) \
 +      in node_list:
 +
      logging.debug("Generating new SSH key for node '%s'.", node_name)
      _GenerateNodeSshKey(node_uuid, node_name, ssh_port_map,
                          pub_key_file=pub_key_file,
        ssh.AddPublicKey(node_uuid, pub_key, key_file=pub_key_file)
  
      logging.debug("Add ssh key of node '%s'.", node_name)
 -    node_errors = AddNodeSshKey(
 -        node_uuid, node_name, potential_master_candidates,
 -        to_authorized_keys=master_candidate,
 -        to_public_keys=potential_master_candidate,
 -        get_public_keys=True,
 -        pub_key_file=pub_key_file, ssconf_store=ssconf_store,
 -        noded_cert_file=noded_cert_file,
 -        run_cmd_fn=run_cmd_fn)
 -    if node_errors:
 -      all_node_errors = all_node_errors + node_errors
 +    node_info = SshAddNodeInfo(name=node_name,
 +                               uuid=node_uuid,
 +                               to_authorized_keys=master_candidate,
 +                               to_public_keys=potential_master_candidate,
 +                               get_public_keys=True)
 +    node_keys_to_add.append(node_info)
 +
 +  node_errors = AddNodeSshKeyBulk(
 +      node_keys_to_add, 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)
 +  if node_errors:
 +    all_node_errors = all_node_errors + node_errors
  
    # Renewing the master node's key
  
@@@ -3202,8 -3002,31 +3203,8 @@@ def ModifyInstanceMetadata(metadata)
      if result.failed:
        raise errors.HypervisorError("Failed to start metadata daemon")
  
 -  def _Connect():
 -    return transport.Transport(pathutils.SOCKET_DIR + "/ganeti-metad",
 -                               allow_non_master=True)
 -
 -  retries = 5
 -
 -  while True:
 -    try:
 -      trans = utils.Retry(_Connect, 1.0, constants.LUXI_DEF_CTMO)
 -      break
 -    except utils.RetryTimeout:
 -      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
 -
 -  data = serializer.DumpJson(metadata,
 -                             private_encoder=serializer.EncodeWithPrivateFields)
 -
 -  trans.Send(data)
 -  trans.Close()
 +  with contextlib.closing(metad.Client()) as client:
 +    client.UpdateConfig(metadata)
  
  
  def BlockdevCreate(disk, size, owner, on_primary, info, excl_stor):