0

The user creation policy in our organization is to force changing password on first login and use SSH keys for remote connections; the password is only used to run sudo or login locally.

To force password change we commonly run chage -d 0 <username>. So, I need to run this in Ansible, but only if it just created user in the same play in previous task.

I create users with ansible.builtin.user module. My plan is to register it's output into the variable, then I want to run shell module but with some condition, and I need to determine the precise condition.

My playbook so far is:

- hosts: hosts
  vars_files:
  - users-config.yaml
  tasks:
  - name: create users
    user:
      name: "{{ item.username }}"
      state: "{{ item.state }}"
      comment: "{{ item.gecos }}"
      shell: /bin/bash
      groups: "{{ item.groups }}"
    loop: "{{ users }}"
    register: users_processed
  - name: set password age to 0 to force change on next login for new users
    debug:
      var: "{{ item }}"
    loop: "{{ users_processed.results }}"

So far, I found out the user module returns useful changed and failed, so my condition will definitely contain item.changed and not item.failed, but what other conditions I need to to properly catch only newly created users and not just updated pre-existing?

For reference, in the example output below apushkin was not changed, nkhrushchov was updated but does not need the password age to be reset, and only rraskolnikov was just created and needs their password age to be reset.

ok: [host3] => (item={'comment': 'Alexander Pushkin', 'shell': '/bin/bash', 'group': 1000, 'name': 'apushkin', 'changed': False, 'state': 'present', 'groups': 'wheel', 'invocation': {'module_args': {'comment': 'Alexander Pushkin', 'ssh_key_bits': 0, 'update_password': 'always', 'non_unique': False, 'force': False, 'ssh_key_type': 'rsa', 'create_home': True, 'password_lock': None, 'ssh_key_passphrase': None, 'uid': None, 'home': None, 'append': False, 'skeleton': None, 'ssh_key_comment': 'ansible-generated on host3', 'group': None, 'system': False, 'state': 'present', 'role': None, 'hidden': None, 'local': None, 'authorization': None, 'profile': None, 'shell': '/bin/bash', 'expires': None, 'ssh_key_file': None, 'groups': ['wheel'], 'move_home': False, 'password': None, 'name': 'apushkin', 'seuser': None, 'remove': False, 'login_class': None, 'generate_ssh_key': None}}, 'home': '/home/apushkin', 'move_home': False, 'append': False, 'uid': 1000, 'failed': False, 'item': {'username': 'apushkin', 'gecos': 'Alexander Pushkin', 'state': 'present', 'groups': 'wheel'}, 'ansible_loop_var': 'item'}) => {                                                                                                                                                                       
    "<class 'dict'>": "VARIABLE IS NOT DEFINED!",
    "ansible_loop_var": "item",
    "item": {
        "ansible_loop_var": "item",
        "append": false,
        "changed": false,
        "comment": "Alexander Pushkin",
        "failed": false,
        "group": 1000,
        "groups": "wheel",
        "home": "/home/apushkin",
        "invocation": {
            "module_args": {
                "append": false,
                "authorization": null,
                "comment": "Alexander Pushkin",
                "create_home": true,
                "expires": null,
                "force": false,
                "generate_ssh_key": null,
                "group": null,
                "groups": [
                    "wheel"
                ],
                "hidden": null,
                "home": null,
                "local": null,
                "login_class": null,
                "move_home": false,
                "name": "apushkin",
                "non_unique": false,
                "password": null,
                "password_lock": null,
                "profile": null,
                "remove": false,
                "role": null,
                "seuser": null,
                "shell": "/bin/bash",
                "skeleton": null,
                "ssh_key_bits": 0,
                "ssh_key_comment": "ansible-generated on host3",
                "ssh_key_file": null,
                "ssh_key_passphrase": null,
                "ssh_key_type": "rsa",
                "state": "present",
                "system": false,
                "uid": null,
                "update_password": "always"
            }
        },
        "item": {
            "gecos": "Alexander Pushkin",
            "groups": "wheel",
            "state": "present",
            "username": "apushkin"
        },
        "move_home": false,
        "name": "apushkin",
        "shell": "/bin/bash",
        "state": "present",
        "uid": 1000
    }
}
ok: [host3] => (item={'comment': '', 'shell': '/bin/bash', 'group': 1001, 'name': 'nhrushchov', 'changed': True, 'state': 'present', 'groups': 'wheel', 'invocation': {'module_args': {'comment': 'Nikita Khrushchov', 'ssh_key_bits': 0, 'update_password': 'always', 'non_unique': False, 'force': False, 'ssh_key_type': 'rsa', 'create_home': True, 'password_lock': None, 'ssh_key_passphrase': None, 'uid': None, 'home': None, 'append': False, 'skeleton': None, 'ssh_key_comment': 'ansible-generated on host3', 'group': None, 'system': False, 'state': 'present', 'role': None, 'hidden': None, 'local': None, 'authorization': None, 'profile': None, 'shell': '/bin/bash', 'expires': None, 'ssh_key_file': None, 'groups': ['wheel'], 'move_home': False, 'password': None, 'name': 'nhrushchov', 'seuser': None, 'remove': False, 'login_class': None, 'generate_ssh_key': None}}, 'home': '/home/nhrushchov', 'move_home': False, 'append': False, 'uid': 1001, 'failed': False, 'item': {'username': 'nhrushchov', 'gecos': 'Nikita Khrushchov', 'state': 'present', 'groups': 'wheel'}, 'ansible_loop_var': 'item'}) => {                                                                                                                                                                                           
    "<class 'dict'>": "VARIABLE IS NOT DEFINED!",
    "ansible_loop_var": "item",
    "item": {
        "ansible_loop_var": "item",
        "append": false,
        "changed": true,
        "comment": "",
        "failed": false,
        "group": 1001,
        "groups": "wheel",
        "home": "/home/nhrushchov",
        "invocation": {
            "module_args": {
                "append": false,
                "authorization": null,
                "comment": "Nikita Khrushchov",
                "create_home": true,
                "expires": null,
                "force": false,
                "generate_ssh_key": null,
                "group": null,
                "groups": [
                    "wheel"
                ],
                "hidden": null,
                "home": null,
                "local": null,
                "login_class": null,
                "move_home": false,
                "name": "nhrushchov",
                "non_unique": false,
                "password": null,
                "password_lock": null,
                "profile": null,
                "remove": false,
                "role": null,
                "seuser": null,
                "shell": "/bin/bash",
                "skeleton": null,
                "ssh_key_bits": 0,
                "ssh_key_comment": "ansible-generated on host3",
                "ssh_key_file": null,
                "ssh_key_passphrase": null,
                "ssh_key_type": "rsa",
                "state": "present",
                "system": false,
                "uid": null,
                "update_password": "always"
            }
        },
        "item": {
            "gecos": "Nikita Khrushchov",
            "groups": "wheel",
            "state": "present",
            "username": "nhrushchov"
        },
        "move_home": false,
        "name": "nhrushchov",
        "shell": "/bin/bash",
        "state": "present",
        "uid": 1001
    }
}
ok: [host3] => (item={'invocation': {'module_args': {'comment': 'Rodion Raskolnikov', 'ssh_key_bits': 0, 'update_password': 'always', 'non_unique': False, 'force': False, 'ssh_key_type': 'rsa', 'create_home': True, 'password_lock': None, 'ssh_key_passphrase': None, 'uid': None, 'home': None, 'append': False, 'skeleton': None, 'ssh_key_comment': 'ansible-generated on host3', 'group': None, 'system': False, 'state': 'present', 'role': None, 'hidden': None, 'local': None, 'authorization': None, 'profile': None, 'shell': '/bin/bash', 'expires': None, 'ssh_key_file': None, 'groups': ['wheel'], 'move_home': False, 'password': None, 'name': 'rraskolnikov', 'seuser': None, 'remove': False, 'login_class': None, 'generate_ssh_key': None}}, 'changed': True, 'failed': False, 'item': {'username': 'rraskolnikov', 'gecos': 'Rodion Raskolnikov', 'state': 'present', 'groups': 'wheel'}, 'ansible_loop_var': 'item'}) => {          
    "<class 'dict'>": "VARIABLE IS NOT DEFINED!",
    "ansible_loop_var": "item",
    "item": {
        "ansible_loop_var": "item",
        "changed": true,
        "failed": false,
        "invocation": {
            "module_args": {
                "append": false,
                "authorization": null,
                "comment": "Rodion Raskolnikov",
                "create_home": true,
                "expires": null,
                "force": false,
                "generate_ssh_key": null,
                "group": null,
                "groups": [
                    "wheel"
                ],
                "hidden": null,
                "home": null,
                "local": null,
                "login_class": null,
                "move_home": false,
                "name": "rraskolnikov",
                "non_unique": false,
                "password": null,
                "password_lock": null,
                "profile": null,
                "remove": false,
                "role": null,
                "seuser": null,
                "shell": "/bin/bash",
                "skeleton": null,
                "ssh_key_bits": 0,
                "ssh_key_comment": "ansible-generated on host3",
                "ssh_key_file": null,
                "ssh_key_passphrase": null,
                "ssh_key_type": "rsa",
                "state": "present",
                "system": false,
                "uid": null,
                "update_password": "always"
            }
        },
        "item": {
            "gecos": "Rodion Raskolnikov",
            "groups": "wheel",
            "state": "present",
            "username": "rraskolnikov"
        }
    }
}

