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
diff --git a/cloudinit/cloud.py b/cloudinit/cloud.py
index d8a9fc8..ba61678 100644
--- a/cloudinit/cloud.py
+++ b/cloudinit/cloud.py
@@ -56,8 +56,8 @@ class Cloud(object):
56 def get_template_filename(self, name):56 def get_template_filename(self, name):
57 fn = self.paths.template_tpl % (name)57 fn = self.paths.template_tpl % (name)
58 if not os.path.isfile(fn):58 if not os.path.isfile(fn):
59 LOG.warning("No template found at %s for template named %s",59 LOG.warning("No template found in %s for template named %s",
60 fn, name)60 os.path.dirname(fn), name)
61 return None61 return None
62 return fn62 return fn
6363
diff --git a/cloudinit/config/cc_ntp.py b/cloudinit/config/cc_ntp.py
index d43d060..f50bcb3 100644
--- a/cloudinit/config/cc_ntp.py
+++ b/cloudinit/config/cc_ntp.py
@@ -23,7 +23,7 @@ frequency = PER_INSTANCE
23NTP_CONF = '/etc/ntp.conf'23NTP_CONF = '/etc/ntp.conf'
24TIMESYNCD_CONF = '/etc/systemd/timesyncd.conf.d/cloud-init.conf'24TIMESYNCD_CONF = '/etc/systemd/timesyncd.conf.d/cloud-init.conf'
25NR_POOL_SERVERS = 425NR_POOL_SERVERS = 4
26distros = ['centos', 'debian', 'fedora', 'opensuse', 'ubuntu']26distros = ['centos', 'debian', 'fedora', 'opensuse', 'sles', 'ubuntu']
2727
2828
29# The schema definition for each cloud-config module is a strict contract for29# The schema definition for each cloud-config module is a strict contract for
@@ -174,8 +174,13 @@ def rename_ntp_conf(config=None):
174174
175def generate_server_names(distro):175def generate_server_names(distro):
176 names = []176 names = []
177 pool_distro = distro
178 # For legal reasons x.pool.sles.ntp.org does not exist,
179 # use the opensuse pool
180 if distro == 'sles':
181 pool_distro = 'opensuse'
177 for x in range(0, NR_POOL_SERVERS):182 for x in range(0, NR_POOL_SERVERS):
178 name = "%d.%s.pool.ntp.org" % (x, distro)183 name = "%d.%s.pool.ntp.org" % (x, pool_distro)
179 names.append(name)184 names.append(name)
180 return names185 return names
181186
diff --git a/cloudinit/config/cc_rh_subscription.py b/cloudinit/config/cc_rh_subscription.py
index 7f36cf8..a9d21e7 100644
--- a/cloudinit/config/cc_rh_subscription.py
+++ b/cloudinit/config/cc_rh_subscription.py
@@ -38,14 +38,16 @@ Subscription`` example config.
38 server-hostname: <hostname>38 server-hostname: <hostname>
39"""39"""
4040
41from cloudinit import log as logging
41from cloudinit import util42from cloudinit import util
4243
44LOG = logging.getLogger(__name__)
45
43distros = ['fedora', 'rhel']46distros = ['fedora', 'rhel']
4447
4548
46def handle(name, cfg, _cloud, log, _args):49def handle(name, cfg, _cloud, log, _args):
47 sm = SubscriptionManager(cfg)50 sm = SubscriptionManager(cfg, log=log)
48 sm.log = log
49 if not sm.is_configured():51 if not sm.is_configured():
50 log.debug("%s: module not configured.", name)52 log.debug("%s: module not configured.", name)
51 return None53 return None
@@ -86,10 +88,9 @@ def handle(name, cfg, _cloud, log, _args):
86 if not return_stat:88 if not return_stat:
87 raise SubscriptionError("Unable to attach pools {0}"89 raise SubscriptionError("Unable to attach pools {0}"
88 .format(sm.pools))90 .format(sm.pools))
89 if (sm.enable_repo is not None) or (sm.disable_repo is not None):91 return_stat = sm.update_repos()
90 return_stat = sm.update_repos(sm.enable_repo, sm.disable_repo)92 if not return_stat:
91 if not return_stat:93 raise SubscriptionError("Unable to add or remove repos")
92 raise SubscriptionError("Unable to add or remove repos")
93 sm.log_success("rh_subscription plugin completed successfully")94 sm.log_success("rh_subscription plugin completed successfully")
94 except SubscriptionError as e:95 except SubscriptionError as e:
95 sm.log_warn(str(e))96 sm.log_warn(str(e))
@@ -108,7 +109,10 @@ class SubscriptionManager(object):
108 'rhsm-baseurl', 'server-hostname',109 'rhsm-baseurl', 'server-hostname',
109 'auto-attach', 'service-level']110 'auto-attach', 'service-level']
110111
111 def __init__(self, cfg):112 def __init__(self, cfg, log=None):
113 if log is None:
114 log = LOG
115 self.log = log
112 self.cfg = cfg116 self.cfg = cfg
113 self.rhel_cfg = self.cfg.get('rh_subscription', {})117 self.rhel_cfg = self.cfg.get('rh_subscription', {})
114 self.rhsm_baseurl = self.rhel_cfg.get('rhsm-baseurl')118 self.rhsm_baseurl = self.rhel_cfg.get('rhsm-baseurl')
@@ -130,7 +134,7 @@ class SubscriptionManager(object):
130134
131 def log_warn(self, msg):135 def log_warn(self, msg):
132 '''Simple wrapper for logging warning messages. Useful for unittests'''136 '''Simple wrapper for logging warning messages. Useful for unittests'''
133 self.log.warn(msg)137 self.log.warning(msg)
134138
135 def _verify_keys(self):139 def _verify_keys(self):
136 '''140 '''
@@ -245,7 +249,7 @@ class SubscriptionManager(object):
245 return False249 return False
246250
247 reg_id = return_out.split("ID: ")[1].rstrip()251 reg_id = return_out.split("ID: ")[1].rstrip()
248 self.log.debug("Registered successfully with ID {0}".format(reg_id))252 self.log.debug("Registered successfully with ID %s", reg_id)
249 return True253 return True
250254
251 def _set_service_level(self):255 def _set_service_level(self):
@@ -347,7 +351,7 @@ class SubscriptionManager(object):
347 try:351 try:
348 self._sub_man_cli(cmd)352 self._sub_man_cli(cmd)
349 self.log.debug("Attached the following pools to your "353 self.log.debug("Attached the following pools to your "
350 "system: %s" % (", ".join(pool_list))354 "system: %s", (", ".join(pool_list))
351 .replace('--pool=', ''))355 .replace('--pool=', ''))
352 return True356 return True
353 except util.ProcessExecutionError as e:357 except util.ProcessExecutionError as e:
@@ -355,18 +359,24 @@ class SubscriptionManager(object):
355 "due to {1}".format(pool, e))359 "due to {1}".format(pool, e))
356 return False360 return False
357361
358 def update_repos(self, erepos, drepos):362 def update_repos(self):
359 '''363 '''
360 Takes a list of yum repo ids that need to be disabled or enabled; then364 Takes a list of yum repo ids that need to be disabled or enabled; then
361 it verifies if they are already enabled or disabled and finally365 it verifies if they are already enabled or disabled and finally
362 executes the action to disable or enable366 executes the action to disable or enable
363 '''367 '''
364368
365 if (erepos is not None) and (not isinstance(erepos, list)):369 erepos = self.enable_repo
370 drepos = self.disable_repo
371 if erepos is None:
372 erepos = []
373 if drepos is None:
374 drepos = []
375 if not isinstance(erepos, list):
366 self.log_warn("Repo IDs must in the format of a list.")376 self.log_warn("Repo IDs must in the format of a list.")
367 return False377 return False
368378
369 if (drepos is not None) and (not isinstance(drepos, list)):379 if not isinstance(drepos, list):
370 self.log_warn("Repo IDs must in the format of a list.")380 self.log_warn("Repo IDs must in the format of a list.")
371 return False381 return False
372382
@@ -399,14 +409,14 @@ class SubscriptionManager(object):
399 for fail in enable_list_fail:409 for fail in enable_list_fail:
400 # Check if the repo exists or not410 # Check if the repo exists or not
401 if fail in active_repos:411 if fail in active_repos:
402 self.log.debug("Repo {0} is already enabled".format(fail))412 self.log.debug("Repo %s is already enabled", fail)
403 else:413 else:
404 self.log_warn("Repo {0} does not appear to "414 self.log_warn("Repo {0} does not appear to "
405 "exist".format(fail))415 "exist".format(fail))
406 if len(disable_list_fail) > 0:416 if len(disable_list_fail) > 0:
407 for fail in disable_list_fail:417 for fail in disable_list_fail:
408 self.log.debug("Repo {0} not disabled "418 self.log.debug("Repo %s not disabled "
409 "because it is not enabled".format(fail))419 "because it is not enabled", fail)
410420
411 cmd = ['repos']421 cmd = ['repos']
412 if len(disable_list) > 0:422 if len(disable_list) > 0:
@@ -422,10 +432,10 @@ class SubscriptionManager(object):
422 return False432 return False
423433
424 if len(enable_list) > 0:434 if len(enable_list) > 0:
425 self.log.debug("Enabled the following repos: %s" %435 self.log.debug("Enabled the following repos: %s",
426 (", ".join(enable_list)).replace('--enable=', ''))436 (", ".join(enable_list)).replace('--enable=', ''))
427 if len(disable_list) > 0:437 if len(disable_list) > 0:
428 self.log.debug("Disabled the following repos: %s" %438 self.log.debug("Disabled the following repos: %s",
429 (", ".join(disable_list)).replace('--disable=', ''))439 (", ".join(disable_list)).replace('--disable=', ''))
430 return True440 return True
431441
diff --git a/cloudinit/config/cc_update_etc_hosts.py b/cloudinit/config/cc_update_etc_hosts.py
index b394784..c96eede 100644
--- a/cloudinit/config/cc_update_etc_hosts.py
+++ b/cloudinit/config/cc_update_etc_hosts.py
@@ -23,8 +23,8 @@ using the template located in ``/etc/cloud/templates/hosts.tmpl``. In the
2323
24If ``manage_etc_hosts`` is set to ``localhost``, then cloud-init will not24If ``manage_etc_hosts`` is set to ``localhost``, then cloud-init will not
25rewrite ``/etc/hosts`` entirely, but rather will ensure that a entry for the25rewrite ``/etc/hosts`` entirely, but rather will ensure that a entry for the
26fqdn with ip ``127.0.1.1`` is present in ``/etc/hosts`` (i.e.26fqdn with a distribution dependent ip is present in ``/etc/hosts`` (i.e.
27``ping <hostname>`` will ping ``127.0.1.1``).27``ping <hostname>`` will ping ``127.0.0.1`` or ``127.0.1.1`` or other ip).
2828
29.. note::29.. note::
30 if ``manage_etc_hosts`` is set ``true`` or ``template``, the contents30 if ``manage_etc_hosts`` is set ``true`` or ``template``, the contents
diff --git a/cloudinit/net/dhcp.py b/cloudinit/net/dhcp.py
index 0cba703..d8624d8 100644
--- a/cloudinit/net/dhcp.py
+++ b/cloudinit/net/dhcp.py
@@ -8,6 +8,7 @@ import configobj
8import logging8import logging
9import os9import os
10import re10import re
11import signal
1112
12from cloudinit.net import find_fallback_nic, get_devicelist13from cloudinit.net import find_fallback_nic, get_devicelist
13from cloudinit import temp_utils14from cloudinit import temp_utils
@@ -41,8 +42,7 @@ def maybe_perform_dhcp_discovery(nic=None):
41 if nic is None:42 if nic is None:
42 nic = find_fallback_nic()43 nic = find_fallback_nic()
43 if nic is None:44 if nic is None:
44 LOG.debug(45 LOG.debug('Skip dhcp_discovery: Unable to find fallback nic.')
45 'Skip dhcp_discovery: Unable to find fallback nic.')
46 return {}46 return {}
47 elif nic not in get_devicelist():47 elif nic not in get_devicelist():
48 LOG.debug(48 LOG.debug(
@@ -119,7 +119,13 @@ def dhcp_discovery(dhclient_cmd_path, interface, cleandir):
119 cmd = [sandbox_dhclient_cmd, '-1', '-v', '-lf', lease_file,119 cmd = [sandbox_dhclient_cmd, '-1', '-v', '-lf', lease_file,
120 '-pf', pid_file, interface, '-sf', '/bin/true']120 '-pf', pid_file, interface, '-sf', '/bin/true']
121 util.subp(cmd, capture=True)121 util.subp(cmd, capture=True)
122 return parse_dhcp_lease_file(lease_file)122 pid = None
123 try:
124 pid = int(util.load_file(pid_file).strip())
125 return parse_dhcp_lease_file(lease_file)
126 finally:
127 if pid:
128 os.kill(pid, signal.SIGKILL)
123129
124130
125def networkd_parse_lease(content):131def networkd_parse_lease(content):
diff --git a/cloudinit/net/tests/test_dhcp.py b/cloudinit/net/tests/test_dhcp.py
index 1c1f504..3d8e15c 100644
--- a/cloudinit/net/tests/test_dhcp.py
+++ b/cloudinit/net/tests/test_dhcp.py
@@ -2,6 +2,7 @@
22
3import mock3import mock
4import os4import os
5import signal
5from textwrap import dedent6from textwrap import dedent
67
7from cloudinit.net.dhcp import (8from cloudinit.net.dhcp import (
@@ -114,8 +115,9 @@ class TestDHCPDiscoveryClean(CiTestCase):
114 self.assertEqual('eth9', call[0][1])115 self.assertEqual('eth9', call[0][1])
115 self.assertIn('/var/tmp/cloud-init/cloud-init-dhcp-', call[0][2])116 self.assertIn('/var/tmp/cloud-init/cloud-init-dhcp-', call[0][2])
116117
118 @mock.patch('cloudinit.net.dhcp.os.kill')
117 @mock.patch('cloudinit.net.dhcp.util.subp')119 @mock.patch('cloudinit.net.dhcp.util.subp')
118 def test_dhcp_discovery_run_in_sandbox(self, m_subp):120 def test_dhcp_discovery_run_in_sandbox(self, m_subp, m_kill):
119 """dhcp_discovery brings up the interface and runs dhclient.121 """dhcp_discovery brings up the interface and runs dhclient.
120122
121 It also returns the parsed dhcp.leases file generated in the sandbox.123 It also returns the parsed dhcp.leases file generated in the sandbox.
@@ -134,6 +136,10 @@ class TestDHCPDiscoveryClean(CiTestCase):
134 """)136 """)
135 lease_file = os.path.join(tmpdir, 'dhcp.leases')137 lease_file = os.path.join(tmpdir, 'dhcp.leases')
136 write_file(lease_file, lease_content)138 write_file(lease_file, lease_content)
139 pid_file = os.path.join(tmpdir, 'dhclient.pid')
140 my_pid = 1
141 write_file(pid_file, "%d\n" % my_pid)
142
137 self.assertItemsEqual(143 self.assertItemsEqual(
138 [{'interface': 'eth9', 'fixed-address': '192.168.2.74',144 [{'interface': 'eth9', 'fixed-address': '192.168.2.74',
139 'subnet-mask': '255.255.255.0', 'routers': '192.168.2.1'}],145 'subnet-mask': '255.255.255.0', 'routers': '192.168.2.1'}],
@@ -149,6 +155,7 @@ class TestDHCPDiscoveryClean(CiTestCase):
149 [os.path.join(tmpdir, 'dhclient'), '-1', '-v', '-lf',155 [os.path.join(tmpdir, 'dhclient'), '-1', '-v', '-lf',
150 lease_file, '-pf', os.path.join(tmpdir, 'dhclient.pid'),156 lease_file, '-pf', os.path.join(tmpdir, 'dhclient.pid'),
151 'eth9', '-sf', '/bin/true'], capture=True)])157 'eth9', '-sf', '/bin/true'], capture=True)])
158 m_kill.assert_has_calls([mock.call(my_pid, signal.SIGKILL)])
152159
153160
154class TestSystemdParseLeases(CiTestCase):161class TestSystemdParseLeases(CiTestCase):
diff --git a/cloudinit/sources/DataSourceAzure.py b/cloudinit/sources/DataSourceAzure.py
index 80c2bd1..8c3492d 100644
--- a/cloudinit/sources/DataSourceAzure.py
+++ b/cloudinit/sources/DataSourceAzure.py
@@ -465,10 +465,8 @@ class DataSourceAzure(sources.DataSource):
465465
466 1. Probe the drivers of the net-devices present and inject them in466 1. Probe the drivers of the net-devices present and inject them in
467 the network configuration under params: driver: <driver> value467 the network configuration under params: driver: <driver> value
468 2. If the driver value is 'mlx4_core', the control mode should be468 2. Generate a fallback network config that does not include any of
469 set to manual. The device will be later used to build a bond,469 the blacklisted devices.
470 for now we want to ensure the device gets named but does not
471 break any network configuration
472 """470 """
473 blacklist = ['mlx4_core']471 blacklist = ['mlx4_core']
474 if not self._network_config:472 if not self._network_config:
@@ -477,25 +475,6 @@ class DataSourceAzure(sources.DataSource):
477 netconfig = net.generate_fallback_config(475 netconfig = net.generate_fallback_config(
478 blacklist_drivers=blacklist, config_driver=True)476 blacklist_drivers=blacklist, config_driver=True)
479477
480 # if we have any blacklisted devices, update the network_config to
481 # include the device, mac, and driver values, but with no ip
482 # config; this ensures udev rules are generated but won't affect
483 # ip configuration
484 bl_found = 0
485 for bl_dev in [dev for dev in net.get_devicelist()
486 if net.device_driver(dev) in blacklist]:
487 bl_found += 1
488 cfg = {
489 'type': 'physical',
490 'name': 'vf%d' % bl_found,
491 'mac_address': net.get_interface_mac(bl_dev),
492 'params': {
493 'driver': net.device_driver(bl_dev),
494 'device_id': net.device_devid(bl_dev),
495 },
496 }
497 netconfig['config'].append(cfg)
498
499 self._network_config = netconfig478 self._network_config = netconfig
500479
501 return self._network_config480 return self._network_config
diff --git a/cloudinit/sources/DataSourceEc2.py b/cloudinit/sources/DataSourceEc2.py
index 0ef2217..7bbbfb6 100644
--- a/cloudinit/sources/DataSourceEc2.py
+++ b/cloudinit/sources/DataSourceEc2.py
@@ -65,7 +65,7 @@ class DataSourceEc2(sources.DataSource):
65 get_network_metadata = False65 get_network_metadata = False
6666
67 # Track the discovered fallback nic for use in configuration generation.67 # Track the discovered fallback nic for use in configuration generation.
68 fallback_nic = None68 _fallback_interface = None
6969
70 def __init__(self, sys_cfg, distro, paths):70 def __init__(self, sys_cfg, distro, paths):
71 sources.DataSource.__init__(self, sys_cfg, distro, paths)71 sources.DataSource.__init__(self, sys_cfg, distro, paths)
@@ -92,18 +92,17 @@ class DataSourceEc2(sources.DataSource):
92 elif self.cloud_platform == Platforms.NO_EC2_METADATA:92 elif self.cloud_platform == Platforms.NO_EC2_METADATA:
93 return False93 return False
9494
95 self.fallback_nic = net.find_fallback_nic()
96 if self.get_network_metadata: # Setup networking in init-local stage.95 if self.get_network_metadata: # Setup networking in init-local stage.
97 if util.is_FreeBSD():96 if util.is_FreeBSD():
98 LOG.debug("FreeBSD doesn't support running dhclient with -sf")97 LOG.debug("FreeBSD doesn't support running dhclient with -sf")
99 return False98 return False
100 dhcp_leases = dhcp.maybe_perform_dhcp_discovery(self.fallback_nic)99 dhcp_leases = dhcp.maybe_perform_dhcp_discovery(
100 self.fallback_interface)
101 if not dhcp_leases:101 if not dhcp_leases:
102 # DataSourceEc2Local failed in init-local stage. DataSourceEc2102 # DataSourceEc2Local failed in init-local stage. DataSourceEc2
103 # will still run in init-network stage.103 # will still run in init-network stage.
104 return False104 return False
105 dhcp_opts = dhcp_leases[-1]105 dhcp_opts = dhcp_leases[-1]
106 self.fallback_nic = dhcp_opts.get('interface')
107 net_params = {'interface': dhcp_opts.get('interface'),106 net_params = {'interface': dhcp_opts.get('interface'),
108 'ip': dhcp_opts.get('fixed-address'),107 'ip': dhcp_opts.get('fixed-address'),
109 'prefix_or_mask': dhcp_opts.get('subnet-mask'),108 'prefix_or_mask': dhcp_opts.get('subnet-mask'),
@@ -301,21 +300,44 @@ class DataSourceEc2(sources.DataSource):
301 return None300 return None
302301
303 result = None302 result = None
304 net_md = self.metadata.get('network')303 no_network_metadata_on_aws = bool(
304 'network' not in self.metadata and
305 self.cloud_platform == Platforms.AWS)
306 if no_network_metadata_on_aws:
307 LOG.debug("Metadata 'network' not present:"
308 " Refreshing stale metadata from prior to upgrade.")
309 util.log_time(
310 logfunc=LOG.debug, msg='Re-crawl of metadata service',
311 func=self._crawl_metadata)
312
305 # Limit network configuration to only the primary/fallback nic313 # Limit network configuration to only the primary/fallback nic
306 macs_to_nics = {314 iface = self.fallback_interface
307 net.get_interface_mac(self.fallback_nic): self.fallback_nic}315 macs_to_nics = {net.get_interface_mac(iface): iface}
316 net_md = self.metadata.get('network')
308 if isinstance(net_md, dict):317 if isinstance(net_md, dict):
309 result = convert_ec2_metadata_network_config(318 result = convert_ec2_metadata_network_config(
310 net_md, macs_to_nics=macs_to_nics,319 net_md, macs_to_nics=macs_to_nics, fallback_nic=iface)
311 fallback_nic=self.fallback_nic)
312 else:320 else:
313 LOG.warning("unexpected metadata 'network' key not valid: %s",321 LOG.warning("Metadata 'network' key not valid: %s.", net_md)
314 net_md)
315 self._network_config = result322 self._network_config = result
316323
317 return self._network_config324 return self._network_config
318325
326 @property
327 def fallback_interface(self):
328 if self._fallback_interface is None:
329 # fallback_nic was used at one point, so restored objects may
330 # have an attribute there. respect that if found.
331 _legacy_fbnic = getattr(self, 'fallback_nic', None)
332 if _legacy_fbnic:
333 self._fallback_interface = _legacy_fbnic
334 self.fallback_nic = None
335 else:
336 self._fallback_interface = net.find_fallback_nic()
337 if self._fallback_interface is None:
338 LOG.warning("Did not find a fallback interface on EC2.")
339 return self._fallback_interface
340
319 def _crawl_metadata(self):341 def _crawl_metadata(self):
320 """Crawl metadata service when available.342 """Crawl metadata service when available.
321343
diff --git a/cloudinit/user_data.py b/cloudinit/user_data.py
index 88cb7f8..cc55daf 100644
--- a/cloudinit/user_data.py
+++ b/cloudinit/user_data.py
@@ -19,6 +19,7 @@ import six
1919
20from cloudinit import handlers20from cloudinit import handlers
21from cloudinit import log as logging21from cloudinit import log as logging
22from cloudinit.url_helper import UrlError
22from cloudinit import util23from cloudinit import util
2324
24LOG = logging.getLogger(__name__)25LOG = logging.getLogger(__name__)
@@ -222,16 +223,28 @@ class UserDataProcessor(object):
222 if include_once_on and os.path.isfile(include_once_fn):223 if include_once_on and os.path.isfile(include_once_fn):
223 content = util.load_file(include_once_fn)224 content = util.load_file(include_once_fn)
224 else:225 else:
225 resp = util.read_file_or_url(include_url,226 try:
226 ssl_details=self.ssl_details)227 resp = util.read_file_or_url(include_url,
227 if include_once_on and resp.ok():228 ssl_details=self.ssl_details)
228 util.write_file(include_once_fn, resp.contents, mode=0o600)229 if include_once_on and resp.ok():
229 if resp.ok():230 util.write_file(include_once_fn, resp.contents,
230 content = resp.contents231 mode=0o600)
231 else:232 if resp.ok():
232 LOG.warning(("Fetching from %s resulted in"233 content = resp.contents
233 " a invalid http code of %s"),234 else:
234 include_url, resp.code)235 LOG.warning(("Fetching from %s resulted in"
236 " a invalid http code of %s"),
237 include_url, resp.code)
238 except UrlError as urle:
239 message = str(urle)
240 # Older versions of requests.exceptions.HTTPError may not
241 # include the errant url. Append it for clarity in logs.
242 if include_url not in message:
243 message += ' for url: {0}'.format(include_url)
244 LOG.warning(message)
245 except IOError as ioe:
246 LOG.warning("Fetching from %s resulted in %s",
247 include_url, ioe)
235248
236 if content is not None:249 if content is not None:
237 new_msg = convert_string(content)250 new_msg = convert_string(content)
diff --git a/debian/changelog b/debian/changelog
index bc6c449..6c01326 100644
--- a/debian/changelog
+++ b/debian/changelog
@@ -1,3 +1,29 @@
1cloud-init (17.1-41-g76243487-0ubuntu1~17.04.1) zesty-proposed; urgency=medium
2
3 * cherry-pick 1110f30: debian/cloud-init.templates: Fix capitilazation
4 in 'AliYun' (LP: #1728186)
5 * New upstream snapshot (LP: #1733653)
6 - integration test: replace curtin test ppa with cloud-init test ppa.
7 - EC2: Fix bug using fallback_nic and metadata when restoring from cache.
8 - EC2: Kill dhclient process used in sandbox dhclient.
9 - ntp: fix configuration template rendering for openSUSE and SLES
10 - centos: Provide the failed #include url in error messages
11 - Catch UrlError when #include'ing URLs [Andrew Jorgensen]
12 - hosts: Fix openSUSE and SLES setup for /etc/hosts and clarify docs.
13 [Robert Schweikert]
14 - rh_subscription: Perform null checks for enabled and disabled repos.
15 [Dave Mulford]
16 - Improve warning message when a template is not found.
17 [Robert Schweikert]
18 - Replace the temporary i9n.brickies.net with i9n.cloud-init.io.
19 - Azure: don't generate network configuration for SRIOV devices
20 - tests: address some minor feedback missed in last merge.
21 - tests: integration test cleanup and full pass of nocloud-kvm.
22 - Gentoo: chmod +x on all files in sysvinit/gentoo/
23 [Carlos Konstanski]
24
25 -- Chad Smith <chad.smith@canonical.com> Tue, 21 Nov 2017 11:48:32 -0700
26
1cloud-init (17.1-27-geb292c18-0ubuntu1~17.04.1) zesty-proposed; urgency=medium27cloud-init (17.1-27-geb292c18-0ubuntu1~17.04.1) zesty-proposed; urgency=medium
228
3 * New upstream snapshot.29 * New upstream snapshot.
diff --git a/debian/cloud-init.templates b/debian/cloud-init.templates
index 0a251e3..5ed37f7 100644
--- a/debian/cloud-init.templates
+++ b/debian/cloud-init.templates
@@ -1,8 +1,8 @@
1Template: cloud-init/datasources1Template: cloud-init/datasources
2Type: multiselect2Type: multiselect
3Default: NoCloud, ConfigDrive, OpenNebula, DigitalOcean, Azure, AltCloud, OVF, MAAS, GCE, OpenStack, CloudSigma, SmartOS, Bigstep, Scaleway, Aliyun, Ec2, CloudStack, None3Default: NoCloud, ConfigDrive, OpenNebula, DigitalOcean, Azure, AltCloud, OVF, MAAS, GCE, OpenStack, CloudSigma, SmartOS, Bigstep, Scaleway, AliYun, Ec2, CloudStack, None
4Choices-C: NoCloud, ConfigDrive, OpenNebula, DigitalOcean, Azure, AltCloud, OVF, MAAS, GCE, OpenStack, CloudSigma, SmartOS, Bigstep, Scaleway, Aliyun, Ec2, CloudStack, None4Choices-C: NoCloud, ConfigDrive, OpenNebula, DigitalOcean, Azure, AltCloud, OVF, MAAS, GCE, OpenStack, CloudSigma, SmartOS, Bigstep, Scaleway, AliYun, Ec2, CloudStack, None
5Choices: 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 datasource5Choices: 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
6Description: Which data sources should be searched?6Description: Which data sources should be searched?
7 Cloud-init supports searching different "Data Sources" for information7 Cloud-init supports searching different "Data Sources" for information
8 that it uses to configure a cloud instance.8 that it uses to configure a cloud instance.
diff --git a/sysvinit/gentoo/cloud-config b/sysvinit/gentoo/cloud-config
9old mode 1006449old mode 100644
10new mode 10075510new mode 100755
index 5618472..5618472
--- a/sysvinit/gentoo/cloud-config
+++ b/sysvinit/gentoo/cloud-config
diff --git a/sysvinit/gentoo/cloud-final b/sysvinit/gentoo/cloud-final
11old mode 10064411old mode 100644
12new mode 10075512new mode 100755
index a9bf01f..a9bf01f
--- a/sysvinit/gentoo/cloud-final
+++ b/sysvinit/gentoo/cloud-final
diff --git a/sysvinit/gentoo/cloud-init b/sysvinit/gentoo/cloud-init
13old mode 10064413old mode 100644
14new mode 10075514new mode 100755
index 531a715..531a715
--- a/sysvinit/gentoo/cloud-init
+++ b/sysvinit/gentoo/cloud-init
diff --git a/sysvinit/gentoo/cloud-init-local b/sysvinit/gentoo/cloud-init-local
15old mode 10064415old mode 100644
16new mode 10075516new mode 100755
index 0f8cf65..0f8cf65
--- a/sysvinit/gentoo/cloud-init-local
+++ b/sysvinit/gentoo/cloud-init-local
diff --git a/templates/hosts.opensuse.tmpl b/templates/hosts.opensuse.tmpl
17deleted file mode 10064417deleted file mode 100644
index 655da3f..0000000
--- a/templates/hosts.opensuse.tmpl
+++ /dev/null
@@ -1,26 +0,0 @@
1*
2 This file /etc/cloud/templates/hosts.opensuse.tmpl is only utilized
3 if enabled in cloud-config. Specifically, in order to enable it
4 you need to add the following to config:
5 manage_etc_hosts: True
6*#
7# Your system has configured 'manage_etc_hosts' as True.
8# As a result, if you wish for changes to this file to persist
9# then you will need to either
10# a.) make changes to the master file in
11# /etc/cloud/templates/hosts.opensuse.tmpl
12# b.) change or remove the value of 'manage_etc_hosts' in
13# /etc/cloud/cloud.cfg or cloud-config from user-data
14#
15# The following lines are desirable for IPv4 capable hosts
16127.0.0.1 localhost
17
18# The following lines are desirable for IPv6 capable hosts
19::1 localhost ipv6-localhost ipv6-loopback
20fe00::0 ipv6-localnet
21
22ff00::0 ipv6-mcastprefix
23ff02::1 ipv6-allnodes
24ff02::2 ipv6-allrouters
25ff02::3 ipv6-allhosts
26
diff --git a/templates/hosts.suse.tmpl b/templates/hosts.suse.tmpl
index b608269..8e664db 100644
--- a/templates/hosts.suse.tmpl
+++ b/templates/hosts.suse.tmpl
@@ -13,12 +13,18 @@ you need to add the following to config:
13# /etc/cloud/cloud.cfg or cloud-config from user-data13# /etc/cloud/cloud.cfg or cloud-config from user-data
14#14#
15# The following lines are desirable for IPv4 capable hosts15# The following lines are desirable for IPv4 capable hosts
16127.0.0.1 localhost16127.0.0.1 {{fqdn}} {{hostname}}
17127.0.0.1 localhost.localdomain localhost
18127.0.0.1 localhost4.localdomain4 localhost4
1719
18# The following lines are desirable for IPv6 capable hosts20# The following lines are desirable for IPv6 capable hosts
21::1 {{fqdn}} {{hostname}}
22::1 localhost.localdomain localhost
23::1 localhost6.localdomain6 localhost6
19::1 localhost ipv6-localhost ipv6-loopback24::1 localhost ipv6-localhost ipv6-loopback
20fe00::0 ipv6-localnet
2125
26
27fe00::0 ipv6-localnet
22ff00::0 ipv6-mcastprefix28ff00::0 ipv6-mcastprefix
23ff02::1 ipv6-allnodes29ff02::1 ipv6-allnodes
24ff02::2 ipv6-allrouters30ff02::2 ipv6-allrouters
diff --git a/templates/ntp.conf.opensuse.tmpl b/templates/ntp.conf.opensuse.tmpl
25new file mode 10064431new file mode 100644
index 0000000..f3ab565
--- /dev/null
+++ b/templates/ntp.conf.opensuse.tmpl
@@ -0,0 +1,88 @@
1## template:jinja
2
3##
4## Radio and modem clocks by convention have addresses in the
5## form 127.127.t.u, where t is the clock type and u is a unit
6## number in the range 0-3.
7##
8## Most of these clocks require support in the form of a
9## serial port or special bus peripheral. The particular
10## device is normally specified by adding a soft link
11## /dev/device-u to the particular hardware device involved,
12## where u correspond to the unit number above.
13##
14## Generic DCF77 clock on serial port (Conrad DCF77)
15## Address: 127.127.8.u
16## Serial Port: /dev/refclock-u
17##
18## (create soft link /dev/refclock-0 to the particular ttyS?)
19##
20# server 127.127.8.0 mode 5 prefer
21
22##
23## Undisciplined Local Clock. This is a fake driver intended for backup
24## and when no outside source of synchronized time is available.
25##
26# server 127.127.1.0 # local clock (LCL)
27# fudge 127.127.1.0 stratum 10 # LCL is unsynchronized
28
29##
30## Add external Servers using
31## # rcntpd addserver <yourserver>
32## The servers will only be added to the currently running instance, not
33## to /etc/ntp.conf.
34##
35{% if pools %}# pools
36{% endif %}
37{% for pool in pools -%}
38pool {{pool}} iburst
39{% endfor %}
40{%- if servers %}# servers
41{% endif %}
42{% for server in servers -%}
43server {{server}} iburst
44{% endfor %}
45
46# Access control configuration; see /usr/share/doc/packages/ntp/html/accopt.html for
47# details. The web page <http://support.ntp.org/bin/view/Support/AccessRestrictions>
48# might also be helpful.
49#
50# Note that "restrict" applies to both servers and clients, so a configuration
51# that might be intended to block requests from certain clients could also end
52# up blocking replies from your own upstream servers.
53
54# By default, exchange time with everybody, but don't allow configuration.
55restrict -4 default notrap nomodify nopeer noquery
56restrict -6 default notrap nomodify nopeer noquery
57
58# Local users may interrogate the ntp server more closely.
59restrict 127.0.0.1
60restrict ::1
61
62# Clients from this (example!) subnet have unlimited access, but only if
63# cryptographically authenticated.
64#restrict 192.168.123.0 mask 255.255.255.0 notrust
65
66##
67## Miscellaneous stuff
68##
69
70driftfile /var/lib/ntp/drift/ntp.drift # path for drift file
71
72logfile /var/log/ntp # alternate log file
73# logconfig =syncstatus + sysevents
74# logconfig =all
75
76# statsdir /tmp/ # directory for statistics files
77# filegen peerstats file peerstats type day enable
78# filegen loopstats file loopstats type day enable
79# filegen clockstats file clockstats type day enable
80
81#
82# Authentication stuff
83#
84keys /etc/ntp.keys # path for keys file
85trustedkey 1 # define trusted keys
86requestkey 1 # key (7) for accessing server variables
87controlkey 1 # key (6) for accessing server variables
88
diff --git a/templates/ntp.conf.sles.tmpl b/templates/ntp.conf.sles.tmpl
index 5c5fc4d..f3ab565 100644
--- a/templates/ntp.conf.sles.tmpl
+++ b/templates/ntp.conf.sles.tmpl
@@ -1,17 +1,5 @@
1## template:jinja1## template:jinja
22
3################################################################################
4## /etc/ntp.conf
5##
6## Sample NTP configuration file.
7## See package 'ntp-doc' for documentation, Mini-HOWTO and FAQ.
8## Copyright (c) 1998 S.u.S.E. GmbH Fuerth, Germany.
9##
10## Author: Michael Andres, <ma@suse.de>
11## Michael Skibbe, <mskibbe@suse.de>
12##
13################################################################################
14
15##3##
16## Radio and modem clocks by convention have addresses in the 4## Radio and modem clocks by convention have addresses in the
17## form 127.127.t.u, where t is the clock type and u is a unit 5## form 127.127.t.u, where t is the clock type and u is a unit
diff --git a/tests/cloud_tests/collect.py b/tests/cloud_tests/collect.py
index 4a2422e..71ee764 100644
--- a/tests/cloud_tests/collect.py
+++ b/tests/cloud_tests/collect.py
@@ -22,11 +22,21 @@ def collect_script(instance, base_dir, script, script_name):
22 """22 """
23 LOG.debug('running collect script: %s', script_name)23 LOG.debug('running collect script: %s', script_name)
24 (out, err, exit) = instance.run_script(24 (out, err, exit) = instance.run_script(
25 script, rcs=range(0, 256),25 script.encode(), rcs=False,
26 description='collect: {}'.format(script_name))26 description='collect: {}'.format(script_name))
27 c_util.write_file(os.path.join(base_dir, script_name), out)27 c_util.write_file(os.path.join(base_dir, script_name), out)
2828
2929
30def collect_console(instance, base_dir):
31 LOG.debug('getting console log')
32 try:
33 data = instance.console_log()
34 except NotImplementedError as e:
35 data = 'Not Implemented: %s' % e
36 with open(os.path.join(base_dir, 'console.log'), "wb") as fp:
37 fp.write(data)
38
39
30def collect_test_data(args, snapshot, os_name, test_name):40def collect_test_data(args, snapshot, os_name, test_name):
31 """Collect data for test case.41 """Collect data for test case.
3242
@@ -79,8 +89,12 @@ def collect_test_data(args, snapshot, os_name, test_name):
79 test_output_dir, script, script_name))89 test_output_dir, script, script_name))
80 for script_name, script in test_scripts.items()]90 for script_name, script in test_scripts.items()]
8191
92 console_log = partial(
93 run_single, 'collect console',
94 partial(collect_console, instance, test_output_dir))
95
82 res = run_stage('collect for test: {}'.format(test_name),96 res = run_stage('collect for test: {}'.format(test_name),
83 [start_call] + collect_calls)97 [start_call] + collect_calls + [console_log])
8498
85 return res99 return res
86100
diff --git a/tests/cloud_tests/images/base.py b/tests/cloud_tests/images/base.py
index 0a1e056..d503108 100644
--- a/tests/cloud_tests/images/base.py
+++ b/tests/cloud_tests/images/base.py
@@ -2,8 +2,10 @@
22
3"""Base class for images."""3"""Base class for images."""
44
5from ..util import TargetBase
56
6class Image(object):7
8class Image(TargetBase):
7 """Base class for images."""9 """Base class for images."""
810
9 platform_name = None11 platform_name = None
@@ -43,21 +45,6 @@ class Image(object):
43 # NOTE: more sophisticated options may be requied at some point45 # NOTE: more sophisticated options may be requied at some point
44 return self.config.get('setup_overrides', {})46 return self.config.get('setup_overrides', {})
4547
46 def execute(self, *args, **kwargs):
47 """Execute command in image, modifying image."""
48 raise NotImplementedError
49
50 def push_file(self, local_path, remote_path):
51 """Copy file at 'local_path' to instance at 'remote_path'."""
52 raise NotImplementedError
53
54 def run_script(self, *args, **kwargs):
55 """Run script in image, modifying image.
56
57 @return_value: script output
58 """
59 raise NotImplementedError
60
61 def snapshot(self):48 def snapshot(self):
62 """Create snapshot of image, block until done."""49 """Create snapshot of image, block until done."""
63 raise NotImplementedError50 raise NotImplementedError
diff --git a/tests/cloud_tests/images/lxd.py b/tests/cloud_tests/images/lxd.py
index fd4e93c..5caeba4 100644
--- a/tests/cloud_tests/images/lxd.py
+++ b/tests/cloud_tests/images/lxd.py
@@ -24,7 +24,7 @@ class LXDImage(base.Image):
24 @param config: image configuration24 @param config: image configuration
25 """25 """
26 self.modified = False26 self.modified = False
27 self._instance = None27 self._img_instance = None
28 self._pylxd_image = None28 self._pylxd_image = None
29 self.pylxd_image = pylxd_image29 self.pylxd_image = pylxd_image
30 super(LXDImage, self).__init__(platform, config)30 super(LXDImage, self).__init__(platform, config)
@@ -38,9 +38,9 @@ class LXDImage(base.Image):
3838
39 @pylxd_image.setter39 @pylxd_image.setter
40 def pylxd_image(self, pylxd_image):40 def pylxd_image(self, pylxd_image):
41 if self._instance:41 if self._img_instance:
42 self._instance.destroy()42 self._instance.destroy()
43 self._instance = None43 self._img_instance = None
44 if (self._pylxd_image and44 if (self._pylxd_image and
45 (self._pylxd_image is not pylxd_image) and45 (self._pylxd_image is not pylxd_image) and
46 (not self.config.get('cache_base_image') or self.modified)):46 (not self.config.get('cache_base_image') or self.modified)):
@@ -49,15 +49,19 @@ class LXDImage(base.Image):
49 self._pylxd_image = pylxd_image49 self._pylxd_image = pylxd_image
5050
51 @property51 @property
52 def instance(self):52 def _instance(self):
53 """Property function."""53 """Internal use only, returns a instance
54 if not self._instance:54
55 self._instance = self.platform.launch_container(55 This starts an lxc instance from the image, so it is "dirty".
56 Better would be some way to modify this "at rest".
57 lxc-pstart would be an option."""
58 if not self._img_instance:
59 self._img_instance = self.platform.launch_container(
56 self.properties, self.config, self.features,60 self.properties, self.config, self.features,
57 use_desc='image-modification', image_desc=str(self),61 use_desc='image-modification', image_desc=str(self),
58 image=self.pylxd_image.fingerprint)62 image=self.pylxd_image.fingerprint)
59 self._instance.start()63 self._img_instance.start()
60 return self._instance64 return self._img_instance
6165
62 @property66 @property
63 def properties(self):67 def properties(self):
@@ -144,20 +148,20 @@ class LXDImage(base.Image):
144 shutil.rmtree(export_dir)148 shutil.rmtree(export_dir)
145 shutil.rmtree(extract_dir)149 shutil.rmtree(extract_dir)
146150
147 def execute(self, *args, **kwargs):151 def _execute(self, *args, **kwargs):
148 """Execute command in image, modifying image."""152 """Execute command in image, modifying image."""
149 return self.instance.execute(*args, **kwargs)153 return self._instance._execute(*args, **kwargs)
150154
151 def push_file(self, local_path, remote_path):155 def push_file(self, local_path, remote_path):
152 """Copy file at 'local_path' to instance at 'remote_path'."""156 """Copy file at 'local_path' to instance at 'remote_path'."""
153 return self.instance.push_file(local_path, remote_path)157 return self._instance.push_file(local_path, remote_path)
154158
155 def run_script(self, *args, **kwargs):159 def run_script(self, *args, **kwargs):
156 """Run script in image, modifying image.160 """Run script in image, modifying image.
157161
158 @return_value: script output162 @return_value: script output
159 """163 """
160 return self.instance.run_script(*args, **kwargs)164 return self._instance.run_script(*args, **kwargs)
161165
162 def snapshot(self):166 def snapshot(self):
163 """Create snapshot of image, block until done."""167 """Create snapshot of image, block until done."""
@@ -169,7 +173,7 @@ class LXDImage(base.Image):
169 # clone current instance173 # clone current instance
170 instance = self.platform.launch_container(174 instance = self.platform.launch_container(
171 self.properties, self.config, self.features,175 self.properties, self.config, self.features,
172 container=self.instance.name, image_desc=str(self),176 container=self._instance.name, image_desc=str(self),
173 use_desc='snapshot', container_config=conf)177 use_desc='snapshot', container_config=conf)
174 # wait for cloud-init before boot_clean_script is run to ensure178 # wait for cloud-init before boot_clean_script is run to ensure
175 # /var/lib/cloud is removed cleanly179 # /var/lib/cloud is removed cleanly
diff --git a/tests/cloud_tests/images/nocloudkvm.py b/tests/cloud_tests/images/nocloudkvm.py
index a7af0e5..1e7962c 100644
--- a/tests/cloud_tests/images/nocloudkvm.py
+++ b/tests/cloud_tests/images/nocloudkvm.py
@@ -2,6 +2,8 @@
22
3"""NoCloud KVM Image Base Class."""3"""NoCloud KVM Image Base Class."""
44
5from cloudinit import util as c_util
6
5from tests.cloud_tests.images import base7from tests.cloud_tests.images import base
6from tests.cloud_tests.snapshots import nocloudkvm as nocloud_kvm_snapshot8from tests.cloud_tests.snapshots import nocloudkvm as nocloud_kvm_snapshot
79
@@ -19,24 +21,11 @@ class NoCloudKVMImage(base.Image):
19 @param img_path: path to the image21 @param img_path: path to the image
20 """22 """
21 self.modified = False23 self.modified = False
22 self._instance = None
23 self._img_path = img_path24 self._img_path = img_path
2425
25 super(NoCloudKVMImage, self).__init__(platform, config)26 super(NoCloudKVMImage, self).__init__(platform, config)
2627
27 @property28 @property
28 def instance(self):
29 """Returns an instance of an image."""
30 if not self._instance:
31 if not self._img_path:
32 raise RuntimeError()
33
34 self._instance = self.platform.create_image(
35 self.properties, self.config, self.features, self._img_path,
36 image_desc=str(self), use_desc='image-modification')
37 return self._instance
38
39 @property
40 def properties(self):29 def properties(self):
41 """Dictionary containing: 'arch', 'os', 'version', 'release'."""30 """Dictionary containing: 'arch', 'os', 'version', 'release'."""
42 return {31 return {
@@ -46,20 +35,26 @@ class NoCloudKVMImage(base.Image):
46 'version': self.config['version'],35 'version': self.config['version'],
47 }36 }
4837
49 def execute(self, *args, **kwargs):38 def _execute(self, command, stdin=None, env=None):
50 """Execute command in image, modifying image."""39 """Execute command in image, modifying image."""
51 return self.instance.execute(*args, **kwargs)40 return self.mount_image_callback(command, stdin=stdin, env=env)
5241
53 def push_file(self, local_path, remote_path):42 def mount_image_callback(self, command, stdin=None, env=None):
54 """Copy file at 'local_path' to instance at 'remote_path'."""43 """Run mount-image-callback."""
55 return self.instance.push_file(local_path, remote_path)
5644
57 def run_script(self, *args, **kwargs):45 env_args = []
58 """Run script in image, modifying image.46 if env:
47 env_args = ['env'] + ["%s=%s" for k, v in env.items()]
5948
60 @return_value: script output49 mic_chroot = ['sudo', 'mount-image-callback', '--system-mounts',
61 """50 '--system-resolvconf', self._img_path,
62 return self.instance.run_script(*args, **kwargs)51 '--', 'chroot', '_MOUNTPOINT_']
52 try:
53 out, err = c_util.subp(mic_chroot + env_args + list(command),
54 data=stdin, decode=False)
55 return (out, err, 0)
56 except c_util.ProcessExecutionError as e:
57 return (e.stdout, e.stderr, e.exit_code)
6358
64 def snapshot(self):59 def snapshot(self):
65 """Create snapshot of image, block until done."""60 """Create snapshot of image, block until done."""
@@ -82,7 +77,6 @@ class NoCloudKVMImage(base.Image):
82 framework decide whether to keep or destroy everything.77 framework decide whether to keep or destroy everything.
83 """78 """
84 self._img_path = None79 self._img_path = None
85 self._instance.destroy()
86 super(NoCloudKVMImage, self).destroy()80 super(NoCloudKVMImage, self).destroy()
8781
88# vi: ts=4 expandtab82# vi: ts=4 expandtab
diff --git a/tests/cloud_tests/instances/base.py b/tests/cloud_tests/instances/base.py
index 9bdda60..8c59d62 100644
--- a/tests/cloud_tests/instances/base.py
+++ b/tests/cloud_tests/instances/base.py
@@ -2,8 +2,10 @@
22
3"""Base instance."""3"""Base instance."""
44
5from ..util import TargetBase
56
6class Instance(object):7
8class Instance(TargetBase):
7 """Base instance object."""9 """Base instance object."""
810
9 platform_name = None11 platform_name = None
@@ -22,82 +24,7 @@ class Instance(object):
22 self.properties = properties24 self.properties = properties
23 self.config = config25 self.config = config
24 self.features = features26 self.features = features
2527 self._tmp_count = 0
26 def execute(self, command, stdout=None, stderr=None, env=None,
27 rcs=None, description=None):
28 """Execute command in instance, recording output, error and exit code.
29
30 Assumes functional networking and execution as root with the
31 target filesystem being available at /.
32
33 @param command: the command to execute as root inside the image
34 if command is a string, then it will be executed as:
35 ['sh', '-c', command]
36 @param stdout, stderr: file handles to write output and error to
37 @param env: environment variables
38 @param rcs: allowed return codes from command
39 @param description: purpose of command
40 @return_value: tuple containing stdout data, stderr data, exit code
41 """
42 raise NotImplementedError
43
44 def read_data(self, remote_path, decode=False):
45 """Read data from instance filesystem.
46
47 @param remote_path: path in instance
48 @param decode: return as string
49 @return_value: data as str or bytes
50 """
51 raise NotImplementedError
52
53 def write_data(self, remote_path, data):
54 """Write data to instance filesystem.
55
56 @param remote_path: path in instance
57 @param data: data to write, either str or bytes
58 """
59 raise NotImplementedError
60
61 def pull_file(self, remote_path, local_path):
62 """Copy file at 'remote_path', from instance to 'local_path'.
63
64 @param remote_path: path on remote instance
65 @param local_path: path on local instance
66 """
67 with open(local_path, 'wb') as fp:
68 fp.write(self.read_data(remote_path))
69
70 def push_file(self, local_path, remote_path):
71 """Copy file at 'local_path' to instance at 'remote_path'.
72
73 @param local_path: path on local instance
74 @param remote_path: path on remote instance
75 """
76 with open(local_path, 'rb') as fp:
77 self.write_data(remote_path, fp.read())
78
79 def run_script(self, script, rcs=None, description=None):
80 """Run script in target and return stdout.
81
82 @param script: script contents
83 @param rcs: allowed return codes from script
84 @param description: purpose of script
85 @return_value: stdout from script
86 """
87 script_path = self.tmpfile()
88 try:
89 self.write_data(script_path, script)
90 return self.execute(
91 ['/bin/bash', script_path], rcs=rcs, description=description)
92 finally:
93 self.execute(['rm', '-f', script_path], rcs=rcs)
94
95 def tmpfile(self):
96 """Get a tmp file in the target.
97
98 @return_value: path to new file in target
99 """
100 return self.execute(['mktemp'])[0].strip()
10128
102 def console_log(self):29 def console_log(self):
103 """Instance console.30 """Instance console.
diff --git a/tests/cloud_tests/instances/lxd.py b/tests/cloud_tests/instances/lxd.py
index a43918c..3b035d8 100644
--- a/tests/cloud_tests/instances/lxd.py
+++ b/tests/cloud_tests/instances/lxd.py
@@ -2,8 +2,11 @@
22
3"""Base LXD instance."""3"""Base LXD instance."""
44
5from tests.cloud_tests.instances import base5from . import base
6from tests.cloud_tests import util6
7import os
8import shutil
9from tempfile import mkdtemp
710
811
9class LXDInstance(base.Instance):12class LXDInstance(base.Instance):
@@ -24,6 +27,8 @@ class LXDInstance(base.Instance):
24 self._pylxd_container = pylxd_container27 self._pylxd_container = pylxd_container
25 super(LXDInstance, self).__init__(28 super(LXDInstance, self).__init__(
26 platform, name, properties, config, features)29 platform, name, properties, config, features)
30 self.tmpd = mkdtemp(prefix="%s-%s" % (type(self).__name__, name))
31 self._setup_console_log()
2732
28 @property33 @property
29 def pylxd_container(self):34 def pylxd_container(self):
@@ -31,74 +36,69 @@ class LXDInstance(base.Instance):
31 self._pylxd_container.sync()36 self._pylxd_container.sync()
32 return self._pylxd_container37 return self._pylxd_container
3338
34 def execute(self, command, stdout=None, stderr=None, env=None,39 def _setup_console_log(self):
35 rcs=None, description=None):40 logf = os.path.join(self.tmpd, "console.log")
36 """Execute command in instance, recording output, error and exit code.41
3742 # doing this ensures we can read it. Otherwise it ends up root:root.
38 Assumes functional networking and execution as root with the43 with open(logf, "w") as fp:
39 target filesystem being available at /.44 fp.write("# %s\n" % self.name)
4045
41 @param command: the command to execute as root inside the image46 cfg = "lxc.console.logfile=%s" % logf
42 if command is a string, then it will be executed as:47 orig = self._pylxd_container.config.get('raw.lxc', "")
43 ['sh', '-c', command]48 if orig:
44 @param stdout: file handler to write output49 orig += "\n"
45 @param stderr: file handler to write error50 self._pylxd_container.config['raw.lxc'] = orig + cfg
46 @param env: environment variables51 self._pylxd_container.save()
47 @param rcs: allowed return codes from command52 self._console_log_file = logf
48 @param description: purpose of command53
49 @return_value: tuple containing stdout data, stderr data, exit code54 def _execute(self, command, stdin=None, env=None):
50 """
51 if env is None:55 if env is None:
52 env = {}56 env = {}
5357
54 if isinstance(command, str):58 if stdin is not None:
55 command = ['sh', '-c', command]59 # pylxd does not support input to execute.
60 # https://github.com/lxc/pylxd/issues/244
61 #
62 # The solution here is write a tmp file in the container
63 # and then execute a shell that sets it standard in to
64 # be from that file, removes it, and calls the comand.
65 tmpf = self.tmpfile()
66 self.write_data(tmpf, stdin)
67 ncmd = 'exec <"{tmpf}"; rm -f "{tmpf}"; exec "$@"'
68 command = (['sh', '-c', ncmd.format(tmpf=tmpf), 'stdinhack'] +
69 list(command))
5670
57 # ensure instance is running and execute the command71 # ensure instance is running and execute the command
58 self.start()72 self.start()
73 # execute returns a ContainerExecuteResult, named tuple
74 # (exit_code, stdout, stderr)
59 res = self.pylxd_container.execute(command, environment=env)75 res = self.pylxd_container.execute(command, environment=env)
6076
61 # get out, exit and err from pylxd return77 # get out, exit and err from pylxd return
62 if hasattr(res, 'exit_code'):78 if not hasattr(res, 'exit_code'):
63 # pylxd 2.2 returns ContainerExecuteResult, named tuple of
64 # (exit_code, out, err)
65 (exit, out, err) = res
66 else:
67 # pylxd 2.1.3 and earlier only return out and err, no exit79 # pylxd 2.1.3 and earlier only return out and err, no exit
68 # LOG.warning('using pylxd version < 2.2')80 raise RuntimeError(
69 (out, err) = res81 "No 'exit_code' in pylxd.container.execute return.\n"
70 exit = 082 "pylxd > 2.2 is required.")
71
72 # write data to file descriptors if needed
73 if stdout:
74 stdout.write(out)
75 if stderr:
76 stderr.write(err)
77
78 # if the command exited with a code not allowed in rcs, then fail
79 if exit not in (rcs if rcs else (0,)):
80 error_desc = ('Failed command to: {}'.format(description)
81 if description else None)
82 raise util.InTargetExecuteError(
83 out, err, exit, command, self.name, error_desc)
8483
85 return (out, err, exit)84 return res.stdout, res.stderr, res.exit_code
8685
87 def read_data(self, remote_path, decode=False):86 def read_data(self, remote_path, decode=False):
88 """Read data from instance filesystem.87 """Read data from instance filesystem.
8988
90 @param remote_path: path in instance89 @param remote_path: path in instance
91 @param decode: return as string90 @param decode: decode data before returning.
92 @return_value: data as str or bytes91 @return_value: content of remote_path as bytes if 'decode' is False,
92 and as string if 'decode' is True.
93 """93 """
94 data = self.pylxd_container.files.get(remote_path)94 data = self.pylxd_container.files.get(remote_path)
95 return data.decode() if decode and isinstance(data, bytes) else data95 return data.decode() if decode else data
9696
97 def write_data(self, remote_path, data):97 def write_data(self, remote_path, data):
98 """Write data to instance filesystem.98 """Write data to instance filesystem.
9999
100 @param remote_path: path in instance100 @param remote_path: path in instance
101 @param data: data to write, either str or bytes101 @param data: data to write in bytes
102 """102 """
103 self.pylxd_container.files.put(remote_path, data)103 self.pylxd_container.files.put(remote_path, data)
104104
@@ -107,7 +107,14 @@ class LXDInstance(base.Instance):
107107
108 @return_value: bytes of this instance’s console108 @return_value: bytes of this instance’s console
109 """109 """
110 raise NotImplementedError110 if not os.path.exists(self._console_log_file):
111 raise NotImplementedError(
112 "Console log '%s' does not exist. If this is a remote "
113 "lxc, then this is really NotImplementedError. If it is "
114 "A local lxc, then this is a RuntimeError."
115 "https://github.com/lxc/lxd/issues/1129")
116 with open(self._console_log_file, "rb") as fp:
117 return fp.read()
111118
112 def reboot(self, wait=True):119 def reboot(self, wait=True):
113 """Reboot instance."""120 """Reboot instance."""
@@ -144,6 +151,7 @@ class LXDInstance(base.Instance):
144 if self.platform.container_exists(self.name):151 if self.platform.container_exists(self.name):
145 raise OSError('container {} was not properly removed'152 raise OSError('container {} was not properly removed'
146 .format(self.name))153 .format(self.name))
154 shutil.rmtree(self.tmpd)
147 super(LXDInstance, self).destroy()155 super(LXDInstance, self).destroy()
148156
149# vi: ts=4 expandtab157# vi: ts=4 expandtab
diff --git a/tests/cloud_tests/instances/nocloudkvm.py b/tests/cloud_tests/instances/nocloudkvm.py
index 8a0e531..cc82580 100644
--- a/tests/cloud_tests/instances/nocloudkvm.py
+++ b/tests/cloud_tests/instances/nocloudkvm.py
@@ -12,11 +12,18 @@ from cloudinit import util as c_util
12from tests.cloud_tests.instances import base12from tests.cloud_tests.instances import base
13from tests.cloud_tests import util13from tests.cloud_tests import util
1414
15# This domain contains reverse lookups for hostnames that are used.
16# The primary reason is so sudo will return quickly when it attempts
17# to look up the hostname. i9n is just short for 'integration'.
18# see also bug 1730744 for why we had to do this.
19CI_DOMAIN = "i9n.cloud-init.io"
20
1521
16class NoCloudKVMInstance(base.Instance):22class NoCloudKVMInstance(base.Instance):
17 """NoCloud KVM backed instance."""23 """NoCloud KVM backed instance."""
1824
19 platform_name = "nocloud-kvm"25 platform_name = "nocloud-kvm"
26 _ssh_client = None
2027
21 def __init__(self, platform, name, properties, config, features,28 def __init__(self, platform, name, properties, config, features,
22 user_data, meta_data):29 user_data, meta_data):
@@ -35,6 +42,7 @@ class NoCloudKVMInstance(base.Instance):
35 self.ssh_port = None42 self.ssh_port = None
36 self.pid = None43 self.pid = None
37 self.pid_file = None44 self.pid_file = None
45 self.console_file = None
3846
39 super(NoCloudKVMInstance, self).__init__(47 super(NoCloudKVMInstance, self).__init__(
40 platform, name, properties, config, features)48 platform, name, properties, config, features)
@@ -51,43 +59,18 @@ class NoCloudKVMInstance(base.Instance):
51 os.remove(self.pid_file)59 os.remove(self.pid_file)
5260
53 self.pid = None61 self.pid = None
54 super(NoCloudKVMInstance, self).destroy()62 if self._ssh_client:
5563 self._ssh_client.close()
56 def execute(self, command, stdout=None, stderr=None, env=None,64 self._ssh_client = None
57 rcs=None, description=None):
58 """Execute command in instance.
59
60 Assumes functional networking and execution as root with the
61 target filesystem being available at /.
62
63 @param command: the command to execute as root inside the image
64 if command is a string, then it will be executed as:
65 ['sh', '-c', command]
66 @param stdout, stderr: file handles to write output and error to
67 @param env: environment variables
68 @param rcs: allowed return codes from command
69 @param description: purpose of command
70 @return_value: tuple containing stdout data, stderr data, exit code
71 """
72 if env is None:
73 env = {}
74
75 if isinstance(command, str):
76 command = ['sh', '-c', command]
7765
78 if self.pid:66 super(NoCloudKVMInstance, self).destroy()
79 return self.ssh(command)
80 else:
81 return self.mount_image_callback(command) + (0,)
8267
83 def mount_image_callback(self, cmd):68 def _execute(self, command, stdin=None, env=None):
84 """Run mount-image-callback."""69 env_args = []
85 out, err = c_util.subp(['sudo', 'mount-image-callback',70 if env:
86 '--system-mounts', '--system-resolvconf',71 env_args = ['env'] + ["%s=%s" for k, v in env.items()]
87 self.name, '--', 'chroot',
88 '_MOUNTPOINT_'] + cmd)
8972
90 return out, err73 return self.ssh(['sudo'] + env_args + list(command), stdin=stdin)
9174
92 def generate_seed(self, tmpdir):75 def generate_seed(self, tmpdir):
93 """Generate nocloud seed from user-data"""76 """Generate nocloud seed from user-data"""
@@ -109,57 +92,31 @@ class NoCloudKVMInstance(base.Instance):
109 s.close()92 s.close()
110 return num93 return num
11194
112 def push_file(self, local_path, remote_path):95 def ssh(self, command, stdin=None):
113 """Copy file at 'local_path' to instance at 'remote_path'.
114
115 If we have a pid then SSH is up, otherwise, use
116 mount-image-callback.
117
118 @param local_path: path on local instance
119 @param remote_path: path on remote instance
120 """
121 if self.pid:
122 super(NoCloudKVMInstance, self).push_file()
123 else:
124 local_file = open(local_path)
125 p = subprocess.Popen(['sudo', 'mount-image-callback',
126 '--system-mounts', '--system-resolvconf',
127 self.name, '--', 'chroot', '_MOUNTPOINT_',
128 '/bin/sh', '-c', 'cat - > %s' % remote_path],
129 stdin=local_file,
130 stdout=subprocess.PIPE,
131 stderr=subprocess.PIPE)
132 p.wait()
133
134 def sftp_put(self, path, data):
135 """SFTP put a file."""
136 client = self._ssh_connect()
137 sftp = client.open_sftp()
138
139 with sftp.open(path, 'w') as f:
140 f.write(data)
141
142 client.close()
143
144 def ssh(self, command):
145 """Run a command via SSH."""96 """Run a command via SSH."""
146 client = self._ssh_connect()97 client = self._ssh_connect()
14798
99 cmd = util.shell_pack(command)
148 try:100 try:
149 _, out, err = client.exec_command(util.shell_pack(command))101 fp_in, fp_out, fp_err = client.exec_command(cmd)
150 except paramiko.SSHException:102 channel = fp_in.channel
151 raise util.InTargetExecuteError('', '', -1, command, self.name)103 if stdin is not None:
152104 fp_in.write(stdin)
153 exit = out.channel.recv_exit_status()105 fp_in.close()
154 out = ''.join(out.readlines())106
155 err = ''.join(err.readlines())107 channel.shutdown_write()
156 client.close()108 rc = channel.recv_exit_status()
157109 return (fp_out.read(), fp_err.read(), rc)
158 return out, err, exit110 except paramiko.SSHException as e:
111 raise util.InTargetExecuteError(
112 b'', b'', -1, command, self.name, reason=e)
159113
160 def _ssh_connect(self, hostname='localhost', username='ubuntu',114 def _ssh_connect(self, hostname='localhost', username='ubuntu',
161 banner_timeout=120, retry_attempts=30):115 banner_timeout=120, retry_attempts=30):
162 """Connect via SSH."""116 """Connect via SSH."""
117 if self._ssh_client:
118 return self._ssh_client
119
163 private_key = paramiko.RSAKey.from_private_key_file(self.ssh_key_file)120 private_key = paramiko.RSAKey.from_private_key_file(self.ssh_key_file)
164 client = paramiko.SSHClient()121 client = paramiko.SSHClient()
165 client.set_missing_host_key_policy(paramiko.AutoAddPolicy())122 client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
@@ -168,6 +125,7 @@ class NoCloudKVMInstance(base.Instance):
168 client.connect(hostname=hostname, username=username,125 client.connect(hostname=hostname, username=username,
169 port=self.ssh_port, pkey=private_key,126 port=self.ssh_port, pkey=private_key,
170 banner_timeout=banner_timeout)127 banner_timeout=banner_timeout)
128 self._ssh_client = client
171 return client129 return client
172 except (paramiko.SSHException, TypeError):130 except (paramiko.SSHException, TypeError):
173 time.sleep(1)131 time.sleep(1)
@@ -183,15 +141,19 @@ class NoCloudKVMInstance(base.Instance):
183 tmpdir = self.platform.config['data_dir']141 tmpdir = self.platform.config['data_dir']
184 seed = self.generate_seed(tmpdir)142 seed = self.generate_seed(tmpdir)
185 self.pid_file = os.path.join(tmpdir, '%s.pid' % self.name)143 self.pid_file = os.path.join(tmpdir, '%s.pid' % self.name)
144 self.console_file = os.path.join(tmpdir, '%s-console.log' % self.name)
186 self.ssh_port = self.get_free_port()145 self.ssh_port = self.get_free_port()
187146
188 subprocess.Popen(['./tools/xkvm',147 cmd = ['./tools/xkvm',
189 '--disk', '%s,cache=unsafe' % self.name,148 '--disk', '%s,cache=unsafe' % self.name,
190 '--disk', '%s,cache=unsafe' % seed,149 '--disk', '%s,cache=unsafe' % seed,
191 '--netdev',150 '--netdev', ','.join(['user',
192 'user,hostfwd=tcp::%s-:22' % self.ssh_port,151 'hostfwd=tcp::%s-:22' % self.ssh_port,
193 '--', '-pidfile', self.pid_file, '-vnc', 'none',152 'dnssearch=%s' % CI_DOMAIN]),
194 '-m', '2G', '-smp', '2'],153 '--', '-pidfile', self.pid_file, '-vnc', 'none',
154 '-m', '2G', '-smp', '2', '-nographic',
155 '-serial', 'file:' + self.console_file]
156 subprocess.Popen(cmd,
195 close_fds=True,157 close_fds=True,
196 stdin=subprocess.PIPE,158 stdin=subprocess.PIPE,
197 stdout=subprocess.PIPE,159 stdout=subprocess.PIPE,
@@ -206,12 +168,10 @@ class NoCloudKVMInstance(base.Instance):
206 if wait:168 if wait:
207 self._wait_for_system(wait_for_cloud_init)169 self._wait_for_system(wait_for_cloud_init)
208170
209 def write_data(self, remote_path, data):171 def console_log(self):
210 """Write data to instance filesystem.172 if not self.console_file:
211173 return b''
212 @param remote_path: path in instance174 with open(self.console_file, "rb") as fp:
213 @param data: data to write, either str or bytes175 return fp.read()
214 """
215 self.sftp_put(remote_path, data)
216176
217# vi: ts=4 expandtab177# vi: ts=4 expandtab
diff --git a/tests/cloud_tests/testcases/examples/run_commands.yaml b/tests/cloud_tests/testcases/examples/run_commands.yaml
index b0e311b..f80eb8c 100644
--- a/tests/cloud_tests/testcases/examples/run_commands.yaml
+++ b/tests/cloud_tests/testcases/examples/run_commands.yaml
@@ -7,10 +7,10 @@ enabled: False
7cloud_config: |7cloud_config: |
8 #cloud-config8 #cloud-config
9 runcmd:9 runcmd:
10 - echo cloud-init run cmd test > /tmp/run_cmd10 - echo cloud-init run cmd test > /var/tmp/run_cmd
11collect_scripts:11collect_scripts:
12 run_cmd: |12 run_cmd: |
13 #!/bin/bash13 #!/bin/bash
14 cat /tmp/run_cmd14 cat /var/tmp/run_cmd
1515
16# vi: ts=4 expandtab16# vi: ts=4 expandtab
diff --git a/tests/cloud_tests/testcases/modules/apt_configure_sources_ppa.py b/tests/cloud_tests/testcases/modules/apt_configure_sources_ppa.py
index d299e9a..dfbdead 100644
--- a/tests/cloud_tests/testcases/modules/apt_configure_sources_ppa.py
+++ b/tests/cloud_tests/testcases/modules/apt_configure_sources_ppa.py
@@ -11,13 +11,13 @@ class TestAptconfigureSourcesPPA(base.CloudTestCase):
11 """Test specific ppa added."""11 """Test specific ppa added."""
12 out = self.get_data_file('sources.list')12 out = self.get_data_file('sources.list')
13 self.assertIn(13 self.assertIn(
14 'http://ppa.launchpad.net/curtin-dev/test-archive/ubuntu', out)14 'http://ppa.launchpad.net/cloud-init-dev/test-archive/ubuntu', out)
1515
16 def test_ppa_key(self):16 def test_ppa_key(self):
17 """Test ppa key added."""17 """Test ppa key added."""
18 out = self.get_data_file('apt-key')18 out = self.get_data_file('apt-key')
19 self.assertIn(19 self.assertIn(
20 '1BC3 0F71 5A3B 8612 47A8 1A5E 55FE 7C8C 0165 013E', out)20 '1FF0 D853 5EF7 E719 E5C8 1B9C 083D 06FB E4D3 04DF', out)
21 self.assertIn('Launchpad PPA for curtin developers', out)21 self.assertIn('Launchpad PPA for cloud init development team', out)
2222
23# vi: ts=4 expandtab23# vi: ts=4 expandtab
diff --git a/tests/cloud_tests/testcases/modules/apt_configure_sources_ppa.yaml b/tests/cloud_tests/testcases/modules/apt_configure_sources_ppa.yaml
index 9efdae5..b997bcf 100644
--- a/tests/cloud_tests/testcases/modules/apt_configure_sources_ppa.yaml
+++ b/tests/cloud_tests/testcases/modules/apt_configure_sources_ppa.yaml
@@ -2,7 +2,7 @@
2# Add a PPA to source.list2# Add a PPA to source.list
3#3#
4# NOTE: on older ubuntu releases the sources file added is named4# NOTE: on older ubuntu releases the sources file added is named
5# 'curtin-dev-test-archive-trusty', without 'ubuntu' in the middle5# 'cloud-init-dev-test-archive-trusty', without 'ubuntu' in the middle
6required_features:6required_features:
7 - apt7 - apt
8 - ppa8 - ppa
@@ -14,11 +14,11 @@ cloud_config: |
14 source1:14 source1:
15 keyid: 0165013E15 keyid: 0165013E
16 keyserver: keyserver.ubuntu.com16 keyserver: keyserver.ubuntu.com
17 source: "ppa:curtin-dev/test-archive"17 source: "ppa:cloud-init-dev/test-archive"
18collect_scripts:18collect_scripts:
19 sources.list: |19 sources.list: |
20 #!/bin/bash20 #!/bin/bash
21 cat /etc/apt/sources.list.d/curtin-dev-ubuntu-test-archive-*.list21 cat /etc/apt/sources.list.d/cloud-init-dev-ubuntu-test-archive-*.list
22 apt-key: |22 apt-key: |
23 #!/bin/bash23 #!/bin/bash
24 apt-key finger24 apt-key finger
diff --git a/tests/cloud_tests/testcases/modules/keys_to_console.py b/tests/cloud_tests/testcases/modules/keys_to_console.py
index 88b6812..07f3811 100644
--- a/tests/cloud_tests/testcases/modules/keys_to_console.py
+++ b/tests/cloud_tests/testcases/modules/keys_to_console.py
@@ -10,13 +10,13 @@ class TestKeysToConsole(base.CloudTestCase):
10 def test_excluded_keys(self):10 def test_excluded_keys(self):
11 """Test excluded keys missing."""11 """Test excluded keys missing."""
12 out = self.get_data_file('syslog')12 out = self.get_data_file('syslog')
13 self.assertNotIn('DSA', out)13 self.assertNotIn('(DSA)', out)
14 self.assertNotIn('ECDSA', out)14 self.assertNotIn('(ECDSA)', out)
1515
16 def test_expected_keys(self):16 def test_expected_keys(self):
17 """Test expected keys exist."""17 """Test expected keys exist."""
18 out = self.get_data_file('syslog')18 out = self.get_data_file('syslog')
19 self.assertIn('ED25519', out)19 self.assertIn('(ED25519)', out)
20 self.assertIn('RSA', out)20 self.assertIn('(RSA)', out)
2121
22# vi: ts=4 expandtab22# vi: ts=4 expandtab
diff --git a/tests/cloud_tests/testcases/modules/runcmd.yaml b/tests/cloud_tests/testcases/modules/runcmd.yaml
index 04e5a05..8309a88 100644
--- a/tests/cloud_tests/testcases/modules/runcmd.yaml
+++ b/tests/cloud_tests/testcases/modules/runcmd.yaml
@@ -4,10 +4,10 @@
4cloud_config: |4cloud_config: |
5 #cloud-config5 #cloud-config
6 runcmd:6 runcmd:
7 - echo cloud-init run cmd test > /tmp/run_cmd7 - echo cloud-init run cmd test > /var/tmp/run_cmd
8collect_scripts:8collect_scripts:
9 run_cmd: |9 run_cmd: |
10 #!/bin/bash10 #!/bin/bash
11 cat /tmp/run_cmd11 cat /var/tmp/run_cmd
1212
13# vi: ts=4 expandtab13# vi: ts=4 expandtab
diff --git a/tests/cloud_tests/testcases/modules/set_hostname.py b/tests/cloud_tests/testcases/modules/set_hostname.py
index 6e96a75..1dbe64c 100644
--- a/tests/cloud_tests/testcases/modules/set_hostname.py
+++ b/tests/cloud_tests/testcases/modules/set_hostname.py
@@ -7,9 +7,11 @@ from tests.cloud_tests.testcases import base
7class TestHostname(base.CloudTestCase):7class TestHostname(base.CloudTestCase):
8 """Test hostname module."""8 """Test hostname module."""
99
10 ex_hostname = "cloudinit2"
11
10 def test_hostname(self):12 def test_hostname(self):
11 """Test hostname command shows correct output."""13 """Test hostname command shows correct output."""
12 out = self.get_data_file('hostname')14 out = self.get_data_file('hostname')
13 self.assertIn('myhostname', out)15 self.assertIn(self.ex_hostname, out)
1416
15# vi: ts=4 expandtab17# vi: ts=4 expandtab
diff --git a/tests/cloud_tests/testcases/modules/set_hostname.yaml b/tests/cloud_tests/testcases/modules/set_hostname.yaml
index c96344c..071fb22 100644
--- a/tests/cloud_tests/testcases/modules/set_hostname.yaml
+++ b/tests/cloud_tests/testcases/modules/set_hostname.yaml
@@ -5,7 +5,8 @@ required_features:
5 - hostname5 - hostname
6cloud_config: |6cloud_config: |
7 #cloud-config7 #cloud-config
8 hostname: myhostname8 hostname: cloudinit2
9
9collect_scripts:10collect_scripts:
10 hosts: |11 hosts: |
11 #!/bin/bash12 #!/bin/bash
diff --git a/tests/cloud_tests/testcases/modules/set_hostname_fqdn.py b/tests/cloud_tests/testcases/modules/set_hostname_fqdn.py
index 398f3d4..eb6f065 100644
--- a/tests/cloud_tests/testcases/modules/set_hostname_fqdn.py
+++ b/tests/cloud_tests/testcases/modules/set_hostname_fqdn.py
@@ -1,26 +1,31 @@
1# This file is part of cloud-init. See LICENSE file for license information.1# This file is part of cloud-init. See LICENSE file for license information.
22
3"""cloud-init Integration Test Verify Script."""3"""cloud-init Integration Test Verify Script."""
4from tests.cloud_tests.instances.nocloudkvm import CI_DOMAIN
4from tests.cloud_tests.testcases import base5from tests.cloud_tests.testcases import base
56
67
7class TestHostnameFqdn(base.CloudTestCase):8class TestHostnameFqdn(base.CloudTestCase):
8 """Test Hostname module."""9 """Test Hostname module."""
910
11 ex_hostname = "cloudinit1"
12 ex_fqdn = "cloudinit2." + CI_DOMAIN
13
10 def test_hostname(self):14 def test_hostname(self):
11 """Test hostname output."""15 """Test hostname output."""
12 out = self.get_data_file('hostname')16 out = self.get_data_file('hostname')
13 self.assertIn('myhostname', out)17 self.assertIn(self.ex_hostname, out)
1418
15 def test_hostname_fqdn(self):19 def test_hostname_fqdn(self):
16 """Test hostname fqdn output."""20 """Test hostname fqdn output."""
17 out = self.get_data_file('fqdn')21 out = self.get_data_file('fqdn')
18 self.assertIn('host.myorg.com', out)22 self.assertIn(self.ex_fqdn, out)
1923
20 def test_hosts(self):24 def test_hosts(self):
21 """Test /etc/hosts file."""25 """Test /etc/hosts file."""
22 out = self.get_data_file('hosts')26 out = self.get_data_file('hosts')
23 self.assertIn('127.0.1.1 host.myorg.com myhostname', out)27 self.assertIn('127.0.1.1 %s %s' % (self.ex_fqdn, self.ex_hostname),
28 out)
24 self.assertIn('127.0.0.1 localhost', out)29 self.assertIn('127.0.0.1 localhost', out)
2530
26# vi: ts=4 expandtab31# vi: ts=4 expandtab
diff --git a/tests/cloud_tests/testcases/modules/set_hostname_fqdn.yaml b/tests/cloud_tests/testcases/modules/set_hostname_fqdn.yaml
index daf7593..a85ee79 100644
--- a/tests/cloud_tests/testcases/modules/set_hostname_fqdn.yaml
+++ b/tests/cloud_tests/testcases/modules/set_hostname_fqdn.yaml
@@ -6,8 +6,9 @@ required_features:
6cloud_config: |6cloud_config: |
7 #cloud-config7 #cloud-config
8 manage_etc_hosts: true8 manage_etc_hosts: true
9 hostname: myhostname9 hostname: cloudinit1
10 fqdn: host.myorg.com10 # this needs changing if CI_DOMAIN were updated.
11 fqdn: cloudinit2.i9n.cloud-init.io
11collect_scripts:12collect_scripts:
12 hosts: |13 hosts: |
13 #!/bin/bash14 #!/bin/bash
diff --git a/tests/cloud_tests/testcases/modules/set_password_expire.py b/tests/cloud_tests/testcases/modules/set_password_expire.py
index a1c3aa0..967aca7 100644
--- a/tests/cloud_tests/testcases/modules/set_password_expire.py
+++ b/tests/cloud_tests/testcases/modules/set_password_expire.py
@@ -18,6 +18,6 @@ class TestPasswordExpire(base.CloudTestCase):
18 def test_sshd_config(self):18 def test_sshd_config(self):
19 """Test sshd config allows passwords."""19 """Test sshd config allows passwords."""
20 out = self.get_data_file('sshd_config')20 out = self.get_data_file('sshd_config')
21 self.assertIn('PasswordAuthentication no', out)21 self.assertIn('PasswordAuthentication yes', out)
2222
23# vi: ts=4 expandtab23# vi: ts=4 expandtab
diff --git a/tests/cloud_tests/testcases/modules/set_password_expire.yaml b/tests/cloud_tests/testcases/modules/set_password_expire.yaml
index 789604b..ba6344b 100644
--- a/tests/cloud_tests/testcases/modules/set_password_expire.yaml
+++ b/tests/cloud_tests/testcases/modules/set_password_expire.yaml
@@ -6,7 +6,9 @@ required_features:
6cloud_config: |6cloud_config: |
7 #cloud-config7 #cloud-config
8 chpasswd: { expire: True }8 chpasswd: { expire: True }
9 ssh_pwauth: yes
9 users:10 users:
11 - default
10 - name: tom12 - name: tom
11 password: $1$xyz$sPMsLNmf66Ohl.ol6JvzE.13 password: $1$xyz$sPMsLNmf66Ohl.ol6JvzE.
12 lock_passwd: false14 lock_passwd: false
diff --git a/tests/cloud_tests/testcases/modules/set_password_list.yaml b/tests/cloud_tests/testcases/modules/set_password_list.yaml
index a2a89c9..fd3e1e4 100644
--- a/tests/cloud_tests/testcases/modules/set_password_list.yaml
+++ b/tests/cloud_tests/testcases/modules/set_password_list.yaml
@@ -5,6 +5,7 @@ cloud_config: |
5 #cloud-config5 #cloud-config
6 ssh_pwauth: yes6 ssh_pwauth: yes
7 users:7 users:
8 - default
8 - name: tom9 - name: tom
9 # md5 gotomgo10 # md5 gotomgo
10 passwd: "$1$S7$tT1BEDIYrczeryDQJfdPe0"11 passwd: "$1$S7$tT1BEDIYrczeryDQJfdPe0"
diff --git a/tests/cloud_tests/testcases/modules/set_password_list_string.yaml b/tests/cloud_tests/testcases/modules/set_password_list_string.yaml
index c2a0f63..e9fe54b 100644
--- a/tests/cloud_tests/testcases/modules/set_password_list_string.yaml
+++ b/tests/cloud_tests/testcases/modules/set_password_list_string.yaml
@@ -5,6 +5,7 @@ cloud_config: |
5 #cloud-config5 #cloud-config
6 ssh_pwauth: yes6 ssh_pwauth: yes
7 users:7 users:
8 - default
8 - name: tom9 - name: tom
9 # md5 gotomgo10 # md5 gotomgo
10 passwd: "$1$S7$tT1BEDIYrczeryDQJfdPe0"11 passwd: "$1$S7$tT1BEDIYrczeryDQJfdPe0"
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
index 8222321..e7329d4 100644
--- a/tests/cloud_tests/testcases/modules/ssh_auth_key_fingerprints_disable.py
+++ b/tests/cloud_tests/testcases/modules/ssh_auth_key_fingerprints_disable.py
@@ -13,12 +13,4 @@ class TestSshKeyFingerprintsDisable(base.CloudTestCase):
13 self.assertIn('Skipping module named ssh-authkey-fingerprints, '13 self.assertIn('Skipping module named ssh-authkey-fingerprints, '
14 'logging of ssh fingerprints disabled', out)14 'logging of ssh fingerprints disabled', out)
1515
16 def test_syslog(self):
17 """Verify output of syslog."""
18 out = self.get_data_file('syslog')
19 self.assertNotRegex(out, r'256 SHA256:.*(ECDSA)')
20 self.assertNotRegex(out, r'256 SHA256:.*(ED25519)')
21 self.assertNotRegex(out, r'1024 SHA256:.*(DSA)')
22 self.assertNotRegex(out, r'2048 SHA256:.*(RSA)')
23
24# vi: ts=4 expandtab16# vi: ts=4 expandtab
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
index 746653e..d93893e 100644
--- a/tests/cloud_tests/testcases/modules/ssh_auth_key_fingerprints_disable.yaml
+++ b/tests/cloud_tests/testcases/modules/ssh_auth_key_fingerprints_disable.yaml
@@ -5,7 +5,6 @@ required_features:
5 - syslog5 - syslog
6cloud_config: |6cloud_config: |
7 #cloud-config7 #cloud-config
8 ssh_genkeytypes: []
9 no_ssh_fingerprints: true8 no_ssh_fingerprints: true
10collect_scripts:9collect_scripts:
11 syslog: |10 syslog: |
diff --git a/tests/cloud_tests/testcases/modules/ssh_keys_generate.py b/tests/cloud_tests/testcases/modules/ssh_keys_generate.py
index fd6d9ba..b68f556 100644
--- a/tests/cloud_tests/testcases/modules/ssh_keys_generate.py
+++ b/tests/cloud_tests/testcases/modules/ssh_keys_generate.py
@@ -9,11 +9,6 @@ class TestSshKeysGenerate(base.CloudTestCase):
99
10 # TODO: Check cloud-init-output for the correct keys being generated10 # TODO: Check cloud-init-output for the correct keys being generated
1111
12 def test_ubuntu_authorized_keys(self):
13 """Test passed in key is not in list for ubuntu."""
14 out = self.get_data_file('auth_keys_ubuntu')
15 self.assertEqual('', out)
16
17 def test_dsa_public(self):12 def test_dsa_public(self):
18 """Test dsa public key not generated."""13 """Test dsa public key not generated."""
19 out = self.get_data_file('dsa_public')14 out = self.get_data_file('dsa_public')
diff --git a/tests/cloud_tests/testcases/modules/ssh_keys_generate.yaml b/tests/cloud_tests/testcases/modules/ssh_keys_generate.yaml
index 659fd93..0a7adf6 100644
--- a/tests/cloud_tests/testcases/modules/ssh_keys_generate.yaml
+++ b/tests/cloud_tests/testcases/modules/ssh_keys_generate.yaml
@@ -10,12 +10,6 @@ cloud_config: |
10 - ed2551910 - ed25519
11 authkey_hash: sha51211 authkey_hash: sha512
12collect_scripts:12collect_scripts:
13 auth_keys_root: |
14 #!/bin/bash
15 cat /root/.ssh/authorized_keys
16 auth_keys_ubuntu: |
17 #!/bin/bash
18 cat /home/ubuntu/ssh/authorized_keys
19 dsa_public: |13 dsa_public: |
20 #!/bin/bash14 #!/bin/bash
21 cat /etc/ssh/ssh_host_dsa_key.pub15 cat /etc/ssh/ssh_host_dsa_key.pub
diff --git a/tests/cloud_tests/testcases/modules/ssh_keys_provided.py b/tests/cloud_tests/testcases/modules/ssh_keys_provided.py
index 544649d..add3f46 100644
--- a/tests/cloud_tests/testcases/modules/ssh_keys_provided.py
+++ b/tests/cloud_tests/testcases/modules/ssh_keys_provided.py
@@ -7,17 +7,6 @@ from tests.cloud_tests.testcases import base
7class TestSshKeysProvided(base.CloudTestCase):7class TestSshKeysProvided(base.CloudTestCase):
8 """Test ssh keys module."""8 """Test ssh keys module."""
99
10 def test_ubuntu_authorized_keys(self):
11 """Test passed in key is not in list for ubuntu."""
12 out = self.get_data_file('auth_keys_ubuntu')
13 self.assertEqual('', out)
14
15 def test_root_authorized_keys(self):
16 """Test passed in key is in authorized list for root."""
17 out = self.get_data_file('auth_keys_root')
18 self.assertIn('lzrkPqONphoZx0LDV86w7RUz1ksDzAdcm0tvmNRFMN1a0frDs50'
19 '6oA3aWK0oDk4Nmvk8sXGTYYw3iQSkOvDUUlIsqdaO+w==', out)
20
21 def test_dsa_public(self):10 def test_dsa_public(self):
22 """Test dsa public key passed in."""11 """Test dsa public key passed in."""
23 out = self.get_data_file('dsa_public')12 out = self.get_data_file('dsa_public')
diff --git a/tests/cloud_tests/testcases/modules/ssh_keys_provided.yaml b/tests/cloud_tests/testcases/modules/ssh_keys_provided.yaml
index 5ceb362..41f6355 100644
--- a/tests/cloud_tests/testcases/modules/ssh_keys_provided.yaml
+++ b/tests/cloud_tests/testcases/modules/ssh_keys_provided.yaml
@@ -71,12 +71,6 @@ cloud_config: |
71 -----END EC PRIVATE KEY-----71 -----END EC PRIVATE KEY-----
72 ecdsa_public: ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBFsS5Tvky/IC/dXhE/afxxUG6kdQOvdQJCYGZN42OZqWasYF+L3IG+3/wrV7jOrNrL3AyagHl6+lpPDiSXDMcpQ= root@xenial-lxd72 ecdsa_public: ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBFsS5Tvky/IC/dXhE/afxxUG6kdQOvdQJCYGZN42OZqWasYF+L3IG+3/wrV7jOrNrL3AyagHl6+lpPDiSXDMcpQ= root@xenial-lxd
73collect_scripts:73collect_scripts:
74 auth_keys_root: |
75 #!/bin/bash
76 cat /root/.ssh/authorized_keys
77 auth_keys_ubuntu: |
78 #!/bin/bash
79 cat /home/ubuntu/ssh/authorized_keys
80 dsa_public: |74 dsa_public: |
81 #!/bin/bash75 #!/bin/bash
82 cat /etc/ssh/ssh_host_dsa_key.pub76 cat /etc/ssh/ssh_host_dsa_key.pub
diff --git a/tests/cloud_tests/util.py b/tests/cloud_tests/util.py
index 4357fbb..c5cd697 100644
--- a/tests/cloud_tests/util.py
+++ b/tests/cloud_tests/util.py
@@ -7,6 +7,7 @@ import copy
7import glob7import glob
8import os8import os
9import random9import random
10import shlex
10import shutil11import shutil
11import string12import string
12import subprocess13import subprocess
@@ -285,20 +286,165 @@ def shell_pack(cmd):
285 return 'eval set -- "$(echo %s | base64 --decode)" && exec "$@"' % b64286 return 'eval set -- "$(echo %s | base64 --decode)" && exec "$@"' % b64
286287
287288
289def shell_quote(cmd):
290 if isinstance(cmd, (tuple, list)):
291 return ' '.join([shlex.quote(x) for x in cmd])
292 return shlex.quote(cmd)
293
294
295class TargetBase(object):
296 _tmp_count = 0
297
298 def execute(self, command, stdin=None, env=None,
299 rcs=None, description=None):
300 """Execute command in instance, recording output, error and exit code.
301
302 Assumes functional networking and execution as root with the
303 target filesystem being available at /.
304
305 @param command: the command to execute as root inside the image
306 if command is a string, then it will be executed as:
307 ['sh', '-c', command]
308 @param stdin: bytes content for standard in
309 @param env: environment variables
310 @param rcs: return codes.
311 None (default): non-zero exit code will raise exception.
312 False: any is allowed (No execption raised).
313 list of int: any rc not in the list will raise exception.
314 @param description: purpose of command
315 @return_value: tuple containing stdout data, stderr data, exit code
316 """
317 if isinstance(command, str):
318 command = ['sh', '-c', command]
319
320 if rcs is None:
321 rcs = (0,)
322
323 if description:
324 LOG.debug('Executing "%s"', description)
325 else:
326 LOG.debug("Executing command: %s", shell_quote(command))
327
328 out, err, rc = self._execute(command=command, stdin=stdin, env=env)
329
330 # False means accept anything.
331 if (rcs is False or rc in rcs):
332 return out, err, rc
333
334 raise InTargetExecuteError(out, err, rc, command, description)
335
336 def _execute(self, command, stdin=None, env=None):
337 """Execute command in inside, return stdout, stderr and exit code.
338
339 Assumes functional networking and execution as root with the
340 target filesystem being available at /.
341
342 @param stdin: bytes content for standard in
343 @param env: environment variables
344 @return_value: tuple containing stdout data, stderr data, exit code
345
346 This is intended to be implemented by the Image or Instance.
347 Many callers will use the higher level 'execute'."""
348 raise NotImplementedError("_execute must be implemented by subclass.")
349
350 def read_data(self, remote_path, decode=False):
351 """Read data from instance filesystem.
352
353 @param remote_path: path in instance
354 @param decode: decode data before returning.
355 @return_value: content of remote_path as bytes if 'decode' is False,
356 and as string if 'decode' is True.
357 """
358 # when sh is invoked with '-c', then the first argument is "$0"
359 # which is commonly understood as the "program name".
360 # 'read_data' is the program name, and 'remote_path' is '$1'
361 stdout, stderr, rc = self._execute(
362 ["sh", "-c", 'exec cat "$1"', 'read_data', remote_path])
363 if rc != 0:
364 raise RuntimeError("Failed to read file '%s'" % remote_path)
365
366 if decode:
367 return stdout.decode()
368 return stdout
369
370 def write_data(self, remote_path, data):
371 """Write data to instance filesystem.
372
373 @param remote_path: path in instance
374 @param data: data to write in bytes
375 """
376 # when sh is invoked with '-c', then the first argument is "$0"
377 # which is commonly understood as the "program name".
378 # 'write_data' is the program name, and 'remote_path' is '$1'
379 _, _, rc = self._execute(
380 ["sh", "-c", 'exec cat >"$1"', 'write_data', remote_path],
381 stdin=data)
382
383 if rc != 0:
384 raise RuntimeError("Failed to write to '%s'" % remote_path)
385 return
386
387 def pull_file(self, remote_path, local_path):
388 """Copy file at 'remote_path', from instance to 'local_path'.
389
390 @param remote_path: path on remote instance
391 @param local_path: path on local instance
392 """
393 with open(local_path, 'wb') as fp:
394 fp.write(self.read_data(remote_path))
395
396 def push_file(self, local_path, remote_path):
397 """Copy file at 'local_path' to instance at 'remote_path'.
398
399 @param local_path: path on local instance
400 @param remote_path: path on remote instance"""
401 with open(local_path, "rb") as fp:
402 self.write_data(remote_path, data=fp.read())
403
404 def run_script(self, script, rcs=None, description=None):
405 """Run script in target and return stdout.
406
407 @param script: script contents
408 @param rcs: allowed return codes from script
409 @param description: purpose of script
410 @return_value: stdout from script
411 """
412 # Just write to a file, add execute, run it, then remove it.
413 shblob = '; '.join((
414 'set -e',
415 's="$1"',
416 'shift',
417 'cat > "$s"',
418 'trap "rm -f $s" EXIT',
419 'chmod +x "$s"',
420 '"$s" "$@"'))
421 return self.execute(
422 ['sh', '-c', shblob, 'runscript', self.tmpfile()],
423 stdin=script, description=description, rcs=rcs)
424
425 def tmpfile(self):
426 """Get a tmp file in the target.
427
428 @return_value: path to new file in target
429 """
430 path = "/tmp/%s-%04d" % (type(self).__name__, self._tmp_count)
431 self._tmp_count += 1
432 return path
433
434
288class InTargetExecuteError(c_util.ProcessExecutionError):435class InTargetExecuteError(c_util.ProcessExecutionError):
289 """Error type for in target commands that fail."""436 """Error type for in target commands that fail."""
290437
291 default_desc = 'Unexpected error while running command in target instance'438 default_desc = 'Unexpected error while running command.'
292439
293 def __init__(self, stdout, stderr, exit_code, cmd, instance,440 def __init__(self, stdout, stderr, exit_code, cmd, description=None,
294 description=None):441 reason=None):
295 """Init error and parent error class."""442 """Init error and parent error class."""
296 if isinstance(cmd, (tuple, list)):
297 cmd = ' '.join(cmd)
298 super(InTargetExecuteError, self).__init__(443 super(InTargetExecuteError, self).__init__(
299 stdout=stdout, stderr=stderr, exit_code=exit_code, cmd=cmd,444 stdout=stdout, stderr=stderr, exit_code=exit_code,
300 reason="Instance: {}".format(instance),445 cmd=shell_quote(cmd),
301 description=description if description else self.default_desc)446 description=description if description else self.default_desc,
447 reason=reason)
302448
303449
304class TempDir(object):450class TempDir(object):
diff --git a/tests/unittests/test_data.py b/tests/unittests/test_data.py
index 6d621d2..275b16d 100644
--- a/tests/unittests/test_data.py
+++ b/tests/unittests/test_data.py
@@ -18,6 +18,8 @@ from email.mime.application import MIMEApplication
18from email.mime.base import MIMEBase18from email.mime.base import MIMEBase
19from email.mime.multipart import MIMEMultipart19from email.mime.multipart import MIMEMultipart
2020
21import httpretty
22
21from cloudinit import handlers23from cloudinit import handlers
22from cloudinit import helpers as c_helpers24from cloudinit import helpers as c_helpers
23from cloudinit import log25from cloudinit import log
@@ -522,6 +524,54 @@ c: 4
522 self.assertEqual(cfg.get('password'), 'gocubs')524 self.assertEqual(cfg.get('password'), 'gocubs')
523 self.assertEqual(cfg.get('locale'), 'chicago')525 self.assertEqual(cfg.get('locale'), 'chicago')
524526
527 @httpretty.activate
528 @mock.patch('cloudinit.url_helper.time.sleep')
529 def test_include(self, mock_sleep):
530 """Test #include."""
531 included_url = 'http://hostname/path'
532 included_data = '#cloud-config\nincluded: true\n'
533 httpretty.register_uri(httpretty.GET, included_url, included_data)
534
535 blob = '#include\n%s\n' % included_url
536
537 self.reRoot()
538 ci = stages.Init()
539 ci.datasource = FakeDataSource(blob)
540 ci.fetch()
541 ci.consume_data()
542 cc_contents = util.load_file(ci.paths.get_ipath("cloud_config"))
543 cc = util.load_yaml(cc_contents)
544 self.assertTrue(cc.get('included'))
545
546 @httpretty.activate
547 @mock.patch('cloudinit.url_helper.time.sleep')
548 def test_include_bad_url(self, mock_sleep):
549 """Test #include with a bad URL."""
550 bad_url = 'http://bad/forbidden'
551 bad_data = '#cloud-config\nbad: true\n'
552 httpretty.register_uri(httpretty.GET, bad_url, bad_data, status=403)
553
554 included_url = 'http://hostname/path'
555 included_data = '#cloud-config\nincluded: true\n'
556 httpretty.register_uri(httpretty.GET, included_url, included_data)
557
558 blob = '#include\n%s\n%s' % (bad_url, included_url)
559
560 self.reRoot()
561 ci = stages.Init()
562 ci.datasource = FakeDataSource(blob)
563 log_file = self.capture_log(logging.WARNING)
564 ci.fetch()
565 ci.consume_data()
566
567 self.assertIn("403 Client Error: Forbidden for url: %s" % bad_url,
568 log_file.getvalue())
569
570 cc_contents = util.load_file(ci.paths.get_ipath("cloud_config"))
571 cc = util.load_yaml(cc_contents)
572 self.assertIsNone(cc.get('bad'))
573 self.assertTrue(cc.get('included'))
574
525575
526class TestUDProcess(helpers.ResourceUsingTestCase):576class TestUDProcess(helpers.ResourceUsingTestCase):
527577
diff --git a/tests/unittests/test_datasource/test_ec2.py b/tests/unittests/test_datasource/test_ec2.py
index 6af699a..ba328ee 100644
--- a/tests/unittests/test_datasource/test_ec2.py
+++ b/tests/unittests/test_datasource/test_ec2.py
@@ -307,6 +307,39 @@ class TestEc2(test_helpers.HttprettyTestCase):
307307
308 @httpretty.activate308 @httpretty.activate
309 @mock.patch('cloudinit.net.dhcp.maybe_perform_dhcp_discovery')309 @mock.patch('cloudinit.net.dhcp.maybe_perform_dhcp_discovery')
310 def test_network_config_cached_property_refreshed_on_upgrade(self, m_dhcp):
311 """Refresh the network_config Ec2 cache if network key is absent.
312
313 This catches an upgrade issue where obj.pkl contained stale metadata
314 which lacked newly required network key.
315 """
316 old_metadata = copy.deepcopy(DEFAULT_METADATA)
317 old_metadata.pop('network')
318 ds = self._setup_ds(
319 platform_data=self.valid_platform_data,
320 sys_cfg={'datasource': {'Ec2': {'strict_id': True}}},
321 md=old_metadata)
322 self.assertTrue(ds.get_data())
323 # Provide new revision of metadata that contains network data
324 register_mock_metaserver(
325 'http://169.254.169.254/2009-04-04/meta-data/', DEFAULT_METADATA)
326 mac1 = '06:17:04:d7:26:09' # Defined in DEFAULT_METADATA
327 get_interface_mac_path = (
328 'cloudinit.sources.DataSourceEc2.net.get_interface_mac')
329 ds.fallback_nic = 'eth9'
330 with mock.patch(get_interface_mac_path) as m_get_interface_mac:
331 m_get_interface_mac.return_value = mac1
332 ds.network_config # Will re-crawl network metadata
333 self.assertIn('Re-crawl of metadata service', self.logs.getvalue())
334 expected = {'version': 1, 'config': [
335 {'mac_address': '06:17:04:d7:26:09',
336 'name': 'eth9',
337 'subnets': [{'type': 'dhcp4'}, {'type': 'dhcp6'}],
338 'type': 'physical'}]}
339 self.assertEqual(expected, ds.network_config)
340
341 @httpretty.activate
342 @mock.patch('cloudinit.net.dhcp.maybe_perform_dhcp_discovery')
310 def test_valid_platform_with_strict_true(self, m_dhcp):343 def test_valid_platform_with_strict_true(self, m_dhcp):
311 """Valid platform data should return true with strict_id true."""344 """Valid platform data should return true with strict_id true."""
312 ds = self._setup_ds(345 ds = self._setup_ds(
diff --git a/tests/unittests/test_handler/test_handler_etc_hosts.py b/tests/unittests/test_handler/test_handler_etc_hosts.py
313new file mode 100644346new file mode 100644
index 0000000..ced05a8
--- /dev/null
+++ b/tests/unittests/test_handler/test_handler_etc_hosts.py
@@ -0,0 +1,69 @@
1# This file is part of cloud-init. See LICENSE file for license information.
2
3from cloudinit.config import cc_update_etc_hosts
4
5from cloudinit import cloud
6from cloudinit import distros
7from cloudinit import helpers
8from cloudinit import util
9
10from cloudinit.tests import helpers as t_help
11
12import logging
13import os
14import shutil
15
16LOG = logging.getLogger(__name__)
17
18
19class TestHostsFile(t_help.FilesystemMockingTestCase):
20 def setUp(self):
21 super(TestHostsFile, self).setUp()
22 self.tmp = self.tmp_dir()
23
24 def _fetch_distro(self, kind):
25 cls = distros.fetch(kind)
26 paths = helpers.Paths({})
27 return cls(kind, {}, paths)
28
29 def test_write_etc_hosts_suse_localhost(self):
30 cfg = {
31 'manage_etc_hosts': 'localhost',
32 'hostname': 'cloud-init.test.us'
33 }
34 os.makedirs('%s/etc/' % self.tmp)
35 hosts_content = '192.168.1.1 blah.blah.us blah\n'
36 fout = open('%s/etc/hosts' % self.tmp, 'w')
37 fout.write(hosts_content)
38 fout.close()
39 distro = self._fetch_distro('sles')
40 distro.hosts_fn = '%s/etc/hosts' % self.tmp
41 paths = helpers.Paths({})
42 ds = None
43 cc = cloud.Cloud(ds, paths, {}, distro, None)
44 self.patchUtils(self.tmp)
45 cc_update_etc_hosts.handle('test', cfg, cc, LOG, [])
46 contents = util.load_file('%s/etc/hosts' % self.tmp)
47 if '127.0.0.1\tcloud-init.test.us\tcloud-init' not in contents:
48 self.assertIsNone('No entry for 127.0.0.1 in etc/hosts')
49 if '192.168.1.1\tblah.blah.us\tblah' not in contents:
50 self.assertIsNone('Default etc/hosts content modified')
51
52 def test_write_etc_hosts_suse_template(self):
53 cfg = {
54 'manage_etc_hosts': 'template',
55 'hostname': 'cloud-init.test.us'
56 }
57 shutil.copytree('templates', '%s/etc/cloud/templates' % self.tmp)
58 distro = self._fetch_distro('sles')
59 paths = helpers.Paths({})
60 paths.template_tpl = '%s' % self.tmp + '/etc/cloud/templates/%s.tmpl'
61 ds = None
62 cc = cloud.Cloud(ds, paths, {}, distro, None)
63 self.patchUtils(self.tmp)
64 cc_update_etc_hosts.handle('test', cfg, cc, LOG, [])
65 contents = util.load_file('%s/etc/hosts' % self.tmp)
66 if '127.0.0.1 cloud-init.test.us cloud-init' not in contents:
67 self.assertIsNone('No entry for 127.0.0.1 in etc/hosts')
68 if '::1 cloud-init.test.us cloud-init' not in contents:
69 self.assertIsNone('No entry for 127.0.0.1 in etc/hosts')
diff --git a/tests/unittests/test_handler/test_handler_ntp.py b/tests/unittests/test_handler/test_handler_ntp.py
index 3abe578..28a8455 100644
--- a/tests/unittests/test_handler/test_handler_ntp.py
+++ b/tests/unittests/test_handler/test_handler_ntp.py
@@ -430,5 +430,31 @@ class TestNtp(FilesystemMockingTestCase):
430 "[Time]\nNTP=192.168.2.1 192.168.2.2 0.mypool.org \n",430 "[Time]\nNTP=192.168.2.1 192.168.2.2 0.mypool.org \n",
431 content.decode())431 content.decode())
432432
433 def test_write_ntp_config_template_defaults_pools_empty_lists_sles(self):
434 """write_ntp_config_template defaults pools servers upon empty config.
435
436 When both pools and servers are empty, default NR_POOL_SERVERS get
437 configured.
438 """
439 distro = 'sles'
440 mycloud = self._get_cloud(distro)
441 ntp_conf = self.tmp_path('ntp.conf', self.new_root) # Doesn't exist
442 # Create ntp.conf.tmpl
443 with open('{0}.tmpl'.format(ntp_conf), 'wb') as stream:
444 stream.write(NTP_TEMPLATE)
445 with mock.patch('cloudinit.config.cc_ntp.NTP_CONF', ntp_conf):
446 cc_ntp.write_ntp_config_template({}, mycloud, ntp_conf)
447 content = util.read_file_or_url('file://' + ntp_conf).contents
448 default_pools = [
449 "{0}.opensuse.pool.ntp.org".format(x)
450 for x in range(0, cc_ntp.NR_POOL_SERVERS)]
451 self.assertEqual(
452 "servers []\npools {0}\n".format(default_pools),
453 content.decode())
454 self.assertIn(
455 "Adding distro default ntp pool servers: {0}".format(
456 ",".join(default_pools)),
457 self.logs.getvalue())
458
433459
434# vi: ts=4 expandtab460# vi: ts=4 expandtab
diff --git a/tests/unittests/test_rh_subscription.py b/tests/unittests/test_rh_subscription.py
index e9d5702..2271810 100644
--- a/tests/unittests/test_rh_subscription.py
+++ b/tests/unittests/test_rh_subscription.py
@@ -2,6 +2,7 @@
22
3"""Tests for registering RHEL subscription via rh_subscription."""3"""Tests for registering RHEL subscription via rh_subscription."""
44
5import copy
5import logging6import logging
67
7from cloudinit.config import cc_rh_subscription8from cloudinit.config import cc_rh_subscription
@@ -68,6 +69,20 @@ class GoodTests(TestCase):
68 self.assertEqual(self.SM.log_success.call_count, 1)69 self.assertEqual(self.SM.log_success.call_count, 1)
69 self.assertEqual(self.SM._sub_man_cli.call_count, 2)70 self.assertEqual(self.SM._sub_man_cli.call_count, 2)
7071
72 @mock.patch.object(cc_rh_subscription.SubscriptionManager, "_getRepos")
73 @mock.patch.object(cc_rh_subscription.SubscriptionManager, "_sub_man_cli")
74 def test_update_repos_disable_with_none(self, m_sub_man_cli, m_get_repos):
75 cfg = copy.deepcopy(self.config)
76 m_get_repos.return_value = ([], ['repo1'])
77 m_sub_man_cli.return_value = (b'', b'')
78 cfg['rh_subscription'].update(
79 {'enable-repo': ['repo1'], 'disable-repo': None})
80 mysm = cc_rh_subscription.SubscriptionManager(cfg)
81 self.assertEqual(True, mysm.update_repos())
82 m_get_repos.assert_called_with()
83 self.assertEqual(m_sub_man_cli.call_args_list,
84 [mock.call(['repos', '--enable=repo1'])])
85
71 def test_full_registration(self):86 def test_full_registration(self):
72 '''87 '''
73 Registration with auto-attach, service-level, adding pools,88 Registration with auto-attach, service-level, adding pools,

Subscribers

People subscribed via source and target branches