diff --git a/changelogs/fragments/end-host-meta-task.yaml b/changelogs/fragments/end-host-meta-task.yaml new file mode 100644 index 00000000000..83cbe81b5ea --- /dev/null +++ b/changelogs/fragments/end-host-meta-task.yaml @@ -0,0 +1,2 @@ +minor_changes: + - Add new meta task end_host - https://github.com/ansible/ansible/issues/40904 diff --git a/lib/ansible/modules/utilities/helper/meta.py b/lib/ansible/modules/utilities/helper/meta.py index 3de3f6bf8a2..150c61e5aaf 100644 --- a/lib/ansible/modules/utilities/helper/meta.py +++ b/lib/ansible/modules/utilities/helper/meta.py @@ -39,7 +39,8 @@ options: - "C(clear_host_errors) (added in 2.1) clears the failed state (if any) from hosts specified in the play's list of hosts." - "C(end_play) (added in 2.2) causes the play to end without failing the host(s). Note that this affects all hosts." - "C(reset_connection) (added in 2.3) interrupts a persistent connection (i.e. ssh + control persist)" - choices: ['flush_handlers', 'refresh_inventory', 'noop', 'clear_facts', 'clear_host_errors', 'end_play', 'reset_connection'] + - "C(end_host) (added in 2.8) is a per-host variation of C(end_play). Causes the play to end for the current host without failing it." + choices: ['flush_handlers', 'refresh_inventory', 'noop', 'clear_facts', 'clear_host_errors', 'end_play', 'reset_connection', 'end_host'] required: true notes: - C(meta) is not really a module nor action_plugin as such it cannot be overwritten. @@ -77,4 +78,10 @@ EXAMPLES = ''' - user: name={{ansible_user}} groups=input - name: reset ssh connection to allow user changes to affect 'current login user' meta: reset_connection + +- name: End the play for hosts that run CentOS 6 + meta: end_host + when: + - ansible_distribution == 'CentOS' + - ansible_distribution_major_version == '6' ''' diff --git a/lib/ansible/plugins/strategy/__init__.py b/lib/ansible/plugins/strategy/__init__.py index 6a5d86e69a7..26f359f0a99 100644 --- a/lib/ansible/plugins/strategy/__init__.py +++ b/lib/ansible/plugins/strategy/__init__.py @@ -1067,6 +1067,13 @@ class StrategyBase: if host.name not in self._tqm._unreachable_hosts: iterator._host_states[host.name].run_state = iterator.ITERATING_COMPLETE msg = "ending play" + elif meta_action == 'end_host': + if _evaluate_conditional(target_host): + iterator._host_states[target_host.name].run_state = iterator.ITERATING_COMPLETE + msg = "ending play for %s" % target_host.name + else: + skipped = True + msg = "end_host conditional evaluated to false, continuing execution for %s" % target_host.name elif meta_action == 'reset_connection': all_vars = self._variable_manager.get_vars(play=iterator._play, host=target_host, task=task) templar = Templar(loader=self._loader, variables=all_vars) diff --git a/lib/ansible/plugins/strategy/linear.py b/lib/ansible/plugins/strategy/linear.py index 203e019a11f..6db63dc558c 100644 --- a/lib/ansible/plugins/strategy/linear.py +++ b/lib/ansible/plugins/strategy/linear.py @@ -265,7 +265,7 @@ class StrategyModule(StrategyBase): # for the linear strategy, we run meta tasks just once and for # all hosts currently being iterated over rather than one host results.extend(self._execute_meta(task, play_context, iterator, host)) - if task.args.get('_raw_params', None) not in ('noop', 'reset_connection'): + if task.args.get('_raw_params', None) not in ('noop', 'reset_connection', 'end_host'): run_once = True if (task.any_errors_fatal or run_once) and not task.ignore_errors: any_errors_fatal = True diff --git a/test/integration/targets/meta_tasks/aliases b/test/integration/targets/meta_tasks/aliases new file mode 100644 index 00000000000..b59832142f2 --- /dev/null +++ b/test/integration/targets/meta_tasks/aliases @@ -0,0 +1 @@ +shippable/posix/group3 diff --git a/test/integration/targets/meta_tasks/runme.sh b/test/integration/targets/meta_tasks/runme.sh new file mode 100755 index 00000000000..1549b4c5788 --- /dev/null +++ b/test/integration/targets/meta_tasks/runme.sh @@ -0,0 +1,23 @@ +#!/usr/bin/env bash + +set -eux + +# test end_host meta task, with when conditional +for test_strategy in linear free; do + out="$(ansible-playbook test_end_host.yml -i ../../inventory -e test_strategy=$test_strategy -vv "$@")" + + grep -q "META: end_host conditional evaluated to false, continuing execution for testhost" <<< "$out" + grep -q "META: ending play for testhost2" <<< "$out" + grep -q "play not ended for testhost" <<< "$out" + grep -qv "play not ended for testhost2" <<< "$out" +done + +# test end_host meta task, on all hosts +for test_strategy in linear free; do + out="$(ansible-playbook test_end_host_all.yml -i ../../inventory -e test_strategy=$test_strategy -vv "$@")" + + grep -q "META: ending play for testhost" <<< "$out" + grep -q "META: ending play for testhost2" <<< "$out" + grep -qv "play not ended for testhost" <<< "$out" + grep -qv "play not ended for testhost2" <<< "$out" +done diff --git a/test/integration/targets/meta_tasks/test_end_host.yml b/test/integration/targets/meta_tasks/test_end_host.yml new file mode 100644 index 00000000000..131d4d4e265 --- /dev/null +++ b/test/integration/targets/meta_tasks/test_end_host.yml @@ -0,0 +1,14 @@ +- name: "Testing end_host with strategy={{ test_strategy | default('linear') }}" + hosts: + - testhost + - testhost2 + gather_facts: no + strategy: "{{ test_strategy | default('linear') }}" + tasks: + - debug: + + - meta: end_host + when: "host_var_role_name == 'role2'" # end play for testhost2, see test/integration/inventory + + - debug: + msg: "play not ended for {{ inventory_hostname }}" diff --git a/test/integration/targets/meta_tasks/test_end_host_all.yml b/test/integration/targets/meta_tasks/test_end_host_all.yml new file mode 100644 index 00000000000..dab5e881fa4 --- /dev/null +++ b/test/integration/targets/meta_tasks/test_end_host_all.yml @@ -0,0 +1,13 @@ +- name: "Testing end_host all hosts with strategy={{ test_strategy | default('linear') }}" + hosts: + - testhost + - testhost2 + gather_facts: no + strategy: "{{ test_strategy | default('linear') }}" + tasks: + - debug: + + - meta: end_host + + - debug: + msg: "play not ended {{ inventory_hostname }}"