diff --git a/cloudinit/sources/DataSourceAzure.py b/cloudinit/sources/DataSourceAzure.py index 535268d9f01..74a32fca5df 100644 --- a/cloudinit/sources/DataSourceAzure.py +++ b/cloudinit/sources/DataSourceAzure.py @@ -295,6 +295,7 @@ def get_resource_disk_on_freebsd(port_id) -> Optional[str]: "disk_aliases": {"ephemeral0": RESOURCE_DISK_PATH}, "apply_network_config": True, # Use IMDS published network configuration "apply_network_config_for_secondary_ips": True, # Configure secondary ips + "apply_network_config_set_name": True, # Use set-name for NICs "experimental_skip_ready_report": False, # Skip final ready report } @@ -362,6 +363,10 @@ def _unpickle(self, ci_pkl_version: int) -> None: self._system_uuid = None self._vm_id = None self._wireserver_endpoint = DEFAULT_WIRESERVER_ENDPOINT + self.ds_cfg.setdefault( + "apply_network_config_set_name", + BUILTIN_DS_CONFIG["apply_network_config_set_name"], + ) def __str__(self): root = sources.DataSource.__str__(self) @@ -1619,6 +1624,9 @@ def _generate_network_config(self): apply_network_config_for_secondary_ips=self.ds_cfg.get( "apply_network_config_for_secondary_ips" ), + apply_network_config_set_name=self.ds_cfg.get( + "apply_network_config_set_name" + ), ) except Exception as e: LOG.error( @@ -2095,6 +2103,7 @@ def generate_network_config_from_instance_network_metadata( network_metadata: dict, *, apply_network_config_for_secondary_ips: bool, + apply_network_config_set_name: bool = True, ) -> dict: """Convert imds network metadata dictionary to network v2 configuration. @@ -2108,7 +2117,11 @@ def generate_network_config_from_instance_network_metadata( # First IPv4 and/or IPv6 address will be obtained via DHCP. # Any additional IPs of each type will be set as static # addresses. - nicname = "eth{idx}".format(idx=idx) + mac = normalize_mac_address(intf["macAddress"]) + if apply_network_config_set_name: + nicname = "eth{idx}".format(idx=idx) + else: + nicname = "enx{mac}".format(mac=mac.replace(":", "")) dhcp_override = {"route-metric": (idx + 1) * 100} # DNS resolution through secondary NICs is not supported, disable it. if idx > 0: @@ -2152,10 +2165,9 @@ def generate_network_config_from_instance_network_metadata( "{ip}/{prefix}".format(ip=privateIp, prefix=netPrefix) ) if dev_config and has_ip_address: - mac = normalize_mac_address(intf["macAddress"]) - dev_config.update( - {"match": {"macaddress": mac.lower()}, "set-name": nicname} - ) + dev_config["match"] = {"macaddress": mac.lower()} + if apply_network_config_set_name: + dev_config["set-name"] = nicname driver = determine_device_driver_for_mac(mac) if driver: dev_config["match"]["driver"] = driver diff --git a/doc/rtd/reference/datasources/azure.rst b/doc/rtd/reference/datasources/azure.rst index a3c6ffa0d98..f89069f7963 100644 --- a/doc/rtd/reference/datasources/azure.rst +++ b/doc/rtd/reference/datasources/azure.rst @@ -45,6 +45,22 @@ The settings that may be configured are: Boolean to configure secondary IP address(es) for each NIC per IMDS configuration. Default is True. + +* :command:`apply_network_config_set_name` + + Boolean to include ``set-name`` directives in the generated network + configuration, which renames interfaces to ``ethX`` naming. When set to + False, interfaces are matched by MAC address (and optionally driver) + without renaming, and retain kernel-assigned names. Default is True. + + Azure's IMDS does not guarantee the ordering of NICs in the network metadata + response (see `Azure IMDS documentation + `_). + Because cloud-init derives ``ethX`` names from the IMDS response order, + NIC names may change between reboots. Disabling this option avoids that + problem by matching interfaces on MAC address (and optionally driver), + allowing the kernel or udev to assign and retain stable names. + * :command:`data_dir` Path used to read meta-data files and write crawled data. @@ -67,6 +83,7 @@ An example configuration with the default values is provided below: Azure: apply_network_config: true apply_network_config_for_secondary_ips: true + apply_network_config_set_name: false data_dir: /var/lib/waagent disk_aliases: ephemeral0: /dev/disk/cloud/azure_resource diff --git a/tests/unittests/sources/test_azure.py b/tests/unittests/sources/test_azure.py index e032c25f279..1d94cbadc4e 100644 --- a/tests/unittests/sources/test_azure.py +++ b/tests/unittests/sources/test_azure.py @@ -989,6 +989,91 @@ def test_parsing_scenarios( == expected ) + @pytest.mark.parametrize( + "set_name,expected", + [ + ( + True, + { + "ethernets": { + "eth0": { + "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", + }, + "eth1": { + "dhcp4": True, + "dhcp4-overrides": { + "route-metric": 200, + "use-dns": False, + }, + "dhcp6": False, + "match": {"macaddress": "22:0d:3a:04:75:98"}, + "set-name": "eth1", + }, + }, + "version": 2, + }, + ), + ( + False, + { + "ethernets": { + "enx000d3a047598": { + "dhcp4": True, + "dhcp4-overrides": {"route-metric": 100}, + "dhcp6": True, + "dhcp6-overrides": {"route-metric": 100}, + "match": {"macaddress": "00:0d:3a:04:75:98"}, + }, + "enx220d3a047598": { + "dhcp4": True, + "dhcp4-overrides": { + "route-metric": 200, + "use-dns": False, + }, + "dhcp6": False, + "match": {"macaddress": "22:0d:3a:04:75:98"}, + }, + }, + "version": 2, + }, + ), + ], + ) + def test_set_name_config(self, mock_get_interfaces, set_name, expected): + """Verify set-name with two NICs (primary with IPv6, secondary).""" + two_nic_metadata = { + "interface": [ + { + "macAddress": "000D3A047598", + "ipv6": { + "subnet": [{"prefix": "64", "address": "fd00::"}], + "ipAddress": [{"privateIpAddress": "fd00::4"}], + }, + "ipv4": { + "subnet": [{"prefix": "24", "address": "10.0.0.0"}], + "ipAddress": [ + { + "privateIpAddress": "10.0.0.4", + "publicIpAddress": "104.46.124.81", + } + ], + }, + }, + SECONDARY_INTERFACE, + ] + } + result = dsaz.generate_network_config_from_instance_network_metadata( + two_nic_metadata, + apply_network_config_for_secondary_ips=True, + apply_network_config_set_name=set_name, + ) + assert result == expected + class TestNetworkConfig: fallback_config = { @@ -1004,22 +1089,45 @@ class TestNetworkConfig: ], } - def test_single_ipv4_nic_configuration( - self, azure_ds, mock_get_interfaces - ): - """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", + @pytest.mark.parametrize( + "set_name,expected", + [ + ( + True, + { + "ethernets": { + "eth0": { + "dhcp4": True, + "dhcp4-overrides": {"route-metric": 100}, + "dhcp6": False, + "match": {"macaddress": "00:0d:3a:04:75:98"}, + "set-name": "eth0", + }, + }, + "version": 2, }, - }, - "version": 2, - } + ), + ( + False, + { + "ethernets": { + "enx000d3a047598": { + "dhcp4": True, + "dhcp4-overrides": {"route-metric": 100}, + "dhcp6": False, + "match": {"macaddress": "00:0d:3a:04:75:98"}, + }, + }, + "version": 2, + }, + ), + ], + ) + def test_network_config( + self, azure_ds, mock_get_interfaces, set_name, expected + ): + """Verify network_config via ds_cfg for set-name enabled/disabled.""" + azure_ds.ds_cfg["apply_network_config_set_name"] = set_name azure_ds._metadata_imds = NETWORK_METADATA assert azure_ds.network_config == expected