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
diff --git a/cloudinit/net/dhcp.py b/cloudinit/net/dhcp.py
index d8624d8..875a460 100644
--- a/cloudinit/net/dhcp.py
+++ b/cloudinit/net/dhcp.py
@@ -36,22 +36,23 @@ def maybe_perform_dhcp_discovery(nic=None):
36 skip dhcp_discovery and return an empty dict.36 skip dhcp_discovery and return an empty dict.
3737
38 @param nic: Name of the network interface we want to run dhclient on.38 @param nic: Name of the network interface we want to run dhclient on.
39 @return: A dict of dhcp options from the dhclient discovery if run,39 @return: A list of dicts representing dhcp options for each lease obtained
40 otherwise an empty dict is returned.40 from the dhclient discovery if run, otherwise an empty list is
41 returned.
41 """42 """
42 if nic is None:43 if nic is None:
43 nic = find_fallback_nic()44 nic = find_fallback_nic()
44 if nic is None:45 if nic is None:
45 LOG.debug('Skip dhcp_discovery: Unable to find fallback nic.')46 LOG.debug('Skip dhcp_discovery: Unable to find fallback nic.')
46 return {}47 return []
47 elif nic not in get_devicelist():48 elif nic not in get_devicelist():
48 LOG.debug(49 LOG.debug(
49 'Skip dhcp_discovery: nic %s not found in get_devicelist.', nic)50 'Skip dhcp_discovery: nic %s not found in get_devicelist.', nic)
50 return {}51 return []
51 dhclient_path = util.which('dhclient')52 dhclient_path = util.which('dhclient')
52 if not dhclient_path:53 if not dhclient_path:
53 LOG.debug('Skip dhclient configuration: No dhclient command found.')54 LOG.debug('Skip dhclient configuration: No dhclient command found.')
54 return {}55 return []
55 with temp_utils.tempdir(prefix='cloud-init-dhcp-', needs_exe=True) as tdir:56 with temp_utils.tempdir(prefix='cloud-init-dhcp-', needs_exe=True) as tdir:
56 # Use /var/tmp because /run/cloud-init/tmp is mounted noexec57 # Use /var/tmp because /run/cloud-init/tmp is mounted noexec
57 return dhcp_discovery(dhclient_path, nic, tdir)58 return dhcp_discovery(dhclient_path, nic, tdir)
@@ -60,8 +61,8 @@ def maybe_perform_dhcp_discovery(nic=None):
60def parse_dhcp_lease_file(lease_file):61def parse_dhcp_lease_file(lease_file):
61 """Parse the given dhcp lease file for the most recent lease.62 """Parse the given dhcp lease file for the most recent lease.
6263
63 Return a dict of dhcp options as key value pairs for the most recent lease64 Return a list of dicts of dhcp options. Each dict contains key value pairs
64 block.65 a specific lease in order from oldest to newest.
6566
66 @raises: InvalidDHCPLeaseFileError on empty of unparseable leasefile67 @raises: InvalidDHCPLeaseFileError on empty of unparseable leasefile
67 content.68 content.
@@ -96,8 +97,8 @@ def dhcp_discovery(dhclient_cmd_path, interface, cleandir):
96 @param cleandir: The directory from which to run dhclient as well as store97 @param cleandir: The directory from which to run dhclient as well as store
97 dhcp leases.98 dhcp leases.
9899
99 @return: A dict of dhcp options parsed from the dhcp.leases file or empty100 @return: A list of dicts of representing the dhcp leases parsed from the
100 dict.101 dhcp.leases file or empty list.
101 """102 """
102 LOG.debug('Performing a dhcp discovery on %s', interface)103 LOG.debug('Performing a dhcp discovery on %s', interface)
103104
@@ -119,13 +120,26 @@ def dhcp_discovery(dhclient_cmd_path, interface, cleandir):
119 cmd = [sandbox_dhclient_cmd, '-1', '-v', '-lf', lease_file,120 cmd = [sandbox_dhclient_cmd, '-1', '-v', '-lf', lease_file,
120 '-pf', pid_file, interface, '-sf', '/bin/true']121 '-pf', pid_file, interface, '-sf', '/bin/true']
121 util.subp(cmd, capture=True)122 util.subp(cmd, capture=True)
122 pid = None123
124 # dhclient doesn't write a pid file until after it forks when it gets a
125 # proper lease response. Since cleandir is a temp directory that gets
126 # removed, we need to wait for that pidfile creation before the
127 # cleandir is removed, otherwise we get FileNotFound errors.
128 missing = util.wait_for_files(
129 [pid_file, lease_file], maxwait=5, naplen=0.01)
130 if missing:
131 LOG.warning("dhclient did not produce expected files: %s",
132 ', '.join(os.path.basename(f) for f in missing))
133 return []
134 pid_content = util.load_file(pid_file).strip()
123 try:135 try:
124 pid = int(util.load_file(pid_file).strip())136 pid = int(pid_content)
125 return parse_dhcp_lease_file(lease_file)137 except ValueError:
126 finally:138 LOG.debug(
127 if pid:139 "pid file contains non-integer content '%s'", pid_content)
128 os.kill(pid, signal.SIGKILL)140 else:
141 os.kill(pid, signal.SIGKILL)
142 return parse_dhcp_lease_file(lease_file)
129143
130144
131def networkd_parse_lease(content):145def networkd_parse_lease(content):
diff --git a/cloudinit/net/network_state.py b/cloudinit/net/network_state.py
index 0e830ee..e9e2cf4 100644
--- a/cloudinit/net/network_state.py
+++ b/cloudinit/net/network_state.py
@@ -746,6 +746,14 @@ def _normalize_subnet(subnet):
746 _normalize_net_keys(normal_subnet, address_keys=('address',)))746 _normalize_net_keys(normal_subnet, address_keys=('address',)))
747 normal_subnet['routes'] = [_normalize_route(r)747 normal_subnet['routes'] = [_normalize_route(r)
748 for r in subnet.get('routes', [])]748 for r in subnet.get('routes', [])]
749
750 def listify(snet, name):
751 if name in snet and not isinstance(snet[name], list):
752 snet[name] = snet[name].split()
753
754 for k in ('dns_search', 'dns_nameservers'):
755 listify(normal_subnet, k)
756
749 return normal_subnet757 return normal_subnet
750758
751759
diff --git a/cloudinit/net/sysconfig.py b/cloudinit/net/sysconfig.py
index f572796..39d89c4 100644
--- a/cloudinit/net/sysconfig.py
+++ b/cloudinit/net/sysconfig.py
@@ -7,12 +7,15 @@ import six
77
8from cloudinit.distros.parsers import networkmanager_conf8from cloudinit.distros.parsers import networkmanager_conf
9from cloudinit.distros.parsers import resolv_conf9from cloudinit.distros.parsers import resolv_conf
10from cloudinit import log as logging
10from cloudinit import util11from cloudinit import util
1112
12from . import renderer13from . import renderer
13from .network_state import (14from .network_state import (
14 is_ipv6_addr, net_prefix_to_ipv4_mask, subnet_is_ipv6)15 is_ipv6_addr, net_prefix_to_ipv4_mask, subnet_is_ipv6)
1516
17LOG = logging.getLogger(__name__)
18
1619
17def _make_header(sep='#'):20def _make_header(sep='#'):
18 lines = [21 lines = [
@@ -347,6 +350,18 @@ class Renderer(renderer.Renderer):
347 else:350 else:
348 iface_cfg['GATEWAY'] = subnet['gateway']351 iface_cfg['GATEWAY'] = subnet['gateway']
349352
353 if 'dns_search' in subnet:
354 iface_cfg['DOMAIN'] = ' '.join(subnet['dns_search'])
355
356 if 'dns_nameservers' in subnet:
357 if len(subnet['dns_nameservers']) > 3:
358 # per resolv.conf(5) MAXNS sets this to 3.
359 LOG.debug("%s has %d entries in dns_nameservers. "
360 "Only 3 are used.", iface_cfg.name,
361 len(subnet['dns_nameservers']))
362 for i, k in enumerate(subnet['dns_nameservers'][:3], 1):
363 iface_cfg['DNS' + str(i)] = k
364
350 @classmethod365 @classmethod
351 def _render_subnet_routes(cls, iface_cfg, route_cfg, subnets):366 def _render_subnet_routes(cls, iface_cfg, route_cfg, subnets):
352 for i, subnet in enumerate(subnets, start=len(iface_cfg.children)):367 for i, subnet in enumerate(subnets, start=len(iface_cfg.children)):
diff --git a/cloudinit/net/tests/test_dhcp.py b/cloudinit/net/tests/test_dhcp.py
index 3d8e15c..db25b6f 100644
--- a/cloudinit/net/tests/test_dhcp.py
+++ b/cloudinit/net/tests/test_dhcp.py
@@ -1,6 +1,5 @@
1# This file is part of cloud-init. See LICENSE file for license information.1# This file is part of cloud-init. See LICENSE file for license information.
22
3import mock
4import os3import os
5import signal4import signal
6from textwrap import dedent5from textwrap import dedent
@@ -9,7 +8,8 @@ from cloudinit.net.dhcp import (
9 InvalidDHCPLeaseFileError, maybe_perform_dhcp_discovery,8 InvalidDHCPLeaseFileError, maybe_perform_dhcp_discovery,
10 parse_dhcp_lease_file, dhcp_discovery, networkd_load_leases)9 parse_dhcp_lease_file, dhcp_discovery, networkd_load_leases)
11from cloudinit.util import ensure_file, write_file10from cloudinit.util import ensure_file, write_file
12from cloudinit.tests.helpers import CiTestCase, wrap_and_call, populate_dir11from cloudinit.tests.helpers import (
12 CiTestCase, mock, populate_dir, wrap_and_call)
1313
1414
15class TestParseDHCPLeasesFile(CiTestCase):15class TestParseDHCPLeasesFile(CiTestCase):
@@ -69,14 +69,14 @@ class TestDHCPDiscoveryClean(CiTestCase):
69 def test_no_fallback_nic_found(self, m_fallback_nic):69 def test_no_fallback_nic_found(self, m_fallback_nic):
70 """Log and do nothing when nic is absent and no fallback is found."""70 """Log and do nothing when nic is absent and no fallback is found."""
71 m_fallback_nic.return_value = None # No fallback nic found71 m_fallback_nic.return_value = None # No fallback nic found
72 self.assertEqual({}, maybe_perform_dhcp_discovery())72 self.assertEqual([], maybe_perform_dhcp_discovery())
73 self.assertIn(73 self.assertIn(
74 'Skip dhcp_discovery: Unable to find fallback nic.',74 'Skip dhcp_discovery: Unable to find fallback nic.',
75 self.logs.getvalue())75 self.logs.getvalue())
7676
77 def test_provided_nic_does_not_exist(self):77 def test_provided_nic_does_not_exist(self):
78 """When the provided nic doesn't exist, log a message and no-op."""78 """When the provided nic doesn't exist, log a message and no-op."""
79 self.assertEqual({}, maybe_perform_dhcp_discovery('idontexist'))79 self.assertEqual([], maybe_perform_dhcp_discovery('idontexist'))
80 self.assertIn(80 self.assertIn(
81 'Skip dhcp_discovery: nic idontexist not found in get_devicelist.',81 'Skip dhcp_discovery: nic idontexist not found in get_devicelist.',
82 self.logs.getvalue())82 self.logs.getvalue())
@@ -87,7 +87,7 @@ class TestDHCPDiscoveryClean(CiTestCase):
87 """When dhclient doesn't exist in the OS, log the issue and no-op."""87 """When dhclient doesn't exist in the OS, log the issue and no-op."""
88 m_fallback.return_value = 'eth9'88 m_fallback.return_value = 'eth9'
89 m_which.return_value = None # dhclient isn't found89 m_which.return_value = None # dhclient isn't found
90 self.assertEqual({}, maybe_perform_dhcp_discovery())90 self.assertEqual([], maybe_perform_dhcp_discovery())
91 self.assertIn(91 self.assertIn(
92 'Skip dhclient configuration: No dhclient command found.',92 'Skip dhclient configuration: No dhclient command found.',
93 self.logs.getvalue())93 self.logs.getvalue())
@@ -117,6 +117,62 @@ class TestDHCPDiscoveryClean(CiTestCase):
117117
118 @mock.patch('cloudinit.net.dhcp.os.kill')118 @mock.patch('cloudinit.net.dhcp.os.kill')
119 @mock.patch('cloudinit.net.dhcp.util.subp')119 @mock.patch('cloudinit.net.dhcp.util.subp')
120 def test_dhcp_discovery_run_in_sandbox_warns_invalid_pid(self, m_subp,
121 m_kill):
122 """dhcp_discovery logs a warning when pidfile contains invalid content.
123
124 Lease processing still occurs and no proc kill is attempted.
125 """
126 tmpdir = self.tmp_dir()
127 dhclient_script = os.path.join(tmpdir, 'dhclient.orig')
128 script_content = '#!/bin/bash\necho fake-dhclient'
129 write_file(dhclient_script, script_content, mode=0o755)
130 write_file(self.tmp_path('dhclient.pid', tmpdir), '') # Empty pid ''
131 lease_content = dedent("""
132 lease {
133 interface "eth9";
134 fixed-address 192.168.2.74;
135 option subnet-mask 255.255.255.0;
136 option routers 192.168.2.1;
137 }
138 """)
139 write_file(self.tmp_path('dhcp.leases', tmpdir), lease_content)
140
141 self.assertItemsEqual(
142 [{'interface': 'eth9', 'fixed-address': '192.168.2.74',
143 'subnet-mask': '255.255.255.0', 'routers': '192.168.2.1'}],
144 dhcp_discovery(dhclient_script, 'eth9', tmpdir))
145 self.assertIn(
146 "pid file contains non-integer content ''", self.logs.getvalue())
147 m_kill.assert_not_called()
148
149 @mock.patch('cloudinit.net.dhcp.os.kill')
150 @mock.patch('cloudinit.net.dhcp.util.wait_for_files')
151 @mock.patch('cloudinit.net.dhcp.util.subp')
152 def test_dhcp_discovery_run_in_sandbox_waits_on_lease_and_pid(self,
153 m_subp,
154 m_wait,
155 m_kill):
156 """dhcp_discovery waits for the presence of pidfile and dhcp.leases."""
157 tmpdir = self.tmp_dir()
158 dhclient_script = os.path.join(tmpdir, 'dhclient.orig')
159 script_content = '#!/bin/bash\necho fake-dhclient'
160 write_file(dhclient_script, script_content, mode=0o755)
161 # Don't create pid or leases file
162 pidfile = self.tmp_path('dhclient.pid', tmpdir)
163 leasefile = self.tmp_path('dhcp.leases', tmpdir)
164 m_wait.return_value = [pidfile] # Return the missing pidfile wait for
165 self.assertEqual([], dhcp_discovery(dhclient_script, 'eth9', tmpdir))
166 self.assertEqual(
167 mock.call([pidfile, leasefile], maxwait=5, naplen=0.01),
168 m_wait.call_args_list[0])
169 self.assertIn(
170 'WARNING: dhclient did not produce expected files: dhclient.pid',
171 self.logs.getvalue())
172 m_kill.assert_not_called()
173
174 @mock.patch('cloudinit.net.dhcp.os.kill')
175 @mock.patch('cloudinit.net.dhcp.util.subp')
120 def test_dhcp_discovery_run_in_sandbox(self, m_subp, m_kill):176 def test_dhcp_discovery_run_in_sandbox(self, m_subp, m_kill):
121 """dhcp_discovery brings up the interface and runs dhclient.177 """dhcp_discovery brings up the interface and runs dhclient.
122178
diff --git a/cloudinit/sources/DataSourceAzure.py b/cloudinit/sources/DataSourceAzure.py
index 8c3492d..14367e9 100644
--- a/cloudinit/sources/DataSourceAzure.py
+++ b/cloudinit/sources/DataSourceAzure.py
@@ -11,7 +11,6 @@ from functools import partial
11import os11import os
12import os.path12import os.path
13import re13import re
14import time
15from xml.dom import minidom14from xml.dom import minidom
16import xml.etree.ElementTree as ET15import xml.etree.ElementTree as ET
1716
@@ -321,7 +320,7 @@ class DataSourceAzure(sources.DataSource):
321 # https://bugs.launchpad.net/cloud-init/+bug/1717611320 # https://bugs.launchpad.net/cloud-init/+bug/1717611
322 missing = util.log_time(logfunc=LOG.debug,321 missing = util.log_time(logfunc=LOG.debug,
323 msg="waiting for SSH public key files",322 msg="waiting for SSH public key files",
324 func=wait_for_files,323 func=util.wait_for_files,
325 args=(fp_files, 900))324 args=(fp_files, 900))
326325
327 if len(missing):326 if len(missing):
@@ -556,8 +555,8 @@ def address_ephemeral_resize(devpath=RESOURCE_DISK_PATH, maxwait=120,
556 is_new_instance=False):555 is_new_instance=False):
557 # wait for ephemeral disk to come up556 # wait for ephemeral disk to come up
558 naplen = .2557 naplen = .2
559 missing = wait_for_files([devpath], maxwait=maxwait, naplen=naplen,558 missing = util.wait_for_files([devpath], maxwait=maxwait, naplen=naplen,
560 log_pre="Azure ephemeral disk: ")559 log_pre="Azure ephemeral disk: ")
561560
562 if missing:561 if missing:
563 LOG.warning("ephemeral device '%s' did not appear after %d seconds.",562 LOG.warning("ephemeral device '%s' did not appear after %d seconds.",
@@ -639,28 +638,6 @@ def pubkeys_from_crt_files(flist):
639 return pubkeys638 return pubkeys
640639
641640
642def wait_for_files(flist, maxwait, naplen=.5, log_pre=""):
643 need = set(flist)
644 waited = 0
645 while True:
646 need -= set([f for f in need if os.path.exists(f)])
647 if len(need) == 0:
648 LOG.debug("%sAll files appeared after %s seconds: %s",
649 log_pre, waited, flist)
650 return []
651 if waited == 0:
652 LOG.info("%sWaiting up to %s seconds for the following files: %s",
653 log_pre, maxwait, flist)
654 if waited + naplen > maxwait:
655 break
656 time.sleep(naplen)
657 waited += naplen
658
659 LOG.warning("%sStill missing files after %s seconds: %s",
660 log_pre, maxwait, need)
661 return need
662
663
664def write_files(datadir, files, dirmode=None):641def write_files(datadir, files, dirmode=None):
665642
666 def _redact_password(cnt, fname):643 def _redact_password(cnt, fname):
diff --git a/cloudinit/util.py b/cloudinit/util.py
index e1290aa..6c014ba 100644
--- a/cloudinit/util.py
+++ b/cloudinit/util.py
@@ -2541,4 +2541,26 @@ def load_shell_content(content, add_empty=False, empty_val=None):
2541 return data2541 return data
25422542
25432543
2544def wait_for_files(flist, maxwait, naplen=.5, log_pre=""):
2545 need = set(flist)
2546 waited = 0
2547 while True:
2548 need -= set([f for f in need if os.path.exists(f)])
2549 if len(need) == 0:
2550 LOG.debug("%sAll files appeared after %s seconds: %s",
2551 log_pre, waited, flist)
2552 return []
2553 if waited == 0:
2554 LOG.debug("%sWaiting up to %s seconds for the following files: %s",
2555 log_pre, maxwait, flist)
2556 if waited + naplen > maxwait:
2557 break
2558 time.sleep(naplen)
2559 waited += naplen
2560
2561 LOG.debug("%sStill missing files after %s seconds: %s",
2562 log_pre, maxwait, need)
2563 return need
2564
2565
2544# vi: ts=4 expandtab2566# vi: ts=4 expandtab
diff --git a/debian/changelog b/debian/changelog
index b209fa9..ea72cf2 100644
--- a/debian/changelog
+++ b/debian/changelog
@@ -1,3 +1,16 @@
1cloud-init (17.1-46-g7acc9e68-0ubuntu1~17.10.1) artful-proposed; urgency=medium
2
3 * New upstream snapshot.
4 - ec2: Fix sandboxed dhclient background process cleanup.
5 (LP: #1735331)
6 - tests: NoCloudKVMImage do not modify the original local cache image.
7 - tests: Enable bionic in integration tests. [Joshua Powers]
8 - tests: Use apt-get to install a deb so that depends get resolved.
9 - sysconfig: Correctly render dns and dns search info.
10 [Ryan McCabe]
11
12 -- Chad Smith <chad.smith@canonical.com> Fri, 01 Dec 2017 09:58:48 -0700
13
1cloud-init (17.1-41-g76243487-0ubuntu1~17.10.1) artful-proposed; urgency=medium14cloud-init (17.1-41-g76243487-0ubuntu1~17.10.1) artful-proposed; urgency=medium
215
3 * debian/cloud-init.templates: Fix capitilazation in 'AliYun'16 * debian/cloud-init.templates: Fix capitilazation in 'AliYun'
diff --git a/tests/cloud_tests/images/nocloudkvm.py b/tests/cloud_tests/images/nocloudkvm.py
index 1e7962c..8678b07 100644
--- a/tests/cloud_tests/images/nocloudkvm.py
+++ b/tests/cloud_tests/images/nocloudkvm.py
@@ -4,6 +4,10 @@
44
5from cloudinit import util as c_util5from cloudinit import util as c_util
66
7import os
8import shutil
9import tempfile
10
7from tests.cloud_tests.images import base11from tests.cloud_tests.images import base
8from tests.cloud_tests.snapshots import nocloudkvm as nocloud_kvm_snapshot12from tests.cloud_tests.snapshots import nocloudkvm as nocloud_kvm_snapshot
913
@@ -13,7 +17,7 @@ class NoCloudKVMImage(base.Image):
1317
14 platform_name = "nocloud-kvm"18 platform_name = "nocloud-kvm"
1519
16 def __init__(self, platform, config, img_path):20 def __init__(self, platform, config, orig_img_path):
17 """Set up image.21 """Set up image.
1822
19 @param platform: platform object23 @param platform: platform object
@@ -21,7 +25,13 @@ class NoCloudKVMImage(base.Image):
21 @param img_path: path to the image25 @param img_path: path to the image
22 """26 """
23 self.modified = False27 self.modified = False
24 self._img_path = img_path28 self._workd = tempfile.mkdtemp(prefix='NoCloudKVMImage')
29 self._orig_img_path = orig_img_path
30 self._img_path = os.path.join(self._workd,
31 os.path.basename(self._orig_img_path))
32
33 c_util.subp(['qemu-img', 'create', '-f', 'qcow2',
34 '-b', orig_img_path, self._img_path])
2535
26 super(NoCloudKVMImage, self).__init__(platform, config)36 super(NoCloudKVMImage, self).__init__(platform, config)
2737
@@ -61,13 +71,9 @@ class NoCloudKVMImage(base.Image):
61 if not self._img_path:71 if not self._img_path:
62 raise RuntimeError()72 raise RuntimeError()
6373
64 instance = self.platform.create_image(
65 self.properties, self.config, self.features,
66 self._img_path, image_desc=str(self), use_desc='snapshot')
67
68 return nocloud_kvm_snapshot.NoCloudKVMSnapshot(74 return nocloud_kvm_snapshot.NoCloudKVMSnapshot(
69 self.platform, self.properties, self.config,75 self.platform, self.properties, self.config,
70 self.features, instance)76 self.features, self._img_path)
7177
72 def destroy(self):78 def destroy(self):
73 """Unset path to signal image is no longer used.79 """Unset path to signal image is no longer used.
@@ -77,6 +83,8 @@ class NoCloudKVMImage(base.Image):
77 framework decide whether to keep or destroy everything.83 framework decide whether to keep or destroy everything.
78 """84 """
79 self._img_path = None85 self._img_path = None
86 shutil.rmtree(self._workd)
87
80 super(NoCloudKVMImage, self).destroy()88 super(NoCloudKVMImage, self).destroy()
8189
82# vi: ts=4 expandtab90# vi: ts=4 expandtab
diff --git a/tests/cloud_tests/instances/nocloudkvm.py b/tests/cloud_tests/instances/nocloudkvm.py
index cc82580..bc06a79 100644
--- a/tests/cloud_tests/instances/nocloudkvm.py
+++ b/tests/cloud_tests/instances/nocloudkvm.py
@@ -25,12 +25,13 @@ class NoCloudKVMInstance(base.Instance):
25 platform_name = "nocloud-kvm"25 platform_name = "nocloud-kvm"
26 _ssh_client = None26 _ssh_client = None
2727
28 def __init__(self, platform, name, properties, config, features,28 def __init__(self, platform, name, image_path, properties, config,
29 user_data, meta_data):29 features, user_data, meta_data):
30 """Set up instance.30 """Set up instance.
3131
32 @param platform: platform object32 @param platform: platform object
33 @param name: image path33 @param name: image path
34 @param image_path: path to disk image to boot.
34 @param properties: dictionary of properties35 @param properties: dictionary of properties
35 @param config: dictionary of configuration values36 @param config: dictionary of configuration values
36 @param features: dictionary of supported feature flags37 @param features: dictionary of supported feature flags
@@ -43,6 +44,7 @@ class NoCloudKVMInstance(base.Instance):
43 self.pid = None44 self.pid = None
44 self.pid_file = None45 self.pid_file = None
45 self.console_file = None46 self.console_file = None
47 self.disk = image_path
4648
47 super(NoCloudKVMInstance, self).__init__(49 super(NoCloudKVMInstance, self).__init__(
48 platform, name, properties, config, features)50 platform, name, properties, config, features)
@@ -145,7 +147,7 @@ class NoCloudKVMInstance(base.Instance):
145 self.ssh_port = self.get_free_port()147 self.ssh_port = self.get_free_port()
146148
147 cmd = ['./tools/xkvm',149 cmd = ['./tools/xkvm',
148 '--disk', '%s,cache=unsafe' % self.name,150 '--disk', '%s,cache=unsafe' % self.disk,
149 '--disk', '%s,cache=unsafe' % seed,151 '--disk', '%s,cache=unsafe' % seed,
150 '--netdev', ','.join(['user',152 '--netdev', ','.join(['user',
151 'hostfwd=tcp::%s-:22' % self.ssh_port,153 'hostfwd=tcp::%s-:22' % self.ssh_port,
diff --git a/tests/cloud_tests/platforms/nocloudkvm.py b/tests/cloud_tests/platforms/nocloudkvm.py
index f1f8187..76cd83a 100644
--- a/tests/cloud_tests/platforms/nocloudkvm.py
+++ b/tests/cloud_tests/platforms/nocloudkvm.py
@@ -55,19 +55,20 @@ class NoCloudKVMPlatform(base.Platform):
55 for fname in glob.iglob(search_d, recursive=True):55 for fname in glob.iglob(search_d, recursive=True):
56 images.append(fname)56 images.append(fname)
5757
58 if len(images) != 1:58 if len(images) < 1:
59 raise Exception('No unique images found')59 raise RuntimeError("No images found under '%s'" % search_d)
60 if len(images) > 1:
61 raise RuntimeError(
62 "Multiple images found in '%s': %s" % (search_d,
63 ' '.join(images)))
6064
61 image = nocloud_kvm_image.NoCloudKVMImage(self, img_conf, images[0])65 image = nocloud_kvm_image.NoCloudKVMImage(self, img_conf, images[0])
62 if img_conf.get('override_templates', False):
63 image.update_templates(self.config.get('template_overrides', {}),
64 self.config.get('template_files', {}))
65 return image66 return image
6667
67 def create_image(self, properties, config, features,68 def create_instance(self, properties, config, features,
68 src_img_path, image_desc=None, use_desc=None,69 src_img_path, image_desc=None, use_desc=None,
69 user_data=None, meta_data=None):70 user_data=None, meta_data=None):
70 """Create an image71 """Create an instance
7172
72 @param src_img_path: image path to launch from73 @param src_img_path: image path to launch from
73 @param properties: image properties74 @param properties: image properties
@@ -82,7 +83,7 @@ class NoCloudKVMPlatform(base.Platform):
82 c_util.subp(['qemu-img', 'create', '-f', 'qcow2',83 c_util.subp(['qemu-img', 'create', '-f', 'qcow2',
83 '-b', src_img_path, img_path])84 '-b', src_img_path, img_path])
8485
85 return nocloud_kvm_instance.NoCloudKVMInstance(self, img_path,86 return nocloud_kvm_instance.NoCloudKVMInstance(self, name, img_path,
86 properties, config,87 properties, config,
87 features, user_data,88 features, user_data,
88 meta_data)89 meta_data)
diff --git a/tests/cloud_tests/releases.yaml b/tests/cloud_tests/releases.yaml
index ec7e2d5..e593380 100644
--- a/tests/cloud_tests/releases.yaml
+++ b/tests/cloud_tests/releases.yaml
@@ -122,6 +122,22 @@ features:
122122
123releases:123releases:
124 # UBUNTU =================================================================124 # UBUNTU =================================================================
125 bionic:
126 # EOL: Apr 2023
127 default:
128 enabled: true
129 release: bionic
130 version: 18.04
131 family: ubuntu
132 feature_groups:
133 - base
134 - debian_base
135 - ubuntu_specific
136 lxd:
137 sstreams_server: https://cloud-images.ubuntu.com/daily
138 alias: bionic
139 setup_overrides: null
140 override_templates: false
125 artful:141 artful:
126 # EOL: Jul 2018142 # EOL: Jul 2018
127 default:143 default:
diff --git a/tests/cloud_tests/setup_image.py b/tests/cloud_tests/setup_image.py
index 6672ffb..179f40d 100644
--- a/tests/cloud_tests/setup_image.py
+++ b/tests/cloud_tests/setup_image.py
@@ -50,9 +50,9 @@ def install_deb(args, image):
50 LOG.debug(msg)50 LOG.debug(msg)
51 remote_path = os.path.join('/tmp', os.path.basename(args.deb))51 remote_path = os.path.join('/tmp', os.path.basename(args.deb))
52 image.push_file(args.deb, remote_path)52 image.push_file(args.deb, remote_path)
53 cmd = 'dpkg -i {}; apt-get install --yes -f'.format(remote_path)53 image.execute(
54 image.execute(cmd, description=msg)54 ['apt-get', 'install', '--allow-downgrades', '--assume-yes',
5555 remote_path], description=msg)
56 # check installed deb version matches package56 # check installed deb version matches package
57 fmt = ['-W', "--showformat=${Version}"]57 fmt = ['-W', "--showformat=${Version}"]
58 (out, err, exit) = image.execute(['dpkg-deb'] + fmt + [remote_path])58 (out, err, exit) = image.execute(['dpkg-deb'] + fmt + [remote_path])
diff --git a/tests/cloud_tests/snapshots/nocloudkvm.py b/tests/cloud_tests/snapshots/nocloudkvm.py
index 0999834..21e908d 100644
--- a/tests/cloud_tests/snapshots/nocloudkvm.py
+++ b/tests/cloud_tests/snapshots/nocloudkvm.py
@@ -2,6 +2,8 @@
22
3"""Base NoCloud KVM snapshot."""3"""Base NoCloud KVM snapshot."""
4import os4import os
5import shutil
6import tempfile
57
6from tests.cloud_tests.snapshots import base8from tests.cloud_tests.snapshots import base
79
@@ -11,16 +13,19 @@ class NoCloudKVMSnapshot(base.Snapshot):
1113
12 platform_name = "nocloud-kvm"14 platform_name = "nocloud-kvm"
1315
14 def __init__(self, platform, properties, config, features,16 def __init__(self, platform, properties, config, features, image_path):
15 instance):
16 """Set up snapshot.17 """Set up snapshot.
1718
18 @param platform: platform object19 @param platform: platform object
19 @param properties: image properties20 @param properties: image properties
20 @param config: image config21 @param config: image config
21 @param features: supported feature flags22 @param features: supported feature flags
23 @param image_path: image file to snapshot.
22 """24 """
23 self.instance = instance25 self._workd = tempfile.mkdtemp(prefix='NoCloudKVMSnapshot')
26 snapshot = os.path.join(self._workd, 'snapshot')
27 shutil.copyfile(image_path, snapshot)
28 self._image_path = snapshot
2429
25 super(NoCloudKVMSnapshot, self).__init__(30 super(NoCloudKVMSnapshot, self).__init__(
26 platform, properties, config, features)31 platform, properties, config, features)
@@ -40,9 +45,9 @@ class NoCloudKVMSnapshot(base.Snapshot):
40 self.platform.config['public_key'])45 self.platform.config['public_key'])
41 user_data = self.inject_ssh_key(user_data, key_file)46 user_data = self.inject_ssh_key(user_data, key_file)
4247
43 instance = self.platform.create_image(48 instance = self.platform.create_instance(
44 self.properties, self.config, self.features,49 self.properties, self.config, self.features,
45 self.instance.name, image_desc=str(self), use_desc=use_desc,50 self._image_path, image_desc=str(self), use_desc=use_desc,
46 user_data=user_data, meta_data=meta_data)51 user_data=user_data, meta_data=meta_data)
4752
48 if start:53 if start:
@@ -68,7 +73,7 @@ class NoCloudKVMSnapshot(base.Snapshot):
6873
69 def destroy(self):74 def destroy(self):
70 """Clean up snapshot data."""75 """Clean up snapshot data."""
71 self.instance.destroy()76 shutil.rmtree(self._workd)
72 super(NoCloudKVMSnapshot, self).destroy()77 super(NoCloudKVMSnapshot, self).destroy()
7378
74# vi: ts=4 expandtab79# vi: ts=4 expandtab
diff --git a/tests/unittests/test_datasource/test_azure.py b/tests/unittests/test_datasource/test_azure.py
index 0a11777..7cb1812 100644
--- a/tests/unittests/test_datasource/test_azure.py
+++ b/tests/unittests/test_datasource/test_azure.py
@@ -171,7 +171,6 @@ scbus-1 on xpt0 bus 0
171 self.apply_patches([171 self.apply_patches([
172 (dsaz, 'list_possible_azure_ds_devs', dsdevs),172 (dsaz, 'list_possible_azure_ds_devs', dsdevs),
173 (dsaz, 'invoke_agent', _invoke_agent),173 (dsaz, 'invoke_agent', _invoke_agent),
174 (dsaz, 'wait_for_files', _wait_for_files),
175 (dsaz, 'pubkeys_from_crt_files', _pubkeys_from_crt_files),174 (dsaz, 'pubkeys_from_crt_files', _pubkeys_from_crt_files),
176 (dsaz, 'perform_hostname_bounce', mock.MagicMock()),175 (dsaz, 'perform_hostname_bounce', mock.MagicMock()),
177 (dsaz, 'get_hostname', mock.MagicMock()),176 (dsaz, 'get_hostname', mock.MagicMock()),
@@ -179,6 +178,8 @@ scbus-1 on xpt0 bus 0
179 (dsaz, 'get_metadata_from_fabric', self.get_metadata_from_fabric),178 (dsaz, 'get_metadata_from_fabric', self.get_metadata_from_fabric),
180 (dsaz.util, 'read_dmi_data', mock.MagicMock(179 (dsaz.util, 'read_dmi_data', mock.MagicMock(
181 side_effect=_dmi_mocks)),180 side_effect=_dmi_mocks)),
181 (dsaz.util, 'wait_for_files', mock.MagicMock(
182 side_effect=_wait_for_files)),
182 ])183 ])
183184
184 dsrc = dsaz.DataSourceAzure(185 dsrc = dsaz.DataSourceAzure(
@@ -647,7 +648,7 @@ class TestAzureBounce(TestCase):
647 self.patches.enter_context(648 self.patches.enter_context(
648 mock.patch.object(dsaz, 'invoke_agent'))649 mock.patch.object(dsaz, 'invoke_agent'))
649 self.patches.enter_context(650 self.patches.enter_context(
650 mock.patch.object(dsaz, 'wait_for_files'))651 mock.patch.object(dsaz.util, 'wait_for_files'))
651 self.patches.enter_context(652 self.patches.enter_context(
652 mock.patch.object(dsaz, 'list_possible_azure_ds_devs',653 mock.patch.object(dsaz, 'list_possible_azure_ds_devs',
653 mock.MagicMock(return_value=[])))654 mock.MagicMock(return_value=[])))
diff --git a/tests/unittests/test_net.py b/tests/unittests/test_net.py
index bbb63cb..f3fa2a3 100644
--- a/tests/unittests/test_net.py
+++ b/tests/unittests/test_net.py
@@ -436,6 +436,9 @@ NETWORK_CONFIGS = {
436 BOOTPROTO=dhcp436 BOOTPROTO=dhcp
437 DEFROUTE=yes437 DEFROUTE=yes
438 DEVICE=eth99438 DEVICE=eth99
439 DNS1=8.8.8.8
440 DNS2=8.8.4.4
441 DOMAIN="barley.maas sach.maas"
439 GATEWAY=65.61.151.37442 GATEWAY=65.61.151.37
440 HWADDR=c0:d6:9f:2c:e8:80443 HWADDR=c0:d6:9f:2c:e8:80
441 IPADDR=192.168.21.3444 IPADDR=192.168.21.3
@@ -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
836 BOOTPROTO=none839 BOOTPROTO=none
837 DEFROUTE=yes840 DEFROUTE=yes
838 DEVICE=eth0.101841 DEVICE=eth0.101
842 DNS1=192.168.0.10
843 DNS2=10.23.23.134
844 DOMAIN="barley.maas sacchromyces.maas brettanomyces.maas"
839 GATEWAY=192.168.0.1845 GATEWAY=192.168.0.1
840 IPADDR=192.168.0.2846 IPADDR=192.168.0.2
841 IPADDR1=192.168.2.10847 IPADDR1=192.168.2.10

Subscribers

People subscribed via source and target branches