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
diff --git a/cloudinit/net/eni.py b/cloudinit/net/eni.py
index 530922b..458c229 100644
--- a/cloudinit/net/eni.py
+++ b/cloudinit/net/eni.py
@@ -399,6 +399,7 @@ class Renderer(renderer.Renderer):
399 def _render_iface(self, iface, render_hwaddress=False):399 def _render_iface(self, iface, render_hwaddress=False):
400 sections = []400 sections = []
401 subnets = iface.get('subnets', {})401 subnets = iface.get('subnets', {})
402 accept_ra = iface.pop('accept-ra', None)
402 if subnets:403 if subnets:
403 for index, subnet in enumerate(subnets):404 for index, subnet in enumerate(subnets):
404 ipv4_subnet_mtu = None405 ipv4_subnet_mtu = None
@@ -415,9 +416,18 @@ class Renderer(renderer.Renderer):
415 subnet['type'] == 'ipv6_dhcpv6-stateful'):416 subnet['type'] == 'ipv6_dhcpv6-stateful'):
416 # Configure network settings using DHCP or DHCPv6417 # Configure network settings using DHCP or DHCPv6
417 iface['mode'] = 'dhcp'418 iface['mode'] = 'dhcp'
419 if accept_ra is not None:
420 # Accept router advertisements (0=off, 1=on)
421 iface['accept_ra'] = ('1' if util.is_true(accept_ra)
422 else '0')
418 elif subnet['type'] == 'ipv6_dhcpv6-stateless':423 elif subnet['type'] == 'ipv6_dhcpv6-stateless':
419 # Configure network settings using SLAAC from RAs424 # Configure network settings using SLAAC from RAs
420 iface['mode'] = 'auto'425 iface['mode'] = 'auto'
426 elif subnet_is_ipv6(subnet) and subnet['type'] == 'static':
427 if accept_ra is not None:
428 # Accept router advertisements (0=off, 1=on)
429 iface['accept_ra'] = ('1' if util.is_true(accept_ra)
430 else '0')
421431
422 # do not emit multiple 'auto $IFACE' lines as older (precise)432 # do not emit multiple 'auto $IFACE' lines as older (precise)
423 # ifupdown complains433 # ifupdown complains
diff --git a/cloudinit/net/netplan.py b/cloudinit/net/netplan.py
index e54a34e..78023f8 100644
--- a/cloudinit/net/netplan.py
+++ b/cloudinit/net/netplan.py
@@ -51,7 +51,8 @@ def _extract_addresses(config, entry, ifname):
51 'mtu': 1480,51 'mtu': 1480,
52 'netmask': 64,52 'netmask': 64,
53 'type': 'static'}],53 'type': 'static'}],
54 'type: physical'54 'type: physical',
55 'accept-ra': 'true'
55 }56 }
5657
57 An entry dictionary looks like:58 An entry dictionary looks like:
@@ -144,6 +145,8 @@ def _extract_addresses(config, entry, ifname):
144 ns = entry.get('nameservers', {})145 ns = entry.get('nameservers', {})
145 ns.update({'search': searchdomains})146 ns.update({'search': searchdomains})
146 entry.update({'nameservers': ns})147 entry.update({'nameservers': ns})
148 if 'accept-ra' in config:
149 entry.update({'accept-ra': config.get('accept-ra')})
147150
148151
149def _extract_bond_slaves_by_name(interfaces, entry, bond_master):152def _extract_bond_slaves_by_name(interfaces, entry, bond_master):
diff --git a/cloudinit/net/network_state.py b/cloudinit/net/network_state.py
index c0c415d..cc8beaa 100644
--- a/cloudinit/net/network_state.py
+++ b/cloudinit/net/network_state.py
@@ -22,7 +22,8 @@ NETWORK_STATE_REQUIRED_KEYS = {
22}22}
23NETWORK_V2_KEY_FILTER = [23NETWORK_V2_KEY_FILTER = [
24 'addresses', 'dhcp4', 'dhcp6', 'gateway4', 'gateway6', 'interfaces',24 'addresses', 'dhcp4', 'dhcp6', 'gateway4', 'gateway6', 'interfaces',
25 'match', 'mtu', 'nameservers', 'renderer', 'set-name', 'wakeonlan'25 'match', 'mtu', 'nameservers', 'renderer', 'set-name', 'wakeonlan',
26 'accept-ra'
26]27]
2728
28NET_CONFIG_TO_V2 = {29NET_CONFIG_TO_V2 = {
@@ -340,7 +341,8 @@ class NetworkStateInterpreter(object):
340 'name': 'eth0',341 'name': 'eth0',
341 'subnets': [342 'subnets': [
342 {'type': 'dhcp4'}343 {'type': 'dhcp4'}
343 ]344 ],
345 'accept-ra': 'true'
344 }346 }
345 '''347 '''
346348
@@ -370,6 +372,7 @@ class NetworkStateInterpreter(object):
370 'address': None,372 'address': None,
371 'gateway': None,373 'gateway': None,
372 'subnets': subnets,374 'subnets': subnets,
375 'accept-ra': command.get('accept-ra')
373 })376 })
374 self._network_state['interfaces'].update({command.get('name'): iface})377 self._network_state['interfaces'].update({command.get('name'): iface})
375 self.dump_network_state()378 self.dump_network_state()
@@ -613,6 +616,7 @@ class NetworkStateInterpreter(object):
613 driver: ixgbe616 driver: ixgbe
614 set-name: lom1617 set-name: lom1
615 dhcp6: true618 dhcp6: true
619 accept-ra: true
616 switchports:620 switchports:
617 match:621 match:
618 name: enp2*622 name: enp2*
@@ -641,7 +645,7 @@ class NetworkStateInterpreter(object):
641 driver = match.get('driver', None)645 driver = match.get('driver', None)
642 if driver:646 if driver:
643 phy_cmd['params'] = {'driver': driver}647 phy_cmd['params'] = {'driver': driver}
644 for key in ['mtu', 'match', 'wakeonlan']:648 for key in ['mtu', 'match', 'wakeonlan', 'accept-ra']:
645 if key in cfg:649 if key in cfg:
646 phy_cmd[key] = cfg[key]650 phy_cmd[key] = cfg[key]
647651
@@ -919,7 +923,7 @@ def is_ipv6_addr(address):
919def subnet_is_ipv6(subnet):923def subnet_is_ipv6(subnet):
920 """Common helper for checking network_state subnets for ipv6."""924 """Common helper for checking network_state subnets for ipv6."""
921 # 'static6' or 'dhcp6'925 # 'static6' or 'dhcp6'
922 if subnet['type'].endswith('6'):926 if subnet['type'].endswith('6') or subnet['type'].startswith('ipv6'):
923 # This is a request for DHCPv6.927 # This is a request for DHCPv6.
924 return True928 return True
925 elif subnet['type'] == 'static' and is_ipv6_addr(subnet.get('address')):929 elif subnet['type'] == 'static' and is_ipv6_addr(subnet.get('address')):
diff --git a/cloudinit/net/sysconfig.py b/cloudinit/net/sysconfig.py
index 4e65676..3fe5793 100644
--- a/cloudinit/net/sysconfig.py
+++ b/cloudinit/net/sysconfig.py
@@ -333,6 +333,9 @@ class Renderer(renderer.Renderer):
333 if old_key == 'mac_address' and iface['type'] != 'physical':333 if old_key == 'mac_address' and iface['type'] != 'physical':
334 continue334 continue
335 iface_cfg[new_key] = old_value335 iface_cfg[new_key] = old_value
336 accept_ra = iface.get('accept-ra', None)
337 if accept_ra is not None:
338 iface_cfg['IPV6_FORCE_ACCEPT_RA'] = util.is_true(accept_ra)
336339
337 @classmethod340 @classmethod
338 def _render_subnets(cls, iface_cfg, subnets, has_default_route):341 def _render_subnets(cls, iface_cfg, subnets, has_default_route):
@@ -343,11 +346,16 @@ class Renderer(renderer.Renderer):
343 for i, subnet in enumerate(subnets, start=len(iface_cfg.children)):346 for i, subnet in enumerate(subnets, start=len(iface_cfg.children)):
344 mtu_key = 'MTU'347 mtu_key = 'MTU'
345 subnet_type = subnet.get('type')348 subnet_type = subnet.get('type')
346 if subnet_type == 'dhcp6' or subnet_type == 'ipv6_dhcpv6-stateful':349 if subnet_type == 'dhcp6':
347 # TODO need to set BOOTPROTO to dhcp6 on SUSE350 # TODO need to set BOOTPROTO to dhcp6 on SUSE
348 iface_cfg['IPV6INIT'] = True351 iface_cfg['IPV6INIT'] = True
349 # Configure network settings using DHCPv6352 # Configure network settings using DHCPv6
350 iface_cfg['DHCPV6C'] = True353 iface_cfg['DHCPV6C'] = True
354 elif subnet_type == 'ipv6_dhcpv6-stateful':
355 iface_cfg['IPV6INIT'] = True
356 # Configure network settings using DHCPv6
357 iface_cfg['DHCPV6C'] = True
358 iface_cfg['IPV6_FORCE_ACCEPT_RA'] = True
351 elif subnet_type == 'ipv6_dhcpv6-stateless':359 elif subnet_type == 'ipv6_dhcpv6-stateless':
352 iface_cfg['IPV6INIT'] = True360 iface_cfg['IPV6INIT'] = True
353 # Configure network settings using SLAAC from RAs361 # Configure network settings using SLAAC from RAs
diff --git a/tests/unittests/test_net.py b/tests/unittests/test_net.py
index f5a9cae..62b6ac6 100644
--- a/tests/unittests/test_net.py
+++ b/tests/unittests/test_net.py
@@ -1070,6 +1070,104 @@ NETWORK_CONFIGS = {
1070 """),1070 """),
1071 },1071 },
1072 },1072 },
1073 'dhcpv6_accept_ra': {
1074 'expected_eni': textwrap.dedent("""\
1075 auto lo
1076 iface lo inet loopback
1077
1078 auto iface0
1079 iface iface0 inet6 dhcp
1080 accept_ra 1
1081 """).rstrip(' '),
1082 'expected_netplan': textwrap.dedent("""
1083 network:
1084 version: 2
1085 ethernets:
1086 iface0:
1087 accept-ra: 'true'
1088 dhcp6: true
1089 """).rstrip(' '),
1090 'yaml_v1': textwrap.dedent("""\
1091 version: 1
1092 config:
1093 - type: 'physical'
1094 name: 'iface0'
1095 subnets:
1096 - {'type': 'dhcp6'}
1097 accept-ra: 'true'
1098 """).rstrip(' '),
1099 'yaml_v2': textwrap.dedent("""\
1100 version: 2
1101 ethernets:
1102 iface0:
1103 dhcp6: true
1104 accept-ra: 'true'
1105 """).rstrip(' '),
1106 'expected_sysconfig': {
1107 'ifcfg-iface0': textwrap.dedent("""\
1108 BOOTPROTO=none
1109 DEVICE=iface0
1110 DHCPV6C=yes
1111 IPV6INIT=yes
1112 IPV6_FORCE_ACCEPT_RA=yes
1113 DEVICE=iface0
1114 NM_CONTROLLED=no
1115 ONBOOT=yes
1116 STARTMODE=auto
1117 TYPE=Ethernet
1118 USERCTL=no
1119 """),
1120 },
1121 },
1122 'dhcpv6_reject_ra': {
1123 'expected_eni': textwrap.dedent("""\
1124 auto lo
1125 iface lo inet loopback
1126
1127 auto iface0
1128 iface iface0 inet6 dhcp
1129 accept_ra 0
1130 """).rstrip(' '),
1131 'expected_netplan': textwrap.dedent("""
1132 network:
1133 version: 2
1134 ethernets:
1135 iface0:
1136 accept-ra: 'false'
1137 dhcp6: true
1138 """).rstrip(' '),
1139 'yaml_v1': textwrap.dedent("""\
1140 version: 1
1141 config:
1142 - type: 'physical'
1143 name: 'iface0'
1144 subnets:
1145 - {'type': 'dhcp6'}
1146 accept-ra: 'false'
1147 """).rstrip(' '),
1148 'yaml_v2': textwrap.dedent("""\
1149 version: 2
1150 ethernets:
1151 iface0:
1152 dhcp6: true
1153 accept-ra: 'false'
1154 """).rstrip(' '),
1155 'expected_sysconfig': {
1156 'ifcfg-iface0': textwrap.dedent("""\
1157 BOOTPROTO=none
1158 DEVICE=iface0
1159 DHCPV6C=yes
1160 IPV6INIT=yes
1161 IPV6_FORCE_ACCEPT_RA=no
1162 DEVICE=iface0
1163 NM_CONTROLLED=no
1164 ONBOOT=yes
1165 STARTMODE=auto
1166 TYPE=Ethernet
1167 USERCTL=no
1168 """),
1169 },
1170 },
1073 'dhcpv6_stateless': {1171 'dhcpv6_stateless': {
1074 'expected_eni': textwrap.dedent("""\1172 'expected_eni': textwrap.dedent("""\
1075 auto lo1173 auto lo
@@ -1137,6 +1235,7 @@ NETWORK_CONFIGS = {
1137 DEVICE=iface01235 DEVICE=iface0
1138 DHCPV6C=yes1236 DHCPV6C=yes
1139 IPV6INIT=yes1237 IPV6INIT=yes
1238 IPV6_FORCE_ACCEPT_RA=yes
1140 DEVICE=iface01239 DEVICE=iface0
1141 NM_CONTROLLED=no1240 NM_CONTROLLED=no
1142 ONBOOT=yes1241 ONBOOT=yes
@@ -2857,6 +2956,34 @@ USERCTL=no
2857 self._compare_files_to_expected(entry[self.expected_name], found)2956 self._compare_files_to_expected(entry[self.expected_name], found)
2858 self._assert_headers(found)2957 self._assert_headers(found)
28592958
2959 def test_dhcpv6_accept_ra_config_v1(self):
2960 entry = NETWORK_CONFIGS['dhcpv6_accept_ra']
2961 found = self._render_and_read(network_config=yaml.load(
2962 entry['yaml_v1']))
2963 self._compare_files_to_expected(entry[self.expected_name], found)
2964 self._assert_headers(found)
2965
2966 def test_dhcpv6_accept_ra_config_v2(self):
2967 entry = NETWORK_CONFIGS['dhcpv6_accept_ra']
2968 found = self._render_and_read(network_config=yaml.load(
2969 entry['yaml_v2']))
2970 self._compare_files_to_expected(entry[self.expected_name], found)
2971 self._assert_headers(found)
2972
2973 def test_dhcpv6_reject_ra_config_v1(self):
2974 entry = NETWORK_CONFIGS['dhcpv6_reject_ra']
2975 found = self._render_and_read(network_config=yaml.load(
2976 entry['yaml_v1']))
2977 self._compare_files_to_expected(entry[self.expected_name], found)
2978 self._assert_headers(found)
2979
2980 def test_dhcpv6_reject_ra_config_v2(self):
2981 entry = NETWORK_CONFIGS['dhcpv6_reject_ra']
2982 found = self._render_and_read(network_config=yaml.load(
2983 entry['yaml_v2']))
2984 self._compare_files_to_expected(entry[self.expected_name], found)
2985 self._assert_headers(found)
2986
2860 def test_dhcpv6_stateless_config(self):2987 def test_dhcpv6_stateless_config(self):
2861 entry = NETWORK_CONFIGS['dhcpv6_stateless']2988 entry = NETWORK_CONFIGS['dhcpv6_stateless']
2862 found = self._render_and_read(network_config=yaml.load(entry['yaml']))2989 found = self._render_and_read(network_config=yaml.load(entry['yaml']))
@@ -3918,6 +4045,22 @@ class TestNetplanRoundTrip(CiTestCase):
3918 entry['expected_netplan'].splitlines(),4045 entry['expected_netplan'].splitlines(),
3919 files['/etc/netplan/50-cloud-init.yaml'].splitlines())4046 files['/etc/netplan/50-cloud-init.yaml'].splitlines())
39204047
4048 def testsimple_render_dhcpv6_accept_ra(self):
4049 entry = NETWORK_CONFIGS['dhcpv6_accept_ra']
4050 files = self._render_and_read(network_config=yaml.load(
4051 entry['yaml_v1']))
4052 self.assertEqual(
4053 entry['expected_netplan'].splitlines(),
4054 files['/etc/netplan/50-cloud-init.yaml'].splitlines())
4055
4056 def testsimple_render_dhcpv6_reject_ra(self):
4057 entry = NETWORK_CONFIGS['dhcpv6_reject_ra']
4058 files = self._render_and_read(network_config=yaml.load(
4059 entry['yaml_v1']))
4060 self.assertEqual(
4061 entry['expected_netplan'].splitlines(),
4062 files['/etc/netplan/50-cloud-init.yaml'].splitlines())
4063
3921 def testsimple_render_all(self):4064 def testsimple_render_all(self):
3922 entry = NETWORK_CONFIGS['all']4065 entry = NETWORK_CONFIGS['all']
3923 files = self._render_and_read(network_config=yaml.load(entry['yaml']))4066 files = self._render_and_read(network_config=yaml.load(entry['yaml']))
@@ -4048,6 +4191,38 @@ class TestEniRoundTrip(CiTestCase):
4048 entry['expected_eni'].splitlines(),4191 entry['expected_eni'].splitlines(),
4049 files['/etc/network/interfaces'].splitlines())4192 files['/etc/network/interfaces'].splitlines())
40504193
4194 def testsimple_render_dhcpv6_stateless(self):
4195 entry = NETWORK_CONFIGS['dhcpv6_stateless']
4196 files = self._render_and_read(network_config=yaml.load(
4197 entry['yaml']))
4198 self.assertEqual(
4199 entry['expected_eni'].splitlines(),
4200 files['/etc/network/interfaces'].splitlines())
4201
4202 def testsimple_render_dhcpv6_stateful(self):
4203 entry = NETWORK_CONFIGS['dhcpv6_stateless']
4204 files = self._render_and_read(network_config=yaml.load(
4205 entry['yaml']))
4206 self.assertEqual(
4207 entry['expected_eni'].splitlines(),
4208 files['/etc/network/interfaces'].splitlines())
4209
4210 def testsimple_render_dhcpv6_accept_ra(self):
4211 entry = NETWORK_CONFIGS['dhcpv6_accept_ra']
4212 files = self._render_and_read(network_config=yaml.load(
4213 entry['yaml_v1']))
4214 self.assertEqual(
4215 entry['expected_eni'].splitlines(),
4216 files['/etc/network/interfaces'].splitlines())
4217
4218 def testsimple_render_dhcpv6_reject_ra(self):
4219 entry = NETWORK_CONFIGS['dhcpv6_reject_ra']
4220 files = self._render_and_read(network_config=yaml.load(
4221 entry['yaml_v1']))
4222 self.assertEqual(
4223 entry['expected_eni'].splitlines(),
4224 files['/etc/network/interfaces'].splitlines())
4225
4051 def testsimple_render_manual(self):4226 def testsimple_render_manual(self):
4052 """Test rendering of 'manual' for 'type' and 'control'.4227 """Test rendering of 'manual' for 'type' and 'control'.
40534228

Subscribers

People subscribed via source and target branches