Merge ~akaris/cloud-init:bug1679817 into cloud-init:master

Proposed by Andreas Karis
Status: Superseded
Proposed branch: ~akaris/cloud-init:bug1679817
Merge into: cloud-init:master
Diff against target: 452 lines (+193/-133)
3 files modified
cloudinit/net/sysconfig.py (+168/-71)
tests/unittests/test_distros/test_netconfig.py (+3/-5)
tests/unittests/test_net.py (+22/-57)
Reviewer Review Type Date Requested Status
Server Team CI bot continuous-integration Approve
cloud-init Commiters Pending
Review via email: mp+323082@code.launchpad.net

This proposal supersedes a proposal from 2017-04-24.

Commit message

Fix dual stack IPv4/IPv6 configuration for RHEL

Dual stack IPv4/IPv6 configuration via config drive is broken for RHEL7.
This patch fixes several scenarios for IPv4/IPv6/dual stack with multiple IP assignment
Removes unpopular IPv4 alias files and invalid IPv6 alias files

Also fixes associated unit tests

LP: #1679817
LP: #1685534
LP: #1685532

Description of the change

Fix dual stack IPv4/IPv6 configuration for RHEL

Dual stack IPv4/IPv6 configuration via config drive is broken for RHEL7.
This patch fixes several scenarios for IPv4/IPv6/dual stack with multiple IP assignment
Removes unpopular IPv4 alias files and invalid IPv6 alias files

Also fixes associated unit tests

LP: #1679817
LP: #1685534
LP: #1685532

To post a comment you must log in.
Revision history for this message
Server Team CI bot (server-team-bot) wrote : Posted in a previous version of this proposal
review: Needs Fixing (continuous-integration)
Revision history for this message
Server Team CI bot (server-team-bot) wrote : Posted in a previous version of this proposal
review: Needs Fixing (continuous-integration)
Revision history for this message
Server Team CI bot (server-team-bot) wrote : Posted in a previous version of this proposal
review: Needs Fixing (continuous-integration)
Revision history for this message
Server Team CI bot (server-team-bot) wrote : Posted in a previous version of this proposal
review: Needs Fixing (continuous-integration)
Revision history for this message
Server Team CI bot (server-team-bot) wrote :
review: Needs Fixing (continuous-integration)
~akaris/cloud-init:bug1679817 updated
5a0bc13... by Andreas Karis

fix for flake

Revision history for this message
Server Team CI bot (server-team-bot) wrote :
review: Approve (continuous-integration)
~akaris/cloud-init:bug1679817 updated
025f52b... by Andreas Karis

consolidated to_string_ipv4 and to_string_ipv6

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

Unmerged commits

025f52b... by Andreas Karis

consolidated to_string_ipv4 and to_string_ipv6

5a0bc13... by Andreas Karis

fix for flake

46c3541... by Andreas Karis

cleanup for flake

23c7007... by Andreas Karis

cleanup for flake

a94ded7... by Andreas Karis

Moved IPv6 routes to route6-

7569e43... by Andreas Karis

added route handling for ipv4 and ipv6 routes

2d818c2... by Andreas Karis

wrapped str around netmask in cases it is integer

9ad5280... by Andreas Karis

Remove BOOTPROTO=static and make it BOOTPROTO=none as it should be

e43facb... by Andreas Karis

bug1679817, bug1685534, bug1685532

ca539b9... by Andreas Karis

