← Back to team overview

cloud-init-dev team mailing list archive

[Merge] ~chad.smith/cloud-init:bug/azure-dhcp6-metric into cloud-init:master

 

Chad Smith has proposed merging ~chad.smith/cloud-init:bug/azure-dhcp6-metric into cloud-init:master.

Commit message:
azure: support matching dhcp route-metrics for dual-stack ipv4 ipv6
    
When an Azure vm has multiple nics set to dhcp, use increasing
route-metric values for each nic to ensure the default route chosen is
the primary nic (eth0). For netplan configuration, if providing
dhcp4-overrides: route-metric, the same route-metric value also needs to
be provided as a dhcp6-override.  Otherwise the network config is
rejected wholesale and a the system is left without network.
    
When reading Azure IMDS, cloud-init will enable dhcp4 or dhcp6 for the
first ip allocated to the primary NIC with a dhcp[46]-overrrides
route-metric of 100. For every additional NIC attached to the vm, the
route-metric override is increased by 100.
    
When configuring IP addresses on each NIC:
  - The primary IP for a NIC will be configured via dhcp (or dhcp6 if
    allocated the the vm).
  - Any additional ipv4 or ipv6 addresses configured
    will be setup as secondary static IP addresses on the NIC.

LP: #1850308


Requested reviews:
  cloud-init Commiters (cloud-init-dev)
Related bugs:
  Bug #1850308 in cloud-init: "cloud-init on azure with dual stack needs to add route-metric for dhcp6 also"
  https://bugs.launchpad.net/cloud-init/+bug/1850308

For more details, see:
https://code.launchpad.net/~chad.smith/cloud-init/+git/cloud-init/+merge/374994
-- 
Your team cloud-init Commiters is requested to review the proposed merge of ~chad.smith/cloud-init:bug/azure-dhcp6-metric into cloud-init:master.
diff --git a/cloudinit/sources/DataSourceAzure.py b/cloudinit/sources/DataSourceAzure.py
index cdf49d3..434d8f2 100755
--- a/cloudinit/sources/DataSourceAzure.py
+++ b/cloudinit/sources/DataSourceAzure.py
@@ -1322,7 +1322,7 @@ 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 = {'addresses': []}
                 for addr4 in intf['ipv4']['ipAddress']:
                     privateIpv4 = addr4['privateIpAddress']
                     if privateIpv4:
@@ -1330,8 +1330,6 @@ def parse_network_config(imds_metadata):
                             # Append static address config for ip > 1
                             netPrefix = intf['ipv4']['subnet'][0].get(
                                 'prefix', '24')
-                            if not dev_config.get('addresses'):
-                                dev_config['addresses'] = []
                             dev_config['addresses'].append(
                                 '{ip}/{prefix}'.format(
                                     ip=privateIpv4, prefix=netPrefix))
@@ -1345,8 +1343,22 @@ def parse_network_config(imds_metadata):
                 for addr6 in intf['ipv6']['ipAddress']:
                     privateIpv6 = addr6['privateIpAddress']
                     if privateIpv6:
-                        dev_config['dhcp6'] = True
-                        break
+                        if dev_config.get('dhcp6', False):
+                            # Append static address config for ip > 1
+                            netPrefix = intf['ipv6']['subnet'][0].get(
+                                'prefix', '64')
+                            dev_config['addresses'].append(
+                                '{ip}/{prefix}'.format(
+                                    ip=privateIpv6, prefix=netPrefix))
+                        else:
+                            # 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'] = {
+                                'route-metric': (idx + 1) * 100}
+                            dev_config['dhcp6'] = True
+                if dev_config['addresses'] == []:  # drop addresses if empty
+                   dev_config.pop('addresses')
                 if dev_config:
                     mac = ':'.join(re.findall(r'..', intf['macAddress']))
                     dev_config.update(
diff --git a/tests/unittests/test_datasource/test_azure.py b/tests/unittests/test_datasource/test_azure.py
index 80c6f01..f8ecd95 100644
--- a/tests/unittests/test_datasource/test_azure.py
+++ b/tests/unittests/test_datasource/test_azure.py
@@ -132,9 +132,7 @@ NETWORK_METADATA = {
 
 SECONDARY_INTERFACE = {
     "macAddress": "220D3A047598",
-    "ipv6": {
-        "ipAddress": []
-    },
+    "ipv6": {"ipAddress": []},
     "ipv4": {
         "subnet": [
             {
@@ -153,6 +151,93 @@ 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},
+                     '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},
+                     '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,
+                     'dhcp4-overrides': {'route-metric': 200}},
+            'eth2': {'set-name': 'eth2',
+                     'match': {'macaddress': '33:0d:3a:04:75:98'},
+                     '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},
+                     '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,
+                     '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_and_ipv6_secondary_ips_will_be_static_addrs(self):
+        """parse_network_config emits primary ip as dhcp others are static"""
+        expected = {'ethernets': {
+            'eth0': {'addresses': ['10.0.0.5/24', '2001:dead:beef::2/10'],
+                     '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'})
+        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

Follow ups