Repository: cobbler/cobbler

Issue:
self.__interface_edits trying to handle changes to hostname or ks_meta or other attributes
### Describe the bug

When editing non interface specific attributes, like hostname or ks_meta, e.g.:

`cobbler system edit --name=mysystem --hostname=mysystem.com`

for systems with more than interface it fails with:

```
[Thread-5] 2022-09-12T12:11:26 - INFO | Exception value: Interface is required.
[Thread-5] 2022-09-12T12:11:26 - INFO | Exception Info:
  File "/usr/lib/python3.6/site-packages/cobbler/remote.py", line 3723, in _dispatch
    return method_handle(*params)

  File "/usr/lib/python3.6/site-packages/cobbler/remote.py", line 2086, in xapi_object_edit
    self.__interface_edits(handle, attributes, object_name)

  File "/usr/lib/python3.6/site-packages/cobbler/remote.py", line 2118, in __interface_edits
    raise ValueError("Interface is required.")
```

### Steps to reproduce

1. Create a system with more than one interface (so that there is no default interface)
2. Try to edit hostname

### Cobbler version

3.3.3 patched with default interface fix (689cb2371078196d31c739f20e3f7459d2cec3fa)

### Operating system

RHEL8

### Fix/workaround

```
diff -up cobbler-3.3.3/cobbler/remote.py.fixinterface cobbler-3.3.3/cobbler/remote.py
--- cobbler-3.3.3/cobbler/remote.py.fixinterface        2022-09-12 12:32:09.385510188 -0400
+++ cobbler-3.3.3/cobbler/remote.py     2022-09-12 12:30:42.543189134 -0400
@@ -2077,8 +2077,9 @@ class CobblerXMLRPCInterface:
                 else:
                     modkey = "%s-%s" % (key, attributes.get("interface", ""))
                     imods[modkey] = value
+                del attributes[key]
 
-            if object_type == "system":
+            if object_type == "system" and len(attributes)>0:
                 # FIXME: Don't call this tree if we are not doing any interface stuff.
                 self.__interface_edits(handle, attributes, object_name)
         else:
```
The problem is that the FIXME section is reached regardless of whether the code above already handled that particular change/attribute, even for options that cannot be handled by it.


Discussion:
Turns out that was a terrible way to fix it, this works better:

```
diff -up cobbler-3.3.3/cobbler/remote.py.fixinterface cobbler-3.3.3/cobbler/remote.py
--- cobbler-3.3.3/cobbler/remote.py.fixinterface        2022-09-12 12:32:09.385510188 -0400
+++ cobbler-3.3.3/cobbler/remote.py     2022-09-22 16:08:49.287477955 -0400
@@ -2056,7 +2056,10 @@ class CobblerXMLRPCInterface:
             for attr_name in priority_attributes:
                 if attr_name in attributes:
                     self.modify_item(object_type, handle, attr_name, attributes.pop(attr_name), token)
+            have_interface_keys = False
             for (key, value) in list(attributes.items()):
+                if self.__is_interface_field(key):
+                    have_interface_keys = True
                 if object_type != "system" or not self.__is_interface_field(key):
                     # in place modifications allow for adding a key/value pair while keeping other k/v pairs intact.
                     if key in ["autoinstall_meta", "kernel_options", "kernel_options_post", "template_files",
@@ -2078,7 +2081,7 @@ class CobblerXMLRPCInterface:
                     modkey = "%s-%s" % (key, attributes.get("interface", ""))
                     imods[modkey] = value
 
-            if object_type == "system":
+            if object_type == "system" and have_interface_keys:
                 # FIXME: Don't call this tree if we are not doing any interface stuff.
                 self.__interface_edits(handle, attributes, object_name)
         else:
```
Thanks for submitting this diff @plazonic ! Are you interested in submitting this as a PR? I believe while it may be what we need until the new CLI is in place. I would even accept a PR without a test in this case here.
Sure - I'll try to get that done later today. This is used in production so it reflects ongoing fixes - I just didn't have time to do it right...

Resolving patch:
diff --git a/cobbler/remote.py b/cobbler/remote.py
--- a/cobbler/remote.py
+++ b/cobbler/remote.py
@@ -18,7 +18,7 @@
 import re
 import xmlrpc.server
 from socketserver import ThreadingMixIn
-from typing import Dict, List, Optional, Union
+from typing import Any, Dict, List, Optional, Union
 from xmlrpc.server import SimpleXMLRPCRequestHandler
 
 from cobbler import enums
