69219a
From b7b814bc0f4e7b63b50106d292e91f2c9555ad87 Mon Sep 17 00:00:00 2001
69219a
From: Eduardo Otubo <otubo@redhat.com>
69219a
Date: Mon, 4 May 2020 12:40:00 +0200
69219a
Subject: [PATCH 3/6] azure/net: generate_fallback_nic emits network v2 config
69219a
 instead of v1
69219a
69219a
RH-Author: Eduardo Otubo <otubo@redhat.com>
69219a
Message-id: <20200327152826.13343-4-otubo@redhat.com>
69219a
Patchwork-id: 94460
69219a
O-Subject: [RHEL-8.1.z/RHEL-8.2.z cloud-init PATCHv2 3/6] azure/net: generate_fallback_nic emits network v2 config instead of v1
69219a
Bugzilla: 1811753
69219a
RH-Acked-by: Cathy Avery <cavery@redhat.com>
69219a
RH-Acked-by: Vitaly Kuznetsov <vkuznets@redhat.com>
69219a
69219a
commit 7f674256c1426ffc419fd6b13e66a58754d94939
69219a
Author: Chad Smith <chad.smith@canonical.com>
69219a
Date:   Tue Aug 13 20:13:05 2019 +0000
69219a
69219a
    azure/net: generate_fallback_nic emits network v2 config instead of v1
69219a
69219a
    The function generate_fallback_config is used by Azure by default when
69219a
    not consuming IMDS configuration data. This function is also used by any
69219a
    datasource which does not implement it's own network config. This simple
69219a
    fallback configuration sets up dhcp on the most likely NIC. It will now
69219a
    emit network v2 instead of network v1.
69219a
69219a
    This is a step toward moving all components talking in v2 and allows us
69219a
    to avoid costly conversions between v1 and v2 for newer distributions
69219a
    which rely on netplan.
69219a
69219a
Signed-off-by: Eduardo Otubo <otubo@redhat.com>
69219a
Signed-off-by: Miroslav Rezanina <mrezanin@redhat.com>
69219a
---
69219a
 cloudinit/net/__init__.py                     | 31 +++++---------
69219a
 cloudinit/net/network_state.py                | 12 ++++--
69219a
 cloudinit/net/tests/test_init.py              | 19 +++++----
69219a
 cloudinit/sources/DataSourceAzure.py          |  7 +++-
69219a
 tests/unittests/test_datasource/test_azure.py | 59 ++++++++++++++++++++++++++-
69219a
 tests/unittests/test_net.py                   | 41 +++++++++++++++++--
69219a
 6 files changed, 130 insertions(+), 39 deletions(-)
69219a
69219a
diff --git a/cloudinit/net/__init__.py b/cloudinit/net/__init__.py
69219a
index 41659b1..b26a190 100644
69219a
--- a/cloudinit/net/__init__.py
69219a
+++ b/cloudinit/net/__init__.py
69219a
@@ -268,32 +268,23 @@ def find_fallback_nic(blacklist_drivers=None):
69219a
 
69219a
 
69219a
 def generate_fallback_config(blacklist_drivers=None, config_driver=None):
69219a
-    """Determine which attached net dev is most likely to have a connection and
69219a
-       generate network state to run dhcp on that interface"""
69219a
-
69219a
+    """Generate network cfg v2 for dhcp on the NIC most likely connected."""
69219a
     if not config_driver:
69219a
         config_driver = False
69219a
 
69219a
     target_name = find_fallback_nic(blacklist_drivers=blacklist_drivers)
69219a
-    if target_name:
69219a
-        target_mac = read_sys_net_safe(target_name, 'address')
69219a
-        nconf = {'config': [], 'version': 1}
69219a
-        cfg = {'type': 'physical', 'name': target_name,
69219a
-               'mac_address': target_mac, 'subnets': [{'type': 'dhcp'}]}
69219a
-        # inject the device driver name, dev_id into config if enabled and
69219a
-        # device has a valid device driver value
69219a
-        if config_driver:
69219a
-            driver = device_driver(target_name)
69219a
-            if driver:
69219a
-                cfg['params'] = {
69219a
-                    'driver': driver,
69219a
-                    'device_id': device_devid(target_name),
69219a
-                }
69219a
-        nconf['config'].append(cfg)
69219a
-        return nconf
69219a
-    else:
69219a
+    if not target_name:
69219a
         # can't read any interfaces addresses (or there are none); give up
