From 777309fb5a6c4a292b7ce78d59d0e68e1124574f Mon Sep 17 00:00:00 2001 From: challvy Date: Tue, 26 May 2026 15:23:36 +0800 Subject: [PATCH] feat(datasource/aliyun): add IPv6 metaserver and DHCP strategy based on NIC metadata - Add IPv6 metadata server address (http://[fd00:100::100:200]) as fallback endpoint in metadata_urls - Disable DHCPv4 when NIC metadata lacks 'private-ipv4s' field, indicating no IPv4 address is assigned to the interface - Enable DHCPv6 when 'ipv6s' field is present in NIC metadata - Remove dhcp4-overrides when DHCPv4 is disabled for the interface - Update unit tests to cover IPv6-only NIC, dual-stack NIC, and mixed multi-NIC scenarios Signed-off-by: Cang Yu --- cloudinit/sources/DataSourceAliYun.py | 2 +- cloudinit/sources/helpers/aliyun.py | 7 ++- tests/unittests/sources/test_aliyun.py | 81 ++++++++++++++++++++++++++ 3 files changed, 88 insertions(+), 2 deletions(-) diff --git a/cloudinit/sources/DataSourceAliYun.py b/cloudinit/sources/DataSourceAliYun.py index 6986abfc1d1..7d31357c443 100644 --- a/cloudinit/sources/DataSourceAliYun.py +++ b/cloudinit/sources/DataSourceAliYun.py @@ -21,7 +21,7 @@ class DataSourceAliYun(sources.DataSource): dsname = "AliYun" - metadata_urls = ["http://100.100.100.200"] + metadata_urls = ["http://100.100.100.200", "http://[fd00:100::100:200]"] # The minimum supported metadata_version from the ecs metadata apis min_metadata_version = "2016-01-01" diff --git a/cloudinit/sources/helpers/aliyun.py b/cloudinit/sources/helpers/aliyun.py index fe97c8d2ae6..407719ae43a 100644 --- a/cloudinit/sources/helpers/aliyun.py +++ b/cloudinit/sources/helpers/aliyun.py @@ -171,6 +171,8 @@ def convert_ecs_metadata_network_config( nic_metadata = macs_metadata.get(mac) if nic_metadata.get("ipv6s"): # Any IPv6 addresses configured dev_config["dhcp6"] = True + if not nic_metadata.get("private-ipv4s"): # No IPv4 addresses + dev_config["dhcp4"] = False netcfg["ethernets"][nic_name] = dev_config return netcfg nic_name_2_mac_map = dict() @@ -198,13 +200,16 @@ def convert_ecs_metadata_network_config( if nic_metadata.get("ipv6s"): # Any IPv6 addresses configured dev_config["dhcp6"] = True dev_config["dhcp6-overrides"] = dhcp_override + if not nic_metadata.get("private-ipv4s"): # No IPv4 addresses + dev_config["dhcp4"] = False + dev_config.pop("dhcp4-overrides", None) netcfg["ethernets"][nic_name] = dev_config # Remove route-metric dhcp overrides and routes / routing-policy if only # one nic configured if len(netcfg["ethernets"]) == 1: for nic_name in netcfg["ethernets"].keys(): - netcfg["ethernets"][nic_name].pop("dhcp4-overrides") + netcfg["ethernets"][nic_name].pop("dhcp4-overrides", None) netcfg["ethernets"][nic_name].pop("dhcp6-overrides", None) netcfg["ethernets"][nic_name].pop("routes", None) netcfg["ethernets"][nic_name].pop("routing-policy", None) diff --git a/tests/unittests/sources/test_aliyun.py b/tests/unittests/sources/test_aliyun.py index 5a201dbdc19..e2f49ca3553 100644 --- a/tests/unittests/sources/test_aliyun.py +++ b/tests/unittests/sources/test_aliyun.py @@ -332,11 +332,13 @@ def test_route_metric_calculated_with_multiple_network_cards(self): "00:16:3e:14:59:58": { "ipv6-gateway": "2408:xxxxx", "ipv6s": "[2408:xxxxxx]", + "private-ipv4s": "172.16.101.100", "network-interface-id": "eni-bp13i1xxxxx", }, "00:16:3e:39:43:27": { "gateway": "172.16.101.253", "netmask": "255.255.255.0", + "private-ipv4s": "172.16.101.200", "network-interface-id": "eni-bp13i2xxxx", }, } @@ -373,6 +375,7 @@ def test_route_metric_calculated_with_multiple_network_cards(self): "00:16:3e:14:59:58": { "gateway": "172.16.101.253", "netmask": "255.255.255.0", + "private-ipv4s": "172.16.101.100", "network-interface-id": "eni-bp13ixxxx", } } @@ -384,6 +387,84 @@ def test_route_metric_calculated_with_multiple_network_cards(self): # single network card would have no dhcp4-overrides assert "dhcp4-overrides" not in met0 + def test_dhcp4_disabled_when_no_private_ipv4s(self): + """Test DHCPv4 is disabled when private-ipv4s is absent.""" + # Multi-NIC: one NIC has private-ipv4s, other does not + netcfg = convert_ecs_metadata_network_config( + { + "interfaces": { + "macs": { + "00:16:3e:14:59:58": { + "private-ipv4s": "172.16.101.100", + "gateway": "172.16.101.253", + "netmask": "255.255.255.0", + "network-interface-id": "eni-bp13i1xxxxx", + }, + "00:16:3e:39:43:27": { + "ipv6s": "[2408:xxxxxx]", + "network-interface-id": "eni-bp13i2xxxx", + }, + } + } + }, + macs_to_nics={ + "00:16:3e:14:59:58": "eth0", + "00:16:3e:39:43:27": "eth1", + }, + ) + + # eth0 has private-ipv4s, dhcp4 should be True + assert netcfg["ethernets"]["eth0"]["dhcp4"] is True + assert "dhcp4-overrides" in netcfg["ethernets"]["eth0"] + + # eth1 has no private-ipv4s, dhcp4 should be False + assert netcfg["ethernets"]["eth1"]["dhcp4"] is False + assert "dhcp4-overrides" not in netcfg["ethernets"]["eth1"] + # eth1 has ipv6s, dhcp6 should be True + assert netcfg["ethernets"]["eth1"]["dhcp6"] is True + + def test_dhcp6_enabled_when_ipv6s_present(self): + """Test DHCPv6 is enabled when ipv6s field is present.""" + # Single NIC with ipv6s and private-ipv4s + netcfg = convert_ecs_metadata_network_config( + { + "interfaces": { + "macs": { + "00:16:3e:14:59:58": { + "private-ipv4s": "172.16.101.100", + "ipv6s": "[2408:xxxxxx]", + "network-interface-id": "eni-bp13i1xxxxx", + } + } + } + }, + macs_to_nics={"00:16:3e:14:59:58": "eth0"}, + ) + + assert netcfg["ethernets"]["eth0"]["dhcp4"] is True + assert netcfg["ethernets"]["eth0"]["dhcp6"] is True + + def test_ipv6_only_nic_config(self): + """Test a NIC with only IPv6 (no private-ipv4s).""" + netcfg = convert_ecs_metadata_network_config( + { + "interfaces": { + "macs": { + "00:16:3e:14:59:58": { + "ipv6s": "[2408:xxxxxx]", + "network-interface-id": "eni-bp13i1xxxxx", + } + } + } + }, + macs_to_nics={"00:16:3e:14:59:58": "eth0"}, + ) + + # No private-ipv4s: dhcp4 disabled + assert netcfg["ethernets"]["eth0"]["dhcp4"] is False + # Has ipv6s: dhcp6 enabled + assert netcfg["ethernets"]["eth0"]["dhcp6"] is True + class TestIsAliYun: ALIYUN_PRODUCT = "Alibaba Cloud ECS"