diff --git a/cloudinit/sources/DataSourceScaleway.py b/cloudinit/sources/DataSourceScaleway.py index b2e8740b68a..f876186f99e 100644 --- a/cloudinit/sources/DataSourceScaleway.py +++ b/cloudinit/sources/DataSourceScaleway.py @@ -322,12 +322,12 @@ def network_config(self): if self._network_config != sources.UNSET: return self._network_config - if self.metadata["private_ip"] is None: + if not self.metadata.get("private_ip"): # New method of network configuration netcfg = {} ip_cfg = {} - for ip in self.metadata["public_ips"]: + for ip in self.metadata.get("public_ips", []): # Use DHCP for primary address if ip["address"] == self.ephemeral_fixed_address: ip_cfg["dhcp4"] = True @@ -342,15 +342,16 @@ def network_config(self): else: ip_cfg["routes"] = [route] else: + address_cidr = ip["address"] + if ip.get("netmask"): + address_cidr += f"/{ip['netmask']}" + if "addresses" in ip_cfg.keys(): - ip_cfg["addresses"] += ( - f'{ip["address"]}/{ip["netmask"]}', - ) + ip_cfg["addresses"] += (address_cidr,) else: - ip_cfg["addresses"] = ( - f'{ip["address"]}/{ip["netmask"]}', - ) - if ip["family"] == "inet6": + ip_cfg["addresses"] = (address_cidr,) + + if ip["family"] == "inet6" and ip.get("gateway"): route = {"via": ip["gateway"], "to": "::/0"} if "routes" in ip_cfg.keys(): ip_cfg["routes"] += [route] @@ -365,22 +366,26 @@ def network_config(self): "name": "%s" % self.distro.fallback_interface, } subnets = [{"type": "dhcp4"}] - if self.metadata["ipv6"]: - subnets += [ - { - "type": "static", - "address": "%s" % self.metadata["ipv6"]["address"], - "netmask": "%s" % self.metadata["ipv6"]["netmask"], - "routes": [ - { - "network": "::", - "prefix": "0", - "gateway": "%s" - % self.metadata["ipv6"]["gateway"], - } - ], - } - ] + ipv6 = self.metadata.get("ipv6") + if ( + isinstance(ipv6, dict) + and "address" in ipv6 + ): + subnet = { + "type": "static", + "address": ipv6.get("address"), + } + if ipv6.get("netmask"): + subnet["netmask"] = ipv6.get("netmask") + if ipv6.get("gateway"): + subnet["routes"] = [ + { + "network": "::", + "prefix": "0", + "gateway": ipv6.get("gateway"), + } + ] + subnets.append(subnet) netcfg["subnets"] = subnets self._network_config = {"version": 1, "config": [netcfg]} LOG.debug("network_config : %s", self._network_config) @@ -391,10 +396,12 @@ def launch_index(self): return None def get_instance_id(self): - return self.metadata["id"] + return self.metadata.get("id") def get_public_ssh_keys(self): - ssh_keys = [key["key"] for key in self.metadata["ssh_public_keys"]] + ssh_keys = [ + key["key"] for key in self.metadata.get("ssh_public_keys", []) + ] akeypre = "AUTHORIZED_KEY=" plen = len(akeypre) @@ -406,7 +413,7 @@ def get_public_ssh_keys(self): return ssh_keys def get_hostname(self, fqdn=False, resolve_ip=False, metadata_only=False): - return DataSourceHostname(self.metadata["hostname"], False) + return DataSourceHostname(self.metadata.get("hostname"), False) @property def availability_zone(self): diff --git a/tests/unittests/sources/test_scaleway.py b/tests/unittests/sources/test_scaleway.py index 8a0736443e5..c2abf6d7df8 100644 --- a/tests/unittests/sources/test_scaleway.py +++ b/tests/unittests/sources/test_scaleway.py @@ -741,6 +741,69 @@ def test_legacy_network_config_ipv6_ok(self, m_get_cmdline, fallback_nic): } assert netcfg == resp + @mock.patch("cloudinit.distros.net.find_fallback_nic") + @mock.patch("cloudinit.util.get_cmdline") + def test_legacy_network_config_ipv6_missing_key( + self, m_get_cmdline, fallback_nic + ): + """ + network_config will include IPv6 config even if gateway is missing + """ + m_get_cmdline.return_value = "scaleway" + fallback_nic.return_value = "ens2" + self.datasource.metadata["private_ip"] = "10.10.10.10" + # Missing 'gateway' + self.datasource.metadata["ipv6"] = { + "address": "2000:abc:4444:9876::42:999", + "netmask": "127", + } + + netcfg = self.datasource.network_config + resp = { + "version": 1, + "config": [ + { + "type": "physical", + "name": "ens2", + "subnets": [ + {"type": "dhcp4"}, + { + "type": "static", + "address": "2000:abc:4444:9876::42:999", + "netmask": "127", + }, + ], + } + ], + } + assert netcfg == resp + + @mock.patch("cloudinit.distros.net.find_fallback_nic") + @mock.patch("cloudinit.util.get_cmdline") + def test_legacy_network_config_ipv6_empty_dict( + self, m_get_cmdline, fallback_nic + ): + """ + network_config will ignore IPv6 config if it is an empty dict + """ + m_get_cmdline.return_value = "scaleway" + fallback_nic.return_value = "ens2" + self.datasource.metadata["private_ip"] = "10.10.10.10" + self.datasource.metadata["ipv6"] = {} + + netcfg = self.datasource.network_config + resp = { + "version": 1, + "config": [ + { + "type": "physical", + "name": "ens2", + "subnets": [{"type": "dhcp4"}], + } + ], + } + assert netcfg == resp + @mock.patch("cloudinit.distros.net.find_fallback_nic") @mock.patch("cloudinit.util.get_cmdline") def test_legacy_network_config_existing(self, m_get_cmdline, fallback_nic): @@ -848,6 +911,40 @@ def test_ipmob_primary_ipv4_config_ok(self, m_get_cmdline, fallback_nic): assert netcfg == resp + @mock.patch("cloudinit.distros.net.find_fallback_nic") + @mock.patch("cloudinit.util.get_cmdline") + def test_ipmob_private_ip_unset(self, m_get_cmdline, fallback_nic): + """ + network_config will generate Version 2 config if 'private_ip' is unset + """ + m_get_cmdline.return_value = "scaleway" + fallback_nic.return_value = "ens2" + # Ensure private_ip is completely absent from metadata + if "private_ip" in self.datasource.metadata: + del self.datasource.metadata["private_ip"] + self.datasource.metadata["ipv6"] = None + self.datasource.ephemeral_fixed_address = "10.10.10.10" + self.datasource.metadata["public_ips"] = [{"address": "10.10.10.10"}] + + netcfg = self.datasource.network_config + resp = { + "version": 2, + "ethernets": { + fallback_nic.return_value: { + "routes": [ + { + "on-link": True, + "to": "169.254.42.42/32", + "via": "62.210.0.1", + } + ], + "dhcp4": True, + }, + }, + } + + assert netcfg == resp + @mock.patch("cloudinit.distros.net.find_fallback_nic") @mock.patch("cloudinit.util.get_cmdline") def test_ipmob_additional_ipv4_config_ok( @@ -1033,3 +1130,35 @@ def test_ipmob_primary_ipv6_v4_config_ok( } assert netcfg == resp + + @mock.patch("cloudinit.distros.net.find_fallback_nic") + @mock.patch("cloudinit.util.get_cmdline") + def test_ipmob_ipv6_missing_netmask_gateway( + self, m_get_cmdline, fallback_nic + ): + """ + Generate network_config with IPv6 missing netmask and gateway + """ + m_get_cmdline.return_value = "scaleway" + fallback_nic.return_value = "ens2" + self.datasource.metadata["private_ip"] = None + self.datasource.metadata["ipv6"] = None + self.datasource.ephemeral_fixed_address = "10.10.10.10" + self.datasource.metadata["public_ips"] = [ + { + "address": "2001:aaa:aaaa:a:aaaa:aaaa:aaaa:1", + "family": "inet6", + }, + ] + + netcfg = self.datasource.network_config + resp = { + "version": 2, + "ethernets": { + fallback_nic.return_value: { + "addresses": ("2001:aaa:aaaa:a:aaaa:aaaa:aaaa:1",), + }, + }, + } + + assert netcfg == resp