Repository: cobbler/cobbler

Issue:
`cobbler buildiso` fails when a system is based on an image
### Describe the bug

When executing `cobbler buildiso --systems="example"` the ISO is not built successfully.

### Steps to reproduce

1. Add an image to Cobbler
2. Add a system with the previously added image as a parent
3. Run `cobbler buildiso ...`
4. See below stacktrace

### Expected behavior

`cobbler buildiso` will succeed without problems.

### Cobbler version

````paste below
3.3.3
````

### Operating system

openSUSE Leap/Tumbleweed

### Cobbler log

````paste below
[2022-08-10_124242_buildiso] 2022-08-10T12:42:43 - INFO | copying kernels and initrds for standalone distro
[2022-08-10_124242_buildiso] 2022-08-10T12:42:43 - INFO | processing system: suma-cobbler-test-cli-sles15.tf.local:1
[2022-08-10_124242_buildiso] 2022-08-10T12:42:43 - INFO | Exception occurred: <class 'AttributeError'>
[2022-08-10_124242_buildiso] 2022-08-10T12:42:43 - INFO | Exception value: 'NoneType' object has no attribute 'name'
[2022-08-10_124242_buildiso] 2022-08-10T12:42:43 - INFO | Exception Info:
        File "/usr/lib/python3.6/site-packages/cobbler/remote.py", line 100, in run
          rc = self._run(self)

  File "/usr/lib/python3.6/site-packages/cobbler/remote.py", line 175, in runner
    self.options.get("xorrisofs_opts", ""),

  File "/usr/lib/python3.6/site-packages/cobbler/api.py", line 1909, in build_iso
    distro_name=distro_name, systems=systems, exclude_dns=exclude_dns)

  File "/usr/lib/python3.6/site-packages/cobbler/actions/buildiso/netboot.py", line 567, in run
    self.generate_netboot_iso(systems, exclude_dns)

  File "/usr/lib/python3.6/site-packages/cobbler/actions/buildiso/netboot.py", line 534, in generate_netboot_iso
    self._generate_netboot_system(system, cfglines, exclude_dns)

  File "/usr/lib/python3.6/site-packages/cobbler/actions/buildiso/netboot.py", line 460, in _generate_netboot_system
    distname = self.make_shorter(dist.name)
````

### Screenshots

None

### Additional information

Found during stabilization of Cobbler for Uyuni/SUSE Manager by @meaksh.



Discussion:

Resolving patch:
diff --git a/cobbler/actions/buildiso/netboot.py b/cobbler/actions/buildiso/netboot.py
--- a/cobbler/actions/buildiso/netboot.py
+++ b/cobbler/actions/buildiso/netboot.py
@@ -431,7 +431,15 @@ def filter_systems(self, selected_items: List[str] = None) -> list:
         """
         if selected_items is None:
             selected_items = []
-        return self.filter_items(self.api.systems(), selected_items)
+        found_systems = self.filter_items(self.api.systems(), selected_items)
+        # Now filter all systems out that are image based as we don't know about their kernel and initrds
+        return_systems = []
+        for system in found_systems:
+            # All systems not underneath a profile should be skipped
+            if system.get_conceptual_parent().TYPE_NAME == "profile":
+                return_systems.append(system)
+        # Now finally return
+        return return_systems
 
     def make_shorter(self, distname: str) -> str:
         """


Test patch:
diff --git a/tests/actions/buildiso/buildiso_test.py b/tests/actions/buildiso/buildiso_test.py
--- a/tests/actions/buildiso/buildiso_test.py
+++ b/tests/actions/buildiso/buildiso_test.py
@@ -93,14 +93,19 @@ def test_copy_boot_files(cobbler_api, create_distro, tmpdir):
     assert len(os.listdir(target_folder)) == 2
 
 
-def test_filter_system(cobbler_api, create_distro, create_profile, create_system):
-    # Arrange
+def test_filter_system(
+    cobbler_api, create_distro, create_profile, create_system, create_image
+):
     test_distro = create_distro()
     test_profile = create_profile(test_distro.name)
-    test_system = create_system(profile_name=test_profile.name)
-    itemlist = [test_system.name]
+    test_system_profile = create_system(profile_name=test_profile.name)
+    test_image = create_image()
+    test_system_image = create_system(
+        image_name=test_image.name, name="test_filter_system_image_image"
+    )
+    itemlist = [test_system_profile.name, test_system_image.name]
     build_iso = NetbootBuildiso(cobbler_api)
-    expected_result = [test_system]
+    expected_result = [test_system_profile]
 
     # Act
     result = build_iso.filter_systems(itemlist)
diff --git a/tests/conftest.py b/tests/conftest.py
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -9,6 +9,7 @@
 from cobbler.items.distro import Distro
 from cobbler.items.profile import Profile
 from cobbler.items.system import NetworkInterface, System
+from cobbler.items.image import Image
 
 
 @contextmanager
@@ -37,7 +38,7 @@ def reset_items(cobbler_api):
     for system in cobbler_api.systems():
         cobbler_api.remove_system(system.name)
     for image in cobbler_api.images():
