Merge ~chad.smith/cloud-init:ubuntu/zesty into cloud-init:ubuntu/zesty
- Git
- lp:~chad.smith/cloud-init
- ubuntu/zesty
- Merge into ubuntu/zesty
Proposed by
Chad Smith
on 2017-12-01
| Status: | Merged |
|---|---|
| Merged at revision: | 40551a784ad473b437e5ddd68f811fd2255f896f |
| Proposed branch: | ~chad.smith/cloud-init:ubuntu/zesty |
| Merge into: | cloud-init:ubuntu/zesty |
| Diff against target: |
667 lines (+221/-77) 15 files modified
cloudinit/net/dhcp.py (+29/-15) cloudinit/net/network_state.py (+8/-0) cloudinit/net/sysconfig.py (+15/-0) cloudinit/net/tests/test_dhcp.py (+61/-5) cloudinit/sources/DataSourceAzure.py (+3/-26) cloudinit/util.py (+22/-0) debian/changelog (+13/-0) tests/cloud_tests/images/nocloudkvm.py (+15/-7) tests/cloud_tests/instances/nocloudkvm.py (+5/-3) tests/cloud_tests/platforms/nocloudkvm.py (+11/-10) tests/cloud_tests/releases.yaml (+16/-0) tests/cloud_tests/setup_image.py (+3/-3) tests/cloud_tests/snapshots/nocloudkvm.py (+11/-6) tests/unittests/test_datasource/test_azure.py (+3/-2) tests/unittests/test_net.py (+6/-0) |
| Related bugs: |
| Reviewer | Review Type | Date Requested | Status |
|---|---|---|---|
| Server Team CI bot | continuous-integration | Approve on 2017-12-01 | |
| Scott Moser | 2017-12-01 | Pending | |
|
Review via email:
|
|||
Commit Message
Description of the Change
Upstream snapshot for SRU into Zesty
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/net/dhcp.py b/cloudinit/net/dhcp.py |
| 2 | index d8624d8..875a460 100644 |
| 3 | --- a/cloudinit/net/dhcp.py |
| 4 | +++ b/cloudinit/net/dhcp.py |
| 5 | @@ -36,22 +36,23 @@ def maybe_perform_dhcp_discovery(nic=None): |
| 6 | skip dhcp_discovery and return an empty dict. |
| 7 | |
| 8 | @param nic: Name of the network interface we want to run dhclient on. |
| 9 | - @return: A dict of dhcp options from the dhclient discovery if run, |
| 10 | - otherwise an empty dict is returned. |
| 11 | + @return: A list of dicts representing dhcp options for each lease obtained |
| 12 | + from the dhclient discovery if run, otherwise an empty list is |
| 13 | + returned. |
| 14 | """ |
| 15 | if nic is None: |
| 16 | nic = find_fallback_nic() |
| 17 | if nic is None: |
| 18 | LOG.debug('Skip dhcp_discovery: Unable to find fallback nic.') |
| 19 | - return {} |
| 20 | + return [] |
| 21 | elif nic not in get_devicelist(): |
| 22 | LOG.debug( |
| 23 | 'Skip dhcp_discovery: nic %s not found in get_devicelist.', nic) |
| 24 | - return {} |
| 25 | + return [] |
| 26 | dhclient_path = util.which('dhclient') |
| 27 | if not dhclient_path: |
| 28 | LOG.debug('Skip dhclient configuration: No dhclient command found.') |
| 29 | - return {} |
| 30 | + return [] |
| 31 | with temp_utils.tempdir(prefix='cloud-init-dhcp-', needs_exe=True) as tdir: |
| 32 | # Use /var/tmp because /run/cloud-init/tmp is mounted noexec |
| 33 | return dhcp_discovery(dhclient_path, nic, tdir) |
| 34 | @@ -60,8 +61,8 @@ def maybe_perform_dhcp_discovery(nic=None): |
| 35 | def parse_dhcp_lease_file(lease_file): |
| 36 | """Parse the given dhcp lease file for the most recent lease. |
| 37 | |
| 38 | - Return a dict of dhcp options as key value pairs for the most recent lease |
| 39 | - block. |
| 40 | + Return a list of dicts of dhcp options. Each dict contains key value pairs |
| 41 | + a specific lease in order from oldest to newest. |
| 42 | |
| 43 | @raises: InvalidDHCPLeaseFileError on empty of unparseable leasefile |
| 44 | content. |
| 45 | @@ -96,8 +97,8 @@ def dhcp_discovery(dhclient_cmd_path, interface, cleandir): |
| 46 | @param cleandir: The directory from which to run dhclient as well as store |
| 47 | dhcp leases. |
| 48 | |
| 49 | - @return: A dict of dhcp options parsed from the dhcp.leases file or empty |
| 50 | - dict. |
| 51 | + @return: A list of dicts of representing the dhcp leases parsed from the |
| 52 | + dhcp.leases file or empty list. |
| 53 | """ |
| 54 | LOG.debug('Performing a dhcp discovery on %s', interface) |
| 55 | |
| 56 | @@ -119,13 +120,26 @@ def dhcp_discovery(dhclient_cmd_path, interface, cleandir): |
| 57 | cmd = [sandbox_dhclient_cmd, '-1', '-v', '-lf', lease_file, |
| 58 | '-pf', pid_file, interface, '-sf', '/bin/true'] |
| 59 | util.subp(cmd, capture=True) |
| 60 | - pid = None |
| 61 | + |
| 62 | + # dhclient doesn't write a pid file until after it forks when it gets a |
| 63 | + # proper lease response. Since cleandir is a temp directory that gets |
| 64 | + # removed, we need to wait for that pidfile creation before the |
| 65 | + # cleandir is removed, otherwise we get FileNotFound errors. |
| 66 | + missing = util.wait_for_files( |
| 67 | + [pid_file, lease_file], maxwait=5, naplen=0.01) |
| 68 | + if missing: |
| 69 | + LOG.warning("dhclient did not produce expected files: %s", |
| 70 | + ', '.join(os.path.basename(f) for f in missing)) |
| 71 | + return [] |
| 72 | + pid_content = util.load_file(pid_file).strip() |
| 73 | try: |
| 74 | - pid = int(util.load_file(pid_file).strip()) |
| 75 | - return parse_dhcp_lease_file(lease_file) |
| 76 | - finally: |
| 77 | - if pid: |
| 78 | - os.kill(pid, signal.SIGKILL) |
| 79 | + pid = int(pid_content) |
| 80 | + except ValueError: |
| 81 | + LOG.debug( |
| 82 | + "pid file contains non-integer content '%s'", pid_content) |
| 83 | + else: |
| 84 | + os.kill(pid, signal.SIGKILL) |
| 85 | + return parse_dhcp_lease_file(lease_file) |
| 86 | |
| 87 | |
| 88 | def networkd_parse_lease(content): |
| 89 | diff --git a/cloudinit/net/network_state.py b/cloudinit/net/network_state.py |
| 90 | index 0e830ee..e9e2cf4 100644 |
| 91 | --- a/cloudinit/net/network_state.py |
| 92 | +++ b/cloudinit/net/network_state.py |
| 93 | @@ -746,6 +746,14 @@ def _normalize_subnet(subnet): |
| 94 | _normalize_net_keys(normal_subnet, address_keys=('address',))) |
| 95 | normal_subnet['routes'] = [_normalize_route(r) |
| 96 | for r in subnet.get('routes', [])] |
| 97 | + |
| 98 | + def listify(snet, name): |
| 99 | + if name in snet and not isinstance(snet[name], list): |
| 100 | + snet[name] = snet[name].split() |
| 101 | + |
| 102 | + for k in ('dns_search', 'dns_nameservers'): |
| 103 | + listify(normal_subnet, k) |
| 104 | + |
| 105 | return normal_subnet |
| 106 | |
| 107 | |
| 108 | diff --git a/cloudinit/net/sysconfig.py b/cloudinit/net/sysconfig.py |
| 109 | index f572796..39d89c4 100644 |
| 110 | --- a/cloudinit/net/sysconfig.py |
| 111 | +++ b/cloudinit/net/sysconfig.py |
| 112 | @@ -7,12 +7,15 @@ import six |
| 113 | |
| 114 | from cloudinit.distros.parsers import networkmanager_conf |
| 115 | from cloudinit.distros.parsers import resolv_conf |
| 116 | +from cloudinit import log as logging |
| 117 | from cloudinit import util |
| 118 | |
| 119 | from . import renderer |
| 120 | from .network_state import ( |
| 121 | is_ipv6_addr, net_prefix_to_ipv4_mask, subnet_is_ipv6) |
| 122 | |
| 123 | +LOG = logging.getLogger(__name__) |
| 124 | + |
| 125 | |
| 126 | def _make_header(sep='#'): |
| 127 | lines = [ |
| 128 | @@ -347,6 +350,18 @@ class Renderer(renderer.Renderer): |
| 129 | else: |
| 130 | iface_cfg['GATEWAY'] = subnet['gateway'] |
| 131 | |
| 132 | + if 'dns_search' in subnet: |
| 133 | + iface_cfg['DOMAIN'] = ' '.join(subnet['dns_search']) |
| 134 | + |
| 135 | + if 'dns_nameservers' in subnet: |
| 136 | + if len(subnet['dns_nameservers']) > 3: |
| 137 | + # per resolv.conf(5) MAXNS sets this to 3. |
| 138 | + LOG.debug("%s has %d entries in dns_nameservers. " |
| 139 | + "Only 3 are used.", iface_cfg.name, |
| 140 | + len(subnet['dns_nameservers'])) |
| 141 | + for i, k in enumerate(subnet['dns_nameservers'][:3], 1): |
| 142 | + iface_cfg['DNS' + str(i)] = k |
| 143 | + |
| 144 | @classmethod |
| 145 | def _render_subnet_routes(cls, iface_cfg, route_cfg, subnets): |
| 146 | for i, subnet in enumerate(subnets, start=len(iface_cfg.children)): |
| 147 | diff --git a/cloudinit/net/tests/test_dhcp.py b/cloudinit/net/tests/test_dhcp.py |
| 148 | index 3d8e15c..db25b6f 100644 |
| 149 | --- a/cloudinit/net/tests/test_dhcp.py |
| 150 | +++ b/cloudinit/net/tests/test_dhcp.py |
| 151 | @@ -1,6 +1,5 @@ |
| 152 | # This file is part of cloud-init. See LICENSE file for license information. |
| 153 | |
| 154 | -import mock |
| 155 | import os |
| 156 | import signal |
| 157 | from textwrap import dedent |
| 158 | @@ -9,7 +8,8 @@ from cloudinit.net.dhcp import ( |
| 159 | InvalidDHCPLeaseFileError, maybe_perform_dhcp_discovery, |
| 160 | parse_dhcp_lease_file, dhcp_discovery, networkd_load_leases) |
| 161 | from cloudinit.util import ensure_file, write_file |
| 162 | -from cloudinit.tests.helpers import CiTestCase, wrap_and_call, populate_dir |
| 163 | +from cloudinit.tests.helpers import ( |
| 164 | + CiTestCase, mock, populate_dir, wrap_and_call) |
| 165 | |
| 166 | |
| 167 | class TestParseDHCPLeasesFile(CiTestCase): |
| 168 | @@ -69,14 +69,14 @@ class TestDHCPDiscoveryClean(CiTestCase): |
| 169 | def test_no_fallback_nic_found(self, m_fallback_nic): |
| 170 | """Log and do nothing when nic is absent and no fallback is found.""" |
| 171 | m_fallback_nic.return_value = None # No fallback nic found |
| 172 | - self.assertEqual({}, maybe_perform_dhcp_discovery()) |
| 173 | + self.assertEqual([], maybe_perform_dhcp_discovery()) |
| 174 | self.assertIn( |
| 175 | 'Skip dhcp_discovery: Unable to find fallback nic.', |
| 176 | self.logs.getvalue()) |
| 177 | |
| 178 | def test_provided_nic_does_not_exist(self): |
| 179 | """When the provided nic doesn't exist, log a message and no-op.""" |
| 180 | - self.assertEqual({}, maybe_perform_dhcp_discovery('idontexist')) |
| 181 | + self.assertEqual([], maybe_perform_dhcp_discovery('idontexist')) |
| 182 | self.assertIn( |
| 183 | 'Skip dhcp_discovery: nic idontexist not found in get_devicelist.', |
| 184 | self.logs.getvalue()) |
| 185 | @@ -87,7 +87,7 @@ class TestDHCPDiscoveryClean(CiTestCase): |
| 186 | """When dhclient doesn't exist in the OS, log the issue and no-op.""" |
| 187 | m_fallback.return_value = 'eth9' |
| 188 | m_which.return_value = None # dhclient isn't found |
| 189 | - self.assertEqual({}, maybe_perform_dhcp_discovery()) |
| 190 | + self.assertEqual([], maybe_perform_dhcp_discovery()) |
| 191 | self.assertIn( |
| 192 | 'Skip dhclient configuration: No dhclient command found.', |
| 193 | self.logs.getvalue()) |
| 194 | @@ -117,6 +117,62 @@ class TestDHCPDiscoveryClean(CiTestCase): |
| 195 | |
| 196 | @mock.patch('cloudinit.net.dhcp.os.kill') |
| 197 | @mock.patch('cloudinit.net.dhcp.util.subp') |
| 198 | + def test_dhcp_discovery_run_in_sandbox_warns_invalid_pid(self, m_subp, |
| 199 | + m_kill): |
| 200 | + """dhcp_discovery logs a warning when pidfile contains invalid content. |
| 201 | + |
| 202 | + Lease processing still occurs and no proc kill is attempted. |
| 203 | + """ |
| 204 | + tmpdir = self.tmp_dir() |
| 205 | + dhclient_script = os.path.join(tmpdir, 'dhclient.orig') |
| 206 | + script_content = '#!/bin/bash\necho fake-dhclient' |
| 207 | + write_file(dhclient_script, script_content, mode=0o755) |
| 208 | + write_file(self.tmp_path('dhclient.pid', tmpdir), '') # Empty pid '' |
| 209 | + lease_content = dedent(""" |
| 210 | + lease { |
| 211 | + interface "eth9"; |
| 212 | + fixed-address 192.168.2.74; |
| 213 | + option subnet-mask 255.255.255.0; |
| 214 | + option routers 192.168.2.1; |
| 215 | + } |
| 216 | + """) |
| 217 | + write_file(self.tmp_path('dhcp.leases', tmpdir), lease_content) |
| 218 | + |
| 219 | + self.assertItemsEqual( |
| 220 | + [{'interface': 'eth9', 'fixed-address': '192.168.2.74', |
| 221 | + 'subnet-mask': '255.255.255.0', 'routers': '192.168.2.1'}], |
| 222 | + dhcp_discovery(dhclient_script, 'eth9', tmpdir)) |
| 223 | + self.assertIn( |
| 224 | + "pid file contains non-integer content ''", self.logs.getvalue()) |
| 225 | + m_kill.assert_not_called() |
| 226 | + |
| 227 | + @mock.patch('cloudinit.net.dhcp.os.kill') |
| 228 | + @mock.patch('cloudinit.net.dhcp.util.wait_for_files') |
| 229 | + @mock.patch('cloudinit.net.dhcp.util.subp') |
| 230 | + def test_dhcp_discovery_run_in_sandbox_waits_on_lease_and_pid(self, |
| 231 | + m_subp, |
| 232 | + m_wait, |
| 233 | + m_kill): |
| 234 | + """dhcp_discovery waits for the presence of pidfile and dhcp.leases.""" |
| 235 | + tmpdir = self.tmp_dir() |
| 236 | + dhclient_script = os.path.join(tmpdir, 'dhclient.orig') |
| 237 | + script_content = '#!/bin/bash\necho fake-dhclient' |
| 238 | + write_file(dhclient_script, script_content, mode=0o755) |
| 239 | + # Don't create pid or leases file |
| 240 | + pidfile = self.tmp_path('dhclient.pid', tmpdir) |
| 241 | + leasefile = self.tmp_path('dhcp.leases', tmpdir) |
| 242 | + m_wait.return_value = [pidfile] # Return the missing pidfile wait for |
| 243 | + self.assertEqual([], dhcp_discovery(dhclient_script, 'eth9', tmpdir)) |
| 244 | + self.assertEqual( |
| 245 | + mock.call([pidfile, leasefile], maxwait=5, naplen=0.01), |
| 246 | + m_wait.call_args_list[0]) |
| 247 | + self.assertIn( |
| 248 | + 'WARNING: dhclient did not produce expected files: dhclient.pid', |
| 249 | + self.logs.getvalue()) |
| 250 | + m_kill.assert_not_called() |
| 251 | + |
| 252 | + @mock.patch('cloudinit.net.dhcp.os.kill') |
| 253 | + @mock.patch('cloudinit.net.dhcp.util.subp') |
| 254 | def test_dhcp_discovery_run_in_sandbox(self, m_subp, m_kill): |
| 255 | """dhcp_discovery brings up the interface and runs dhclient. |
| 256 | |
| 257 | diff --git a/cloudinit/sources/DataSourceAzure.py b/cloudinit/sources/DataSourceAzure.py |
| 258 | index 8c3492d..14367e9 100644 |
| 259 | --- a/cloudinit/sources/DataSourceAzure.py |
| 260 | +++ b/cloudinit/sources/DataSourceAzure.py |
| 261 | @@ -11,7 +11,6 @@ from functools import partial |
| 262 | import os |
| 263 | import os.path |
| 264 | import re |
| 265 | -import time |
| 266 | from xml.dom import minidom |
| 267 | import xml.etree.ElementTree as ET |
| 268 | |
| 269 | @@ -321,7 +320,7 @@ class DataSourceAzure(sources.DataSource): |
| 270 | # https://bugs.launchpad.net/cloud-init/+bug/1717611 |
| 271 | missing = util.log_time(logfunc=LOG.debug, |
| 272 | msg="waiting for SSH public key files", |
| 273 | - func=wait_for_files, |
| 274 | + func=util.wait_for_files, |
| 275 | args=(fp_files, 900)) |
| 276 | |
| 277 | if len(missing): |
| 278 | @@ -556,8 +555,8 @@ def address_ephemeral_resize(devpath=RESOURCE_DISK_PATH, maxwait=120, |
| 279 | is_new_instance=False): |
| 280 | # wait for ephemeral disk to come up |
| 281 | naplen = .2 |
| 282 | - missing = wait_for_files([devpath], maxwait=maxwait, naplen=naplen, |
| 283 | - log_pre="Azure ephemeral disk: ") |
| 284 | + missing = util.wait_for_files([devpath], maxwait=maxwait, naplen=naplen, |
| 285 | + log_pre="Azure ephemeral disk: ") |
| 286 | |
| 287 | if missing: |
| 288 | LOG.warning("ephemeral device '%s' did not appear after %d seconds.", |
| 289 | @@ -639,28 +638,6 @@ def pubkeys_from_crt_files(flist): |
| 290 | return pubkeys |
| 291 | |
| 292 | |
| 293 | -def wait_for_files(flist, maxwait, naplen=.5, log_pre=""): |
| 294 | - need = set(flist) |
| 295 | - waited = 0 |
| 296 | - while True: |
| 297 | - need -= set([f for f in need if os.path.exists(f)]) |
| 298 | - if len(need) == 0: |
| 299 | - LOG.debug("%sAll files appeared after %s seconds: %s", |
| 300 | - log_pre, waited, flist) |
| 301 | - return [] |
| 302 | - if waited == 0: |
| 303 | - LOG.info("%sWaiting up to %s seconds for the following files: %s", |
| 304 | - log_pre, maxwait, flist) |
| 305 | - if waited + naplen > maxwait: |
| 306 | - break |
| 307 | - time.sleep(naplen) |
| 308 | - waited += naplen |
| 309 | - |
| 310 | - LOG.warning("%sStill missing files after %s seconds: %s", |
| 311 | - log_pre, maxwait, need) |
| 312 | - return need |
| 313 | - |
| 314 | - |
| 315 | def write_files(datadir, files, dirmode=None): |
| 316 | |
| 317 | def _redact_password(cnt, fname): |
| 318 | diff --git a/cloudinit/util.py b/cloudinit/util.py |
| 319 | index e1290aa..6c014ba 100644 |
| 320 | --- a/cloudinit/util.py |
| 321 | +++ b/cloudinit/util.py |
| 322 | @@ -2541,4 +2541,26 @@ def load_shell_content(content, add_empty=False, empty_val=None): |
| 323 | return data |
| 324 | |
| 325 | |
| 326 | +def wait_for_files(flist, maxwait, naplen=.5, log_pre=""): |
| 327 | + need = set(flist) |
| 328 | + waited = 0 |
| 329 | + while True: |
| 330 | + need -= set([f for f in need if os.path.exists(f)]) |
| 331 | + if len(need) == 0: |
| 332 | + LOG.debug("%sAll files appeared after %s seconds: %s", |
| 333 | + log_pre, waited, flist) |
| 334 | + return [] |
| 335 | + if waited == 0: |
| 336 | + LOG.debug("%sWaiting up to %s seconds for the following files: %s", |
| 337 | + log_pre, maxwait, flist) |
| 338 | + if waited + naplen > maxwait: |
| 339 | + break |
| 340 | + time.sleep(naplen) |
| 341 | + waited += naplen |
| 342 | + |
| 343 | + LOG.debug("%sStill missing files after %s seconds: %s", |
| 344 | + log_pre, maxwait, need) |
| 345 | + return need |
| 346 | + |
| 347 | + |
| 348 | # vi: ts=4 expandtab |
| 349 | diff --git a/debian/changelog b/debian/changelog |
| 350 | index fc93879..4954b20 100644 |
| 351 | --- a/debian/changelog |
| 352 | +++ b/debian/changelog |
| 353 | @@ -1,3 +1,16 @@ |
| 354 | +cloud-init (17.1-46-g7acc9e68-0ubuntu1~17.04.1) zesty-proposed; urgency=medium |
| 355 | + |
| 356 | + * New upstream snapshot. |
| 357 | + - ec2: Fix sandboxed dhclient background process cleanup. |
| 358 | + (LP: #1735331) |
| 359 | + - tests: NoCloudKVMImage do not modify the original local cache image. |
| 360 | + - tests: Enable bionic in integration tests. [Joshua Powers] |
| 361 | + - tests: Use apt-get to install a deb so that depends get resolved. |
| 362 | + - sysconfig: Correctly render dns and dns search info. |
| 363 | + [Ryan McCabe] |
| 364 | + |
| 365 | + -- Chad Smith <chad.smith@canonical.com> Fri, 01 Dec 2017 10:02:24 -0700 |
| 366 | + |
| 367 | cloud-init (17.1-41-g76243487-0ubuntu1~17.04.1) zesty-proposed; urgency=medium |
| 368 | |
| 369 | * debian/cloud-init.templates: Fix capitilazation in 'AliYun' |
| 370 | diff --git a/tests/cloud_tests/images/nocloudkvm.py b/tests/cloud_tests/images/nocloudkvm.py |
| 371 | index 1e7962c..8678b07 100644 |
| 372 | --- a/tests/cloud_tests/images/nocloudkvm.py |
| 373 | +++ b/tests/cloud_tests/images/nocloudkvm.py |
| 374 | @@ -4,6 +4,10 @@ |
| 375 | |
| 376 | from cloudinit import util as c_util |
| 377 | |
| 378 | +import os |
| 379 | +import shutil |
| 380 | +import tempfile |
| 381 | + |
| 382 | from tests.cloud_tests.images import base |
| 383 | from tests.cloud_tests.snapshots import nocloudkvm as nocloud_kvm_snapshot |
| 384 | |
| 385 | @@ -13,7 +17,7 @@ class NoCloudKVMImage(base.Image): |
| 386 | |
| 387 | platform_name = "nocloud-kvm" |
| 388 | |
| 389 | - def __init__(self, platform, config, img_path): |
| 390 | + def __init__(self, platform, config, orig_img_path): |
| 391 | """Set up image. |
| 392 | |
| 393 | @param platform: platform object |
| 394 | @@ -21,7 +25,13 @@ class NoCloudKVMImage(base.Image): |
| 395 | @param img_path: path to the image |
| 396 | """ |
| 397 | self.modified = False |
| 398 | - self._img_path = img_path |
| 399 | + self._workd = tempfile.mkdtemp(prefix='NoCloudKVMImage') |
| 400 | + self._orig_img_path = orig_img_path |
| 401 | + self._img_path = os.path.join(self._workd, |
| 402 | + os.path.basename(self._orig_img_path)) |
| 403 | + |
| 404 | + c_util.subp(['qemu-img', 'create', '-f', 'qcow2', |
| 405 | + '-b', orig_img_path, self._img_path]) |
| 406 | |
| 407 | super(NoCloudKVMImage, self).__init__(platform, config) |
| 408 | |
| 409 | @@ -61,13 +71,9 @@ class NoCloudKVMImage(base.Image): |
| 410 | if not self._img_path: |
| 411 | raise RuntimeError() |
| 412 | |
| 413 | - instance = self.platform.create_image( |
| 414 | - self.properties, self.config, self.features, |
| 415 | - self._img_path, image_desc=str(self), use_desc='snapshot') |
| 416 | - |
| 417 | return nocloud_kvm_snapshot.NoCloudKVMSnapshot( |
| 418 | self.platform, self.properties, self.config, |
| 419 | - self.features, instance) |
| 420 | + self.features, self._img_path) |
| 421 | |
| 422 | def destroy(self): |
| 423 | """Unset path to signal image is no longer used. |
| 424 | @@ -77,6 +83,8 @@ class NoCloudKVMImage(base.Image): |
| 425 | framework decide whether to keep or destroy everything. |
| 426 | """ |
| 427 | self._img_path = None |
| 428 | + shutil.rmtree(self._workd) |
| 429 | + |
| 430 | super(NoCloudKVMImage, self).destroy() |
| 431 | |
| 432 | # vi: ts=4 expandtab |
| 433 | diff --git a/tests/cloud_tests/instances/nocloudkvm.py b/tests/cloud_tests/instances/nocloudkvm.py |
| 434 | index cc82580..bc06a79 100644 |
| 435 | --- a/tests/cloud_tests/instances/nocloudkvm.py |
| 436 | +++ b/tests/cloud_tests/instances/nocloudkvm.py |
| 437 | @@ -25,12 +25,13 @@ class NoCloudKVMInstance(base.Instance): |
| 438 | platform_name = "nocloud-kvm" |
| 439 | _ssh_client = None |
| 440 | |
| 441 | - def __init__(self, platform, name, properties, config, features, |
| 442 | - user_data, meta_data): |
| 443 | + def __init__(self, platform, name, image_path, properties, config, |
| 444 | + features, user_data, meta_data): |
| 445 | """Set up instance. |
| 446 | |
| 447 | @param platform: platform object |
| 448 | @param name: image path |
| 449 | + @param image_path: path to disk image to boot. |
| 450 | @param properties: dictionary of properties |
| 451 | @param config: dictionary of configuration values |
| 452 | @param features: dictionary of supported feature flags |
| 453 | @@ -43,6 +44,7 @@ class NoCloudKVMInstance(base.Instance): |
| 454 | self.pid = None |
| 455 | self.pid_file = None |
| 456 | self.console_file = None |
| 457 | + self.disk = image_path |
| 458 | |
| 459 | super(NoCloudKVMInstance, self).__init__( |
| 460 | platform, name, properties, config, features) |
| 461 | @@ -145,7 +147,7 @@ class NoCloudKVMInstance(base.Instance): |
| 462 | self.ssh_port = self.get_free_port() |
| 463 | |
| 464 | cmd = ['./tools/xkvm', |
| 465 | - '--disk', '%s,cache=unsafe' % self.name, |
| 466 | + '--disk', '%s,cache=unsafe' % self.disk, |
| 467 | '--disk', '%s,cache=unsafe' % seed, |
| 468 | '--netdev', ','.join(['user', |
| 469 | 'hostfwd=tcp::%s-:22' % self.ssh_port, |
| 470 | diff --git a/tests/cloud_tests/platforms/nocloudkvm.py b/tests/cloud_tests/platforms/nocloudkvm.py |
| 471 | index f1f8187..76cd83a 100644 |
| 472 | --- a/tests/cloud_tests/platforms/nocloudkvm.py |
| 473 | +++ b/tests/cloud_tests/platforms/nocloudkvm.py |
| 474 | @@ -55,19 +55,20 @@ class NoCloudKVMPlatform(base.Platform): |
| 475 | for fname in glob.iglob(search_d, recursive=True): |
| 476 | images.append(fname) |
| 477 | |
| 478 | - if len(images) != 1: |
| 479 | - raise Exception('No unique images found') |
| 480 | + if len(images) < 1: |
| 481 | + raise RuntimeError("No images found under '%s'" % search_d) |
| 482 | + if len(images) > 1: |
| 483 | + raise RuntimeError( |
| 484 | + "Multiple images found in '%s': %s" % (search_d, |
| 485 | + ' '.join(images))) |
| 486 | |
| 487 | image = nocloud_kvm_image.NoCloudKVMImage(self, img_conf, images[0]) |
| 488 | - if img_conf.get('override_templates', False): |
| 489 | - image.update_templates(self.config.get('template_overrides', {}), |
| 490 | - self.config.get('template_files', {})) |
| 491 | return image |
| 492 | |
| 493 | - def create_image(self, properties, config, features, |
| 494 | - src_img_path, image_desc=None, use_desc=None, |
| 495 | - user_data=None, meta_data=None): |
| 496 | - """Create an image |
| 497 | + def create_instance(self, properties, config, features, |
| 498 | + src_img_path, image_desc=None, use_desc=None, |
| 499 | + user_data=None, meta_data=None): |
| 500 | + """Create an instance |
| 501 | |
| 502 | @param src_img_path: image path to launch from |
| 503 | @param properties: image properties |
| 504 | @@ -82,7 +83,7 @@ class NoCloudKVMPlatform(base.Platform): |
| 505 | c_util.subp(['qemu-img', 'create', '-f', 'qcow2', |
| 506 | '-b', src_img_path, img_path]) |
| 507 | |
| 508 | - return nocloud_kvm_instance.NoCloudKVMInstance(self, img_path, |
| 509 | + return nocloud_kvm_instance.NoCloudKVMInstance(self, name, img_path, |
| 510 | properties, config, |
| 511 | features, user_data, |
| 512 | meta_data) |
| 513 | diff --git a/tests/cloud_tests/releases.yaml b/tests/cloud_tests/releases.yaml |
| 514 | index ec7e2d5..e593380 100644 |
| 515 | --- a/tests/cloud_tests/releases.yaml |
| 516 | +++ b/tests/cloud_tests/releases.yaml |
| 517 | @@ -122,6 +122,22 @@ features: |
| 518 | |
| 519 | releases: |
| 520 | # UBUNTU ================================================================= |
| 521 | + bionic: |
| 522 | + # EOL: Apr 2023 |
| 523 | + default: |
| 524 | + enabled: true |
| 525 | + release: bionic |
| 526 | + version: 18.04 |
| 527 | + family: ubuntu |
| 528 | + feature_groups: |
| 529 | + - base |
| 530 | + - debian_base |
| 531 | + - ubuntu_specific |
| 532 | + lxd: |
| 533 | + sstreams_server: https://cloud-images.ubuntu.com/daily |
| 534 | + alias: bionic |
| 535 | + setup_overrides: null |
| 536 | + override_templates: false |
| 537 | artful: |
| 538 | # EOL: Jul 2018 |
| 539 | default: |
| 540 | diff --git a/tests/cloud_tests/setup_image.py b/tests/cloud_tests/setup_image.py |
| 541 | index 6672ffb..179f40d 100644 |
| 542 | --- a/tests/cloud_tests/setup_image.py |
| 543 | +++ b/tests/cloud_tests/setup_image.py |
| 544 | @@ -50,9 +50,9 @@ def install_deb(args, image): |
| 545 | LOG.debug(msg) |
| 546 | remote_path = os.path.join('/tmp', os.path.basename(args.deb)) |
| 547 | image.push_file(args.deb, remote_path) |
| 548 | - cmd = 'dpkg -i {}; apt-get install --yes -f'.format(remote_path) |
| 549 | - image.execute(cmd, description=msg) |
| 550 | - |
| 551 | + image.execute( |
| 552 | + ['apt-get', 'install', '--allow-downgrades', '--assume-yes', |
| 553 | + remote_path], description=msg) |
| 554 | # check installed deb version matches package |
| 555 | fmt = ['-W', "--showformat=${Version}"] |
| 556 | (out, err, exit) = image.execute(['dpkg-deb'] + fmt + [remote_path]) |
| 557 | diff --git a/tests/cloud_tests/snapshots/nocloudkvm.py b/tests/cloud_tests/snapshots/nocloudkvm.py |
| 558 | index 0999834..21e908d 100644 |
| 559 | --- a/tests/cloud_tests/snapshots/nocloudkvm.py |
| 560 | +++ b/tests/cloud_tests/snapshots/nocloudkvm.py |
| 561 | @@ -2,6 +2,8 @@ |
| 562 | |
| 563 | """Base NoCloud KVM snapshot.""" |
| 564 | import os |
| 565 | +import shutil |
| 566 | +import tempfile |
| 567 | |
| 568 | from tests.cloud_tests.snapshots import base |
| 569 | |
| 570 | @@ -11,16 +13,19 @@ class NoCloudKVMSnapshot(base.Snapshot): |
| 571 | |
| 572 | platform_name = "nocloud-kvm" |
| 573 | |
| 574 | - def __init__(self, platform, properties, config, features, |
| 575 | - instance): |
| 576 | + def __init__(self, platform, properties, config, features, image_path): |
| 577 | """Set up snapshot. |
| 578 | |
| 579 | @param platform: platform object |
| 580 | @param properties: image properties |
| 581 | @param config: image config |
| 582 | @param features: supported feature flags |
| 583 | + @param image_path: image file to snapshot. |
| 584 | """ |
| 585 | - self.instance = instance |
| 586 | + self._workd = tempfile.mkdtemp(prefix='NoCloudKVMSnapshot') |
| 587 | + snapshot = os.path.join(self._workd, 'snapshot') |
| 588 | + shutil.copyfile(image_path, snapshot) |
| 589 | + self._image_path = snapshot |
| 590 | |
| 591 | super(NoCloudKVMSnapshot, self).__init__( |
| 592 | platform, properties, config, features) |
| 593 | @@ -40,9 +45,9 @@ class NoCloudKVMSnapshot(base.Snapshot): |
| 594 | self.platform.config['public_key']) |
| 595 | user_data = self.inject_ssh_key(user_data, key_file) |
| 596 | |
| 597 | - instance = self.platform.create_image( |
| 598 | + instance = self.platform.create_instance( |
| 599 | self.properties, self.config, self.features, |
| 600 | - self.instance.name, image_desc=str(self), use_desc=use_desc, |
| 601 | + self._image_path, image_desc=str(self), use_desc=use_desc, |
| 602 | user_data=user_data, meta_data=meta_data) |
| 603 | |
| 604 | if start: |
| 605 | @@ -68,7 +73,7 @@ class NoCloudKVMSnapshot(base.Snapshot): |
| 606 | |
| 607 | def destroy(self): |
| 608 | """Clean up snapshot data.""" |
| 609 | - self.instance.destroy() |
| 610 | + shutil.rmtree(self._workd) |
| 611 | super(NoCloudKVMSnapshot, self).destroy() |
| 612 | |
| 613 | # vi: ts=4 expandtab |
| 614 | diff --git a/tests/unittests/test_datasource/test_azure.py b/tests/unittests/test_datasource/test_azure.py |
| 615 | index 0a11777..7cb1812 100644 |
| 616 | --- a/tests/unittests/test_datasource/test_azure.py |
| 617 | +++ b/tests/unittests/test_datasource/test_azure.py |
| 618 | @@ -171,7 +171,6 @@ scbus-1 on xpt0 bus 0 |
| 619 | self.apply_patches([ |
| 620 | (dsaz, 'list_possible_azure_ds_devs', dsdevs), |
| 621 | (dsaz, 'invoke_agent', _invoke_agent), |
| 622 | - (dsaz, 'wait_for_files', _wait_for_files), |
| 623 | (dsaz, 'pubkeys_from_crt_files', _pubkeys_from_crt_files), |
| 624 | (dsaz, 'perform_hostname_bounce', mock.MagicMock()), |
| 625 | (dsaz, 'get_hostname', mock.MagicMock()), |
| 626 | @@ -179,6 +178,8 @@ scbus-1 on xpt0 bus 0 |
| 627 | (dsaz, 'get_metadata_from_fabric', self.get_metadata_from_fabric), |
| 628 | (dsaz.util, 'read_dmi_data', mock.MagicMock( |
| 629 | side_effect=_dmi_mocks)), |
| 630 | + (dsaz.util, 'wait_for_files', mock.MagicMock( |
| 631 | + side_effect=_wait_for_files)), |
| 632 | ]) |
| 633 | |
| 634 | dsrc = dsaz.DataSourceAzure( |
| 635 | @@ -647,7 +648,7 @@ class TestAzureBounce(TestCase): |
| 636 | self.patches.enter_context( |
| 637 | mock.patch.object(dsaz, 'invoke_agent')) |
| 638 | self.patches.enter_context( |
| 639 | - mock.patch.object(dsaz, 'wait_for_files')) |
| 640 | + mock.patch.object(dsaz.util, 'wait_for_files')) |
| 641 | self.patches.enter_context( |
| 642 | mock.patch.object(dsaz, 'list_possible_azure_ds_devs', |
| 643 | mock.MagicMock(return_value=[]))) |
| 644 | diff --git a/tests/unittests/test_net.py b/tests/unittests/test_net.py |
| 645 | index bbb63cb..f3fa2a3 100644 |
| 646 | --- a/tests/unittests/test_net.py |
| 647 | +++ b/tests/unittests/test_net.py |
| 648 | @@ -436,6 +436,9 @@ NETWORK_CONFIGS = { |
| 649 | BOOTPROTO=dhcp |
| 650 | DEFROUTE=yes |
| 651 | DEVICE=eth99 |
| 652 | + DNS1=8.8.8.8 |
| 653 | + DNS2=8.8.4.4 |
| 654 | + DOMAIN="barley.maas sach.maas" |
| 655 | GATEWAY=65.61.151.37 |
| 656 | HWADDR=c0:d6:9f:2c:e8:80 |
| 657 | IPADDR=192.168.21.3 |
| 658 | @@ -836,6 +839,9 @@ pre-down route del -net 10.0.0.0 netmask 255.0.0.0 gw 11.0.0.1 metric 3 || true |
| 659 | BOOTPROTO=none |
| 660 | DEFROUTE=yes |
| 661 | DEVICE=eth0.101 |
| 662 | + DNS1=192.168.0.10 |
| 663 | + DNS2=10.23.23.134 |
| 664 | + DOMAIN="barley.maas sacchromyces.maas brettanomyces.maas" |
| 665 | GATEWAY=192.168.0.1 |
| 666 | IPADDR=192.168.0.2 |
| 667 | IPADDR1=192.168.2.10 |


PASSED: Continuous integration, rev:40551a784ad 473b437e5ddd68f 811fd2255f896f /jenkins. ubuntu. com/server/ job/cloud- init-ci/ 573/
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/ 573/rebuild
https:/