From e820fe10a66ac266d78b1a6c11e50cbeb8e27024 Mon Sep 17 00:00:00 2001 From: a-dubs Date: Thu, 17 Jul 2025 10:05:47 -0400 Subject: [PATCH 1/2] feat(network): add keep_configuration setting to v1 This config option is only implemented for the Netplan renderer. It is used to designate the connection as 'critical to the system', meaning that special care will be taken not to release the assigned IP when the daemon is restarted. PR: 6097 --- cloudinit/config/schemas/schema-network-config-v1.json | 4 ++++ cloudinit/net/netplan.py | 2 ++ cloudinit/net/network_state.py | 1 + doc/rtd/reference/network-config-format-v1.rst | 9 +++++++++ 4 files changed, 16 insertions(+) diff --git a/cloudinit/config/schemas/schema-network-config-v1.json b/cloudinit/config/schemas/schema-network-config-v1.json index 38270e98235..92ded462706 100644 --- a/cloudinit/config/schemas/schema-network-config-v1.json +++ b/cloudinit/config/schemas/schema-network-config-v1.json @@ -40,6 +40,10 @@ "accept-ra": { "type": "boolean", "description": "Whether to accept IPv6 Router Advertisements (RA) on this interface. If unset, it will not be rendered" + }, + "keep_configuration": { + "type": "boolean", + "description": "Designate the connection as 'critical to the system', meaning that special care will be taken not to release the assigned IP when the daemon is restarted. (only recognized by Netplan renderer)." } } }, diff --git a/cloudinit/net/netplan.py b/cloudinit/net/netplan.py index bce89bbd302..64a3c4786a3 100644 --- a/cloudinit/net/netplan.py +++ b/cloudinit/net/netplan.py @@ -481,6 +481,8 @@ def _render_content(self, network_state: NetworkState) -> str: "set-name": ifname, "match": ifcfg.get("match", None), } + if "keep_configuration" in ifcfg: + eth["critical"] = ifcfg["keep_configuration"] if eth["match"] is None: macaddr = ifcfg.get("mac_address", None) if macaddr is not None: diff --git a/cloudinit/net/network_state.py b/cloudinit/net/network_state.py index 2bdd2b614c0..9fd51e42916 100644 --- a/cloudinit/net/network_state.py +++ b/cloudinit/net/network_state.py @@ -428,6 +428,7 @@ def handle_physical(self, command): "accept-ra": accept_ra, "wakeonlan": wakeonlan, "optional": optional, + "keep_configuration": command.get("keep_configuration"), } ) iface_key = command.get("config_id", command.get("name")) diff --git a/doc/rtd/reference/network-config-format-v1.rst b/doc/rtd/reference/network-config-format-v1.rst index 3598b4b377f..5186075b093 100644 --- a/doc/rtd/reference/network-config-format-v1.rst +++ b/doc/rtd/reference/network-config-format-v1.rst @@ -95,6 +95,15 @@ Physical example .. literalinclude:: ../../examples/network-config-v1-physical-3-nic.yaml :language: yaml +``keep_configuration: `` +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Designate the connection as 'critical to the system', meaning that special care +will be taken not to release the assigned IP when the daemon is restarted. + +.. note:: + This is only recognized by Netplan renderer. + Bond ---- From 757dcfe31d4b27b92224a1f73a6898329b12550b Mon Sep 17 00:00:00 2001 From: a-dubs Date: Thu, 17 Jul 2025 10:09:19 -0400 Subject: [PATCH 2/2] feat(oracle): set keep_configuration to true for iscsi instances This is necessary for Oracle Baremetal instances on IPv6-only networks which rely on the iscsi connection. Without this, after shutting down the instance, the instance is not recoverable. This setting will be automatically set for all iscsi instances via the Oracle Datasource based on metadata/config files left behind by initramfs during boot on ISCSI instances. PR: #6097 --- cloudinit/sources/DataSourceOracle.py | 100 +++++++----------- .../datasources/test_oci_networking.py | 47 ++++++++ tests/unittests/sources/test_oracle.py | 89 ---------------- 3 files changed, 88 insertions(+), 148 deletions(-) diff --git a/cloudinit/sources/DataSourceOracle.py b/cloudinit/sources/DataSourceOracle.py index e4195f8f7f8..aa62aa2d2aa 100644 --- a/cloudinit/sources/DataSourceOracle.py +++ b/cloudinit/sources/DataSourceOracle.py @@ -293,9 +293,15 @@ def network_config(self): return self._network_config set_primary = False - # this is v1 if self._is_iscsi_root(): self._network_config = self._get_iscsi_config() + logging.debug( + "Instance is using iSCSI root, setting primary NIC as critical" + ) + # This is necessary for Oracle baremetal instances in case they are + # running on an IPv6-only network. Without this, they become + # unreachable/unrecoverable after a shutdown. + self._network_config["config"][0]["keep_configuration"] = True if not self._has_network_config(): LOG.debug( "Could not obtain network configuration from initramfs. " @@ -380,65 +386,41 @@ def _add_network_config_from_opc_imds(self, set_primary: bool = False): else: network = ipaddress.ip_network(vnic_dict["subnetCidrBlock"]) - if self._network_config["version"] == 1: - if is_primary: - if is_ipv6_only: - subnets = [{"type": "dhcp6"}] - else: - subnets = [{"type": "dhcp"}] + if is_primary: + if is_ipv6_only: + subnets = [{"type": "dhcp6"}] else: - subnets = [] - if vnic_dict.get("privateIp"): - subnets.append( - { - "type": "static", - "address": ( - f"{vnic_dict['privateIp']}/" - f"{network.prefixlen}" - ), - } - ) - if vnic_dict.get("ipv6Addresses"): - subnets.append( - { - "type": "static", - "address": ( - f"{vnic_dict['ipv6Addresses'][0]}/" - f"{network.prefixlen}" - ), - } - ) - interface_config = { - "name": name, - "type": "physical", - "mac_address": mac_address, - "mtu": MTU, - "subnets": subnets, - } - self._network_config["config"].append(interface_config) - elif self._network_config["version"] == 2: - # Why does this elif exist??? - # Are there plans to switch to v2? - interface_config = { - "mtu": MTU, - "match": {"macaddress": mac_address}, - } - self._network_config["ethernets"][name] = interface_config - - interface_config["dhcp6"] = is_primary and is_ipv6_only - interface_config["dhcp4"] = is_primary and not is_ipv6_only - if not is_primary: - interface_config["addresses"] = [] - if vnic_dict.get("privateIp"): - interface_config["addresses"].append( - f"{vnic_dict['privateIp']}/{network.prefixlen}" - ) - if vnic_dict.get("ipv6Addresses"): - interface_config["addresses"].append( - f"{vnic_dict['ipv6Addresses'][0]}/" - f"{network.prefixlen}" - ) - self._network_config["ethernets"][name] = interface_config + subnets = [{"type": "dhcp"}] + else: + subnets = [] + if vnic_dict.get("privateIp"): + subnets.append( + { + "type": "static", + "address": ( + f"{vnic_dict['privateIp']}/" + f"{network.prefixlen}" + ), + } + ) + if vnic_dict.get("ipv6Addresses"): + subnets.append( + { + "type": "static", + "address": ( + f"{vnic_dict['ipv6Addresses'][0]}/" + f"{network.prefixlen}" + ), + } + ) + interface_config = { + "name": name, + "type": "physical", + "mac_address": mac_address, + "mtu": MTU, + "subnets": subnets, + } + self._network_config["config"].append(interface_config) class DataSourceOracleNet(DataSourceOracle): diff --git a/tests/integration_tests/datasources/test_oci_networking.py b/tests/integration_tests/datasources/test_oci_networking.py index 9b807927668..f50d6b96cf1 100644 --- a/tests/integration_tests/datasources/test_oci_networking.py +++ b/tests/integration_tests/datasources/test_oci_networking.py @@ -157,3 +157,50 @@ def test_oci_networking_system_cfg(client: IntegrationInstance, tmpdir): netplan_cfg = yaml.safe_load(netplan_yaml) expected_netplan_cfg = yaml.safe_load(SYSTEM_CFG) assert expected_netplan_cfg == netplan_cfg + + +@pytest.mark.skipif(PLATFORM != "oci", reason="Test is OCI specific") +def test_oci_keep_configuration_networking_config( + session_cloud: IntegrationCloud, +): + """ + Test to ensure the keep_configuration is applied on Oracle ISCSI instances. + + This test launches a Baremetal OCI instance so that ISCSI is used, and + checks that the primary systemd network configuration file contains the + 'KeepConfiguration=true' directive, which indicates that the network + configuration is preserved as expected. + + Assertions: + - At least one netplan file exists under '/run/systemd/network'. + - The primary systemd network configuration file includes the + 'KeepConfiguration=true' directive. + - The netplan configuration includes the 'critical: true' directive. + """ + with session_cloud.launch( + launch_kwargs={ + "instance_type": "BM.Optimized3.36", + }, + ) as client: + r = client.execute("ls /run/systemd/network/10-netplan-*.network") + assert r.ok, ( + "No netplan files found under /run/systemd/network. We are looking" + " for netplan files here to check that the underlying " + "'KeepConfiguration=true' directive is actually being applied to " + "the systemd network configuration." + ) + primary_systemd_file: str = r.stdout.strip().splitlines()[0] + systemd_config = client.read_from_file(primary_systemd_file) + assert ( + "KeepConfiguration=true" in systemd_config + or "CriticalConnection=true" in systemd_config + ), ( + f"Neither 'KeepConfiguration=true' nor 'CriticalConnection=true' " + f"found in '{primary_systemd_file}':\n{primary_systemd_file}" + ) + netplan_config = client.read_from_file( + "/etc/netplan/50-cloud-init.yaml", + ) + assert ( + "critical: true" in netplan_config + ), "critical: true not found in netplan config" diff --git a/tests/unittests/sources/test_oracle.py b/tests/unittests/sources/test_oracle.py index efa492d1058..1a61510a3b2 100644 --- a/tests/unittests/sources/test_oracle.py +++ b/tests/unittests/sources/test_oracle.py @@ -485,48 +485,6 @@ def test_imds_nic_setup_v1(self, set_primary, oracle_ds): assert "10.0.0.231/24" == secondary_cfg["subnets"][0]["address"] assert "static" == secondary_cfg["subnets"][0]["type"] - @pytest.mark.parametrize( - "set_primary", - [True, False], - ) - def test_secondary_nic_v2(self, set_primary, oracle_ds): - oracle_ds._vnics_data = json.loads(OPC_VM_SECONDARY_VNIC_RESPONSE) - oracle_ds._network_config = { - "version": 2, - "ethernets": {"primary": {"nic": {}}}, - } - with mock.patch( - f"{DS_PATH}.get_interfaces_by_mac", - return_value={ - "02:00:17:05:d1:db": "ens3", - "00:00:17:02:2b:b1": "ens4", - }, - ): - oracle_ds._add_network_config_from_opc_imds( - set_primary=set_primary - ) - - nic_cfg = oracle_ds.network_config["ethernets"] - if set_primary: - assert "ens3" in nic_cfg - primary_cfg = nic_cfg["ens3"] - - assert primary_cfg["dhcp4"] is True - assert primary_cfg["dhcp6"] is False - assert "02:00:17:05:d1:db" == primary_cfg["match"]["macaddress"] - assert 9000 == primary_cfg["mtu"] - assert "addresses" not in primary_cfg - - assert "ens4" in nic_cfg - secondary_cfg = nic_cfg["ens4"] - assert secondary_cfg["dhcp4"] is False - assert secondary_cfg["dhcp6"] is False - assert "00:00:17:02:2b:b1" == secondary_cfg["match"]["macaddress"] - assert 9000 == secondary_cfg["mtu"] - - assert 1 == len(secondary_cfg["addresses"]) - assert "10.0.0.231/24" == secondary_cfg["addresses"][0] - @pytest.mark.parametrize( "set_primary", [ @@ -578,53 +536,6 @@ def test_imds_nic_setup_v1_ipv6_only(self, set_primary, oracle_ds): ) assert "static" == secondary_cfg["subnets"][0]["type"] - @pytest.mark.parametrize( - "set_primary", - [True, False], - ) - def test_secondary_nic_v2_ipv6_only(self, set_primary, oracle_ds): - oracle_ds._vnics_data = json.loads( - OPC_VM_IPV6_ONLY_SECONDARY_VNIC_RESPONSE - ) - oracle_ds._network_config = { - "version": 2, - "ethernets": {"primary": {"nic": {}}}, - } - with mock.patch( - f"{DS_PATH}.get_interfaces_by_mac", - return_value={ - "02:00:17:0d:6b:be": "ens3", - "02:00:17:18:f6:ff": "ens4", - }, - ): - oracle_ds._add_network_config_from_opc_imds( - set_primary=set_primary - ) - - nic_cfg = oracle_ds.network_config["ethernets"] - if set_primary: - assert "ens3" in nic_cfg - primary_cfg = nic_cfg["ens3"] - - assert primary_cfg["dhcp4"] is False - assert primary_cfg["dhcp6"] is True - assert "02:00:17:0d:6b:be" == primary_cfg["match"]["macaddress"] - assert 9000 == primary_cfg["mtu"] - assert "addresses" not in primary_cfg - - assert "ens4" in nic_cfg - secondary_cfg = nic_cfg["ens4"] - assert secondary_cfg["dhcp4"] is False - assert secondary_cfg["dhcp6"] is False - assert "02:00:17:18:f6:ff" == secondary_cfg["match"]["macaddress"] - assert 9000 == secondary_cfg["mtu"] - - assert 1 == len(secondary_cfg["addresses"]) - assert ( - "2603:c020:400d:5d7e:aacc:8e5f:3b1b:3a4a/128" - == secondary_cfg["addresses"][0] - ) - @pytest.mark.parametrize("error_add_network", [None, Exception]) @pytest.mark.parametrize( "configure_secondary_nics",