69219a
         return None
69219a
+    target_mac = read_sys_net_safe(target_name, 'address')
69219a
+    cfg = {'dhcp4': True, 'set-name': target_name,
69219a
+           'match': {'macaddress': target_mac.lower()}}
69219a
+    if config_driver:
69219a
+        driver = device_driver(target_name)
69219a
+        if driver:
69219a
+            cfg['match']['driver'] = driver
69219a
+    nconf = {'ethernets': {target_name: cfg}, 'version': 2}
69219a
+    return nconf
69219a
 
69219a
 
69219a
 def apply_network_config_names(netcfg, strict_present=True, strict_busy=True):
69219a
diff --git a/cloudinit/net/network_state.py b/cloudinit/net/network_state.py
69219a
index 4d19f56..71d3c0f 100644
69219a
--- a/cloudinit/net/network_state.py
69219a
+++ b/cloudinit/net/network_state.py
69219a
@@ -596,6 +596,7 @@ class NetworkStateInterpreter(object):
69219a
           eno1:
69219a
             match:
69219a
               macaddress: 00:11:22:33:44:55
69219a
+              driver: hv_netsvc
69219a
             wakeonlan: true
69219a
             dhcp4: true
69219a
             dhcp6: false
69219a
@@ -631,15 +632,18 @@ class NetworkStateInterpreter(object):
69219a
                 'type': 'physical',
69219a
                 'name': cfg.get('set-name', eth),
69219a
             }
69219a
-            mac_address = cfg.get('match', {}).get('macaddress', None)
69219a
+            match = cfg.get('match', {})
69219a
+            mac_address = match.get('macaddress', None)
69219a
             if not mac_address:
69219a
                 LOG.debug('NetworkState Version2: missing "macaddress" info '
69219a
                           'in config entry: %s: %s', eth, str(cfg))
69219a
-            phy_cmd.update({'mac_address': mac_address})
69219a
-
69219a
+            phy_cmd['mac_address'] = mac_address
69219a
+            driver = match.get('driver', None)
69219a
+            if driver:
69219a
+                phy_cmd['params'] = {'driver': driver}
69219a
             for key in ['mtu', 'match', 'wakeonlan']:
69219a
                 if key in cfg:
69219a
-                    phy_cmd.update({key: cfg.get(key)})
69219a
+                    phy_cmd[key] = cfg[key]
69219a
 
69219a
             subnets = self._v2_to_v1_ipcfg(cfg)
69219a
             if len(subnets) > 0:
69219a
diff --git a/cloudinit/net/tests/test_init.py b/cloudinit/net/tests/test_init.py
69219a
index 5519867..5349176 100644
69219a
--- a/cloudinit/net/tests/test_init.py
69219a
+++ b/cloudinit/net/tests/test_init.py
69219a
@@ -218,9 +218,9 @@ class TestGenerateFallbackConfig(CiTestCase):
69219a
         mac = 'aa:bb:cc:aa:bb:cc'
69219a
         write_file(os.path.join(self.sysdir, 'eth1', 'address'), mac)
69219a
         expected = {
69219a
-            'config': [{'type': 'physical', 'mac_address': mac,
69219a
-                        'name': 'eth1', 'subnets': [{'type': 'dhcp'}]}],
69219a
-            'version': 1}
69219a
+            'ethernets': {'eth1': {'match': {'macaddress': mac},
69219a
+                                   'dhcp4': True, 'set-name': 'eth1'}},
69219a
+            'version': 2}
69219a
         self.assertEqual(expected, net.generate_fallback_config())
69219a
 
69219a
     def test_generate_fallback_finds_dormant_eth_with_mac(self):
69219a
@@ -229,9 +229,9 @@ class TestGenerateFallbackConfig(CiTestCase):
69219a
         mac = 'aa:bb:cc:aa:bb:cc'
69219a
         write_file(os.path.join(self.sysdir, 'eth0', 'address'), mac)
