Blob Blame History Raw
From f14ed869e5784b1d5a3dfbabc1484eb266e8c3ab Mon Sep 17 00:00:00 2001
From: Eduardo Otubo <otubo@redhat.com>
Date: Mon, 4 May 2020 12:40:13 +0200
Subject: [PATCH 6/6] net: IPv6, accept_ra, slaac, stateless (#51)

RH-Author: Eduardo Otubo <otubo@redhat.com>
Message-id: <20200327152826.13343-7-otubo@redhat.com>
Patchwork-id: 94458
O-Subject: [RHEL-8.1.z/RHEL-8.2.z cloud-init PATCHv2 6/6] net: IPv6, accept_ra, slaac, stateless (#51)
Bugzilla: 1811753
RH-Acked-by: Cathy Avery <cavery@redhat.com>
RH-Acked-by: Vitaly Kuznetsov <vkuznets@redhat.com>

commit 62bbc262c3c7f633eac1d09ec78c055eef05166a
Author: Harald <hjensas@redhat.com>
Date:   Wed Nov 20 18:55:27 2019 +0100

    net: IPv6, accept_ra, slaac, stateless (#51)

    Router advertisements are required for the default route
    to be set up, thus accept_ra should be enabled for
    dhcpv6-stateful.

    sysconf: IPV6_FORCE_ACCEPT_RA controls accept_ra sysctl.
    eni: mode static and mode dhcp 'accept_ra' controls sysctl.

    Add 'accept-ra: true|false' parameter to config v1 and
    v2. When True: accept_ra is set to '1'. When False:
    accept_ra is set to '0'. When not defined in config the
    value is left to the operating system default.

    This change also extend the IPv6 support to distinguish
    between slaac and dhcpv6-stateless. SLAAC is autoconfig
    without any options from DHCP, while stateless auto-configures
    the address and the uses DHCP for other options.

    LP: #1806014
    LP: #1808647

Signed-off-by: Eduardo Otubo <otubo@redhat.com>
Signed-off-by: Miroslav Rezanina <mrezanin@redhat.com>
---
 cloudinit/net/eni.py                               |  15 ++
 cloudinit/net/netplan.py                           |   9 +-
 cloudinit/net/network_state.py                     |  21 +-
 cloudinit/net/sysconfig.py                         |  34 ++-
 cloudinit/sources/helpers/openstack.py             |  21 +-
 .../unittests/test_datasource/test_configdrive.py  |   3 +-
 tests/unittests/test_net.py                        | 249 +++++++++++++++++++++
 7 files changed, 334 insertions(+), 18 deletions(-)

diff --git a/cloudinit/net/eni.py b/cloudinit/net/eni.py
index 896a39b..c70435a 100644
--- a/cloudinit/net/eni.py
+++ b/cloudinit/net/eni.py
@@ -393,6 +393,7 @@ class Renderer(renderer.Renderer):
     def _render_iface(self, iface, render_hwaddress=False):
         sections = []
         subnets = iface.get('subnets', {})
+        accept_ra = iface.pop('accept-ra', None)
         if subnets:
             for index, subnet in enumerate(subnets):
                 ipv4_subnet_mtu = None
@@ -409,9 +410,23 @@ class Renderer(renderer.Renderer):
                         subnet['type'] == 'ipv6_dhcpv6-stateful'):
                     # Configure network settings using DHCP or DHCPv6
                     iface['mode'] = 'dhcp'
+                    if accept_ra is not None:
+                        # Accept router advertisements (0=off, 1=on)
+                        iface['accept_ra'] = '1' if accept_ra else '0'
                 elif subnet['type'] == 'ipv6_dhcpv6-stateless':
                     # Configure network settings using SLAAC from RAs
                     iface['mode'] = 'auto'
+                    # Use stateless DHCPv6 (0=off, 1=on)
+                    iface['dhcp'] = '1'
+                elif subnet['type'] == 'ipv6_slaac':
+                    # Configure network settings using SLAAC from RAs
+                    iface['mode'] = 'auto'
+                    # Use stateless DHCPv6 (0=off, 1=on)
+                    iface['dhcp'] = '0'
+                elif subnet_is_ipv6(subnet) and subnet['type'] == 'static':
+                    if accept_ra is not None:
+                        # Accept router advertisements (0=off, 1=on)
+                        iface['accept_ra'] = '1' if accept_ra else '0'
 
                 # do not emit multiple 'auto $IFACE' lines as older (precise)
                 # ifupdown complains
diff --git a/cloudinit/net/netplan.py b/cloudinit/net/netplan.py
index 21517fd..78ec38c 100644
--- a/cloudinit/net/netplan.py
+++ b/cloudinit/net/netplan.py
@@ -4,7 +4,7 @@ import copy
 import os
 
 from . import renderer
-from .network_state import subnet_is_ipv6, NET_CONFIG_TO_V2
+from .network_state import subnet_is_ipv6, NET_CONFIG_TO_V2, IPV6_DYNAMIC_TYPES
 
 from cloudinit import log as logging
 from cloudinit import util
@@ -51,7 +51,8 @@ def _extract_addresses(config, entry, ifname):
          'mtu': 1480,
          'netmask': 64,
          'type': 'static'}],
