84

I want to exit without an error (I know about assert and fail modules) when I meet a certain condition. The following code exits but with a failure:

  tasks:

    - name: Check if there is something to upgrade
      shell: if apt-get --dry-run upgrade | grep -q "0 upgraded, 0 newly installed, 0 to remove and 0 not upgraded"; then echo "no"; else echo "yes"; fi
      register: upgrading

    - name: Exit if nothing to upgrade
      fail: msg="Nothing to upgrade"
      when: upgrading.stdout == "no"
Chirlo
  • 5,989
  • 1
  • 29
  • 45
jreisinger
  • 1,493
  • 1
  • 10
  • 21

7 Answers7

144

Since Ansible 2.2, you can use end_play with the meta module:

- meta: end_play

You can also specify when for conditionally ending the play:

- meta: end_play
  when: upgrading.stdout == "no"

Note, though, that the task is not listed in the output of ansible-playbook, regardless of whether or not the play actually ends. Also, the task is not counted in the recap. So, you could do something like:

- block:
    - name: "end play if nothing to upgrade"
      debug:
        msg: "nothing to upgrade, ending play"

    - meta: end_play
  when: upgrading.stdout == "no"

which will announce the end of the play right before ending it, only when the condition is met. If the condition is not met, you'll see the task named end play if nothing to upgrade appropriately skipped, which would provide more info to the user as to why the play is, or is not, ending.

Of course, this will only end the current play and not all remaining plays in the playbook.


UPDATE June 20 2019:

As reto mentions in comments, end_play ends the play for all hosts. In Ansible 2.8, end_host was added to meta:

end_host (added in Ansible 2.8) is a per-host variation of end_play. Causes the play to end for the current host without failing it.


UPDATE Feb 2021: fixed broken link to meta module

snapfla
  • 1,550
  • 1
  • 8
  • 6
  • 2
    Great find! Unfortunately, `end_play` ends the entire playbook, even when used in a sub playbook called from a main playbook using e.g. `import_tasks`. – ssc Jan 21 '18 at 17:09
  • 1
    @ssc You are mixing up tasks and playbooks importing. With `import_tasks` execution remains in the same play so it is supposed to be ended with that command. To import separate playbook use `import_playbook` statement instead. – Yaroslav Shabalin Jan 25 '18 at 08:52
  • 2
    Important: end_play seems to stop the execution on all hosts, not just the one where the condition is matched! https://github.com/ansible/ansible/issues/27973 – reto May 15 '18 at 16:51
  • If you run the playbook using `-vv`, you'll see **META: ending play** in the output, if it stops. – William Turrell Dec 23 '18 at 18:37
  • 3
    This is nice but if `end_host` is used inside a `role` and if you have a playbook with `roles:` then it ends the whole playbook for the host. Isn't it possible to just skip to next role in the list? Is this worth a new question? :) – Evren Yurtesen Jan 15 '20 at 08:14
12

Just a little note: meta: end_play ends just the play, not a playbook. So this playbook:

---
- name: 1st play with end play
  hosts: localhost
  connection: local
  gather_facts: no
  tasks:
    - name: I'll always be printed
      debug:
        msg: next task terminates first play

    - name: Ending the 1st play now
      meta: end_play

    - name: I want to be printed!
      debug:
        msg: However I'm unreachable so this message won't appear in the output

- name: 2nd play
  hosts: localhost
  connection: local
  gather_facts: no
  tasks:
    - name: I will also be printed always
      debug:
        msg: "meta: end_play ended just the 1st play. This is 2nd one."

will produce this output:

$ ansible-playbook -i localhost, playbooks/end_play.yml 

PLAY [1st play with end play] **************************************************

