sailesh1993 / rpms / cloud-init

Forked from rpms/cloud-init a year ago
Clone
Blob Blame History Raw
From 1b1a179bab885c19a4793c4a6cc2b48a541b9a84 Mon Sep 17 00:00:00 2001
From: Eduardo Otubo <otubo@redhat.com>
Date: Mon, 4 May 2020 12:40:04 +0200
Subject: [PATCH 4/6] azure: support matching dhcp route-metrics for dual-stack
 ipv4 ipv6

RH-Author: Eduardo Otubo <otubo@redhat.com>
Message-id: <20200327152826.13343-5-otubo@redhat.com>
Patchwork-id: 94455
O-Subject: [RHEL-8.1.z/RHEL-8.2.z cloud-init PATCHv2 4/6] azure: support matching dhcp route-metrics for dual-stack ipv4 ipv6
Bugzilla: 1811753
RH-Acked-by: Cathy Avery <cavery@redhat.com>
RH-Acked-by: Vitaly Kuznetsov <vkuznets@redhat.com>

commit 02f07b666adc62d70c4f1a98c2ae80cb6629fa9a
Author: Chad Smith <chad.smith@canonical.com>
Date:   Mon Nov 4 22:11:37 2019 +0000

    azure: support matching dhcp route-metrics for dual-stack ipv4 ipv6

    Network v2 configuration for Azure will set both dhcp4 and
    dhcp6 to False by default.

    When IPv6 privateIpAddresses are present for an interface in Azure's
    Instance Metadata Service (IMDS), set dhcp6: True and provide a
    route-metric value that will match the corresponding dhcp4 route-metric.
    The route-metric value will increase by 100 for each additional
    interface present to ensure the primary interface has a route to IMDS.

    Also fix dhcp route-metric rendering for eni and sysconfig distros.

    LP: #1850308

Signed-off-by: Eduardo Otubo <otubo@redhat.com>
Signed-off-by: Miroslav Rezanina <mrezanin@redhat.com>
---
 cloudinit/net/network_state.py                |  17 ++++-
 cloudinit/net/sysconfig.py                    |   6 +-
 cloudinit/sources/DataSourceAzure.py          |  10 ++-
 tests/unittests/test_datasource/test_azure.py | 101 ++++++++++++++++++++++++++
 tests/unittests/test_net.py                   |  54 ++++++++++++++
 5 files changed, 178 insertions(+), 10 deletions(-)

