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


PASSED: Continuous integration, rev:5e890f55a17 d991c34053f78c0 e83b5b700345b3 /jenkins. ubuntu. com/server/ job/cloud- init-ci/ 533/
https:/
Executed test runs:
SUCCESS: Checkout
SUCCESS: Unit & Style Tests
SUCCESS: Ubuntu LTS: Build
SUCCESS: Ubuntu LTS: Integration
SUCCESS: MAAS Compatability Testing
IN_PROGRESS: Declarative: Post Actions
Click here to trigger a rebuild: /jenkins. ubuntu. com/server/ job/cloud- init-ci/ 533/rebuild
https:/