Repository: cobbler/cobbler (language: python)

Issue:
[Backport] Collection indicies
### Original feature issue

- Issue: #3169
- PR: #3671

### Target release

- [x] release33
- [ ] release32
- [ ] release30

### Reason

To counter the performance issues the search has to work faster. As such the collection indices should be backported.



Discussion / hints:
Backporting this means that https://github.com/cobbler/cobbler/commit/b3c43857ed179f1c063249e3d89c28dfb09a712e#diff-e042c447369ddaa30aa68eb10250d2e9d8a3bfc3995241b5d35bd93f4c4f575b needs to be backported as well.

Fix patch:
diff --git a/changelog.d/3725.added b/changelog.d/3725.added
new file mode 100644
index 0000000000..388b8eecde
--- /dev/null
+++ b/changelog.d/3725.added
@@ -0,0 +1,1 @@
+Add collection indices for UUID's, MAC's, IP addresses and hostnames
\ No newline at end of file
diff --git a/cobbler/cobbler_collections/collection.py b/cobbler/cobbler_collections/collection.py
index dae35f939d..c7552197be 100644
--- a/cobbler/cobbler_collections/collection.py
+++ b/cobbler/cobbler_collections/collection.py
@@ -22,7 +22,7 @@
 import os
 import uuid
 from threading import Lock
-from typing import List, Union, Optional
+from typing import List, Union, Optional, Dict, Any
 
 from cobbler import utils
 from cobbler.items import package, system, item as item_base, image, profile, repo, mgmtclass, distro, file, menu
@@ -48,6 +48,10 @@ def __init__(self, collection_mgr):
         self.lock: Lock = Lock()
         self._inmemory: bool = not self.api.settings().lazy_start
         self._deserialize_running: bool = False
+        # Secondary indexes for the collection.
+        # Only unique indexes on string values are supported.
+        # Empty strings are not indexed.
+        self.indexes: Dict[str, Dict[str, str]] = {"uid": {}}
         self.logger = logging.getLogger()
 
     def __iter__(self):
@@ -127,7 +131,7 @@ def get(self, name: str) -> Optional[item_base.Item]:
         :param name: The name of the object to retrieve from the collection.
         :return: The object if it exists. Otherwise None.
         """
-        return self.listing.get(name.lower(), None)
+        return self.listing.get(name, None)
 
     def get_names(self) -> List[str]:
         """