diff --git a/cloudinit/net/network_state.py b/cloudinit/net/network_state.py
index 71d3c0f..571eb57 100644
--- a/cloudinit/net/network_state.py
+++ b/cloudinit/net/network_state.py
@@ -21,8 +21,9 @@ NETWORK_STATE_REQUIRED_KEYS = {
     1: ['version', 'config', 'network_state'],
 }
 NETWORK_V2_KEY_FILTER = [
-    'addresses', 'dhcp4', 'dhcp6', 'gateway4', 'gateway6', 'interfaces',
-    'match', 'mtu', 'nameservers', 'renderer', 'set-name', 'wakeonlan'
+    'addresses', 'dhcp4', 'dhcp4-overrides', 'dhcp6', 'dhcp6-overrides',
+    'gateway4', 'gateway6', 'interfaces', 'match', 'mtu', 'nameservers',
+    'renderer', 'set-name', 'wakeonlan'
 ]
 
 NET_CONFIG_TO_V2 = {
@@ -734,12 +735,20 @@ class NetworkStateInterpreter(object):
     def _v2_to_v1_ipcfg(self, cfg):
         """Common ipconfig extraction from v2 to v1 subnets array."""
 
+        def _add_dhcp_overrides(overrides, subnet):
+            if 'route-metric' in overrides:
+                subnet['metric'] = overrides['route-metric']
+
         subnets = []
         if cfg.get('dhcp4'):
-            subnets.append({'type': 'dhcp4'})
+            subnet = {'type': 'dhcp4'}
+            _add_dhcp_overrides(cfg.get('dhcp4-overrides', {}), subnet)
+            subnets.append(subnet)
         if cfg.get('dhcp6'):
+            subnet = {'type': 'dhcp6'}
             self.use_ipv6 = True
-            subnets.append({'type': 'dhcp6'})
+            _add_dhcp_overrides(cfg.get('dhcp6-overrides', {}), subnet)
+            subnets.append(subnet)
 
         gateway4 = None
         gateway6 = None
diff --git a/cloudinit/net/sysconfig.py b/cloudinit/net/sysconfig.py
index 5c1b4eb..a4c7660 100644
--- a/cloudinit/net/sysconfig.py
+++ b/cloudinit/net/sysconfig.py
@@ -373,6 +373,9 @@ class Renderer(renderer.Renderer):
         ipv6_index = -1
         for i, subnet in enumerate(subnets, start=len(iface_cfg.children)):
             subnet_type = subnet.get('type')
+            # metric may apply to both dhcp and static config
+            if 'metric' in subnet:
+                iface_cfg['METRIC'] = subnet['metric']
             if subnet_type in ['dhcp', 'dhcp4', 'dhcp6']:
                 if has_default_route and iface_cfg['BOOTPROTO'] != 'none':
                     iface_cfg['DHCLIENT_SET_DEFAULT_ROUTE'] = False
@@ -401,9 +404,6 @@ class Renderer(renderer.Renderer):
                     else:
                         iface_cfg['GATEWAY'] = subnet['gateway']
 
-                if 'metric' in subnet:
-                    iface_cfg['METRIC'] = subnet['metric']
-
                 if 'dns_search' in subnet:
                     iface_cfg['DOMAIN'] = ' '.join(subnet['dns_search'])
 
diff --git a/cloudinit/sources/DataSourceAzure.py b/cloudinit/sources/DataSourceAzure.py
index 8cb7e5c..e4d0708 100755
--- a/cloudinit/sources/DataSourceAzure.py
+++ b/cloudinit/sources/DataSourceAzure.py
@@ -1237,7 +1237,8 @@ def parse_network_config(imds_metadata):
             network_metadata = imds_metadata['network']
             for idx, intf in enumerate(network_metadata['interface']):
                 nicname = 'eth{idx}'.format(idx=idx)
-                dev_config = {}
+                dev_config = {'dhcp4': False, 'dhcp6': False}
+                dhcp_override = {'route-metric': (idx + 1) * 100}
                 for addr4 in intf['ipv4']['ipAddress']:
                     privateIpv4 = addr4['privateIpAddress']
                     if privateIpv4:
@@ -1255,12 +1256,15 @@ def parse_network_config(imds_metadata):
                             # non-primary interfaces should have a higher
                             # route-metric (cost) so default routes prefer
                             # primary nic due to lower route-metric value
-                            dev_config['dhcp4-overrides'] = {
-                                'route-metric': (idx + 1) * 100}
+                            dev_config['dhcp4-overrides'] = dhcp_override
                 for addr6 in intf['ipv6']['ipAddress']:
                     privateIpv6 = addr6['privateIpAddress']
                     if privateIpv6:
                         dev_config['dhcp6'] = True
+                        # non-primary interfaces should have a higher
+                        # route-metric (cost) so default routes prefer
+                        # primary nic due to lower route-metric value
+                        dev_config['dhcp6-overrides'] = dhcp_override
                         break
                 if dev_config:
                     mac = ':'.join(re.findall(r'..', intf['macAddress']))
diff --git a/tests/unittests/test_datasource/test_azure.py b/tests/unittests/test_datasource/test_azure.py
index f2ff967..8da3233 100644
--- a/tests/unittests/test_datasource/test_azure.py
+++ b/tests/unittests/test_datasource/test_azure.py
@@ -135,6 +135,102 @@ SECONDARY_INTERFACE = {
 MOCKPATH = 'cloudinit.sources.DataSourceAzure.'
 
 
+class TestParseNetworkConfig(CiTestCase):
+
+    maxDiff = None
+
+    def test_single_ipv4_nic_configuration(self):
+        """parse_network_config emits dhcp on single nic with ipv4"""
+        expected = {'ethernets': {
+            'eth0': {'dhcp4': True,
+                     'dhcp4-overrides': {'route-metric': 100},
+                     'dhcp6': False,
+                     'match': {'macaddress': '00:0d:3a:04:75:98'},
+                     'set-name': 'eth0'}}, 'version': 2}
+        self.assertEqual(expected, dsaz.parse_network_config(NETWORK_METADATA))
+
+    def test_increases_route_metric_for_non_primary_nics(self):
+        """parse_network_config increases route-metric for each nic"""
+        expected = {'ethernets': {
+            'eth0': {'dhcp4': True,
+                     'dhcp4-overrides': {'route-metric': 100},
+                     'dhcp6': False,
+                     'match': {'macaddress': '00:0d:3a:04:75:98'},
+                     'set-name': 'eth0'},
+            'eth1': {'set-name': 'eth1',
+                     'match': {'macaddress': '22:0d:3a:04:75:98'},
+                     'dhcp6': False,
+                     'dhcp4': True,
+                     'dhcp4-overrides': {'route-metric': 200}},
+            'eth2': {'set-name': 'eth2',
+                     'match': {'macaddress': '33:0d:3a:04:75:98'},
+                     'dhcp6': False,
+                     'dhcp4': True,
+                     'dhcp4-overrides': {'route-metric': 300}}}, 'version': 2}
+        imds_data = copy.deepcopy(NETWORK_METADATA)
+        imds_data['network']['interface'].append(SECONDARY_INTERFACE)
+        third_intf = copy.deepcopy(SECONDARY_INTERFACE)
+        third_intf['macAddress'] = third_intf['macAddress'].replace('22', '33')
+        third_intf['ipv4']['subnet'][0]['address'] = '10.0.2.0'
+        third_intf['ipv4']['ipAddress'][0]['privateIpAddress'] = '10.0.2.6'
+        imds_data['network']['interface'].append(third_intf)
+        self.assertEqual(expected, dsaz.parse_network_config(imds_data))
+
+    def test_ipv4_and_ipv6_route_metrics_match_for_nics(self):
+        """parse_network_config emits matching ipv4 and ipv6 route-metrics."""
+        expected = {'ethernets': {
+            'eth0': {'dhcp4': True,
+                     'dhcp4-overrides': {'route-metric': 100},
+                     'dhcp6': False,
+                     'match': {'macaddress': '00:0d:3a:04:75:98'},
+                     'set-name': 'eth0'},
+            'eth1': {'set-name': 'eth1',
+                     'match': {'macaddress': '22:0d:3a:04:75:98'},
+                     'dhcp4': True,
+                     'dhcp6': False,
+                     'dhcp4-overrides': {'route-metric': 200}},
+            'eth2': {'set-name': 'eth2',
+                     'match': {'macaddress': '33:0d:3a:04:75:98'},
+                     'dhcp4': True,
+                     'dhcp4-overrides': {'route-metric': 300},
+                     'dhcp6': True,
+                     'dhcp6-overrides': {'route-metric': 300}}}, 'version': 2}
+        imds_data = copy.deepcopy(NETWORK_METADATA)
+        imds_data['network']['interface'].append(SECONDARY_INTERFACE)
+        third_intf = copy.deepcopy(SECONDARY_INTERFACE)
+        third_intf['macAddress'] = third_intf['macAddress'].replace('22', '33')
+        third_intf['ipv4']['subnet'][0]['address'] = '10.0.2.0'
+        third_intf['ipv4']['ipAddress'][0]['privateIpAddress'] = '10.0.2.6'
+        third_intf['ipv6'] = {
+            "subnet": [{"prefix": "64", "address": "2001:dead:beef::2"}],
+            "ipAddress": [{"privateIpAddress": "2001:dead:beef::1"}]
+        }
+        imds_data['network']['interface'].append(third_intf)
+        self.assertEqual(expected, dsaz.parse_network_config(imds_data))
+
+    def test_ipv4_secondary_ips_will_be_static_addrs(self):
+        """parse_network_config emits primary ipv4 as dhcp others are static"""
+        expected = {'ethernets': {
+            'eth0': {'addresses': ['10.0.0.5/24'],
+                     'dhcp4': True,
+                     'dhcp4-overrides': {'route-metric': 100},
+                     'dhcp6': True,
+                     'dhcp6-overrides': {'route-metric': 100},
+                     'match': {'macaddress': '00:0d:3a:04:75:98'},
+                     'set-name': 'eth0'}}, 'version': 2}
+        imds_data = copy.deepcopy(NETWORK_METADATA)
+        nic1 = imds_data['network']['interface'][0]
+        nic1['ipv4']['ipAddress'].append({'privateIpAddress': '10.0.0.5'})
+
+        # Secondary ipv6 addresses currently ignored/unconfigured
+        nic1['ipv6'] = {
+            "subnet": [{"prefix": "10", "address": "2001:dead:beef::16"}],
+            "ipAddress": [{"privateIpAddress": "2001:dead:beef::1"},
+                          {"privateIpAddress": "2001:dead:beef::2"}]
+        }
+        self.assertEqual(expected, dsaz.parse_network_config(imds_data))
+
+
 class TestGetMetadataFromIMDS(HttprettyTestCase):
 
     with_logs = True
@@ -653,6 +749,7 @@ fdescfs            /dev/fd          fdescfs rw              0 0
             'ethernets': {
                 'eth0': {'set-name': 'eth0',
                          'match': {'macaddress': '00:0d:3a:04:75:98'},
+                         'dhcp6': False,
                          'dhcp4': True,
                          'dhcp4-overrides': {'route-metric': 100}}},
             'version': 2}
@@ -670,14 +767,17 @@ fdescfs            /dev/fd          fdescfs rw              0 0
             'ethernets': {
                 'eth0': {'set-name': 'eth0',
                          'match': {'macaddress': '00:0d:3a:04:75:98'},
+                         'dhcp6': False,
                          'dhcp4': True,
                          'dhcp4-overrides': {'route-metric': 100}},
                 'eth1': {'set-name': 'eth1',
                          'match': {'macaddress': '22:0d:3a:04:75:98'},
+                         'dhcp6': False,
                          'dhcp4': True,
                          'dhcp4-overrides': {'route-metric': 200}},
                 'eth2': {'set-name': 'eth2',
                          'match': {'macaddress': '33:0d:3a:04:75:98'},
+                         'dhcp6': False,
                          'dhcp4': True,
                          'dhcp4-overrides': {'route-metric': 300}}},
             'version': 2}
