How to see output in one place, from job invocations?

Is there a way to see the output from each host, in one place, from a job?
So instead of having to click on each one in the list, we want to be able to see the output from all of them, in one place. I’ve tried generating a report but that doesnt show much either.

Report:

We’ll be running jobs against 90,000 hosts in some cases…

As far as I know not in the ui. We’re currently working on a redesign that would, among other things, allow you to view outputs of multiple hosts at once, but I don’t think it will be feasible to watch 90k hosts at once even when the redesign is done.

However, there is the /api/job_invocations/:id/outputs api, which allows you to get outputs of multiple (even all) hosts within a job. So either this or the report is probably your best bet. You ca build your own on top of the api or change the template yourself, depending on what you exactly want to see.

2 Likes

Ok thanks. Sounds awesome.
Its the number 1 thing all our users ask for. Since rarely do we run on one host at a time. Even when you run a job on 5 hosts, the combined output would be nice.

Ya, for 90k hosts it might be a bit much, but lots of times we are simply trying to get a single salt grain back from them, and could easily jq/grep through a unified output to get what we want.

Thanks for the info about the endpoint. Ill see what I can do with it.

Side note, with Broadcom buying out vmware, and increasing the costs of SaltStack UI (now VMware Aria) have gone up 10x. I think you’ll be seeing an increase in people looking for better/other options. Our cost savings where I am, moving from SaltStack to Foreman is over $7M. So, thank you for all you do.

@aruzicka Are you saying that you think the job invocation report could be changed to show the output from each job? The documentation is a bit lacking, and Im trying to figure out where to start. Would the job invocation template actually be able to get the template invocation id for each jid? Then spit out the standard error/out, etc?

I see in the help section: sub_task_for_host template_invocations_hosts for JobInvocation. So it seems like its possible, but Im having a heck of a time figuring it out.

Here is what I have so far:

name: Job - Detailed Invocation Report
snippet: false
template_inputs:
- name: job_id
  required: true
  input_type: user
  description: Id of job invocation to report
  advanced: false
  value_type: plain
  resource_type: JobInvocation
  hidden_value: false
- name: hosts
  required: false
  input_type: user
  description: Field for filter hosts of job invocation. Leave blank for all hosts.
  advanced: false
  value_type: search
  resource_type: Host
  hidden_value: false
model: ReportTemplate
require:
- plugin: foreman_remote_execution
  version: 4.4.0
-%>
<%- report_headers 'Host', 'Task ID', 'Action', 'stdout', 'stderr', 'debug', 'Result', 'Finished' -%>
<%- invocation = find_job_invocation_by_id(input('job_id')) -%>
<%- parts = ["job_invocation.id = #{input('job_id')}"] -%>
<%- parts << input('hosts') unless input('hosts').blank? -%>
<%- search = parts.map { |part| '(' + part + ')' }.join(' AND ') -%>
<%- load_hosts(search: search).each do |batch| -%>
  <%- batch.each do |host| -%>
    <%- task = invocation.sub_task_for_host(host) -%>
    <%- if task.present? -%>
      <%- outputs = { 'stdout' => [], 'stderr' => [], 'debug' => [] }  -%>
      <%- task.action_continuous_output.each do |output| -%>
        <%- outputs[output['output_type']] << output['output'] -%>
      <%- end -%>
      <%- report_row(
              'Host': host.name,
              'Task ID': task.id,
              'Action': task.action,
              'stdout': join_with_line_break(outputs['stdout']),
              'stderr': join_with_line_break(outputs['stderr']),
              'debug': join_with_line_break(outputs['debug']),
              'Result': task.result,
              'Finished': task.ended_at.blank? ? nil : format_time(task.ended_at),
          ) -%>
    <%- else -%>
      <%- report_row(
              'Host': host.name,
              'Task ID': 'N/A',
              'Action': 'N/A',
              'stdout': 'No Task Found',
              'stderr': 'No Task Found',
              'debug': 'No Task Found',
              'Result': 'N/A',
              'Finished': 'N/A',
          ) -%>
    <%- end -%>
  <%- end -%>
