Merge ~sw37th/cloud-init:fix_for_opennebula into cloud-init:master

Proposed by Akihiko Ota
Status: Merged
Approved by: Scott Moser
Approved revision: 3cca98162744fbed83da1634779be92af4343b39
Merged at revision: 8a9421421497b3e7c05589c62389745d565c6633
Proposed branch: ~sw37th/cloud-init:fix_for_opennebula
Merge into: cloud-init:master
Diff against target: 501 lines (+241/-104)
4 files modified
cloudinit/net/__init__.py (+2/-2)
cloudinit/sources/DataSourceOpenNebula.py (+59/-53)
tests/unittests/test_datasource/test_opennebula.py (+177/-46)
tests/unittests/test_net.py (+3/-3)
Reviewer Review Type Date Requested Status
Scott Moser Approve
Server Team CI bot continuous-integration Needs Fixing
Review via email: mp+335217@code.launchpad.net

Commit message

OpenNebula: Improve network configuration support.

Network configuration in OpenNebula would only work if the host correctly
guessed the names of the devices in the guest. OpenNebula provided data
in its context.sh like 'ETH0_NETWORK', but if the guest named devices
differently then results were not predictable. This would occur with
Predictable Network Interface Names. To address this,
newer versions (of OpenNebula provide the mac address ETH0_MAC.
This function is present in 4.14 and documented officially in 5.0 docs.

This provides support for reading the mac addresses from the context.sh.
It also fixes cases where context.sh provided a field (ETH0_NETWORK
or ETH0_MASK) with a empty string. Previously the empty string would
be used rather than falling back to the default.

LP: #1719157, #1716397, #1736750

To post a comment you must log in.
Revision history for this message
Scott Moser (smoser) wrote :

Akihko,

Hi, Thanks for your merge proposal and for your patience in sticking with it. I'm sorry you went largely ignored in those bugs.

Over all this looks good and you're clearly fixing things that were broken or unimplemented.

Please feel free to ping me in IRC if you need more immediate feedback.

Some requests:
 a.) can you add some tests? there are tests at tests/unittests/test_datasource/test_opennebula.py
 b.) here is some general cleanups i saw when reviewing. Just reduces some copy-and-paste. http://paste.ubuntu.com/26183949/
 c.) can you find and add information here on what versions of OpenNebula add the mac address fields?

 d.) Do you happen to know if trunk was actually just completely broken for network config generation?
 e.) ultimately i'd like for OpenNebulaNetwork.gen_conf() to return the newer network config rather than returning ENI format and then converting.

 I wont push on 'd' now, but i'd like to have that in the future. I think its probably pretty easy to do, you'll a 'v1' or 'v2' as described in
  http://cloudinit.readthedocs.io/en/latest/topics/network-config-format-v1.html
  http://cloudinit.readthedocs.io/en/latest/topics/network-config-format-v2.html

Also, I'll update your "commit message" here to give squashed description (we squash when we merge).

Thanks again!

review: Needs Fixing
Revision history for this message
Scott Moser (smoser) wrote :

http://paste.ubuntu.com/26184053/

^ is an updated paste to support get_mask and get_network having empty strings.

72e6213... by Akihiko Ota

OpenNebula: clean up and reduces copy-and-paste

Applied a patch by Scott Moser (smoser).
http://paste.ubuntu.com/26183949/

0abbfc5... by Akihiko Ota

tests: add some tests for OpenNebula network interface

Revision history for this message
Akihiko Ota (sw37th) wrote :

Scott,

> Some requests:
> a.) can you add some tests? there are tests at
> tests/unittests/test_datasource/test_opennebula.py

I have added some tests. Please reviwe it.

> b.) here is some general cleanups i saw when reviewing. Just reduces some
> copy-and-paste. http://paste.ubuntu.com/26183949/

I applied this patch. Thanks.

> c.) can you find and add information here on what versions of OpenNebula add
> the mac address fields?

