Merge ~smoser/cloud-init:fix/1677205-eol-on-sshd_config into cloud-init:master
- Git
- lp:~smoser/cloud-init
- fix/1677205-eol-on-sshd_config
- Merge into master
Status: | Merged | ||||
---|---|---|---|---|---|
Approved by: | Scott Moser | ||||
Approved revision: | 770101c0b5a9e7455fbd97a04c7164b1b213eb5d | ||||
Merge reported by: | Chad Smith | ||||
Merged at revision: | 4952a8545b61ceb2fe26a933d2f64020d0281703 | ||||
Proposed branch: | ~smoser/cloud-init:fix/1677205-eol-on-sshd_config | ||||
Merge into: | cloud-init:master | ||||
Diff against target: |
440 lines (+273/-70) 4 files modified
cloudinit/config/cc_set_passwords.py (+45/-60) cloudinit/config/tests/test_set_passwords.py (+71/-0) cloudinit/ssh_util.py (+63/-7) tests/unittests/test_sshutil.py (+94/-3) |
||||
Related bugs: |
|
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Server Team CI bot | continuous-integration | Approve | |
Ryan Harper | Approve | ||
Review via email: mp+343246@code.launchpad.net |
Commit message
set_passwords: Add newline to end of sshd config, only restart if updated.
This admittedly does a fairly extensive re-factor to simply add a newline
to the end of sshd_config.
It makes the ssh_config updating portion of set_passwords more testable
and adds tests for that.
The new function is in 'update_
to update a config with multiple changes even though only a single one
is currently used.
We also only restart the ssh daemon now if a change was made to the
config file. Before it was always restarted if the user specified
a value for ssh_pwauth other than 'unchanged'.
Thanks to Lorens Kockum for initial diagnosis and patch.
LP: #1677205
Description of the change
see commit message
Scott Moser (smoser) wrote : | # |
Server Team CI bot (server-team-bot) wrote : | # |
FAILED: Continuous integration, rev:3c15d5bfaa7
https:/
Executed test runs:
SUCCESS: Checkout
FAILED: Unit & Style Tests
Click here to trigger a rebuild:
https:/
Server Team CI bot (server-team-bot) wrote : | # |
PASSED: Continuous integration, rev:6229047dea8
https:/
Executed test runs:
SUCCESS: Checkout
SUCCESS: Unit & Style Tests
SUCCESS: Ubuntu LTS: Build
SUCCESS: Ubuntu LTS: Integration
SUCCESS: MAAS Compatability Testing
IN_PROGRESS: Declarative: Post Actions
Click here to trigger a rebuild:
https:/
Ryan Harper (raharper) : | # |
Scott Moser (smoser) : | # |
Server Team CI bot (server-team-bot) wrote : | # |
PASSED: Continuous integration, rev:f388b171bcb
https:/
Executed test runs:
SUCCESS: Checkout
SUCCESS: Unit & Style Tests
SUCCESS: Ubuntu LTS: Build
SUCCESS: Ubuntu LTS: Integration
SUCCESS: MAAS Compatability Testing
IN_PROGRESS: Declarative: Post Actions
Click here to trigger a rebuild:
https:/
Ryan Harper (raharper) wrote : | # |
This is fine. One comment w.r.t the boolean construction. Take that or ignore.
Scott Moser (smoser) wrote : | # |
Your use of timeit is a bit flawed. You really need to use 'setup'
in almost all cases. otherwise you're testing both the construction
of the list and the evaluation of your condition in each loop. Obviously
if the construction of the list takes 10x the test then your results
will not be meaningful.
So I tried..
>>> timeit.timeit('True if a else False', setup='
0.0481131076812
>>> timeit.
0.0543799400329
>>> timeit.
0.06807184219360352
>>> timeit.
0.11591291427612305
That is mind-boggling.
How can 'True if a else False' *not* call bool(a) in order to evaluate 'if a'?
Even crazier...
Here we define 'mybool' a regular old function and take the function overhead but still do the 'True if a else False'.
*That* is faster than bool(a) when 'a' is an empty list bug does get slower when 'a' is a larger list.
>>> timeit.
0.09648299217224121
>>> timeit.
0.12272787094116211
>>> timeit.
0.09898996353149414
>>> timeit.
0.11946702003479004
Scott Moser (smoser) wrote : | # |
I think the most obvious implementation of "return True if the list has entries and False otherwise" is in order of most to least obvious:
return len(changed) == 0
return bool(changed)
return True if changed else False
The fact that bool(changed) is "so slow" is unfortunate.
I think I'll go with len(changed) == 0
Server Team CI bot (server-team-bot) wrote : | # |
FAILED: Continuous integration, rev:5ff0a5d4479
https:/
Executed test runs:
SUCCESS: Checkout
FAILED: Unit & Style Tests
Click here to trigger a rebuild:
https:/
Server Team CI bot (server-team-bot) wrote : | # |
PASSED: Continuous integration, rev:770101c0b5a
https:/
Executed test runs:
SUCCESS: Checkout
SUCCESS: Unit & Style Tests
SUCCESS: Ubuntu LTS: Build
SUCCESS: Ubuntu LTS: Integration
SUCCESS: MAAS Compatability Testing
IN_PROGRESS: Declarative: Post Actions
Click here to trigger a rebuild:
https:/
Ryan Harper (raharper) wrote : | # |
> Your use of timeit is a bit flawed. You really need to use 'setup'
> in almost all cases. otherwise you're testing both the construction
> of the list and the evaluation of your condition in each loop. Obviously
> if the construction of the list takes 10x the test then your results
> will not be meaningful.
I'm well aware of construction costs, however, given the size of list it won't have
a significant effect on the outcome.
>
> So I tried..
>
> >>> timeit.timeit('True if a else False', setup='
> number=number)
> 0.0481131076812
> >>> timeit.
> 0.0543799400329
> >>> timeit.
> 0.06807184219360352
> >>> timeit.
> 0.11591291427612305
That matches what I found.
>
> That is mind-boggling.
> How can 'True if a else False' *not* call bool(a) in order to evaluate 'if a'?
https:/
>
> Even crazier...
> Here we define 'mybool' a regular old function and take the function overhead
> but still do the 'True if a else False'.
> *That* is faster than bool(a) when 'a' is an empty list bug does get slower
> when 'a' is a larger list.
>
> >>> timeit.
> else False', number=number)
> 0.09648299217224121
> >>> timeit.
> else False', number=number)
> 0.12272787094116211
> >>> timeit.
> return True if b else False', number=number)
> 0.09898996353149414
> >>> timeit.
> return True if b else False', number=number)
> 0.11946702003479004
Indeed; I suspect the cost of allocating memory for bool() where that's not used
in any other case. A boolean is a subclass of integers and requires 24 bytes of memory.
Chad Smith (chad.smith) wrote : | # |
An upstream commit landed for this bug.
To view that commit see the following URL:
https:/
There was an error fetching revisions from git servers. Please try again in a few minutes. If the problem persists, contact Launchpad support.
Preview Diff
1 | diff --git a/cloudinit/config/cc_set_passwords.py b/cloudinit/config/cc_set_passwords.py |
2 | index bb24d57..5ef9737 100755 |
3 | --- a/cloudinit/config/cc_set_passwords.py |
4 | +++ b/cloudinit/config/cc_set_passwords.py |
5 | @@ -68,16 +68,57 @@ import re |
6 | import sys |
7 | |
8 | from cloudinit.distros import ug_util |
9 | -from cloudinit import ssh_util |
10 | +from cloudinit import log as logging |
11 | +from cloudinit.ssh_util import update_ssh_config |
12 | from cloudinit import util |
13 | |
14 | from string import ascii_letters, digits |
15 | |
16 | +LOG = logging.getLogger(__name__) |
17 | + |
18 | # We are removing certain 'painful' letters/numbers |
19 | PW_SET = (''.join([x for x in ascii_letters + digits |
20 | if x not in 'loLOI01'])) |
21 | |
22 | |
23 | +def handle_ssh_pwauth(pw_auth, service_cmd=None, service_name="ssh"): |
24 | + """Apply sshd PasswordAuthentication changes. |
25 | + |
26 | + @param pw_auth: config setting from 'pw_auth'. |
27 | + Best given as True, False, or "unchanged". |
28 | + @param service_cmd: The service command list (['service']) |
29 | + @param service_name: The name of the sshd service for the system. |
30 | + |
31 | + @return: None""" |
32 | + cfg_name = "PasswordAuthentication" |
33 | + if service_cmd is None: |
34 | + service_cmd = ["service"] |
35 | + |
36 | + if util.is_true(pw_auth): |
37 | + cfg_val = 'yes' |
38 | + elif util.is_false(pw_auth): |
39 | + cfg_val = 'no' |
40 | + else: |
41 | + bmsg = "Leaving ssh config '%s' unchanged." % cfg_name |
42 | + if pw_auth is None or pw_auth.lower() == 'unchanged': |
43 | + LOG.debug("%s ssh_pwauth=%s", bmsg, pw_auth) |
44 | + else: |
45 | + LOG.warning("%s Unrecognized value: ssh_pwauth=%s", bmsg, pw_auth) |
46 | + return |
47 | + |
48 | + updated = update_ssh_config({cfg_name: cfg_val}) |
49 | + if not updated: |
50 | + LOG.debug("No need to restart ssh service, %s not updated.", cfg_name) |
51 | + return |
52 | + |
53 | + if 'systemctl' in service_cmd: |
54 | + cmd = list(service_cmd) + ["restart", service_name] |
55 | + else: |
56 | + cmd = list(service_cmd) + [service_name, "restart"] |
57 | + util.subp(cmd) |
58 | + LOG.debug("Restarted the ssh daemon.") |
59 | + |
60 | + |
61 | def handle(_name, cfg, cloud, log, args): |
62 | if len(args) != 0: |
63 | # if run from command line, and give args, wipe the chpasswd['list'] |
64 | @@ -170,65 +211,9 @@ def handle(_name, cfg, cloud, log, args): |
65 | if expired_users: |
66 | log.debug("Expired passwords for: %s users", expired_users) |
67 | |
68 | - change_pwauth = False |
69 | - pw_auth = None |
70 | - if 'ssh_pwauth' in cfg: |
71 | - if util.is_true(cfg['ssh_pwauth']): |
72 | - change_pwauth = True |
73 | - pw_auth = 'yes' |
74 | - elif util.is_false(cfg['ssh_pwauth']): |
75 | - change_pwauth = True |
76 | - pw_auth = 'no' |
77 | - elif str(cfg['ssh_pwauth']).lower() == 'unchanged': |
78 | - log.debug('Leaving auth line unchanged') |
79 | - change_pwauth = False |
80 | - elif not str(cfg['ssh_pwauth']).strip(): |
81 | - log.debug('Leaving auth line unchanged') |
82 | - change_pwauth = False |
83 | - elif not cfg['ssh_pwauth']: |
84 | - log.debug('Leaving auth line unchanged') |
85 | - change_pwauth = False |
86 | - else: |
87 | - msg = 'Unrecognized value %s for ssh_pwauth' % cfg['ssh_pwauth'] |
88 | - util.logexc(log, msg) |
89 | - |
90 | - if change_pwauth: |
91 | - replaced_auth = False |
92 | - |
93 | - # See: man sshd_config |
94 | - old_lines = ssh_util.parse_ssh_config(ssh_util.DEF_SSHD_CFG) |
95 | - new_lines = [] |
96 | - i = 0 |
97 | - for (i, line) in enumerate(old_lines): |
98 | - # Keywords are case-insensitive and arguments are case-sensitive |
99 | - if line.key == 'passwordauthentication': |
100 | - log.debug("Replacing auth line %s with %s", i + 1, pw_auth) |
101 | - replaced_auth = True |
102 | - line.value = pw_auth |
103 | - new_lines.append(line) |
104 | - |
105 | - if not replaced_auth: |
106 | - log.debug("Adding new auth line %s", i + 1) |
107 | - replaced_auth = True |
108 | - new_lines.append(ssh_util.SshdConfigLine('', |
109 | - 'PasswordAuthentication', |
110 | - pw_auth)) |
111 | - |
112 | - lines = [str(l) for l in new_lines] |
113 | - util.write_file(ssh_util.DEF_SSHD_CFG, "\n".join(lines), |
114 | - copy_mode=True) |
115 | - |
116 | - try: |
117 | - cmd = cloud.distro.init_cmd # Default service |
118 | - cmd.append(cloud.distro.get_option('ssh_svcname', 'ssh')) |
119 | - cmd.append('restart') |
120 | - if 'systemctl' in cmd: # Switch action ordering |
121 | - cmd[1], cmd[2] = cmd[2], cmd[1] |
122 | - cmd = filter(None, cmd) # Remove empty arguments |
123 | - util.subp(cmd) |
124 | - log.debug("Restarted the ssh daemon") |
125 | - except Exception: |
126 | - util.logexc(log, "Restarting of the ssh daemon failed") |
127 | + handle_ssh_pwauth( |
128 | + cfg.get('ssh_pwauth'), service_cmd=cloud.distro.init_cmd, |
129 | + service_name=cloud.distro.get_option('ssh_svcname', 'ssh')) |
130 | |
131 | if len(errors): |
132 | log.debug("%s errors occured, re-raising the last one", len(errors)) |
133 | diff --git a/cloudinit/config/tests/test_set_passwords.py b/cloudinit/config/tests/test_set_passwords.py |
134 | new file mode 100644 |
135 | index 0000000..b051ec8 |
136 | --- /dev/null |
137 | +++ b/cloudinit/config/tests/test_set_passwords.py |
138 | @@ -0,0 +1,71 @@ |
139 | +# This file is part of cloud-init. See LICENSE file for license information. |
140 | + |
141 | +import mock |
142 | + |
143 | +from cloudinit.config import cc_set_passwords as setpass |
144 | +from cloudinit.tests.helpers import CiTestCase |
145 | +from cloudinit import util |
146 | + |
147 | +MODPATH = "cloudinit.config.cc_set_passwords." |
148 | + |
149 | + |
150 | +class TestHandleSshPwauth(CiTestCase): |
151 | + """Test cc_set_passwords handling of ssh_pwauth in handle_ssh_pwauth.""" |
152 | + |
153 | + with_logs = True |
154 | + |
155 | + @mock.patch(MODPATH + "util.subp") |
156 | + def test_unknown_value_logs_warning(self, m_subp): |
157 | + setpass.handle_ssh_pwauth("floo") |
158 | + self.assertIn("Unrecognized value: ssh_pwauth=floo", |
159 | + self.logs.getvalue()) |
160 | + m_subp.assert_not_called() |
161 | + |
162 | + @mock.patch(MODPATH + "update_ssh_config", return_value=True) |
163 | + @mock.patch(MODPATH + "util.subp") |
164 | + def test_systemctl_as_service_cmd(self, m_subp, m_update_ssh_config): |
165 | + """If systemctl in service cmd: systemctl restart name.""" |
166 | + setpass.handle_ssh_pwauth( |
167 | + True, service_cmd=["systemctl"], service_name="myssh") |
168 | + self.assertEqual(mock.call(["systemctl", "restart", "myssh"]), |
169 | + m_subp.call_args) |
170 | + |
171 | + @mock.patch(MODPATH + "update_ssh_config", return_value=True) |
172 | + @mock.patch(MODPATH + "util.subp") |
173 | + def test_service_as_service_cmd(self, m_subp, m_update_ssh_config): |
174 | + """If systemctl in service cmd: systemctl restart name.""" |
175 | + setpass.handle_ssh_pwauth( |
176 | + True, service_cmd=["service"], service_name="myssh") |
177 | + self.assertEqual(mock.call(["service", "myssh", "restart"]), |
178 | + m_subp.call_args) |
179 | + |
180 | + @mock.patch(MODPATH + "update_ssh_config", return_value=False) |
181 | + @mock.patch(MODPATH + "util.subp") |
182 | + def test_not_restarted_if_not_updated(self, m_subp, m_update_ssh_config): |
183 | + """If config is not updated, then no system restart should be done.""" |
184 | + setpass.handle_ssh_pwauth(True) |
185 | + m_subp.assert_not_called() |
186 | + self.assertIn("No need to restart ssh", self.logs.getvalue()) |
187 | + |
188 | + @mock.patch(MODPATH + "update_ssh_config", return_value=True) |
189 | + @mock.patch(MODPATH + "util.subp") |
190 | + def test_unchanged_does_nothing(self, m_subp, m_update_ssh_config): |
191 | + """If 'unchanged', then no updates to config and no restart.""" |
192 | + setpass.handle_ssh_pwauth( |
193 | + "unchanged", service_cmd=["systemctl"], service_name="myssh") |
194 | + m_update_ssh_config.assert_not_called() |
195 | + m_subp.assert_not_called() |
196 | + |
197 | + @mock.patch(MODPATH + "util.subp") |
198 | + def test_valid_change_values(self, m_subp): |
199 | + """If value is a valid changen value, then update should be called.""" |
200 | + upname = MODPATH + "update_ssh_config" |
201 | + optname = "PasswordAuthentication" |
202 | + for value in util.FALSE_STRINGS + util.TRUE_STRINGS: |
203 | + optval = "yes" if value in util.TRUE_STRINGS else "no" |
204 | + with mock.patch(upname, return_value=False) as m_update: |
205 | + setpass.handle_ssh_pwauth(value) |
206 | + m_update.assert_called_with({optname: optval}) |
207 | + m_subp.assert_not_called() |
208 | + |
209 | +# vi: ts=4 expandtab |
210 | diff --git a/cloudinit/ssh_util.py b/cloudinit/ssh_util.py |
211 | index 882517f..73c3177 100644 |
212 | --- a/cloudinit/ssh_util.py |
213 | +++ b/cloudinit/ssh_util.py |
214 | @@ -279,24 +279,28 @@ class SshdConfigLine(object): |
215 | |
216 | |
217 | def parse_ssh_config(fname): |
218 | + if not os.path.isfile(fname): |
219 | + return [] |
220 | + return parse_ssh_config_lines(util.load_file(fname).splitlines()) |
221 | + |
222 | + |
223 | +def parse_ssh_config_lines(lines): |
224 | # See: man sshd_config |
225 | # The file contains keyword-argument pairs, one per line. |
226 | # Lines starting with '#' and empty lines are interpreted as comments. |
227 | # Note: key-words are case-insensitive and arguments are case-sensitive |
228 | - lines = [] |
229 | - if not os.path.isfile(fname): |
230 | - return lines |
231 | - for line in util.load_file(fname).splitlines(): |
232 | + ret = [] |
233 | + for line in lines: |
234 | line = line.strip() |
235 | if not line or line.startswith("#"): |
236 | - lines.append(SshdConfigLine(line)) |
237 | + ret.append(SshdConfigLine(line)) |
238 | continue |
239 | try: |
240 | key, val = line.split(None, 1) |
241 | except ValueError: |
242 | key, val = line.split('=', 1) |
243 | - lines.append(SshdConfigLine(line, key, val)) |
244 | - return lines |
245 | + ret.append(SshdConfigLine(line, key, val)) |
246 | + return ret |
247 | |
248 | |
249 | def parse_ssh_config_map(fname): |
250 | @@ -310,4 +314,56 @@ def parse_ssh_config_map(fname): |
251 | ret[line.key] = line.value |
252 | return ret |
253 | |
254 | + |
255 | +def update_ssh_config(updates, fname=DEF_SSHD_CFG): |
256 | + """Read fname, and update if changes are necessary. |
257 | + |
258 | + @param updates: dictionary of desired values {Option: value} |
259 | + @return: boolean indicating if an update was done.""" |
260 | + lines = parse_ssh_config(fname) |
261 | + changed = update_ssh_config_lines(lines=lines, updates=updates) |
262 | + if changed: |
263 | + util.write_file( |
264 | + fname, "\n".join([str(l) for l in lines]) + "\n", copy_mode=True) |
265 | + return len(changed) != 0 |
266 | + |
267 | + |
268 | +def update_ssh_config_lines(lines, updates): |
269 | + """Update the ssh config lines per updates. |
270 | + |
271 | + @param lines: array of SshdConfigLine. This array is updated in place. |
272 | + @param updates: dictionary of desired values {Option: value} |
273 | + @return: A list of keys in updates that were changed.""" |
274 | + found = set() |
275 | + changed = [] |
276 | + |
277 | + # Keywords are case-insensitive and arguments are case-sensitive |
278 | + casemap = dict([(k.lower(), k) for k in updates.keys()]) |
279 | + |
280 | + for (i, line) in enumerate(lines, start=1): |
281 | + if not line.key: |
282 | + continue |
283 | + if line.key in casemap: |
284 | + key = casemap[line.key] |
285 | + value = updates[key] |
286 | + found.add(key) |
287 | + if line.value == value: |
288 | + LOG.debug("line %d: option %s already set to %s", |
289 | + i, key, value) |
290 | + else: |
291 | + changed.append(key) |
292 | + LOG.debug("line %d: option %s updated %s -> %s", i, |
293 | + key, line.value, value) |
294 | + line.value = value |
295 | + |
296 | + if len(found) != len(updates): |
297 | + for key, value in updates.items(): |
298 | + if key in found: |
299 | + continue |
300 | + changed.append(key) |
301 | + lines.append(SshdConfigLine('', key, value)) |
302 | + LOG.debug("line %d: option %s added with %s", |
303 | + len(lines), key, value) |
304 | + return changed |
305 | + |
306 | # vi: ts=4 expandtab |
307 | diff --git a/tests/unittests/test_sshutil.py b/tests/unittests/test_sshutil.py |
308 | index 4c62c8b..73ae897 100644 |
309 | --- a/tests/unittests/test_sshutil.py |
310 | +++ b/tests/unittests/test_sshutil.py |
311 | @@ -4,6 +4,7 @@ from mock import patch |
312 | |
313 | from cloudinit import ssh_util |
314 | from cloudinit.tests import helpers as test_helpers |
315 | +from cloudinit import util |
316 | |
317 | |
318 | VALID_CONTENT = { |
319 | @@ -56,7 +57,7 @@ TEST_OPTIONS = ( |
320 | 'user \"root\".\';echo;sleep 10"') |
321 | |
322 | |
323 | -class TestAuthKeyLineParser(test_helpers.TestCase): |
324 | +class TestAuthKeyLineParser(test_helpers.CiTestCase): |
325 | |
326 | def test_simple_parse(self): |
327 | # test key line with common 3 fields (keytype, base64, comment) |
328 | @@ -126,7 +127,7 @@ class TestAuthKeyLineParser(test_helpers.TestCase): |
329 | self.assertFalse(key.valid()) |
330 | |
331 | |
332 | -class TestUpdateAuthorizedKeys(test_helpers.TestCase): |
333 | +class TestUpdateAuthorizedKeys(test_helpers.CiTestCase): |
334 | |
335 | def test_new_keys_replace(self): |
336 | """new entries with the same base64 should replace old.""" |
337 | @@ -168,7 +169,7 @@ class TestUpdateAuthorizedKeys(test_helpers.TestCase): |
338 | self.assertEqual(expected, found) |
339 | |
340 | |
341 | -class TestParseSSHConfig(test_helpers.TestCase): |
342 | +class TestParseSSHConfig(test_helpers.CiTestCase): |
343 | |
344 | def setUp(self): |
345 | self.load_file_patch = patch('cloudinit.ssh_util.util.load_file') |
346 | @@ -235,4 +236,94 @@ class TestParseSSHConfig(test_helpers.TestCase): |
347 | self.assertEqual('foo', ret[0].key) |
348 | self.assertEqual('bar', ret[0].value) |
349 | |
350 | + |
351 | +class TestUpdateSshConfigLines(test_helpers.CiTestCase): |
352 | + """Test the update_ssh_config_lines method.""" |
353 | + exlines = [ |
354 | + "#PasswordAuthentication yes", |
355 | + "UsePAM yes", |
356 | + "# Comment line", |
357 | + "AcceptEnv LANG LC_*", |
358 | + "X11Forwarding no", |
359 | + ] |
360 | + pwauth = "PasswordAuthentication" |
361 | + |
362 | + def check_line(self, line, opt, val): |
363 | + self.assertEqual(line.key, opt.lower()) |
364 | + self.assertEqual(line.value, val) |
365 | + self.assertIn(opt, str(line)) |
366 | + self.assertIn(val, str(line)) |
367 | + |
368 | + def test_new_option_added(self): |
369 | + """A single update of non-existing option.""" |
370 | + lines = ssh_util.parse_ssh_config_lines(list(self.exlines)) |
371 | + result = ssh_util.update_ssh_config_lines(lines, {'MyKey': 'MyVal'}) |
372 | + self.assertEqual(['MyKey'], result) |
373 | + self.check_line(lines[-1], "MyKey", "MyVal") |
374 | + |
375 | + def test_commented_out_not_updated_but_appended(self): |
376 | + """Implementation does not un-comment and update lines.""" |
377 | + lines = ssh_util.parse_ssh_config_lines(list(self.exlines)) |
378 | + result = ssh_util.update_ssh_config_lines(lines, {self.pwauth: "no"}) |
379 | + self.assertEqual([self.pwauth], result) |
380 | + self.check_line(lines[-1], self.pwauth, "no") |
381 | + |
382 | + def test_single_option_updated(self): |
383 | + """A single update should have change made and line updated.""" |
384 | + opt, val = ("UsePAM", "no") |
385 | + lines = ssh_util.parse_ssh_config_lines(list(self.exlines)) |
386 | + result = ssh_util.update_ssh_config_lines(lines, {opt: val}) |
387 | + self.assertEqual([opt], result) |
388 | + self.check_line(lines[1], opt, val) |
389 | + |
390 | + def test_multiple_updates_with_add(self): |
391 | + """Verify multiple updates some added some changed, some not.""" |
392 | + updates = {"UsePAM": "no", "X11Forwarding": "no", "NewOpt": "newval", |
393 | + "AcceptEnv": "LANG ADD LC_*"} |
394 | + lines = ssh_util.parse_ssh_config_lines(list(self.exlines)) |
395 | + result = ssh_util.update_ssh_config_lines(lines, updates) |
396 | + self.assertEqual(set(["UsePAM", "NewOpt", "AcceptEnv"]), set(result)) |
397 | + self.check_line(lines[3], "AcceptEnv", updates["AcceptEnv"]) |
398 | + |
399 | + def test_return_empty_if_no_changes(self): |
400 | + """If there are no changes, then return should be empty list.""" |
401 | + updates = {"UsePAM": "yes"} |
402 | + lines = ssh_util.parse_ssh_config_lines(list(self.exlines)) |
403 | + result = ssh_util.update_ssh_config_lines(lines, updates) |
404 | + self.assertEqual([], result) |
405 | + self.assertEqual(self.exlines, [str(l) for l in lines]) |
406 | + |
407 | + def test_keycase_not_modified(self): |
408 | + """Original case of key should not be changed on update. |
409 | + This behavior is to keep original config as much intact as can be.""" |
410 | + updates = {"usepam": "no"} |
411 | + lines = ssh_util.parse_ssh_config_lines(list(self.exlines)) |
412 | + result = ssh_util.update_ssh_config_lines(lines, updates) |
413 | + self.assertEqual(["usepam"], result) |
414 | + self.assertEqual("UsePAM no", str(lines[1])) |
415 | + |
416 | + |
417 | +class TestUpdateSshConfig(test_helpers.CiTestCase): |
418 | + cfgdata = '\n'.join(["#Option val", "MyKey ORIG_VAL", ""]) |
419 | + |
420 | + def test_modified(self): |
421 | + mycfg = self.tmp_path("ssh_config_1") |
422 | + util.write_file(mycfg, self.cfgdata) |
423 | + ret = ssh_util.update_ssh_config({"MyKey": "NEW_VAL"}, mycfg) |
424 | + self.assertTrue(ret) |
425 | + found = util.load_file(mycfg) |
426 | + self.assertEqual(self.cfgdata.replace("ORIG_VAL", "NEW_VAL"), found) |
427 | + # assert there is a newline at end of file (LP: #1677205) |
428 | + self.assertEqual('\n', found[-1]) |
429 | + |
430 | + def test_not_modified(self): |
431 | + mycfg = self.tmp_path("ssh_config_2") |
432 | + util.write_file(mycfg, self.cfgdata) |
433 | + with patch("cloudinit.ssh_util.util.write_file") as m_write_file: |
434 | + ret = ssh_util.update_ssh_config({"MyKey": "ORIG_VAL"}, mycfg) |
435 | + self.assertFalse(ret) |
436 | + self.assertEqual(self.cfgdata, util.load_file(mycfg)) |
437 | + m_write_file.assert_not_called() |
438 | + |
439 | + |
440 | # vi: ts=4 expandtab |
Increases test coverage:
* ssh_util from 60 -> 70%
* cc_set_passwords: 6% -> 27%