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

Proposed by Chad Smith
Status: Merged
Merged at revision: 22acf15e71e8665fa485a2ef6fe01da9cd2cfd1f
Proposed branch: ~chad.smith/cloud-init:ubuntu/bionic
Merge into: cloud-init:ubuntu/bionic
Diff against target: 557 lines (+279/-81)
7 files modified
cloudinit/net/cmdline.py (+95/-32)
cloudinit/sources/DataSourceExoscale.py (+17/-9)
cloudinit/sources/DataSourceOracle.py (+20/-16)
cloudinit/sources/tests/test_oracle.py (+36/-3)
debian/changelog (+9/-0)
tests/unittests/test_datasource/test_exoscale.py (+16/-8)
tests/unittests/test_net.py (+86/-13)
Reviewer Review Type Date Requested Status
Server Team CI bot continuous-integration Needs Fixing
cloud-init Commiters Pending
Review via email: mp+371962@code.launchpad.net

Commit message

new-upstream-snapshot to fix Exoscale datasource which is scheduled for release

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

FAILED: Continuous integration, rev:22acf15e71e8665fa485a2ef6fe01da9cd2cfd1f
https://jenkins.ubuntu.com/server/job/cloud-init-ci/1090/
Executed test runs:
    SUCCESS: Checkout
    SUCCESS: Unit & Style Tests
    FAILED: Ubuntu LTS: Build

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

