Repository: ansible-collections/community.general

Issue:
[RFE] ansible filter to/from ini
### Summary

Business case:
The existing ansible filters[1] to_json/from_json and to_yaml/from_yaml are helpful for some conditions. However, a number of templates/transforms/etc could be radically simplified if ansible had a to_ini and from_ini filter.

Technical requirements:
Add a to_ini/from_ini filter to lib/ansible/plugins/filter/core.py using the configparser module shipping with python.

[1] https://docs.ansible.com/ansible/latest/user_guide/playbooks_filters.html#formatting-data-yaml-and-json

### Issue Type

Feature Idea

### Component Name

Core

### Additional Information

<!--- Paste example playbooks or commands between quotes below -->
```yaml (paste below)
# This is a start, but there is design work the ansible folks will need to do:


try:
    from configparser import ConfigParser
except ImportError:
    from ConfigParser import SafeConfigParser as ConfigParser
import io

def to_ini(a, *args, **kw):
    '''Convert a dict into ini format'''
    if not isinstance(a, dict):
        raise AnsibleFilterError('type (%s) not permitted, only dict is supported' % type(a))
    config = ConfigParser(*args, **kw)
    try:
        config.read_dict(a)
    except AttributeError:
        try:
            for section in a.keys():
                config.add_section(section)
                # get keys in a determinstic order
                for option in sorted(a[section].keys()):
                    config.set(section, option, value)
        except Exception as e:
            raise AnsibleFilterError("to_ini - %s" % to_native(e), orig_exc=e)
    except Exception as e:
        raise AnsibleFilterError("to_ini - %s" % to_native(e), orig_exc=e)

    ini = io.StringIO()
    config.write(ini)

    return ini.getvalue()


def from_ini(data, *args, **kwargs):
    config = ConfigParser(*args, **kw)
    try:
        config.read_string(data)
    except AttributeError:
        try:
            ini = io.StringIO(data)
            config.readfp(ini)
        except Exception as e:
            raise AnsibleFilterError("from_ini - %s" % to_native(e), orig_exc=e)
    except Exception as e:
        raise AnsibleFilterError("from_ini - %s" % to_native(e), orig_exc=e)
    return config
```


### Code of Conduct

- [X] I agree to follow the Ansible Code of Conduct


Discussion:
Files identified in the description:
None

If these files are incorrect, please update the `component name` section of the description or use the `!component` bot command.

[click here for bot help](https://github.com/ansible/ansibullbot/blob/devel/ISSUE_HELP.md)
<!--- boilerplate: components_banner --->
For someone who wants to implement this feature - https://github.com/sivel/toiletwater/blob/master/plugins/filter/ini.py
cc @jatorcasso

Resolving patch:
diff --git a/plugins/filter/from_ini.py b/plugins/filter/from_ini.py
new file mode 100644
--- /dev/null
+++ b/plugins/filter/from_ini.py
@@ -0,0 +1,99 @@
+# -*- coding: utf-8 -*-
+
+# Copyright (c) 2023, Steffen Scheib <steffen@scheib.me>
+# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
+# SPDX-License-Identifier: GPL-3.0-or-later
+
+from __future__ import absolute_import, division, print_function
+
+DOCUMENTATION = r'''
+  name: from_ini
+  short_description: Converts INI text input into a dictionary
+  version_added: 8.2.0
+  author: Steffen Scheib (@sscheib)
+  description:
+    - Converts INI text input into a dictionary.
+  options:
+    _input:
+      description: A string containing an INI document.
+      type: string
+      required: true
+'''
+
+EXAMPLES = r'''
+  - name: Slurp an INI file
+    ansible.builtin.slurp:
+      src: /etc/rhsm/rhsm.conf
+    register: rhsm_conf
+
+  - name: Display the INI file as dictionary
+    ansible.builtin.debug:
+      var: rhsm_conf.content | b64decode | community.general.from_ini
+
+  - name: Set a new dictionary fact with the contents of the INI file
+    ansible.builtin.set_fact:
+      rhsm_dict: >-
+        {{
+            rhsm_conf.content | b64decode | community.general.from_ini
+        }}
+'''
+
+RETURN = '''
+  _value:
+    description: A dictionary representing the INI file.
+    type: dictionary
+'''
+
+__metaclass__ = type
+
+from ansible.errors import AnsibleFilterError
+from ansible.module_utils.six import string_types
+from ansible.module_utils.six.moves import StringIO
+from ansible.module_utils.six.moves.configparser import ConfigParser
+from ansible.module_utils.common.text.converters import to_native
+
+
+class IniParser(ConfigParser):
+    ''' Implements a configparser which is able to return a dict '''
+
+    def __init__(self):
+        super().__init__()
+        self.optionxform = str
+
+    def as_dict(self):
+        d = dict(self._sections)
+        for k in d:
+            d[k] = dict(self._defaults, **d[k])
+            d[k].pop('__name__', None)
+
+        if self._defaults:
+            d['DEFAULT'] = dict(self._defaults)
+
+        return d
+
+
+def from_ini(obj):
+    ''' Read the given string as INI file and return a dict '''
+
+    if not isinstance(obj, string_types):
+        raise AnsibleFilterError(f'from_ini requires a str, got {type(obj)}')
+
+    parser = IniParser()
+
+    try:
+        parser.read_file(StringIO(obj))
+    except Exception as ex:
+        raise AnsibleFilterError(f'from_ini failed to parse given string: '
+                                 f'{to_native(ex)}', orig_exc=ex)
+
+    return parser.as_dict()
+
+
+class FilterModule(object):
+    ''' Query filter '''
+
+    def filters(self):
+
+        return {
+            'from_ini': from_ini
+        }


Test patch:
diff --git a/tests/integration/targets/filter_from_ini/tasks/main.yml b/tests/integration/targets/filter_from_ini/tasks/main.yml
new file mode 100644
--- /dev/null
+++ b/tests/integration/targets/filter_from_ini/tasks/main.yml
@@ -0,0 +1,60 @@
+---
+# Copyright (c) 2023, Steffen Scheib <steffen@scheib.me>
+# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
+# SPDX-License-Identifier: GPL-3.0-or-later
+
+- name: 'Define ini_test_dict'
+  ansible.builtin.set_fact:
+    ini_test_dict:
+      section_name:
+        key_name: 'key value'
+
+      another_section:
+        connection: 'ssh'
+
+- name: 'Write INI file that reflects ini_test_dict to {{ ini_test_file }}'
+  ansible.builtin.copy:
+    dest: '{{ ini_test_file }}'
+    content: |
+      [section_name]
+      key_name=key value
+
+      [another_section]
+      connection=ssh
+
+- name: 'Slurp the test file: {{ ini_test_file }}'
+  ansible.builtin.slurp:
+    src: '{{ ini_test_file }}'
+  register: 'ini_file_content'
+
+- name: >-
+    Ensure defined ini_test_dict is the same when retrieved
+    from {{ ini_test_file }}
+  ansible.builtin.assert:
+    that:
+      - 'ini_file_content.content | b64decode | community.general.from_ini ==
+         ini_test_dict'
+
+- name: 'Create a file that is not INI formatted: {{ ini_bad_file }}'
+  ansible.builtin.copy:
+    dest: '{{ ini_bad_file }}'
+    content: |
+      Testing a not INI formatted file.
+
+- name: 'Slurp the file that is not INI formatted: {{ ini_bad_file }}'
+  ansible.builtin.slurp:
+    src: '{{ ini_bad_file }}'
+  register: 'ini_bad_file_content'
+
+- name: 'Try parsing the bad file with from_ini: {{ ini_bad_file }}'
+  ansible.builtin.debug:
+    var: ini_bad_file_content | b64decode | community.general.from_ini
+  register: 'ini_bad_file_debug'
+  ignore_errors: true
+
+- name: 'Ensure from_ini raised the correct exception'
+  ansible.builtin.assert:
+    that:
+      - "'from_ini failed to parse given string' in ini_bad_file_debug.msg"
+      - "'File contains no section headers' in ini_bad_file_debug.msg"
+...
diff --git a/tests/integration/targets/filter_from_ini/vars/main.yml b/tests/integration/targets/filter_from_ini/vars/main.yml
new file mode 100644
--- /dev/null
+++ b/tests/integration/targets/filter_from_ini/vars/main.yml
@@ -0,0 +1,8 @@
+---
+# Copyright (c) 2023, Steffen Scheib <steffen@scheib.me>
+# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
+# SPDX-License-Identifier: GPL-3.0-or-later
+
+ini_test_file: '/tmp/test.ini'
+ini_bad_file: '/tmp/bad.file'
+...