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
diff --git a/cloudinit/net/sysconfig.py b/cloudinit/net/sysconfig.py
index d981277..d04ae8f 100644
--- a/cloudinit/net/sysconfig.py
+++ b/cloudinit/net/sysconfig.py
@@ -59,6 +59,9 @@ class ConfigMap(object):
59 def __setitem__(self, key, value):59 def __setitem__(self, key, value):
60 self._conf[key] = value60 self._conf[key] = value
6161
62 def __getitem__(self, key):
63 return self._conf[key]
64
62 def drop(self, key):65 def drop(self, key):
63 self._conf.pop(key, None)66 self._conf.pop(key, None)
6467
@@ -83,7 +86,8 @@ class ConfigMap(object):
83class Route(ConfigMap):86class Route(ConfigMap):
84 """Represents a route configuration."""87 """Represents a route configuration."""
8588
86 route_fn_tpl = '%(base)s/network-scripts/route-%(name)s'89 route_fn_tpl_ipv4 = '%(base)s/network-scripts/route-%(name)s'
90 route_fn_tpl_ipv6 = '%(base)s/network-scripts/route6-%(name)s'
8791
88 def __init__(self, route_name, base_sysconf_dir):92 def __init__(self, route_name, base_sysconf_dir):
89 super(Route, self).__init__()93 super(Route, self).__init__()
@@ -102,9 +106,53 @@ class Route(ConfigMap):
102 return r106 return r
103107
104 @property108 @property
105 def path(self):109 def path_ipv4(self):
106 return self.route_fn_tpl % ({'base': self._base_sysconf_dir,110 return self.route_fn_tpl_ipv4 % ({'base': self._base_sysconf_dir,
107 'name': self._route_name})111 'name': self._route_name})
112
113 @property
114 def path_ipv6(self):
115 return self.route_fn_tpl_ipv6 % ({'base': self._base_sysconf_dir,
116 'name': self._route_name})
117
118 def is_ipv6_route(self, address):
119 return ':' in address
120
121 def to_string(self, proto="ipv4"):
122 buf = six.StringIO()
123 buf.write(_make_header())
124 if self._conf:
125 buf.write("\n")
126 # need to reindex IPv4 addresses
127 # (because Route can contain a mix of IPv4 and IPv6)
128 reindex = -1
129 for key in sorted(self._conf.keys()):
130 if 'ADDRESS' in key:
131 index = key.replace('ADDRESS', '')
132 address_value = str(self._conf[key])
133 # only accept combinations:
134 # ipv6 route and proto ipv6
135 # ipv4 route and proto ipv4
136 # do not add any other routes
137 if proto == "ipv4" and not self.is_ipv6_route(address_value):
138 netmask_value = str(self._conf['NETMASK' + index])
139 gateway_value = str(self._conf['GATEWAY' + index])
140 # increase IPv4 index
141 reindex = reindex + 1
142 buf.write("%s=%s\n" % ('ADDRESS' + str(reindex),
143 _quote_value(address_value)))
144 buf.write("%s=%s\n" % ('GATEWAY' + str(reindex),
145 _quote_value(gateway_value)))
146 buf.write("%s=%s\n" % ('NETMASK' + str(reindex),
147 _quote_value(netmask_value)))
148 elif proto == "ipv6" and self.is_ipv6_route(address_value):
149 netmask_value = str(self._conf['NETMASK' + index])
150 gateway_value = str(self._conf['GATEWAY' + index])
151 buf.write("%s/%s via %s\n" % (address_value,
152 netmask_value,
153 gateway_value))
154
155 return buf.getvalue()
108156
109157
110class NetInterface(ConfigMap):158class NetInterface(ConfigMap):
@@ -211,65 +259,119 @@ class Renderer(renderer.Renderer):
211 iface_cfg[new_key] = old_value259 iface_cfg[new_key] = old_value
212260
213 @classmethod261 @classmethod
214 def _render_subnet(cls, iface_cfg, route_cfg, subnet):262 def _render_subnets(cls, iface_cfg, subnets):
215 subnet_type = subnet.get('type')263 # setting base values
216 if subnet_type == 'dhcp6':264 iface_cfg['BOOTPROTO'] = 'none'
217 iface_cfg['DHCPV6C'] = True265
218 iface_cfg['IPV6INIT'] = True266 # modifying base values according to subnets
219 iface_cfg['BOOTPROTO'] = 'dhcp'267 for i, subnet in enumerate(subnets, start=len(iface_cfg.children)):
220 elif subnet_type in ['dhcp4', 'dhcp']:268 subnet_type = subnet.get('type')
221 iface_cfg['BOOTPROTO'] = 'dhcp'269 if subnet_type == 'dhcp6':
222 elif subnet_type == 'static':
223 iface_cfg['BOOTPROTO'] = 'static'
224 if subnet_is_ipv6(subnet):
225 iface_cfg['IPV6ADDR'] = subnet['address']
226 iface_cfg['IPV6INIT'] = True270 iface_cfg['IPV6INIT'] = True
271 iface_cfg['DHCPV6C'] = True
272 iface_cfg['BOOTPROTO'] = 'dhcp'
273 elif subnet_type in ['dhcp4', 'dhcp']:
274 iface_cfg['BOOTPROTO'] = 'dhcp'
275 elif subnet_type == 'static':
276 # grep BOOTPROTO sysconfig.txt -A2 | head -3
277 # BOOTPROTO=none|bootp|dhcp
278 # 'bootp' or 'dhcp' cause a DHCP client
279 # to run on the device. Any other
280 # value causes any static configuration
281 # in the file to be applied.
282 # ==> the following should not be set to 'static'
283 # but should remain 'none'
284 # if iface_cfg['BOOTPROTO'] == 'none':
285 # iface_cfg['BOOTPROTO'] = 'static'
286 if subnet_is_ipv6(subnet):
287 iface_cfg['IPV6INIT'] = True
227 else:288 else:
228 iface_cfg['IPADDR'] = subnet['address']289 raise ValueError("Unknown subnet type '%s' found"
229 else:290 " for interface '%s'" % (subnet_type,
230 raise ValueError("Unknown subnet type '%s' found"291 iface_cfg.name))
231 " for interface '%s'" % (subnet_type,292
232 iface_cfg.name))293 # set IPv4 and IPv6 static addresses
233 if 'netmask' in subnet:294 ipv4_index = -1
234 iface_cfg['NETMASK'] = subnet['netmask']295 ipv6_index = -1
235 is_ipv6 = subnet.get('ipv6')296 for i, subnet in enumerate(subnets, start=len(iface_cfg.children)):
236 for route in subnet.get('routes', []):297 subnet_type = subnet.get('type')
237 if _is_default_route(route):298 if subnet_type == 'dhcp6':
238 if (299 continue
239 (subnet.get('ipv4') and300 elif subnet_type in ['dhcp4', 'dhcp']:
240 route_cfg.has_set_default_ipv4) or301 continue
241 (subnet.get('ipv6') and302 elif subnet_type == 'static':
242 route_cfg.has_set_default_ipv6)303 if subnet_is_ipv6(subnet):
243 ):304 ipv6_index = ipv6_index + 1
244 raise ValueError("Duplicate declaration of default "305 if 'netmask' in subnet and str(subnet['netmask']) != "":
245 "route found for interface '%s'"306 ipv6_cidr = (subnet['address'] +
246 % (iface_cfg.name))307 '/' +
247 # NOTE(harlowja): ipv6 and ipv4 default gateways308 str(subnet['netmask']))
248 gw_key = 'GATEWAY0'
249 nm_key = 'NETMASK0'
250 addr_key = 'ADDRESS0'
251 # The owning interface provides the default route.
252 #
253 # TODO(harlowja): add validation that no other iface has
254 # also provided the default route?
255 iface_cfg['DEFROUTE'] = True
256 if 'gateway' in route:
257 if is_ipv6:
258 iface_cfg['IPV6_DEFAULTGW'] = route['gateway']
259 route_cfg.has_set_default_ipv6 = True
260 else:309 else:
261 iface_cfg['GATEWAY'] = route['gateway']310 ipv6_cidr = subnet['address']
262 route_cfg.has_set_default_ipv4 = True311 if ipv6_index == 0:
263 else:312 iface_cfg['IPV6ADDR'] = ipv6_cidr
264 gw_key = 'GATEWAY%s' % route_cfg.last_idx313 elif ipv6_index == 1:
265 nm_key = 'NETMASK%s' % route_cfg.last_idx314 iface_cfg['IPV6ADDR_SECONDARIES'] = ipv6_cidr
266 addr_key = 'ADDRESS%s' % route_cfg.last_idx315 else:
267 route_cfg.last_idx += 1316 iface_cfg['IPV6ADDR_SECONDARIES'] = (
268 for (old_key, new_key) in [('gateway', gw_key),317 iface_cfg['IPV6ADDR_SECONDARIES'] +
269 ('netmask', nm_key),318 " " + ipv6_cidr)
270 ('network', addr_key)]:319 else:
271 if old_key in route:320 ipv4_index = ipv4_index + 1
272 route_cfg[new_key] = route[old_key]321 if ipv4_index == 0:
322 iface_cfg['IPADDR'] = subnet['address']
323 if 'netmask' in subnet:
324 iface_cfg['NETMASK'] = subnet['netmask']
325 else:
326 iface_cfg['IPADDR' + str(ipv4_index)] = \
327 subnet['address']
328 if 'netmask' in subnet:
329 iface_cfg['NETMASK' + str(ipv4_index)] = \
330 subnet['netmask']
331
332 @classmethod
333 def _render_subnet_routes(cls, iface_cfg, route_cfg, subnets):
334 for i, subnet in enumerate(subnets, start=len(iface_cfg.children)):
335 for route in subnet.get('routes', []):
336 is_ipv6 = subnet.get('ipv6')
337
338 if _is_default_route(route):
339 if (
340 (subnet.get('ipv4') and
341 route_cfg.has_set_default_ipv4) or
342 (subnet.get('ipv6') and
343 route_cfg.has_set_default_ipv6)
344 ):
345 raise ValueError("Duplicate declaration of default "
346 "route found for interface '%s'"
347 % (iface_cfg.name))
348 # NOTE(harlowja): ipv6 and ipv4 default gateways
349 gw_key = 'GATEWAY0'
350 nm_key = 'NETMASK0'
351 addr_key = 'ADDRESS0'
352 # The owning interface provides the default route.
353 #
354 # TODO(harlowja): add validation that no other iface has
355 # also provided the default route?
356 iface_cfg['DEFROUTE'] = True
357 if 'gateway' in route:
358 if is_ipv6:
359 iface_cfg['IPV6_DEFAULTGW'] = route['gateway']
360 route_cfg.has_set_default_ipv6 = True
361 else:
362 iface_cfg['GATEWAY'] = route['gateway']
363 route_cfg.has_set_default_ipv4 = True
364
365 else:
366 gw_key = 'GATEWAY%s' % route_cfg.last_idx
367 nm_key = 'NETMASK%s' % route_cfg.last_idx
368 addr_key = 'ADDRESS%s' % route_cfg.last_idx
369 route_cfg.last_idx += 1
370 for (old_key, new_key) in [('gateway', gw_key),
371 ('netmask', nm_key),
372 ('network', addr_key)]:
373 if old_key in route:
374 route_cfg[new_key] = route[old_key]
273375
274 @classmethod376 @classmethod
275 def _render_bonding_opts(cls, iface_cfg, iface):377 def _render_bonding_opts(cls, iface_cfg, iface):
@@ -295,15 +397,9 @@ class Renderer(renderer.Renderer):
295 iface_subnets = iface.get("subnets", [])397 iface_subnets = iface.get("subnets", [])
296 iface_cfg = iface_contents[iface_name]398 iface_cfg = iface_contents[iface_name]
297 route_cfg = iface_cfg.routes399 route_cfg = iface_cfg.routes
298 if len(iface_subnets) == 1:400
299 cls._render_subnet(iface_cfg, route_cfg, iface_subnets[0])401 cls._render_subnets(iface_cfg, iface_subnets)
300 elif len(iface_subnets) > 1:402 cls._render_subnet_routes(iface_cfg, route_cfg, iface_subnets)
301 for i, isubnet in enumerate(iface_subnets,
302 start=len(iface_cfg.children)):
303 iface_sub_cfg = iface_cfg.copy()
304 iface_sub_cfg.name = "%s:%s" % (iface_name, i)
305 iface_cfg.children.append(iface_sub_cfg)
306 cls._render_subnet(iface_sub_cfg, route_cfg, isubnet)
307403
308 @classmethod404 @classmethod
309 def _render_bond_interfaces(cls, network_state, iface_contents):405 def _render_bond_interfaces(cls, network_state, iface_contents):
@@ -387,7 +483,10 @@ class Renderer(renderer.Renderer):
387 if iface_cfg:483 if iface_cfg:
388 contents[iface_cfg.path] = iface_cfg.to_string()484 contents[iface_cfg.path] = iface_cfg.to_string()
389 if iface_cfg.routes:485 if iface_cfg.routes:
390 contents[iface_cfg.routes.path] = iface_cfg.routes.to_string()486 contents[iface_cfg.routes.path_ipv4] = \
487 iface_cfg.routes.to_string("ipv4")
488 contents[iface_cfg.routes.path_ipv6] = \
489 iface_cfg.routes.to_string("ipv6")
391 return contents490 return contents
392491
393 def render_network_state(self, network_state, target=None):492 def render_network_state(self, network_state, target=None):
diff --git a/tests/unittests/test_distros/test_netconfig.py b/tests/unittests/test_distros/test_netconfig.py
index 1e10a33..fd7c051 100644
--- a/tests/unittests/test_distros/test_netconfig.py
+++ b/tests/unittests/test_distros/test_netconfig.py
@@ -476,7 +476,7 @@ NETWORKING=yes
476 expected_buf = '''476 expected_buf = '''
477# Created by cloud-init on instance boot automatically, do not edit.477# Created by cloud-init on instance boot automatically, do not edit.
478#478#
479BOOTPROTO=static479BOOTPROTO=none
480DEVICE=eth0480DEVICE=eth0
481IPADDR=192.168.1.5481IPADDR=192.168.1.5
482NETMASK=255.255.255.0482NETMASK=255.255.255.0
@@ -533,7 +533,6 @@ NETWORKING=yes
533 mock.patch.object(util, 'load_file', return_value=''))533 mock.patch.object(util, 'load_file', return_value=''))
534 mocks.enter_context(534 mocks.enter_context(
535 mock.patch.object(os.path, 'isfile', return_value=False))535 mock.patch.object(os.path, 'isfile', return_value=False))
536
537 rh_distro.apply_network(BASE_NET_CFG_IPV6, False)536 rh_distro.apply_network(BASE_NET_CFG_IPV6, False)
538537
539 self.assertEqual(len(write_bufs), 4)538 self.assertEqual(len(write_bufs), 4)
@@ -626,11 +625,10 @@ IPV6_AUTOCONF=no
626 expected_buf = '''625 expected_buf = '''
627# Created by cloud-init on instance boot automatically, do not edit.626# Created by cloud-init on instance boot automatically, do not edit.
628#627#
629BOOTPROTO=static628BOOTPROTO=none
630DEVICE=eth0629DEVICE=eth0
631IPV6ADDR=2607:f0d0:1002:0011::2630IPV6ADDR=2607:f0d0:1002:0011::2/64
632IPV6INIT=yes631IPV6INIT=yes
633NETMASK=64
634NM_CONTROLLED=no632NM_CONTROLLED=no
635ONBOOT=yes633ONBOOT=yes
636TYPE=Ethernet634TYPE=Ethernet
diff --git a/tests/unittests/test_net.py b/tests/unittests/test_net.py
index d36d0e7..cf4cedc 100644
--- a/tests/unittests/test_net.py
+++ b/tests/unittests/test_net.py
@@ -137,7 +137,7 @@ OS_SAMPLES = [
137 """137 """
138# Created by cloud-init on instance boot automatically, do not edit.138# Created by cloud-init on instance boot automatically, do not edit.
139#139#
140BOOTPROTO=static140BOOTPROTO=none
141DEFROUTE=yes141DEFROUTE=yes
142DEVICE=eth0142DEVICE=eth0
143GATEWAY=172.19.3.254143GATEWAY=172.19.3.254
@@ -205,38 +205,14 @@ nameserver 172.19.0.12
205# Created by cloud-init on instance boot automatically, do not edit.205# Created by cloud-init on instance boot automatically, do not edit.
206#206#
207BOOTPROTO=none207BOOTPROTO=none
208DEVICE=eth0
209HWADDR=fa:16:3e:ed:9a:59
210NM_CONTROLLED=no
211ONBOOT=yes
212TYPE=Ethernet
213USERCTL=no
214""".lstrip()),
215 ('etc/sysconfig/network-scripts/ifcfg-eth0:0',
216 """
217# Created by cloud-init on instance boot automatically, do not edit.
218#
219BOOTPROTO=static
220DEFROUTE=yes208DEFROUTE=yes
221DEVICE=eth0:0209DEVICE=eth0
222GATEWAY=172.19.3.254210GATEWAY=172.19.3.254
223HWADDR=fa:16:3e:ed:9a:59211HWADDR=fa:16:3e:ed:9a:59
224IPADDR=172.19.1.34212IPADDR=172.19.1.34
213IPADDR1=10.0.0.10
225NETMASK=255.255.252.0214NETMASK=255.255.252.0
226NM_CONTROLLED=no215NETMASK1=255.255.255.0
227ONBOOT=yes
228TYPE=Ethernet
229USERCTL=no
230""".lstrip()),
231 ('etc/sysconfig/network-scripts/ifcfg-eth0:1',
232 """
233# Created by cloud-init on instance boot automatically, do not edit.
234#
235BOOTPROTO=static
236DEVICE=eth0:1
237HWADDR=fa:16:3e:ed:9a:59
238IPADDR=10.0.0.10
239NETMASK=255.255.255.0
240NM_CONTROLLED=no216NM_CONTROLLED=no
241ONBOOT=yes217ONBOOT=yes
242TYPE=Ethernet218TYPE=Ethernet
@@ -266,7 +242,7 @@ nameserver 172.19.0.12
266 }],242 }],
267 "ip_address": "172.19.1.34", "id": "network0"243 "ip_address": "172.19.1.34", "id": "network0"
268 }, {244 }, {
269 "network_id": "public-ipv6",245 "network_id": "public-ipv6-a",
270 "type": "ipv6", "netmask": "",246 "type": "ipv6", "netmask": "",
271 "link": "tap1a81968a-79",247 "link": "tap1a81968a-79",
272 "routes": [248 "routes": [
@@ -277,6 +253,20 @@ nameserver 172.19.0.12
277 }253 }
278 ],254 ],
279 "ip_address": "2001:DB8::10", "id": "network1"255 "ip_address": "2001:DB8::10", "id": "network1"
256 }, {
257 "network_id": "public-ipv6-b",
258 "type": "ipv6", "netmask": "64",
259 "link": "tap1a81968a-79",
260 "routes": [
261 ],
262 "ip_address": "2001:DB9::10", "id": "network2"
263 }, {
264 "network_id": "public-ipv6-c",
265 "type": "ipv6", "netmask": "64",
266 "link": "tap1a81968a-79",
267 "routes": [
268 ],
269 "ip_address": "2001:DB10::10", "id": "network3"
280 }],270 }],
281 "links": [271 "links": [
282 {272 {
@@ -296,41 +286,16 @@ nameserver 172.19.0.12
296# Created by cloud-init on instance boot automatically, do not edit.286# Created by cloud-init on instance boot automatically, do not edit.
297#287#
298BOOTPROTO=none288BOOTPROTO=none
299DEVICE=eth0
300HWADDR=fa:16:3e:ed:9a:59
301NM_CONTROLLED=no
302ONBOOT=yes
303TYPE=Ethernet
304USERCTL=no
305""".lstrip()),
306 ('etc/sysconfig/network-scripts/ifcfg-eth0:0',
307 """
308# Created by cloud-init on instance boot automatically, do not edit.
309#
310BOOTPROTO=static
311DEFROUTE=yes289DEFROUTE=yes
312DEVICE=eth0:0290DEVICE=eth0
313GATEWAY=172.19.3.254291GATEWAY=172.19.3.254
314HWADDR=fa:16:3e:ed:9a:59292HWADDR=fa:16:3e:ed:9a:59
315IPADDR=172.19.1.34293IPADDR=172.19.1.34
316NETMASK=255.255.252.0
317NM_CONTROLLED=no
318ONBOOT=yes
319TYPE=Ethernet
320USERCTL=no
321""".lstrip()),
322 ('etc/sysconfig/network-scripts/ifcfg-eth0:1',
323 """
324# Created by cloud-init on instance boot automatically, do not edit.
325#
326BOOTPROTO=static
327DEFROUTE=yes
328DEVICE=eth0:1
329HWADDR=fa:16:3e:ed:9a:59
330IPV6ADDR=2001:DB8::10294IPV6ADDR=2001:DB8::10
295IPV6ADDR_SECONDARIES="2001:DB9::10/64 2001:DB10::10/64"
331IPV6INIT=yes296IPV6INIT=yes
332IPV6_DEFAULTGW=2001:DB8::1297IPV6_DEFAULTGW=2001:DB8::1
333NETMASK=298NETMASK=255.255.252.0
334NM_CONTROLLED=no299NM_CONTROLLED=no
335ONBOOT=yes300ONBOOT=yes
336TYPE=Ethernet301TYPE=Ethernet

Subscribers

People subscribed via source and target branches