@@ -2077,17 +2077,13 @@ def __is_interface_field(self, field_name: str) -> bool:
         :param field_name: The fieldname to check.
         :return: True if the fields is related to a network interface, otherwise False.
         """
-        # FIXME: This is not tested and I believe prone to errors. Needs explicit testing.
-        if field_name in ("delete_interface", "rename_interface"):
+        if field_name in ("interface", "delete_interface", "rename_interface"):
             return True
 
-        interface = system.NetworkInterface(self.api)
         fields = []
-        for attribute in interface.__dict__:
-            if attribute.startswith("_") and (
-                "api" not in attribute or "logger" in attribute
-            ):
-                fields.append(attribute[1:])
+        for key, value in system.NetworkInterface.__dict__.items():
+            if isinstance(value, property):
+                fields.append(key)
 
         return field_name in fields
 
@@ -2173,15 +2169,16 @@ def xapi_object_edit(
         if edit_type != "remove":
             # FIXME: this doesn't know about interfaces yet!
             # if object type is system and fields add to dict and then modify when done, rather than now.
-            imods = {}
-            # FIXME: needs to know about how to delete interfaces too!
             priority_attributes = ["name", "parent", "distro", "profile", "image"]
             for attr_name in priority_attributes:
                 if attr_name in attributes:
                     self.modify_item(
                         object_type, handle, attr_name, attributes.pop(attr_name), token
                     )
-            for (key, value) in list(attributes.items()):
+            have_interface_keys = False
+            for (key, value) in attributes.items():
+                if self.__is_interface_field(key):
+                    have_interface_keys = True
                 if object_type != "system" or not self.__is_interface_field(key):
                     # in place modifications allow for adding a key/value pair while keeping other k/v pairs intact.
                     if key in [
@@ -2205,12 +2202,7 @@ def xapi_object_edit(
 
                     self.modify_item(object_type, handle, key, value, token)
 
-                else:
-                    modkey = f"{key}-{attributes.get('interface', '')}"
-                    imods[modkey] = value
-
-            if object_type == "system":
-                # FIXME: Don't call this tree if we are not doing any interface stuff.
+            if object_type == "system" and have_interface_keys:
                 self.__interface_edits(handle, attributes, object_name)
         else:
             # remove item
@@ -2234,64 +2226,69 @@ def xapi_object_edit(
         self.save_item(object_type, handle, token)
         return True
 
-    def __interface_edits(self, handle, attributes, object_name):
-        if (
-            "delete_interface" not in attributes
-            and "rename_interface" not in attributes
-        ):
-            # This if is taking care of interface logic. The interfaces are a dict, thus when we get the obj via
-            # the api we get references to the original interfaces dict. Thus this trick saves us the pain of
-            # writing the modified obj back to the collection. Always remember that dicts are mutable.
-            system_to_edit = self.__get_object(handle)
-            if system_to_edit is None:
-                raise ValueError(
-                    f'No system found with the specified name (name given: "{object_name}")!'
-                )
+    def __interface_edits(
+        self, handle: str, attributes: Dict[str, Any], object_name: str
+    ):
+        """
+        Handles all edits in relation to network interfaces.
 
-            # 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:
-                # If the interface is not existing, create a new one.
-                interface = system.NetworkInterface(self.api)
-            for attribute_key in attributes:
-                if self.__is_interface_field(attribute_key):
-                    if hasattr(interface, attribute_key):
-                        setattr(interface, attribute_key, attributes[attribute_key])
-                    else:
-                        self.logger.warning(
-                            'Network interface field "%s" could not be set. Skipping it.',
-                            attribute_key,
-                        )
-                else:
-                    self.logger.debug(
-                        "Field %s was not an interface field.", attribute_key
-                    )
-            system_to_edit.interfaces.update({interface_name: interface})
-        elif "delete_interface" in attributes:
+        :param handle: XML-RPC handle for the system that is being edited.
+        :param attributes: The attributes that are being passed by the CLI to the server.
+        :param object_name: The system name.
+        """
+        if "delete_interface" in attributes:
             if attributes.get("interface") is None:
                 raise ValueError("Interface is required for deletion.")
             system_to_edit = self.__get_object(handle)
             system_to_edit.delete_interface(attributes.get("interface"))
-        elif "rename_interface" in attributes:
+            return
+
+        if "rename_interface" in attributes:
             system_to_edit = self.__get_object(handle)
             system_to_edit.rename_interface(
                 attributes.get("interface", ""), attributes.get("rename_interface", "")
             )
+            return
+
+        # This if is taking care of interface logic. The interfaces are a dict, thus when we get the obj via
+        # the api we get references to the original interfaces dict. Thus this trick saves us the pain of
+        # writing the modified obj back to the collection. Always remember that dicts are mutable.
+        system_to_edit = self.__get_object(handle)
+        if system_to_edit is None:
+            raise ValueError(
+                f'No system found with the specified name (name given: "{object_name}")!'
+            )
+
+        # 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")
+        attributes.pop("interface", None)
+        self.logger.debug('Interface "%s" is being edited.', interface_name)
+        interface = system_to_edit.interfaces.get(interface_name)
+        if interface is None:
+            # If the interface is not existing, create a new one.
+            interface = system.NetworkInterface(self.api)
+        for attribute_key in attributes:
+            if self.__is_interface_field(attribute_key):
+                if hasattr(interface, attribute_key):
+                    setattr(interface, attribute_key, attributes[attribute_key])
+                else:
+                    self.logger.warning(
+                        'Network interface field "%s" could not be set. Skipping it.',
+                        attribute_key,
+                    )
+            else:
+                self.logger.debug("Field %s was not an interface field.", attribute_key)
+        system_to_edit.interfaces.update({interface_name: interface})
 
     def save_item(self, what: str, object_id: str, token, editmode: str = "bypass"):
         """