I am just struggling to deduce the proper reliable condition out of this output. The module manual says that append is defined when user exists and force is defined when state is absent and user exists. Does "exists" mean "existed before module run"? Is it safe to use the following condition:

    when: item.changed and not item.failed and item.append is not defined and item.force is not defined

or is there a better way of doing this?

Nikita Kipriyanov
  • 8,033
  • 1
  • 21
  • 39

1 Answers1

1

Use getent and create the list of present users

    - name: Get all users
      getent:
        database: passwd
    - name: Set present users
      set_fact:
        present_users: "{{ ansible_facts.getent_passwd.keys()|list }}"

After you update users get the list of new users

    - name: Get all users
      getent:
        database: passwd
    - name: Set new users
      set_fact:
        new_users: "{{ ansible_facts.getent_passwd.keys()|
                       difference(present_users) }}"

Example of a complete playbook

- hosts: test_11
  vars:
    users:
      - username: alice
        state: present
        gecos: Alice
        groups: [guest]
      - username: bob
        state: present
        gecos: Bob
        groups: [guest]
  tasks:
    - name: Get all users
      getent:
        database: passwd
    - name: Set present users
      set_fact:
        present_users: "{{ ansible_facts.getent_passwd.keys()|list }}"
    - name: Create users
      user:
        name: "{{ item.username }}"
        state: "{{ item.state }}"
        comment: "{{ item.gecos }}"
        shell: /bin/bash
        groups: "{{ item.groups }}"
      loop: "{{ users }}"
      loop_control:
        label: "{{ item.username }}"
    - name: Get all users
      getent:
        database: passwd
    - name: Set new users
      set_fact:
        new_users: "{{ ansible_facts.getent_passwd.keys()|
                       difference(present_users) }}"
    - debug:
        var: new_users
    - debug:
        msg: "Force change on next login for {{ item }}"
      loop: "{{ new_users }}"

gives

PLAY [test_11] *******************************************************************************

TASK [Get all users] *************************************************************************
ok: [test_11]

TASK [Set present users] *********************************************************************
ok: [test_11]

TASK [Create users] **************************************************************************
changed: [test_11] => (item=alice)
changed: [test_11] => (item=bob)

TASK [Get all users] *************************************************************************
ok: [test_11]

TASK [Set new users] *************************************************************************
ok: [test_11]

TASK [debug] *********************************************************************************
ok: [test_11] => 
  new_users:
  - alice
  - bob

TASK [debug] *********************************************************************************
ok: [test_11] => (item=alice) => 
  msg: Force change on next login for alice
ok: [test_11] => (item=bob) => 
  msg: Force change on next login for bob

PLAY RECAP ***********************************************************************************
test_11: ok=7    changed=1    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0

The playbook is idempotent.

Vladimir Botka
  • 3,791
  • 6
  • 17