@@ -993,6 +1093,7 @@ fdescfs            /dev/fd          fdescfs rw              0 0
             'ethernets': {
                 'eth0': {'dhcp4': True,
                          'dhcp4-overrides': {'route-metric': 100},
+                         'dhcp6': False,
                          'match': {'macaddress': '00:0d:3a:04:75:98'},
                          'set-name': 'eth0'}},
             'version': 2}
diff --git a/tests/unittests/test_net.py b/tests/unittests/test_net.py
index 206de56..df5658d 100644
--- a/tests/unittests/test_net.py
+++ b/tests/unittests/test_net.py
@@ -2304,6 +2304,36 @@ USERCTL=no
         self._compare_files_to_expected(entry['expected_sysconfig'], found)
         self._assert_headers(found)
 
+    def test_from_v2_route_metric(self):
+        """verify route-metric gets rendered on nic when source is netplan."""
+        overrides = {'route-metric': 100}
+        v2base = {
+            'version': 2,
+            'ethernets': {
+                'eno1': {'dhcp4': True,
+                         'match': {'macaddress': '07-1c-c6-75-a4-be'}}}}
+        expected = {
+            'ifcfg-eno1': textwrap.dedent("""\
+                BOOTPROTO=dhcp
+                DEVICE=eno1
+                HWADDR=07-1c-c6-75-a4-be
+                METRIC=100
+                NM_CONTROLLED=no
+                ONBOOT=yes
+                STARTMODE=auto
+                TYPE=Ethernet
+                USERCTL=no
+                """),
+            }
+        for dhcp_ver in ('dhcp4', 'dhcp6'):
+            v2data = copy.deepcopy(v2base)
+            if dhcp_ver == 'dhcp6':
+                expected['ifcfg-eno1'] += "IPV6INIT=yes\nDHCPV6C=yes\n"
+            v2data['ethernets']['eno1'].update(
+                {dhcp_ver: True, '{0}-overrides'.format(dhcp_ver): overrides})
+            self._compare_files_to_expected(
+                expected, self._render_and_read(network_config=v2data))
+
 
 class TestOpenSuseSysConfigRendering(CiTestCase):
 
