Merge ~raharper/cloud-init:ubuntu/devel/newupstream-20180426 into cloud-init:ubuntu/devel
- Git
- lp:~raharper/cloud-init
- ubuntu/devel/newupstream-20180426
- Merge into ubuntu/devel
Status: | Merged | ||||||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
Merged at revision: | 195d6130fd8a39efd08214edfae5eceb050e432d | ||||||||||||||||||||
Proposed branch: | ~raharper/cloud-init:ubuntu/devel/newupstream-20180426 | ||||||||||||||||||||
Merge into: | cloud-init:ubuntu/devel | ||||||||||||||||||||
Diff against target: |
2991 lines (+1186/-256) 75 files modified
.pylintrc (+1/-1) cloudinit/analyze/dump.py (+1/-1) cloudinit/cmd/tests/test_main.py (+3/-3) cloudinit/config/cc_apt_configure.py (+1/-1) cloudinit/config/cc_bootcmd.py (+0/-1) cloudinit/config/cc_disk_setup.py (+4/-8) cloudinit/config/cc_emit_upstart.py (+1/-1) cloudinit/config/cc_resizefs.py (+3/-5) cloudinit/config/cc_rh_subscription.py (+8/-10) cloudinit/config/cc_runcmd.py (+0/-1) cloudinit/config/cc_set_passwords.py (+45/-60) cloudinit/config/cc_snap.py (+2/-3) cloudinit/config/cc_snappy.py (+2/-2) cloudinit/config/cc_ubuntu_advantage.py (+2/-3) cloudinit/config/schema.py (+2/-2) cloudinit/config/tests/test_set_passwords.py (+71/-0) cloudinit/config/tests/test_snap.py (+27/-2) cloudinit/config/tests/test_ubuntu_advantage.py (+28/-2) cloudinit/distros/freebsd.py (+1/-1) cloudinit/distros/ubuntu.py (+1/-1) cloudinit/net/__init__.py (+27/-1) cloudinit/net/cmdline.py (+1/-1) cloudinit/net/dhcp.py (+1/-1) cloudinit/net/sysconfig.py (+1/-2) cloudinit/net/tests/test_init.py (+1/-0) cloudinit/reporting/events.py (+1/-1) cloudinit/sources/DataSourceAliYun.py (+1/-1) cloudinit/sources/DataSourceAltCloud.py (+1/-4) cloudinit/sources/DataSourceAzure.py (+13/-20) cloudinit/sources/DataSourceIBMCloud.py (+92/-14) cloudinit/sources/DataSourceMAAS.py (+1/-1) cloudinit/sources/DataSourceOVF.py (+1/-1) cloudinit/sources/DataSourceOpenStack.py (+2/-2) cloudinit/sources/DataSourceSmartOS.py (+13/-5) cloudinit/sources/helpers/digitalocean.py (+3/-4) cloudinit/sources/helpers/openstack.py (+1/-1) cloudinit/sources/helpers/vmware/imc/config_nic.py (+1/-1) cloudinit/sources/helpers/vmware/imc/config_passwd.py (+2/-2) cloudinit/sources/helpers/vmware/imc/guestcust_util.py (+2/-2) cloudinit/sources/tests/test_init.py (+1/-1) cloudinit/ssh_util.py (+63/-7) cloudinit/templater.py (+1/-1) cloudinit/tests/helpers.py (+32/-2) cloudinit/tests/test_util.py (+50/-1) cloudinit/url_helper.py (+1/-1) cloudinit/util.py (+16/-1) debian/changelog (+25/-0) doc/rtd/topics/datasources.rst (+1/-0) doc/rtd/topics/datasources/aliyun.rst (+74/-0) packages/debian/control.in (+1/-0) tests/cloud_tests/bddeb.py (+1/-1) tests/cloud_tests/collect.py (+2/-1) tests/cloud_tests/platforms/instances.py (+1/-1) tests/cloud_tests/platforms/lxd/instance.py (+4/-6) tests/cloud_tests/setup_image.py (+5/-6) tests/cloud_tests/testcases/base.py (+1/-1) tests/cloud_tests/testcases/examples/including_user_groups.py (+1/-1) tests/cloud_tests/testcases/modules/user_groups.py (+1/-1) tests/cloud_tests/util.py (+1/-1) tests/unittests/test__init__.py (+1/-1) tests/unittests/test_datasource/test_azure.py (+2/-2) tests/unittests/test_datasource/test_ibmcloud.py (+50/-0) tests/unittests/test_datasource/test_maas.py (+2/-2) tests/unittests/test_datasource/test_nocloud.py (+0/-3) tests/unittests/test_datasource/test_smartos.py (+117/-4) tests/unittests/test_ds_identify.py (+64/-8) tests/unittests/test_handler/test_handler_apt_source_v3.py (+1/-1) tests/unittests/test_handler/test_handler_bootcmd.py (+26/-8) tests/unittests/test_handler/test_handler_ntp.py (+1/-1) tests/unittests/test_handler/test_handler_runcmd.py (+26/-7) tests/unittests/test_net.py (+127/-5) tests/unittests/test_sshutil.py (+94/-3) tests/unittests/test_templating.py (+2/-2) tests/unittests/test_util.py (+3/-3) tools/ds-identify (+20/-1) |
||||||||||||||||||||
Related bugs: |
|
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Server Team CI bot | continuous-integration | Approve | |
cloud-init Commiters | Pending | ||
Review via email:
|
Commit message
cloud-init (18.2-27-
* New upstream snapshot.
- IBMCloud: recognize provisioning environment during debug boots.
(LP: #1767166)
- net: detect unstable network names and trigger a settle if needed
(LP: #1766287)
- IBMCloud: improve documentation in datasource.
- sysconfig: dhcp6 subnet type should not imply dhcpv4 [Vitaly Kuznetsov]
- packages/
(LP: #1766711)
- DataSourceSmartOS: add locking of serial device.
[Mike Gerdts] (LP: #1746605)
- DataSourceSmartOS: sdc:hostname is ignored [Mike Gerdts] (LP: #1765085)
- DataSourceSmartOS: list() should always return a list
[Mike Gerdts] (LP: #1763480)
- schema: in validation, raise ImportError if strict but no jsonschema.
- set_passwords: Add newline to end of sshd config, only restart if
updated. (LP: #1677205)
- pylint: pay attention to unused variable warnings.
- doc: Add documentation for AliYun datasource. [Junjie Wang]
- Schema: do not warn on duplicate items in commands. (LP: #1764264)
-- Ryan Harper <email address hidden> Thu, 26 Apr 2018 16:33:59 -0500
Description of the change

Server Team CI bot (server-team-bot) wrote : | # |
Preview Diff
1 | diff --git a/.pylintrc b/.pylintrc |
2 | index 0bdfa59..3bfa0c8 100644 |
3 | --- a/.pylintrc |
4 | +++ b/.pylintrc |
5 | @@ -28,7 +28,7 @@ jobs=4 |
6 | # W0703(broad-except) |
7 | # W1401(anomalous-backslash-in-string) |
8 | |
9 | -disable=C, F, I, R, W0105, W0107, W0201, W0212, W0221, W0222, W0223, W0231, W0311, W0511, W0602, W0603, W0611, W0612, W0613, W0621, W0622, W0631, W0703, W1401 |
10 | +disable=C, F, I, R, W0105, W0107, W0201, W0212, W0221, W0222, W0223, W0231, W0311, W0511, W0602, W0603, W0611, W0613, W0621, W0622, W0631, W0703, W1401 |
11 | |
12 | |
13 | [REPORTS] |
14 | diff --git a/cloudinit/analyze/dump.py b/cloudinit/analyze/dump.py |
15 | index b071aa1..1f3060d 100644 |
16 | --- a/cloudinit/analyze/dump.py |
17 | +++ b/cloudinit/analyze/dump.py |
18 | @@ -112,7 +112,7 @@ def parse_ci_logline(line): |
19 | return None |
20 | event_description = stage_to_description[event_name] |
21 | else: |
22 | - (pymodloglvl, event_type, event_name) = eventstr.split()[0:3] |
23 | + (_pymodloglvl, event_type, event_name) = eventstr.split()[0:3] |
24 | event_description = eventstr.split(event_name)[1].strip() |
25 | |
26 | event = { |
27 | diff --git a/cloudinit/cmd/tests/test_main.py b/cloudinit/cmd/tests/test_main.py |
28 | index dbe421c..e2c54ae 100644 |
29 | --- a/cloudinit/cmd/tests/test_main.py |
30 | +++ b/cloudinit/cmd/tests/test_main.py |
31 | @@ -56,7 +56,7 @@ class TestMain(FilesystemMockingTestCase): |
32 | cmdargs = myargs( |
33 | debug=False, files=None, force=False, local=False, reporter=None, |
34 | subcommand='init') |
35 | - (item1, item2) = wrap_and_call( |
36 | + (_item1, item2) = wrap_and_call( |
37 | 'cloudinit.cmd.main', |
38 | {'util.close_stdin': True, |
39 | 'netinfo.debug_info': 'my net debug info', |
40 | @@ -85,7 +85,7 @@ class TestMain(FilesystemMockingTestCase): |
41 | cmdargs = myargs( |
42 | debug=False, files=None, force=False, local=False, reporter=None, |
43 | subcommand='init') |
44 | - (item1, item2) = wrap_and_call( |
45 | + (_item1, item2) = wrap_and_call( |
46 | 'cloudinit.cmd.main', |
47 | {'util.close_stdin': True, |
48 | 'netinfo.debug_info': 'my net debug info', |
49 | @@ -133,7 +133,7 @@ class TestMain(FilesystemMockingTestCase): |
50 | self.assertEqual(main.LOG, log) |
51 | self.assertIsNone(args) |
52 | |
53 | - (item1, item2) = wrap_and_call( |
54 | + (_item1, item2) = wrap_and_call( |
55 | 'cloudinit.cmd.main', |
56 | {'util.close_stdin': True, |
57 | 'netinfo.debug_info': 'my net debug info', |
58 | diff --git a/cloudinit/config/cc_apt_configure.py b/cloudinit/config/cc_apt_configure.py |
59 | index afaca46..e18944e 100644 |
60 | --- a/cloudinit/config/cc_apt_configure.py |
61 | +++ b/cloudinit/config/cc_apt_configure.py |
62 | @@ -378,7 +378,7 @@ def apply_debconf_selections(cfg, target=None): |
63 | |
64 | # get a complete list of packages listed in input |
65 | pkgs_cfgd = set() |
66 | - for key, content in selsets.items(): |
67 | + for _key, content in selsets.items(): |
68 | for line in content.splitlines(): |
69 | if line.startswith("#"): |
70 | continue |
71 | diff --git a/cloudinit/config/cc_bootcmd.py b/cloudinit/config/cc_bootcmd.py |
72 | index 233da1e..db64f0a 100644 |
73 | --- a/cloudinit/config/cc_bootcmd.py |
74 | +++ b/cloudinit/config/cc_bootcmd.py |
75 | @@ -63,7 +63,6 @@ schema = { |
76 | 'additionalProperties': False, |
77 | 'minItems': 1, |
78 | 'required': [], |
79 | - 'uniqueItems': True |
80 | } |
81 | } |
82 | } |
83 | diff --git a/cloudinit/config/cc_disk_setup.py b/cloudinit/config/cc_disk_setup.py |
84 | index c3e8c48..943089e 100644 |
85 | --- a/cloudinit/config/cc_disk_setup.py |
86 | +++ b/cloudinit/config/cc_disk_setup.py |
87 | @@ -680,13 +680,13 @@ def read_parttbl(device): |
88 | reliable way to probe the partition table. |
89 | """ |
90 | blkdev_cmd = [BLKDEV_CMD, '--rereadpt', device] |
91 | - udevadm_settle() |
92 | + util.udevadm_settle() |
93 | try: |
94 | util.subp(blkdev_cmd) |
95 | except Exception as e: |
96 | util.logexc(LOG, "Failed reading the partition table %s" % e) |
97 | |
98 | - udevadm_settle() |
99 | + util.udevadm_settle() |
100 | |
101 | |
102 | def exec_mkpart_mbr(device, layout): |
103 | @@ -737,14 +737,10 @@ def exec_mkpart(table_type, device, layout): |
104 | return get_dyn_func("exec_mkpart_%s", table_type, device, layout) |
105 | |
106 | |
107 | -def udevadm_settle(): |
108 | - util.subp(['udevadm', 'settle']) |
109 | - |
110 | - |
111 | def assert_and_settle_device(device): |
112 | """Assert that device exists and settle so it is fully recognized.""" |
113 | if not os.path.exists(device): |
114 | - udevadm_settle() |
115 | + util.udevadm_settle() |
116 | if not os.path.exists(device): |
117 | raise RuntimeError("Device %s did not exist and was not created " |
118 | "with a udevamd settle." % device) |
119 | @@ -752,7 +748,7 @@ def assert_and_settle_device(device): |
120 | # Whether or not the device existed above, it is possible that udev |
121 | # events that would populate udev database (for reading by lsdname) have |
122 | # not yet finished. So settle again. |
123 | - udevadm_settle() |
124 | + util.udevadm_settle() |
125 | |
126 | |
127 | def mkpart(device, definition): |
128 | diff --git a/cloudinit/config/cc_emit_upstart.py b/cloudinit/config/cc_emit_upstart.py |
129 | index 69dc2d5..eb9fbe6 100644 |
130 | --- a/cloudinit/config/cc_emit_upstart.py |
131 | +++ b/cloudinit/config/cc_emit_upstart.py |
132 | @@ -43,7 +43,7 @@ def is_upstart_system(): |
133 | del myenv['UPSTART_SESSION'] |
134 | check_cmd = ['initctl', 'version'] |
135 | try: |
136 | - (out, err) = util.subp(check_cmd, env=myenv) |
137 | + (out, _err) = util.subp(check_cmd, env=myenv) |
138 | return 'upstart' in out |
139 | except util.ProcessExecutionError as e: |
140 | LOG.debug("'%s' returned '%s', not using upstart", |
141 | diff --git a/cloudinit/config/cc_resizefs.py b/cloudinit/config/cc_resizefs.py |
142 | index 013e69b..82f29e1 100644 |
143 | --- a/cloudinit/config/cc_resizefs.py |
144 | +++ b/cloudinit/config/cc_resizefs.py |
145 | @@ -89,13 +89,11 @@ def _resize_zfs(mount_point, devpth): |
146 | |
147 | |
148 | def _get_dumpfs_output(mount_point): |
149 | - dumpfs_res, err = util.subp(['dumpfs', '-m', mount_point]) |
150 | - return dumpfs_res |
151 | + return util.subp(['dumpfs', '-m', mount_point])[0] |
152 | |
153 | |
154 | def _get_gpart_output(part): |
155 | - gpart_res, err = util.subp(['gpart', 'show', part]) |
156 | - return gpart_res |
157 | + return util.subp(['gpart', 'show', part])[0] |
158 | |
159 | |
160 | def _can_skip_resize_ufs(mount_point, devpth): |
161 | @@ -113,7 +111,7 @@ def _can_skip_resize_ufs(mount_point, devpth): |
162 | if not line.startswith('#'): |
163 | newfs_cmd = shlex.split(line) |
164 | opt_value = 'O:Ua:s:b:d:e:f:g:h:i:jk:m:o:' |
165 | - optlist, args = getopt.getopt(newfs_cmd[1:], opt_value) |
166 | + optlist, _args = getopt.getopt(newfs_cmd[1:], opt_value) |
167 | for o, a in optlist: |
168 | if o == "-s": |
169 | cur_fs_sz = int(a) |
170 | diff --git a/cloudinit/config/cc_rh_subscription.py b/cloudinit/config/cc_rh_subscription.py |
171 | index 530808c..1c67943 100644 |
172 | --- a/cloudinit/config/cc_rh_subscription.py |
173 | +++ b/cloudinit/config/cc_rh_subscription.py |
174 | @@ -209,8 +209,7 @@ class SubscriptionManager(object): |
175 | cmd.append("--serverurl={0}".format(self.server_hostname)) |
176 | |
177 | try: |
178 | - return_out, return_err = self._sub_man_cli(cmd, |
179 | - logstring_val=True) |
180 | + return_out = self._sub_man_cli(cmd, logstring_val=True)[0] |
181 | except util.ProcessExecutionError as e: |
182 | if e.stdout == "": |
183 | self.log_warn("Registration failed due " |
184 | @@ -233,8 +232,7 @@ class SubscriptionManager(object): |
185 | |
186 | # Attempting to register the system only |
187 | try: |
188 | - return_out, return_err = self._sub_man_cli(cmd, |
189 | - logstring_val=True) |
190 | + return_out = self._sub_man_cli(cmd, logstring_val=True)[0] |
191 | except util.ProcessExecutionError as e: |
192 | if e.stdout == "": |
193 | self.log_warn("Registration failed due " |
194 | @@ -257,7 +255,7 @@ class SubscriptionManager(object): |
195 | .format(self.servicelevel)] |
196 | |
197 | try: |
198 | - return_out, return_err = self._sub_man_cli(cmd) |
199 | + return_out = self._sub_man_cli(cmd)[0] |
200 | except util.ProcessExecutionError as e: |
201 | if e.stdout.rstrip() != '': |
202 | for line in e.stdout.split("\n"): |
203 | @@ -275,7 +273,7 @@ class SubscriptionManager(object): |
204 | def _set_auto_attach(self): |
205 | cmd = ['attach', '--auto'] |
206 | try: |
207 | - return_out, return_err = self._sub_man_cli(cmd) |
208 | + return_out = self._sub_man_cli(cmd)[0] |
209 | except util.ProcessExecutionError as e: |
210 | self.log_warn("Auto-attach failed with: {0}".format(e)) |
211 | return False |
212 | @@ -294,12 +292,12 @@ class SubscriptionManager(object): |
213 | |
214 | # Get all available pools |
215 | cmd = ['list', '--available', '--pool-only'] |
216 | - results, errors = self._sub_man_cli(cmd) |
217 | + results = self._sub_man_cli(cmd)[0] |
218 | available = (results.rstrip()).split("\n") |
219 | |
220 | # Get all consumed pools |
221 | cmd = ['list', '--consumed', '--pool-only'] |
222 | - results, errors = self._sub_man_cli(cmd) |
223 | + results = self._sub_man_cli(cmd)[0] |
224 | consumed = (results.rstrip()).split("\n") |
225 | |
226 | return available, consumed |
227 | @@ -311,14 +309,14 @@ class SubscriptionManager(object): |
228 | ''' |
229 | |
230 | cmd = ['repos', '--list-enabled'] |
231 | - return_out, return_err = self._sub_man_cli(cmd) |
232 | + return_out = self._sub_man_cli(cmd)[0] |
233 | active_repos = [] |
234 | for repo in return_out.split("\n"): |
235 | if "Repo ID:" in repo: |
236 | active_repos.append((repo.split(':')[1]).strip()) |
237 | |
238 | cmd = ['repos', '--list-disabled'] |
239 | - return_out, return_err = self._sub_man_cli(cmd) |
240 | + return_out = self._sub_man_cli(cmd)[0] |
241 | |
242 | inactive_repos = [] |
243 | for repo in return_out.split("\n"): |
244 | diff --git a/cloudinit/config/cc_runcmd.py b/cloudinit/config/cc_runcmd.py |
245 | index 539cbd5..b6f6c80 100644 |
246 | --- a/cloudinit/config/cc_runcmd.py |
247 | +++ b/cloudinit/config/cc_runcmd.py |
248 | @@ -66,7 +66,6 @@ schema = { |
249 | 'additionalProperties': False, |
250 | 'minItems': 1, |
251 | 'required': [], |
252 | - 'uniqueItems': True |
253 | } |
254 | } |
255 | } |
256 | diff --git a/cloudinit/config/cc_set_passwords.py b/cloudinit/config/cc_set_passwords.py |
257 | index bb24d57..5ef9737 100755 |
258 | --- a/cloudinit/config/cc_set_passwords.py |
259 | +++ b/cloudinit/config/cc_set_passwords.py |
260 | @@ -68,16 +68,57 @@ import re |
261 | import sys |
262 | |
263 | from cloudinit.distros import ug_util |
264 | -from cloudinit import ssh_util |
265 | +from cloudinit import log as logging |
266 | +from cloudinit.ssh_util import update_ssh_config |
267 | from cloudinit import util |
268 | |
269 | from string import ascii_letters, digits |
270 | |
271 | +LOG = logging.getLogger(__name__) |
272 | + |
273 | # We are removing certain 'painful' letters/numbers |
274 | PW_SET = (''.join([x for x in ascii_letters + digits |
275 | if x not in 'loLOI01'])) |
276 | |
277 | |
278 | +def handle_ssh_pwauth(pw_auth, service_cmd=None, service_name="ssh"): |
279 | + """Apply sshd PasswordAuthentication changes. |
280 | + |
281 | + @param pw_auth: config setting from 'pw_auth'. |
282 | + Best given as True, False, or "unchanged". |
283 | + @param service_cmd: The service command list (['service']) |
284 | + @param service_name: The name of the sshd service for the system. |
285 | + |
286 | + @return: None""" |
287 | + cfg_name = "PasswordAuthentication" |
288 | + if service_cmd is None: |
289 | + service_cmd = ["service"] |
290 | + |
291 | + if util.is_true(pw_auth): |
292 | + cfg_val = 'yes' |
293 | + elif util.is_false(pw_auth): |
294 | + cfg_val = 'no' |
295 | + else: |
296 | + bmsg = "Leaving ssh config '%s' unchanged." % cfg_name |
297 | + if pw_auth is None or pw_auth.lower() == 'unchanged': |
298 | + LOG.debug("%s ssh_pwauth=%s", bmsg, pw_auth) |
299 | + else: |
300 | + LOG.warning("%s Unrecognized value: ssh_pwauth=%s", bmsg, pw_auth) |
301 | + return |
302 | + |
303 | + updated = update_ssh_config({cfg_name: cfg_val}) |
304 | + if not updated: |
305 | + LOG.debug("No need to restart ssh service, %s not updated.", cfg_name) |
306 | + return |
307 | + |
308 | + if 'systemctl' in service_cmd: |
309 | + cmd = list(service_cmd) + ["restart", service_name] |
310 | + else: |
311 | + cmd = list(service_cmd) + [service_name, "restart"] |
312 | + util.subp(cmd) |
313 | + LOG.debug("Restarted the ssh daemon.") |
314 | + |
315 | + |
316 | def handle(_name, cfg, cloud, log, args): |
317 | if len(args) != 0: |
318 | # if run from command line, and give args, wipe the chpasswd['list'] |
319 | @@ -170,65 +211,9 @@ def handle(_name, cfg, cloud, log, args): |
320 | if expired_users: |
321 | log.debug("Expired passwords for: %s users", expired_users) |
322 | |
323 | - change_pwauth = False |
324 | - pw_auth = None |
325 | - if 'ssh_pwauth' in cfg: |
326 | - if util.is_true(cfg['ssh_pwauth']): |
327 | - change_pwauth = True |
328 | - pw_auth = 'yes' |
329 | - elif util.is_false(cfg['ssh_pwauth']): |
330 | - change_pwauth = True |
331 | - pw_auth = 'no' |
332 | - elif str(cfg['ssh_pwauth']).lower() == 'unchanged': |
333 | - log.debug('Leaving auth line unchanged') |
334 | - change_pwauth = False |
335 | - elif not str(cfg['ssh_pwauth']).strip(): |
336 | - log.debug('Leaving auth line unchanged') |
337 | - change_pwauth = False |
338 | - elif not cfg['ssh_pwauth']: |
339 | - log.debug('Leaving auth line unchanged') |
340 | - change_pwauth = False |
341 | - else: |
342 | - msg = 'Unrecognized value %s for ssh_pwauth' % cfg['ssh_pwauth'] |
343 | - util.logexc(log, msg) |
344 | - |
345 | - if change_pwauth: |
346 | - replaced_auth = False |
347 | - |
348 | - # See: man sshd_config |
349 | - old_lines = ssh_util.parse_ssh_config(ssh_util.DEF_SSHD_CFG) |
350 | - new_lines = [] |
351 | - i = 0 |
352 | - for (i, line) in enumerate(old_lines): |
353 | - # Keywords are case-insensitive and arguments are case-sensitive |
354 | - if line.key == 'passwordauthentication': |
355 | - log.debug("Replacing auth line %s with %s", i + 1, pw_auth) |
356 | - replaced_auth = True |
357 | - line.value = pw_auth |
358 | - new_lines.append(line) |
359 | - |
360 | - if not replaced_auth: |
361 | - log.debug("Adding new auth line %s", i + 1) |
362 | - replaced_auth = True |
363 | - new_lines.append(ssh_util.SshdConfigLine('', |
364 | - 'PasswordAuthentication', |
365 | - pw_auth)) |
366 | - |
367 | - lines = [str(l) for l in new_lines] |
368 | - util.write_file(ssh_util.DEF_SSHD_CFG, "\n".join(lines), |
369 | - copy_mode=True) |
370 | - |
371 | - try: |
372 | - cmd = cloud.distro.init_cmd # Default service |
373 | - cmd.append(cloud.distro.get_option('ssh_svcname', 'ssh')) |
374 | - cmd.append('restart') |
375 | - if 'systemctl' in cmd: # Switch action ordering |
376 | - cmd[1], cmd[2] = cmd[2], cmd[1] |
377 | - cmd = filter(None, cmd) # Remove empty arguments |
378 | - util.subp(cmd) |
379 | - log.debug("Restarted the ssh daemon") |
380 | - except Exception: |
381 | - util.logexc(log, "Restarting of the ssh daemon failed") |
382 | + handle_ssh_pwauth( |
383 | + cfg.get('ssh_pwauth'), service_cmd=cloud.distro.init_cmd, |
384 | + service_name=cloud.distro.get_option('ssh_svcname', 'ssh')) |
385 | |
386 | if len(errors): |
387 | log.debug("%s errors occured, re-raising the last one", len(errors)) |
388 | diff --git a/cloudinit/config/cc_snap.py b/cloudinit/config/cc_snap.py |
389 | index 34a53fd..90724b8 100644 |
390 | --- a/cloudinit/config/cc_snap.py |
391 | +++ b/cloudinit/config/cc_snap.py |
392 | @@ -110,7 +110,6 @@ schema = { |
393 | 'additionalItems': False, # Reject non-string & non-list |
394 | 'minItems': 1, |
395 | 'minProperties': 1, |
396 | - 'uniqueItems': True |
397 | }, |
398 | 'squashfuse_in_container': { |
399 | 'type': 'boolean' |
400 | @@ -204,12 +203,12 @@ def maybe_install_squashfuse(cloud): |
401 | return |
402 | try: |
403 | cloud.distro.update_package_sources() |
404 | - except Exception as e: |
405 | + except Exception: |
406 | util.logexc(LOG, "Package update failed") |
407 | raise |
408 | try: |
409 | cloud.distro.install_packages(['squashfuse']) |
410 | - except Exception as e: |
411 | + except Exception: |
412 | util.logexc(LOG, "Failed to install squashfuse") |
413 | raise |
414 | |
415 | diff --git a/cloudinit/config/cc_snappy.py b/cloudinit/config/cc_snappy.py |
416 | index bab80bb..15bee2d 100644 |
417 | --- a/cloudinit/config/cc_snappy.py |
418 | +++ b/cloudinit/config/cc_snappy.py |
419 | @@ -213,7 +213,7 @@ def render_snap_op(op, name, path=None, cfgfile=None, config=None): |
420 | |
421 | def read_installed_packages(): |
422 | ret = [] |
423 | - for (name, date, version, dev) in read_pkg_data(): |
424 | + for (name, _date, _version, dev) in read_pkg_data(): |
425 | if dev: |
426 | ret.append(NAMESPACE_DELIM.join([name, dev])) |
427 | else: |
428 | @@ -222,7 +222,7 @@ def read_installed_packages(): |
429 | |
430 | |
431 | def read_pkg_data(): |
432 | - out, err = util.subp([SNAPPY_CMD, "list"]) |
433 | + out, _err = util.subp([SNAPPY_CMD, "list"]) |
434 | pkg_data = [] |
435 | for line in out.splitlines()[1:]: |
436 | toks = line.split(sep=None, maxsplit=3) |
437 | diff --git a/cloudinit/config/cc_ubuntu_advantage.py b/cloudinit/config/cc_ubuntu_advantage.py |
438 | index 16b1868..5e082bd 100644 |
439 | --- a/cloudinit/config/cc_ubuntu_advantage.py |
440 | +++ b/cloudinit/config/cc_ubuntu_advantage.py |
441 | @@ -87,7 +87,6 @@ schema = { |
442 | 'additionalItems': False, # Reject non-string & non-list |
443 | 'minItems': 1, |
444 | 'minProperties': 1, |
445 | - 'uniqueItems': True |
446 | } |
447 | }, |
448 | 'additionalProperties': False, # Reject keys not in schema |
449 | @@ -149,12 +148,12 @@ def maybe_install_ua_tools(cloud): |
450 | return |
451 | try: |
452 | cloud.distro.update_package_sources() |
453 | - except Exception as e: |
454 | + except Exception: |
455 | util.logexc(LOG, "Package update failed") |
456 | raise |
457 | try: |
458 | cloud.distro.install_packages(['ubuntu-advantage-tools']) |
459 | - except Exception as e: |
460 | + except Exception: |
461 | util.logexc(LOG, "Failed to install ubuntu-advantage-tools") |
462 | raise |
463 | |
464 | diff --git a/cloudinit/config/schema.py b/cloudinit/config/schema.py |
465 | index ca7d0d5..76826e0 100644 |
466 | --- a/cloudinit/config/schema.py |
467 | +++ b/cloudinit/config/schema.py |
468 | @@ -297,8 +297,8 @@ def get_schema(): |
469 | |
470 | configs_dir = os.path.dirname(os.path.abspath(__file__)) |
471 | potential_handlers = find_modules(configs_dir) |
472 | - for (fname, mod_name) in potential_handlers.items(): |
473 | - mod_locs, looked_locs = importer.find_module( |
474 | + for (_fname, mod_name) in potential_handlers.items(): |
475 | + mod_locs, _looked_locs = importer.find_module( |
476 | mod_name, ['cloudinit.config'], ['schema']) |
477 | if mod_locs: |
478 | mod = importer.import_module(mod_locs[0]) |
479 | diff --git a/cloudinit/config/tests/test_set_passwords.py b/cloudinit/config/tests/test_set_passwords.py |
480 | new file mode 100644 |
481 | index 0000000..b051ec8 |
482 | --- /dev/null |
483 | +++ b/cloudinit/config/tests/test_set_passwords.py |
484 | @@ -0,0 +1,71 @@ |
485 | +# This file is part of cloud-init. See LICENSE file for license information. |
486 | + |
487 | +import mock |
488 | + |
489 | +from cloudinit.config import cc_set_passwords as setpass |
490 | +from cloudinit.tests.helpers import CiTestCase |
491 | +from cloudinit import util |
492 | + |
493 | +MODPATH = "cloudinit.config.cc_set_passwords." |
494 | + |
495 | + |
496 | +class TestHandleSshPwauth(CiTestCase): |
497 | + """Test cc_set_passwords handling of ssh_pwauth in handle_ssh_pwauth.""" |
498 | + |
499 | + with_logs = True |
500 | + |
501 | + @mock.patch(MODPATH + "util.subp") |
502 | + def test_unknown_value_logs_warning(self, m_subp): |
503 | + setpass.handle_ssh_pwauth("floo") |
504 | + self.assertIn("Unrecognized value: ssh_pwauth=floo", |
505 | + self.logs.getvalue()) |
506 | + m_subp.assert_not_called() |
507 | + |
508 | + @mock.patch(MODPATH + "update_ssh_config", return_value=True) |
509 | + @mock.patch(MODPATH + "util.subp") |
510 | + def test_systemctl_as_service_cmd(self, m_subp, m_update_ssh_config): |
511 | + """If systemctl in service cmd: systemctl restart name.""" |
512 | + setpass.handle_ssh_pwauth( |
513 | + True, service_cmd=["systemctl"], service_name="myssh") |
514 | + self.assertEqual(mock.call(["systemctl", "restart", "myssh"]), |
515 | + m_subp.call_args) |
516 | + |
517 | + @mock.patch(MODPATH + "update_ssh_config", return_value=True) |
518 | + @mock.patch(MODPATH + "util.subp") |
519 | + def test_service_as_service_cmd(self, m_subp, m_update_ssh_config): |
520 | + """If systemctl in service cmd: systemctl restart name.""" |
521 | + setpass.handle_ssh_pwauth( |
522 | + True, service_cmd=["service"], service_name="myssh") |
523 | + self.assertEqual(mock.call(["service", "myssh", "restart"]), |
524 | + m_subp.call_args) |
525 | + |
526 | + @mock.patch(MODPATH + "update_ssh_config", return_value=False) |
527 | + @mock.patch(MODPATH + "util.subp") |
528 | + def test_not_restarted_if_not_updated(self, m_subp, m_update_ssh_config): |
529 | + """If config is not updated, then no system restart should be done.""" |
530 | + setpass.handle_ssh_pwauth(True) |
531 | + m_subp.assert_not_called() |
532 | + self.assertIn("No need to restart ssh", self.logs.getvalue()) |
533 | + |
534 | + @mock.patch(MODPATH + "update_ssh_config", return_value=True) |
535 | + @mock.patch(MODPATH + "util.subp") |
536 | + def test_unchanged_does_nothing(self, m_subp, m_update_ssh_config): |
537 | + """If 'unchanged', then no updates to config and no restart.""" |
538 | + setpass.handle_ssh_pwauth( |
539 | + "unchanged", service_cmd=["systemctl"], service_name="myssh") |
540 | + m_update_ssh_config.assert_not_called() |
541 | + m_subp.assert_not_called() |
542 | + |
543 | + @mock.patch(MODPATH + "util.subp") |
544 | + def test_valid_change_values(self, m_subp): |
545 | + """If value is a valid changen value, then update should be called.""" |
546 | + upname = MODPATH + "update_ssh_config" |
547 | + optname = "PasswordAuthentication" |
548 | + for value in util.FALSE_STRINGS + util.TRUE_STRINGS: |
549 | + optval = "yes" if value in util.TRUE_STRINGS else "no" |
550 | + with mock.patch(upname, return_value=False) as m_update: |
551 | + setpass.handle_ssh_pwauth(value) |
552 | + m_update.assert_called_with({optname: optval}) |
553 | + m_subp.assert_not_called() |
554 | + |
555 | +# vi: ts=4 expandtab |
556 | diff --git a/cloudinit/config/tests/test_snap.py b/cloudinit/config/tests/test_snap.py |
557 | index c5b4a9d..34c80f1 100644 |
558 | --- a/cloudinit/config/tests/test_snap.py |
559 | +++ b/cloudinit/config/tests/test_snap.py |
560 | @@ -9,7 +9,7 @@ from cloudinit.config.cc_snap import ( |
561 | from cloudinit.config.schema import validate_cloudconfig_schema |
562 | from cloudinit import util |
563 | from cloudinit.tests.helpers import ( |
564 | - CiTestCase, mock, wrap_and_call, skipUnlessJsonSchema) |
565 | + CiTestCase, SchemaTestCaseMixin, mock, wrap_and_call, skipUnlessJsonSchema) |
566 | |
567 | |
568 | SYSTEM_USER_ASSERTION = """\ |
569 | @@ -245,9 +245,10 @@ class TestRunCommands(CiTestCase): |
570 | |
571 | |
572 | @skipUnlessJsonSchema() |
573 | -class TestSchema(CiTestCase): |
574 | +class TestSchema(CiTestCase, SchemaTestCaseMixin): |
575 | |
576 | with_logs = True |
577 | + schema = schema |
578 | |
579 | def test_schema_warns_on_snap_not_as_dict(self): |
580 | """If the snap configuration is not a dict, emit a warning.""" |
581 | @@ -340,6 +341,30 @@ class TestSchema(CiTestCase): |
582 | {'snap': {'assertions': {'01': 'also valid'}}}, schema) |
583 | self.assertEqual('', self.logs.getvalue()) |
584 | |
585 | + def test_duplicates_are_fine_array_array(self): |
586 | + """Duplicated commands array/array entries are allowed.""" |
587 | + self.assertSchemaValid( |
588 | + {'commands': [["echo", "bye"], ["echo" "bye"]]}, |
589 | + "command entries can be duplicate.") |
590 | + |
591 | + def test_duplicates_are_fine_array_string(self): |
592 | + """Duplicated commands array/string entries are allowed.""" |
593 | + self.assertSchemaValid( |
594 | + {'commands': ["echo bye", "echo bye"]}, |
595 | + "command entries can be duplicate.") |
596 | + |
597 | + def test_duplicates_are_fine_dict_array(self): |
598 | + """Duplicated commands dict/array entries are allowed.""" |
599 | + self.assertSchemaValid( |
600 | + {'commands': {'00': ["echo", "bye"], '01': ["echo", "bye"]}}, |
601 | + "command entries can be duplicate.") |
602 | + |
603 | + def test_duplicates_are_fine_dict_string(self): |
604 | + """Duplicated commands dict/string entries are allowed.""" |
605 | + self.assertSchemaValid( |
606 | + {'commands': {'00': "echo bye", '01': "echo bye"}}, |
607 | + "command entries can be duplicate.") |
608 | + |
609 | |
610 | class TestHandle(CiTestCase): |
611 | |
612 | diff --git a/cloudinit/config/tests/test_ubuntu_advantage.py b/cloudinit/config/tests/test_ubuntu_advantage.py |
613 | index f2a59fa..f1beeff 100644 |
614 | --- a/cloudinit/config/tests/test_ubuntu_advantage.py |
615 | +++ b/cloudinit/config/tests/test_ubuntu_advantage.py |
616 | @@ -7,7 +7,8 @@ from cloudinit.config.cc_ubuntu_advantage import ( |
617 | handle, maybe_install_ua_tools, run_commands, schema) |
618 | from cloudinit.config.schema import validate_cloudconfig_schema |
619 | from cloudinit import util |
620 | -from cloudinit.tests.helpers import CiTestCase, mock, skipUnlessJsonSchema |
621 | +from cloudinit.tests.helpers import ( |
622 | + CiTestCase, mock, SchemaTestCaseMixin, skipUnlessJsonSchema) |
623 | |
624 | |
625 | # Module path used in mocks |
626 | @@ -105,9 +106,10 @@ class TestRunCommands(CiTestCase): |
627 | |
628 | |
629 | @skipUnlessJsonSchema() |
630 | -class TestSchema(CiTestCase): |
631 | +class TestSchema(CiTestCase, SchemaTestCaseMixin): |
632 | |
633 | with_logs = True |
634 | + schema = schema |
635 | |
636 | def test_schema_warns_on_ubuntu_advantage_not_as_dict(self): |
637 | """If ubuntu-advantage configuration is not a dict, emit a warning.""" |
638 | @@ -169,6 +171,30 @@ class TestSchema(CiTestCase): |
639 | {'ubuntu-advantage': {'commands': {'01': 'also valid'}}}, schema) |
640 | self.assertEqual('', self.logs.getvalue()) |
641 | |
642 | + def test_duplicates_are_fine_array_array(self): |
643 | + """Duplicated commands array/array entries are allowed.""" |
644 | + self.assertSchemaValid( |
645 | + {'commands': [["echo", "bye"], ["echo" "bye"]]}, |
646 | + "command entries can be duplicate.") |
647 | + |
648 | + def test_duplicates_are_fine_array_string(self): |
649 | + """Duplicated commands array/string entries are allowed.""" |
650 | + self.assertSchemaValid( |
651 | + {'commands': ["echo bye", "echo bye"]}, |
652 | + "command entries can be duplicate.") |
653 | + |
654 | + def test_duplicates_are_fine_dict_array(self): |
655 | + """Duplicated commands dict/array entries are allowed.""" |
656 | + self.assertSchemaValid( |
657 | + {'commands': {'00': ["echo", "bye"], '01': ["echo", "bye"]}}, |
658 | + "command entries can be duplicate.") |
659 | + |
660 | + def test_duplicates_are_fine_dict_string(self): |
661 | + """Duplicated commands dict/string entries are allowed.""" |
662 | + self.assertSchemaValid( |
663 | + {'commands': {'00': "echo bye", '01': "echo bye"}}, |
664 | + "command entries can be duplicate.") |
665 | + |
666 | |
667 | class TestHandle(CiTestCase): |
668 | |
669 | diff --git a/cloudinit/distros/freebsd.py b/cloudinit/distros/freebsd.py |
670 | index 099fac5..5b1718a 100644 |
671 | --- a/cloudinit/distros/freebsd.py |
672 | +++ b/cloudinit/distros/freebsd.py |
673 | @@ -113,7 +113,7 @@ class Distro(distros.Distro): |
674 | n = re.search(r'\d+$', dev) |
675 | index = n.group(0) |
676 | |
677 | - (out, err) = util.subp(['ifconfig', '-a']) |
678 | + (out, _err) = util.subp(['ifconfig', '-a']) |
679 | ifconfigoutput = [x for x in (out.strip()).splitlines() |
680 | if len(x.split()) > 0] |
681 | bsddev = 'NOT_FOUND' |
682 | diff --git a/cloudinit/distros/ubuntu.py b/cloudinit/distros/ubuntu.py |
683 | index fdc1f62..6815410 100644 |
684 | --- a/cloudinit/distros/ubuntu.py |
685 | +++ b/cloudinit/distros/ubuntu.py |
686 | @@ -25,7 +25,7 @@ class Distro(debian.Distro): |
687 | def preferred_ntp_clients(self): |
688 | """The preferred ntp client is dependent on the version.""" |
689 | if not self._preferred_ntp_clients: |
690 | - (name, version, codename) = util.system_info()['dist'] |
691 | + (_name, _version, codename) = util.system_info()['dist'] |
692 | # Xenial cloud-init only installed ntp, UbuntuCore has timesyncd. |
693 | if codename == "xenial" and not util.system_is_snappy(): |
694 | self._preferred_ntp_clients = ['ntp'] |
695 | diff --git a/cloudinit/net/__init__.py b/cloudinit/net/__init__.py |
696 | index f69c0ef..43226bd 100644 |
697 | --- a/cloudinit/net/__init__.py |
698 | +++ b/cloudinit/net/__init__.py |
699 | @@ -107,6 +107,21 @@ def is_bond(devname): |
700 | return os.path.exists(sys_dev_path(devname, "bonding")) |
701 | |
702 | |
703 | +def is_renamed(devname): |
704 | + """ |
705 | + /* interface name assignment types (sysfs name_assign_type attribute) */ |
706 | + #define NET_NAME_UNKNOWN 0 /* unknown origin (not exposed to user) */ |
707 | + #define NET_NAME_ENUM 1 /* enumerated by kernel */ |
708 | + #define NET_NAME_PREDICTABLE 2 /* predictably named by the kernel */ |
709 | + #define NET_NAME_USER 3 /* provided by user-space */ |
710 | + #define NET_NAME_RENAMED 4 /* renamed by user-space */ |
711 | + """ |
712 | + name_assign_type = read_sys_net_safe(devname, 'name_assign_type') |
713 | + if name_assign_type and name_assign_type in ['3', '4']: |
714 | + return True |
715 | + return False |
716 | + |
717 | + |
718 | def is_vlan(devname): |
719 | uevent = str(read_sys_net_safe(devname, "uevent")) |
720 | return 'DEVTYPE=vlan' in uevent.splitlines() |
721 | @@ -180,6 +195,17 @@ def find_fallback_nic(blacklist_drivers=None): |
722 | if not blacklist_drivers: |
723 | blacklist_drivers = [] |
724 | |
725 | + if 'net.ifnames=0' in util.get_cmdline(): |
726 | + LOG.debug('Stable ifnames disabled by net.ifnames=0 in /proc/cmdline') |
727 | + else: |
728 | + unstable = [device for device in get_devicelist() |
729 | + if device != 'lo' and not is_renamed(device)] |
730 | + if len(unstable): |
731 | + LOG.debug('Found unstable nic names: %s; calling udevadm settle', |
732 | + unstable) |
733 | + msg = 'Waiting for udev events to settle' |
734 | + util.log_time(LOG.debug, msg, func=util.udevadm_settle) |
735 | + |
736 | # get list of interfaces that could have connections |
737 | invalid_interfaces = set(['lo']) |
738 | potential_interfaces = set([device for device in get_devicelist() |
739 | @@ -295,7 +321,7 @@ def apply_network_config_names(netcfg, strict_present=True, strict_busy=True): |
740 | |
741 | def _version_2(netcfg): |
742 | renames = [] |
743 | - for key, ent in netcfg.get('ethernets', {}).items(): |
744 | + for ent in netcfg.get('ethernets', {}).values(): |
745 | # only rename if configured to do so |
746 | name = ent.get('set-name') |
747 | if not name: |
748 | diff --git a/cloudinit/net/cmdline.py b/cloudinit/net/cmdline.py |
749 | index 9e9fe0f..f89a0f7 100755 |
750 | --- a/cloudinit/net/cmdline.py |
751 | +++ b/cloudinit/net/cmdline.py |
752 | @@ -65,7 +65,7 @@ def _klibc_to_config_entry(content, mac_addrs=None): |
753 | iface['mac_address'] = mac_addrs[name] |
754 | |
755 | # Handle both IPv4 and IPv6 values |
756 | - for v, pre in (('ipv4', 'IPV4'), ('ipv6', 'IPV6')): |
757 | + for pre in ('IPV4', 'IPV6'): |
758 | # if no IPV4ADDR or IPV6ADDR, then go on. |
759 | if pre + "ADDR" not in data: |
760 | continue |
761 | diff --git a/cloudinit/net/dhcp.py b/cloudinit/net/dhcp.py |
762 | index 087c0c0..12cf509 100644 |
763 | --- a/cloudinit/net/dhcp.py |
764 | +++ b/cloudinit/net/dhcp.py |
765 | @@ -216,7 +216,7 @@ def networkd_get_option_from_leases(keyname, leases_d=None): |
766 | if leases_d is None: |
767 | leases_d = NETWORKD_LEASES_DIR |
768 | leases = networkd_load_leases(leases_d=leases_d) |
769 | - for ifindex, data in sorted(leases.items()): |
770 | + for _ifindex, data in sorted(leases.items()): |
771 | if data.get(keyname): |
772 | return data[keyname] |
773 | return None |
774 | diff --git a/cloudinit/net/sysconfig.py b/cloudinit/net/sysconfig.py |
775 | index 39d89c4..e53b9f1 100644 |
776 | --- a/cloudinit/net/sysconfig.py |
777 | +++ b/cloudinit/net/sysconfig.py |
778 | @@ -287,7 +287,6 @@ class Renderer(renderer.Renderer): |
779 | if subnet_type == 'dhcp6': |
780 | iface_cfg['IPV6INIT'] = True |
781 | iface_cfg['DHCPV6C'] = True |
782 | - iface_cfg['BOOTPROTO'] = 'dhcp' |
783 | elif subnet_type in ['dhcp4', 'dhcp']: |
784 | iface_cfg['BOOTPROTO'] = 'dhcp' |
785 | elif subnet_type == 'static': |
786 | @@ -364,7 +363,7 @@ class Renderer(renderer.Renderer): |
787 | |
788 | @classmethod |
789 | def _render_subnet_routes(cls, iface_cfg, route_cfg, subnets): |
790 | - for i, subnet in enumerate(subnets, start=len(iface_cfg.children)): |
791 | + for _, subnet in enumerate(subnets, start=len(iface_cfg.children)): |
792 | for route in subnet.get('routes', []): |
793 | is_ipv6 = subnet.get('ipv6') or is_ipv6_addr(route['gateway']) |
794 | |
795 | diff --git a/cloudinit/net/tests/test_init.py b/cloudinit/net/tests/test_init.py |
796 | index 276556e..5c017d1 100644 |
797 | --- a/cloudinit/net/tests/test_init.py |
798 | +++ b/cloudinit/net/tests/test_init.py |
799 | @@ -199,6 +199,7 @@ class TestGenerateFallbackConfig(CiTestCase): |
800 | self.sysdir = self.tmp_dir() + '/' |
801 | self.m_sys_path.return_value = self.sysdir |
802 | self.addCleanup(sys_mock.stop) |
803 | + self.add_patch('cloudinit.net.util.udevadm_settle', 'm_settle') |
804 | |
805 | def test_generate_fallback_finds_connected_eth_with_mac(self): |
806 | """generate_fallback_config finds any connected device with a mac.""" |
807 | diff --git a/cloudinit/reporting/events.py b/cloudinit/reporting/events.py |
808 | index 4f62d2f..e5dfab3 100644 |
809 | --- a/cloudinit/reporting/events.py |
810 | +++ b/cloudinit/reporting/events.py |
811 | @@ -192,7 +192,7 @@ class ReportEventStack(object): |
812 | |
813 | def _childrens_finish_info(self): |
814 | for cand_result in (status.FAIL, status.WARN): |
815 | - for name, (value, msg) in self.children.items(): |
816 | + for _name, (value, _msg) in self.children.items(): |
817 | if value == cand_result: |
818 | return (value, self.message) |
819 | return (self.result, self.message) |
820 | diff --git a/cloudinit/sources/DataSourceAliYun.py b/cloudinit/sources/DataSourceAliYun.py |
821 | index 22279d0..858e082 100644 |
822 | --- a/cloudinit/sources/DataSourceAliYun.py |
823 | +++ b/cloudinit/sources/DataSourceAliYun.py |
824 | @@ -45,7 +45,7 @@ def _is_aliyun(): |
825 | |
826 | def parse_public_keys(public_keys): |
827 | keys = [] |
828 | - for key_id, key_body in public_keys.items(): |
829 | + for _key_id, key_body in public_keys.items(): |
830 | if isinstance(key_body, str): |
831 | keys.append(key_body.strip()) |
832 | elif isinstance(key_body, list): |
833 | diff --git a/cloudinit/sources/DataSourceAltCloud.py b/cloudinit/sources/DataSourceAltCloud.py |
834 | index e1d0055..f6e86f3 100644 |
835 | --- a/cloudinit/sources/DataSourceAltCloud.py |
836 | +++ b/cloudinit/sources/DataSourceAltCloud.py |
837 | @@ -29,7 +29,6 @@ CLOUD_INFO_FILE = '/etc/sysconfig/cloud-info' |
838 | |
839 | # Shell command lists |
840 | CMD_PROBE_FLOPPY = ['modprobe', 'floppy'] |
841 | -CMD_UDEVADM_SETTLE = ['udevadm', 'settle', '--timeout=5'] |
842 | |
843 | META_DATA_NOT_SUPPORTED = { |
844 | 'block-device-mapping': {}, |
845 | @@ -196,9 +195,7 @@ class DataSourceAltCloud(sources.DataSource): |
846 | |
847 | # udevadm settle for floppy device |
848 | try: |
849 | - cmd = CMD_UDEVADM_SETTLE |
850 | - cmd.append('--exit-if-exists=' + floppy_dev) |
851 | - (cmd_out, _err) = util.subp(cmd) |
852 | + (cmd_out, _err) = util.udevadm_settle(exists=floppy_dev, timeout=5) |
853 | LOG.debug('Command: %s\nOutput%s', ' '.join(cmd), cmd_out) |
854 | except ProcessExecutionError as _err: |
855 | util.logexc(LOG, 'Failed command: %s\n%s', ' '.join(cmd), _err) |
856 | diff --git a/cloudinit/sources/DataSourceAzure.py b/cloudinit/sources/DataSourceAzure.py |
857 | index 0ee622e..a71197a 100644 |
858 | --- a/cloudinit/sources/DataSourceAzure.py |
859 | +++ b/cloudinit/sources/DataSourceAzure.py |
860 | @@ -107,31 +107,24 @@ def find_dev_from_busdev(camcontrol_out, busdev): |
861 | return None |
862 | |
863 | |
864 | -def get_dev_storvsc_sysctl(): |
865 | +def execute_or_debug(cmd, fail_ret=None): |
866 | try: |
867 | - sysctl_out, err = util.subp(['sysctl', 'dev.storvsc']) |
868 | + return util.subp(cmd)[0] |
869 | except util.ProcessExecutionError: |
870 | - LOG.debug("Fail to execute sysctl dev.storvsc") |
871 | - sysctl_out = "" |
872 | - return sysctl_out |
873 | + LOG.debug("Failed to execute: %s", ' '.join(cmd)) |
874 | + return fail_ret |
875 | + |
876 | + |
877 | +def get_dev_storvsc_sysctl(): |
878 | + return execute_or_debug(["sysctl", "dev.storvsc"], fail_ret="") |
879 | |
880 | |
881 | def get_camcontrol_dev_bus(): |
882 | - try: |
883 | - camcontrol_b_out, err = util.subp(['camcontrol', 'devlist', '-b']) |
884 | - except util.ProcessExecutionError: |
885 | - LOG.debug("Fail to execute camcontrol devlist -b") |
886 | - return None |
887 | - return camcontrol_b_out |
888 | + return execute_or_debug(['camcontrol', 'devlist', '-b']) |
889 | |
890 | |
891 | def get_camcontrol_dev(): |
892 | - try: |
893 | - camcontrol_out, err = util.subp(['camcontrol', 'devlist']) |
894 | - except util.ProcessExecutionError: |
895 | - LOG.debug("Fail to execute camcontrol devlist") |
896 | - return None |
897 | - return camcontrol_out |
898 | + return execute_or_debug(['camcontrol', 'devlist']) |
899 | |
900 | |
901 | def get_resource_disk_on_freebsd(port_id): |
902 | @@ -474,7 +467,7 @@ class DataSourceAzure(sources.DataSource): |
903 | before we go into our polling loop.""" |
904 | try: |
905 | get_metadata_from_fabric(None, lease['unknown-245']) |
906 | - except Exception as exc: |
907 | + except Exception: |
908 | LOG.warning( |
909 | "Error communicating with Azure fabric; You may experience." |
910 | "connectivity issues.", exc_info=True) |
911 | @@ -492,7 +485,7 @@ class DataSourceAzure(sources.DataSource): |
912 | jump back into the polling loop in order to retrieve the ovf_env.""" |
913 | if not ret: |
914 | return False |
915 | - (md, self.userdata_raw, cfg, files) = ret |
916 | + (_md, self.userdata_raw, cfg, _files) = ret |
917 | path = REPROVISION_MARKER_FILE |
918 | if (cfg.get('PreprovisionedVm') is True or |
919 | os.path.isfile(path)): |
920 | @@ -528,7 +521,7 @@ class DataSourceAzure(sources.DataSource): |
921 | self.ds_cfg['agent_command']) |
922 | try: |
923 | fabric_data = metadata_func() |
924 | - except Exception as exc: |
925 | + except Exception: |
926 | LOG.warning( |
927 | "Error communicating with Azure fabric; You may experience." |
928 | "connectivity issues.", exc_info=True) |
929 | diff --git a/cloudinit/sources/DataSourceIBMCloud.py b/cloudinit/sources/DataSourceIBMCloud.py |
930 | index 02b3d56..01106ec 100644 |
931 | --- a/cloudinit/sources/DataSourceIBMCloud.py |
932 | +++ b/cloudinit/sources/DataSourceIBMCloud.py |
933 | @@ -8,17 +8,11 @@ There are 2 different api exposed launch methods. |
934 | * template: This is the legacy method of launching instances. |
935 | When booting from an image template, the system boots first into |
936 | a "provisioning" mode. There, host <-> guest mechanisms are utilized |
937 | - to execute code in the guest and provision it. |
938 | + to execute code in the guest and configure it. The configuration |
939 | + includes configuring the system network and possibly installing |
940 | + packages and other software stack. |
941 | |
942 | - Cloud-init will disable itself when it detects that it is in the |
943 | - provisioning mode. It detects this by the presence of |
944 | - a file '/root/provisioningConfiguration.cfg'. |
945 | - |
946 | - When provided with user-data, the "first boot" will contain a |
947 | - ConfigDrive-like disk labeled with 'METADATA'. If there is no user-data |
948 | - provided, then there is no data-source. |
949 | - |
950 | - Cloud-init never does any network configuration in this mode. |
951 | + After the provisioning is finished, the system reboots. |
952 | |
953 | * os_code: Essentially "launch by OS Code" (Operating System Code). |
954 | This is a more modern approach. There is no specific "provisioning" boot. |
955 | @@ -30,11 +24,73 @@ There are 2 different api exposed launch methods. |
956 | mean that 1 in 8^16 (~4 billion) Xen ConfigDrive systems will be |
957 | incorrectly identified as IBMCloud. |
958 | |
959 | +The combination of these 2 launch methods and with or without user-data |
960 | +creates 6 boot scenarios. |
961 | + A. os_code with user-data |
962 | + B. os_code without user-data |
963 | + Cloud-init is fully operational in this mode. |
964 | + |
965 | + There is a block device attached with label 'config-2'. |
966 | + As it differs from OpenStack's config-2, we have to differentiate. |
967 | + We do so by requiring the UUID on the filesystem to be "9796-932E". |
968 | + |
969 | + This disk will have the following files. Specifically note, there |
970 | + is no versioned path to the meta-data, only 'latest': |
971 | + openstack/latest/meta_data.json |
972 | + openstack/latest/network_data.json |
973 | + openstack/latest/user_data [optional] |
974 | + openstack/latest/vendor_data.json |
975 | + |
976 | + vendor_data.json as of 2018-04 looks like this: |
977 | + {"cloud-init":"#!/bin/bash\necho 'root:$6$<snip>' | chpasswd -e"} |
978 | + |
979 | + The only difference between A and B in this mode is the presence |
980 | + of user_data on the config disk. |
981 | + |
982 | + C. template, provisioning boot with user-data |
983 | + D. template, provisioning boot without user-data. |
984 | + With ds-identify cloud-init is fully disabled in this mode. |
985 | + Without ds-identify, cloud-init None datasource will be used. |
986 | + |
987 | + This is currently identified by the presence of |
988 | + /root/provisioningConfiguration.cfg . That file is placed into the |
989 | + system before it is booted. |
990 | + |
991 | + The difference between C and D is the presence of the METADATA disk |
992 | + as described in E below. There is no METADATA disk attached unless |
993 | + user-data is provided. |
994 | + |
995 | + E. template, post-provisioning boot with user-data. |
996 | + Cloud-init is fully operational in this mode. |
997 | + |
998 | + This is identified by a block device with filesystem label "METADATA". |
999 | + The looks similar to a version-1 OpenStack config drive. It will |
1000 | + have the following files: |
1001 | + |
1002 | + openstack/latest/user_data |
1003 | + openstack/latest/meta_data.json |
1004 | + openstack/content/interfaces |
1005 | + meta.js |
1006 | + |
1007 | + meta.js contains something similar to user_data. cloud-init ignores it. |
1008 | + cloud-init ignores the 'interfaces' style file here. |
1009 | + In this mode, cloud-init has networking code disabled. It relies |
1010 | + on the provisioning boot to have configured networking. |
1011 | + |
1012 | + F. template, post-provisioning boot without user-data. |
1013 | + With ds-identify, cloud-init will be fully disabled. |
1014 | + Without ds-identify, cloud-init None datasource will be used. |
1015 | + |
1016 | + There is no information available to identify this scenario. |
1017 | + |
1018 | + The user will be able to ssh in as as root with their public keys that |
1019 | + have been installed into /root/ssh/.authorized_keys |
1020 | + during the provisioning stage. |
1021 | + |
1022 | TODO: |
1023 | * is uuid (/sys/hypervisor/uuid) stable for life of an instance? |
1024 | it seems it is not the same as data's uuid in the os_code case |
1025 | but is in the template case. |
1026 | - |
1027 | """ |
1028 | import base64 |
1029 | import json |
1030 | @@ -138,8 +194,30 @@ def _is_xen(): |
1031 | return os.path.exists("/proc/xen") |
1032 | |
1033 | |
1034 | -def _is_ibm_provisioning(): |
1035 | - return os.path.exists("/root/provisioningConfiguration.cfg") |
1036 | +def _is_ibm_provisioning( |
1037 | + prov_cfg="/root/provisioningConfiguration.cfg", |
1038 | + inst_log="/root/swinstall.log", |
1039 | + boot_ref="/proc/1/environ"): |
1040 | + """Return boolean indicating if this boot is ibm provisioning boot.""" |
1041 | + if os.path.exists(prov_cfg): |
1042 | + msg = "config '%s' exists." % prov_cfg |
1043 | + result = True |
1044 | + if os.path.exists(inst_log): |
1045 | + if os.path.exists(boot_ref): |
1046 | + result = (os.stat(inst_log).st_mtime > |
1047 | + os.stat(boot_ref).st_mtime) |
1048 | + msg += (" log '%s' from %s boot." % |
1049 | + (inst_log, "current" if result else "previous")) |
1050 | + else: |
1051 | + msg += (" log '%s' existed, but no reference file '%s'." % |
1052 | + (inst_log, boot_ref)) |
1053 | + result = False |
1054 | + else: |
1055 | + msg += " log '%s' did not exist." % inst_log |
1056 | + else: |
1057 | + result, msg = (False, "config '%s' did not exist." % prov_cfg) |
1058 | + LOG.debug("ibm_provisioning=%s: %s", result, msg) |
1059 | + return result |
1060 | |
1061 | |
1062 | def get_ibm_platform(): |
1063 | @@ -189,7 +267,7 @@ def get_ibm_platform(): |
1064 | else: |
1065 | return (Platforms.TEMPLATE_LIVE_METADATA, metadata_path) |
1066 | elif _is_ibm_provisioning(): |
1067 | - return (Platforms.TEMPLATE_PROVISIONING_NODATA, None) |
1068 | + return (Platforms.TEMPLATE_PROVISIONING_NODATA, None) |
1069 | return not_found |
1070 | |
1071 | |
1072 | diff --git a/cloudinit/sources/DataSourceMAAS.py b/cloudinit/sources/DataSourceMAAS.py |
1073 | index 6ac8863..aa56add 100644 |
1074 | --- a/cloudinit/sources/DataSourceMAAS.py |
1075 | +++ b/cloudinit/sources/DataSourceMAAS.py |
1076 | @@ -204,7 +204,7 @@ def read_maas_seed_url(seed_url, read_file_or_url=None, timeout=None, |
1077 | seed_url = seed_url[:-1] |
1078 | |
1079 | md = {} |
1080 | - for path, dictname, binary, optional in DS_FIELDS: |
1081 | + for path, _dictname, binary, optional in DS_FIELDS: |
1082 | if version is None: |
1083 | url = "%s/%s" % (seed_url, path) |
1084 | else: |
1085 | diff --git a/cloudinit/sources/DataSourceOVF.py b/cloudinit/sources/DataSourceOVF.py |
1086 | index dc914a7..178ccb0 100644 |
1087 | --- a/cloudinit/sources/DataSourceOVF.py |
1088 | +++ b/cloudinit/sources/DataSourceOVF.py |
1089 | @@ -556,7 +556,7 @@ def search_file(dirpath, filename): |
1090 | if not dirpath or not filename: |
1091 | return None |
1092 | |
1093 | - for root, dirs, files in os.walk(dirpath): |
1094 | + for root, _dirs, files in os.walk(dirpath): |
1095 | if filename in files: |
1096 | return os.path.join(root, filename) |
1097 | |
1098 | diff --git a/cloudinit/sources/DataSourceOpenStack.py b/cloudinit/sources/DataSourceOpenStack.py |
1099 | index e55a763..fb166ae 100644 |
1100 | --- a/cloudinit/sources/DataSourceOpenStack.py |
1101 | +++ b/cloudinit/sources/DataSourceOpenStack.py |
1102 | @@ -86,7 +86,7 @@ class DataSourceOpenStack(openstack.SourceMixin, sources.DataSource): |
1103 | md_urls.append(md_url) |
1104 | url2base[md_url] = url |
1105 | |
1106 | - (max_wait, timeout, retries) = self._get_url_settings() |
1107 | + (max_wait, timeout, _retries) = self._get_url_settings() |
1108 | start_time = time.time() |
1109 | avail_url = url_helper.wait_for_url(urls=md_urls, max_wait=max_wait, |
1110 | timeout=timeout) |
1111 | @@ -106,7 +106,7 @@ class DataSourceOpenStack(openstack.SourceMixin, sources.DataSource): |
1112 | except IOError: |
1113 | return False |
1114 | |
1115 | - (max_wait, timeout, retries) = self._get_url_settings() |
1116 | + (_max_wait, timeout, retries) = self._get_url_settings() |
1117 | |
1118 | try: |
1119 | results = util.log_time(LOG.debug, |
1120 | diff --git a/cloudinit/sources/DataSourceSmartOS.py b/cloudinit/sources/DataSourceSmartOS.py |
1121 | index c8998b4..4ea00eb 100644 |
1122 | --- a/cloudinit/sources/DataSourceSmartOS.py |
1123 | +++ b/cloudinit/sources/DataSourceSmartOS.py |
1124 | @@ -11,7 +11,7 @@ |
1125 | # SmartOS hosts use a serial console (/dev/ttyS1) on KVM Linux Guests |
1126 | # The meta-data is transmitted via key/value pairs made by |
1127 | # requests on the console. For example, to get the hostname, you |
1128 | -# would send "GET hostname" on /dev/ttyS1. |
1129 | +# would send "GET sdc:hostname" on /dev/ttyS1. |
1130 | # For Linux Guests running in LX-Brand Zones on SmartOS hosts |
1131 | # a socket (/native/.zonecontrol/metadata.sock) is used instead |
1132 | # of a serial console. |
1133 | @@ -23,6 +23,7 @@ |
1134 | import base64 |
1135 | import binascii |
1136 | import errno |
1137 | +import fcntl |
1138 | import json |
1139 | import os |
1140 | import random |
1141 | @@ -273,8 +274,14 @@ class DataSourceSmartOS(sources.DataSource): |
1142 | write_boot_content(u_data, u_data_f) |
1143 | |
1144 | # Handle the cloud-init regular meta |
1145 | + |
1146 | + # The hostname may or may not be qualified with the local domain name. |
1147 | + # This follows section 3.14 of RFC 2132. |
1148 | if not md['local-hostname']: |
1149 | - md['local-hostname'] = md['instance-id'] |
1150 | + if md['hostname']: |
1151 | + md['local-hostname'] = md['hostname'] |
1152 | + else: |
1153 | + md['local-hostname'] = md['instance-id'] |
1154 | |
1155 | ud = None |
1156 | if md['user-data']: |
1157 | @@ -455,9 +462,9 @@ class JoyentMetadataClient(object): |
1158 | |
1159 | def list(self): |
1160 | result = self.request(rtype='KEYS') |
1161 | - if result: |
1162 | - result = result.split('\n') |
1163 | - return result |
1164 | + if not result: |
1165 | + return [] |
1166 | + return result.split('\n') |
1167 | |
1168 | def put(self, key, val): |
1169 | param = b' '.join([base64.b64encode(i.encode()) |
1170 | @@ -520,6 +527,7 @@ class JoyentMetadataSerialClient(JoyentMetadataClient): |
1171 | if not ser.isOpen(): |
1172 | raise SystemError("Unable to open %s" % self.device) |
1173 | self.fp = ser |
1174 | + fcntl.lockf(ser, fcntl.LOCK_EX) |
1175 | self._flush() |
1176 | self._negotiate() |
1177 | |
1178 | diff --git a/cloudinit/sources/helpers/digitalocean.py b/cloudinit/sources/helpers/digitalocean.py |
1179 | index 693f8d5..0e7ccca 100644 |
1180 | --- a/cloudinit/sources/helpers/digitalocean.py |
1181 | +++ b/cloudinit/sources/helpers/digitalocean.py |
1182 | @@ -41,10 +41,9 @@ def assign_ipv4_link_local(nic=None): |
1183 | "address") |
1184 | |
1185 | try: |
1186 | - (result, _err) = util.subp(ip_addr_cmd) |
1187 | + util.subp(ip_addr_cmd) |
1188 | LOG.debug("assigned ip4LL address '%s' to '%s'", addr, nic) |
1189 | - |
1190 | - (result, _err) = util.subp(ip_link_cmd) |
1191 | + util.subp(ip_link_cmd) |
1192 | LOG.debug("brought device '%s' up", nic) |
1193 | except Exception: |
1194 | util.logexc(LOG, "ip4LL address assignment of '%s' to '%s' failed." |
1195 | @@ -75,7 +74,7 @@ def del_ipv4_link_local(nic=None): |
1196 | ip_addr_cmd = ['ip', 'addr', 'flush', 'dev', nic] |
1197 | |
1198 | try: |
1199 | - (result, _err) = util.subp(ip_addr_cmd) |
1200 | + util.subp(ip_addr_cmd) |
1201 | LOG.debug("removed ip4LL addresses from %s", nic) |
1202 | |
1203 | except Exception as e: |
1204 | diff --git a/cloudinit/sources/helpers/openstack.py b/cloudinit/sources/helpers/openstack.py |
1205 | index 26f3168..a4cf066 100644 |
1206 | --- a/cloudinit/sources/helpers/openstack.py |
1207 | +++ b/cloudinit/sources/helpers/openstack.py |
1208 | @@ -638,7 +638,7 @@ def convert_net_json(network_json=None, known_macs=None): |
1209 | known_macs = net.get_interfaces_by_mac() |
1210 | |
1211 | # go through and fill out the link_id_info with names |
1212 | - for link_id, info in link_id_info.items(): |
1213 | + for _link_id, info in link_id_info.items(): |
1214 | if info.get('name'): |
1215 | continue |
1216 | if info.get('mac') in known_macs: |
1217 | diff --git a/cloudinit/sources/helpers/vmware/imc/config_nic.py b/cloudinit/sources/helpers/vmware/imc/config_nic.py |
1218 | index 2d8900e..3ef8c62 100644 |
1219 | --- a/cloudinit/sources/helpers/vmware/imc/config_nic.py |
1220 | +++ b/cloudinit/sources/helpers/vmware/imc/config_nic.py |
1221 | @@ -73,7 +73,7 @@ class NicConfigurator(object): |
1222 | The mac address(es) are in the lower case |
1223 | """ |
1224 | cmd = ['ip', 'addr', 'show'] |
1225 | - (output, err) = util.subp(cmd) |
1226 | + output, _err = util.subp(cmd) |
1227 | sections = re.split(r'\n\d+: ', '\n' + output)[1:] |
1228 | |
1229 | macPat = r'link/ether (([0-9A-Fa-f]{2}[:]){5}([0-9A-Fa-f]{2}))' |
1230 | diff --git a/cloudinit/sources/helpers/vmware/imc/config_passwd.py b/cloudinit/sources/helpers/vmware/imc/config_passwd.py |
1231 | index 75cfbaa..8c91fa4 100644 |
1232 | --- a/cloudinit/sources/helpers/vmware/imc/config_passwd.py |
1233 | +++ b/cloudinit/sources/helpers/vmware/imc/config_passwd.py |
1234 | @@ -56,10 +56,10 @@ class PasswordConfigurator(object): |
1235 | LOG.info('Expiring password.') |
1236 | for user in uidUserList: |
1237 | try: |
1238 | - out, err = util.subp(['passwd', '--expire', user]) |
1239 | + util.subp(['passwd', '--expire', user]) |
1240 | except util.ProcessExecutionError as e: |
1241 | if os.path.exists('/usr/bin/chage'): |
1242 | - out, e = util.subp(['chage', '-d', '0', user]) |
1243 | + util.subp(['chage', '-d', '0', user]) |
1244 | else: |
1245 | LOG.warning('Failed to expire password for %s with error: ' |
1246 | '%s', user, e) |
1247 | diff --git a/cloudinit/sources/helpers/vmware/imc/guestcust_util.py b/cloudinit/sources/helpers/vmware/imc/guestcust_util.py |
1248 | index 4407525..a590f32 100644 |
1249 | --- a/cloudinit/sources/helpers/vmware/imc/guestcust_util.py |
1250 | +++ b/cloudinit/sources/helpers/vmware/imc/guestcust_util.py |
1251 | @@ -91,7 +91,7 @@ def enable_nics(nics): |
1252 | |
1253 | for attempt in range(0, enableNicsWaitRetries): |
1254 | logger.debug("Trying to connect interfaces, attempt %d", attempt) |
1255 | - (out, err) = set_customization_status( |
1256 | + (out, _err) = set_customization_status( |
1257 | GuestCustStateEnum.GUESTCUST_STATE_RUNNING, |
1258 | GuestCustEventEnum.GUESTCUST_EVENT_ENABLE_NICS, |
1259 | nics) |
1260 | @@ -104,7 +104,7 @@ def enable_nics(nics): |
1261 | return |
1262 | |
1263 | for count in range(0, enableNicsWaitCount): |
1264 | - (out, err) = set_customization_status( |
1265 | + (out, _err) = set_customization_status( |
1266 | GuestCustStateEnum.GUESTCUST_STATE_RUNNING, |
1267 | GuestCustEventEnum.GUESTCUST_EVENT_QUERY_NICS, |
1268 | nics) |
1269 | diff --git a/cloudinit/sources/tests/test_init.py b/cloudinit/sources/tests/test_init.py |
1270 | index e7fda22..452e921 100644 |
1271 | --- a/cloudinit/sources/tests/test_init.py |
1272 | +++ b/cloudinit/sources/tests/test_init.py |
1273 | @@ -278,7 +278,7 @@ class TestDataSource(CiTestCase): |
1274 | base_args = get_args(DataSource.get_hostname) # pylint: disable=W1505 |
1275 | # Import all DataSource subclasses so we can inspect them. |
1276 | modules = util.find_modules(os.path.dirname(os.path.dirname(__file__))) |
1277 | - for loc, name in modules.items(): |
1278 | + for _loc, name in modules.items(): |
1279 | mod_locs, _ = importer.find_module(name, ['cloudinit.sources'], []) |
1280 | if mod_locs: |
1281 | importer.import_module(mod_locs[0]) |
1282 | diff --git a/cloudinit/ssh_util.py b/cloudinit/ssh_util.py |
1283 | index 882517f..73c3177 100644 |
1284 | --- a/cloudinit/ssh_util.py |
1285 | +++ b/cloudinit/ssh_util.py |
1286 | @@ -279,24 +279,28 @@ class SshdConfigLine(object): |
1287 | |
1288 | |
1289 | def parse_ssh_config(fname): |
1290 | + if not os.path.isfile(fname): |
1291 | + return [] |
1292 | + return parse_ssh_config_lines(util.load_file(fname).splitlines()) |
1293 | + |
1294 | + |
1295 | +def parse_ssh_config_lines(lines): |
1296 | # See: man sshd_config |
1297 | # The file contains keyword-argument pairs, one per line. |
1298 | # Lines starting with '#' and empty lines are interpreted as comments. |
1299 | # Note: key-words are case-insensitive and arguments are case-sensitive |
1300 | - lines = [] |
1301 | - if not os.path.isfile(fname): |
1302 | - return lines |
1303 | - for line in util.load_file(fname).splitlines(): |
1304 | + ret = [] |
1305 | + for line in lines: |
1306 | line = line.strip() |
1307 | if not line or line.startswith("#"): |
1308 | - lines.append(SshdConfigLine(line)) |
1309 | + ret.append(SshdConfigLine(line)) |
1310 | continue |
1311 | try: |
1312 | key, val = line.split(None, 1) |
1313 | except ValueError: |
1314 | key, val = line.split('=', 1) |
1315 | - lines.append(SshdConfigLine(line, key, val)) |
1316 | - return lines |
1317 | + ret.append(SshdConfigLine(line, key, val)) |
1318 | + return ret |
1319 | |
1320 | |
1321 | def parse_ssh_config_map(fname): |
1322 | @@ -310,4 +314,56 @@ def parse_ssh_config_map(fname): |
1323 | ret[line.key] = line.value |
1324 | return ret |
1325 | |
1326 | + |
1327 | +def update_ssh_config(updates, fname=DEF_SSHD_CFG): |
1328 | + """Read fname, and update if changes are necessary. |
1329 | + |
1330 | + @param updates: dictionary of desired values {Option: value} |
1331 | + @return: boolean indicating if an update was done.""" |
1332 | + lines = parse_ssh_config(fname) |
1333 | + changed = update_ssh_config_lines(lines=lines, updates=updates) |
1334 | + if changed: |
1335 | + util.write_file( |
1336 | + fname, "\n".join([str(l) for l in lines]) + "\n", copy_mode=True) |
1337 | + return len(changed) != 0 |
1338 | + |
1339 | + |
1340 | +def update_ssh_config_lines(lines, updates): |
1341 | + """Update the ssh config lines per updates. |
1342 | + |
1343 | + @param lines: array of SshdConfigLine. This array is updated in place. |
1344 | + @param updates: dictionary of desired values {Option: value} |
1345 | + @return: A list of keys in updates that were changed.""" |
1346 | + found = set() |
1347 | + changed = [] |
1348 | + |
1349 | + # Keywords are case-insensitive and arguments are case-sensitive |
1350 | + casemap = dict([(k.lower(), k) for k in updates.keys()]) |
1351 | + |
1352 | + for (i, line) in enumerate(lines, start=1): |
1353 | + if not line.key: |
1354 | + continue |
1355 | + if line.key in casemap: |
1356 | + key = casemap[line.key] |
1357 | + value = updates[key] |
1358 | + found.add(key) |
1359 | + if line.value == value: |
1360 | + LOG.debug("line %d: option %s already set to %s", |
1361 | + i, key, value) |
1362 | + else: |
1363 | + changed.append(key) |
1364 | + LOG.debug("line %d: option %s updated %s -> %s", i, |
1365 | + key, line.value, value) |
1366 | + line.value = value |
1367 | + |
1368 | + if len(found) != len(updates): |
1369 | + for key, value in updates.items(): |
1370 | + if key in found: |
1371 | + continue |
1372 | + changed.append(key) |
1373 | + lines.append(SshdConfigLine('', key, value)) |
1374 | + LOG.debug("line %d: option %s added with %s", |
1375 | + len(lines), key, value) |
1376 | + return changed |
1377 | + |
1378 | # vi: ts=4 expandtab |
1379 | diff --git a/cloudinit/templater.py b/cloudinit/templater.py |
1380 | index 9a087e1..7e7acb8 100644 |
1381 | --- a/cloudinit/templater.py |
1382 | +++ b/cloudinit/templater.py |
1383 | @@ -147,7 +147,7 @@ def render_string(content, params): |
1384 | Warning: py2 str with non-ascii chars will cause UnicodeDecodeError.""" |
1385 | if not params: |
1386 | params = {} |
1387 | - template_type, renderer, content = detect_template(content) |
1388 | + _template_type, renderer, content = detect_template(content) |
1389 | return renderer(content, params) |
1390 | |
1391 | # vi: ts=4 expandtab |
1392 | diff --git a/cloudinit/tests/helpers.py b/cloudinit/tests/helpers.py |
1393 | index 82fd347..117a9cf 100644 |
1394 | --- a/cloudinit/tests/helpers.py |
1395 | +++ b/cloudinit/tests/helpers.py |
1396 | @@ -8,6 +8,7 @@ import os |
1397 | import shutil |
1398 | import sys |
1399 | import tempfile |
1400 | +import time |
1401 | import unittest |
1402 | |
1403 | import mock |
1404 | @@ -24,6 +25,8 @@ try: |
1405 | except ImportError: |
1406 | from ConfigParser import ConfigParser |
1407 | |
1408 | +from cloudinit.config.schema import ( |
1409 | + SchemaValidationError, validate_cloudconfig_schema) |
1410 | from cloudinit import helpers as ch |
1411 | from cloudinit import util |
1412 | |
1413 | @@ -261,7 +264,8 @@ class FilesystemMockingTestCase(ResourceUsingTestCase): |
1414 | os.path: [('isfile', 1), ('exists', 1), |
1415 | ('islink', 1), ('isdir', 1), ('lexists', 1)], |
1416 | os: [('listdir', 1), ('mkdir', 1), |
1417 | - ('lstat', 1), ('symlink', 2)] |
1418 | + ('lstat', 1), ('symlink', 2), |
1419 | + ('stat', 1)] |
1420 | } |
1421 | |
1422 | if hasattr(os, 'scandir'): |
1423 | @@ -312,6 +316,23 @@ class HttprettyTestCase(CiTestCase): |
1424 | super(HttprettyTestCase, self).tearDown() |
1425 | |
1426 | |
1427 | +class SchemaTestCaseMixin(unittest2.TestCase): |
1428 | + |
1429 | + def assertSchemaValid(self, cfg, msg="Valid Schema failed validation."): |
1430 | + """Assert the config is valid per self.schema. |
1431 | + |
1432 | + If there is only one top level key in the schema properties, then |
1433 | + the cfg will be put under that key.""" |
1434 | + props = list(self.schema.get('properties')) |
1435 | + # put cfg under top level key if there is only one in the schema |
1436 | + if len(props) == 1: |
1437 | + cfg = {props[0]: cfg} |
1438 | + try: |
1439 | + validate_cloudconfig_schema(cfg, self.schema, strict=True) |
1440 | + except SchemaValidationError: |
1441 | + self.fail(msg) |
1442 | + |
1443 | + |
1444 | def populate_dir(path, files): |
1445 | if not os.path.exists(path): |
1446 | os.makedirs(path) |
1447 | @@ -330,11 +351,20 @@ def populate_dir(path, files): |
1448 | return ret |
1449 | |
1450 | |
1451 | +def populate_dir_with_ts(path, data): |
1452 | + """data is {'file': ('contents', mtime)}. mtime relative to now.""" |
1453 | + populate_dir(path, dict((k, v[0]) for k, v in data.items())) |
1454 | + btime = time.time() |
1455 | + for fpath, (_contents, mtime) in data.items(): |
1456 | + ts = btime + mtime if mtime else btime |
1457 | + os.utime(os.path.sep.join((path, fpath)), (ts, ts)) |
1458 | + |
1459 | + |
1460 | def dir2dict(startdir, prefix=None): |
1461 | flist = {} |
1462 | if prefix is None: |
1463 | prefix = startdir |
1464 | - for root, dirs, files in os.walk(startdir): |
1465 | + for root, _dirs, files in os.walk(startdir): |
1466 | for fname in files: |
1467 | fpath = os.path.join(root, fname) |
1468 | key = fpath[len(prefix):] |
1469 | diff --git a/cloudinit/tests/test_util.py b/cloudinit/tests/test_util.py |
1470 | index 3f37dbb..3c05a43 100644 |
1471 | --- a/cloudinit/tests/test_util.py |
1472 | +++ b/cloudinit/tests/test_util.py |
1473 | @@ -135,7 +135,7 @@ class TestGetHostnameFqdn(CiTestCase): |
1474 | def test_get_hostname_fqdn_from_passes_metadata_only_to_cloud(self): |
1475 | """Calls to cloud.get_hostname pass the metadata_only parameter.""" |
1476 | mycloud = FakeCloud('cloudhost', 'cloudhost.mycloud.com') |
1477 | - hostname, fqdn = util.get_hostname_fqdn( |
1478 | + _hn, _fqdn = util.get_hostname_fqdn( |
1479 | cfg={}, cloud=mycloud, metadata_only=True) |
1480 | self.assertEqual( |
1481 | [{'fqdn': True, 'metadata_only': True}, |
1482 | @@ -212,4 +212,53 @@ class TestBlkid(CiTestCase): |
1483 | capture=True, decode="replace") |
1484 | |
1485 | |
1486 | +@mock.patch('cloudinit.util.subp') |
1487 | +class TestUdevadmSettle(CiTestCase): |
1488 | + def test_with_no_params(self, m_subp): |
1489 | + """called with no parameters.""" |
1490 | + util.udevadm_settle() |
1491 | + m_subp.called_once_with(mock.call(['udevadm', 'settle'])) |
1492 | + |
1493 | + def test_with_exists_and_not_exists(self, m_subp): |
1494 | + """with exists=file where file does not exist should invoke subp.""" |
1495 | + mydev = self.tmp_path("mydev") |
1496 | + util.udevadm_settle(exists=mydev) |
1497 | + m_subp.called_once_with( |
1498 | + ['udevadm', 'settle', '--exit-if-exists=%s' % mydev]) |
1499 | + |
1500 | + def test_with_exists_and_file_exists(self, m_subp): |
1501 | + """with exists=file where file does exist should not invoke subp.""" |
1502 | + mydev = self.tmp_path("mydev") |
1503 | + util.write_file(mydev, "foo\n") |
1504 | + util.udevadm_settle(exists=mydev) |
1505 | + self.assertIsNone(m_subp.call_args) |
1506 | + |
1507 | + def test_with_timeout_int(self, m_subp): |
1508 | + """timeout can be an integer.""" |
1509 | + timeout = 9 |
1510 | + util.udevadm_settle(timeout=timeout) |
1511 | + m_subp.called_once_with( |
1512 | + ['udevadm', 'settle', '--timeout=%s' % timeout]) |
1513 | + |
1514 | + def test_with_timeout_string(self, m_subp): |
1515 | + """timeout can be a string.""" |
1516 | + timeout = "555" |
1517 | + util.udevadm_settle(timeout=timeout) |
1518 | + m_subp.assert_called_once_with( |
1519 | + ['udevadm', 'settle', '--timeout=%s' % timeout]) |
1520 | + |
1521 | + def test_with_exists_and_timeout(self, m_subp): |
1522 | + """test call with both exists and timeout.""" |
1523 | + mydev = self.tmp_path("mydev") |
1524 | + timeout = "3" |
1525 | + util.udevadm_settle(exists=mydev) |
1526 | + m_subp.called_once_with( |
1527 | + ['udevadm', 'settle', '--exit-if-exists=%s' % mydev, |
1528 | + '--timeout=%s' % timeout]) |
1529 | + |
1530 | + def test_subp_exception_raises_to_caller(self, m_subp): |
1531 | + m_subp.side_effect = util.ProcessExecutionError("BOOM") |
1532 | + self.assertRaises(util.ProcessExecutionError, util.udevadm_settle) |
1533 | + |
1534 | + |
1535 | # vi: ts=4 expandtab |
1536 | diff --git a/cloudinit/url_helper.py b/cloudinit/url_helper.py |
1537 | index 03a573a..1de07b1 100644 |
1538 | --- a/cloudinit/url_helper.py |
1539 | +++ b/cloudinit/url_helper.py |
1540 | @@ -519,7 +519,7 @@ def oauth_headers(url, consumer_key, token_key, token_secret, consumer_secret, |
1541 | resource_owner_secret=token_secret, |
1542 | signature_method=oauth1.SIGNATURE_PLAINTEXT, |
1543 | timestamp=timestamp) |
1544 | - uri, signed_headers, body = client.sign(url) |
1545 | + _uri, signed_headers, _body = client.sign(url) |
1546 | return signed_headers |
1547 | |
1548 | # vi: ts=4 expandtab |
1549 | diff --git a/cloudinit/util.py b/cloudinit/util.py |
1550 | index 1717b52..2828ca3 100644 |
1551 | --- a/cloudinit/util.py |
1552 | +++ b/cloudinit/util.py |
1553 | @@ -2214,7 +2214,7 @@ def parse_mtab(path): |
1554 | def find_freebsd_part(label_part): |
1555 | if label_part.startswith("/dev/label/"): |
1556 | target_label = label_part[5:] |
1557 | - (label_part, err) = subp(['glabel', 'status', '-s']) |
1558 | + (label_part, _err) = subp(['glabel', 'status', '-s']) |
1559 | for labels in label_part.split("\n"): |
1560 | items = labels.split() |
1561 | if len(items) > 0 and items[0].startswith(target_label): |
1562 | @@ -2727,4 +2727,19 @@ def mount_is_read_write(mount_point): |
1563 | mount_opts = result[-1].split(',') |
1564 | return mount_opts[0] == 'rw' |
1565 | |
1566 | + |
1567 | +def udevadm_settle(exists=None, timeout=None): |
1568 | + """Invoke udevadm settle with optional exists and timeout parameters""" |
1569 | + settle_cmd = ["udevadm", "settle"] |
1570 | + if exists: |
1571 | + # skip the settle if the requested path already exists |
1572 | + if os.path.exists(exists): |
1573 | + return |
1574 | + settle_cmd.extend(['--exit-if-exists=%s' % exists]) |
1575 | + if timeout: |
1576 | + settle_cmd.extend(['--timeout=%s' % timeout]) |
1577 | + |
1578 | + return subp(settle_cmd) |
1579 | + |
1580 | + |
1581 | # vi: ts=4 expandtab |
1582 | diff --git a/debian/changelog b/debian/changelog |
1583 | index 45016a5..7199b4f 100644 |
1584 | --- a/debian/changelog |
1585 | +++ b/debian/changelog |
1586 | @@ -1,3 +1,28 @@ |
1587 | +cloud-init (18.2-27-g6ef92c98-0ubuntu1) bionic; urgency=medium |
1588 | + |
1589 | + * New upstream snapshot. |
1590 | + - IBMCloud: recognize provisioning environment during debug boots. |
1591 | + (LP: #1767166) |
1592 | + - net: detect unstable network names and trigger a settle if needed |
1593 | + (LP: #1766287) |
1594 | + - IBMCloud: improve documentation in datasource. |
1595 | + - sysconfig: dhcp6 subnet type should not imply dhcpv4 [Vitaly Kuznetsov] |
1596 | + - packages/debian/control.in: add missing dependency on iproute2. |
1597 | + (LP: #1766711) |
1598 | + - DataSourceSmartOS: add locking of serial device. |
1599 | + [Mike Gerdts] (LP: #1746605) |
1600 | + - DataSourceSmartOS: sdc:hostname is ignored [Mike Gerdts] (LP: #1765085) |
1601 | + - DataSourceSmartOS: list() should always return a list |
1602 | + [Mike Gerdts] (LP: #1763480) |
1603 | + - schema: in validation, raise ImportError if strict but no jsonschema. |
1604 | + - set_passwords: Add newline to end of sshd config, only restart if |
1605 | + updated. (LP: #1677205) |
1606 | + - pylint: pay attention to unused variable warnings. |
1607 | + - doc: Add documentation for AliYun datasource. [Junjie Wang] |
1608 | + - Schema: do not warn on duplicate items in commands. (LP: #1764264) |
1609 | + |
1610 | + -- Ryan Harper <ryan.harper@canonical.com> Thu, 26 Apr 2018 16:33:59 -0500 |
1611 | + |
1612 | cloud-init (18.2-14-g6d48d265-0ubuntu1) bionic; urgency=medium |
1613 | |
1614 | * New upstream snapshot. |
1615 | diff --git a/doc/rtd/topics/datasources.rst b/doc/rtd/topics/datasources.rst |
1616 | index 7e2854d..38ba75d 100644 |
1617 | --- a/doc/rtd/topics/datasources.rst |
1618 | +++ b/doc/rtd/topics/datasources.rst |
1619 | @@ -80,6 +80,7 @@ Follow for more information. |
1620 | .. toctree:: |
1621 | :maxdepth: 2 |
1622 | |
1623 | + datasources/aliyun.rst |
1624 | datasources/altcloud.rst |
1625 | datasources/azure.rst |
1626 | datasources/cloudsigma.rst |
1627 | diff --git a/doc/rtd/topics/datasources/aliyun.rst b/doc/rtd/topics/datasources/aliyun.rst |
1628 | new file mode 100644 |
1629 | index 0000000..3f4f40c |
1630 | --- /dev/null |
1631 | +++ b/doc/rtd/topics/datasources/aliyun.rst |
1632 | @@ -0,0 +1,74 @@ |
1633 | +.. _datasource_aliyun: |
1634 | + |
1635 | +Alibaba Cloud (AliYun) |
1636 | +====================== |
1637 | +The ``AliYun`` datasource reads data from Alibaba Cloud ECS. Support is |
1638 | +present in cloud-init since 0.7.9. |
1639 | + |
1640 | +Metadata Service |
1641 | +---------------- |
1642 | +The Alibaba Cloud metadata service is available at the well known url |
1643 | +``http://100.100.100.200/``. For more information see |
1644 | +Alibaba Cloud ECS on `metadata |
1645 | +<https://www.alibabacloud.com/help/zh/faq-detail/49122.htm>`__. |
1646 | + |
1647 | +Versions |
1648 | +^^^^^^^^ |
1649 | +Like the EC2 metadata service, Alibaba Cloud's metadata service provides |
1650 | +versioned data under specific paths. As of April 2018, there are only |
1651 | +``2016-01-01`` and ``latest`` versions. |
1652 | + |
1653 | +It is expected that the dated version will maintain a stable interface but |
1654 | +``latest`` may change content at a future date. |
1655 | + |
1656 | +Cloud-init uses the ``2016-01-01`` version. |
1657 | + |
1658 | +You can list the versions available to your instance with: |
1659 | + |
1660 | +.. code-block:: shell-session |
1661 | + |
1662 | + $ curl http://100.100.100.200/ |
1663 | + 2016-01-01 |
1664 | + latest |
1665 | + |
1666 | +Metadata |
1667 | +^^^^^^^^ |
1668 | +Instance metadata can be queried at |
1669 | +``http://100.100.100.200/2016-01-01/meta-data`` |
1670 | + |
1671 | +.. code-block:: shell-session |
1672 | + |
1673 | + $ curl http://100.100.100.200/2016-01-01/meta-data |
1674 | + dns-conf/ |
1675 | + eipv4 |
1676 | + hostname |
1677 | + image-id |
1678 | + instance-id |
1679 | + instance/ |
1680 | + mac |
1681 | + network-type |
1682 | + network/ |
1683 | + ntp-conf/ |
1684 | + owner-account-id |
1685 | + private-ipv4 |
1686 | + public-keys/ |
1687 | + region-id |
1688 | + serial-number |
1689 | + source-address |
1690 | + sub-private-ipv4-list |
1691 | + vpc-cidr-block |
1692 | + vpc-id |
1693 | + |
1694 | +Userdata |
1695 | +^^^^^^^^ |
1696 | +If provided, user-data will appear at |
1697 | +``http://100.100.100.200/2016-01-01/user-data``. |
1698 | +If no user-data is provided, this will return a 404. |
1699 | + |
1700 | +.. code-block:: shell-session |
1701 | + |
1702 | + $ curl http://100.100.100.200/2016-01-01/user-data |
1703 | + #!/bin/sh |
1704 | + echo "Hello World." |
1705 | + |
1706 | +.. vi: textwidth=78 |
1707 | diff --git a/packages/debian/control.in b/packages/debian/control.in |
1708 | index 46da6df..e9ed64f 100644 |
1709 | --- a/packages/debian/control.in |
1710 | +++ b/packages/debian/control.in |
1711 | @@ -11,6 +11,7 @@ Package: cloud-init |
1712 | Architecture: all |
1713 | Depends: ${misc:Depends}, |
1714 | ${${python}:Depends}, |
1715 | + iproute2, |
1716 | isc-dhcp-client |
1717 | Recommends: eatmydata, sudo, software-properties-common, gdisk |
1718 | XB-Python-Version: ${python:Versions} |
1719 | diff --git a/tests/cloud_tests/bddeb.py b/tests/cloud_tests/bddeb.py |
1720 | index b9cfcfa..f04d0cd 100644 |
1721 | --- a/tests/cloud_tests/bddeb.py |
1722 | +++ b/tests/cloud_tests/bddeb.py |
1723 | @@ -113,7 +113,7 @@ def bddeb(args): |
1724 | @return_value: fail count |
1725 | """ |
1726 | LOG.info('preparing to build cloud-init deb') |
1727 | - (res, failed) = run_stage('build deb', [partial(setup_build, args)]) |
1728 | + _res, failed = run_stage('build deb', [partial(setup_build, args)]) |
1729 | return failed |
1730 | |
1731 | # vi: ts=4 expandtab |
1732 | diff --git a/tests/cloud_tests/collect.py b/tests/cloud_tests/collect.py |
1733 | index d4f9135..1ba7285 100644 |
1734 | --- a/tests/cloud_tests/collect.py |
1735 | +++ b/tests/cloud_tests/collect.py |
1736 | @@ -25,7 +25,8 @@ def collect_script(instance, base_dir, script, script_name): |
1737 | script.encode(), rcs=False, |
1738 | description='collect: {}'.format(script_name)) |
1739 | if err: |
1740 | - LOG.debug("collect script %s had stderr: %s", script_name, err) |
1741 | + LOG.debug("collect script %s exited '%s' and had stderr: %s", |
1742 | + script_name, err, exit) |
1743 | if not isinstance(out, bytes): |
1744 | raise util.PlatformError( |
1745 | "Collection of '%s' returned type %s, expected bytes: %s" % |
1746 | diff --git a/tests/cloud_tests/platforms/instances.py b/tests/cloud_tests/platforms/instances.py |
1747 | index 3bad021..cc439d2 100644 |
1748 | --- a/tests/cloud_tests/platforms/instances.py |
1749 | +++ b/tests/cloud_tests/platforms/instances.py |
1750 | @@ -108,7 +108,7 @@ class Instance(TargetBase): |
1751 | return client |
1752 | except (ConnectionRefusedError, AuthenticationException, |
1753 | BadHostKeyException, ConnectionResetError, SSHException, |
1754 | - OSError) as e: |
1755 | + OSError): |
1756 | retries -= 1 |
1757 | time.sleep(10) |
1758 | |
1759 | diff --git a/tests/cloud_tests/platforms/lxd/instance.py b/tests/cloud_tests/platforms/lxd/instance.py |
1760 | index 0d957bc..1c17c78 100644 |
1761 | --- a/tests/cloud_tests/platforms/lxd/instance.py |
1762 | +++ b/tests/cloud_tests/platforms/lxd/instance.py |
1763 | @@ -152,9 +152,8 @@ class LXDInstance(Instance): |
1764 | return fp.read() |
1765 | |
1766 | try: |
1767 | - stdout, stderr = subp( |
1768 | - ['lxc', 'console', '--show-log', self.name], decode=False) |
1769 | - return stdout |
1770 | + return subp(['lxc', 'console', '--show-log', self.name], |
1771 | + decode=False)[0] |
1772 | except ProcessExecutionError as e: |
1773 | raise PlatformError( |
1774 | "console log", |
1775 | @@ -214,11 +213,10 @@ def _has_proper_console_support(): |
1776 | reason = "LXD Driver version not 3.x+ (%s)" % dver |
1777 | else: |
1778 | try: |
1779 | - stdout, stderr = subp(['lxc', 'console', '--help'], |
1780 | - decode=False) |
1781 | + stdout = subp(['lxc', 'console', '--help'], decode=False)[0] |
1782 | if not (b'console' in stdout and b'log' in stdout): |
1783 | reason = "no '--log' in lxc console --help" |
1784 | - except ProcessExecutionError as e: |
1785 | + except ProcessExecutionError: |
1786 | reason = "no 'console' command in lxc client" |
1787 | |
1788 | if reason: |
1789 | diff --git a/tests/cloud_tests/setup_image.py b/tests/cloud_tests/setup_image.py |
1790 | index 6d24211..4e19570 100644 |
1791 | --- a/tests/cloud_tests/setup_image.py |
1792 | +++ b/tests/cloud_tests/setup_image.py |
1793 | @@ -25,10 +25,9 @@ def installed_package_version(image, package, ensure_installed=True): |
1794 | else: |
1795 | raise NotImplementedError |
1796 | |
1797 | - msg = 'query version for package: {}'.format(package) |
1798 | - (out, err, exit) = image.execute( |
1799 | - cmd, description=msg, rcs=(0,) if ensure_installed else range(0, 256)) |
1800 | - return out.strip() |
1801 | + return image.execute( |
1802 | + cmd, description='query version for package: {}'.format(package), |
1803 | + rcs=(0,) if ensure_installed else range(0, 256))[0].strip() |
1804 | |
1805 | |
1806 | def install_deb(args, image): |
1807 | @@ -54,7 +53,7 @@ def install_deb(args, image): |
1808 | remote_path], description=msg) |
1809 | # check installed deb version matches package |
1810 | fmt = ['-W', "--showformat=${Version}"] |
1811 | - (out, err, exit) = image.execute(['dpkg-deb'] + fmt + [remote_path]) |
1812 | + out = image.execute(['dpkg-deb'] + fmt + [remote_path])[0] |
1813 | expected_version = out.strip() |
1814 | found_version = installed_package_version(image, 'cloud-init') |
1815 | if expected_version != found_version: |
1816 | @@ -85,7 +84,7 @@ def install_rpm(args, image): |
1817 | image.execute(['rpm', '-U', remote_path], description=msg) |
1818 | |
1819 | fmt = ['--queryformat', '"%{VERSION}"'] |
1820 | - (out, err, exit) = image.execute(['rpm', '-q'] + fmt + [remote_path]) |
1821 | + (out, _err, _exit) = image.execute(['rpm', '-q'] + fmt + [remote_path]) |
1822 | expected_version = out.strip() |
1823 | found_version = installed_package_version(image, 'cloud-init') |
1824 | if expected_version != found_version: |
1825 | diff --git a/tests/cloud_tests/testcases/base.py b/tests/cloud_tests/testcases/base.py |
1826 | index 4fda8f9..0d1916b 100644 |
1827 | --- a/tests/cloud_tests/testcases/base.py |
1828 | +++ b/tests/cloud_tests/testcases/base.py |
1829 | @@ -159,7 +159,7 @@ class CloudTestCase(unittest.TestCase): |
1830 | expected_net_keys = [ |
1831 | 'public-ipv4s', 'ipv4-associations', 'local-hostname', |
1832 | 'public-hostname'] |
1833 | - for mac, mac_data in macs.items(): |
1834 | + for mac_data in macs.values(): |
1835 | for key in expected_net_keys: |
1836 | self.assertIn(key, mac_data) |
1837 | self.assertIsNotNone( |
1838 | diff --git a/tests/cloud_tests/testcases/examples/including_user_groups.py b/tests/cloud_tests/testcases/examples/including_user_groups.py |
1839 | index 93b7a82..4067348 100644 |
1840 | --- a/tests/cloud_tests/testcases/examples/including_user_groups.py |
1841 | +++ b/tests/cloud_tests/testcases/examples/including_user_groups.py |
1842 | @@ -42,7 +42,7 @@ class TestUserGroups(base.CloudTestCase): |
1843 | |
1844 | def test_user_root_in_secret(self): |
1845 | """Test root user is in 'secret' group.""" |
1846 | - user, _, groups = self.get_data_file('root_groups').partition(":") |
1847 | + _user, _, groups = self.get_data_file('root_groups').partition(":") |
1848 | self.assertIn("secret", groups.split(), |
1849 | msg="User root is not in group 'secret'") |
1850 | |
1851 | diff --git a/tests/cloud_tests/testcases/modules/user_groups.py b/tests/cloud_tests/testcases/modules/user_groups.py |
1852 | index 93b7a82..4067348 100644 |
1853 | --- a/tests/cloud_tests/testcases/modules/user_groups.py |
1854 | +++ b/tests/cloud_tests/testcases/modules/user_groups.py |
1855 | @@ -42,7 +42,7 @@ class TestUserGroups(base.CloudTestCase): |
1856 | |
1857 | def test_user_root_in_secret(self): |
1858 | """Test root user is in 'secret' group.""" |
1859 | - user, _, groups = self.get_data_file('root_groups').partition(":") |
1860 | + _user, _, groups = self.get_data_file('root_groups').partition(":") |
1861 | self.assertIn("secret", groups.split(), |
1862 | msg="User root is not in group 'secret'") |
1863 | |
1864 | diff --git a/tests/cloud_tests/util.py b/tests/cloud_tests/util.py |
1865 | index 3dd4996..06f7d86 100644 |
1866 | --- a/tests/cloud_tests/util.py |
1867 | +++ b/tests/cloud_tests/util.py |
1868 | @@ -358,7 +358,7 @@ class TargetBase(object): |
1869 | # when sh is invoked with '-c', then the first argument is "$0" |
1870 | # which is commonly understood as the "program name". |
1871 | # 'read_data' is the program name, and 'remote_path' is '$1' |
1872 | - stdout, stderr, rc = self._execute( |
1873 | + stdout, _stderr, rc = self._execute( |
1874 | ["sh", "-c", 'exec cat "$1"', 'read_data', remote_path]) |
1875 | if rc != 0: |
1876 | raise RuntimeError("Failed to read file '%s'" % remote_path) |
1877 | diff --git a/tests/unittests/test__init__.py b/tests/unittests/test__init__.py |
1878 | index 25878d7..f1ab02e 100644 |
1879 | --- a/tests/unittests/test__init__.py |
1880 | +++ b/tests/unittests/test__init__.py |
1881 | @@ -214,7 +214,7 @@ class TestCmdlineUrl(CiTestCase): |
1882 | def test_no_key_found(self, m_read): |
1883 | cmdline = "ro mykey=http://example.com/foo root=foo" |
1884 | fpath = self.tmp_path("ccpath") |
1885 | - lvl, msg = main.attempt_cmdline_url( |
1886 | + lvl, _msg = main.attempt_cmdline_url( |
1887 | fpath, network=True, cmdline=cmdline) |
1888 | |
1889 | m_read.assert_not_called() |
1890 | diff --git a/tests/unittests/test_datasource/test_azure.py b/tests/unittests/test_datasource/test_azure.py |
1891 | index 3e8b791..88fe76c 100644 |
1892 | --- a/tests/unittests/test_datasource/test_azure.py |
1893 | +++ b/tests/unittests/test_datasource/test_azure.py |
1894 | @@ -214,7 +214,7 @@ scbus-1 on xpt0 bus 0 |
1895 | self.assertIn(tag, x) |
1896 | |
1897 | def tags_equal(x, y): |
1898 | - for x_tag, x_val in x.items(): |
1899 | + for x_val in x.values(): |
1900 | y_val = y.get(x_val.tag) |
1901 | self.assertEqual(x_val.text, y_val.text) |
1902 | |
1903 | @@ -1216,7 +1216,7 @@ class TestAzureDataSourcePreprovisioning(CiTestCase): |
1904 | fake_resp.return_value = mock.MagicMock(status_code=200, text=content, |
1905 | content=content) |
1906 | dsa = dsaz.DataSourceAzure({}, distro=None, paths=self.paths) |
1907 | - md, ud, cfg, d = dsa._reprovision() |
1908 | + md, _ud, cfg, _d = dsa._reprovision() |
1909 | self.assertEqual(md['local-hostname'], hostname) |
1910 | self.assertEqual(cfg['system_info']['default_user']['name'], username) |
1911 | self.assertEqual(fake_resp.call_args_list, |
1912 | diff --git a/tests/unittests/test_datasource/test_ibmcloud.py b/tests/unittests/test_datasource/test_ibmcloud.py |
1913 | index 621cfe4..e639ae4 100644 |
1914 | --- a/tests/unittests/test_datasource/test_ibmcloud.py |
1915 | +++ b/tests/unittests/test_datasource/test_ibmcloud.py |
1916 | @@ -259,4 +259,54 @@ class TestReadMD(test_helpers.CiTestCase): |
1917 | ret['metadata']) |
1918 | |
1919 | |
1920 | +class TestIsIBMProvisioning(test_helpers.FilesystemMockingTestCase): |
1921 | + """Test the _is_ibm_provisioning method.""" |
1922 | + inst_log = "/root/swinstall.log" |
1923 | + prov_cfg = "/root/provisioningConfiguration.cfg" |
1924 | + boot_ref = "/proc/1/environ" |
1925 | + with_logs = True |
1926 | + |
1927 | + def _call_with_root(self, rootd): |
1928 | + self.reRoot(rootd) |
1929 | + return ibm._is_ibm_provisioning() |
1930 | + |
1931 | + def test_no_config(self): |
1932 | + """No provisioning config means not provisioning.""" |
1933 | + self.assertFalse(self._call_with_root(self.tmp_dir())) |
1934 | + |
1935 | + def test_config_only(self): |
1936 | + """A provisioning config without a log means provisioning.""" |
1937 | + rootd = self.tmp_dir() |
1938 | + test_helpers.populate_dir(rootd, {self.prov_cfg: "key=value"}) |
1939 | + self.assertTrue(self._call_with_root(rootd)) |
1940 | + |
1941 | + def test_config_with_old_log(self): |
1942 | + """A config with a log from previous boot is not provisioning.""" |
1943 | + rootd = self.tmp_dir() |
1944 | + data = {self.prov_cfg: ("key=value\nkey2=val2\n", -10), |
1945 | + self.inst_log: ("log data\n", -30), |
1946 | + self.boot_ref: ("PWD=/", 0)} |
1947 | + test_helpers.populate_dir_with_ts(rootd, data) |
1948 | + self.assertFalse(self._call_with_root(rootd=rootd)) |
1949 | + self.assertIn("from previous boot", self.logs.getvalue()) |
1950 | + |
1951 | + def test_config_with_new_log(self): |
1952 | + """A config with a log from this boot is provisioning.""" |
1953 | + rootd = self.tmp_dir() |
1954 | + data = {self.prov_cfg: ("key=value\nkey2=val2\n", -10), |
1955 | + self.inst_log: ("log data\n", 30), |
1956 | + self.boot_ref: ("PWD=/", 0)} |
1957 | + test_helpers.populate_dir_with_ts(rootd, data) |
1958 | + self.assertTrue(self._call_with_root(rootd=rootd)) |
1959 | + self.assertIn("from current boot", self.logs.getvalue()) |
1960 | + |
1961 | + def test_config_and_log_no_reference(self): |
1962 | + """If the config and log existed, but no reference, assume not.""" |
1963 | + rootd = self.tmp_dir() |
1964 | + test_helpers.populate_dir( |
1965 | + rootd, {self.prov_cfg: "key=value", self.inst_log: "log data\n"}) |
1966 | + self.assertFalse(self._call_with_root(rootd=rootd)) |
1967 | + self.assertIn("no reference file", self.logs.getvalue()) |
1968 | + |
1969 | + |
1970 | # vi: ts=4 expandtab |
1971 | diff --git a/tests/unittests/test_datasource/test_maas.py b/tests/unittests/test_datasource/test_maas.py |
1972 | index 6e4031c..c84d067 100644 |
1973 | --- a/tests/unittests/test_datasource/test_maas.py |
1974 | +++ b/tests/unittests/test_datasource/test_maas.py |
1975 | @@ -53,7 +53,7 @@ class TestMAASDataSource(CiTestCase): |
1976 | my_d = os.path.join(self.tmp, "valid_extra") |
1977 | populate_dir(my_d, data) |
1978 | |
1979 | - ud, md, vd = DataSourceMAAS.read_maas_seed_dir(my_d) |
1980 | + ud, md, _vd = DataSourceMAAS.read_maas_seed_dir(my_d) |
1981 | |
1982 | self.assertEqual(userdata, ud) |
1983 | for key in ('instance-id', 'local-hostname'): |
1984 | @@ -149,7 +149,7 @@ class TestMAASDataSource(CiTestCase): |
1985 | 'meta-data/local-hostname': 'test-hostname', |
1986 | 'meta-data/vendor-data': yaml.safe_dump(expected_vd).encode(), |
1987 | } |
1988 | - ud, md, vd = self.mock_read_maas_seed_url( |
1989 | + _ud, md, vd = self.mock_read_maas_seed_url( |
1990 | valid, "http://example.com/foo") |
1991 | |
1992 | self.assertEqual(valid['meta-data/instance-id'], md['instance-id']) |
1993 | diff --git a/tests/unittests/test_datasource/test_nocloud.py b/tests/unittests/test_datasource/test_nocloud.py |
1994 | index 70d50de..cdbd1e1 100644 |
1995 | --- a/tests/unittests/test_datasource/test_nocloud.py |
1996 | +++ b/tests/unittests/test_datasource/test_nocloud.py |
1997 | @@ -51,9 +51,6 @@ class TestNoCloudDataSource(CiTestCase): |
1998 | class PsuedoException(Exception): |
1999 | pass |
2000 | |
2001 | - def my_find_devs_with(*args, **kwargs): |
2002 | - raise PsuedoException |
2003 | - |
2004 | self.mocks.enter_context( |
2005 | mock.patch.object(util, 'find_devs_with', |
2006 | side_effect=PsuedoException)) |
2007 | diff --git a/tests/unittests/test_datasource/test_smartos.py b/tests/unittests/test_datasource/test_smartos.py |
2008 | index 2bea7a1..706e8eb 100644 |
2009 | --- a/tests/unittests/test_datasource/test_smartos.py |
2010 | +++ b/tests/unittests/test_datasource/test_smartos.py |
2011 | @@ -16,23 +16,27 @@ from __future__ import print_function |
2012 | |
2013 | from binascii import crc32 |
2014 | import json |
2015 | +import multiprocessing |
2016 | import os |
2017 | import os.path |
2018 | import re |
2019 | import shutil |
2020 | +import signal |
2021 | import stat |
2022 | import tempfile |
2023 | +import unittest2 |
2024 | import uuid |
2025 | |
2026 | from cloudinit import serial |
2027 | from cloudinit.sources import DataSourceSmartOS |
2028 | from cloudinit.sources.DataSourceSmartOS import ( |
2029 | - convert_smartos_network_data as convert_net) |
2030 | + convert_smartos_network_data as convert_net, |
2031 | + SMARTOS_ENV_KVM, SERIAL_DEVICE, get_smartos_environ) |
2032 | |
2033 | import six |
2034 | |
2035 | from cloudinit import helpers as c_helpers |
2036 | -from cloudinit.util import b64e |
2037 | +from cloudinit.util import (b64e, subp) |
2038 | |
2039 | from cloudinit.tests.helpers import mock, FilesystemMockingTestCase, TestCase |
2040 | |
2041 | @@ -319,6 +323,12 @@ MOCK_RETURNS = { |
2042 | |
2043 | DMI_DATA_RETURN = 'smartdc' |
2044 | |
2045 | +# Useful for calculating the length of a frame body. A SUCCESS body will be |
2046 | +# followed by more characters or be one character less if SUCCESS with no |
2047 | +# payload. See Section 4.3 of https://eng.joyent.com/mdata/protocol.html. |
2048 | +SUCCESS_LEN = len('0123abcd SUCCESS ') |
2049 | +NOTFOUND_LEN = len('0123abcd NOTFOUND') |
2050 | + |
2051 | |
2052 | class PsuedoJoyentClient(object): |
2053 | def __init__(self, data=None): |
2054 | @@ -431,6 +441,34 @@ class TestSmartOSDataSource(FilesystemMockingTestCase): |
2055 | self.assertEqual(MOCK_RETURNS['hostname'], |
2056 | dsrc.metadata['local-hostname']) |
2057 | |
2058 | + def test_hostname_if_no_sdc_hostname(self): |
2059 | + my_returns = MOCK_RETURNS.copy() |
2060 | + my_returns['sdc:hostname'] = 'sdc-' + my_returns['hostname'] |
2061 | + dsrc = self._get_ds(mockdata=my_returns) |
2062 | + ret = dsrc.get_data() |
2063 | + self.assertTrue(ret) |
2064 | + self.assertEqual(my_returns['hostname'], |
2065 | + dsrc.metadata['local-hostname']) |
2066 | + |
2067 | + def test_sdc_hostname_if_no_hostname(self): |
2068 | + my_returns = MOCK_RETURNS.copy() |
2069 | + my_returns['sdc:hostname'] = 'sdc-' + my_returns['hostname'] |
2070 | + del my_returns['hostname'] |
2071 | + dsrc = self._get_ds(mockdata=my_returns) |
2072 | + ret = dsrc.get_data() |
2073 | + self.assertTrue(ret) |
2074 | + self.assertEqual(my_returns['sdc:hostname'], |
2075 | + dsrc.metadata['local-hostname']) |
2076 | + |
2077 | + def test_sdc_uuid_if_no_hostname_or_sdc_hostname(self): |
2078 | + my_returns = MOCK_RETURNS.copy() |
2079 | + del my_returns['hostname'] |
2080 | + dsrc = self._get_ds(mockdata=my_returns) |
2081 | + ret = dsrc.get_data() |
2082 | + self.assertTrue(ret) |
2083 | + self.assertEqual(my_returns['sdc:uuid'], |
2084 | + dsrc.metadata['local-hostname']) |
2085 | + |
2086 | def test_userdata(self): |
2087 | dsrc = self._get_ds(mockdata=MOCK_RETURNS) |
2088 | ret = dsrc.get_data() |
2089 | @@ -651,7 +689,7 @@ class TestJoyentMetadataClient(FilesystemMockingTestCase): |
2090 | self.response_parts = { |
2091 | 'command': 'SUCCESS', |
2092 | 'crc': 'b5a9ff00', |
2093 | - 'length': 17 + len(b64e(self.metadata_value)), |
2094 | + 'length': SUCCESS_LEN + len(b64e(self.metadata_value)), |
2095 | 'payload': b64e(self.metadata_value), |
2096 | 'request_id': '{0:08x}'.format(self.request_id), |
2097 | } |
2098 | @@ -787,7 +825,7 @@ class TestJoyentMetadataClient(FilesystemMockingTestCase): |
2099 | def test_get_metadata_returns_None_if_value_not_found(self): |
2100 | self.response_parts['payload'] = '' |
2101 | self.response_parts['command'] = 'NOTFOUND' |
2102 | - self.response_parts['length'] = 17 |
2103 | + self.response_parts['length'] = NOTFOUND_LEN |
2104 | client = self._get_client() |
2105 | client._checksum = lambda _: self.response_parts['crc'] |
2106 | self.assertIsNone(client.get('some_key')) |
2107 | @@ -838,6 +876,22 @@ class TestJoyentMetadataClient(FilesystemMockingTestCase): |
2108 | client.open_transport() |
2109 | self.assertTrue(reader.emptied) |
2110 | |
2111 | + def test_list_metadata_returns_list(self): |
2112 | + parts = ['foo', 'bar'] |
2113 | + value = b64e('\n'.join(parts)) |
2114 | + self.response_parts['payload'] = value |
2115 | + self.response_parts['crc'] = '40873553' |
2116 | + self.response_parts['length'] = SUCCESS_LEN + len(value) |
2117 | + client = self._get_client() |
2118 | + self.assertEqual(client.list(), parts) |
2119 | + |
2120 | + def test_list_metadata_returns_empty_list_if_no_customer_metadata(self): |
2121 | + del self.response_parts['payload'] |
2122 | + self.response_parts['length'] = SUCCESS_LEN - 1 |
2123 | + self.response_parts['crc'] = '14e563ba' |
2124 | + client = self._get_client() |
2125 | + self.assertEqual(client.list(), []) |
2126 | + |
2127 | |
2128 | class TestNetworkConversion(TestCase): |
2129 | def test_convert_simple(self): |
2130 | @@ -973,4 +1027,63 @@ class TestNetworkConversion(TestCase): |
2131 | found = convert_net(SDC_NICS_SINGLE_GATEWAY) |
2132 | self.assertEqual(expected, found) |
2133 | |
2134 | + |
2135 | +@unittest2.skipUnless(get_smartos_environ() == SMARTOS_ENV_KVM, |
2136 | + "Only supported on KVM and bhyve guests under SmartOS") |
2137 | +@unittest2.skipUnless(os.access(SERIAL_DEVICE, os.W_OK), |
2138 | + "Requires write access to " + SERIAL_DEVICE) |
2139 | +class TestSerialConcurrency(TestCase): |
2140 | + """ |
2141 | + This class tests locking on an actual serial port, and as such can only |
2142 | + be run in a kvm or bhyve guest running on a SmartOS host. A test run on |
2143 | + a metadata socket will not be valid because a metadata socket ensures |
2144 | + there is only one session over a connection. In contrast, in the |
2145 | + absence of proper locking multiple processes opening the same serial |
2146 | + port can corrupt each others' exchanges with the metadata server. |
2147 | + """ |
2148 | + def setUp(self): |
2149 | + self.mdata_proc = multiprocessing.Process(target=self.start_mdata_loop) |
2150 | + self.mdata_proc.start() |
2151 | + super(TestSerialConcurrency, self).setUp() |
2152 | + |
2153 | + def tearDown(self): |
2154 | + # os.kill() rather than mdata_proc.terminate() to avoid console spam. |
2155 | + os.kill(self.mdata_proc.pid, signal.SIGKILL) |
2156 | + self.mdata_proc.join() |
2157 | + super(TestSerialConcurrency, self).tearDown() |
2158 | + |
2159 | + def start_mdata_loop(self): |
2160 | + """ |
2161 | + The mdata-get command is repeatedly run in a separate process so |
2162 | + that it may try to race with metadata operations performed in the |
2163 | + main test process. Use of mdata-get is better than two processes |
2164 | + using the protocol implementation in DataSourceSmartOS because we |
2165 | + are testing to be sure that cloud-init and mdata-get respect each |
2166 | + others locks. |
2167 | + """ |
2168 | + rcs = list(range(0, 256)) |
2169 | + while True: |
2170 | + subp(['mdata-get', 'sdc:routes'], rcs=rcs) |
2171 | + |
2172 | + def test_all_keys(self): |
2173 | + self.assertIsNotNone(self.mdata_proc.pid) |
2174 | + ds = DataSourceSmartOS |
2175 | + keys = [tup[0] for tup in ds.SMARTOS_ATTRIB_MAP.values()] |
2176 | + keys.extend(ds.SMARTOS_ATTRIB_JSON.values()) |
2177 | + |
2178 | + client = ds.jmc_client_factory() |
2179 | + self.assertIsNotNone(client) |
2180 | + |
2181 | + # The behavior that we are testing for was observed mdata-get running |
2182 | + # 10 times at roughly the same time as cloud-init fetched each key |
2183 | + # once. cloud-init would regularly see failures before making it |
2184 | + # through all keys once. |
2185 | + for _ in range(0, 3): |
2186 | + for key in keys: |
2187 | + # We don't care about the return value, just that it doesn't |
2188 | + # thrown any exceptions. |
2189 | + client.get(key) |
2190 | + |
2191 | + self.assertIsNone(self.mdata_proc.exitcode) |
2192 | + |
2193 | # vi: ts=4 expandtab |
2194 | diff --git a/tests/unittests/test_ds_identify.py b/tests/unittests/test_ds_identify.py |
2195 | index 5364398..ad7fe41 100644 |
2196 | --- a/tests/unittests/test_ds_identify.py |
2197 | +++ b/tests/unittests/test_ds_identify.py |
2198 | @@ -1,5 +1,6 @@ |
2199 | # This file is part of cloud-init. See LICENSE file for license information. |
2200 | |
2201 | +from collections import namedtuple |
2202 | import copy |
2203 | import os |
2204 | from uuid import uuid4 |
2205 | @@ -7,7 +8,7 @@ from uuid import uuid4 |
2206 | from cloudinit import safeyaml |
2207 | from cloudinit import util |
2208 | from cloudinit.tests.helpers import ( |
2209 | - CiTestCase, dir2dict, populate_dir) |
2210 | + CiTestCase, dir2dict, populate_dir, populate_dir_with_ts) |
2211 | |
2212 | from cloudinit.sources import DataSourceIBMCloud as dsibm |
2213 | |
2214 | @@ -66,7 +67,6 @@ P_SYS_VENDOR = "sys/class/dmi/id/sys_vendor" |
2215 | P_SEED_DIR = "var/lib/cloud/seed" |
2216 | P_DSID_CFG = "etc/cloud/ds-identify.cfg" |
2217 | |
2218 | -IBM_PROVISIONING_CHECK_PATH = "/root/provisioningConfiguration.cfg" |
2219 | IBM_CONFIG_UUID = "9796-932E" |
2220 | |
2221 | MOCK_VIRT_IS_KVM = {'name': 'detect_virt', 'RET': 'kvm', 'ret': 0} |
2222 | @@ -74,11 +74,17 @@ MOCK_VIRT_IS_VMWARE = {'name': 'detect_virt', 'RET': 'vmware', 'ret': 0} |
2223 | MOCK_VIRT_IS_XEN = {'name': 'detect_virt', 'RET': 'xen', 'ret': 0} |
2224 | MOCK_UNAME_IS_PPC64 = {'name': 'uname', 'out': UNAME_PPC64EL, 'ret': 0} |
2225 | |
2226 | +shell_true = 0 |
2227 | +shell_false = 1 |
2228 | |
2229 | -class TestDsIdentify(CiTestCase): |
2230 | +CallReturn = namedtuple('CallReturn', |
2231 | + ['rc', 'stdout', 'stderr', 'cfg', 'files']) |
2232 | + |
2233 | + |
2234 | +class DsIdentifyBase(CiTestCase): |
2235 | dsid_path = os.path.realpath('tools/ds-identify') |
2236 | |
2237 | - def call(self, rootd=None, mocks=None, args=None, files=None, |
2238 | + def call(self, rootd=None, mocks=None, func="main", args=None, files=None, |
2239 | policy_dmi=DI_DEFAULT_POLICY, |
2240 | policy_no_dmi=DI_DEFAULT_POLICY_NO_DMI, |
2241 | ec2_strict_id=DI_EC2_STRICT_ID_DEFAULT): |
2242 | @@ -135,7 +141,7 @@ class TestDsIdentify(CiTestCase): |
2243 | mocklines.append(write_mock(d)) |
2244 | |
2245 | endlines = [ |
2246 | - 'main %s' % ' '.join(['"%s"' % s for s in args]) |
2247 | + func + ' ' + ' '.join(['"%s"' % s for s in args]) |
2248 | ] |
2249 | |
2250 | with open(wrap, "w") as fp: |
2251 | @@ -159,7 +165,7 @@ class TestDsIdentify(CiTestCase): |
2252 | cfg = {"_INVALID_YAML": contents, |
2253 | "_EXCEPTION": str(e)} |
2254 | |
2255 | - return rc, out, err, cfg, dir2dict(rootd) |
2256 | + return CallReturn(rc, out, err, cfg, dir2dict(rootd)) |
2257 | |
2258 | def _call_via_dict(self, data, rootd=None, **kwargs): |
2259 | # return output of self.call with a dict input like VALID_CFG[item] |
2260 | @@ -190,6 +196,8 @@ class TestDsIdentify(CiTestCase): |
2261 | _print_run_output(rc, out, err, cfg, files) |
2262 | return rc, out, err, cfg, files |
2263 | |
2264 | + |
2265 | +class TestDsIdentify(DsIdentifyBase): |
2266 | def test_wb_print_variables(self): |
2267 | """_print_info reports an array of discovered variables to stderr.""" |
2268 | data = VALID_CFG['Azure-dmi-detection'] |
2269 | @@ -250,7 +258,10 @@ class TestDsIdentify(CiTestCase): |
2270 | Template provisioning with user-data has METADATA disk, |
2271 | datasource should return not found.""" |
2272 | data = copy.deepcopy(VALID_CFG['IBMCloud-metadata']) |
2273 | - data['files'] = {IBM_PROVISIONING_CHECK_PATH: 'xxx'} |
2274 | + # change the 'is_ibm_provisioning' mock to return 1 (false) |
2275 | + isprov_m = [m for m in data['mocks'] |
2276 | + if m["name"] == "is_ibm_provisioning"][0] |
2277 | + isprov_m['ret'] = shell_true |
2278 | return self._check_via_dict(data, RC_NOT_FOUND) |
2279 | |
2280 | def test_ibmcloud_template_userdata(self): |
2281 | @@ -265,7 +276,8 @@ class TestDsIdentify(CiTestCase): |
2282 | |
2283 | no disks attached. Datasource should return not found.""" |
2284 | data = copy.deepcopy(VALID_CFG['IBMCloud-nodisks']) |
2285 | - data['files'] = {IBM_PROVISIONING_CHECK_PATH: 'xxx'} |
2286 | + data['mocks'].append( |
2287 | + {'name': 'is_ibm_provisioning', 'ret': shell_true}) |
2288 | return self._check_via_dict(data, RC_NOT_FOUND) |
2289 | |
2290 | def test_ibmcloud_template_no_userdata(self): |
2291 | @@ -446,6 +458,47 @@ class TestDsIdentify(CiTestCase): |
2292 | self._test_ds_found('Hetzner') |
2293 | |
2294 | |
2295 | +class TestIsIBMProvisioning(DsIdentifyBase): |
2296 | + """Test the is_ibm_provisioning method in ds-identify.""" |
2297 | + |
2298 | + inst_log = "/root/swinstall.log" |
2299 | + prov_cfg = "/root/provisioningConfiguration.cfg" |
2300 | + boot_ref = "/proc/1/environ" |
2301 | + funcname = "is_ibm_provisioning" |
2302 | + |
2303 | + def test_no_config(self): |
2304 | + """No provisioning config means not provisioning.""" |
2305 | + ret = self.call(files={}, func=self.funcname) |
2306 | + self.assertEqual(shell_false, ret.rc) |
2307 | + |
2308 | + def test_config_only(self): |
2309 | + """A provisioning config without a log means provisioning.""" |
2310 | + ret = self.call(files={self.prov_cfg: "key=value"}, func=self.funcname) |
2311 | + self.assertEqual(shell_true, ret.rc) |
2312 | + |
2313 | + def test_config_with_old_log(self): |
2314 | + """A config with a log from previous boot is not provisioning.""" |
2315 | + rootd = self.tmp_dir() |
2316 | + data = {self.prov_cfg: ("key=value\nkey2=val2\n", -10), |
2317 | + self.inst_log: ("log data\n", -30), |
2318 | + self.boot_ref: ("PWD=/", 0)} |
2319 | + populate_dir_with_ts(rootd, data) |
2320 | + ret = self.call(rootd=rootd, func=self.funcname) |
2321 | + self.assertEqual(shell_false, ret.rc) |
2322 | + self.assertIn("from previous boot", ret.stderr) |
2323 | + |
2324 | + def test_config_with_new_log(self): |
2325 | + """A config with a log from this boot is provisioning.""" |
2326 | + rootd = self.tmp_dir() |
2327 | + data = {self.prov_cfg: ("key=value\nkey2=val2\n", -10), |
2328 | + self.inst_log: ("log data\n", 30), |
2329 | + self.boot_ref: ("PWD=/", 0)} |
2330 | + populate_dir_with_ts(rootd, data) |
2331 | + ret = self.call(rootd=rootd, func=self.funcname) |
2332 | + self.assertEqual(shell_true, ret.rc) |
2333 | + self.assertIn("from current boot", ret.stderr) |
2334 | + |
2335 | + |
2336 | def blkid_out(disks=None): |
2337 | """Convert a list of disk dictionaries into blkid content.""" |
2338 | if disks is None: |
2339 | @@ -639,6 +692,7 @@ VALID_CFG = { |
2340 | 'ds': 'IBMCloud', |
2341 | 'mocks': [ |
2342 | MOCK_VIRT_IS_XEN, |
2343 | + {'name': 'is_ibm_provisioning', 'ret': shell_false}, |
2344 | {'name': 'blkid', 'ret': 0, |
2345 | 'out': blkid_out( |
2346 | [{'DEVNAME': 'xvda1', 'TYPE': 'vfat', 'PARTUUID': uuid4()}, |
2347 | @@ -652,6 +706,7 @@ VALID_CFG = { |
2348 | 'ds': 'IBMCloud', |
2349 | 'mocks': [ |
2350 | MOCK_VIRT_IS_XEN, |
2351 | + {'name': 'is_ibm_provisioning', 'ret': shell_false}, |
2352 | {'name': 'blkid', 'ret': 0, |
2353 | 'out': blkid_out( |
2354 | [{'DEVNAME': 'xvda1', 'TYPE': 'ext3', 'PARTUUID': uuid4(), |
2355 | @@ -669,6 +724,7 @@ VALID_CFG = { |
2356 | 'ds': 'IBMCloud', |
2357 | 'mocks': [ |
2358 | MOCK_VIRT_IS_XEN, |
2359 | + {'name': 'is_ibm_provisioning', 'ret': shell_false}, |
2360 | {'name': 'blkid', 'ret': 0, |
2361 | 'out': blkid_out( |
2362 | [{'DEVNAME': 'xvda1', 'TYPE': 'vfat', 'PARTUUID': uuid4()}, |
2363 | diff --git a/tests/unittests/test_handler/test_handler_apt_source_v3.py b/tests/unittests/test_handler/test_handler_apt_source_v3.py |
2364 | index 7bb1b7c..e486862 100644 |
2365 | --- a/tests/unittests/test_handler/test_handler_apt_source_v3.py |
2366 | +++ b/tests/unittests/test_handler/test_handler_apt_source_v3.py |
2367 | @@ -528,7 +528,7 @@ class TestAptSourceConfig(t_help.FilesystemMockingTestCase): |
2368 | |
2369 | expected = sorted([npre + suff for opre, npre, suff in files]) |
2370 | # create files |
2371 | - for (opre, npre, suff) in files: |
2372 | + for (opre, _npre, suff) in files: |
2373 | fpath = os.path.join(apt_lists_d, opre + suff) |
2374 | util.write_file(fpath, content=fpath) |
2375 | |
2376 | diff --git a/tests/unittests/test_handler/test_handler_bootcmd.py b/tests/unittests/test_handler/test_handler_bootcmd.py |
2377 | index 29fc25e..b137526 100644 |
2378 | --- a/tests/unittests/test_handler/test_handler_bootcmd.py |
2379 | +++ b/tests/unittests/test_handler/test_handler_bootcmd.py |
2380 | @@ -1,9 +1,10 @@ |
2381 | # This file is part of cloud-init. See LICENSE file for license information. |
2382 | |
2383 | -from cloudinit.config import cc_bootcmd |
2384 | +from cloudinit.config.cc_bootcmd import handle, schema |
2385 | from cloudinit.sources import DataSourceNone |
2386 | from cloudinit import (distros, helpers, cloud, util) |
2387 | -from cloudinit.tests.helpers import CiTestCase, mock, skipUnlessJsonSchema |
2388 | +from cloudinit.tests.helpers import ( |
2389 | + CiTestCase, mock, SchemaTestCaseMixin, skipUnlessJsonSchema) |
2390 | |
2391 | import logging |
2392 | import tempfile |
2393 | @@ -50,7 +51,7 @@ class TestBootcmd(CiTestCase): |
2394 | """When the provided config doesn't contain bootcmd, skip it.""" |
2395 | cfg = {} |
2396 | mycloud = self._get_cloud('ubuntu') |
2397 | - cc_bootcmd.handle('notimportant', cfg, mycloud, LOG, None) |
2398 | + handle('notimportant', cfg, mycloud, LOG, None) |
2399 | self.assertIn( |
2400 | "Skipping module named notimportant, no 'bootcmd' key", |
2401 | self.logs.getvalue()) |
2402 | @@ -60,7 +61,7 @@ class TestBootcmd(CiTestCase): |
2403 | invalid_config = {'bootcmd': 1} |
2404 | cc = self._get_cloud('ubuntu') |
2405 | with self.assertRaises(TypeError) as context_manager: |
2406 | - cc_bootcmd.handle('cc_bootcmd', invalid_config, cc, LOG, []) |
2407 | + handle('cc_bootcmd', invalid_config, cc, LOG, []) |
2408 | self.assertIn('Failed to shellify bootcmd', self.logs.getvalue()) |
2409 | self.assertEqual( |
2410 | "Input to shellify was type 'int'. Expected list or tuple.", |
2411 | @@ -76,7 +77,7 @@ class TestBootcmd(CiTestCase): |
2412 | invalid_config = {'bootcmd': 1} |
2413 | cc = self._get_cloud('ubuntu') |
2414 | with self.assertRaises(TypeError): |
2415 | - cc_bootcmd.handle('cc_bootcmd', invalid_config, cc, LOG, []) |
2416 | + handle('cc_bootcmd', invalid_config, cc, LOG, []) |
2417 | self.assertIn( |
2418 | 'Invalid config:\nbootcmd: 1 is not of type \'array\'', |
2419 | self.logs.getvalue()) |
2420 | @@ -93,7 +94,7 @@ class TestBootcmd(CiTestCase): |
2421 | 'bootcmd': ['ls /', 20, ['wget', 'http://stuff/blah'], {'a': 'n'}]} |
2422 | cc = self._get_cloud('ubuntu') |
2423 | with self.assertRaises(TypeError) as context_manager: |
2424 | - cc_bootcmd.handle('cc_bootcmd', invalid_config, cc, LOG, []) |
2425 | + handle('cc_bootcmd', invalid_config, cc, LOG, []) |
2426 | expected_warnings = [ |
2427 | 'bootcmd.1: 20 is not valid under any of the given schemas', |
2428 | 'bootcmd.3: {\'a\': \'n\'} is not valid under any of the given' |
2429 | @@ -117,7 +118,7 @@ class TestBootcmd(CiTestCase): |
2430 | 'echo {0} $INSTANCE_ID > {1}'.format(my_id, out_file)]} |
2431 | |
2432 | with mock.patch(self._etmpfile_path, FakeExtendedTempFile): |
2433 | - cc_bootcmd.handle('cc_bootcmd', valid_config, cc, LOG, []) |
2434 | + handle('cc_bootcmd', valid_config, cc, LOG, []) |
2435 | self.assertEqual(my_id + ' iid-datasource-none\n', |
2436 | util.load_file(out_file)) |
2437 | |
2438 | @@ -128,7 +129,7 @@ class TestBootcmd(CiTestCase): |
2439 | |
2440 | with mock.patch(self._etmpfile_path, FakeExtendedTempFile): |
2441 | with self.assertRaises(util.ProcessExecutionError) as ctxt_manager: |
2442 | - cc_bootcmd.handle('does-not-matter', valid_config, cc, LOG, []) |
2443 | + handle('does-not-matter', valid_config, cc, LOG, []) |
2444 | self.assertIn( |
2445 | 'Unexpected error while running command.\n' |
2446 | "Command: ['/bin/sh',", |
2447 | @@ -138,4 +139,21 @@ class TestBootcmd(CiTestCase): |
2448 | self.logs.getvalue()) |
2449 | |
2450 | |
2451 | +@skipUnlessJsonSchema() |
2452 | +class TestSchema(CiTestCase, SchemaTestCaseMixin): |
2453 | + """Directly test schema rather than through handle.""" |
2454 | + |
2455 | + schema = schema |
2456 | + |
2457 | + def test_duplicates_are_fine_array_array(self): |
2458 | + """Duplicated commands array/array entries are allowed.""" |
2459 | + self.assertSchemaValid( |
2460 | + ["byebye", "byebye"], 'command entries can be duplicate') |
2461 | + |
2462 | + def test_duplicates_are_fine_array_string(self): |
2463 | + """Duplicated commands array/string entries are allowed.""" |
2464 | + self.assertSchemaValid( |
2465 | + ["echo bye", "echo bye"], "command entries can be duplicate.") |
2466 | + |
2467 | + |
2468 | # vi: ts=4 expandtab |
2469 | diff --git a/tests/unittests/test_handler/test_handler_ntp.py b/tests/unittests/test_handler/test_handler_ntp.py |
2470 | index 02676aa..17c5355 100644 |
2471 | --- a/tests/unittests/test_handler/test_handler_ntp.py |
2472 | +++ b/tests/unittests/test_handler/test_handler_ntp.py |
2473 | @@ -76,7 +76,7 @@ class TestNtp(FilesystemMockingTestCase): |
2474 | template = TIMESYNCD_TEMPLATE |
2475 | else: |
2476 | template = NTP_TEMPLATE |
2477 | - (confpath, template_fn) = self._generate_template(template=template) |
2478 | + (confpath, _template_fn) = self._generate_template(template=template) |
2479 | ntpconfig = copy.deepcopy(dcfg[client]) |
2480 | ntpconfig['confpath'] = confpath |
2481 | ntpconfig['template_name'] = os.path.basename(confpath) |
2482 | diff --git a/tests/unittests/test_handler/test_handler_runcmd.py b/tests/unittests/test_handler/test_handler_runcmd.py |
2483 | index dbbb271..9ce334a 100644 |
2484 | --- a/tests/unittests/test_handler/test_handler_runcmd.py |
2485 | +++ b/tests/unittests/test_handler/test_handler_runcmd.py |
2486 | @@ -1,10 +1,11 @@ |
2487 | # This file is part of cloud-init. See LICENSE file for license information. |
2488 | |
2489 | -from cloudinit.config import cc_runcmd |
2490 | +from cloudinit.config.cc_runcmd import handle, schema |
2491 | from cloudinit.sources import DataSourceNone |
2492 | from cloudinit import (distros, helpers, cloud, util) |
2493 | from cloudinit.tests.helpers import ( |
2494 | - FilesystemMockingTestCase, skipUnlessJsonSchema) |
2495 | + CiTestCase, FilesystemMockingTestCase, SchemaTestCaseMixin, |
2496 | + skipUnlessJsonSchema) |
2497 | |
2498 | import logging |
2499 | import os |
2500 | @@ -35,7 +36,7 @@ class TestRuncmd(FilesystemMockingTestCase): |
2501 | """When the provided config doesn't contain runcmd, skip it.""" |
2502 | cfg = {} |
2503 | mycloud = self._get_cloud('ubuntu') |
2504 | - cc_runcmd.handle('notimportant', cfg, mycloud, LOG, None) |
2505 | + handle('notimportant', cfg, mycloud, LOG, None) |
2506 | self.assertIn( |
2507 | "Skipping module named notimportant, no 'runcmd' key", |
2508 | self.logs.getvalue()) |
2509 | @@ -44,7 +45,7 @@ class TestRuncmd(FilesystemMockingTestCase): |
2510 | """Commands which can't be converted to shell will raise errors.""" |
2511 | invalid_config = {'runcmd': 1} |
2512 | cc = self._get_cloud('ubuntu') |
2513 | - cc_runcmd.handle('cc_runcmd', invalid_config, cc, LOG, []) |
2514 | + handle('cc_runcmd', invalid_config, cc, LOG, []) |
2515 | self.assertIn( |
2516 | 'Failed to shellify 1 into file' |
2517 | ' /var/lib/cloud/instances/iid-datasource-none/scripts/runcmd', |
2518 | @@ -59,7 +60,7 @@ class TestRuncmd(FilesystemMockingTestCase): |
2519 | """ |
2520 | invalid_config = {'runcmd': 1} |
2521 | cc = self._get_cloud('ubuntu') |
2522 | - cc_runcmd.handle('cc_runcmd', invalid_config, cc, LOG, []) |
2523 | + handle('cc_runcmd', invalid_config, cc, LOG, []) |
2524 | self.assertIn( |
2525 | 'Invalid config:\nruncmd: 1 is not of type \'array\'', |
2526 | self.logs.getvalue()) |
2527 | @@ -75,7 +76,7 @@ class TestRuncmd(FilesystemMockingTestCase): |
2528 | invalid_config = { |
2529 | 'runcmd': ['ls /', 20, ['wget', 'http://stuff/blah'], {'a': 'n'}]} |
2530 | cc = self._get_cloud('ubuntu') |
2531 | - cc_runcmd.handle('cc_runcmd', invalid_config, cc, LOG, []) |
2532 | + handle('cc_runcmd', invalid_config, cc, LOG, []) |
2533 | expected_warnings = [ |
2534 | 'runcmd.1: 20 is not valid under any of the given schemas', |
2535 | 'runcmd.3: {\'a\': \'n\'} is not valid under any of the given' |
2536 | @@ -90,7 +91,7 @@ class TestRuncmd(FilesystemMockingTestCase): |
2537 | """Valid runcmd schema is written to a runcmd shell script.""" |
2538 | valid_config = {'runcmd': [['ls', '/']]} |
2539 | cc = self._get_cloud('ubuntu') |
2540 | - cc_runcmd.handle('cc_runcmd', valid_config, cc, LOG, []) |
2541 | + handle('cc_runcmd', valid_config, cc, LOG, []) |
2542 | runcmd_file = os.path.join( |
2543 | self.new_root, |
2544 | 'var/lib/cloud/instances/iid-datasource-none/scripts/runcmd') |
2545 | @@ -99,4 +100,22 @@ class TestRuncmd(FilesystemMockingTestCase): |
2546 | self.assertEqual(0o700, stat.S_IMODE(file_stat.st_mode)) |
2547 | |
2548 | |
2549 | +@skipUnlessJsonSchema() |
2550 | +class TestSchema(CiTestCase, SchemaTestCaseMixin): |
2551 | + """Directly test schema rather than through handle.""" |
2552 | + |
2553 | + schema = schema |
2554 | + |
2555 | + def test_duplicates_are_fine_array_array(self): |
2556 | + """Duplicated commands array/array entries are allowed.""" |
2557 | + self.assertSchemaValid( |
2558 | + [["echo", "bye"], ["echo", "bye"]], |
2559 | + "command entries can be duplicate.") |
2560 | + |
2561 | + def test_duplicates_are_fine_array_string(self): |
2562 | + """Duplicated commands array/string entries are allowed.""" |
2563 | + self.assertSchemaValid( |
2564 | + ["echo bye", "echo bye"], |
2565 | + "command entries can be duplicate.") |
2566 | + |
2567 | # vi: ts=4 expandtab |
2568 | diff --git a/tests/unittests/test_net.py b/tests/unittests/test_net.py |
2569 | index c12a487..fac8267 100644 |
2570 | --- a/tests/unittests/test_net.py |
2571 | +++ b/tests/unittests/test_net.py |
2572 | @@ -553,6 +553,43 @@ NETWORK_CONFIGS = { |
2573 | """), |
2574 | }, |
2575 | }, |
2576 | + 'dhcpv6_only': { |
2577 | + 'expected_eni': textwrap.dedent("""\ |
2578 | + auto lo |
2579 | + iface lo inet loopback |
2580 | + |
2581 | + auto iface0 |
2582 | + iface iface0 inet6 dhcp |
2583 | + """).rstrip(' '), |
2584 | + 'expected_netplan': textwrap.dedent(""" |
2585 | + network: |
2586 | + version: 2 |
2587 | + ethernets: |
2588 | + iface0: |
2589 | + dhcp6: true |
2590 | + """).rstrip(' '), |
2591 | + 'yaml': textwrap.dedent("""\ |
2592 | + version: 1 |
2593 | + config: |
2594 | + - type: 'physical' |
2595 | + name: 'iface0' |
2596 | + subnets: |
2597 | + - {'type': 'dhcp6'} |
2598 | + """).rstrip(' '), |
2599 | + 'expected_sysconfig': { |
2600 | + 'ifcfg-iface0': textwrap.dedent("""\ |
2601 | + BOOTPROTO=none |
2602 | + DEVICE=iface0 |
2603 | + DHCPV6C=yes |
2604 | + IPV6INIT=yes |
2605 | + DEVICE=iface0 |
2606 | + NM_CONTROLLED=no |
2607 | + ONBOOT=yes |
2608 | + TYPE=Ethernet |
2609 | + USERCTL=no |
2610 | + """), |
2611 | + }, |
2612 | + }, |
2613 | 'all': { |
2614 | 'expected_eni': ("""\ |
2615 | auto lo |
2616 | @@ -740,7 +777,7 @@ pre-down route del -net 10.0.0.0 netmask 255.0.0.0 gw 11.0.0.1 metric 3 || true |
2617 | """miimon=100" |
2618 | BONDING_SLAVE0=eth1 |
2619 | BONDING_SLAVE1=eth2 |
2620 | - BOOTPROTO=dhcp |
2621 | + BOOTPROTO=none |
2622 | DEVICE=bond0 |
2623 | DHCPV6C=yes |
2624 | IPV6INIT=yes |
2625 | @@ -1405,6 +1442,7 @@ DEFAULT_DEV_ATTRS = { |
2626 | "address": "07-1C-C6-75-A4-BE", |
2627 | "device/driver": None, |
2628 | "device/device": None, |
2629 | + "name_assign_type": "4", |
2630 | } |
2631 | } |
2632 | |
2633 | @@ -1452,11 +1490,14 @@ class TestGenerateFallbackConfig(CiTestCase): |
2634 | 'eth0': { |
2635 | 'bridge': False, 'carrier': False, 'dormant': False, |
2636 | 'operstate': 'down', 'address': '00:11:22:33:44:55', |
2637 | - 'device/driver': 'hv_netsvc', 'device/device': '0x3'}, |
2638 | + 'device/driver': 'hv_netsvc', 'device/device': '0x3', |
2639 | + 'name_assign_type': '4'}, |
2640 | 'eth1': { |
2641 | 'bridge': False, 'carrier': False, 'dormant': False, |
2642 | 'operstate': 'down', 'address': '00:11:22:33:44:55', |
2643 | - 'device/driver': 'mlx4_core', 'device/device': '0x7'}, |
2644 | + 'device/driver': 'mlx4_core', 'device/device': '0x7', |
2645 | + 'name_assign_type': '4'}, |
2646 | + |
2647 | } |
2648 | |
2649 | tmp_dir = self.tmp_dir() |
2650 | @@ -1512,11 +1553,13 @@ iface eth0 inet dhcp |
2651 | 'eth1': { |
2652 | 'bridge': False, 'carrier': False, 'dormant': False, |
2653 | 'operstate': 'down', 'address': '00:11:22:33:44:55', |
2654 | - 'device/driver': 'hv_netsvc', 'device/device': '0x3'}, |
2655 | + 'device/driver': 'hv_netsvc', 'device/device': '0x3', |
2656 | + 'name_assign_type': '4'}, |
2657 | 'eth0': { |
2658 | 'bridge': False, 'carrier': False, 'dormant': False, |
2659 | 'operstate': 'down', 'address': '00:11:22:33:44:55', |
2660 | - 'device/driver': 'mlx4_core', 'device/device': '0x7'}, |
2661 | + 'device/driver': 'mlx4_core', 'device/device': '0x7', |
2662 | + 'name_assign_type': '4'}, |
2663 | } |
2664 | |
2665 | tmp_dir = self.tmp_dir() |
2666 | @@ -1565,6 +1608,65 @@ iface eth1 inet dhcp |
2667 | ] |
2668 | self.assertEqual(", ".join(expected_rule) + '\n', contents.lstrip()) |
2669 | |
2670 | + @mock.patch("cloudinit.util.udevadm_settle") |
2671 | + @mock.patch("cloudinit.net.sys_dev_path") |
2672 | + @mock.patch("cloudinit.net.read_sys_net") |
2673 | + @mock.patch("cloudinit.net.get_devicelist") |
2674 | + def test_unstable_names(self, mock_get_devicelist, mock_read_sys_net, |
2675 | + mock_sys_dev_path, mock_settle): |
2676 | + """verify that udevadm settle is called when we find unstable names""" |
2677 | + devices = { |
2678 | + 'eth0': { |
2679 | + 'bridge': False, 'carrier': False, 'dormant': False, |
2680 | + 'operstate': 'down', 'address': '00:11:22:33:44:55', |
2681 | + 'device/driver': 'hv_netsvc', 'device/device': '0x3', |
2682 | + 'name_assign_type': False}, |
2683 | + 'ens4': { |
2684 | + 'bridge': False, 'carrier': False, 'dormant': False, |
2685 | + 'operstate': 'down', 'address': '00:11:22:33:44:55', |
2686 | + 'device/driver': 'mlx4_core', 'device/device': '0x7', |
2687 | + 'name_assign_type': '4'}, |
2688 | + |
2689 | + } |
2690 | + |
2691 | + tmp_dir = self.tmp_dir() |
2692 | + _setup_test(tmp_dir, mock_get_devicelist, |
2693 | + mock_read_sys_net, mock_sys_dev_path, |
2694 | + dev_attrs=devices) |
2695 | + net.generate_fallback_config(config_driver=True) |
2696 | + self.assertEqual(1, mock_settle.call_count) |
2697 | + |
2698 | + @mock.patch("cloudinit.util.get_cmdline") |
2699 | + @mock.patch("cloudinit.util.udevadm_settle") |
2700 | + @mock.patch("cloudinit.net.sys_dev_path") |
2701 | + @mock.patch("cloudinit.net.read_sys_net") |
2702 | + @mock.patch("cloudinit.net.get_devicelist") |
2703 | + def test_unstable_names_disabled(self, mock_get_devicelist, |
2704 | + mock_read_sys_net, mock_sys_dev_path, |
2705 | + mock_settle, m_get_cmdline): |
2706 | + """verify udevadm settle not called when cmdline has net.ifnames=0""" |
2707 | + devices = { |
2708 | + 'eth0': { |
2709 | + 'bridge': False, 'carrier': False, 'dormant': False, |
2710 | + 'operstate': 'down', 'address': '00:11:22:33:44:55', |
2711 | + 'device/driver': 'hv_netsvc', 'device/device': '0x3', |
2712 | + 'name_assign_type': False}, |
2713 | + 'ens4': { |
2714 | + 'bridge': False, 'carrier': False, 'dormant': False, |
2715 | + 'operstate': 'down', 'address': '00:11:22:33:44:55', |
2716 | + 'device/driver': 'mlx4_core', 'device/device': '0x7', |
2717 | + 'name_assign_type': '4'}, |
2718 | + |
2719 | + } |
2720 | + |
2721 | + m_get_cmdline.return_value = 'net.ifnames=0' |
2722 | + tmp_dir = self.tmp_dir() |
2723 | + _setup_test(tmp_dir, mock_get_devicelist, |
2724 | + mock_read_sys_net, mock_sys_dev_path, |
2725 | + dev_attrs=devices) |
2726 | + net.generate_fallback_config(config_driver=True) |
2727 | + self.assertEqual(0, mock_settle.call_count) |
2728 | + |
2729 | |
2730 | class TestSysConfigRendering(CiTestCase): |
2731 | |
2732 | @@ -1829,6 +1931,12 @@ USERCTL=no |
2733 | self._compare_files_to_expected(entry['expected_sysconfig'], found) |
2734 | self._assert_headers(found) |
2735 | |
2736 | + def test_dhcpv6_only_config(self): |
2737 | + entry = NETWORK_CONFIGS['dhcpv6_only'] |
2738 | + found = self._render_and_read(network_config=yaml.load(entry['yaml'])) |
2739 | + self._compare_files_to_expected(entry['expected_sysconfig'], found) |
2740 | + self._assert_headers(found) |
2741 | + |
2742 | |
2743 | class TestEniNetRendering(CiTestCase): |
2744 | |
2745 | @@ -2277,6 +2385,13 @@ class TestNetplanRoundTrip(CiTestCase): |
2746 | entry['expected_netplan'].splitlines(), |
2747 | files['/etc/netplan/50-cloud-init.yaml'].splitlines()) |
2748 | |
2749 | + def testsimple_render_dhcpv6_only(self): |
2750 | + entry = NETWORK_CONFIGS['dhcpv6_only'] |
2751 | + files = self._render_and_read(network_config=yaml.load(entry['yaml'])) |
2752 | + self.assertEqual( |
2753 | + entry['expected_netplan'].splitlines(), |
2754 | + files['/etc/netplan/50-cloud-init.yaml'].splitlines()) |
2755 | + |
2756 | def testsimple_render_all(self): |
2757 | entry = NETWORK_CONFIGS['all'] |
2758 | files = self._render_and_read(network_config=yaml.load(entry['yaml'])) |
2759 | @@ -2345,6 +2460,13 @@ class TestEniRoundTrip(CiTestCase): |
2760 | entry['expected_eni'].splitlines(), |
2761 | files['/etc/network/interfaces'].splitlines()) |
2762 | |
2763 | + def testsimple_render_dhcpv6_only(self): |
2764 | + entry = NETWORK_CONFIGS['dhcpv6_only'] |
2765 | + files = self._render_and_read(network_config=yaml.load(entry['yaml'])) |
2766 | + self.assertEqual( |
2767 | + entry['expected_eni'].splitlines(), |
2768 | + files['/etc/network/interfaces'].splitlines()) |
2769 | + |
2770 | def testsimple_render_v4_and_v6_static(self): |
2771 | entry = NETWORK_CONFIGS['v4_and_v6_static'] |
2772 | files = self._render_and_read(network_config=yaml.load(entry['yaml'])) |
2773 | diff --git a/tests/unittests/test_sshutil.py b/tests/unittests/test_sshutil.py |
2774 | index 4c62c8b..73ae897 100644 |
2775 | --- a/tests/unittests/test_sshutil.py |
2776 | +++ b/tests/unittests/test_sshutil.py |
2777 | @@ -4,6 +4,7 @@ from mock import patch |
2778 | |
2779 | from cloudinit import ssh_util |
2780 | from cloudinit.tests import helpers as test_helpers |
2781 | +from cloudinit import util |
2782 | |
2783 | |
2784 | VALID_CONTENT = { |
2785 | @@ -56,7 +57,7 @@ TEST_OPTIONS = ( |
2786 | 'user \"root\".\';echo;sleep 10"') |
2787 | |
2788 | |
2789 | -class TestAuthKeyLineParser(test_helpers.TestCase): |
2790 | +class TestAuthKeyLineParser(test_helpers.CiTestCase): |
2791 | |
2792 | def test_simple_parse(self): |
2793 | # test key line with common 3 fields (keytype, base64, comment) |
2794 | @@ -126,7 +127,7 @@ class TestAuthKeyLineParser(test_helpers.TestCase): |
2795 | self.assertFalse(key.valid()) |
2796 | |
2797 | |
2798 | -class TestUpdateAuthorizedKeys(test_helpers.TestCase): |
2799 | +class TestUpdateAuthorizedKeys(test_helpers.CiTestCase): |
2800 | |
2801 | def test_new_keys_replace(self): |
2802 | """new entries with the same base64 should replace old.""" |
2803 | @@ -168,7 +169,7 @@ class TestUpdateAuthorizedKeys(test_helpers.TestCase): |
2804 | self.assertEqual(expected, found) |
2805 | |
2806 | |
2807 | -class TestParseSSHConfig(test_helpers.TestCase): |
2808 | +class TestParseSSHConfig(test_helpers.CiTestCase): |
2809 | |
2810 | def setUp(self): |
2811 | self.load_file_patch = patch('cloudinit.ssh_util.util.load_file') |
2812 | @@ -235,4 +236,94 @@ class TestParseSSHConfig(test_helpers.TestCase): |
2813 | self.assertEqual('foo', ret[0].key) |
2814 | self.assertEqual('bar', ret[0].value) |
2815 | |
2816 | + |
2817 | +class TestUpdateSshConfigLines(test_helpers.CiTestCase): |
2818 | + """Test the update_ssh_config_lines method.""" |
2819 | + exlines = [ |
2820 | + "#PasswordAuthentication yes", |
2821 | + "UsePAM yes", |
2822 | + "# Comment line", |
2823 | + "AcceptEnv LANG LC_*", |
2824 | + "X11Forwarding no", |
2825 | + ] |
2826 | + pwauth = "PasswordAuthentication" |
2827 | + |
2828 | + def check_line(self, line, opt, val): |
2829 | + self.assertEqual(line.key, opt.lower()) |
2830 | + self.assertEqual(line.value, val) |
2831 | + self.assertIn(opt, str(line)) |
2832 | + self.assertIn(val, str(line)) |
2833 | + |
2834 | + def test_new_option_added(self): |
2835 | + """A single update of non-existing option.""" |
2836 | + lines = ssh_util.parse_ssh_config_lines(list(self.exlines)) |
2837 | + result = ssh_util.update_ssh_config_lines(lines, {'MyKey': 'MyVal'}) |
2838 | + self.assertEqual(['MyKey'], result) |
2839 | + self.check_line(lines[-1], "MyKey", "MyVal") |
2840 | + |
2841 | + def test_commented_out_not_updated_but_appended(self): |
2842 | + """Implementation does not un-comment and update lines.""" |
2843 | + lines = ssh_util.parse_ssh_config_lines(list(self.exlines)) |
2844 | + result = ssh_util.update_ssh_config_lines(lines, {self.pwauth: "no"}) |
2845 | + self.assertEqual([self.pwauth], result) |
2846 | + self.check_line(lines[-1], self.pwauth, "no") |
2847 | + |
2848 | + def test_single_option_updated(self): |
2849 | + """A single update should have change made and line updated.""" |
2850 | + opt, val = ("UsePAM", "no") |
2851 | + lines = ssh_util.parse_ssh_config_lines(list(self.exlines)) |
2852 | + result = ssh_util.update_ssh_config_lines(lines, {opt: val}) |
2853 | + self.assertEqual([opt], result) |
2854 | + self.check_line(lines[1], opt, val) |
2855 | + |
2856 | + def test_multiple_updates_with_add(self): |
2857 | + """Verify multiple updates some added some changed, some not.""" |
2858 | + updates = {"UsePAM": "no", "X11Forwarding": "no", "NewOpt": "newval", |
2859 | + "AcceptEnv": "LANG ADD LC_*"} |
2860 | + lines = ssh_util.parse_ssh_config_lines(list(self.exlines)) |
2861 | + result = ssh_util.update_ssh_config_lines(lines, updates) |
2862 | + self.assertEqual(set(["UsePAM", "NewOpt", "AcceptEnv"]), set(result)) |
2863 | + self.check_line(lines[3], "AcceptEnv", updates["AcceptEnv"]) |
2864 | + |
2865 | + def test_return_empty_if_no_changes(self): |
2866 | + """If there are no changes, then return should be empty list.""" |
2867 | + updates = {"UsePAM": "yes"} |
2868 | + lines = ssh_util.parse_ssh_config_lines(list(self.exlines)) |
2869 | + result = ssh_util.update_ssh_config_lines(lines, updates) |
2870 | + self.assertEqual([], result) |
2871 | + self.assertEqual(self.exlines, [str(l) for l in lines]) |
2872 | + |
2873 | + def test_keycase_not_modified(self): |
2874 | + """Original case of key should not be changed on update. |
2875 | + This behavior is to keep original config as much intact as can be.""" |
2876 | + updates = {"usepam": "no"} |
2877 | + lines = ssh_util.parse_ssh_config_lines(list(self.exlines)) |
2878 | + result = ssh_util.update_ssh_config_lines(lines, updates) |
2879 | + self.assertEqual(["usepam"], result) |
2880 | + self.assertEqual("UsePAM no", str(lines[1])) |
2881 | + |
2882 | + |
2883 | +class TestUpdateSshConfig(test_helpers.CiTestCase): |
2884 | + cfgdata = '\n'.join(["#Option val", "MyKey ORIG_VAL", ""]) |
2885 | + |
2886 | + def test_modified(self): |
2887 | + mycfg = self.tmp_path("ssh_config_1") |
2888 | + util.write_file(mycfg, self.cfgdata) |
2889 | + ret = ssh_util.update_ssh_config({"MyKey": "NEW_VAL"}, mycfg) |
2890 | + self.assertTrue(ret) |
2891 | + found = util.load_file(mycfg) |
2892 | + self.assertEqual(self.cfgdata.replace("ORIG_VAL", "NEW_VAL"), found) |
2893 | + # assert there is a newline at end of file (LP: #1677205) |
2894 | + self.assertEqual('\n', found[-1]) |
2895 | + |
2896 | + def test_not_modified(self): |
2897 | + mycfg = self.tmp_path("ssh_config_2") |
2898 | + util.write_file(mycfg, self.cfgdata) |
2899 | + with patch("cloudinit.ssh_util.util.write_file") as m_write_file: |
2900 | + ret = ssh_util.update_ssh_config({"MyKey": "ORIG_VAL"}, mycfg) |
2901 | + self.assertFalse(ret) |
2902 | + self.assertEqual(self.cfgdata, util.load_file(mycfg)) |
2903 | + m_write_file.assert_not_called() |
2904 | + |
2905 | + |
2906 | # vi: ts=4 expandtab |
2907 | diff --git a/tests/unittests/test_templating.py b/tests/unittests/test_templating.py |
2908 | index 1080e13..20c87ef 100644 |
2909 | --- a/tests/unittests/test_templating.py |
2910 | +++ b/tests/unittests/test_templating.py |
2911 | @@ -50,12 +50,12 @@ class TestTemplates(test_helpers.CiTestCase): |
2912 | def test_detection(self): |
2913 | blob = "## template:cheetah" |
2914 | |
2915 | - (template_type, renderer, contents) = templater.detect_template(blob) |
2916 | + (template_type, _renderer, contents) = templater.detect_template(blob) |
2917 | self.assertIn("cheetah", template_type) |
2918 | self.assertEqual("", contents.strip()) |
2919 | |
2920 | blob = "blahblah $blah" |
2921 | - (template_type, renderer, contents) = templater.detect_template(blob) |
2922 | + (template_type, _renderer, _contents) = templater.detect_template(blob) |
2923 | self.assertIn("cheetah", template_type) |
2924 | self.assertEqual(blob, contents) |
2925 | |
2926 | diff --git a/tests/unittests/test_util.py b/tests/unittests/test_util.py |
2927 | index e04ea03..84941c7 100644 |
2928 | --- a/tests/unittests/test_util.py |
2929 | +++ b/tests/unittests/test_util.py |
2930 | @@ -774,11 +774,11 @@ class TestSubp(helpers.CiTestCase): |
2931 | |
2932 | def test_subp_reads_env(self): |
2933 | with mock.patch.dict("os.environ", values={'FOO': 'BAR'}): |
2934 | - out, err = util.subp(self.printenv + ['FOO'], capture=True) |
2935 | + out, _err = util.subp(self.printenv + ['FOO'], capture=True) |
2936 | self.assertEqual('FOO=BAR', out.splitlines()[0]) |
2937 | |
2938 | def test_subp_env_and_update_env(self): |
2939 | - out, err = util.subp( |
2940 | + out, _err = util.subp( |
2941 | self.printenv + ['FOO', 'HOME', 'K1', 'K2'], capture=True, |
2942 | env={'FOO': 'BAR'}, |
2943 | update_env={'HOME': '/myhome', 'K2': 'V2'}) |
2944 | @@ -788,7 +788,7 @@ class TestSubp(helpers.CiTestCase): |
2945 | def test_subp_update_env(self): |
2946 | extra = {'FOO': 'BAR', 'HOME': '/root', 'K1': 'V1'} |
2947 | with mock.patch.dict("os.environ", values=extra): |
2948 | - out, err = util.subp( |
2949 | + out, _err = util.subp( |
2950 | self.printenv + ['FOO', 'HOME', 'K1', 'K2'], capture=True, |
2951 | update_env={'HOME': '/myhome', 'K2': 'V2'}) |
2952 | |
2953 | diff --git a/tools/ds-identify b/tools/ds-identify |
2954 | index 9a2db5c..7fff5d1 100755 |
2955 | --- a/tools/ds-identify |
2956 | +++ b/tools/ds-identify |
2957 | @@ -125,6 +125,7 @@ DI_ON_NOTFOUND="" |
2958 | DI_EC2_STRICT_ID_DEFAULT="true" |
2959 | |
2960 | _IS_IBM_CLOUD="" |
2961 | +_IS_IBM_PROVISIONING="" |
2962 | |
2963 | error() { |
2964 | set -- "ERROR:" "$@"; |
2965 | @@ -1006,7 +1007,25 @@ dscheck_Hetzner() { |
2966 | } |
2967 | |
2968 | is_ibm_provisioning() { |
2969 | - [ -f "${PATH_ROOT}/root/provisioningConfiguration.cfg" ] |
2970 | + local pcfg="${PATH_ROOT}/root/provisioningConfiguration.cfg" |
2971 | + local logf="${PATH_ROOT}/root/swinstall.log" |
2972 | + local is_prov=false msg="config '$pcfg' did not exist." |
2973 | + if [ -f "$pcfg" ]; then |
2974 | + msg="config '$pcfg' exists." |
2975 | + is_prov=true |
2976 | + if [ -f "$logf" ]; then |
2977 | + if [ "$logf" -nt "$PATH_PROC_1_ENVIRON" ]; then |
2978 | + msg="$msg log '$logf' from current boot." |
2979 | + else |
2980 | + is_prov=false |
2981 | + msg="$msg log '$logf' from previous boot." |
2982 | + fi |
2983 | + else |
2984 | + msg="$msg log '$logf' did not exist." |
2985 | + fi |
2986 | + fi |
2987 | + debug 2 "ibm_provisioning=$is_prov: $msg" |
2988 | + [ "$is_prov" = "true" ] |
2989 | } |
2990 | |
2991 | is_ibm_cloud() { |
PASSED: Continuous integration, rev:7609065fd30 c3a02122bded6ab 213ebc3912584b /jenkins. ubuntu. com/server/ job/cloud- init-ci/ 1071/
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: /jenkins. ubuntu. com/server/ job/cloud- init-ci/ 1071/rebuild
https:/