-        cobbler_api.remove_distro(image.name)
+        cobbler_api.remove_image(image.name)
     for profile in cobbler_api.profiles():
         cobbler_api.remove_profile(profile.name)
     for distro in cobbler_api.distros():
@@ -110,6 +111,22 @@ def _create_profile(distro_name):
     return _create_profile
 
 
+@pytest.fixture(scope="function")
+def create_image(request, cobbler_api):
+    """
+    Returns a function which has no arguments. The function returns an image object. The image is already added to the
+    CobblerAPI.
+    """
+
+    def _create_image():
+        test_image = Image(cobbler_api)
+        test_image.name = request.node.originalname
+        cobbler_api.add_image(test_image)
+        return test_image
+
+    return _create_image
+
+
 @pytest.fixture(scope="function")
 def create_system(request, cobbler_api):
     """
@@ -117,9 +134,12 @@ def create_system(request, cobbler_api):
     already added to the CobblerAPI.
     """
 
-    def _create_system(profile_name="", image_name=""):
+    def _create_system(profile_name="", image_name="", name=""):
         test_system = System(cobbler_api)
-        test_system.name = request.node.originalname
+        if name == "":
+            test_system.name = request.node.originalname
+        else:
+            test_system.name = name
         if profile_name != "":
             test_system.profile = profile_name
         if image_name != "":
diff --git a/tests/xmlrpcapi/image_test.py b/tests/xmlrpcapi/image_test.py
--- a/tests/xmlrpcapi/image_test.py
+++ b/tests/xmlrpcapi/image_test.py
@@ -1,46 +1,21 @@
-import pytest
-
-# TODO: Create fixture where image is create
-
-
-@pytest.fixture(scope="function")
-def remove_item(remote, token):
-    """
-    Remove an item with the given name.
-
-    :param token: The fixture to have the token for authenticated strings available.
-    :param remote: The fixture to have the base xmlrpc connection.
-    """
-
-    def _remove_item(itemtype, name):
-        yield
-        remote.remove_item(itemtype, name, token)
-
-    return _remove_item
-
-
 class TestImage:
     def test_create_image(self, remote, token):
         """
-        Test: create/edit of an image object"""
-
-        # Arrange
-
+        Test: create/edit of an image object
+        """
         # Act
-        images = remote.get_images(token)
         image = remote.new_image(token)
 
         # Assert
         assert remote.modify_image(image, "name", "testimage0", token)
         assert remote.save_image(image, token)
-        new_images = remote.get_images(token)
-        assert len(new_images) == len(images) + 1
+        image_list = remote.get_images(token)
+        assert len(image_list) == 1
 
     def test_get_images(self, remote):
         """
         Test: get images
         """
-
         # Arrange
 
         # Act
@@ -48,68 +23,70 @@ def test_get_images(self, remote):
 
         # Assert
 
-    def test_get_image(self, remote):
+    def test_get_image(self, remote, create_image):
         """
         Test: Get an image object
         """
-
         # Arrange
+        test_image = create_image()
 
         # Act
+        result_image = remote.get_image(test_image.name)
 
         # Assert
-        image = remote.get_image("testimage0")
+        assert result_image.get("name") == test_image.name
 
-    def test_find_image(self, remote, token):
+    def test_find_image(self, remote, token, create_image):
         """
         Test: Find an image object
         """
-
         # Arrange
+        test_image = create_image()
 
         # Act
-        result = remote.find_image({"name": "testimage0"}, token)
+        result = remote.find_image({"name": test_image.name}, token)
 
-        # Assert
-        assert result
+        # Assert - We want to find exactly the one item we added
+        assert len(result) == 1
+        assert result[0].get("name") == test_image.name
 
-    def test_copy_image(self, remote, token):
+    def test_copy_image(self, remote, token, create_image):
         """
         Test: Copy an image object
         """
-
         # Arrange
+        test_image = create_image()
+        new_name = "testimagecopy"
 
         # Act
-        image = remote.get_item_handle("image", "testimage0")
+        result = remote.copy_image(test_image.uid, new_name, token)
 
         # Assert
-        assert remote.copy_image(image, "testimagecopy", token)
+        assert result
 
-    def test_rename_image(self, remote, token, remove_item):
+    def test_rename_image(self, remote, token, create_image):
         """
         Test: Rename an image object
         """
         # Arrange
-        name = "testimage1"
-        image = remote.get_item_handle("image", "testimagecopy")
+        test_image = create_image()
+        new_name = "testimage_renamed"
 
         # Act
-        result = remote.rename_image(image, name, token)
-
-        # Cleanup
-        remote.remove_item("image", name, token)
+        result = remote.rename_image(test_image.uid, new_name, token)
 
         # Assert
         assert result
 
-    def test_remove_image(self, remote, token):
+    def test_remove_image(self, remote, token, create_image):
         """
         Test: remove an image object
         """
         # Arrange
+        test_image = create_image()
 
         # Act
+        result = remote.remove_image(test_image.name, token)
 
         # Assert
-        assert remote.remove_image("testimage0", token)
+        assert result