Template with to_ini in a loop: 'str' object has no attribute 'items'

I’m trying to generate some files using ansible.builtin.template and I would like to use the community.general.to_ini jinja filter.
The problem is that I have an error The error was: ansible.errors.AnsibleFilterError: to_ini failed to parse given dict:'str' object has no attribute 'items'. 'str' object has no attribute 'items'

My code is the following

- name: Configure action                                                                                                                                                                                                                                                  
  become: yes
  ansible.builtin.template:
    src: "{{ action_file_tmpl }}"  
    dest: "{{ [actions_directory, action.name] | path_join }}" 
    owner: root
    group: root
    mode: '0600'
    vars:
      name: "{{ action.name }}"
      config: "{{ action.config }}"                                                                                                                                                                                                                                                     
    loop: "{{ daks_backupninja_actions }}"
    loop_control:
      loop_var: action
      label: "{{ name }}"

and the template file contains

# some header
{{ config | community.general.to_ini }}

The complete error message is

An exception occurred during task execution. To see the full traceback, use -vvv. The error was: ansible.errors.AnsibleFilterError: to_ini failed to parse given dict:'str' object has no attribute 'items'. 'str' object has no attribute 'items'
failed: [debian-12] (item=10-test.sh) => {"action": {"config": {"bl": "environment", "ee": "value"}, "name": "10-test.sh"}, "ansible_loop_var": "action", "changed": false, "msg": "AnsibleFilterError: to_ini failed to parse given dict:'str' object has no attribute 'items'. 'str' object has no attribute 'items'"}

I tried several solutions (like using an intermediate task file and loop over it giving the correct var) but everytime I got this error. It looks like my vars become a string and I can’t loop over it with to_ini.

I don’t understand what is the correct way to do it.

ps: I could probably use community.general.ini_file but I would like to have more control over my file, like add an header as seen in the example.

I’m trying to get my head around what you are trying to do, what does the daks_backupninja_actions list look like?

currently just

vars:
  daks_backupninja_actions:
    - name: 10-test.sh 
      config:
         bl: environment
         ee: value

but at the end something like this:

   - name: 90-remote.rdiff
     config:
        source:
           type: local
           include:
             - /etc
             - /var
           exclude:
            - /var/lock    
           keep: 300D      
        dest:
           type: remote
           host: host.example.com
           user: backup-user
           directory: /path/to/backups       
           sshoptions: -o IdentityFile=/home/backup-user/.ssh/backup_id

to generate

[source]
include = /etc
include = /var
exclude = /var/lock
keep = 300D
type = local

[dest]
type = remote
host = host.example.com
user = backup-user
directory = /path/to/backups
sshoptions = -o IdentityFile=/home/backup-user/.ssh/backup_id

I think you might need to add a dict2items filter to the mix, I’m doing this kind of thing for php and systemd, perhaps like this in the template?

{{ config | ansible.builtin.dict2items | community.general.to_ini }}

you mean in the task or in the template?

Sorry, template, I’ve edited my answer above, also you could check the type like this in the template:

{{ config | ansible.builtin.type_debug }}

But I’m tired and could well have got the wrong :roll_eyes:

Actually ignore what I wrote above, since the ini files have sections I think you need a more complicated template? I have one like this:

; {{ ansible_managed }}
{% if php_conf_file_proposed_sections | bool %}
{%     for php_section in php_conf_file_proposed_vars | dict2items %}
[{{ php_section.key }}]
{%         for php_variable_pair in php_section.value | dict2items %}
{%             if php_conf_file_no_extra_spaces | bool %}
{{ php_variable_pair.key }}={{ php_variable_pair.value }}
{%             else %}
{{ php_variable_pair.key }} = {{ php_variable_pair.value }}
{%             endif %}
{%         endfor %}
{%     endfor %}
{% else %}
{%     for php_conf_pair in php_conf_file_proposed_vars | ansible.builtin.dict2items %}
{%         if php_conf_file_no_extra_spaces | bool %}
{{ php_conf_pair.key }}={{ php_conf_pair.value }}
{%         else %}
{{ php_conf_pair.key }} = {{ php_conf_pair.value }}
{%         endif %}
{%     endfor %}
{% endif %}
; vim: syntax=dosini