fix1

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1diff --git a/cloudinit/net/sysconfig.py b/cloudinit/net/sysconfig.py
2index 504e4d0..49da3a3 100644
3--- a/cloudinit/net/sysconfig.py
4+++ b/cloudinit/net/sysconfig.py
5@@ -59,6 +59,9 @@ class ConfigMap(object):
6 def __setitem__(self, key, value):
7 self._conf[key] = value
8
9+ def __getitem__(self, key):
10+ return self._conf[key]
11+
12 def drop(self, key):
13 self._conf.pop(key, None)
14
15@@ -83,7 +86,8 @@ class ConfigMap(object):
16 class Route(ConfigMap):
17 """Represents a route configuration."""
18
19- route_fn_tpl = '%(base)s/network-scripts/route-%(name)s'
20+ route_fn_tpl_ipv4 = '%(base)s/network-scripts/route-%(name)s'
21+ route_fn_tpl_ipv6 = '%(base)s/network-scripts/route6-%(name)s'
22
23 def __init__(self, route_name, base_sysconf_dir):
24 super(Route, self).__init__()
25@@ -102,9 +106,53 @@ class Route(ConfigMap):
26 return r
27
28 @property
29- def path(self):
30- return self.route_fn_tpl % ({'base': self._base_sysconf_dir,
31- 'name': self._route_name})
32+ def path_ipv4(self):
33+ return self.route_fn_tpl_ipv4 % ({'base': self._base_sysconf_dir,
34+ 'name': self._route_name})
35+
36+ @property
37+ def path_ipv6(self):
38+ return self.route_fn_tpl_ipv6 % ({'base': self._base_sysconf_dir,
39+ 'name': self._route_name})
40+
41+ def is_ipv6_route(self, address):
42+ return ':' in address
43+
44+ def to_string(self, proto="ipv4"):
45+ buf = six.StringIO()
46+ buf.write(_make_header())
47+ if self._conf:
48+ buf.write("\n")
49+ # need to reindex IPv4 addresses
50+ # (because Route can contain a mix of IPv4 and IPv6)
51+ reindex = -1
52+ for key in sorted(self._conf.keys()):
53+ if 'ADDRESS' in key:
54+ index = key.replace('ADDRESS', '')
55+ address_value = str(self._conf[key])
56+ # only accept combinations:
57+ # ipv6 route and proto ipv6
58+ # ipv4 route and proto ipv4
59+ # do not add any other routes
60+ if proto == "ipv4" and not self.is_ipv6_route(address_value):
61+ netmask_value = str(self._conf['NETMASK' + index])
62+ gateway_value = str(self._conf['GATEWAY' + index])
63+ # increase IPv4 index
64+ reindex = reindex + 1
65+ buf.write("%s=%s\n" % ('ADDRESS' + str(reindex),
66+ _quote_value(address_value)))
67+ buf.write("%s=%s\n" % ('GATEWAY' + str(reindex),
68+ _quote_value(gateway_value)))
69+ buf.write("%s=%s\n" % ('NETMASK' + str(reindex),
70+ _quote_value(netmask_value)))
71+ elif proto == "ipv6" and self.is_ipv6_route(address_value):
72+ netmask_value = str(self._conf['NETMASK' + index])
73+ gateway_value = str(self._conf['GATEWAY' + index])
74+ buf.write("%s/%s via %s\n" % (address_value,
75+ netmask_value,
76+ gateway_value))
77+
78+ return buf.getvalue()
79
80
81 class NetInterface(ConfigMap):
82@@ -211,65 +259,117 @@ class Renderer(renderer.Renderer):
83 iface_cfg[new_key] = old_value
84
85 @classmethod
86- def _render_subnet(cls, iface_cfg, route_cfg, subnet):
87- subnet_type = subnet.get('type')
88- if subnet_type == 'dhcp6':
89- iface_cfg['DHCPV6C'] = True
90- iface_cfg['IPV6INIT'] = True
91- iface_cfg['BOOTPROTO'] = 'dhcp'
92- elif subnet_type in ['dhcp4', 'dhcp']:
93- iface_cfg['BOOTPROTO'] = 'dhcp'
94- elif subnet_type == 'static':
95- iface_cfg['BOOTPROTO'] = 'static'
96- if subnet_is_ipv6(subnet):
97- iface_cfg['IPV6ADDR'] = subnet['address']
98+ def _render_subnets(cls, iface_cfg, subnets):
99+ # setting base values
100+ iface_cfg['BOOTPROTO'] = 'none'
101+
102+ # modifying base values according to subnets
103+ for i, subnet in enumerate(subnets, start=len(iface_cfg.children)):
104+ subnet_type = subnet.get('type')
105+ if subnet_type == 'dhcp6':
106 iface_cfg['IPV6INIT'] = True
107+ iface_cfg['DHCPV6C'] = True
108+ iface_cfg['BOOTPROTO'] = 'dhcp'
109+ elif subnet_type in ['dhcp4', 'dhcp']:
110+ iface_cfg['BOOTPROTO'] = 'dhcp'
111+ elif subnet_type == 'static':
112+ # grep BOOTPROTO sysconfig.txt -A2 | head -3
113+ # BOOTPROTO=none|bootp|dhcp
114+ # 'bootp' or 'dhcp' cause a DHCP client
115+ # to run on the device. Any other
116+ # value causes any static configuration
117+ # in the file to be applied.
118+ # ==> the following should not be set to 'static'
119+ # but should remain 'none'
120+ # if iface_cfg['BOOTPROTO'] == 'none':
121+ # iface_cfg['BOOTPROTO'] = 'static'
122+ if subnet_is_ipv6(subnet):
123+ iface_cfg['IPV6INIT'] = True
124 else:
125- iface_cfg['IPADDR'] = subnet['address']
126- else:
127- raise ValueError("Unknown subnet type '%s' found"
128- " for interface '%s'" % (subnet_type,
129- iface_cfg.name))
130- if 'netmask' in subnet:
131- iface_cfg['NETMASK'] = subnet['netmask']
132- for route in subnet.get('routes', []):
133- if subnet.get('ipv6'):
134- gw_cfg = 'IPV6_DEFAULTGW'
135- else:
136- gw_cfg = 'GATEWAY'
137-
138- if _is_default_route(route):
139- if (
140- (subnet.get('ipv4') and
141- route_cfg.has_set_default_ipv4) or
142- (subnet.get('ipv6') and
143- route_cfg.has_set_default_ipv6)
144- ):
145- raise ValueError("Duplicate declaration of default "
146- "route found for interface '%s'"
147- % (iface_cfg.name))
148- # NOTE(harlowja): ipv6 and ipv4 default gateways
149- gw_key = 'GATEWAY0'
150- nm_key = 'NETMASK0'
151- addr_key = 'ADDRESS0'
152- # The owning interface provides the default route.
153- #
154- # TODO(harlowja): add validation that no other iface has
155- # also provided the default route?
156- iface_cfg['DEFROUTE'] = True
157- if 'gateway' in route:
158- iface_cfg[gw_cfg] = route['gateway']
159- route_cfg.has_set_default = True
160- else:
161- gw_key = 'GATEWAY%s' % route_cfg.last_idx
162- nm_key = 'NETMASK%s' % route_cfg.last_idx
163- addr_key = 'ADDRESS%s' % route_cfg.last_idx
164- route_cfg.last_idx += 1
165- for (old_key, new_key) in [('gateway', gw_key),
166- ('netmask', nm_key),
167- ('network', addr_key)]:
168- if old_key in route:
169- route_cfg[new_key] = route[old_key]
170+ raise ValueError("Unknown subnet type '%s' found"
171+ " for interface '%s'" % (subnet_type,
172+ iface_cfg.name))
173+
174+ # set IPv4 and IPv6 static addresses
175+ ipv4_index = -1
176+ ipv6_index = -1
177+ for i, subnet in enumerate(subnets, start=len(iface_cfg.children)):
178+ subnet_type = subnet.get('type')
179+ if subnet_type == 'dhcp6':
180+ continue
181+ elif subnet_type in ['dhcp4', 'dhcp']:
182+ continue
183+ elif subnet_type == 'static':
184+ if subnet_is_ipv6(subnet):
185+ ipv6_index = ipv6_index + 1
186+ if 'netmask' in subnet and str(subnet['netmask']) != "":
187+ ipv6_cidr = (subnet['address'] +
188+ '/' +
189+ str(subnet['netmask']))
190+ else:
191+ ipv6_cidr = subnet['address']
192+ if ipv6_index == 0:
193+ iface_cfg['IPV6ADDR'] = ipv6_cidr
194+ elif ipv6_index == 1:
195+ iface_cfg['IPV6ADDR_SECONDARIES'] = ipv6_cidr
196+ else:
197+ iface_cfg['IPV6ADDR_SECONDARIES'] = (
198+ iface_cfg['IPV6ADDR_SECONDARIES'] +
199+ " " + ipv6_cidr)
200+ else:
201+ ipv4_index = ipv4_index + 1
202+ if ipv4_index == 0:
203+ iface_cfg['IPADDR'] = subnet['address']
204+ if 'netmask' in subnet:
205+ iface_cfg['NETMASK'] = subnet['netmask']
206+ else:
207+ iface_cfg['IPADDR' + str(ipv4_index)] = \
208+ subnet['address']
209+ if 'netmask' in subnet:
210+ iface_cfg['NETMASK' + str(ipv4_index)] = \
211+ subnet['netmask']
212+
213+ @classmethod
214+ def _render_subnet_routes(cls, iface_cfg, route_cfg, subnets):
215+ for i, subnet in enumerate(subnets, start=len(iface_cfg.children)):
216+ for route in subnet.get('routes', []):
217+ if subnet.get('ipv6'):
218+ gw_cfg = 'IPV6_DEFAULTGW'
219+ else:
220+ gw_cfg = 'GATEWAY'
221+
222+ if _is_default_route(route):
223+ if (
224+ (subnet.get('ipv4') and
225+ route_cfg.has_set_default_ipv4) or
226+ (subnet.get('ipv6') and
227+ route_cfg.has_set_default_ipv6)
228+ ):
229+ raise ValueError("Duplicate declaration of default "
230+ "route found for interface '%s'"
231+ % (iface_cfg.name))
232+ # NOTE(harlowja): ipv6 and ipv4 default gateways
233+ gw_key = 'GATEWAY0'
234+ nm_key = 'NETMASK0'
235+ addr_key = 'ADDRESS0'
236+ # The owning interface provides the default route.
237+ #
238+ # TODO(harlowja): add validation that no other iface has
239+ # also provided the default route?
240+ iface_cfg['DEFROUTE'] = True
241+ if 'gateway' in route:
242+ iface_cfg[gw_cfg] = route['gateway']
243+ route_cfg.has_set_default = True
244+ else:
245+ gw_key = 'GATEWAY%s' % route_cfg.last_idx
246+ nm_key = 'NETMASK%s' % route_cfg.last_idx
247+ addr_key = 'ADDRESS%s' % route_cfg.last_idx
248+ route_cfg.last_idx += 1
249+ for (old_key, new_key) in [('gateway', gw_key),
250+ ('netmask', nm_key),
251+ ('network', addr_key)]:
252+ if old_key in route:
253+ route_cfg[new_key] = route[old_key]
254
255 @classmethod
256 def _render_bonding_opts(cls, iface_cfg, iface):
257@@ -295,15 +395,9 @@ class Renderer(renderer.Renderer):
258 iface_subnets = iface.get("subnets", [])
259 iface_cfg = iface_contents[iface_name]
260 route_cfg = iface_cfg.routes
261- if len(iface_subnets) == 1:
262- cls._render_subnet(iface_cfg, route_cfg, iface_subnets[0])
263- elif len(iface_subnets) > 1:
264- for i, isubnet in enumerate(iface_subnets,
265- start=len(iface_cfg.children)):
266- iface_sub_cfg = iface_cfg.copy()
267- iface_sub_cfg.name = "%s:%s" % (iface_name, i)
268- iface_cfg.children.append(iface_sub_cfg)
269- cls._render_subnet(iface_sub_cfg, route_cfg, isubnet)
270+
271+ cls._render_subnets(iface_cfg, iface_subnets)
272+ cls._render_subnet_routes(iface_cfg, route_cfg, iface_subnets)
273
274 @classmethod
275 def _render_bond_interfaces(cls, network_state, iface_contents):
276@@ -387,7 +481,10 @@ class Renderer(renderer.Renderer):
277 if iface_cfg:
278 contents[iface_cfg.path] = iface_cfg.to_string()
279 if iface_cfg.routes:
280- contents[iface_cfg.routes.path] = iface_cfg.routes.to_string()
281+ contents[iface_cfg.routes.path_ipv4] = \
282+ iface_cfg.routes.to_string("ipv4")
283+ contents[iface_cfg.routes.path_ipv6] = \
284+ iface_cfg.routes.to_string("ipv6")
285 return contents
286
287 def render_network_state(self, network_state, target=None):
288diff --git a/tests/unittests/test_distros/test_netconfig.py b/tests/unittests/test_distros/test_netconfig.py
289index 8837066..6d6b985 100644
290--- a/tests/unittests/test_distros/test_netconfig.py
291+++ b/tests/unittests/test_distros/test_netconfig.py
292@@ -431,7 +431,7 @@ NETWORKING=yes
293 expected_buf = '''
294 # Created by cloud-init on instance boot automatically, do not edit.
295 #
296-BOOTPROTO=static
297+BOOTPROTO=none
298 DEVICE=eth0
299 IPADDR=192.168.1.5
300 NETMASK=255.255.255.0
301@@ -488,7 +488,6 @@ NETWORKING=yes
302 mock.patch.object(util, 'load_file', return_value=''))
303 mocks.enter_context(
304 mock.patch.object(os.path, 'isfile', return_value=False))
305-
306 rh_distro.apply_network(BASE_NET_CFG_IPV6, False)
307
308 self.assertEqual(len(write_bufs), 4)
309@@ -581,11 +580,10 @@ IPV6_AUTOCONF=no
310 expected_buf = '''
311 # Created by cloud-init on instance boot automatically, do not edit.
312 #
313-BOOTPROTO=static
314+BOOTPROTO=none
315 DEVICE=eth0
316-IPV6ADDR=2607:f0d0:1002:0011::2
317+IPV6ADDR=2607:f0d0:1002:0011::2/64
318 IPV6INIT=yes
319-NETMASK=64
320 NM_CONTROLLED=no
321 ONBOOT=yes
322 TYPE=Ethernet
323diff --git a/tests/unittests/test_net.py b/tests/unittests/test_net.py
324index 89e7536..1157c95 100644
325--- a/tests/unittests/test_net.py
326+++ b/tests/unittests/test_net.py
327@@ -136,7 +136,7 @@ OS_SAMPLES = [
328 """
329 # Created by cloud-init on instance boot automatically, do not edit.
330 #
331-BOOTPROTO=static
332+BOOTPROTO=none
333 DEFROUTE=yes
334 DEVICE=eth0
335 GATEWAY=172.19.3.254
336@@ -204,38 +204,14 @@ nameserver 172.19.0.12
337 # Created by cloud-init on instance boot automatically, do not edit.
338 #
339 BOOTPROTO=none
340-DEVICE=eth0
341-HWADDR=fa:16:3e:ed:9a:59
342-NM_CONTROLLED=no
343-ONBOOT=yes
344-TYPE=Ethernet
345-USERCTL=no
346-""".lstrip()),
347- ('etc/sysconfig/network-scripts/ifcfg-eth0:0',
348- """
349-# Created by cloud-init on instance boot automatically, do not edit.
350-#
351-BOOTPROTO=static
352 DEFROUTE=yes
353-DEVICE=eth0:0
354+DEVICE=eth0
355 GATEWAY=172.19.3.254
356 HWADDR=fa:16:3e:ed:9a:59
357 IPADDR=172.19.1.34
358+IPADDR1=10.0.0.10
359 NETMASK=255.255.252.0
360-NM_CONTROLLED=no
361-ONBOOT=yes
362-TYPE=Ethernet
363-USERCTL=no
364-""".lstrip()),
365- ('etc/sysconfig/network-scripts/ifcfg-eth0:1',
366- """
367-# Created by cloud-init on instance boot automatically, do not edit.
368-#
369-BOOTPROTO=static
370-DEVICE=eth0:1
371-HWADDR=fa:16:3e:ed:9a:59
372-IPADDR=10.0.0.10
373-NETMASK=255.255.255.0
374+NETMASK1=255.255.255.0
375 NM_CONTROLLED=no
376 ONBOOT=yes
377 TYPE=Ethernet
378@@ -265,7 +241,7 @@ nameserver 172.19.0.12
379 }],
380 "ip_address": "172.19.1.34", "id": "network0"
381 }, {
382- "network_id": "public-ipv6",
383+ "network_id": "public-ipv6-a",
384 "type": "ipv6", "netmask": "",
385 "link": "tap1a81968a-79",
386 "routes": [
387@@ -276,6 +252,20 @@ nameserver 172.19.0.12
388 }
389 ],
390 "ip_address": "2001:DB8::10", "id": "network1"
391+ }, {
392+ "network_id": "public-ipv6-b",
393+ "type": "ipv6", "netmask": "64",
394+ "link": "tap1a81968a-79",
395+ "routes": [
396+ ],
397+ "ip_address": "2001:DB9::10", "id": "network2"
398+ }, {
399+ "network_id": "public-ipv6-c",
400+ "type": "ipv6", "netmask": "64",
401+ "link": "tap1a81968a-79",
402+ "routes": [
403+ ],
404+ "ip_address": "2001:DB10::10", "id": "network3"
405 }],
406 "links": [
407 {
408@@ -295,41 +285,16 @@ nameserver 172.19.0.12
409 # Created by cloud-init on instance boot automatically, do not edit.
410 #
411 BOOTPROTO=none
412-DEVICE=eth0
413-HWADDR=fa:16:3e:ed:9a:59
414-NM_CONTROLLED=no
415-ONBOOT=yes
416-TYPE=Ethernet
417-USERCTL=no
418-""".lstrip()),
419- ('etc/sysconfig/network-scripts/ifcfg-eth0:0',
420- """
421-# Created by cloud-init on instance boot automatically, do not edit.
422-#
423-BOOTPROTO=static
424 DEFROUTE=yes
425-DEVICE=eth0:0
426+DEVICE=eth0
427 GATEWAY=172.19.3.254
428 HWADDR=fa:16:3e:ed:9a:59
429 IPADDR=172.19.1.34
430-NETMASK=255.255.252.0
431-NM_CONTROLLED=no
432-ONBOOT=yes
433-TYPE=Ethernet
434-USERCTL=no
435-""".lstrip()),
436- ('etc/sysconfig/network-scripts/ifcfg-eth0:1',
437- """
438-# Created by cloud-init on instance boot automatically, do not edit.
439-#
440-BOOTPROTO=static
441-DEFROUTE=yes
442-DEVICE=eth0:1
443-HWADDR=fa:16:3e:ed:9a:59
444 IPV6ADDR=2001:DB8::10
445+IPV6ADDR_SECONDARIES="2001:DB9::10/64 2001:DB10::10/64"
446 IPV6INIT=yes
447 IPV6_DEFAULTGW=2001:DB8::1
448-NETMASK=
449+NETMASK=255.255.252.0
450 NM_CONTROLLED=no
451 ONBOOT=yes
452 TYPE=Ethernet

Subscribers

People subscribed via source and target branches