<%- end -%>
<%= report_render -%>

Its printing out the task_id now, but that doesnt seem to provide information on the output of each task/job.

Any help would be awesome.

Aha! Its working. Now I just need to make the output look a little nicer:

name: Job - Detailed Invocation Report with Debugging
snippet: false
template_inputs:
- name: job_id
  required: true
  input_type: user
  description: Id of job invocation to report
  advanced: false
  value_type: plain
  resource_type: JobInvocation
  hidden_value: false
- name: hosts
  required: false
  input_type: user
  description: Field for filter hosts of job invocation. Leave blank for all hosts.
  advanced: false
  value_type: search
  resource_type: Host
  hidden_value: false
model: ReportTemplate
require:
- plugin: foreman_remote_execution
  version: 4.4.0
-%>
<%- report_headers 'Host', 'Task ID', 'Action', 'stdout', 'stderr', 'debug', 'Result', 'Finished', 'SubTask Inspection' -%>
<%- invocation = find_job_invocation_by_id(input('job_id')) -%>
<%- parts = ["job_invocation.id = #{input('job_id')}"] -%>
<%- parts << input('hosts') unless input('hosts').blank? -%>
<%- search = parts.map { |part| '(' + part + ')' }.join(' AND ') -%>
<%- load_hosts(search: search).each do |batch| -%>
  <%- batch.each do |host| -%>
    <%- # Use sub_task_for_host to retrieve the sub-task -%>
    <%- sub_task = invocation.sub_task_for_host(host) -%>
    <%- if sub_task.present? -%>
      <%- # Initialize outputs hash -%>
      <%- outputs = { 'stdout' => [], 'stderr' => [], 'debug' => [] } -%>
      <%- # Attempt to access outputs -%>
      <%- if sub_task.respond_to?(:action_continuous_output) && sub_task.action_continuous_output.present? -%>
        <%- sub_task.action_continuous_output.each do |output| -%>
          <%- outputs[output['output_type']] << output['output'] -%>
        <%- end -%>
      <%- else -%>
        <%- # If action_continuous_output is not available, attempt alternative methods or note absence -%>
        <%- outputs['stdout'] << 'No stdout output available' -%>
        <%- outputs['stderr'] << 'No stderr output available' -%>
        <%- outputs['debug'] << 'No debug output available' -%>
      <%- end -%>
      <%- # Report row with an additional inspection column -%>
      <%- report_row(
              'Host': host.name,
              'Task ID': sub_task.id,
              'Action': sub_task.action,
              'stdout': join_with_line_break(outputs['stdout']),
              'stderr': join_with_line_break(outputs['stderr']),
              'debug': join_with_line_break(outputs['debug']),
              'Result': sub_task.result,
              'Finished': sub_task.ended_at.blank? ? nil : format_time(sub_task.ended_at),
              'SubTask Inspection': sub_task.inspect
          ) -%>
    <%- else -%>
      <%- report_row(
              'Host': host.name,
              'Task ID': 'N/A',
              'Action': 'N/A',
              'stdout': 'No Task Found',
              'stderr': 'No Task Found',
              'debug': 'No Task Found',
              'Result': 'N/A',
              'Finished': 'N/A',
              'SubTask Inspection': 'No SubTask Available'
          ) -%>
    <%- end -%>
  <%- end -%>
<%- end -%>
<%= report_render -%>

Output:


e[0;32m10-173-26-210.ssnc-corp.cloud:e[0;0m
e[0;36m----------e[0;0m
    e[0;36m      ID: run_commande[0;0m
    e[0;36mFunction: cmd.rune[0;0m
    e[0;36m    Name: cat /etc/salt/minion.d/99-metadata-grains.conf|grep -i patching_groupe[0;0m
    e[0;36m  Result: Truee[0;0m
    e[0;36m Comment: Command "cat /etc/salt/minion.d/99-metadata-grains.conf|grep -i patching_group" rune[0;0m
    e[0;36m Started: 14:27:39.229273e[0;0m
    e[0;36mDuration: 16.323 mse[0;0m
e[0;36m     Changes:   
              e[0;36m----------e[0;0m
              e[0;36mpide[0;0m:
                  e[0;1;33m1051484e[0;0m
              e[0;36mretcodee[0;0m:
                  e[0;1;33m0e[0;0m
              e[0;36mstderre[0;0m:
              e[0;36mstdoute[0;0m:
                  e[0;32m  ssnc_patching_group: 3rd-sun-2am-5ame[0;0me[0;0m
e[0;36m
Summary for 10-173-26-210.ssnc-corp.cloud
------------e[0;0m
e[0;32mSucceeded: 1e[0;0m (e[0;32mchanged=1e[0;0m)
e[0;36mFailed:    0e[0;0m
e[0;36m------------
Total states run:     1e[0;0m
e[0;36mTotal run time:  16.323 mse[0;0m

Exit status: 0```

WooHoo. Just gotta strip the ANSI characters. I hope others find this helpful. Thanks again for the reply Adam. This will work perfectly for our needs.

name: Job - Detailed Invocation Report without ANSI Codes
snippet: false
template_inputs:
  - name: job_id
    required: true
    input_type: user
    description: Id of job invocation to report
    advanced: false
    value_type: plain
    resource_type: JobInvocation
    hidden_value: false
  - name: hosts
    required: false
    input_type: user
    description: Field for filter hosts of job invocation. Leave blank for all hosts.
    advanced: false
    value_type: search
    resource_type: Host
    hidden_value: false
model: ReportTemplate
require:
  - plugin: foreman_remote_execution
    version: 4.4.0
-%>

<%# Define a helper method to strip ANSI codes %>
<% def strip_ansi_codes(text)
     text.to_s.gsub(/\e\[[0-9;]*m/, '')
   end %>

<%- report_headers 'Host', 'Task ID', 'Action', 'stdout', 'stderr', 'debug', 'Result', 'Finished' -%>
<%- invocation = find_job_invocation_by_id(input('job_id')) -%>
<%- parts = ["job_invocation.id = #{input('job_id')}"] -%>
<%- parts << input('hosts') unless input('hosts').blank? -%>
<%- search = parts.map { |part| '(' + part + ')' }.join(' AND ') -%>
<%- load_hosts(search: search).each do |batch| -%>
  <%- batch.each do |host| -%>
    <%- sub_task = invocation.sub_task_for_host(host) -%>
    <%- if sub_task.present? -%>
      <%- outputs = { 'stdout' => [], 'stderr' => [], 'debug' => [] } -%>
      <%- if sub_task.respond_to?(:action_continuous_output) && sub_task.action_continuous_output.present? -%>
        <%- sub_task.action_continuous_output.each do |output| -%>
          <%- outputs[output['output_type']] << output['output'] -%>
        <%- end -%>
      <%- else -%>
        <%- outputs['stdout'] << 'No stdout output available' -%>
        <%- outputs['stderr'] << 'No stderr output available' -%>
        <%- outputs['debug'] << 'No debug output available' -%>
      <%- end -%>
      <%- report_row(
              'Host': host.name,
              'Task ID': sub_task.id,
              'Action': sub_task.action,
              'stdout': strip_ansi_codes(join_with_line_break(outputs['stdout'])),
              'stderr': strip_ansi_codes(join_with_line_break(outputs['stderr'])),
              'debug': strip_ansi_codes(join_with_line_break(outputs['debug'])),
              'Result': sub_task.result,
              'Finished': sub_task.ended_at.blank? ? nil : format_time(sub_task.ended_at),
          ) -%>
    <%- else -%>
      <%- report_row(
              'Host': host.name,
              'Task ID': 'N/A',
              'Action': 'N/A',
              'stdout': 'No Task Found',
              'stderr': 'No Task Found',
              'debug': 'No Task Found',
              'Result': 'N/A',
              'Finished': 'N/A',
          ) -%>
    <%- end -%>
  <%- end -%>
<%- end -%>
<%= report_render -%>
3 Likes