[Date Prev][Date Next] [Thread Prev][Thread Next] [Date Index] [Thread Index]

Bug#863320: marked as done (unblock: ganeti/2.15.2-8)



Your message dated Sat, 10 Jun 2017 15:54:00 +0000
with message-id <3d4fe46c-6755-d0fe-a806-073de3d069a0@thykier.net>
and subject line Re: Bug#863320: Acknowledgement ((pre-approval) unblock: ganeti/2.15.2-8)
has caused the Debian Bug report #863320,
regarding unblock: ganeti/2.15.2-8
to be marked as done.

This means that you claim that the problem has been dealt with.
If this is not the case it is now your responsibility to reopen the
Bug report if necessary, and/or fix the problem forthwith.

(NB: If you are a system administrator and have no idea what this
message is talking about, this may indicate a serious mail system
misconfiguration somewhere. Please contact owner@bugs.debian.org
immediately.)


-- 
863320: http://bugs.debian.org/cgi-bin/bugreport.cgi?bug=863320
Debian Bug Tracking System
Contact owner@bugs.debian.org with problems
--- Begin Message ---
Package: release.debian.org
Severity: normal
User: release.debian.org@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 <apoikos@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 <ultrotter@debian.org>,
  Apollon Oikonomopoulos <apoikos@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-ganeti@lists.alioth.debian.org>
