Merge ~chad.smith/cloud-init:ubuntu/artful into cloud-init:ubuntu/artful

Proposed by Chad Smith
Status: Merged
Merged at revision: c491d8344104610bfd3ea0cb48ca4403c789a23d
Proposed branch: ~chad.smith/cloud-init:ubuntu/artful
Merge into: cloud-init:ubuntu/artful
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)
Reviewer Review Type Date Requested Status
Server Team CI bot continuous-integration Approve
Scott Moser Pending
Review via email: mp+334594@code.launchpad.net

Description of the change

Upstream snapshot for SRU into Artful

To post a comment you must log in.
Revision history for this message
Server Team CI bot (server-team-bot) wrote :

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

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

review: Approve (continuous-integration)

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1diff --git a/cloudinit/net/dhcp.py b/cloudinit/net/dhcp.py
2index 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):
89diff --git a/cloudinit/net/network_state.py b/cloudinit/net/network_state.py
90index 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
108diff --git a/cloudinit/net/sysconfig.py b/cloudinit/net/sysconfig.py
109index 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)):
147diff --git a/cloudinit/net/tests/test_dhcp.py b/cloudinit/net/tests/test_dhcp.py
148index 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
257diff --git a/cloudinit/sources/DataSourceAzure.py b/cloudinit/sources/DataSourceAzure.py
258index 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):
318diff --git a/cloudinit/util.py b/cloudinit/util.py
319index 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
349diff --git a/debian/changelog b/debian/changelog
350index b209fa9..ea72cf2 100644
351--- a/debian/changelog
352+++ b/debian/changelog
353@@ -1,3 +1,16 @@
354+cloud-init (17.1-46-g7acc9e68-0ubuntu1~17.10.1) artful-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 09:58:48 -0700
366+
367 cloud-init (17.1-41-g76243487-0ubuntu1~17.10.1) artful-proposed; urgency=medium
368
369 * debian/cloud-init.templates: Fix capitilazation in 'AliYun'
370diff --git a/tests/cloud_tests/images/nocloudkvm.py b/tests/cloud_tests/images/nocloudkvm.py
371index 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
433diff --git a/tests/cloud_tests/instances/nocloudkvm.py b/tests/cloud_tests/instances/nocloudkvm.py
434index 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,
470diff --git a/tests/cloud_tests/platforms/nocloudkvm.py b/tests/cloud_tests/platforms/nocloudkvm.py
471index 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)
513diff --git a/tests/cloud_tests/releases.yaml b/tests/cloud_tests/releases.yaml
514index 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:
540diff --git a/tests/cloud_tests/setup_image.py b/tests/cloud_tests/setup_image.py
541index 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])
557diff --git a/tests/cloud_tests/snapshots/nocloudkvm.py b/tests/cloud_tests/snapshots/nocloudkvm.py
558index 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
614diff --git a/tests/unittests/test_datasource/test_azure.py b/tests/unittests/test_datasource/test_azure.py
615index 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=[])))
644diff --git a/tests/unittests/test_net.py b/tests/unittests/test_net.py
645index 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

Subscribers

People subscribed via source and target branches