Repository: cobbler/cobbler

Issue:
[Backport] incorrect filename in dhcpd.conf
### Original feature issue

- Issue: #3161
- PR: #3241

### Target release

- [x] release33
- [ ] release32
- [ ] release30

### Reason

Stabilization of SUSE Manager and Uyuni.



Discussion:

Resolving patch:
diff --git a/cobbler/items/menu.py b/cobbler/items/menu.py
--- a/cobbler/items/menu.py
+++ b/cobbler/items/menu.py
@@ -20,8 +20,8 @@
 import uuid
 from typing import List, Optional
 
-from cobbler.items import item
 from cobbler.cexceptions import CX
+from cobbler.items import item
 
 
 class Menu(item.Item):
@@ -64,7 +64,7 @@ def from_dict(self, dictionary: dict):
         super().from_dict(dictionary)
 
     @property
-    def parent(self) -> Optional['Menu']:
+    def parent(self) -> Optional["Menu"]:
         """
         Parent menu of a menu instance.
 
@@ -83,6 +83,8 @@ def parent(self, value: str):
         :param value: The name of the parent to set.
         :raises CX: Raised in case of self parenting or if the menu with value ``value`` is not found.
         """
+        if not isinstance(value, str):  # type: ignore
+            raise TypeError('Property "parent" must be of type str!')
         old_parent = self._parent
         if isinstance(old_parent, item.Item):
             old_parent.children.remove(self.name)
@@ -123,15 +125,20 @@ def children(self, value: List[str]):
             raise TypeError("Field children of object menu must be of type list.")
         if isinstance(value, list):
             if not all(isinstance(x, str) for x in value):
-                raise TypeError("Field children of object menu must be of type list and all items need to be menu "
-                                "names (str).")
+                raise TypeError(
+                    "Field children of object menu must be of type list and all items need to be menu "
+                    "names (str)."
+                )
             self._children = []
             for name in value:
                 menu = self.api.find_menu(name=name)
                 if menu is not None:
                     self._children.append(name)
                 else:
-                    self.logger.warning("Menu with the name \"%s\" did not exist. Skipping setting as a child!", name)
+                    self.logger.warning(
+                        'Menu with the name "%s" did not exist. Skipping setting as a child!',
+                        name,
+                    )
 
     #
     # specific methods for item.Menu
diff --git a/cobbler/items/profile.py b/cobbler/items/profile.py
--- a/cobbler/items/profile.py
+++ b/cobbler/items/profile.py
@@ -20,12 +20,11 @@
 import uuid
 from typing import Optional, Union
 
-from cobbler import autoinstall_manager
-from cobbler.items import item
-from cobbler import utils, validate, enums
+from cobbler import autoinstall_manager, enums, utils, validate
 from cobbler.cexceptions import CX
-from cobbler.items.distro import Distro
 from cobbler.decorator import InheritableProperty
+from cobbler.items import item
+from cobbler.items.distro import Distro
 
 
 class Profile(item.Item):
@@ -88,7 +87,9 @@ def __getattr__(self, name):
             return self.autoinstall
         elif name == "ks_meta":
             return self.autoinstall_meta
-        raise AttributeError("Attribute \"%s\" did not exist on object type Profile." % name)
+        raise AttributeError(
+            'Attribute "%s" did not exist on object type Profile.' % name
+        )
 
     #
     # override some base class methods first (item.Item)
@@ -172,11 +173,13 @@ def parent(self, parent: str):
         :param parent: The name of the parent object.
         :raises CX: In case self parentage is found or the profile given could not be found.
         """
+        if not isinstance(parent, str):  # type: ignore
+            raise TypeError('Property "parent" must be of type str!')
         old_parent = self.parent
         if isinstance(old_parent, item.Item) and self.name in old_parent.children:
             old_parent.children.remove(self.name)
         if not parent:
-            self._parent = ''
+            self._parent = ""
             return
         if parent == self.name:
             # check must be done in two places as setting parent could be called before/after setting name...
@@ -235,7 +238,9 @@ def distro(self, distro_name: str):
         if isinstance(old_parent, item.Item) and self.name in old_parent.children:
             old_parent.children.remove(self.name)
         self._distro = distro_name
-        self.depth = distro.depth + 1    # reset depth if previously a subprofile and now top-level
+        self.depth = (
+            distro.depth + 1
+        )  # reset depth if previously a subprofile and now top-level
         if self.name not in distro.children:
             distro.children.append(self.name)
 
@@ -460,12 +465,21 @@ def filename(self, filename: str):
         :param filename: The new ``filename`` for the profile.
         :raises TypeError: In case the new value was not of type ``str``.
         """
