diff --git a/cloudinit/sources/DataSourceAzure.py b/cloudinit/sources/DataSourceAzure.py index 259905db6fe..69fcf9915c5 100644 --- a/cloudinit/sources/DataSourceAzure.py +++ b/cloudinit/sources/DataSourceAzure.py @@ -1965,6 +1965,9 @@ def generate_network_config_from_instance_network_metadata( # addresses. nicname = "eth{idx}".format(idx=idx) dhcp_override = {"route-metric": (idx + 1) * 100} + # DNS resolution through secondary NICs is not supported, disable it. + if idx > 0: + dhcp_override["use-dns"] = False dev_config: Dict[str, Any] = { "dhcp4": True, "dhcp4-overrides": dhcp_override, diff --git a/tests/integration_tests/datasources/test_azure.py b/tests/integration_tests/datasources/test_azure.py index 8e663ac21d6..c1d36abe4ed 100644 --- a/tests/integration_tests/datasources/test_azure.py +++ b/tests/integration_tests/datasources/test_azure.py @@ -1,11 +1,14 @@ +import datetime + import pytest +from pycloudlib.azure.util import AzureCreateParams, AzureParams from pycloudlib.cloud import ImageType from tests.integration_tests.clouds import IntegrationCloud from tests.integration_tests.conftest import get_validated_source from tests.integration_tests.instances import IntegrationInstance from tests.integration_tests.integration_settings import PLATFORM -from tests.integration_tests.releases import CURRENT_RELEASE +from tests.integration_tests.releases import BIONIC, CURRENT_RELEASE def _check_for_eject_errors( @@ -45,3 +48,70 @@ def test_azure_eject(session_cloud: IntegrationCloud): session_cloud.cloud_instance.delete_image(snapshot_id) else: _check_for_eject_errors(instance) + + +def parse_resolvectl_dns(output: str) -> dict: + """Parses the output of 'resolvectl dns'. + + >>> parse_resolvectl_dns( + ... "Global:", + ... "Link 2 (eth0): 168.63.129.16", + ... "Link 3 (eth1): 168.63.129.16", + ... ) + {'Global': '', + 'Link 2 (eth0)': '168.63.129.16', + 'Link 3 (eth1)': '168.63.129.16'} + """ + + parsed = dict() + for line in output.splitlines(): + if line.isspace(): + continue + splitted = line.split(":") + k = splitted.pop(0).strip() + v = splitted.pop(0).strip() if splitted else "" + parsed[k] = v + return parsed + + +@pytest.mark.skipif(PLATFORM != "azure", reason="Test is Azure specific") +@pytest.mark.skipif( + CURRENT_RELEASE < BIONIC, reason="Easier to test on Bionic+" +) +def test_azure_multi_nic_setup( + setup_image, session_cloud: IntegrationCloud +) -> None: + """Integration test for https://warthogs.atlassian.net/browse/CPC-3999. + + Azure should have the primary NIC only route to DNS. + Ensure other NICs do not have route to DNS. + """ + us = datetime.datetime.now().strftime("%f") + rg_params = AzureParams(f"ci-test-multi-nic-setup-{us}", None) + nic_one = AzureCreateParams(f"ci-nic1-test-{us}", rg_params.name, None) + nic_two = AzureCreateParams(f"ci-nic2-test-{us}", rg_params.name, None) + with session_cloud.launch( + launch_kwargs={ + "resource_group_params": rg_params, + "network_interfaces_params": [nic_one, nic_two], + } + ) as client: + _check_for_eject_errors(client) + if CURRENT_RELEASE == BIONIC: + ret = client.execute("systemd-resolve --status") + assert ret.ok, ret.stderr + assert ret.stdout.count("Current Scopes: DNS") == 1 + else: + ret = client.execute("resolvectl dns") + assert ret.ok, ret.stderr + routes = parse_resolvectl_dns(ret.stdout) + routes_devices = list(routes.keys()) + eth1_dev = [dev for dev in routes_devices if "(eth1)" in dev][0] + assert not routes[eth1_dev], ( + f"Expected eth1 to not have routes to dns." + f" Found: {routes[eth1_dev]}" + ) + + # check the instance can resolve something + res = client.execute("resolvectl query google.com") + assert res.ok, res.stderr diff --git a/tests/unittests/sources/test_azure.py b/tests/unittests/sources/test_azure.py index 5f11405d63e..b96f5c718da 100644 --- a/tests/unittests/sources/test_azure.py +++ b/tests/unittests/sources/test_azure.py @@ -742,14 +742,20 @@ class TestGenerateNetworkConfig: "match": {"macaddress": "00:0d:3a:04:75:98"}, "dhcp6": False, "dhcp4": True, - "dhcp4-overrides": {"route-metric": 200}, + "dhcp4-overrides": { + "route-metric": 200, + "use-dns": False, + }, }, "eth2": { "set-name": "eth2", "match": {"macaddress": "00:0d:3a:04:75:98"}, "dhcp6": False, "dhcp4": True, - "dhcp4-overrides": {"route-metric": 300}, + "dhcp4-overrides": { + "route-metric": 300, + "use-dns": False, + }, }, }, "version": 2, @@ -976,7 +982,7 @@ def test_single_ipv4_nic_configuration( "dhcp6": False, "match": {"macaddress": "00:0d:3a:04:75:98"}, "set-name": "eth0", - } + }, }, "version": 2, } @@ -1557,7 +1563,7 @@ def test_network_config_set_from_imds(self): "dhcp6": False, "dhcp4": True, "dhcp4-overrides": {"route-metric": 100}, - } + }, }, "version": 2, } @@ -1586,14 +1592,14 @@ def test_network_config_set_from_imds_route_metric_for_secondary_nic(self): "match": {"macaddress": "22:0d:3a:04:75:98"}, "dhcp6": False, "dhcp4": True, - "dhcp4-overrides": {"route-metric": 200}, + "dhcp4-overrides": {"route-metric": 200, "use-dns": False}, }, "eth2": { "set-name": "eth2", "match": {"macaddress": "33:0d:3a:04:75:98"}, "dhcp6": False, "dhcp4": True, - "dhcp4-overrides": {"route-metric": 300}, + "dhcp4-overrides": {"route-metric": 300, "use-dns": False}, }, }, "version": 2, @@ -1626,7 +1632,7 @@ def test_network_config_set_from_imds_for_secondary_nic_no_ip(self): "dhcp6": False, "dhcp4": True, "dhcp4-overrides": {"route-metric": 100}, - } + }, }, "version": 2, }