File items_Fix_creation_of_default_NetworkInterface.patch of Package cobbler

From 689cb2371078196d31c739f20e3f7459d2cec3fa Mon Sep 17 00:00:00 2001
From: Bruno Travouillon <devel@travouillon.fr>
Date: Fri, 3 Jun 2022 23:20:54 -0400
Subject: [PATCH 1/3] Items: Fix creation of "default" NetworkInterface

Do not force creation of a network interface named `default`.

Closes: #2838

From 2d04011b4535fb4011189cfae68509c2870cd2b0 Mon Sep 17 00:00:00 2001
From: Bruno Travouillon <devel@travouillon.fr>
Date: Sat, 4 Jun 2022 11:27:41 -0400
Subject: [PATCH 2/3] Tests: Define default system interface explicitly

Do not assume the `default` interface is implicitly created with a
system.

From 7ad5981fec4c7f68b847b112708f6ca6de27a9fc Mon Sep 17 00:00:00 2001
From: Bruno Travouillon <devel@travouillon.fr>
Date: Tue, 7 Jun 2022 21:31:47 -0400
Subject: [PATCH 3/3] Tests: Create systems in xmlrpcapi

Ensure that:
 - only the `default` network interface exists when no `--interface`.
 - only one interface exists when interface name is provided.
 - `--interface` is optional with several interfaces and `default`.
 - `--interface` is mandatory with several interfaces and no `default`.


---
 cobbler/items/system.py                |    2 
 cobbler/remote.py                      |   18 +
 tests/conftest.py                      |    4 
 tests/items/system_test.py             |    2 
 tests/modules/managers/dnsmasq_test.py |    5 
 tests/modules/managers/ndjbdns_test.py |    3 
 tests/xmlrpcapi/miscellaneous_test.py  |  299 +++++++++++++++++++++++++++++++++
 7 files changed, 326 insertions(+), 7 deletions(-)