Test patch:
diff --git a/tests/xmlrpcapi/miscellaneous_test.py b/tests/xmlrpcapi/miscellaneous_test.py
--- a/tests/xmlrpcapi/miscellaneous_test.py
+++ b/tests/xmlrpcapi/miscellaneous_test.py
@@ -696,335 +696,6 @@ def test_version(self, remote):
         # Will fail if the version is adjusted in the setup.py
         assert result == 3.4
 
-    def test_xapi_object_edit(self, remote, token, remove_distro, create_kernel_initrd):
-        # Arrange
-        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)
-        name = "testdistro_xapi_edit"
-
-        # Act
-        result = remote.xapi_object_edit(
-            "distro",
-            name,
-            "add",
-            {
-                "name": name,
-                "arch": "x86_64",
-                "breed": "suse",
-                "kernel": path_kernel,
-                "initrd": path_initrd,
-            },
-            token,
-        )
-
-        # Cleanup
-        remove_distro(name)
-
-        # 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",
diff --git a/tests/xmlrpcapi/xapi_test.py b/tests/xmlrpcapi/xapi_test.py
new file mode 100644
--- /dev/null
+++ b/tests/xmlrpcapi/xapi_test.py
@@ -0,0 +1,303 @@
+"""
+All tests that are realted to ensuring the xapi_object_edit functionallity.
+"""
+
+import os
+from typing import Callable
+
+import pytest
+
+from cobbler.remote import CobblerXMLRPCInterface
+
+
+def test_xapi_object_edit(
+    remote: CobblerXMLRPCInterface,
+    token: str,
+    create_kernel_initrd: Callable[[str, str], str],
+):
+    """
+    Test that asserts that objects can be successfully edited.
+    """
+    # Arrange
+    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)
+    name = "testdistro_xapi_edit"
+
+    # Act
+    result = remote.xapi_object_edit(
+        "distro",
+        name,
+        "add",
+        {
+            "name": name,
+            "arch": "x86_64",
+            "breed": "suse",
+            "kernel": path_kernel,
+            "initrd": path_initrd,
+        },
+        token,
+    )
+
+    # Assert
+    assert result
+
+
+def test_xapi_system_edit(
+    remote: CobblerXMLRPCInterface,
+    token: str,
+    create_kernel_initrd: Callable[[str, str], str],
+    create_distro: Callable[[str, str, str, str, str], str],
+    create_profile: Callable[[str, str, str], str],
+):
+    """
+    Test that asserts if system objects can be correctly edited.
+    """
+    # 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
+    # There won't be any interface until the user adds implicitly or explicitly the first interface
+    assert len(remote.get_system("testsystem_xapi_edit").get("interfaces", {})) == 0
+
+
+def test_xapi_system_edit_interface_name(
+    remote: CobblerXMLRPCInterface,
+    token: str,
+    create_kernel_initrd: Callable[[str, str], str],
+    create_distro: Callable[[str, str, str, str, str], str],
+    create_profile: Callable[[str, str, str], str],
+):
+    """
+    TODO
+    """
+    # 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_no_default(
+    remote: CobblerXMLRPCInterface,
+    token: str,
+    create_kernel_initrd: Callable[[str, str], str],
+    create_distro: Callable[[str, str, str, str, str], str],
+    create_profile: Callable[[str, str, str], str],
+):
+    """
+    Test that asserts that more then one interface can be added successfully.
+    """
+    # 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(
+    remote: CobblerXMLRPCInterface,
+    token: str,
+    create_kernel_initrd: Callable[[str, str], str],
+    create_distro: Callable[[str, str, str, str, str], str],
+    create_profile: Callable[[str, str, str], str],
+):
+    """
+    Test that asserts that on a system with a single interface that has not the name "default", the correct network
+    interface key is edited.
+    """
+    # 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("eth2", {})
+        .get("mac_address")
+        == "aa:bb:cc:dd:ee:ff"
+    )
+
+
+def test_xapi_system_edit_two_interfaces_no_default_negative(
+    remote: CobblerXMLRPCInterface,
+    token: str,
+    create_kernel_initrd: Callable[[str, str], str],
+    create_distro: Callable[[str, str, str, str, str], str],
+    create_profile: Callable[[str, str, str], str],
+):
+    """
+    Test that asserts that on a system with two interfaces, you must pass the interface name if you edit an interface
+    specifc field.
+    """
+    # 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,
+        )