To generate ini files from lists like this:

  - name: PHP 8.3 configuration
    version: "8.3"
    state: present
    files:
      - name: PHP 8.3 CLI configuration
        path: /etc/php/8.3/cli/php.ini
        state: present
        conf:
          apc:
            "apc.coredump_unmap": "0"
            "apc.enable_cli": "1"
            "apc.enabled": "1"
            "apc.entries_hint": "4096"
            "apc.gc_ttl": "3600"
            "apc.serializer": "php"
            "apc.shm_segments": "1"
            "apc.shm_size": "32M"
            "apc.slam_defense": "1"
            "apc.ttl": "0"
            "apc.use_request_time": "0"
          "CLI Server":
            "cli_server.color": "1"
          MySQLi:
            "mysqli.allow_local_infile": "0"
            "mysqli.allow_persistent": "1"
            "mysqli.default_port": '3306'
            "mysqli.max_links": "-1"
            "mysqli.max_persistent": "-1"
            "mysqli.reconnect": "0"
            "mysqli.default_socket": "/run/mysqld/mysqld.sock"

However when I wrote this role community.general.to_ini didn’t exist… so perhaps all the above can be ignored!

I posted some links to ini resources for Ansible here, I don’t know if these will help:

Sorry I’m too knackered tonight to help any more but if you are still stuck later this week I’m sure I could help solve it!

Actually this MariaDB template is a better (simpler) example:

# {# j2lint: disable=jinja-statements-indentation j2lint: disable=single-statement-per-line #}
{% for mariadb_cnf_section in mariadb_cnf.conf | dict2items %}
[{{ mariadb_cnf_section.key }}]
{%     for mariadb_cnf_variable_pair in mariadb_cnf_section.value | dict2items %}
{{ mariadb_cnf_variable_pair.key }}{% if mariadb_cnf_variable_pair.value is defined %} = {{ mariadb_cnf_variable_pair.value }}{% endif %}
{%     endfor %}
{% endfor %}
# vim: syntax=dosini

With this list of vars:

mariadb_config:
  - name: MariaDB server configuration
    path: /etc/mysql/mariadb.conf.d/50-server.cnf
    state: edited
    conf:
      mysqld:
        binlog_format: "ROW"
        ignore_db_dir: "lost+found"
        innodb_buffer_pool_instances: "1"
        innodb_buffer_pool_size: "256M"
        innodb_log_file_size: "64M"
        join_buffer_size: "8M"
        key_buffer_size: "16M"
        max_allowed_packet: "64M"
        max_connections: "80"
        max_heap_table_size: "32M"
        max_user_connections: "0"
        open_files_limit: "122880"
        query_cache_limit: "0"
        query_cache_size: "0"
        query_cache_type: "0"
        skip_name_resolve:
        slow_query_log_file: "/var/log/mysql/mariadb-slow.log"
        table_cache: "64"
        table_open_cache: "4000"
        thread_concurrency: "10"
        tmp_table_size: "32M"
        transaction_isolation: "READ-COMMITTED"
        performance_schema: "1"

But again this role was written before the community.general.to_ini filter so I fear I’m just adding noise rather than helping at this point… :frowning:

This means doing it ‘manually’ without using the to_ini filter.
I had thought about it and I may need to do it this way, given my data structure.

But I really want to be able to use (or at least test) the to_ini filter and understand why it doesn’t work in my example. I may want (or need) to use it in another use case.

1 Like

I’ve just tested updating my fail2ban role, which currently uses the community.general.ini_file module, to update it to use a template which is like the one you have and the community.general.to_ini filter and it seems fine, other than the fact that booleans are capitalized when previously I have set them to to be lower case, I’m not sure if this is OK so I’ve asked in the fail2ban discussion form — so I haven’t applied this update yet, I hope I can as it will simplify the role quite a lot.

Could you post your whole backupninja role somewhere so I can run and debug it without having to recreate it — I’m confident I can help you get it working.

Nice :slight_smile:
I’ll look at your code in a few days I hope. I’m busy with other things in the mean time.
I’ll comment here if I’m still confused or my code stills has errors.
Thanks

1 Like

The first problem is indentation. Everything below “mode: '0600'” needs to be shifted two columns to the left.

The second problem is that your simplified daks_backupninja_actions variable has no “section” level(s). The spurious message about the string having no items is because the underlying python module is trying to parse the value of the “bl” section as a dict, but its value is the string “environment”. Throw in another level between “config” and “bl”. (Ironically, your “but at the end something like this” data contains the “section” levels missing from the simplified data.)

1 Like

Thanks for spotting that, I have written a couple of (probably terrible!) sets of Ansible tasks in the past to detect if “section” level heading are present or not — the from_ini filter doesn’t support files without sections so I’m not surprised that the to_ini filter doesn’t either, it would be nice if they did…