-      'type: physical'
+      'type: physical',
+      'accept-ra': 'true'
     }
 
     An entry dictionary looks like:
@@ -92,6 +93,8 @@ def _extract_addresses(config, entry, ifname):
             if sn_type == 'dhcp':
                 sn_type += '4'
             entry.update({sn_type: True})
+        elif sn_type in IPV6_DYNAMIC_TYPES:
+            entry.update({'dhcp6': True})
         elif sn_type in ['static']:
             addr = "%s" % subnet.get('address')
             if 'prefix' in subnet:
@@ -144,6 +147,8 @@ def _extract_addresses(config, entry, ifname):
         ns = entry.get('nameservers', {})
         ns.update({'search': searchdomains})
         entry.update({'nameservers': ns})
+    if 'accept-ra' in config and config['accept-ra'] is not None:
+        entry.update({'accept-ra': util.is_true(config.get('accept-ra'))})
 
 
 def _extract_bond_slaves_by_name(interfaces, entry, bond_master):
diff --git a/cloudinit/net/network_state.py b/cloudinit/net/network_state.py
index 571eb57..82cfa42 100644
--- a/cloudinit/net/network_state.py
+++ b/cloudinit/net/network_state.py
@@ -17,13 +17,17 @@ from cloudinit import util
 LOG = logging.getLogger(__name__)
 
 NETWORK_STATE_VERSION = 1
