|
|
591b7e |
From 60991b1241a5efb585df889d4343007e501fd70c Mon Sep 17 00:00:00 2001
|
|
|
591b7e |
From: Eduardo Otubo <otubo@redhat.com>
|
|
|
591b7e |
Date: Tue, 5 May 2020 08:08:15 +0200
|
|
|
591b7e |
Subject: [PATCH 2/5] Add support for publishing host keys to GCE guest
|
|
|
591b7e |
attributes
|
|
|
591b7e |
|
|
|
591b7e |
RH-Author: Eduardo Otubo <otubo@redhat.com>
|
|
|
591b7e |
Message-id: <20200504085238.25884-3-otubo@redhat.com>
|
|
|
591b7e |
Patchwork-id: 96243
|
|
|
591b7e |
O-Subject: [RHEL-7.8.z cloud-init PATCH 2/5] Add support for publishing host keys to GCE guest attributes
|
|
|
591b7e |
Bugzilla: 1827207
|
|
|
591b7e |
RH-Acked-by: Cathy Avery <cavery@redhat.com>
|
|
|
591b7e |
RH-Acked-by: Mohammed Gamal <mgamal@redhat.com>
|
|
|
591b7e |
RH-Acked-by: Vitaly Kuznetsov <vkuznets@redhat.com>
|
|
|
591b7e |
|
|
|
591b7e |
commit 155847209e6a3ed5face91a133d8488a703f3f93
|
|
|
591b7e |
Author: Rick Wright <rickw@google.com>
|
|
|
591b7e |
Date: Fri Aug 9 17:11:05 2019 +0000
|
|
|
591b7e |
|
|
|
591b7e |
Add support for publishing host keys to GCE guest attributes
|
|
|
591b7e |
|
|
|
591b7e |
This adds an empty publish_host_keys() method to the default datasource
|
|
|
591b7e |
that is called by cc_ssh.py. This feature can be controlled by the
|
|
|
591b7e |
'ssh_publish_hostkeys' config option. It is enabled by default but can
|
|
|
591b7e |
be disabled by setting 'enabled' to false. Also, a blacklist of key
|
|
|
591b7e |
types is supported.
|
|
|
591b7e |
|
|
|
591b7e |
In addition, this change implements ssh_publish_hostkeys() for the GCE
|
|
|
591b7e |
datasource, attempting to write the hostkeys to the instance's guest
|
|
|
591b7e |
attributes. Using these hostkeys for ssh connections is currently
|
|
|
591b7e |
supported by the alpha version of Google's 'gcloud' command-line tool.
|
|
|
591b7e |
|
|
|
591b7e |
(On Google Compute Engine, this feature will be enabled by setting the
|
|
|
591b7e |
'enable-guest-attributes' metadata key to 'true' for the
|
|
|
591b7e |
project/instance that you would like to use this feature for. When
|
|
|
591b7e |
connecting to the instance for the first time using 'gcloud compute ssh'
|
|
|
591b7e |
the hostkeys will be read from the guest attributes for the instance and
|
|
|
591b7e |
written to the user's local known_hosts file for Google Compute Engine
|
|
|
591b7e |
instances.)
|
|
|
591b7e |
|
|
|
591b7e |
Signed-off-by: Eduardo Otubo <otubo@redhat.com>
|
|
|
591b7e |
Signed-off-by: Miroslav Rezanina <mrezanin@redhat.com>
|
|
|
591b7e |
---
|
|
|
591b7e |
cloudinit/config/cc_ssh.py | 55 +++++++++
|
|
|
591b7e |
cloudinit/config/tests/test_ssh.py | 166 ++++++++++++++++++++++++++++
|
|
|
591b7e |
cloudinit/sources/DataSourceGCE.py | 22 +++-
|
|
|
591b7e |
cloudinit/sources/__init__.py | 10 ++
|
|
|
591b7e |
cloudinit/url_helper.py | 9 +-
|
|
|
591b7e |
tests/unittests/test_datasource/test_gce.py | 18 +++
|
|
|
591b7e |
6 files changed, 274 insertions(+), 6 deletions(-)
|
|
|
591b7e |
|
|
|
591b7e |
diff --git a/cloudinit/config/cc_ssh.py b/cloudinit/config/cc_ssh.py
|
|
|
591b7e |
index f8f7cb3..53f6939 100755
|
|
|
591b7e |
--- a/cloudinit/config/cc_ssh.py
|
|
|
591b7e |
+++ b/cloudinit/config/cc_ssh.py
|
|
|
591b7e |
@@ -91,6 +91,9 @@ public keys.
|
|
|
591b7e |
ssh_authorized_keys:
|
|
|
591b7e |
- ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAGEA3FSyQwBI6Z+nCSjUU ...
|
|
|
591b7e |
- ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEA3I7VUf2l5gSn5uavROsc5HRDpZ ...
|
|
|
591b7e |
+ ssh_publish_hostkeys:
|
|
|
591b7e |
+ enabled: <true/false> (Defaults to true)
|
|
|
591b7e |
+ blacklist: <list of key types> (Defaults to [dsa])
|
|
|
591b7e |
"""
|
|
|
591b7e |
|
|
|
591b7e |
import glob
|
|
|
591b7e |
@@ -104,6 +107,10 @@ from cloudinit import util
|
|
|
591b7e |
|
|
|
591b7e |
GENERATE_KEY_NAMES = ['rsa', 'dsa', 'ecdsa', 'ed25519']
|
|
|
591b7e |
KEY_FILE_TPL = '/etc/ssh/ssh_host_%s_key'
|
|
|
591b7e |
+PUBLISH_HOST_KEYS = True
|
|
|
591b7e |
+# Don't publish the dsa hostkey by default since OpenSSH recommends not using
|
|
|
591b7e |
+# it.
|
|
|
591b7e |
+HOST_KEY_PUBLISH_BLACKLIST = ['dsa']
|
|
|
591b7e |
|
|
|
591b7e |
CONFIG_KEY_TO_FILE = {}
|
|
|
591b7e |
PRIV_TO_PUB = {}
|
|
|
591b7e |
@@ -176,6 +183,23 @@ def handle(_name, cfg, cloud, log, _args):
|
|
|
591b7e |
util.logexc(log, "Failed generating key type %s to "
|
|
|
591b7e |
"file %s", keytype, keyfile)
|
|
|
591b7e |
|
|
|
591b7e |
+ if "ssh_publish_hostkeys" in cfg:
|
|
|
591b7e |
+ host_key_blacklist = util.get_cfg_option_list(
|
|
|
591b7e |
+ cfg["ssh_publish_hostkeys"], "blacklist",
|
|
|
591b7e |
+ HOST_KEY_PUBLISH_BLACKLIST)
|
|
|
591b7e |
+ publish_hostkeys = util.get_cfg_option_bool(
|
|
|
591b7e |
+ cfg["ssh_publish_hostkeys"], "enabled", PUBLISH_HOST_KEYS)
|
|
|
591b7e |
+ else:
|
|
|
591b7e |
+ host_key_blacklist = HOST_KEY_PUBLISH_BLACKLIST
|
|
|
591b7e |
+ publish_hostkeys = PUBLISH_HOST_KEYS
|
|
|
591b7e |
+
|
|
|
591b7e |
+ if publish_hostkeys:
|
|
|
591b7e |
+ hostkeys = get_public_host_keys(blacklist=host_key_blacklist)
|
|
|
591b7e |
+ try:
|
|
|
591b7e |
+ cloud.datasource.publish_host_keys(hostkeys)
|
|
|
591b7e |
+ except Exception as e:
|
|
|
591b7e |
+ util.logexc(log, "Publishing host keys failed!")
|
|
|
591b7e |
+
|
|
|
591b7e |
try:
|
|
|
591b7e |
(users, _groups) = ug_util.normalize_users_groups(cfg, cloud.distro)
|
|
|
591b7e |
(user, _user_config) = ug_util.extract_default(users)
|
|
|
591b7e |
@@ -209,4 +233,35 @@ def apply_credentials(keys, user, disable_root, disable_root_opts):
|
|
|
591b7e |
|
|
|
591b7e |
ssh_util.setup_user_keys(keys, 'root', options=key_prefix)
|
|
|
591b7e |
|
|
|
591b7e |
+
|
|
|
591b7e |
+def get_public_host_keys(blacklist=None):
|
|
|
591b7e |
+ """Read host keys from /etc/ssh/*.pub files and return them as a list.
|
|
|
591b7e |
+
|
|
|
591b7e |
+ @param blacklist: List of key types to ignore. e.g. ['dsa', 'rsa']
|
|
|
591b7e |
+ @returns: List of keys, each formatted as a two-element tuple.
|
|
|
591b7e |
+ e.g. [('ssh-rsa', 'AAAAB3Nz...'), ('ssh-ed25519', 'AAAAC3Nx...')]
|
|
|
591b7e |
+ """
|
|
|
591b7e |
+ public_key_file_tmpl = '%s.pub' % (KEY_FILE_TPL,)
|
|
|
591b7e |
+ key_list = []
|
|
|
591b7e |
+ blacklist_files = []
|
|
|
591b7e |
+ if blacklist:
|
|
|
591b7e |
+ # Convert blacklist to filenames:
|
|
|
591b7e |
+ # 'dsa' -> '/etc/ssh/ssh_host_dsa_key.pub'
|
|
|
591b7e |
+ blacklist_files = [public_key_file_tmpl % (key_type,)
|
|
|
591b7e |
+ for key_type in blacklist]
|
|
|
591b7e |
+ # Get list of public key files and filter out blacklisted files.
|
|
|
591b7e |
+ file_list = [hostfile for hostfile
|
|
|
591b7e |
+ in glob.glob(public_key_file_tmpl % ('*',))
|
|
|
591b7e |
+ if hostfile not in blacklist_files]
|
|
|
591b7e |
+
|
|
|
591b7e |
+ # Read host key files, retrieve first two fields as a tuple and
|
|
|
591b7e |
+ # append that tuple to key_list.
|
|
|
591b7e |
+ for file_name in file_list:
|
|
|
591b7e |
+ file_contents = util.load_file(file_name)
|
|
|
591b7e |
+ key_data = file_contents.split()
|
|
|
591b7e |
+ if key_data and len(key_data) > 1:
|
|
|
591b7e |
+ key_list.append(tuple(key_data[:2]))
|
|
|
591b7e |
+ return key_list
|
|
|
591b7e |
+
|
|
|
591b7e |
+
|
|
|
591b7e |
# vi: ts=4 expandtab
|
|
|
591b7e |
diff --git a/cloudinit/config/tests/test_ssh.py b/cloudinit/config/tests/test_ssh.py
|
|
|
591b7e |
index c8a4271..e778984 100644
|
|
|
591b7e |
--- a/cloudinit/config/tests/test_ssh.py
|
|
|
591b7e |
+++ b/cloudinit/config/tests/test_ssh.py
|
|
|
591b7e |
@@ -1,5 +1,6 @@
|
|
|
591b7e |
# This file is part of cloud-init. See LICENSE file for license information.
|
|
|
591b7e |
|
|
|
591b7e |
+import os.path
|
|
|
591b7e |
|
|
|
591b7e |
from cloudinit.config import cc_ssh
|
|
|
591b7e |
from cloudinit import ssh_util
|
|
|
591b7e |
@@ -12,6 +13,25 @@ MODPATH = "cloudinit.config.cc_ssh."
|
|
|
591b7e |
class TestHandleSsh(CiTestCase):
|
|
|
591b7e |
"""Test cc_ssh handling of ssh config."""
|
|
|
591b7e |
|
|
|
591b7e |
+ def _publish_hostkey_test_setup(self):
|
|
|
591b7e |
+ self.test_hostkeys = {
|
|
|
591b7e |
+ 'dsa': ('ssh-dss', 'AAAAB3NzaC1kc3MAAACB'),
|
|
|
591b7e |
+ 'ecdsa': ('ecdsa-sha2-nistp256', 'AAAAE2VjZ'),
|
|
|
591b7e |
+ 'ed25519': ('ssh-ed25519', 'AAAAC3NzaC1lZDI'),
|
|
|
591b7e |
+ 'rsa': ('ssh-rsa', 'AAAAB3NzaC1yc2EAAA'),
|
|
|
591b7e |
+ }
|
|
|
591b7e |
+ self.test_hostkey_files = []
|
|
|
591b7e |
+ hostkey_tmpdir = self.tmp_dir()
|
|
|
591b7e |
+ for key_type in ['dsa', 'ecdsa', 'ed25519', 'rsa']:
|
|
|
591b7e |
+ key_data = self.test_hostkeys[key_type]
|
|
|
591b7e |
+ filename = 'ssh_host_%s_key.pub' % key_type
|
|
|
591b7e |
+ filepath = os.path.join(hostkey_tmpdir, filename)
|
|
|
591b7e |
+ self.test_hostkey_files.append(filepath)
|
|
|
591b7e |
+ with open(filepath, 'w') as f:
|
|
|
591b7e |
+ f.write(' '.join(key_data))
|
|
|
591b7e |
+
|
|
|
591b7e |
+ cc_ssh.KEY_FILE_TPL = os.path.join(hostkey_tmpdir, 'ssh_host_%s_key')
|
|
|
591b7e |
+
|
|
|
591b7e |
def test_apply_credentials_with_user(self, m_setup_keys):
|
|
|
591b7e |
"""Apply keys for the given user and root."""
|
|
|
591b7e |
keys = ["key1"]
|
|
|
591b7e |
@@ -64,6 +84,7 @@ class TestHandleSsh(CiTestCase):
|
|
|
591b7e |
# Mock os.path.exits to True to short-circuit the key writing logic
|
|
|
591b7e |
m_path_exists.return_value = True
|
|
|
591b7e |
m_nug.return_value = ([], {})
|
|
|
591b7e |
+ cc_ssh.PUBLISH_HOST_KEYS = False
|
|
|
591b7e |
cloud = self.tmp_cloud(
|
|
|
591b7e |
distro='ubuntu', metadata={'public-keys': keys})
|
|
|
591b7e |
cc_ssh.handle("name", cfg, cloud, None, None)
|
|
|
591b7e |
@@ -149,3 +170,148 @@ class TestHandleSsh(CiTestCase):
|
|
|
591b7e |
self.assertEqual([mock.call(set(keys), user),
|
|
|
591b7e |
mock.call(set(keys), "root", options="")],
|
|
|
591b7e |
m_setup_keys.call_args_list)
|
|
|
591b7e |
+
|
|
|
591b7e |
+ @mock.patch(MODPATH + "glob.glob")
|
|
|
591b7e |
+ @mock.patch(MODPATH + "ug_util.normalize_users_groups")
|
|
|
591b7e |
+ @mock.patch(MODPATH + "os.path.exists")
|
|
|
591b7e |
+ def test_handle_publish_hostkeys_default(
|
|
|
591b7e |
+ self, m_path_exists, m_nug, m_glob, m_setup_keys):
|
|
|
591b7e |
+ """Test handle with various configs for ssh_publish_hostkeys."""
|
|
|
591b7e |
+ self._publish_hostkey_test_setup()
|
|
|
591b7e |
+ cc_ssh.PUBLISH_HOST_KEYS = True
|
|
|
591b7e |
+ keys = ["key1"]
|
|
|
591b7e |
+ user = "clouduser"
|
|
|
591b7e |
+ # Return no matching keys for first glob, test keys for second.
|
|
|
591b7e |
+ m_glob.side_effect = iter([
|
|
|
591b7e |
+ [],
|
|
|
591b7e |
+ self.test_hostkey_files,
|
|
|
591b7e |
+ ])
|
|
|
591b7e |
+ # Mock os.path.exits to True to short-circuit the key writing logic
|
|
|
591b7e |
+ m_path_exists.return_value = True
|
|
|
591b7e |
+ m_nug.return_value = ({user: {"default": user}}, {})
|
|
|
591b7e |
+ cloud = self.tmp_cloud(
|
|
|
591b7e |
+ distro='ubuntu', metadata={'public-keys': keys})
|
|
|
591b7e |
+ cloud.datasource.publish_host_keys = mock.Mock()
|
|
|
591b7e |
+
|
|
|
591b7e |
+ cfg = {}
|
|
|
591b7e |
+ expected_call = [self.test_hostkeys[key_type] for key_type
|
|
|
591b7e |
+ in ['ecdsa', 'ed25519', 'rsa']]
|
|
|
591b7e |
+ cc_ssh.handle("name", cfg, cloud, None, None)
|
|
|
591b7e |
+ self.assertEqual([mock.call(expected_call)],
|
|
|
591b7e |
+ cloud.datasource.publish_host_keys.call_args_list)
|
|
|
591b7e |
+
|
|
|
591b7e |
+ @mock.patch(MODPATH + "glob.glob")
|
|
|
591b7e |
+ @mock.patch(MODPATH + "ug_util.normalize_users_groups")
|
|
|
591b7e |
+ @mock.patch(MODPATH + "os.path.exists")
|
|
|
591b7e |
+ def test_handle_publish_hostkeys_config_enable(
|
|
|
591b7e |
+ self, m_path_exists, m_nug, m_glob, m_setup_keys):
|
|
|
591b7e |
+ """Test handle with various configs for ssh_publish_hostkeys."""
|
|
|
591b7e |
+ self._publish_hostkey_test_setup()
|
|
|
591b7e |
+ cc_ssh.PUBLISH_HOST_KEYS = False
|
|
|
591b7e |
+ keys = ["key1"]
|
|
|
591b7e |
+ user = "clouduser"
|
|
|
591b7e |
+ # Return no matching keys for first glob, test keys for second.
|
|
|
591b7e |
+ m_glob.side_effect = iter([
|
|
|
591b7e |
+ [],
|
|
|
591b7e |
+ self.test_hostkey_files,
|
|
|
591b7e |
+ ])
|
|
|
591b7e |
+ # Mock os.path.exits to True to short-circuit the key writing logic
|
|
|
591b7e |
+ m_path_exists.return_value = True
|
|
|
591b7e |
+ m_nug.return_value = ({user: {"default": user}}, {})
|
|
|
591b7e |
+ cloud = self.tmp_cloud(
|
|
|
591b7e |
+ distro='ubuntu', metadata={'public-keys': keys})
|
|
|
591b7e |
+ cloud.datasource.publish_host_keys = mock.Mock()
|
|
|
591b7e |
+
|
|
|
591b7e |
+ cfg = {'ssh_publish_hostkeys': {'enabled': True}}
|
|
|
591b7e |
+ expected_call = [self.test_hostkeys[key_type] for key_type
|
|
|
591b7e |
+ in ['ecdsa', 'ed25519', 'rsa']]
|
|
|
591b7e |
+ cc_ssh.handle("name", cfg, cloud, None, None)
|
|
|
591b7e |
+ self.assertEqual([mock.call(expected_call)],
|
|
|
591b7e |
+ cloud.datasource.publish_host_keys.call_args_list)
|
|
|
591b7e |
+
|
|
|
591b7e |
+ @mock.patch(MODPATH + "glob.glob")
|
|
|
591b7e |
+ @mock.patch(MODPATH + "ug_util.normalize_users_groups")
|
|
|
591b7e |
+ @mock.patch(MODPATH + "os.path.exists")
|
|
|
591b7e |
+ def test_handle_publish_hostkeys_config_disable(
|
|
|
591b7e |
+ self, m_path_exists, m_nug, m_glob, m_setup_keys):
|
|
|
591b7e |
+ """Test handle with various configs for ssh_publish_hostkeys."""
|
|
|
591b7e |
+ self._publish_hostkey_test_setup()
|
|
|
591b7e |
+ cc_ssh.PUBLISH_HOST_KEYS = True
|
|
|
591b7e |
+ keys = ["key1"]
|
|
|
591b7e |
+ user = "clouduser"
|
|
|
591b7e |
+ # Return no matching keys for first glob, test keys for second.
|
|
|
591b7e |
+ m_glob.side_effect = iter([
|
|
|
591b7e |
+ [],
|
|
|
591b7e |
+ self.test_hostkey_files,
|
|
|
591b7e |
+ ])
|
|
|
591b7e |
+ # Mock os.path.exits to True to short-circuit the key writing logic
|
|
|
591b7e |
+ m_path_exists.return_value = True
|
|
|
591b7e |
+ m_nug.return_value = ({user: {"default": user}}, {})
|
|
|
591b7e |
+ cloud = self.tmp_cloud(
|
|
|
591b7e |
+ distro='ubuntu', metadata={'public-keys': keys})
|
|
|
591b7e |
+ cloud.datasource.publish_host_keys = mock.Mock()
|
|
|
591b7e |
+
|
|
|
591b7e |
+ cfg = {'ssh_publish_hostkeys': {'enabled': False}}
|
|
|
591b7e |
+ cc_ssh.handle("name", cfg, cloud, None, None)
|
|
|
591b7e |
+ self.assertFalse(cloud.datasource.publish_host_keys.call_args_list)
|
|
|
591b7e |
+ cloud.datasource.publish_host_keys.assert_not_called()
|
|
|
591b7e |
+
|
|
|
591b7e |
+ @mock.patch(MODPATH + "glob.glob")
|
|
|
591b7e |
+ @mock.patch(MODPATH + "ug_util.normalize_users_groups")
|
|
|
591b7e |
+ @mock.patch(MODPATH + "os.path.exists")
|
|
|
591b7e |
+ def test_handle_publish_hostkeys_config_blacklist(
|
|
|
591b7e |
+ self, m_path_exists, m_nug, m_glob, m_setup_keys):
|
|
|
591b7e |
+ """Test handle with various configs for ssh_publish_hostkeys."""
|
|
|
591b7e |
+ self._publish_hostkey_test_setup()
|
|
|
591b7e |
+ cc_ssh.PUBLISH_HOST_KEYS = True
|
|
|
591b7e |
+ keys = ["key1"]
|
|
|
591b7e |
+ user = "clouduser"
|
|
|
591b7e |
+ # Return no matching keys for first glob, test keys for second.
|
|
|
591b7e |
+ m_glob.side_effect = iter([
|
|
|
591b7e |
+ [],
|
|
|
591b7e |
+ self.test_hostkey_files,
|
|
|
591b7e |
+ ])
|
|
|
591b7e |
+ # Mock os.path.exits to True to short-circuit the key writing logic
|
|
|
591b7e |
+ m_path_exists.return_value = True
|
|
|
591b7e |
+ m_nug.return_value = ({user: {"default": user}}, {})
|
|
|
591b7e |
+ cloud = self.tmp_cloud(
|
|
|
591b7e |
+ distro='ubuntu', metadata={'public-keys': keys})
|
|
|
591b7e |
+ cloud.datasource.publish_host_keys = mock.Mock()
|
|
|
591b7e |
+
|
|
|
591b7e |
+ cfg = {'ssh_publish_hostkeys': {'enabled': True,
|
|
|
591b7e |
+ 'blacklist': ['dsa', 'rsa']}}
|
|
|
591b7e |
+ expected_call = [self.test_hostkeys[key_type] for key_type
|
|
|
591b7e |
+ in ['ecdsa', 'ed25519']]
|
|
|
591b7e |
+ cc_ssh.handle("name", cfg, cloud, None, None)
|
|
|
591b7e |
+ self.assertEqual([mock.call(expected_call)],
|
|
|
591b7e |
+ cloud.datasource.publish_host_keys.call_args_list)
|
|
|
591b7e |
+
|
|
|
591b7e |
+ @mock.patch(MODPATH + "glob.glob")
|
|
|
591b7e |
+ @mock.patch(MODPATH + "ug_util.normalize_users_groups")
|
|
|
591b7e |
+ @mock.patch(MODPATH + "os.path.exists")
|
|
|
591b7e |
+ def test_handle_publish_hostkeys_empty_blacklist(
|
|
|
591b7e |
+ self, m_path_exists, m_nug, m_glob, m_setup_keys):
|
|
|
591b7e |
+ """Test handle with various configs for ssh_publish_hostkeys."""
|
|
|
591b7e |
+ self._publish_hostkey_test_setup()
|
|
|
591b7e |
+ cc_ssh.PUBLISH_HOST_KEYS = True
|
|
|
591b7e |
+ keys = ["key1"]
|
|
|
591b7e |
+ user = "clouduser"
|
|
|
591b7e |
+ # Return no matching keys for first glob, test keys for second.
|
|
|
591b7e |
+ m_glob.side_effect = iter([
|
|
|
591b7e |
+ [],
|
|
|
591b7e |
+ self.test_hostkey_files,
|
|
|
591b7e |
+ ])
|
|
|
591b7e |
+ # Mock os.path.exits to True to short-circuit the key writing logic
|
|
|
591b7e |
+ m_path_exists.return_value = True
|
|
|
591b7e |
+ m_nug.return_value = ({user: {"default": user}}, {})
|
|
|
591b7e |
+ cloud = self.tmp_cloud(
|
|
|
591b7e |
+ distro='ubuntu', metadata={'public-keys': keys})
|
|
|
591b7e |
+ cloud.datasource.publish_host_keys = mock.Mock()
|
|
|
591b7e |
+
|
|
|
591b7e |
+ cfg = {'ssh_publish_hostkeys': {'enabled': True,
|
|
|
591b7e |
+ 'blacklist': []}}
|
|
|
591b7e |
+ expected_call = [self.test_hostkeys[key_type] for key_type
|
|
|
591b7e |
+ in ['dsa', 'ecdsa', 'ed25519', 'rsa']]
|
|
|
591b7e |
+ cc_ssh.handle("name", cfg, cloud, None, None)
|
|
|
591b7e |
+ self.assertEqual([mock.call(expected_call)],
|
|
|
591b7e |
+ cloud.datasource.publish_host_keys.call_args_list)
|
|
|
591b7e |
diff --git a/cloudinit/sources/DataSourceGCE.py b/cloudinit/sources/DataSourceGCE.py
|
|
|
591b7e |
index d816262..6cbfbba 100644
|
|
|
591b7e |
--- a/cloudinit/sources/DataSourceGCE.py
|
|
|
591b7e |
+++ b/cloudinit/sources/DataSourceGCE.py
|
|
|
591b7e |
@@ -18,10 +18,13 @@ LOG = logging.getLogger(__name__)
|
|
|
591b7e |
MD_V1_URL = 'http://metadata.google.internal/computeMetadata/v1/'
|
|
|
591b7e |
BUILTIN_DS_CONFIG = {'metadata_url': MD_V1_URL}
|
|
|
591b7e |
REQUIRED_FIELDS = ('instance-id', 'availability-zone', 'local-hostname')
|
|
|
591b7e |
+GUEST_ATTRIBUTES_URL = ('http://metadata.google.internal/computeMetadata/'
|
|
|
591b7e |
+ 'v1/instance/guest-attributes')
|
|
|
591b7e |
+HOSTKEY_NAMESPACE = 'hostkeys'
|
|
|
591b7e |
+HEADERS = {'Metadata-Flavor': 'Google'}
|
|
|
591b7e |
|
|
|
591b7e |
|
|
|
591b7e |
class GoogleMetadataFetcher(object):
|
|
|
591b7e |
- headers = {'Metadata-Flavor': 'Google'}
|
|
|
591b7e |
|
|
|
591b7e |
def __init__(self, metadata_address):
|
|
|
591b7e |
self.metadata_address = metadata_address
|
|
|
591b7e |
@@ -32,7 +35,7 @@ class GoogleMetadataFetcher(object):
|
|
|
591b7e |
url = self.metadata_address + path
|
|
|
591b7e |
if is_recursive:
|
|
|
591b7e |
url += '/?recursive=True'
|
|
|
591b7e |
- resp = url_helper.readurl(url=url, headers=self.headers)
|
|
|
591b7e |
+ resp = url_helper.readurl(url=url, headers=HEADERS)
|
|
|
591b7e |
except url_helper.UrlError as exc:
|
|
|
591b7e |
msg = "url %s raised exception %s"
|
|
|
591b7e |
LOG.debug(msg, path, exc)
|
|
|
591b7e |
@@ -90,6 +93,10 @@ class DataSourceGCE(sources.DataSource):
|
|
|
591b7e |
public_keys_data = self.metadata['public-keys-data']
|
|
|
591b7e |
return _parse_public_keys(public_keys_data, self.default_user)
|
|
|
591b7e |
|
|
|
591b7e |
+ def publish_host_keys(self, hostkeys):
|
|
|
591b7e |
+ for key in hostkeys:
|
|
|
591b7e |
+ _write_host_key_to_guest_attributes(*key)
|
|
|
591b7e |
+
|
|
|
591b7e |
def get_hostname(self, fqdn=False, resolve_ip=False, metadata_only=False):
|
|
|
591b7e |
# GCE has long FDQN's and has asked for short hostnames.
|
|
|
591b7e |
return self.metadata['local-hostname'].split('.')[0]
|
|
|
591b7e |
@@ -103,6 +110,17 @@ class DataSourceGCE(sources.DataSource):
|
|
|
591b7e |
return self.availability_zone.rsplit('-', 1)[0]
|
|
|
591b7e |
|
|
|
591b7e |
|
|
|
591b7e |
+def _write_host_key_to_guest_attributes(key_type, key_value):
|
|
|
591b7e |
+ url = '%s/%s/%s' % (GUEST_ATTRIBUTES_URL, HOSTKEY_NAMESPACE, key_type)
|
|
|
591b7e |
+ key_value = key_value.encode('utf-8')
|
|
|
591b7e |
+ resp = url_helper.readurl(url=url, data=key_value, headers=HEADERS,
|
|
|
591b7e |
+ request_method='PUT', check_status=False)
|
|
|
591b7e |
+ if resp.ok():
|
|
|
591b7e |
+ LOG.debug('Wrote %s host key to guest attributes.', key_type)
|
|
|
591b7e |
+ else:
|
|
|
591b7e |
+ LOG.debug('Unable to write %s host key to guest attributes.', key_type)
|
|
|
591b7e |
+
|
|
|
591b7e |
+
|
|
|
591b7e |
def _has_expired(public_key):
|
|
|
591b7e |
# Check whether an SSH key is expired. Public key input is a single SSH
|
|
|
591b7e |
# public key in the GCE specific key format documented here:
|
|
|
591b7e |
diff --git a/cloudinit/sources/__init__.py b/cloudinit/sources/__init__.py
|
|
|
591b7e |
index e6966b3..425e703 100644
|
|
|
591b7e |
--- a/cloudinit/sources/__init__.py
|
|
|
591b7e |
+++ b/cloudinit/sources/__init__.py
|
|
|
591b7e |
@@ -474,6 +474,16 @@ class DataSource(object):
|
|
|
591b7e |
def get_public_ssh_keys(self):
|
|
|
591b7e |
return normalize_pubkey_data(self.metadata.get('public-keys'))
|
|
|
591b7e |
|
|
|
591b7e |
+ def publish_host_keys(self, hostkeys):
|
|
|
591b7e |
+ """Publish the public SSH host keys (found in /etc/ssh/*.pub).
|
|
|
591b7e |
+
|
|
|
591b7e |
+ @param hostkeys: List of host key tuples (key_type, key_value),
|
|
|
591b7e |
+ where key_type is the first field in the public key file
|
|
|
591b7e |
+ (e.g. 'ssh-rsa') and key_value is the key itself
|
|
|
591b7e |
+ (e.g. 'AAAAB3NzaC1y...').
|
|
|
591b7e |
+ """
|
|
|
591b7e |
+ pass
|
|
|
591b7e |
+
|
|
|
591b7e |
def _remap_device(self, short_name):
|
|
|
591b7e |
# LP: #611137
|
|
|
591b7e |
# the metadata service may believe that devices are named 'sda'
|
|
|
591b7e |
diff --git a/cloudinit/url_helper.py b/cloudinit/url_helper.py
|
|
|
591b7e |
index 396d69a..1b0721b 100644
|
|
|
591b7e |
--- a/cloudinit/url_helper.py
|
|
|
591b7e |
+++ b/cloudinit/url_helper.py
|
|
|
591b7e |
@@ -199,18 +199,19 @@ def _get_ssl_args(url, ssl_details):
|
|
|
591b7e |
def readurl(url, data=None, timeout=None, retries=0, sec_between=1,
|
|
|
591b7e |
headers=None, headers_cb=None, ssl_details=None,
|
|
|
591b7e |
check_status=True, allow_redirects=True, exception_cb=None,
|
|
|
591b7e |
- session=None, infinite=False, log_req_resp=True):
|
|
|
591b7e |
+ session=None, infinite=False, log_req_resp=True,
|
|
|
591b7e |
+ request_method=None):
|
|
|
591b7e |
url = _cleanurl(url)
|
|
|
591b7e |
req_args = {
|
|
|
591b7e |
'url': url,
|
|
|
591b7e |
}
|
|
|
591b7e |
req_args.update(_get_ssl_args(url, ssl_details))
|
|
|
591b7e |
req_args['allow_redirects'] = allow_redirects
|
|
|
591b7e |
- req_args['method'] = 'GET'
|
|
|
591b7e |
+ if not request_method:
|
|
|
591b7e |
+ request_method = 'POST' if data else 'GET'
|
|
|
591b7e |
+ req_args['method'] = request_method
|
|
|
591b7e |
if timeout is not None:
|
|
|
591b7e |
req_args['timeout'] = max(float(timeout), 0)
|
|
|
591b7e |
- if data:
|
|
|
591b7e |
- req_args['method'] = 'POST'
|
|
|
591b7e |
# It doesn't seem like config
|
|
|
591b7e |
# was added in older library versions (or newer ones either), thus we
|
|
|
591b7e |
# need to manually do the retries if it wasn't...
|
|
|
591b7e |
diff --git a/tests/unittests/test_datasource/test_gce.py b/tests/unittests/test_datasource/test_gce.py
|
|
|
591b7e |
index 41176c6..67744d3 100644
|
|
|
591b7e |
--- a/tests/unittests/test_datasource/test_gce.py
|
|
|
591b7e |
+++ b/tests/unittests/test_datasource/test_gce.py
|
|
|
591b7e |
@@ -55,6 +55,8 @@ GCE_USER_DATA_TEXT = {
|
|
|
591b7e |
HEADERS = {'Metadata-Flavor': 'Google'}
|
|
|
591b7e |
MD_URL_RE = re.compile(
|
|
|
591b7e |
r'http://metadata.google.internal/computeMetadata/v1/.*')
|
|
|
591b7e |
+GUEST_ATTRIBUTES_URL = ('http://metadata.google.internal/computeMetadata/'
|
|
|
591b7e |
+ 'v1/instance/guest-attributes/hostkeys/')
|
|
|
591b7e |
|
|
|
591b7e |
|
|
|
591b7e |
def _set_mock_metadata(gce_meta=None):
|
|
|
591b7e |
@@ -341,4 +343,20 @@ class TestDataSourceGCE(test_helpers.HttprettyTestCase):
|
|
|
591b7e |
public_key_data, default_user='default')
|
|
|
591b7e |
self.assertEqual(sorted(found), sorted(expected))
|
|
|
591b7e |
|
|
|
591b7e |
+ @mock.patch("cloudinit.url_helper.readurl")
|
|
|
591b7e |
+ def test_publish_host_keys(self, m_readurl):
|
|
|
591b7e |
+ hostkeys = [('ssh-rsa', 'asdfasdf'),
|
|
|
591b7e |
+ ('ssh-ed25519', 'qwerqwer')]
|
|
|
591b7e |
+ readurl_expected_calls = [
|
|
|
591b7e |
+ mock.call(check_status=False, data=b'asdfasdf', headers=HEADERS,
|
|
|
591b7e |
+ request_method='PUT',
|
|
|
591b7e |
+ url='%s%s' % (GUEST_ATTRIBUTES_URL, 'ssh-rsa')),
|
|
|
591b7e |
+ mock.call(check_status=False, data=b'qwerqwer', headers=HEADERS,
|
|
|
591b7e |
+ request_method='PUT',
|
|
|
591b7e |
+ url='%s%s' % (GUEST_ATTRIBUTES_URL, 'ssh-ed25519')),
|
|
|
591b7e |
+ ]
|
|
|
591b7e |
+ self.ds.publish_host_keys(hostkeys)
|
|
|
591b7e |
+ m_readurl.assert_has_calls(readurl_expected_calls, any_order=True)
|
|
|
591b7e |
+
|
|
|
591b7e |
+
|
|
|
591b7e |
# vi: ts=4 expandtab
|
|
|
591b7e |
--
|
|
|
591b7e |
1.8.3.1
|
|
|
591b7e |
|