-        if not isinstance(filename, str):
+        if not isinstance(filename, str):  # type: ignore
             raise TypeError("Field filename of object profile needs to be of type str!")
+        parent = self.parent
+        if (
+            filename == enums.VALUE_INHERITED
+            and parent
+            and parent.TYPE_NAME == "distro"  # type: ignore
+        ):
+            filename = ""
         if not filename:
-            self._filename = enums.VALUE_INHERITED
-        else:
-            self._filename = filename.strip()
+            if parent and parent.TYPE_NAME == "profile":  # type: ignore
+                filename = enums.VALUE_INHERITED
+            else:
+                filename = ""
+        self._filename = filename
 
     @property
     def autoinstall(self) -> str:
@@ -482,8 +496,11 @@ def autoinstall(self) -> str:
             elif parent is not None and isinstance(parent, Distro):
                 return self.api.settings().autoinstall
             else:
-                self.logger.info("Profile \"%s\" did not have a valid parent of type Profile but autoinstall is set to "
-                                 "\"<<inherit>>\".", self.name)
+                self.logger.info(
+                    'Profile "%s" did not have a valid parent of type Profile but autoinstall is set to '
+                    '"<<inherit>>".',
+                    self.name,
+                )
                 return ""
         return self._autoinstall
 
@@ -495,7 +512,9 @@ def autoinstall(self, autoinstall: str):
         :param autoinstall: local automatic installation template path
         """
         autoinstall_mgr = autoinstall_manager.AutoInstallationManager(self.api)
-        self._autoinstall = autoinstall_mgr.validate_autoinstall_template_file_path(autoinstall)
+        self._autoinstall = autoinstall_mgr.validate_autoinstall_template_file_path(
+            autoinstall
+        )
 
     @InheritableProperty
     def virt_auto_boot(self) -> bool:
@@ -742,12 +761,17 @@ def boot_loaders(self, boot_loaders: list):
             if parent is not None:
                 parent_boot_loaders = parent.boot_loaders
             else:
-                self.logger.warning("Parent of profile \"%s\" could not be found for resolving the parent bootloaders.",
-                                    self.name)
+                self.logger.warning(
+                    'Parent of profile "%s" could not be found for resolving the parent bootloaders.',
+                    self.name,
+                )
                 parent_boot_loaders = []
             if not set(boot_loaders_split).issubset(parent_boot_loaders):
-                raise CX("Error with profile \"%s\" - not all boot_loaders are supported (given: \"%s\"; supported:"
-                         "\"%s\")" % (self.name, str(boot_loaders_split), str(parent_boot_loaders)))
+                raise CX(
+                    'Error with profile "%s" - not all boot_loaders are supported (given: "%s"; supported:'
+                    '"%s")'
+                    % (self.name, str(boot_loaders_split), str(parent_boot_loaders))
+                )
             self._boot_loaders = boot_loaders_split
         else:
             self._boot_loaders = []


Test patch:
diff --git a/tests/items/profile_test.py b/tests/items/profile_test.py
--- a/tests/items/profile_test.py
+++ b/tests/items/profile_test.py
@@ -1,6 +1,7 @@
 import pytest
 
 from cobbler import enums
+from cobbler.cexceptions import CX
 from cobbler.items.distro import Distro
 from cobbler.items.profile import Profile
 from tests.conftest import does_not_raise
@@ -53,7 +54,7 @@ def test_to_dict(cobbler_api, cleanup_to_dict):
 # Properties Tests
 
 
-def test_parent(cobbler_api):
+def test_parent_empty(cobbler_api):
     # Arrange
     profile = Profile(cobbler_api)
 
@@ -64,6 +65,48 @@ def test_parent(cobbler_api):
     assert profile.parent is None
 
 
+def test_parent_profile(cobbler_api, create_distro, create_profile):
+    # Arrange
+    test_dist = create_distro()
+    test_profile = create_profile(test_dist.name)
+    profile = Profile(cobbler_api)
+
+    # Act
+    profile.parent = test_profile.name
+
+    # Assert
+    assert profile.parent is test_profile
+
+
+def test_parent_other_object_type(cobbler_api, create_image):
+    # Arrange
+    test_image = create_image()
+    profile = Profile(cobbler_api)
+
+    # Act
+    with pytest.raises(CX):
+        profile.parent = test_image.name
+
+
+def test_parent_invalid_type(cobbler_api):
+    # Arrange
+    profile = Profile(cobbler_api)
+
+    # Act & Assert
+    with pytest.raises(TypeError):
+        profile.parent = 0
+
+
+def test_parent_self(cobbler_api):
+    # Arrange
+    profile = Profile(cobbler_api)
+    profile.name = "testname"
+
+    # Act & Assert
+    with pytest.raises(CX):
+        profile.parent = profile.name
+
+
 def test_distro(cobbler_api):
     # Arrange
     profile = Profile(cobbler_api)
@@ -108,9 +151,7 @@ def test_proxy(cobbler_api):
     assert profile.proxy == ""
 
 
-@pytest.mark.parametrize("value,expected_exception", [
-    (False, does_not_raise())
-])
+@pytest.mark.parametrize("value,expected_exception", [(False, does_not_raise())])
 def test_enable_ipxe(cobbler_api, value, expected_exception):
     # Arrange
     profile = Profile(cobbler_api)
@@ -123,12 +164,15 @@ def test_enable_ipxe(cobbler_api, value, expected_exception):
         assert profile.enable_ipxe is value
 
 
-@pytest.mark.parametrize("value,expected_exception", [
-    (True, does_not_raise()),
-    (False, does_not_raise()),
-    ("", does_not_raise()),
-    (0, does_not_raise())
-])
+@pytest.mark.parametrize(
+    "value,expected_exception",
+    [
+        (True, does_not_raise()),
+        (False, does_not_raise()),
+        ("", does_not_raise()),
+        (0, does_not_raise()),
+    ],
+)
 def test_enable_menu(cobbler_api, value, expected_exception):
     # Arrange
     profile = Profile(cobbler_api)
@@ -206,6 +250,44 @@ def test_filename(cobbler_api):
     assert profile.filename == "<<inherit>>"
 
 
+@pytest.mark.parametrize(
+    "input_filename,expected_result,is_subitem,expected_exception",
+    [
+        ("", "", False, does_not_raise()),
+        ("", "", True, does_not_raise()),
+        ("<<inherit>>", "", False, does_not_raise()),
+        ("<<inherit>>", "", True, does_not_raise()),
+        ("test", "test", False, does_not_raise()),
+        ("test", "test", True, does_not_raise()),
+        (0, "", True, pytest.raises(TypeError)),
+    ],
+)
+def test_filename(
+    cobbler_api,
+    create_distro,
+    create_profile,
+    input_filename,
+    expected_result,
+    is_subitem,
+    expected_exception,
+):
+    # Arrange
+    test_dist = create_distro()
+    profile = Profile(cobbler_api)
+    profile.name = "filename_test_profile"
+    if is_subitem:
+        test_profile = create_profile(test_dist.name)
+        profile.parent = test_profile.name
+    profile.distro = test_dist.name
+
+    # Act
+    with expected_exception:
+        profile.filename = input_filename
+
+        # Assert
+        assert profile.filename == expected_result
+
+
 def test_autoinstall(cobbler_api):
     # Arrange
     profile = Profile(cobbler_api)
@@ -239,13 +321,16 @@ def test_virt_auto_boot(cobbler_api, value, expected_exception, expected_result)
         assert profile.virt_auto_boot is expected_result
 
 
-@pytest.mark.parametrize("value,expected_exception, expected_result", [
-    ("", does_not_raise(), 0),
-    # FIXME: (False, pytest.raises(TypeError)), --> does not raise
-    (-5, pytest.raises(ValueError), -5),
-    (0, does_not_raise(), 0),
-    (5, does_not_raise(), 5)
-])
+@pytest.mark.parametrize(
+    "value,expected_exception, expected_result",
+    [
+        ("", does_not_raise(), 0),
+        # FIXME: (False, pytest.raises(TypeError)), --> does not raise
+        (-5, pytest.raises(ValueError), -5),
+        (0, does_not_raise(), 0),
+        (5, does_not_raise(), 5),
+    ],
+)
 def test_virt_cpus(cobbler_api, value, expected_exception, expected_result):
     # Arrange
     profile = Profile(cobbler_api)
@@ -258,14 +343,17 @@ def test_virt_cpus(cobbler_api, value, expected_exception, expected_result):
         assert profile.virt_cpus == expected_result
 
 
-@pytest.mark.parametrize("value,expected_exception,expected_result", [
-    ("5", does_not_raise(), 5.0),
-    ("<<inherit>>", does_not_raise(), 5.0),
-    # FIXME: (False, pytest.raises(TypeError)), --> does not raise
-    (-5, pytest.raises(ValueError), 0),
-    (0, does_not_raise(), 0.0),
-    (5, does_not_raise(), 5.0)
-])
+@pytest.mark.parametrize(
+    "value,expected_exception,expected_result",
+    [
+        ("5", does_not_raise(), 5.0),
+        ("<<inherit>>", does_not_raise(), 5.0),
+        # FIXME: (False, pytest.raises(TypeError)), --> does not raise
+        (-5, pytest.raises(ValueError), 0),
+        (0, does_not_raise(), 0.0),
+        (5, does_not_raise(), 5.0),
+    ],
+)
 def test_virt_file_size(cobbler_api, value, expected_exception, expected_result):
     # Arrange
     profile = Profile(cobbler_api)
@@ -300,12 +388,15 @@ def test_virt_disk_driver(cobbler_api, value, expected_exception, expected_resul
         assert profile.virt_disk_driver == expected_result
 
 
-@pytest.mark.parametrize("value,expected_exception,expected_result", [
-    ("", does_not_raise(), 0),
-    ("<<inherit>>", does_not_raise(), 512),
-    (0, does_not_raise(), 0),
-    (0.0, pytest.raises(TypeError), 0)
-])
+@pytest.mark.parametrize(
+    "value,expected_exception,expected_result",
+    [
+        ("", does_not_raise(), 0),
+        ("<<inherit>>", does_not_raise(), 512),
+        (0, does_not_raise(), 0),
+        (0.0, pytest.raises(TypeError), 0),
+    ],
+)
 def test_virt_ram(cobbler_api, value, expected_exception, expected_result):
     # Arrange
     profile = Profile(cobbler_api)
@@ -318,13 +409,16 @@ def test_virt_ram(cobbler_api, value, expected_exception, expected_result):
         assert profile.virt_ram == expected_result
 
 
-@pytest.mark.parametrize("value,expected_exception,expected_result", [
-    ("<<inherit>>", does_not_raise(), enums.VirtType.XENPV),
-    ("qemu", does_not_raise(), enums.VirtType.QEMU),
-    (enums.VirtType.QEMU, does_not_raise(), enums.VirtType.QEMU),
-    ("", pytest.raises(ValueError), None),
-    (False, pytest.raises(TypeError), None)
-])
+@pytest.mark.parametrize(
+    "value,expected_exception,expected_result",
+    [
+        ("<<inherit>>", does_not_raise(), enums.VirtType.XENPV),
+        ("qemu", does_not_raise(), enums.VirtType.QEMU),
+        (enums.VirtType.QEMU, does_not_raise(), enums.VirtType.QEMU),
+        ("", pytest.raises(ValueError), None),
+        (False, pytest.raises(TypeError), None),
+    ],
+)
 def test_virt_type(cobbler_api, value, expected_exception, expected_result):
     # Arrange
     profile = Profile(cobbler_api)