Different messages for different conditions in one assert

There was discussion during CfgMgmtCamp that it is not possible to have different messages for different conditions in one assert action with ansible.

I do not agree with that!
Here is the code:

- name: Different messages for different conditions in one asserts
  hosts: localhost
  gather_facts: false
  vars:
    one: true
    two: false
  tasks:
    - name: Simple assert
      ansible.builtin.assert:
        that: conditions is ansible.builtin.all
        fail_msg: "One of the conditions failed"
        success_msg: "All of the conditions successful"
      failed_when: false
      vars:
        conditions:
          - "{{ one }}"
          - "{{ two }}"

    - name: Extended assert
      ansible.builtin.assert:
        that: extended_conditions | map(attribute='condition') is ansible.builtin.all
        fail_msg: "Following conditions have failed: {{ extended_conditions | rejectattr('condition') | map(attribute='msg') | join('; ') }}"
        success_msg: "Following conditions are succesful: {{ extended_conditions | selectattr('condition') | map(attribute='msg') | join('; ') }}"
      failed_when: false
      vars:
        extended_conditions:
          - msg: "message"
            condition: "{{ one or two }}"
          - msg: "this condition failed"
            condition: "{{ two }}"

This code work with both ansible-core 2.20 and 2.16 (I’ve also checked other versions, here just for brevity)

bash-5.2$ uv run --with ansible-core==2.16.16 ansible-playbook playbook.yml 
Installed 9 packages in 17ms
[WARNING]: No inventory was parsed, only implicit localhost is available
[WARNING]: provided hosts list is empty, only localhost is available. Note that the implicit localhost does not match 'all'

PLAY [Different messages for different conditions in one asserts] ***************************************************************************

TASK [Simple assert] ************************************************************************************************************************
ok: [localhost] => {
    "assertion": "conditions is ansible.builtin.all",
    "changed": false,
    "evaluated_to": false,
    "failed_when_result": false,
    "msg": "One of the conditions failed"
}

TASK [Extended assert] **********************************************************************************************************************
ok: [localhost] => {
    "assertion": "extended_conditions | map(attribute='condition') is ansible.builtin.all",
    "changed": false,
    "evaluated_to": false,
    "failed_when_result": false,
    "msg": "Following conditions have failed: this condition failed"
}

PLAY RECAP **********************************************************************************************************************************
localhost                  : ok=2    changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0   

bash-5.2$ uv run --with ansible-core==2.20.2 ansible-playbook playbook.yml 
[WARNING]: No inventory was parsed, only implicit localhost is available
[WARNING]: provided hosts list is empty, only localhost is available. Note that the implicit localhost does not match 'all'

PLAY [Different messages for different conditions in one asserts] ***************************************************************************

TASK [Simple assert] ************************************************************************************************************************
ok: [localhost] => {
    "assertion": "conditions is ansible.builtin.all",
    "changed": false,
    "evaluated_to": false,
    "failed_when_result": false,
    "msg": "One of the conditions failed"
}

TASK [Extended assert] **********************************************************************************************************************
ok: [localhost] => {
    "assertion": "extended_conditions | map(attribute='condition') is ansible.builtin.all",
    "changed": false,
    "evaluated_to": false,
    "failed_when_result": false,
    "msg": "Following conditions have failed: this condition failed"
}

PLAY RECAP **********************************************************************************************************************************
localhost                  : ok=2    changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0   


There are some caveats of course, if you look closely, each condition is evaluated as variable (inside double curly brackets), not as condition (with out any curly brackets):

    extended_conditions:
      - msg: "message"
        condition: "{{ one or two }}"
      - msg: "this condition failed"
        condition: "{{ two }}"

But I am yet to see condition that cannot be written this way (as variable)

8 Likes

Thanks for your examples, good catch (and good Jinjas there) !

I was at that same talk, for anyone wondering here are the links to the presentation :

He has a survey up for feedbacks on the Github link above, you should consider linking that post there !

3 Likes

@nbttmbrg Thank you for your kind words.

