Merge ~fginther/cloud-init:feature/test-cc-ssh into cloud-init:master

Proposed by Francis Ginther
Status: Merged
Approved by: Chad Smith
Approved revision: c588f406a1d3a844b19634e3ace2cbd2a91a23b1
Merge reported by: Server Team CI bot
Merged at revision: not available
Proposed branch: ~fginther/cloud-init:feature/test-cc-ssh
Merge into: cloud-init:master
Diff against target: 197 lines (+173/-0)
2 files modified
cloudinit/config/tests/test_ssh.py (+147/-0)
cloudinit/tests/helpers.py (+26/-0)
Reviewer Review Type Date Requested Status
Server Team CI bot continuous-integration Approve
Chad Smith Approve
Review via email: mp+353816@code.launchpad.net

Commit message

Add unit tests for config/cc_ssh.py

These tests focus on the apply_credentials method and the ssh setup for
root and a distro default user.

Description of the change

This is based on https://code.launchpad.net/~fginther/cloud-init/+git/cloud-init/+merge/352053 with feedback from chad.smith incorporated.

This has successfully built in a PPA for bionic: https://launchpad.net/~fginther/+archive/ubuntu/cloud-init-2/+build/15307849

To post a comment you must log in.
Revision history for this message
Server Team CI bot (server-team-bot) wrote :

PASSED: Continuous integration, rev:ce95999dc667691df3d4d82321be57fe8a658799
https://jenkins.ubuntu.com/server/job/cloud-init-ci/263/
Executed test runs:
    SUCCESS: Checkout
    SUCCESS: Unit & Style Tests
    SUCCESS: Ubuntu LTS: Build
    SUCCESS: Ubuntu LTS: Integration
    IN_PROGRESS: Declarative: Post Actions

Click here to trigger a rebuild:
https://jenkins.ubuntu.com/server/job/cloud-init-ci/263/rebuild

review: Approve (continuous-integration)
Revision history for this message
Chad Smith (chad.smith) wrote :

Thanks for these Francis. Here's a consolidated patch that adds a CiTestCase.tmp_cloud method to setup a temporary pathed Cloud/distro object so we don't have to magicMock them. Also I added some mocks of glob.glob for any handler calls because they were leaking into the underlying filesystem and attempting to remove the ssh keys.

http://paste.ubuntu.com/p/yFt3MbHV7W/

I bet we can work out the sys_cfg params to tmp_cloud to avoid some of the normalize_users_groups mocking too, but I've got to bail for today on this and think it's good enough as is.

review: Approve
c588f40... by Francis Ginther

Improve unittest mocking with a tmp_cloud helper

Instead of mocking out the same cloud object and methods several times,
create a helper to return a vanilla cloud object (thanks Chad Smith).

Revision history for this message
Server Team CI bot (server-team-bot) wrote :

PASSED: Continuous integration, rev:c588f406a1d3a844b19634e3ace2cbd2a91a23b1
https://jenkins.ubuntu.com/server/job/cloud-init-ci/282/
Executed test runs:
    SUCCESS: Checkout
    SUCCESS: Unit & Style Tests
    SUCCESS: Ubuntu LTS: Build
    SUCCESS: Ubuntu LTS: Integration
    IN_PROGRESS: Declarative: Post Actions

Click here to trigger a rebuild:
https://jenkins.ubuntu.com/server/job/cloud-init-ci/282/rebuild

review: Approve (continuous-integration)
Revision history for this message
Francis Ginther (fginther) wrote :

Thanks Chad, I've updated as requested and tested again via a ppa build:

https://launchpad.net/~fginther/+archive/ubuntu/cloud-init-2/+build/15319263

