From 4fb6fd8a046a6bcce01216c386f3b691a2c466bb Mon Sep 17 00:00:00 2001
From: Chad Smith <chad.smith@canonical.com>
Date: Mon, 30 Mar 2020 21:24:51 -0600
Subject: [PATCH] net: ubuntu focal prioritize netplan over eni even if both
 present (#267)

On Focal and later, Ubuntu will prioritize netplan renderer over eni,
even if ifupdown and netplan are both installed.

ENI on Focal and later is considered an unsupported configuration so
cloud-init should generally prefer netplan. On many cloud images,
the /etc/network/interfaces config file does not include the dir
/etc/network/interfaces.d thereby ignoring cloud-init's
/etc/network/interfaces.d/50-cloud-init.cfg file.

LP: #1867029
---
 config/cloud.cfg.tmpl                   |  3 +
 tests/unittests/test_net.py             | 86 +++++++++++++------------
 tests/unittests/test_render_cloudcfg.py | 57 ++++++++++++++++
 3 files changed, 106 insertions(+), 40 deletions(-)
 create mode 100644 tests/unittests/test_render_cloudcfg.py

--- a/config/cloud.cfg.tmpl
+++ b/config/cloud.cfg.tmpl
@@ -158,6 +158,9 @@ system_info:
      groups: [adm, audio, cdrom, dialout, dip, floppy, lxd, netdev, plugdev, sudo, video]
      sudo: ["ALL=(ALL) NOPASSWD:ALL"]
      shell: /bin/bash
+{# SRU_BLOCKER: do not ship network renderers on Xenial, Bionic or Eoan #}
+   network:
+     renderers: ['netplan', 'eni', 'sysconfig']
    # Automatically discover the best ntp_client
    ntp_client: auto
    # Other config here will be given to the distro class and/or path classes
--- a/tests/unittests/test_net.py
+++ b/tests/unittests/test_net.py
@@ -24,6 +24,7 @@ import re
 import textwrap
 from yaml.serializer import Serializer
 
+import pytest
 
 DHCP_CONTENT_1 = """
 DEVICE='eth0'
@@ -4671,6 +4672,51 @@ class TestEniRoundTrip(CiTestCase):
             files['/etc/network/interfaces'].splitlines())
 
 
+class TestRenderersSelect:
+
+    @pytest.mark.parametrize(
+        'renderer_selected,netplan,eni,nm,scfg,sys', (
+            # -netplan -ifupdown -nm -scfg -sys raises error
+            (net.RendererNotFoundError, False, False, False, False, False),
+            # -netplan +ifupdown -nm -scfg -sys selects eni
+            ('eni', False, True, False, False, False),
+            # +netplan +ifupdown -nm -scfg -sys selects eni
+            ('eni', True, True, False, False, False),
+            # +netplan -ifupdown -nm -scfg -sys selects netplan
+            ('netplan', True, False, False, False, False),
+            # Ubuntu with Network-Manager installed
+            # +netplan -ifupdown +nm -scfg -sys selects netplan
+            ('netplan', True, False, True, False, False),
+            # Centos/OpenSuse with Network-Manager installed selects sysconfig
+            # -netplan -ifupdown +nm -scfg +sys selects netplan
+            ('sysconfig', False, False, True, False, True),
+        ),
+    )
+    @mock.patch("cloudinit.net.renderers.netplan.available")
+    @mock.patch("cloudinit.net.renderers.sysconfig.available")
+    @mock.patch("cloudinit.net.renderers.sysconfig.available_sysconfig")
+    @mock.patch("cloudinit.net.renderers.sysconfig.available_nm")
+    @mock.patch("cloudinit.net.renderers.eni.available")
+    def test_valid_renderer_from_defaults_depending_on_availability(
+        self, m_eni_avail, m_nm_avail, m_scfg_avail, m_sys_avail,
+        m_netplan_avail, renderer_selected, netplan, eni, nm, scfg, sys
+    ):
+        """Assert proper renderer per DEFAULT_PRIORITY given availability."""
+        m_eni_avail.return_value = eni          # ifupdown pkg presence
+        m_nm_avail.return_value = nm            # network-manager presence
+        m_scfg_avail.return_value = scfg        # sysconfig presence
+        m_sys_avail.return_value = sys          # sysconfig/ifup/down presence
+        m_netplan_avail.return_value = netplan  # netplan presence
+        if isinstance(renderer_selected, str):
+            (renderer_name, _rnd_class) = renderers.select(
+                priority=renderers.DEFAULT_PRIORITY
+            )
+            assert renderer_selected == renderer_name
+        else:
+            with pytest.raises(renderer_selected):
+                renderers.select(priority=renderers.DEFAULT_PRIORITY)
+
+
 class TestNetRenderers(CiTestCase):
     @mock.patch("cloudinit.net.renderers.sysconfig.available")
     @mock.patch("cloudinit.net.renderers.eni.available")
@@ -4714,46 +4760,6 @@ class TestNetRenderers(CiTestCase):
         self.assertRaises(net.RendererNotFoundError, renderers.select,
                           priority=['sysconfig', 'eni'])
 
-    @mock.patch("cloudinit.net.renderers.netplan.available")
-    @mock.patch("cloudinit.net.renderers.sysconfig.available")
-    @mock.patch("cloudinit.net.renderers.sysconfig.available_sysconfig")
-    @mock.patch("cloudinit.net.renderers.sysconfig.available_nm")
-    @mock.patch("cloudinit.net.renderers.eni.available")
-    @mock.patch("cloudinit.net.renderers.sysconfig.util.get_linux_distro")
-    def test_sysconfig_selected_on_sysconfig_enabled_distros(self, m_distro,
-                                                             m_eni, m_sys_nm,
-                                                             m_sys_scfg,
-                                                             m_sys_avail,
-                                                             m_netplan):
-        """sysconfig only selected on specific distros (rhel/sles)."""
-
-        # Ubuntu with Network-Manager installed
-        m_eni.return_value = False        # no ifupdown (ifquery)
-        m_sys_scfg.return_value = False   # no sysconfig/ifup/ifdown
-        m_sys_nm.return_value = True      # network-manager is installed
-        m_netplan.return_value = True     # netplan is installed
-        m_sys_avail.return_value = False  # no sysconfig on Ubuntu
-        m_distro.return_value = ('ubuntu', None, None)
-        self.assertEqual('netplan', renderers.select(priority=None)[0])
-
-        # Centos with Network-Manager installed
-        m_eni.return_value = False       # no ifupdown (ifquery)
-        m_sys_scfg.return_value = False  # no sysconfig/ifup/ifdown
-        m_sys_nm.return_value = True     # network-manager is installed
-        m_netplan.return_value = False   # netplan is not installed
-        m_sys_avail.return_value = True  # sysconfig is available on centos
-        m_distro.return_value = ('centos', None, None)
-        self.assertEqual('sysconfig', renderers.select(priority=None)[0])
-
-        # OpenSuse with Network-Manager installed
-        m_eni.return_value = False       # no ifupdown (ifquery)
-        m_sys_scfg.return_value = False  # no sysconfig/ifup/ifdown
-        m_sys_nm.return_value = True     # network-manager is installed
-        m_netplan.return_value = False   # netplan is not installed
-        m_sys_avail.return_value = True  # sysconfig is available on opensuse
-        m_distro.return_value = ('opensuse', None, None)
-        self.assertEqual('sysconfig', renderers.select(priority=None)[0])
-
     @mock.patch("cloudinit.net.sysconfig.available_sysconfig")
     @mock.patch("cloudinit.util.get_linux_distro")
     def test_sysconfig_available_uses_variant_mapping(self, m_distro, m_avail):
--- /dev/null
+++ b/tests/unittests/test_render_cloudcfg.py
@@ -0,0 +1,57 @@
+"""Tests for tools/render-cloudcfg"""
+
+import os
+import sys
+
+import pytest
+
+from cloudinit import util
+
+# TODO(Look to align with tools.render-cloudcfg or cloudinit.distos.OSFAMILIES)
+DISTRO_VARIANTS = ["amazon", "arch", "centos", "debian", "fedora", "freebsd",
+                   "netbsd", "openbsd", "rhel", "suse", "ubuntu", "unknown"]
+
+
+class TestRenderCloudCfg:
+
+    cmd = [sys.executable, os.path.realpath('tools/render-cloudcfg')]
+    tmpl_path = os.path.realpath('config/cloud.cfg.tmpl')
+
+    @pytest.mark.parametrize('variant', (DISTRO_VARIANTS))
+    def test_variant_sets_distro_in_cloud_cfg(self, variant, tmpdir):
+        outfile = tmpdir.join('outcfg').strpath
+        util.subp(
+            self.cmd + ['--variant', variant, self.tmpl_path, outfile])
+        with open(outfile) as stream:
+            system_cfg = util.load_yaml(stream.read())
+        if variant == 'unknown':
+            variant = 'ubuntu'  # Unknown is defaulted to ubuntu
+        assert system_cfg['system_info']['distro'] == variant
+
+    @pytest.mark.parametrize('variant', (DISTRO_VARIANTS))
+    def test_variant_sets_default_user_in_cloud_cfg(self, variant, tmpdir):
+        outfile = tmpdir.join('outcfg').strpath
+        util.subp(
+            self.cmd + ['--variant', variant, self.tmpl_path, outfile])
+        with open(outfile) as stream:
+            system_cfg = util.load_yaml(stream.read())
+
+        default_user_exceptions = {
+            'amazon': 'ec2-user', 'debian': 'ubuntu', 'unknown': 'ubuntu'}
+        default_user = system_cfg['system_info']['default_user']['name']
+        assert default_user == default_user_exceptions.get(variant, variant)
+
+    @pytest.mark.parametrize('variant,renderers', (
+        ('freebsd', ['freebsd']), ('netbsd', ['netbsd']),
+        ('openbsd', ['openbsd']), ('ubuntu', ['netplan', 'eni', 'sysconfig']))
+    )
+    def test_variant_sets_network_renderer_priority_in_cloud_cfg(
+        self, variant, renderers, tmpdir
+    ):
+        outfile = tmpdir.join('outcfg').strpath
+        util.subp(
+            self.cmd + ['--variant', variant, self.tmpl_path, outfile])
+        with open(outfile) as stream:
+            system_cfg = util.load_yaml(stream.read())
+
+        assert renderers == system_cfg['system_info']['network']['renderers']