Index: cobbler-3.3.3/cobbler/items/system.py
===================================================================
--- cobbler-3.3.3.orig/cobbler/items/system.py
+++ cobbler-3.3.3/cobbler/items/system.py
@@ -743,7 +743,7 @@ class System(Item):
         :param api: The Cobbler API
         """
         super().__init__(api, *args, **kwargs)
-        self._interfaces: Dict[str, NetworkInterface] = {"default": NetworkInterface(api)}
+        self._interfaces: Dict[str, NetworkInterface] = {}
         self._ipv6_autoconfiguration = False
         self._repos_enabled = False
         self._autoinstall = enums.VALUE_INHERITED
Index: cobbler-3.3.3/cobbler/remote.py
===================================================================
--- cobbler-3.3.3.orig/cobbler/remote.py
+++ cobbler-3.3.3/cobbler/remote.py
@@ -2151,8 +2151,22 @@ class CobblerXMLRPCInterface:
             system_to_edit = self.__get_object(handle)
             if system_to_edit is None:
                 raise ValueError("No system found with the specified name (name given: \"%s\")!" % object_name)
-            # If we don't have an explicit interface name use the default interface
-            interface_name = attributes.get("interface", "default")
+
+            # If we don't have an explicit interface name use the default interface or require an explicit
+            # interface if default cannot be found.
+            if (
+                len(system_to_edit.interfaces) > 1
+                and attributes.get("interface") is None
+            ):
+                if "default" not in system_to_edit.interfaces.keys():
+                    raise ValueError("Interface is required.")
+                interface_name = "default"
+            if len(system_to_edit.interfaces) == 1:
+                interface_name = attributes.get(
+                    "interface", next(iter(system_to_edit.interfaces))
+                )
+            else:
+                interface_name = attributes.get("interface", "default")
             self.logger.debug("Interface \"%s\" is being edited.", interface_name)
             interface = system_to_edit.interfaces.get(interface_name)
             if interface is None:
Index: cobbler-3.3.3/tests/conftest.py
===================================================================
--- cobbler-3.3.3.orig/tests/conftest.py
+++ cobbler-3.3.3/tests/conftest.py
@@ -8,8 +8,8 @@ import pytest
 from cobbler.api import CobblerAPI
 from cobbler.items.distro import Distro
 from cobbler.items.profile import Profile
-from cobbler.items.system import System
 from cobbler.items.image import Image
+from cobbler.items.system import NetworkInterface, System
 
 
 @contextmanager
@@ -142,6 +142,7 @@ def create_system(request, cobbler_api):
             test_system.profile = profile_name
         if image_name != "":
             test_system.image = image_name
+        test_system.interfaces = {"default": NetworkInterface(cobbler_api)}
         cobbler_api.add_system(test_system)
         return test_system
 
Index: cobbler-3.3.3/tests/items/system_test.py
===================================================================
--- cobbler-3.3.3.orig/tests/items/system_test.py
+++ cobbler-3.3.3/tests/items/system_test.py
@@ -644,6 +644,7 @@ def test_serial_baud_rate(cobbler_api, v
 def test_from_dict_with_network_interface(cobbler_api):
     # Arrange
     system = System(cobbler_api)
+    system.interfaces = {"default": NetworkInterface(cobbler_api)}
     sys_dict = system.to_dict()
 
     # Act
@@ -663,6 +664,7 @@ def test_from_dict_with_network_interfac
 def test_is_management_supported(cobbler_api, input_mac, input_ipv4, input_ipv6, expected_result):
     # Arrange
     system = System(cobbler_api)
+    system.interfaces = {"default": NetworkInterface(cobbler_api)}
     system.interfaces["default"].mac_address = input_mac
     system.interfaces["default"].ip_address = input_ipv4
     system.interfaces["default"].ipv6_address = input_ipv6
Index: cobbler-3.3.3/tests/modules/managers/dnsmasq_test.py
===================================================================
--- cobbler-3.3.3.orig/tests/modules/managers/dnsmasq_test.py
+++ cobbler-3.3.3/tests/modules/managers/dnsmasq_test.py
@@ -2,7 +2,7 @@ import time
 from unittest.mock import MagicMock
 
 from cobbler.modules.managers import dnsmasq
-from cobbler.items.system import System
+from cobbler.items.system import NetworkInterface, System
 from cobbler.items.distro import Distro
 from cobbler.items.profile import Profile
 from cobbler.templar import Templar
@@ -41,6 +41,7 @@ def test_manager_write_configs(mocker, c
     mock_profile = Profile(cobbler_api)
     mock_system = System(cobbler_api)
     mock_system.name = "test_manager_regen_hosts_system"
+    mock_system.interfaces = {"default": NetworkInterface(cobbler_api)}
     mock_system.interfaces["default"].dns_name = "host.example.org"
     mock_system.interfaces["default"].mac_address = "aa:bb:cc:dd:ee:ff"
     mock_system.interfaces["default"].ip_address = "192.168.1.2"
@@ -74,6 +75,7 @@ def test_manager_regen_ethers(mocker, co
     mock_builtins_open = mocker.patch("builtins.open", mocker.mock_open())
     mock_system = System(cobbler_api)
     mock_system.name = "test_manager_regen_ethers_system"
+    mock_system.interfaces = {"default": NetworkInterface(cobbler_api)}
     mock_system.interfaces["default"].dns_name = "host.example.org"
     mock_system.interfaces["default"].mac_address = "aa:bb:cc:dd:ee:ff"
     mock_system.interfaces["default"].ip_address = "192.168.1.2"
@@ -96,6 +98,7 @@ def test_manager_regen_hosts(mocker, cob
     mock_builtins_open = mocker.patch("builtins.open", mocker.mock_open())
     mock_system = System(cobbler_api)
     mock_system.name = "test_manager_regen_hosts_system"
+    mock_system.interfaces = {"default": NetworkInterface(cobbler_api)}
     mock_system.interfaces["default"].dns_name = "host.example.org"
     mock_system.interfaces["default"].mac_address = "AA:BB:CC:DD:EE:FF"
     mock_system.interfaces["default"].ip_address = "192.168.1.2"
Index: cobbler-3.3.3/tests/modules/managers/ndjbdns_test.py
===================================================================
--- cobbler-3.3.3.orig/tests/modules/managers/ndjbdns_test.py
+++ cobbler-3.3.3/tests/modules/managers/ndjbdns_test.py
@@ -2,7 +2,7 @@ import subprocess
 from unittest.mock import MagicMock
 
 from cobbler.modules.managers import ndjbdns
-from cobbler.items.system import System
+from cobbler.items.system import NetworkInterface, System
 from cobbler.templar import Templar
 
 
@@ -36,6 +36,7 @@ def test_manager_write_configs(mocker, c
     mock_subproc_popen.return_value.returncode = 0
     mock_system = System(cobbler_api)
     mock_system.name = "test_manager_regen_hosts_system"
+    mock_system.interfaces = {"default": NetworkInterface(cobbler_api)}
     mock_system.interfaces["default"].dns_name = "host.example.org"
     mock_system.interfaces["default"].mac_address = "aa:bb:cc:dd:ee:ff"
     mock_system.interfaces["default"].ip_address = "192.168.1.2"
Index: cobbler-3.3.3/tests/xmlrpcapi/miscellaneous_test.py
===================================================================
--- cobbler-3.3.3.orig/tests/xmlrpcapi/miscellaneous_test.py
+++ cobbler-3.3.3/tests/xmlrpcapi/miscellaneous_test.py
@@ -720,6 +720,305 @@ class TestMiscellaneous:
         # Assert
         assert result
 
+    def test_xapi_system_edit(
+        self,
+        remote,
+        token,
+        create_kernel_initrd,
+        create_distro,
+        create_profile,
+        remove_system,
+    ):
+        # Arrange
+        name_distro = "testsystem_xapi_edit"
+        name_profile = "testsystem_xapi_edit"
+        name_system = "testsystem_xapi_edit"
+        fk_kernel = "vmlinuz1"
+        fk_initrd = "initrd1.img"
+        basepath = create_kernel_initrd(fk_kernel, fk_initrd)
+        path_kernel = os.path.join(basepath, fk_kernel)
+        path_initrd = os.path.join(basepath, fk_initrd)
+        create_distro(name_distro, "x86_64", "suse", path_kernel, path_initrd)
+        create_profile(name_profile, name_distro, "a=1 b=2 c=3 c=4 c=5 d e")
+
+        # Act
+        result = remote.xapi_object_edit(
+            "system",
+            name_system,
+            "add",
+            {
+                "name": name_system,
+                "profile": name_profile,
+            },
+            token,
+        )
+
+        # Assert
+        assert result
+        assert len(remote.get_system("testsystem_xapi_edit").get("interfaces", {})) == 1
+        assert "default" in remote.get_system("testsystem_xapi_edit").get(
+            "interfaces", {}
+        )
+
+        # Cleanup
+        remove_system(name_system)
+
+    def test_xapi_system_edit_interface_name(
+        self,
+        remote,
+        token,
+        create_kernel_initrd,
+        create_distro,
+        create_profile,
+        remove_system,
+    ):
+        # Arrange
+        name_distro = "testsystem_xapi_edit"
+        name_profile = "testsystem_xapi_edit"
+        name_system = "testsystem_xapi_edit"
+        fk_kernel = "vmlinuz1"
+        fk_initrd = "initrd1.img"
+        basepath = create_kernel_initrd(fk_kernel, fk_initrd)
+        path_kernel = os.path.join(basepath, fk_kernel)
+        path_initrd = os.path.join(basepath, fk_initrd)
+        create_distro(name_distro, "x86_64", "suse", path_kernel, path_initrd)
+        create_profile(name_profile, name_distro, "a=1 b=2 c=3 c=4 c=5 d e")
+
+        # Act
+        result = remote.xapi_object_edit(
+            "system",
+            name_system,
+            "add",
+            {
+                "name": name_system,
+                "profile": name_profile,
+                "interface": "eth1",
+            },
+            token,
+        )
+
+        # Assert
+        assert result
+        assert len(remote.get_system("testsystem_xapi_edit").get("interfaces", {})) == 1
+        assert "eth1" in remote.get_system("testsystem_xapi_edit").get("interfaces", {})
+
+    def test_xapi_system_edit_two_interfaces(
+        self,
+        remote,
+        token,
+        create_kernel_initrd,
+        create_distro,
+        create_profile,
+    ):
+        # Arrange
+        name_distro = "testsystem_xapi_edit"
+        name_profile = "testsystem_xapi_edit"
+        name_system = "testsystem_xapi_edit"
+        fk_kernel = "vmlinuz1"
+        fk_initrd = "initrd1.img"
+        basepath = create_kernel_initrd(fk_kernel, fk_initrd)
+        path_kernel = os.path.join(basepath, fk_kernel)
+        path_initrd = os.path.join(basepath, fk_initrd)
+        create_distro(name_distro, "x86_64", "suse", path_kernel, path_initrd)
+        create_profile(name_profile, name_distro, "a=1 b=2 c=3 c=4 c=5 d e")
+
+        # Act
+        result_add = remote.xapi_object_edit(
+            "system",
+            name_system,
+            "add",
+            {
+                "name": name_system,
+                "profile": name_profile,
+            },
+            token,
+        )
+        result_edit = remote.xapi_object_edit(
+            "system",
+            name_system,
+            "edit",
+            {
+                "name": name_system,
+                "interface": "eth1",
+            },
+            token,
+        )
+
+        # Assert
+        assert result_add
+        assert result_edit
+        assert len(remote.get_system("testsystem_xapi_edit").get("interfaces", {})) == 2
+        assert "default" in remote.get_system("testsystem_xapi_edit").get(
+            "interfaces", {}
+        )
+        assert "eth1" in remote.get_system("testsystem_xapi_edit").get("interfaces", {})
+
+    def test_xapi_system_edit_two_interfaces_no_default(
+        self,
+        remote,
+        token,
+        create_kernel_initrd,
+        create_distro,
+        create_profile,
+    ):
+        # Arrange
+        name_distro = "testsystem_xapi_edit"
+        name_profile = "testsystem_xapi_edit"
+        name_system = "testsystem_xapi_edit"
+        fk_kernel = "vmlinuz1"
+        fk_initrd = "initrd1.img"
+        basepath = create_kernel_initrd(fk_kernel, fk_initrd)
+        path_kernel = os.path.join(basepath, fk_kernel)
+        path_initrd = os.path.join(basepath, fk_initrd)
+        create_distro(name_distro, "x86_64", "suse", path_kernel, path_initrd)
+        create_profile(name_profile, name_distro, "a=1 b=2 c=3 c=4 c=5 d e")
+
+        # Act
+        result_add = remote.xapi_object_edit(
+            "system",
+            name_system,
+            "add",
+            {
+                "name": name_system,
+                "profile": name_profile,
+                "interface": "eth1",
+            },
+            token,
+        )
+        result_edit = remote.xapi_object_edit(
+            "system",
+            name_system,
+            "edit",
+            {
+                "name": name_system,
+                "interface": "eth2",
+            },
+            token,
+        )
+
+        # Assert
+        assert result_add
+        assert result_edit
+        assert len(remote.get_system("testsystem_xapi_edit").get("interfaces", {})) == 2
+        assert "eth1" in remote.get_system("testsystem_xapi_edit").get("interfaces", {})
+        assert "eth2" in remote.get_system("testsystem_xapi_edit").get("interfaces", {})
+
+    def test_xapi_system_edit_two_interfaces_default(
+        self,
+        remote,
+        token,
+        create_kernel_initrd,
+        create_distro,
+        create_profile,
+    ):
+        # Arrange
+        name_distro = "testsystem_xapi_edit"
+        name_profile = "testsystem_xapi_edit"
+        name_system = "testsystem_xapi_edit"
+        fk_kernel = "vmlinuz1"
+        fk_initrd = "initrd1.img"
+        basepath = create_kernel_initrd(fk_kernel, fk_initrd)
+        path_kernel = os.path.join(basepath, fk_kernel)
+        path_initrd = os.path.join(basepath, fk_initrd)
+        create_distro(name_distro, "x86_64", "suse", path_kernel, path_initrd)
+        create_profile(name_profile, name_distro, "a=1 b=2 c=3 c=4 c=5 d e")
+        remote.xapi_object_edit(
+            "system",
+            name_system,
+            "add",
+            {
+                "name": name_system,
+                "profile": name_profile,
+            },
+            token,
+        )
+        remote.xapi_object_edit(
+            "system",
+            name_system,
+            "edit",
+            {
+                "name": name_system,
+                "interface": "eth2",
+            },
+            token,
+        )
+
+        # Act
+        result = remote.xapi_object_edit(
+            "system",
+            name_system,
+            "edit",
+            {
+                "name": name_system,
+                "mac_address": "aa:bb:cc:dd:ee:ff",
+            },
+            token,
+        )
+
+        # Assert
+        assert result
+        assert (
+            remote.get_system(name_system)
+            .get("interfaces", {})
+            .get("default", {})
+            .get("mac_address")
+            == "aa:bb:cc:dd:ee:ff"
+        )
+
+    def test_xapi_system_edit_two_interfaces_no_default_negative(
+        self,
+        remote,
+        token,
+        create_kernel_initrd,
+        create_distro,
+        create_profile,
+    ):
+        # Arrange
+        name_distro = "testsystem_xapi_edit"
+        name_profile = "testsystem_xapi_edit"
+        name_system = "testsystem_xapi_edit"
+        fk_kernel = "vmlinuz1"
+        fk_initrd = "initrd1.img"
+        basepath = create_kernel_initrd(fk_kernel, fk_initrd)
+        path_kernel = os.path.join(basepath, fk_kernel)
+        path_initrd = os.path.join(basepath, fk_initrd)
+        create_distro(name_distro, "x86_64", "suse", path_kernel, path_initrd)
+        create_profile(name_profile, name_distro, "a=1 b=2 c=3 c=4 c=5 d e")
+        remote.xapi_object_edit(
+            "system",
+            name_system,
+            "add",
+            {
+                "name": name_system,
+                "profile": name_profile,
+                "interface": "eth1",
+            },
+            token,
+        )
+        remote.xapi_object_edit(
+            "system",
+            name_system,
+            "edit",
+            {
+                "name": name_system,
+                "interface": "eth2",
+            },
+            token,
+        )
+
+        # Act & Assert
+        with pytest.raises(ValueError):
+            remote.xapi_object_edit(
+                "system",
+                name_system,
+                "edit",
+                {
+                    "name": name_system,
+                    "mac_address": "aa:bb:cc:dd:ee:ff",
+                },
+                token,
+            )
+
     @pytest.mark.usefixtures(
         "create_testdistro",
         "create_testmenu",
openSUSE Build Service is sponsored by