Skip to content

Fixes #36353 - Route Ansible task output per host - #117

Open
jakduch wants to merge 2 commits into
theforeman:masterfrom
jakduch:fix/36353
Open

Fixes #36353 - Route Ansible task output per host#117
jakduch wants to merge 2 commits into
theforeman:masterfrom
jakduch:fix/36353

Conversation

@jakduch

@jakduch jakduch commented Aug 19, 2026

Copy link
Copy Markdown
Contributor

Summary

Fixes #36353.

Ansible emits playbook_on_task_start without a host and follows it with host-specific events carrying the same task_uuid. The runner broadcast every task heading immediately, so a role task that only ran on host A was also shown in host B's job details.

Keep task headings until the subsequent host event identifies each actual target. Publish the heading once for each host that runs or skips the task, followed by that host's result output. Non-task global events and the per-host exit status handling remain unchanged.

Add a focused event-sequence test that verifies the task heading is not broadcast and is published only once for the participating host.

Testing

  • Ruby syntax checks pass
  • Added a focused runner event-routing test; CI runs it with the repository's supported dependency bundle
  • Verified the event relationship against the Ansible 2.12 artifact fixtures from the earlier runner test work

AI usage disclosure

Per the community discussion on AI policy, the issue was investigated and the changes, tests, and PR wording were prepared with the assistance of Codex 5.6 Sol High. The resulting changes were reviewed before submitting. The commit also includes an Assisted-By trailer.

Buffer task headings until host events identify the targets so unrelated role tasks are not broadcast to every host.

Assisted-By: Codex 5.6 Sol High

@adamruzicka adamruzicka left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The general idea seems to be sound. I have some time off coming up and I probably won't have time to properly test this properly.

What I'd like to see tested, even if just manually:

  • a playbook with many hosts, where some plays (or just tasks) are scoped to specific hosts
  • a playbook with many hosts and strategy: free
  • a playbook with many hosts and strategy: serial 1
  • a playbook where a task loops
  • a playbook where a task delegates to a different host

Comment on lines +190 to +194
@task_outputs[task_uuid] = {
'stdout' => event['stdout'],
'uuid' => event['uuid'],
'created' => event['created']
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
@task_outputs[task_uuid] = {
'stdout' => event['stdout'],
'uuid' => event['uuid'],
'created' => event['created']
}
@task_outputs[task_uuid] = event.slice('stdout', 'uuid', 'created')

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay, thanks. I've used event.slice in the follow-up commit.

task_output = @task_outputs&.fetch(task_uuid, nil)
return unless task_output

@published_task_outputs ||= {}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nitpick: This could be a Set

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay, thanks. A Set makes sense here, so I've switched it over.

Comment on lines +147 to +148
if event['event'] == 'playbook_on_task_start' && remember_task_output(event)
# The following host events identify which hosts this task applies to.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a slightly clunky construct, could we split it into two? One function that would check if there is something to be remembered and then another one to actually do the remembering that would be called in the body?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, that makes sense. I've split the predicate from remember_task_output in the follow-up commit.

@jakduch

jakduch commented Aug 20, 2026

Copy link
Copy Markdown
Contributor Author

Thanks for the suggested test cases. I tested this with ansible-runner 2.4.3, Ansible Core 2.19.12 and 2.21.0, using three inventory hosts.

These cases behaved as expected:

Scoped task:    host-a=1, host-b=1, host-c=0
serial: 1:     host-a=1, host-b=1, host-c=1
loop:          host-a=1, host-b=1, host-c=0
delegated task: host-a=1, host-b=1, host-c=0

strategy: free exposed an issue. Its task-start events have an empty stdout, and the TASK [...] banner is included only in the result of the first host that finishes. With the hosts completing in the order host-b, host-c, host-a, I got:

Interleaved task: host-a=0, host-b=1, host-c=0

instead of one task heading per host. The same pattern affects RUNNING HANDLER [...] when a handler runs for multiple hosts with the free strategy.

I'll update the implementation to keep the task pending, capture the banner from the first host result, and publish it for the remaining matching hosts, with regression tests for both cases.

@jakduch

jakduch commented Aug 20, 2026

Copy link
Copy Markdown
Contributor Author

I reran the requested scenarios against 765bca3 with a three-host local inventory and ansible-runner 2.4.3. The main matrix passed with Ansible Core 2.19.12 and 2.21.0; the multi-host handler case was also checked with 2.19.12.

Scoped task:       host-a=1  host-b=1  host-c=0
strategy: free:    host-a=1  host-b=1  host-c=1
serial: 1:         host-a=1  host-b=1  host-c=1
loop task:         host-a=1  host-b=1  host-c=0
delegated task:    host-a=1  host-b=1  host-c=0
free handler:      host-a=1  host-b=1  host-c=0

The free task completed in the order host-b, host-c, host-a. The runner events contained the banner only in the first completed result:

playbook_on_task_start  host=-       stdout=""
runner_on_ok            host=host-b  stdout="TASK [Interleaved task] ... ok: [host-b]"
runner_on_ok            host=host-c  stdout="ok: [host-c]"
runner_on_ok            host=host-a  stdout="ok: [host-a]"

After routing, the per-host output was:

host-b: TASK [Interleaved task] ... ok: [host-b]
host-c: TASK [Interleaved task] ... ok: [host-c]
host-a: TASK [Interleaved task] ... ok: [host-a]

Loops emitted the heading once per participating host, and delegated results stayed attributed to the original hosts (host-a and host-b), not the delegated host-c.

Capture task and handler headings from the first host result when free strategy task-start events contain no output.

Assisted-By: Codex 5.6 Sol High
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants