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

Proposed by Andreas Karis
Status: Merged
Merge reported by: Scott Moser
Merged at revision: 1a401c57978855b5d56ac23976252e2506d206fa
Proposed branch: ~akaris/cloud-init:bug1679817-c
Merge into: cloud-init:master
Diff against target: 453 lines (+194/-132)
3 files modified
cloudinit/net/sysconfig.py (+169/-70)
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+324195@code.launchpad.net

This proposal has been superseded by a proposal from 2017-05-17.

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

To post a comment you must log in.
Revision history for this message
Andreas Karis (akaris) wrote :

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

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

FAILED: Continuous integration, rev:1a401c57978855b5d56ac23976252e2506d206fa
No commit message was specified in the merge proposal. Click on the following link and set the commit message (if you want a jenkins rebuild you need to trigger it yourself):
https://code.launchpad.net/~akaris/cloud-init/+git/cloud-init/+merge/324195/+edit-commit-message

https://jenkins.ubuntu.com/server/job/cloud-init-ci/345/
Executed test runs:
    SUCCESS: https://jenkins.ubuntu.com/server/job/cloud-init-ci/nodes=metal-amd64/345
    SUCCESS: https://jenkins.ubuntu.com/server/job/cloud-init-ci/nodes=metal-arm64/345
    SUCCESS: https://jenkins.ubuntu.com/server/job/cloud-init-ci/nodes=metal-ppc64el/345
    SUCCESS: https://jenkins.ubuntu.com/server/job/cloud-init-ci/nodes=vm-i386/345

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

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

