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