@@ -164,10 +168,7 @@ def find(self, name: str = "", return_list: bool = False, no_errors=False,
 
         # performance: if the only key is name we can skip the whole loop
         if len(kargs) == 1 and "name" in kargs and not return_list:
-            try:
-                return self.listing.get(kargs["name"].lower(), None)
-            except:
-                return self.listing.get(kargs["name"], None)
+            return self.listing.get(kargs["name"], None)
 
         if self.api.settings().lazy_start:
             # Forced deserialization of the entire collection to prevent deadlock in the search loop
@@ -175,9 +176,13 @@ def find(self, name: str = "", return_list: bool = False, no_errors=False,
 
         self.lock.acquire()
         try:
-            for obj in self:
-                if obj.inmemory and obj.find_match(kargs, no_errors=no_errors):
-                    matches.append(obj)
+            result = self.find_by_indexes(kargs)
+            if result is not None:
+                matches = result
+            if len(kargs) > 0:
+                for obj in self:
+                    if obj.inmemory and obj.find_match(kargs, no_errors=no_errors):
+                        matches.append(obj)
         finally:
             self.lock.release()
 
@@ -259,13 +264,6 @@ def copy(self, ref, newname):
         copied_item.uid = uuid.uuid4().hex
         copied_item.ctime = time.time()
         copied_item.name = newname
-        if copied_item.COLLECTION_TYPE == "system":
-            # this should only happen for systems
-            for interface in copied_item.interfaces:
-                # clear all these out to avoid DHCP/DNS conflicts
-                copied_item.interfaces[interface].dns_name = ""
-                copied_item.interfaces[interface].mac_address = ""
-                copied_item.interfaces[interface].ip_address = ""
 
         self.add(
             copied_item,
@@ -295,12 +293,14 @@ def rename(self, ref: item_base.Item, newname, with_sync: bool = True, with_trig
         self.listing[newname] = None
         # Delete the old item
         self.collection_mgr.serialize_delete_one_item(ref)
+        self.remove_from_indexes(ref)
         self.listing.pop(oldname)
         # Change the name of the object
         ref.name = newname
         # Save just this item
         self.collection_mgr.serialize_one_item(ref)
         self.listing[newname] = ref
+        self.add_to_indexes(ref)
 
         for dep_type in item_base.Item.TYPE_DEPENDENCIES[ref.COLLECTION_TYPE]:
             items = self.api.find_items(dep_type[0], {dep_type[1]: oldname}, return_list = True)
@@ -353,7 +353,6 @@ def rename(self, ref: item_base.Item, newname, with_sync: bool = True, with_trig
                         d.initrd = d.initrd.replace(path, newpath)
                         self.collection_mgr.serialize_one_item(d)
 
-
     def add(self, ref, save: bool = False, with_copy: bool = False, with_triggers: bool = True, with_sync: bool = True,
             quick_pxe_update: bool = False, check_for_duplicate_names: bool = False):
         """
@@ -414,7 +413,8 @@ def add(self, ref, save: bool = False, with_copy: bool = False, with_triggers: b
 
         self.lock.acquire()
         try:
-            self.listing[ref.name.lower()] = ref
+            self.listing[ref.name] = ref
+            self.add_to_indexes(ref)
         finally:
             self.lock.release()
 
@@ -430,7 +430,6 @@ def add(self, ref, save: bool = False, with_copy: bool = False, with_triggers: b
                     # we don't need openvz containers to be network bootable
                     if ref.virt_type == "openvz":
                         ref.netboot_enabled = False
-                    self.lite_sync.add_single_system(ref.name)
                     self.api.sync_systems(systems=[ref.name])
                 elif isinstance(ref, profile.Profile):
                     # we don't need openvz containers to be network bootable
@@ -488,6 +487,58 @@ def _deserialize(self) -> None:
         self.inmemory = True
         self.deserialize_running = False
 
+    def add_to_indexes(self, ref: "ITEM") -> None:
+        """
+        Add indexes for the object.
+        :param ref: The reference to the object whose indexes are updated.
+        """
+        indx_dict = self.indexes["uid"]
+        indx_dict[ref.uid] = ref.name
+
+    def remove_from_indexes(self, ref: "ITEM") -> None:
+        """
+        Remove index keys for the object.
+        :param ref: The reference to the object whose index keys are removed.
+        """
+        indx_dict = self.indexes["uid"]
+        indx_dict.pop(ref.uid, None)
+
+    def find_by_indexes(self, kargs: Dict[str, Any]) -> Optional[List["ITEM"]]:
+        """
+        Searching for items in the collection by indexes.
+        :param kwargs: The dict to match for the items.
+        """
+        result: Optional[List["ITEM"]] = None
+        found_keys: List[str] = []
+
+        for key, value in kargs.items():
+            # fnmatch and "~" are not supported
+            if (
+                    key not in self.indexes
+                    or value[:1] == "~"
+                    or "?" in value
+                    or "*" in value
+                    or "[" in value
+            ):
+                continue
+
+            indx_dict = self.indexes[key]
+            if value in indx_dict:
+                if result is None:
+                    result = []
+                obj = self.listing.get(indx_dict[value])
+                if obj is not None:
+                    result.append(obj)
+                else:
+                    self.logger.error(
+                        'Internal error. The "%s" index is corrupted.', key
+                    )
+            found_keys.append(key)
+
+        for key in found_keys:
+            kargs.pop(key)
+        return result
+
     def to_string(self) -> str:
         """
         Creates a printable representation of the collection suitable for reading by humans or parsing from scripts.
diff --git a/cobbler/cobbler_collections/distros.py b/cobbler/cobbler_collections/distros.py
index d3e4261d4f..3c15266092 100644
--- a/cobbler/cobbler_collections/distros.py
+++ b/cobbler/cobbler_collections/distros.py
@@ -56,8 +56,6 @@ def remove(self, name, with_delete: bool = True, with_sync: bool = True, with_tr
         :raises CX: In case any subitem (profiles or systems) would be orphaned. If the option ``recursive`` is set then
                     the orphaned items would be removed automatically.
         """
-        name = name.lower()
-
         obj = self.find(name=name)
 
         if obj is None:
@@ -66,7 +64,7 @@ def remove(self, name, with_delete: bool = True, with_sync: bool = True, with_tr
         # first see if any Groups use this distro
         if not recursive:
             for profile in self.api.profiles():
-                if profile.distro and profile.distro.name.lower() == name:
+                if profile.distro and profile.distro.name == name:
                     raise CX("removal would orphan profile: %s" % profile.name)
 
         kernel = obj.kernel
@@ -83,6 +81,7 @@ def remove(self, name, with_delete: bool = True, with_sync: bool = True, with_tr
                 lite_sync.remove_single_distro(name)
         self.lock.acquire()
         try:
+            self.remove_from_indexes(obj)
             del self.listing[name]
         finally:
             self.lock.release()
diff --git a/cobbler/cobbler_collections/files.py b/cobbler/cobbler_collections/files.py
index 18e589ce8b..99f9efd936 100644
--- a/cobbler/cobbler_collections/files.py
+++ b/cobbler/cobbler_collections/files.py
@@ -52,7 +52,6 @@ def remove(self, name, with_delete: bool = True, with_sync: bool = True, with_tr
 
         :raises CX: In case a non existent object should be deleted.
         """
-        name = name.lower()
         obj = self.find(name=name)
 
         if obj is None:
@@ -64,6 +63,7 @@ def remove(self, name, with_delete: bool = True, with_sync: bool = True, with_tr
 
         self.lock.acquire()
         try:
+            self.remove_from_indexes(obj)
             del self.listing[name]
         finally:
             self.lock.release()
diff --git a/cobbler/cobbler_collections/images.py b/cobbler/cobbler_collections/images.py
index b634587fff..a06e491207 100644
--- a/cobbler/cobbler_collections/images.py
+++ b/cobbler/cobbler_collections/images.py
@@ -47,7 +47,6 @@ def remove(self, name, with_delete: bool = True, with_sync: bool = True, with_tr
         :raises CX: In case object does not exist or it would orhan a system.
         """
         # NOTE: with_delete isn't currently meaningful for repos but is left in for consistency in the API. Unused.
-        name = name.lower()
         obj = self.find(name=name)
         if obj is None:
             raise CX("cannot delete an object that does not exist: %s" % name)
@@ -55,7 +54,7 @@ def remove(self, name, with_delete: bool = True, with_sync: bool = True, with_tr
         # first see if any Groups use this distro
         if not recursive:
             for v in self.api.systems():
-                if v.image is not None and v.image.lower() == name:
+                if v.image is not None and v.image == name:
                     raise CX("removal would orphan system: %s" % v.name)
 
         if recursive:
@@ -72,6 +71,7 @@ def remove(self, name, with_delete: bool = True, with_sync: bool = True, with_tr
 
         self.lock.acquire()
         try:
+            self.remove_from_indexes(obj)
             del self.listing[name]
         finally:
             self.lock.release()
diff --git a/cobbler/cobbler_collections/menus.py b/cobbler/cobbler_collections/menus.py
index 69f3eb8cde..2d9beda17b 100644
--- a/cobbler/cobbler_collections/menus.py
+++ b/cobbler/cobbler_collections/menus.py
@@ -91,6 +91,7 @@ def remove(self, name: str, with_delete: bool = True, with_sync: bool = True, wi
                 utils.run_triggers(self.api, obj, "/var/lib/cobbler/triggers/delete/menu/pre/*", [])
         self.lock.acquire()
         try:
+            self.remove_from_indexes(obj)
             del self.listing[name]
         finally:
             self.lock.release()
diff --git a/cobbler/cobbler_collections/mgmtclasses.py b/cobbler/cobbler_collections/mgmtclasses.py
index 333a3f4903..bccca3208b 100644
--- a/cobbler/cobbler_collections/mgmtclasses.py
+++ b/cobbler/cobbler_collections/mgmtclasses.py
@@ -56,7 +56,6 @@ def remove(self, name, with_delete: bool = True, with_sync: bool = True, with_tr
 
         :raises CX: In case the object does not exist.
         """
-        name = name.lower()
         obj = self.find(name=name)
         if obj is None:
             raise CX("cannot delete an object that does not exist: %s" % name)
@@ -67,6 +66,7 @@ def remove(self, name, with_delete: bool = True, with_sync: bool = True, with_tr
 
         self.lock.acquire()
         try:
+            self.remove_from_indexes(obj)
             del self.listing[name]
         finally:
             self.lock.release()
diff --git a/cobbler/cobbler_collections/packages.py b/cobbler/cobbler_collections/packages.py
index 669133f0a7..d7e0a3ad37 100644
--- a/cobbler/cobbler_collections/packages.py
+++ b/cobbler/cobbler_collections/packages.py
@@ -52,7 +52,6 @@ def remove(self, name, with_delete: bool = True, with_sync: bool = True, with_tr
 
         :raises CX: In case the object does not exist.
         """
-        name = name.lower()
         obj = self.find(name=name)
         if obj is None:
             raise CX("cannot delete an object that does not exist: %s" % name)
@@ -63,6 +62,7 @@ def remove(self, name, with_delete: bool = True, with_sync: bool = True, with_tr
 
         self.lock.acquire()
         try:
+            self.remove_from_indexes(obj)
             del self.listing[name]
         finally:
             self.lock.release()
diff --git a/cobbler/cobbler_collections/profiles.py b/cobbler/cobbler_collections/profiles.py
index 56ee931cb0..9c224f24ba 100644
--- a/cobbler/cobbler_collections/profiles.py
+++ b/cobbler/cobbler_collections/profiles.py
@@ -52,10 +52,9 @@ def remove(self, name: str, with_delete: bool = True, with_sync: bool = True, wi
 
         :raises CX: In case the name of the object was not given or any other descendant would be orphaned.
         """
-        name = name.lower()
         if not recursive:
             for v in self.api.systems():
-                if v.profile is not None and v.profile.lower() == name:
+                if v.profile is not None and v.profile == name:
                     raise CX("removal would orphan system: %s" % v.name)
 
         obj = self.find(name=name)
@@ -81,6 +80,7 @@ def remove(self, name: str, with_delete: bool = True, with_sync: bool = True, wi
 
         self.lock.acquire()
         try:
+            self.remove_from_indexes(obj)
             del self.listing[name]
         finally:
             self.lock.release()
diff --git a/cobbler/cobbler_collections/repos.py b/cobbler/cobbler_collections/repos.py
index b7143d5815..89101b51d8 100644
--- a/cobbler/cobbler_collections/repos.py
+++ b/cobbler/cobbler_collections/repos.py
@@ -57,7 +57,6 @@ def remove(self, name, with_delete: bool = True, with_sync: bool = True, with_tr
         """
         # NOTE: with_delete isn't currently meaningful for repos
         # but is left in for consistancy in the API.  Unused.
-        name = name.lower()
         obj = self.find(name=name)
         if obj is None:
             raise CX("cannot delete an object that does not exist: %s" % name)
@@ -68,6 +67,7 @@ def remove(self, name, with_delete: bool = True, with_sync: bool = True, with_tr
 
         self.lock.acquire()
         try:
+            self.remove_from_indexes(obj)
             del self.listing[name]
         finally:
             self.lock.release()
diff --git a/cobbler/cobbler_collections/systems.py b/cobbler/cobbler_collections/systems.py
index 62f0f50975..124d054a5f 100644
--- a/cobbler/cobbler_collections/systems.py
+++ b/cobbler/cobbler_collections/systems.py
@@ -17,12 +17,16 @@
 Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
 02110-1301  USA
 """
+from typing import TYPE_CHECKING, Dict, Set
 
 from cobbler.cobbler_collections import collection
 from cobbler.items import system as system
 from cobbler import utils
 from cobbler.cexceptions import CX
 
+if TYPE_CHECKING:
+    from cobbler.cobbler_collections.manager import CollectionManager
+
 
 class Systems(collection.Collection):
     """
@@ -38,9 +42,30 @@ def collection_type() -> str:
     def collection_types() -> str:
         return "systems"
 
+    def __init__(self, collection_mgr: "CollectionManager"):
+        """
+        Constructor.
+        :param collection_mgr: The collection manager to resolve all information with.
+        """
+        super().__init__(collection_mgr)
+        self.indexes: Dict[str, Dict[str, str]] = {
+            "uid": {},
+            "mac_address": {},
+            "ip_address": {},
+            "ipv6_address": {},
+            "dns_name": {},
+        }
+        settings = self.api.settings()
+        self.disabled_indexes: Dict[str, bool] = {
+            "mac_address": settings.allow_duplicate_macs,
+            "ip_address": settings.allow_duplicate_ips,
+            "ipv6_address": settings.allow_duplicate_ips,
+            "dns_name": settings.allow_duplicate_hostnames,
+        }
+
     def factory_produce(self, api, item_dict):
         """
-        Return a Distro forged from item_dict
+        Return a System forged from item_dict
 
         :param api: TODO
         :param item_dict: TODO
@@ -57,7 +82,6 @@ def remove(self, name: str, with_delete: bool = True, with_sync: bool = True, wi
 
         :raises CX: In case the name of the object was not given.
         """
-        name = name.lower()
         obj = self.find(name=name)
 
         if obj is None:
@@ -70,9 +94,9 @@ def remove(self, name: str, with_delete: bool = True, with_sync: bool = True, wi
                 lite_sync = self.api.get_sync()
                 lite_sync.remove_single_system(name)
 
-
         self.lock.acquire()
         try:
+            self.remove_from_indexes(obj)
             del self.listing[name]
         finally:
             self.lock.release()
@@ -81,3 +105,133 @@ def remove(self, name: str, with_delete: bool = True, with_sync: bool = True, wi
             if with_triggers:
                 utils.run_triggers(self.api, obj, "/var/lib/cobbler/triggers/delete/system/post/*", [])
                 utils.run_triggers(self.api, obj, "/var/lib/cobbler/triggers/change/*", [])
+
+    def add_to_indexes(self, ref: system.System) -> None:
+        """
+        Add indexes for the system.
+        :param ref: The reference to the system whose indexes are updated.
+        """
+        super().add_to_indexes(ref)
+        if not ref.inmemory:
+            return
+
+        for indx_key, indx_val in self.indexes.items():
+            if indx_key == "uid" or self.disabled_indexes[indx_key]:
+                continue
+
+            for interface in ref.interfaces.values():
+                if hasattr(interface, indx_key):
+                    secondary_key = getattr(interface, indx_key)
+                    if secondary_key is not None and secondary_key != "":
+                        indx_val[secondary_key] = ref.name
+
+    def update_interface_index_value(
+        self,
+        interface: system.NetworkInterface,
+        attribute_name: str,
+        old_value: str,
+        new_value: str,
+    ) -> None:
+        """
+        TODO
+
+        :param interface: TODO
+        :param attribute_name: TODO
+        :param old_value: TODO
+        :param new_value: TODO
+        """
+        if (
+            interface.system_name in self.listing
+            and not self.disabled_indexes[attribute_name]
+            and interface in self.listing[interface.system_name].interfaces.values()
+        ):
+            indx_dict = self.indexes[attribute_name]
+            with self.lock:
+                if (
+                    old_value != ""
+                    and old_value in indx_dict
+                    and indx_dict[old_value] == interface.system_name
+                ):
+                    del indx_dict[old_value]
+                if new_value != "":
+                    indx_dict[new_value] = interface.system_name
+
+    def update_interfaces_indexes(
+        self, ref: system.System, new_ifaces: Dict[str, system.NetworkInterface]
+    ) -> None:
+        """
+        Update interfaces indexes for the system.
+        :param ref: The reference to the system whose interfaces indexes are updated.
+        :param new_ifaces: The new interfaces.
+        """
+        if ref.name not in self.listing:
+            return
+
+        for indx_key, indx_val in self.indexes.items():
+            if indx_key == "uid" or self.disabled_indexes[indx_key]:
+                continue
+
+            old_ifaces = ref.interfaces
+            old_values: Set[str] = {
+                getattr(x, indx_key)
+                for x in old_ifaces.values()
+                if hasattr(x, indx_key) and getattr(x, indx_key) != ""
+            }
+            new_values: Set[str] = {
+                getattr(x, indx_key)
+                for x in new_ifaces.values()
+                if hasattr(x, indx_key) and getattr(x, indx_key) != ""
+            }
+
+            with self.lock:
+                for value in old_values - new_values:
+                    del indx_val[value]
+                for value in new_values - old_values:
+                    indx_val[value] = ref.name
+
+    def update_interface_indexes(
+        self, ref: system.System, iface_name: str, new_iface: system.NetworkInterface
+    ) -> None:
+        """
+        Update interface indexes for the system.
+        :param ref: The reference to the system whose interfaces indexes are updated.
+        :param iface_name: The new interface name.
+        :param new_iface: The new interface.
+        """
+        self.update_interfaces_indexes(
+            ref, {**ref.interfaces, **{iface_name: new_iface}}
+        )
+
+    def remove_from_indexes(self, ref: system.System) -> None:
+        """
+        Remove index keys for the system.
+        :param ref: The reference to the system whose index keys are removed.
+        """
+        if not ref.inmemory:
+            return
+
+        super().remove_from_indexes(ref)
+        for indx_key, indx_val in self.indexes.items():
+            if indx_key == "uid" or self.disabled_indexes[indx_key]:
+                continue
+
+            for interface in ref.interfaces.values():
+                if hasattr(interface, indx_key):
+                    indx_val.pop(getattr(interface, indx_key), None)
+
+    def remove_interface_from_indexes(self, ref: system.System, name: str) -> None:
+        """
+        Remove index keys for the system interface.
+        :param ref: The reference to the system whose index keys are removed.
+        :param name: The reference to the system whose index keys are removed.
+        """
+        if not ref.inmemory or name not in ref.interfaces:
+            return
+
+        interface = ref.interfaces[name]
+        with self.lock:
+            for indx_key, indx_val in self.indexes.items():
+                if indx_key == "uid" or self.disabled_indexes[indx_key]:
+                    continue
+                if hasattr(interface, indx_key):
+                    indx_val.pop(getattr(interface, indx_key), None)
diff --git a/cobbler/items/distro.py b/cobbler/items/distro.py
index 98e5f06491..55535af971 100644
--- a/cobbler/items/distro.py
+++ b/cobbler/items/distro.py
@@ -87,13 +87,13 @@ def make_clone(self):
         """
         # FIXME: Change unique base attributes
         _dict = self.to_dict()
+        _dict.pop("uid", None)
         # Drop attributes which are computed from other attributes
         computed_properties = ["remote_grub_initrd", "remote_grub_kernel"]
         for property_name in computed_properties:
             _dict.pop(property_name, None)
         cloned = Distro(self.api)
         cloned.from_dict(_dict)
-        cloned.uid = uuid.uuid4().hex
         return cloned
 
     @classmethod
diff --git a/cobbler/items/file.py b/cobbler/items/file.py
index 83c9286214..262e8c6406 100644
--- a/cobbler/items/file.py
+++ b/cobbler/items/file.py
@@ -62,9 +62,9 @@ def make_clone(self):
         :return: The cloned instance of this object.
         """
         _dict = self.to_dict()
+        _dict.pop("uid", None)
         cloned = File(self.api)
         cloned.from_dict(_dict)
-        cloned.uid = uuid.uuid4().hex
         return cloned
 
     def check_if_valid(self):
diff --git a/cobbler/items/image.py b/cobbler/items/image.py
index babfcaa325..9f0ff705ee 100644
--- a/cobbler/items/image.py
+++ b/cobbler/items/image.py
@@ -84,6 +84,7 @@ def make_clone(self):
         :return: The cloned instance of this object.
         """
         _dict = self.to_dict()
+        _dict.pop("uid", None)
         cloned = Image(self.api)
         cloned.from_dict(_dict)
         cloned.uid = uuid.uuid4().hex
diff --git a/cobbler/items/item.py b/cobbler/items/item.py
index 0b3ebb1825..c9fa1c70db 100644
--- a/cobbler/items/item.py
+++ b/cobbler/items/item.py
@@ -451,6 +451,15 @@ def uid(self, uid: str):
 
         :param uid: The new uid.
         """
+        if self._uid != uid and self.COLLECTION_TYPE != Item.COLLECTION_TYPE:
+            collection = self.api.get_items(self.COLLECTION_TYPE)
+            with collection.lock:
+                item = collection.get(self.name)
+                if item is not None and item.uid == self._uid:
+                    # Update uid index
+                    indx_dict = collection.indexes["uid"]
+                    del indx_dict[self._uid]
+                    indx_dict[uid] = self.name
         self._uid = uid
 
     @property
diff --git a/cobbler/items/menu.py b/cobbler/items/menu.py
index da19938664..79be6d8c83 100644
--- a/cobbler/items/menu.py
+++ b/cobbler/items/menu.py
@@ -57,9 +57,9 @@ def make_clone(self):
         :return: The cloned instance of this object.
         """
         _dict = self.to_dict()
+        _dict.pop("uid", None)
         cloned = Menu(self.api)
         cloned.from_dict(_dict)
-        cloned.uid = uuid.uuid4().hex
         return cloned
 
     #
diff --git a/cobbler/items/mgmtclass.py b/cobbler/items/mgmtclass.py
index 5b80fa05e7..9cdbf8978f 100644
--- a/cobbler/items/mgmtclass.py
+++ b/cobbler/items/mgmtclass.py
@@ -66,9 +66,9 @@ def make_clone(self):
         """
 
         _dict = self.to_dict()
+        _dict.pop("uid", None)
         cloned = Mgmtclass(self.api)
         cloned.from_dict(_dict)
-        cloned.uid = uuid.uuid4().hex
         return cloned
 
     #
diff --git a/cobbler/items/package.py b/cobbler/items/package.py
index b777a1fd63..7452acccda 100644
--- a/cobbler/items/package.py
+++ b/cobbler/items/package.py
@@ -60,9 +60,9 @@ def make_clone(self):
         :return: The cloned instance of this object.
         """
         _dict = self.to_dict()
+        _dict.pop("uid", None)
         cloned = Package(self.api)
         cloned.from_dict(_dict)
-        cloned.uid = uuid.uuid4().hex
         return cloned
 
     #
diff --git a/cobbler/items/profile.py b/cobbler/items/profile.py
index f8861414ab..451eaec3bb 100644
--- a/cobbler/items/profile.py
+++ b/cobbler/items/profile.py
@@ -108,9 +108,9 @@ def make_clone(self):
         :return: The cloned instance of this object.
         """
         _dict = self.to_dict()
+        _dict.pop("uid", None)
         cloned = Profile(self.api)
         cloned.from_dict(_dict)
-        cloned.uid = uuid.uuid4().hex
         return cloned
 
     def check_if_valid(self):
diff --git a/cobbler/items/repo.py b/cobbler/items/repo.py
index c82f5e5553..7311fb2718 100644
--- a/cobbler/items/repo.py
+++ b/cobbler/items/repo.py
@@ -77,9 +77,9 @@ def make_clone(self):
         :return: The cloned instance of this object.
         """
         _dict = self.to_dict()
+        _dict.pop("uid", None)
         cloned = Repo(self.api)
         cloned.from_dict(_dict)
-        cloned.uid = uuid.uuid4().hex
         return cloned
 
     def check_if_valid(self):
diff --git a/cobbler/items/resource.py b/cobbler/items/resource.py
index 591532703c..59d2f4aa3a 100644
--- a/cobbler/items/resource.py
+++ b/cobbler/items/resource.py
@@ -57,9 +57,9 @@ def make_clone(self):
         :return: The cloned instance of this object.
         """
         _dict = self.to_dict()
+        _dict.pop("uid", None)
         cloned = Resource(self.api)
         cloned.from_dict(_dict)
-        cloned.uid = uuid.uuid4().hex
         return cloned
 
     #
diff --git a/cobbler/items/system.py b/cobbler/items/system.py
index ab225db8bf..34df6deffa 100644
--- a/cobbler/items/system.py
+++ b/cobbler/items/system.py
@@ -1,6 +1,7 @@
 """
 All code belonging to Cobbler systems. This includes network interfaces.
 """
+
 # SPDX-License-Identifier: GPL-2.0-or-later
 # SPDX-FileCopyrightText: Copyright 2006-2008, Red Hat, Inc and Others
 # SPDX-FileCopyrightText: Michael DeHaan <michael.dehaan AT gmail>
@@ -23,7 +24,7 @@ class NetworkInterface:
     A subobject of a Cobbler System which represents the network interfaces
     """
 
-    def __init__(self, api):
+    def __init__(self, api, system_name: str, *args, **kwargs):
         """
         Constructor.
 
@@ -54,6 +55,21 @@ def __init__(self, api):
         self._static = False
         self._static_routes = []
         self._virt_bridge = ""
+        self.__system_name = system_name
+
+        if len(kwargs) > 0:
+            self.from_dict(kwargs)
+
+    def __hash__(self):
+        """
+        Hash table for NetworkInterfaces.
+        Requires special handling if the uid value changes and the Item
+        is present in set, frozenset, and dict types.
+        :return: hash(uid).
+        """
+        return hash(
+            (self._mac_address, self._ip_address, self._dns_name, self._ipv6_address)
+        )
 
     def from_dict(self, dictionary: dict):
         """
@@ -240,15 +256,20 @@ def dns_name(self, dns_name: str):
         :param dns_name: DNS Name of the system
         :raises ValueError: In case the DNS name is already existing inside Cobbler
         """
+        if self._dns_name == dns_name:
+            return
         dns_name = validate.hostname(dns_name)
         if dns_name != "" and not self.__api.settings().allow_duplicate_hostnames:
-            matched = self.__api.find_items("system", {"dns_name": dns_name})
+            matched = self.__api.find_system(return_list=True, dns_name=dns_name)
             for match in matched:
                 if self in match.interfaces.values():
                     continue
                 raise ValueError(
                     f'DNS name duplicate found "{dns_name}". Object with the conflict has the name "{match.name}"'
                 )
+        self.__api.systems().update_interface_index_value(
+            self, "dns_name", self._dns_name, dns_name
+        )
         self._dns_name = dns_name
 
     @property
@@ -269,9 +290,11 @@ def ip_address(self, address: str):
         :param address: IP address
         :raises ValueError: In case the IP address is already existing inside Cobbler.
         """
+        if self._ip_address == address:
+            return
         address = validate.ipv4_address(address)
         if address != "" and not self.__api.settings().allow_duplicate_ips:
-            matched = self.__api.find_items("system", {"ip_address": address})
+            matched = self.__api.find_system(return_list=True, ip_address=address)
             for match in matched:
                 if self in match.interfaces.values():
                     continue
@@ -279,6 +302,9 @@ def ip_address(self, address: str):
                     raise ValueError(
                         f'IP address duplicate found "{address}". Object with the conflict has the name "{match.name}"'
                     )
+        self.__api.systems().update_interface_index_value(
+            self, "ip_address", self._ip_address, address
+        )
         self._ip_address = address
 
     @property
@@ -299,6 +325,8 @@ def mac_address(self, address: str):
         :param address: MAC address
         :raises CX: In case there a random mac can't be computed
         """
+        if self._mac_address == address:
+            return
         address = validate.mac_address(address)
         if address == "random":
             # FIXME: Pass virt_type of system
@@ -312,6 +340,9 @@ def mac_address(self, address: str):
                     raise ValueError(
                         f'MAC address duplicate found "{address}". Object with the conflict has the name "{match.name}"'
                     )
+        self.__api.systems().update_interface_index_value(
+            self, "mac_address", self._mac_address, address
+        )
         self._mac_address = address
 
     @property
@@ -497,9 +528,11 @@ def ipv6_address(self, address: str):
         :param address: IP address
         :raises ValueError: IN case the IP is duplicated
         """
+        if self._ipv6_address == address:
+            return
         address = validate.ipv6_address(address)
         if address != "" and not self.__api.settings().allow_duplicate_ips:
-            matched = self.__api.find_items("system", {"ipv6_address": address})
+            matched = self.__api.find_system(return_list=True, ipv6_address=address)
             for match in matched:
                 if self in match.interfaces.values():
                     continue
@@ -508,6 +541,9 @@ def ipv6_address(self, address: str):
                         f'IPv6 address duplicate found "{address}". Object with the conflict has the name'
                         f'"{match.name}"'
                     )
+        self.__api.systems().update_interface_index_value(
+            self, "ipv6_address", self._ipv6_address, address
+        )
         self._ipv6_address = address
 
     @property
@@ -667,6 +703,23 @@ def connected_mode(self, truthiness: bool):
             ) from e
         self._connected_mode = truthiness
 
+    @property
+    def system_name(self) -> str:
+        """
+        system_name property.
+        :getter: Returns the value for ``system_name``.
+        :setter: Sets the value for the property ``system_name``.
+        """
+        return self.__system_name
+
+    @system_name.setter
+    def system_name(self, system_name: str):
+        """
+        Setter for the system_name of the NetworkInterface class.
+        :param system_name: The new system_name.
+        """
+        self.__system_name = system_name
+
     def modify_interface(self, _dict: dict):
         """
         Modify the interface
@@ -809,6 +862,18 @@ def __getattr__(self, name):
 
     def make_clone(self):
         _dict = self.to_dict()
+        _dict.pop("uid", None)
+        collection = self.api.systems()
+        # clear all these out to avoid DHCP/DNS conflicts
+        for interface in _dict["interfaces"].values():
+            if not collection.disabled_indexes["mac_address"]:
+                interface.pop("mac_address", None)
+            if not collection.disabled_indexes["ip_address"]:
+                interface.pop("ip_address", None)
+            if not collection.disabled_indexes["ipv6_address"]:
+                interface.pop("ipv6_address", None)
+            if not collection.disabled_indexes["dns_name"]:
+                interface.pop("dns_name", None)
         cloned = System(self.api)
         cloned.from_dict(_dict)
         cloned.uid = uuid.uuid4().hex
@@ -849,6 +914,16 @@ def check_if_valid(self):
             if self.image is None or self.image == "":
                 raise CX("Error with system %s - profile or image is required" % self.name)
 
+    @Item.name.setter
+    def name(self, name: str) -> None:
+        """
+        The systems name.
+        :param name: system name string
+        """
+        Item.name.fset(self, name)
+        for interface in self.interfaces.values():
+            interface.system_name = name
+
     #
     # specific methods for item.System
     #
@@ -874,13 +949,18 @@ def interfaces(self, value: Dict[str, Any]):
         if not isinstance(value, dict):
             raise TypeError("interfaces must be of type dict")
         dict_values = list(value.values())
+        collection = self.api.systems()
         if all(isinstance(x, NetworkInterface) for x in dict_values):
+            for network_iface in value.values():
+                network_iface.system_name = self.name
+            collection.update_interfaces_indexes(self, value)
             self._interfaces = value
             return
         if all(isinstance(x, dict) for x in dict_values):
             for key in value:
-                network_iface = NetworkInterface(self.api)
+                network_iface = NetworkInterface(self.api, self.name)
                 network_iface.from_dict(value[key])
+                collection.update_interface_indexes(self, key, network_iface)
                 self._interfaces[key] = network_iface
             return
         raise ValueError("The values of the interfaces must be fully of type dict (one level with values) or "
@@ -902,14 +982,17 @@ def delete_interface(self, name: Union[str, dict]):
 
         :raises TypeError: If the name of the interface is not of type str or dict.
         """
+        collection = self.api.systems()
         if isinstance(name, str):
             if not name:
                 return
             if name in self.interfaces:
+                collection.remove_interface_from_indexes(self, name)
                 self.interfaces.pop(name)
                 return
         if isinstance(name, dict):
             interface_name = name.get("interface", "")
+            collection.remove_interface_from_indexes(self, interface_name)
             self.interfaces.pop(interface_name)
             return
         raise TypeError("The name of the interface must be of type str or dict")
@@ -1236,7 +1319,7 @@ def __create_interface(self, interface: str):
 
         :param interface: The name of the interface
         """
-        self.interfaces[interface] = NetworkInterface(self.api)
+        self.interfaces[interface] = NetworkInterface(self.api, self.name)
 
     def __get_interface(self, interface_name: str = "default") -> NetworkInterface:
         """
diff --git a/cobbler/modules/installation/post_report.py b/cobbler/modules/installation/post_report.py
index a31533885a..36556888ac 100644
--- a/cobbler/modules/installation/post_report.py
+++ b/cobbler/modules/installation/post_report.py
@@ -97,7 +97,7 @@ def run(api, args) -> int:
 
         sendmail = True
         for prefix in settings.build_reporting_ignorelist:
-            if prefix != "" and name.lower().startswith(prefix):
+            if prefix != "" and name.startswith(prefix):
                 sendmail = False
 
         if sendmail:
diff --git a/cobbler/remote.py b/cobbler/remote.py
index 72e1026519..16872d7018 100644
--- a/cobbler/remote.py
+++ b/cobbler/remote.py
@@ -1978,7 +1978,7 @@ def __is_interface_field(self, field_name: str) -> bool:
         if field_name in ("delete_interface", "rename_interface"):
             return True
 
-        interface = system.NetworkInterface(self.api)
+        interface = system.NetworkInterface(self.api, "")
         fields = []
         for attribute in interface.__dict__.keys():
             if attribute.startswith("_") and ("api" not in attribute or "logger" in attribute):
@@ -2129,7 +2129,7 @@ def __interface_edits(self, handle, attributes, object_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)
+                interface = system.NetworkInterface(self.api, system_to_edit.name)
             for attribute_key in attributes:
                 if self.__is_interface_field(attribute_key):
                     if hasattr(interface, attribute_key):
diff --git a/docker/develop/develop.dockerfile b/docker/develop/develop.dockerfile
index 4b7867b263..1310a06c4a 100644
--- a/docker/develop/develop.dockerfile
+++ b/docker/develop/develop.dockerfile
@@ -60,6 +60,7 @@ RUN zypper install --no-recommends -y \
     python3-pykickstart        \
     python3-netaddr            \
     python3-pymongo            \
+    python3-psutil             \
     rpm-build                  \
     rsync                      \
     supervisor                 \


Test patch:
diff --git a/tests/collections/distro_collection_test.py b/tests/collections/distro_collection_test.py
index 949b1f6391..e930589e96 100644
--- a/tests/collections/distro_collection_test.py
+++ b/tests/collections/distro_collection_test.py
@@ -1,7 +1,9 @@
 import os.path
+from typing import Callable
 
 import pytest
 
+from cobbler.api import CobblerAPI
 from cobbler.cexceptions import CX
 from cobbler.cobbler_collections import distros
 from cobbler.items import distro
@@ -37,10 +39,12 @@ def test_get(cobbler_api, distro_collection):
 
     # Act
     item = distro_collection.get(name)
+    fake_item = distro_collection.get("fake_name")
 
     # Assert
     assert isinstance(item, distro.Distro)
     assert item.name == name
+    assert fake_item is None
 
 
 def test_find(cobbler_api, distro_collection):
@@ -83,6 +87,7 @@ def test_from_list(distro_collection):
 
     # Assert
     assert len(distro_collection.listing) == 1
+    assert len(distro_collection.indexes["uid"]) == 1
 
 
 def test_copy(cobbler_api, distro_collection, create_kernel_initrd, fk_initrd, fk_kernel):
@@ -96,42 +101,59 @@ def test_copy(cobbler_api, distro_collection, create_kernel_initrd, fk_initrd, f
     distro_collection.add(item1)
 
     # Act
-    new_item_name = "test_copy_successful"
+    new_item_name = "test_copy_new"
     distro_collection.copy(item1, new_item_name)
+    item2 = distro_collection.find(new_item_name, False)
 
     # Assert
     assert len(distro_collection.listing) == 2
     assert name in distro_collection.listing
     assert new_item_name in distro_collection.listing
-
-
-def test_rename(cobbler_api, distro_collection):
+    assert len(distro_collection.indexes["uid"]) == 2
+    assert (distro_collection.indexes["uid"])[item1.uid] == name
+    assert (distro_collection.indexes["uid"])[item2.uid] == new_item_name
+
+
+@pytest.mark.parametrize(
+    "input_new_name",
+    [
+        ("to_be_renamed"),
+        ("UpperCase"),
+    ],
+)
+def test_rename(
+    cobbler_api: CobblerAPI,
+    create_distro: Callable[[str], distro.Distro],
+    distro_collection: distros.Distros,
+    input_new_name: str,
+):
     # Arrange
-    name = "to_be_renamed"
-    item1 = distro.Distro(cobbler_api)
-    item1.name = name
+    item1 = create_distro("old_name")
     distro_collection.add(item1)
 
     # Act
-    new_name = "new_name"
-    distro_collection.rename(item1, new_name)
+    distro_collection.rename(item1, input_new_name)
 
     # Assert
-    assert new_name in distro_collection.listing
-    assert distro_collection.listing.get(new_name).name == new_name
+    assert input_new_name in distro_collection.listing
+    assert distro_collection.listing[input_new_name].name == input_new_name
+    assert (distro_collection.indexes["uid"])[item1.uid] == input_new_name
 
 
-def test_collection_add(cobbler_api, distro_collection):
+def test_collection_add(
+    cobbler_api: CobblerAPI,
+    create_distro: Callable[[str], distro.Distro],
+    distro_collection: distros.Distros,
+):
     # Arrange
     name = "collection_add"
-    item1 = distro.Distro(cobbler_api)
-    item1.name = name
-
+    item1 = create_distro(name)
     # Act
     distro_collection.add(item1)
 
     # Assert
     assert name in distro_collection.listing
+    assert item1.uid in distro_collection.indexes["uid"]
 
 
 def test_duplicate_add(cobbler_api, distro_collection):
@@ -155,13 +177,91 @@ def test_remove(cobbler_api, distro_collection):
     item1.name = name
     distro_collection.add(item1)
     assert name in distro_collection.listing
+    assert len(distro_collection.indexes["uid"]) == 1
+    assert (distro_collection.indexes["uid"])[item1.uid] == item1.name
 
     # Act
     distro_collection.remove(name)
 
     # Assert
     assert name not in distro_collection.listing
+    assert len(distro_collection.indexes["uid"]) == 0
+
+
+def test_indexes(
+    cobbler_api: CobblerAPI,
+    create_distro: Callable[[str], distro.Distro],
+    distro_collection: distros.Distros,
+):
+    # Arrange
+
+    # Assert
+    assert len(distro_collection.indexes) == 1
+    assert len(distro_collection.indexes["uid"]) == 0
+
+
+def test_add_to_indexes(
+    cobbler_api: CobblerAPI,
+    create_distro: Callable[[str], distro.Distro],
+    distro_collection: distros.Distros,
+):
+    # Arrange
+    name = "to_be_removed"
+    item1 = create_distro(name)
+    distro_collection.add(item1)
+
+    # Act
+    del (distro_collection.indexes["uid"])[item1.uid]
+    distro_collection.add_to_indexes(item1)
 
+    # Assert
+    assert item1.uid in distro_collection.indexes["uid"]
+
+
+def test_remove_from_indexes(
+    cobbler_api: CobblerAPI,
+    create_distro: Callable[[str], distro.Distro],
+    distro_collection: distros.Distros,
+):
+    # Arrange
+    name = "to_be_removed"
+    item1 = create_distro(name)
+    distro_collection.add(item1)
+
+    # Act
+    distro_collection.remove_from_indexes(item1)
+
+    # Assert
+    assert item1.uid not in distro_collection.indexes["uid"]
+
+
+def test_find_by_indexes(
+    cobbler_api: CobblerAPI,
+    create_distro: Callable[[str], distro.Distro],
+    distro_collection: distros.Distros,
+):
+    # Arrange
+    name = "to_be_removed"
+    item1 = create_distro(name)
+    distro_collection.add(item1)
+    kargs1 = {"uid": item1.uid}
+    kargs2 = {"uid": "fake_uid"}
+    kargs3 = {"fake_index": item1.uid}
+
+    # Act
+    result1 = distro_collection.find_by_indexes(kargs1)
+    result2 = distro_collection.find_by_indexes(kargs2)
+    result3 = distro_collection.find_by_indexes(kargs3)
+
+    # Assert
+    assert isinstance(result1, list)
+    assert len(result1) == 1
+    assert result1[0] == item1
+    assert len(kargs1) == 0
+    assert result2 is None
+    assert len(kargs2) == 0
+    assert result3 is None
+    assert len(kargs3) == 1
 
 @pytest.mark.skip("Method which is under test is broken!")
 def test_to_string(cobbler_api, distro_collection):
diff --git a/tests/collections/system_collection_test.py b/tests/collections/system_collection_test.py
new file mode 100644
index 0000000000..4fd405fa46
--- /dev/null
+++ b/tests/collections/system_collection_test.py
@@ -0,0 +1,774 @@
+"""
+TODO
+"""
+
+from typing import Callable
+
+import pytest
+
+from cobbler.api import CobblerAPI
+from cobbler.cexceptions import CX
+from cobbler.cobbler_collections import systems
+from cobbler.cobbler_collections.manager import CollectionManager
+from cobbler.items import distro, profile, system
+
+
+@pytest.fixture(name="system_collection")
+def fixture_system_collection(collection_mgr: CollectionManager):
+    """
+    Fixture to provide a concrete implementation (Systems) of a generic collection.
+    """
+    return systems.Systems(collection_mgr)
+
+
+def test_obj_create(collection_mgr: CollectionManager):
+    """
+    TODO
+    """
+    # Arrange & Act
+    test_system_collection = systems.Systems(collection_mgr)
+
+    # Assert
+    assert isinstance(test_system_collection, systems.Systems)
+
+
+def test_factory_produce(cobbler_api: CobblerAPI, system_collection: systems.Systems):
+    """
+    TODO
+    """
+    # Arrange & Act
+    result_system = system_collection.factory_produce(cobbler_api, {})
+
+    # Assert
+    assert isinstance(result_system, system.System)
+
+
+def test_get(
+    cobbler_api: CobblerAPI,
+    create_distro: Callable[[str], distro.Distro],
+    create_profile: Callable[[str], profile.Profile],
+    create_system: Callable[[str], system.System],
+):
+    """
+    TODO
+    """
+    # Arrange
+    name = "test_get"
+    distro1 = create_distro()
+    profile1 = create_profile(name)
+    system1 = create_system(name)
+    system_collection = cobbler_api.systems()
+
+    # Act
+    item = system_collection.get(name)
+    fake_item = system_collection.get("fake_name")
+
+    # Assert
+    assert item.name == name
+    assert fake_item is None
+
+
+def test_find(
+    cobbler_api: CobblerAPI,
+    create_distro: Callable[[str], distro.Distro],
+    create_profile: Callable[[str], profile.Profile],
+    create_system: Callable[[str], system.System],
+):
+    """
+    TODO
+    """
+    # Arrange
+    name = "test_find"
+    distro1 = create_distro()
+    profile1 = create_profile(name)
+    system1 = create_system(name)
+    system_collection = cobbler_api.systems()
+
+    # Act
+    result = system_collection.find(name, True, True)
+
+    # Assert
+    assert isinstance(result, list)
+    assert len(result) == 1
+    assert result[0].name == name
+
+
+def test_to_list(
+    cobbler_api: CobblerAPI,
+    create_distro: Callable[[str], distro.Distro],
+    create_profile: Callable[[str], profile.Profile],
+    create_system: Callable[[str], system.System],
+):
+    """
+    TODO
+    """
+    # Arrange
+    name = "test_to_list"
+    distro1 = create_distro()
+    profile1 = create_profile(name)
+    system1 = create_system(name)
+    system_collection = cobbler_api.systems()
+
+    # Act
+    result = system_collection.to_list()
+
+    # Assert
+    assert len(result) == 1
+    assert result[0].get("name") == name
+
+
+def test_from_list(
+    cobbler_api: CobblerAPI,
+    create_distro: Callable[[str], distro.Distro],
+    create_profile: Callable[[str], profile.Profile],
+):
+    """
+    TODO
+    """
+    # Arrange
+    name = "test_from_list"
+    distro1 = create_distro()
+    profile1 = create_profile(name)
+    system_collection = cobbler_api.systems()
+    item_list = [
+        {
+            "name": "test_from_list",
+            "profile": name,
+            "interfaces": {
+                "default": {
+                    "ip_address": "192.168.1.1",
+                    "ipv6_address": "::1",
+                    "dns_name": "example.org",
+                    "mac_address": "52:54:00:7d:81:f4",
+                }
+            },
+        }
+    ]
+
+    # Act
+    system_collection.from_list(item_list)
+
+    # Assert
+    assert len(system_collection.listing) == 1
+    for indx in system_collection.indexes.values():
+        assert len(indx) == 1
+
+
+def test_copy(
+    cobbler_api: CobblerAPI,
+    create_distro: Callable[[str], distro.Distro],
+    create_profile: Callable[[str], profile.Profile],
+    create_system: Callable[[str], system.System],
+):
+    """
+    TODO
+    """
+    # Arrange
+    name = "test_copy"
+    distro1 = create_distro()
+    profile1 = create_profile(name)
+    system1 = create_system(name)
+    system1.interfaces = {
+        "default": {
+            "ip_address": "192.168.1.1",
+            "ipv6_address": "::1",
+            "dns_name": "example.org",
+            "mac_address": "52:54:00:7d:81:f4",
+        }
+    }
+    system_collection = cobbler_api.systems()
+
+    # Act
+    test1 = {
+        x for y in system_collection.listing.values() for x in y.interfaces.values()
+    }
+    new_item_name = "test_copy_successful"
+    system_collection.copy(system1, new_item_name)
+    system2 = system_collection.find(new_item_name, False)
+
+    # Assert
+    assert len(system_collection.listing) == 2
+    assert name in system_collection.listing
+    assert new_item_name in system_collection.listing
+    for key, value in system_collection.indexes.items():
+        if key == "uid":
+            assert len(value) == 2
+            assert value[getattr(system1, key)] == name
+            assert value[getattr(system2, key)] == new_item_name
+        else:
+            assert len(value) == 1
+            assert value[getattr(system1.interfaces["default"], key)] == name
+
+
+@pytest.mark.parametrize(
+    "input_new_name",
+    [
+        ("to_be_renamed"),
+        ("UpperCase"),
+    ],
+)
+def test_rename(
+    cobbler_api: CobblerAPI,
+    create_distro: Callable[[str], distro.Distro],
+    create_profile: Callable[[str], profile.Profile],
+    create_system: Callable[[str], system.System],
+    input_new_name: str,
+):
+    """
+    TODO
+    """
+    # Arrange
+    name = "test_rename"
+    distro1 = create_distro()
+    profile1 = create_profile(name)
+    system1 = create_system(name)
+    system1.interfaces = {
+        "default": {
+            "ip_address": "192.168.1.1",
+            "ipv6_address": "::1",
+            "dns_name": "example.org",
+            "mac_address": "52:54:00:7d:81:f4",
+        }
+    }
+    system_collection = cobbler_api.systems()
+
+    # Act
+    system_collection.rename(system1, input_new_name)
+
+    # Assert
+    assert input_new_name in system_collection.listing
+    assert system_collection.listing[input_new_name].name == input_new_name
+    for interface in system1.interfaces.values():
+        assert interface.system_name == input_new_name
+    for key, value in system_collection.indexes.items():
+        if key == "uid":
+            assert value[getattr(system1, key)] == input_new_name
+        else:
+            assert value[getattr(system1.interfaces["default"], key)] == input_new_name
+
+
+def test_collection_add(
+    cobbler_api: CobblerAPI,
+    create_distro: Callable[[str], distro.Distro],
+    create_profile: Callable[[str], profile.Profile],
+    create_system: Callable[[str], system.System],
+):
+    """
+    TODO
+    """
+    # Arrange
+    name = "test_collection_add"
+    distro1 = create_distro()
+    profile1 = create_profile(name)
+    system1 = system.System(cobbler_api)
+    system1.name = name
+    system1.profile = profile1.name
+    system1.interfaces = {
+        "default": {
+            "ip_address": "192.168.1.1",
+            "ipv6_address": "::1",
+            "dns_name": "example.org",
+            "mac_address": "52:54:00:7d:81:f4",
+        }
+    }
+    system_collection = cobbler_api.systems()
+
+    # Act
+    system_collection.add(system1)
+
+    # Assert
+    assert name in system_collection.listing
+    assert system_collection.listing[name].name == name
+    for interface in system1.interfaces.values():
+        assert interface.system_name == name
+    for key, value in system_collection.indexes.items():
+        if key == "uid":
+            assert value[getattr(system1, key)] == name
+        else:
+            assert value[getattr(system1.interfaces["default"], key)] == name
+
+
+def test_duplicate_add(
+    cobbler_api: CobblerAPI,
+    create_distro: Callable[[str], distro.Distro],
+    create_profile: Callable[[str], profile.Profile],
+    create_system: Callable[[str], system.System],
+):
+    """
+    TODO
+    """
+    # Arrange
+    name = "test_duplicate_add"
+    distro1 = create_distro(name="duplicate_add")
+    profile1 = create_profile(distro1.name)
+    system1 = create_system(name)
+    system1.interfaces = {
+        "default": {
+            "ip_address": "192.168.1.1",
+            "ipv6_address": "::1",
+            "dns_name": "example.org",
+            "mac_address": "52:54:00:7d:81:f4",
+        }
+    }
+    system_collection = cobbler_api.systems()
+    system2 = system.System(cobbler_api)
+    system2.name = name
+    system2.profile = name
+
+    # Act & Assert
+    assert len(system_collection.indexes["uid"]) == 1
+    with pytest.raises(CX):
+        system_collection.add(system2, check_for_duplicate_names=True)
+    for key, value in system_collection.indexes.items():
+        assert len(system_collection.indexes[key]) == 1
+
+
+def test_remove(
+    cobbler_api: CobblerAPI,
+    create_distro: Callable[[str], distro.Distro],
+    create_profile: Callable[[str], profile.Profile],
+    create_system: Callable[[str], system.System],
+):
+    """
+    TODO
+    """
+    # Arrange
+    name = "test_remove"
+    distro1 = create_distro()
+    profile1 = create_profile(name)
+    system1 = create_system(name)
+    system1.interfaces = {
+        "default": {
+            "ip_address": "192.168.1.1",
+            "ipv6_address": "::1",
+            "dns_name": "example.org",
+            "mac_address": "52:54:00:7d:81:f4",
+        }
+    }
+    system_collection = cobbler_api.systems()
+    assert name in system_collection.listing
+
+    # Pre-Assert to validate if the index is in its correct state
+    for key, value in system_collection.indexes.items():
+        assert len(system_collection.indexes[key]) == 1
+
+    # Act
+    system_collection.remove(name)
+
+    # Assert
+    assert name not in system_collection.listing
+    for key, value in system_collection.indexes.items():
+        assert len(system_collection.indexes[key]) == 0
+
+
+def test_indexes(
+    cobbler_api: CobblerAPI,
+    create_system: Callable[[str], system.System],
+    system_collection: systems.Systems,
+):
+    """
+    TODO
+    """
+    # Arrange
+
+    # Assert
+    assert len(system_collection.indexes) == 5
+    assert len(system_collection.indexes["uid"]) == 0
+    assert len(system_collection.indexes["mac_address"]) == 0
+    assert len(system_collection.indexes["ip_address"]) == 0
+    assert len(system_collection.indexes["ipv6_address"]) == 0
+    assert len(system_collection.indexes["dns_name"]) == 0
+
+    assert len(system_collection.disabled_indexes) == 4
+    assert not system_collection.disabled_indexes["mac_address"]
+    assert not system_collection.disabled_indexes["ip_address"]
+    assert not system_collection.disabled_indexes["ipv6_address"]
+    assert not system_collection.disabled_indexes["dns_name"]
+
+
+def test_add_to_indexes(
+    cobbler_api: CobblerAPI,
+    create_distro: Callable[[str], distro.Distro],
+    create_profile: Callable[[str], profile.Profile],
+    create_system: Callable[[str], system.System],
+):
+    """
+    TODO
+    """
+    # Arrange
+    name = "test_add_to_indexes"
+    distro1 = create_distro()
+    profile1 = create_profile(name)
+    system1 = create_system(name)
+    system1.interfaces = {
+        "default": {
+            "ip_address": "192.168.1.1",
+            "ipv6_address": "::1",
+            "dns_name": "example.org",
+            "mac_address": "52:54:00:7d:81:f4",
+        }
+    }
+    system_collection = cobbler_api.systems()
+    for key, value in system_collection.indexes.items():
+        if key == "uid":
+            del value[getattr(system1, key)]
+        else:
+            del value[getattr(system1.interfaces["default"], key)]
+
+    # Act
+    system_collection.add_to_indexes(system1)
+
+    # Assert
+    for key, value in system_collection.indexes.items():
+        if key == "uid":
+            assert {getattr(system1, key): system1.name} == value
+        else:
+            assert {getattr(system1.interfaces["default"], key): system1.name} == value
+
+
+def test_update_interfaces_indexes(
+    cobbler_api: CobblerAPI,
+    create_distro: Callable[[str], distro.Distro],
+    create_profile: Callable[[str], profile.Profile],
+    create_system: Callable[[str], system.System],
+):
+    """
+    TODO
+    """
+    # Arrange
+    name = "test_update_interfaces_indexes"
+    distro1 = create_distro()
+    profile1 = create_profile(name)
+    system1 = create_system(name)
+    system1.interfaces = {
+        "test1": {
+            "ip_address": "192.168.1.1",
+            "ipv6_address": "::1",
+            "dns_name": "test1.example.org",
+            "mac_address": "52:54:00:7d:81:f4",
+        },
+        "test2": {
+            "ip_address": "192.168.1.2",
+            "ipv6_address": "::2",
+            "dns_name": "test2.example.org",
+            "mac_address": "52:54:00:7d:81:f5",
+        },
+    }
+    system_collection = cobbler_api.systems()
+    interface1 = system.NetworkInterface(cobbler_api, system1.name)
+    interface1.from_dict(
+        {
+            "ip_address": "192.168.1.3",
+            "ipv6_address": "::3",
+            "dns_name": "",
+            "mac_address": "52:54:00:7d:81:f6",
+        }
+    )
+    interface2 = system.NetworkInterface(cobbler_api, system1.name)
+    interface2.from_dict(
+        {
+            "ip_address": "192.168.1.4",
+            "ipv6_address": "",
+            "dns_name": "test4.example.org",
+            "mac_address": "52:54:00:7d:81:f7",
+        }
+    )
+    new_interfaces = {
+        "test1": interface1,
+        "test2": interface2,
+        "test3": system1.interfaces["test2"],
+    }
+
+    # Act
+    system_collection.update_interfaces_indexes(system1, new_interfaces)
+
+    # Assert
+    assert set(system_collection.indexes["ip_address"].keys()) == {
+        "192.168.1.2",
+        "192.168.1.3",
+        "192.168.1.4",
+    }
+    assert set(system_collection.indexes["ipv6_address"].keys()) == {"::2", "::3"}
+    assert set(system_collection.indexes["dns_name"].keys()) == {
+        "test2.example.org",
+        "test4.example.org",
+    }
+    assert set(system_collection.indexes["mac_address"].keys()) == {
+        "52:54:00:7d:81:f5",
+        "52:54:00:7d:81:f6",
+        "52:54:00:7d:81:f7",
+    }
+
+    # Cleanup
+    for indx_key, indx_val in system_collection.indexes.items():
+        if indx_key == "uid":
+            continue
+        system_collection.indexes[indx_key] = {}
+
+
+def test_update_interface_indexes(
+    cobbler_api: CobblerAPI,
+    create_distro: Callable[[str], distro.Distro],
+    create_profile: Callable[[str], profile.Profile],
+    create_system: Callable[[str], system.System],
+):
+    """
+    TODO
+    """
+    # Arrange
+    name = "test_update_interface_indexes"
+    distro1 = create_distro()
+    profile1 = create_profile(name)
+    system1 = create_system(name)
+    system1.interfaces = {
+        "test1": {
+            "ip_address": "192.168.1.1",
+            "ipv6_address": "::1",
+            "dns_name": "test1.example.org",
+            "mac_address": "52:54:00:7d:81:f4",
+        },
+        "test2": {
+            "ip_address": "192.168.1.2",
+            "ipv6_address": "::2",
+            "dns_name": "test2.example.org",
+            "mac_address": "52:54:00:7d:81:f5",
+        },
+    }
+    system_collection = cobbler_api.systems()
+    interface1 = system.NetworkInterface(cobbler_api, system1.name)
+    interface1.from_dict(
+        {
+            "ip_address": "192.168.1.3",
+            "ipv6_address": "::3",
+            "dns_name": "",
+            "mac_address": "52:54:00:7d:81:f6",
+        }
+    )
+
+    # Act
+    system_collection.update_interface_indexes(system1, "test2", interface1)
+
+    # Assert
+    assert set(system_collection.indexes["ip_address"].keys()) == {
+        "192.168.1.1",
+        "192.168.1.3",
+    }
+    assert set(system_collection.indexes["ipv6_address"].keys()) == {"::1", "::3"}
+    assert set(system_collection.indexes["dns_name"].keys()) == {"test1.example.org"}
+    assert set(system_collection.indexes["mac_address"].keys()) == {
+        "52:54:00:7d:81:f4",
+        "52:54:00:7d:81:f6",
+    }
+
+    # Cleanup
+    for indx_key, indx_val in system_collection.indexes.items():
+        if indx_key == "uid":
+            continue
+        system_collection.indexes[indx_key] = {}
+
+
+def test_update_system_indexes(
+    cobbler_api: CobblerAPI,
+    create_distro: Callable[[str], distro.Distro],
+    create_profile: Callable[[str], profile.Profile],
+    create_system: Callable[[str], system.System],
+):
+    """
+    TODO
+    """
+    # Arrange
+    name = "test_update_system_indexes"
+    distro1 = create_distro()
+    profile1 = create_profile(name)
+    system1 = create_system(name)
+    system1.interfaces = {
+        "test1": {
+            "ip_address": "192.168.1.1",
+            "ipv6_address": "::1",
+            "dns_name": "test1.example.org",
+            "mac_address": "52:54:00:7d:81:f4",
+        },
+    }
+    system_collection = cobbler_api.systems()
+    interface1 = system.NetworkInterface(cobbler_api, system1.name)
+    interface1.from_dict(
+        {
+            "ip_address": "192.168.1.3",
+            "ipv6_address": "::3",
+            "dns_name": "",
+            "mac_address": "52:54:00:7d:81:f6",
+        }
+    )
+
+    # Act
+    original_uid = system1.uid
+    system1.uid = "test_uid"
+    system1.interfaces["test1"].ip_address = "192.168.1.2"
+    system1.interfaces["test1"].ipv6_address = "::2"
+    system1.interfaces["test1"].dns_name = "test2.example.org"
+    system1.interfaces["test1"].mac_address = "52:54:00:7d:81:f5"
+
+    interface1.ip_address = "192.168.1.4"
+    interface1.ipv6_address = "::4"
+    interface1.dns_name = "test4.example.org"
+    interface1.mac_address = "52:54:00:7d:81:f7"
+
+    # Assert
+    assert original_uid not in system_collection.indexes["uid"]
+    assert (system_collection.indexes["uid"])["test_uid"] == system1.name
+    assert "192.168.1.1" not in system_collection.indexes["ip_address"]
+    assert (system_collection.indexes["ip_address"])["192.168.1.2"] == system1.name
+    assert "::1" not in system_collection.indexes["ipv6_address"]
+    assert (system_collection.indexes["ipv6_address"])["::2"] == system1.name
+    assert "test1.example.org" not in system_collection.indexes["dns_name"]
+    assert (system_collection.indexes["dns_name"])["test2.example.org"] == system1.name
+    assert "52:54:00:7d:81:f4" not in system_collection.indexes["mac_address"]
+    assert (system_collection.indexes["mac_address"])[
+        "52:54:00:7d:81:f5"
+    ] == system1.name
+
+    assert interface1.ip_address not in system_collection.indexes["ip_address"]
+    assert interface1.ipv6_address not in system_collection.indexes["ipv6_address"]
+    assert interface1.dns_name not in system_collection.indexes["dns_name"]
+    assert interface1.mac_address not in system_collection.indexes["mac_address"]
+
+
+def test_remove_from_indexes(
+    cobbler_api: CobblerAPI,
+    create_distro: Callable[[str], distro.Distro],
+    create_profile: Callable[[str], profile.Profile],
+    create_system: Callable[[str], system.System],
+):
+    """
+    TODO
+    """
+    # Arrange
+    name = "test_remove_from_indexes"
+    distro1 = create_distro()
+    profile1 = create_profile(name)
+    system1 = create_system(name)
+    system1.interfaces = {
+        "default": {
+            "ip_address": "192.168.1.1",
+            "ipv6_address": "::1",
+            "dns_name": "example.org",
+            "mac_address": "52:54:00:7d:81:f4",
+        }
+    }
+    system_collection = cobbler_api.systems()
+    original_uid = system1.uid
+
+    # Act
+    system_collection.remove_from_indexes(system1)
+
+    # Assert
+    assert system1.uid not in system_collection.indexes["uid"]
+    assert (
+        system1.interfaces["default"].ip_address
+        not in system_collection.indexes["ip_address"]
+    )
+    assert (
+        system1.interfaces["default"].ipv6_address
+        not in system_collection.indexes["ipv6_address"]
+    )
+    assert (
+        system1.interfaces["default"].dns_name
+        not in system_collection.indexes["dns_name"]
+    )
+    assert (
+        system1.interfaces["default"].mac_address
+        not in system_collection.indexes["mac_address"]
+    )
+
+    # Cleanup
+    system1.uid = original_uid
+
+
+def test_find_by_indexes(
+    cobbler_api: CobblerAPI,
+    create_distro: Callable[[str], distro.Distro],
+    create_profile: Callable[[str], profile.Profile],
+    create_system: Callable[[str], system.System],
+):
+    """
+    TODO
+    """
+    # Arrange
+    name = "test_find_by_indexes"
+    distro1 = create_distro()
+    profile1 = create_profile(name)
+    system1 = create_system(name)
+    system1.interfaces = {
+        "default": {
+            "ip_address": "192.168.1.1",
+            "ipv6_address": "::1",
+            "dns_name": "example.org",
+            "mac_address": "52:54:00:7d:81:f4",
+        }
+    }
+    system_collection = cobbler_api.systems()
+
+    kargs = {
+        "uid": [
+            {"uid": system1.uid},
+            {"uid": "fake_uid"},
+            {"fake_index": system1.uid},
+        ],
+        "ip_address": [
+            {"ip_address": system1.interfaces["default"].ip_address},
+            {"ip_address": "fake_ip_addres"},
+            {"fake_index": system1.interfaces["default"].ip_address},
+        ],
+        "ipv6_address": [
+            {"ipv6_address": system1.interfaces["default"].ipv6_address},
+            {"ipv6_address": "fake_ipv6_addres"},
+            {"fake_index": system1.interfaces["default"].ipv6_address},
+        ],
+        "dns_name": [
+            {"dns_name": system1.interfaces["default"].dns_name},
+            {"dns_name": "fake_dns_name"},
+            {"fake_index": system1.interfaces["default"].dns_name},
+        ],
+        "mac_address": [
+            {"mac_address": system1.interfaces["default"].mac_address},
+            {"mac_address": "fake_mac_address"},
+            {"fake_index": system1.interfaces["default"].mac_address},
+        ],
+    }
+    results = {}
+
+    # Act
+    for indx, args in kargs.items():
+        results[indx] = []
+        for arg in args:
+            results[indx].append(system_collection.find_by_indexes(arg))
+
+    # Assert
+    for indx, result in results.items():
+        assert isinstance(result[0], list)
+        assert len(result[0]) == 1
+        assert (result[0])[0] == system1
+        assert len((kargs[indx])[0]) == 0
+        assert result[1] is None
+        assert len((kargs[indx])[1]) == 0
+        assert result[2] is None
+        assert len((kargs[indx])[2]) == 1
+
+
+@pytest.mark.skip("Method which is under test is broken!")
+def test_to_string(cobbler_api: CobblerAPI, system_collection: systems.Systems):
+    """
+    TODO
+    """
+    # Arrange
+    name = "to_string"
+    item1 = system.System(cobbler_api)
+    item1.name = name
+    system_collection.add(item1)
+
+    # Act
+    result = system_collection.to_string()
+
+    # Assert
+    print(result)
+    assert False
\ No newline at end of file
diff --git a/tests/conftest.py b/tests/conftest.py
index a4eec0b620..7fe01f53d5 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -35,6 +35,7 @@ def reset_settings_yaml(tmp_path):
 
 @pytest.fixture(scope="function", autouse=True)
 def reset_items(cobbler_api):
+    print(list(cobbler_api.systems().listing.keys()))
     for system in cobbler_api.systems():
         cobbler_api.remove_system(system.name)
     for image in cobbler_api.images():
@@ -167,7 +168,9 @@ def _create_system(profile_name="", image_name="", name=""):
             test_system.profile = profile_name
         if image_name != "":
             test_system.image = image_name
-        test_system.interfaces = {"default": NetworkInterface(cobbler_api)}
+        test_system.interfaces = {
+            "default": NetworkInterface(cobbler_api, test_system.name)
+        }
         cobbler_api.add_system(test_system)
         return test_system
 
diff --git a/tests/items/network_interface_test.py b/tests/items/network_interface_test.py
index 896a16ad4f..956276c501 100644
--- a/tests/items/network_interface_test.py
+++ b/tests/items/network_interface_test.py
@@ -12,7 +12,7 @@ def test_network_interface_object_creation(cobbler_api):
     # Arrange
 
     # Act
-    interface = NetworkInterface(cobbler_api)
+    interface = NetworkInterface(cobbler_api, "")
 
     # Assert
     assert isinstance(interface, NetworkInterface)
@@ -20,7 +20,7 @@ def test_network_interface_object_creation(cobbler_api):
 
 def test_network_interface_to_dict(cobbler_api):
     # Arrange
-    interface = NetworkInterface(cobbler_api)
+    interface = NetworkInterface(cobbler_api, "")
 
     # Act
     result = interface.to_dict()
@@ -49,7 +49,7 @@ def test_network_interface_from_dict(
 ):
     # Arrange
     caplog.set_level(logging.INFO)
-    interface = NetworkInterface(cobbler_api)
+    interface = NetworkInterface(cobbler_api, "")
 
     # Act
     interface.from_dict(input_dict)
@@ -80,7 +80,7 @@ def test_deserialize():
 )
 def test_dhcp_tag(cobbler_api, input_dhcp_tag, expected_result, expected_exception):
     # Arrange
-    interface = NetworkInterface(cobbler_api)
+    interface = NetworkInterface(cobbler_api, "")
 
     # Act
     with expected_exception:
@@ -93,7 +93,7 @@ def test_dhcp_tag(cobbler_api, input_dhcp_tag, expected_result, expected_excepti
 
 def test_cnames(cobbler_api):
     # Arrange
-    interface = NetworkInterface(cobbler_api)
+    interface = NetworkInterface(cobbler_api, "")
 
     # Act
     interface.cnames = []
@@ -105,7 +105,7 @@ def test_cnames(cobbler_api):
 
 def test_static_routes(cobbler_api):
     # Arrange
-    interface = NetworkInterface(cobbler_api)
+    interface = NetworkInterface(cobbler_api, "")
 
     # Act
     interface.static_routes = []
@@ -126,7 +126,7 @@ def test_static_routes(cobbler_api):
 )
 def test_static(cobbler_api, input_static, expected_result, expected_exception):
     # Arrange
-    interface = NetworkInterface(cobbler_api)
+    interface = NetworkInterface(cobbler_api, "")
 
     # Act
     with expected_exception:
@@ -148,7 +148,7 @@ def test_static(cobbler_api, input_static, expected_result, expected_exception):
 )
 def test_management(cobbler_api, input_management, expected_result, expected_exception):
     # Arrange
-    interface = NetworkInterface(cobbler_api)
+    interface = NetworkInterface(cobbler_api, "")
 
     # Act
     with expected_exception:
@@ -182,7 +182,7 @@ def test_dns_name(
     system = create_system(profile.name)
     system.interfaces["default"].dns_name = "duplicate.example.org"
     cobbler_api.add_system(system)
-    interface = NetworkInterface(cobbler_api)
+    interface = NetworkInterface(cobbler_api, system.name)
 
     # Act
     with expected_exception:
@@ -224,7 +224,7 @@ def test_mac_address(
     system2.interfaces["default"].mac_address = "random"
     cobbler_api.add_system(system2)
     mocker.patch("cobbler.utils.get_random_mac", return_value="AA:BB:CC:DD:EE:FF")
-    interface = NetworkInterface(cobbler_api)
+    interface = NetworkInterface(cobbler_api, system.name)
 
     # Act
     with expected_exception:
@@ -238,7 +238,7 @@ def test_mac_address(
 
 def test_netmask(cobbler_api):
     # Arrange
-    interface = NetworkInterface(cobbler_api)
+    interface = NetworkInterface(cobbler_api, "")
 
     # Act
     interface.netmask = ""
@@ -250,7 +250,7 @@ def test_netmask(cobbler_api):
 
 def test_if_gateway(cobbler_api):
     # Arrange
-    interface = NetworkInterface(cobbler_api)
+    interface = NetworkInterface(cobbler_api, "")
 
     # Act
     interface.if_gateway = ""
@@ -273,7 +273,7 @@ def test_virt_bridge(
     cobbler_api, input_virt_bridge, expected_result, expected_exception
 ):
     # Arrange
-    interface = NetworkInterface(cobbler_api)
+    interface = NetworkInterface(cobbler_api, "")
 
     # Act
     with expected_exception:
@@ -300,7 +300,7 @@ def test_interface_type(
     cobbler_api, input_interface_type, expected_result, expected_exception
 ):
     # Arrange
-    interface = NetworkInterface(cobbler_api)
+    interface = NetworkInterface(cobbler_api, "")
 
     # Act
     with expected_exception:
@@ -322,7 +322,7 @@ def test_interface_master(
     cobbler_api, input_interface_master, expected_result, expected_exception
 ):
     # Arrange
-    interface = NetworkInterface(cobbler_api)
+    interface = NetworkInterface(cobbler_api, "")
 
     # Act
     with expected_exception:
@@ -344,7 +344,7 @@ def test_bonding_opts(
     cobbler_api, input_bonding_opts, expected_result, expected_exception
 ):
     # Arrange
-    interface = NetworkInterface(cobbler_api)
+    interface = NetworkInterface(cobbler_api, "")
 
     # Act
     with expected_exception:
@@ -366,7 +366,7 @@ def test_bridge_opts(
     cobbler_api, input_bridge_opts, expected_result, expected_exception
 ):
     # Arrange
-    interface = NetworkInterface(cobbler_api)
+    interface = NetworkInterface(cobbler_api, "")
 
     # Act
     with expected_exception:
@@ -400,7 +400,7 @@ def test_ip_address(
     system = create_system(profile.name)
     system.interfaces["default"].ip_address = "172.30.0.2"
     cobbler_api.add_system(system)
-    interface = NetworkInterface(cobbler_api)
+    interface = NetworkInterface(cobbler_api, system.name)
 
     # Act
     with expected_exception:
@@ -435,7 +435,7 @@ def test_ipv6_address(
     system = create_system(profile.name)
     system.interfaces["default"].ipv6_address = "2001:db8:3c4d::2"
     cobbler_api.add_system(system)
-    interface = NetworkInterface(cobbler_api)
+    interface = NetworkInterface(cobbler_api, system.name)
 
     # Act
     with expected_exception:
@@ -458,7 +458,7 @@ def test_ipv6_prefix(
     cobbler_api, input_ipv6_prefix, expected_result, expected_exception
 ):
     # Arrange
-    interface = NetworkInterface(cobbler_api)
+    interface = NetworkInterface(cobbler_api, "")
 
     # Act
     with expected_exception:
@@ -481,7 +481,7 @@ def test_ipv6_secondaries(
     cobbler_api, input_secondaries, expected_result, expected_exception
 ):
     # Arrange
-    interface = NetworkInterface(cobbler_api)
+    interface = NetworkInterface(cobbler_api, "")
 
     # Act
     with expected_exception:
@@ -505,7 +505,7 @@ def test_ipv6_default_gateway(
     cobbler_api, input_address, expected_result, expected_exception
 ):
     # Arrange
-    interface = NetworkInterface(cobbler_api)
+    interface = NetworkInterface(cobbler_api, "")
 
     # Act
     with expected_exception:
@@ -518,7 +518,7 @@ def test_ipv6_default_gateway(
 
 def test_ipv6_static_routes(cobbler_api):
     # Arrange
-    interface = NetworkInterface(cobbler_api)
+    interface = NetworkInterface(cobbler_api, "")
 
     # Act
     interface.ipv6_static_routes = []
@@ -537,7 +537,7 @@ def test_ipv6_static_routes(cobbler_api):
 )
 def test_ipv6_mtu(cobbler_api, input_ipv6_mtu, expected_result, expected_exception):
     # Arrange
-    interface = NetworkInterface(cobbler_api)
+    interface = NetworkInterface(cobbler_api, "")
 
     # Act
     with expected_exception:
@@ -557,7 +557,7 @@ def test_ipv6_mtu(cobbler_api, input_ipv6_mtu, expected_result, expected_excepti
 )
 def test_mtu(cobbler_api, input_mtu, expected_result, expected_exception):
     # Arrange
-    interface = NetworkInterface(cobbler_api)
+    interface = NetworkInterface(cobbler_api, "")
 
     # Act
     with expected_exception:
@@ -581,7 +581,7 @@ def test_connected_mode(
     cobbler_api, input_connected_mode, expected_result, expected_exception
 ):
     # Arrange
-    interface = NetworkInterface(cobbler_api)
+    interface = NetworkInterface(cobbler_api, "")
 
     # Act
     with expected_exception:
@@ -607,7 +607,7 @@ def test_modify_interface(
     expected_exception,
 ):
     # Arrange
-    interface = NetworkInterface(cobbler_api)
+    interface = NetworkInterface(cobbler_api, "")
 
     # Act
     with expected_exception:
diff --git a/tests/items/system_test.py b/tests/items/system_test.py
index 749ecb82be..0dcba269f2 100644
--- a/tests/items/system_test.py
+++ b/tests/items/system_test.py
@@ -1,5 +1,4 @@
 import pytest
-from docutils.nodes import error
 
 from cobbler import enums
 from cobbler.cexceptions import CX
@@ -648,7 +647,7 @@ def test_serial_baud_rate(cobbler_api, value, expected_exception):
 def test_from_dict_with_network_interface(cobbler_api):
     # Arrange
     system = System(cobbler_api)
-    system.interfaces = {"default": NetworkInterface(cobbler_api)}
+    system.interfaces = {"default": NetworkInterface(cobbler_api, system.name)}
     sys_dict = system.to_dict()
 
     # Act
@@ -668,7 +667,7 @@ def test_from_dict_with_network_interface(cobbler_api):
 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": NetworkInterface(cobbler_api, system.name)}
     system.interfaces["default"].mac_address = input_mac
     system.interfaces["default"].ip_address = input_ipv4
     system.interfaces["default"].ipv6_address = input_ipv6
diff --git a/tests/modules/managers/dnsmasq_test.py b/tests/modules/managers/dnsmasq_test.py
index da8dcce52f..cdf8fd3142 100644
--- a/tests/modules/managers/dnsmasq_test.py
+++ b/tests/modules/managers/dnsmasq_test.py
@@ -41,7 +41,9 @@ def test_manager_write_configs(mocker, cobbler_api):
     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": NetworkInterface(cobbler_api, mock_system.name)
+    }
     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"
@@ -75,7 +77,9 @@ def test_manager_regen_ethers(mocker, cobbler_api):
     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": NetworkInterface(cobbler_api, mock_system.name)
+    }
     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"
@@ -98,7 +102,9 @@ def test_manager_regen_hosts(mocker, cobbler_api):
     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": NetworkInterface(cobbler_api, mock_system.name)
+    }
     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"
diff --git a/tests/modules/managers/ndjbdns_test.py b/tests/modules/managers/ndjbdns_test.py
index 0ca4873b82..f7760e9e11 100644
--- a/tests/modules/managers/ndjbdns_test.py
+++ b/tests/modules/managers/ndjbdns_test.py
@@ -36,7 +36,9 @@ def test_manager_write_configs(mocker, cobbler_api):
     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": NetworkInterface(cobbler_api, mock_system.name)
+    }
     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"