From c68305a91791e28031df8b9ebd33bfe7ffd7e75d Mon Sep 17 00:00:00 2001
From: James Falcon <james.falcon@canonical.com>
Date: Tue, 4 Jul 2023 07:00:22 -0500
Subject: [PATCH] Fix network v2 metric rendering (#4220)

Reference:https://github.com/canonical/cloud-init/commit/c68305a91791e28031df8b9ebd33bfe7ffd7e75d
Conflict:(1)change 'small' to 'small_v1'
(2)do not change TestNetworkManagerRendering.
(3)do not add "NM_CONTROLLED=no" in test because of cloud-init-20.4-nm-controlled.patch.
(4)format diffs.

Metric info was not being included in v2-based routes.

Fixes GH-4217
---
 cloudinit/net/network_state.py |   4 +-
 tests/unittests/test_net.py    | 232 ++++++++++++++++++++++++++++++---
 2 files changed, 216 insertions(+), 20 deletions(-)

diff --git a/cloudinit/net/network_state.py b/cloudinit/net/network_state.py
index 4862bf9..ac44304 100644
--- a/cloudinit/net/network_state.py
+++ b/cloudinit/net/network_state.py
@@ -187,7 +187,6 @@ class NetworkState(object):
 
 
 class NetworkStateInterpreter(metaclass=CommandHandlerMeta):
-
     initial_network_state = {
         'interfaces': {},
         'routes': [],
@@ -582,7 +581,6 @@ class NetworkStateInterpreter(metaclass=CommandHandlerMeta):
         self._handle_bond_bridge(command, cmd_type='bond')
 
     def handle_bridges(self, command):
-
         '''
         v2_command = {
           br0: {
@@ -815,7 +813,7 @@ class NetworkStateInterpreter(metaclass=CommandHandlerMeta):
         routes = []
         for route in cfg.get('routes', []):
             routes.append(_normalize_route(
-                {'destination': route.get('to'), 'gateway': route.get('via')}))
+                {'destination': route.get('to'), 'gateway': route.get('via'), "metric": route.get("metric"),}))
 
         # v2 routes are bound to the interface, in v1 we add them under
         # the first subnet since there isn't an equivalent interface level.
diff --git a/tests/unittests/test_net.py b/tests/unittests/test_net.py
index 7cde102..764e1c7 100644
--- a/tests/unittests/test_net.py
+++ b/tests/unittests/test_net.py
@@ -818,7 +818,7 @@ iface eth1 inet static
 """.lstrip()
 
 NETWORK_CONFIGS = {
-    'small': {
+    'small_v1': {
         'expected_networkd_eth99': textwrap.dedent("""\
             [Match]
             Name=eth99
@@ -959,6 +959,164 @@ NETWORK_CONFIGS = {
                     - wark.maas
         """),
     },
+    # We test a separate set of configs here because v2 doesn't support
+    # generic nameservers, so that aspect needs to be modified
+    "small_v2": {
+        "expected_networkd_eth99": textwrap.dedent(
+            """\
+            [Match]
+            Name=eth99
+            MACAddress=c0:d6:9f:2c:e8:80
+            [Address]
+            Address=192.168.21.3/24
+            [Network]
+            DHCP=ipv4
+            Domains=barley.maas sach.maas
+            DNS=8.8.8.8 8.8.4.4
+            [Route]
+            Gateway=65.61.151.37
+            Destination=0.0.0.0/0
+            Metric=10000
+        """
+        ).rstrip(" "),
+        "expected_networkd_eth1": textwrap.dedent(
+            """\
+            [Match]
+            Name=eth1
+            MACAddress=cf:d6:af:48:e8:80
+            [Network]
+            DHCP=no
+        """
+        ).rstrip(" "),
+        "expected_eni": textwrap.dedent(
+            """\
+            auto lo
+            iface lo inet loopback
+                dns-nameservers 8.8.8.8 8.8.4.4
+                dns-search wark.maas
+            iface eth1 inet manual
+            auto eth99
+            iface eth99 inet dhcp
+            # control-alias eth99
+            iface eth99 inet static
+                address 192.168.21.3/24
+                dns-nameservers 8.8.8.8 8.8.4.4
+                dns-search barley.maas sach.maas
+                post-up route add default gw 65.61.151.37 metric 10000 || true
+                pre-down route del default gw 65.61.151.37 metric 10000 || true
+        """
+        ).rstrip(" "),
+        "expected_sysconfig_opensuse": {
+            "ifcfg-eth1": textwrap.dedent(
+                """\
+                BOOTPROTO=static
+                LLADDR=cf:d6:af:48:e8:80
+                STARTMODE=auto"""
+            ),
+            "ifcfg-eth99": textwrap.dedent(
+                """\
+                BOOTPROTO=dhcp4
+                LLADDR=c0:d6:9f:2c:e8:80
+                IPADDR=192.168.21.3
+                NETMASK=255.255.255.0
+                STARTMODE=auto"""
+            ),
+        },
+        "expected_sysconfig_rhel": {
+            "ifcfg-eth1": textwrap.dedent(
+                """\
+                BOOTPROTO=none
+                DEVICE=eth1
+                HWADDR=cf:d6:af:48:e8:80
+                ONBOOT=yes
+                TYPE=Ethernet
+                USERCTL=no"""
+            ),
+            "ifcfg-eth99": textwrap.dedent(
+                """\
+                BOOTPROTO=dhcp
+                DEFROUTE=yes
+                DEVICE=eth99
+                DHCLIENT_SET_DEFAULT_ROUTE=yes
+                DNS1=8.8.8.8
+                DNS2=8.8.4.4
+                DOMAIN="barley.maas sach.maas"
+                GATEWAY=65.61.151.37
+                HWADDR=c0:d6:9f:2c:e8:80
+                IPADDR=192.168.21.3
+                NETMASK=255.255.255.0
+                METRIC=10000
+                ONBOOT=yes
+                TYPE=Ethernet
+                USERCTL=no"""
+            ),
+        },
+        "expected_network_manager": {
+            "cloud-init-eth1.nmconnection": textwrap.dedent(
+                """\
+                # Generated by cloud-init. Changes will be lost.
+                [connection]
+                id=cloud-init eth1
+                uuid=3c50eb47-7260-5a6d-801d-bd4f587d6b58
+                autoconnect-priority=120
+                type=ethernet
+                [user]
+                org.freedesktop.NetworkManager.origin=cloud-init
+                [ethernet]
+                mac-address=CF:D6:AF:48:E8:80
+                """
+            ),
+            "cloud-init-eth99.nmconnection": textwrap.dedent(
+                """\
+                # Generated by cloud-init. Changes will be lost.
+                [connection]
+                id=cloud-init eth99
+                uuid=b1b88000-1f03-5360-8377-1a2205efffb4
+                autoconnect-priority=120
+                type=ethernet
+                [user]
+                org.freedesktop.NetworkManager.origin=cloud-init
+                [ethernet]
+                mac-address=C0:D6:9F:2C:E8:80
+                [ipv4]
+                method=auto
+                may-fail=false
+                route1=0.0.0.0/0,65.61.151.37
+                address1=192.168.21.3/24
+                dns=8.8.8.8;8.8.4.4;
+                dns-search=barley.maas;sach.maas;
+                """
+            ),
+        },
+        "yaml": textwrap.dedent(
+            """
+            version: 2
+            ethernets:
+                eth1:
+                    match:
+                        macaddress: cf:d6:af:48:e8:80
+                    set-name: eth1
+                eth99:
+                    addresses:
+                    - 192.168.21.3/24
+                    dhcp4: true
+                    match:
+                        macaddress: c0:d6:9f:2c:e8:80
+                    nameservers:
+                        addresses:
+                        - 8.8.8.8
+                        - 8.8.4.4
+                        search:
+                        - barley.maas
+                        - sach.maas
+                    routes:
+                    -   metric: 10000
+                        to: 0.0.0.0/0
+                        via: 65.61.151.37
+                    set-name: eth99
+            """
+        ),
+    },
     'v4_and_v6': {
         'expected_networkd': textwrap.dedent("""\
             [Match]
@@ -2965,7 +3123,6 @@ iface eth1 inet dhcp
     mock.Mock(return_value=False)
 )
 class TestRhelSysConfigRendering(CiTestCase):
-
     with_logs = True
 
     nm_cfg_file = "/etc/NetworkManager/NetworkManager.conf"
@@ -3286,8 +3443,14 @@ USERCTL=no
             'WARNING: Network config: ignoring eth0.101 device-level mtu',
             self.logs.getvalue())
 
-    def test_small_config(self):
-        entry = NETWORK_CONFIGS['small']
+    def test_small_config_v1(self):
+        entry = NETWORK_CONFIGS["small_v1"]
+        found = self._render_and_read(network_config=yaml.load(entry["yaml"]))
+        self._compare_files_to_expected(entry[self.expected_name], found)
+        self._assert_headers(found)
+
+    def test_small_config_v2(self):
+        entry = NETWORK_CONFIGS["small_v2"]
         found = self._render_and_read(network_config=yaml.load(entry['yaml']))
         self._compare_files_to_expected(entry[self.expected_name], found)
         self._assert_headers(found)
@@ -3429,7 +3592,7 @@ USERCTL=no
         self.assertTrue(os.path.exists(nm_cfg))
 
         # render and read
-        entry = NETWORK_CONFIGS['small']
+        entry = NETWORK_CONFIGS['small_v1']
         found = self._render_and_read(network_config=yaml.load(entry['yaml']),
                                       dir=render_dir)
         self._compare_files_to_expected(entry[self.expected_name], found)
@@ -3450,7 +3613,7 @@ USERCTL=no
         util.write_file(nm_cfg, '# test_check_ifcfg_rh\n[main]\nplugins=foo\n')
 
         # render and read
-        entry = NETWORK_CONFIGS['small']
+        entry = NETWORK_CONFIGS['small_v1']
         found = self._render_and_read(network_config=yaml.load(entry['yaml']),
                                       dir=render_dir)
         self._compare_files_to_expected(entry[self.expected_name], found)
@@ -3476,7 +3639,7 @@ USERCTL=no
         self.assertTrue(os.path.exists(nm_cfg))
 
         # render and read
-        entry = NETWORK_CONFIGS['small']
+        entry = NETWORK_CONFIGS['small_v1']
         found = self._render_and_read(network_config=yaml.load(entry['yaml']),
                                       dir=render_dir)
         self._compare_files_to_expected(entry[self.expected_name], found)
@@ -3644,7 +3807,6 @@ USERCTL=no
     mock.Mock(return_value=False)
 )
 class TestOpenSuseSysConfigRendering(CiTestCase):
-
     with_logs = True
 
     scripts_dir = '/etc/sysconfig/network'
@@ -3916,8 +4078,14 @@ STARTMODE=auto
             'WARNING: Network config: ignoring eth0.101 device-level mtu',
             self.logs.getvalue())
 
-    def test_small_config(self):
-        entry = NETWORK_CONFIGS['small']
+    def test_small_config_v1(self):
+        entry = NETWORK_CONFIGS["small_v1"]
+        found = self._render_and_read(network_config=yaml.load(entry["yaml"]))
+        self._compare_files_to_expected(entry[self.expected_name], found)
+        self._assert_headers(found)
+
+    def test_small_config_v2(self):
+        entry = NETWORK_CONFIGS["small_v1"]
         found = self._render_and_read(network_config=yaml.load(entry['yaml']))
         self._compare_files_to_expected(entry[self.expected_name], found)
         self._assert_headers(found)
@@ -4539,7 +4707,6 @@ class TestReadInitramfsConfig(CiTestCase):
 
 
 class TestNetplanRoundTrip(CiTestCase):
-
     NETPLAN_INFO_OUT = textwrap.dedent("""
     netplan.io:
       features:
@@ -4596,7 +4763,7 @@ class TestNetplanRoundTrip(CiTestCase):
             files['/etc/netplan/50-cloud-init.yaml'].splitlines())
 
     def testsimple_render_small_netplan(self):
-        entry = NETWORK_CONFIGS['small']
+        entry = NETWORK_CONFIGS['small_v1']
         files = self._render_and_read(network_config=yaml.load(entry['yaml']))
         self.assertEqual(
             entry['expected_netplan'].splitlines(),
@@ -4781,8 +4948,17 @@ class TestEniRoundTrip(CiTestCase):
             entry['expected_eni'].splitlines(),
             files['/etc/network/interfaces'].splitlines())
 
-    def testsimple_render_small(self):
-        entry = NETWORK_CONFIGS['small']
+    def testsimple_render_small_v1(self):
+        entry = NETWORK_CONFIGS["small_v1"]
+        files = self._render_and_read(network_config=yaml.load(entry["yaml"]))
+        self.assertEqual(
+            entry["expected_eni"].splitlines(),
+            files["/etc/network/interfaces"].splitlines(),
+        )
+
+    @pytest.mark.xfail(reason="GH-4219")
+    def testsimple_render_small_v2(self):
+        entry = NETWORK_CONFIGS["small_v2"]
         files = self._render_and_read(network_config=yaml.load(entry['yaml']))
         self.assertEqual(
             entry['expected_eni'].splitlines(),
@@ -5105,10 +5281,33 @@ class TestNetworkdRoundTrip(CiTestCase):
         return dir2dict(dir)
 
     @mock.patch("cloudinit.net.util.chownbyname", return_value=True)
-    def testsimple_render_small_networkd(self, m_chown):
+    def testsimple_render_small_networkd_v1(self, m_chown):
+        nwk_fn1 = "/etc/systemd/network/10-cloud-init-eth99.network"
+        nwk_fn2 = "/etc/systemd/network/10-cloud-init-eth1.network"
+        entry = NETWORK_CONFIGS["small_v1"]
+        files = self._render_and_read(network_config=yaml.load(entry["yaml"]))
+
+        actual = files[nwk_fn1].splitlines()
+        actual = self.create_conf_dict(actual)
+
+        expected = entry["expected_networkd_eth99"].splitlines()
+        expected = self.create_conf_dict(expected)
+
+        self.compare_dicts(actual, expected)
+
+        actual = files[nwk_fn2].splitlines()
+        actual = self.create_conf_dict(actual)
+
+        expected = entry["expected_networkd_eth1"].splitlines()
+        expected = self.create_conf_dict(expected)
+
+        self.compare_dicts(actual, expected)
+
+    @mock.patch("cloudinit.net.util.chownbyname", return_value=True)
+    def testsimple_render_small_networkd_v2(self, m_chown):
         nwk_fn1 = '/etc/systemd/network/10-cloud-init-eth99.network'
         nwk_fn2 = '/etc/systemd/network/10-cloud-init-eth1.network'
-        entry = NETWORK_CONFIGS['small']
+        entry = NETWORK_CONFIGS['small_v2']
         files = self._render_and_read(network_config=yaml.load(entry['yaml']))
 
         actual = files[nwk_fn1].splitlines()
@@ -5735,7 +5934,6 @@ class TestInterfacesSorting(CiTestCase):
     mock.Mock(return_value=False)
 )
 class TestGetIBHwaddrsByInterface(CiTestCase):
-
     _ib_addr = '80:00:00:28:fe:80:00:00:00:00:00:00:00:11:22:03:00:33:44:56'
     _ib_addr_eth_format = '00:11:22:33:44:56'
     _data = {'devices': ['enp0s1', 'enp0s2', 'bond1', 'bridge1',
-- 
2.33.0


