Repository: cobbler/cobbler

Issue:
System Interface: Include name of duplicated object
### Is your feature request related to a problem?

At the moment we just say that something is duplicated not by WHAT exactly. This should be improved.

### Provide a detailed description of the proposed feature

The attributes:

- `hostname`
- `ip_address`
- `ipv6_address`
- `mac`

should report what conflicting objects is conflicting with the object you are trying to modify/create.

### Alternatives you've considered

None

### Additional information

Found during internal customer debugging at SUSE.



Discussion:

Resolving patch:
diff --git a/cobbler/items/item.py b/cobbler/items/item.py
--- a/cobbler/items/item.py
+++ b/cobbler/items/item.py
@@ -803,8 +803,20 @@ def find_match_single_key(self, data, key, value, no_errors: bool = False) -> bo
         key_found_already = False
         if "interfaces" in data:
             if key in [
+                "cnames",
+                "connected_mode",
+                "if_gateway",
+                "ipv6_default_gateway",
+                "ipv6_mtu",
+                "ipv6_prefix",
+                "ipv6_secondaries",
+                "ipv6_static_routes",
+                "management",
+                "mtu",
+                "static",
                 "mac_address",
                 "ip_address",
+                "ipv6_address",
                 "netmask",
                 "virt_bridge",
                 "dhcp_tag",
diff --git a/cobbler/items/system.py b/cobbler/items/system.py
--- a/cobbler/items/system.py
+++ b/cobbler/items/system.py
@@ -1,22 +1,10 @@
 """
-Copyright 2006-2009, Red Hat, Inc and Others
-Michael DeHaan <michael.dehaan AT gmail>
-
-This program is free software; you can redistribute it and/or modify
-it under the terms of the GNU General Public License as published by
-the Free Software Foundation; either version 2 of the License, or
-(at your option) any later version.
-
-This program is distributed in the hope that it will be useful,
-but WITHOUT ANY WARRANTY; without even the implied warranty of
-MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-GNU General Public License for more details.
-
-You should have received a copy of the GNU General Public License
-along with this program; if not, write to the Free Software
-Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
-02110-1301  USA
+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>
+
 import enum
 import logging
 import uuid
@@ -115,7 +103,7 @@ def deserialize(self, interface_dict: dict):
         """
         This is currently a proxy for :py:meth:`~cobbler.items.item.Item.from_dict` .
 
-        :param item_dict: The dictionary with the data to deserialize.
+        :param interface_dict: The dictionary with the data to deserialize.
         """
         self.from_dict(interface_dict)
 
@@ -126,7 +114,6 @@ def dhcp_tag(self) -> str:
 
         :getter: Returns the value for ``dhcp_tag``.
         :setter: Sets the value for the property ``dhcp_tag``.
-        :return:
         """
         return self._dhcp_tag
 
@@ -135,8 +122,7 @@ def dhcp_tag(self, dhcp_tag: str):
         """
         Setter for the dhcp_tag of the NetworkInterface class.
 
-
-        :param dhcp_tag:
+        :param dhcp_tag: The new dhcp tag.
         """
         if not isinstance(dhcp_tag, str):
             raise TypeError(
@@ -151,7 +137,6 @@ def cnames(self) -> list:
 
         :getter: Returns the value for ``cnames``.
         :setter: Sets the value for the property ``cnames``.
-        :return:
         """
         return self._cnames
 
@@ -160,10 +145,9 @@ def cnames(self, cnames: list):
         """
         Setter for the cnames of the NetworkInterface class.
 
-
-        :param cnames:
+        :param cnames: The new cnames.
         """
-        self._cnames = utils.input_string_or_list(cnames)
+        self._cnames = utils.input_string_or_list_no_inherit(cnames)
 
     @property
     def static_routes(self) -> list:
@@ -172,7 +156,6 @@ def static_routes(self) -> list:
 
         :getter: Returns the value for ``static_routes``.
         :setter: Sets the value for the property ``static_routes``.
-        :return:
         """
         return self._static_routes
 
@@ -181,10 +164,9 @@ def static_routes(self, routes: list):
         """
         Setter for the static_routes of the NetworkInterface class.
 
-
-        :param routes:
+        :param routes: The new routes.
         """
-        self._static_routes = utils.input_string_or_list(routes)
+        self._static_routes = utils.input_string_or_list_no_inherit(routes)
 
     @property
     def static(self) -> bool:
@@ -193,7 +175,6 @@ def static(self) -> bool:
 
         :getter: Returns the value for ``static``.
         :setter: Sets the value for the property ``static``.
-        :return:
         """
         return self._static
 
@@ -202,14 +183,14 @@ def static(self, truthiness: bool):
         """
         Setter for the static of the NetworkInterface class.
 
-
-        :param truthiness:
+        :param truthiness: The new value if the interface is static or not.
         """
-        truthiness = utils.input_boolean(truthiness)
-        if not isinstance(truthiness, bool):
+        try:
+            truthiness = utils.input_boolean(truthiness)
+        except TypeError as e:
             raise TypeError(
                 "Field static of NetworkInterface needs to be of Type bool!"
-            )
+            ) from e
         self._static = truthiness
 
     @property
@@ -219,7 +200,6 @@ def management(self) -> bool:
 
         :getter: Returns the value for ``management``.
         :setter: Sets the value for the property ``management``.
-        :return:
         """
         return self._management
 
@@ -228,14 +208,14 @@ def management(self, truthiness: bool):
         """
         Setter for the management of the NetworkInterface class.
 
-
-        :param truthiness:
+        :param truthiness: The new value for management.
         """
-        truthiness = utils.input_boolean(truthiness)
-        if not isinstance(truthiness, bool):
+        try:
+            truthiness = utils.input_boolean(truthiness)
+        except TypeError as e:
             raise TypeError(
                 "Field management of object NetworkInterface needs to be of type bool!"
-            )
+            ) from e
         self._management = truthiness
 
     @property
@@ -245,7 +225,6 @@ def dns_name(self) -> str:
 
         :getter: Returns the value for ``dns_name`.
         :setter: Sets the value for the property ``dns_name``.
-        :return:
         """
         return self._dns_name
 
@@ -263,7 +242,9 @@ def dns_name(self, dns_name: str):
             for match in matched:
                 if self in match.interfaces.values():
                     continue
-                raise ValueError("DNS duplicate found: %s" % dns_name)
+                raise ValueError(
+                    f'DNS name duplicate found "{dns_name}". Object with the conflict has the name "{match.name}"'
+                )
         self._dns_name = dns_name
 
     @property
@@ -273,7 +254,6 @@ def ip_address(self) -> str:
 
         :getter: Returns the value for ``ip_address``.
         :setter: Sets the value for the property ``ip_address``.
-        :return:
         """
         return self._ip_address
 
@@ -292,7 +272,9 @@ def ip_address(self, address: str):
                 if self in match.interfaces.values():
                     continue
                 else:
-                    raise ValueError("IP address duplicate found: %s" % address)
+                    raise ValueError(
+                        f'IP address duplicate found "{address}". Object with the conflict has the name "{match.name}"'
+                    )
         self._ip_address = address
 
     @property
@@ -302,7 +284,6 @@ def mac_address(self) -> str:
 
         :getter: Returns the value for ``mac_address``.
         :setter: Sets the value for the property ``mac_address``.
-        :return:
         """
         return self._mac_address
 
@@ -312,10 +293,11 @@ def mac_address(self, address: str):
         Set MAC address on interface.
 
         :param address: MAC address
-        :raises CX:
+        :raises CX: In case there a random mac can't be computed
         """
         address = validate.mac_address(address)
         if address == "random":
+            # FIXME: Pass virt_type of system
             address = utils.get_random_mac(self.__api)
         if address != "" and not self.__api.settings().allow_duplicate_macs:
             matched = self.__api.find_items("system", {"mac_address": address})
@@ -323,7 +305,9 @@ def mac_address(self, address: str):
                 if self in match.interfaces.values():
                     continue
                 else:
-                    raise ValueError("MAC address duplicate found: %s" % address)
+                    raise ValueError(
+                        f'MAC address duplicate found "{address}". Object with the conflict has the name "{match.name}"'
+                    )
         self._mac_address = address
 
     @property
@@ -333,7 +317,6 @@ def netmask(self) -> str:
 
         :getter: Returns the value for ``netmask``.
         :setter: Sets the value for the property ``netmask``.
-        :return:
         """
         return self._netmask
 
@@ -353,29 +336,29 @@ def if_gateway(self) -> str:
 
         :getter: Returns the value for ``if_gateway``.
         :setter: Sets the value for the property ``if_gateway``.
-        :return:
         """
         return self._if_gateway
 
     @if_gateway.setter
     def if_gateway(self, gateway: str):
         """
-        Set the per-interface gateway.
+        Set the per-interface gateway. Exceptions are raised if the value is invalid. For details see
+        :meth:`~cobbler.validate.ipv4_address`.
 
         :param gateway: IPv4 address for the gateway
-        :returns: True or CX
         """
         self._if_gateway = validate.ipv4_address(gateway)
 
     @property
     def virt_bridge(self) -> str:
         """
-        virt_bridge property.
+        virt_bridge property. If set to ``<<inherit>>`` this will read the value from the setting "default_virt_bridge".
 
         :getter: Returns the value for ``virt_bridge``.
         :setter: Sets the value for the property ``virt_bridge``.
-        :return:
         """
+        if self._virt_bridge == enums.VALUE_INHERITED:
+            return self.__api.settings().default_virt_bridge
         return self._virt_bridge
 
     @virt_bridge.setter
@@ -383,15 +366,15 @@ def virt_bridge(self, bridge: str):
         """
         Setter for the virt_bridge of the NetworkInterface class.
 
-
-        :param bridge:
+        :param bridge: The new value for "virt_bridge".
         """
         if not isinstance(bridge, str):
             raise TypeError(
                 "Field virt_bridge of object NetworkInterface should be of type str!"
             )
         if bridge == "":
-            bridge = self.__api.settings().default_virt_bridge
+            self._virt_bridge = enums.VALUE_INHERITED
+            return
         self._virt_bridge = bridge
 
     @property
@@ -401,7 +384,6 @@ def interface_type(self) -> enums.NetworkInterfaceType:
 
         :getter: Returns the value for ``interface_type``.
         :setter: Sets the value for the property ``interface_type``.
-        :return:
         """
         return self._interface_type
 
@@ -447,7 +429,6 @@ def interface_master(self) -> str:
 
         :getter: Returns the value for ``interface_master``.
         :setter: Sets the value for the property ``interface_master``.
-        :return:
         """
         return self._interface_master
 
@@ -456,8 +437,7 @@ def interface_master(self, interface_master: str):
         """
         Setter for the interface_master of the NetworkInterface class.
 
-
-        :param interface_master:
+        :param interface_master: The new interface master.
         """
         if not isinstance(interface_master, str):
             raise TypeError(
@@ -472,7 +452,6 @@ def bonding_opts(self) -> str:
 
         :getter: Returns the value for ``bonding_opts``.
         :setter: Sets the value for the property ``bonding_opts``.
-        :return:
         """
         return self._bonding_opts
 
@@ -481,8 +460,7 @@ def bonding_opts(self, bonding_opts: str):
         """
         Setter for the bonding_opts of the NetworkInterface class.
 
-
-        :param bonding_opts:
+        :param bonding_opts: The new bonding options for the interface.
         """
         if not isinstance(bonding_opts, str):
             raise TypeError(
@@ -497,7 +475,6 @@ def bridge_opts(self) -> str:
 
         :getter: Returns the value for ``bridge_opts``.
         :setter: Sets the value for the property ``bridge_opts``.
-        :return:
         """
         return self._bridge_opts
 
@@ -506,8 +483,7 @@ def bridge_opts(self, bridge_opts: str):
         """
         Setter for the bridge_opts of the NetworkInterface class.
 
-
-        :param bridge_opts:
+        :param bridge_opts: The new bridge options to set for the interface.
         """
         if not isinstance(bridge_opts, str):
             raise TypeError(
@@ -522,7 +498,6 @@ def ipv6_address(self) -> str:
 
         :getter: Returns the value for ``ipv6_address``.
         :setter: Sets the value for the property ``ipv6_address``.
-        :return:
         """
         return self._ipv6_address
 
@@ -532,7 +507,7 @@ def ipv6_address(self, address: str):
         Set IPv6 address on interface.
 
         :param address: IP address
-        :raises CX
+        :raises ValueError: IN case the IP is duplicated
         """
         address = validate.ipv6_address(address)
         if address != "" and not self.__api.settings().allow_duplicate_ips:
@@ -541,7 +516,10 @@ def ipv6_address(self, address: str):
                 if self in match.interfaces.values():
                     continue
                 else:
-                    raise ValueError("IPv6 address duplicated: %s" % address)
+                    raise ValueError(
+                        f'IPv6 address duplicate found "{address}". Object with the conflict has the name'
+                        f'"{match.name}"'
+                    )
         self._ipv6_address = address
 
     @property
@@ -551,7 +529,6 @@ def ipv6_prefix(self) -> str:
 
         :getter: Returns the value for ``ipv6_prefix``.
         :setter: Sets the value for the property ``ipv6_prefix``.
-        :return:
         """
         return self._ipv6_address
 
@@ -560,7 +537,7 @@ def ipv6_prefix(self, prefix: str):
         """
         Assign a IPv6 prefix
 
-        :param prefix:
+        :param prefix: The new IPv6 prefix for the interface.
         """
         if not isinstance(prefix, str):
             raise TypeError(
@@ -575,7 +552,6 @@ def ipv6_secondaries(self) -> list:
 
         :getter: Returns the value for ``ipv6_secondaries``.
         :setter: Sets the value for the property ``ipv6_secondaries``.
-        :return:
         """
         return self._ipv6_secondaries
 
@@ -584,8 +560,7 @@ def ipv6_secondaries(self, addresses: list):
         """
         Setter for the ipv6_secondaries of the NetworkInterface class.
 
-
-        :param addresses:
+        :param addresses: The new secondaries for the interface.
         """
         data = utils.input_string_or_list(addresses)
         secondaries = []
@@ -605,7 +580,6 @@ def ipv6_default_gateway(self) -> str:
 
         :getter: Returns the value for ``ipv6_default_gateway``.
         :setter: Sets the value for the property ``ipv6_default_gateway``.
-        :return:
         """
         return self._ipv6_default_gateway
 
@@ -614,8 +588,7 @@ def ipv6_default_gateway(self, address: str):
         """
         Setter for the ipv6_default_gateway of the NetworkInterface class.
 
-
-        :param address:
+        :param address: The new default gateway for the interface.
         """
         if not isinstance(address, str):
             raise TypeError(
@@ -633,7 +606,6 @@ def ipv6_static_routes(self) -> list:
 
         :getter: Returns the value for ``ipv6_static_routes``.
         :setter: Sets the value for the property `ipv6_static_routes``.
-        :return:
         """
         return self._ipv6_static_routes
 
@@ -642,8 +614,7 @@ def ipv6_static_routes(self, routes: list):
         """
         Setter for the ipv6_static_routes of the NetworkInterface class.
 
-
-        :param routes:
+        :param routes: The new static routes for the interface.
         """
         self._ipv6_static_routes = utils.input_string_or_list(routes)
 
@@ -654,7 +625,6 @@ def ipv6_mtu(self) -> str:
 
         :getter: Returns the value for ``ipv6_mtu``.
         :setter: Sets the value for the property ``ipv6_mtu``.
-        :return:
         """
         return self._ipv6_mtu
 
@@ -663,8 +633,7 @@ def ipv6_mtu(self, mtu: str):
         """
         Setter for the ipv6_mtu of the NetworkInterface class.
 
-
-        :param mtu:
+        :param mtu: The new IPv6 MTU for the interface.
         """
         if not isinstance(mtu, str):
             raise TypeError(
@@ -679,7 +648,6 @@ def mtu(self) -> str:
 
         :getter: Returns the value for ``mtu``.
         :setter: Sets the value for the property ``mtu``.
-        :return:
         """
         return self._mtu
 
@@ -688,8 +656,7 @@ def mtu(self, mtu: str):
         """
         Setter for the mtu of the NetworkInterface class.
 
-
-        :param mtu:
+        :param mtu: The new value for the mtu of the interface
         """
         if not isinstance(mtu, str):
             raise TypeError(
@@ -704,7 +671,6 @@ def connected_mode(self) -> bool:
 
         :getter: Returns the value for ``connected_mode``.
         :setter: Sets the value for the property ``connected_mode``.
-        :return:
         """
         return self._connected_mode
 
@@ -713,14 +679,14 @@ def connected_mode(self, truthiness: bool):
         """
         Setter for the connected_mode of the NetworkInterface class.
 
-
-        :param truthiness:
+        :param truthiness: The new value for connected mode of the interface.
         """
-        truthiness = utils.input_boolean(truthiness)
-        if not isinstance(truthiness, bool):
+        try:
+            truthiness = utils.input_boolean(truthiness)
+        except TypeError as e:
             raise TypeError(
                 "Field connected_mode of object NetworkInterface needs to be of type bool!"
-            )
+            ) from e
         self._connected_mode = truthiness
 
     def modify_interface(self, _dict: dict):


Test patch:
diff --git a/tests/items/network_interface_test.py b/tests/items/network_interface_test.py
new file mode 100644
--- /dev/null
+++ b/tests/items/network_interface_test.py
@@ -0,0 +1,614 @@
+import logging
+from ipaddress import AddressValueError
+
+import pytest
+
+from cobbler import enums
+from cobbler.items.system import NetworkInterface
+from tests.conftest import does_not_raise
+
+
+def test_network_interface_object_creation(cobbler_api):
+    # Arrange
+
+    # Act
+    interface = NetworkInterface(cobbler_api)
+
+    # Assert
+    assert isinstance(interface, NetworkInterface)
+
+
+def test_network_interface_to_dict(cobbler_api):
+    # Arrange
+    interface = NetworkInterface(cobbler_api)
+
+    # Act
+    result = interface.to_dict()
+
+    # Assert
+    assert isinstance(result, dict)
+    assert "logger" not in result
+    assert "api" not in result
+    assert len(result) == 23
+
+
+@pytest.mark.parametrize(
+    "input_dict,modified_field,expected_result,expect_logger_warning",
+    [
+        ({"dns_name": "host.example.com"}, "dns_name", "host.example.com", False),
+        ({"not_existing": "invalid"}, "dhcp_tag", "", True),
+    ],
+)
+def test_network_interface_from_dict(
+    caplog,
+    cobbler_api,
+    input_dict,
+    modified_field,
+    expected_result,
+    expect_logger_warning,
+):
+    # Arrange
+    caplog.set_level(logging.INFO)
+    interface = NetworkInterface(cobbler_api)
+
+    # Act
+    interface.from_dict(input_dict)
+
+    # Assert
+    assert getattr(interface, f"_{modified_field}") == expected_result
+    if expect_logger_warning:
+        assert "The following keys were ignored" in caplog.records[0].message
+    else:
+        assert len(caplog.records) == 0
+
+
+def test_serialize():
+    pass
+
+
+def test_deserialize():
+    pass
+
+
+@pytest.mark.parametrize(
+    "input_dhcp_tag,expected_result,expected_exception",
+    [
+        ("", "", does_not_raise()),
+        ("test", "test", does_not_raise()),
+        (0, "", pytest.raises(TypeError)),
+    ],
+)
+def test_dhcp_tag(cobbler_api, input_dhcp_tag, expected_result, expected_exception):
+    # Arrange
+    interface = NetworkInterface(cobbler_api)
+
+    # Act
+    with expected_exception:
+        interface.dhcp_tag = input_dhcp_tag
+
+        # Assert
+        assert isinstance(interface.dhcp_tag, str)
+        assert interface.dhcp_tag == expected_result
+
+
+def test_cnames(cobbler_api):
+    # Arrange
+    interface = NetworkInterface(cobbler_api)
+
+    # Act
+    interface.cnames = []
+
+    # Assert
+    assert isinstance(interface.cnames, list)
+    assert interface.cnames == []
+
+
+def test_static_routes(cobbler_api):
+    # Arrange
+    interface = NetworkInterface(cobbler_api)
+
+    # Act
+    interface.static_routes = []
+
+    # Assert
+    assert isinstance(interface.static_routes, list)
+    assert interface.static_routes == []
+
+
+@pytest.mark.parametrize(
+    "input_static,expected_result,expected_exception",
+    [
+        ("", False, does_not_raise()),
+        (0, False, does_not_raise()),
+        (1, True, does_not_raise()),
+        ([], "", pytest.raises(TypeError)),
+    ],
+)
+def test_static(cobbler_api, input_static, expected_result, expected_exception):
+    # Arrange
+    interface = NetworkInterface(cobbler_api)
+
+    # Act
+    with expected_exception:
+        interface.static = input_static
+
+        # Assert
+        assert isinstance(interface.static, bool)
+        assert interface.static is expected_result
+
+
+@pytest.mark.parametrize(
+    "input_management,expected_result,expected_exception",
+    [
+        ("", False, does_not_raise()),
+        (0, False, does_not_raise()),
+        (1, True, does_not_raise()),
+        ([], "", pytest.raises(TypeError)),
+    ],
+)
+def test_management(cobbler_api, input_management, expected_result, expected_exception):
+    # Arrange
+    interface = NetworkInterface(cobbler_api)
+
+    # Act
+    with expected_exception:
+        interface.management = input_management
+
+        # Assert
+        assert isinstance(interface.management, bool)
+        assert interface.management is expected_result
+
+
+@pytest.mark.parametrize(
+    "input_dns_name,expected_result,expected_exception",
+    [
+        ("", "", does_not_raise()),
+        ("host.example.org", "host.example.org", does_not_raise()),
+        ("duplicate.example.org", "", pytest.raises(ValueError)),
+    ],
+)
+def test_dns_name(
+    cobbler_api,
+    create_distro,
+    create_profile,
+    create_system,
+    input_dns_name,
+    expected_result,
+    expected_exception,
+):
+    # Arrange
+    distro = create_distro()
+    profile = create_profile(distro.name)
+    system = create_system(profile.name)
+    system.interfaces["default"].dns_name = "duplicate.example.org"
+    cobbler_api.add_system(system)
+    interface = NetworkInterface(cobbler_api)
+
+    # Act
+    with expected_exception:
+        # TODO: Test matching self
+        interface.dns_name = input_dns_name
+
+        # Assert
+        assert isinstance(interface.dns_name, str)
+        assert interface.dns_name == expected_result
+
+
+@pytest.mark.parametrize(
+    "input_mac,expected_result,expected_exception",
+    [
+        ("", "", does_not_raise()),
+        ("AA:BB", "", pytest.raises(ValueError)),
+        (0, "", pytest.raises(TypeError)),
+        ("random", "AA:BB:CC:DD:EE:FF", does_not_raise()),
+        ("AA:AA:AA:AA:AA:AA", "", pytest.raises(ValueError)),
+    ],
+)
+def test_mac_address(
+    mocker,
+    cobbler_api,
+    create_distro,
+    create_profile,
+    create_system,
+    input_mac,
+    expected_result,
+    expected_exception,
+):
+    # Arrange
+    distro = create_distro()
+    profile = create_profile(distro.name)
+    system = create_system(profile.name)
+    system.interfaces["default"].mac_address = "AA:AA:AA:AA:AA:AA"
+    cobbler_api.add_system(system)
+    mocker.patch("cobbler.utils.get_random_mac", return_value="AA:BB:CC:DD:EE:FF")
+    interface = NetworkInterface(cobbler_api)
+
+    # Act
+    with expected_exception:
+        # TODO: match self
+        interface.mac_address = input_mac
+
+        # Assert
+        assert isinstance(interface.mac_address, str)
+        assert interface.mac_address == expected_result
+
+
+def test_netmask(cobbler_api):
+    # Arrange
+    interface = NetworkInterface(cobbler_api)
+
+    # Act
+    interface.netmask = ""
+
+    # Assert
+    assert isinstance(interface.netmask, str)
+    assert interface.netmask == ""
+
+
+def test_if_gateway(cobbler_api):
+    # Arrange
+    interface = NetworkInterface(cobbler_api)
+
+    # Act
+    interface.if_gateway = ""
+
+    # Assert
+    assert isinstance(interface.if_gateway, str)
+    assert interface.if_gateway == ""
+
+
+@pytest.mark.parametrize(
+    "input_virt_bridge,expected_result,expected_exception",
+    [
+        ("", "xenbr0", does_not_raise()),
+        ("<<inherit>>", "xenbr0", does_not_raise()),
+        ("test", "test", does_not_raise()),
+        (0, "", pytest.raises(TypeError)),
+    ],
+)
+def test_virt_bridge(
+    cobbler_api, input_virt_bridge, expected_result, expected_exception
+):
+    # Arrange
+    interface = NetworkInterface(cobbler_api)
+
+    # Act
+    with expected_exception:
+        interface.virt_bridge = input_virt_bridge
+
+        # Assert
+        assert isinstance(interface.virt_bridge, str)
+        assert interface.virt_bridge == expected_result
+
+
+@pytest.mark.parametrize(
+    "input_interface_type,expected_result,expected_exception",
+    [
+        ([], enums.NetworkInterfaceType.NA, pytest.raises(TypeError)),
+        (0, enums.NetworkInterfaceType.NA, does_not_raise()),
+        (100, enums.NetworkInterfaceType.NA, pytest.raises(ValueError)),
+        ("INVALID", enums.NetworkInterfaceType.NA, pytest.raises(ValueError)),
+        # TODO: Create test for last ValueError
+        ("NA", enums.NetworkInterfaceType.NA, does_not_raise()),
+        ("na", enums.NetworkInterfaceType.NA, does_not_raise()),
+    ],
+)
+def test_interface_type(
+    cobbler_api, input_interface_type, expected_result, expected_exception
+):
+    # Arrange
+    interface = NetworkInterface(cobbler_api)
+
+    # Act
+    with expected_exception:
+        interface.interface_type = input_interface_type
+
+        # Assert
+        assert isinstance(interface.interface_type, enums.NetworkInterfaceType)
+        assert interface.interface_type == expected_result
+
+
+@pytest.mark.parametrize(
+    "input_interface_master,expected_result,expected_exception",
+    [
+        ("", "", does_not_raise()),
+        (0, "", pytest.raises(TypeError)),
+    ],
+)
+def test_interface_master(
+    cobbler_api, input_interface_master, expected_result, expected_exception
+):
+    # Arrange
+    interface = NetworkInterface(cobbler_api)
+
+    # Act
+    with expected_exception:
+        interface.interface_master = input_interface_master
+
+        # Assert
+        assert isinstance(interface.interface_master, str)
+        assert interface.interface_master == expected_result
+
+
+@pytest.mark.parametrize(
+    "input_bonding_opts,expected_result,expected_exception",
+    [
+        ("", "", does_not_raise()),
+        (0, "", pytest.raises(TypeError)),
+    ],
+)
+def test_bonding_opts(
+    cobbler_api, input_bonding_opts, expected_result, expected_exception
+):
+    # Arrange
+    interface = NetworkInterface(cobbler_api)
+
+    # Act
+    with expected_exception:
+        interface.bonding_opts = input_bonding_opts
+
+        # Assert
+        assert isinstance(interface.bonding_opts, str)
+        assert interface.bonding_opts == expected_result
+
+
+@pytest.mark.parametrize(
+    "input_bridge_opts,expected_result,expected_exception",
+    [
+        ("", "", does_not_raise()),
+        (0, "", pytest.raises(TypeError)),
+    ],
+)
+def test_bridge_opts(
+    cobbler_api, input_bridge_opts, expected_result, expected_exception
+):
+    # Arrange
+    interface = NetworkInterface(cobbler_api)
+
+    # Act
+    with expected_exception:
+        interface.bridge_opts = input_bridge_opts
+
+        # Assert
+        assert isinstance(interface.bridge_opts, str)
+        assert interface.bridge_opts == expected_result
+
+
+@pytest.mark.parametrize(
+    "input_ip_address,expected_result,expected_exception",
+    [
+        ("", "", does_not_raise()),
+        ("172.30.0.1", "172.30.0.1", does_not_raise()),
+        ("172.30.0.2", "", pytest.raises(ValueError)),
+    ],
+)
+def test_ip_address(
+    cobbler_api,
+    create_distro,
+    create_profile,
+    create_system,
+    input_ip_address,
+    expected_result,
+    expected_exception,
+):
+    # Arrange
+    distro = create_distro()
+    profile = create_profile(distro.name)
+    system = create_system(profile.name)
+    system.interfaces["default"].ip_address = "172.30.0.2"
+    cobbler_api.add_system(system)
+    interface = NetworkInterface(cobbler_api)
+
+    # Act
+    with expected_exception:
+        # TODO: Match self in loop
+        interface.ip_address = input_ip_address
+
+        # Assert
+        assert isinstance(interface.ip_address, str)
+        assert interface.ip_address == expected_result
+
+
+@pytest.mark.parametrize(
+    "input_address,expected_result,expected_exception",
+    [
+        ("", "", does_not_raise()),
+        ("2001:db8:3c4d::1", "2001:db8:3c4d::1", does_not_raise()),
+        ("2001:db8:3c4d::2", "", pytest.raises(ValueError)),
+    ],
+)
+def test_ipv6_address(
+    cobbler_api,
+    create_distro,
+    create_profile,
+    create_system,
+    input_address,
+    expected_result,
+    expected_exception,
+):
+    # Arrange
+    distro = create_distro()
+    profile = create_profile(distro.name)
+    system = create_system(profile.name)
+    system.interfaces["default"].ipv6_address = "2001:db8:3c4d::2"
+    cobbler_api.add_system(system)
+    interface = NetworkInterface(cobbler_api)
+
+    # Act
+    with expected_exception:
+        # TODO: match self
+        interface.ipv6_address = input_address
+
+        # Assert
+        assert isinstance(interface.ipv6_address, str)
+        assert interface.ipv6_address == expected_result
+
+
+@pytest.mark.parametrize(
+    "input_ipv6_prefix,expected_result,expected_exception",
+    [
+        ("", "", does_not_raise()),
+        (0, "", pytest.raises(TypeError)),
+    ],
+)
+def test_ipv6_prefix(
+    cobbler_api, input_ipv6_prefix, expected_result, expected_exception
+):
+    # Arrange
+    interface = NetworkInterface(cobbler_api)
+
+    # Act
+    with expected_exception:
+        interface.ipv6_prefix = input_ipv6_prefix
+
+        # Assert
+        assert isinstance(interface.ipv6_prefix, str)
+        assert interface.ipv6_prefix == expected_result
+
+
+@pytest.mark.parametrize(
+    "input_secondaries,expected_result,expected_exception",
+    [
+        ([""], [""], does_not_raise()),
+        (["::1"], ["::1"], does_not_raise()),
+        ("invalid", [], pytest.raises(AddressValueError)),
+    ],
+)
+def test_ipv6_secondaries(
+    cobbler_api, input_secondaries, expected_result, expected_exception
+):
+    # Arrange
+    interface = NetworkInterface(cobbler_api)
+
+    # Act
+    with expected_exception:
+        interface.ipv6_secondaries = input_secondaries
+
+        # Assert
+        assert isinstance(interface.ipv6_secondaries, list)
+        assert interface.ipv6_secondaries == expected_result
+
+
+@pytest.mark.parametrize(
+    "input_address,expected_result,expected_exception",
+    [
+        ("", "", does_not_raise()),
+        ("::1", "::1", does_not_raise()),
+        (None, "", pytest.raises(TypeError)),
+        ("invalid", "", pytest.raises(AddressValueError)),
+    ],
+)
+def test_ipv6_default_gateway(
+    cobbler_api, input_address, expected_result, expected_exception
+):
+    # Arrange
+    interface = NetworkInterface(cobbler_api)
+
+    # Act
+    with expected_exception:
+        interface.ipv6_default_gateway = input_address
+
+        # Assert
+        assert isinstance(interface.ipv6_default_gateway, str)
+        assert interface.ipv6_default_gateway == expected_result
+
+
+def test_ipv6_static_routes(cobbler_api):
+    # Arrange
+    interface = NetworkInterface(cobbler_api)
+
+    # Act
+    interface.ipv6_static_routes = []
+
+    # Assert
+    assert isinstance(interface.ipv6_static_routes, list)
+    assert interface.ipv6_static_routes == []
+
+
+@pytest.mark.parametrize(
+    "input_ipv6_mtu,expected_result,expected_exception",
+    [
+        ("", "", does_not_raise()),
+        (0, "", pytest.raises(TypeError)),
+    ],
+)
+def test_ipv6_mtu(cobbler_api, input_ipv6_mtu, expected_result, expected_exception):
+    # Arrange
+    interface = NetworkInterface(cobbler_api)
+
+    # Act
+    with expected_exception:
+        interface.ipv6_mtu = input_ipv6_mtu
+
+        # Assert
+        assert isinstance(interface.ipv6_mtu, str)
+        assert interface.ipv6_mtu == expected_result
+
+
+@pytest.mark.parametrize(
+    "input_mtu,expected_result,expected_exception",
+    [
+        ("", "", does_not_raise()),
+        (0, "", pytest.raises(TypeError)),
+    ],
+)
+def test_mtu(cobbler_api, input_mtu, expected_result, expected_exception):
+    # Arrange
+    interface = NetworkInterface(cobbler_api)
+
+    # Act
+    with expected_exception:
+        interface.mtu = input_mtu
+
+        # Assert
+        assert isinstance(interface.mtu, str)
+        assert interface.mtu == expected_result
+
+
+@pytest.mark.parametrize(
+    "input_connected_mode,expected_result,expected_exception",
+    [
+        ("", False, does_not_raise()),
+        (0, False, does_not_raise()),
+        (1, True, does_not_raise()),
+        ([], "", pytest.raises(TypeError)),
+    ],
+)
+def test_connected_mode(
+    cobbler_api, input_connected_mode, expected_result, expected_exception
+):
+    # Arrange
+    interface = NetworkInterface(cobbler_api)
+
+    # Act
+    with expected_exception:
+        interface.connected_mode = input_connected_mode
+
+        # Assert
+        assert isinstance(interface.connected_mode, bool)
+        assert interface.connected_mode is expected_result
+
+
+@pytest.mark.parametrize(
+    "input_modify_interface,expected_modified_field,expected_value,expected_exception",
+    [
+        ({}, "mtu", "", does_not_raise()),
+        ({"mtu-eth0": "test"}, "mtu", "test", does_not_raise()),
+    ],
+)
+def test_modify_interface(
+    cobbler_api,
+    input_modify_interface,
+    expected_modified_field,
+    expected_value,
+    expected_exception,
+):
+    # Arrange
+    interface = NetworkInterface(cobbler_api)
+
+    # Act
+    with expected_exception:
+        interface.modify_interface(input_modify_interface)
+
+        # Assert
+        assert getattr(interface, expected_modified_field) == expected_value
diff --git a/tests/items/system_test.py b/tests/items/system_test.py
--- a/tests/items/system_test.py
+++ b/tests/items/system_test.py
@@ -690,306 +690,3 @@ def test_is_management_supported(
 
     # Assert
     assert result is expected_result
-
-
-############################################################################################
-
-
-def test_network_interface_object_creation(cobbler_api):
-    # Arrange
-
-    # Act
-    interface = NetworkInterface(cobbler_api)
-
-    # Assert
-    assert isinstance(interface, NetworkInterface)
-
-
-def test_network_interface_to_dict(cobbler_api):
-    # Arrange
-    interface = NetworkInterface(cobbler_api)
-
-    # Act
-    result = interface.to_dict()
-
-    # Assert
-    assert isinstance(result, dict)
-    assert "logger" not in result
-    assert "api" not in result
-    assert len(result) == 23
-
-
-def test_network_interface_from_dict(cobbler_api):
-    # Arrange
-    interface = NetworkInterface(cobbler_api)
-    intf_dict = interface.to_dict()
-
-    # Act
-    interface.from_dict(intf_dict)
-
-    # Assert
-    assert True
-
-
-def test_dhcp_tag(cobbler_api):
-    # Arrange
-    interface = NetworkInterface(cobbler_api)
-
-    # Act
-    interface.dhcp_tag = ""
-
-    # Assert
-    assert isinstance(interface.dhcp_tag, str)
-    assert interface.dhcp_tag == ""
-
-
-def test_cnames(cobbler_api):
-    # Arrange
-    interface = NetworkInterface(cobbler_api)
-
-    # Act
-    interface.cnames = []
-
-    # Assert
-    assert isinstance(interface.cnames, list)
-    assert interface.cnames == []
-
-
-def test_static_routes(cobbler_api):
-    # Arrange
-    interface = NetworkInterface(cobbler_api)
-
-    # Act
-    interface.static_routes = []
-
-    # Assert
-    assert isinstance(interface.static_routes, list)
-    assert interface.static_routes == []
-
-
-def test_static(cobbler_api):
-    # Arrange
-    interface = NetworkInterface(cobbler_api)
-
-    # Act
-    interface.static = True
-
-    # Assert
-    assert isinstance(interface.static, bool)
-    assert interface.static is True
-
-
-def test_management(cobbler_api):
-    # Arrange
-    interface = NetworkInterface(cobbler_api)
-
-    # Act
-    interface.management = True
-
-    # Assert
-    assert isinstance(interface.management, bool)
-    assert interface.management is True
-
-
-def test_dns_name(cobbler_api):
-    # Arrange
-    interface = NetworkInterface(cobbler_api)
-
-    # Act
-    interface.dns_name = ""
-
-    # Assert
-    assert isinstance(interface.dns_name, str)
-    assert interface.dns_name == ""
-
-
-def test_mac_address(cobbler_api):
-    # Arrange
-    interface = NetworkInterface(cobbler_api)
-
-    # Act
-    interface.mac_address = ""
-
-    # Assert
-    assert isinstance(interface.mac_address, str)
-    assert interface.mac_address == ""
-
-
-def test_netmask(cobbler_api):
-    # Arrange
-    interface = NetworkInterface(cobbler_api)
-
-    # Act
-    interface.netmask = ""
-
-    # Assert
-    assert isinstance(interface.netmask, str)
-    assert interface.netmask == ""
-
-
-def test_if_gateway(cobbler_api):
-    # Arrange
-    interface = NetworkInterface(cobbler_api)
-
-    # Act
-    interface.if_gateway = ""
-
-    # Assert
-    assert isinstance(interface.if_gateway, str)
-    assert interface.if_gateway == ""
-
-
-def test_virt_bridge(cobbler_api):
-    # Arrange
-    interface = NetworkInterface(cobbler_api)
-
-    # Act
-    interface.virt_bridge = ""
-
-    # Assert
-    assert isinstance(interface.virt_bridge, str)
-    assert interface.virt_bridge == "xenbr0"
-
-
-def test_interface_type(cobbler_api):
-    # Arrange
-    interface = NetworkInterface(cobbler_api)
-
-    # Act
-    interface.interface_type = enums.NetworkInterfaceType.NA
-
-    # Assert
-    assert isinstance(interface.interface_type, enums.NetworkInterfaceType)
-    assert interface.interface_type == enums.NetworkInterfaceType.NA
-
-
-def test_interface_master(cobbler_api):
-    # Arrange
-    interface = NetworkInterface(cobbler_api)
-
-    # Act
-    interface.interface_master = ""
-
-    # Assert
-    assert isinstance(interface.interface_master, str)
-    assert interface.interface_master == ""
-
-
-def test_bonding_opts(cobbler_api):
-    # Arrange
-    interface = NetworkInterface(cobbler_api)
-
-    # Act
-    interface.bonding_opts = ""
-
-    # Assert
-    assert isinstance(interface.bonding_opts, str)
-    assert interface.bonding_opts == ""
-
-
-def test_bridge_opts(cobbler_api):
-    # Arrange
-    interface = NetworkInterface(cobbler_api)
-
-    # Act
-    interface.bridge_opts = ""
-
-    # Assert
-    assert isinstance(interface.bridge_opts, str)
-    assert interface.bridge_opts == ""
-
-
-def test_ipv6_address(cobbler_api):
-    # Arrange
-    interface = NetworkInterface(cobbler_api)
-
-    # Act
-    interface.ipv6_address = ""
-
-    # Assert
-    assert isinstance(interface.ipv6_address, str)
-    assert interface.ipv6_address == ""
-
-
-def test_ipv6_prefix(cobbler_api):
-    # Arrange
-    interface = NetworkInterface(cobbler_api)
-
-    # Act
-    interface.ipv6_prefix = ""
-
-    # Assert
-    assert isinstance(interface.ipv6_prefix, str)
-    assert interface.ipv6_prefix == ""
-
-
-def test_ipv6_secondaries(cobbler_api):
-    # Arrange
-    interface = NetworkInterface(cobbler_api)
-
-    # Act
-    interface.ipv6_secondaries = []
-
-    # Assert
-    assert isinstance(interface.ipv6_secondaries, list)
-    assert interface.ipv6_secondaries == []
-
-
-def test_ipv6_default_gateway(cobbler_api):
-    # Arrange
-    interface = NetworkInterface(cobbler_api)
-
-    # Act
-    interface.ipv6_default_gateway = ""
-
-    # Assert
-    assert isinstance(interface.ipv6_default_gateway, str)
-    assert interface.ipv6_default_gateway == ""
-
-
-def test_ipv6_static_routes(cobbler_api):
-    # Arrange
-    interface = NetworkInterface(cobbler_api)
-
-    # Act
-    interface.ipv6_static_routes = []
-
-    # Assert
-    assert isinstance(interface.ipv6_static_routes, list)
-    assert interface.ipv6_static_routes == []
-
-
-def test_ipv6_mtu(cobbler_api):
-    # Arrange
-    interface = NetworkInterface(cobbler_api)
-
-    # Act
-    interface.ipv6_mtu = ""
-
-    # Assert
-    assert isinstance(interface.ipv6_mtu, str)
-    assert interface.ipv6_mtu == ""
-
-
-def test_mtu(cobbler_api):
-    # Arrange
-    interface = NetworkInterface(cobbler_api)
-
-    # Act
-    interface.mtu = ""
-
-    # Assert
-    assert isinstance(interface.mtu, str)
-    assert interface.mtu == ""
-
-
-def test_connected_mode(cobbler_api):
-    # Arrange
-    interface = NetworkInterface(cobbler_api)
-
-    # Act
-    interface.connected_mode = True
-
-    # Assert
-    assert isinstance(interface.connected_mode, bool)
-    assert interface.connected_mode is True
diff --git a/tests/validate_test.py b/tests/validate_test.py
--- a/tests/validate_test.py
+++ b/tests/validate_test.py
@@ -1,4 +1,5 @@
 import uuid
+from ipaddress import AddressValueError, NetmaskValueError
 
 import pytest
 
@@ -6,6 +7,46 @@
 from tests.conftest import does_not_raise
 
 
+@pytest.mark.parametrize(
+    "input_dnsname,expected_result,expected_exception",
+    [
+        (0, "", pytest.raises(TypeError)),
+        ("", "", does_not_raise()),
+        ("host", "host", does_not_raise()),
+        ("host.cobbler.org", "host.cobbler.org", does_not_raise()),
+    ],
+)
+def test_hostname(input_dnsname, expected_result, expected_exception):
+    # Arrange
+
+    # Act
+    with expected_exception:
+        result = validate.hostname(input_dnsname)
+
+        # Assert
+        assert result == expected_result
+
+
+@pytest.mark.parametrize(
+    "input_addr,expected_result,expected_exception",
+    [
+        ("", "", does_not_raise()),
+        ("192.168.1.5", "192.168.1.5", does_not_raise()),
+        ("my-invalid-ip", "", pytest.raises(AddressValueError)),
+        ("255.255.255.0", "", pytest.raises(NetmaskValueError)),
+        (0, "", pytest.raises(TypeError)),
+    ],
+)
+def test_ipv4_address(input_addr, expected_result, expected_exception):
+    # Arrange
+    # Act
+    with expected_exception:
+        result = validate.ipv4_address(input_addr)
+
+        # Assert
+        assert result == expected_result
+
+
 def test_validate_os_version():
     # Arrange
     utils.load_signatures("/var/lib/cobbler/distro_signatures.json")