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
diff --git a/cloudinit/net/sysconfig.py b/cloudinit/net/sysconfig.py
index 504e4d0..49da3a3 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,117 @@ 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 for route in subnet.get('routes', []):296 for i, subnet in enumerate(subnets, start=len(iface_cfg.children)):
236 if subnet.get('ipv6'):297 subnet_type = subnet.get('type')
237 gw_cfg = 'IPV6_DEFAULTGW'298 if subnet_type == 'dhcp6':
238 else:299 continue
239 gw_cfg = 'GATEWAY'300 elif subnet_type in ['dhcp4', 'dhcp']:
240301 continue
241 if _is_default_route(route):302 elif subnet_type == 'static':
242 if (303 if subnet_is_ipv6(subnet):
243 (subnet.get('ipv4') and304 ipv6_index = ipv6_index + 1
244 route_cfg.has_set_default_ipv4) or305 if 'netmask' in subnet and str(subnet['netmask']) != "":
245 (subnet.get('ipv6') and306 ipv6_cidr = (subnet['address'] +
246 route_cfg.has_set_default_ipv6)307 '/' +
247 ):308 str(subnet['netmask']))
248 raise ValueError("Duplicate declaration of default "309 else:
249 "route found for interface '%s'"310 ipv6_cidr = subnet['address']
250 % (iface_cfg.name))311 if ipv6_index == 0:
251 # NOTE(harlowja): ipv6 and ipv4 default gateways312 iface_cfg['IPV6ADDR'] = ipv6_cidr
252 gw_key = 'GATEWAY0'313 elif ipv6_index == 1:
253 nm_key = 'NETMASK0'314 iface_cfg['IPV6ADDR_SECONDARIES'] = ipv6_cidr
254 addr_key = 'ADDRESS0'315 else:
255 # The owning interface provides the default route.316 iface_cfg['IPV6ADDR_SECONDARIES'] = (
256 #317 iface_cfg['IPV6ADDR_SECONDARIES'] +
257 # TODO(harlowja): add validation that no other iface has318 " " + ipv6_cidr)
258 # also provided the default route?319 else:
259 iface_cfg['DEFROUTE'] = True320 ipv4_index = ipv4_index + 1
260 if 'gateway' in route:321 if ipv4_index == 0:
261 iface_cfg[gw_cfg] = route['gateway']322 iface_cfg['IPADDR'] = subnet['address']
262 route_cfg.has_set_default = True323 if 'netmask' in subnet:
263 else:324 iface_cfg['NETMASK'] = subnet['netmask']
264 gw_key = 'GATEWAY%s' % route_cfg.last_idx325 else:
265 nm_key = 'NETMASK%s' % route_cfg.last_idx326 iface_cfg['IPADDR' + str(ipv4_index)] = \
266 addr_key = 'ADDRESS%s' % route_cfg.last_idx327 subnet['address']
267 route_cfg.last_idx += 1328 if 'netmask' in subnet:
268 for (old_key, new_key) in [('gateway', gw_key),329 iface_cfg['NETMASK' + str(ipv4_index)] = \
269 ('netmask', nm_key),330 subnet['netmask']
270 ('network', addr_key)]:331
271 if old_key in route:332 @classmethod
272 route_cfg[new_key] = route[old_key]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 if subnet.get('ipv6'):
337 gw_cfg = 'IPV6_DEFAULTGW'
338 else:
339 gw_cfg = 'GATEWAY'
340
341 if _is_default_route(route):
342 if (
343 (subnet.get('ipv4') and
344 route_cfg.has_set_default_ipv4) or
345 (subnet.get('ipv6') and
346 route_cfg.has_set_default_ipv6)
347 ):
348 raise ValueError("Duplicate declaration of default "
349 "route found for interface '%s'"
350 % (iface_cfg.name))
351 # NOTE(harlowja): ipv6 and ipv4 default gateways
352 gw_key = 'GATEWAY0'
353 nm_key = 'NETMASK0'
354 addr_key = 'ADDRESS0'
355 # The owning interface provides the default route.
356 #
357 # TODO(harlowja): add validation that no other iface has
358 # also provided the default route?
359 iface_cfg['DEFROUTE'] = True
360 if 'gateway' in route:
361 iface_cfg[gw_cfg] = route['gateway']
362 route_cfg.has_set_default = True
363 else:
364 gw_key = 'GATEWAY%s' % route_cfg.last_idx
365 nm_key = 'NETMASK%s' % route_cfg.last_idx
366 addr_key = 'ADDRESS%s' % route_cfg.last_idx
367 route_cfg.last_idx += 1
368 for (old_key, new_key) in [('gateway', gw_key),
369 ('netmask', nm_key),
370 ('network', addr_key)]:
371 if old_key in route:
372 route_cfg[new_key] = route[old_key]
273373
274 @classmethod374 @classmethod
275 def _render_bonding_opts(cls, iface_cfg, iface):375 def _render_bonding_opts(cls, iface_cfg, iface):
@@ -295,15 +395,9 @@ class Renderer(renderer.Renderer):
295 iface_subnets = iface.get("subnets", [])395 iface_subnets = iface.get("subnets", [])
296 iface_cfg = iface_contents[iface_name]396 iface_cfg = iface_contents[iface_name]
297 route_cfg = iface_cfg.routes397 route_cfg = iface_cfg.routes
298 if len(iface_subnets) == 1:398
299 cls._render_subnet(iface_cfg, route_cfg, iface_subnets[0])399 cls._render_subnets(iface_cfg, iface_subnets)
300 elif len(iface_subnets) > 1:400 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)
307401
308 @classmethod402 @classmethod
309 def _render_bond_interfaces(cls, network_state, iface_contents):403 def _render_bond_interfaces(cls, network_state, iface_contents):
@@ -387,7 +481,10 @@ class Renderer(renderer.Renderer):
387 if iface_cfg:481 if iface_cfg:
388 contents[iface_cfg.path] = iface_cfg.to_string()482 contents[iface_cfg.path] = iface_cfg.to_string()
389 if iface_cfg.routes:483 if iface_cfg.routes:
390 contents[iface_cfg.routes.path] = iface_cfg.routes.to_string()484 contents[iface_cfg.routes.path_ipv4] = \
485 iface_cfg.routes.to_string("ipv4")
486 contents[iface_cfg.routes.path_ipv6] = \
487 iface_cfg.routes.to_string("ipv6")
391 return contents488 return contents
392489
393 def render_network_state(self, network_state, target=None):490 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 8837066..6d6b985 100644
--- a/tests/unittests/test_distros/test_netconfig.py
+++ b/tests/unittests/test_distros/test_netconfig.py
@@ -431,7 +431,7 @@ NETWORKING=yes
431 expected_buf = '''431 expected_buf = '''
432# Created by cloud-init on instance boot automatically, do not edit.432# Created by cloud-init on instance boot automatically, do not edit.
433#433#
434BOOTPROTO=static434BOOTPROTO=none
435DEVICE=eth0435DEVICE=eth0
436IPADDR=192.168.1.5436IPADDR=192.168.1.5
437NETMASK=255.255.255.0437NETMASK=255.255.255.0
@@ -488,7 +488,6 @@ NETWORKING=yes
488 mock.patch.object(util, 'load_file', return_value=''))488 mock.patch.object(util, 'load_file', return_value=''))
489 mocks.enter_context(489 mocks.enter_context(
490 mock.patch.object(os.path, 'isfile', return_value=False))490 mock.patch.object(os.path, 'isfile', return_value=False))
491
492 rh_distro.apply_network(BASE_NET_CFG_IPV6, False)491 rh_distro.apply_network(BASE_NET_CFG_IPV6, False)
493492
494 self.assertEqual(len(write_bufs), 4)493 self.assertEqual(len(write_bufs), 4)
@@ -581,11 +580,10 @@ IPV6_AUTOCONF=no
581 expected_buf = '''580 expected_buf = '''
582# Created by cloud-init on instance boot automatically, do not edit.581# Created by cloud-init on instance boot automatically, do not edit.
583#582#
584BOOTPROTO=static583BOOTPROTO=none
585DEVICE=eth0584DEVICE=eth0
586IPV6ADDR=2607:f0d0:1002:0011::2585IPV6ADDR=2607:f0d0:1002:0011::2/64
587IPV6INIT=yes586IPV6INIT=yes
588NETMASK=64
589NM_CONTROLLED=no587NM_CONTROLLED=no
590ONBOOT=yes588ONBOOT=yes
591TYPE=Ethernet589TYPE=Ethernet
diff --git a/tests/unittests/test_net.py b/tests/unittests/test_net.py
index 89e7536..1157c95 100644
--- a/tests/unittests/test_net.py
+++ b/tests/unittests/test_net.py
@@ -136,7 +136,7 @@ OS_SAMPLES = [
136 """136 """
137# Created by cloud-init on instance boot automatically, do not edit.137# Created by cloud-init on instance boot automatically, do not edit.
138#138#
139BOOTPROTO=static139BOOTPROTO=none
140DEFROUTE=yes140DEFROUTE=yes
141DEVICE=eth0141DEVICE=eth0
142GATEWAY=172.19.3.254142GATEWAY=172.19.3.254
@@ -204,38 +204,14 @@ nameserver 172.19.0.12
204# Created by cloud-init on instance boot automatically, do not edit.204# Created by cloud-init on instance boot automatically, do not edit.
205#205#
206BOOTPROTO=none206BOOTPROTO=none
207DEVICE=eth0
208HWADDR=fa:16:3e:ed:9a:59
209NM_CONTROLLED=no
210ONBOOT=yes
211TYPE=Ethernet
212USERCTL=no
213""".lstrip()),
214 ('etc/sysconfig/network-scripts/ifcfg-eth0:0',
215 """
216# Created by cloud-init on instance boot automatically, do not edit.
217#
218BOOTPROTO=static
219DEFROUTE=yes207DEFROUTE=yes
220DEVICE=eth0:0208DEVICE=eth0
221GATEWAY=172.19.3.254209GATEWAY=172.19.3.254
222HWADDR=fa:16:3e:ed:9a:59210HWADDR=fa:16:3e:ed:9a:59
223IPADDR=172.19.1.34211IPADDR=172.19.1.34
212IPADDR1=10.0.0.10
224NETMASK=255.255.252.0213NETMASK=255.255.252.0
225NM_CONTROLLED=no214NETMASK1=255.255.255.0
226ONBOOT=yes
227TYPE=Ethernet
228USERCTL=no
229""".lstrip()),
230 ('etc/sysconfig/network-scripts/ifcfg-eth0:1',
231 """
232# Created by cloud-init on instance boot automatically, do not edit.
233#
234BOOTPROTO=static
235DEVICE=eth0:1
236HWADDR=fa:16:3e:ed:9a:59
237IPADDR=10.0.0.10
238NETMASK=255.255.255.0
239NM_CONTROLLED=no215NM_CONTROLLED=no
240ONBOOT=yes216ONBOOT=yes
241TYPE=Ethernet217TYPE=Ethernet
@@ -265,7 +241,7 @@ nameserver 172.19.0.12
265 }],241 }],
266 "ip_address": "172.19.1.34", "id": "network0"242 "ip_address": "172.19.1.34", "id": "network0"
267 }, {243 }, {
268 "network_id": "public-ipv6",244 "network_id": "public-ipv6-a",
269 "type": "ipv6", "netmask": "",245 "type": "ipv6", "netmask": "",
270 "link": "tap1a81968a-79",246 "link": "tap1a81968a-79",
271 "routes": [247 "routes": [
@@ -276,6 +252,20 @@ nameserver 172.19.0.12
276 }252 }
277 ],253 ],
278 "ip_address": "2001:DB8::10", "id": "network1"254 "ip_address": "2001:DB8::10", "id": "network1"
255 }, {
256 "network_id": "public-ipv6-b",
257 "type": "ipv6", "netmask": "64",
258 "link": "tap1a81968a-79",
259 "routes": [
260 ],
261 "ip_address": "2001:DB9::10", "id": "network2"
262 }, {
263 "network_id": "public-ipv6-c",
264 "type": "ipv6", "netmask": "64",
265 "link": "tap1a81968a-79",
266 "routes": [
267 ],
268 "ip_address": "2001:DB10::10", "id": "network3"
279 }],269 }],
280 "links": [270 "links": [
281 {271 {
@@ -295,41 +285,16 @@ nameserver 172.19.0.12
295# Created by cloud-init on instance boot automatically, do not edit.285# Created by cloud-init on instance boot automatically, do not edit.
296#286#
297BOOTPROTO=none287BOOTPROTO=none
298DEVICE=eth0
299HWADDR=fa:16:3e:ed:9a:59
300NM_CONTROLLED=no
301ONBOOT=yes
302TYPE=Ethernet
303USERCTL=no
304""".lstrip()),
305 ('etc/sysconfig/network-scripts/ifcfg-eth0:0',
306 """
307# Created by cloud-init on instance boot automatically, do not edit.
308#
309BOOTPROTO=static
310DEFROUTE=yes288DEFROUTE=yes
311DEVICE=eth0:0289DEVICE=eth0
312GATEWAY=172.19.3.254290GATEWAY=172.19.3.254
313HWADDR=fa:16:3e:ed:9a:59291HWADDR=fa:16:3e:ed:9a:59
314IPADDR=172.19.1.34292IPADDR=172.19.1.34
315NETMASK=255.255.252.0
316NM_CONTROLLED=no
317ONBOOT=yes
318TYPE=Ethernet
319USERCTL=no
320""".lstrip()),
321 ('etc/sysconfig/network-scripts/ifcfg-eth0:1',
322 """
323# Created by cloud-init on instance boot automatically, do not edit.
324#
325BOOTPROTO=static
326DEFROUTE=yes
327DEVICE=eth0:1
328HWADDR=fa:16:3e:ed:9a:59
329IPV6ADDR=2001:DB8::10293IPV6ADDR=2001:DB8::10
294IPV6ADDR_SECONDARIES="2001:DB9::10/64 2001:DB10::10/64"
330IPV6INIT=yes295IPV6INIT=yes
331IPV6_DEFAULTGW=2001:DB8::1296IPV6_DEFAULTGW=2001:DB8::1
332NETMASK=297NETMASK=255.255.252.0
333NM_CONTROLLED=no298NM_CONTROLLED=no
334ONBOOT=yes299ONBOOT=yes
335TYPE=Ethernet300TYPE=Ethernet

Subscribers

People subscribed via source and target branches