+IPV6_DYNAMIC_TYPES = ['dhcp6',
+                      'ipv6_slaac',
+                      'ipv6_dhcpv6-stateless',
+                      'ipv6_dhcpv6-stateful']
 NETWORK_STATE_REQUIRED_KEYS = {
     1: ['version', 'config', 'network_state'],
 }
 NETWORK_V2_KEY_FILTER = [
     'addresses', 'dhcp4', 'dhcp4-overrides', 'dhcp6', 'dhcp6-overrides',
     'gateway4', 'gateway6', 'interfaces', 'match', 'mtu', 'nameservers',
-    'renderer', 'set-name', 'wakeonlan'
+    'renderer', 'set-name', 'wakeonlan', 'accept-ra'
 ]
 
 NET_CONFIG_TO_V2 = {
@@ -341,7 +345,8 @@ class NetworkStateInterpreter(object):
             'name': 'eth0',
             'subnets': [
                 {'type': 'dhcp4'}
-             ]
+             ],
+            'accept-ra': 'true'
         }
         '''
 
@@ -361,6 +366,9 @@ class NetworkStateInterpreter(object):
                     self.use_ipv6 = True
                     break
 
+        accept_ra = command.get('accept-ra', None)
+        if accept_ra is not None:
+            accept_ra = util.is_true(accept_ra)
         iface.update({
             'name': command.get('name'),
             'type': command.get('type'),
@@ -371,6 +379,7 @@ class NetworkStateInterpreter(object):
             'address': None,
             'gateway': None,
             'subnets': subnets,
+            'accept-ra': accept_ra
         })
         self._network_state['interfaces'].update({command.get('name'): iface})
         self.dump_network_state()
@@ -614,6 +623,7 @@ class NetworkStateInterpreter(object):
               driver: ixgbe
             set-name: lom1
             dhcp6: true
+            accept-ra: true
           switchports:
             match:
               name: enp2*
@@ -642,7 +652,7 @@ class NetworkStateInterpreter(object):
             driver = match.get('driver', None)
             if driver:
                 phy_cmd['params'] = {'driver': driver}
-            for key in ['mtu', 'match', 'wakeonlan']:
+            for key in ['mtu', 'match', 'wakeonlan', 'accept-ra']:
                 if key in cfg:
                     phy_cmd[key] = cfg[key]
 
@@ -915,8 +925,9 @@ def is_ipv6_addr(address):
 
 def subnet_is_ipv6(subnet):
     """Common helper for checking network_state subnets for ipv6."""
-    # 'static6' or 'dhcp6'
-    if subnet['type'].endswith('6'):
+    # 'static6', 'dhcp6', 'ipv6_dhcpv6-stateful', 'ipv6_dhcpv6-stateless' or
+    # 'ipv6_slaac'
+    if subnet['type'].endswith('6') or subnet['type'] in IPV6_DYNAMIC_TYPES:
         # This is a request for DHCPv6.
         return True
     elif subnet['type'] == 'static' and is_ipv6_addr(subnet.get('address')):
diff --git a/cloudinit/net/sysconfig.py b/cloudinit/net/sysconfig.py
index 8b11dbb..13c0a65 100644
--- a/cloudinit/net/sysconfig.py
+++ b/cloudinit/net/sysconfig.py
@@ -14,7 +14,7 @@ from configobj import ConfigObj
 
 from . import renderer
 from .network_state import (
-    is_ipv6_addr, net_prefix_to_ipv4_mask, subnet_is_ipv6)
+    is_ipv6_addr, net_prefix_to_ipv4_mask, subnet_is_ipv6, IPV6_DYNAMIC_TYPES)
 
 LOG = logging.getLogger(__name__)
 NM_CFG_FILE = "/etc/NetworkManager/NetworkManager.conf"
@@ -319,6 +319,9 @@ class Renderer(renderer.Renderer):
                     continue
                 iface_cfg[new_key] = old_value
 
+        if iface['accept-ra'] is not None:
+            iface_cfg['IPV6_FORCE_ACCEPT_RA'] = iface['accept-ra']
+
     @classmethod
     def _render_subnets(cls, iface_cfg, subnets, has_default_route):
         # setting base values
@@ -335,6 +338,15 @@ class Renderer(renderer.Renderer):
                 iface_cfg['DHCPV6C'] = True
             elif subnet_type == 'ipv6_dhcpv6-stateless':
                 iface_cfg['IPV6INIT'] = True
+                # Configure network settings using SLAAC from RAs and optional
+                # info from dhcp server using DHCPv6
+                iface_cfg['IPV6_AUTOCONF'] = True
+                iface_cfg['DHCPV6C'] = True
+                # Use Information-request to get only stateless configuration
+                # parameters (i.e., without address).
+                iface_cfg['DHCPV6C_OPTIONS'] = '-S'
+            elif subnet_type == 'ipv6_slaac':
+                iface_cfg['IPV6INIT'] = True
                 # Configure network settings using SLAAC from RAs
                 iface_cfg['IPV6_AUTOCONF'] = True
             elif subnet_type in ['dhcp4', 'dhcp']:
@@ -381,10 +393,15 @@ class Renderer(renderer.Renderer):
             # metric may apply to both dhcp and static config
             if 'metric' in subnet:
                 iface_cfg['METRIC'] = subnet['metric']
+            # TODO(hjensas): Including dhcp6 here is likely incorrect. DHCPv6
+            # does not ever provide a default gateway, the default gateway
+            # come from RA's. (https://github.com/openSUSE/wicked/issues/570)
             if subnet_type in ['dhcp', 'dhcp4', 'dhcp6']:
                 if has_default_route and iface_cfg['BOOTPROTO'] != 'none':
                     iface_cfg['DHCLIENT_SET_DEFAULT_ROUTE'] = False
                 continue
+            elif subnet_type in IPV6_DYNAMIC_TYPES:
+                continue
             elif subnet_type == 'static':
                 if subnet_is_ipv6(subnet):
                     ipv6_index = ipv6_index + 1
@@ -424,10 +441,14 @@ class Renderer(renderer.Renderer):
     @classmethod
     def _render_subnet_routes(cls, iface_cfg, route_cfg, subnets):
         for _, subnet in enumerate(subnets, start=len(iface_cfg.children)):
+            subnet_type = subnet.get('type')
             for route in subnet.get('routes', []):
                 is_ipv6 = subnet.get('ipv6') or is_ipv6_addr(route['gateway'])
 
-                if _is_default_route(route):
+                # Any dynamic configuration method, slaac, dhcpv6-stateful/
+                # stateless should get router information from router RA's.
+                if (_is_default_route(route) and subnet_type not in
+                        IPV6_DYNAMIC_TYPES):
                     if (
                             (subnet.get('ipv4') and
                              route_cfg.has_set_default_ipv4) or
@@ -446,10 +467,17 @@ class Renderer(renderer.Renderer):
                     # TODO(harlowja): add validation that no other iface has
                     # also provided the default route?
                     iface_cfg['DEFROUTE'] = True
+                    # TODO(hjensas): Including dhcp6 here is likely incorrect.
+                    # DHCPv6 does not ever provide a default gateway, the
+                    # default gateway come from RA's.
+                    # (https://github.com/openSUSE/wicked/issues/570)
                     if iface_cfg['BOOTPROTO'] in ('dhcp', 'dhcp4', 'dhcp6'):
+                        # NOTE(hjensas): DHCLIENT_SET_DEFAULT_ROUTE is SuSE
+                        # only. RHEL, CentOS, Fedora does not implement this
+                        # option.
                         iface_cfg['DHCLIENT_SET_DEFAULT_ROUTE'] = True
                     if 'gateway' in route:
-                        if is_ipv6 or is_ipv6_addr(route['gateway']):
+                        if is_ipv6:
                             iface_cfg['IPV6_DEFAULTGW'] = route['gateway']
                             route_cfg.has_set_default_ipv6 = True
                         else:
diff --git a/cloudinit/sources/helpers/openstack.py b/cloudinit/sources/helpers/openstack.py
index 9f2fd2d..77fcdd3 100644
--- a/cloudinit/sources/helpers/openstack.py
+++ b/cloudinit/sources/helpers/openstack.py
@@ -584,17 +584,24 @@ def convert_net_json(network_json=None, known_macs=None):
                         if n['link'] == link['id']]:
             subnet = dict((k, v) for k, v in network.items()
                           if k in valid_keys['subnet'])
-            if 'dhcp' in network['type']:
-                t = (network['type'] if network['type'].startswith('ipv6')
-                     else 'dhcp4')
-                subnet.update({
-                    'type': t,
-                })
-            else:
+
+            if network['type'] == 'ipv4_dhcp':
+                subnet.update({'type': 'dhcp4'})
+            elif network['type'] == 'ipv6_dhcp':
+                subnet.update({'type': 'dhcp6'})
+            elif network['type'] in ['ipv6_slaac', 'ipv6_dhcpv6-stateless',
+                                     'ipv6_dhcpv6-stateful']:
+                subnet.update({'type': network['type']})
+            elif network['type'] in ['ipv4', 'ipv6']:
                 subnet.update({
                     'type': 'static',
                     'address': network.get('ip_address'),
                 })
+
+            # Enable accept_ra for stateful and legacy ipv6_dhcp types
+            if network['type'] in ['ipv6_dhcpv6-stateful', 'ipv6_dhcp']:
+                cfg.update({'accept-ra': True})
+
             if network['type'] == 'ipv4':
                 subnet['ipv4'] = True
             if network['type'] == 'ipv6':
diff --git a/tests/unittests/test_datasource/test_configdrive.py b/tests/unittests/test_datasource/test_configdrive.py
index ed4e9d5..bd4c310 100644
--- a/tests/unittests/test_datasource/test_configdrive.py
+++ b/tests/unittests/test_datasource/test_configdrive.py
@@ -536,7 +536,8 @@ class TestNetJson(CiTestCase):
                  'mtu': None,
                  'name': 'enp0s2',
                  'subnets': [{'type': 'ipv6_dhcpv6-stateful'}],
-                 'type': 'physical'}
+                 'type': 'physical',
+                 'accept-ra': True}
             ],
         }
         conv_data = openstack.convert_net_json(in_data, known_macs=KNOWN_MACS)
diff --git a/tests/unittests/test_net.py b/tests/unittests/test_net.py
index 70d13f3..21a3f0e 100644
--- a/tests/unittests/test_net.py
+++ b/tests/unittests/test_net.py
@@ -712,6 +712,143 @@ NETWORK_CONFIGS = {
                 """),
         },
     },
+    'dhcpv6_accept_ra': {
+        'expected_eni': textwrap.dedent("""\
+            auto lo
+            iface lo inet loopback
+
+            auto iface0
+            iface iface0 inet6 dhcp
+                accept_ra 1
+        """).rstrip(' '),
+        'expected_netplan': textwrap.dedent("""
+            network:
+                version: 2
+                ethernets:
+                    iface0:
+                        accept-ra: true
+                        dhcp6: true
+        """).rstrip(' '),
+        'yaml_v1': textwrap.dedent("""\
+            version: 1
+            config:
+              - type: 'physical'
+                name: 'iface0'
+                subnets:
+                - {'type': 'dhcp6'}
+                accept-ra: true
+        """).rstrip(' '),
+        'yaml_v2': textwrap.dedent("""\
+            version: 2
+            ethernets:
+                iface0:
+                    dhcp6: true
+                    accept-ra: true
+        """).rstrip(' '),
+        'expected_sysconfig': {
+            'ifcfg-iface0': textwrap.dedent("""\
+                BOOTPROTO=none
+                DEVICE=iface0
+                DHCPV6C=yes
+                IPV6INIT=yes
+                IPV6_FORCE_ACCEPT_RA=yes
+                DEVICE=iface0
+                NM_CONTROLLED=no
+                ONBOOT=yes
+                STARTMODE=auto
+                TYPE=Ethernet
+                USERCTL=no
+            """),
+        },
+    },
+    'dhcpv6_reject_ra': {
+        'expected_eni': textwrap.dedent("""\
+            auto lo
+            iface lo inet loopback
+
+            auto iface0
+            iface iface0 inet6 dhcp
+                accept_ra 0
+        """).rstrip(' '),
+        'expected_netplan': textwrap.dedent("""
+            network:
+                version: 2
+                ethernets:
+                    iface0:
+                        accept-ra: false
+                        dhcp6: true
+        """).rstrip(' '),
+        'yaml_v1': textwrap.dedent("""\
+            version: 1
+            config:
+            - type: 'physical'
+              name: 'iface0'
+              subnets:
+              - {'type': 'dhcp6'}
+              accept-ra: false
+        """).rstrip(' '),
+        'yaml_v2': textwrap.dedent("""\
+            version: 2
+            ethernets:
+                iface0:
+                    dhcp6: true
+                    accept-ra: false
+        """).rstrip(' '),
+        'expected_sysconfig': {
+            'ifcfg-iface0': textwrap.dedent("""\
+                BOOTPROTO=none
+                DEVICE=iface0
+                DHCPV6C=yes
+                IPV6INIT=yes
+                IPV6_FORCE_ACCEPT_RA=no
+                DEVICE=iface0
+                NM_CONTROLLED=no
+                ONBOOT=yes
+                STARTMODE=auto
+                TYPE=Ethernet
+                USERCTL=no
+            """),
+        },
+    },
+    'ipv6_slaac': {
+        'expected_eni': textwrap.dedent("""\
+            auto lo
+            iface lo inet loopback
+
+            auto iface0
+            iface iface0 inet6 auto
+                dhcp 0
+        """).rstrip(' '),
+        'expected_netplan': textwrap.dedent("""
+            network:
+                version: 2
+                ethernets:
+                    iface0:
+                        dhcp6: true
+        """).rstrip(' '),
+        'yaml': textwrap.dedent("""\
+            version: 1
+            config:
+            - type: 'physical'
+              name: 'iface0'
+              subnets:
+              - {'type': 'ipv6_slaac'}
+        """).rstrip(' '),
+        'expected_sysconfig': {
+            'ifcfg-iface0': textwrap.dedent("""\
+                BOOTPROTO=none
+                DEVICE=iface0
+                IPV6_AUTOCONF=yes
+                IPV6INIT=yes
+                DEVICE=iface0
+                NM_CONTROLLED=no
+                ONBOOT=yes
+                STARTMODE=auto
+                TYPE=Ethernet
+                USERCTL=no
+            """),
+        },
+    },
     'dhcpv6_stateless': {
         'expected_eni': textwrap.dedent("""\
         auto lo
@@ -719,6 +856,7 @@ NETWORK_CONFIGS = {
 
         auto iface0
         iface iface0 inet6 auto
+            dhcp 1
     """).rstrip(' '),
         'expected_netplan': textwrap.dedent("""
         network:
@@ -739,6 +877,8 @@ NETWORK_CONFIGS = {
             'ifcfg-iface0': textwrap.dedent("""\
             BOOTPROTO=none
             DEVICE=iface0
+            DHCPV6C=yes
+            DHCPV6C_OPTIONS=-S
             IPV6_AUTOCONF=yes
             IPV6INIT=yes
             DEVICE=iface0
@@ -763,6 +903,7 @@ NETWORK_CONFIGS = {
             version: 2
             ethernets:
                 iface0:
+                    accept-ra: true
                     dhcp6: true
     """).rstrip(' '),
         'yaml': textwrap.dedent("""\
@@ -772,6 +913,7 @@ NETWORK_CONFIGS = {
             name: 'iface0'
             subnets:
             - {'type': 'ipv6_dhcpv6-stateful'}
+            accept-ra: true
     """).rstrip(' '),
         'expected_sysconfig': {
             'ifcfg-iface0': textwrap.dedent("""\
@@ -779,6 +921,7 @@ NETWORK_CONFIGS = {
             DEVICE=iface0
             DHCPV6C=yes
             IPV6INIT=yes
+            IPV6_FORCE_ACCEPT_RA=yes
             DEVICE=iface0
             NM_CONTROLLED=no
             ONBOOT=yes
@@ -2376,6 +2519,33 @@ USERCTL=no
             }
         }
 
+    def test_dhcpv6_accept_ra_config_v1(self):
+        entry = NETWORK_CONFIGS['dhcpv6_accept_ra']
+        found = self._render_and_read(network_config=yaml.load(
+            entry['yaml_v1']))
+        self._compare_files_to_expected(entry[self.expected_name], found)
+        self._assert_headers(found)
+
+    def test_dhcpv6_accept_ra_config_v2(self):
+        entry = NETWORK_CONFIGS['dhcpv6_accept_ra']
+        found = self._render_and_read(network_config=yaml.load(
+            entry['yaml_v2']))
+        self._compare_files_to_expected(entry[self.expected_name], found)
+        self._assert_headers(found)
+
+    def test_dhcpv6_reject_ra_config_v1(self):
+        entry = NETWORK_CONFIGS['dhcpv6_reject_ra']
+        found = self._render_and_read(network_config=yaml.load(
+            entry['yaml_v1']))
+        self._compare_files_to_expected(entry[self.expected_name], found)
+        self._assert_headers(found)
+
+    def test_dhcpv6_reject_ra_config_v2(self):
+        entry = NETWORK_CONFIGS['dhcpv6_reject_ra']
+        found = self._render_and_read(network_config=yaml.load(
+            entry['yaml_v2']))
+        self._compare_files_to_expected(entry[self.expected_name], found)
+        self._assert_headers(found)
 
     def test_dhcpv6_stateless_config(self):
         entry = NETWORK_CONFIGS['dhcpv6_stateless']
@@ -3267,6 +3437,46 @@ class TestNetplanRoundTrip(CiTestCase):
             entry['expected_netplan'].splitlines(),
             files['/etc/netplan/50-cloud-init.yaml'].splitlines())
 
+    def testsimple_render_dhcpv6_accept_ra(self):
+        entry = NETWORK_CONFIGS['dhcpv6_accept_ra']
+        files = self._render_and_read(network_config=yaml.load(
+            entry['yaml_v1']))
+        self.assertEqual(
+            entry['expected_netplan'].splitlines(),
+            files['/etc/netplan/50-cloud-init.yaml'].splitlines())
+
+    def testsimple_render_dhcpv6_reject_ra(self):
+        entry = NETWORK_CONFIGS['dhcpv6_reject_ra']
+        files = self._render_and_read(network_config=yaml.load(
+            entry['yaml_v1']))
+        self.assertEqual(
+            entry['expected_netplan'].splitlines(),
+            files['/etc/netplan/50-cloud-init.yaml'].splitlines())
+
+    def testsimple_render_ipv6_slaac(self):
+        entry = NETWORK_CONFIGS['ipv6_slaac']
+        files = self._render_and_read(network_config=yaml.load(
+            entry['yaml']))
+        self.assertEqual(
+            entry['expected_netplan'].splitlines(),
+            files['/etc/netplan/50-cloud-init.yaml'].splitlines())
+
+    def testsimple_render_dhcpv6_stateless(self):
+        entry = NETWORK_CONFIGS['dhcpv6_stateless']
+        files = self._render_and_read(network_config=yaml.load(
+            entry['yaml']))
+        self.assertEqual(
+            entry['expected_netplan'].splitlines(),
+            files['/etc/netplan/50-cloud-init.yaml'].splitlines())
+
+    def testsimple_render_dhcpv6_stateful(self):
+        entry = NETWORK_CONFIGS['dhcpv6_stateful']
+        files = self._render_and_read(network_config=yaml.load(
+            entry['yaml']))
+        self.assertEqual(
+            entry['expected_netplan'].splitlines(),
+            files['/etc/netplan/50-cloud-init.yaml'].splitlines())
+
     def testsimple_render_all(self):
         entry = NETWORK_CONFIGS['all']
         files = self._render_and_read(network_config=yaml.load(entry['yaml']))
@@ -3350,6 +3560,45 @@ class TestEniRoundTrip(CiTestCase):
             entry['expected_eni'].splitlines(),
             files['/etc/network/interfaces'].splitlines())
 
+    def testsimple_render_dhcpv6_stateless(self):
+        entry = NETWORK_CONFIGS['dhcpv6_stateless']
+        files = self._render_and_read(network_config=yaml.load(
+            entry['yaml']))
+        files = self._render_and_read(network_config=yaml.load(entry['yaml']))
+        self.assertEqual(
+            entry['expected_eni'].splitlines(),
+            files['/etc/network/interfaces'].splitlines())
+
+    def testsimple_render_ipv6_slaac(self):
+        entry = NETWORK_CONFIGS['ipv6_slaac']
+        files = self._render_and_read(network_config=yaml.load(entry['yaml']))
+        self.assertEqual(
+            entry['expected_eni'].splitlines(),
+            files['/etc/network/interfaces'].splitlines())
+
+    def testsimple_render_dhcpv6_stateful(self):
+        entry = NETWORK_CONFIGS['dhcpv6_stateless']
+        files = self._render_and_read(network_config=yaml.load(entry['yaml']))
+        self.assertEqual(
+            entry['expected_eni'].splitlines(),
+            files['/etc/network/interfaces'].splitlines())
+
+    def testsimple_render_dhcpv6_accept_ra(self):
+        entry = NETWORK_CONFIGS['dhcpv6_accept_ra']
+        files = self._render_and_read(network_config=yaml.load(
+            entry['yaml_v1']))
+        self.assertEqual(
+            entry['expected_eni'].splitlines(),
+            files['/etc/network/interfaces'].splitlines())
+
+    def testsimple_render_dhcpv6_reject_ra(self):
+        entry = NETWORK_CONFIGS['dhcpv6_reject_ra']
+        files = self._render_and_read(network_config=yaml.load(
+            entry['yaml_v1']))
+        self.assertEqual(
+            entry['expected_eni'].splitlines(),
+            files['/etc/network/interfaces'].splitlines())
+
     def testsimple_render_manual(self):
         """Test rendering of 'manual' for 'type' and 'control'.
 
-- 
1.8.3.1