@@ -2725,6 +2755,30 @@ iface eth0 inet dhcp
         config = sysconfig.ConfigObj(nm_cfg)
         self.assertIn('ifcfg-rh', config['main']['plugins'])
 
+    def test_v2_route_metric_to_eni(self):
+        """Network v2 route-metric overrides are preserved in eni output"""
+        tmp_dir = self.tmp_dir()
+        renderer = eni.Renderer()
+        expected_tmpl = textwrap.dedent("""\
+            auto lo
+            iface lo inet loopback
+
+            auto eth0
+            iface eth0 inet{suffix} dhcp
+                metric 100
+            """)
+        for dhcp_ver in ('dhcp4', 'dhcp6'):
+            suffix = '6' if dhcp_ver == 'dhcp6' else ''
+            dhcp_cfg = {
+                dhcp_ver: True,
+                '{ver}-overrides'.format(ver=dhcp_ver): {'route-metric': 100}}
+            v2_input = {'version': 2, 'ethernets': {'eth0': dhcp_cfg}}
+            ns = network_state.parse_net_config_data(v2_input)
+            renderer.render_network_state(ns, target=tmp_dir)
+            self.assertEqual(
+                expected_tmpl.format(suffix=suffix),
+                dir2dict(tmp_dir)['/etc/network/interfaces'])
+
 
 class TestNetplanNetRendering(CiTestCase):
 
-- 
1.8.3.1