cloud-init-dev team mailing list archive
-
cloud-init-dev team
-
Mailing list archive
-
Message #06835
[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