Repository: cobbler/cobbler

Issue:
incorrect filename in dhcpd.conf 
### Describe the bug

Generated dhcpd.conf has `filename "<<inherit>>";` for a system if filename has not been set in profile/system

### Steps to reproduce

1.Import distro
`cobbler import --name=esxi70 --path=/mnt/iso --arch=x86_64 --breed=vmware --os-version=esxi70`
2.Create system
`cobbler system add --name some-host2 --profile "esxi70-x86_64" --netboot-enabled=true --interface="vmnic0" --ip-address="192.168.112.102" --mac-address="52:54:00:54:de:11"`
3.Generated dhcpd.conf
```
...
# group for Cobbler DHCP tag: default
group {
    host generic1 {
            hardware ethernet 52:54:00:54:de:11;
            fixed-address 192.168.112.102;
            next-server 127.0.0.1;
            filename "<<inherit>>";
    }
}
```
### Expected behavior

Generated dhcpd.conf
```
...
# group for Cobbler DHCP tag: default
group {
    host generic1 {
            hardware ethernet 52:54:00:54:de:11;
            fixed-address 192.168.112.102;
            next-server 127.0.0.1;
    }
}
```

### Cobbler version

````
Cobbler 3.4.0
  source: adbb11ef, Mon Jun 20 12:35:43 2022 +0200
  build time: Mon Jun 20 10:39:37 2022
````

### Operating system

Centos 8 Stream

### Cobbler log

No errors or relevant info

### Additional information

See https://gitter.im/cobbler/community?at=62b22a3aab6b634f7355fbc9


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
@@ -71,6 +71,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):
+            raise TypeError('Property "parent" must be of type str!')
         old_parent = self._parent
         if isinstance(old_parent, Menu):
             old_parent.children.remove(self.name)
diff --git a/cobbler/items/profile.py b/cobbler/items/profile.py
--- a/cobbler/items/profile.py
+++ b/cobbler/items/profile.py
@@ -165,6 +165,8 @@ 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):
+            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)
@@ -457,10 +459,19 @@ def filename(self, filename: str):
         """
         if not isinstance(filename, str):
             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"
+        ):
+            filename = ""
         if not filename:
-            self._filename = enums.VALUE_INHERITED
-        else:
-            self._filename = filename.strip()
+            if parent and parent.TYPE_NAME == "profile":
+                filename = enums.VALUE_INHERITED
+            else:
+                filename = ""
+        self._filename = filename
 
     @property
     def autoinstall(self) -> str:
diff --git a/cobbler/items/system.py b/cobbler/items/system.py
--- a/cobbler/items/system.py
+++ b/cobbler/items/system.py
@@ -1020,7 +1020,6 @@ def hostname(self) -> str:
 
         :getter: Returns the value for ``hostname``.
         :setter: Sets the value for the property ``hostname``.
-        :return:
         """
         return self._hostname
 
@@ -1029,8 +1028,7 @@ def hostname(self, value: str):
         """
         Setter for the hostname of the System class.
 
-
-        :param value:
+        :param value: The new hostname
         """
         if not isinstance(value, str):
             raise TypeError("Field hostname of object system needs to be of type str!")
@@ -1043,7 +1041,6 @@ def status(self) -> str:
 
         :getter: Returns the value for ``status``.
         :setter: Sets the value for the property ``status``.
-        :return:
         """
         return self._status
 
@@ -1052,8 +1049,7 @@ def status(self, status: str):
         """
         Setter for the status of the System class.
 
-
-        :param status:
+        :param status: The new system status.
         """
         if not isinstance(status, str):
             raise TypeError("Field status of object system needs to be of type str!")
@@ -1068,7 +1064,6 @@ def boot_loaders(self) -> list:
 
         :getter: Returns the value for ``boot_loaders``.
         :setter: Sets the value for the property ``boot_loaders``.
-        :return:
         """
         return self._resolve("boot_loaders")
 
@@ -1130,7 +1125,7 @@ def server(self, server: str):
         If a system can't reach the boot server at the value configured in settings
         because it doesn't have the same name on it's subnet this is there for an override.
 
-        :param server:
+        :param server: The new value for the ``server`` property.
         :raises TypeError: In case server is no string.
         """
         if not isinstance(server, str):
@@ -1148,7 +1143,6 @@ def next_server_v4(self) -> str:
 
         :getter: Returns the value for ``next_server_v4``.
         :setter: Sets the value for the property ``next_server_v4``.
-        :return:
         """
         return self._resolve("next_server_v4")
 
@@ -1176,7 +1170,6 @@ def next_server_v6(self) -> str:
 
         :getter: Returns the value for ``next_server_v6``.
         :setter: Sets the value for the property ``next_server_v6``.
