Merge ~chad.smith/cloud-init:ubuntu/zesty into cloud-init:ubuntu/zesty
- Git
- lp:~chad.smith/cloud-init
- ubuntu/zesty
- Merge into ubuntu/zesty
Proposed by
Chad Smith
Status: | Merged | ||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
Approved by: | Scott Moser | ||||||||||||||||
Approved revision: | e6747555e0dc787fa99bd6bfff43846b02ef1bd1 | ||||||||||||||||
Merged at revision: | 2f3aae742b0a8456013f441b65b118872c60c001 | ||||||||||||||||
Proposed branch: | ~chad.smith/cloud-init:ubuntu/zesty | ||||||||||||||||
Merge into: | cloud-init:ubuntu/zesty | ||||||||||||||||
Diff against target: |
2207 lines (+787/-454) 51 files modified
cloudinit/cloud.py (+2/-2) cloudinit/config/cc_ntp.py (+7/-2) cloudinit/config/cc_rh_subscription.py (+28/-18) cloudinit/config/cc_update_etc_hosts.py (+2/-2) cloudinit/net/dhcp.py (+9/-3) cloudinit/net/tests/test_dhcp.py (+8/-1) cloudinit/sources/DataSourceAzure.py (+2/-23) cloudinit/sources/DataSourceEc2.py (+33/-11) cloudinit/user_data.py (+23/-10) debian/changelog (+26/-0) debian/cloud-init.templates (+3/-3) dev/null (+0/-26) sysvinit/gentoo/cloud-config (+0/-0) sysvinit/gentoo/cloud-final (+0/-0) sysvinit/gentoo/cloud-init (+0/-0) sysvinit/gentoo/cloud-init-local (+0/-0) templates/hosts.suse.tmpl (+8/-2) templates/ntp.conf.opensuse.tmpl (+88/-0) templates/ntp.conf.sles.tmpl (+0/-12) tests/cloud_tests/collect.py (+16/-2) tests/cloud_tests/images/base.py (+3/-16) tests/cloud_tests/images/lxd.py (+18/-14) tests/cloud_tests/images/nocloudkvm.py (+18/-24) tests/cloud_tests/instances/base.py (+4/-77) tests/cloud_tests/instances/lxd.py (+56/-48) tests/cloud_tests/instances/nocloudkvm.py (+51/-91) tests/cloud_tests/testcases/examples/run_commands.yaml (+2/-2) tests/cloud_tests/testcases/modules/apt_configure_sources_ppa.py (+3/-3) tests/cloud_tests/testcases/modules/apt_configure_sources_ppa.yaml (+3/-3) tests/cloud_tests/testcases/modules/keys_to_console.py (+4/-4) tests/cloud_tests/testcases/modules/runcmd.yaml (+2/-2) tests/cloud_tests/testcases/modules/set_hostname.py (+3/-1) tests/cloud_tests/testcases/modules/set_hostname.yaml (+2/-1) tests/cloud_tests/testcases/modules/set_hostname_fqdn.py (+8/-3) tests/cloud_tests/testcases/modules/set_hostname_fqdn.yaml (+3/-2) tests/cloud_tests/testcases/modules/set_password_expire.py (+1/-1) tests/cloud_tests/testcases/modules/set_password_expire.yaml (+2/-0) tests/cloud_tests/testcases/modules/set_password_list.yaml (+1/-0) tests/cloud_tests/testcases/modules/set_password_list_string.yaml (+1/-0) tests/cloud_tests/testcases/modules/ssh_auth_key_fingerprints_disable.py (+0/-8) tests/cloud_tests/testcases/modules/ssh_auth_key_fingerprints_disable.yaml (+0/-1) tests/cloud_tests/testcases/modules/ssh_keys_generate.py (+0/-5) tests/cloud_tests/testcases/modules/ssh_keys_generate.yaml (+0/-6) tests/cloud_tests/testcases/modules/ssh_keys_provided.py (+0/-11) tests/cloud_tests/testcases/modules/ssh_keys_provided.yaml (+0/-6) tests/cloud_tests/util.py (+154/-8) tests/unittests/test_data.py (+50/-0) tests/unittests/test_datasource/test_ec2.py (+33/-0) tests/unittests/test_handler/test_handler_etc_hosts.py (+69/-0) tests/unittests/test_handler/test_handler_ntp.py (+26/-0) tests/unittests/test_rh_subscription.py (+15/-0) |
||||||||||||||||
Related bugs: |
|
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Server Team CI bot | continuous-integration | Approve | |
cloud-init Commiters | Pending | ||
Review via email: mp+334071@code.launchpad.net |
Commit message
Description of the change
Upstream master snapshot for SRU into zesty.
To post a comment you must log in.
Revision history for this message
Server Team CI bot (server-team-bot) wrote : | # |
review:
Approve
(continuous-integration)
There was an error fetching revisions from git servers. Please try again in a few minutes. If the problem persists, contact Launchpad support.
Preview Diff
[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1 | diff --git a/cloudinit/cloud.py b/cloudinit/cloud.py |
2 | index d8a9fc8..ba61678 100644 |
3 | --- a/cloudinit/cloud.py |
4 | +++ b/cloudinit/cloud.py |
5 | @@ -56,8 +56,8 @@ class Cloud(object): |
6 | def get_template_filename(self, name): |
7 | fn = self.paths.template_tpl % (name) |
8 | if not os.path.isfile(fn): |
9 | - LOG.warning("No template found at %s for template named %s", |
10 | - fn, name) |
11 | + LOG.warning("No template found in %s for template named %s", |
12 | + os.path.dirname(fn), name) |
13 | return None |
14 | return fn |
15 | |
16 | diff --git a/cloudinit/config/cc_ntp.py b/cloudinit/config/cc_ntp.py |
17 | index d43d060..f50bcb3 100644 |
18 | --- a/cloudinit/config/cc_ntp.py |
19 | +++ b/cloudinit/config/cc_ntp.py |
20 | @@ -23,7 +23,7 @@ frequency = PER_INSTANCE |
21 | NTP_CONF = '/etc/ntp.conf' |
22 | TIMESYNCD_CONF = '/etc/systemd/timesyncd.conf.d/cloud-init.conf' |
23 | NR_POOL_SERVERS = 4 |
24 | -distros = ['centos', 'debian', 'fedora', 'opensuse', 'ubuntu'] |
25 | +distros = ['centos', 'debian', 'fedora', 'opensuse', 'sles', 'ubuntu'] |
26 | |
27 | |
28 | # The schema definition for each cloud-config module is a strict contract for |
29 | @@ -174,8 +174,13 @@ def rename_ntp_conf(config=None): |
30 | |
31 | def generate_server_names(distro): |
32 | names = [] |
33 | + pool_distro = distro |
34 | + # For legal reasons x.pool.sles.ntp.org does not exist, |
35 | + # use the opensuse pool |
36 | + if distro == 'sles': |
37 | + pool_distro = 'opensuse' |
38 | for x in range(0, NR_POOL_SERVERS): |
39 | - name = "%d.%s.pool.ntp.org" % (x, distro) |
40 | + name = "%d.%s.pool.ntp.org" % (x, pool_distro) |
41 | names.append(name) |
42 | return names |
43 | |
44 | diff --git a/cloudinit/config/cc_rh_subscription.py b/cloudinit/config/cc_rh_subscription.py |
45 | index 7f36cf8..a9d21e7 100644 |
46 | --- a/cloudinit/config/cc_rh_subscription.py |
47 | +++ b/cloudinit/config/cc_rh_subscription.py |
48 | @@ -38,14 +38,16 @@ Subscription`` example config. |
49 | server-hostname: <hostname> |
50 | """ |
51 | |
52 | +from cloudinit import log as logging |
53 | from cloudinit import util |
54 | |
55 | +LOG = logging.getLogger(__name__) |
56 | + |
57 | distros = ['fedora', 'rhel'] |
58 | |
59 | |
60 | def handle(name, cfg, _cloud, log, _args): |
61 | - sm = SubscriptionManager(cfg) |
62 | - sm.log = log |
63 | + sm = SubscriptionManager(cfg, log=log) |
64 | if not sm.is_configured(): |
65 | log.debug("%s: module not configured.", name) |
66 | return None |
67 | @@ -86,10 +88,9 @@ def handle(name, cfg, _cloud, log, _args): |
68 | if not return_stat: |
69 | raise SubscriptionError("Unable to attach pools {0}" |
70 | .format(sm.pools)) |
71 | - if (sm.enable_repo is not None) or (sm.disable_repo is not None): |
72 | - return_stat = sm.update_repos(sm.enable_repo, sm.disable_repo) |
73 | - if not return_stat: |
74 | - raise SubscriptionError("Unable to add or remove repos") |
75 | + return_stat = sm.update_repos() |
76 | + if not return_stat: |
77 | + raise SubscriptionError("Unable to add or remove repos") |
78 | sm.log_success("rh_subscription plugin completed successfully") |
79 | except SubscriptionError as e: |
80 | sm.log_warn(str(e)) |
81 | @@ -108,7 +109,10 @@ class SubscriptionManager(object): |
82 | 'rhsm-baseurl', 'server-hostname', |
83 | 'auto-attach', 'service-level'] |
84 | |
85 | - def __init__(self, cfg): |
86 | + def __init__(self, cfg, log=None): |
87 | + if log is None: |
88 | + log = LOG |
89 | + self.log = log |
90 | self.cfg = cfg |
91 | self.rhel_cfg = self.cfg.get('rh_subscription', {}) |
92 | self.rhsm_baseurl = self.rhel_cfg.get('rhsm-baseurl') |
93 | @@ -130,7 +134,7 @@ class SubscriptionManager(object): |
94 | |
95 | def log_warn(self, msg): |
96 | '''Simple wrapper for logging warning messages. Useful for unittests''' |
97 | - self.log.warn(msg) |
98 | + self.log.warning(msg) |
99 | |
100 | def _verify_keys(self): |
101 | ''' |
102 | @@ -245,7 +249,7 @@ class SubscriptionManager(object): |
103 | return False |
104 | |
105 | reg_id = return_out.split("ID: ")[1].rstrip() |
106 | - self.log.debug("Registered successfully with ID {0}".format(reg_id)) |
107 | + self.log.debug("Registered successfully with ID %s", reg_id) |
108 | return True |
109 | |
110 | def _set_service_level(self): |
111 | @@ -347,7 +351,7 @@ class SubscriptionManager(object): |
112 | try: |
113 | self._sub_man_cli(cmd) |
114 | self.log.debug("Attached the following pools to your " |
115 | - "system: %s" % (", ".join(pool_list)) |
116 | + "system: %s", (", ".join(pool_list)) |
117 | .replace('--pool=', '')) |
118 | return True |
119 | except util.ProcessExecutionError as e: |
120 | @@ -355,18 +359,24 @@ class SubscriptionManager(object): |
121 | "due to {1}".format(pool, e)) |
122 | return False |
123 | |
124 | - def update_repos(self, erepos, drepos): |
125 | + def update_repos(self): |
126 | ''' |
127 | Takes a list of yum repo ids that need to be disabled or enabled; then |
128 | it verifies if they are already enabled or disabled and finally |
129 | executes the action to disable or enable |
130 | ''' |
131 | |
132 | - if (erepos is not None) and (not isinstance(erepos, list)): |
133 | + erepos = self.enable_repo |
134 | + drepos = self.disable_repo |
135 | + if erepos is None: |
136 | + erepos = [] |
137 | + if drepos is None: |
138 | + drepos = [] |
139 | + if not isinstance(erepos, list): |
140 | self.log_warn("Repo IDs must in the format of a list.") |
141 | return False |
142 | |
143 | - if (drepos is not None) and (not isinstance(drepos, list)): |
144 | + if not isinstance(drepos, list): |
145 | self.log_warn("Repo IDs must in the format of a list.") |
146 | return False |
147 | |
148 | @@ -399,14 +409,14 @@ class SubscriptionManager(object): |
149 | for fail in enable_list_fail: |
150 | # Check if the repo exists or not |
151 | if fail in active_repos: |
152 | - self.log.debug("Repo {0} is already enabled".format(fail)) |
153 | + self.log.debug("Repo %s is already enabled", fail) |
154 | else: |
155 | self.log_warn("Repo {0} does not appear to " |
156 | "exist".format(fail)) |
157 | if len(disable_list_fail) > 0: |
158 | for fail in disable_list_fail: |
159 | - self.log.debug("Repo {0} not disabled " |
160 | - "because it is not enabled".format(fail)) |
161 | + self.log.debug("Repo %s not disabled " |
162 | + "because it is not enabled", fail) |
163 | |
164 | cmd = ['repos'] |
165 | if len(disable_list) > 0: |
166 | @@ -422,10 +432,10 @@ class SubscriptionManager(object): |
167 | return False |
168 | |
169 | if len(enable_list) > 0: |
170 | - self.log.debug("Enabled the following repos: %s" % |
171 | + self.log.debug("Enabled the following repos: %s", |
172 | (", ".join(enable_list)).replace('--enable=', '')) |
173 | if len(disable_list) > 0: |
174 | - self.log.debug("Disabled the following repos: %s" % |
175 | + self.log.debug("Disabled the following repos: %s", |
176 | (", ".join(disable_list)).replace('--disable=', '')) |
177 | return True |
178 | |
179 | diff --git a/cloudinit/config/cc_update_etc_hosts.py b/cloudinit/config/cc_update_etc_hosts.py |
180 | index b394784..c96eede 100644 |
181 | --- a/cloudinit/config/cc_update_etc_hosts.py |
182 | +++ b/cloudinit/config/cc_update_etc_hosts.py |
183 | @@ -23,8 +23,8 @@ using the template located in ``/etc/cloud/templates/hosts.tmpl``. In the |
184 | |
185 | If ``manage_etc_hosts`` is set to ``localhost``, then cloud-init will not |
186 | rewrite ``/etc/hosts`` entirely, but rather will ensure that a entry for the |
187 | -fqdn with ip ``127.0.1.1`` is present in ``/etc/hosts`` (i.e. |
188 | -``ping <hostname>`` will ping ``127.0.1.1``). |
189 | +fqdn with a distribution dependent ip is present in ``/etc/hosts`` (i.e. |
190 | +``ping <hostname>`` will ping ``127.0.0.1`` or ``127.0.1.1`` or other ip). |
191 | |
192 | .. note:: |
193 | if ``manage_etc_hosts`` is set ``true`` or ``template``, the contents |
194 | diff --git a/cloudinit/net/dhcp.py b/cloudinit/net/dhcp.py |
195 | index 0cba703..d8624d8 100644 |
196 | --- a/cloudinit/net/dhcp.py |
197 | +++ b/cloudinit/net/dhcp.py |
198 | @@ -8,6 +8,7 @@ import configobj |
199 | import logging |
200 | import os |
201 | import re |
202 | +import signal |
203 | |
204 | from cloudinit.net import find_fallback_nic, get_devicelist |
205 | from cloudinit import temp_utils |
206 | @@ -41,8 +42,7 @@ def maybe_perform_dhcp_discovery(nic=None): |
207 | if nic is None: |
208 | nic = find_fallback_nic() |
209 | if nic is None: |
210 | - LOG.debug( |
211 | - 'Skip dhcp_discovery: Unable to find fallback nic.') |
212 | + LOG.debug('Skip dhcp_discovery: Unable to find fallback nic.') |
213 | return {} |
214 | elif nic not in get_devicelist(): |
215 | LOG.debug( |
216 | @@ -119,7 +119,13 @@ def dhcp_discovery(dhclient_cmd_path, interface, cleandir): |
217 | cmd = [sandbox_dhclient_cmd, '-1', '-v', '-lf', lease_file, |
218 | '-pf', pid_file, interface, '-sf', '/bin/true'] |
219 | util.subp(cmd, capture=True) |
220 | - return parse_dhcp_lease_file(lease_file) |
221 | + pid = None |
222 | + try: |
223 | + pid = int(util.load_file(pid_file).strip()) |
224 | + return parse_dhcp_lease_file(lease_file) |
225 | + finally: |
226 | + if pid: |
227 | + os.kill(pid, signal.SIGKILL) |
228 | |
229 | |
230 | def networkd_parse_lease(content): |
231 | diff --git a/cloudinit/net/tests/test_dhcp.py b/cloudinit/net/tests/test_dhcp.py |
232 | index 1c1f504..3d8e15c 100644 |
233 | --- a/cloudinit/net/tests/test_dhcp.py |
234 | +++ b/cloudinit/net/tests/test_dhcp.py |
235 | @@ -2,6 +2,7 @@ |
236 | |
237 | import mock |
238 | import os |
239 | +import signal |
240 | from textwrap import dedent |
241 | |
242 | from cloudinit.net.dhcp import ( |
243 | @@ -114,8 +115,9 @@ class TestDHCPDiscoveryClean(CiTestCase): |
244 | self.assertEqual('eth9', call[0][1]) |
245 | self.assertIn('/var/tmp/cloud-init/cloud-init-dhcp-', call[0][2]) |
246 | |
247 | + @mock.patch('cloudinit.net.dhcp.os.kill') |
248 | @mock.patch('cloudinit.net.dhcp.util.subp') |
249 | - def test_dhcp_discovery_run_in_sandbox(self, m_subp): |
250 | + def test_dhcp_discovery_run_in_sandbox(self, m_subp, m_kill): |
251 | """dhcp_discovery brings up the interface and runs dhclient. |
252 | |
253 | It also returns the parsed dhcp.leases file generated in the sandbox. |
254 | @@ -134,6 +136,10 @@ class TestDHCPDiscoveryClean(CiTestCase): |
255 | """) |
256 | lease_file = os.path.join(tmpdir, 'dhcp.leases') |
257 | write_file(lease_file, lease_content) |
258 | + pid_file = os.path.join(tmpdir, 'dhclient.pid') |
259 | + my_pid = 1 |
260 | + write_file(pid_file, "%d\n" % my_pid) |
261 | + |
262 | self.assertItemsEqual( |
263 | [{'interface': 'eth9', 'fixed-address': '192.168.2.74', |
264 | 'subnet-mask': '255.255.255.0', 'routers': '192.168.2.1'}], |
265 | @@ -149,6 +155,7 @@ class TestDHCPDiscoveryClean(CiTestCase): |
266 | [os.path.join(tmpdir, 'dhclient'), '-1', '-v', '-lf', |
267 | lease_file, '-pf', os.path.join(tmpdir, 'dhclient.pid'), |
268 | 'eth9', '-sf', '/bin/true'], capture=True)]) |
269 | + m_kill.assert_has_calls([mock.call(my_pid, signal.SIGKILL)]) |
270 | |
271 | |
272 | class TestSystemdParseLeases(CiTestCase): |
273 | diff --git a/cloudinit/sources/DataSourceAzure.py b/cloudinit/sources/DataSourceAzure.py |
274 | index 80c2bd1..8c3492d 100644 |
275 | --- a/cloudinit/sources/DataSourceAzure.py |
276 | +++ b/cloudinit/sources/DataSourceAzure.py |
277 | @@ -465,10 +465,8 @@ class DataSourceAzure(sources.DataSource): |
278 | |
279 | 1. Probe the drivers of the net-devices present and inject them in |
280 | the network configuration under params: driver: <driver> value |
281 | - 2. If the driver value is 'mlx4_core', the control mode should be |
282 | - set to manual. The device will be later used to build a bond, |
283 | - for now we want to ensure the device gets named but does not |
284 | - break any network configuration |
285 | + 2. Generate a fallback network config that does not include any of |
286 | + the blacklisted devices. |
287 | """ |
288 | blacklist = ['mlx4_core'] |
289 | if not self._network_config: |
290 | @@ -477,25 +475,6 @@ class DataSourceAzure(sources.DataSource): |
291 | netconfig = net.generate_fallback_config( |
292 | blacklist_drivers=blacklist, config_driver=True) |
293 | |
294 | - # if we have any blacklisted devices, update the network_config to |
295 | - # include the device, mac, and driver values, but with no ip |
296 | - # config; this ensures udev rules are generated but won't affect |
297 | - # ip configuration |
298 | - bl_found = 0 |
299 | - for bl_dev in [dev for dev in net.get_devicelist() |
300 | - if net.device_driver(dev) in blacklist]: |
301 | - bl_found += 1 |
302 | - cfg = { |
303 | - 'type': 'physical', |
304 | - 'name': 'vf%d' % bl_found, |
305 | - 'mac_address': net.get_interface_mac(bl_dev), |
306 | - 'params': { |
307 | - 'driver': net.device_driver(bl_dev), |
308 | - 'device_id': net.device_devid(bl_dev), |
309 | - }, |
310 | - } |
311 | - netconfig['config'].append(cfg) |
312 | - |
313 | self._network_config = netconfig |
314 | |
315 | return self._network_config |
316 | diff --git a/cloudinit/sources/DataSourceEc2.py b/cloudinit/sources/DataSourceEc2.py |
317 | index 0ef2217..7bbbfb6 100644 |
318 | --- a/cloudinit/sources/DataSourceEc2.py |
319 | +++ b/cloudinit/sources/DataSourceEc2.py |
320 | @@ -65,7 +65,7 @@ class DataSourceEc2(sources.DataSource): |
321 | get_network_metadata = False |
322 | |
323 | # Track the discovered fallback nic for use in configuration generation. |
324 | - fallback_nic = None |
325 | + _fallback_interface = None |
326 | |
327 | def __init__(self, sys_cfg, distro, paths): |
328 | sources.DataSource.__init__(self, sys_cfg, distro, paths) |
329 | @@ -92,18 +92,17 @@ class DataSourceEc2(sources.DataSource): |
330 | elif self.cloud_platform == Platforms.NO_EC2_METADATA: |
331 | return False |
332 | |
333 | - self.fallback_nic = net.find_fallback_nic() |
334 | if self.get_network_metadata: # Setup networking in init-local stage. |
335 | if util.is_FreeBSD(): |
336 | LOG.debug("FreeBSD doesn't support running dhclient with -sf") |
337 | return False |
338 | - dhcp_leases = dhcp.maybe_perform_dhcp_discovery(self.fallback_nic) |
339 | + dhcp_leases = dhcp.maybe_perform_dhcp_discovery( |
340 | + self.fallback_interface) |
341 | if not dhcp_leases: |
342 | # DataSourceEc2Local failed in init-local stage. DataSourceEc2 |
343 | # will still run in init-network stage. |
344 | return False |
345 | dhcp_opts = dhcp_leases[-1] |
346 | - self.fallback_nic = dhcp_opts.get('interface') |
347 | net_params = {'interface': dhcp_opts.get('interface'), |
348 | 'ip': dhcp_opts.get('fixed-address'), |
349 | 'prefix_or_mask': dhcp_opts.get('subnet-mask'), |
350 | @@ -301,21 +300,44 @@ class DataSourceEc2(sources.DataSource): |
351 | return None |
352 | |
353 | result = None |
354 | - net_md = self.metadata.get('network') |
355 | + no_network_metadata_on_aws = bool( |
356 | + 'network' not in self.metadata and |
357 | + self.cloud_platform == Platforms.AWS) |
358 | + if no_network_metadata_on_aws: |
359 | + LOG.debug("Metadata 'network' not present:" |
360 | + " Refreshing stale metadata from prior to upgrade.") |
361 | + util.log_time( |
362 | + logfunc=LOG.debug, msg='Re-crawl of metadata service', |
363 | + func=self._crawl_metadata) |
364 | + |
365 | # Limit network configuration to only the primary/fallback nic |
366 | - macs_to_nics = { |
367 | - net.get_interface_mac(self.fallback_nic): self.fallback_nic} |
368 | + iface = self.fallback_interface |
369 | + macs_to_nics = {net.get_interface_mac(iface): iface} |
370 | + net_md = self.metadata.get('network') |
371 | if isinstance(net_md, dict): |
372 | result = convert_ec2_metadata_network_config( |
373 | - net_md, macs_to_nics=macs_to_nics, |
374 | - fallback_nic=self.fallback_nic) |
375 | + net_md, macs_to_nics=macs_to_nics, fallback_nic=iface) |
376 | else: |
377 | - LOG.warning("unexpected metadata 'network' key not valid: %s", |
378 | - net_md) |
379 | + LOG.warning("Metadata 'network' key not valid: %s.", net_md) |
380 | self._network_config = result |
381 | |
382 | return self._network_config |
383 | |
384 | + @property |
385 | + def fallback_interface(self): |
386 | + if self._fallback_interface is None: |
387 | + # fallback_nic was used at one point, so restored objects may |
388 | + # have an attribute there. respect that if found. |
389 | + _legacy_fbnic = getattr(self, 'fallback_nic', None) |
390 | + if _legacy_fbnic: |
391 | + self._fallback_interface = _legacy_fbnic |
392 | + self.fallback_nic = None |
393 | + else: |
394 | + self._fallback_interface = net.find_fallback_nic() |
395 | + if self._fallback_interface is None: |
396 | + LOG.warning("Did not find a fallback interface on EC2.") |
397 | + return self._fallback_interface |
398 | + |
399 | def _crawl_metadata(self): |
400 | """Crawl metadata service when available. |
401 | |
402 | diff --git a/cloudinit/user_data.py b/cloudinit/user_data.py |
403 | index 88cb7f8..cc55daf 100644 |
404 | --- a/cloudinit/user_data.py |
405 | +++ b/cloudinit/user_data.py |
406 | @@ -19,6 +19,7 @@ import six |
407 | |
408 | from cloudinit import handlers |
409 | from cloudinit import log as logging |
410 | +from cloudinit.url_helper import UrlError |
411 | from cloudinit import util |
412 | |
413 | LOG = logging.getLogger(__name__) |
414 | @@ -222,16 +223,28 @@ class UserDataProcessor(object): |
415 | if include_once_on and os.path.isfile(include_once_fn): |
416 | content = util.load_file(include_once_fn) |
417 | else: |
418 | - resp = util.read_file_or_url(include_url, |
419 | - ssl_details=self.ssl_details) |
420 | - if include_once_on and resp.ok(): |
421 | - util.write_file(include_once_fn, resp.contents, mode=0o600) |
422 | - if resp.ok(): |
423 | - content = resp.contents |
424 | - else: |
425 | - LOG.warning(("Fetching from %s resulted in" |
426 | - " a invalid http code of %s"), |
427 | - include_url, resp.code) |
428 | + try: |
429 | + resp = util.read_file_or_url(include_url, |
430 | + ssl_details=self.ssl_details) |
431 | + if include_once_on and resp.ok(): |
432 | + util.write_file(include_once_fn, resp.contents, |
433 | + mode=0o600) |
434 | + if resp.ok(): |
435 | + content = resp.contents |
436 | + else: |
437 | + LOG.warning(("Fetching from %s resulted in" |
438 | + " a invalid http code of %s"), |
439 | + include_url, resp.code) |
440 | + except UrlError as urle: |
441 | + message = str(urle) |
442 | + # Older versions of requests.exceptions.HTTPError may not |
443 | + # include the errant url. Append it for clarity in logs. |
444 | + if include_url not in message: |
445 | + message += ' for url: {0}'.format(include_url) |
446 | + LOG.warning(message) |
447 | + except IOError as ioe: |
448 | + LOG.warning("Fetching from %s resulted in %s", |
449 | + include_url, ioe) |
450 | |
451 | if content is not None: |
452 | new_msg = convert_string(content) |
453 | diff --git a/debian/changelog b/debian/changelog |
454 | index bc6c449..6c01326 100644 |
455 | --- a/debian/changelog |
456 | +++ b/debian/changelog |
457 | @@ -1,3 +1,29 @@ |
458 | +cloud-init (17.1-41-g76243487-0ubuntu1~17.04.1) zesty-proposed; urgency=medium |
459 | + |
460 | + * cherry-pick 1110f30: debian/cloud-init.templates: Fix capitilazation |
461 | + in 'AliYun' (LP: #1728186) |
462 | + * New upstream snapshot (LP: #1733653) |
463 | + - integration test: replace curtin test ppa with cloud-init test ppa. |
464 | + - EC2: Fix bug using fallback_nic and metadata when restoring from cache. |
465 | + - EC2: Kill dhclient process used in sandbox dhclient. |
466 | + - ntp: fix configuration template rendering for openSUSE and SLES |
467 | + - centos: Provide the failed #include url in error messages |
468 | + - Catch UrlError when #include'ing URLs [Andrew Jorgensen] |
469 | + - hosts: Fix openSUSE and SLES setup for /etc/hosts and clarify docs. |
470 | + [Robert Schweikert] |
471 | + - rh_subscription: Perform null checks for enabled and disabled repos. |
472 | + [Dave Mulford] |
473 | + - Improve warning message when a template is not found. |
474 | + [Robert Schweikert] |
475 | + - Replace the temporary i9n.brickies.net with i9n.cloud-init.io. |
476 | + - Azure: don't generate network configuration for SRIOV devices |
477 | + - tests: address some minor feedback missed in last merge. |
478 | + - tests: integration test cleanup and full pass of nocloud-kvm. |
479 | + - Gentoo: chmod +x on all files in sysvinit/gentoo/ |
480 | + [Carlos Konstanski] |
481 | + |
482 | + -- Chad Smith <chad.smith@canonical.com> Tue, 21 Nov 2017 11:48:32 -0700 |
483 | + |
484 | cloud-init (17.1-27-geb292c18-0ubuntu1~17.04.1) zesty-proposed; urgency=medium |
485 | |
486 | * New upstream snapshot. |
487 | diff --git a/debian/cloud-init.templates b/debian/cloud-init.templates |
488 | index 0a251e3..5ed37f7 100644 |
489 | --- a/debian/cloud-init.templates |
490 | +++ b/debian/cloud-init.templates |
491 | @@ -1,8 +1,8 @@ |
492 | Template: cloud-init/datasources |
493 | Type: multiselect |
494 | -Default: NoCloud, ConfigDrive, OpenNebula, DigitalOcean, Azure, AltCloud, OVF, MAAS, GCE, OpenStack, CloudSigma, SmartOS, Bigstep, Scaleway, Aliyun, Ec2, CloudStack, None |
495 | -Choices-C: NoCloud, ConfigDrive, OpenNebula, DigitalOcean, Azure, AltCloud, OVF, MAAS, GCE, OpenStack, CloudSigma, SmartOS, Bigstep, Scaleway, Aliyun, Ec2, CloudStack, None |
496 | -Choices: NoCloud: Reads info from /var/lib/cloud/seed only, ConfigDrive: Reads data from Openstack Config Drive, OpenNebula: read from OpenNebula context disk, DigitalOcean: reads data from Droplet datasource, Azure: read from MS Azure cdrom. Requires walinux-agent, AltCloud: config disks for RHEVm and vSphere, OVF: Reads data from OVF Transports, MAAS: Reads data from Ubuntu MAAS, GCE: google compute metadata service, OpenStack: native openstack metadata service, CloudSigma: metadata over serial for cloudsigma.com, SmartOS: Read from SmartOS metadata service, Bigstep: Bigstep metadata service, Scaleway: Scaleway metadata service, Aliyun: Alibaba metadata service, Ec2: reads data from EC2 Metadata service, CloudStack: Read from CloudStack metadata service, None: Failsafe datasource |
497 | +Default: NoCloud, ConfigDrive, OpenNebula, DigitalOcean, Azure, AltCloud, OVF, MAAS, GCE, OpenStack, CloudSigma, SmartOS, Bigstep, Scaleway, AliYun, Ec2, CloudStack, None |
498 | +Choices-C: NoCloud, ConfigDrive, OpenNebula, DigitalOcean, Azure, AltCloud, OVF, MAAS, GCE, OpenStack, CloudSigma, SmartOS, Bigstep, Scaleway, AliYun, Ec2, CloudStack, None |
499 | +Choices: NoCloud: Reads info from /var/lib/cloud/seed only, ConfigDrive: Reads data from Openstack Config Drive, OpenNebula: read from OpenNebula context disk, DigitalOcean: reads data from Droplet datasource, Azure: read from MS Azure cdrom. Requires walinux-agent, AltCloud: config disks for RHEVm and vSphere, OVF: Reads data from OVF Transports, MAAS: Reads data from Ubuntu MAAS, GCE: google compute metadata service, OpenStack: native openstack metadata service, CloudSigma: metadata over serial for cloudsigma.com, SmartOS: Read from SmartOS metadata service, Bigstep: Bigstep metadata service, Scaleway: Scaleway metadata service, AliYun: Alibaba metadata service, Ec2: reads data from EC2 Metadata service, CloudStack: Read from CloudStack metadata service, None: Failsafe datasource |
500 | Description: Which data sources should be searched? |
501 | Cloud-init supports searching different "Data Sources" for information |
502 | that it uses to configure a cloud instance. |
503 | diff --git a/sysvinit/gentoo/cloud-config b/sysvinit/gentoo/cloud-config |
504 | old mode 100644 |
505 | new mode 100755 |
506 | index 5618472..5618472 |
507 | --- a/sysvinit/gentoo/cloud-config |
508 | +++ b/sysvinit/gentoo/cloud-config |
509 | diff --git a/sysvinit/gentoo/cloud-final b/sysvinit/gentoo/cloud-final |
510 | old mode 100644 |
511 | new mode 100755 |
512 | index a9bf01f..a9bf01f |
513 | --- a/sysvinit/gentoo/cloud-final |
514 | +++ b/sysvinit/gentoo/cloud-final |
515 | diff --git a/sysvinit/gentoo/cloud-init b/sysvinit/gentoo/cloud-init |
516 | old mode 100644 |
517 | new mode 100755 |
518 | index 531a715..531a715 |
519 | --- a/sysvinit/gentoo/cloud-init |
520 | +++ b/sysvinit/gentoo/cloud-init |
521 | diff --git a/sysvinit/gentoo/cloud-init-local b/sysvinit/gentoo/cloud-init-local |
522 | old mode 100644 |
523 | new mode 100755 |
524 | index 0f8cf65..0f8cf65 |
525 | --- a/sysvinit/gentoo/cloud-init-local |
526 | +++ b/sysvinit/gentoo/cloud-init-local |
527 | diff --git a/templates/hosts.opensuse.tmpl b/templates/hosts.opensuse.tmpl |
528 | deleted file mode 100644 |
529 | index 655da3f..0000000 |
530 | --- a/templates/hosts.opensuse.tmpl |
531 | +++ /dev/null |
532 | @@ -1,26 +0,0 @@ |
533 | -* |
534 | - This file /etc/cloud/templates/hosts.opensuse.tmpl is only utilized |
535 | - if enabled in cloud-config. Specifically, in order to enable it |
536 | - you need to add the following to config: |
537 | - manage_etc_hosts: True |
538 | -*# |
539 | -# Your system has configured 'manage_etc_hosts' as True. |
540 | -# As a result, if you wish for changes to this file to persist |
541 | -# then you will need to either |
542 | -# a.) make changes to the master file in |
543 | -# /etc/cloud/templates/hosts.opensuse.tmpl |
544 | -# b.) change or remove the value of 'manage_etc_hosts' in |
545 | -# /etc/cloud/cloud.cfg or cloud-config from user-data |
546 | -# |
547 | -# The following lines are desirable for IPv4 capable hosts |
548 | -127.0.0.1 localhost |
549 | - |
550 | -# The following lines are desirable for IPv6 capable hosts |
551 | -::1 localhost ipv6-localhost ipv6-loopback |
552 | -fe00::0 ipv6-localnet |
553 | - |
554 | -ff00::0 ipv6-mcastprefix |
555 | -ff02::1 ipv6-allnodes |
556 | -ff02::2 ipv6-allrouters |
557 | -ff02::3 ipv6-allhosts |
558 | - |
559 | diff --git a/templates/hosts.suse.tmpl b/templates/hosts.suse.tmpl |
560 | index b608269..8e664db 100644 |
561 | --- a/templates/hosts.suse.tmpl |
562 | +++ b/templates/hosts.suse.tmpl |
563 | @@ -13,12 +13,18 @@ you need to add the following to config: |
564 | # /etc/cloud/cloud.cfg or cloud-config from user-data |
565 | # |
566 | # The following lines are desirable for IPv4 capable hosts |
567 | -127.0.0.1 localhost |
568 | +127.0.0.1 {{fqdn}} {{hostname}} |
569 | +127.0.0.1 localhost.localdomain localhost |
570 | +127.0.0.1 localhost4.localdomain4 localhost4 |
571 | |
572 | # The following lines are desirable for IPv6 capable hosts |
573 | +::1 {{fqdn}} {{hostname}} |
574 | +::1 localhost.localdomain localhost |
575 | +::1 localhost6.localdomain6 localhost6 |
576 | ::1 localhost ipv6-localhost ipv6-loopback |
577 | -fe00::0 ipv6-localnet |
578 | |
579 | + |
580 | +fe00::0 ipv6-localnet |
581 | ff00::0 ipv6-mcastprefix |
582 | ff02::1 ipv6-allnodes |
583 | ff02::2 ipv6-allrouters |
584 | diff --git a/templates/ntp.conf.opensuse.tmpl b/templates/ntp.conf.opensuse.tmpl |
585 | new file mode 100644 |
586 | index 0000000..f3ab565 |
587 | --- /dev/null |
588 | +++ b/templates/ntp.conf.opensuse.tmpl |
589 | @@ -0,0 +1,88 @@ |
590 | +## template:jinja |
591 | + |
592 | +## |
593 | +## Radio and modem clocks by convention have addresses in the |
594 | +## form 127.127.t.u, where t is the clock type and u is a unit |
595 | +## number in the range 0-3. |
596 | +## |
597 | +## Most of these clocks require support in the form of a |
598 | +## serial port or special bus peripheral. The particular |
599 | +## device is normally specified by adding a soft link |
600 | +## /dev/device-u to the particular hardware device involved, |
601 | +## where u correspond to the unit number above. |
602 | +## |
603 | +## Generic DCF77 clock on serial port (Conrad DCF77) |
604 | +## Address: 127.127.8.u |
605 | +## Serial Port: /dev/refclock-u |
606 | +## |
607 | +## (create soft link /dev/refclock-0 to the particular ttyS?) |
608 | +## |
609 | +# server 127.127.8.0 mode 5 prefer |
610 | + |
611 | +## |
612 | +## Undisciplined Local Clock. This is a fake driver intended for backup |
613 | +## and when no outside source of synchronized time is available. |
614 | +## |
615 | +# server 127.127.1.0 # local clock (LCL) |
616 | +# fudge 127.127.1.0 stratum 10 # LCL is unsynchronized |
617 | + |
618 | +## |
619 | +## Add external Servers using |
620 | +## # rcntpd addserver <yourserver> |
621 | +## The servers will only be added to the currently running instance, not |
622 | +## to /etc/ntp.conf. |
623 | +## |
624 | +{% if pools %}# pools |
625 | +{% endif %} |
626 | +{% for pool in pools -%} |
627 | +pool {{pool}} iburst |
628 | +{% endfor %} |
629 | +{%- if servers %}# servers |
630 | +{% endif %} |
631 | +{% for server in servers -%} |
632 | +server {{server}} iburst |
633 | +{% endfor %} |
634 | + |
635 | +# Access control configuration; see /usr/share/doc/packages/ntp/html/accopt.html for |
636 | +# details. The web page <http://support.ntp.org/bin/view/Support/AccessRestrictions> |
637 | +# might also be helpful. |
638 | +# |
639 | +# Note that "restrict" applies to both servers and clients, so a configuration |
640 | +# that might be intended to block requests from certain clients could also end |
641 | +# up blocking replies from your own upstream servers. |
642 | + |
643 | +# By default, exchange time with everybody, but don't allow configuration. |
644 | +restrict -4 default notrap nomodify nopeer noquery |
645 | +restrict -6 default notrap nomodify nopeer noquery |
646 | + |
647 | +# Local users may interrogate the ntp server more closely. |
648 | +restrict 127.0.0.1 |
649 | +restrict ::1 |
650 | + |
651 | +# Clients from this (example!) subnet have unlimited access, but only if |
652 | +# cryptographically authenticated. |
653 | +#restrict 192.168.123.0 mask 255.255.255.0 notrust |
654 | + |
655 | +## |
656 | +## Miscellaneous stuff |
657 | +## |
658 | + |
659 | +driftfile /var/lib/ntp/drift/ntp.drift # path for drift file |
660 | + |
661 | +logfile /var/log/ntp # alternate log file |
662 | +# logconfig =syncstatus + sysevents |
663 | +# logconfig =all |
664 | + |
665 | +# statsdir /tmp/ # directory for statistics files |
666 | +# filegen peerstats file peerstats type day enable |
667 | +# filegen loopstats file loopstats type day enable |
668 | +# filegen clockstats file clockstats type day enable |
669 | + |
670 | +# |
671 | +# Authentication stuff |
672 | +# |
673 | +keys /etc/ntp.keys # path for keys file |
674 | +trustedkey 1 # define trusted keys |
675 | +requestkey 1 # key (7) for accessing server variables |
676 | +controlkey 1 # key (6) for accessing server variables |
677 | + |
678 | diff --git a/templates/ntp.conf.sles.tmpl b/templates/ntp.conf.sles.tmpl |
679 | index 5c5fc4d..f3ab565 100644 |
680 | --- a/templates/ntp.conf.sles.tmpl |
681 | +++ b/templates/ntp.conf.sles.tmpl |
682 | @@ -1,17 +1,5 @@ |
683 | ## template:jinja |
684 | |
685 | -################################################################################ |
686 | -## /etc/ntp.conf |
687 | -## |
688 | -## Sample NTP configuration file. |
689 | -## See package 'ntp-doc' for documentation, Mini-HOWTO and FAQ. |
690 | -## Copyright (c) 1998 S.u.S.E. GmbH Fuerth, Germany. |
691 | -## |
692 | -## Author: Michael Andres, <ma@suse.de> |
693 | -## Michael Skibbe, <mskibbe@suse.de> |
694 | -## |
695 | -################################################################################ |
696 | - |
697 | ## |
698 | ## Radio and modem clocks by convention have addresses in the |
699 | ## form 127.127.t.u, where t is the clock type and u is a unit |
700 | diff --git a/tests/cloud_tests/collect.py b/tests/cloud_tests/collect.py |
701 | index 4a2422e..71ee764 100644 |
702 | --- a/tests/cloud_tests/collect.py |
703 | +++ b/tests/cloud_tests/collect.py |
704 | @@ -22,11 +22,21 @@ def collect_script(instance, base_dir, script, script_name): |
705 | """ |
706 | LOG.debug('running collect script: %s', script_name) |
707 | (out, err, exit) = instance.run_script( |
708 | - script, rcs=range(0, 256), |
709 | + script.encode(), rcs=False, |
710 | description='collect: {}'.format(script_name)) |
711 | c_util.write_file(os.path.join(base_dir, script_name), out) |
712 | |
713 | |
714 | +def collect_console(instance, base_dir): |
715 | + LOG.debug('getting console log') |
716 | + try: |
717 | + data = instance.console_log() |
718 | + except NotImplementedError as e: |
719 | + data = 'Not Implemented: %s' % e |
720 | + with open(os.path.join(base_dir, 'console.log'), "wb") as fp: |
721 | + fp.write(data) |
722 | + |
723 | + |
724 | def collect_test_data(args, snapshot, os_name, test_name): |
725 | """Collect data for test case. |
726 | |
727 | @@ -79,8 +89,12 @@ def collect_test_data(args, snapshot, os_name, test_name): |
728 | test_output_dir, script, script_name)) |
729 | for script_name, script in test_scripts.items()] |
730 | |
731 | + console_log = partial( |
732 | + run_single, 'collect console', |
733 | + partial(collect_console, instance, test_output_dir)) |
734 | + |
735 | res = run_stage('collect for test: {}'.format(test_name), |
736 | - [start_call] + collect_calls) |
737 | + [start_call] + collect_calls + [console_log]) |
738 | |
739 | return res |
740 | |
741 | diff --git a/tests/cloud_tests/images/base.py b/tests/cloud_tests/images/base.py |
742 | index 0a1e056..d503108 100644 |
743 | --- a/tests/cloud_tests/images/base.py |
744 | +++ b/tests/cloud_tests/images/base.py |
745 | @@ -2,8 +2,10 @@ |
746 | |
747 | """Base class for images.""" |
748 | |
749 | +from ..util import TargetBase |
750 | |
751 | -class Image(object): |
752 | + |
753 | +class Image(TargetBase): |
754 | """Base class for images.""" |
755 | |
756 | platform_name = None |
757 | @@ -43,21 +45,6 @@ class Image(object): |
758 | # NOTE: more sophisticated options may be requied at some point |
759 | return self.config.get('setup_overrides', {}) |
760 | |
761 | - def execute(self, *args, **kwargs): |
762 | - """Execute command in image, modifying image.""" |
763 | - raise NotImplementedError |
764 | - |
765 | - def push_file(self, local_path, remote_path): |
766 | - """Copy file at 'local_path' to instance at 'remote_path'.""" |
767 | - raise NotImplementedError |
768 | - |
769 | - def run_script(self, *args, **kwargs): |
770 | - """Run script in image, modifying image. |
771 | - |
772 | - @return_value: script output |
773 | - """ |
774 | - raise NotImplementedError |
775 | - |
776 | def snapshot(self): |
777 | """Create snapshot of image, block until done.""" |
778 | raise NotImplementedError |
779 | diff --git a/tests/cloud_tests/images/lxd.py b/tests/cloud_tests/images/lxd.py |
780 | index fd4e93c..5caeba4 100644 |
781 | --- a/tests/cloud_tests/images/lxd.py |
782 | +++ b/tests/cloud_tests/images/lxd.py |
783 | @@ -24,7 +24,7 @@ class LXDImage(base.Image): |
784 | @param config: image configuration |
785 | """ |
786 | self.modified = False |
787 | - self._instance = None |
788 | + self._img_instance = None |
789 | self._pylxd_image = None |
790 | self.pylxd_image = pylxd_image |
791 | super(LXDImage, self).__init__(platform, config) |
792 | @@ -38,9 +38,9 @@ class LXDImage(base.Image): |
793 | |
794 | @pylxd_image.setter |
795 | def pylxd_image(self, pylxd_image): |
796 | - if self._instance: |
797 | + if self._img_instance: |
798 | self._instance.destroy() |
799 | - self._instance = None |
800 | + self._img_instance = None |
801 | if (self._pylxd_image and |
802 | (self._pylxd_image is not pylxd_image) and |
803 | (not self.config.get('cache_base_image') or self.modified)): |
804 | @@ -49,15 +49,19 @@ class LXDImage(base.Image): |
805 | self._pylxd_image = pylxd_image |
806 | |
807 | @property |
808 | - def instance(self): |
809 | - """Property function.""" |
810 | - if not self._instance: |
811 | - self._instance = self.platform.launch_container( |
812 | + def _instance(self): |
813 | + """Internal use only, returns a instance |
814 | + |
815 | + This starts an lxc instance from the image, so it is "dirty". |
816 | + Better would be some way to modify this "at rest". |
817 | + lxc-pstart would be an option.""" |
818 | + if not self._img_instance: |
819 | + self._img_instance = self.platform.launch_container( |
820 | self.properties, self.config, self.features, |
821 | use_desc='image-modification', image_desc=str(self), |
822 | image=self.pylxd_image.fingerprint) |
823 | - self._instance.start() |
824 | - return self._instance |
825 | + self._img_instance.start() |
826 | + return self._img_instance |
827 | |
828 | @property |
829 | def properties(self): |
830 | @@ -144,20 +148,20 @@ class LXDImage(base.Image): |
831 | shutil.rmtree(export_dir) |
832 | shutil.rmtree(extract_dir) |
833 | |
834 | - def execute(self, *args, **kwargs): |
835 | + def _execute(self, *args, **kwargs): |
836 | """Execute command in image, modifying image.""" |
837 | - return self.instance.execute(*args, **kwargs) |
838 | + return self._instance._execute(*args, **kwargs) |
839 | |
840 | def push_file(self, local_path, remote_path): |
841 | """Copy file at 'local_path' to instance at 'remote_path'.""" |
842 | - return self.instance.push_file(local_path, remote_path) |
843 | + return self._instance.push_file(local_path, remote_path) |
844 | |
845 | def run_script(self, *args, **kwargs): |
846 | """Run script in image, modifying image. |
847 | |
848 | @return_value: script output |
849 | """ |
850 | - return self.instance.run_script(*args, **kwargs) |
851 | + return self._instance.run_script(*args, **kwargs) |
852 | |
853 | def snapshot(self): |
854 | """Create snapshot of image, block until done.""" |
855 | @@ -169,7 +173,7 @@ class LXDImage(base.Image): |
856 | # clone current instance |
857 | instance = self.platform.launch_container( |
858 | self.properties, self.config, self.features, |
859 | - container=self.instance.name, image_desc=str(self), |
860 | + container=self._instance.name, image_desc=str(self), |
861 | use_desc='snapshot', container_config=conf) |
862 | # wait for cloud-init before boot_clean_script is run to ensure |
863 | # /var/lib/cloud is removed cleanly |
864 | diff --git a/tests/cloud_tests/images/nocloudkvm.py b/tests/cloud_tests/images/nocloudkvm.py |
865 | index a7af0e5..1e7962c 100644 |
866 | --- a/tests/cloud_tests/images/nocloudkvm.py |
867 | +++ b/tests/cloud_tests/images/nocloudkvm.py |
868 | @@ -2,6 +2,8 @@ |
869 | |
870 | """NoCloud KVM Image Base Class.""" |
871 | |
872 | +from cloudinit import util as c_util |
873 | + |
874 | from tests.cloud_tests.images import base |
875 | from tests.cloud_tests.snapshots import nocloudkvm as nocloud_kvm_snapshot |
876 | |
877 | @@ -19,24 +21,11 @@ class NoCloudKVMImage(base.Image): |
878 | @param img_path: path to the image |
879 | """ |
880 | self.modified = False |
881 | - self._instance = None |
882 | self._img_path = img_path |
883 | |
884 | super(NoCloudKVMImage, self).__init__(platform, config) |
885 | |
886 | @property |
887 | - def instance(self): |
888 | - """Returns an instance of an image.""" |
889 | - if not self._instance: |
890 | - if not self._img_path: |
891 | - raise RuntimeError() |
892 | - |
893 | - self._instance = self.platform.create_image( |
894 | - self.properties, self.config, self.features, self._img_path, |
895 | - image_desc=str(self), use_desc='image-modification') |
896 | - return self._instance |
897 | - |
898 | - @property |
899 | def properties(self): |
900 | """Dictionary containing: 'arch', 'os', 'version', 'release'.""" |
901 | return { |
902 | @@ -46,20 +35,26 @@ class NoCloudKVMImage(base.Image): |
903 | 'version': self.config['version'], |
904 | } |
905 | |
906 | - def execute(self, *args, **kwargs): |
907 | + def _execute(self, command, stdin=None, env=None): |
908 | """Execute command in image, modifying image.""" |
909 | - return self.instance.execute(*args, **kwargs) |
910 | + return self.mount_image_callback(command, stdin=stdin, env=env) |
911 | |
912 | - def push_file(self, local_path, remote_path): |
913 | - """Copy file at 'local_path' to instance at 'remote_path'.""" |
914 | - return self.instance.push_file(local_path, remote_path) |
915 | + def mount_image_callback(self, command, stdin=None, env=None): |
916 | + """Run mount-image-callback.""" |
917 | |
918 | - def run_script(self, *args, **kwargs): |
919 | - """Run script in image, modifying image. |
920 | + env_args = [] |
921 | + if env: |
922 | + env_args = ['env'] + ["%s=%s" for k, v in env.items()] |
923 | |
924 | - @return_value: script output |
925 | - """ |
926 | - return self.instance.run_script(*args, **kwargs) |
927 | + mic_chroot = ['sudo', 'mount-image-callback', '--system-mounts', |
928 | + '--system-resolvconf', self._img_path, |
929 | + '--', 'chroot', '_MOUNTPOINT_'] |
930 | + try: |
931 | + out, err = c_util.subp(mic_chroot + env_args + list(command), |
932 | + data=stdin, decode=False) |
933 | + return (out, err, 0) |
934 | + except c_util.ProcessExecutionError as e: |
935 | + return (e.stdout, e.stderr, e.exit_code) |
936 | |
937 | def snapshot(self): |
938 | """Create snapshot of image, block until done.""" |
939 | @@ -82,7 +77,6 @@ class NoCloudKVMImage(base.Image): |
940 | framework decide whether to keep or destroy everything. |
941 | """ |
942 | self._img_path = None |
943 | - self._instance.destroy() |
944 | super(NoCloudKVMImage, self).destroy() |
945 | |
946 | # vi: ts=4 expandtab |
947 | diff --git a/tests/cloud_tests/instances/base.py b/tests/cloud_tests/instances/base.py |
948 | index 9bdda60..8c59d62 100644 |
949 | --- a/tests/cloud_tests/instances/base.py |
950 | +++ b/tests/cloud_tests/instances/base.py |
951 | @@ -2,8 +2,10 @@ |
952 | |
953 | """Base instance.""" |
954 | |
955 | +from ..util import TargetBase |
956 | |
957 | -class Instance(object): |
958 | + |
959 | +class Instance(TargetBase): |
960 | """Base instance object.""" |
961 | |
962 | platform_name = None |
963 | @@ -22,82 +24,7 @@ class Instance(object): |
964 | self.properties = properties |
965 | self.config = config |
966 | self.features = features |
967 | - |
968 | - def execute(self, command, stdout=None, stderr=None, env=None, |
969 | - rcs=None, description=None): |
970 | - """Execute command in instance, recording output, error and exit code. |
971 | - |
972 | - Assumes functional networking and execution as root with the |
973 | - target filesystem being available at /. |
974 | - |
975 | - @param command: the command to execute as root inside the image |
976 | - if command is a string, then it will be executed as: |
977 | - ['sh', '-c', command] |
978 | - @param stdout, stderr: file handles to write output and error to |
979 | - @param env: environment variables |
980 | - @param rcs: allowed return codes from command |
981 | - @param description: purpose of command |
982 | - @return_value: tuple containing stdout data, stderr data, exit code |
983 | - """ |
984 | - raise NotImplementedError |
985 | - |
986 | - def read_data(self, remote_path, decode=False): |
987 | - """Read data from instance filesystem. |
988 | - |
989 | - @param remote_path: path in instance |
990 | - @param decode: return as string |
991 | - @return_value: data as str or bytes |
992 | - """ |
993 | - raise NotImplementedError |
994 | - |
995 | - def write_data(self, remote_path, data): |
996 | - """Write data to instance filesystem. |
997 | - |
998 | - @param remote_path: path in instance |
999 | - @param data: data to write, either str or bytes |
1000 | - """ |
1001 | - raise NotImplementedError |
1002 | - |
1003 | - def pull_file(self, remote_path, local_path): |
1004 | - """Copy file at 'remote_path', from instance to 'local_path'. |
1005 | - |
1006 | - @param remote_path: path on remote instance |
1007 | - @param local_path: path on local instance |
1008 | - """ |
1009 | - with open(local_path, 'wb') as fp: |
1010 | - fp.write(self.read_data(remote_path)) |
1011 | - |
1012 | - def push_file(self, local_path, remote_path): |
1013 | - """Copy file at 'local_path' to instance at 'remote_path'. |
1014 | - |
1015 | - @param local_path: path on local instance |
1016 | - @param remote_path: path on remote instance |
1017 | - """ |
1018 | - with open(local_path, 'rb') as fp: |
1019 | - self.write_data(remote_path, fp.read()) |
1020 | - |
1021 | - def run_script(self, script, rcs=None, description=None): |
1022 | - """Run script in target and return stdout. |
1023 | - |
1024 | - @param script: script contents |
1025 | - @param rcs: allowed return codes from script |
1026 | - @param description: purpose of script |
1027 | - @return_value: stdout from script |
1028 | - """ |
1029 | - script_path = self.tmpfile() |
1030 | - try: |
1031 | - self.write_data(script_path, script) |
1032 | - return self.execute( |
1033 | - ['/bin/bash', script_path], rcs=rcs, description=description) |
1034 | - finally: |
1035 | - self.execute(['rm', '-f', script_path], rcs=rcs) |
1036 | - |
1037 | - def tmpfile(self): |
1038 | - """Get a tmp file in the target. |
1039 | - |
1040 | - @return_value: path to new file in target |
1041 | - """ |
1042 | - return self.execute(['mktemp'])[0].strip() |
1043 | + self._tmp_count = 0 |
1044 | |
1045 | def console_log(self): |
1046 | """Instance console. |
1047 | diff --git a/tests/cloud_tests/instances/lxd.py b/tests/cloud_tests/instances/lxd.py |
1048 | index a43918c..3b035d8 100644 |
1049 | --- a/tests/cloud_tests/instances/lxd.py |
1050 | +++ b/tests/cloud_tests/instances/lxd.py |
1051 | @@ -2,8 +2,11 @@ |
1052 | |
1053 | """Base LXD instance.""" |
1054 | |
1055 | -from tests.cloud_tests.instances import base |
1056 | -from tests.cloud_tests import util |
1057 | +from . import base |
1058 | + |
1059 | +import os |
1060 | +import shutil |
1061 | +from tempfile import mkdtemp |
1062 | |
1063 | |
1064 | class LXDInstance(base.Instance): |
1065 | @@ -24,6 +27,8 @@ class LXDInstance(base.Instance): |
1066 | self._pylxd_container = pylxd_container |
1067 | super(LXDInstance, self).__init__( |
1068 | platform, name, properties, config, features) |
1069 | + self.tmpd = mkdtemp(prefix="%s-%s" % (type(self).__name__, name)) |
1070 | + self._setup_console_log() |
1071 | |
1072 | @property |
1073 | def pylxd_container(self): |
1074 | @@ -31,74 +36,69 @@ class LXDInstance(base.Instance): |
1075 | self._pylxd_container.sync() |
1076 | return self._pylxd_container |
1077 | |
1078 | - def execute(self, command, stdout=None, stderr=None, env=None, |
1079 | - rcs=None, description=None): |
1080 | - """Execute command in instance, recording output, error and exit code. |
1081 | - |
1082 | - Assumes functional networking and execution as root with the |
1083 | - target filesystem being available at /. |
1084 | - |
1085 | - @param command: the command to execute as root inside the image |
1086 | - if command is a string, then it will be executed as: |
1087 | - ['sh', '-c', command] |
1088 | - @param stdout: file handler to write output |
1089 | - @param stderr: file handler to write error |
1090 | - @param env: environment variables |
1091 | - @param rcs: allowed return codes from command |
1092 | - @param description: purpose of command |
1093 | - @return_value: tuple containing stdout data, stderr data, exit code |
1094 | - """ |
1095 | + def _setup_console_log(self): |
1096 | + logf = os.path.join(self.tmpd, "console.log") |
1097 | + |
1098 | + # doing this ensures we can read it. Otherwise it ends up root:root. |
1099 | + with open(logf, "w") as fp: |
1100 | + fp.write("# %s\n" % self.name) |
1101 | + |
1102 | + cfg = "lxc.console.logfile=%s" % logf |
1103 | + orig = self._pylxd_container.config.get('raw.lxc', "") |
1104 | + if orig: |
1105 | + orig += "\n" |
1106 | + self._pylxd_container.config['raw.lxc'] = orig + cfg |
1107 | + self._pylxd_container.save() |
1108 | + self._console_log_file = logf |
1109 | + |
1110 | + def _execute(self, command, stdin=None, env=None): |
1111 | if env is None: |
1112 | env = {} |
1113 | |
1114 | - if isinstance(command, str): |
1115 | - command = ['sh', '-c', command] |
1116 | + if stdin is not None: |
1117 | + # pylxd does not support input to execute. |
1118 | + # https://github.com/lxc/pylxd/issues/244 |
1119 | + # |
1120 | + # The solution here is write a tmp file in the container |
1121 | + # and then execute a shell that sets it standard in to |
1122 | + # be from that file, removes it, and calls the comand. |
1123 | + tmpf = self.tmpfile() |
1124 | + self.write_data(tmpf, stdin) |
1125 | + ncmd = 'exec <"{tmpf}"; rm -f "{tmpf}"; exec "$@"' |
1126 | + command = (['sh', '-c', ncmd.format(tmpf=tmpf), 'stdinhack'] + |
1127 | + list(command)) |
1128 | |
1129 | # ensure instance is running and execute the command |
1130 | self.start() |
1131 | + # execute returns a ContainerExecuteResult, named tuple |
1132 | + # (exit_code, stdout, stderr) |
1133 | res = self.pylxd_container.execute(command, environment=env) |
1134 | |
1135 | # get out, exit and err from pylxd return |
1136 | - if hasattr(res, 'exit_code'): |
1137 | - # pylxd 2.2 returns ContainerExecuteResult, named tuple of |
1138 | - # (exit_code, out, err) |
1139 | - (exit, out, err) = res |
1140 | - else: |
1141 | + if not hasattr(res, 'exit_code'): |
1142 | # pylxd 2.1.3 and earlier only return out and err, no exit |
1143 | - # LOG.warning('using pylxd version < 2.2') |
1144 | - (out, err) = res |
1145 | - exit = 0 |
1146 | - |
1147 | - # write data to file descriptors if needed |
1148 | - if stdout: |
1149 | - stdout.write(out) |
1150 | - if stderr: |
1151 | - stderr.write(err) |
1152 | - |
1153 | - # if the command exited with a code not allowed in rcs, then fail |
1154 | - if exit not in (rcs if rcs else (0,)): |
1155 | - error_desc = ('Failed command to: {}'.format(description) |
1156 | - if description else None) |
1157 | - raise util.InTargetExecuteError( |
1158 | - out, err, exit, command, self.name, error_desc) |
1159 | + raise RuntimeError( |
1160 | + "No 'exit_code' in pylxd.container.execute return.\n" |
1161 | + "pylxd > 2.2 is required.") |
1162 | |
1163 | - return (out, err, exit) |
1164 | + return res.stdout, res.stderr, res.exit_code |
1165 | |
1166 | def read_data(self, remote_path, decode=False): |
1167 | """Read data from instance filesystem. |
1168 | |
1169 | @param remote_path: path in instance |
1170 | - @param decode: return as string |
1171 | - @return_value: data as str or bytes |
1172 | + @param decode: decode data before returning. |
1173 | + @return_value: content of remote_path as bytes if 'decode' is False, |
1174 | + and as string if 'decode' is True. |
1175 | """ |
1176 | data = self.pylxd_container.files.get(remote_path) |
1177 | - return data.decode() if decode and isinstance(data, bytes) else data |
1178 | + return data.decode() if decode else data |
1179 | |
1180 | def write_data(self, remote_path, data): |
1181 | """Write data to instance filesystem. |
1182 | |
1183 | @param remote_path: path in instance |
1184 | - @param data: data to write, either str or bytes |
1185 | + @param data: data to write in bytes |
1186 | """ |
1187 | self.pylxd_container.files.put(remote_path, data) |
1188 | |
1189 | @@ -107,7 +107,14 @@ class LXDInstance(base.Instance): |
1190 | |
1191 | @return_value: bytes of this instance’s console |
1192 | """ |
1193 | - raise NotImplementedError |
1194 | + if not os.path.exists(self._console_log_file): |
1195 | + raise NotImplementedError( |
1196 | + "Console log '%s' does not exist. If this is a remote " |
1197 | + "lxc, then this is really NotImplementedError. If it is " |
1198 | + "A local lxc, then this is a RuntimeError." |
1199 | + "https://github.com/lxc/lxd/issues/1129") |
1200 | + with open(self._console_log_file, "rb") as fp: |
1201 | + return fp.read() |
1202 | |
1203 | def reboot(self, wait=True): |
1204 | """Reboot instance.""" |
1205 | @@ -144,6 +151,7 @@ class LXDInstance(base.Instance): |
1206 | if self.platform.container_exists(self.name): |
1207 | raise OSError('container {} was not properly removed' |
1208 | .format(self.name)) |
1209 | + shutil.rmtree(self.tmpd) |
1210 | super(LXDInstance, self).destroy() |
1211 | |
1212 | # vi: ts=4 expandtab |
1213 | diff --git a/tests/cloud_tests/instances/nocloudkvm.py b/tests/cloud_tests/instances/nocloudkvm.py |
1214 | index 8a0e531..cc82580 100644 |
1215 | --- a/tests/cloud_tests/instances/nocloudkvm.py |
1216 | +++ b/tests/cloud_tests/instances/nocloudkvm.py |
1217 | @@ -12,11 +12,18 @@ from cloudinit import util as c_util |
1218 | from tests.cloud_tests.instances import base |
1219 | from tests.cloud_tests import util |
1220 | |
1221 | +# This domain contains reverse lookups for hostnames that are used. |
1222 | +# The primary reason is so sudo will return quickly when it attempts |
1223 | +# to look up the hostname. i9n is just short for 'integration'. |
1224 | +# see also bug 1730744 for why we had to do this. |
1225 | +CI_DOMAIN = "i9n.cloud-init.io" |
1226 | + |
1227 | |
1228 | class NoCloudKVMInstance(base.Instance): |
1229 | """NoCloud KVM backed instance.""" |
1230 | |
1231 | platform_name = "nocloud-kvm" |
1232 | + _ssh_client = None |
1233 | |
1234 | def __init__(self, platform, name, properties, config, features, |
1235 | user_data, meta_data): |
1236 | @@ -35,6 +42,7 @@ class NoCloudKVMInstance(base.Instance): |
1237 | self.ssh_port = None |
1238 | self.pid = None |
1239 | self.pid_file = None |
1240 | + self.console_file = None |
1241 | |
1242 | super(NoCloudKVMInstance, self).__init__( |
1243 | platform, name, properties, config, features) |
1244 | @@ -51,43 +59,18 @@ class NoCloudKVMInstance(base.Instance): |
1245 | os.remove(self.pid_file) |
1246 | |
1247 | self.pid = None |
1248 | - super(NoCloudKVMInstance, self).destroy() |
1249 | - |
1250 | - def execute(self, command, stdout=None, stderr=None, env=None, |
1251 | - rcs=None, description=None): |
1252 | - """Execute command in instance. |
1253 | - |
1254 | - Assumes functional networking and execution as root with the |
1255 | - target filesystem being available at /. |
1256 | - |
1257 | - @param command: the command to execute as root inside the image |
1258 | - if command is a string, then it will be executed as: |
1259 | - ['sh', '-c', command] |
1260 | - @param stdout, stderr: file handles to write output and error to |
1261 | - @param env: environment variables |
1262 | - @param rcs: allowed return codes from command |
1263 | - @param description: purpose of command |
1264 | - @return_value: tuple containing stdout data, stderr data, exit code |
1265 | - """ |
1266 | - if env is None: |
1267 | - env = {} |
1268 | - |
1269 | - if isinstance(command, str): |
1270 | - command = ['sh', '-c', command] |
1271 | + if self._ssh_client: |
1272 | + self._ssh_client.close() |
1273 | + self._ssh_client = None |
1274 | |
1275 | - if self.pid: |
1276 | - return self.ssh(command) |
1277 | - else: |
1278 | - return self.mount_image_callback(command) + (0,) |
1279 | + super(NoCloudKVMInstance, self).destroy() |
1280 | |
1281 | - def mount_image_callback(self, cmd): |
1282 | - """Run mount-image-callback.""" |
1283 | - out, err = c_util.subp(['sudo', 'mount-image-callback', |
1284 | - '--system-mounts', '--system-resolvconf', |
1285 | - self.name, '--', 'chroot', |
1286 | - '_MOUNTPOINT_'] + cmd) |
1287 | + def _execute(self, command, stdin=None, env=None): |
1288 | + env_args = [] |
1289 | + if env: |
1290 | + env_args = ['env'] + ["%s=%s" for k, v in env.items()] |
1291 | |
1292 | - return out, err |
1293 | + return self.ssh(['sudo'] + env_args + list(command), stdin=stdin) |
1294 | |
1295 | def generate_seed(self, tmpdir): |
1296 | """Generate nocloud seed from user-data""" |
1297 | @@ -109,57 +92,31 @@ class NoCloudKVMInstance(base.Instance): |
1298 | s.close() |
1299 | return num |
1300 | |
1301 | - def push_file(self, local_path, remote_path): |
1302 | - """Copy file at 'local_path' to instance at 'remote_path'. |
1303 | - |
1304 | - If we have a pid then SSH is up, otherwise, use |
1305 | - mount-image-callback. |
1306 | - |
1307 | - @param local_path: path on local instance |
1308 | - @param remote_path: path on remote instance |
1309 | - """ |
1310 | - if self.pid: |
1311 | - super(NoCloudKVMInstance, self).push_file() |
1312 | - else: |
1313 | - local_file = open(local_path) |
1314 | - p = subprocess.Popen(['sudo', 'mount-image-callback', |
1315 | - '--system-mounts', '--system-resolvconf', |
1316 | - self.name, '--', 'chroot', '_MOUNTPOINT_', |
1317 | - '/bin/sh', '-c', 'cat - > %s' % remote_path], |
1318 | - stdin=local_file, |
1319 | - stdout=subprocess.PIPE, |
1320 | - stderr=subprocess.PIPE) |
1321 | - p.wait() |
1322 | - |
1323 | - def sftp_put(self, path, data): |
1324 | - """SFTP put a file.""" |
1325 | - client = self._ssh_connect() |
1326 | - sftp = client.open_sftp() |
1327 | - |
1328 | - with sftp.open(path, 'w') as f: |
1329 | - f.write(data) |
1330 | - |
1331 | - client.close() |
1332 | - |
1333 | - def ssh(self, command): |
1334 | + def ssh(self, command, stdin=None): |
1335 | """Run a command via SSH.""" |
1336 | client = self._ssh_connect() |
1337 | |
1338 | + cmd = util.shell_pack(command) |
1339 | try: |
1340 | - _, out, err = client.exec_command(util.shell_pack(command)) |
1341 | - except paramiko.SSHException: |
1342 | - raise util.InTargetExecuteError('', '', -1, command, self.name) |
1343 | - |
1344 | - exit = out.channel.recv_exit_status() |
1345 | - out = ''.join(out.readlines()) |
1346 | - err = ''.join(err.readlines()) |
1347 | - client.close() |
1348 | - |
1349 | - return out, err, exit |
1350 | + fp_in, fp_out, fp_err = client.exec_command(cmd) |
1351 | + channel = fp_in.channel |
1352 | + if stdin is not None: |
1353 | + fp_in.write(stdin) |
1354 | + fp_in.close() |
1355 | + |
1356 | + channel.shutdown_write() |
1357 | + rc = channel.recv_exit_status() |
1358 | + return (fp_out.read(), fp_err.read(), rc) |
1359 | + except paramiko.SSHException as e: |
1360 | + raise util.InTargetExecuteError( |
1361 | + b'', b'', -1, command, self.name, reason=e) |
1362 | |
1363 | def _ssh_connect(self, hostname='localhost', username='ubuntu', |
1364 | banner_timeout=120, retry_attempts=30): |
1365 | """Connect via SSH.""" |
1366 | + if self._ssh_client: |
1367 | + return self._ssh_client |
1368 | + |
1369 | private_key = paramiko.RSAKey.from_private_key_file(self.ssh_key_file) |
1370 | client = paramiko.SSHClient() |
1371 | client.set_missing_host_key_policy(paramiko.AutoAddPolicy()) |
1372 | @@ -168,6 +125,7 @@ class NoCloudKVMInstance(base.Instance): |
1373 | client.connect(hostname=hostname, username=username, |
1374 | port=self.ssh_port, pkey=private_key, |
1375 | banner_timeout=banner_timeout) |
1376 | + self._ssh_client = client |
1377 | return client |
1378 | except (paramiko.SSHException, TypeError): |
1379 | time.sleep(1) |
1380 | @@ -183,15 +141,19 @@ class NoCloudKVMInstance(base.Instance): |
1381 | tmpdir = self.platform.config['data_dir'] |
1382 | seed = self.generate_seed(tmpdir) |
1383 | self.pid_file = os.path.join(tmpdir, '%s.pid' % self.name) |
1384 | + self.console_file = os.path.join(tmpdir, '%s-console.log' % self.name) |
1385 | self.ssh_port = self.get_free_port() |
1386 | |
1387 | - subprocess.Popen(['./tools/xkvm', |
1388 | - '--disk', '%s,cache=unsafe' % self.name, |
1389 | - '--disk', '%s,cache=unsafe' % seed, |
1390 | - '--netdev', |
1391 | - 'user,hostfwd=tcp::%s-:22' % self.ssh_port, |
1392 | - '--', '-pidfile', self.pid_file, '-vnc', 'none', |
1393 | - '-m', '2G', '-smp', '2'], |
1394 | + cmd = ['./tools/xkvm', |
1395 | + '--disk', '%s,cache=unsafe' % self.name, |
1396 | + '--disk', '%s,cache=unsafe' % seed, |
1397 | + '--netdev', ','.join(['user', |
1398 | + 'hostfwd=tcp::%s-:22' % self.ssh_port, |
1399 | + 'dnssearch=%s' % CI_DOMAIN]), |
1400 | + '--', '-pidfile', self.pid_file, '-vnc', 'none', |
1401 | + '-m', '2G', '-smp', '2', '-nographic', |
1402 | + '-serial', 'file:' + self.console_file] |
1403 | + subprocess.Popen(cmd, |
1404 | close_fds=True, |
1405 | stdin=subprocess.PIPE, |
1406 | stdout=subprocess.PIPE, |
1407 | @@ -206,12 +168,10 @@ class NoCloudKVMInstance(base.Instance): |
1408 | if wait: |
1409 | self._wait_for_system(wait_for_cloud_init) |
1410 | |
1411 | - def write_data(self, remote_path, data): |
1412 | - """Write data to instance filesystem. |
1413 | - |
1414 | - @param remote_path: path in instance |
1415 | - @param data: data to write, either str or bytes |
1416 | - """ |
1417 | - self.sftp_put(remote_path, data) |
1418 | + def console_log(self): |
1419 | + if not self.console_file: |
1420 | + return b'' |
1421 | + with open(self.console_file, "rb") as fp: |
1422 | + return fp.read() |
1423 | |
1424 | # vi: ts=4 expandtab |
1425 | diff --git a/tests/cloud_tests/testcases/examples/run_commands.yaml b/tests/cloud_tests/testcases/examples/run_commands.yaml |
1426 | index b0e311b..f80eb8c 100644 |
1427 | --- a/tests/cloud_tests/testcases/examples/run_commands.yaml |
1428 | +++ b/tests/cloud_tests/testcases/examples/run_commands.yaml |
1429 | @@ -7,10 +7,10 @@ enabled: False |
1430 | cloud_config: | |
1431 | #cloud-config |
1432 | runcmd: |
1433 | - - echo cloud-init run cmd test > /tmp/run_cmd |
1434 | + - echo cloud-init run cmd test > /var/tmp/run_cmd |
1435 | collect_scripts: |
1436 | run_cmd: | |
1437 | #!/bin/bash |
1438 | - cat /tmp/run_cmd |
1439 | + cat /var/tmp/run_cmd |
1440 | |
1441 | # vi: ts=4 expandtab |
1442 | diff --git a/tests/cloud_tests/testcases/modules/apt_configure_sources_ppa.py b/tests/cloud_tests/testcases/modules/apt_configure_sources_ppa.py |
1443 | index d299e9a..dfbdead 100644 |
1444 | --- a/tests/cloud_tests/testcases/modules/apt_configure_sources_ppa.py |
1445 | +++ b/tests/cloud_tests/testcases/modules/apt_configure_sources_ppa.py |
1446 | @@ -11,13 +11,13 @@ class TestAptconfigureSourcesPPA(base.CloudTestCase): |
1447 | """Test specific ppa added.""" |
1448 | out = self.get_data_file('sources.list') |
1449 | self.assertIn( |
1450 | - 'http://ppa.launchpad.net/curtin-dev/test-archive/ubuntu', out) |
1451 | + 'http://ppa.launchpad.net/cloud-init-dev/test-archive/ubuntu', out) |
1452 | |
1453 | def test_ppa_key(self): |
1454 | """Test ppa key added.""" |
1455 | out = self.get_data_file('apt-key') |
1456 | self.assertIn( |
1457 | - '1BC3 0F71 5A3B 8612 47A8 1A5E 55FE 7C8C 0165 013E', out) |
1458 | - self.assertIn('Launchpad PPA for curtin developers', out) |
1459 | + '1FF0 D853 5EF7 E719 E5C8 1B9C 083D 06FB E4D3 04DF', out) |
1460 | + self.assertIn('Launchpad PPA for cloud init development team', out) |
1461 | |
1462 | # vi: ts=4 expandtab |
1463 | diff --git a/tests/cloud_tests/testcases/modules/apt_configure_sources_ppa.yaml b/tests/cloud_tests/testcases/modules/apt_configure_sources_ppa.yaml |
1464 | index 9efdae5..b997bcf 100644 |
1465 | --- a/tests/cloud_tests/testcases/modules/apt_configure_sources_ppa.yaml |
1466 | +++ b/tests/cloud_tests/testcases/modules/apt_configure_sources_ppa.yaml |
1467 | @@ -2,7 +2,7 @@ |
1468 | # Add a PPA to source.list |
1469 | # |
1470 | # NOTE: on older ubuntu releases the sources file added is named |
1471 | -# 'curtin-dev-test-archive-trusty', without 'ubuntu' in the middle |
1472 | +# 'cloud-init-dev-test-archive-trusty', without 'ubuntu' in the middle |
1473 | required_features: |
1474 | - apt |
1475 | - ppa |
1476 | @@ -14,11 +14,11 @@ cloud_config: | |
1477 | source1: |
1478 | keyid: 0165013E |
1479 | keyserver: keyserver.ubuntu.com |
1480 | - source: "ppa:curtin-dev/test-archive" |
1481 | + source: "ppa:cloud-init-dev/test-archive" |
1482 | collect_scripts: |
1483 | sources.list: | |
1484 | #!/bin/bash |
1485 | - cat /etc/apt/sources.list.d/curtin-dev-ubuntu-test-archive-*.list |
1486 | + cat /etc/apt/sources.list.d/cloud-init-dev-ubuntu-test-archive-*.list |
1487 | apt-key: | |
1488 | #!/bin/bash |
1489 | apt-key finger |
1490 | diff --git a/tests/cloud_tests/testcases/modules/keys_to_console.py b/tests/cloud_tests/testcases/modules/keys_to_console.py |
1491 | index 88b6812..07f3811 100644 |
1492 | --- a/tests/cloud_tests/testcases/modules/keys_to_console.py |
1493 | +++ b/tests/cloud_tests/testcases/modules/keys_to_console.py |
1494 | @@ -10,13 +10,13 @@ class TestKeysToConsole(base.CloudTestCase): |
1495 | def test_excluded_keys(self): |
1496 | """Test excluded keys missing.""" |
1497 | out = self.get_data_file('syslog') |
1498 | - self.assertNotIn('DSA', out) |
1499 | - self.assertNotIn('ECDSA', out) |
1500 | + self.assertNotIn('(DSA)', out) |
1501 | + self.assertNotIn('(ECDSA)', out) |
1502 | |
1503 | def test_expected_keys(self): |
1504 | """Test expected keys exist.""" |
1505 | out = self.get_data_file('syslog') |
1506 | - self.assertIn('ED25519', out) |
1507 | - self.assertIn('RSA', out) |
1508 | + self.assertIn('(ED25519)', out) |
1509 | + self.assertIn('(RSA)', out) |
1510 | |
1511 | # vi: ts=4 expandtab |
1512 | diff --git a/tests/cloud_tests/testcases/modules/runcmd.yaml b/tests/cloud_tests/testcases/modules/runcmd.yaml |
1513 | index 04e5a05..8309a88 100644 |
1514 | --- a/tests/cloud_tests/testcases/modules/runcmd.yaml |
1515 | +++ b/tests/cloud_tests/testcases/modules/runcmd.yaml |
1516 | @@ -4,10 +4,10 @@ |
1517 | cloud_config: | |
1518 | #cloud-config |
1519 | runcmd: |
1520 | - - echo cloud-init run cmd test > /tmp/run_cmd |
1521 | + - echo cloud-init run cmd test > /var/tmp/run_cmd |
1522 | collect_scripts: |
1523 | run_cmd: | |
1524 | #!/bin/bash |
1525 | - cat /tmp/run_cmd |
1526 | + cat /var/tmp/run_cmd |
1527 | |
1528 | # vi: ts=4 expandtab |
1529 | diff --git a/tests/cloud_tests/testcases/modules/set_hostname.py b/tests/cloud_tests/testcases/modules/set_hostname.py |
1530 | index 6e96a75..1dbe64c 100644 |
1531 | --- a/tests/cloud_tests/testcases/modules/set_hostname.py |
1532 | +++ b/tests/cloud_tests/testcases/modules/set_hostname.py |
1533 | @@ -7,9 +7,11 @@ from tests.cloud_tests.testcases import base |
1534 | class TestHostname(base.CloudTestCase): |
1535 | """Test hostname module.""" |
1536 | |
1537 | + ex_hostname = "cloudinit2" |
1538 | + |
1539 | def test_hostname(self): |
1540 | """Test hostname command shows correct output.""" |
1541 | out = self.get_data_file('hostname') |
1542 | - self.assertIn('myhostname', out) |
1543 | + self.assertIn(self.ex_hostname, out) |
1544 | |
1545 | # vi: ts=4 expandtab |
1546 | diff --git a/tests/cloud_tests/testcases/modules/set_hostname.yaml b/tests/cloud_tests/testcases/modules/set_hostname.yaml |
1547 | index c96344c..071fb22 100644 |
1548 | --- a/tests/cloud_tests/testcases/modules/set_hostname.yaml |
1549 | +++ b/tests/cloud_tests/testcases/modules/set_hostname.yaml |
1550 | @@ -5,7 +5,8 @@ required_features: |
1551 | - hostname |
1552 | cloud_config: | |
1553 | #cloud-config |
1554 | - hostname: myhostname |
1555 | + hostname: cloudinit2 |
1556 | + |
1557 | collect_scripts: |
1558 | hosts: | |
1559 | #!/bin/bash |
1560 | diff --git a/tests/cloud_tests/testcases/modules/set_hostname_fqdn.py b/tests/cloud_tests/testcases/modules/set_hostname_fqdn.py |
1561 | index 398f3d4..eb6f065 100644 |
1562 | --- a/tests/cloud_tests/testcases/modules/set_hostname_fqdn.py |
1563 | +++ b/tests/cloud_tests/testcases/modules/set_hostname_fqdn.py |
1564 | @@ -1,26 +1,31 @@ |
1565 | # This file is part of cloud-init. See LICENSE file for license information. |
1566 | |
1567 | """cloud-init Integration Test Verify Script.""" |
1568 | +from tests.cloud_tests.instances.nocloudkvm import CI_DOMAIN |
1569 | from tests.cloud_tests.testcases import base |
1570 | |
1571 | |
1572 | class TestHostnameFqdn(base.CloudTestCase): |
1573 | """Test Hostname module.""" |
1574 | |
1575 | + ex_hostname = "cloudinit1" |
1576 | + ex_fqdn = "cloudinit2." + CI_DOMAIN |
1577 | + |
1578 | def test_hostname(self): |
1579 | """Test hostname output.""" |
1580 | out = self.get_data_file('hostname') |
1581 | - self.assertIn('myhostname', out) |
1582 | + self.assertIn(self.ex_hostname, out) |
1583 | |
1584 | def test_hostname_fqdn(self): |
1585 | """Test hostname fqdn output.""" |
1586 | out = self.get_data_file('fqdn') |
1587 | - self.assertIn('host.myorg.com', out) |
1588 | + self.assertIn(self.ex_fqdn, out) |
1589 | |
1590 | def test_hosts(self): |
1591 | """Test /etc/hosts file.""" |
1592 | out = self.get_data_file('hosts') |
1593 | - self.assertIn('127.0.1.1 host.myorg.com myhostname', out) |
1594 | + self.assertIn('127.0.1.1 %s %s' % (self.ex_fqdn, self.ex_hostname), |
1595 | + out) |
1596 | self.assertIn('127.0.0.1 localhost', out) |
1597 | |
1598 | # vi: ts=4 expandtab |
1599 | diff --git a/tests/cloud_tests/testcases/modules/set_hostname_fqdn.yaml b/tests/cloud_tests/testcases/modules/set_hostname_fqdn.yaml |
1600 | index daf7593..a85ee79 100644 |
1601 | --- a/tests/cloud_tests/testcases/modules/set_hostname_fqdn.yaml |
1602 | +++ b/tests/cloud_tests/testcases/modules/set_hostname_fqdn.yaml |
1603 | @@ -6,8 +6,9 @@ required_features: |
1604 | cloud_config: | |
1605 | #cloud-config |
1606 | manage_etc_hosts: true |
1607 | - hostname: myhostname |
1608 | - fqdn: host.myorg.com |
1609 | + hostname: cloudinit1 |
1610 | + # this needs changing if CI_DOMAIN were updated. |
1611 | + fqdn: cloudinit2.i9n.cloud-init.io |
1612 | collect_scripts: |
1613 | hosts: | |
1614 | #!/bin/bash |
1615 | diff --git a/tests/cloud_tests/testcases/modules/set_password_expire.py b/tests/cloud_tests/testcases/modules/set_password_expire.py |
1616 | index a1c3aa0..967aca7 100644 |
1617 | --- a/tests/cloud_tests/testcases/modules/set_password_expire.py |
1618 | +++ b/tests/cloud_tests/testcases/modules/set_password_expire.py |
1619 | @@ -18,6 +18,6 @@ class TestPasswordExpire(base.CloudTestCase): |
1620 | def test_sshd_config(self): |
1621 | """Test sshd config allows passwords.""" |
1622 | out = self.get_data_file('sshd_config') |
1623 | - self.assertIn('PasswordAuthentication no', out) |
1624 | + self.assertIn('PasswordAuthentication yes', out) |
1625 | |
1626 | # vi: ts=4 expandtab |
1627 | diff --git a/tests/cloud_tests/testcases/modules/set_password_expire.yaml b/tests/cloud_tests/testcases/modules/set_password_expire.yaml |
1628 | index 789604b..ba6344b 100644 |
1629 | --- a/tests/cloud_tests/testcases/modules/set_password_expire.yaml |
1630 | +++ b/tests/cloud_tests/testcases/modules/set_password_expire.yaml |
1631 | @@ -6,7 +6,9 @@ required_features: |
1632 | cloud_config: | |
1633 | #cloud-config |
1634 | chpasswd: { expire: True } |
1635 | + ssh_pwauth: yes |
1636 | users: |
1637 | + - default |
1638 | - name: tom |
1639 | password: $1$xyz$sPMsLNmf66Ohl.ol6JvzE. |
1640 | lock_passwd: false |
1641 | diff --git a/tests/cloud_tests/testcases/modules/set_password_list.yaml b/tests/cloud_tests/testcases/modules/set_password_list.yaml |
1642 | index a2a89c9..fd3e1e4 100644 |
1643 | --- a/tests/cloud_tests/testcases/modules/set_password_list.yaml |
1644 | +++ b/tests/cloud_tests/testcases/modules/set_password_list.yaml |
1645 | @@ -5,6 +5,7 @@ cloud_config: | |
1646 | #cloud-config |
1647 | ssh_pwauth: yes |
1648 | users: |
1649 | + - default |
1650 | - name: tom |
1651 | # md5 gotomgo |
1652 | passwd: "$1$S7$tT1BEDIYrczeryDQJfdPe0" |
1653 | diff --git a/tests/cloud_tests/testcases/modules/set_password_list_string.yaml b/tests/cloud_tests/testcases/modules/set_password_list_string.yaml |
1654 | index c2a0f63..e9fe54b 100644 |
1655 | --- a/tests/cloud_tests/testcases/modules/set_password_list_string.yaml |
1656 | +++ b/tests/cloud_tests/testcases/modules/set_password_list_string.yaml |
1657 | @@ -5,6 +5,7 @@ cloud_config: | |
1658 | #cloud-config |
1659 | ssh_pwauth: yes |
1660 | users: |
1661 | + - default |
1662 | - name: tom |
1663 | # md5 gotomgo |
1664 | passwd: "$1$S7$tT1BEDIYrczeryDQJfdPe0" |
1665 | diff --git a/tests/cloud_tests/testcases/modules/ssh_auth_key_fingerprints_disable.py b/tests/cloud_tests/testcases/modules/ssh_auth_key_fingerprints_disable.py |
1666 | index 8222321..e7329d4 100644 |
1667 | --- a/tests/cloud_tests/testcases/modules/ssh_auth_key_fingerprints_disable.py |
1668 | +++ b/tests/cloud_tests/testcases/modules/ssh_auth_key_fingerprints_disable.py |
1669 | @@ -13,12 +13,4 @@ class TestSshKeyFingerprintsDisable(base.CloudTestCase): |
1670 | self.assertIn('Skipping module named ssh-authkey-fingerprints, ' |
1671 | 'logging of ssh fingerprints disabled', out) |
1672 | |
1673 | - def test_syslog(self): |
1674 | - """Verify output of syslog.""" |
1675 | - out = self.get_data_file('syslog') |
1676 | - self.assertNotRegex(out, r'256 SHA256:.*(ECDSA)') |
1677 | - self.assertNotRegex(out, r'256 SHA256:.*(ED25519)') |
1678 | - self.assertNotRegex(out, r'1024 SHA256:.*(DSA)') |
1679 | - self.assertNotRegex(out, r'2048 SHA256:.*(RSA)') |
1680 | - |
1681 | # vi: ts=4 expandtab |
1682 | diff --git a/tests/cloud_tests/testcases/modules/ssh_auth_key_fingerprints_disable.yaml b/tests/cloud_tests/testcases/modules/ssh_auth_key_fingerprints_disable.yaml |
1683 | index 746653e..d93893e 100644 |
1684 | --- a/tests/cloud_tests/testcases/modules/ssh_auth_key_fingerprints_disable.yaml |
1685 | +++ b/tests/cloud_tests/testcases/modules/ssh_auth_key_fingerprints_disable.yaml |
1686 | @@ -5,7 +5,6 @@ required_features: |
1687 | - syslog |
1688 | cloud_config: | |
1689 | #cloud-config |
1690 | - ssh_genkeytypes: [] |
1691 | no_ssh_fingerprints: true |
1692 | collect_scripts: |
1693 | syslog: | |
1694 | diff --git a/tests/cloud_tests/testcases/modules/ssh_keys_generate.py b/tests/cloud_tests/testcases/modules/ssh_keys_generate.py |
1695 | index fd6d9ba..b68f556 100644 |
1696 | --- a/tests/cloud_tests/testcases/modules/ssh_keys_generate.py |
1697 | +++ b/tests/cloud_tests/testcases/modules/ssh_keys_generate.py |
1698 | @@ -9,11 +9,6 @@ class TestSshKeysGenerate(base.CloudTestCase): |
1699 | |
1700 | # TODO: Check cloud-init-output for the correct keys being generated |
1701 | |
1702 | - def test_ubuntu_authorized_keys(self): |
1703 | - """Test passed in key is not in list for ubuntu.""" |
1704 | - out = self.get_data_file('auth_keys_ubuntu') |
1705 | - self.assertEqual('', out) |
1706 | - |
1707 | def test_dsa_public(self): |
1708 | """Test dsa public key not generated.""" |
1709 | out = self.get_data_file('dsa_public') |
1710 | diff --git a/tests/cloud_tests/testcases/modules/ssh_keys_generate.yaml b/tests/cloud_tests/testcases/modules/ssh_keys_generate.yaml |
1711 | index 659fd93..0a7adf6 100644 |
1712 | --- a/tests/cloud_tests/testcases/modules/ssh_keys_generate.yaml |
1713 | +++ b/tests/cloud_tests/testcases/modules/ssh_keys_generate.yaml |
1714 | @@ -10,12 +10,6 @@ cloud_config: | |
1715 | - ed25519 |
1716 | authkey_hash: sha512 |
1717 | collect_scripts: |
1718 | - auth_keys_root: | |
1719 | - #!/bin/bash |
1720 | - cat /root/.ssh/authorized_keys |
1721 | - auth_keys_ubuntu: | |
1722 | - #!/bin/bash |
1723 | - cat /home/ubuntu/ssh/authorized_keys |
1724 | dsa_public: | |
1725 | #!/bin/bash |
1726 | cat /etc/ssh/ssh_host_dsa_key.pub |
1727 | diff --git a/tests/cloud_tests/testcases/modules/ssh_keys_provided.py b/tests/cloud_tests/testcases/modules/ssh_keys_provided.py |
1728 | index 544649d..add3f46 100644 |
1729 | --- a/tests/cloud_tests/testcases/modules/ssh_keys_provided.py |
1730 | +++ b/tests/cloud_tests/testcases/modules/ssh_keys_provided.py |
1731 | @@ -7,17 +7,6 @@ from tests.cloud_tests.testcases import base |
1732 | class TestSshKeysProvided(base.CloudTestCase): |
1733 | """Test ssh keys module.""" |
1734 | |
1735 | - def test_ubuntu_authorized_keys(self): |
1736 | - """Test passed in key is not in list for ubuntu.""" |
1737 | - out = self.get_data_file('auth_keys_ubuntu') |
1738 | - self.assertEqual('', out) |
1739 | - |
1740 | - def test_root_authorized_keys(self): |
1741 | - """Test passed in key is in authorized list for root.""" |
1742 | - out = self.get_data_file('auth_keys_root') |
1743 | - self.assertIn('lzrkPqONphoZx0LDV86w7RUz1ksDzAdcm0tvmNRFMN1a0frDs50' |
1744 | - '6oA3aWK0oDk4Nmvk8sXGTYYw3iQSkOvDUUlIsqdaO+w==', out) |
1745 | - |
1746 | def test_dsa_public(self): |
1747 | """Test dsa public key passed in.""" |
1748 | out = self.get_data_file('dsa_public') |
1749 | diff --git a/tests/cloud_tests/testcases/modules/ssh_keys_provided.yaml b/tests/cloud_tests/testcases/modules/ssh_keys_provided.yaml |
1750 | index 5ceb362..41f6355 100644 |
1751 | --- a/tests/cloud_tests/testcases/modules/ssh_keys_provided.yaml |
1752 | +++ b/tests/cloud_tests/testcases/modules/ssh_keys_provided.yaml |
1753 | @@ -71,12 +71,6 @@ cloud_config: | |
1754 | -----END EC PRIVATE KEY----- |
1755 | ecdsa_public: ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBFsS5Tvky/IC/dXhE/afxxUG6kdQOvdQJCYGZN42OZqWasYF+L3IG+3/wrV7jOrNrL3AyagHl6+lpPDiSXDMcpQ= root@xenial-lxd |
1756 | collect_scripts: |
1757 | - auth_keys_root: | |
1758 | - #!/bin/bash |
1759 | - cat /root/.ssh/authorized_keys |
1760 | - auth_keys_ubuntu: | |
1761 | - #!/bin/bash |
1762 | - cat /home/ubuntu/ssh/authorized_keys |
1763 | dsa_public: | |
1764 | #!/bin/bash |
1765 | cat /etc/ssh/ssh_host_dsa_key.pub |
1766 | diff --git a/tests/cloud_tests/util.py b/tests/cloud_tests/util.py |
1767 | index 4357fbb..c5cd697 100644 |
1768 | --- a/tests/cloud_tests/util.py |
1769 | +++ b/tests/cloud_tests/util.py |
1770 | @@ -7,6 +7,7 @@ import copy |
1771 | import glob |
1772 | import os |
1773 | import random |
1774 | +import shlex |
1775 | import shutil |
1776 | import string |
1777 | import subprocess |
1778 | @@ -285,20 +286,165 @@ def shell_pack(cmd): |
1779 | return 'eval set -- "$(echo %s | base64 --decode)" && exec "$@"' % b64 |
1780 | |
1781 | |
1782 | +def shell_quote(cmd): |
1783 | + if isinstance(cmd, (tuple, list)): |
1784 | + return ' '.join([shlex.quote(x) for x in cmd]) |
1785 | + return shlex.quote(cmd) |
1786 | + |
1787 | + |
1788 | +class TargetBase(object): |
1789 | + _tmp_count = 0 |
1790 | + |
1791 | + def execute(self, command, stdin=None, env=None, |
1792 | + rcs=None, description=None): |
1793 | + """Execute command in instance, recording output, error and exit code. |
1794 | + |
1795 | + Assumes functional networking and execution as root with the |
1796 | + target filesystem being available at /. |
1797 | + |
1798 | + @param command: the command to execute as root inside the image |
1799 | + if command is a string, then it will be executed as: |
1800 | + ['sh', '-c', command] |
1801 | + @param stdin: bytes content for standard in |
1802 | + @param env: environment variables |
1803 | + @param rcs: return codes. |
1804 | + None (default): non-zero exit code will raise exception. |
1805 | + False: any is allowed (No execption raised). |
1806 | + list of int: any rc not in the list will raise exception. |
1807 | + @param description: purpose of command |
1808 | + @return_value: tuple containing stdout data, stderr data, exit code |
1809 | + """ |
1810 | + if isinstance(command, str): |
1811 | + command = ['sh', '-c', command] |
1812 | + |
1813 | + if rcs is None: |
1814 | + rcs = (0,) |
1815 | + |
1816 | + if description: |
1817 | + LOG.debug('Executing "%s"', description) |
1818 | + else: |
1819 | + LOG.debug("Executing command: %s", shell_quote(command)) |
1820 | + |
1821 | + out, err, rc = self._execute(command=command, stdin=stdin, env=env) |
1822 | + |
1823 | + # False means accept anything. |
1824 | + if (rcs is False or rc in rcs): |
1825 | + return out, err, rc |
1826 | + |
1827 | + raise InTargetExecuteError(out, err, rc, command, description) |
1828 | + |
1829 | + def _execute(self, command, stdin=None, env=None): |
1830 | + """Execute command in inside, return stdout, stderr and exit code. |
1831 | + |
1832 | + Assumes functional networking and execution as root with the |
1833 | + target filesystem being available at /. |
1834 | + |
1835 | + @param stdin: bytes content for standard in |
1836 | + @param env: environment variables |
1837 | + @return_value: tuple containing stdout data, stderr data, exit code |
1838 | + |
1839 | + This is intended to be implemented by the Image or Instance. |
1840 | + Many callers will use the higher level 'execute'.""" |
1841 | + raise NotImplementedError("_execute must be implemented by subclass.") |
1842 | + |
1843 | + def read_data(self, remote_path, decode=False): |
1844 | + """Read data from instance filesystem. |
1845 | + |
1846 | + @param remote_path: path in instance |
1847 | + @param decode: decode data before returning. |
1848 | + @return_value: content of remote_path as bytes if 'decode' is False, |
1849 | + and as string if 'decode' is True. |
1850 | + """ |
1851 | + # when sh is invoked with '-c', then the first argument is "$0" |
1852 | + # which is commonly understood as the "program name". |
1853 | + # 'read_data' is the program name, and 'remote_path' is '$1' |
1854 | + stdout, stderr, rc = self._execute( |
1855 | + ["sh", "-c", 'exec cat "$1"', 'read_data', remote_path]) |
1856 | + if rc != 0: |
1857 | + raise RuntimeError("Failed to read file '%s'" % remote_path) |
1858 | + |
1859 | + if decode: |
1860 | + return stdout.decode() |
1861 | + return stdout |
1862 | + |
1863 | + def write_data(self, remote_path, data): |
1864 | + """Write data to instance filesystem. |
1865 | + |
1866 | + @param remote_path: path in instance |
1867 | + @param data: data to write in bytes |
1868 | + """ |
1869 | + # when sh is invoked with '-c', then the first argument is "$0" |
1870 | + # which is commonly understood as the "program name". |
1871 | + # 'write_data' is the program name, and 'remote_path' is '$1' |
1872 | + _, _, rc = self._execute( |
1873 | + ["sh", "-c", 'exec cat >"$1"', 'write_data', remote_path], |
1874 | + stdin=data) |
1875 | + |
1876 | + if rc != 0: |
1877 | + raise RuntimeError("Failed to write to '%s'" % remote_path) |
1878 | + return |
1879 | + |
1880 | + def pull_file(self, remote_path, local_path): |
1881 | + """Copy file at 'remote_path', from instance to 'local_path'. |
1882 | + |
1883 | + @param remote_path: path on remote instance |
1884 | + @param local_path: path on local instance |
1885 | + """ |
1886 | + with open(local_path, 'wb') as fp: |
1887 | + fp.write(self.read_data(remote_path)) |
1888 | + |
1889 | + def push_file(self, local_path, remote_path): |
1890 | + """Copy file at 'local_path' to instance at 'remote_path'. |
1891 | + |
1892 | + @param local_path: path on local instance |
1893 | + @param remote_path: path on remote instance""" |
1894 | + with open(local_path, "rb") as fp: |
1895 | + self.write_data(remote_path, data=fp.read()) |
1896 | + |
1897 | + def run_script(self, script, rcs=None, description=None): |
1898 | + """Run script in target and return stdout. |
1899 | + |
1900 | + @param script: script contents |
1901 | + @param rcs: allowed return codes from script |
1902 | + @param description: purpose of script |
1903 | + @return_value: stdout from script |
1904 | + """ |
1905 | + # Just write to a file, add execute, run it, then remove it. |
1906 | + shblob = '; '.join(( |
1907 | + 'set -e', |
1908 | + 's="$1"', |
1909 | + 'shift', |
1910 | + 'cat > "$s"', |
1911 | + 'trap "rm -f $s" EXIT', |
1912 | + 'chmod +x "$s"', |
1913 | + '"$s" "$@"')) |
1914 | + return self.execute( |
1915 | + ['sh', '-c', shblob, 'runscript', self.tmpfile()], |
1916 | + stdin=script, description=description, rcs=rcs) |
1917 | + |
1918 | + def tmpfile(self): |
1919 | + """Get a tmp file in the target. |
1920 | + |
1921 | + @return_value: path to new file in target |
1922 | + """ |
1923 | + path = "/tmp/%s-%04d" % (type(self).__name__, self._tmp_count) |
1924 | + self._tmp_count += 1 |
1925 | + return path |
1926 | + |
1927 | + |
1928 | class InTargetExecuteError(c_util.ProcessExecutionError): |
1929 | """Error type for in target commands that fail.""" |
1930 | |
1931 | - default_desc = 'Unexpected error while running command in target instance' |
1932 | + default_desc = 'Unexpected error while running command.' |
1933 | |
1934 | - def __init__(self, stdout, stderr, exit_code, cmd, instance, |
1935 | - description=None): |
1936 | + def __init__(self, stdout, stderr, exit_code, cmd, description=None, |
1937 | + reason=None): |
1938 | """Init error and parent error class.""" |
1939 | - if isinstance(cmd, (tuple, list)): |
1940 | - cmd = ' '.join(cmd) |
1941 | super(InTargetExecuteError, self).__init__( |
1942 | - stdout=stdout, stderr=stderr, exit_code=exit_code, cmd=cmd, |
1943 | - reason="Instance: {}".format(instance), |
1944 | - description=description if description else self.default_desc) |
1945 | + stdout=stdout, stderr=stderr, exit_code=exit_code, |
1946 | + cmd=shell_quote(cmd), |
1947 | + description=description if description else self.default_desc, |
1948 | + reason=reason) |
1949 | |
1950 | |
1951 | class TempDir(object): |
1952 | diff --git a/tests/unittests/test_data.py b/tests/unittests/test_data.py |
1953 | index 6d621d2..275b16d 100644 |
1954 | --- a/tests/unittests/test_data.py |
1955 | +++ b/tests/unittests/test_data.py |
1956 | @@ -18,6 +18,8 @@ from email.mime.application import MIMEApplication |
1957 | from email.mime.base import MIMEBase |
1958 | from email.mime.multipart import MIMEMultipart |
1959 | |
1960 | +import httpretty |
1961 | + |
1962 | from cloudinit import handlers |
1963 | from cloudinit import helpers as c_helpers |
1964 | from cloudinit import log |
1965 | @@ -522,6 +524,54 @@ c: 4 |
1966 | self.assertEqual(cfg.get('password'), 'gocubs') |
1967 | self.assertEqual(cfg.get('locale'), 'chicago') |
1968 | |
1969 | + @httpretty.activate |
1970 | + @mock.patch('cloudinit.url_helper.time.sleep') |
1971 | + def test_include(self, mock_sleep): |
1972 | + """Test #include.""" |
1973 | + included_url = 'http://hostname/path' |
1974 | + included_data = '#cloud-config\nincluded: true\n' |
1975 | + httpretty.register_uri(httpretty.GET, included_url, included_data) |
1976 | + |
1977 | + blob = '#include\n%s\n' % included_url |
1978 | + |
1979 | + self.reRoot() |
1980 | + ci = stages.Init() |
1981 | + ci.datasource = FakeDataSource(blob) |
1982 | + ci.fetch() |
1983 | + ci.consume_data() |
1984 | + cc_contents = util.load_file(ci.paths.get_ipath("cloud_config")) |
1985 | + cc = util.load_yaml(cc_contents) |
1986 | + self.assertTrue(cc.get('included')) |
1987 | + |
1988 | + @httpretty.activate |
1989 | + @mock.patch('cloudinit.url_helper.time.sleep') |
1990 | + def test_include_bad_url(self, mock_sleep): |
1991 | + """Test #include with a bad URL.""" |
1992 | + bad_url = 'http://bad/forbidden' |
1993 | + bad_data = '#cloud-config\nbad: true\n' |
1994 | + httpretty.register_uri(httpretty.GET, bad_url, bad_data, status=403) |
1995 | + |
1996 | + included_url = 'http://hostname/path' |
1997 | + included_data = '#cloud-config\nincluded: true\n' |
1998 | + httpretty.register_uri(httpretty.GET, included_url, included_data) |
1999 | + |
2000 | + blob = '#include\n%s\n%s' % (bad_url, included_url) |
2001 | + |
2002 | + self.reRoot() |
2003 | + ci = stages.Init() |
2004 | + ci.datasource = FakeDataSource(blob) |
2005 | + log_file = self.capture_log(logging.WARNING) |
2006 | + ci.fetch() |
2007 | + ci.consume_data() |
2008 | + |
2009 | + self.assertIn("403 Client Error: Forbidden for url: %s" % bad_url, |
2010 | + log_file.getvalue()) |
2011 | + |
2012 | + cc_contents = util.load_file(ci.paths.get_ipath("cloud_config")) |
2013 | + cc = util.load_yaml(cc_contents) |
2014 | + self.assertIsNone(cc.get('bad')) |
2015 | + self.assertTrue(cc.get('included')) |
2016 | + |
2017 | |
2018 | class TestUDProcess(helpers.ResourceUsingTestCase): |
2019 | |
2020 | diff --git a/tests/unittests/test_datasource/test_ec2.py b/tests/unittests/test_datasource/test_ec2.py |
2021 | index 6af699a..ba328ee 100644 |
2022 | --- a/tests/unittests/test_datasource/test_ec2.py |
2023 | +++ b/tests/unittests/test_datasource/test_ec2.py |
2024 | @@ -307,6 +307,39 @@ class TestEc2(test_helpers.HttprettyTestCase): |
2025 | |
2026 | @httpretty.activate |
2027 | @mock.patch('cloudinit.net.dhcp.maybe_perform_dhcp_discovery') |
2028 | + def test_network_config_cached_property_refreshed_on_upgrade(self, m_dhcp): |
2029 | + """Refresh the network_config Ec2 cache if network key is absent. |
2030 | + |
2031 | + This catches an upgrade issue where obj.pkl contained stale metadata |
2032 | + which lacked newly required network key. |
2033 | + """ |
2034 | + old_metadata = copy.deepcopy(DEFAULT_METADATA) |
2035 | + old_metadata.pop('network') |
2036 | + ds = self._setup_ds( |
2037 | + platform_data=self.valid_platform_data, |
2038 | + sys_cfg={'datasource': {'Ec2': {'strict_id': True}}}, |
2039 | + md=old_metadata) |
2040 | + self.assertTrue(ds.get_data()) |
2041 | + # Provide new revision of metadata that contains network data |
2042 | + register_mock_metaserver( |
2043 | + 'http://169.254.169.254/2009-04-04/meta-data/', DEFAULT_METADATA) |
2044 | + mac1 = '06:17:04:d7:26:09' # Defined in DEFAULT_METADATA |
2045 | + get_interface_mac_path = ( |
2046 | + 'cloudinit.sources.DataSourceEc2.net.get_interface_mac') |
2047 | + ds.fallback_nic = 'eth9' |
2048 | + with mock.patch(get_interface_mac_path) as m_get_interface_mac: |
2049 | + m_get_interface_mac.return_value = mac1 |
2050 | + ds.network_config # Will re-crawl network metadata |
2051 | + self.assertIn('Re-crawl of metadata service', self.logs.getvalue()) |
2052 | + expected = {'version': 1, 'config': [ |
2053 | + {'mac_address': '06:17:04:d7:26:09', |
2054 | + 'name': 'eth9', |
2055 | + 'subnets': [{'type': 'dhcp4'}, {'type': 'dhcp6'}], |
2056 | + 'type': 'physical'}]} |
2057 | + self.assertEqual(expected, ds.network_config) |
2058 | + |
2059 | + @httpretty.activate |
2060 | + @mock.patch('cloudinit.net.dhcp.maybe_perform_dhcp_discovery') |
2061 | def test_valid_platform_with_strict_true(self, m_dhcp): |
2062 | """Valid platform data should return true with strict_id true.""" |
2063 | ds = self._setup_ds( |
2064 | diff --git a/tests/unittests/test_handler/test_handler_etc_hosts.py b/tests/unittests/test_handler/test_handler_etc_hosts.py |
2065 | new file mode 100644 |
2066 | index 0000000..ced05a8 |
2067 | --- /dev/null |
2068 | +++ b/tests/unittests/test_handler/test_handler_etc_hosts.py |
2069 | @@ -0,0 +1,69 @@ |
2070 | +# This file is part of cloud-init. See LICENSE file for license information. |
2071 | + |
2072 | +from cloudinit.config import cc_update_etc_hosts |
2073 | + |
2074 | +from cloudinit import cloud |
2075 | +from cloudinit import distros |
2076 | +from cloudinit import helpers |
2077 | +from cloudinit import util |
2078 | + |
2079 | +from cloudinit.tests import helpers as t_help |
2080 | + |
2081 | +import logging |
2082 | +import os |
2083 | +import shutil |
2084 | + |
2085 | +LOG = logging.getLogger(__name__) |
2086 | + |
2087 | + |
2088 | +class TestHostsFile(t_help.FilesystemMockingTestCase): |
2089 | + def setUp(self): |
2090 | + super(TestHostsFile, self).setUp() |
2091 | + self.tmp = self.tmp_dir() |
2092 | + |
2093 | + def _fetch_distro(self, kind): |
2094 | + cls = distros.fetch(kind) |
2095 | + paths = helpers.Paths({}) |
2096 | + return cls(kind, {}, paths) |
2097 | + |
2098 | + def test_write_etc_hosts_suse_localhost(self): |
2099 | + cfg = { |
2100 | + 'manage_etc_hosts': 'localhost', |
2101 | + 'hostname': 'cloud-init.test.us' |
2102 | + } |
2103 | + os.makedirs('%s/etc/' % self.tmp) |
2104 | + hosts_content = '192.168.1.1 blah.blah.us blah\n' |
2105 | + fout = open('%s/etc/hosts' % self.tmp, 'w') |
2106 | + fout.write(hosts_content) |
2107 | + fout.close() |
2108 | + distro = self._fetch_distro('sles') |
2109 | + distro.hosts_fn = '%s/etc/hosts' % self.tmp |
2110 | + paths = helpers.Paths({}) |
2111 | + ds = None |
2112 | + cc = cloud.Cloud(ds, paths, {}, distro, None) |
2113 | + self.patchUtils(self.tmp) |
2114 | + cc_update_etc_hosts.handle('test', cfg, cc, LOG, []) |
2115 | + contents = util.load_file('%s/etc/hosts' % self.tmp) |
2116 | + if '127.0.0.1\tcloud-init.test.us\tcloud-init' not in contents: |
2117 | + self.assertIsNone('No entry for 127.0.0.1 in etc/hosts') |
2118 | + if '192.168.1.1\tblah.blah.us\tblah' not in contents: |
2119 | + self.assertIsNone('Default etc/hosts content modified') |
2120 | + |
2121 | + def test_write_etc_hosts_suse_template(self): |
2122 | + cfg = { |
2123 | + 'manage_etc_hosts': 'template', |
2124 | + 'hostname': 'cloud-init.test.us' |
2125 | + } |
2126 | + shutil.copytree('templates', '%s/etc/cloud/templates' % self.tmp) |
2127 | + distro = self._fetch_distro('sles') |
2128 | + paths = helpers.Paths({}) |
2129 | + paths.template_tpl = '%s' % self.tmp + '/etc/cloud/templates/%s.tmpl' |
2130 | + ds = None |
2131 | + cc = cloud.Cloud(ds, paths, {}, distro, None) |
2132 | + self.patchUtils(self.tmp) |
2133 | + cc_update_etc_hosts.handle('test', cfg, cc, LOG, []) |
2134 | + contents = util.load_file('%s/etc/hosts' % self.tmp) |
2135 | + if '127.0.0.1 cloud-init.test.us cloud-init' not in contents: |
2136 | + self.assertIsNone('No entry for 127.0.0.1 in etc/hosts') |
2137 | + if '::1 cloud-init.test.us cloud-init' not in contents: |
2138 | + self.assertIsNone('No entry for 127.0.0.1 in etc/hosts') |
2139 | diff --git a/tests/unittests/test_handler/test_handler_ntp.py b/tests/unittests/test_handler/test_handler_ntp.py |
2140 | index 3abe578..28a8455 100644 |
2141 | --- a/tests/unittests/test_handler/test_handler_ntp.py |
2142 | +++ b/tests/unittests/test_handler/test_handler_ntp.py |
2143 | @@ -430,5 +430,31 @@ class TestNtp(FilesystemMockingTestCase): |
2144 | "[Time]\nNTP=192.168.2.1 192.168.2.2 0.mypool.org \n", |
2145 | content.decode()) |
2146 | |
2147 | + def test_write_ntp_config_template_defaults_pools_empty_lists_sles(self): |
2148 | + """write_ntp_config_template defaults pools servers upon empty config. |
2149 | + |
2150 | + When both pools and servers are empty, default NR_POOL_SERVERS get |
2151 | + configured. |
2152 | + """ |
2153 | + distro = 'sles' |
2154 | + mycloud = self._get_cloud(distro) |
2155 | + ntp_conf = self.tmp_path('ntp.conf', self.new_root) # Doesn't exist |
2156 | + # Create ntp.conf.tmpl |
2157 | + with open('{0}.tmpl'.format(ntp_conf), 'wb') as stream: |
2158 | + stream.write(NTP_TEMPLATE) |
2159 | + with mock.patch('cloudinit.config.cc_ntp.NTP_CONF', ntp_conf): |
2160 | + cc_ntp.write_ntp_config_template({}, mycloud, ntp_conf) |
2161 | + content = util.read_file_or_url('file://' + ntp_conf).contents |
2162 | + default_pools = [ |
2163 | + "{0}.opensuse.pool.ntp.org".format(x) |
2164 | + for x in range(0, cc_ntp.NR_POOL_SERVERS)] |
2165 | + self.assertEqual( |
2166 | + "servers []\npools {0}\n".format(default_pools), |
2167 | + content.decode()) |
2168 | + self.assertIn( |
2169 | + "Adding distro default ntp pool servers: {0}".format( |
2170 | + ",".join(default_pools)), |
2171 | + self.logs.getvalue()) |
2172 | + |
2173 | |
2174 | # vi: ts=4 expandtab |
2175 | diff --git a/tests/unittests/test_rh_subscription.py b/tests/unittests/test_rh_subscription.py |
2176 | index e9d5702..2271810 100644 |
2177 | --- a/tests/unittests/test_rh_subscription.py |
2178 | +++ b/tests/unittests/test_rh_subscription.py |
2179 | @@ -2,6 +2,7 @@ |
2180 | |
2181 | """Tests for registering RHEL subscription via rh_subscription.""" |
2182 | |
2183 | +import copy |
2184 | import logging |
2185 | |
2186 | from cloudinit.config import cc_rh_subscription |
2187 | @@ -68,6 +69,20 @@ class GoodTests(TestCase): |
2188 | self.assertEqual(self.SM.log_success.call_count, 1) |
2189 | self.assertEqual(self.SM._sub_man_cli.call_count, 2) |
2190 | |
2191 | + @mock.patch.object(cc_rh_subscription.SubscriptionManager, "_getRepos") |
2192 | + @mock.patch.object(cc_rh_subscription.SubscriptionManager, "_sub_man_cli") |
2193 | + def test_update_repos_disable_with_none(self, m_sub_man_cli, m_get_repos): |
2194 | + cfg = copy.deepcopy(self.config) |
2195 | + m_get_repos.return_value = ([], ['repo1']) |
2196 | + m_sub_man_cli.return_value = (b'', b'') |
2197 | + cfg['rh_subscription'].update( |
2198 | + {'enable-repo': ['repo1'], 'disable-repo': None}) |
2199 | + mysm = cc_rh_subscription.SubscriptionManager(cfg) |
2200 | + self.assertEqual(True, mysm.update_repos()) |
2201 | + m_get_repos.assert_called_with() |
2202 | + self.assertEqual(m_sub_man_cli.call_args_list, |
2203 | + [mock.call(['repos', '--enable=repo1'])]) |
2204 | + |
2205 | def test_full_registration(self): |
2206 | ''' |
2207 | Registration with auto-attach, service-level, adding pools, |
PASSED: Continuous integration, rev:e6747555e0d c787fa99bd6bfff 43846b02ef1bd1 /jenkins. ubuntu. com/server/ job/cloud- init-ci/ 532/
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/ 532/rebuild
https:/