From 08a71d148e2b26e1e2b3102cd616d23650305741 Mon Sep 17 00:00:00 2001 From: David Shrewsbury Date: Tue, 9 Jun 2026 12:16:31 -0400 Subject: [PATCH] Fix IndexError in free strategy when hosts become unreachable (#87037) * Fix IndexError in free strategy when hosts become unreachable Fixes an IndexError crash in the free strategy plugin when hosts become unreachable during playbook execution. The bug occurred when: - last_host index persists across outer loop iterations - hosts_left is regenerated each iteration via get_hosts_left() - Some hosts become unreachable between iterations - hosts_left shrinks but last_host retains its previous value - Accessing hosts_left[last_host] raises IndexError The fix adds a bounds check after regenerating hosts_left to reset last_host to 0 if it's out of bounds. This preserves the round-robin fairness algorithm while preventing the IndexError. Assisted-by: Claude Sonnet 4.5 * integration test * review comment fixes * ci_complete --- .../fragments/87027-free-strategy-indexerror.yml | 2 ++ lib/ansible/plugins/strategy/free.py | 5 +++++ test/integration/targets/strategy_free/free_hosts | 4 ++++ .../targets/strategy_free/free_index_error.yml | 11 +++++++++++ test/integration/targets/strategy_free/runme.sh | 6 ++++++ 5 files changed, 28 insertions(+) create mode 100644 changelogs/fragments/87027-free-strategy-indexerror.yml create mode 100644 test/integration/targets/strategy_free/free_hosts create mode 100644 test/integration/targets/strategy_free/free_index_error.yml diff --git a/changelogs/fragments/87027-free-strategy-indexerror.yml b/changelogs/fragments/87027-free-strategy-indexerror.yml new file mode 100644 index 00000000000..7649bc0548e --- /dev/null +++ b/changelogs/fragments/87027-free-strategy-indexerror.yml @@ -0,0 +1,2 @@ +bugfixes: + - free strategy - Fix ``IndexError`` when hosts become unreachable during playbook execution (https://github.com/ansible/ansible/issues/87027). diff --git a/lib/ansible/plugins/strategy/free.py b/lib/ansible/plugins/strategy/free.py index b267fa21ca1..25370b04222 100644 --- a/lib/ansible/plugins/strategy/free.py +++ b/lib/ansible/plugins/strategy/free.py @@ -92,6 +92,11 @@ class StrategyModule(StrategyBase): result = False break + # Reset last_host if it's out of bounds for the current hosts_left + # This can happen when hosts become unreachable between iterations + if last_host >= len(hosts_left): + last_host = 0 + work_to_do = False # assume we have no more work to do starting_host = last_host # save current position so we know when we've looped back around and need to break diff --git a/test/integration/targets/strategy_free/free_hosts b/test/integration/targets/strategy_free/free_hosts new file mode 100644 index 00000000000..80f771600d2 --- /dev/null +++ b/test/integration/targets/strategy_free/free_hosts @@ -0,0 +1,4 @@ +[local] +host0 ansible_connection=local ansible_python_interpreter="{{ ansible_playbook_python }}" +host1 ansible_connection=ssh ansible_host=127.0.0.1 ansible_port=1011 # IANA Reserved port +host2 ansible_connection=local ansible_python_interpreter="{{ ansible_playbook_python }}" diff --git a/test/integration/targets/strategy_free/free_index_error.yml b/test/integration/targets/strategy_free/free_index_error.yml new file mode 100644 index 00000000000..be3360d1bee --- /dev/null +++ b/test/integration/targets/strategy_free/free_index_error.yml @@ -0,0 +1,11 @@ +--- +- hosts: host0, host1, host2 + strategy: free + gather_facts: false + tasks: + - name: EXPECTED FAILURE - First ping + ping: + throttle: 2 + + - name: Second ping + ping: diff --git a/test/integration/targets/strategy_free/runme.sh b/test/integration/targets/strategy_free/runme.sh index f5b912c6da7..84b70aa6ec0 100755 --- a/test/integration/targets/strategy_free/runme.sh +++ b/test/integration/targets/strategy_free/runme.sh @@ -8,3 +8,9 @@ set +e result="$(ansible-playbook test_last_include_in_always.yml -i inventory "$@" 2>&1)" set -e grep -q "INCLUDED TASK EXECUTED" <<< "$result" + +set +e +result="$(ansible-playbook free_index_error.yml -i free_hosts "$@" 2>&1)" +set -e +grep -q "\[host1\]: UNREACHABLE!" <<< "$result" +! grep -q "IndexError: list index out of range" <<< "$result"