Bug#863320: (pre-approval) unblock: ganeti/2.15.2-8
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
+
Reply to: