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