Merge ~chad.smith/cloud-init:ubuntu/zesty into cloud-init: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)
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

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 :

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

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

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
1diff --git a/cloudinit/cloud.py b/cloudinit/cloud.py
2index 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
16diff --git a/cloudinit/config/cc_ntp.py b/cloudinit/config/cc_ntp.py
17index 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
44diff --git a/cloudinit/config/cc_rh_subscription.py b/cloudinit/config/cc_rh_subscription.py
45index 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
179diff --git a/cloudinit/config/cc_update_etc_hosts.py b/cloudinit/config/cc_update_etc_hosts.py
180index 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
194diff --git a/cloudinit/net/dhcp.py b/cloudinit/net/dhcp.py
195index 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):
231diff --git a/cloudinit/net/tests/test_dhcp.py b/cloudinit/net/tests/test_dhcp.py
232index 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):
273diff --git a/cloudinit/sources/DataSourceAzure.py b/cloudinit/sources/DataSourceAzure.py
274index 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
316diff --git a/cloudinit/sources/DataSourceEc2.py b/cloudinit/sources/DataSourceEc2.py
317index 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
402diff --git a/cloudinit/user_data.py b/cloudinit/user_data.py
403index 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)
453diff --git a/debian/changelog b/debian/changelog
454index 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.
487diff --git a/debian/cloud-init.templates b/debian/cloud-init.templates
488index 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.
503diff --git a/sysvinit/gentoo/cloud-config b/sysvinit/gentoo/cloud-config
504old mode 100644
505new mode 100755
506index 5618472..5618472
507--- a/sysvinit/gentoo/cloud-config
508+++ b/sysvinit/gentoo/cloud-config
509diff --git a/sysvinit/gentoo/cloud-final b/sysvinit/gentoo/cloud-final
510old mode 100644
511new mode 100755
512index a9bf01f..a9bf01f
513--- a/sysvinit/gentoo/cloud-final
514+++ b/sysvinit/gentoo/cloud-final
515diff --git a/sysvinit/gentoo/cloud-init b/sysvinit/gentoo/cloud-init
516old mode 100644
517new mode 100755
518index 531a715..531a715
519--- a/sysvinit/gentoo/cloud-init
520+++ b/sysvinit/gentoo/cloud-init
521diff --git a/sysvinit/gentoo/cloud-init-local b/sysvinit/gentoo/cloud-init-local
522old mode 100644
523new mode 100755
524index 0f8cf65..0f8cf65
525--- a/sysvinit/gentoo/cloud-init-local
526+++ b/sysvinit/gentoo/cloud-init-local
527diff --git a/templates/hosts.opensuse.tmpl b/templates/hosts.opensuse.tmpl
528deleted file mode 100644
529index 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-
559diff --git a/templates/hosts.suse.tmpl b/templates/hosts.suse.tmpl
560index 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
584diff --git a/templates/ntp.conf.opensuse.tmpl b/templates/ntp.conf.opensuse.tmpl
585new file mode 100644
586index 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+
678diff --git a/templates/ntp.conf.sles.tmpl b/templates/ntp.conf.sles.tmpl
679index 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
700diff --git a/tests/cloud_tests/collect.py b/tests/cloud_tests/collect.py
701index 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
741diff --git a/tests/cloud_tests/images/base.py b/tests/cloud_tests/images/base.py
742index 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
779diff --git a/tests/cloud_tests/images/lxd.py b/tests/cloud_tests/images/lxd.py
780index 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
864diff --git a/tests/cloud_tests/images/nocloudkvm.py b/tests/cloud_tests/images/nocloudkvm.py
865index 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
947diff --git a/tests/cloud_tests/instances/base.py b/tests/cloud_tests/instances/base.py
948index 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.
1047diff --git a/tests/cloud_tests/instances/lxd.py b/tests/cloud_tests/instances/lxd.py
1048index 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
1213diff --git a/tests/cloud_tests/instances/nocloudkvm.py b/tests/cloud_tests/instances/nocloudkvm.py
1214index 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
1425diff --git a/tests/cloud_tests/testcases/examples/run_commands.yaml b/tests/cloud_tests/testcases/examples/run_commands.yaml
1426index 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
1442diff --git a/tests/cloud_tests/testcases/modules/apt_configure_sources_ppa.py b/tests/cloud_tests/testcases/modules/apt_configure_sources_ppa.py
1443index 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
1463diff --git a/tests/cloud_tests/testcases/modules/apt_configure_sources_ppa.yaml b/tests/cloud_tests/testcases/modules/apt_configure_sources_ppa.yaml
1464index 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
1490diff --git a/tests/cloud_tests/testcases/modules/keys_to_console.py b/tests/cloud_tests/testcases/modules/keys_to_console.py
1491index 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
1512diff --git a/tests/cloud_tests/testcases/modules/runcmd.yaml b/tests/cloud_tests/testcases/modules/runcmd.yaml
1513index 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
1529diff --git a/tests/cloud_tests/testcases/modules/set_hostname.py b/tests/cloud_tests/testcases/modules/set_hostname.py
1530index 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
1546diff --git a/tests/cloud_tests/testcases/modules/set_hostname.yaml b/tests/cloud_tests/testcases/modules/set_hostname.yaml
1547index 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
1560diff --git a/tests/cloud_tests/testcases/modules/set_hostname_fqdn.py b/tests/cloud_tests/testcases/modules/set_hostname_fqdn.py
1561index 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
1599diff --git a/tests/cloud_tests/testcases/modules/set_hostname_fqdn.yaml b/tests/cloud_tests/testcases/modules/set_hostname_fqdn.yaml
1600index 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
1615diff --git a/tests/cloud_tests/testcases/modules/set_password_expire.py b/tests/cloud_tests/testcases/modules/set_password_expire.py
1616index 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
1627diff --git a/tests/cloud_tests/testcases/modules/set_password_expire.yaml b/tests/cloud_tests/testcases/modules/set_password_expire.yaml
1628index 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
1641diff --git a/tests/cloud_tests/testcases/modules/set_password_list.yaml b/tests/cloud_tests/testcases/modules/set_password_list.yaml
1642index 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"
1653diff --git a/tests/cloud_tests/testcases/modules/set_password_list_string.yaml b/tests/cloud_tests/testcases/modules/set_password_list_string.yaml
1654index 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"
1665diff --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
1666index 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
1682diff --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
1683index 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: |
1694diff --git a/tests/cloud_tests/testcases/modules/ssh_keys_generate.py b/tests/cloud_tests/testcases/modules/ssh_keys_generate.py
1695index 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')
1710diff --git a/tests/cloud_tests/testcases/modules/ssh_keys_generate.yaml b/tests/cloud_tests/testcases/modules/ssh_keys_generate.yaml
1711index 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
1727diff --git a/tests/cloud_tests/testcases/modules/ssh_keys_provided.py b/tests/cloud_tests/testcases/modules/ssh_keys_provided.py
1728index 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')
1749diff --git a/tests/cloud_tests/testcases/modules/ssh_keys_provided.yaml b/tests/cloud_tests/testcases/modules/ssh_keys_provided.yaml
1750index 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
1766diff --git a/tests/cloud_tests/util.py b/tests/cloud_tests/util.py
1767index 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):
1952diff --git a/tests/unittests/test_data.py b/tests/unittests/test_data.py
1953index 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
2020diff --git a/tests/unittests/test_datasource/test_ec2.py b/tests/unittests/test_datasource/test_ec2.py
2021index 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(
2064diff --git a/tests/unittests/test_handler/test_handler_etc_hosts.py b/tests/unittests/test_handler/test_handler_etc_hosts.py
2065new file mode 100644
2066index 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')
2139diff --git a/tests/unittests/test_handler/test_handler_ntp.py b/tests/unittests/test_handler/test_handler_ntp.py
2140index 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
2175diff --git a/tests/unittests/test_rh_subscription.py b/tests/unittests/test_rh_subscription.py
2176index 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,

Subscribers

People subscribed via source and target branches