69219a
         expected = {
69219a
-            'config': [{'type': 'physical', 'mac_address': mac,
69219a
-                        'name': 'eth0', 'subnets': [{'type': 'dhcp'}]}],
69219a
-            'version': 1}
69219a
+            'ethernets': {'eth0': {'match': {'macaddress': mac}, 'dhcp4': True,
69219a
+                                   'set-name': 'eth0'}},
69219a
+            'version': 2}
69219a
         self.assertEqual(expected, net.generate_fallback_config())
69219a
 
69219a
     def test_generate_fallback_finds_eth_by_operstate(self):
69219a
@@ -239,9 +239,10 @@ class TestGenerateFallbackConfig(CiTestCase):
69219a
         mac = 'aa:bb:cc:aa:bb:cc'
69219a
         write_file(os.path.join(self.sysdir, 'eth0', 'address'), mac)
69219a
         expected = {
69219a
-            'config': [{'type': 'physical', 'mac_address': mac,
69219a
-                        'name': 'eth0', 'subnets': [{'type': 'dhcp'}]}],
69219a
-            'version': 1}
69219a
+            'ethernets': {
69219a
+                'eth0': {'dhcp4': True, 'match': {'macaddress': mac},
69219a
+                         'set-name': 'eth0'}},
69219a
+            'version': 2}
69219a
         valid_operstates = ['dormant', 'down', 'lowerlayerdown', 'unknown']
69219a
         for state in valid_operstates:
69219a
             write_file(os.path.join(self.sysdir, 'eth0', 'operstate'), state)
69219a
diff --git a/cloudinit/sources/DataSourceAzure.py b/cloudinit/sources/DataSourceAzure.py
69219a
index 66bbe5e..8cb7e5c 100755
69219a
--- a/cloudinit/sources/DataSourceAzure.py
69219a
+++ b/cloudinit/sources/DataSourceAzure.py
69219a
@@ -1242,7 +1242,7 @@ def parse_network_config(imds_metadata):
69219a
                     privateIpv4 = addr4['privateIpAddress']
69219a
                     if privateIpv4:
69219a
                         if dev_config.get('dhcp4', False):
69219a
-                            # Append static address config for nic > 1
69219a
+                            # Append static address config for ip > 1
69219a
                             netPrefix = intf['ipv4']['subnet'][0].get(
69219a
                                 'prefix', '24')
69219a
                             if not dev_config.get('addresses'):
69219a
@@ -1252,6 +1252,11 @@ def parse_network_config(imds_metadata):
69219a
                                     ip=privateIpv4, prefix=netPrefix))
69219a
                         else:
69219a
                             dev_config['dhcp4'] = True
69219a
+                            # non-primary interfaces should have a higher
69219a
+                            # route-metric (cost) so default routes prefer
69219a
+                            # primary nic due to lower route-metric value
69219a
+                            dev_config['dhcp4-overrides'] = {
69219a
+                                'route-metric': (idx + 1) * 100}
69219a
                 for addr6 in intf['ipv6']['ipAddress']:
69219a
                     privateIpv6 = addr6['privateIpAddress']
69219a
                     if privateIpv6:
69219a
diff --git a/tests/unittests/test_datasource/test_azure.py b/tests/unittests/test_datasource/test_azure.py
69219a
index 1fb0565..f2ff967 100644
69219a
--- a/tests/unittests/test_datasource/test_azure.py
69219a
+++ b/tests/unittests/test_datasource/test_azure.py
69219a
@@ -13,6 +13,7 @@ from cloudinit.tests.helpers import (
69219a
     HttprettyTestCase, CiTestCase, populate_dir, mock, wrap_and_call,
69219a
     ExitStack, PY26, SkipTest, resourceLocation)
69219a
 
69219a
+import copy
69219a
 import crypt
69219a
 import httpretty
69219a
 import json
69219a
@@ -111,6 +112,26 @@ NETWORK_METADATA = {
69219a
     }
69219a
 }
69219a
 
69219a
+SECONDARY_INTERFACE = {
69219a
+    "macAddress": "220D3A047598",
69219a
+    "ipv6": {
69219a
+        "ipAddress": []
69219a
+    },
69219a
+    "ipv4": {
69219a
+        "subnet": [
69219a
+            {
69219a
+                "prefix": "24",
69219a
+                "address": "10.0.1.0"
69219a
+            }
69219a
+        ],
69219a
+        "ipAddress": [
69219a
+            {
69219a
+                "privateIpAddress": "10.0.1.5",
69219a
+            }
69219a
+        ]
69219a
+    }
69219a
+}
69219a
+
69219a
 MOCKPATH = 'cloudinit.sources.DataSourceAzure.'
