Package: release.debian.org
Severity: normal
User: release.debian....@packages.debian.org
Usertags: unblock

Dear Release Team,

I would like to get your approval before uploading Ganeti 2.15.2-8. The 
upload will fix Ganeti's reliance on DSA SSH keys, which are weak and no 
more accepted by Stretch's OpenSSH by default (#853129). I address the 
issue by cherry-picking upstream changes from the (unreleased) next 
stable branch. Unfortunately, these changes were never part of a stable 
upstream release, although they are already a couple years old, as 
upstream development has slowed down in the recent years.

I have deployed the package in question to a production cluster and 
successfully migrated to 2048-bit RSA keys, which is the best testing I 
can currently think of.

Full source debdiff attached. You can safely ignore the changes in 
d/control.in, the file is only used to generate d/control manually and 
on minor version upgrades.

Regards,
Apollon

unblock ganeti/2.15.2-8
diff -Nru ganeti-2.15.2/debian/changelog ganeti-2.15.2/debian/changelog
--- ganeti-2.15.2/debian/changelog	2016-12-13 17:40:29.000000000 +0200
+++ ganeti-2.15.2/debian/changelog	2017-05-23 15:49:40.000000000 +0300
@@ -1,3 +1,20 @@
+ganeti (2.15.2-8) unstable; urgency=medium
+
+  * Bump Standards to 3.9.8; no changes needed
+  * ganeti: Depend on lsb-base (>= 3.0-6) for init-functions
+  * Backport support for non-DSA SSH keys (Closes: #853129)
+    + non-DSA-SSH-key-support.patch: backport upstream work from the
+      (unreleased as of today) stable-2.16 branch.
+    + fix-ssh-key-renewal-on-single-node-clusters.patch: fix gnt-cluster
+      renew-crypto --new-ssh-keys on single-node clusters.
+    + set-defaults-for-ssh-type-bits.patch: transparently handle the new SSH
+      key type/length parameters without running cfgupgrade.
+  * Document the new SSH key support in d/NEWS.
+  * Update project Homepage (Closes: #862829)
+  * d/copyright: bump years
+
+ -- Apollon Oikonomopoulos <apoi...@debian.org>  Tue, 23 May 2017 15:49:40 +0300
+
 ganeti (2.15.2-7) unstable; urgency=medium
 
   * Drop dependency on MonadCatchIO-transformers (Closes: #844970)
diff -Nru ganeti-2.15.2/debian/control ganeti-2.15.2/debian/control
--- ganeti-2.15.2/debian/control	2016-12-13 17:40:29.000000000 +0200
+++ ganeti-2.15.2/debian/control	2017-05-23 15:49:40.000000000 +0300
@@ -54,9 +54,9 @@
  iproute2 | iproute,
  bash-completion,
  po-debconf
-Standards-Version: 3.9.7
+Standards-Version: 3.9.8
 X-Python-Version: >= 2.6
-Homepage: https://code.google.com/p/ganeti/
+Homepage: http://www.ganeti.org/
 Vcs-Browser: https://anonscm.debian.org/gitweb/?p=pkg-ganeti/ganeti.git
 Vcs-Git: https://anonscm.debian.org/git/pkg-ganeti/ganeti.git
 
@@ -75,7 +75,7 @@
  ganeti-haskell-2.15 (<< ${source:Version}.1~),
  ganeti-htools-2.15 (>= ${source:Version}),
  ganeti-htools-2.15 (<< ${source:Version}.1~),
- adduser, ${misc:Depends}, python
+ adduser, ${misc:Depends}, python, lsb-base (>= 3.0-6)
 Recommends: drbd-utils | drbd8-utils (>= 8.0.7),
  qemu-kvm | xen-system-amd64,
  ganeti-instance-debootstrap, ndisc6
diff -Nru ganeti-2.15.2/debian/control.in ganeti-2.15.2/debian/control.in
--- ganeti-2.15.2/debian/control.in	2016-07-09 14:58:06.000000000 +0300
+++ ganeti-2.15.2/debian/control.in	2017-05-23 15:49:40.000000000 +0300
@@ -5,6 +5,7 @@
 Uploaders: Guido Trotter <ultrot...@debian.org>,
  Apollon Oikonomopoulos <apoi...@debian.org>
 Build-Depends: debhelper (>= 9), dh-autoreconf,
+ dh-python,
  m4,
  pandoc,
  python-all,
@@ -35,6 +36,7 @@
  libghc-test-framework-quickcheck2-dev,
  libghc-test-framework-hunit-dev,
  libghc-temporary-dev,
+ libghc-old-time-dev,
  libpcre3-dev,
  libcurl4-openssl-dev,
  python-simplejson,
@@ -52,11 +54,11 @@
  iproute2 | iproute,
  bash-completion,
  po-debconf
-Standards-Version: 3.9.6
+Standards-Version: 3.9.8
 X-Python-Version: >= 2.6
-Homepage: http://code.google.com/p/ganeti/
-Vcs-Browser: http://anonscm.debian.org/gitweb/?p=pkg-ganeti/ganeti.git
-Vcs-Git: git://anonscm.debian.org/pkg-ganeti/ganeti.git
+Homepage: http://www.ganeti.org/
+Vcs-Browser: https://anonscm.debian.org/gitweb/?p=pkg-ganeti/ganeti.git
+Vcs-Git: https://anonscm.debian.org/git/pkg-ganeti/ganeti.git
 
 Package: ganeti2
 Architecture: all
@@ -73,9 +75,9 @@
  ganeti-haskell-#VER# (<< ${source:Version}.1~),
  ganeti-htools-#VER# (>= ${source:Version}),
  ganeti-htools-#VER# (<< ${source:Version}.1~),
- adduser, ${misc:Depends}, python
-Recommends: drbd-utils | drbd8-utils (>= 8.0.7), qemu-kvm |
- xen-linux-system-amd64 | xen-linux-system-686-pae,
+ adduser, ${misc:Depends}, python, lsb-base (>= 3.0-6)
+Recommends: drbd-utils | drbd8-utils (>= 8.0.7),
+ qemu-kvm | xen-system-amd64,
  ganeti-instance-debootstrap, ndisc6
 Suggests: ganeti-doc, blktap-dkms, molly-guard
 Conflicts: ganeti-htools
@@ -100,6 +102,7 @@
 Depends: ${shlibs:Depends},
  ${misc:Depends},
  ${python:Depends},
+ python,
  lvm2,
  openssh-client,
  openssh-server,
diff -Nru ganeti-2.15.2/debian/copyright ganeti-2.15.2/debian/copyright
--- ganeti-2.15.2/debian/copyright	2016-07-09 14:58:06.000000000 +0300
+++ ganeti-2.15.2/debian/copyright	2017-05-23 15:49:40.000000000 +0300
@@ -1,6 +1,6 @@
 Format: http://www.debian.org/doc/packaging-manuals/copyright-format/1.0/
 Upstream-Name: ganeti
-Source: http://code.google.com/p/ganeti
+Source: https://github.com/ganeti/ganeti
 
 Files: *
 Copyright: Copyright (c) 2006-2015 Google Inc.
@@ -8,7 +8,7 @@
 
 Files: debian/*
 Copyright: Copyright (c) 2007 Leonardo Rodrigues de Mello <l...@lmello.eu.org>
-           Copyright (c) 2007-2015 Debian Ganeti Team <pkg-gan...@lists.alioth.debian.org>
+           Copyright (c) 2007-2017 Debian Ganeti Team <pkg-gan...@lists.alioth.debian.org>
 License: GPL-2+
 
 License: BSD-2-Clause
diff -Nru ganeti-2.15.2/debian/NEWS ganeti-2.15.2/debian/NEWS
--- ganeti-2.15.2/debian/NEWS	2016-07-09 14:58:06.000000000 +0300
+++ ganeti-2.15.2/debian/NEWS	2017-05-23 15:49:40.000000000 +0300
@@ -1,3 +1,23 @@
+ganeti (2.15.2-8) unstable; urgency=medium
+
+  This version introduces support for non-DSA SSH keys. Previously, Ganeti
+  relied exclusively on DSA SSH keys for intra-cluster SSH as a hardcoded
+  default. However, DSA keys are regarded as weak and are no longer accepted
+  by sshd since openssh 7.1, leading to cumbersome Ganeti cluster setups. This
+  version adds support for specifying additional key types (RSA and ECDSA), as
+  well as key length.
+
+  The default for newly-created clusters is to use 2048-bit RSA keys. For
+  existing clusters you can switch over to RSA or ECDSA keys, using
+
+  gnt-cluster renew-crypto --new-ssh-keys --ssh-key-type=RSA --ssh-key-bits=2048
+
+  The new key type support introduces backend changes and requires that all
+  nodes run at least 2.15.2-8, so please make sure to upgrade all nodes at the
+  same time.
+
+ -- Apollon Oikonomopoulos <apoi...@debian.org>  Thu, 25 May 2017 11:58:31 +0300
+
 ganeti (2.15.2-1) unstable; urgency=high
 
   ganeti-rapi is now bound to the loopback interface by default and anonymous
diff -Nru ganeti-2.15.2/debian/patches/fix-ssh-key-renewal-on-single-node-clusters.patch ganeti-2.15.2/debian/patches/fix-ssh-key-renewal-on-single-node-clusters.patch
--- ganeti-2.15.2/debian/patches/fix-ssh-key-renewal-on-single-node-clusters.patch	1970-01-01 02:00:00.000000000 +0200
+++ ganeti-2.15.2/debian/patches/fix-ssh-key-renewal-on-single-node-clusters.patch	2017-05-23 15:49:40.000000000 +0300
@@ -0,0 +1,49 @@
+From be5be52a0af2e887889cd7bdeb76d4ab1529b137 Mon Sep 17 00:00:00 2001
+From: Apollon Oikonomopoulos <apoi...@debian.org>
+Date: Wed, 24 May 2017 16:15:54 +0300
+Subject: [PATCH 1/2] backend: make SSH key renewal work on single-node
+ clusters
+
+Currently gnt-cluster renew-crypt will unconditionally call
+AddNodeSshKeyBulk() to replace non-master node keys, regardless of
+whether there are non-master nodes or not. OTOH, AddNodeSshKeyBulk()
+expects that at least one operation should be perfomed and dies with an
+assertion error otherwise. Thus, on single node clusters, where there is
+only a single master node, gnt-cluster renew-crypto --new-ssh-keys will
+always fail.
+
+Fix this by calling AddNodeSshKeyBulk only if node_keys_to_add is not
+empty.
+---
+ lib/backend.py | 15 ++++++++-------
+ 1 file changed, 8 insertions(+), 7 deletions(-)
+
+diff --git a/lib/backend.py b/lib/backend.py
+index 9b363d297..89e93e010 100644
+--- a/lib/backend.py
++++ b/lib/backend.py
+@@ -2100,13 +2100,14 @@ def RenewSshKeys(node_uuids, node_names, master_candidate_uuids,
+                                get_public_keys=True)
+     node_keys_to_add.append(node_info)
+ 
+-  node_errors = AddNodeSshKeyBulk(
+-      node_keys_to_add, potential_master_candidates,
+-      pub_key_file=ganeti_pub_keys_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
++  if node_keys_to_add:
++    node_errors = AddNodeSshKeyBulk(
++        node_keys_to_add, potential_master_candidates,
++        pub_key_file=ganeti_pub_keys_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
+ 
+-- 
+2.11.0
+
diff -Nru ganeti-2.15.2/debian/patches/non-DSA-SSH-key-support.patch ganeti-2.15.2/debian/patches/non-DSA-SSH-key-support.patch
--- ganeti-2.15.2/debian/patches/non-DSA-SSH-key-support.patch	1970-01-01 02:00:00.000000000 +0200
+++ ganeti-2.15.2/debian/patches/non-DSA-SSH-key-support.patch	2017-05-23 15:49:40.000000000 +0300
@@ -0,0 +1,2013 @@
+From 45a89715dea9a6e038103f01d024fe2b555061d2 Mon Sep 17 00:00:00 2001
+From: Apollon Oikonomopoulos <apoi...@debian.org>
+Date: Tue, 23 May 2017 15:36:20 +0300
+Subject: [PATCH] Backport non-DSA SSH key support
+Bug-Debian: https://bugs.debian.org/853129
+Forwarded: not-needed
+
+Cherry-pick and squash the following commits from stable-2.15:
+
+commit 6890735f98338c6e154906e97f931a77a478ea2f
+Author: Hrvoje Ribicic <r...@google.com>
+Date:   Mon Nov 9 18:49:53 2015 +0100
+
+    Add entries describing new gnt-cluster params to manpage
+
+    And also sprinkle reminders of when to update them across the codebase.
+
+    Signed-off-by: Hrvoje Ribicic <r...@google.com>
+    Reviewed-by: Helga Velroyen <hel...@google.com>
+    (cherry picked from commit 2ebf4e8b0644b9ea05f377bd654e36c6d9e4e8bc)
+
+commit e8a4295edc787a2e6353c97664fbc5d612c40d49
+Author: Hrvoje Ribicic <r...@google.com>
+Date:   Mon Nov 9 18:18:33 2015 +0100
+
+    QA: Add ssh-key-type and -bits tests
+
+    This patch expands the testing of SSH key renewal by changing the key
+    type existing on a cluster during the QA.
+
+    Signed-off-by: Hrvoje Ribicic <r...@google.com>
+    Reviewed-by: Helga Velroyen <hel...@google.com>
+    (cherry picked from commit af0a89fd07276e3373f2d2991d0c76ed13a2c29a)
+
+commit 32a3f5f0dca90021b967f6ccce08e4c3ce32b5f8
+Author: Hrvoje Ribicic <r...@google.com>
+Date:   Fri Nov 6 16:01:42 2015 +0000
+
+    QA: Extend AssertCommand to allow not forwarding the agent
+
+    When testing SSH-related behavior in Ganeti, having the SSH agent
+    forwarded in all the command-running utilities can produce spurious
+    errors, or worse yet, allow real ones to sneak by. In this patch, the
+    AssertCommand function is extended to allow disabling of agent
+    forwarding. This also switches off connection multiplexing, as the
+    multiplexed connection forwards agents implicitly.
+
+    Signed-off-by: Hrvoje Ribicic <r...@google.com>
+    Reviewed-by: Helga Velroyen <hel...@google.com>
+    (cherry picked from commit e90da976de6cbae6831f8c11dd96c0eab7e29abe)
+
+commit 15d155105feb7a54b5b16f77763ef3b088b13434
+Author: Hrvoje Ribicic <r...@google.com>
+Date:   Fri Nov 6 02:53:00 2015 +0100
+
+    Fix typo
+
+    Signed-off-by: Hrvoje Ribicic <r...@google.com>
+    Reviewed-by: Helga Velroyen <hel...@google.com>
+    (cherry picked from commit e047dee890c2e731dbabb390402fc8358221c7af)
+
+commit 1d4b4fb15cb46b628205b859458662403dc5e9d5
+Author: Hrvoje Ribicic <r...@google.com>
+Date:   Fri Nov 6 02:35:51 2015 +0100
+
+    Fail early for invalid key type and size combinations
+
+    The ssh-keygen utility permits only some combinations of key types and
+    bit sizes. As many more things can go wrong late in the renewal
+    process, this patch introduces prerequisite checks mimicking those of
+    ssh-keygen.
+
+    Signed-off-by: Hrvoje Ribicic <r...@google.com>
+    Reviewed-by: Helga Velroyen <hel...@google.com>
+    (cherry picked from commit ff8695380c59642671c17c30b4a24264b1530d10)
+
+commit c00981f1ddc49ec89947a63d8b399b8a2c6572ea
+Author: Hrvoje Ribicic <r...@google.com>
+Date:   Thu Nov 5 14:13:58 2015 +0100
+
+    Handle SSH key changes in upgrades and downgrades
+
+    When performing an upgrade of an old cluster, it is necessary to set
+    the SSH key parameters to the exact same values earlier versions
+    implicitly used - DSA with 1024 bits.
+
+    In the other direction, we simply do not permit downgrades if keys
+    other than DSA are being used. Triggering a gnt-cluster renew-crypto
+    might be time-consuming and surprising for the user, so we are simply
+    throwing out an error message, explaining that the downgrade cannot be
+    performed in the current state of the cluster.
+
+    Signed-off-by: Hrvoje Ribicic <r...@google.com>
+    Reviewed-by: Helga Velroyen <hel...@google.com>
+    (cherry picked from commit e3a489401eab9041788b231532a8c2c4971aa3cf)
+
+commit 00966081d5770726c66b1b129c46873eb8552633
+Author: Hrvoje Ribicic <r...@google.com>
+Date:   Wed Nov 4 13:24:03 2015 +0000
+
+    Allow SSH key property changes
+
+    By explicitly specifying the old and new SSH key type in the SSH key
+    renewal, this patch allows the switching of SSH key types to take place
+    during such an operation.
+
+    Signed-off-by: Hrvoje Ribicic <r...@google.com>
+    Reviewed-by: Helga Velroyen <hel...@google.com>
+    (cherry picked from commit c64af824018a505c59453d6a645f11f0b8fb8877)
+
+commit 7af8f17b5442207f56d36e0ecb616eba506925c2
+Author: Hrvoje Ribicic <r...@google.com>
+Date:   Tue Oct 13 12:05:18 2015 -0400
+
+    Use the SSH key parameters when generating keys
+
+    This patch makes sure that the parameters introduced in previous
+    patches propagates wherever SSH keys are generated and used, allowing
+    Ganeti to use different types of SSH keys. With tis patch, the key type
+    can be set only at cluster initialization time.
+
+    Signed-off-by: Hrvoje Ribicic <r...@google.com>
+    Reviewed-by: Helga Velroyen <hel...@google.com>
+    (cherry picked from commit 33dea8cba2be219e6be1204e1e27def85616dc5b)
+
+commit f3cb90123e3aea20b38f46674b23aba9a83d9470
+Author: Hrvoje Ribicic <r...@google.com>
+Date:   Wed Nov 18 14:58:51 2015 +0000
+
+    Do not generate the ganeti_pub_keys file with --no-ssh-init
+
+    Prior to this patch, gnt-cluster renew-crypto still created the
+    ganeti_pub_keys file regardless of whether the cluster was initiated
+    with --no-ssh-init or not. Instead, query the matching config parameter
+    and build the file only if Ganeti manages SSH keys.
+
+    Signed-off-by: Hrvoje Ribicic <r...@google.com>
+    Reviewed-by: Helga Velroyen <hel...@google.com>
+    (cherry picked from commit b33ef423ed4d3ddd22af2ea050920fb30a945d04)
+
+commit 1e7f97429025438a89f376c921f46dfcd6c8d99b
+Author: Hrvoje Ribicic <r...@google.com>
+Date:   Wed Nov 18 14:53:53 2015 +0000
+
+    Add querying of ssh-related config values
+
+    To allow various command-line operations like renew-crypto and node
+    adds to know how to generate SSH keys, some config values need to be
+    queried outside of LUs. This patch adds the ssh_key_type and
+    ssh_key_bits to the config values that can be queried.
+
+    Signed-off-by: Hrvoje Ribicic <r...@google.com>
+    Reviewed-by: Helga Velroyen <hel...@google.com>
+    (cherry picked from commit db6051b118fcca9181fd91ae497c9be3b97ca5e3)
+
+commit 64e8298d284b5d50e49a592b684c3c178417fe9f
+Author: Hrvoje Ribicic <r...@google.com>
+Date:   Wed Nov 18 14:49:19 2015 +0000
+
+    Add modify_ssh_setup to queryable config params
+
+    As this will be necessary for checking whether to create the
+    ganeti_pub_keys file.
+
+    Signed-off-by: Hrvoje Ribicic <r...@google.com>
+    Reviewed-by: Helga Velroyen <hel...@google.com>
+    (cherry picked from commit e4bd432d69d33cfe70c8c08130ec2d25d9f1673f)
+
+commit a8b24d1a6b77340bc0df6fca36222e5e97a70594
+Author: Hrvoje Ribicic <r...@google.com>
+Date:   Tue Oct 13 21:57:02 2015 +0000
+
+    Add helper function for querying cluster properties
+
+    As more and more configuration values will have to be made available via
+    queries, this patch adds a small helper method for these.
+
+    Signed-off-by: Hrvoje Ribicic <r...@google.com>
+    Reviewed-by: Helga Velroyen <hel...@google.com>
+    (cherry picked from commit 6b2e842cb625f58e6bb455198282e1d0fdc62fbe)
+
+commit a35558d0271bf0ec14ff917e50cd55c5617179b0
+Author: Hrvoje Ribicic <r...@google.com>
+Date:   Mon Oct 12 20:42:35 2015 +0000
+
+    Show info about new params in gnt-cluster info
+
+    With this patch, gnt-cluster info shows both the ssh key type and the
+    key length.
+
+    Signed-off-by: Hrvoje Ribicic <r...@google.com>
+    Reviewed-by: Helga Velroyen <hel...@google.com>
+    (cherry picked from commit f8dc2eb986a7e242515604bf42fbb2879fbccd54)
+
+commit 51699c1ad79546b67cf8f321a3143902332cf481
+Author: Helga Velroyen <hel...@google.com>
+Date:   Fri Nov 6 13:46:44 2015 +0100
+
+    Make 'modify ssh setup' queryable
+
+    This enables the config to be queried for the configuration
+    parameter 'modify ssh setup'. This will later be used in
+    gnt-node add.
+
+    Signed-off-by: Helga Velroyen <hel...@google.com>
+    Reviewed-by: Klaus Aehlig <aeh...@google.com>
+    (cherry picked from commit 56eb7d77ea018030937e4efe8a32777d66c449d4)
+
+commit 60001eb085b0cd7cc237c658002dcb6aacca0d51
+Author: Helga Velroyen <hel...@google.com>
+Date:   Wed Nov 11 13:02:13 2015 +0100
+
+    Show 'modify ssh setup' in cluster info
+
+    This shows the parameter 'modify ssh setup' in the
+    output of 'gnt-cluster info', to make the information
+    more accessible than only writing it in the configuration.
+
+    Signed-off-by: Helga Velroyen <hel...@google.com>
+    Reviewed-by: Klaus Aehlig <aeh...@google.com>
+    (cherry picked from commit bbb08fcc9ade9fd0ebe166c78277423bb68a1ac0)
+
+commit 3a40bbcd5ee81b8c703b41dff8bae54841b4f032
+Author: Hrvoje Ribicic <r...@google.com>
+Date:   Mon Oct 12 11:39:11 2015 -0400
+
+    Add the SSH key type and length to the config, and set them
+
+    This patch uses the previously added CLI options to allow the key
+    parameters to be specified at initialization time and saved in the
+    configuration.
+
+    Signed-off-by: Hrvoje Ribicic <r...@google.com>
+    Reviewed-by: Helga Velroyen <hel...@google.com>
+    (cherry picked from commit ce90bd13691a2be26458754b5e185e300fa843c4)
+
+commit a2f04aea6bcd929e98b66311b62cd307c34318ba
+Author: Hrvoje Ribicic <r...@google.com>
+Date:   Sun Oct 11 19:03:09 2015 -0400
+
+    Change SSH key types to a proper Haskell sum type
+
+    This will allow us to perform validation of opcode params that are SSH
+    key types.
+
+    Signed-off-by: Hrvoje Ribicic <r...@google.com>
+    Reviewed-by: Helga Velroyen <hel...@google.com>
+    (cherry picked from commit 0d75de77077b0a02154a224f838562172ec29284)
+
+commit 0c8c0a3f68b41cf8064457ad5f73d45c3cebc455
+Author: Hrvoje Ribicic <r...@google.com>
+Date:   Fri Oct 9 20:57:38 2015 +0000
+
+    Add the SSH key options
+
+    The two options added in this patch are ssh-key-bits and
+    ssh-key-type, which will control the length and type of key later.
+    They are added to the gnt-cluster init and renew-crypto submethods.
+
+    Signed-off-by: Hrvoje Ribicic <r...@google.com>
+    Reviewed-by: Helga Velroyen <hel...@google.com>
+    (cherry picked from commit 87416ca571d38e72394ac37d5e8aa82cb7d559c8)
+---
+ lib/backend.py                                     | 86 +++++++++++++---------
+ lib/bootstrap.py                                   | 27 ++++---
+ lib/cli_opts.py                                    | 13 ++++
+ lib/client/gnt_cluster.py                          | 55 ++++++++++----
+ lib/client/gnt_node.py                             | 11 ++-
+ lib/cmdlib/cluster/__init__.py                     | 49 ++++++++----
+ lib/cmdlib/cluster/verify.py                       |  3 +-
+ lib/ht.py                                          |  1 +
+ lib/objects.py                                     |  8 ++
+ lib/rpc_defs.py                                    |  5 +-
+ lib/server/noded.py                                |  9 ++-
+ lib/ssh.py                                         | 64 +++++++++++++---
+ lib/tools/cfgupgrade.py                            | 51 ++++++++++++-
+ lib/tools/common.py                                |  6 +-
+ lib/tools/prepare_node_join.py                     |  9 ++-
+ lib/tools/ssh_update.py                            | 13 +++-
+ man/gnt-cluster.rst                                | 19 +++++
+ qa/qa_cluster.py                                   | 64 +++++++++++++++-
+ qa/qa_utils.py                                     | 28 +++++--
+ src/Ganeti/Constants.hs                            | 21 +++++-
+ src/Ganeti/Objects.hs                              |  2 +
+ src/Ganeti/OpCodes.hs                              |  4 +-
+ src/Ganeti/OpParams.hs                             | 20 ++++-
+ src/Ganeti/Query/Server.hs                         | 12 ++-
+ src/Ganeti/Rpc.hs                                  | 12 +--
+ src/Ganeti/Types.hs                                | 11 +++
+ test/hs/Test/Ganeti/Objects.hs                     |  7 ++
+ test/hs/Test/Ganeti/OpCodes.hs                     |  9 ++-
+ test/py/cfgupgrade_unittest.py                     |  2 +
+ test/py/ganeti.backend_unittest.py                 | 20 +++--
+ test/py/ganeti.client.gnt_cluster_unittest.py      |  5 +-
+ test/py/ganeti.ssh_unittest.py                     | 61 ++++++++++++++-
+ test/py/ganeti.tools.prepare_node_join_unittest.py |  6 +-
+ 33 files changed, 575 insertions(+), 138 deletions(-)
+
+diff --git a/lib/backend.py b/lib/backend.py
+index d470060f8..9b363d297 100644
+--- a/lib/backend.py
++++ b/lib/backend.py
+@@ -967,8 +967,8 @@ def _VerifyClientCertificate(cert_file=pathutils.NODED_CLIENT_CERT_FILE):
+   return (None, utils.GetCertificateDigest(cert_filename=cert_file))
+ 
+ 
+-def _VerifySshSetup(node_status_list, my_name,
+-                    pub_key_file=pathutils.SSH_PUB_KEYS):
++def _VerifySshSetup(node_status_list, my_name, ssh_key_type,
++                    ganeti_pub_keys_file=pathutils.SSH_PUB_KEYS):
+   """Verifies the state of the SSH key files.
+ 
+   @type node_status_list: list of tuples
+@@ -977,8 +977,10 @@ def _VerifySshSetup(node_status_list, my_name,
+     is_potential_master_candidate, online)
+   @type my_name: str
+   @param my_name: name of this node
+-  @type pub_key_file: str
+-  @param pub_key_file: filename of the public key file
++  @type ssh_key_type: one of L{constants.SSHK_ALL}
++  @param ssh_key_type: type of key used on nodes
++  @type ganeti_pub_keys_file: str
++  @param ganeti_pub_keys_file: filename of the public keys file
+ 
+   """
+   if node_status_list is None:
+@@ -994,16 +996,16 @@ def _VerifySshSetup(node_status_list, my_name,
+ 
+   result = []
+ 
+-  if not os.path.exists(pub_key_file):
++  if not os.path.exists(ganeti_pub_keys_file):
+     result.append("The public key file '%s' does not exist. Consider running"
+                   " 'gnt-cluster renew-crypto --new-ssh-keys"
+-                  " [--no-ssh-key-check]' to fix this." % pub_key_file)
++                  " [--no-ssh-key-check]' to fix this." % ganeti_pub_keys_file)
+     return result
+ 
+   pot_mc_uuids = [uuid for (uuid, _, _, _, _) in node_status_list]
+   offline_nodes = [uuid for (uuid, _, _, _, online) in node_status_list
+                    if not online]
+-  pub_keys = ssh.QueryPubKeyFile(None)
++  pub_keys = ssh.QueryPubKeyFile(None, key_file=ganeti_pub_keys_file)
+ 
+   if potential_master_candidate:
+     # Check that the set of potential master candidates matches the
+@@ -1026,14 +1028,14 @@ def _VerifySshSetup(node_status_list, my_name,
+ 
+     (_, key_files) = \
+       ssh.GetAllUserFiles(constants.SSH_LOGIN_USER, mkdir=False, dircheck=False)
+-    (_, dsa_pub_key_filename) = key_files[constants.SSHK_DSA]
++    (_, node_pub_key_file) = key_files[ssh_key_type]
+ 
+     my_keys = pub_keys[my_uuid]
+ 
+-    dsa_pub_key = utils.ReadFile(dsa_pub_key_filename)
+-    if dsa_pub_key.strip() not in my_keys:
++    node_pub_key = utils.ReadFile(node_pub_key_file)
++    if node_pub_key.strip() not in my_keys:
+       result.append("The dsa key of node %s does not match this node's key"
+-                    " in the pub key file." % (my_name))
++                    " in the pub key file." % my_name)
+     if len(my_keys) != 1:
+       result.append("There is more than one key for node %s in the public key"
+                     " file." % my_name)
+@@ -1157,8 +1159,9 @@ def VerifyNode(what, cluster_name, all_hvparams, node_groups, groups_cfg):
+     result[constants.NV_CLIENT_CERT] = _VerifyClientCertificate()
+ 
+   if constants.NV_SSH_SETUP in what:
++    node_status_list, key_type = what[constants.NV_SSH_SETUP]
+     result[constants.NV_SSH_SETUP] = \
+-      _VerifySshSetup(what[constants.NV_SSH_SETUP], my_name)
++      _VerifySshSetup(node_status_list, my_name, key_type)
+     if constants.NV_SSH_CLUTTER in what:
+       result[constants.NV_SSH_CLUTTER] = \
+         _VerifySshClutter(what[constants.NV_SSH_SETUP], my_name)
+@@ -1857,8 +1860,8 @@ def RemoveNodeSshKey(node_uuid, node_name,
+   return result_msgs
+ 
+ 
+-def _GenerateNodeSshKey(node_uuid, node_name, ssh_port_map,
+-                        pub_key_file=pathutils.SSH_PUB_KEYS,
++def _GenerateNodeSshKey(node_uuid, node_name, ssh_port_map, ssh_key_type,
++                        ssh_key_bits, pub_key_file=pathutils.SSH_PUB_KEYS,
+                         ssconf_store=None,
+                         noded_cert_file=pathutils.NODED_CERT_FILE,
+                         run_cmd_fn=ssh.RunSshCmdWithStdin,
+@@ -1871,6 +1874,10 @@ def _GenerateNodeSshKey(node_uuid, node_name, ssh_port_map,
+   @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
++  @type ssh_key_type: One of L{constants.SSHK_ALL}
++  @param ssh_key_type: the type of SSH key to be generated
++  @type ssh_key_bits: int
++  @param ssh_key_bits: the length of the key to be generated
+ 
+   """
+   if not ssconf_store:
+@@ -1885,7 +1892,7 @@ def _GenerateNodeSshKey(node_uuid, node_name, ssh_port_map,
+   data = {}
+   _InitSshUpdateData(data, noded_cert_file, ssconf_store)
+   cluster_name = data[constants.SSHS_CLUSTER_NAME]
+-  data[constants.SSHS_GENERATE] = {constants.SSHS_SUFFIX: suffix}
++  data[constants.SSHS_GENERATE] = (ssh_key_type, ssh_key_bits, suffix)
+ 
+   run_cmd_fn(cluster_name, node_name, pathutils.SSH_UPDATE,
+              ssh_port_map.get(node_name), data,
+@@ -1960,8 +1967,9 @@ def _ReplaceMasterKeyOnMaster(root_keyfiles):
+ 
+ 
+ def RenewSshKeys(node_uuids, node_names, master_candidate_uuids,
+-                 potential_master_candidates,
+-                 pub_key_file=pathutils.SSH_PUB_KEYS,
++                 potential_master_candidates, old_key_type, new_key_type,
++                 new_key_bits,
++                 ganeti_pub_keys_file=pathutils.SSH_PUB_KEYS,
+                  ssconf_store=None,
+                  noded_cert_file=pathutils.NODED_CERT_FILE,
+                  run_cmd_fn=ssh.RunSshCmdWithStdin):
+@@ -1975,8 +1983,14 @@ def RenewSshKeys(node_uuids, node_names, master_candidate_uuids,
+   @type master_candidate_uuids: list of str
+   @param master_candidate_uuids: list of UUIDs of master candidates or
+     master node
+-  @type pub_key_file: str
+-  @param pub_key_file: file path of the the public key file
++  @type old_key_type: One of L{constants.SSHK_ALL}
++  @param old_key_type: the type of SSH key already present on nodes
++  @type new_key_type: One of L{constants.SSHK_ALL}
++  @param new_key_type: the type of SSH key to be generated
++  @type new_key_bits: int
++  @param new_key_bits: the length of the key to be generated
++  @type ganeti_pub_keys_file: str
++  @param ganeti_pub_keys_file: file path of the the public key file
+   @type noded_cert_file: str
+   @param noded_cert_file: path of the noded SSL certificate file
+   @type run_cmd_fn: function
+@@ -1998,8 +2012,9 @@ def RenewSshKeys(node_uuids, node_names, master_candidate_uuids,
+ 
+   (_, root_keyfiles) = \
+     ssh.GetAllUserFiles(constants.SSH_LOGIN_USER, mkdir=False, dircheck=False)
+-  (_, dsa_pub_keyfile) = root_keyfiles[constants.SSHK_DSA]
+-  old_master_key = utils.ReadFile(dsa_pub_keyfile)
++  (_, old_pub_keyfile) = root_keyfiles[old_key_type]
++  (_, new_pub_keyfile) = root_keyfiles[new_key_type]
++  old_master_key = utils.ReadFile(old_pub_keyfile)
+ 
+   node_uuid_name_map = zip(node_uuids, node_names)
+ 
+@@ -2022,7 +2037,8 @@ def RenewSshKeys(node_uuids, node_names, master_candidate_uuids,
+     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)
++    keys_by_uuid = ssh.QueryPubKeyFile([node_uuid],
++                                       key_file=ganeti_pub_keys_file)
+     if not keys_by_uuid:
+       raise errors.SshUpdateError("No public key of node %s (UUID %s) found,"
+                                   " not generating a new key."
+@@ -2030,7 +2046,7 @@ def RenewSshKeys(node_uuids, node_names, master_candidate_uuids,
+ 
+     if master_candidate:
+       logging.debug("Fetching old SSH key from node '%s'.", node_name)
+-      old_pub_key = ssh.ReadRemoteSshPubKeys(dsa_pub_keyfile,
++      old_pub_key = ssh.ReadRemoteSshPubKeys(old_pub_keyfile,
+                                              node_name, cluster_name,
+                                              ssh_port_map[node_name],
+                                              False, # ask_key
+@@ -2055,15 +2071,15 @@ def RenewSshKeys(node_uuids, node_names, master_candidate_uuids,
+                       " key. Not deleting that key on the node.", node_name)
+ 
+     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,
++    _GenerateNodeSshKey(node_uuid, node_name, ssh_port_map, new_key_type,
++                        new_key_bits, pub_key_file=ganeti_pub_keys_file,
+                         ssconf_store=ssconf_store,
+                         noded_cert_file=noded_cert_file,
+                         run_cmd_fn=run_cmd_fn)
+ 
+     try:
+       logging.debug("Fetching newly created SSH key from node '%s'.", node_name)
+-      pub_key = ssh.ReadRemoteSshPubKeys(dsa_pub_keyfile,
++      pub_key = ssh.ReadRemoteSshPubKeys(new_pub_keyfile,
+                                          node_name, cluster_name,
+                                          ssh_port_map[node_name],
+                                          False, # ask_key
+@@ -2073,8 +2089,8 @@ def RenewSshKeys(node_uuids, node_names, master_candidate_uuids,
+                                   " (UUID %s)" % (node_name, node_uuid))
+ 
+     if potential_master_candidate:
+-      ssh.RemovePublicKey(node_uuid, key_file=pub_key_file)
+-      ssh.AddPublicKey(node_uuid, pub_key, key_file=pub_key_file)
++      ssh.RemovePublicKey(node_uuid, key_file=ganeti_pub_keys_file)
++      ssh.AddPublicKey(node_uuid, pub_key, key_file=ganeti_pub_keys_file)
+ 
+     logging.debug("Add ssh key of node '%s'.", node_name)
+     node_info = SshAddNodeInfo(name=node_name,
+@@ -2086,7 +2102,7 @@ def RenewSshKeys(node_uuids, node_names, master_candidate_uuids,
+ 
+   node_errors = AddNodeSshKeyBulk(
+       node_keys_to_add, potential_master_candidates,
+-      pub_key_file=pub_key_file, ssconf_store=ssconf_store,
++      pub_key_file=ganeti_pub_keys_file, ssconf_store=ssconf_store,
+       noded_cert_file=noded_cert_file,
+       run_cmd_fn=run_cmd_fn)
+   if node_errors:
+@@ -2095,12 +2111,14 @@ def RenewSshKeys(node_uuids, node_names, master_candidate_uuids,
+   # Renewing the master node's key
+ 
+   # Preserve the old keys for now
+-  old_master_keys_by_uuid = _GetOldMasterKeys(master_node_uuid, pub_key_file)
++  old_master_keys_by_uuid = _GetOldMasterKeys(master_node_uuid,
++                                              ganeti_pub_keys_file)
+ 
+   # Generate a new master key with a suffix, don't touch the old one for now
+   logging.debug("Generate new ssh key of master.")
+   _GenerateNodeSshKey(master_node_uuid, master_node_name, ssh_port_map,
+-                      pub_key_file=pub_key_file,
++                      new_key_type, new_key_bits,
++                      pub_key_file=ganeti_pub_keys_file,
+                       ssconf_store=ssconf_store,
+                       noded_cert_file=noded_cert_file,
+                       run_cmd_fn=run_cmd_fn,
+@@ -2109,16 +2127,16 @@ def RenewSshKeys(node_uuids, node_names, master_candidate_uuids,
+   new_master_key_dict = _GetNewMasterKey(root_keyfiles, master_node_uuid)
+ 
+   # Replace master key in the master nodes' public key file
+-  ssh.RemovePublicKey(master_node_uuid, key_file=pub_key_file)
++  ssh.RemovePublicKey(master_node_uuid, key_file=ganeti_pub_keys_file)
+   for pub_key in new_master_key_dict[master_node_uuid]:
+-    ssh.AddPublicKey(master_node_uuid, pub_key, key_file=pub_key_file)
++    ssh.AddPublicKey(master_node_uuid, pub_key, key_file=ganeti_pub_keys_file)
+ 
+   # Add new master key to all node's public and authorized keys
+   logging.debug("Add new master key to all nodes.")
+   node_errors = AddNodeSshKey(
+       master_node_uuid, master_node_name, potential_master_candidates,
+       to_authorized_keys=True, to_public_keys=True,
+-      get_public_keys=False, pub_key_file=pub_key_file,
++      get_public_keys=False, pub_key_file=ganeti_pub_keys_file,
+       ssconf_store=ssconf_store, noded_cert_file=noded_cert_file,
+       run_cmd_fn=run_cmd_fn)
+   if node_errors:
+diff --git a/lib/bootstrap.py b/lib/bootstrap.py
+index d649b8ec2..370b4c76b 100644
+--- a/lib/bootstrap.py
++++ b/lib/bootstrap.py
+@@ -485,16 +485,17 @@ def _InitCheckDrbdHelper(drbd_helper, drbd_enabled):
+ def InitCluster(cluster_name, mac_prefix, # pylint: disable=R0913, R0914
+                 master_netmask, master_netdev, file_storage_dir,
+                 shared_file_storage_dir, gluster_storage_dir,
+-                candidate_pool_size, secondary_ip=None,
+-                vg_name=None, beparams=None, nicparams=None, ndparams=None,
+-                hvparams=None, diskparams=None, enabled_hypervisors=None,
+-                modify_etc_hosts=True, modify_ssh_setup=True,
+-                maintain_node_health=False, drbd_helper=None, uid_pool=None,
+-                default_iallocator=None, default_iallocator_params=None,
+-                primary_ip_version=None, ipolicy=None,
+-                prealloc_wipe_disks=False, use_external_mip_script=False,
+-                hv_state=None, disk_state=None, enabled_disk_templates=None,
+-                install_image=None, zeroing_image=None, compression_tools=None,
++                candidate_pool_size, ssh_key_type, ssh_key_bits,
++                secondary_ip=None, vg_name=None, beparams=None, nicparams=None,
++                ndparams=None, hvparams=None, diskparams=None,
++                enabled_hypervisors=None, modify_etc_hosts=True,
++                modify_ssh_setup=True, maintain_node_health=False,
++                drbd_helper=None, uid_pool=None, default_iallocator=None,
++                default_iallocator_params=None, primary_ip_version=None,
++                ipolicy=None, prealloc_wipe_disks=False,
++                use_external_mip_script=False, hv_state=None, disk_state=None,
++                enabled_disk_templates=None, install_image=None,
++                zeroing_image=None, compression_tools=None,
+                 enabled_user_shutdown=False):
+   """Initialise the cluster.
+ 
+@@ -713,7 +714,7 @@ def InitCluster(cluster_name, mac_prefix, # pylint: disable=R0913, R0914
+     utils.AddHostToEtcHosts(hostname.name, hostname.ip)
+ 
+   if modify_ssh_setup:
+-    ssh.InitSSHSetup()
++    ssh.InitSSHSetup(ssh_key_type, ssh_key_bits)
+ 
+   if default_iallocator is not None:
+     alloc_script = utils.FindFile(default_iallocator,
+@@ -797,6 +798,8 @@ def InitCluster(cluster_name, mac_prefix, # pylint: disable=R0913, R0914
+     zeroing_image=zeroing_image,
+     compression_tools=compression_tools,
+     enabled_user_shutdown=enabled_user_shutdown,
++    ssh_key_type=ssh_key_type,
++    ssh_key_bits=ssh_key_bits,
+     )
+   master_node_config = objects.Node(name=hostname.name,
+                                     primary_ip=hostname.ip,
+@@ -814,7 +817,7 @@ def InitCluster(cluster_name, mac_prefix, # pylint: disable=R0913, R0914
+ 
+   master_uuid = cfg.GetMasterNode()
+   if modify_ssh_setup:
+-    ssh.InitPubKeyFile(master_uuid)
++    ssh.InitPubKeyFile(master_uuid, ssh_key_type)
+   # set up the inter-node password and certificate
+   _InitGanetiServerSetup(hostname.name, cfg)
+ 
+diff --git a/lib/cli_opts.py b/lib/cli_opts.py
+index ae58edec8..9f4d5309c 100644
+--- a/lib/cli_opts.py
++++ b/lib/cli_opts.py
+@@ -238,6 +238,8 @@ __all__ = [
+   "SPLIT_ISPECS_OPTS",
+   "SRC_DIR_OPT",
+   "SRC_NODE_OPT",
++  "SSH_KEY_BITS_OPT",
++  "SSH_KEY_TYPE_OPT",
+   "STARTUP_PAUSED_OPT",
+   "STATIC_OPT",
+   "SUBMIT_OPT",
+@@ -1594,6 +1596,17 @@ LONG_SLEEP_OPT = cli_option(
+     "--long-sleep", default=False, dest="long_sleep",
+     help="Allow long shutdowns when backing up instances", action="store_true")
+ 
++SSH_KEY_TYPE_OPT = \
++    cli_option("--ssh-key-type", default=None,
++               choices=list(constants.SSHK_ALL), dest="ssh_key_type",
++               help="Type of SSH key deployed by Ganeti for cluster actions")
++
++SSH_KEY_BITS_OPT = \
++    cli_option("--ssh-key-bits", default=None,
++               type="int", dest="ssh_key_bits",
++               help="Length of SSH keys generated by Ganeti, in bits")
++
++
+ #: Options provided by all commands
+ COMMON_OPTS = [DEBUG_OPT, REASON_OPT]
+ 
+diff --git a/lib/client/gnt_cluster.py b/lib/client/gnt_cluster.py
+index 27877a7b6..1bb9e8d60 100644
+--- a/lib/client/gnt_cluster.py
++++ b/lib/client/gnt_cluster.py
+@@ -299,6 +299,14 @@ def InitCluster(opts, args):
+   else:
+     enabled_user_shutdown = False
+ 
++  if opts.ssh_key_type:
++    ssh_key_type = opts.ssh_key_type
++  else:
++    ssh_key_type = constants.SSH_DEFAULT_KEY_TYPE
++
++  ssh_key_bits = ssh.DetermineKeyBits(ssh_key_type, opts.ssh_key_bits, None,
++                                      None)
++
+   bootstrap.InitCluster(cluster_name=args[0],
+                         secondary_ip=opts.secondary_ip,
+                         vg_name=vg_name,
+@@ -333,6 +341,8 @@ def InitCluster(opts, args):
+                         zeroing_image=zeroing_image,
+                         compression_tools=compression_tools,
+                         enabled_user_shutdown=enabled_user_shutdown,
++                        ssh_key_type=ssh_key_type,
++                        ssh_key_bits=ssh_key_bits,
+                         )
+   op = opcodes.OpClusterPostInit()
+   SubmitOpCode(op, opts=opts)
+@@ -612,6 +622,9 @@ def ShowClusterConfig(opts, args):
+       ("zeroing image", result["zeroing_image"]),
+       ("compression tools", result["compression_tools"]),
+       ("enabled user shutdown", result["enabled_user_shutdown"]),
++      ("modify ssh setup", result["modify_ssh_setup"]),
++      ("ssh_key_type", result["ssh_key_type"]),
++      ("ssh_key_bits", result["ssh_key_bits"]),
+       ]),
+ 
+     ("Default node parameters",
+@@ -964,11 +977,12 @@ def _ReadAndVerifyCert(cert_filename, verify_private_key=False):
+   return pem
+ 
+ 
++# pylint: disable=R0913
+ 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, new_ssh_keys,
+-                 verbose, debug):
++                 ssh_key_type, ssh_key_bits, verbose, debug):
+   """Renews cluster certificates, keys and secrets.
+ 
+   @type new_cluster_cert: bool
+@@ -996,10 +1010,14 @@ def _RenewCrypto(new_cluster_cert, new_rapi_cert, # pylint: disable=R0911
+   @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
++  @type ssh_key_type: One of L{constants.SSHK_ALL}
++  @param ssh_key_type: The type of SSH key to be generated
++  @type ssh_key_bits: int
++  @param ssh_key_bits: The length of the key to be generated
+   @type verbose: boolean
+-  @param verbose: show verbose output
++  @param verbose: Show verbose output
+   @type debug: boolean
+-  @param debug: show debug output
++  @param debug: Show debug output
+ 
+   """
+   ToStdout("Updating certificates now. Running \"gnt-cluster verify\" "
+@@ -1180,7 +1198,9 @@ def _RenewCrypto(new_cluster_cert, new_rapi_cert, # pylint: disable=R0911
+     cl = GetClient()
+     renew_op = opcodes.OpClusterRenewCrypto(
+         node_certificates=new_node_cert or new_cluster_cert,
+-        ssh_keys=new_ssh_keys)
++        renew_ssh_keys=new_ssh_keys,
++        ssh_key_type=ssh_key_type,
++        ssh_key_bits=ssh_key_bits)
+     SubmitOpCode(renew_op, cl=cl)
+ 
+   ToStdout("All requested certificates and keys have been replaced."
+@@ -1197,18 +1217,25 @@ def _BuildGanetiPubKeys(options, pub_key_file=pathutils.SSH_PUB_KEYS, cl=None,
+   """Recreates the 'ganeti_pub_key' file by polling all nodes.
+ 
+   """
++
++  if not cl:
++    cl = GetClient()
++
++  (cluster_name, master_node, modify_ssh_setup, ssh_key_type) = \
++    cl.QueryConfigValues(["cluster_name", "master_node", "modify_ssh_setup",
++                          "ssh_key_type"])
++
++  # In case Ganeti is not supposed to modify the SSH setup, simply exit and do
++  # not update this file.
++  if not modify_ssh_setup:
++    return
++
+   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))
+@@ -1221,7 +1248,7 @@ def _BuildGanetiPubKeys(options, pub_key_file=pathutils.SSH_PUB_KEYS, cl=None,
+ 
+   _, pub_key_filename, _ = \
+     ssh.GetUserFiles(constants.SSH_LOGIN_USER, mkdir=False, dircheck=False,
+-                     kind=constants.SSHK_DSA, _homedir_fn=homedir_fn)
++                     kind=ssh_key_type, _homedir_fn=homedir_fn)
+ 
+   # get the key file of the master node
+   pub_key = utils.ReadFile(pub_key_filename)
+@@ -1255,6 +1282,8 @@ def RenewCrypto(opts, args):
+                       opts.force,
+                       opts.new_node_cert,
+                       opts.new_ssh_keys,
++                      opts.ssh_key_type,
++                      opts.ssh_key_bits,
+                       opts.verbose,
+                       opts.debug > 0)
+ 
+@@ -2405,7 +2434,7 @@ commands = {
+      HV_STATE_OPT, DISK_STATE_OPT, ENABLED_DISK_TEMPLATES_OPT,
+      IPOLICY_STD_SPECS_OPT, GLOBAL_GLUSTER_FILEDIR_OPT, INSTALL_IMAGE_OPT,
+      ZEROING_IMAGE_OPT, COMPRESSION_TOOLS_OPT,
+-     ENABLED_USER_SHUTDOWN_OPT,
++     ENABLED_USER_SHUTDOWN_OPT, SSH_KEY_BITS_OPT, SSH_KEY_TYPE_OPT,
+      ]
+      + INSTANCE_POLICY_OPTS + SPLIT_ISPECS_OPTS,
+     "[opts...] <cluster_name>", "Initialises a new cluster configuration"),
+@@ -2505,7 +2534,7 @@ commands = {
+      NEW_CLUSTER_DOMAIN_SECRET_OPT, CLUSTER_DOMAIN_SECRET_OPT,
+      NEW_SPICE_CERT_OPT, SPICE_CERT_OPT, SPICE_CACERT_OPT,
+      NEW_NODE_CERT_OPT, NEW_SSH_KEY_OPT, NOSSH_KEYCHECK_OPT,
+-     VERBOSE_OPT],
++     VERBOSE_OPT, SSH_KEY_BITS_OPT, SSH_KEY_TYPE_OPT],
+     "[opts...]",
+     "Renews cluster certificates, keys and secrets"),
+   "epo": (
+diff --git a/lib/client/gnt_node.py b/lib/client/gnt_node.py
+index 87f3d19e1..25099fe0c 100644
+--- a/lib/client/gnt_node.py
++++ b/lib/client/gnt_node.py
+@@ -230,12 +230,17 @@ def _SetupSSH(options, cluster_name, node, ssh_port, cl):
+   (_, cert_pem) = \
+     utils.ExtractX509Certificate(utils.ReadFile(pathutils.NODED_CERT_FILE))
+ 
++  (ssh_key_type, ssh_key_bits) = \
++    cl.QueryConfigValues(["ssh_key_type", "ssh_key_bits"])
++
+   data = {
+     constants.SSHS_CLUSTER_NAME: cluster_name,
+     constants.SSHS_NODE_DAEMON_CERTIFICATE: cert_pem,
+     constants.SSHS_SSH_HOST_KEY: host_keys,
+     constants.SSHS_SSH_ROOT_KEY: root_keys,
+     constants.SSHS_SSH_AUTHORIZED_KEYS: candidate_keys,
++    constants.SSHS_SSH_KEY_TYPE: ssh_key_type,
++    constants.SSHS_SSH_KEY_BITS: ssh_key_bits,
+     }
+ 
+   ssh.RunSshCmdWithStdin(cluster_name, node, pathutils.PREPARE_NODE_JOIN,
+@@ -244,9 +249,9 @@ def _SetupSSH(options, cluster_name, node, ssh_port, cl):
+                          use_cluster_key=False, ask_key=options.ssh_key_check,
+                          strict_host_check=options.ssh_key_check)
+ 
+-  (_, dsa_pub_keyfile) = root_keyfiles[constants.SSHK_DSA]
+-  pub_key = ssh.ReadRemoteSshPubKeys(dsa_pub_keyfile, node, cluster_name,
+-                                     ssh_port, options.ssh_key_check,
++  (_, pub_keyfile) = root_keyfiles[ssh_key_type]
++  pub_key = ssh.ReadRemoteSshPubKeys(pub_keyfile, node, cluster_name, ssh_port,
++                                     options.ssh_key_check,
+                                      options.ssh_key_check)
+   # Unfortunately, we have to add the key with the node name rather than
+   # the node's UUID here, because at this point, we do not have a UUID yet.
+diff --git a/lib/cmdlib/cluster/__init__.py b/lib/cmdlib/cluster/__init__.py
+index cfe5feb9a..43df84420 100644
+--- a/lib/cmdlib/cluster/__init__.py
++++ b/lib/cmdlib/cluster/__init__.py
+@@ -90,13 +90,19 @@ class LUClusterRenewCrypto(NoHooksLU):
+   def CheckPrereq(self):
+     """Check prerequisites.
+ 
+-    This checks whether the cluster is empty.
+-
+-    Any errors are signaled by raising errors.OpPrereqError.
++    Notably the compatibility of specified key bits and key type.
+ 
+     """
+-    self._ssh_renewal_suppressed = \
+-      not self.cfg.GetClusterInfo().modify_ssh_setup and self.op.ssh_keys
++    cluster_info = self.cfg.GetClusterInfo()
++
++    self.ssh_key_type = self.op.ssh_key_type
++    if self.ssh_key_type is None:
++      self.ssh_key_type = cluster_info.ssh_key_type
++
++    self.ssh_key_bits = ssh.DetermineKeyBits(self.ssh_key_type,
++                                             self.op.ssh_key_bits,
++                                             cluster_info.ssh_key_type,
++                                             cluster_info.ssh_key_bits)
+ 
+   def _RenewNodeSslCertificates(self, feedback_fn):
+     """Renews the nodes' SSL certificates.
+@@ -159,9 +165,12 @@ class LUClusterRenewCrypto(NoHooksLU):
+ 
+     self.cfg.SetCandidateCerts(digest_map)
+ 
+-  def _RenewSshKeys(self):
++  def _RenewSshKeys(self, feedback_fn):
+     """Renew all nodes' SSH keys.
+ 
++    @type feedback_fn: function
++    @param feedback_fn: logging function, see L{ganeti.cmdlist.base.LogicalUnit}
++
+     """
+     master_uuid = self.cfg.GetMasterNode()
+ 
+@@ -172,23 +181,37 @@ class LUClusterRenewCrypto(NoHooksLU):
+     node_uuids = [uuid for (uuid, _) in nodes_uuid_names]
+     potential_master_candidates = self.cfg.GetPotentialMasterCandidates()
+     master_candidate_uuids = self.cfg.GetMasterCandidateUuids()
++
++    cluster_info = self.cfg.GetClusterInfo()
++
+     result = self.rpc.call_node_ssh_keys_renew(
+       [master_uuid],
+       node_uuids, node_names,
+       master_candidate_uuids,
+-      potential_master_candidates)
++      potential_master_candidates,
++      cluster_info.ssh_key_type, # Old key type
++      self.ssh_key_type,         # New key type
++      self.ssh_key_bits)         # New key bits
+     result[master_uuid].Raise("Could not renew the SSH keys of all nodes")
+ 
++    # After the keys have been successfully swapped, time to commit the change
++    # in key type
++    cluster_info.ssh_key_type = self.ssh_key_type
++    cluster_info.ssh_key_bits = self.ssh_key_bits
++    self.cfg.Update(cluster_info, feedback_fn)
++
+   def Exec(self, feedback_fn):
+     if self.op.node_certificates:
+       feedback_fn("Renewing Node SSL certificates")
+       self._RenewNodeSslCertificates(feedback_fn)
+-    if self.op.ssh_keys and not self._ssh_renewal_suppressed:
+-      feedback_fn("Renewing SSH keys")
+-      self._RenewSshKeys()
+-    elif self._ssh_renewal_suppressed:
+-      feedback_fn("Cannot renew SSH keys if the cluster is configured to not"
+-                  " modify the SSH setup.")
++
++    if self.op.renew_ssh_keys:
++      if self.cfg.GetClusterInfo().modify_ssh_setup:
++        feedback_fn("Renewing SSH keys")
++        self._RenewSshKeys(feedback_fn)
++      else:
++        feedback_fn("Cannot renew SSH keys if the cluster is configured to not"
++                    " modify the SSH setup.")
+ 
+ 
+ class LUClusterActivateMasterIp(NoHooksLU):
+diff --git a/lib/cmdlib/cluster/verify.py b/lib/cmdlib/cluster/verify.py
+index dfa1294d7..789d43912 100644
+--- a/lib/cmdlib/cluster/verify.py
++++ b/lib/cmdlib/cluster/verify.py
+@@ -1838,7 +1838,8 @@ class LUClusterVerifyGroup(LogicalUnit, _VerifyErrors):
+       }
+ 
+     if self.cfg.GetClusterInfo().modify_ssh_setup:
+-      node_verify_param[constants.NV_SSH_SETUP] = self._PrepareSshSetupCheck()
++      node_verify_param[constants.NV_SSH_SETUP] = \
++        (self._PrepareSshSetupCheck(), self.cfg.GetClusterInfo().ssh_key_type)
+       if self.op.verify_clutter:
+         node_verify_param[constants.NV_SSH_CLUTTER] = True
+ 
+diff --git a/lib/ht.py b/lib/ht.py
+index 3a194e00c..3644b1cce 100644
+--- a/lib/ht.py
++++ b/lib/ht.py
+@@ -637,6 +637,7 @@ def TStorageType(val):
+ TTagKind = TElemOf(constants.VALID_TAG_TYPES)
+ TDdmSimple = TElemOf(constants.DDMS_VALUES)
+ TVerifyOptionalChecks = TElemOf(constants.VERIFY_OPTIONAL_CHECKS)
++TSshKeyType = TElemOf(constants.SSHK_ALL)
+ 
+ 
+ @WithDesc("IPv4 network")
+diff --git a/lib/objects.py b/lib/objects.py
+index 633353dd5..72a27b899 100644
+--- a/lib/objects.py
++++ b/lib/objects.py
+@@ -1663,6 +1663,8 @@ class Cluster(TaggableObject):
+     "compression_tools",
+     "enabled_user_shutdown",
+     "data_collectors",
++    "ssh_key_type",
++    "ssh_key_bits",
+     ] + _TIMESTAMPS + _UUID
+ 
+   def UpgradeConfig(self):
+@@ -1818,6 +1820,12 @@ class Cluster(TaggableObject):
+     if self.enabled_user_shutdown is None:
+       self.enabled_user_shutdown = False
+ 
++    if self.ssh_key_type is None:
++      self.ssh_key_type = constants.SSH_DEFAULT_KEY_TYPE
++
++    if self.ssh_key_bits is None:
++      self.ssh_key_bits = constants.SSH_DEFAULT_KEY_BITS
++
+   @property
+   def primary_hypervisor(self):
+     """The first hypervisor is the primary.
+diff --git a/lib/rpc_defs.py b/lib/rpc_defs.py
+index 09b2fa85a..821d8525c 100644
+--- a/lib/rpc_defs.py
++++ b/lib/rpc_defs.py
+@@ -568,7 +568,10 @@ _NODE_CALLS = [
+     ("node_uuids", None, "UUIDs of the nodes whose key is renewed"),
+     ("node_names", None, "Names of the nodes whose key is renewed"),
+     ("master_candidate_uuids", None, "List of UUIDs of master candidates."),
+-    ("potential_master_candidates", None, "Potential master candidates")],
++    ("potential_master_candidates", None, "Potential master candidates"),
++    ("old_key_type", None, "The type of key previously used"),
++    ("new_key_type", None, "The type of key to generate"),
++    ("new_key_bits", None, "The length of the key to generate")],
+     None, None, "Renew all SSH key pairs of all nodes nodes."),
+   ]
+ 
+diff --git a/lib/server/noded.py b/lib/server/noded.py
+index 880f2e131..3d9b2d854 100644
+--- a/lib/server/noded.py
++++ b/lib/server/noded.py
+@@ -946,10 +946,11 @@ class NodeRequestHandler(http.server.HttpServerHandler):
+ 
+     """
+     (node_uuids, node_names, master_candidate_uuids,
+-     potential_master_candidates) = params
+-    return backend.RenewSshKeys(node_uuids, node_names,
+-                                master_candidate_uuids,
+-                                potential_master_candidates)
++     potential_master_candidates, old_key_type, new_key_type,
++     new_key_bits) = params
++    return backend.RenewSshKeys(node_uuids, node_names, master_candidate_uuids,
++                                potential_master_candidates, old_key_type,
++                                new_key_type, new_key_bits)
+ 
+   @staticmethod
+   def perspective_node_ssh_key_remove(params):
+diff --git a/lib/ssh.py b/lib/ssh.py
+index 7d34f2957..5a6e9fdc6 100644
+--- a/lib/ssh.py
++++ b/lib/ssh.py
+@@ -37,6 +37,7 @@ import logging
+ import os
+ import tempfile
+ 
++from collections import namedtuple
+ from functools import partial
+ 
+ from ganeti import utils
+@@ -677,15 +678,18 @@ def QueryPubKeyFile(target_uuids, key_file=pathutils.SSH_PUB_KEYS,
+   return result
+ 
+ 
+-def InitSSHSetup(error_fn=errors.OpPrereqError, _homedir_fn=None,
+-                 _suffix=""):
++def InitSSHSetup(key_type, key_bits, error_fn=errors.OpPrereqError,
++                 _homedir_fn=None, _suffix=""):
+   """Setup the SSH configuration for the node.
+ 
+   This generates a dsa keypair for root, adds the pub key to the
+   permitted hosts and adds the hostkey to its own known hosts.
+ 
++  @param key_type: the type of SSH keypair to be generated
++  @param key_bits: the key length, in bits, to be used
++
+   """
+-  priv_key, _, auth_keys = GetUserFiles(constants.SSH_LOGIN_USER,
++  priv_key, _, auth_keys = GetUserFiles(constants.SSH_LOGIN_USER, kind=key_type,
+                                         _homedir_fn=_homedir_fn)
+ 
+   new_priv_key_name = priv_key + _suffix
+@@ -696,7 +700,7 @@ def InitSSHSetup(error_fn=errors.OpPrereqError, _homedir_fn=None,
+       utils.CreateBackup(name)
+     utils.RemoveFile(name)
+ 
+-  result = utils.RunCmd(["ssh-keygen", "-t", "dsa",
++  result = utils.RunCmd(["ssh-keygen", "-b", str(key_bits), "-t", key_type,
+                          "-f", new_priv_key_name,
+                          "-q", "-N", ""])
+   if result.failed:
+@@ -706,16 +710,18 @@ def InitSSHSetup(error_fn=errors.OpPrereqError, _homedir_fn=None,
+   AddAuthorizedKey(auth_keys, utils.ReadFile(new_pub_key_name))
+ 
+ 
+-def InitPubKeyFile(master_uuid, key_file=pathutils.SSH_PUB_KEYS):
++def InitPubKeyFile(master_uuid, key_type, key_file=pathutils.SSH_PUB_KEYS):
+   """Creates the public key file and adds the master node's SSH key.
+ 
+   @type master_uuid: str
+   @param master_uuid: the master node's UUID
++  @type key_type: one of L{constants.SSHK_ALL}
++  @param key_type: the type of ssh key to be used
+   @type key_file: str
+   @param key_file: name of the file containing the public keys
+ 
+   """
+-  _, pub_key, _ = GetUserFiles(constants.SSH_LOGIN_USER)
++  _, pub_key, _ = GetUserFiles(constants.SSH_LOGIN_USER, kind=key_type)
+   ClearPubKeyFile(key_file=key_file)
+   key = utils.ReadFile(pub_key)
+   AddPublicKey(master_uuid, key, key_file=key_file)
+@@ -1069,7 +1075,7 @@ def RunSshCmdWithStdin(cluster_name, node, basecmd, port, data,
+ 
+ def ReadRemoteSshPubKeys(pub_key_file, node, cluster_name, port, ask_key,
+                          strict_host_check):
+-  """Fetches the public DSA SSH key from a node via SSH.
++  """Fetches a public SSH key from a node via SSH.
+ 
+   @type pub_key_file: string
+   @param pub_key_file: a tuple consisting of the file name of the public DSA key
+@@ -1087,7 +1093,47 @@ def ReadRemoteSshPubKeys(pub_key_file, node, cluster_name, port, ask_key,
+ 
+   result = utils.RunCmd(ssh_cmd)
+   if result.failed:
+-    raise errors.OpPrereqError("Could not fetch a public DSA SSH key from node"
++    raise errors.OpPrereqError("Could not fetch a public SSH key (%s) from node"
+                                " '%s': ran command '%s', failure reason: '%s'."
+-                               % (node, cmd, result.fail_reason))
++                               % (pub_key_file, node, cmd, result.fail_reason),
++                               errors.ECODE_INVAL)
+   return result.stdout
++
++
++# Update gnt-cluster.rst when changing which combinations are valid.
++KeyBitInfo = namedtuple('KeyBitInfo', ['default', 'validation_fn'])
++SSH_KEY_VALID_BITS = {
++  constants.SSHK_DSA: KeyBitInfo(1024, lambda b: b == 1024),
++  constants.SSHK_RSA: KeyBitInfo(2048, lambda b: b >= 768),
++  constants.SSHK_ECDSA: KeyBitInfo(384, lambda b: b in [256, 384, 521]),
++}
++
++
++def DetermineKeyBits(key_type, key_bits, old_key_type, old_key_bits):
++  """Checks the key bits to be used for a given key type, or provides defaults.
++
++  @type key_type: one of L{constants.SSHK_ALL}
++  @param key_type: The key type to use.
++  @type key_bits: positive int or None
++  @param key_bits: The number of bits to use, if supplied by user.
++  @type old_key_type: one of L{constants.SSHK_ALL} or None
++  @param old_key_type: The previously used key type, if any.
++  @type old_key_bits: positive int or None
++  @param old_key_bits: The previously used number of bits, if any.
++
++  @rtype: positive int
++  @return: The number of bits to use.
++
++  """
++  if key_bits is None:
++    if old_key_type is not None and old_key_type == key_type:
++      key_bits = old_key_bits
++    else:
++      key_bits = SSH_KEY_VALID_BITS[key_type].default
++
++  if not SSH_KEY_VALID_BITS[key_type].validation_fn(key_bits):
++    raise errors.OpPrereqError("Invalid key type and bit size combination:"
++                               " %s with %s bits" % (key_type, key_bits),
++                               errors.ECODE_INVAL)
++
++  return key_bits
+diff --git a/lib/tools/cfgupgrade.py b/lib/tools/cfgupgrade.py
+index e071b7919..a500df6ca 100644
+--- a/lib/tools/cfgupgrade.py
++++ b/lib/tools/cfgupgrade.py
+@@ -307,24 +307,33 @@ class CfgUpgrade(object):
+     cluster = self.config_data.get("cluster", None)
+     if cluster is None:
+       raise Error("Cannot find cluster")
++
+     ipolicy = cluster.setdefault("ipolicy", None)
+     if ipolicy:
+       self.UpgradeIPolicy(ipolicy, constants.IPOLICY_DEFAULTS, False)
+     ial_params = cluster.get("default_iallocator_params", None)
++
+     if not ial_params:
+       cluster["default_iallocator_params"] = {}
++
+     if not "candidate_certs" in cluster:
+       cluster["candidate_certs"] = {}
++
+     cluster["instance_communication_network"] = \
+       cluster.get("instance_communication_network", "")
++
+     cluster["install_image"] = \
+       cluster.get("install_image", "")
++
+     cluster["zeroing_image"] = \
+       cluster.get("zeroing_image", "")
++
+     cluster["compression_tools"] = \
+       cluster.get("compression_tools", constants.IEC_DEFAULT_TOOLS)
++
+     if "enabled_user_shutdown" not in cluster:
+       cluster["enabled_user_shutdown"] = False
++
+     cluster["data_collectors"] = cluster.get("data_collectors", {})
+     for name in constants.DATA_COLLECTOR_NAMES:
+       cluster["data_collectors"][name] = \
+@@ -332,6 +341,14 @@ class CfgUpgrade(object):
+             name, dict(active=True,
+                        interval=constants.MOND_TIME_INTERVAL * 1e6))
+ 
++    # These parameters are set to pre-2.16 default values, which
++    # differ from post-2.16 default values
++    if "ssh_key_type" not in cluster:
++      cluster["ssh_key_type"] = constants.SSHK_DSA
++
++    if "ssh_key_bits" not in cluster:
++      cluster["ssh_key_bits"] = 1024
++
+   @OrFail("Upgrading groups")
+   def UpgradeGroups(self):
+     cl_ipolicy = self.config_data["cluster"].get("ipolicy")
+@@ -709,11 +726,43 @@ class CfgUpgrade(object):
+   def DowngradeCluster(self, cluster):
+     self.DowngradeCollectors(cluster["data_collectors"])
+ 
++  @OrFail("Removing SSH parameters")
++  def DowngradeSshKeyParams(self):
++    """Removes the SSH key type and bits parameters from the config.
++
++    Also fails if these have been changed from values appropriate in lower
++    Ganeti versions.
++
++    """
++    # pylint: disable=E1103
++    # Because config_data is a dictionary which has the get method.
++    cluster = self.config_data.get("cluster", None)
++    if cluster is None:
++      raise Error("Can't find the cluster entry in the configuration")
++
++    def _FetchAndDelete(key):
++      val = cluster.get(key, None)
++      if key in cluster:
++        del cluster[key]
++      return val
++
++    ssh_key_type = _FetchAndDelete("ssh_key_type")
++    _FetchAndDelete("ssh_key_bits")
++
++    if ssh_key_type is not None and ssh_key_type != "dsa":
++      raise Error("The current Ganeti setup is using non-DSA SSH keys, and"
++                  " versions below 2.16 do not support these. To downgrade,"
++                  " please perform a gnt-cluster renew-crypto using the "
++                  " --new-ssh-keys and --ssh-key-type=dsa options, generating"
++                  " DSA keys that older versions can also use.")
++
+   def DowngradeAll(self):
+     self.DowngradeCluster(self.config_data["cluster"])
+     self.config_data["version"] = version.BuildVersion(DOWNGRADE_MAJOR,
+                                                        DOWNGRADE_MINOR, 0)
+-    return True
++
++    self.DowngradeSshKeyParams()
++    return not self.errors
+ 
+   def _ComposePaths(self):
+     # We need to keep filenames locally because they might be renamed between
+diff --git a/lib/tools/common.py b/lib/tools/common.py
+index a9149f68f..ca8288a01 100644
+--- a/lib/tools/common.py
++++ b/lib/tools/common.py
+@@ -191,11 +191,13 @@ def LoadData(raw, data_check):
+   return serializer.LoadAndVerifyJson(raw, data_check)
+ 
+ 
+-def GenerateRootSshKeys(error_fn, _suffix="", _homedir_fn=None):
++def GenerateRootSshKeys(key_type, key_bits, error_fn, _suffix="",
++                        _homedir_fn=None):
+   """Generates root's SSH keys for this node.
+ 
+   """
+-  ssh.InitSSHSetup(error_fn=error_fn, _homedir_fn=_homedir_fn, _suffix=_suffix)
++  ssh.InitSSHSetup(key_type, key_bits, error_fn=error_fn,
++                   _homedir_fn=_homedir_fn, _suffix=_suffix)
+ 
+ 
+ def GenerateClientCertificate(
+diff --git a/lib/tools/prepare_node_join.py b/lib/tools/prepare_node_join.py
+index 82a35dcc7..fa45a5895 100644
+--- a/lib/tools/prepare_node_join.py
++++ b/lib/tools/prepare_node_join.py
+@@ -50,7 +50,7 @@ from ganeti.tools import common
+ _SSH_KEY_LIST_ITEM = \
+   ht.TAnd(ht.TIsLength(3),
+           ht.TItems([
+-            ht.TElemOf(constants.SSHK_ALL),
++            ht.TSshKeyType,
+             ht.Comment("public")(ht.TNonEmptyString),
+             ht.Comment("private")(ht.TNonEmptyString),
+           ]))
+@@ -64,6 +64,8 @@ _DATA_CHECK = ht.TStrictDict(False, True, {
+   constants.SSHS_SSH_ROOT_KEY: _SSH_KEY_LIST,
+   constants.SSHS_SSH_AUTHORIZED_KEYS:
+     ht.TDictOf(ht.TNonEmptyString, ht.TListOf(ht.TNonEmptyString)),
++  constants.SSHS_SSH_KEY_TYPE: ht.TSshKeyType,
++  constants.SSHS_SSH_KEY_BITS: ht.TPositive,
+   })
+ 
+ 
+@@ -172,7 +174,10 @@ def UpdateSshRoot(data, dry_run, _homedir_fn=None):
+   if dry_run:
+     logging.info("This is a dry run, not replacing the SSH keys.")
+   else:
+-    common.GenerateRootSshKeys(error_fn=JoinError, _homedir_fn=_homedir_fn)
++    ssh_key_type = data.get(constants.SSHS_SSH_KEY_TYPE)
++    ssh_key_bits = data.get(constants.SSHS_SSH_KEY_BITS)
++    common.GenerateRootSshKeys(ssh_key_type, ssh_key_bits, error_fn=JoinError,
++                               _homedir_fn=_homedir_fn)
+ 
+   if authorized_keys:
+     if dry_run:
+diff --git a/lib/tools/ssh_update.py b/lib/tools/ssh_update.py
+index f9d1b6db3..b37972ec1 100644
+--- a/lib/tools/ssh_update.py
++++ b/lib/tools/ssh_update.py
+@@ -62,7 +62,13 @@ _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.TDictOf(ht.TNonEmptyString, ht.TString),
++  constants.SSHS_GENERATE:
++    ht.TItems(
++      [ht.TSshKeyType, # The type of key to generate
++       ht.TPositive, # The number of bits in the key
++       ht.TString]), # The suffix
++  constants.SSHS_SSH_KEY_TYPE: ht.TSshKeyType,
++  constants.SSHS_SSH_KEY_BITS: ht.TPositive,
+   })
+ 
+ 
+@@ -190,11 +196,12 @@ def GenerateRootSshKeys(data, dry_run):
+   """
+   generate_info = data.get(constants.SSHS_GENERATE)
+   if generate_info:
+-    suffix = generate_info[constants.SSHS_SUFFIX]
++    key_type, key_bits, suffix = generate_info
+     if dry_run:
+       logging.info("This is a dry run, not generating any files.")
+     else:
+-      common.GenerateRootSshKeys(SshUpdateError, _suffix=suffix)
++      common.GenerateRootSshKeys(key_type, key_bits, SshUpdateError,
++                                 _suffix=suffix)
+ 
+ 
+ def Main():
+diff --git a/man/gnt-cluster.rst b/man/gnt-cluster.rst
+index a04d50c79..b30e17ca1 100644
+--- a/man/gnt-cluster.rst
++++ b/man/gnt-cluster.rst
+@@ -206,6 +206,8 @@ INIT
+ | [\--zeroing-image *image*]
+ | [\--compression-tools [*tool*, [*tool*]]]
+ | [\--user-shutdown {yes \| no}]
++| [\--ssh-key-type *type*]
++| [\--ssh-key-bits *bits*]
+ | {*clustername*}
+ 
+ This commands is only run once initially on the first node of the
+@@ -632,6 +634,18 @@ of testing whether the executable exists. These requirements are
+ compatible with the gzip command line options, allowing many tools to
+ be easily wrapped and used.
+ 
++The ``--ssh-key-type`` and ``--ssh-key-bits`` options determine the
++properties of the SSH keys Ganeti generates and uses to execute
++commands on nodes. The supported types are currently 'dsa', 'rsa', and
++'ecdsa'. The supported bit sizes vary across keys, reflecting the
++options **ssh-keygen**\(1) exposes. These are currently:
++
++- dsa: 1024 bits
++- rsa: >=768 bits
++- ecdsa: 256, 384, or 521 bits
++
++Ganeti defaults to using 2048-bit RSA keys.
++
+ MASTER-FAILOVER
+ ~~~~~~~~~~~~~~~
+ 
+@@ -857,6 +871,7 @@ RENEW-CRYPTO
+ | \--spice-ca-certificate *spice-ca-cert*]
+ | [\--new-ssh-keys] [\--no-ssh-key-check]
+ | [\--new-cluster-domain-secret] [\--cluster-domain-secret *filename*]
++| [\--ssh-key-type *type*] | [\--ssh-key-bits *bits*]
+ 
+ This command will stop all Ganeti daemons in the cluster and start
+ them again once the new certificates and keys are replicated. The
+@@ -898,6 +913,10 @@ cluster domain secret, and ``--cluster-domain-secret`` reads the
+ secret from a file. The cluster domain secret is used to sign
+ information exchanged between separate clusters via a third party.
+ 
++The options ``--ssh-key-type`` and ``ssh-key-bits`` determine the
++properties of the disk types used. They are described in more detail
++in the ``init`` option description.
++
+ REPAIR-DISK-SIZES
+ ~~~~~~~~~~~~~~~~~
+ 
+diff --git a/qa/qa_cluster.py b/qa/qa_cluster.py
+index ac1d3a880..9105018c6 100644
+--- a/qa/qa_cluster.py
++++ b/qa/qa_cluster.py
+@@ -1195,6 +1195,63 @@ def _AssertSsconfCertFiles():
+                       " '%s'." % (node, first_node))
+ 
+ 
++def _TestSSHKeyChanges(master_node):
++  """Tests a lot of SSH key type- and size- related functionality.
++
++  @type master_node: L{qa_config._QaNode}
++  @param master_node: The cluster master.
++
++  """
++  # Helper fn to avoid specifying base params too many times
++  def _RenewWithParams(new_params, verify=True, fail=False):
++    AssertCommand(["gnt-cluster", "renew-crypto", "--new-ssh-keys", "-f",
++                   "--no-ssh-key-check"] + new_params, fail=fail)
++    if not fail and verify:
++      AssertCommand(["gnt-cluster", "verify"])
++
++  # First test the simplest change
++  _RenewWithParams([])
++
++  # And stop here if vcluster
++  (vcluster_master, _) = qa_config.GetVclusterSettings()
++  if vcluster_master:
++    print "Skipping further SSH key replacement checks for vcluster"
++    return
++
++  # And the actual tests
++  with qa_config.AcquireManyNodesCtx(1, exclude=[master_node]) as nodes:
++    node_name = nodes[0].primary
++
++    # Another helper function for checking whether a specific key can log in
++    def _CheckLoginWithKey(key_path, fail=False):
++      AssertCommand(["ssh", "-oIdentityFile=%s" % key_path, "-oBatchMode=yes",
++                     "-oStrictHostKeyChecking=no", "-oIdentitiesOnly=yes",
++                     "-F/dev/null", node_name, "true"],
++                    fail=fail, forward_agent=False)
++
++    _RenewWithParams(["--ssh-key-type=dsa"])
++    _CheckLoginWithKey("/root/.ssh/id_dsa")
++    # Stash the key for now
++    old_key_backup = qa_utils.BackupFile(master_node.primary,
++                                         "/root/.ssh/id_dsa")
++
++    try:
++      _RenewWithParams(["--ssh-key-type=rsa"])
++      _CheckLoginWithKey("/root/.ssh/id_rsa")
++      # And check that we cannot log in with the old key
++      _CheckLoginWithKey(old_key_backup, fail=True)
++    finally:
++      AssertCommand(["rm", "-f", old_key_backup])
++
++    _RenewWithParams(["--ssh-key-bits=4096"])
++    _RenewWithParams(["--ssh-key-bits=521"], fail=True)
++
++    # Restore the cluster to its pristine state, skipping the verify as we did
++    # way too many already
++    _RenewWithParams(["--ssh-key-type=rsa", "--ssh-key-bits=2048"],
++                     verify=False)
++
++
+ def TestClusterRenewCrypto():
+   """gnt-cluster renew-crypto"""
+   master = qa_config.GetMasterNode()
+@@ -1266,9 +1323,8 @@ def TestClusterRenewCrypto():
+     _AssertSsconfCertFiles()
+     AssertCommand(["gnt-cluster", "verify"])
+ 
+-    # Only renew SSH keys
+-    AssertCommand(["gnt-cluster", "renew-crypto", "--force",
+-                   "--new-ssh-keys", "--no-ssh-key-check"])
++    # Comprehensively test various types of SSH key changes
++    _TestSSHKeyChanges(master)
+ 
+     # Restore RAPI certificate
+     AssertCommand(["gnt-cluster", "renew-crypto", "--force",
+@@ -1371,7 +1427,7 @@ def TestUpgrade():
+ 
+   This tests the 'gnt-cluster upgrade' command by flipping
+   between the current and a different version of Ganeti.
+-  To also recover subtile points in the configuration up/down
++  To also recover subtle points in the configuration up/down
+   grades, instances are left over both upgrades.
+ 
+   """
+diff --git a/qa/qa_utils.py b/qa/qa_utils.py
+index 3dfe03f27..a519b22bd 100644
+--- a/qa/qa_utils.py
++++ b/qa/qa_utils.py
+@@ -175,7 +175,8 @@ def _PrintCommandOutput(stdout, stderr):
+     print >> sys.stderr, stderr.rstrip('\n')
+ 
+ 
+-def AssertCommand(cmd, fail=False, node=None, log_cmd=True, max_seconds=None):
++def AssertCommand(cmd, fail=False, node=None, log_cmd=True, forward_agent=True,
++                  max_seconds=None):
+   """Checks that a remote command succeeds.
+ 
+   @param cmd: either a string (the command to execute) or a list (to
+@@ -188,6 +189,10 @@ def AssertCommand(cmd, fail=False, node=None, log_cmd=True, max_seconds=None):
+       dict or a string)
+   @param log_cmd: if False, the command won't be logged (simply passed to
+       StartSSH)
++  @type forward_agent: boolean
++  @param forward_agent: whether to forward the agent when starting the SSH
++                        session or not, sometimes useful for crypto-related
++                        operations which can use a key they should not
+   @type max_seconds: double
+   @param max_seconds: fail if the command takes more than C{max_seconds}
+       seconds
+@@ -206,7 +211,8 @@ def AssertCommand(cmd, fail=False, node=None, log_cmd=True, max_seconds=None):
+     cmdstr = utils.ShellQuoteArgs(cmd)
+ 
+   start = datetime.datetime.now()
+-  popen = StartSSH(nodename, cmdstr, log_cmd=log_cmd)
++  popen = StartSSH(nodename, cmdstr, log_cmd=log_cmd,
++                   forward_agent=forward_agent)
+   # Run the command
+   stdout, stderr = popen.communicate()
+   rcode = popen.returncode
+@@ -263,7 +269,7 @@ def AssertRedirectedCommand(cmd, fail=False, node=None, log_cmd=True):
+ 
+ 
+ def GetSSHCommand(node, cmd, strict=True, opts=None, tty=False,
+-                  use_multiplexer=True):
++                  use_multiplexer=True, forward_agent=True):
+   """Builds SSH command to be executed.
+ 
+   @type node: string
+@@ -279,6 +285,8 @@ def GetSSHCommand(node, cmd, strict=True, opts=None, tty=False,
+   @param tty: if we should use tty; if None, will be auto-detected
+   @type use_multiplexer: boolean
+   @param use_multiplexer: if the multiplexer for the node should be used
++  @type forward_agent: boolean
++  @param forward_agent: whether to forward the ssh agent or not
+ 
+   """
+   args = ["ssh", "-oEscapeChar=none", "-oBatchMode=yes", "-lroot"]
+@@ -289,9 +297,14 @@ def GetSSHCommand(node, cmd, strict=True, opts=None, tty=False,
+   if tty:
+     args.append("-t")
+ 
++  # Multiplexers we use right now forward agents, so even if we ought to be
++  # using one, ignore it if agent forwarding is disabled.
++  if not forward_agent:
++    use_multiplexer = False
++
+   args.append("-oStrictHostKeyChecking=%s" % ("yes" if strict else "no", ))
+   args.append("-oClearAllForwardings=yes")
+-  args.append("-oForwardAgent=yes")
++  args.append("-oForwardAgent=%s" % ("yes" if forward_agent else "no", ))
+   if opts:
+     args.extend(opts)
+   if node in _MULTIPLEXERS and use_multiplexer:
+@@ -335,12 +348,13 @@ def StartLocalCommand(cmd, _nolog_opts=False, log_cmd=True, **kwargs):
+   return subprocess.Popen(cmd, shell=False, **kwargs)
+ 
+ 
+-def StartSSH(node, cmd, strict=True, log_cmd=True):
++def StartSSH(node, cmd, strict=True, log_cmd=True, forward_agent=True):
+   """Starts SSH.
+ 
+   """
+-  return StartLocalCommand(GetSSHCommand(node, cmd, strict=strict),
+-                           _nolog_opts=True, log_cmd=log_cmd,
++  ssh_command = GetSSHCommand(node, cmd, strict=strict,
++                              forward_agent=forward_agent)
++  return StartLocalCommand(ssh_command, _nolog_opts=True, log_cmd=log_cmd,
+                            stdout=subprocess.PIPE, stderr=subprocess.PIPE)
+ 
+ 
+diff --git a/src/Ganeti/Constants.hs b/src/Ganeti/Constants.hs
+index 645296218..b05c50b55 100644
+--- a/src/Ganeti/Constants.hs
++++ b/src/Ganeti/Constants.hs
+@@ -4577,13 +4577,13 @@ cryptoOptionSerialNo = "serial_no"
+ -- * SSH key types
+ 
+ sshkDsa :: String
+-sshkDsa = "dsa"
++sshkDsa = Types.sshKeyTypeToRaw DSA
+ 
+ sshkEcdsa :: String
+-sshkEcdsa = "ecdsa"
++sshkEcdsa = Types.sshKeyTypeToRaw ECDSA
+ 
+ sshkRsa :: String
+-sshkRsa = "rsa"
++sshkRsa = Types.sshKeyTypeToRaw RSA
+ 
+ sshkAll :: FrozenSet String
+ sshkAll = ConstantUtils.mkSet [sshkRsa, sshkDsa, sshkEcdsa]
+@@ -4599,6 +4599,15 @@ sshakRsa = "ssh-rsa"
+ sshakAll :: FrozenSet String
+ sshakAll = ConstantUtils.mkSet [sshakDss, sshakRsa]
+ 
++-- * SSH key default values
++-- Document the change in gnt-cluster.rst when changing these
++
++sshDefaultKeyType :: String
++sshDefaultKeyType = sshkRsa
++
++sshDefaultKeyBits :: Int
++sshDefaultKeyBits = 2048
++
+ -- * SSH setup
+ 
+ sshsClusterName :: String
+@@ -4619,6 +4628,12 @@ sshsSshPublicKeys = "public_keys"
+ sshsNodeDaemonCertificate :: String
+ sshsNodeDaemonCertificate = "node_daemon_certificate"
+ 
++sshsSshKeyType :: String
++sshsSshKeyType = "ssh_key_type"
++
++sshsSshKeyBits :: String
++sshsSshKeyBits = "ssh_key_bits"
++
+ -- Number of maximum retries when contacting nodes per SSH
+ -- during SSH update operations.
+ sshsMaxRetries :: Integer
+diff --git a/src/Ganeti/Objects.hs b/src/Ganeti/Objects.hs
+index 423f28e7c..281885171 100644
+--- a/src/Ganeti/Objects.hs
++++ b/src/Ganeti/Objects.hs
+@@ -678,6 +678,8 @@ $(buildObject "Cluster" "cluster" $
+   , simpleField "compression_tools"              [t| [String]                |]
+   , simpleField "enabled_user_shutdown"          [t| Bool                    |]
+   , simpleField "data_collectors"         [t| Container DataCollectorConfig  |]
++  , simpleField "ssh_key_type"                   [t| SshKeyType              |]
++  , simpleField "ssh_key_bits"                   [t| Int                     |]
+  ]
+  ++ timeStampFields
+  ++ uuidFields
+diff --git a/src/Ganeti/OpCodes.hs b/src/Ganeti/OpCodes.hs
+index 8e4f7c02f..c54403dcb 100644
+--- a/src/Ganeti/OpCodes.hs
++++ b/src/Ganeti/OpCodes.hs
+@@ -282,7 +282,9 @@ $(genOpCode "OpCode"
+      [t| () |],
+      OpDoc.opClusterRenewCrypto,
+      [ pNodeSslCerts
+-     , pSshKeys
++     , pRenewSshKeys
++     , pSshKeyType
++     , pSshKeyBits
+      , pVerbose
+      , pDebug
+      ],
+diff --git a/src/Ganeti/OpParams.hs b/src/Ganeti/OpParams.hs
+index 79d476e45..c731a2eb0 100644
+--- a/src/Ganeti/OpParams.hs
++++ b/src/Ganeti/OpParams.hs
+@@ -299,7 +299,9 @@ module Ganeti.OpParams
+   , pEnabledDataCollectors
+   , pDataCollectorInterval
+   , pNodeSslCerts
+-  , pSshKeys
++  , pSshKeyBits
++  , pSshKeyType
++  , pRenewSshKeys
+   , pNodeSetup
+   , pVerifyClutter
+   , pLongSleep
+@@ -1895,11 +1897,21 @@ pNodeSslCerts =
+   defaultField [| False |] $
+   simpleField "node_certificates" [t| Bool |]
+ 
+-pSshKeys :: Field
+-pSshKeys =
++pSshKeyBits :: Field
++pSshKeyBits =
++  withDoc "The number of bits of the SSH key Ganeti uses" .
++  optionalField $ simpleField "ssh_key_bits" [t| Positive Int |]
++
++pSshKeyType :: Field
++pSshKeyType =
++  withDoc "The type of the SSH key Ganeti uses" .
++  optionalField $ simpleField "ssh_key_type" [t| SshKeyType |]
++
++pRenewSshKeys :: Field
++pRenewSshKeys =
+   withDoc "Whether to renew SSH keys" .
+   defaultField [| False |] $
+-  simpleField "ssh_keys" [t| Bool |]
++  simpleField "renew_ssh_keys" [t| Bool |]
+ 
+ pNodeSetup :: Field
+ pNodeSetup =
+diff --git a/src/Ganeti/Query/Server.hs b/src/Ganeti/Query/Server.hs
+index 352e0f2ff..aaefe2431 100644
+--- a/src/Ganeti/Query/Server.hs
++++ b/src/Ganeti/Query/Server.hs
+@@ -271,6 +271,10 @@ handleCall _ _ cdata QueryClusterInfo =
+             , ("data_collector_interval",
+                showJSON . fmap dataCollectorInterval
+                         $ clusterDataCollectors cluster)
++            , ("modify_ssh_setup",
++               showJSON $ clusterModifySshSetup cluster)
++            , ("ssh_key_type", showJSON $ clusterSshKeyType cluster)
++            , ("ssh_key_bits", showJSON $ clusterSshKeyBits cluster)
+             ]
+ 
+   in case master of
+@@ -374,13 +378,17 @@ handleCall _ _ cfg (QueryNetworks names fields lock) =
+     (map Left names) fields lock
+ 
+ handleCall _ _ cfg (QueryConfigValues fields) = do
+-  let params = [ ("cluster_name", return . showJSON . clusterClusterName
+-                                    . configCluster $ cfg)
++  let clusterProperty fn = showJSON . fn . configCluster $ cfg
++  let params = [ ("cluster_name", return $ clusterProperty clusterClusterName)
+                , ("watcher_pause", liftM (maybe JSNull showJSON)
+                                      QCluster.isWatcherPaused)
+                , ("master_node", return . genericResult (const JSNull) showJSON
+                                    $ QCluster.clusterMasterNodeName cfg)
+                , ("drain_flag", liftM (showJSON . not) isQueueOpen)
++               , ("modify_ssh_setup",
++                  return $ clusterProperty clusterModifySshSetup)
++               , ("ssh_key_type", return $ clusterProperty clusterSshKeyType)
++               , ("ssh_key_bits", return $ clusterProperty clusterSshKeyBits)
+                ] :: [(String, IO JSValue)]
+   let answer = map (fromMaybe (return JSNull) . flip lookup params) fields
+   answerEval <- sequence answer
+diff --git a/src/Ganeti/Rpc.hs b/src/Ganeti/Rpc.hs
+index c042cbe43..5416eed34 100644
+--- a/src/Ganeti/Rpc.hs
++++ b/src/Ganeti/Rpc.hs
+@@ -650,9 +650,9 @@ instance Rpc RpcCallExportList RpcResultExportList where
+   rpcResultFill _ res = fromJSValueToRes res RpcResultExportList
+ 
+ -- ** Job Queue Replication
+-  
++
+ -- | Update a job queue file
+-  
++
+ $(buildObject "RpcCallJobqueueUpdate" "rpcCallJobqueueUpdate"
+   [ simpleField "file_name" [t| String |]
+   , simpleField "content" [t| String |]
+@@ -702,9 +702,9 @@ instance Rpc RpcCallJobqueueRename RpcResultJobqueueRename where
+              $ JsonDecodeError ("Expected JSNull, got " ++ show (pp_value res))
+ 
+ -- ** Watcher Status Update
+-      
++
+ -- | Set the watcher status
+-      
++
+ $(buildObject "RpcCallSetWatcherPause" "rpcCallSetWatcherPause"
+   [ optionalField $ timeAsDoubleField "time"
+   ])
+@@ -724,9 +724,9 @@ instance Rpc RpcCallSetWatcherPause RpcResultSetWatcherPause where
+            ("Expected JSNull, got " ++ show (pp_value res))
+ 
+ -- ** Queue drain status
+-      
++
+ -- | Set the queu drain flag
+-      
++
+ $(buildObject "RpcCallSetDrainFlag" "rpcCallSetDrainFlag"
+   [ simpleField "value" [t| Bool |]
+   ])
+diff --git a/src/Ganeti/Types.hs b/src/Ganeti/Types.hs
+index 4d430cb0a..eb297e0cf 100644
+--- a/src/Ganeti/Types.hs
++++ b/src/Ganeti/Types.hs
+@@ -171,6 +171,8 @@ module Ganeti.Types
+   , hotplugTargetToRaw
+   , HotplugAction(..)
+   , hotplugActionToRaw
++  , SshKeyType(..)
++  , sshKeyTypeToRaw
+   , Private(..)
+   , showPrivateJSObject
+   , HvParams
+@@ -930,6 +932,15 @@ $(THH.declareLADT ''String "HotplugTarget"
+   ])
+ $(THH.makeJSONInstance ''HotplugTarget)
+ 
++-- | SSH key type.
++
++$(THH.declareLADT ''String "SshKeyType"
++  [ ("RSA", "rsa")
++  , ("DSA", "dsa")
++  , ("ECDSA", "ecdsa")
++  ])
++$(THH.makeJSONInstance ''SshKeyType)
++
+ -- * Private type and instances
+ 
+ -- | A container for values that should be happy to be manipulated yet
+diff --git a/test/hs/Test/Ganeti/Objects.hs b/test/hs/Test/Ganeti/Objects.hs
+index 71a1d3c73..ffc7b17f4 100644
+--- a/test/hs/Test/Ganeti/Objects.hs
++++ b/test/hs/Test/Ganeti/Objects.hs
+@@ -379,6 +379,13 @@ instance Arbitrary FilterRule where
+                          <*> arbitrary
+                          <*> fmap UTF8.fromString genUUID
+ 
++instance Arbitrary SshKeyType where
++  arbitrary = oneof
++    [ pure RSA
++    , pure DSA
++    , pure ECDSA
++    ]
++
+ -- | Generates a network instance with minimum netmasks of /24. Generating
+ -- bigger networks slows down the tests, because long bit strings are generated
+ -- for the reservations.
+diff --git a/test/hs/Test/Ganeti/OpCodes.hs b/test/hs/Test/Ganeti/OpCodes.hs
+index 229696f1f..fad4df13a 100644
+--- a/test/hs/Test/Ganeti/OpCodes.hs
++++ b/test/hs/Test/Ganeti/OpCodes.hs
+@@ -168,8 +168,13 @@ instance Arbitrary OpCodes.OpCode where
+       "OP_TAGS_DEL" ->
+         arbitraryOpTagsDel
+       "OP_CLUSTER_POST_INIT" -> pure OpCodes.OpClusterPostInit
+-      "OP_CLUSTER_RENEW_CRYPTO" -> OpCodes.OpClusterRenewCrypto <$>
+-         arbitrary <*> arbitrary <*> arbitrary <*> arbitrary
++      "OP_CLUSTER_RENEW_CRYPTO" -> OpCodes.OpClusterRenewCrypto
++         <$> arbitrary -- Node SSL certificates
++         <*> arbitrary -- renew_ssh_keys
++         <*> arbitrary -- ssh_key_type
++         <*> arbitrary -- ssh_key_bits
++         <*> arbitrary -- verbose
++         <*> arbitrary -- debug
+       "OP_CLUSTER_DESTROY" -> pure OpCodes.OpClusterDestroy
+       "OP_CLUSTER_QUERY" -> pure OpCodes.OpClusterQuery
+       "OP_CLUSTER_VERIFY" ->
+diff --git a/test/py/cfgupgrade_unittest.py b/test/py/cfgupgrade_unittest.py
+index dc0bcdd49..f50d5efa8 100755
+--- a/test/py/cfgupgrade_unittest.py
++++ b/test/py/cfgupgrade_unittest.py
+@@ -74,6 +74,8 @@ def GetMinimalConfig():
+         "cpu-avg-load": { "active": True, "interval": 5000000 },
+         "xen-cpu-avg-load": { "active": True, "interval": 5000000 },
+       },
++      "ssh_key_type": "dsa",
++      "ssh_key_bits": 1024,
+     },
+     "instances": {},
+     "disks": {},
+diff --git a/test/py/ganeti.backend_unittest.py b/test/py/ganeti.backend_unittest.py
+index 43d2dde18..14fafc113 100755
+--- a/test/py/ganeti.backend_unittest.py
++++ b/test/py/ganeti.backend_unittest.py
+@@ -1055,6 +1055,7 @@ class TestAddRemoveGenerateNodeSshKey(testutils.GanetiTestCase):
+     backend._GenerateNodeSshKey(
+         test_node_uuid, test_node_name,
+         self._ssh_file_manager.GetSshPortMap(self._SSH_PORT),
++        "rsa", 2048,
+         pub_key_file=self._pub_key_file,
+         ssconf_store=self._ssconf_mock,
+         noded_cert_file=self.noded_cert_file,
+@@ -1825,8 +1826,8 @@ class TestVerifySshSetup(testutils.GanetiTestCase):
+     self._read_file_mock = self._read_file_patcher.start()
+     self._read_file_mock.return_value = self._NODE1_KEYS[0]
+     self.tmpdir = tempfile.mkdtemp()
+-    self.pub_key_file = os.path.join(self.tmpdir, "pub_key_file")
+-    open(self.pub_key_file, "w").close()
++    self.pub_keys_file = os.path.join(self.tmpdir, "pub_keys_file")
++    open(self.pub_keys_file, "w").close()
+ 
+   def tearDown(self):
+     super(testutils.GanetiTestCase, self).tearDown()
+@@ -1841,7 +1842,8 @@ class TestVerifySshSetup(testutils.GanetiTestCase):
+     self._query_mock.return_value = self._PUB_KEY_RESULT
+     result = backend._VerifySshSetup(self._NODE_STATUS_LIST,
+                                      self._NODE1_NAME,
+-                                     pub_key_file=self.pub_key_file)
++                                     "dsa",
++                                     ganeti_pub_keys_file=self.pub_keys_file)
+     self.assertEqual(result, [])
+ 
+   def testMissingKey(self):
+@@ -1852,7 +1854,8 @@ class TestVerifySshSetup(testutils.GanetiTestCase):
+     self._query_mock.return_value = pub_key_missing
+     result = backend._VerifySshSetup(self._NODE_STATUS_LIST,
+                                      self._NODE1_NAME,
+-                                     pub_key_file=self.pub_key_file)
++                                     "dsa",
++                                     ganeti_pub_keys_file=self.pub_keys_file)
+     self.assertTrue(self._NODE2_UUID in result[0])
+ 
+   def testUnknownKey(self):
+@@ -1863,7 +1866,8 @@ class TestVerifySshSetup(testutils.GanetiTestCase):
+     self._query_mock.return_value = pub_key_missing
+     result = backend._VerifySshSetup(self._NODE_STATUS_LIST,
+                                      self._NODE1_NAME,
+-                                     pub_key_file=self.pub_key_file)
++                                     "dsa",
++                                     ganeti_pub_keys_file=self.pub_keys_file)
+     self.assertTrue("unkownnodeuuid" in result[0])
+ 
+   def testMissingMasterCandidate(self):
+@@ -1874,7 +1878,8 @@ class TestVerifySshSetup(testutils.GanetiTestCase):
+     self._query_mock.return_value = self._PUB_KEY_RESULT
+     result = backend._VerifySshSetup(self._NODE_STATUS_LIST,
+                                      self._NODE1_NAME,
+-                                     pub_key_file=self.pub_key_file)
++                                     "dsa",
++                                     ganeti_pub_keys_file=self.pub_keys_file)
+     self.assertTrue(self._NODE1_UUID in result[0])
+ 
+   def testSuperfluousNormalNode(self):
+@@ -1885,7 +1890,8 @@ class TestVerifySshSetup(testutils.GanetiTestCase):
+     self._query_mock.return_value = self._PUB_KEY_RESULT
+     result = backend._VerifySshSetup(self._NODE_STATUS_LIST,
+                                      self._NODE1_NAME,
+-                                     pub_key_file=self.pub_key_file)
++                                     "dsa",
++                                     ganeti_pub_keys_file=self.pub_keys_file)
+     self.assertTrue(self._NODE3_UUID in result[0])
+ 
+ 
+diff --git a/test/py/ganeti.client.gnt_cluster_unittest.py b/test/py/ganeti.client.gnt_cluster_unittest.py
+index be28eb27d..c2cb9f5e0 100755
+--- a/test/py/ganeti.client.gnt_cluster_unittest.py
++++ b/test/py/ganeti.client.gnt_cluster_unittest.py
+@@ -380,7 +380,9 @@ class TestBuildGanetiPubKeys(testutils.GanetiTestCase):
+   _CLUSTER_NAME = "cluster_name"
+   _PRIV_KEY = "master_private_key"
+   _PUB_KEY = "master_public_key"
++  _MODIFY_SSH_SETUP = True
+   _AUTH_KEYS = "a\nb\nc"
++  _SSH_KEY_TYPE = "dsa"
+ 
+   def _setUpFakeKeys(self):
+     os.makedirs(os.path.join(self.tmpdir, ".ssh"))
+@@ -411,7 +413,8 @@ class TestBuildGanetiPubKeys(testutils.GanetiTestCase):
+     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._CLUSTER_NAME, self._MASTER_NODE_NAME, self._MODIFY_SSH_SETUP,
++       self._SSH_KEY_TYPE)
+ 
+     self._get_online_nodes_mock = mock.Mock()
+     self._get_online_nodes_mock.return_value = \
+diff --git a/test/py/ganeti.ssh_unittest.py b/test/py/ganeti.ssh_unittest.py
+index 9ec2397b0..265adeca4 100755
+--- a/test/py/ganeti.ssh_unittest.py
++++ b/test/py/ganeti.ssh_unittest.py
+@@ -279,6 +279,30 @@ class TestSshKeys(testutils.GanetiTestCase):
+       "ssh-dss AAAAB3asdfasdfaYTUCB laracroft@test\n"
+       "ssh-dss AasdfliuobaosfMAAACB frodo@test\n")
+ 
++  def testOtherKeyTypes(self):
++    key_rsa = "ssh-rsa AAAAimnottypingallofthathere0jfJs22 test@test"
++    key_ed25519 = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOlcZ6cpQTGow0LZECRHWn9"\
++                  "7Yvn16J5un501T/RcbfuF fast@secure"
++    key_ecdsa = "ecdsa-sha2-nistp256 AAAAE2VjZHNtoolongk/TNhVbEg= secure@secure"
++
++    def _ToFileContent(keys):
++      return '\n'.join(keys) + '\n'
++
++    ssh.AddAuthorizedKeys(self.tmpname, [key_rsa, key_ed25519, key_ecdsa])
++    self.assertFileContent(self.tmpname,
++                           _ToFileContent([self.KEY_A, self.KEY_B, key_rsa,
++                                           key_ed25519, key_ecdsa]))
++
++    ssh.RemoveAuthorizedKey(self.tmpname, key_ed25519)
++    self.assertFileContent(self.tmpname,
++                           _ToFileContent([self.KEY_A, self.KEY_B, key_rsa,
++                                           key_ecdsa]))
++
++    ssh.RemoveAuthorizedKey(self.tmpname, key_rsa)
++    ssh.RemoveAuthorizedKey(self.tmpname, key_ecdsa)
++    self.assertFileContent(self.tmpname,
++                           _ToFileContent([self.KEY_A, self.KEY_B]))
++
+ 
+ class TestPublicSshKeys(testutils.GanetiTestCase):
+   """Test case for the handling of the list of public ssh keys."""
+@@ -450,18 +474,51 @@ class TestGetUserFiles(testutils.GanetiTestCase):
+     return self.tmpdir
+ 
+   def testNewKeysOverrideOldKeys(self):
+-    ssh.InitSSHSetup(_homedir_fn=self._GetTempHomedir)
++    ssh.InitSSHSetup("dsa", 1024, _homedir_fn=self._GetTempHomedir)
+     self.assertFileContentNotEqual(self.priv_filename, self._PRIV_KEY)
+     self.assertFileContentNotEqual(self.pub_filename, self._PUB_KEY)
+ 
+   def testSuffix(self):
+     suffix = "_pinkbunny"
+-    ssh.InitSSHSetup(_homedir_fn=self._GetTempHomedir, _suffix=suffix)
++    ssh.InitSSHSetup("dsa", 1024, _homedir_fn=self._GetTempHomedir,
++                     _suffix=suffix)
+     self.assertFileContent(self.priv_filename, self._PRIV_KEY)
+     self.assertFileContent(self.pub_filename, self._PUB_KEY)
+     self.assertTrue(os.path.exists(self.priv_filename + suffix))
+     self.assertTrue(os.path.exists(self.priv_filename + suffix + ".pub"))
+ 
+ 
++class TestDetermineKeyBits():
++  def testCompleteness(self):
++    self.assertEquals(constants.SSHK_ALL, ssh.SSH_KEY_VALID_BITS.keys())
++
++  def testAdoptDefault(self):
++    self.assertEquals(2048, DetermineKeyBits("rsa", None, None, None))
++    self.assertEquals(1024, DetermineKeyBits("dsa", None, None, None))
++
++  def testAdoptOldKeySize(self):
++    self.assertEquals(4098, DetermineKeyBits("rsa", None, "rsa", 4098))
++    self.assertEquals(2048, DetermineKeyBits("rsa", None, "dsa", 1024))
++
++  def testDsaSpecificValues(self):
++    self.assertRaises(errors.OpPrereqError, DetermineKeyBits, "dsa", 2048,
++                      None, None)
++    self.assertRaises(errors.OpPrereqError, DetermineKeyBits, "dsa", 512,
++                      None, None)
++    self.assertEquals(1024, DetermineKeyBits("dsa", None, None, None))
++
++  def testEcdsaSpecificValues(self):
++    self.assertRaises(errors.OpPrereqError, DetermineKeyBits, "ecdsa", 2048,
++                      None, None)
++    for b in [256, 384, 521]:
++      self.assertEquals(b, DetermineKeyBits("ecdsa", b, None, None))
++
++  def testRsaSpecificValues(self):
++    self.assertRaises(errors.OpPrereqError, DetermineKeyBits, "dsa", 766,
++                      None, None)
++    for b in [768, 769, 2048, 2049, 4096]:
++      self.assertEquals(b, DetermineKeyBits("rsa", b, None, None))
++
++
+ if __name__ == "__main__":
+   testutils.GanetiTestProgram()
+diff --git a/test/py/ganeti.tools.prepare_node_join_unittest.py b/test/py/ganeti.tools.prepare_node_join_unittest.py
+index a76db1557..7901199bf 100755
+--- a/test/py/ganeti.tools.prepare_node_join_unittest.py
++++ b/test/py/ganeti.tools.prepare_node_join_unittest.py
+@@ -164,6 +164,8 @@ class TestUpdateSshDaemon(unittest.TestCase):
+         (constants.SSHK_ECDSA, "ecdsapriv", "ecdsapub"),
+         (constants.SSHK_RSA, "rsapriv", "rsapub"),
+         ],
++      constants.SSHS_SSH_KEY_TYPE: "dsa",
++      constants.SSHS_SSH_KEY_BITS: 1024,
+       }
+     runcmd_fn = compat.partial(self._RunCmd, failcmd)
+     if failcmd:
+@@ -228,7 +230,9 @@ class TestUpdateSshRoot(unittest.TestCase):
+     data = {
+       constants.SSHS_SSH_ROOT_KEY: [
+         (constants.SSHK_DSA, "privatedsa", "ssh-dss pubdsa"),
+-        ]
++        ],
++      constants.SSHS_SSH_KEY_TYPE: "dsa",
++      constants.SSHS_SSH_KEY_BITS: 1024,
+       }
+ 
+     prepare_node_join.UpdateSshRoot(data, False,
+-- 
+2.11.0
+
diff -Nru ganeti-2.15.2/debian/patches/series ganeti-2.15.2/debian/patches/series
--- ganeti-2.15.2/debian/patches/series	2016-12-13 17:40:29.000000000 +0200
+++ ganeti-2.15.2/debian/patches/series	2017-05-23 15:49:40.000000000 +0300
@@ -10,3 +10,6 @@
 0001-GHC-8-support.patch
 ghc8-fixes
 snap-server-1.0-compat
+non-DSA-SSH-key-support.patch
+fix-ssh-key-renewal-on-single-node-clusters.patch
+set-defaults-for-ssh-type-bits.patch
diff -Nru ganeti-2.15.2/debian/patches/set-defaults-for-ssh-type-bits.patch ganeti-2.15.2/debian/patches/set-defaults-for-ssh-type-bits.patch
--- ganeti-2.15.2/debian/patches/set-defaults-for-ssh-type-bits.patch	1970-01-01 02:00:00.000000000 +0200
+++ ganeti-2.15.2/debian/patches/set-defaults-for-ssh-type-bits.patch	2017-05-23 15:49:40.000000000 +0300
@@ -0,0 +1,31 @@
+From 423832d2f3ba90deae1e58c52aad6aac5b3a1e9d Mon Sep 17 00:00:00 2001
+From: Apollon Oikonomopoulos <apoi...@debian.org>
+Date: Wed, 24 May 2017 16:36:30 +0300
+Subject: [PATCH 2/2] Use runtime defaults for ssh_key_type and ssh_key_bits
+
+Since we are introducing config changes in a minor version, we need to
+assume sane defaults.
+---
+ src/Ganeti/Objects.hs | 6 ++++--
+ 1 file changed, 4 insertions(+), 2 deletions(-)
+
+diff --git a/src/Ganeti/Objects.hs b/src/Ganeti/Objects.hs
+index 281885171..76a7e7128 100644
+--- a/src/Ganeti/Objects.hs
++++ b/src/Ganeti/Objects.hs
+@@ -678,8 +678,10 @@ $(buildObject "Cluster" "cluster" $
+   , simpleField "compression_tools"              [t| [String]                |]
+   , simpleField "enabled_user_shutdown"          [t| Bool                    |]
+   , simpleField "data_collectors"         [t| Container DataCollectorConfig  |]
+-  , simpleField "ssh_key_type"                   [t| SshKeyType              |]
+-  , simpleField "ssh_key_bits"                   [t| Int                     |]
++  , defaultField [| DSA |] $
++    simpleField "ssh_key_type"                   [t| SshKeyType             |]
++  , defaultField [| 1024 |] $
++    simpleField "ssh_key_bits"                   [t| Int                    |]
+  ]
+  ++ timeStampFields
+  ++ uuidFields
+-- 
+2.11.0
+

Reply via email to