TASK [I'll always be printed] **************************************************
ok: [localhost] => {
    "msg": "next task terminates first play"
}

PLAY [2nd play] ****************************************************************

TASK [I will also be printed always] *******************************************
ok: [localhost] => {
    "msg": "meta: end_play ended just the 1st play. This is 2nd one."
}

PLAY RECAP *********************************************************************
localhost                  : ok=2    changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0
djasa
  • 121
  • 1
  • 2
  • 1
    This does not provide an answer to the question. Once you have sufficient [reputation](https://stackoverflow.com/help/whats-reputation) you will be able to [comment on any post](https://stackoverflow.com/help/privileges/comment); instead, [provide answers that don't require clarification from the asker](https://meta.stackexchange.com/questions/214173/why-do-i-need-50-reputation-to-comment-what-can-i-do-instead). - [From Review](/review/low-quality-posts/24278009) – Rob Oct 10 '19 at 20:26
  • 1
    Hello Rob, yes. I'm not able to comment, I wasn't aware of ability to edit answers of other people and I was missing this piece of information under this question. – djasa Oct 11 '19 at 23:27
  • 2
    How do you get the entire playbook to end? – vab2048 Oct 31 '21 at 21:13
5

A better and more logical way to solve it may be to do the reverse and rather than fail if there is nothing to upgrade (which is a separate step that does only that) you could append all your upgrading tasks with a conditional depending on the upgrade variable. In essence just add

when: upgrading.changed

to tasks that should be only executed during an upgrade.

It is a bit more work, but it also brings clarity and self contains the logic that affects given task within itself, rather than depending on something way above which may or may not terminate it early.

Tymoteusz Paul
  • 2,732
  • 17
  • 20
  • 3
    This is inconvenient when there is lots of tasks after the one which is to decide if to continue or not :-/ – jhutar Oct 06 '16 at 19:39
  • 4
    Well, this could also be combined with a block and when statement, effectively grouping tasks to be run or skipped. Which should drastically reduce the inconvenience. – Mitms Jan 07 '19 at 08:29
  • 2
    This approach also works well with `include_tasks` or `import_tasks` to keep the output clean, so there aren't a bunch of skipped tasks every time. – thinkmassive May 23 '19 at 22:04
5

Lets use what Tymoteusz suggested for roles:

Split your play into two roles where first role will execute the check (and sets some variable holding check's result) and second one will act based on result of the check.

I have created aaa.yaml with this content:

---
- hosts: all
  remote_user: root
  roles:
    - check
    - { role: doit, when: "check.stdout == '0'" }
...

then role check in roles/check/tasks/main.yaml:

---
- name: "Check if we should continue"
  shell:
    echo $(( $RANDOM % 2 ))
  register: check
- debug:
    var: check.stdout
...

and then role doit in roles/doit/tasks/main.yaml:

---
- name: "Do it only on systems where check returned 0"
  command:
    date
...

And this was the output:

TASK [check : Check if we should continue] *************************************
Thursday 06 October 2016  21:49:49 +0200 (0:00:09.800)       0:00:09.832 ****** 
changed: [capsule.example.com]
changed: [monitoring.example.com]
changed: [satellite.example.com]
changed: [docker.example.com]

TASK [check : debug] ***********************************************************
Thursday 06 October 2016  21:49:55 +0200 (0:00:05.171)       0:00:15.004 ****** 
ok: [monitoring.example.com] => {
    "check.stdout": "0"
}
ok: [satellite.example.com] => {
    "check.stdout": "1"
}
ok: [capsule.example.com] => {
    "check.stdout": "0"
}
ok: [docker.example.com] => {
    "check.stdout": "0"
}

TASK [doit : Do it only on systems where check returned 0] *********************
Thursday 06 October 2016  21:49:55 +0200 (0:00:00.072)       0:00:15.076 ****** 
skipping: [satellite.example.com]
changed: [capsule.example.com]
changed: [docker.example.com]
changed: [monitoring.example.com]

It is not perfect: looks like you will keep seeing skipping status for all tasks for skipped systems, but might do the trick.

jhutar
  • 1,369
  • 2
  • 17
  • 32
1

The following was helpful in my case, as meta: end_play seems to stop the execution for all hosts, not just the one that matches.

First establish a fact:

- name: Determine current version
  become: yes
  slurp:
    src: /opt/app/CHECKSUM
  register: version_check
  ignore_errors: yes

- set_fact:
  is_update_needed: "{{ ( version_check['checksum'] | b64decode != installer_file.stat.checksum)  }}"

Now include that part that should only executed on this condition:

# update-app.yml can be placed in the same role folder
- import_tasks: update-app.yml
  when: is_update_needed
reto
  • 16,189
  • 7
  • 53
  • 67
0

You can do this making assert condition false

- name: WE ARE DONE. EXITING
  assert:
    that:
      - "'test' in 'stop'
0

Running into the same issue, ugly as it might look to you, I decided temporarily to tweak the code to allow for a end_playbook value for the ansible.builtin.meta plugin. So I added below condition to file /path/to/python.../site-packages/ansible/plugins/strategy/__init__.py:

        elif meta_action == 'end_playbook':
            if _evaluate_conditional(target_host):
                for host in self._inventory.get_hosts(iterator._play.hosts):
                    if host.name not in self._tqm._unreachable_hosts:
                        iterator.set_run_state_for_host(host.name, IteratingStates.COMPLETE)
                        sys.exit(0)
                msg = "ending playbook"
            else:
                skipped = True
                skip_reason += ', continuing play'

As you can see, pretty simple end effective to stop the entire process using sys.exit(0).

Here's an example playbook to test with:

- name: test
  hosts: localhost
  connection: local
  gather_facts: no
  become: no
  tasks:
    - meta: end_playbook
      when: true
    
    - fail:

- name: test2
  hosts: localhost
  connection: local
  gather_facts: no
  become: no
  tasks:    
    - debug:
        msg: hi
PLAY [test] ****************

TASK [meta] ****************

When I switch to when: false, it skips to next task.

PLAY [test] ********************

TASK [meta] ********************
2023-01-05 15:04:39 (0:00:00.021)       0:00:00.021 *************************** 
skipping: [localhost]

TASK [fail] ********************
2023-01-05 15:04:39 (0:00:00.013)       0:00:00.035 *************************** 
fatal: [localhost]: FAILED! => changed=false 
  msg: Failed as requested from task

PLAY RECAP *****************
localhost                  : ok=0    changed=0    unreachable=0    failed=1    skipped=0    rescued=0    ignored=0   
fabiog1901
  • 352
  • 3
  • 12