Merge ~harald-jensas/cloud-init:bug/1806014 into cloud-init:master

Proposed by Harald Jensås
Status: Superseded
Proposed branch: ~harald-jensas/cloud-init:bug/1806014
Merge into: cloud-init:master
Prerequisite: ~harald-jensas/cloud-init:bug/1847517
Diff against target: 357 lines (+206/-6)
5 files modified
cloudinit/net/eni.py (+10/-0)
cloudinit/net/netplan.py (+4/-1)
cloudinit/net/network_state.py (+8/-4)
cloudinit/net/sysconfig.py (+9/-1)
tests/unittests/test_net.py (+175/-0)
Reviewer Review Type Date Requested Status
Ryan Harper Needs Fixing
Review via email: mp+374138@code.launchpad.net
To post a comment you must log in.
Revision history for this message
Ryan Harper (raharper) wrote :

Thanks for sorting this out. One more element here is related to kernel defaults; at least on Ubuntu the kernel defaults the interfaces accept_ra value to 1; which allows RAs on interfaces that aren't forwarding.

The code below which defaults accept-ra to False is going to break that. I would like to suggest that only *if* accept-ra is set in the source config (for example in the openstack network_data.json where dhcp6 stateful is specified, then it makes sense to include accept-ra: true in the interface config.

In the renderers, if accept-ra is *not* defined, I'd like to avoid emitting any RA related config at all.

Now that cloud-init handles dhcp6-stateless/stateful; I think we only want to enable RA on stateful dhcp6, otherwise we should leave the kernel defaults.

Additionally, ifupdown (eni) can toggle these values as well:

accept_ra [0, 1, 2]
autoconf [0, 1]

review: Needs Fixing
b8ddae4... by Harald Jensås

net: fix subnet_is_ipv6() for stateless|stateful

Function return false for ipv6_dhcpv6-stateless|stateful,
the eni renderer does not add '6' to 'inet' which is
incorrect.

The subnet_is_ipv6() function is updated to also return
true if startswith('ipv6').

LP: #1848690

Revision history for this message
Harald Jensås (harald-jensas) wrote :

> Thanks for sorting this out. One more element here is related to kernel
> defaults; at least on Ubuntu the kernel defaults the interfaces accept_ra
> value to 1; which allows RAs on interfaces that aren't forwarding.
>
> The code below which defaults accept-ra to False is going to break that. I
> would like to suggest that only *if* accept-ra is set in the source config
> (for example in the openstack network_data.json where dhcp6 stateful is
> specified, then it makes sense to include accept-ra: true in the interface
> config.
>
> In the renderers, if accept-ra is *not* defined, I'd like to avoid emitting
> any RA related config at all.
>
> Now that cloud-init handles dhcp6-stateless/stateful; I think we only want to
> enable RA on stateful dhcp6, otherwise we should leave the kernel defaults.
>
> Additionally, ifupdown (eni) can toggle these values as well:
>
> accept_ra [0, 1, 2]
> autoconf [0, 1]

Thanks Ryan, this all makes sense.
I have updated the patch to not just enable if accept-ra: true. But also disable if false. And it should not change the OS defaults if accept-ra is not defined at all.

Unmerged commits

577ccfa... by Harald Jensås

net: IPv6 implement accept-ra RA's

Router advertisements are required for the default route
to be set up.

sysconf: IPV6_FORCE_ACCEPT_RA controls accept_ra sysctl.
eni: mode static and mode dhcp 'accept_ra' controls sysctl.

Add 'accept-ra: true|false' parameter to config v1 and
v2. When True: accept_ra is set to '1'. When False:
accept_ra is set to '0'. When not defined in config the
value is left to the operating system default.

Also, set IPV6_FORCE_ACCEPT_RA=yes in sysconfig when
using Openstack and dhcpv6-stateful.

LP: #1806014
LP: #1808647

b8ddae4... by Harald Jensås

net: fix subnet_is_ipv6() for stateless|stateful

Function return false for ipv6_dhcpv6-stateless|stateful,
the eni renderer does not add '6' to 'inet' which is
incorrect.

The subnet_is_ipv6() function is updated to also return
true if startswith('ipv6').

LP: #1848690

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1diff --git a/cloudinit/net/eni.py b/cloudinit/net/eni.py
2index 530922b..458c229 100644
3--- a/cloudinit/net/eni.py
4+++ b/cloudinit/net/eni.py
5@@ -399,6 +399,7 @@ class Renderer(renderer.Renderer):
6 def _render_iface(self, iface, render_hwaddress=False):
7 sections = []
8 subnets = iface.get('subnets', {})
9+ accept_ra = iface.pop('accept-ra', None)
10 if subnets:
11 for index, subnet in enumerate(subnets):
12 ipv4_subnet_mtu = None
13@@ -415,9 +416,18 @@ class Renderer(renderer.Renderer):
14 subnet['type'] == 'ipv6_dhcpv6-stateful'):
15 # Configure network settings using DHCP or DHCPv6
16 iface['mode'] = 'dhcp'
17+ if accept_ra is not None:
18+ # Accept router advertisements (0=off, 1=on)
19+ iface['accept_ra'] = ('1' if util.is_true(accept_ra)
20+ else '0')
21 elif subnet['type'] == 'ipv6_dhcpv6-stateless':
22 # Configure network settings using SLAAC from RAs
23 iface['mode'] = 'auto'
24+ elif subnet_is_ipv6(subnet) and subnet['type'] == 'static':
25+ if accept_ra is not None:
26+ # Accept router advertisements (0=off, 1=on)
27+ iface['accept_ra'] = ('1' if util.is_true(accept_ra)
28+ else '0')
29
30 # do not emit multiple 'auto $IFACE' lines as older (precise)
31 # ifupdown complains
32diff --git a/cloudinit/net/netplan.py b/cloudinit/net/netplan.py
33index e54a34e..78023f8 100644
34--- a/cloudinit/net/netplan.py
35+++ b/cloudinit/net/netplan.py
36@@ -51,7 +51,8 @@ def _extract_addresses(config, entry, ifname):
37 'mtu': 1480,
38 'netmask': 64,
39 'type': 'static'}],
40- 'type: physical'
41+ 'type: physical',
42+ 'accept-ra': 'true'
43 }
44
45 An entry dictionary looks like:
46@@ -144,6 +145,8 @@ def _extract_addresses(config, entry, ifname):
47 ns = entry.get('nameservers', {})
48 ns.update({'search': searchdomains})
49 entry.update({'nameservers': ns})
50+ if 'accept-ra' in config:
51+ entry.update({'accept-ra': config.get('accept-ra')})
52
53
54 def _extract_bond_slaves_by_name(interfaces, entry, bond_master):
55diff --git a/cloudinit/net/network_state.py b/cloudinit/net/network_state.py
56index c0c415d..cc8beaa 100644
57--- a/cloudinit/net/network_state.py
58+++ b/cloudinit/net/network_state.py
59@@ -22,7 +22,8 @@ NETWORK_STATE_REQUIRED_KEYS = {
60 }
61 NETWORK_V2_KEY_FILTER = [
62 'addresses', 'dhcp4', 'dhcp6', 'gateway4', 'gateway6', 'interfaces',
63- 'match', 'mtu', 'nameservers', 'renderer', 'set-name', 'wakeonlan'
64+ 'match', 'mtu', 'nameservers', 'renderer', 'set-name', 'wakeonlan',
65+ 'accept-ra'
66 ]
67
68 NET_CONFIG_TO_V2 = {
69@@ -340,7 +341,8 @@ class NetworkStateInterpreter(object):
70 'name': 'eth0',
71 'subnets': [
72 {'type': 'dhcp4'}
73- ]
74+ ],
75+ 'accept-ra': 'true'
76 }
77 '''
78
79@@ -370,6 +372,7 @@ class NetworkStateInterpreter(object):
80 'address': None,
81 'gateway': None,
82 'subnets': subnets,
83+ 'accept-ra': command.get('accept-ra')
84 })
85 self._network_state['interfaces'].update({command.get('name'): iface})
86 self.dump_network_state()
87@@ -613,6 +616,7 @@ class NetworkStateInterpreter(object):
88 driver: ixgbe
89 set-name: lom1
90 dhcp6: true
91+ accept-ra: true
92 switchports:
93 match:
94 name: enp2*
95@@ -641,7 +645,7 @@ class NetworkStateInterpreter(object):
96 driver = match.get('driver', None)
97 if driver:
98 phy_cmd['params'] = {'driver': driver}
99- for key in ['mtu', 'match', 'wakeonlan']:
100+ for key in ['mtu', 'match', 'wakeonlan', 'accept-ra']:
101 if key in cfg:
102 phy_cmd[key] = cfg[key]
103
104@@ -919,7 +923,7 @@ def is_ipv6_addr(address):
105 def subnet_is_ipv6(subnet):
106 """Common helper for checking network_state subnets for ipv6."""
107 # 'static6' or 'dhcp6'
108- if subnet['type'].endswith('6'):
109+ if subnet['type'].endswith('6') or subnet['type'].startswith('ipv6'):
110 # This is a request for DHCPv6.
111 return True
112 elif subnet['type'] == 'static' and is_ipv6_addr(subnet.get('address')):
113diff --git a/cloudinit/net/sysconfig.py b/cloudinit/net/sysconfig.py
114index 4e65676..3fe5793 100644
115--- a/cloudinit/net/sysconfig.py
116+++ b/cloudinit/net/sysconfig.py
117@@ -333,6 +333,9 @@ class Renderer(renderer.Renderer):
118 if old_key == 'mac_address' and iface['type'] != 'physical':
119 continue
120 iface_cfg[new_key] = old_value
121+ accept_ra = iface.get('accept-ra', None)
122+ if accept_ra is not None:
123+ iface_cfg['IPV6_FORCE_ACCEPT_RA'] = util.is_true(accept_ra)
124
125 @classmethod
126 def _render_subnets(cls, iface_cfg, subnets, has_default_route):
127@@ -343,11 +346,16 @@ class Renderer(renderer.Renderer):
128 for i, subnet in enumerate(subnets, start=len(iface_cfg.children)):
129 mtu_key = 'MTU'
130 subnet_type = subnet.get('type')
131- if subnet_type == 'dhcp6' or subnet_type == 'ipv6_dhcpv6-stateful':
132+ if subnet_type == 'dhcp6':
133 # TODO need to set BOOTPROTO to dhcp6 on SUSE
134 iface_cfg['IPV6INIT'] = True
135 # Configure network settings using DHCPv6
136 iface_cfg['DHCPV6C'] = True
137+ elif subnet_type == 'ipv6_dhcpv6-stateful':
138+ iface_cfg['IPV6INIT'] = True
139+ # Configure network settings using DHCPv6
140+ iface_cfg['DHCPV6C'] = True
141+ iface_cfg['IPV6_FORCE_ACCEPT_RA'] = True
142 elif subnet_type == 'ipv6_dhcpv6-stateless':
143 iface_cfg['IPV6INIT'] = True
144 # Configure network settings using SLAAC from RAs
145diff --git a/tests/unittests/test_net.py b/tests/unittests/test_net.py
146index f5a9cae..62b6ac6 100644
147--- a/tests/unittests/test_net.py
148+++ b/tests/unittests/test_net.py
149@@ -1070,6 +1070,104 @@ NETWORK_CONFIGS = {
150 """),
151 },
152 },
153+ 'dhcpv6_accept_ra': {
154+ 'expected_eni': textwrap.dedent("""\
155+ auto lo
156+ iface lo inet loopback
157+
158+ auto iface0
159+ iface iface0 inet6 dhcp
160+ accept_ra 1
161+ """).rstrip(' '),
162+ 'expected_netplan': textwrap.dedent("""
163+ network:
164+ version: 2
165+ ethernets:
166+ iface0:
167+ accept-ra: 'true'
168+ dhcp6: true
169+ """).rstrip(' '),
170+ 'yaml_v1': textwrap.dedent("""\
171+ version: 1
172+ config:
173+ - type: 'physical'
174+ name: 'iface0'
175+ subnets:
176+ - {'type': 'dhcp6'}
177+ accept-ra: 'true'
178+ """).rstrip(' '),
179+ 'yaml_v2': textwrap.dedent("""\
180+ version: 2
181+ ethernets:
182+ iface0:
183+ dhcp6: true
184+ accept-ra: 'true'
185+ """).rstrip(' '),
186+ 'expected_sysconfig': {
187+ 'ifcfg-iface0': textwrap.dedent("""\
188+ BOOTPROTO=none
189+ DEVICE=iface0
190+ DHCPV6C=yes
191+ IPV6INIT=yes
192+ IPV6_FORCE_ACCEPT_RA=yes
193+ DEVICE=iface0
194+ NM_CONTROLLED=no
195+ ONBOOT=yes
196+ STARTMODE=auto
197+ TYPE=Ethernet
198+ USERCTL=no
199+ """),
200+ },
201+ },
202+ 'dhcpv6_reject_ra': {
203+ 'expected_eni': textwrap.dedent("""\
204+ auto lo
205+ iface lo inet loopback
206+
207+ auto iface0
208+ iface iface0 inet6 dhcp
209+ accept_ra 0
210+ """).rstrip(' '),
211+ 'expected_netplan': textwrap.dedent("""
212+ network:
213+ version: 2
214+ ethernets:
215+ iface0:
216+ accept-ra: 'false'
217+ dhcp6: true
218+ """).rstrip(' '),
219+ 'yaml_v1': textwrap.dedent("""\
220+ version: 1
221+ config:
222+ - type: 'physical'
223+ name: 'iface0'
224+ subnets:
225+ - {'type': 'dhcp6'}
226+ accept-ra: 'false'
227+ """).rstrip(' '),
228+ 'yaml_v2': textwrap.dedent("""\
229+ version: 2
230+ ethernets:
231+ iface0:
232+ dhcp6: true
233+ accept-ra: 'false'
234+ """).rstrip(' '),
235+ 'expected_sysconfig': {
236+ 'ifcfg-iface0': textwrap.dedent("""\
237+ BOOTPROTO=none
238+ DEVICE=iface0
239+ DHCPV6C=yes
240+ IPV6INIT=yes
241+ IPV6_FORCE_ACCEPT_RA=no
242+ DEVICE=iface0
243+ NM_CONTROLLED=no
244+ ONBOOT=yes
245+ STARTMODE=auto
246+ TYPE=Ethernet
247+ USERCTL=no
248+ """),
249+ },
250+ },
251 'dhcpv6_stateless': {
252 'expected_eni': textwrap.dedent("""\
253 auto lo
254@@ -1137,6 +1235,7 @@ NETWORK_CONFIGS = {
255 DEVICE=iface0
256 DHCPV6C=yes
257 IPV6INIT=yes
258+ IPV6_FORCE_ACCEPT_RA=yes
259 DEVICE=iface0
260 NM_CONTROLLED=no
261 ONBOOT=yes
262@@ -2857,6 +2956,34 @@ USERCTL=no
263 self._compare_files_to_expected(entry[self.expected_name], found)
264 self._assert_headers(found)
265
266+ def test_dhcpv6_accept_ra_config_v1(self):
267+ entry = NETWORK_CONFIGS['dhcpv6_accept_ra']
268+ found = self._render_and_read(network_config=yaml.load(
269+ entry['yaml_v1']))
270+ self._compare_files_to_expected(entry[self.expected_name], found)
271+ self._assert_headers(found)
272+
273+ def test_dhcpv6_accept_ra_config_v2(self):
274+ entry = NETWORK_CONFIGS['dhcpv6_accept_ra']
275+ found = self._render_and_read(network_config=yaml.load(
276+ entry['yaml_v2']))
277+ self._compare_files_to_expected(entry[self.expected_name], found)
278+ self._assert_headers(found)
279+
280+ def test_dhcpv6_reject_ra_config_v1(self):
281+ entry = NETWORK_CONFIGS['dhcpv6_reject_ra']
282+ found = self._render_and_read(network_config=yaml.load(
283+ entry['yaml_v1']))
284+ self._compare_files_to_expected(entry[self.expected_name], found)
285+ self._assert_headers(found)
286+
287+ def test_dhcpv6_reject_ra_config_v2(self):
288+ entry = NETWORK_CONFIGS['dhcpv6_reject_ra']
289+ found = self._render_and_read(network_config=yaml.load(
290+ entry['yaml_v2']))
291+ self._compare_files_to_expected(entry[self.expected_name], found)
292+ self._assert_headers(found)
293+
294 def test_dhcpv6_stateless_config(self):
295 entry = NETWORK_CONFIGS['dhcpv6_stateless']
296 found = self._render_and_read(network_config=yaml.load(entry['yaml']))
297@@ -3918,6 +4045,22 @@ class TestNetplanRoundTrip(CiTestCase):
298 entry['expected_netplan'].splitlines(),
299 files['/etc/netplan/50-cloud-init.yaml'].splitlines())
300
301+ def testsimple_render_dhcpv6_accept_ra(self):
302+ entry = NETWORK_CONFIGS['dhcpv6_accept_ra']
303+ files = self._render_and_read(network_config=yaml.load(
304+ entry['yaml_v1']))
305+ self.assertEqual(
306+ entry['expected_netplan'].splitlines(),
307+ files['/etc/netplan/50-cloud-init.yaml'].splitlines())
308+
309+ def testsimple_render_dhcpv6_reject_ra(self):
310+ entry = NETWORK_CONFIGS['dhcpv6_reject_ra']
311+ files = self._render_and_read(network_config=yaml.load(
312+ entry['yaml_v1']))
313+ self.assertEqual(
314+ entry['expected_netplan'].splitlines(),
315+ files['/etc/netplan/50-cloud-init.yaml'].splitlines())
316+
317 def testsimple_render_all(self):
318 entry = NETWORK_CONFIGS['all']
319 files = self._render_and_read(network_config=yaml.load(entry['yaml']))
320@@ -4048,6 +4191,38 @@ class TestEniRoundTrip(CiTestCase):
321 entry['expected_eni'].splitlines(),
322 files['/etc/network/interfaces'].splitlines())
323
324+ def testsimple_render_dhcpv6_stateless(self):
325+ entry = NETWORK_CONFIGS['dhcpv6_stateless']
326+ files = self._render_and_read(network_config=yaml.load(
327+ entry['yaml']))
328+ self.assertEqual(
329+ entry['expected_eni'].splitlines(),
330+ files['/etc/network/interfaces'].splitlines())
331+
332+ def testsimple_render_dhcpv6_stateful(self):
333+ entry = NETWORK_CONFIGS['dhcpv6_stateless']
334+ files = self._render_and_read(network_config=yaml.load(
335+ entry['yaml']))
336+ self.assertEqual(
337+ entry['expected_eni'].splitlines(),
338+ files['/etc/network/interfaces'].splitlines())
339+
340+ def testsimple_render_dhcpv6_accept_ra(self):
341+ entry = NETWORK_CONFIGS['dhcpv6_accept_ra']
342+ files = self._render_and_read(network_config=yaml.load(
343+ entry['yaml_v1']))
344+ self.assertEqual(
345+ entry['expected_eni'].splitlines(),
346+ files['/etc/network/interfaces'].splitlines())
347+
348+ def testsimple_render_dhcpv6_reject_ra(self):
349+ entry = NETWORK_CONFIGS['dhcpv6_reject_ra']
350+ files = self._render_and_read(network_config=yaml.load(
351+ entry['yaml_v1']))
352+ self.assertEqual(
353+ entry['expected_eni'].splitlines(),
354+ files['/etc/network/interfaces'].splitlines())
355+
356 def testsimple_render_manual(self):
357 """Test rendering of 'manual' for 'type' and 'control'.
358

Subscribers

People subscribed via source and target branches