Ansible has some really useful filters for various types of objects. A few of those are for sets of something. Recently, I had to use them in practice.

In this situation there was a set of system users in users group. Some users needed to be present and their shell to be set to /usr/sbin/nologin, so they would not have direct shell access, and some users were not supposed to be present so they needed to be removed. There were also some system users present in users group, which should be left as-is.

To start, a list with legit users was created (documented_users), along with a list of system users (system_users):

documented_users:
  - user.name
  - another.user
...

system_users:
  - spamas
  - openvpn
...

To get what we needed, we first got all the users in users group on remote systems, and assign them to an Ansible variable:

- name: get users in users group
  shell: cut -d":" -f1,4 /etc/passwd | grep $(getent group users | cut -d":" -f3) | cut -d":" -f1
  register: users_users

We then create a list of legit_users by joining documented_users and system_users:

- name: set legit_users fact
  set_fact:
    legit_users: "{{ documented_users | union(system_users) }}"

When that was done, we could determine, which users were not needed on the system (undocumented_users), and which of the documented users were actually present (present_documented_users):

- name: set undocumented_users fact
  set_fact:
    undocumented_users: "{{ users_users.stdout_lines | difference(legit_users) }}"

- name: set present_documented_users fact
  set_fact:
    present_documented_users: "{{ users_users.stdout_lines | intersect(documented_users) }}"

Here, difference and intersect Ansible set theory filters were really useful. It would take a bit more code to do the comparison by other means. difference returned, which elements were in the first, but not the second set, while intersect returned a list with elements in both sets.

Now that we knew, which users are in which set, we can proceed to removing the undocumented users and seting proper shell for the documented users using the user module:

- name: remove undocumented users
  user:
    name: "{{ item }}"
    state: absent
  with_items: "{{ undocumented_users }}"
  when:
    - undocumented_users is defined
    - undocumented_users | length > 0

- name: set nologin as shell for documented users
  user:
    name: "{{ item }}"
    shell: /usr/sbin/nologin
  with_items: "{{ present_documented_users }}"
  when:
    - present_documented_users is defined
    - present_documented_users | length > 0