69219a
 
69219a
 
69219a
@@ -632,8 +653,43 @@ fdescfs            /dev/fd          fdescfs rw              0 0
69219a
             'ethernets': {
69219a
                 'eth0': {'set-name': 'eth0',
69219a
                          'match': {'macaddress': '00:0d:3a:04:75:98'},
69219a
-                         'dhcp4': True}},
69219a
+                         'dhcp4': True,
69219a
+                         'dhcp4-overrides': {'route-metric': 100}}},
69219a
+            'version': 2}
69219a
+        dsrc = self._get_ds(data)
69219a
+        dsrc.get_data()
69219a
+        self.assertEqual(expected_network_config, dsrc.network_config)
69219a
+
69219a
+    def test_network_config_set_from_imds_route_metric_for_secondary_nic(self):
69219a
+        """Datasource.network_config adds route-metric to secondary nics."""
69219a
+        sys_cfg = {'datasource': {'Azure': {'apply_network_config': True}}}
69219a
+        odata = {}
69219a
+        data = {'ovfcontent': construct_valid_ovf_env(data=odata),
69219a
+                'sys_cfg': sys_cfg}
69219a
+        expected_network_config = {
69219a
+            'ethernets': {
69219a
+                'eth0': {'set-name': 'eth0',
69219a
+                         'match': {'macaddress': '00:0d:3a:04:75:98'},
69219a
+                         'dhcp4': True,
69219a
+                         'dhcp4-overrides': {'route-metric': 100}},
69219a
+                'eth1': {'set-name': 'eth1',
69219a
+                         'match': {'macaddress': '22:0d:3a:04:75:98'},
69219a
+                         'dhcp4': True,
69219a
+                         'dhcp4-overrides': {'route-metric': 200}},
69219a
+                'eth2': {'set-name': 'eth2',
69219a
+                         'match': {'macaddress': '33:0d:3a:04:75:98'},
69219a
+                         'dhcp4': True,
69219a
+                         'dhcp4-overrides': {'route-metric': 300}}},
69219a
             'version': 2}
69219a
+        imds_data = copy.deepcopy(NETWORK_METADATA)
69219a
+        imds_data['network']['interface'].append(SECONDARY_INTERFACE)
69219a
+        third_intf = copy.deepcopy(SECONDARY_INTERFACE)
69219a
+        third_intf['macAddress'] = third_intf['macAddress'].replace('22', '33')
69219a
+        third_intf['ipv4']['subnet'][0]['address'] = '10.0.2.0'
69219a
+        third_intf['ipv4']['ipAddress'][0]['privateIpAddress'] = '10.0.2.6'
69219a
+        imds_data['network']['interface'].append(third_intf)
69219a
+
69219a
+        self.m_get_metadata_from_imds.return_value = imds_data
69219a
         dsrc = self._get_ds(data)
69219a
         dsrc.get_data()
69219a
         self.assertEqual(expected_network_config, dsrc.network_config)
69219a
@@ -936,6 +992,7 @@ fdescfs            /dev/fd          fdescfs rw              0 0
69219a
         expected_cfg = {
69219a
             'ethernets': {
69219a
                 'eth0': {'dhcp4': True,
69219a
+                         'dhcp4-overrides': {'route-metric': 100},
69219a
                          'match': {'macaddress': '00:0d:3a:04:75:98'},
69219a
                          'set-name': 'eth0'}},
69219a
             'version': 2}