Hi,
I'm marking this as 'merged' based on the fact that the new merge proposal *is* merged.
(https://code.launchpad.net/~akaris/cloud-init/+git/cloud-init/+merge/324196)
Please move back to 'Needs Review' (and explain) if you think otherwise.

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 d981277..d04ae8f 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,119 @@ 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- is_ipv6 = subnet.get('ipv6')
133- for route in subnet.get('routes', []):
134- if _is_default_route(route):
135- if (
136- (subnet.get('ipv4') and
137- route_cfg.has_set_default_ipv4) or
138- (subnet.get('ipv6') and
139- route_cfg.has_set_default_ipv6)
140- ):
141- raise ValueError("Duplicate declaration of default "
142- "route found for interface '%s'"
143- % (iface_cfg.name))
144- # NOTE(harlowja): ipv6 and ipv4 default gateways
145- gw_key = 'GATEWAY0'
146- nm_key = 'NETMASK0'
147- addr_key = 'ADDRESS0'
148- # The owning interface provides the default route.
149- #
150- # TODO(harlowja): add validation that no other iface has
151- # also provided the default route?
152- iface_cfg['DEFROUTE'] = True
153- if 'gateway' in route:
154- if is_ipv6:
155- iface_cfg['IPV6_DEFAULTGW'] = route['gateway']
156- route_cfg.has_set_default_ipv6 = True
157+ raise ValueError("Unknown subnet type '%s' found"
158+ " for interface '%s'" % (subnet_type,
159+ iface_cfg.name))
160+
161+ # set IPv4 and IPv6 static addresses
162+ ipv4_index = -1
163+ ipv6_index = -1
164+ for i, subnet in enumerate(subnets, start=len(iface_cfg.children)):
165+ subnet_type = subnet.get('type')
166+ if subnet_type == 'dhcp6':
167+ continue
168+ elif subnet_type in ['dhcp4', 'dhcp']:
169+ continue
170+ elif subnet_type == 'static':
171+ if subnet_is_ipv6(subnet):
172+ ipv6_index = ipv6_index + 1
173+ if 'netmask' in subnet and str(subnet['netmask']) != "":
174+ ipv6_cidr = (subnet['address'] +
175+ '/' +
176+ str(subnet['netmask']))
177 else:
178- iface_cfg['GATEWAY'] = route['gateway']
179- route_cfg.has_set_default_ipv4 = True
180- else:
181- gw_key = 'GATEWAY%s' % route_cfg.last_idx
182- nm_key = 'NETMASK%s' % route_cfg.last_idx
183- addr_key = 'ADDRESS%s' % route_cfg.last_idx
184- route_cfg.last_idx += 1
185- for (old_key, new_key) in [('gateway', gw_key),
186- ('netmask', nm_key),
187- ('network', addr_key)]:
188- if old_key in route:
189- route_cfg[new_key] = route[old_key]
190+ ipv6_cidr = subnet['address']
191+ if ipv6_index == 0:
192+ iface_cfg['IPV6ADDR'] = ipv6_cidr
193+ elif ipv6_index == 1:
194+ iface_cfg['IPV6ADDR_SECONDARIES'] = ipv6_cidr
195+ else:
196+ iface_cfg['IPV6ADDR_SECONDARIES'] = (
197+ iface_cfg['IPV6ADDR_SECONDARIES'] +
198+ " " + ipv6_cidr)
199+ else:
200+ ipv4_index = ipv4_index + 1
201+ if ipv4_index == 0:
202+ iface_cfg['IPADDR'] = subnet['address']
203+ if 'netmask' in subnet:
204+ iface_cfg['NETMASK'] = subnet['netmask']
205+ else:
206+ iface_cfg['IPADDR' + str(ipv4_index)] = \
207+ subnet['address']
208+ if 'netmask' in subnet:
209+ iface_cfg['NETMASK' + str(ipv4_index)] = \
210+ subnet['netmask']
211+
212+ @classmethod
213+ def _render_subnet_routes(cls, iface_cfg, route_cfg, subnets):
214+ for i, subnet in enumerate(subnets, start=len(iface_cfg.children)):
215+ for route in subnet.get('routes', []):
216+ is_ipv6 = subnet.get('ipv6')
217+
218+ if _is_default_route(route):
219+ if (
220+ (subnet.get('ipv4') and
221+ route_cfg.has_set_default_ipv4) or
222+ (subnet.get('ipv6') and
223+ route_cfg.has_set_default_ipv6)
224+ ):
225+ raise ValueError("Duplicate declaration of default "
226+ "route found for interface '%s'"
227+ % (iface_cfg.name))
228+ # NOTE(harlowja): ipv6 and ipv4 default gateways
229+ gw_key = 'GATEWAY0'
230+ nm_key = 'NETMASK0'
231+ addr_key = 'ADDRESS0'
232+ # The owning interface provides the default route.
233+ #
234+ # TODO(harlowja): add validation that no other iface has
235+ # also provided the default route?
236+ iface_cfg['DEFROUTE'] = True
237+ if 'gateway' in route:
238+ if is_ipv6:
239+ iface_cfg['IPV6_DEFAULTGW'] = route['gateway']
240+ route_cfg.has_set_default_ipv6 = True
241+ else:
242+ iface_cfg['GATEWAY'] = route['gateway']
243+ route_cfg.has_set_default_ipv4 = True
244+
245+ else:
246+ gw_key = 'GATEWAY%s' % route_cfg.last_idx
247+ nm_key = 'NETMASK%s' % route_cfg.last_idx
248+ addr_key = 'ADDRESS%s' % route_cfg.last_idx
249+ route_cfg.last_idx += 1
250+ for (old_key, new_key) in [('gateway', gw_key),
251+ ('netmask', nm_key),
252+ ('network', addr_key)]:
253+ if old_key in route:
254+ route_cfg[new_key] = route[old_key]
255
256 @classmethod
257 def _render_bonding_opts(cls, iface_cfg, iface):
258@@ -295,15 +397,9 @@ class Renderer(renderer.Renderer):
259 iface_subnets = iface.get("subnets", [])
260 iface_cfg = iface_contents[iface_name]
261 route_cfg = iface_cfg.routes
262- if len(iface_subnets) == 1:
263- cls._render_subnet(iface_cfg, route_cfg, iface_subnets[0])
264- elif len(iface_subnets) > 1:
265- for i, isubnet in enumerate(iface_subnets,
266- start=len(iface_cfg.children)):
267- iface_sub_cfg = iface_cfg.copy()
268- iface_sub_cfg.name = "%s:%s" % (iface_name, i)
269- iface_cfg.children.append(iface_sub_cfg)
270- cls._render_subnet(iface_sub_cfg, route_cfg, isubnet)
271+
272+ cls._render_subnets(iface_cfg, iface_subnets)
273+ cls._render_subnet_routes(iface_cfg, route_cfg, iface_subnets)
274
275 @classmethod
276 def _render_bond_interfaces(cls, network_state, iface_contents):
277@@ -387,7 +483,10 @@ class Renderer(renderer.Renderer):
278 if iface_cfg:
279 contents[iface_cfg.path] = iface_cfg.to_string()
280 if iface_cfg.routes:
281- contents[iface_cfg.routes.path] = iface_cfg.routes.to_string()
282+ contents[iface_cfg.routes.path_ipv4] = \
283+ iface_cfg.routes.to_string("ipv4")
284+ contents[iface_cfg.routes.path_ipv6] = \
285+ iface_cfg.routes.to_string("ipv6")
286 return contents
287
288 def render_network_state(self, network_state, target=None):
289diff --git a/tests/unittests/test_distros/test_netconfig.py b/tests/unittests/test_distros/test_netconfig.py
290index 1e10a33..fd7c051 100644
291--- a/tests/unittests/test_distros/test_netconfig.py
292+++ b/tests/unittests/test_distros/test_netconfig.py
293@@ -476,7 +476,7 @@ NETWORKING=yes
294 expected_buf = '''
295 # Created by cloud-init on instance boot automatically, do not edit.
296 #
297-BOOTPROTO=static
298+BOOTPROTO=none
299 DEVICE=eth0
300 IPADDR=192.168.1.5
301 NETMASK=255.255.255.0
302@@ -533,7 +533,6 @@ NETWORKING=yes
303 mock.patch.object(util, 'load_file', return_value=''))
304 mocks.enter_context(
305 mock.patch.object(os.path, 'isfile', return_value=False))
306-
307 rh_distro.apply_network(BASE_NET_CFG_IPV6, False)
308
309 self.assertEqual(len(write_bufs), 4)
310@@ -626,11 +625,10 @@ IPV6_AUTOCONF=no
311 expected_buf = '''
312 # Created by cloud-init on instance boot automatically, do not edit.
313 #
314-BOOTPROTO=static
315+BOOTPROTO=none
316 DEVICE=eth0
317-IPV6ADDR=2607:f0d0:1002:0011::2
318+IPV6ADDR=2607:f0d0:1002:0011::2/64
319 IPV6INIT=yes
320-NETMASK=64
321 NM_CONTROLLED=no
322 ONBOOT=yes
323 TYPE=Ethernet
324diff --git a/tests/unittests/test_net.py b/tests/unittests/test_net.py
325index d36d0e7..cf4cedc 100644
326--- a/tests/unittests/test_net.py
327+++ b/tests/unittests/test_net.py
328@@ -137,7 +137,7 @@ OS_SAMPLES = [
329 """
330 # Created by cloud-init on instance boot automatically, do not edit.
331 #
332-BOOTPROTO=static
333+BOOTPROTO=none
334 DEFROUTE=yes
335 DEVICE=eth0
336 GATEWAY=172.19.3.254
337@@ -205,38 +205,14 @@ nameserver 172.19.0.12
338 # Created by cloud-init on instance boot automatically, do not edit.
339 #
340 BOOTPROTO=none
341-DEVICE=eth0
342-HWADDR=fa:16:3e:ed:9a:59
343-NM_CONTROLLED=no
344-ONBOOT=yes
345-TYPE=Ethernet
346-USERCTL=no
347-""".lstrip()),
348- ('etc/sysconfig/network-scripts/ifcfg-eth0:0',
349- """
350-# Created by cloud-init on instance boot automatically, do not edit.
351-#
352-BOOTPROTO=static
353 DEFROUTE=yes
354-DEVICE=eth0:0
355+DEVICE=eth0
356 GATEWAY=172.19.3.254
357 HWADDR=fa:16:3e:ed:9a:59
358 IPADDR=172.19.1.34
359+IPADDR1=10.0.0.10
360 NETMASK=255.255.252.0
361-NM_CONTROLLED=no
362-ONBOOT=yes
363-TYPE=Ethernet
364-USERCTL=no
365-""".lstrip()),
366- ('etc/sysconfig/network-scripts/ifcfg-eth0:1',
367- """
368-# Created by cloud-init on instance boot automatically, do not edit.
369-#
370-BOOTPROTO=static
371-DEVICE=eth0:1
372-HWADDR=fa:16:3e:ed:9a:59
373-IPADDR=10.0.0.10
374-NETMASK=255.255.255.0
375+NETMASK1=255.255.255.0
376 NM_CONTROLLED=no
377 ONBOOT=yes
378 TYPE=Ethernet
379@@ -266,7 +242,7 @@ nameserver 172.19.0.12
380 }],
381 "ip_address": "172.19.1.34", "id": "network0"
382 }, {
383- "network_id": "public-ipv6",
384+ "network_id": "public-ipv6-a",
385 "type": "ipv6", "netmask": "",
386 "link": "tap1a81968a-79",
387 "routes": [
388@@ -277,6 +253,20 @@ nameserver 172.19.0.12
389 }
390 ],
391 "ip_address": "2001:DB8::10", "id": "network1"
392+ }, {
393+ "network_id": "public-ipv6-b",
394+ "type": "ipv6", "netmask": "64",
395+ "link": "tap1a81968a-79",
396+ "routes": [
397+ ],
398+ "ip_address": "2001:DB9::10", "id": "network2"
399+ }, {
400+ "network_id": "public-ipv6-c",
401+ "type": "ipv6", "netmask": "64",
402+ "link": "tap1a81968a-79",
403+ "routes": [
404+ ],
405+ "ip_address": "2001:DB10::10", "id": "network3"
406 }],
407 "links": [
408 {
409@@ -296,41 +286,16 @@ nameserver 172.19.0.12
410 # Created by cloud-init on instance boot automatically, do not edit.
411 #
412 BOOTPROTO=none
413-DEVICE=eth0
414-HWADDR=fa:16:3e:ed:9a:59
415-NM_CONTROLLED=no
416-ONBOOT=yes
417-TYPE=Ethernet
418-USERCTL=no
419-""".lstrip()),
420- ('etc/sysconfig/network-scripts/ifcfg-eth0:0',
421- """
422-# Created by cloud-init on instance boot automatically, do not edit.
423-#
424-BOOTPROTO=static
425 DEFROUTE=yes
426-DEVICE=eth0:0
427+DEVICE=eth0
428 GATEWAY=172.19.3.254
429 HWADDR=fa:16:3e:ed:9a:59
430 IPADDR=172.19.1.34
431-NETMASK=255.255.252.0
432-NM_CONTROLLED=no
433-ONBOOT=yes
434-TYPE=Ethernet
435-USERCTL=no
436-""".lstrip()),
437- ('etc/sysconfig/network-scripts/ifcfg-eth0:1',
438- """
439-# Created by cloud-init on instance boot automatically, do not edit.
440-#
441-BOOTPROTO=static
442-DEFROUTE=yes
443-DEVICE=eth0:1
444-HWADDR=fa:16:3e:ed:9a:59
445 IPV6ADDR=2001:DB8::10
446+IPV6ADDR_SECONDARIES="2001:DB9::10/64 2001:DB10::10/64"
447 IPV6INIT=yes
448 IPV6_DEFAULTGW=2001:DB8::1
449-NETMASK=
450+NETMASK=255.255.252.0
451 NM_CONTROLLED=no
452 ONBOOT=yes
453 TYPE=Ethernet

Subscribers

People subscribed via source and target branches