|
|
20a859 |
From 8741bba1983532e6aefa78f350fdae91b8b151a1 Mon Sep 17 00:00:00 2001
|
|
|
20a859 |
From: Andreas Karis <akaris@redhat.com>
|
|
|
20a859 |
Date: Fri, 21 Apr 2017 20:35:39 -0400
|
|
|
20a859 |
Subject: [PATCH] Fix dual stack IPv4/IPv6 configuration for RHEL
|
|
|
20a859 |
|
|
|
20a859 |
Dual stack IPv4/IPv6 configuration via config drive is broken for RHEL7.
|
|
|
20a859 |
This patch fixes several scenarios for IPv4/IPv6/dual stack with multiple IP assignment
|
|
|
20a859 |
Removes unpopular IPv4 alias files and invalid IPv6 alias files
|
|
|
20a859 |
|
|
|
20a859 |
Also fixes associated unit tests
|
|
|
20a859 |
|
|
|
20a859 |
LP: #1679817
|
|
|
20a859 |
LP: #1685534
|
|
|
20a859 |
LP: #1685532
|
|
|
20a859 |
|
|
|
20a859 |
Resolves: rhbz#1438082
|
|
|
20a859 |
X-approved-upstream: true
|
|
|
20a859 |
---
|
|
|
20a859 |
cloudinit/net/sysconfig.py | 244 ++++++++++++++++++-------
|
|
|
20a859 |
tests/unittests/test_distros/test_netconfig.py | 139 +++++++++++++-
|
|
|
20a859 |
tests/unittests/test_net.py | 144 ++++++++++++++-
|
|
|
20a859 |
3 files changed, 455 insertions(+), 72 deletions(-)
|
|
|
20a859 |
|
|
|
20a859 |
diff --git a/cloudinit/net/sysconfig.py b/cloudinit/net/sysconfig.py
|
|
|
20a859 |
index d521d5c..240ed23 100644
|
|
|
20a859 |
--- a/cloudinit/net/sysconfig.py
|
|
|
20a859 |
+++ b/cloudinit/net/sysconfig.py
|
|
|
20a859 |
@@ -58,6 +58,9 @@ class ConfigMap(object):
|
|
|
20a859 |
def __setitem__(self, key, value):
|
|
|
20a859 |
self._conf[key] = value
|
|
|
20a859 |
|
|
|
20a859 |
+ def __getitem__(self, key):
|
|
|
20a859 |
+ return self._conf[key]
|
|
|
20a859 |
+
|
|
|
20a859 |
def drop(self, key):
|
|
|
20a859 |
self._conf.pop(key, None)
|
|
|
20a859 |
|
|
|
20a859 |
@@ -82,7 +85,8 @@ class ConfigMap(object):
|
|
|
20a859 |
class Route(ConfigMap):
|
|
|
20a859 |
"""Represents a route configuration."""
|
|
|
20a859 |
|
|
|
20a859 |
- route_fn_tpl = '%(base)s/network-scripts/route-%(name)s'
|
|
|
20a859 |
+ route_fn_tpl_ipv4 = '%(base)s/network-scripts/route-%(name)s'
|
|
|
20a859 |
+ route_fn_tpl_ipv6 = '%(base)s/network-scripts/route6-%(name)s'
|
|
|
20a859 |
|
|
|
20a859 |
def __init__(self, route_name, base_sysconf_dir):
|
|
|
20a859 |
super(Route, self).__init__()
|
|
|
20a859 |
@@ -101,9 +105,58 @@ class Route(ConfigMap):
|
|
|
20a859 |
return r
|
|
|
20a859 |
|
|
|
20a859 |
@property
|
|
|
20a859 |
- def path(self):
|
|
|
20a859 |
- return self.route_fn_tpl % ({'base': self._base_sysconf_dir,
|
|
|
20a859 |
- 'name': self._route_name})
|
|
|
20a859 |
+ def path_ipv4(self):
|
|
|
20a859 |
+ return self.route_fn_tpl_ipv4 % ({'base': self._base_sysconf_dir,
|
|
|
20a859 |
+ 'name': self._route_name})
|
|
|
20a859 |
+
|
|
|
20a859 |
+ @property
|
|
|
20a859 |
+ def path_ipv6(self):
|
|
|
20a859 |
+ return self.route_fn_tpl_ipv6 % ({'base': self._base_sysconf_dir,
|
|
|
20a859 |
+ 'name': self._route_name})
|
|
|
20a859 |
+
|
|
|
20a859 |
+ def is_ipv6_route(self, address):
|
|
|
20a859 |
+ return ':' in address
|
|
|
20a859 |
+
|
|
|
20a859 |
+ def to_string(self, proto="ipv4"):
|
|
|
20a859 |
+ # only accept ipv4 and ipv6
|
|
|
20a859 |
+ if proto not in ['ipv4', 'ipv6']:
|
|
|
20a859 |
+ raise ValueError("Unknown protocol '%s'" % (str(proto)))
|
|
|
20a859 |
+ buf = six.StringIO()
|
|
|
20a859 |
+ buf.write(_make_header())
|
|
|
20a859 |
+ if self._conf:
|
|
|
20a859 |
+ buf.write("\n")
|
|
|
20a859 |
+ # need to reindex IPv4 addresses
|
|
|
20a859 |
+ # (because Route can contain a mix of IPv4 and IPv6)
|
|
|
20a859 |
+ reindex = -1
|
|
|
20a859 |
+ for key in sorted(self._conf.keys()):
|
|
|
20a859 |
+ if 'ADDRESS' in key:
|
|
|
20a859 |
+ index = key.replace('ADDRESS', '')
|
|
|
20a859 |
+ address_value = str(self._conf[key])
|
|
|
20a859 |
+ # only accept combinations:
|
|
|
20a859 |
+ # if proto ipv6 only display ipv6 routes
|
|
|
20a859 |
+ # if proto ipv4 only display ipv4 routes
|
|
|
20a859 |
+ # do not add ipv6 routes if proto is ipv4
|
|
|
20a859 |
+ # do not add ipv4 routes if proto is ipv6
|
|
|
20a859 |
+ # (this array will contain a mix of ipv4 and ipv6)
|
|
|
20a859 |
+ if proto == "ipv4" and not self.is_ipv6_route(address_value):
|
|
|
20a859 |
+ netmask_value = str(self._conf['NETMASK' + index])
|
|
|
20a859 |
+ gateway_value = str(self._conf['GATEWAY' + index])
|
|
|
20a859 |
+ # increase IPv4 index
|
|
|
20a859 |
+ reindex = reindex + 1
|
|
|
20a859 |
+ buf.write("%s=%s\n" % ('ADDRESS' + str(reindex),
|
|
|
20a859 |
+ _quote_value(address_value)))
|
|
|
20a859 |
+ buf.write("%s=%s\n" % ('GATEWAY' + str(reindex),
|
|
|
20a859 |
+ _quote_value(gateway_value)))
|
|
|
20a859 |
+ buf.write("%s=%s\n" % ('NETMASK' + str(reindex),
|
|
|
20a859 |
+ _quote_value(netmask_value)))
|
|
|
20a859 |
+ elif proto == "ipv6" and self.is_ipv6_route(address_value):
|
|
|
20a859 |
+ netmask_value = str(self._conf['NETMASK' + index])
|
|
|
20a859 |
+ gateway_value = str(self._conf['GATEWAY' + index])
|
|
|
20a859 |
+ buf.write("%s/%s via %s\n" % (address_value,
|
|
|
20a859 |
+ netmask_value,
|
|
|
20a859 |
+ gateway_value))
|
|
|
20a859 |
+
|
|
|
20a859 |
+ return buf.getvalue()
|
|
|
20a859 |
|
|
|
20a859 |
|
|
|
20a859 |
class NetInterface(ConfigMap):
|
|
|
20a859 |
@@ -209,65 +262,119 @@ class Renderer(renderer.Renderer):
|
|
|
20a859 |
iface_cfg[new_key] = old_value
|
|
|
20a859 |
|
|
|
20a859 |
@classmethod
|
|
|
20a859 |
- def _render_subnet(cls, iface_cfg, route_cfg, subnet):
|
|
|
20a859 |
- subnet_type = subnet.get('type')
|
|
|
20a859 |
- if subnet_type == 'dhcp6':
|
|
|
20a859 |
- iface_cfg['DHCPV6C'] = True
|
|
|
20a859 |
- iface_cfg['IPV6INIT'] = True
|
|
|
20a859 |
- iface_cfg['BOOTPROTO'] = 'dhcp'
|
|
|
20a859 |
- elif subnet_type in ['dhcp4', 'dhcp']:
|
|
|
20a859 |
- iface_cfg['BOOTPROTO'] = 'dhcp'
|
|
|
20a859 |
- elif subnet_type == 'static':
|
|
|
20a859 |
- iface_cfg['BOOTPROTO'] = 'static'
|
|
|
20a859 |
- if subnet.get('ipv6'):
|
|
|
20a859 |
- iface_cfg['IPV6ADDR'] = subnet['address']
|
|
|
20a859 |
+ def _render_subnets(cls, iface_cfg, subnets):
|
|
|
20a859 |
+ # setting base values
|
|
|
20a859 |
+ iface_cfg['BOOTPROTO'] = 'none'
|
|
|
20a859 |
+
|
|
|
20a859 |
+ # modifying base values according to subnets
|
|
|
20a859 |
+ for i, subnet in enumerate(subnets, start=len(iface_cfg.children)):
|
|
|
20a859 |
+ subnet_type = subnet.get('type')
|
|
|
20a859 |
+ if subnet_type == 'dhcp6':
|
|
|
20a859 |
iface_cfg['IPV6INIT'] = True
|
|
|
20a859 |
+ iface_cfg['DHCPV6C'] = True
|
|
|
20a859 |
+ iface_cfg['BOOTPROTO'] = 'dhcp'
|
|
|
20a859 |
+ elif subnet_type in ['dhcp4', 'dhcp']:
|
|
|
20a859 |
+ iface_cfg['BOOTPROTO'] = 'dhcp'
|
|
|
20a859 |
+ elif subnet_type == 'static':
|
|
|
20a859 |
+ # grep BOOTPROTO sysconfig.txt -A2 | head -3
|
|
|
20a859 |
+ # BOOTPROTO=none|bootp|dhcp
|
|
|
20a859 |
+ # 'bootp' or 'dhcp' cause a DHCP client
|
|
|
20a859 |
+ # to run on the device. Any other
|
|
|
20a859 |
+ # value causes any static configuration
|
|
|
20a859 |
+ # in the file to be applied.
|
|
|
20a859 |
+ # ==> the following should not be set to 'static'
|
|
|
20a859 |
+ # but should remain 'none'
|
|
|
20a859 |
+ # if iface_cfg['BOOTPROTO'] == 'none':
|
|
|
20a859 |
+ # iface_cfg['BOOTPROTO'] = 'static'
|
|
|
20a859 |
+ if subnet.get('ipv6'):
|
|
|
20a859 |
+ iface_cfg['IPV6INIT'] = True
|
|
|
20a859 |
else:
|
|
|
20a859 |
- iface_cfg['IPADDR'] = subnet['address']
|
|
|
20a859 |
- else:
|
|
|
20a859 |
- raise ValueError("Unknown subnet type '%s' found"
|
|
|
20a859 |
- " for interface '%s'" % (subnet_type,
|
|
|
20a859 |
- iface_cfg.name))
|
|
|
20a859 |
- if 'netmask' in subnet:
|
|
|
20a859 |
- iface_cfg['NETMASK'] = subnet['netmask']
|
|
|
20a859 |
- is_ipv6 = subnet.get('ipv6')
|
|
|
20a859 |
- for route in subnet.get('routes', []):
|
|
|
20a859 |
- if _is_default_route(route):
|
|
|
20a859 |
- if (
|
|
|
20a859 |
- (subnet.get('ipv4') and
|
|
|
20a859 |
- route_cfg.has_set_default_ipv4) or
|
|
|
20a859 |
- (subnet.get('ipv6') and
|
|
|
20a859 |
- route_cfg.has_set_default_ipv6)
|
|
|
20a859 |
- ):
|
|
|
20a859 |
- raise ValueError("Duplicate declaration of default "
|
|
|
20a859 |
- "route found for interface '%s'"
|
|
|
20a859 |
- % (iface_cfg.name))
|
|
|
20a859 |
- # NOTE(harlowja): ipv6 and ipv4 default gateways
|
|
|
20a859 |
- gw_key = 'GATEWAY0'
|
|
|
20a859 |
- nm_key = 'NETMASK0'
|
|
|
20a859 |
- addr_key = 'ADDRESS0'
|
|
|
20a859 |
- # The owning interface provides the default route.
|
|
|
20a859 |
- #
|
|
|
20a859 |
- # TODO(harlowja): add validation that no other iface has
|
|
|
20a859 |
- # also provided the default route?
|
|
|
20a859 |
- iface_cfg['DEFROUTE'] = True
|
|
|
20a859 |
- if 'gateway' in route:
|
|
|
20a859 |
- if is_ipv6:
|
|
|
20a859 |
- iface_cfg['IPV6_DEFAULTGW'] = route['gateway']
|
|
|
20a859 |
- route_cfg.has_set_default_ipv6 = True
|
|
|
20a859 |
+ raise ValueError("Unknown subnet type '%s' found"
|
|
|
20a859 |
+ " for interface '%s'" % (subnet_type,
|
|
|
20a859 |
+ iface_cfg.name))
|
|
|
20a859 |
+
|
|
|
20a859 |
+ # set IPv4 and IPv6 static addresses
|
|
|
20a859 |
+ ipv4_index = -1
|
|
|
20a859 |
+ ipv6_index = -1
|
|
|
20a859 |
+ for i, subnet in enumerate(subnets, start=len(iface_cfg.children)):
|
|
|
20a859 |
+ subnet_type = subnet.get('type')
|
|
|
20a859 |
+ if subnet_type == 'dhcp6':
|
|
|
20a859 |
+ continue
|
|
|
20a859 |
+ elif subnet_type in ['dhcp4', 'dhcp']:
|
|
|
20a859 |
+ continue
|
|
|
20a859 |
+ elif subnet_type == 'static':
|
|
|
20a859 |
+ if subnet.get('ipv6'):
|
|
|
20a859 |
+ ipv6_index = ipv6_index + 1
|
|
|
20a859 |
+ if 'netmask' in subnet and str(subnet['netmask']) != "":
|
|
|
20a859 |
+ ipv6_cidr = (subnet['address'] +
|
|
|
20a859 |
+ '/' +
|
|
|
20a859 |
+ str(subnet['netmask']))
|
|
|
20a859 |
else:
|
|
|
20a859 |
- iface_cfg['GATEWAY'] = route['gateway']
|
|
|
20a859 |
- route_cfg.has_set_default_ipv4 = True
|
|
|
20a859 |
- else:
|
|
|
20a859 |
- gw_key = 'GATEWAY%s' % route_cfg.last_idx
|
|
|
20a859 |
- nm_key = 'NETMASK%s' % route_cfg.last_idx
|
|
|
20a859 |
- addr_key = 'ADDRESS%s' % route_cfg.last_idx
|
|
|
20a859 |
- route_cfg.last_idx += 1
|
|
|
20a859 |
- for (old_key, new_key) in [('gateway', gw_key),
|
|
|
20a859 |
- ('netmask', nm_key),
|
|
|
20a859 |
- ('network', addr_key)]:
|
|
|
20a859 |
- if old_key in route:
|
|
|
20a859 |
- route_cfg[new_key] = route[old_key]
|
|
|
20a859 |
+ ipv6_cidr = subnet['address']
|
|
|
20a859 |
+ if ipv6_index == 0:
|
|
|
20a859 |
+ iface_cfg['IPV6ADDR'] = ipv6_cidr
|
|
|
20a859 |
+ elif ipv6_index == 1:
|
|
|
20a859 |
+ iface_cfg['IPV6ADDR_SECONDARIES'] = ipv6_cidr
|
|
|
20a859 |
+ else:
|
|
|
20a859 |
+ iface_cfg['IPV6ADDR_SECONDARIES'] = (
|
|
|
20a859 |
+ iface_cfg['IPV6ADDR_SECONDARIES'] +
|
|
|
20a859 |
+ " " + ipv6_cidr)
|
|
|
20a859 |
+ else:
|
|
|
20a859 |
+ ipv4_index = ipv4_index + 1
|
|
|
20a859 |
+ if ipv4_index == 0:
|
|
|
20a859 |
+ iface_cfg['IPADDR'] = subnet['address']
|
|
|
20a859 |
+ if 'netmask' in subnet:
|
|
|
20a859 |
+ iface_cfg['NETMASK'] = subnet['netmask']
|
|
|
20a859 |
+ else:
|
|
|
20a859 |
+ iface_cfg['IPADDR' + str(ipv4_index)] = \
|
|
|
20a859 |
+ subnet['address']
|
|
|
20a859 |
+ if 'netmask' in subnet:
|
|
|
20a859 |
+ iface_cfg['NETMASK' + str(ipv4_index)] = \
|
|
|
20a859 |
+ subnet['netmask']
|
|
|
20a859 |
+
|
|
|
20a859 |
+ @classmethod
|
|
|
20a859 |
+ def _render_subnet_routes(cls, iface_cfg, route_cfg, subnets):
|
|
|
20a859 |
+ for i, subnet in enumerate(subnets, start=len(iface_cfg.children)):
|
|
|
20a859 |
+ for route in subnet.get('routes', []):
|
|
|
20a859 |
+ is_ipv6 = subnet.get('ipv6')
|
|
|
20a859 |
+
|
|
|
20a859 |
+ if _is_default_route(route):
|
|
|
20a859 |
+ if (
|
|
|
20a859 |
+ (subnet.get('ipv4') and
|
|
|
20a859 |
+ route_cfg.has_set_default_ipv4) or
|
|
|
20a859 |
+ (subnet.get('ipv6') and
|
|
|
20a859 |
+ route_cfg.has_set_default_ipv6)
|
|
|
20a859 |
+ ):
|
|
|
20a859 |
+ raise ValueError("Duplicate declaration of default "
|
|
|
20a859 |
+ "route found for interface '%s'"
|
|
|
20a859 |
+ % (iface_cfg.name))
|
|
|
20a859 |
+ # NOTE(harlowja): ipv6 and ipv4 default gateways
|
|
|
20a859 |
+ gw_key = 'GATEWAY0'
|
|
|
20a859 |
+ nm_key = 'NETMASK0'
|
|
|
20a859 |
+ addr_key = 'ADDRESS0'
|
|
|
20a859 |
+ # The owning interface provides the default route.
|
|
|
20a859 |
+ #
|
|
|
20a859 |
+ # TODO(harlowja): add validation that no other iface has
|
|
|
20a859 |
+ # also provided the default route?
|
|
|
20a859 |
+ iface_cfg['DEFROUTE'] = True
|
|
|
20a859 |
+ if 'gateway' in route:
|
|
|
20a859 |
+ if is_ipv6:
|
|
|
20a859 |
+ iface_cfg['IPV6_DEFAULTGW'] = route['gateway']
|
|
|
20a859 |
+ route_cfg.has_set_default_ipv6 = True
|
|
|
20a859 |
+ else:
|
|
|
20a859 |
+ iface_cfg['GATEWAY'] = route['gateway']
|
|
|
20a859 |
+ route_cfg.has_set_default_ipv4 = True
|
|
|
20a859 |
+
|
|
|
20a859 |
+ else:
|
|
|
20a859 |
+ gw_key = 'GATEWAY%s' % route_cfg.last_idx
|
|
|
20a859 |
+ nm_key = 'NETMASK%s' % route_cfg.last_idx
|
|
|
20a859 |
+ addr_key = 'ADDRESS%s' % route_cfg.last_idx
|
|
|
20a859 |
+ route_cfg.last_idx += 1
|
|
|
20a859 |
+ for (old_key, new_key) in [('gateway', gw_key),
|
|
|
20a859 |
+ ('netmask', nm_key),
|
|
|
20a859 |
+ ('network', addr_key)]:
|
|
|
20a859 |
+ if old_key in route:
|
|
|
20a859 |
+ route_cfg[new_key] = route[old_key]
|
|
|
20a859 |
|
|
|
20a859 |
@classmethod
|
|
|
20a859 |
def _render_bonding_opts(cls, iface_cfg, iface):
|
|
|
20a859 |
@@ -293,15 +400,9 @@ class Renderer(renderer.Renderer):
|
|
|
20a859 |
iface_subnets = iface.get("subnets", [])
|
|
|
20a859 |
iface_cfg = iface_contents[iface_name]
|
|
|
20a859 |
route_cfg = iface_cfg.routes
|
|
|
20a859 |
- if len(iface_subnets) == 1:
|
|
|
20a859 |
- cls._render_subnet(iface_cfg, route_cfg, iface_subnets[0])
|
|
|
20a859 |
- elif len(iface_subnets) > 1:
|
|
|
20a859 |
- for i, iface_subnet in enumerate(iface_subnets,
|
|
|
20a859 |
- start=len(iface_cfg.children)):
|
|
|
20a859 |
- iface_sub_cfg = iface_cfg.copy()
|
|
|
20a859 |
- iface_sub_cfg.name = "%s:%s" % (iface_name, i)
|
|
|
20a859 |
- iface_cfg.children.append(iface_sub_cfg)
|
|
|
20a859 |
- cls._render_subnet(iface_sub_cfg, route_cfg, iface_subnet)
|
|
|
20a859 |
+
|
|
|
20a859 |
+ cls._render_subnets(iface_cfg, iface_subnets)
|
|
|
20a859 |
+ cls._render_subnet_routes(iface_cfg, route_cfg, iface_subnets)
|
|
|
20a859 |
|
|
|
20a859 |
@classmethod
|
|
|
20a859 |
def _render_bond_interfaces(cls, network_state, iface_contents):
|
|
|
20a859 |
@@ -383,7 +484,10 @@ class Renderer(renderer.Renderer):
|
|
|
20a859 |
if iface_cfg:
|
|
|
20a859 |
contents[iface_cfg.path] = iface_cfg.to_string()
|
|
|
20a859 |
if iface_cfg.routes:
|
|
|
20a859 |
- contents[iface_cfg.routes.path] = iface_cfg.routes.to_string()
|
|
|
20a859 |
+ contents[iface_cfg.routes.path_ipv4] = \
|
|
|
20a859 |
+ iface_cfg.routes.to_string("ipv4")
|
|
|
20a859 |
+ contents[iface_cfg.routes.path_ipv6] = \
|
|
|
20a859 |
+ iface_cfg.routes.to_string("ipv6")
|
|
|
20a859 |
return contents
|
|
|
20a859 |
|
|
|
20a859 |
def render_network_state(self, target, network_state):
|
|
|
20a859 |
diff --git a/tests/unittests/test_distros/test_netconfig.py b/tests/unittests/test_distros/test_netconfig.py
|
|
|
20a859 |
index bde3bb5..85982cf 100644
|
|
|
20a859 |
--- a/tests/unittests/test_distros/test_netconfig.py
|
|
|
20a859 |
+++ b/tests/unittests/test_distros/test_netconfig.py
|
|
|
20a859 |
@@ -195,6 +195,76 @@ NETWORKING=yes
|
|
|
20a859 |
self.assertCfgEquals(expected_buf, str(write_buf))
|
|
|
20a859 |
self.assertEqual(write_buf.mode, 0o644)
|
|
|
20a859 |
|
|
|
20a859 |
+ def test_apply_network_config_rh(self):
|
|
|
20a859 |
+ rh_distro = self._get_distro('rhel')
|
|
|
20a859 |
+
|
|
|
20a859 |
+ write_bufs = {}
|
|
|
20a859 |
+
|
|
|
20a859 |
+ def replace_write(filename, content, mode=0o644, omode="wb"):
|
|
|
20a859 |
+ buf = WriteBuffer()
|
|
|
20a859 |
+ buf.mode = mode
|
|
|
20a859 |
+ buf.omode = omode
|
|
|
20a859 |
+ buf.write(content)
|
|
|
20a859 |
+ write_bufs[filename] = buf
|
|
|
20a859 |
+
|
|
|
20a859 |
+ with ExitStack() as mocks:
|
|
|
20a859 |
+ # sysconfig availability checks
|
|
|
20a859 |
+ mocks.enter_context(
|
|
|
20a859 |
+ mock.patch.object(util, 'which', return_value=True))
|
|
|
20a859 |
+ mocks.enter_context(
|
|
|
20a859 |
+ mock.patch.object(util, 'write_file', replace_write))
|
|
|
20a859 |
+ mocks.enter_context(
|
|
|
20a859 |
+ mock.patch.object(util, 'load_file', return_value=''))
|
|
|
20a859 |
+ mocks.enter_context(
|
|
|
20a859 |
+ mock.patch.object(os.path, 'isfile', return_value=True))
|
|
|
20a859 |
+
|
|
|
20a859 |
+ rh_distro.apply_network_config(V1_NET_CFG, False)
|
|
|
20a859 |
+
|
|
|
20a859 |
+ self.assertEqual(len(write_bufs), 5)
|
|
|
20a859 |
+
|
|
|
20a859 |
+ # eth0
|
|
|
20a859 |
+ self.assertIn('/etc/sysconfig/network-scripts/ifcfg-eth0',
|
|
|
20a859 |
+ write_bufs)
|
|
|
20a859 |
+ write_buf = write_bufs['/etc/sysconfig/network-scripts/ifcfg-eth0']
|
|
|
20a859 |
+ expected_buf = '''
|
|
|
20a859 |
+# Created by cloud-init on instance boot automatically, do not edit.
|
|
|
20a859 |
+#
|
|
|
20a859 |
+BOOTPROTO=none
|
|
|
20a859 |
+DEVICE=eth0
|
|
|
20a859 |
+IPADDR=192.168.1.5
|
|
|
20a859 |
+NETMASK=255.255.255.0
|
|
|
20a859 |
+ONBOOT=yes
|
|
|
20a859 |
+TYPE=Ethernet
|
|
|
20a859 |
+USERCTL=no
|
|
|
20a859 |
+'''
|
|
|
20a859 |
+ self.assertCfgEquals(expected_buf, str(write_buf))
|
|
|
20a859 |
+ self.assertEqual(write_buf.mode, 0o644)
|
|
|
20a859 |
+
|
|
|
20a859 |
+ # eth1
|
|
|
20a859 |
+ self.assertIn('/etc/sysconfig/network-scripts/ifcfg-eth1',
|
|
|
20a859 |
+ write_bufs)
|
|
|
20a859 |
+ write_buf = write_bufs['/etc/sysconfig/network-scripts/ifcfg-eth1']
|
|
|
20a859 |
+ expected_buf = '''
|
|
|
20a859 |
+# Created by cloud-init on instance boot automatically, do not edit.
|
|
|
20a859 |
+#
|
|
|
20a859 |
+BOOTPROTO=dhcp
|
|
|
20a859 |
+DEVICE=eth1
|
|
|
20a859 |
+ONBOOT=yes
|
|
|
20a859 |
+TYPE=Ethernet
|
|
|
20a859 |
+USERCTL=no
|
|
|
20a859 |
+'''
|
|
|
20a859 |
+ self.assertCfgEquals(expected_buf, str(write_buf))
|
|
|
20a859 |
+ self.assertEqual(write_buf.mode, 0o644)
|
|
|
20a859 |
+
|
|
|
20a859 |
+ self.assertIn('/etc/sysconfig/network', write_bufs)
|
|
|
20a859 |
+ write_buf = write_bufs['/etc/sysconfig/network']
|
|
|
20a859 |
+ expected_buf = '''
|
|
|
20a859 |
+# Created by cloud-init v. 0.7
|
|
|
20a859 |
+NETWORKING=yes
|
|
|
20a859 |
+'''
|
|
|
20a859 |
+ self.assertCfgEquals(expected_buf, str(write_buf))
|
|
|
20a859 |
+ self.assertEqual(write_buf.mode, 0o644)
|
|
|
20a859 |
+
|
|
|
20a859 |
def test_write_ipv6_rhel(self):
|
|
|
20a859 |
rh_distro = self._get_distro('rhel')
|
|
|
20a859 |
|
|
|
20a859 |
@@ -214,7 +284,6 @@ NETWORKING=yes
|
|
|
20a859 |
mock.patch.object(util, 'load_file', return_value=''))
|
|
|
20a859 |
mocks.enter_context(
|
|
|
20a859 |
mock.patch.object(os.path, 'isfile', return_value=False))
|
|
|
20a859 |
-
|
|
|
20a859 |
rh_distro.apply_network(BASE_NET_CFG_IPV6, False)
|
|
|
20a859 |
|
|
|
20a859 |
self.assertEqual(len(write_bufs), 4)
|
|
|
20a859 |
@@ -274,6 +343,74 @@ IPV6_AUTOCONF=no
|
|
|
20a859 |
self.assertCfgEquals(expected_buf, str(write_buf))
|
|
|
20a859 |
self.assertEqual(write_buf.mode, 0o644)
|
|
|
20a859 |
|
|
|
20a859 |
+ def test_apply_network_config_ipv6_rh(self):
|
|
|
20a859 |
+ rh_distro = self._get_distro('rhel')
|
|
|
20a859 |
+
|
|
|
20a859 |
+ write_bufs = {}
|
|
|
20a859 |
+
|
|
|
20a859 |
+ def replace_write(filename, content, mode=0o644, omode="wb"):
|
|
|
20a859 |
+ buf = WriteBuffer()
|
|
|
20a859 |
+ buf.mode = mode
|
|
|
20a859 |
+ buf.omode = omode
|
|
|
20a859 |
+ buf.write(content)
|
|
|
20a859 |
+ write_bufs[filename] = buf
|
|
|
20a859 |
+
|
|
|
20a859 |
+ with ExitStack() as mocks:
|
|
|
20a859 |
+ mocks.enter_context(
|
|
|
20a859 |
+ mock.patch.object(util, 'which', return_value=True))
|
|
|
20a859 |
+ mocks.enter_context(
|
|
|
20a859 |
+ mock.patch.object(util, 'write_file', replace_write))
|
|
|
20a859 |
+ mocks.enter_context(
|
|
|
20a859 |
+ mock.patch.object(util, 'load_file', return_value=''))
|
|
|
20a859 |
+ mocks.enter_context(
|
|
|
20a859 |
+ mock.patch.object(os.path, 'isfile', return_value=True))
|
|
|
20a859 |
+
|
|
|
20a859 |
+ rh_distro.apply_network_config(V1_NET_CFG_IPV6, False)
|
|
|
20a859 |
+
|
|
|
20a859 |
+ self.assertEqual(len(write_bufs), 5)
|
|
|
20a859 |
+
|
|
|
20a859 |
+ self.assertIn('/etc/sysconfig/network-scripts/ifcfg-eth0',
|
|
|
20a859 |
+ write_bufs)
|
|
|
20a859 |
+ write_buf = write_bufs['/etc/sysconfig/network-scripts/ifcfg-eth0']
|
|
|
20a859 |
+ expected_buf = '''
|
|
|
20a859 |
+# Created by cloud-init on instance boot automatically, do not edit.
|
|
|
20a859 |
+#
|
|
|
20a859 |
+BOOTPROTO=none
|
|
|
20a859 |
+DEVICE=eth0
|
|
|
20a859 |
+IPV6ADDR=2607:f0d0:1002:0011::2/64
|
|
|
20a859 |
+IPV6INIT=yes
|
|
|
20a859 |
+ONBOOT=yes
|
|
|
20a859 |
+TYPE=Ethernet
|
|
|
20a859 |
+USERCTL=no
|
|
|
20a859 |
+'''
|
|
|
20a859 |
+ self.assertCfgEquals(expected_buf, str(write_buf))
|
|
|
20a859 |
+ self.assertEqual(write_buf.mode, 0o644)
|
|
|
20a859 |
+ self.assertIn('/etc/sysconfig/network-scripts/ifcfg-eth1',
|
|
|
20a859 |
+ write_bufs)
|
|
|
20a859 |
+ write_buf = write_bufs['/etc/sysconfig/network-scripts/ifcfg-eth1']
|
|
|
20a859 |
+ expected_buf = '''
|
|
|
20a859 |
+# Created by cloud-init on instance boot automatically, do not edit.
|
|
|
20a859 |
+#
|
|
|
20a859 |
+BOOTPROTO=dhcp
|
|
|
20a859 |
+DEVICE=eth1
|
|
|
20a859 |
+ONBOOT=yes
|
|
|
20a859 |
+TYPE=Ethernet
|
|
|
20a859 |
+USERCTL=no
|
|
|
20a859 |
+'''
|
|
|
20a859 |
+ self.assertCfgEquals(expected_buf, str(write_buf))
|
|
|
20a859 |
+ self.assertEqual(write_buf.mode, 0o644)
|
|
|
20a859 |
+
|
|
|
20a859 |
+ self.assertIn('/etc/sysconfig/network', write_bufs)
|
|
|
20a859 |
+ write_buf = write_bufs['/etc/sysconfig/network']
|
|
|
20a859 |
+ expected_buf = '''
|
|
|
20a859 |
+# Created by cloud-init v. 0.7
|
|
|
20a859 |
+NETWORKING=yes
|
|
|
20a859 |
+NETWORKING_IPV6=yes
|
|
|
20a859 |
+IPV6_AUTOCONF=no
|
|
|
20a859 |
+'''
|
|
|
20a859 |
+ self.assertCfgEquals(expected_buf, str(write_buf))
|
|
|
20a859 |
+ self.assertEqual(write_buf.mode, 0o644)
|
|
|
20a859 |
+
|
|
|
20a859 |
def test_simple_write_freebsd(self):
|
|
|
20a859 |
fbsd_distro = self._get_distro('freebsd')
|
|
|
20a859 |
|
|
|
20a859 |
diff --git a/tests/unittests/test_net.py b/tests/unittests/test_net.py
|
|
|
20a859 |
index 262c6d5..172d604 100755
|
|
|
20a859 |
--- a/tests/unittests/test_net.py
|
|
|
20a859 |
+++ b/tests/unittests/test_net.py
|
|
|
20a859 |
@@ -137,7 +137,7 @@ OS_SAMPLES = [
|
|
|
20a859 |
"""
|
|
|
20a859 |
# Created by cloud-init on instance boot automatically, do not edit.
|
|
|
20a859 |
#
|
|
|
20a859 |
-BOOTPROTO=static
|
|
|
20a859 |
+BOOTPROTO=none
|
|
|
20a859 |
DEFROUTE=yes
|
|
|
20a859 |
DEVICE=eth0
|
|
|
20a859 |
GATEWAY=172.19.3.254
|
|
|
20a859 |
@@ -165,6 +165,148 @@ nameserver 172.19.0.12
|
|
|
20a859 |
('etc/udev/rules.d/70-persistent-net.rules',
|
|
|
20a859 |
"".join(['SUBSYSTEM=="net", ACTION=="add", DRIVERS=="?*", ',
|
|
|
20a859 |
'ATTR{address}=="fa:16:3e:ed:9a:59", NAME="eth0"\n']))]
|
|
|
20a859 |
+ },
|
|
|
20a859 |
+ {
|
|
|
20a859 |
+ 'in_data': {
|
|
|
20a859 |
+ "services": [{"type": "dns", "address": "172.19.0.12"}],
|
|
|
20a859 |
+ "networks": [{
|
|
|
20a859 |
+ "network_id": "public-ipv4",
|
|
|
20a859 |
+ "type": "ipv4", "netmask": "255.255.252.0",
|
|
|
20a859 |
+ "link": "tap1a81968a-79",
|
|
|
20a859 |
+ "routes": [{
|
|
|
20a859 |
+ "netmask": "0.0.0.0",
|
|
|
20a859 |
+ "network": "0.0.0.0",
|
|
|
20a859 |
+ "gateway": "172.19.3.254",
|
|
|
20a859 |
+ }],
|
|
|
20a859 |
+ "ip_address": "172.19.1.34", "id": "network0"
|
|
|
20a859 |
+ }, {
|
|
|
20a859 |
+ "network_id": "private-ipv4",
|
|
|
20a859 |
+ "type": "ipv4", "netmask": "255.255.255.0",
|
|
|
20a859 |
+ "link": "tap1a81968a-79",
|
|
|
20a859 |
+ "routes": [],
|
|
|
20a859 |
+ "ip_address": "10.0.0.10", "id": "network1"
|
|
|
20a859 |
+ }],
|
|
|
20a859 |
+ "links": [
|
|
|
20a859 |
+ {
|
|
|
20a859 |
+ "ethernet_mac_address": "fa:16:3e:ed:9a:59",
|
|
|
20a859 |
+ "mtu": None, "type": "bridge", "id":
|
|
|
20a859 |
+ "tap1a81968a-79",
|
|
|
20a859 |
+ "vif_id": "1a81968a-797a-400f-8a80-567f997eb93f"
|
|
|
20a859 |
+ },
|
|
|
20a859 |
+ ],
|
|
|
20a859 |
+ },
|
|
|
20a859 |
+ 'in_macs': {
|
|
|
20a859 |
+ 'fa:16:3e:ed:9a:59': 'eth0',
|
|
|
20a859 |
+ },
|
|
|
20a859 |
+ 'out_sysconfig': [
|
|
|
20a859 |
+ ('etc/sysconfig/network-scripts/ifcfg-eth0',
|
|
|
20a859 |
+ """
|
|
|
20a859 |
+# Created by cloud-init on instance boot automatically, do not edit.
|
|
|
20a859 |
+#
|
|
|
20a859 |
+BOOTPROTO=none
|
|
|
20a859 |
+DEFROUTE=yes
|
|
|
20a859 |
+DEVICE=eth0
|
|
|
20a859 |
+GATEWAY=172.19.3.254
|
|
|
20a859 |
+HWADDR=fa:16:3e:ed:9a:59
|
|
|
20a859 |
+IPADDR=172.19.1.34
|
|
|
20a859 |
+IPADDR1=10.0.0.10
|
|
|
20a859 |
+NETMASK=255.255.252.0
|
|
|
20a859 |
+NETMASK1=255.255.255.0
|
|
|
20a859 |
+ONBOOT=yes
|
|
|
20a859 |
+TYPE=Ethernet
|
|
|
20a859 |
+USERCTL=no
|
|
|
20a859 |
+""".lstrip()),
|
|
|
20a859 |
+ ('etc/resolv.conf',
|
|
|
20a859 |
+ """
|
|
|
20a859 |
+; Created by cloud-init on instance boot automatically, do not edit.
|
|
|
20a859 |
+;
|
|
|
20a859 |
+nameserver 172.19.0.12
|
|
|
20a859 |
+""".lstrip()),
|
|
|
20a859 |
+ ('etc/udev/rules.d/70-persistent-net.rules',
|
|
|
20a859 |
+ "".join(['SUBSYSTEM=="net", ACTION=="add", DRIVERS=="?*", ',
|
|
|
20a859 |
+ 'ATTR{address}=="fa:16:3e:ed:9a:59", NAME="eth0"\n']))]
|
|
|
20a859 |
+ },
|
|
|
20a859 |
+ {
|
|
|
20a859 |
+ 'in_data': {
|
|
|
20a859 |
+ "services": [{"type": "dns", "address": "172.19.0.12"}],
|
|
|
20a859 |
+ "networks": [{
|
|
|
20a859 |
+ "network_id": "public-ipv4",
|
|
|
20a859 |
+ "type": "ipv4", "netmask": "255.255.252.0",
|
|
|
20a859 |
+ "link": "tap1a81968a-79",
|
|
|
20a859 |
+ "routes": [{
|
|
|
20a859 |
+ "netmask": "0.0.0.0",
|
|
|
20a859 |
+ "network": "0.0.0.0",
|
|
|
20a859 |
+ "gateway": "172.19.3.254",
|
|
|
20a859 |
+ }],
|
|
|
20a859 |
+ "ip_address": "172.19.1.34", "id": "network0"
|
|
|
20a859 |
+ }, {
|
|
|
20a859 |
+ "network_id": "public-ipv6-a",
|
|
|
20a859 |
+ "type": "ipv6", "netmask": "",
|
|
|
20a859 |
+ "link": "tap1a81968a-79",
|
|
|
20a859 |
+ "routes": [
|
|
|
20a859 |
+ {
|
|
|
20a859 |
+ "gateway": "2001:DB8::1",
|
|
|
20a859 |
+ "netmask": "::",
|
|
|
20a859 |
+ "network": "::"
|
|
|
20a859 |
+ }
|
|
|
20a859 |
+ ],
|
|
|
20a859 |
+ "ip_address": "2001:DB8::10", "id": "network1"
|
|
|
20a859 |
+ }, {
|
|
|
20a859 |
+ "network_id": "public-ipv6-b",
|
|
|
20a859 |
+ "type": "ipv6", "netmask": "64",
|
|
|
20a859 |
+ "link": "tap1a81968a-79",
|
|
|
20a859 |
+ "routes": [
|
|
|
20a859 |
+ ],
|
|
|
20a859 |
+ "ip_address": "2001:DB9::10", "id": "network2"
|
|
|
20a859 |
+ }, {
|
|
|
20a859 |
+ "network_id": "public-ipv6-c",
|
|
|
20a859 |
+ "type": "ipv6", "netmask": "64",
|
|
|
20a859 |
+ "link": "tap1a81968a-79",
|
|
|
20a859 |
+ "routes": [
|
|
|
20a859 |
+ ],
|
|
|
20a859 |
+ "ip_address": "2001:DB10::10", "id": "network3"
|
|
|
20a859 |
+ }],
|
|
|
20a859 |
+ "links": [
|
|
|
20a859 |
+ {
|
|
|
20a859 |
+ "ethernet_mac_address": "fa:16:3e:ed:9a:59",
|
|
|
20a859 |
+ "mtu": None, "type": "bridge", "id":
|
|
|
20a859 |
+ "tap1a81968a-79",
|
|
|
20a859 |
+ "vif_id": "1a81968a-797a-400f-8a80-567f997eb93f"
|
|
|
20a859 |
+ },
|
|
|
20a859 |
+ ],
|
|
|
20a859 |
+ },
|
|
|
20a859 |
+ 'in_macs': {
|
|
|
20a859 |
+ 'fa:16:3e:ed:9a:59': 'eth0',
|
|
|
20a859 |
+ },
|
|
|
20a859 |
+ 'out_sysconfig': [
|
|
|
20a859 |
+ ('etc/sysconfig/network-scripts/ifcfg-eth0',
|
|
|
20a859 |
+ """
|
|
|
20a859 |
+# Created by cloud-init on instance boot automatically, do not edit.
|
|
|
20a859 |
+#
|
|
|
20a859 |
+BOOTPROTO=none
|
|
|
20a859 |
+DEFROUTE=yes
|
|
|
20a859 |
+DEVICE=eth0
|
|
|
20a859 |
+GATEWAY=172.19.3.254
|
|
|
20a859 |
+HWADDR=fa:16:3e:ed:9a:59
|
|
|
20a859 |
+IPADDR=172.19.1.34
|
|
|
20a859 |
+IPV6ADDR=2001:DB8::10
|
|
|
20a859 |
+IPV6ADDR_SECONDARIES="2001:DB9::10/64 2001:DB10::10/64"
|
|
|
20a859 |
+IPV6INIT=yes
|
|
|
20a859 |
+IPV6_DEFAULTGW=2001:DB8::1
|
|
|
20a859 |
+NETMASK=255.255.252.0
|
|
|
20a859 |
+ONBOOT=yes
|
|
|
20a859 |
+TYPE=Ethernet
|
|
|
20a859 |
+USERCTL=no
|
|
|
20a859 |
+""".lstrip()),
|
|
|
20a859 |
+ ('etc/resolv.conf',
|
|
|
20a859 |
+ """
|
|
|
20a859 |
+; Created by cloud-init on instance boot automatically, do not edit.
|
|
|
20a859 |
+;
|
|
|
20a859 |
+nameserver 172.19.0.12
|
|
|
20a859 |
+""".lstrip()),
|
|
|
20a859 |
+ ('etc/udev/rules.d/70-persistent-net.rules',
|
|
|
20a859 |
+ "".join(['SUBSYSTEM=="net", ACTION=="add", DRIVERS=="?*", ',
|
|
|
20a859 |
+ 'ATTR{address}=="fa:16:3e:ed:9a:59", NAME="eth0"\n']))]
|
|
|
20a859 |
}
|
|
|
20a859 |
]
|
|
|
20a859 |
|