Please let me know if any further updates are suggested.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1diff --git a/cloudinit/config/tests/test_ssh.py b/cloudinit/config/tests/test_ssh.py
2new file mode 100644
3index 0000000..7441d9e
4--- /dev/null
5+++ b/cloudinit/config/tests/test_ssh.py
6@@ -0,0 +1,147 @@
7+# This file is part of cloud-init. See LICENSE file for license information.
8+
9+
10+from cloudinit.config import cc_ssh
11+from cloudinit.tests.helpers import CiTestCase, mock
12+
13+MODPATH = "cloudinit.config.cc_ssh."
14+
15+
16+@mock.patch(MODPATH + "ssh_util.setup_user_keys")
17+class TestHandleSsh(CiTestCase):
18+ """Test cc_ssh handling of ssh config."""
19+
20+ def test_apply_credentials_with_user(self, m_setup_keys):
21+ """Apply keys for the given user and root."""
22+ keys = ["key1"]
23+ user = "clouduser"
24+ options = cc_ssh.DISABLE_ROOT_OPTS
25+ cc_ssh.apply_credentials(keys, user, False, options)
26+ self.assertEqual([mock.call(set(keys), user),
27+ mock.call(set(keys), "root", options="")],
28+ m_setup_keys.call_args_list)
29+
30+ def test_apply_credentials_with_no_user(self, m_setup_keys):
31+ """Apply keys for root only."""
32+ keys = ["key1"]
33+ user = None
34+ options = cc_ssh.DISABLE_ROOT_OPTS
35+ cc_ssh.apply_credentials(keys, user, False, options)
36+ self.assertEqual([mock.call(set(keys), "root", options="")],
37+ m_setup_keys.call_args_list)
38+
39+ def test_apply_credentials_with_user_disable_root(self, m_setup_keys):
40+ """Apply keys for the given user and disable root ssh."""
41+ keys = ["key1"]
42+ user = "clouduser"
43+ options = cc_ssh.DISABLE_ROOT_OPTS
44+ cc_ssh.apply_credentials(keys, user, True, options)
45+ options = options.replace("$USER", user)
46+ self.assertEqual([mock.call(set(keys), user),
47+ mock.call(set(keys), "root", options=options)],
48+ m_setup_keys.call_args_list)
49+
50+ def test_apply_credentials_with_no_user_disable_root(self, m_setup_keys):
51+ """Apply keys no user and disable root ssh."""
52+ keys = ["key1"]
53+ user = None
54+ options = cc_ssh.DISABLE_ROOT_OPTS
55+ cc_ssh.apply_credentials(keys, user, True, options)
56+ options = options.replace("$USER", "NONE")
57+ self.assertEqual([mock.call(set(keys), "root", options=options)],
58+ m_setup_keys.call_args_list)
59+
60+ @mock.patch(MODPATH + "glob.glob")
61+ @mock.patch(MODPATH + "ug_util.normalize_users_groups")
62+ @mock.patch(MODPATH + "os.path.exists")
63+ def test_handle_no_cfg(self, m_path_exists, m_nug,
64+ m_glob, m_setup_keys):
65+ """Test handle with no config ignores generating existing keyfiles."""
66+ cfg = {}
67+ keys = ["key1"]
68+ m_glob.return_value = [] # Return no matching keys to prevent removal
69+ # Mock os.path.exits to True to short-circuit the key writing logic
70+ m_path_exists.return_value = True
71+ m_nug.return_value = ([], {})
72+ cloud = self.tmp_cloud(
73+ distro='ubuntu', metadata={'public-keys': keys})
74+ cc_ssh.handle("name", cfg, cloud, None, None)
75+ options = cc_ssh.DISABLE_ROOT_OPTS.replace("$USER", "NONE")
76+ m_glob.assert_called_once_with('/etc/ssh/ssh_host_*key*')
77+ self.assertIn(
78+ [mock.call('/etc/ssh/ssh_host_rsa_key'),
79+ mock.call('/etc/ssh/ssh_host_dsa_key'),
80+ mock.call('/etc/ssh/ssh_host_ecdsa_key'),
81+ mock.call('/etc/ssh/ssh_host_ed25519_key')],
82+ m_path_exists.call_args_list)
83+ self.assertEqual([mock.call(set(keys), "root", options=options)],
84+ m_setup_keys.call_args_list)
85+
86+ @mock.patch(MODPATH + "glob.glob")
87+ @mock.patch(MODPATH + "ug_util.normalize_users_groups")
88+ @mock.patch(MODPATH + "os.path.exists")
89+ def test_handle_no_cfg_and_default_root(self, m_path_exists, m_nug,
90+ m_glob, m_setup_keys):
91+ """Test handle with no config and a default distro user."""
92+ cfg = {}
93+ keys = ["key1"]
94+ user = "clouduser"
95+ m_glob.return_value = [] # Return no matching keys to prevent removal
96+ # Mock os.path.exits to True to short-circuit the key writing logic
97+ m_path_exists.return_value = True
98+ m_nug.return_value = ({user: {"default": user}}, {})
99+ cloud = self.tmp_cloud(
100+ distro='ubuntu', metadata={'public-keys': keys})
101+ cc_ssh.handle("name", cfg, cloud, None, None)
102+
103+ options = cc_ssh.DISABLE_ROOT_OPTS.replace("$USER", user)
104+ self.assertEqual([mock.call(set(keys), user),
105+ mock.call(set(keys), "root", options=options)],
106+ m_setup_keys.call_args_list)
107+
108+ @mock.patch(MODPATH + "glob.glob")
109+ @mock.patch(MODPATH + "ug_util.normalize_users_groups")
110+ @mock.patch(MODPATH + "os.path.exists")
111+ def test_handle_cfg_with_explicit_disable_root(self, m_path_exists, m_nug,
112+ m_glob, m_setup_keys):
113+ """Test handle with explicit disable_root and a default distro user."""
114+ # This test is identical to test_handle_no_cfg_and_default_root,
115+ # except this uses an explicit cfg value
116+ cfg = {"disable_root": True}
117+ keys = ["key1"]
118+ user = "clouduser"
119+ m_glob.return_value = [] # Return no matching keys to prevent removal
120+ # Mock os.path.exits to True to short-circuit the key writing logic
121+ m_path_exists.return_value = True
122+ m_nug.return_value = ({user: {"default": user}}, {})
123+ cloud = self.tmp_cloud(
124+ distro='ubuntu', metadata={'public-keys': keys})
125+ cc_ssh.handle("name", cfg, cloud, None, None)
126+
127+ options = cc_ssh.DISABLE_ROOT_OPTS.replace("$USER", user)
128+ self.assertEqual([mock.call(set(keys), user),
129+ mock.call(set(keys), "root", options=options)],
130+ m_setup_keys.call_args_list)
131+
132+ @mock.patch(MODPATH + "glob.glob")
133+ @mock.patch(MODPATH + "ug_util.normalize_users_groups")
134+ @mock.patch(MODPATH + "os.path.exists")
135+ def test_handle_cfg_without_disable_root(self, m_path_exists, m_nug,
136+ m_glob, m_setup_keys):
137+ """Test handle with disable_root == False."""
138+ # When disable_root == False, the ssh redirect for root is skipped
139+ cfg = {"disable_root": False}
140+ keys = ["key1"]
141+ user = "clouduser"
142+ m_glob.return_value = [] # Return no matching keys to prevent removal
143+ # Mock os.path.exits to True to short-circuit the key writing logic
144+ m_path_exists.return_value = True
145+ m_nug.return_value = ({user: {"default": user}}, {})
146+ cloud = self.tmp_cloud(
147+ distro='ubuntu', metadata={'public-keys': keys})
148+ cloud.get_public_ssh_keys = mock.Mock(return_value=keys)
149+ cc_ssh.handle("name", cfg, cloud, None, None)
150+
151+ self.assertEqual([mock.call(set(keys), user),
152+ mock.call(set(keys), "root", options="")],
153+ m_setup_keys.call_args_list)
154diff --git a/cloudinit/tests/helpers.py b/cloudinit/tests/helpers.py
155index 5bfe7fa..904864d 100644
156--- a/cloudinit/tests/helpers.py
157+++ b/cloudinit/tests/helpers.py
158@@ -28,7 +28,10 @@ except ImportError:
159
160 from cloudinit.config.schema import (
161 SchemaValidationError, validate_cloudconfig_schema)
162+from cloudinit import cloud
163+from cloudinit import distros
164 from cloudinit import helpers as ch
165+from cloudinit.sources import DataSourceNone
166 from cloudinit import util
167
168 # Used for skipping tests
169@@ -187,6 +190,29 @@ class CiTestCase(TestCase):
170 """
171 raise SystemExit(code)
172
173+ def tmp_cloud(self, distro, sys_cfg=None, metadata=None):
174+ """Create a cloud with tmp working directory paths.
175+
176+ @param distro: Name of the distro to attach to the cloud.
177+ @param metadata: Optional metadata to set on the datasource.
178+
179+ @return: The built cloud instance.
180+ """
181+ self.new_root = self.tmp_dir()
182+ if not sys_cfg:
183+ sys_cfg = {}
184+ tmp_paths = {}
185+ for var in ['templates_dir', 'run_dir', 'cloud_dir']:
186+ tmp_paths[var] = self.tmp_path(var, dir=self.new_root)
187+ util.ensure_dir(tmp_paths[var])
188+ self.paths = ch.Paths(tmp_paths)
189+ cls = distros.fetch(distro)
190+ mydist = cls(distro, sys_cfg, self.paths)
191+ myds = DataSourceNone.DataSourceNone(sys_cfg, mydist, self.paths)
192+ if metadata:
193+ myds.metadata.update(metadata)
194+ return cloud.Cloud(myds, self.paths, sys_cfg, mydist, None)
195+
196
197 class ResourceUsingTestCase(CiTestCase):
198

Subscribers

People subscribed via source and target branches