Merge ~oddbloke/cloud-init/+git/cloud-init:oci-vnic into cloud-init:master

Proposed by Dan Watkins
Status: Merged
Approved by: Dan Watkins
Approved revision: 63e2ed22336e8a8838cf10c586b07d751c811dc2
Merge reported by: Server Team CI bot
Merged at revision: not available
Proposed branch: ~oddbloke/cloud-init/+git/cloud-init:oci-vnic
Merge into: cloud-init:master
Diff against target: 431 lines (+324/-4)
3 files modified
cloudinit/sources/DataSourceOracle.py (+88/-1)
cloudinit/sources/tests/test_oracle.py (+212/-2)
doc/rtd/topics/datasources/oracle.rst (+24/-1)
Reviewer Review Type Date Requested Status
Server Team CI bot continuous-integration Approve
Chad Smith Approve
Dan Watkins Abstain
Review via email: mp+371053@code.launchpad.net

Commit message

DataSourceOracle: configure secondary NICs on Virtual Machines

Oracle Cloud Infrastructure's Instance Metadata Service provides network
configuration information for non-primary NICs. This commit introduces
support, on Virtual Machines[0], for fetching that network metadata,
converting it to v1 network-config[1] and combining it into the network
configuration generated for the primary interface.

By default, this behaviour is not enabled. Configuring the Oracle
datasource to `configure_secondary_nics` enables it:

    datasource:
      Oracle:
        configure_secondary_nics: true

Failures to fetch and generate secondary NIC configuration will log a
warning, but otherwise will not affect boot.

[0] The expected use of the IMDS-provided network configuration is
    substantially different on Bare Metal Machines, so support for that
    will be addressed separately.
[1] This is v1 config, because cloudinit.net.cmdline generates v1 config
    and we need to integrate the secondary NICs into that configuration.

To post a comment you must log in.
Revision history for this message
Dan Watkins (oddbloke) wrote :

Marking myself as Needs Fixing because I need to write docs before this lands (but that can happen while code review is ongoing).

review: Needs Fixing
Revision history for this message
Server Team CI bot (server-team-bot) wrote :

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

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

review: Approve (continuous-integration)
Revision history for this message
Dan Watkins (oddbloke) wrote :

Docs are now present, so switching my review to Abstain.

review: Abstain
Revision history for this message
Server Team CI bot (server-team-bot) wrote :

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

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

review: Approve (continuous-integration)
Revision history for this message
Chad Smith (chad.smith) :
Revision history for this message
Ryan Harper (raharper) wrote :

Looks good. A few in-line questions.

Revision history for this message
Dan Watkins (oddbloke) wrote :

Thanks for the reviews! Responses inline.

Revision history for this message
Dan Watkins (oddbloke) :
eb5dfc5... by Dan Watkins

DataSourceOracle: add explanatory comment about MTU 9000

And move it to a constant so the comment isn't clogging up other logic.

Revision history for this message
Dan Watkins (oddbloke) wrote :

