chief_finanigans
chief_finanigans

Reputation: 17

Ansible "when not in group_names" condition not being applied

Bottom Line

A seemingly trivial when condition on a task is not working as expected. The task runs on a host where it should not. I've tried swapping the placement of the loop: and when: statements, which had no effect. The conditional is so simple, and verified via debug, that I feel like I must be missing something obvious.

Background

I'm attempting to consolidate our project's sudo privileges idempotently across the internal development environment, but explicitly not grant sudo access on our bastion host. This is handled as part of the create-project-accounts role, which creates user accounts across our environment, including on the bastion host. I could break account creation and sudo up into separate roles, but I'd prefer to just get the conditional working here and keep it all together if possible.

add_sudoers.yml is called from the role's main.yml via ansible.builtin.include_tasks. The variable project_sudoersd_file is defined in the role's defaults/main.yml as 'project_sudoers'.

add_sudoers.yml

---

    - name: What's wrong
      tags: create_project_accounts, add_sudoers
      ansible.builtin.debug:
        var: group_names
    
    - name: Remove old style sudoers.d files
      tags: create_project_accounts, add_sudoers
      become: true
      ansible.builtin.file:
        path: /etc/sudoers.d/{{ item }}
        state: absent
      loop:
        - citool
        - itsectool
        - devgroup1
        - developer1
    
    - name: Remove existing project sudoers.d file
      tags: create_project_accounts, add_sudoers
      become: true
      ansible.builtin.file:
        path: "/etc/sudoers.d/{{ project_sudoersd_file }}"
        state: absent
    
    - name: Add lines to project sudoers.d file
      tags: create_project_accounts, add_sudoers
      become: true
      ansible.builtin.lineinfile:
        path: "/etc/sudoers.d/{{ project_sudoersd_file }}"
        state: present
        create: yes
        mode: '644'
        owner: root
        group: root
        line: "{{ item }}"
        validate: 'visudo -cf %s'
      when: ("'bastion' not in group_names")
      loop:
        - '%devgroup1 ALL=(ALL) ALL'
        - 'itsectool ALL=(ALL) ALL'
        - 'citool ALL=(ALL) ALL'

Expected Result

I expect the first three tasks (What's wrong, Remove old style sudoers.d, and Remove existing project sudoers.d file) to run and the last task (Add lines to project sudoers.d file) to be skipped when run on the bastion host.

Actual Result

All four tasks are being executed on the bastion, resulting in an /etc/sudoers.d/project_sudoers file that grants sudo privileges on the bastion host.

[root@project-bastionserver sudoers.d]# cat project_sudoers
%devgroup1 ALL=(ALL) ALL
itsectool ALL=(ALL) ALL
citool ALL=(ALL) ALL

Output

[ansible@project-adminserver ansible]$ ansible-playbook plays/project.yml -K --tags add_sudoers --limit project-bastionserver
BECOME password:

</snip>

PLAY [bastion] *******************************************************************************************************

TASK [Gathering Facts] ***********************************************************************************************

TASK [create-project-accounts : Add sudoers] *************************************************************************
included: /project/ansible/plays/roles/create-project-accounts/tasks/add-sudoers.yml for project-bastionserver

TASK [create-project-accounts : What's wrong] ************************************************************************
ok: [project-bastionserver] =>
  group_names:
  - bastion

TASK [create-project-accounts : Remove old style sudoers.d files] ****************************************************
ok: [project-bastionserver] => (item=citool)
ok: [project-bastionserver] => (item=itsectool)
ok: [project-bastionserver] => (item=devgroup1)
ok: [project-bastionserver] => (item=developer1)

TASK [create-project-accounts : Remove existing project sudoers.d file] **********************************************
changed: [project-bastionserver]

TASK [create-project-accounts : Add lines to project sudoers.d file] *************************************************
changed: [project-bastionserver] => (item=%devgroup1 ALL=(ALL) ALL)
changed: [project-bastionserver] => (item=itsectool ALL=(ALL) ALL)
changed: [project-bastionserver] => (item=citool ALL=(ALL) ALL)

</snip>

PLAY RECAP ***********************************************************************************************************
project-bastionserver         : ok=6    changed=2    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0

Upvotes: 0

Views: 1835

Answers (1)

Vladimir Botka
Vladimir Botka

Reputation: 67959

Two notes on using tests in Ansible conditions

  1. In YAML, a value starting with a quote shall be ended with the same kind of quote. The condition below
      when: 'c' in l

causes a well-described syntax error

... It seems that there is a value started with a quote, and the YAML parser is expecting to see the line ended with the same kind of quote. ...

The correct syntax (suggested by the error message) is to quote the whole expression

      when: "'c' in l"

You can omit the quotation wrapping and put the expression into a Scalar block. This makes the expression easier to read, e.g.

    - debug:
        msg: "c is and x is not in the list {{ l }}"
      when: >
        'c' in l and
        'x' not in l

If the expression doesn't start with a quote (either single or double) you don't have to wrap it in quotes. The condition below is correct

      when: l is contains 'c'
  1. There is a difference in applying Jinja tests and Ansible tests. In Jinja tests, is is not needed.

For example, given the list

  l: [a, b, c]

a) The Jinja test in

    - debug:
        msg: "{{ item }} is in the list {{ l }}"
      loop: [x, y, c]
      when: item in l

    - debug:
        msg: "{{ item }} is not in the list {{ l }}"
      loop: [x, y, c]
      when: item not in l

give

TASK [debug] *********************************************************************************
skipping: [localhost] => (item=x) 
skipping: [localhost] => (item=y) 
ok: [localhost] => (item=c) => 
  msg: c is in the list ['a', 'b', 'c']

TASK [debug] *********************************************************************************
ok: [localhost] => (item=x) => 
  msg: x is not in the list ['a', 'b', 'c']
ok: [localhost] => (item=y) => 
  msg: y is not in the list ['a', 'b', 'c']
skipping: [localhost] => (item=c)

b) You can use is if you want to. The tasks below give the same results

    - debug:
        msg: "{{ item }} is in the list {{ l }}"
      loop: [x, y, c]
      when: item is in l

    - debug:
        msg: "{{ item }} is not in the list {{ l }}"
      loop: [x, y, c]
      when: item is not in l

c) In Ansible tests, like contains is is mandatory

    - debug:
        msg: "{{ item }} is in the list {{ l }}"
      loop: [x, y, c]
      when: l is contains item

    - debug:
        msg: "{{ item }} is not in the list {{ l }}"
      loop: [x, y, c]
      when: l is not contains item

give

TASK [debug] *********************************************************************************
skipping: [localhost] => (item=x) 
skipping: [localhost] => (item=y) 
ok: [localhost] => (item=c) => 
  msg: c is in the list ['a', 'b', 'c']

TASK [debug] *********************************************************************************
ok: [localhost] => (item=x) => 
  msg: x is not in the list ['a', 'b', 'c']
ok: [localhost] => (item=y) => 
  msg: y is not in the list ['a', 'b', 'c']
skipping: [localhost] => (item=c)

d) In an Ansible test if you omit is, e.g.

    - debug:
        msg: "{{ item }} is in the list {{ l }}"
      loop: [x, y, c]
      when: l contains item

the task will fail

The error was: template error while templating string: expected token 'end of statement block', got 'contains'.


Upvotes: 0

Related Questions