The discription of ETHx_MAC field can be found on OpenNebula 5.0 document.
(http://docs.opennebula.org/5.0/operation/references/template.html#context-section)
But it was already implemented at least in OpenNebula 4.14.

> d.) Do you happen to know if trunk was actually just completely broken for
> network config generation?
> e.) ultimately i'd like for OpenNebulaNetwork.gen_conf() to return the newer
> network config rather than returning ENI format and then converting.
>
> I wont push on 'd' now, but i'd like to have that in the future. I think its
> probably pretty easy to do, you'll a 'v1' or 'v2' as described in
> http://cloudinit.readthedocs.io/en/latest/topics/network-config-
> format-v1.html
> http://cloudinit.readthedocs.io/en/latest/topics/network-config-
> format-v2.html

I agree. It's better to modify gen_conf() to use 'v1' or 'v2' configuration.

> Also, I'll update your "commit message" here to give squashed description (we
> squash when we merge).

Thanks!

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

FAILED: Continuous integration, rev:
https://jenkins.ubuntu.com/server/job/cloud-init-ci/652/
Executed test runs:
    SUCCESS: Checkout
    SUCCESS: Unit & Style Tests
    SUCCESS: Ubuntu LTS: Build
    SUCCESS: Ubuntu LTS: Integration
    FAILED: MAAS Compatability Testing

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

review: Needs Fixing (continuous-integration)
Revision history for this message
Scott Moser (smoser) wrote :

Akihiko,

I looked to pull this in, but thought I'd add one more test for multiple nics.
Can you review and then integrate this patch?
  https://paste.ubuntu.com/26227893/

thanks for your patience.
Scott

Revision history for this message
Scott Moser (smoser) wrote :

Also, this one *has* to be done, it is why the c-i bot complained.
http://paste.ubuntu.com/26227914/

Revision history for this message
Scott Moser (smoser) :
review: Needs Fixing
849b139... by Akihiko Ota

merge 238e9da9c4423aed6aeaba8eb77fe747d0d0a071

3cca981... by Akihiko Ota

OpenNebula: avoid the c-i bot complaining

Applied a patch by Scott Moser (smoser).
https://code.launchpad.net/~sw37th/cloud-init/+git/cloud-init/+merge/335217/comments/878981

Revision history for this message
Akihiko Ota (sw37th) wrote :

Scott,

That's fine! I have merged your patches. Thanks!