review: Needs Fixing (continuous-integration)

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1diff --git a/cloudinit/net/cmdline.py b/cloudinit/net/cmdline.py
2index 556a10f..55166ea 100755
3--- a/cloudinit/net/cmdline.py
4+++ b/cloudinit/net/cmdline.py
5@@ -5,20 +5,95 @@
6 #
7 # This file is part of cloud-init. See LICENSE file for license information.
8
9+import abc
10 import base64
11 import glob
12 import gzip
13 import io
14 import os
15
16-from . import get_devicelist
17-from . import read_sys_net_safe
18+import six
19
20 from cloudinit import util
21
22+from . import get_devicelist
23+from . import read_sys_net_safe
24+
25 _OPEN_ISCSI_INTERFACE_FILE = "/run/initramfs/open-iscsi.interface"
26
27
28+@six.add_metaclass(abc.ABCMeta)
29+class InitramfsNetworkConfigSource(object):
30+ """ABC for net config sources that read config written by initramfses"""
31+
32+ @abc.abstractmethod
33+ def is_applicable(self):
34+ # type: () -> bool
35+ """Is this initramfs config source applicable to the current system?"""
36+ pass
37+
38+ @abc.abstractmethod
39+ def render_config(self):
40+ # type: () -> dict
41+ """Render a v1 network config from the initramfs configuration"""
42+ pass
43+
44+
45+class KlibcNetworkConfigSource(InitramfsNetworkConfigSource):
46+ """InitramfsNetworkConfigSource for klibc initramfs (i.e. Debian/Ubuntu)
47+
48+ Has three parameters, but they are intended to make testing simpler, _not_
49+ for use in production code. (This is indicated by the prepended
50+ underscores.)
51+ """
52+
53+ def __init__(self, _files=None, _mac_addrs=None, _cmdline=None):
54+ self._files = _files
55+ self._mac_addrs = _mac_addrs
56+ self._cmdline = _cmdline
57+
58+ # Set defaults here, as they require computation that we don't want to
59+ # do at method definition time
60+ if self._files is None:
61+ self._files = _get_klibc_net_cfg_files()
62+ if self._cmdline is None:
63+ self._cmdline = util.get_cmdline()
64+ if self._mac_addrs is None:
65+ self._mac_addrs = {}
66+ for k in get_devicelist():
67+ mac_addr = read_sys_net_safe(k, 'address')
68+ if mac_addr:
69+ self._mac_addrs[k] = mac_addr
70+
71+ def is_applicable(self):
72+ # type: () -> bool
73+ """
74+ Return whether this system has klibc initramfs network config or not
75+
76+ Will return True if:
77+ (a) klibc files exist in /run, AND
78+ (b) either:
79+ (i) ip= or ip6= are on the kernel cmdline, OR
80+ (ii) an open-iscsi interface file is present in the system
81+ """
82+ if self._files:
83+ if 'ip=' in self._cmdline or 'ip6=' in self._cmdline:
84+ return True
85+ if os.path.exists(_OPEN_ISCSI_INTERFACE_FILE):
86+ # iBft can configure networking without ip=
87+ return True
88+ return False
89+
90+ def render_config(self):
91+ # type: () -> dict
92+ return config_from_klibc_net_cfg(
93+ files=self._files, mac_addrs=self._mac_addrs,
94+ )
95+
96+
97+_INITRAMFS_CONFIG_SOURCES = [KlibcNetworkConfigSource]
98+
99+
100 def _klibc_to_config_entry(content, mac_addrs=None):
101 """Convert a klibc written shell content file to a 'config' entry
102 When ip= is seen on the kernel command line in debian initramfs
103@@ -137,6 +212,24 @@ def config_from_klibc_net_cfg(files=None, mac_addrs=None):
104 return {'config': entries, 'version': 1}
105
106
107+def read_initramfs_config():
108+ """
109+ Return v1 network config for initramfs-configured networking (or None)
110+
111+ This will consider each _INITRAMFS_CONFIG_SOURCES entry in turn, and return
112+ v1 network configuration for the first one that is applicable. If none are
113+ applicable, return None.
114+ """
115+ for src_cls in _INITRAMFS_CONFIG_SOURCES:
116+ cfg_source = src_cls()
117+
118+ if not cfg_source.is_applicable():
119+ continue
120+
121+ return cfg_source.render_config()
122+ return None
123+
124+
125 def _decomp_gzip(blob, strict=True):
126 # decompress blob. raise exception if not compressed unless strict=False.
127 with io.BytesIO(blob) as iobuf:
128@@ -167,36 +260,6 @@ def _b64dgz(b64str, gzipped="try"):
129 return _decomp_gzip(blob, strict=gzipped != "try")
130
131
132-def _is_initramfs_netconfig(files, cmdline):
133- if files:
134- if 'ip=' in cmdline or 'ip6=' in cmdline:
135- return True
136- if os.path.exists(_OPEN_ISCSI_INTERFACE_FILE):
137- # iBft can configure networking without ip=
138- return True
139- return False
140-
141-
142-def read_initramfs_config(files=None, mac_addrs=None, cmdline=None):
143- if cmdline is None:
144- cmdline = util.get_cmdline()
145-
146- if files is None:
147- files = _get_klibc_net_cfg_files()
148-
149- if not _is_initramfs_netconfig(files, cmdline):
150- return None
151-
152- if mac_addrs is None:
153- mac_addrs = {}
154- for k in get_devicelist():
155- mac_addr = read_sys_net_safe(k, 'address')
156- if mac_addr:
157- mac_addrs[k] = mac_addr
158-
159- return config_from_klibc_net_cfg(files=files, mac_addrs=mac_addrs)
160-
161-
162 def read_kernel_cmdline_config(cmdline=None):
163 if cmdline is None:
164 cmdline = util.get_cmdline()
165diff --git a/cloudinit/sources/DataSourceExoscale.py b/cloudinit/sources/DataSourceExoscale.py
166index 52e7f6f..fdfb4ed 100644
167--- a/cloudinit/sources/DataSourceExoscale.py
168+++ b/cloudinit/sources/DataSourceExoscale.py
169@@ -6,6 +6,7 @@
170 from cloudinit import ec2_utils as ec2
171 from cloudinit import log as logging
172 from cloudinit import sources
173+from cloudinit import helpers
174 from cloudinit import url_helper
175 from cloudinit import util
176
177@@ -20,13 +21,6 @@ URL_RETRIES = 6
178
179 EXOSCALE_DMI_NAME = "Exoscale"
180
181-BUILTIN_DS_CONFIG = {
182- # We run the set password config module on every boot in order to enable
183- # resetting the instance's password via the exoscale console (and a
184- # subsequent instance reboot).
185- 'cloud_config_modules': [["set-passwords", "always"]]
186-}
187-
188
189 class DataSourceExoscale(sources.DataSource):
190
191@@ -42,8 +36,22 @@ class DataSourceExoscale(sources.DataSource):
192 self.ds_cfg.get('password_server_port', PASSWORD_SERVER_PORT))
193 self.url_timeout = self.ds_cfg.get('timeout', URL_TIMEOUT)
194 self.url_retries = self.ds_cfg.get('retries', URL_RETRIES)
195-
196- self.extra_config = BUILTIN_DS_CONFIG
197+ self.extra_config = {}
198+
199+ def activate(self, cfg, is_new_instance):
200+ """Adjust set-passwords module to run 'always' during each boot"""
201+ # We run the set password config module on every boot in order to
202+ # enable resetting the instance's password via the exoscale console
203+ # (and a subsequent instance reboot).
204+ # Exoscale password server only provides set-passwords user-data if
205+ # a user has triggered a password reset. So calling that password
206+ # service generally results in no additional cloud-config.
207+ # TODO(Create util functions for overriding merged sys_cfg module freq)
208+ mod = 'set_passwords'
209+ sem_path = self.paths.get_ipath_cur('sem')
210+ sem_helper = helpers.FileSemaphores(sem_path)
211+ if sem_helper.clear('config_' + mod, None):
212+ LOG.debug('Overriding module set-passwords with frequency always')
213
214 def wait_for_metadata_service(self):
215 """Wait for the metadata service to be reachable."""
216diff --git a/cloudinit/sources/DataSourceOracle.py b/cloudinit/sources/DataSourceOracle.py
217index 6e73f56..1cb0636 100644
218--- a/cloudinit/sources/DataSourceOracle.py
219+++ b/cloudinit/sources/DataSourceOracle.py
220@@ -51,8 +51,8 @@ def _add_network_config_from_opc_imds(network_config):
221 include the secondary VNICs.
222
223 :param network_config:
224- A v1 network config dict with the primary NIC already configured. This
225- dict will be mutated.
226+ A v1 or v2 network config dict with the primary NIC already configured.
227+ This dict will be mutated.
228
229 :raises:
230 Exceptions are not handled within this function. Likely exceptions are
231@@ -88,20 +88,24 @@ def _add_network_config_from_opc_imds(network_config):
232 LOG.debug('Interface with MAC %s not found; skipping', mac_address)
233 continue
234 name = interfaces_by_mac[mac_address]
235- subnet = {
236- 'type': 'static',
237- 'address': vnic_dict['privateIp'],
238- 'netmask': vnic_dict['subnetCidrBlock'].split('/')[1],
239- 'gateway': vnic_dict['virtualRouterIp'],
240- 'control': 'manual',
241- }
242- network_config['config'].append({
243- 'name': name,
244- 'type': 'physical',
245- 'mac_address': mac_address,
246- 'mtu': MTU,
247- 'subnets': [subnet],
248- })
249+
250+ if network_config['version'] == 1:
251+ subnet = {
252+ 'type': 'static',
253+ 'address': vnic_dict['privateIp'],
254+ }
255+ network_config['config'].append({
256+ 'name': name,
257+ 'type': 'physical',
258+ 'mac_address': mac_address,
259+ 'mtu': MTU,
260+ 'subnets': [subnet],
261+ })
262+ elif network_config['version'] == 2:
263+ network_config['ethernets'][name] = {
264+ 'addresses': [vnic_dict['privateIp']],
265+ 'mtu': MTU, 'dhcp4': False, 'dhcp6': False,
266+ 'match': {'macaddress': mac_address}}
267
268
269 class DataSourceOracle(sources.DataSource):
270diff --git a/cloudinit/sources/tests/test_oracle.py b/cloudinit/sources/tests/test_oracle.py
271index 3ddf7df..2a70bbc 100644
272--- a/cloudinit/sources/tests/test_oracle.py
273+++ b/cloudinit/sources/tests/test_oracle.py
274@@ -526,6 +526,18 @@ class TestNetworkConfigFromOpcImds(test_helpers.CiTestCase):
275 'Interface with MAC 00:00:17:02:2b:b1 not found; skipping',
276 self.logs.getvalue())
277
278+ def test_missing_mac_skipped_v2(self):
279+ self.m_readurl.return_value = OPC_VM_SECONDARY_VNIC_RESPONSE
280+ self.m_get_interfaces_by_mac.return_value = {}
281+
282+ network_config = {'version': 2, 'ethernets': {'primary': {'nic': {}}}}
283+ oracle._add_network_config_from_opc_imds(network_config)
284+
285+ self.assertEqual(1, len(network_config['ethernets']))
286+ self.assertIn(
287+ 'Interface with MAC 00:00:17:02:2b:b1 not found; skipping',
288+ self.logs.getvalue())
289+
290 def test_secondary_nic(self):
291 self.m_readurl.return_value = OPC_VM_SECONDARY_VNIC_RESPONSE
292 mac_addr, nic_name = '00:00:17:02:2b:b1', 'ens3'
293@@ -549,8 +561,29 @@ class TestNetworkConfigFromOpcImds(test_helpers.CiTestCase):
294 subnet_cfg = secondary_nic_cfg['subnets'][0]
295 # These values are hard-coded in OPC_VM_SECONDARY_VNIC_RESPONSE
296 self.assertEqual('10.0.0.231', subnet_cfg['address'])
297- self.assertEqual('24', subnet_cfg['netmask'])
298- self.assertEqual('10.0.0.1', subnet_cfg['gateway'])
299- self.assertEqual('manual', subnet_cfg['control'])
300+
301+ def test_secondary_nic_v2(self):
302+ self.m_readurl.return_value = OPC_VM_SECONDARY_VNIC_RESPONSE
303+ mac_addr, nic_name = '00:00:17:02:2b:b1', 'ens3'
304+ self.m_get_interfaces_by_mac.return_value = {
305+ mac_addr: nic_name,
306+ }
307+
308+ network_config = {'version': 2, 'ethernets': {'primary': {'nic': {}}}}
309+ oracle._add_network_config_from_opc_imds(network_config)
310+
311+ # The input is mutated
312+ self.assertEqual(2, len(network_config['ethernets']))
313+
314+ secondary_nic_cfg = network_config['ethernets']['ens3']
315+ self.assertFalse(secondary_nic_cfg['dhcp4'])
316+ self.assertFalse(secondary_nic_cfg['dhcp6'])
317+ self.assertEqual(mac_addr, secondary_nic_cfg['match']['macaddress'])
318+ self.assertEqual(9000, secondary_nic_cfg['mtu'])
319+
320+ self.assertEqual(1, len(secondary_nic_cfg['addresses']))
321+ # These values are hard-coded in OPC_VM_SECONDARY_VNIC_RESPONSE
322+ self.assertEqual('10.0.0.231', secondary_nic_cfg['addresses'][0])
323+
324
325 # vi: ts=4 expandtab
326diff --git a/debian/changelog b/debian/changelog
327index 8ae019f..7d71be2 100644
328--- a/debian/changelog
329+++ b/debian/changelog
330@@ -1,3 +1,12 @@
331+cloud-init (19.2-24-ge7881d5c-0ubuntu1~18.04.1) bionic; urgency=medium
332+
333+ * New upstream snapshot. (LP: #1841099)
334+ - Oracle: Render secondary vnic IP and MTU values only
335+ - exoscale: fix sysconfig cloud_config_modules overrides
336+ - net/cmdline: refactor to allow multiple initramfs network config sources
337+
338+ -- Chad Smith <chad.smith@canonical.com> Wed, 28 Aug 2019 15:50:54 -0600
339+
340 cloud-init (19.2-21-ge6383719-0ubuntu1~18.04.1) bionic; urgency=medium
341
342 * refresh patches:
343diff --git a/tests/unittests/test_datasource/test_exoscale.py b/tests/unittests/test_datasource/test_exoscale.py
344index 350c330..f006119 100644
345--- a/tests/unittests/test_datasource/test_exoscale.py
346+++ b/tests/unittests/test_datasource/test_exoscale.py
347@@ -11,8 +11,10 @@ from cloudinit.sources.DataSourceExoscale import (
348 PASSWORD_SERVER_PORT,
349 read_metadata)
350 from cloudinit.tests.helpers import HttprettyTestCase, mock
351+from cloudinit import util
352
353 import httpretty
354+import os
355 import requests
356
357
358@@ -63,6 +65,18 @@ class TestDatasourceExoscale(HttprettyTestCase):
359 password = get_password()
360 self.assertEqual(expected_password, password)
361
362+ def test_activate_removes_set_passwords_semaphore(self):
363+ """Allow set_passwords to run every boot by removing the semaphore."""
364+ path = helpers.Paths({'cloud_dir': self.tmp})
365+ sem_dir = self.tmp_path('instance/sem', dir=self.tmp)
366+ util.ensure_dir(sem_dir)
367+ sem_file = os.path.join(sem_dir, 'config_set_passwords')
368+ with open(sem_file, 'w') as stream:
369+ stream.write('')
370+ ds = DataSourceExoscale({}, None, path)
371+ ds.activate(None, None)
372+ self.assertFalse(os.path.exists(sem_file))
373+
374 def test_get_data(self):
375 """The datasource conforms to expected behavior when supplied
376 full test data."""
377@@ -95,8 +109,6 @@ class TestDatasourceExoscale(HttprettyTestCase):
378 self.assertEqual(ds.get_config_obj(),
379 {'ssh_pwauth': True,
380 'password': expected_password,
381- 'cloud_config_modules': [
382- ["set-passwords", "always"]],
383 'chpasswd': {
384 'expire': False,
385 }})
386@@ -130,9 +142,7 @@ class TestDatasourceExoscale(HttprettyTestCase):
387 self.assertEqual(ds.userdata_raw.decode("utf-8"), "#cloud-config")
388 self.assertEqual(ds.metadata, {"instance-id": expected_id,
389 "local-hostname": expected_hostname})
390- self.assertEqual(ds.get_config_obj(),
391- {'cloud_config_modules': [
392- ["set-passwords", "always"]]})
393+ self.assertEqual(ds.get_config_obj(), {})
394
395 def test_get_data_no_password(self):
396 """The datasource conforms to expected behavior when no password is
397@@ -163,9 +173,7 @@ class TestDatasourceExoscale(HttprettyTestCase):
398 self.assertEqual(ds.userdata_raw.decode("utf-8"), "#cloud-config")
399 self.assertEqual(ds.metadata, {"instance-id": expected_id,
400 "local-hostname": expected_hostname})
401- self.assertEqual(ds.get_config_obj(),
402- {'cloud_config_modules': [
403- ["set-passwords", "always"]]})
404+ self.assertEqual(ds.get_config_obj(), {})
405
406 @mock.patch('cloudinit.sources.DataSourceExoscale.get_password')
407 def test_read_metadata_when_password_server_unreachable(self, m_password):
408diff --git a/tests/unittests/test_net.py b/tests/unittests/test_net.py
409index 4f7e420..e578992 100644
410--- a/tests/unittests/test_net.py
411+++ b/tests/unittests/test_net.py
412@@ -3591,7 +3591,7 @@ class TestCmdlineConfigParsing(CiTestCase):
413 self.assertEqual(found, self.simple_cfg)
414
415
416-class TestCmdlineReadInitramfsConfig(FilesystemMockingTestCase):
417+class TestCmdlineKlibcNetworkConfigSource(FilesystemMockingTestCase):
418 macs = {
419 'eth0': '14:02:ec:42:48:00',
420 'eno1': '14:02:ec:42:48:01',
421@@ -3607,8 +3607,11 @@ class TestCmdlineReadInitramfsConfig(FilesystemMockingTestCase):
422 populate_dir(root, content)
423 self.reRoot(root)
424
425- found = cmdline.read_initramfs_config(
426- cmdline='foo root=/root/bar', mac_addrs=self.macs)
427+ src = cmdline.KlibcNetworkConfigSource(
428+ _cmdline='foo root=/root/bar', _mac_addrs=self.macs,
429+ )
430+ self.assertTrue(src.is_applicable())
431+ found = src.render_config()
432 self.assertEqual(found['version'], 1)
433 self.assertEqual(found['config'], [exp1])
434
435@@ -3621,8 +3624,11 @@ class TestCmdlineReadInitramfsConfig(FilesystemMockingTestCase):
436 populate_dir(root, content)
437 self.reRoot(root)
438
439- found = cmdline.read_initramfs_config(
440- cmdline='foo ip=dhcp', mac_addrs=self.macs)
441+ src = cmdline.KlibcNetworkConfigSource(
442+ _cmdline='foo ip=dhcp', _mac_addrs=self.macs,
443+ )
444+ self.assertTrue(src.is_applicable())
445+ found = src.render_config()
446 self.assertEqual(found['version'], 1)
447 self.assertEqual(found['config'], [exp1])
448
449@@ -3632,9 +3638,11 @@ class TestCmdlineReadInitramfsConfig(FilesystemMockingTestCase):
450 populate_dir(root, content)
451 self.reRoot(root)
452
453- found = cmdline.read_initramfs_config(
454- cmdline='foo ip6=dhcp root=/dev/sda',
455- mac_addrs=self.macs)
456+ src = cmdline.KlibcNetworkConfigSource(
457+ _cmdline='foo ip6=dhcp root=/dev/sda', _mac_addrs=self.macs,
458+ )
459+ self.assertTrue(src.is_applicable())
460+ found = src.render_config()
461 self.assertEqual(
462 found,
463 {'version': 1, 'config': [
464@@ -3648,9 +3656,10 @@ class TestCmdlineReadInitramfsConfig(FilesystemMockingTestCase):
465 # if there is no ip= or ip6= on cmdline, return value should be None
466 content = {'net6-eno1.conf': DHCP6_CONTENT_1}
467 files = sorted(populate_dir(self.tmp_dir(), content))
468- found = cmdline.read_initramfs_config(
469- files=files, cmdline='foo root=/dev/sda', mac_addrs=self.macs)
470- self.assertIsNone(found)
471+ src = cmdline.KlibcNetworkConfigSource(
472+ _files=files, _cmdline='foo root=/dev/sda', _mac_addrs=self.macs,
473+ )
474+ self.assertFalse(src.is_applicable())
475
476 def test_with_both_ip_ip6(self):
477 content = {
478@@ -3667,13 +3676,77 @@ class TestCmdlineReadInitramfsConfig(FilesystemMockingTestCase):
479 populate_dir(root, content)
480 self.reRoot(root)
481
482- found = cmdline.read_initramfs_config(
483- cmdline='foo ip=dhcp ip6=dhcp', mac_addrs=self.macs)
484+ src = cmdline.KlibcNetworkConfigSource(
485+ _cmdline='foo ip=dhcp ip6=dhcp', _mac_addrs=self.macs,
486+ )
487
488+ self.assertTrue(src.is_applicable())
489+ found = src.render_config()
490 self.assertEqual(found['version'], 1)
491 self.assertEqual(found['config'], expected)
492
493
494+class TestReadInitramfsConfig(CiTestCase):
495+
496+ def _config_source_cls_mock(self, is_applicable, render_config=None):
497+ return lambda: mock.Mock(
498+ is_applicable=lambda: is_applicable,
499+ render_config=lambda: render_config,
500+ )
501+
502+ def test_no_sources(self):
503+ with mock.patch('cloudinit.net.cmdline._INITRAMFS_CONFIG_SOURCES', []):
504+ self.assertIsNone(cmdline.read_initramfs_config())
505+
506+ def test_no_applicable_sources(self):
507+ sources = [
508+ self._config_source_cls_mock(is_applicable=False),
509+ self._config_source_cls_mock(is_applicable=False),
510+ self._config_source_cls_mock(is_applicable=False),
511+ ]
512+ with mock.patch('cloudinit.net.cmdline._INITRAMFS_CONFIG_SOURCES',
513+ sources):
514+ self.assertIsNone(cmdline.read_initramfs_config())
515+
516+ def test_one_applicable_source(self):
517+ expected_config = object()
518+ sources = [
519+ self._config_source_cls_mock(
520+ is_applicable=True, render_config=expected_config,
521+ ),
522+ ]
523+ with mock.patch('cloudinit.net.cmdline._INITRAMFS_CONFIG_SOURCES',
524+ sources):
525+ self.assertEqual(expected_config, cmdline.read_initramfs_config())
526+
527+ def test_one_applicable_source_after_inapplicable_sources(self):
528+ expected_config = object()
529+ sources = [
530+ self._config_source_cls_mock(is_applicable=False),
531+ self._config_source_cls_mock(is_applicable=False),
532+ self._config_source_cls_mock(
533+ is_applicable=True, render_config=expected_config,
534+ ),
535+ ]
536+ with mock.patch('cloudinit.net.cmdline._INITRAMFS_CONFIG_SOURCES',
537+ sources):
538+ self.assertEqual(expected_config, cmdline.read_initramfs_config())
539+
540+ def test_first_applicable_source_is_used(self):
541+ first_config, second_config = object(), object()
542+ sources = [
543+ self._config_source_cls_mock(
544+ is_applicable=True, render_config=first_config,
545+ ),
546+ self._config_source_cls_mock(
547+ is_applicable=True, render_config=second_config,
548+ ),
549+ ]
550+ with mock.patch('cloudinit.net.cmdline._INITRAMFS_CONFIG_SOURCES',
551+ sources):
552+ self.assertEqual(first_config, cmdline.read_initramfs_config())
553+
554+
555 class TestNetplanRoundTrip(CiTestCase):
556 def _render_and_read(self, network_config=None, state=None,
557 netplan_path=None, target=None):

Subscribers

People subscribed via source and target branches