Renew SSH keys and upgrade
authorHelga Velroyen <helgav@google.com>
Tue, 1 Jul 2014 09:29:51 +0000 (11:29 +0200)
committerHelga Velroyen <helgav@google.com>
Thu, 2 Oct 2014 07:58:27 +0000 (09:58 +0200)
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 <helgav@google.com>
Reviewed-by: Petr Pudlak <pudlak@google.com>

17 files changed:
UPGRADE
lib/backend.py
lib/cli.py
lib/client/gnt_cluster.py
lib/cmdlib/cluster.py
lib/rpc_defs.py
lib/server/noded.py
lib/tools/ssh_update.py
qa/qa_cluster.py
src/Ganeti/Constants.hs
src/Ganeti/OpCodes.hs
src/Ganeti/OpParams.hs
test/hs/Test/Ganeti/OpCodes.hs
test/py/ganeti.backend_unittest.py
test/py/ganeti.client.gnt_cluster_unittest.py
test/py/ganeti.mcpu_unittest.py
tools/post-upgrade

diff --git a/UPGRADE b/UPGRADE
index 62624ff..2186298 100644 (file)
--- 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
 ----
 
index 797bea1..33563e9 100644 (file)
@@ -1480,7 +1480,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()
@@ -1629,6 +1629,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
 
index ab42cf0..f7a4b0a 100644 (file)
@@ -146,6 +146,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",
@@ -265,6 +266,7 @@ __all__ = [
   "GetClient",
   "GetOnlineNodes",
   "GetNodesSshPorts",
+  "GetNodeUUIDs",
   "JobExecutor",
   "JobSubmittedException",
   "ParseTimespec",
@@ -1500,6 +1502,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")
@@ -3747,6 +3753,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,
@@ -3754,6 +3761,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
 
index e938b55..b7cfc71 100644 (file)
@@ -963,7 +963,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
@@ -987,8 +987,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:
@@ -1083,18 +1085,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,
@@ -1105,7 +1164,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):
@@ -2351,7 +2411,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": (
index 2a77173..3023bc2 100644 (file)
@@ -108,6 +108,28 @@ class LUClusterRenewCrypto(NoHooksLU):
 
   """
 
+  REQ_BGL = False
+
+  def ExpandNames(self):
+    self.needed_locks = {
+      locking.LEVEL_NODE: 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_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 = \
+      not self.cfg.GetClusterInfo().modify_ssh_setup and self.op.ssh_keys
+
   def _RenewNodeSslCertificates(self):
     """Renews the nodes' SSL certificates.
 
@@ -141,9 +163,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):
index c284c90..42558ee 100644 (file)
@@ -558,6 +558,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 = [
index 5cd6125..6043b6f 100644 (file)
@@ -931,6 +931,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.
 
index e7f51e7..784fc15 100644 (file)
@@ -62,6 +62,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,
   })
 
 
@@ -152,11 +153,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:
@@ -175,6 +179,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.
 
@@ -190,9 +211,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
index d5badcc..cd0c8fe 100644 (file)
@@ -1229,12 +1229,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])
index 1ba266a..c54c75e 100644 (file)
@@ -4538,6 +4538,9 @@ sshsNodeDaemonCertificate = "node_daemon_certificate"
 sshsAdd :: String
 sshsAdd = "add"
 
+sshsReplaceOrAdd :: String
+sshsReplaceOrAdd = "replace_or_add"
+
 sshsRemove :: String
 sshsRemove = "remove"
 
@@ -4547,8 +4550,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
 
index 4c8cc52..d06786f 100644 (file)
@@ -278,6 +278,7 @@ $(genOpCode "OpCode"
      [t| () |],
      OpDoc.opClusterRenewCrypto,
      [ pNodeSslCerts
+     , pSshKeys
      ],
      [])
   , ("OpQuery",
index eb7ebbf..4e3d76b 100644 (file)
@@ -294,6 +294,7 @@ module Ganeti.OpParams
   , pEnabledDataCollectors
   , pDataCollectorInterval
   , pNodeSslCerts
+  , pSshKeys
   ) where
 
 import Control.Monad (liftM, mplus)
@@ -1859,3 +1860,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 |]
index 47ba389..98f9d3b 100644 (file)
@@ -168,7 +168,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" ->
index 8423448..bfa297a 100755 (executable)
@@ -950,7 +950,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
@@ -1033,7 +1033,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):
@@ -1060,6 +1061,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"
index 01f5b91..be28eb2 100755 (executable)
 
 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
@@ -363,5 +368,102 @@ 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)
+      utils.WriteFile(self.priv_filename, data=self._PRIV_KEY)
+
+      self.pub_filename = os.path.join(
+        self.tmpdir, ".ssh", "id_%s.pub" % key_type)
+      utils.WriteFile(self.pub_filename, data=self._PUB_KEY)
+
+    self.auth_filename = os.path.join(self.tmpdir, ".ssh", "authorized_keys")
+    utils.WriteFile(self.auth_filename, data=self._AUTH_KEYS)
+
+  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 Bunny" in self.pub_key_filename)
+
+
 if __name__ == "__main__":
   testutils.GanetiTestProgram()
index e79b7ea..df95a8a 100755 (executable)
@@ -55,7 +55,6 @@ REQ_BGL_WHITELIST = compat.UniqueFrozenset([
   opcodes.OpClusterDestroy,
   opcodes.OpClusterPostInit,
   opcodes.OpClusterRename,
-  opcodes.OpClusterRenewCrypto,
   opcodes.OpNodeAdd,
   opcodes.OpNodeRemove,
   opcodes.OpTestAllocator,
index f3a1132..01f8cda 100644 (file)
@@ -57,6 +57,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__":