69219a
diff --git a/tests/unittests/test_net.py b/tests/unittests/test_net.py
69219a
index a975678..206de56 100644
69219a
--- a/tests/unittests/test_net.py
69219a
+++ b/tests/unittests/test_net.py
69219a
@@ -1651,7 +1651,7 @@ DEFAULT_DEV_ATTRS = {
69219a
         "carrier": False,
69219a
         "dormant": False,
69219a
         "operstate": "down",
69219a
-        "address": "07-1C-C6-75-A4-BE",
69219a
+        "address": "07-1c-c6-75-a4-be",
69219a
         "device/driver": None,
69219a
         "device/device": None,
69219a
         "name_assign_type": "4",
69219a
@@ -1702,6 +1702,39 @@ class TestGenerateFallbackConfig(CiTestCase):
69219a
     @mock.patch("cloudinit.net.sys_dev_path")
69219a
     @mock.patch("cloudinit.net.read_sys_net")
69219a
     @mock.patch("cloudinit.net.get_devicelist")
69219a
+    def test_device_driver_v2(self, mock_get_devicelist, mock_read_sys_net,
69219a
+                              mock_sys_dev_path):
69219a
+        """Network configuration for generate_fallback_config is version 2."""
69219a
+        devices = {
69219a
+            'eth0': {
69219a
+                'bridge': False, 'carrier': False, 'dormant': False,
69219a
+                'operstate': 'down', 'address': '00:11:22:33:44:55',
69219a
+                'device/driver': 'hv_netsvc', 'device/device': '0x3',
69219a
+                'name_assign_type': '4'},
69219a
+            'eth1': {
69219a
+                'bridge': False, 'carrier': False, 'dormant': False,
69219a
+                'operstate': 'down', 'address': '00:11:22:33:44:55',
69219a
+                'device/driver': 'mlx4_core', 'device/device': '0x7',
69219a
+                'name_assign_type': '4'},
69219a
+
69219a
+        }
69219a
+
69219a
+        tmp_dir = self.tmp_dir()
69219a
+        _setup_test(tmp_dir, mock_get_devicelist,
69219a
+                    mock_read_sys_net, mock_sys_dev_path,
69219a
+                    dev_attrs=devices)
69219a
+
69219a
+        network_cfg = net.generate_fallback_config(config_driver=True)
69219a
+        expected = {
69219a
+            'ethernets': {'eth0': {'dhcp4': True, 'set-name': 'eth0',
69219a
+                                   'match': {'macaddress': '00:11:22:33:44:55',
69219a
+                                             'driver': 'hv_netsvc'}}},
69219a
+            'version': 2}
69219a
+        self.assertEqual(expected, network_cfg)
69219a
+
69219a
+    @mock.patch("cloudinit.net.sys_dev_path")
69219a
+    @mock.patch("cloudinit.net.read_sys_net")
69219a
+    @mock.patch("cloudinit.net.get_devicelist")
69219a
     def test_device_driver(self, mock_get_devicelist, mock_read_sys_net,
69219a
                            mock_sys_dev_path):
69219a
         devices = {
69219a
@@ -1981,7 +2014,7 @@ class TestRhelSysConfigRendering(CiTestCase):
69219a
 #
69219a
 BOOTPROTO=dhcp
69219a
 DEVICE=eth1000
69219a
-HWADDR=07-1C-C6-75-A4-BE
69219a
+HWADDR=07-1c-c6-75-a4-be
69219a
 ONBOOT=yes
69219a
 TYPE=Ethernet
69219a
 USERCTL=no
69219a
@@ -2354,7 +2387,7 @@ class TestOpenSuseSysConfigRendering(CiTestCase):
69219a
 #
69219a
 BOOTPROTO=dhcp
69219a
 DEVICE=eth1000
69219a
-HWADDR=07-1C-C6-75-A4-BE
69219a
+HWADDR=07-1c-c6-75-a4-be
69219a
 NM_CONTROLLED=no
69219a
 ONBOOT=yes
69219a
 TYPE=Ethernet
69219a
@@ -2728,13 +2761,13 @@ class TestNetplanNetRendering(CiTestCase):
69219a
 
69219a
         expected = """
69219a
 network:
69219a
-    version: 2
69219a
     ethernets:
69219a
         eth1000:
69219a
             dhcp4: true
69219a
             match:
69219a
                 macaddress: 07-1c-c6-75-a4-be
69219a
             set-name: eth1000
69219a
+    version: 2
69219a
 """
69219a
         self.assertEqual(expected.lstrip(), contents.lstrip())
69219a
         self.assertEqual(1, mock_clean_default.call_count)
69219a
-- 
69219a
1.8.3.1
69219a