Made some changes locally, still thinking about one other. (Not pushing them up until all comments are addressed, so I don't have to go digging to find the current set of comments.)

Revision history for this message
Chad Smith (chad.smith) wrote :

LGTM.

review: Approve
Revision history for this message
Chad Smith (chad.smith) wrote :

minor logging nit and a folowup question on _network_config_from_opc_imds mutate vs. altertive return values

8898a51... by Dan Watkins

DataSourceOracle: drop log level down to debug

Revision history for this message
Dan Watkins (oddbloke) :
f7209d5... by Dan Watkins

DataSourceOracle: don't return _and_ mutate network_config

Also raise log level to WARNING on Bare Metal Machines.

Revision history for this message
Server Team CI bot (server-team-bot) wrote :

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

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

review: Approve (continuous-integration)
Revision history for this message
Ryan Harper (raharper) :
Revision history for this message
Chad Smith (chad.smith) wrote :

Again another volley of trivial comments. Otherwise looks good to me!

Revision history for this message
Chad Smith (chad.smith) :
review: Approve
Revision history for this message
Dan Watkins (oddbloke) wrote :

Really good feedback, Chad, thanks! Will address tomorrow.

b950373... by Dan Watkins

DataSourceOracle: add explanatory comment per review

63e2ed2... by Dan Watkins

test_oracle: use a verbatim dump of OPC Bare Metal network data

Revision history for this message
Dan Watkins (oddbloke) wrote :

Both comments requiring action addressed.

Revision history for this message
Server Team CI bot (server-team-bot) wrote :

FAILED: Continuous integration, rev:246c359e4804a06edf43e456f733337329adf5a9
https://jenkins.ubuntu.com/server/job/cloud-init-ci/1041/
Executed test runs:
    SUCCESS: Checkout
    FAILED: Unit & Style Tests

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

review: Needs Fixing (continuous-integration)
Revision history for this message
Chad Smith (chad.smith) wrote :

+1 on this branch once CI is fixed for pycodestyle
cloudinit/sources/tests/test_oracle.py:25:80: E501 line too long (95 > 79 characters)
cloudinit/sources/tests/test_oracle.py:33:80: E501 line too long (95 > 79 characters)

review: Approve
Revision history for this message
Server Team CI bot (server-team-bot) wrote :

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

Click here to trigger a rebuild:
https://jenkins.ubuntu.com/server/job/cloud-init-ci/1042//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/sources/DataSourceOracle.py b/cloudinit/sources/DataSourceOracle.py
2index 76cfa38..086af79 100644
3--- a/cloudinit/sources/DataSourceOracle.py
4+++ b/cloudinit/sources/DataSourceOracle.py
5@@ -16,7 +16,7 @@ Notes:
6 """
7
8 from cloudinit.url_helper import combine_url, readurl, UrlError
9-from cloudinit.net import dhcp
10+from cloudinit.net import dhcp, get_interfaces_by_mac
11 from cloudinit import net
12 from cloudinit import sources
13 from cloudinit import util
14@@ -28,8 +28,80 @@ import re
15
16 LOG = logging.getLogger(__name__)
17
18+BUILTIN_DS_CONFIG = {
19+ # Don't use IMDS to configure secondary NICs by default
20+ 'configure_secondary_nics': False,
21+}
22 CHASSIS_ASSET_TAG = "OracleCloud.com"
23 METADATA_ENDPOINT = "http://169.254.169.254/openstack/"
24+VNIC_METADATA_URL = 'http://169.254.169.254/opc/v1/vnics/'
25+# https://docs.cloud.oracle.com/iaas/Content/Network/Troubleshoot/connectionhang.htm#Overview,
26+# indicates that an MTU of 9000 is used within OCI
27+MTU = 9000
28+
29+
30+def _add_network_config_from_opc_imds(network_config):
31+ """
32+ Fetch data from Oracle's IMDS, generate secondary NIC config, merge it.
33+
34+ The primary NIC configuration should not be modified based on the IMDS
35+ values, as it should continue to be configured for DHCP. As such, this
36+ takes an existing network_config dict which is expected to have the primary
37+ NIC configuration already present. It will mutate the given dict to
38+ include the secondary VNICs.
39+
40+ :param network_config:
41+ A v1 network config dict with the primary NIC already configured. This
42+ dict will be mutated.
43+
44+ :raises:
45+ Exceptions are not handled within this function. Likely exceptions are
46+ those raised by url_helper.readurl (if communicating with the IMDS
47+ fails), ValueError/JSONDecodeError (if the IMDS returns invalid JSON),
48+ and KeyError/IndexError (if the IMDS returns valid JSON with unexpected
49+ contents).
50+ """
51+ resp = readurl(VNIC_METADATA_URL)
52+ vnics = json.loads(str(resp))
53+
54+ if 'nicIndex' in vnics[0]:
55+ # TODO: Once configure_secondary_nics defaults to True, lower the level
56+ # of this log message. (Currently, if we're running this code at all,
57+ # someone has explicitly opted-in to secondary VNIC configuration, so
58+ # we should warn them that it didn't happen. Once it's default, this
59+ # would be emitted on every Bare Metal Machine launch, which means INFO
60+ # or DEBUG would be more appropriate.)
61+ LOG.warning(
62+ 'VNIC metadata indicates this is a bare metal machine; skipping'
63+ ' secondary VNIC configuration.'
64+ )
65+ return
66+
67+ interfaces_by_mac = get_interfaces_by_mac()
68+
69+ for vnic_dict in vnics[1:]:
70+ # We skip the first entry in the response because the primary interface
71+ # is already configured by iSCSI boot; applying configuration from the
72+ # IMDS is not required.
73+ mac_address = vnic_dict['macAddr'].lower()
74+ if mac_address not in interfaces_by_mac:
75+ LOG.debug('Interface with MAC %s not found; skipping', mac_address)
76+ continue
77+ name = interfaces_by_mac[mac_address]
78+ subnet = {
79+ 'type': 'static',
80+ 'address': vnic_dict['privateIp'],
81+ 'netmask': vnic_dict['subnetCidrBlock'].split('/')[1],
82+ 'gateway': vnic_dict['virtualRouterIp'],
83+ 'control': 'manual',
84+ }
85+ network_config['config'].append({
86+ 'name': name,
87+ 'type': 'physical',
88+ 'mac_address': mac_address,
89+ 'mtu': MTU,
90+ 'subnets': [subnet],
91+ })
92
93
94 class DataSourceOracle(sources.DataSource):
95@@ -39,6 +111,13 @@ class DataSourceOracle(sources.DataSource):
96 vendordata_pure = None
97 _network_config = sources.UNSET
98
99+ def __init__(self, sys_cfg, *args, **kwargs):
100+ super(DataSourceOracle, self).__init__(sys_cfg, *args, **kwargs)
101+
102+ self.ds_cfg = util.mergemanydict([
103+ util.get_cfg_by_path(sys_cfg, ['datasource', self.dsname], {}),
104+ BUILTIN_DS_CONFIG])
105+
106 def _is_platform_viable(self):
107 """Check platform environment to report if this datasource may run."""
108 return _is_platform_viable()
109@@ -121,6 +200,14 @@ class DataSourceOracle(sources.DataSource):
110 self._network_config = cmdline.read_initramfs_config()
111 if not self._network_config:
112 self._network_config = self.distro.generate_fallback_config()
113+ if self.ds_cfg.get('configure_secondary_nics'):
114+ try:
115+ # Mutate self._network_config to include secondary VNICs
116+ _add_network_config_from_opc_imds(self._network_config)
117+ except Exception:
118+ util.logexc(
119+ LOG,
120+ "Failed to fetch secondary network configuration!")
121 return self._network_config
122
123
124diff --git a/cloudinit/sources/tests/test_oracle.py b/cloudinit/sources/tests/test_oracle.py
125index 282382c..3e14677 100644
126--- a/cloudinit/sources/tests/test_oracle.py
127+++ b/cloudinit/sources/tests/test_oracle.py
128@@ -18,10 +18,52 @@ import uuid
129 DS_PATH = "cloudinit.sources.DataSourceOracle"
130 MD_VER = "2013-10-17"
131
132+# `curl -L http://169.254.169.254/opc/v1/vnics/` on a Oracle Bare Metal Machine
133+# with a secondary VNIC attached (vnicId truncated for Python line length)
134+OPC_BM_SECONDARY_VNIC_RESPONSE = """\
135+[ {
136+ "vnicId" : "ocid1.vnic.oc1.phx.abyhqljtyvcucqkhdqmgjszebxe4hrb!!TRUNCATED||",
137+ "privateIp" : "10.0.0.8",
138+ "vlanTag" : 0,
139+ "macAddr" : "90:e2:ba:d4:f1:68",
140+ "virtualRouterIp" : "10.0.0.1",
141+ "subnetCidrBlock" : "10.0.0.0/24",
142+ "nicIndex" : 0
143+}, {
144+ "vnicId" : "ocid1.vnic.oc1.phx.abyhqljtfmkxjdy2sqidndiwrsg63zf!!TRUNCATED||",
145+ "privateIp" : "10.0.4.5",
146+ "vlanTag" : 1,
147+ "macAddr" : "02:00:17:05:CF:51",
148+ "virtualRouterIp" : "10.0.4.1",
149+ "subnetCidrBlock" : "10.0.4.0/24",
150+ "nicIndex" : 0
151+} ]"""
152+
153+# `curl -L http://169.254.169.254/opc/v1/vnics/` on a Oracle Virtual Machine
154+# with a secondary VNIC attached
155+OPC_VM_SECONDARY_VNIC_RESPONSE = """\
156+[ {
157+ "vnicId" : "ocid1.vnic.oc1.phx.abyhqljtch72z5pd76cc2636qeqh7z_truncated",
158+ "privateIp" : "10.0.0.230",
159+ "vlanTag" : 1039,
160+ "macAddr" : "02:00:17:05:D1:DB",
161+ "virtualRouterIp" : "10.0.0.1",
162+ "subnetCidrBlock" : "10.0.0.0/24"
163+}, {
164+ "vnicId" : "ocid1.vnic.oc1.phx.abyhqljt4iew3gwmvrwrhhf3bp5drj_truncated",
165+ "privateIp" : "10.0.0.231",
166+ "vlanTag" : 1041,
167+ "macAddr" : "00:00:17:02:2B:B1",
168+ "virtualRouterIp" : "10.0.0.1",
169+ "subnetCidrBlock" : "10.0.0.0/24"
170+} ]"""
171+
172
173 class TestDataSourceOracle(test_helpers.CiTestCase):
174 """Test datasource DataSourceOracle."""
175
176+ with_logs = True
177+
178 ds_class = oracle.DataSourceOracle
179
180 my_uuid = str(uuid.uuid4())
181@@ -79,6 +121,16 @@ class TestDataSourceOracle(test_helpers.CiTestCase):
182 self.assertEqual(
183 'metadata (http://169.254.169.254/openstack/)', ds.subplatform)
184
185+ def test_sys_cfg_can_enable_configure_secondary_nics(self):
186+ # Confirm that behaviour is toggled by sys_cfg
187+ ds, _mocks = self._get_ds()
188+ self.assertFalse(ds.ds_cfg['configure_secondary_nics'])
189+
190+ sys_cfg = {
191+ 'datasource': {'Oracle': {'configure_secondary_nics': True}}}
192+ ds, _mocks = self._get_ds(sys_cfg=sys_cfg)
193+ self.assertTrue(ds.ds_cfg['configure_secondary_nics'])
194+
195 @mock.patch(DS_PATH + "._is_iscsi_root", return_value=True)
196 def test_without_userdata(self, m_is_iscsi_root):
197 """If no user-data is provided, it should not be in return dict."""
198@@ -133,9 +185,12 @@ class TestDataSourceOracle(test_helpers.CiTestCase):
199 self.assertEqual(self.my_md['uuid'], ds.get_instance_id())
200 self.assertEqual(my_userdata, ds.userdata_raw)
201
202+ @mock.patch(DS_PATH + "._add_network_config_from_opc_imds",
203+ side_effect=lambda network_config: network_config)
204 @mock.patch(DS_PATH + ".cmdline.read_initramfs_config")
205 @mock.patch(DS_PATH + "._is_iscsi_root", return_value=True)
206- def test_network_cmdline(self, m_is_iscsi_root, m_initramfs_config):
207+ def test_network_cmdline(self, m_is_iscsi_root, m_initramfs_config,
208+ _m_add_network_config_from_opc_imds):
209 """network_config should read kernel cmdline."""
210 distro = mock.MagicMock()
211 ds, _ = self._get_ds(distro=distro, patches={
212@@ -151,9 +206,12 @@ class TestDataSourceOracle(test_helpers.CiTestCase):
213 self.assertEqual([mock.call()], m_initramfs_config.call_args_list)
214 self.assertFalse(distro.generate_fallback_config.called)
215
216+ @mock.patch(DS_PATH + "._add_network_config_from_opc_imds",
217+ side_effect=lambda network_config: network_config)
218 @mock.patch(DS_PATH + ".cmdline.read_initramfs_config")
219 @mock.patch(DS_PATH + "._is_iscsi_root", return_value=True)
220- def test_network_fallback(self, m_is_iscsi_root, m_initramfs_config):
221+ def test_network_fallback(self, m_is_iscsi_root, m_initramfs_config,
222+ _m_add_network_config_from_opc_imds):
223 """test that fallback network is generated if no kernel cmdline."""
224 distro = mock.MagicMock()
225 ds, _ = self._get_ds(distro=distro, patches={
226@@ -175,6 +233,76 @@ class TestDataSourceOracle(test_helpers.CiTestCase):
227 self.assertEqual(ncfg, ds.network_config)
228 self.assertEqual(1, m_initramfs_config.call_count)
229
230+ @mock.patch(DS_PATH + "._add_network_config_from_opc_imds")
231+ @mock.patch(DS_PATH + ".cmdline.read_initramfs_config",
232+ return_value={'some': 'config'})
233+ @mock.patch(DS_PATH + "._is_iscsi_root", return_value=True)
234+ def test_secondary_nics_added_to_network_config_if_enabled(
235+ self, _m_is_iscsi_root, _m_initramfs_config,
236+ m_add_network_config_from_opc_imds):
237+
238+ needle = object()
239+
240+ def network_config_side_effect(network_config):
241+ network_config['secondary_added'] = needle
242+
243+ m_add_network_config_from_opc_imds.side_effect = (
244+ network_config_side_effect)
245+
246+ distro = mock.MagicMock()
247+ ds, _ = self._get_ds(distro=distro, patches={
248+ '_is_platform_viable': {'return_value': True},
249+ 'crawl_metadata': {
250+ 'return_value': {
251+ MD_VER: {'system_uuid': self.my_uuid,
252+ 'meta_data': self.my_md}}}})
253+ ds.ds_cfg['configure_secondary_nics'] = True
254+ self.assertEqual(needle, ds.network_config['secondary_added'])
255+
256+ @mock.patch(DS_PATH + "._add_network_config_from_opc_imds")
257+ @mock.patch(DS_PATH + ".cmdline.read_initramfs_config",
258+ return_value={'some': 'config'})
259+ @mock.patch(DS_PATH + "._is_iscsi_root", return_value=True)
260+ def test_secondary_nics_not_added_to_network_config_by_default(
261+ self, _m_is_iscsi_root, _m_initramfs_config,
262+ m_add_network_config_from_opc_imds):
263+
264+ def network_config_side_effect(network_config):
265+ network_config['secondary_added'] = True
266+
267+ m_add_network_config_from_opc_imds.side_effect = (
268+ network_config_side_effect)
269+
270+ distro = mock.MagicMock()
271+ ds, _ = self._get_ds(distro=distro, patches={
272+ '_is_platform_viable': {'return_value': True},
273+ 'crawl_metadata': {
274+ 'return_value': {
275+ MD_VER: {'system_uuid': self.my_uuid,
276+ 'meta_data': self.my_md}}}})
277+ self.assertNotIn('secondary_added', ds.network_config)
278+
279+ @mock.patch(DS_PATH + "._add_network_config_from_opc_imds")
280+ @mock.patch(DS_PATH + ".cmdline.read_initramfs_config")
281+ @mock.patch(DS_PATH + "._is_iscsi_root", return_value=True)
282+ def test_secondary_nic_failure_isnt_blocking(
283+ self, _m_is_iscsi_root, m_initramfs_config,
284+ m_add_network_config_from_opc_imds):
285+
286+ m_add_network_config_from_opc_imds.side_effect = Exception()
287+
288+ distro = mock.MagicMock()
289+ ds, _ = self._get_ds(distro=distro, patches={
290+ '_is_platform_viable': {'return_value': True},
291+ 'crawl_metadata': {
292+ 'return_value': {
293+ MD_VER: {'system_uuid': self.my_uuid,
294+ 'meta_data': self.my_md}}}})
295+ ds.ds_cfg['configure_secondary_nics'] = True
296+ self.assertEqual(ds.network_config, m_initramfs_config.return_value)
297+ self.assertIn('Failed to fetch secondary network configuration',
298+ self.logs.getvalue())
299+
300
301 @mock.patch(DS_PATH + "._read_system_uuid", return_value=str(uuid.uuid4()))
302 class TestReadMetaData(test_helpers.HttprettyTestCase):
303@@ -335,4 +463,86 @@ class TestLoadIndex(test_helpers.CiTestCase):
304 oracle._load_index("\n".join(["meta_data.json", "user_data"])))
305
306
307+class TestNetworkConfigFromOpcImds(test_helpers.CiTestCase):
308+
309+ with_logs = True
310+
311+ def setUp(self):
312+ super(TestNetworkConfigFromOpcImds, self).setUp()
313+ self.add_patch(DS_PATH + '.readurl', 'm_readurl')
314+ self.add_patch(DS_PATH + '.get_interfaces_by_mac',
315+ 'm_get_interfaces_by_mac')
316+
317+ def test_failure_to_readurl(self):
318+ # readurl failures should just bubble out to the caller
319+ self.m_readurl.side_effect = Exception('oh no')
320+ with self.assertRaises(Exception) as excinfo:
321+ oracle._add_network_config_from_opc_imds({})
322+ self.assertEqual(str(excinfo.exception), 'oh no')
323+
324+ def test_empty_response(self):
325+ # empty response error should just bubble out to the caller
326+ self.m_readurl.return_value = ''
327+ with self.assertRaises(Exception):
328+ oracle._add_network_config_from_opc_imds([])
329+
330+ def test_invalid_json(self):
331+ # invalid JSON error should just bubble out to the caller
332+ self.m_readurl.return_value = '{'
333+ with self.assertRaises(Exception):
334+ oracle._add_network_config_from_opc_imds([])
335+
336+ def test_no_secondary_nics_does_not_mutate_input(self):
337+ self.m_readurl.return_value = json.dumps([{}])
338+ # We test this by passing in a non-dict to ensure that no dict
339+ # operations are used; failure would be seen as exceptions
340+ oracle._add_network_config_from_opc_imds(object())
341+
342+ def test_bare_metal_machine_skipped(self):
343+ # nicIndex in the first entry indicates a bare metal machine
344+ self.m_readurl.return_value = OPC_BM_SECONDARY_VNIC_RESPONSE
345+ # We test this by passing in a non-dict to ensure that no dict
346+ # operations are used
347+ self.assertFalse(oracle._add_network_config_from_opc_imds(object()))
348+ self.assertIn('bare metal machine', self.logs.getvalue())
349+
350+ def test_missing_mac_skipped(self):
351+ self.m_readurl.return_value = OPC_VM_SECONDARY_VNIC_RESPONSE
352+ self.m_get_interfaces_by_mac.return_value = {}
353+
354+ network_config = {'version': 1, 'config': [{'primary': 'nic'}]}
355+ oracle._add_network_config_from_opc_imds(network_config)
356+
357+ self.assertEqual(1, len(network_config['config']))
358+ self.assertIn(
359+ 'Interface with MAC 00:00:17:02:2b:b1 not found; skipping',
360+ self.logs.getvalue())
361+
362+ def test_secondary_nic(self):
363+ self.m_readurl.return_value = OPC_VM_SECONDARY_VNIC_RESPONSE
364+ mac_addr, nic_name = '00:00:17:02:2b:b1', 'ens3'
365+ self.m_get_interfaces_by_mac.return_value = {
366+ mac_addr: nic_name,
367+ }
368+
369+ network_config = {'version': 1, 'config': [{'primary': 'nic'}]}
370+ oracle._add_network_config_from_opc_imds(network_config)
371+
372+ # The input is mutated
373+ self.assertEqual(2, len(network_config['config']))
374+
375+ secondary_nic_cfg = network_config['config'][1]
376+ self.assertEqual(nic_name, secondary_nic_cfg['name'])
377+ self.assertEqual('physical', secondary_nic_cfg['type'])
378+ self.assertEqual(mac_addr, secondary_nic_cfg['mac_address'])
379+ self.assertEqual(9000, secondary_nic_cfg['mtu'])
380+
381+ self.assertEqual(1, len(secondary_nic_cfg['subnets']))
382+ subnet_cfg = secondary_nic_cfg['subnets'][0]
383+ # These values are hard-coded in OPC_VM_SECONDARY_VNIC_RESPONSE
384+ self.assertEqual('10.0.0.231', subnet_cfg['address'])
385+ self.assertEqual('24', subnet_cfg['netmask'])
386+ self.assertEqual('10.0.0.1', subnet_cfg['gateway'])
387+ self.assertEqual('manual', subnet_cfg['control'])
388+
389 # vi: ts=4 expandtab
390diff --git a/doc/rtd/topics/datasources/oracle.rst b/doc/rtd/topics/datasources/oracle.rst
391index f2383ce..98c4657 100644
392--- a/doc/rtd/topics/datasources/oracle.rst
393+++ b/doc/rtd/topics/datasources/oracle.rst
394@@ -8,7 +8,7 @@ This datasource reads metadata, vendor-data and user-data from
395
396 Oracle Platform
397 ---------------
398-OCI provides bare metal and virtual machines. In both cases,
399+OCI provides bare metal and virtual machines. In both cases,
400 the platform identifies itself via DMI data in the chassis asset tag
401 with the string 'OracleCloud.com'.
402
403@@ -22,5 +22,28 @@ Cloud-init has a specific datasource for Oracle in order to:
404 implementation.
405
406
407+Configuration
408+-------------
409+
410+The following configuration can be set for the datasource in system
411+configuration (in ``/etc/cloud/cloud.cfg`` or ``/etc/cloud/cloud.cfg.d/``).
412+
413+The settings that may be configured are:
414+
415+* **configure_secondary_nics**: A boolean, defaulting to False. If set
416+ to True on an OCI Virtual Machine, cloud-init will fetch networking
417+ metadata from Oracle's IMDS and use it to configure the non-primary
418+ network interface controllers in the system. If set to True on an
419+ OCI Bare Metal Machine, it will have no effect (though this may
420+ change in the future).
421+
422+An example configuration with the default values is provided below:
423+
424+.. sourcecode:: yaml
425+
426+ datasource:
427+ Oracle:
428+ configure_secondary_nics: false
429+
430 .. _Oracle Compute Infrastructure: https://cloud.oracle.com/
431 .. vi: textwidth=78

Subscribers

People subscribed via source and target branches