Revision history for this message
Scott Moser (smoser) :
review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1diff --git a/cloudinit/net/__init__.py b/cloudinit/net/__init__.py
2index a1b0db1..c015e79 100644
3--- a/cloudinit/net/__init__.py
4+++ b/cloudinit/net/__init__.py
5@@ -18,7 +18,7 @@ SYS_CLASS_NET = "/sys/class/net/"
6 DEFAULT_PRIMARY_INTERFACE = 'eth0'
7
8
9-def _natural_sort_key(s, _nsre=re.compile('([0-9]+)')):
10+def natural_sort_key(s, _nsre=re.compile('([0-9]+)')):
11 """Sorting for Humans: natural sort order. Can be use as the key to sort
12 functions.
13 This will sort ['eth0', 'ens3', 'ens10', 'ens12', 'ens8', 'ens0'] as
14@@ -224,7 +224,7 @@ def find_fallback_nic(blacklist_drivers=None):
15
16 # if eth0 exists use it above anything else, otherwise get the interface
17 # that we can read 'first' (using the sorted defintion of first).
18- names = list(sorted(potential_interfaces, key=_natural_sort_key))
19+ names = list(sorted(potential_interfaces, key=natural_sort_key))
20 if DEFAULT_PRIMARY_INTERFACE in names:
21 names.remove(DEFAULT_PRIMARY_INTERFACE)
22 names.insert(0, DEFAULT_PRIMARY_INTERFACE)
23diff --git a/cloudinit/sources/DataSourceOpenNebula.py b/cloudinit/sources/DataSourceOpenNebula.py
24index f66c95d..ce47b6b 100644
25--- a/cloudinit/sources/DataSourceOpenNebula.py
26+++ b/cloudinit/sources/DataSourceOpenNebula.py
27@@ -12,6 +12,7 @@
28 #
29 # This file is part of cloud-init. See LICENSE file for license information.
30
31+import collections
32 import os
33 import pwd
34 import re
35@@ -19,6 +20,7 @@ import string
36
37 from cloudinit import log as logging
38 from cloudinit import net
39+from cloudinit.net import eni
40 from cloudinit import sources
41 from cloudinit import util
42
43@@ -89,11 +91,18 @@ class DataSourceOpenNebula(sources.DataSource):
44 return False
45
46 self.seed = seed
47- self.network_eni = results.get("network_config")
48+ self.network_eni = results.get('network-interfaces')
49 self.metadata = md
50 self.userdata_raw = results.get('userdata')
51 return True
52
53+ @property
54+ def network_config(self):
55+ if self.network_eni is not None:
56+ return eni.convert_eni_data(self.network_eni)
57+ else:
58+ return None
59+
60 def get_hostname(self, fqdn=False, resolve_ip=None):
61 if resolve_ip is None:
62 if self.dsmode == sources.DSMODE_NETWORK:
63@@ -116,58 +125,53 @@ class OpenNebulaNetwork(object):
64 self.context = context
65 if system_nics_by_mac is None:
66 system_nics_by_mac = get_physical_nics_by_mac()
67- self.ifaces = system_nics_by_mac
68+ self.ifaces = collections.OrderedDict(
69+ [k for k in sorted(system_nics_by_mac.items(),
70+ key=lambda k: net.natural_sort_key(k[1]))])
71+
72+ # OpenNebula 4.14+ provide macaddr for ETHX in variable ETH_MAC.
73+ # context_devname provides {mac.lower():ETHX, mac2.lower():ETHX}
74+ self.context_devname = {}
75+ for k, v in context.items():
76+ m = re.match(r'^(.+)_MAC$', k)
77+ if m:
78+ self.context_devname[v.lower()] = m.group(1)
79
80 def mac2ip(self, mac):
81- components = mac.split(':')[2:]
82- return [str(int(c, 16)) for c in components]
83+ return '.'.join([str(int(c, 16)) for c in mac.split(':')[2:]])
84
85- def get_ip(self, dev, components):
86- var_name = dev.upper() + '_IP'
87- if var_name in self.context:
88- return self.context[var_name]
89- else:
90- return '.'.join(components)
91+ def mac2network(self, mac):
92+ return self.mac2ip(mac).rpartition(".")[0] + ".0"
93
94- def get_mask(self, dev):
95- var_name = dev.upper() + '_MASK'
96- if var_name in self.context:
97- return self.context[var_name]
98- else:
99- return '255.255.255.0'
100+ def get_dns(self, dev):
101+ return self.get_field(dev, "dns", "").split()
102
103- def get_network(self, dev, components):
104- var_name = dev.upper() + '_NETWORK'
105- if var_name in self.context:
106- return self.context[var_name]
107- else:
108- return '.'.join(components[:-1]) + '.0'
109+ def get_domain(self, dev):
110+ return self.get_field(dev, "domain")
111+
112+ def get_ip(self, dev, mac):
113+ return self.get_field(dev, "ip", self.mac2ip(mac))
114
115 def get_gateway(self, dev):
116- var_name = dev.upper() + '_GATEWAY'
117- if var_name in self.context:
118- return self.context[var_name]
119- else:
120- return None
121+ return self.get_field(dev, "gateway")
122
123- def get_dns(self, dev):
124- var_name = dev.upper() + '_DNS'
125- if var_name in self.context:
126- return self.context[var_name]
127- else:
128- return None
129+ def get_mask(self, dev):
130+ return self.get_field(dev, "mask", "255.255.255.0")
131
132- def get_domain(self, dev):
133- var_name = dev.upper() + '_DOMAIN'
134- if var_name in self.context:
135- return self.context[var_name]
136- else:
137- return None
138+ def get_network(self, dev, mac):
139+ return self.get_field(dev, "network", self.mac2network(mac))
140+
141+ def get_field(self, dev, name, default=None):
142+ """return the field name in context for device dev.
143+
144+ context stores <dev>_<NAME> (example: eth0_DOMAIN).
145+ an empty string for value will return default."""
146+ val = self.context.get('_'.join((dev, name,)).upper())
147+ # allow empty string to return the default.
148+ return default if val in (None, "") else val
149
150 def gen_conf(self):
151- global_dns = []
152- if 'DNS' in self.context:
153- global_dns.append(self.context['DNS'])
154+ global_dns = self.context.get('DNS', "").split()
155
156 conf = []
157 conf.append('auto lo')
158@@ -175,29 +179,31 @@ class OpenNebulaNetwork(object):
159 conf.append('')
160
161 for mac, dev in self.ifaces.items():
162- ip_components = self.mac2ip(mac)
163+ mac = mac.lower()
164+
165+ # c_dev stores name in context 'ETHX' for this device.
166+ # dev stores the current system name.
167+ c_dev = self.context_devname.get(mac, dev)
168
169 conf.append('auto ' + dev)
170 conf.append('iface ' + dev + ' inet static')
171- conf.append(' address ' + self.get_ip(dev, ip_components))
172- conf.append(' network ' + self.get_network(dev, ip_components))
173- conf.append(' netmask ' + self.get_mask(dev))
174+ conf.append(' #hwaddress %s' % mac)
175+ conf.append(' address ' + self.get_ip(c_dev, mac))
176+ conf.append(' network ' + self.get_network(c_dev, mac))
177+ conf.append(' netmask ' + self.get_mask(c_dev))
178
179- gateway = self.get_gateway(dev)
180+ gateway = self.get_gateway(c_dev)
181 if gateway:
182 conf.append(' gateway ' + gateway)
183
184- domain = self.get_domain(dev)
185+ domain = self.get_domain(c_dev)
186 if domain:
187 conf.append(' dns-search ' + domain)
188
189 # add global DNS servers to all interfaces
190- dns = self.get_dns(dev)
191+ dns = self.get_dns(c_dev)
192 if global_dns or dns:
193- all_dns = global_dns
194- if dns:
195- all_dns.append(dns)
196- conf.append(' dns-nameservers ' + ' '.join(all_dns))
197+ conf.append(' dns-nameservers ' + ' '.join(global_dns + dns))
198
199 conf.append('')
200
201diff --git a/tests/unittests/test_datasource/test_opennebula.py b/tests/unittests/test_datasource/test_opennebula.py
202index 2326dd5..5c3ba01 100644
203--- a/tests/unittests/test_datasource/test_opennebula.py
204+++ b/tests/unittests/test_datasource/test_opennebula.py
205@@ -4,6 +4,7 @@ from cloudinit import helpers
206 from cloudinit.sources import DataSourceOpenNebula as ds
207 from cloudinit import util
208 from cloudinit.tests.helpers import mock, populate_dir, CiTestCase
209+from textwrap import dedent
210
211 import os
212 import pwd
213@@ -30,6 +31,8 @@ USER_DATA = '#cloud-config\napt_upgrade: true'
214 SSH_KEY = 'ssh-rsa AAAAB3NzaC1....sIkJhq8wdX+4I3A4cYbYP ubuntu@server-460-%i'
215 HOSTNAME = 'foo.example.com'
216 PUBLIC_IP = '10.0.0.3'
217+MACADDR = '02:00:0a:12:01:01'
218+IP_BY_MACADDR = '10.18.1.1'
219
220 DS_PATH = "cloudinit.sources.DataSourceOpenNebula"
221
222@@ -195,24 +198,96 @@ class TestOpenNebulaDataSource(CiTestCase):
223
224 @mock.patch(DS_PATH + ".get_physical_nics_by_mac")
225 def test_hostname(self, m_get_phys_by_mac):
226- m_get_phys_by_mac.return_value = {'02:00:0a:12:01:01': 'eth0'}
227- for k in ('HOSTNAME', 'PUBLIC_IP', 'IP_PUBLIC', 'ETH0_IP'):
228- my_d = os.path.join(self.tmp, k)
229- populate_context_dir(my_d, {k: PUBLIC_IP})
230- results = ds.read_context_disk_dir(my_d)
231+ for dev in ('eth0', 'ens3'):
232+ m_get_phys_by_mac.return_value = {MACADDR: dev}
233+ for k in ('HOSTNAME', 'PUBLIC_IP', 'IP_PUBLIC', 'ETH0_IP'):
234+ my_d = os.path.join(self.tmp, k)
235+ populate_context_dir(my_d, {k: PUBLIC_IP})
236+ results = ds.read_context_disk_dir(my_d)
237
238- self.assertTrue('metadata' in results)
239- self.assertTrue('local-hostname' in results['metadata'])
240- self.assertEqual(PUBLIC_IP, results['metadata']['local-hostname'])
241+ self.assertTrue('metadata' in results)
242+ self.assertTrue('local-hostname' in results['metadata'])
243+ self.assertEqual(
244+ PUBLIC_IP, results['metadata']['local-hostname'])
245
246 @mock.patch(DS_PATH + ".get_physical_nics_by_mac")
247 def test_network_interfaces(self, m_get_phys_by_mac):
248- m_get_phys_by_mac.return_value = {'02:00:0a:12:01:01': 'eth0'}
249- populate_context_dir(self.seed_dir, {'ETH0_IP': '1.2.3.4'})
250- results = ds.read_context_disk_dir(self.seed_dir)
251-
252- self.assertTrue('network-interfaces' in results)
253- self.assertTrue('1.2.3.4' in results['network-interfaces'])
254+ for dev in ('eth0', 'ens3'):
255+ m_get_phys_by_mac.return_value = {MACADDR: dev}
256+
257+ # without ETH0_MAC
258+ # for Older OpenNebula?
259+ populate_context_dir(self.seed_dir, {'ETH0_IP': IP_BY_MACADDR})
260+ results = ds.read_context_disk_dir(self.seed_dir)
261+
262+ self.assertTrue('network-interfaces' in results)
263+ self.assertTrue(IP_BY_MACADDR in results['network-interfaces'])
264+
265+ # ETH0_IP and ETH0_MAC
266+ populate_context_dir(
267+ self.seed_dir, {'ETH0_IP': IP_BY_MACADDR, 'ETH0_MAC': MACADDR})
268+ results = ds.read_context_disk_dir(self.seed_dir)
269+
270+ self.assertTrue('network-interfaces' in results)
271+ self.assertTrue(IP_BY_MACADDR in results['network-interfaces'])
272+
273+ # ETH0_IP with empty string and ETH0_MAC
274+ # in the case of using Virtual Network contains
275+ # "AR = [ TYPE = ETHER ]"
276+ populate_context_dir(
277+ self.seed_dir, {'ETH0_IP': '', 'ETH0_MAC': MACADDR})
278+ results = ds.read_context_disk_dir(self.seed_dir)
279+
280+ self.assertTrue('network-interfaces' in results)
281+ self.assertTrue(IP_BY_MACADDR in results['network-interfaces'])
282+
283+ # ETH0_NETWORK
284+ populate_context_dir(
285+ self.seed_dir, {
286+ 'ETH0_IP': IP_BY_MACADDR,
287+ 'ETH0_MAC': MACADDR,
288+ 'ETH0_NETWORK': '10.18.0.0'
289+ })
290+ results = ds.read_context_disk_dir(self.seed_dir)
291+
292+ self.assertTrue('network-interfaces' in results)
293+ self.assertTrue('10.18.0.0' in results['network-interfaces'])
294+
295+ # ETH0_NETWORK with empty string
296+ populate_context_dir(
297+ self.seed_dir, {
298+ 'ETH0_IP': IP_BY_MACADDR,
299+ 'ETH0_MAC': MACADDR,
300+ 'ETH0_NETWORK': ''
301+ })
302+ results = ds.read_context_disk_dir(self.seed_dir)
303+
304+ self.assertTrue('network-interfaces' in results)
305+ self.assertTrue('10.18.1.0' in results['network-interfaces'])
306+
307+ # ETH0_MASK
308+ populate_context_dir(
309+ self.seed_dir, {
310+ 'ETH0_IP': IP_BY_MACADDR,
311+ 'ETH0_MAC': MACADDR,
312+ 'ETH0_MASK': '255.255.0.0'
313+ })
314+ results = ds.read_context_disk_dir(self.seed_dir)
315+
316+ self.assertTrue('network-interfaces' in results)
317+ self.assertTrue('255.255.0.0' in results['network-interfaces'])
318+
319+ # ETH0_MASK with empty string
320+ populate_context_dir(
321+ self.seed_dir, {
322+ 'ETH0_IP': IP_BY_MACADDR,
323+ 'ETH0_MAC': MACADDR,
324+ 'ETH0_MASK': ''
325+ })
326+ results = ds.read_context_disk_dir(self.seed_dir)
327+
328+ self.assertTrue('network-interfaces' in results)
329+ self.assertTrue('255.255.255.0' in results['network-interfaces'])
330
331 def test_find_candidates(self):
332 def my_devs_with(criteria):
333@@ -233,7 +308,7 @@ class TestOpenNebulaDataSource(CiTestCase):
334
335 class TestOpenNebulaNetwork(unittest.TestCase):
336
337- system_nics = {'02:00:0a:12:01:01': 'eth0'}
338+ system_nics = ('eth0', 'ens3')
339
340 def test_lo(self):
341 net = ds.OpenNebulaNetwork(context={}, system_nics_by_mac={})
342@@ -244,45 +319,101 @@ iface lo inet loopback
343
344 @mock.patch(DS_PATH + ".get_physical_nics_by_mac")
345 def test_eth0(self, m_get_phys_by_mac):
346- m_get_phys_by_mac.return_value = self.system_nics
347- net = ds.OpenNebulaNetwork({})
348- self.assertEqual(net.gen_conf(), u'''\
349-auto lo
350-iface lo inet loopback
351-
352-auto eth0
353-iface eth0 inet static
354- address 10.18.1.1
355- network 10.18.1.0
356- netmask 255.255.255.0
357-''')
358+ for nic in self.system_nics:
359+ m_get_phys_by_mac.return_value = {MACADDR: nic}
360+ net = ds.OpenNebulaNetwork({})
361+ self.assertEqual(net.gen_conf(), dedent("""\
362+ auto lo
363+ iface lo inet loopback
364+
365+ auto {dev}
366+ iface {dev} inet static
367+ #hwaddress {macaddr}
368+ address 10.18.1.1
369+ network 10.18.1.0
370+ netmask 255.255.255.0
371+ """.format(dev=nic, macaddr=MACADDR)))
372
373 def test_eth0_override(self):
374 context = {
375 'DNS': '1.2.3.8',
376- 'ETH0_IP': '1.2.3.4',
377- 'ETH0_NETWORK': '1.2.3.0',
378+ 'ETH0_IP': '10.18.1.1',
379+ 'ETH0_NETWORK': '10.18.0.0',
380 'ETH0_MASK': '255.255.0.0',
381 'ETH0_GATEWAY': '1.2.3.5',
382 'ETH0_DOMAIN': 'example.com',
383- 'ETH0_DNS': '1.2.3.6 1.2.3.7'
384+ 'ETH0_DNS': '1.2.3.6 1.2.3.7',
385+ 'ETH0_MAC': '02:00:0a:12:01:01'
386 }
387-
388- net = ds.OpenNebulaNetwork(context,
389- system_nics_by_mac=self.system_nics)
390- self.assertEqual(net.gen_conf(), u'''\
391-auto lo
392-iface lo inet loopback
393-
394-auto eth0
395-iface eth0 inet static
396- address 1.2.3.4
397- network 1.2.3.0
398- netmask 255.255.0.0
399- gateway 1.2.3.5
400- dns-search example.com
401- dns-nameservers 1.2.3.8 1.2.3.6 1.2.3.7
402-''')
403+ for nic in self.system_nics:
404+ expected = dedent("""\
405+ auto lo
406+ iface lo inet loopback
407+
408+ auto {dev}
409+ iface {dev} inet static
410+ #hwaddress {macaddr}
411+ address 10.18.1.1
412+ network 10.18.0.0
413+ netmask 255.255.0.0
414+ gateway 1.2.3.5
415+ dns-search example.com
416+ dns-nameservers 1.2.3.8 1.2.3.6 1.2.3.7
417+ """).format(dev=nic, macaddr=MACADDR)
418+ net = ds.OpenNebulaNetwork(context,
419+ system_nics_by_mac={MACADDR: nic})
420+ self.assertEqual(expected, net.gen_conf())
421+
422+ def test_multiple_nics(self):
423+ """Test rendering multiple nics with names that differ from context."""
424+ MAC_1 = "02:00:0a:12:01:01"
425+ MAC_2 = "02:00:0a:12:01:02"
426+ context = {
427+ 'DNS': '1.2.3.8',
428+ 'ETH0_IP': '10.18.1.1',
429+ 'ETH0_NETWORK': '10.18.0.0',
430+ 'ETH0_MASK': '255.255.0.0',
431+ 'ETH0_GATEWAY': '1.2.3.5',
432+ 'ETH0_DOMAIN': 'example.com',
433+ 'ETH0_DNS': '1.2.3.6 1.2.3.7',
434+ 'ETH0_MAC': MAC_2,
435+ 'ETH3_IP': '10.3.1.3',
436+ 'ETH3_NETWORK': '10.3.0.0',
437+ 'ETH3_MASK': '255.255.0.0',
438+ 'ETH3_GATEWAY': '10.3.0.1',
439+ 'ETH3_DOMAIN': 'third.example.com',
440+ 'ETH3_DNS': '10.3.1.2',
441+ 'ETH3_MAC': MAC_1,
442+ }
443+ net = ds.OpenNebulaNetwork(
444+ context, system_nics_by_mac={MAC_1: 'enp0s25', MAC_2: 'enp1s2'})
445+
446+ expected = dedent("""\
447+ auto lo
448+ iface lo inet loopback
449+
450+ auto enp0s25
451+ iface enp0s25 inet static
452+ #hwaddress 02:00:0a:12:01:01
453+ address 10.3.1.3
454+ network 10.3.0.0
455+ netmask 255.255.0.0
456+ gateway 10.3.0.1
457+ dns-search third.example.com
458+ dns-nameservers 1.2.3.8 10.3.1.2
459+
460+ auto enp1s2
461+ iface enp1s2 inet static
462+ #hwaddress 02:00:0a:12:01:02
463+ address 10.18.1.1
464+ network 10.18.0.0
465+ netmask 255.255.0.0
466+ gateway 1.2.3.5
467+ dns-search example.com
468+ dns-nameservers 1.2.3.8 1.2.3.6 1.2.3.7
469+ """)
470+
471+ self.assertEqual(expected, net.gen_conf())
472
473
474 class TestParseShellConfig(unittest.TestCase):
475diff --git a/tests/unittests/test_net.py b/tests/unittests/test_net.py
476index f3fa2a3..ddea13d 100644
477--- a/tests/unittests/test_net.py
478+++ b/tests/unittests/test_net.py
479@@ -1,9 +1,9 @@
480 # This file is part of cloud-init. See LICENSE file for license information.
481
482 from cloudinit import net
483-from cloudinit.net import _natural_sort_key
484 from cloudinit.net import cmdline
485 from cloudinit.net import eni
486+from cloudinit.net import natural_sort_key
487 from cloudinit.net import netplan
488 from cloudinit.net import network_state
489 from cloudinit.net import renderers
490@@ -2708,11 +2708,11 @@ class TestInterfacesSorting(CiTestCase):
491 def test_natural_order(self):
492 data = ['ens5', 'ens6', 'ens3', 'ens20', 'ens13', 'ens2']
493 self.assertEqual(
494- sorted(data, key=_natural_sort_key),
495+ sorted(data, key=natural_sort_key),
496 ['ens2', 'ens3', 'ens5', 'ens6', 'ens13', 'ens20'])
497 data2 = ['enp2s0', 'enp2s3', 'enp0s3', 'enp0s13', 'enp0s8', 'enp1s2']
498 self.assertEqual(
499- sorted(data2, key=_natural_sort_key),
500+ sorted(data2, key=natural_sort_key),
501 ['enp0s3', 'enp0s8', 'enp0s13', 'enp1s2', 'enp2s0', 'enp2s3'])
502
503

Subscribers

People subscribed via source and target branches