linuxcsvansiblejinja2

How to generate a CSV file with jinja2 template with dict elements in Ansible?


I'm trying to generate a CSV output file with the following template:

Hostname;Login;UID;GID;Comments;SUDO Enabled;Last_Login_Date

This task has to check this information for every user with UID >= 1000 based on info located at /etc/passwd. The comments is the same Comments field in /etc/passwd. If an user has SUDO privileges, it has to show just YES or NO. For the Last_Login_Date, if the user has never logged to this system, it should show N/A.

For this task, I have come with an Ansible playbook solution but I can't figure out how to fix the following error when creating the CSV file.

The playbook:

  tasks:

    - name: Check User List
      shell:
        cmd: |
           awk -F ':' '{ if ($3 >= 999 && $7 != "/sbin/nologin") print $1 }' /etc/passwd
      register: user_list_result

    - name: Get info on /etc/passwd
      ansible.builtin.shell: "grep ^{{ item }}: /etc/passwd"
      loop: "{{ user_list_result.stdout_lines }}"
      register: user_info_results

    - name: Check SUDO Privileges
      ansible.builtin.shell: "sudo -U {{ item.item }} -l | grep -q '(ALL) ALL' && echo 'YES' || echo 'NO'"
      loop: "{{ user_info_results.results }}"
      register: sudo_status_results

    - name: Check Last Login Date
      ansible.builtin.shell: "lastlog -u {{ item.item }} | awk 'NR==2 { print $4 }'"
      loop: "{{ user_info_results.results }}"
      register: last_login_results

    - name: Build CSV File
      ansible.builtin.template:
        src: "userlist_template.j2"
        dest: "output.csv"
      vars:
        user_info_results: "{{ user_info_results.results }}"
        sudo_status_results: "{{ sudo_status_results.results }}"
        last_login_results: "{{ last_login_results.results }}"

And I have come to this Jinja2 Template:

Hostname;Login;UID;GID;Comments;SUDO Enabled;Last_Login_Date
{% for user_info in user_info_results %}
{{ inventory_hostname }};{{ user_info['item'] }};{{ user_info.stdout.split(':')[2] }};{{ user_info.stdout.split(':')[3] }};{{ user_info.stdout.split(':')[4] }};{{ sudo_status_results[loop.index0].stdout }};{{ last_login_results[loop.index0].stdout | default('N/A') }}
{% endfor %}

But when I run this playbook the following error occurs:

The error was: ansible.errors.AnsibleUndefinedVariable: 'str object' has no attribute 'item'

It seems that Ansible is checking for a string object but it's a dict object. How can I achieve the proposed file?


Solution

  • First, if you're not on Mac OS, I'd suggest you to use getent module instead of parsing the files manually.

    Now to the template structure. If we simplify it for debugging purposes, we'll see that the user_info_results variable doesn't seem to have the structure you would expect:

    Hostname;Login;UID;GID;Comments;SUDO Enabled;Last_Login_Date
    {% for user_info in user_info_results %}
    {{ inventory_hostname }};{{ user_info }}
    {% endfor %}
    
    Hostname;Login;UID;GID;Comments;SUDO Enabled;Last_Login_Date
    localhost;results;
    localhost;skipped;
    localhost;changed;
    localhost;msg;
    

    So, using the nested results object of your variables fixes the template:

    Hostname;Login;UID;GID;Comments;SUDO Enabled;Last_Login_Date
    {% for user_info in user_info_results.results %}
    {{ inventory_hostname }};{{ user_info['item'] }};{{ user_info.stdout.split(':')[2] }};{{ user_info.stdout.split(':')[3] }};{{ user_info.stdout.split(':')[4] }};{{ sudo_status_results.results[loop.index0].stdout }};{{ last_login_results.results[loop.index0].stdout | default('N/A') }}
    {% endfor %}
    

    I decreased the UID to 280 for testing because I don't have any UIDs >= 1000 locally:

    Hostname;Login;UID;GID;Comments;SUDO Enabled;Last_Login_Date
    localhost;_coreml;280;280;CoreML Services;NO;
    localhost;_sntpd;281;281;SNTP Server Daemon;NO;
    localhost;_trustd;282;282;trustd;NO;
    localhost;_darwindaemon;284;284;Darwin Daemon;NO;
    localhost;_notification_proxy;285;285;Notification Proxy;NO;
    localhost;_oahd;441;441;OAH Daemon;NO;
    

    Why does it happen? While it looks kind of surprising, it's documented: the registered variables have higher precedence than task vars.

    If we override the variables using set_facts instead, your old template works as you expect:

        - name: Override the variables
          set_fact:
            user_info_results: "{{ user_info_results.results }}"
            sudo_status_results: "{{ sudo_status_results.results }}"
            last_login_results: "{{ last_login_results.results }}"
    
        - name: Build CSV File
          ansible.builtin.template:
            src: "userlist_template.j2"
            dest: "output.csv"