Cannot figure out how to add items to dictionary properly and send consolidated email

I’m not good with Ansible and have been working with this playbook mess for a while now.

My goal is to pull a list of Sudo users in Ubuntu servers and put them all in a single dictionary object which would get emailed to me. It would look something like this for example:

server1: bob, joe, admin
server2: josh, service2
etc…

This is the playbook I have so far:

 - name: root users
   hosts: ubuntu
   become: true
   become_user: root
   gather_facts: no
#I created root_users dictionary to hold all the root users of 
#each server
   vars:
     root_users: {}


     - name: Get root users
       ansible.builtin.shell:
         cmd: getent group sudo | cut -d{{':'}} -f4     
       register: usersStdOut


#I did this to store only the list of users. Otherwise usersStdOut 
returns a dictionary.
     - set_fact:
         users: "{{ usersStdOut.stdout }}"


     - name: test type
       debug:
         msg:
           - "value: '{{ users }}' is of type: {{ users | type_debug }}"


     - debug:
         var: users


     - name: Add root users to dictionary
       set_fact:
         root_users: "{{ root_users | combine({item: 'users'}) }}"
       with_items: "{{ users }}"


     - name: List of Ubuntu root users per host
       ansible.builtin.debug:
         var: sudo_users


     - name: Send email containing root users
       delegate_to: localhost
       run_once: true
       community.general.mail:
         [redacted]

This is my output

TASK [Get root users] 
**********************************************************
changed: [server1]
changed: [server2]

TASK [set_fact] 
**********************************************************
ok: [server1]
ok: [server2]


# Maybe this task is causing me problems given the type of 
# the variable?
TASK [test type] 
**********************************************************
ok: [server1] => {
    "msg": [
        "value: 'bob,joe,admin' is of type: AnsibleUnsafeText"
    ]
}
ok: [server2] => {
    "msg": [
        "value: 'josh,service2' is of type: AnsibleUnsafeText"
    ]
}

TASK [debug] 
**********************************************************
ok: [server1] => {
    "users": "bob,joe,admin"
}
ok: [server2] => {
    "users": "'josh,service2"
}

TASK [Add root users to dictionary] 
**********************************************************
ok: [server1] => (item=bob,joe,admin)
ok: [server2] => (item=josh,service2)


# How can I stop "users" from being added at the end?
TASK [List of Ubuntu root users per host] 
**********************************************************
ok: [server1] => {
    "sudo_users": {
        "bob,joe,admin": "users"
    }
}
ok: [server2] => {
    "sudo_users": {
        "'josh,service2": "users"
    }
}

TASK [Send email containing root users] 
**********************************************************
ok: [server1 -> localhost]
ok: [server2-> localhost]

I get a separate email from each server but I only want one email sent for all servers. The email lists the users like this:
"{'bob,joe,admin': 'users'}"
How can I get them to show up like this with the name of the server before the user list?
"{'bob,joe,admin'}"

1 Like

Try something like this:

---
# stipemd_01.yml
- name: Grouper Users
  hosts: ubuntu
  vars:
    grouper: www # Change back to 'ubuntu'
  tasks:
    - name: Get grouper users
      ansible.builtin.shell:
        cmd: 'getent group {{ grouper }} | cut -d\: -f4'
      register: users_grouper

    - name: 'Send email containing users in group {{ grouper }}'
      delegate_to: localhost
      run_once: true
      # community.general.mail:
      ansible.builtin.debug:
        msg:
         - '{{ mailbody | type_debug }}'
         - '{{ mailbody }}'
      vars:
        mailbody: |
          {
          {%- for server in ansible_play_hosts -%}
          "{{ server }}": "{{ hostvars[server]['users_grouper']['stdout_lines'][0] }}"
          {%    if not loop.last %},{% endif %}
          {%- endfor -%}
          }

Here’s the result I get:

TASK [Send email containing users in group www] ***************
ok: [cloister.trailsong.org -> localhost] => 
  msg:
  - dict
  - bluemoon.trailsong.org: utoddl,tplewis,gregory
    cloister.trailsong.org: tplewis,utoddl,gregory,daniel
    tango.trailsong.org: utoddl,tplewis

3 Likes

Thanks for taking the time to read through my post and for the much more simple solution!

My email task (same as how you put it) comes with slightly different formatting. I don’t know where the difference is coming from.

TASK [Send email containing Ubuntu sudo users] 
*******************************************************
ok: [server1 -> localhost] => {
    "msg": [
        "dict",
        {
            "server1 ": "bob,joe,admin",
            "server2": "josh,service2"
        }
    ]
}

When I send the email, everything ends up on one line as well like:

{'server1':'bob,joe,admin','server2':'josh,service2'}

I’ll try to see how to put them on a new line for each server

The difference in output is because I have this environment variable set:

 ANSIBLE_STDOUT_CALLBACK=yaml

The string variable mailbody is intentionally a JSON expression. Ansible conveniently treats strings that look like JSON as JSON. (I’m speculating a bit here, but that seems to be the behavior. I’m sure that will come back to bite me some day.)
[Edit: it’s more subtle than that. I think it’s more accurate to say that expanded Jinja2 expressions that result in valid JSON are interpreted as JSON. But again, that’s my supposition; I’ve not seen that documented as “intentional as designed” behavior.]

By using the yaml callback, my job logs’ structured data are shown in canonical YAML format rather than the default JSON. In hindsight, I should not have called that variable mailbody. Rather, it’s a data structure containing all the play hosts’ grouper members. The real mailbody would be a Jinja2 template that would expand that variable in the format you really want.

Note also that the “list of users” in a given group is not really a list. It’s a single string that happens to contain user names concatenated with commas. If you want to deal with them individually, or change the way they are presented, you’ll need to split the string on commas and deal with the resulting list appropriately.

1 Like