Repository: cobbler/cobbler

Issue:
Expose input converters to the API
### Is your feature request related to a problem?

It seems that languages like Java cannot reimplement our `input_string_or_dict` functions. Theses functions are important so that users may enter their kernel options in a way they would enter it in a real command line.

### Provide a detailed description of the proposed feature

The functionality of all `input_string_or*` functions should be available via both XML-RPC or HTTP API.

### Alternatives you've considered

None

### Additional information

Found during an L3 at SUSE Manager.



Discussion:

Resolving patch:
diff --git a/cobbler/api.py b/cobbler/api.py
--- a/cobbler/api.py
+++ b/cobbler/api.py
@@ -2355,3 +2355,53 @@ def mkloaders(self):
         """
         action = mkloaders.MkLoaders(self)
         action.run()
+
+    # ==========================================================================
+
+    def input_string_or_list_no_inherit(
+        self, options: Optional[Union[str, list]]
+    ) -> list:
+        """
+        .. seealso:: :func:`~cobbler.utils.input_converters.input_string_or_list_no_inherit`
+        """
+        return input_converters.input_string_or_list_no_inherit(options)
+
+    def input_string_or_list(
+        self, options: Optional[Union[str, list]]
+    ) -> Union[list, str]:
+        """
+        .. seealso:: :func:`~cobbler.utils.input_converters.input_string_or_list`
+        """
+        return input_converters.input_string_or_list(options)
+
+    def input_string_or_dict(
+        self, options: Union[str, list, dict], allow_multiples=True
+    ) -> Union[str, dict]:
+        """
+        .. seealso:: :func:`~cobbler.utils.input_converters.input_string_or_dict`
+        """
+        return input_converters.input_string_or_dict(
+            options, allow_multiples=allow_multiples
+        )
+
+    def input_string_or_dict_no_inherit(
+        self, options: Union[str, list, dict], allow_multiples=True
+    ) -> dict:
+        """
+        .. seealso:: :func:`~cobbler.utils.input_converters.input_string_or_dict_no_inherit`
+        """
+        return input_converters.input_string_or_dict_no_inherit(
+            options, allow_multiples=allow_multiples
+        )
+
+    def input_boolean(self, value: Union[str, bool, int]) -> bool:
+        """
+        .. seealso:: :func:`~cobbler.utils.input_converters.input_boolean`
+        """
+        return input_converters.input_boolean(value)
+
+    def input_int(self, value: Union[str, int, float]) -> int:
+        """
+        .. seealso:: :func:`~cobbler.utils.input_converters.input_int`
+        """
+        return input_converters.input_int(value)
diff --git a/cobbler/remote.py b/cobbler/remote.py
--- a/cobbler/remote.py
+++ b/cobbler/remote.py
@@ -31,7 +31,7 @@
 )
 from cobbler import tftpgen
 from cobbler import utils
-from cobbler.utils import input_converters, signatures
+from cobbler.utils import signatures
 from cobbler.cexceptions import CX
 from cobbler.validate import (
     validate_autoinstall_script_name,
@@ -2116,13 +2116,13 @@ def modify_setting(self, setting_name: str, value, token: str) -> int:
             elif isinstance(getattr(self.api.settings(), setting_name), int):
                 value = int(value)
             elif isinstance(getattr(self.api.settings(), setting_name), bool):
-                value = input_converters.input_boolean(value)
+                value = self.api.input_boolean(value)
             elif isinstance(getattr(self.api.settings(), setting_name), float):
                 value = float(value)
             elif isinstance(getattr(self.api.settings(), setting_name), list):
-                value = input_converters.input_string_or_list(value)
+                value = self.api.input_string_or_list(value)
             elif isinstance(getattr(self.api.settings(), setting_name), dict):
-                value = input_converters.input_string_or_dict(value)
+                value = self.api.input_string_or_dict(value)
             else:
                 self.logger.error(
                     "modify_setting(%s) - Wrong type for value", setting_name
@@ -2270,7 +2270,7 @@ def xapi_object_edit(
                     ] and attributes.get("in_place"):
                         details = self.get_item(object_type, object_name)
                         v2 = details[key]
-                        parsed_input = input_converters.input_string_or_dict(value)
+                        parsed_input = self.api.input_string_or_dict(value)
                         for (a, b) in list(parsed_input.items()):
                             if a.startswith("~") and len(a) > 1:
                                 del v2[a[1:]]
@@ -3958,6 +3958,52 @@ def clear_system_logs(self, object_id: str, token: str):
         self.api.clear_logs(obj)
         return True
 
+    def input_string_or_list_no_inherit(
+        self, options: Optional[Union[str, list]]
+    ) -> list:
+        """
+        .. seealso:: :func:`~cobbler.api.CobblerAPI.input_string_or_list_no_inherit`
+        """
+        return self.api.input_string_or_list_no_inherit(options)
+
+    def input_string_or_list(
+        self, options: Optional[Union[str, list]]
+    ) -> Union[list, str]:
+        """
+        .. seealso:: :func:`~cobbler.api.CobblerAPI.input_string_or_list`
+        """
+        return self.api.input_string_or_list(options)
+
+    def input_string_or_dict(
+        self, options: Union[str, list, dict], allow_multiples=True
+    ) -> Union[str, dict]:
+        """
+        .. seealso:: :func:`~cobbler.api.CobblerAPI.input_string_or_dict`
+        """
+        return self.api.input_string_or_dict(options, allow_multiples=allow_multiples)
+
+    def input_string_or_dict_no_inherit(
+        self, options: Union[str, list, dict], allow_multiples=True
+    ) -> dict:
+        """
+        .. seealso:: :func:`~cobbler.api.CobblerAPI.input_string_or_dict_no_inherit`
+        """
+        return self.api.input_string_or_dict_no_inherit(
+            options, allow_multiples=allow_multiples
+        )
+
+    def input_boolean(self, value: Union[str, bool, int]) -> bool:
+        """
+        .. seealso:: :func:`~cobbler.api.CobblerAPI.input_boolean`
+        """
+        return self.api.input_boolean(value)
+
+    def input_int(self, value: Union[str, int, float]) -> int:
+        """
+        .. seealso:: :func:`~cobbler.api.CobblerAPI.input_int`
+        """
+        return self.api.input_int(value)
+
 
 # *********************************************************************************
 


Test patch:
diff --git a/tests/api/input_converters.py b/tests/api/input_converters.py
new file mode 100644
--- /dev/null
+++ b/tests/api/input_converters.py
@@ -0,0 +1,132 @@
+import pytest
+
+from tests.conftest import does_not_raise
+
+
+@pytest.mark.parametrize(
+    "input_options,expected_result,expected_exception", [([], [], does_not_raise())]
+)
+def test_input_string_or_list_no_inherit(
+    cobbler_api, input_options, expected_result, expected_exception
+):
+    # Arrange & Act
+    with expected_exception:
+        result = cobbler_api.input_string_or_list_no_inherit(input_options)
+
+        # Assert
+        assert result == expected_result
+
+
+@pytest.mark.parametrize(
+    "input_options,expected_result,expected_exception",
+    [
+        ("<<inherit>>", "<<inherit>>", does_not_raise()),
+        ("delete", [], does_not_raise()),
+        (["test"], ["test"], does_not_raise()),
+        ("my_test", ["my_test"], does_not_raise()),
+        ("my_test my_test", ["my_test", "my_test"], does_not_raise()),
+        (5, None, pytest.raises(TypeError)),
+    ],
+)
+def test_input_string_or_list(
+    cobbler_api, input_options, expected_result, expected_exception
+):
+    # Arrange & Act
+    with expected_exception:
+        result = cobbler_api.input_string_or_list(input_options)
+
+        # Assert
+        assert result == expected_result
+
+
+@pytest.mark.parametrize(
+    "input_options,input_allow_multiples,expected_result,possible_exception",
+    [
+        ("<<inherit>>", True, "<<inherit>>", does_not_raise()),
+        ([""], True, None, pytest.raises(TypeError)),
+        ("a b=10 c=abc", True, {"a": None, "b": "10", "c": "abc"}, does_not_raise()),
+        ({"ab": 0}, True, {"ab": 0}, does_not_raise()),
+        (0, True, None, pytest.raises(TypeError)),
+    ],
+)
+def test_input_string_or_dict(
+    cobbler_api,
+    input_options,
+    input_allow_multiples,
+    expected_result,
+    possible_exception,
+):
+    # Arrange & Act
+    with possible_exception:
+        result = cobbler_api.input_string_or_dict(
+            input_options, allow_multiples=input_allow_multiples
+        )
+
+        # Assert
+        assert result == expected_result
+
+
+@pytest.mark.parametrize(
+    "input_options,input_allow_multiples,expected_result,expected_exception",
+    [({}, True, {}, does_not_raise())],
+)
+def test_input_string_or_dict_no_inherit(
+    cobbler_api,
+    input_options,
+    input_allow_multiples,
+    expected_result,
+    expected_exception,
+):
+    # Arrange & Act
+    with expected_exception:
+        result = cobbler_api.input_string_or_dict_no_inherit(
+            input_options, allow_multiples=input_allow_multiples
+        )
+
+        # Assert
+        assert result == expected_result
+
+
+@pytest.mark.parametrize(
+    "input_value,expected_exception,expected_result",
+    [
+        (True, does_not_raise(), True),
+        (1, does_not_raise(), True),
+        ("oN", does_not_raise(), True),
+        ("yEs", does_not_raise(), True),
+        ("Y", does_not_raise(), True),
+        ("Test", does_not_raise(), False),
+        (-5, does_not_raise(), False),
+        (0.5, pytest.raises(TypeError), False),
+    ],
+)
+def test_input_boolean(cobbler_api, input_value, expected_exception, expected_result):
+    # Arrange & Act
+    with expected_exception:
+        result = cobbler_api.input_boolean(input_value)
+
+        # Assert
+        assert result == expected_result
+
+
+@pytest.mark.parametrize(
+    "input_value,expected_exception,expected_result",
+    [
+        (True, does_not_raise(), 1),
+        (1, does_not_raise(), 1),
+        ("1", does_not_raise(), 1),
+        ("text", pytest.raises(TypeError), 1),
+        ("5.0", pytest.raises(TypeError), 0),
+        ([], pytest.raises(TypeError), 0),
+        ({}, pytest.raises(TypeError), 0),
+        (-5, does_not_raise(), -5),
+        (0.5, does_not_raise(), 0),
+    ],
+)
+def test_input_int(cobbler_api, input_value, expected_exception, expected_result):
+    # Arrange & Act
+    with expected_exception:
+        result = cobbler_api.input_int(input_value)
+
+        # Assert
+        assert result == expected_result
diff --git a/tests/utils/input_converters_test.py b/tests/utils/input_converters_test.py
--- a/tests/utils/input_converters_test.py
+++ b/tests/utils/input_converters_test.py
@@ -6,16 +6,23 @@
 
 
 @pytest.mark.parametrize(
-    "test_input,expected_result", [("<<inherit>>", "<<inherit>>"), ("delete", [])]
+    "test_input,expected_result,expected_exception",
+    [
+        ("<<inherit>>", "<<inherit>>", does_not_raise()),
+        ("delete", [], does_not_raise()),
+        (["test"], ["test"], does_not_raise()),
+        ("my_test", ["my_test"], does_not_raise()),
+        ("my_test my_test", ["my_test", "my_test"], does_not_raise()),
+        (5, None, pytest.raises(TypeError)),
+    ],
 )
-def test_input_string_or_list(test_input, expected_result):
-    # Arrange
-
-    # Act
-    result = input_converters.input_string_or_list(test_input)
+def test_input_string_or_list(test_input, expected_result, expected_exception):
+    # Arrange & Act
+    with expected_exception:
+        result = input_converters.input_string_or_list(test_input)
 
-    # Assert
-    assert expected_result == result
+        # Assert
+        assert expected_result == result
 
 
 @pytest.mark.parametrize(
diff --git a/tests/xmlrpcapi/input_converters.py b/tests/xmlrpcapi/input_converters.py
new file mode 100644
--- /dev/null
+++ b/tests/xmlrpcapi/input_converters.py
@@ -0,0 +1,136 @@
+import pytest
+
+from tests.conftest import does_not_raise
+
+
+@pytest.mark.parametrize(
+    "input_options,expected_result,expected_exception", [([], [], does_not_raise())]
+)
+def test_input_string_or_list_no_inherit(
+    remote, input_options, expected_result, expected_exception
+):
+    """
+    Test: check Cobbler status
+    """
+    # Arrange & Act
+    with expected_exception:
+        result = remote.input_string_or_list_no_inherit(input_options)
+
+        # Assert
+        assert result == expected_result
+
+
+@pytest.mark.parametrize(
+    "test_input,expected_result,expected_excpetion",
+    [
+        ("<<inherit>>", "<<inherit>>", does_not_raise()),
+        ("delete", [], does_not_raise()),
+        (["test"], ["test"], does_not_raise()),
+        ("my_test", ["my_test"], does_not_raise()),
+        ("my_test my_test", ["my_test", "my_test"], does_not_raise()),
+        (5, None, pytest.raises(TypeError)),
+    ],
+)
+def test_input_string_or_list(remote, test_input, expected_result, expected_excpetion):
+    """
+    Test: check Cobbler status
+    """
+    # Arrange & Act
+    with expected_excpetion:
+        result = remote.input_string_or_list(test_input)
+
+        # Assert
+        assert result == expected_result
+
+
+@pytest.mark.parametrize(
+    "testinput,expected_result,possible_exception",
+    [
+        ("<<inherit>>", "<<inherit>>", does_not_raise()),
+        ([""], None, pytest.raises(TypeError)),
+        ("a b=10 c=abc", {"a": None, "b": "10", "c": "abc"}, does_not_raise()),
+        ({"ab": 0}, {"ab": 0}, does_not_raise()),
+        (0, None, pytest.raises(TypeError)),
+    ],
+)
+def test_input_string_or_dict(remote, testinput, expected_result, possible_exception):
+    """
+    Test: check Cobbler status
+    """
+    # Arrange & Act
+    with possible_exception:
+        result = remote.input_string_or_dict(testinput)
+
+        # Assert
+        assert result == expected_result
+
+
+@pytest.mark.parametrize(
+    "input_options,input_allow_multiples,expected_result,expected_exception",
+    [({}, True, {}, does_not_raise())],
+)
+def test_input_string_or_dict_no_inherit(
+    remote, input_options, input_allow_multiples, expected_result, expected_exception
+):
+    """
+    Test: check Cobbler status
+    """
+    # Arrange & Act
+    with expected_exception:
+        result = remote.input_string_or_dict_no_inherit(
+            input_options, allow_multiples=input_allow_multiples
+        )
+
+        # Assert
+        assert result == expected_result
+
+
+@pytest.mark.parametrize(
+    "testinput,expected_exception,expected_result",
+    [
+        (True, does_not_raise(), True),
+        (1, does_not_raise(), True),
+        ("oN", does_not_raise(), True),
+        ("yEs", does_not_raise(), True),
+        ("Y", does_not_raise(), True),
+        ("Test", does_not_raise(), False),
+        (-5, does_not_raise(), False),
+        (0.5, pytest.raises(TypeError), False),
+    ],
+)
+def test_input_boolean(remote, testinput, expected_exception, expected_result):
+    """
+    Test: check Cobbler status
+    """
+    # Arrange & Act
+    with expected_exception:
+        result = remote.input_boolean(testinput)
+
+        # Assert
+        assert result == expected_result
+
+
+@pytest.mark.parametrize(
+    "testinput,expected_exception,expected_result",
+    [
+        (True, does_not_raise(), 1),
+        (1, does_not_raise(), 1),
+        ("1", does_not_raise(), 1),
+        ("text", pytest.raises(TypeError), 1),
+        ("5.0", pytest.raises(TypeError), 0),
+        ([], pytest.raises(TypeError), 0),
+        ({}, pytest.raises(TypeError), 0),
+        (-5, does_not_raise(), -5),
+        (0.5, does_not_raise(), 0),
+    ],
+)
+def test_input_int(remote, testinput, expected_exception, expected_result):
+    """
+    Test: check Cobbler status
+    """
+    # Arrange & Act
+    with expected_exception:
+        result = remote.input_int(testinput)
+
+        # Assert
+        assert result == expected_result