← Back to team overview

cloud-init-dev team mailing list archive

[Merge] ~raharper/cloud-init:curtin-centos into cloud-init:master

 

Ryan Harper has proposed merging ~raharper/cloud-init:curtin-centos into cloud-init:master.

Requested reviews:
  cloud-init commiters (cloud-init-dev)
Related bugs:
  Bug #1687725 in cloud-init: "sysconfig render does not support type manual subnets"
  https://bugs.launchpad.net/cloud-init/+bug/1687725
  Bug #1694801 in cloud-init: "sysconfig needs fix for ipv6 gateway routes"
  https://bugs.launchpad.net/cloud-init/+bug/1694801
  Bug #1695092 in cloud-init: "sysconfig only applies subnet/route config to physical interfaces"
  https://bugs.launchpad.net/cloud-init/+bug/1695092
  Bug #1701097 in cloud-init: "eni rendering of ipv6 gateways fails"
  https://bugs.launchpad.net/cloud-init/+bug/1701097
  Bug #1701417 in cloud-init: "cloud-init fails to configure bonding on CentOS 7"
  https://bugs.launchpad.net/cloud-init/+bug/1701417
  Bug #1702513 in cloud-init: "sysconfig should render MTU values from subnets/routes including ipv6"
  https://bugs.launchpad.net/cloud-init/+bug/1702513

For more details, see:
https://code.launchpad.net/~raharper/cloud-init/+git/cloud-init/+merge/327648

WIP branch with changes for el6/el7 networking rendering
-- 
Your team cloud-init commiters is requested to review the proposed merge of ~raharper/cloud-init:curtin-centos into cloud-init:master.
diff --git a/cloudinit/net/eni.py b/cloudinit/net/eni.py
index b707146..bb80ec0 100644
--- a/cloudinit/net/eni.py
+++ b/cloudinit/net/eni.py
@@ -355,7 +355,7 @@ class Renderer(renderer.Renderer):
             default_gw = " default gw %s" % route['gateway']
             content.append(up + default_gw + or_true)
             content.append(down + default_gw + or_true)
-        elif route['network'] == '::' and route['netmask'] == 0:
+        elif route['network'] == '::' and route['prefix'] == 0:
             # ipv6!
             default_gw = " -A inet6 default gw %s" % route['gateway']
             content.append(up + default_gw + or_true)
diff --git a/cloudinit/net/renderer.py b/cloudinit/net/renderer.py
index bba139e..57652e2 100644
--- a/cloudinit/net/renderer.py
+++ b/cloudinit/net/renderer.py
@@ -20,6 +20,10 @@ def filter_by_name(match_name):
     return lambda iface: match_name == iface['name']
 
 
+def filter_by_attr(match_name):
+    return lambda iface: (match_name in iface and iface[match_name])
+
+
 filter_by_physical = filter_by_type('physical')
 
 
diff --git a/cloudinit/net/sysconfig.py b/cloudinit/net/sysconfig.py
index 7ed11d1..95324ea 100644
--- a/cloudinit/net/sysconfig.py
+++ b/cloudinit/net/sysconfig.py
@@ -151,9 +151,10 @@ class Route(ConfigMap):
                 elif proto == "ipv6" and self.is_ipv6_route(address_value):
                     netmask_value = str(self._conf['NETMASK' + index])
                     gateway_value = str(self._conf['GATEWAY' + index])
-                    buf.write("%s/%s via %s\n" % (address_value,
-                                                  netmask_value,
-                                                  gateway_value))
+                    buf.write("%s/%s via %s dev %s\n" % (address_value,
+                                                         netmask_value,
+                                                         gateway_value,
+                                                         self._route_name))
 
         return buf.getvalue()
 
@@ -262,6 +263,9 @@ class Renderer(renderer.Renderer):
         for (old_key, new_key) in [('mac_address', 'HWADDR'), ('mtu', 'MTU')]:
             old_value = iface.get(old_key)
             if old_value is not None:
+                # only set HWADDR on physical interfaces
+                if old_key == 'mac_address' and iface['type'] != 'physical':
+                    continue
                 iface_cfg[new_key] = old_value
 
     @classmethod
@@ -271,6 +275,7 @@ class Renderer(renderer.Renderer):
 
         # modifying base values according to subnets
         for i, subnet in enumerate(subnets, start=len(iface_cfg.children)):
+            mtu_key = 'MTU'
             subnet_type = subnet.get('type')
             if subnet_type == 'dhcp6':
                 iface_cfg['IPV6INIT'] = True
@@ -290,7 +295,13 @@ class Renderer(renderer.Renderer):
                 # if iface_cfg['BOOTPROTO'] == 'none':
                 #    iface_cfg['BOOTPROTO'] = 'static'
                 if subnet_is_ipv6(subnet):
+                    mtu_key = 'IPV6_MTU'
                     iface_cfg['IPV6INIT'] = True
+                if 'mtu' in subnet:
+                    iface_cfg[mtu_key] = subnet['mtu']
+
+            elif subnet_type == 'manual':
+                iface_cfg['ONBOOT'] = False
             else:
                 raise ValueError("Unknown subnet type '%s' found"
                                  " for interface '%s'" % (subnet_type,
@@ -311,7 +322,7 @@ class Renderer(renderer.Renderer):
                     if 'netmask' in subnet and str(subnet['netmask']) != "":
                         ipv6_cidr = (subnet['address'] +
                                      '/' +
-                                     str(subnet['netmask']))
+                                     str(subnet['prefix']))
                     else:
                         ipv6_cidr = subnet['address']
                     if ipv6_index == 0:
@@ -329,11 +340,18 @@ class Renderer(renderer.Renderer):
                     iface_cfg['NETMASK' + suff] = \
                         net_prefix_to_ipv4_mask(subnet['prefix'])
 
+                if 'gateway' in subnet:
+                    iface_cfg['DEFROUTE'] = True
+                    if ':' in subnet['gateway']:
+                        iface_cfg['IPV6_DEFAULTGW'] = subnet['gateway']
+                    else:
+                        iface_cfg['GATEWAY'] = subnet['gateway']
+
     @classmethod
     def _render_subnet_routes(cls, iface_cfg, route_cfg, subnets):
         for i, subnet in enumerate(subnets, start=len(iface_cfg.children)):
             for route in subnet.get('routes', []):
-                is_ipv6 = subnet.get('ipv6')
+                is_ipv6 = subnet.get('ipv6') or ':' in route['gateway']
 
                 if _is_default_route(route):
                     if (
@@ -355,7 +373,7 @@ class Renderer(renderer.Renderer):
                     # also provided the default route?
                     iface_cfg['DEFROUTE'] = True
                     if 'gateway' in route:
-                        if is_ipv6:
+                        if is_ipv6 or ':' in route['gateway']:
                             iface_cfg['IPV6_DEFAULTGW'] = route['gateway']
                             route_cfg.has_set_default_ipv6 = True
                         else:
@@ -406,24 +424,42 @@ class Renderer(renderer.Renderer):
     @classmethod
     def _render_bond_interfaces(cls, network_state, iface_contents):
         bond_filter = renderer.filter_by_type('bond')
+        slave_filter = renderer.filter_by_attr('bond-master')
         for iface in network_state.iter_interfaces(bond_filter):
             iface_name = iface['name']
             iface_cfg = iface_contents[iface_name]
             cls._render_bonding_opts(iface_cfg, iface)
-            iface_master_name = iface['bond-master']
-            iface_cfg['MASTER'] = iface_master_name
-            iface_cfg['SLAVE'] = True
+
             # Ensure that the master interface (and any of its children)
             # are actually marked as being bond types...
-            master_cfg = iface_contents[iface_master_name]
-            master_cfgs = [master_cfg]
-            master_cfgs.extend(master_cfg.children)
+            master_cfgs = [iface_cfg]
+            master_cfgs.extend(iface_cfg.children)
             for master_cfg in master_cfgs:
                 master_cfg['BONDING_MASTER'] = True
                 master_cfg.kind = 'bond'
 
-    @staticmethod
-    def _render_vlan_interfaces(network_state, iface_contents):
+            if 'mac_address' in iface and iface.get('mac_address'):
+                iface_cfg['MACADDR'] = iface.get('mac_address')
+
+            iface_subnets = iface.get("subnets", [])
+            route_cfg = iface_cfg.routes
+            cls._render_subnets(iface_cfg, iface_subnets)
+            cls._render_subnet_routes(iface_cfg, route_cfg, iface_subnets)
+
+            bond_slaves = sorted([slave_iface['name']
+                                  for slave_iface in
+                                  network_state.iter_interfaces(slave_filter)
+                                  if slave_iface['bond-master'] == iface_name])
+            for index, bond_slave in enumerate(bond_slaves):
+                slavestr = 'BONDING_SLAVE%s' % index
+                iface_cfg[slavestr] = bond_slave
+
+                slave_cfg = iface_contents[bond_slave]
+                slave_cfg['MASTER'] = iface_name
+                slave_cfg['SLAVE'] = True
+
+    @classmethod
+    def _render_vlan_interfaces(cls, network_state, iface_contents):
         vlan_filter = renderer.filter_by_type('vlan')
         for iface in network_state.iter_interfaces(vlan_filter):
             iface_name = iface['name']
@@ -431,6 +467,11 @@ class Renderer(renderer.Renderer):
             iface_cfg['VLAN'] = True
             iface_cfg['PHYSDEV'] = iface_name[:iface_name.rfind('.')]
 
+            iface_subnets = iface.get("subnets", [])
+            route_cfg = iface_cfg.routes
+            cls._render_subnets(iface_cfg, iface_subnets)
+            cls._render_subnet_routes(iface_cfg, route_cfg, iface_subnets)
+
     @staticmethod
     def _render_dns(network_state, existing_dns_path=None):
         content = resolv_conf.ResolvConf("")
@@ -467,6 +508,10 @@ class Renderer(renderer.Renderer):
             for old_key, new_key in cls.bridge_opts_keys:
                 if old_key in iface:
                     iface_cfg[new_key] = iface[old_key]
+
+            if 'mac_address' in iface and iface.get('mac_address'):
+                iface_cfg['MACADDR'] = iface.get('mac_address')
+
             # Is this the right key to get all the connected interfaces?
             for bridged_iface_name in iface.get('bridge_ports', []):
                 # Ensure all bridged interfaces are correctly tagged
@@ -477,6 +522,11 @@ class Renderer(renderer.Renderer):
                 for bridge_cfg in bridged_cfgs:
                     bridge_cfg['BRIDGE'] = iface_name
 
+            iface_subnets = iface.get("subnets", [])
+            route_cfg = iface_cfg.routes
+            cls._render_subnets(iface_cfg, iface_subnets)
+            cls._render_subnet_routes(iface_cfg, route_cfg, iface_subnets)
+
     @classmethod
     def _render_sysconfig(cls, base_sysconf_dir, network_state):
         '''Given state, return /etc/sysconfig files + contents'''
diff --git a/cloudinit/sources/DataSourceAzure.py b/cloudinit/sources/DataSourceAzure.py
index b5a95a1..e02d7a7 100644
--- a/cloudinit/sources/DataSourceAzure.py
+++ b/cloudinit/sources/DataSourceAzure.py
@@ -442,7 +442,7 @@ class DataSourceAzure(sources.DataSource):
                   self.ds_cfg['agent_command'])
         try:
             fabric_data = metadata_func()
-        except Exception as exc:
+        except Exception:
             LOG.warning(
                 "Error communicating with Azure fabric; You may experience."
                 "connectivity issues.", exc_info=True)
diff --git a/packages/redhat/cloud-init.spec.in b/packages/redhat/cloud-init.spec.in
index 9f75c4b..ab22e0f 100644
--- a/packages/redhat/cloud-init.spec.in
+++ b/packages/redhat/cloud-init.spec.in
@@ -117,7 +117,8 @@ mkdir -p $RPM_BUILD_ROOT/%{_libexecdir}/%{name}
 
 %if "%{init_system}" == "systemd"
 mkdir -p         $RPM_BUILD_ROOT/%{_unitdir}
-cp -p systemd/*  $RPM_BUILD_ROOT/%{_unitdir}
+# only copy targets, .service files are installed by setup.py
+cp -p systemd/*.target  $RPM_BUILD_ROOT/%{_unitdir}
 %endif
 
 %clean
diff --git a/setup.py b/setup.py
index bce06ad..fbcad6f 100755
--- a/setup.py
+++ b/setup.py
@@ -110,14 +110,45 @@ def render_cloud_cfg():
     relpath = os.path.join(os.path.basename(tmpd), 'cloud.cfg')
     return relpath
 
+def render_systemd_unit(template):
+    """render systemd unit into a tmpdir under same dir as setup.py
+
+    This is rendered to a temporary directory under the top level
+    directory with the name 'systemd.cfg'.  The reason for not just
+    rendering to systemd/{unit}.service is for a.) don't want to write
+    over contents in that file if user had something there. b.) debuild
+    will complain that files are different outside of the debian directory."""
+
+    # older versions of tox use bdist (xenial), and then install from there.
+    # newer versions just use install.
+    if not (sys.argv[1] == 'install' or sys.argv[1].startswith('bdist*')):
+        return template
+
+    # we may get passed a non-template file, just pass it back
+    if not template.endswith('.tmpl'):
+        return template
+
+    topdir = os.path.dirname(sys.argv[0])
+    tmpd = tempfile.mkdtemp(dir=topdir)
+    atexit.register(shutil.rmtree, tmpd)
+    unit_fname = os.path.basename(template.rstrip('.tmpl'))
+    fpath = os.path.join(tmpd, unit_fname)
+    tiny_p([sys.executable, './tools/render-cloudcfg',
+            template, fpath])
+    # relpath is relative to setup.py
+    relpath = os.path.join(os.path.basename(tmpd), unit_fname)
+    return relpath
+
 
 INITSYS_FILES = {
     'sysvinit': [f for f in glob('sysvinit/redhat/*') if is_f(f)],
     'sysvinit_freebsd': [f for f in glob('sysvinit/freebsd/*') if is_f(f)],
     'sysvinit_deb': [f for f in glob('sysvinit/debian/*') if is_f(f)],
     'sysvinit_openrc': [f for f in glob('sysvinit/gentoo/*') if is_f(f)],
-    'systemd': [f for f in (glob('systemd/*.service') +
-                            glob('systemd/*.target')) if is_f(f)],
+    'systemd': [render_systemd_unit(f)
+                for f in (glob('systemd/*.tmpl') +
+                          glob('systemd/*.service') +
+                          glob('systemd/*.target')) if is_f(f)],
     'systemd.generators': [f for f in glob('systemd/*-generator') if is_f(f)],
     'upstart': [f for f in glob('upstart/*') if is_f(f)],
 }
diff --git a/systemd/cloud-config.service b/systemd/cloud-config.service
deleted file mode 100644
index 3309e08..0000000
--- a/systemd/cloud-config.service
+++ /dev/null
@@ -1,16 +0,0 @@
-[Unit]
-Description=Apply the settings specified in cloud-config
-After=network-online.target cloud-config.target
-Wants=network-online.target cloud-config.target
-
-[Service]
-Type=oneshot
-ExecStart=/usr/bin/cloud-init modules --mode=config
-RemainAfterExit=yes
-TimeoutSec=0
-
-# Output needs to appear in instance console output
-StandardOutput=journal+console
-
-[Install]
-WantedBy=cloud-init.target
diff --git a/systemd/cloud-config.service.tmpl b/systemd/cloud-config.service.tmpl
new file mode 100644
index 0000000..bdee3ce
--- /dev/null
+++ b/systemd/cloud-config.service.tmpl
@@ -0,0 +1,17 @@
+## template:jinja
+[Unit]
+Description=Apply the settings specified in cloud-config
+After=network-online.target cloud-config.target
+Wants=network-online.target cloud-config.target
+
+[Service]
+Type=oneshot
+ExecStart=/usr/bin/cloud-init modules --mode=config
+RemainAfterExit=yes
+TimeoutSec=0
+
+# Output needs to appear in instance console output
+StandardOutput=journal+console
+
+[Install]
+WantedBy=cloud-init.target
diff --git a/systemd/cloud-final.service b/systemd/cloud-final.service
deleted file mode 100644
index 66f5b8f..0000000
--- a/systemd/cloud-final.service
+++ /dev/null
@@ -1,18 +0,0 @@
-[Unit]
-Description=Execute cloud user/final scripts
-After=network-online.target cloud-config.service rc-local.service multi-user.target
-Wants=network-online.target cloud-config.service
-Before=apt-daily.service
-
-[Service]
-Type=oneshot
-ExecStart=/usr/bin/cloud-init modules --mode=final
-RemainAfterExit=yes
-TimeoutSec=0
-KillMode=process
-
-# Output needs to appear in instance console output
-StandardOutput=journal+console
-
-[Install]
-WantedBy=cloud-init.target
diff --git a/systemd/cloud-final.service.tmpl b/systemd/cloud-final.service.tmpl
new file mode 100644
index 0000000..fc01b89
--- /dev/null
+++ b/systemd/cloud-final.service.tmpl
@@ -0,0 +1,22 @@
+## template:jinja
+[Unit]
+Description=Execute cloud user/final scripts
+After=network-online.target cloud-config.service rc-local.service
+{% if variant in ["ubuntu", "unknown", "debian"] %}
+After=multi-user.target
+{% endif %}
+Wants=network-online.target cloud-config.service
+Before=apt-daily.service
+
+[Service]
+Type=oneshot
+ExecStart=/usr/bin/cloud-init modules --mode=final
+RemainAfterExit=yes
+TimeoutSec=0
+KillMode=process
+
+# Output needs to appear in instance console output
+StandardOutput=journal+console
+
+[Install]
+WantedBy=cloud-init.target
diff --git a/systemd/cloud-init-local.service b/systemd/cloud-init-local.service
deleted file mode 100644
index 7ee43ed..0000000
--- a/systemd/cloud-init-local.service
+++ /dev/null
@@ -1,24 +0,0 @@
-[Unit]
-Description=Initial cloud-init job (pre-networking)
-DefaultDependencies=no
-Wants=network-pre.target
-After=systemd-remount-fs.service
-Before=NetworkManager.service
-Before=network-pre.target
-Before=shutdown.target
-Before=sysinit.target
-Conflicts=shutdown.target
-RequiresMountsFor=/var/lib/cloud
-
-[Service]
-Type=oneshot
-ExecStart=/usr/bin/cloud-init init --local
-ExecStart=/bin/touch /run/cloud-init/network-config-ready
-RemainAfterExit=yes
-TimeoutSec=0
-
-# Output needs to appear in instance console output
-StandardOutput=journal+console
-
-[Install]
-WantedBy=cloud-init.target
diff --git a/systemd/cloud-init-local.service.tmpl b/systemd/cloud-init-local.service.tmpl
new file mode 100644
index 0000000..ff9c644
--- /dev/null
+++ b/systemd/cloud-init-local.service.tmpl
@@ -0,0 +1,29 @@
+## template:jinja
+[Unit]
+Description=Initial cloud-init job (pre-networking)
+{% if variant in ["ubuntu", "unknown", "debian"] %}
+DefaultDependencies=no
+{% endif %}
+Wants=network-pre.target
+After=systemd-remount-fs.service
+Before=NetworkManager.service
+Before=network-pre.target
+Before=shutdown.target
+{% if variant in ["ubuntu", "unknown", "debian"] %}
+Before=sysinit.target
+Conflicts=shutdown.target
+{% endif %}
+RequiresMountsFor=/var/lib/cloud
+
+[Service]
+Type=oneshot
+ExecStart=/usr/bin/cloud-init init --local
+ExecStart=/bin/touch /run/cloud-init/network-config-ready
+RemainAfterExit=yes
+TimeoutSec=0
+
+# Output needs to appear in instance console output
+StandardOutput=journal+console
+
+[Install]
+WantedBy=cloud-init.target
diff --git a/systemd/cloud-init.service b/systemd/cloud-init.service
deleted file mode 100644
index 39acc20..0000000
--- a/systemd/cloud-init.service
+++ /dev/null
@@ -1,27 +0,0 @@
-[Unit]
-Description=Initial cloud-init job (metadata service crawler)
-DefaultDependencies=no
-Wants=cloud-init-local.service
-Wants=sshd-keygen.service
-Wants=sshd.service
-After=cloud-init-local.service
-After=systemd-networkd-wait-online.service
-After=networking.service
-Before=network-online.target
-Before=sshd-keygen.service
-Before=sshd.service
-Before=sysinit.target
-Before=systemd-user-sessions.service
-Conflicts=shutdown.target
-
-[Service]
-Type=oneshot
-ExecStart=/usr/bin/cloud-init init
-RemainAfterExit=yes
-TimeoutSec=0
-
-# Output needs to appear in instance console output
-StandardOutput=journal+console
-
-[Install]
-WantedBy=cloud-init.target
diff --git a/systemd/cloud-init.service.tmpl b/systemd/cloud-init.service.tmpl
new file mode 100644
index 0000000..2c71889
--- /dev/null
+++ b/systemd/cloud-init.service.tmpl
@@ -0,0 +1,35 @@
+## template:jinja
+[Unit]
+Description=Initial cloud-init job (metadata service crawler)
+DefaultDependencies=no
+Wants=cloud-init-local.service
+Wants=sshd-keygen.service
+Wants=sshd.service
+After=cloud-init-local.service
+After=systemd-networkd-wait-online.service
+{% if variant in ["ubuntu", "unknown", "debian"] %}
+After=networking.service
+{% endif %}
+{% if variant in ["centos", "fedora", "redhat"] %}
+After=network.service
+{% endif %}
+Before=network-online.target
+Before=sshd-keygen.service
+Before=sshd.service
+{% if variant in ["ubuntu", "unknown", "debian"] %}
+Before=sysinit.target
+Conflicts=shutdown.target
+{% endif %}
+Before=systemd-user-sessions.service
+
+[Service]
+Type=oneshot
+ExecStart=/usr/bin/cloud-init init
+RemainAfterExit=yes
+TimeoutSec=0
+
+# Output needs to appear in instance console output
+StandardOutput=journal+console
+
+[Install]
+WantedBy=cloud-init.target
diff --git a/tests/unittests/test_distros/test_netconfig.py b/tests/unittests/test_distros/test_netconfig.py
index 83580cc..2f505d9 100644
--- a/tests/unittests/test_distros/test_netconfig.py
+++ b/tests/unittests/test_distros/test_netconfig.py
@@ -476,7 +476,9 @@ NETWORKING=yes
 # Created by cloud-init on instance boot automatically, do not edit.
 #
 BOOTPROTO=none
+DEFROUTE=yes
 DEVICE=eth0
+GATEWAY=192.168.1.254
 IPADDR=192.168.1.5
 NETMASK=255.255.255.0
 NM_CONTROLLED=no
@@ -625,9 +627,11 @@ IPV6_AUTOCONF=no
 # Created by cloud-init on instance boot automatically, do not edit.
 #
 BOOTPROTO=none
+DEFROUTE=yes
 DEVICE=eth0
 IPV6ADDR=2607:f0d0:1002:0011::2/64
 IPV6INIT=yes
+IPV6_DEFAULTGW=2607:f0d0:1002:0011::1
 NM_CONTROLLED=no
 ONBOOT=yes
 TYPE=Ethernet
diff --git a/tests/unittests/test_net.py b/tests/unittests/test_net.py
index 06e8f09..1c4f4de 100644
--- a/tests/unittests/test_net.py
+++ b/tests/unittests/test_net.py
@@ -422,6 +422,31 @@ NETWORK_CONFIGS = {
                             via: 65.61.151.37
                         set-name: eth99
         """).rstrip(' '),
+        'expected_sysconfig': {
+            'ifcfg-eth1': textwrap.dedent("""\
+        # Created by cloud-init on instance boot automatically, do not edit.
+        #
+        BOOTPROTO=none
+        DEVICE=eth1
+        HWADDR=cf:d6:af:48:e8:80
+        NM_CONTROLLED=no
+        ONBOOT=yes
+        TYPE=Ethernet
+        USERCTL=no""").rstrip(' '),
+            'ifcfg-eth99': textwrap.dedent("""\
+        # Created by cloud-init on instance boot automatically, do not edit.
+        #
+        BOOTPROTO=dhcp
+        DEFROUTE=yes
+        DEVICE=eth99
+        GATEWAY=65.61.151.37
+        HWADDR=c0:d6:9f:2c:e8:80
+        IPADDR=192.168.21.3
+        NETMASK=255.255.255.0
+        NM_CONTROLLED=no
+        ONBOOT=yes
+        TYPE=Ethernet
+        USERCTL=no """).rstrip(' ')},
         'yaml': textwrap.dedent("""
             version: 1
             config:
@@ -541,6 +566,8 @@ iface br0 inet static
 # control-alias br0
 iface br0 inet6 static
     address 2001:1::1/64
+    post-up route add -A inet6 default gw 2001:4800:78ff:1b::1 || true
+    pre-down route del -A inet6 default gw 2001:4800:78ff:1b::1 || true
 
 auto bond0.200
 iface bond0.200 inet dhcp
@@ -675,6 +702,9 @@ pre-down route del -net 10.0.0.0 netmask 255.0.0.0 gw 11.0.0.1 metric 3 || true
                                 eth3: 50
                                 eth4: 75
                             priority: 22
+                        routes:
+                        -   to: ::/0
+                            via: 2001:4800:78ff:1b::1
                 vlans:
                     bond0.200:
                         dhcp4: true
@@ -697,6 +727,146 @@ pre-down route del -net 10.0.0.0 netmask 255.0.0.0 gw 11.0.0.1 metric 3 || true
                             - sacchromyces.maas
                             - brettanomyces.maas
         """).rstrip(' '),
+        'expected_sysconfig': {
+            'ifcfg-bond0': textwrap.dedent("""\
+        # Created by cloud-init on instance boot automatically, do not edit.
+        #
+        BONDING_MASTER=yes
+        BONDING_OPTS="mode=active-backup xmit_hash_policy=layer3+4 miimon=100"
+        BONDING_SLAVE0=eth1
+        BONDING_SLAVE1=eth2
+        BOOTPROTO=dhcp
+        DEVICE=bond0
+        DHCPV6C=yes
+        IPV6INIT=yes
+        MACADDR=aa:bb:cc:dd:ee:ff
+        NM_CONTROLLED=no
+        ONBOOT=yes
+        TYPE=Bond
+        USERCTL=no
+            """).rstrip(' '),
+            'ifcfg-bond0.200': textwrap.dedent("""\
+        # Created by cloud-init on instance boot automatically, do not edit.
+        #
+        BOOTPROTO=dhcp
+        DEVICE=bond0.200
+        NM_CONTROLLED=no
+        ONBOOT=yes
+        PHYSDEV=bond0
+        TYPE=Ethernet
+        USERCTL=no
+        VLAN=yes
+            """).rstrip(' '),
+            'ifcfg-br0': textwrap.dedent("""\
+        # Created by cloud-init on instance boot automatically, do not edit.
+        #
+        AGEING=250
+        BOOTPROTO=none
+        DEFROUTE=yes
+        DEVICE=br0
+        IPADDR=192.168.14.2
+        IPV6ADDR=2001:1::1
+        IPV6INIT=yes
+        IPV6_DEFAULTGW=2001:4800:78ff:1b::1
+        NETMASK=255.255.255.0
+        NM_CONTROLLED=no
+        ONBOOT=yes
+        PRIO=22
+        STP=off
+        TYPE=Bridge
+        USERCTL=no
+            """).rstrip(' '),
+            'ifcfg-eth0': textwrap.dedent("""\
+        # Created by cloud-init on instance boot automatically, do not edit.
+        #
+        BOOTPROTO=none
+        DEVICE=eth0
+        HWADDR=c0:d6:9f:2c:e8:80
+        NM_CONTROLLED=no
+        ONBOOT=yes
+        TYPE=Ethernet
+        USERCTL=no
+            """).rstrip(' '),
+            'ifcfg-eth0.101': textwrap.dedent("""\
+        # Created by cloud-init on instance boot automatically, do not edit.
+        #
+        BOOTPROTO=none
+        DEFROUTE=yes
+        DEVICE=eth0.101
+        GATEWAY=192.168.0.1
+        IPADDR=192.168.0.2
+        IPADDR1=192.168.2.10
+        MTU=1500
+        NETMASK=255.255.255.0
+        NETMASK1=255.255.255.0
+        NM_CONTROLLED=no
+        ONBOOT=yes
+        PHYSDEV=eth0
+        TYPE=Ethernet
+        USERCTL=no
+        VLAN=yes
+            """).rstrip(' '),
+            'ifcfg-eth1': textwrap.dedent("""\
+        # Created by cloud-init on instance boot automatically, do not edit.
+        #
+        BOOTPROTO=none
+        DEVICE=eth1
+        HWADDR=aa:d6:9f:2c:e8:80
+        MASTER=bond0
+        NM_CONTROLLED=no
+        ONBOOT=yes
+        SLAVE=yes
+        TYPE=Ethernet
+        USERCTL=no
+            """).rstrip(' '),
+            'ifcfg-eth2': textwrap.dedent("""\
+        # Created by cloud-init on instance boot automatically, do not edit.
+        #
+        BOOTPROTO=none
+        DEVICE=eth2
+        HWADDR=c0:bb:9f:2c:e8:80
+        MASTER=bond0
+        NM_CONTROLLED=no
+        ONBOOT=yes
+        SLAVE=yes
+        TYPE=Ethernet
+        USERCTL=no
+            """).rstrip(' '),
+            'ifcfg-eth3': textwrap.dedent("""\
+        # Created by cloud-init on instance boot automatically, do not edit.
+        #
+        BOOTPROTO=none
+        BRIDGE=br0
+        DEVICE=eth3
+        HWADDR=66:bb:9f:2c:e8:80
+        NM_CONTROLLED=no
+        ONBOOT=yes
+        TYPE=Ethernet
+        USERCTL=no
+            """).rstrip(' '),
+            'ifcfg-eth4': textwrap.dedent("""\
+        # Created by cloud-init on instance boot automatically, do not edit.
+        #
+        BOOTPROTO=none
+        BRIDGE=br0
+        DEVICE=eth4
+        HWADDR=98:bb:9f:2c:e8:80
+        NM_CONTROLLED=no
+        ONBOOT=yes
+        TYPE=Ethernet
+        USERCTL=no
+            """).rstrip(' '),
+            'ifcfg-eth5': textwrap.dedent("""\
+        # Created by cloud-init on instance boot automatically, do not edit.
+        #
+        BOOTPROTO=dhcp
+        DEVICE=eth5
+        HWADDR=98:bb:9f:2c:e8:8a
+        NM_CONTROLLED=no
+        ONBOOT=yes
+        TYPE=Ethernet
+        USERCTL=no
+            """).rstrip(' ')},
         'yaml': textwrap.dedent("""
             version: 1
             config:
@@ -807,6 +977,10 @@ pre-down route del -net 10.0.0.0 netmask 255.0.0.0 gw 11.0.0.1 metric 3 || true
                         address: 192.168.14.2/24
                       - type: static
                         address: 2001:1::1/64 # default to /64
+                        routes:
+                          - gateway: 2001:4800:78ff:1b::1
+                            netmask: '::'
+                            network: '::'
                 # A global nameserver.
                 - type: nameserver
                   address: 8.8.8.8
@@ -828,6 +1002,7 @@ pre-down route del -net 10.0.0.0 netmask 255.0.0.0 gw 11.0.0.1 metric 3 || true
     }
 }
 
+
 CONFIG_V1_EXPLICIT_LOOPBACK = {
     'version': 1,
     'config': [{'name': 'eth0', 'type': 'physical',
@@ -836,6 +1011,18 @@ CONFIG_V1_EXPLICIT_LOOPBACK = {
                 'subnets': [{'control': 'auto', 'type': 'loopback'}]},
                ]}
 
+
+CONFIG_V1_SIMPLE_SUBNET = {
+    'version': 1,
+    'config': [{'mac_address': '52:54:00:12:34:00',
+                'name': 'interface0',
+                'subnets': [{'address': '10.0.2.15',
+                             'gateway': '10.0.2.2',
+                             'netmask': '255.255.255.0',
+                             'type': 'static'}],
+                'type': 'physical'}]}
+
+
 DEFAULT_DEV_ATTRS = {
     'eth1000': {
         "bridge": False,
@@ -1135,6 +1322,32 @@ USERCTL=no
                 with open(os.path.join(render_dir, fn)) as fh:
                     self.assertEqual(expected_content, fh.read())
 
+    def test_network_config_v1_samples(self):
+        ns = network_state.parse_net_config_data(CONFIG_V1_SIMPLE_SUBNET)
+        render_dir = self.tmp_path("render")
+        os.makedirs(render_dir)
+        renderer = sysconfig.Renderer()
+        renderer.render_network_state(ns, render_dir)
+        found = dir2dict(render_dir)
+        nspath = '/etc/sysconfig/network-scripts/'
+        self.assertNotIn(nspath + 'ifcfg-lo', found.keys())
+        expected = """\
+# Created by cloud-init on instance boot automatically, do not edit.
+#
+BOOTPROTO=none
+DEFROUTE=yes
+DEVICE=interface0
+GATEWAY=10.0.2.2
+HWADDR=52:54:00:12:34:00
+IPADDR=10.0.2.15
+NETMASK=255.255.255.0
+NM_CONTROLLED=no
+ONBOOT=yes
+TYPE=Ethernet
+USERCTL=no
+"""
+        self.assertEqual(expected, found[nspath + 'ifcfg-interface0'])
+
     def test_config_with_explicit_loopback(self):
         ns = network_state.parse_net_config_data(CONFIG_V1_EXPLICIT_LOOPBACK)
         render_dir = self.tmp_path("render")
@@ -1654,6 +1867,43 @@ class TestEniRoundTrip(CiTestCase):
             expected, [line for line in found if line])
 
 
+class TestSysconfigRoundTrip(CiTestCase):
+    def _render_and_read(self, network_config=None, state=None, dir=None):
+        if dir is None:
+            dir = self.tmp_dir()
+
+        if network_config:
+            ns = network_state.parse_net_config_data(network_config)
+        elif state:
+            ns = state
+        else:
+            raise ValueError("Expected data or state, got neither")
+
+        renderer = sysconfig.Renderer()
+        renderer.render_network_state(ns, dir)
+        return dir2dict(dir)
+
+    def testsimple_render_small(self):
+        entry = NETWORK_CONFIGS['small']
+        files = self._render_and_read(network_config=yaml.load(entry['yaml']))
+        expected_sysconfig = entry.get('expected_sysconfig')
+        for ifcfg_name in expected_sysconfig:
+            expected_ifcfg = expected_sysconfig.get(ifcfg_name)
+            ifcfg_path = '/etc/sysconfig/network-scripts/' + ifcfg_name
+            self.assertEqual(expected_ifcfg.splitlines(),
+                             files[ifcfg_path].splitlines())
+
+    def testsimple_render_all(self):
+        entry = NETWORK_CONFIGS['all']
+        files = self._render_and_read(network_config=yaml.load(entry['yaml']))
+        expected_sysconfig = entry.get('expected_sysconfig')
+        for ifcfg_name in expected_sysconfig:
+            expected_ifcfg = expected_sysconfig.get(ifcfg_name)
+            ifcfg_path = '/etc/sysconfig/network-scripts/' + ifcfg_name
+            self.assertEqual(expected_ifcfg.splitlines(),
+                             files[ifcfg_path].splitlines())
+
+
 class TestNetRenderers(CiTestCase):
     @mock.patch("cloudinit.net.renderers.sysconfig.available")
     @mock.patch("cloudinit.net.renderers.eni.available")

References