-        :return:
         """
         return self._resolve("next_server_v6")
 
@@ -1202,7 +1195,6 @@ def filename(self) -> str:
 
         :getter: Returns the value for ``filename``.
         :setter: Sets the value for the property ``filename``.
-        :return:
         """
         return self._resolve("filename")
 
@@ -1211,10 +1203,8 @@ def filename(self, filename: str):
         """
         Setter for the filename of the System class.
 
-
-        :param filename:
+        :param filename: The new value for the ``filename`` property.
         :raises TypeError: In case filename is no string.
-        :return:
         """
         if not isinstance(filename, str):
             raise TypeError("Field filename of object system needs to be of type str!")
@@ -1352,7 +1342,6 @@ def gateway(self):
 
         :getter: Returns the value for ``gateway``.
         :setter: Sets the value for the property ``gateway``.
-        :return:
         """
         return self._gateway
 
@@ -1374,7 +1363,6 @@ def name_servers(self) -> list:
 
         :getter: Returns the value for ``name_servers``.
         :setter: Sets the value for the property ``name_servers``.
-        :return:
         """
         return self._name_servers
 
@@ -1396,7 +1384,6 @@ def name_servers_search(self) -> list:
 
         :getter: Returns the value for ``name_servers_search``.
         :setter: Sets the value for the property ``name_servers_search``.
-        :return:
         """
         return self._name_servers_search
 
@@ -1417,7 +1404,6 @@ def ipv6_autoconfiguration(self) -> bool:
 
         :getter: Returns the value for ``ipv6_autoconfiguration``.
         :setter: Sets the value for the property ``ipv6_autoconfiguration``.
-        :return:
         """
         return self._ipv6_autoconfiguration
 
@@ -1426,8 +1412,7 @@ def ipv6_autoconfiguration(self, value: bool):
         """
         Setter for the ipv6_autoconfiguration of the System class.
 
-
-        :param value:
+        :param value: The new value for the ``ipv6_autoconfiguration`` property.
         """
         value = input_converters.input_boolean(value)
         if not isinstance(value, bool):
@@ -1441,7 +1426,6 @@ def ipv6_default_device(self) -> str:
 
         :getter: Returns the value for ``ipv6_default_device``.
         :setter: Sets the value for the property ``ipv6_default_device``.
-        :return:
         """
         return self._ipv6_default_device
 
@@ -1450,8 +1434,7 @@ def ipv6_default_device(self, interface_name: str):
         """
         Setter for the ipv6_default_device of the System class.
 
-
-        :param interface_name:
+        :param interface_name: The new value for the ``ipv6_default_device`` property.
         """
         if not isinstance(interface_name, str):
             raise TypeError(
@@ -1470,7 +1453,6 @@ def enable_ipxe(self) -> bool:
 
         :getter: Returns the value for ``enable_ipxe``.
         :setter: Sets the value for the property ``enable_ipxe``.
-        :return:
         """
         return self._resolve("enable_ipxe")
 
@@ -1494,7 +1476,6 @@ def profile(self) -> str:
 
         :getter: Returns the value for ``profile``.
         :setter: Sets the value for the property ``profile``.
-        :return:
         """
         return self._profile
 
@@ -1552,7 +1533,6 @@ def image(self) -> str:
 
         :getter: Returns the value for ``image``.
         :setter: Sets the value for the property ``image``.
-        :return:
         """
         return self._image
 
@@ -1811,7 +1791,6 @@ def autoinstall(self) -> str:
 
         :getter: Returns the value for ``autoinstall``.
         :setter: Sets the value for the property ``autoinstall``.
-        :return:
         """
         return self._resolve("autoinstall")
 
@@ -1834,7 +1813,6 @@ def power_type(self) -> str:
 
         :getter: Returns the value for ``power_type``.
         :setter: Sets the value for the property ``power_type``.
-        :return:
         """
         return self._power_type
 
@@ -1843,8 +1821,7 @@ def power_type(self, power_type: str):
         """
         Setter for the power_type of the System class.
 
-
-        :param power_type:
+        :param power_type: The new value for the ``power_type`` property.
         :raises TypeError: In case power_type is no string.
         """
         if not isinstance(power_type, str):
@@ -1862,7 +1839,6 @@ def power_identity_file(self) -> str:
 
         :getter: Returns the value for ``power_identity_file``.
         :setter: Sets the value for the property ``power_identity_file``.
-        :return:
         """
         return self._power_identity_file
 
@@ -1871,8 +1847,7 @@ def power_identity_file(self, power_identity_file: str):
         """
         Setter for the power_identity_file of the System class.
 
-
-        :param power_identity_file:
+        :param power_identity_file: The new value for the ``power_identity_file`` property.
         :raises TypeError: In case power_identity_file is no string.
         """
         if not isinstance(power_identity_file, str):
@@ -1889,7 +1864,6 @@ def power_options(self) -> str:
 
         :getter: Returns the value for ``power_options``.
         :setter: Sets the value for the property ``power_options``.
-        :return:
         """
         return self._power_options
 