I am certain that my code can be improved. Any suggestions, comments, bug reports are welcome.

1 Like

FWIW. Optionally, you can put the conditions into a dictionary

    extended_conditions:
      message: "{{ one }}"
      this condition failed: "{{ two }}"

and use it

    - assert:
        that: extended_conditions.values() is all
        fail_msg: "Following conditions failed: {{ _failed }}"
        success_msg: "All conditions passed: {{ _passed }}"
      vars:
        _passed: "{{ extended_conditions.keys()
                     | join('; ') }}"
        _failed: "{{ extended_conditions
                     | dict2items
                     | rejectattr('value')
                     | map(attribute='key')
                     | join('; ') }}"

There are more options. For example,

      vars:
        _failed: "{{ extended_conditions.keys()
                     | difference(_passed)
                     | join('; ') }}"
        _passed: "{{ extended_conditions
                     | dict2items
                     | selectattr('value')
                     | map(attribute='key')
                     | join('; ') }}"

source code

3 Likes

That is for sure an option. Thank you for suggestion.
How would you make messages for conditions to allow templating and be multiline? In your example they are keys. I cannot figure our how to do something like this with keys.

    extended_conditions:
      - msg: >-
             this error message
             is multiline
        condition: "{{ one or two }}"
      - msg: "this condition failed, variable ‘two’ should be True but it is {{ two }}" 
        condition: "{{ two }}"

Maybe you have an example? I think templating should also work in keys and keys might be multiline, but this is dangerous terrotory from my point of view.

This is not possible. In these options, the messages are constants.

Thanks for sharing those.
We will get the videos processed and online soon, one that’s done we will link from the top of

2 Likes

Regarding the assertions: how about making an action plugin that is similar to ansible.builtin.assert, but more flexible? That way you don’t have to copy’n’paste the implementation around, and you can add better validation for the action’s arguments.

I am not fan to implement something that can be implemented by composing existing elements. This is rather simple composition I would argue.

On the other hand I am not fan of rejecting something without considering it properly.

How would api of this plugin look like from your point of view?

Should there be only condition description (like in example I provided)? Or fail and success messages for each condition? Or both?

Should the be option to fail fast?

Using something that can easily be composed is totally fine (and good). But once it reaches a certain complexity, and is used in different places by basically copy’n’pasting things over, like the following boilerplate:

    - ansible.builtin.assert:
        that: extended_conditions | map(attribute='condition') is ansible.builtin.all
        fail_msg: "Following conditions have failed: {{ extended_conditions | rejectattr('condition') | map(attribute='msg') | join('; ') }}"
        success_msg: "Following conditions are succesful: {{ extended_conditions | selectattr('condition') | map(attribute='msg') | join('; ') }}"

I think it’s a lot better to extract this into a plugin that can be more easily reused.

How would api of this plugin look like from your point of view?

Basically similar to yours:

- foo.bar.extended_assert:
    that:
      - msg: "message"
        condition: one or two
      - msg: "this condition failed"
        condition: two  

(Whether you want to evaluate the expressions in the plugin, or ask the user to use {{ }} doesn’t really matter.)

Should there be only condition description (like in example I provided)? Or fail and success messages for each condition? Or both?

Depends what you want to have. You could allow all of them, and pick what’s there, or fall back to something automatically generated if there isn’t.

Should the be option to fail fast?

That could be made configurable.

1 Like

To some extent: - I am him :), in the way that indeed I was the one presenting Style Guide with assert being one of the topics

I will modify one of the slides in GitHub - abacusresearch/ansible-style-guide, to have something like “postfactum”…

Honestly it wouldn’t occur to me to write the code like above - like it would sound too complicated to write that (just) for sake having some better message in assert.

But once it is available - I can copy & paste & change :slight_smile: , thank you @kks @vbotka @felixfontein

Along those lines - maybe one of you can modify ansible.builtin.assert module – Asserts given expressions are true — Ansible Community Documentation by adding e.g. one of the examples. On a general note it would be totally cool in case some examples are more “powerful” , especially in case examples start with simpler ones but then provide more complicated ones