From 7004ba5d65e6928e4a11ed24c56336ab93b35d40 Mon Sep 17 00:00:00 2001 From: sundeep8967 Date: Sun, 6 Sep 2026 00:53:40 +0530 Subject: [PATCH 1/2] DataSourceUpCloud: recompute network_config when set to UNSET When update_metadata_if_supported() is invoked on boot, it clears cached network configuration by setting _network_config to UNSET ("_unset"). Because DataSourceUpCloud.network_config checked truthiness directly, it returned the UNSET string sentinel instead of recomputing from metadata, causing an AttributeError in apply_network_config. Fixes GH-7067 Signed-off-by: sundeep8967 --- cloudinit/sources/DataSourceUpCloud.py | 2 +- tests/unittests/sources/test_upcloud.py | 7 +++++++ 2 files changed, 8 insertions(+), 1 deletion(-) 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_upcloud.py b/tests/unittests/sources/test_upcloud.py index 68bff97cd04..f0f6d45cfa4 100644 --- a/tests/unittests/sources/test_upcloud.py +++ b/tests/unittests/sources/test_upcloud.py @@ -278,6 +278,13 @@ def test_network_configuration(self, m_get_by_mac, mock_readmd, ds): UC_METADATA.get("network").get("dns")[1] == dns.get("address")[1] ) + # GH-7067: Verify recomputation when _network_config is UNSET + 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): From d9392de5ef88bb5e06df1e5b70aa6f5acb18e936 Mon Sep 17 00:00:00 2001 From: sundeep8967 Date: Thu, 10 Sep 2026 09:19:18 +0530 Subject: [PATCH 2/2] upcloud,digitalocean: check UNSET sentinel in network_config property The network_config property in DataSourceUpCloud and DataSourceDigitalOcean used a bare truthiness check on _network_config. When update_metadata_if_supported() sets _network_config = UNSET (the truthy string "_unset"), the sentinel is returned directly instead of recomputing network configuration, causing AttributeError: 'str' object has no attribute 'get' in apply_network_config on subsequent boots. Fix DataSourceDigitalOcean.network_config to mirror the existing pattern in DataSourceUpCloud by adding an explicit sources.UNSET sentinel check. Separate the regression test for UpCloud into its own dedicated test method (test_network_config_unset_recomputes) and add an equivalent test for DataSourceDigitalOcean. Fixes GH-7067 Signed-off-by: sundeep8967 --- cloudinit/sources/DataSourceDigitalOcean.py | 2 +- tests/unittests/sources/test_digitalocean.py | 31 +++++++++++++++++++- tests/unittests/sources/test_upcloud.py | 28 +++++++++++++++++- 3 files changed, 58 insertions(+), 3 deletions(-) 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/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 f0f6d45cfa4..80bd79df96c 100644 --- a/tests/unittests/sources/test_upcloud.py +++ b/tests/unittests/sources/test_upcloud.py @@ -278,7 +278,33 @@ def test_network_configuration(self, m_get_by_mac, mock_readmd, ds): UC_METADATA.get("network").get("dns")[1] == dns.get("address")[1] ) - # GH-7067: Verify recomputation when _network_config is UNSET + @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