+           Copyright (c) 2007-2017 Debian Ganeti Team <pkg-ganeti@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 <apoikos@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 <apoikos@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 <apoikos@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 <riba@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 <riba@google.com>
+    Reviewed-by: Helga Velroyen <helgav@google.com>
+    (cherry picked from commit 2ebf4e8b0644b9ea05f377bd654e36c6d9e4e8bc)
+
+commit e8a4295edc787a2e6353c97664fbc5d612c40d49
+Author: Hrvoje Ribicic <riba@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 <riba@google.com>
+    Reviewed-by: Helga Velroyen <helgav@google.com>
+    (cherry picked from commit af0a89fd07276e3373f2d2991d0c76ed13a2c29a)
+
+commit 32a3f5f0dca90021b967f6ccce08e4c3ce32b5f8
+Author: Hrvoje Ribicic <riba@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 <riba@google.com>
+    Reviewed-by: Helga Velroyen <helgav@google.com>
+    (cherry picked from commit e90da976de6cbae6831f8c11dd96c0eab7e29abe)
+
+commit 15d155105feb7a54b5b16f77763ef3b088b13434
+Author: Hrvoje Ribicic <riba@google.com>
+Date:   Fri Nov 6 02:53:00 2015 +0100
+
+    Fix typo
+
+    Signed-off-by: Hrvoje Ribicic <riba@google.com>
+    Reviewed-by: Helga Velroyen <helgav@google.com>
+    (cherry picked from commit e047dee890c2e731dbabb390402fc8358221c7af)
+
+commit 1d4b4fb15cb46b628205b859458662403dc5e9d5
+Author: Hrvoje Ribicic <riba@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 <riba@google.com>
+    Reviewed-by: Helga Velroyen <helgav@google.com>
+    (cherry picked from commit ff8695380c59642671c17c30b4a24264b1530d10)
+
+commit c00981f1ddc49ec89947a63d8b399b8a2c6572ea
+Author: Hrvoje Ribicic <riba@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 <riba@google.com>
+    Reviewed-by: Helga Velroyen <helgav@google.com>
+    (cherry picked from commit e3a489401eab9041788b231532a8c2c4971aa3cf)
+
+commit 00966081d5770726c66b1b129c46873eb8552633
+Author: Hrvoje Ribicic <riba@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 <riba@google.com>
+    Reviewed-by: Helga Velroyen <helgav@google.com>
+    (cherry picked from commit c64af824018a505c59453d6a645f11f0b8fb8877)
+
+commit 7af8f17b5442207f56d36e0ecb616eba506925c2
+Author: Hrvoje Ribicic <riba@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 <riba@google.com>
+    Reviewed-by: Helga Velroyen <helgav@google.com>
+    (cherry picked from commit 33dea8cba2be219e6be1204e1e27def85616dc5b)
+
+commit f3cb90123e3aea20b38f46674b23aba9a83d9470
+Author: Hrvoje Ribicic <riba@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 <riba@google.com>
+    Reviewed-by: Helga Velroyen <helgav@google.com>
+    (cherry picked from commit b33ef423ed4d3ddd22af2ea050920fb30a945d04)
+
+commit 1e7f97429025438a89f376c921f46dfcd6c8d99b
+Author: Hrvoje Ribicic <riba@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 <riba@google.com>
+    Reviewed-by: Helga Velroyen <helgav@google.com>
+    (cherry picked from commit db6051b118fcca9181fd91ae497c9be3b97ca5e3)
+
+commit 64e8298d284b5d50e49a592b684c3c178417fe9f
+Author: Hrvoje Ribicic <riba@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 <riba@google.com>
+    Reviewed-by: Helga Velroyen <helgav@google.com>
+    (cherry picked from commit e4bd432d69d33cfe70c8c08130ec2d25d9f1673f)
+
+commit a8b24d1a6b77340bc0df6fca36222e5e97a70594
+Author: Hrvoje Ribicic <riba@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 <riba@google.com>
+    Reviewed-by: Helga Velroyen <helgav@google.com>
+    (cherry picked from commit 6b2e842cb625f58e6bb455198282e1d0fdc62fbe)
+
+commit a35558d0271bf0ec14ff917e50cd55c5617179b0
+Author: Hrvoje Ribicic <riba@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 <riba@google.com>
+    Reviewed-by: Helga Velroyen <helgav@google.com>
+    (cherry picked from commit f8dc2eb986a7e242515604bf42fbb2879fbccd54)
+
+commit 51699c1ad79546b67cf8f321a3143902332cf481
+Author: Helga Velroyen <helgav@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 <helgav@google.com>
+    Reviewed-by: Klaus Aehlig <aehlig@google.com>
+    (cherry picked from commit 56eb7d77ea018030937e4efe8a32777d66c449d4)
+
+commit 60001eb085b0cd7cc237c658002dcb6aacca0d51
+Author: Helga Velroyen <helgav@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 <helgav@google.com>
+    Reviewed-by: Klaus Aehlig <aehlig@google.com>
+    (cherry picked from commit bbb08fcc9ade9fd0ebe166c78277423bb68a1ac0)
+
+commit 3a40bbcd5ee81b8c703b41dff8bae54841b4f032
+Author: Hrvoje Ribicic <riba@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 <riba@google.com>
+    Reviewed-by: Helga Velroyen <helgav@google.com>
+    (cherry picked from commit ce90bd13691a2be26458754b5e185e300fa843c4)
+
+commit a2f04aea6bcd929e98b66311b62cd307c34318ba
+Author: Hrvoje Ribicic <riba@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 <riba@google.com>
+    Reviewed-by: Helga Velroyen <helgav@google.com>
+    (cherry picked from commit 0d75de77077b0a02154a224f838562172ec29284)
+
+commit 0c8c0a3f68b41cf8064457ad5f73d45c3cebc455
+Author: Hrvoje Ribicic <riba@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 <riba@google.com>
+    Reviewed-by: Helga Velroyen <helgav@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 <apoikos@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
+

--- End Message ---
--- Begin Message ---
On Mon, 29 May 2017 12:42:22 +0300 Apollon Oikonomopoulos
<apoikos@debian.org> wrote:
> Control: retitle -1 unblock: ganeti/2.15.2-8
> 

Hi Apollon,

Thanks for wanting to improve stretch. :)

> Since we are near the release deadline, I uploaded 2.15.2-8 (including 
> two new fixes, see below) to unstable, to gain some time and clear 
> piuparts and CI tests.
> 

That was the right choice.  Unfortunately, we were not able to fully
review it.  While I would have loved to remove the reliance on DSA keys,
we have decided to be risk adverse and not include this update in r0.

I see some of the changes in the -8 that would probably be candidates
for an update in stretch after the release.  Please consider proposing a
stable update for these. :)

Thanks,
~Niels


Attachment: signature.asc
Description: OpenPGP digital signature


--- End Message ---

Reply to: