← Back to team overview

cloud-init-dev team mailing list archive

[Merge] ~smoser/cloud-init:feature/curtin-centos4 into cloud-init:master

 

Scott Moser has proposed merging ~smoser/cloud-init:feature/curtin-centos4 into cloud-init:master.

Requested reviews:
  cloud-init commiters (cloud-init-dev)
Related bugs:
  Bug #1695092 in cloud-init: "sysconfig only applies subnet/route config to physical interfaces"
  https://bugs.launchpad.net/cloud-init/+bug/1695092

For more details, see:
https://code.launchpad.net/~smoser/cloud-init/+git/cloud-init/+merge/327821
-- 
Your team cloud-init commiters is requested to review the proposed merge of ~smoser/cloud-init:feature/curtin-centos4 into cloud-init:master.
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 de6601a..eb3c91d 100644
--- a/cloudinit/net/sysconfig.py
+++ b/cloudinit/net/sysconfig.py
@@ -407,24 +407,41 @@ 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):
+            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)
+
+            # iter_interfaces on network-state is not sorted to produce
+            # consistent numbers we need to sort.
+            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']
@@ -432,6 +449,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("")
@@ -478,6 +500,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/tests/unittests/test_net.py b/tests/unittests/test_net.py
index 2224271..7b67e6c 100644
--- a/tests/unittests/test_net.py
+++ b/tests/unittests/test_net.py
@@ -825,7 +825,93 @@ pre-down route del -net 10.0.0.0 netmask 255.0.0.0 gw 11.0.0.1 metric 3 || true
                   gateway: 11.0.0.1
                   metric: 3
         """).lstrip(),
-    }
+    },
+    'bond': {
+        'yaml': textwrap.dedent("""
+            version: 1
+            config:
+              - type: physical
+                name: bond0s0
+                mac_address: "aa:bb:cc:dd:e8:00"
+              - type: physical
+                name: bond0s1
+                mac_address: "aa:bb:cc:dd:e8:01"
+              - type: bond
+                name: bond0
+                mac_address: "aa:bb:cc:dd:e8:ff"
+                bond_interfaces:
+                  - bond0s0
+                  - bond0s1
+                params:
+                  bond-mode: active-backup
+                  bond_miimon: 100
+                  bond-xmit-hash-policy: "layer3+4"
+                subnets:
+                  - type: static
+                    address: 192.168.0.2/24
+                    gateway: 192.168.0.1
+                    routes:
+                     - gateway: 192.168.0.3
+                       netmask: 255.255.255.0
+                       network: 10.1.3.0
+                  - type: static
+                    address: 192.168.1.2/24
+                  - type: static
+                    address: 2001:1::1/92
+            """),
+        'expected_sysconfig': {
+            'ifcfg-bond0': textwrap.dedent("""\
+        BONDING_MASTER=yes
+        BONDING_OPTS="mode=active-backup xmit_hash_policy=layer3+4 miimon=100"
+        BONDING_SLAVE0=bond0s0
+        BONDING_SLAVE1=bond0s1
+        BOOTPROTO=none
+        DEFROUTE=yes
+        DEVICE=bond0
+        GATEWAY=192.168.0.1
+        HWADDR=aa:bb:cc:dd:e8:ff
+        IPADDR=192.168.0.2
+        IPADDR1=192.168.1.2
+        IPV6ADDR=2001:1::1/92
+        IPV6INIT=yes
+        NETMASK=255.255.255.0
+        NETMASK1=255.255.255.0
+        NM_CONTROLLED=no
+        ONBOOT=yes
+        TYPE=Bond
+        USERCTL=no
+        """),
+            'ifcfg-bond0s0': textwrap.dedent("""\
+        BOOTPROTO=none
+        DEVICE=bond0s0
+        HWADDR=aa:bb:cc:dd:e8:00
+        MASTER=bond0
+        NM_CONTROLLED=no
+        ONBOOT=yes
+        SLAVE=yes
+        TYPE=Ethernet
+        USERCTL=no
+        """),
+            'route6-bond0': textwrap.dedent("""\
+            """),
+            'route-bond0': textwrap.dedent("""\
+        ADDRESS0=10.1.3.0
+        GATEWAY0=192.168.0.3
+        NETMASK0=255.255.255.0
+        """),
+            'ifcfg-bond0s1': textwrap.dedent("""\
+        BOOTPROTO=none
+        DEVICE=bond0s1
+        HWADDR=aa:bb:cc:dd:e8:01
+        MASTER=bond0
+        NM_CONTROLLED=no
+        ONBOOT=yes
+        SLAVE=yes
+        TYPE=Ethernet
+        USERCTL=no
+        """),
+        },
+    },
 }
 
 
@@ -1021,6 +1107,48 @@ iface eth1 inet dhcp
 
 class TestSysConfigRendering(CiTestCase):
 
+    scripts_dir = '/etc/sysconfig/network-scripts'
+    header = ('# Created by cloud-init on instance boot automatically, '
+              'do not edit.\n#\n')
+
+    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 _compare_files_to_expected(self, expected, found):
+        orig_maxdiff = self.maxDiff
+        expected_d = dict(
+            (os.path.join(self.scripts_dir, k), util.load_shell_content(v))
+            for k, v in expected.items())
+
+        # only compare the files in scripts_dir
+        scripts_found = dict(
+            (k, util.load_shell_content(v)) for k, v in found.items()
+            if k.startswith(self.scripts_dir))
+        try:
+            self.maxDiff = None
+            self.assertEqual(expected_d, scripts_found)
+        finally:
+            self.maxDiff = orig_maxdiff
+
+    def _assert_headers(self, found):
+        missing = [f for f in found
+                   if (f.startswith(self.scripts_dir) and
+                       not found[f].startswith(self.header))]
+        if missing:
+            raise AssertionError("Missing headers in: %s" % missing)
+
     @mock.patch("cloudinit.net.sys_dev_path")
     @mock.patch("cloudinit.net.read_sys_net")
     @mock.patch("cloudinit.net.get_devicelist")
@@ -1195,6 +1323,12 @@ USERCTL=no
 """
         self.assertEqual(expected, found[nspath + 'ifcfg-eth0'])
 
+    def test_bond_config(self):
+        entry = NETWORK_CONFIGS['bond']
+        found = self._render_and_read(network_config=yaml.load(entry['yaml']))
+        self._compare_files_to_expected(entry['expected_sysconfig'], found)
+        self._assert_headers(found)
+
 
 class TestEniNetRendering(CiTestCase):
 

Follow ups