@@ -1898,8 +1872,7 @@ def power_options(self, power_options: str):
         """
         Setter for the power_options of the System class.
 
-
-        :param power_options:
+        :param power_options: The new value for the ``power_options`` property.
         :raises TypeError: In case power_options is no string.
         """
         if not isinstance(power_options, str):
@@ -1916,7 +1889,6 @@ def power_user(self) -> str:
 
         :getter: Returns the value for ``power_user``.
         :setter: Sets the value for the property ``power_user``.
-        :return:
         """
         return self._power_user
 
@@ -1925,8 +1897,7 @@ def power_user(self, power_user: str):
         """
         Setter for the power_user of the System class.
 
-
-        :param power_user:
+        :param power_user: The new value for the ``power_user`` property.
         :raises TypeError: In case power_user is no string.
         """
         if not isinstance(power_user, str):
@@ -1943,7 +1914,6 @@ def power_pass(self) -> str:
 
         :getter: Returns the value for ``power_pass``.
         :setter: Sets the value for the property ``power_pass``.
-        :return:
         """
         return self._power_pass
 
@@ -1952,8 +1922,7 @@ def power_pass(self, power_pass: str):
         """
         Setter for the power_pass of the System class.
 
-
-        :param power_pass:
+        :param power_pass: The new value for the ``power_pass`` property.
         :raises TypeError: In case power_pass is no string.
         """
         if not isinstance(power_pass, str):
@@ -1970,7 +1939,6 @@ def power_address(self) -> str:
 
         :getter: Returns the value for ``power_address``.
         :setter: Sets the value for the property ``power_address``.
-        :return:
         """
         return self._power_address
 
@@ -1979,8 +1947,7 @@ def power_address(self, power_address: str):
         """
         Setter for the power_address of the System class.
 
-
-        :param power_address:
+        :param power_address: The new value for the ``power_address`` property.
         :raises TypeError: In case power_address is no string.
         """
         if not isinstance(power_address, str):
@@ -1997,7 +1964,6 @@ def power_id(self) -> str:
 
         :getter: Returns the value for ``power_id``.
         :setter: Sets the value for the property ``power_id``.
-        :return:
         """
         return self._power_id
 
@@ -2006,8 +1972,7 @@ def power_id(self, power_id: str):
         """
         Setter for the power_id of the System class.
 
-
-        :param power_id:
+        :param power_id: The new value for the ``power_id`` property.
         :raises TypeError: In case power_id is no string.
         """
         if not isinstance(power_id, str):
@@ -2022,7 +1987,6 @@ def repos_enabled(self) -> bool:
 
         :getter: Returns the value for ``repos_enabled``.
         :setter: Sets the value for the property ``repos_enabled``.
-        :return:
         """
         return self._repos_enabled
 
@@ -2031,8 +1995,7 @@ def repos_enabled(self, repos_enabled: bool):
         """
         Setter for the repos_enabled of the System class.
 
-
-        :param repos_enabled:
+        :param repos_enabled: The new value for the ``repos_enabled`` property.
         :raises TypeError: In case is no string.
         """
         repos_enabled = input_converters.input_boolean(repos_enabled)
@@ -2076,8 +2039,7 @@ def serial_baud_rate(self, baud_rate: int):
         """
         Setter for the serial_baud_rate of the System class.
 
-
-        :param baud_rate:
+        :param baud_rate: The new value for the ``baud_rate`` property.
         """
         self._serial_baud_rate = validate.validate_serial_baud_rate(baud_rate)
 
@@ -2088,7 +2050,6 @@ def children(self) -> List[str]:
 
         :getter: Returns the value for ``children``.
         :setter: Sets the value for the property ``children``.
-        :return:
         """
         return self._children
 
@@ -2097,8 +2058,7 @@ def children(self, value: List[str]):
         """
         Setter for the children of the System class.
 
-
-        :param value:
+        :param value: The new value for the ``children`` property.
         """
         self._children = value
 


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,5 +1,6 @@
 import pytest
 
+from cobbler.cexceptions import CX
 from cobbler import enums
 from cobbler.items.distro import Distro
 from cobbler.items.profile import Profile
@@ -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)
@@ -196,15 +239,42 @@ def test_next_server_v6(cobbler_api):
     assert profile.next_server_v6 == ""
 
 
-def test_filename(cobbler_api):
+@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
-    profile.filename = "<<inherit>>"
+    with expected_exception:
+        profile.filename = input_filename
 
-    # Assert
-    assert profile.filename == "<<inherit>>"
+        # Assert
+        assert profile.filename == expected_result
 
 
 def test_autoinstall(cobbler_api):