diff --git a/cloudinit/sources/DataSourceDigitalOcean.py b/cloudinit/sources/DataSourceDigitalOcean.py index b4856011431..ddd3310892e 100644 --- a/cloudinit/sources/DataSourceDigitalOcean.py +++ b/cloudinit/sources/DataSourceDigitalOcean.py @@ -110,7 +110,7 @@ def network_config(self): migration. """ - if self._network_config: + if self._network_config and self._network_config != sources.UNSET: return self._network_config interfaces = self.metadata.get("interfaces") diff --git a/cloudinit/sources/DataSourceUpCloud.py b/cloudinit/sources/DataSourceUpCloud.py index 618052086dc..62304b117c3 100644 --- a/cloudinit/sources/DataSourceUpCloud.py +++ b/cloudinit/sources/DataSourceUpCloud.py @@ -115,7 +115,7 @@ def network_config(self): due to reconfiguration. """ - if self._network_config: + if self._network_config and self._network_config != sources.UNSET: return self._network_config raw_network_config = self.metadata.get("network") diff --git a/tests/unittests/sources/test_digitalocean.py b/tests/unittests/sources/test_digitalocean.py index a99718b9ba5..57d94e28307 100644 --- a/tests/unittests/sources/test_digitalocean.py +++ b/tests/unittests/sources/test_digitalocean.py @@ -12,7 +12,7 @@ import pytest -from cloudinit import settings +from cloudinit import settings, sources from cloudinit.sources import DataSourceDigitalOcean from cloudinit.sources.helpers import digitalocean @@ -248,6 +248,35 @@ def test_multiple_ssh_keys(self, mock_readmd, get_ds): assert metadata["public_keys"] == ds.get_public_ssh_keys() assert isinstance(ds.get_public_ssh_keys(), list) + @mock.patch("cloudinit.net.get_interfaces_by_mac") + @mock.patch("cloudinit.sources.helpers.digitalocean.read_metadata") + def test_network_config_unset_recomputes( + self, mock_readmd, m_get_by_mac, get_ds + ): + """GH-7067: network_config must recompute when + _network_config is UNSET. + + update_metadata_if_supported() sets _network_config = sources.UNSET + (the truthy string "_unset"). Without the UNSET sentinel check, the + bare truthiness guard returns the sentinel directly instead of + recomputing, causing AttributeError in apply_network_config. + """ + mock_readmd.return_value = DO_META.copy() + m_get_by_mac.return_value = { + "04:01:57:d1:9e:01": "eth0", + "04:01:57:d1:9e:02": "eth1", + } + + ds = get_ds() + assert ds.get_data() is True + + # Simulate what update_metadata_if_supported() does on every boot + ds._network_config = sources.UNSET + recomputed_netcfg = ds.network_config + assert recomputed_netcfg != sources.UNSET + assert isinstance(recomputed_netcfg, dict) + assert "config" in recomputed_netcfg + class TestNetworkConvert: def _get_networking(self): diff --git a/tests/unittests/sources/test_upcloud.py b/tests/unittests/sources/test_upcloud.py index 68bff97cd04..80bd79df96c 100644 --- a/tests/unittests/sources/test_upcloud.py +++ b/tests/unittests/sources/test_upcloud.py @@ -278,6 +278,39 @@ def test_network_configuration(self, m_get_by_mac, mock_readmd, ds): UC_METADATA.get("network").get("dns")[1] == dns.get("address")[1] ) + @mock.patch("cloudinit.sources.helpers.upcloud.read_metadata") + @mock.patch("cloudinit.net.get_interfaces_by_mac") + def test_network_config_unset_recomputes( + self, m_get_by_mac, mock_readmd, ds + ): + """GH-7067: network_config must recompute when + _network_config is UNSET. + + update_metadata_if_supported() sets _network_config = sources.UNSET + (the truthy string "_unset"). Without the UNSET sentinel check, the + bare truthiness guard returns the sentinel directly instead of + recomputing, causing AttributeError in apply_network_config. + """ + mock_readmd.return_value = UC_METADATA.copy() + + raw_ifaces = UC_METADATA.get("network").get("interfaces") + m_get_by_mac.return_value = { + raw_ifaces[0].get("mac"): "eth0", + raw_ifaces[1].get("mac"): "eth1", + raw_ifaces[2].get("mac"): "eth2", + raw_ifaces[3].get("mac"): "eth3", + } + + ds.perform_dhcp_setup = False + assert ds.get_data() is True + + # Simulate what update_metadata_if_supported() does + ds._network_config = sources.UNSET + recomputed_netcfg = ds.network_config + assert recomputed_netcfg != sources.UNSET + assert isinstance(recomputed_netcfg, dict) + assert 1 == recomputed_netcfg.get("version") + class TestUpCloudDatasourceLoading: def test_get_datasource_list_returns_in_local(self):