From dfe6170c222519634dacce2bd934d191e7d9957e Mon Sep 17 00:00:00 2001 From: Ksenija Stanojevic Date: Mon, 25 Sep 2023 18:23:28 -0700 Subject: [PATCH] azure: add option to enable/disable secondary ip config (#4432) Currently cloud-init attempts to configure all IP addresses for all interfaces whose configurations are available in IMDS. To allow users more control over VM network configuration, add an option for cloud-init to enable/disable configuration of secondary IP addresses. This PR also introduces the datasource option apply_network_config_for_secondary_ips for DataSourceAzure, which leverages the new network config above. Default is set to True to maintain compatibility. --- cloudinit/cmd/devel/net_convert.py | 3 +- cloudinit/sources/DataSourceAzure.py | 12 ++- doc/rtd/reference/datasources/azure.rst | 5 ++ tests/unittests/sources/test_azure.py | 103 +++++++++++++++++++++++- 4 files changed, 118 insertions(+), 5 deletions(-) diff --git a/cloudinit/cmd/devel/net_convert.py b/cloudinit/cmd/devel/net_convert.py index 69372f21cce..346674ff009 100755 --- a/cloudinit/cmd/devel/net_convert.py +++ b/cloudinit/cmd/devel/net_convert.py @@ -128,7 +128,8 @@ def handle_args(name, args): ) elif args.kind == "azure-imds": pre_ns = azure.generate_network_config_from_instance_network_metadata( - json.loads(net_data)["network"] + json.loads(net_data)["network"], + apply_network_config_for_secondary_ips=True, ) elif args.kind == "vmware-imc": config = guestcust_util.Config( diff --git a/cloudinit/sources/DataSourceAzure.py b/cloudinit/sources/DataSourceAzure.py index 75727f0e403..e8389c72205 100644 --- a/cloudinit/sources/DataSourceAzure.py +++ b/cloudinit/sources/DataSourceAzure.py @@ -286,6 +286,7 @@ def get_resource_disk_on_freebsd(port_id) -> Optional[str]: "data_dir": AGENT_SEED_DIR, "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 } BUILTIN_CLOUD_EPHEMERAL_DISK_CONFIG = { @@ -1413,7 +1414,10 @@ def _generate_network_config(self): ): try: return generate_network_config_from_instance_network_metadata( - self._metadata_imds["network"] + self._metadata_imds["network"], + apply_network_config_for_secondary_ips=self.ds_cfg.get( + "apply_network_config_for_secondary_ips" + ), ) except Exception as e: LOG.error( @@ -1863,6 +1867,8 @@ def load_azure_ds_dir(source_dir): @azure_ds_telemetry_reporter def generate_network_config_from_instance_network_metadata( network_metadata: dict, + *, + apply_network_config_for_secondary_ips: bool, ) -> dict: """Convert imds network metadata dictionary to network v2 configuration. @@ -1901,6 +1907,10 @@ def generate_network_config_from_instance_network_metadata( # route-metric (cost) so default routes prefer # primary nic due to lower route-metric value dev_config["dhcp6-overrides"] = dhcp_override + + if not apply_network_config_for_secondary_ips: + continue + for addr in addresses[1:]: # Append static address config for ip > 1 netPrefix = intf[addr_type]["subnet"][0].get( diff --git a/doc/rtd/reference/datasources/azure.rst b/doc/rtd/reference/datasources/azure.rst index 5f8f3b8489e..1907e4b5dcc 100644 --- a/doc/rtd/reference/datasources/azure.rst +++ b/doc/rtd/reference/datasources/azure.rst @@ -41,6 +41,10 @@ The settings that may be configured are: Boolean set to True to use network configuration described by Azure's IMDS endpoint instead of fallback network config of DHCP on eth0. Default is True. +* :command:`apply_network_config_for_secondary_ips` + + Boolean to configure secondary IP address(es) for each NIC per IMDS + configuration. Default is True. * :command:`data_dir` Path used to read metadata files and write crawled data. @@ -62,6 +66,7 @@ An example configuration with the default values is provided below: datasource: Azure: apply_network_config: true + apply_network_config_for_secondary_ips: true 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 1a6c77c0d82..a3353ba73b1 100644 --- a/tests/unittests/sources/test_azure.py +++ b/tests/unittests/sources/test_azure.py @@ -520,11 +520,12 @@ def construct_ovf_env( class TestGenerateNetworkConfig: @pytest.mark.parametrize( - "label,metadata,expected", + "label,metadata,ip_config,expected", [ ( "simple interface", NETWORK_METADATA["network"], + True, { "ethernets": { "eth0": { @@ -559,6 +560,7 @@ class TestGenerateNetworkConfig: } ] }, + True, { "ethernets": { "eth0": { @@ -596,6 +598,7 @@ class TestGenerateNetworkConfig: } ] }, + True, { "ethernets": { "eth0": { @@ -633,6 +636,7 @@ class TestGenerateNetworkConfig: } ] }, + True, { "ethernets": { "eth0": { @@ -670,6 +674,7 @@ class TestGenerateNetworkConfig: ] * 3 }, + True, { "ethernets": { "eth0": { @@ -736,6 +741,7 @@ class TestGenerateNetworkConfig: } ] }, + True, { "ethernets": { "eth0": { @@ -751,6 +757,60 @@ class TestGenerateNetworkConfig: "version": 2, }, ), + ( + "secondary IPv4s are not configured", + { + "interface": [ + { + "macAddress": "000D3A047598", + "ipv6": { + "subnet": [ + { + "prefix": "10", + "address": "2001:dead:beef::16", + } + ], + "ipAddress": [ + {"privateIpAddress": "2001:dead:beef::1"} + ], + }, + "ipv4": { + "subnet": [ + {"prefix": "24", "address": "10.0.0.0"}, + ], + "ipAddress": [ + { + "privateIpAddress": "10.0.0.4", + "publicIpAddress": "104.46.124.81", + }, + { + "privateIpAddress": "11.0.0.5", + "publicIpAddress": "104.46.124.82", + }, + { + "privateIpAddress": "12.0.0.6", + "publicIpAddress": "104.46.124.83", + }, + ], + }, + } + ] + }, + False, + { + "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", + } + }, + "version": 2, + }, + ), ( "ipv6 secondaries", { @@ -772,6 +832,7 @@ class TestGenerateNetworkConfig: } ] }, + True, { "ethernets": { "eth0": { @@ -787,14 +848,50 @@ class TestGenerateNetworkConfig: "version": 2, }, ), + ( + "ipv6 secondaries not configured", + { + "interface": [ + { + "macAddress": "000D3A047598", + "ipv6": { + "subnet": [ + { + "prefix": "10", + "address": "2001:dead:beef::16", + } + ], + "ipAddress": [ + {"privateIpAddress": "2001:dead:beef::1"}, + {"privateIpAddress": "2001:dead:beef::2"}, + ], + }, + } + ] + }, + False, + { + "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", + } + }, + "version": 2, + }, + ), ], ) def test_parsing_scenarios( - self, label, mock_get_interfaces, metadata, expected + self, label, mock_get_interfaces, metadata, ip_config, expected ): assert ( dsaz.generate_network_config_from_instance_network_metadata( - metadata + metadata, apply_network_config_for_secondary_ips=ip_config ) == expected )