Permalink
Cannot retrieve contributors at this time
Name already in use
A tag already exists with the provided branch name. Many Git commands accept both tag and branch names, so creating this branch may cause unexpected behavior. Are you sure you want to create this branch?
foreman-tasks/lib/foreman_tasks/tasks/export_tasks.rake
Go to fileThis commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
349 lines (307 sloc)
10.3 KB
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# | |
# export_tasks.rake is a debugging tool to extract tasks from the | |
# current foreman instance. | |
# | |
# Run "foreman-rake foreman_tasks:export_tasks" to export tasks | |
require 'csv' | |
namespace :foreman_tasks do | |
desc <<~DESC | |
Export dynflow tasks based on filter. ENV variables: | |
* TASK_SEARCH : scoped search filter (example: 'label = "Actions::Foreman::Host::ImportFacts"') | |
* TASK_FILE : file to export to | |
* TASK_FORMAT : format to use for the export (either html, html-dir or csv) | |
* TASK_DAYS : number of days to go back | |
* SKIP_FAILED : skip tasks that fail to export (true or false[default]) | |
If TASK_SEARCH is not defined, it defaults to all tasks in the past 7 days and | |
all unsuccessful tasks in the past 60 days. The default TASK_FORMAT is html | |
which requires a tar.gz file extension. | |
DESC | |
task :export_tasks => [:environment, 'dynflow:client'] do | |
deprecated_options = { :tasks => 'TASK_SEARCH', | |
:days => 'TASK_DAYS', | |
:export => 'TASK_FILE' } | |
deprecated_options.each do |option, new_option| | |
raise "The #{option} option is deprecated. Please use #{new_option} instead" if ENV.include?(option.to_s) | |
end | |
class TaskRender | |
def initialize | |
@cache = {} | |
end | |
def h(foo) | |
foo | |
end | |
def url(foo) | |
foo | |
end | |
def render_task(task) | |
@plan = task.execution_plan | |
erb('show') | |
end | |
def world | |
ForemanTasks.dynflow.world | |
end | |
def template(filename) | |
File.join(Gem::Specification.find_by_name('dynflow').gem_dir, 'web', 'views', "#{filename}.erb") # rubocop:disable Rails/DynamicFindBy | |
end | |
def erb(file, options = {}) | |
@cache[file] ||= Tilt.new(template(file)) | |
@cache[file].render(self, options[:locals]) | |
end | |
def prettify_value(value) | |
YAML.dump(value) | |
end | |
def prettyprint(value) | |
value = prettyprint_references(value) | |
if value | |
pretty_value = prettify_value(value) | |
<<-HTML | |
<pre class="prettyprint lang-yaml">#{h(pretty_value)}</pre> | |
HTML | |
else | |
'' | |
end | |
end | |
def prettyprint_references(value) | |
case value | |
when Hash | |
value.reduce({}) do |h, (key, val)| | |
h.update(key => prettyprint_references(val)) | |
end | |
when Array | |
value.map { |val| prettyprint_references(val) } | |
when Dynflow::ExecutionPlan::OutputReference | |
value.inspect | |
else | |
value | |
end | |
end | |
def duration_to_s(duration) | |
h('%0.2fs' % duration) | |
end | |
def load_action(step) | |
world.persistence.load_action_for_presentation(@plan, step.action_id, step) | |
end | |
def step_error(step) | |
if step.error | |
['<pre>', | |
"#{h(step.error.message)} (#{h(step.error.exception_class)})\n", | |
h(step.error.backtrace.join("\n")), | |
'</pre>'].join | |
end | |
end | |
def show_world(world_id) | |
if (registered_world = world.coordinator.find_worlds(false, id: world_id).first) | |
'%{world_id} %{world_meta}' % { world_id: world_id, world_meta: registered_world.meta.inspect } | |
else | |
world_id | |
end | |
end | |
def show_action_data(label, value) | |
value_html = prettyprint(value) | |
if !value_html.empty? | |
<<-HTML | |
<p> | |
<b>#{h(label)}</b> | |
#{value_html} | |
</p> | |
HTML | |
else | |
'' | |
end | |
end | |
def atom_css_classes(atom) | |
classes = ['atom'] | |
step = @plan.steps[atom.step_id] | |
case step.state | |
when :success | |
classes << 'success' | |
when :error | |
classes << 'error' | |
when :skipped, :skipping | |
classes << 'skipped' | |
end | |
classes.join(' ') | |
end | |
def flow_css_classes(flow, sub_flow = nil) | |
classes = [] | |
case flow | |
when Dynflow::Flows::Sequence | |
classes << 'sequence' | |
when Dynflow::Flows::Concurrence | |
classes << 'concurrence' | |
when Dynflow::Flows::Atom | |
classes << atom_css_classes(flow) | |
else | |
raise "Unknown run plan #{run_plan.inspect}" | |
end | |
classes << atom_css_classes(sub_flow) if sub_flow.is_a? Dynflow::Flows::Atom | |
classes.join(' ') | |
end | |
def step_css_class(step) | |
case step.state | |
when :success | |
'success' | |
when :error | |
'important' | |
end | |
end | |
def progress_width(step) | |
if step.state == :error | |
100 # we want to show the red bar in full width | |
else | |
step.progress_done * 100 | |
end | |
end | |
def step(step_id) | |
@plan.steps[step_id] | |
end | |
def updated_url(new_params) | |
url('?' + Rack::Utils.build_nested_query(params.merge(new_params.stringify_keys))) | |
end | |
end | |
class PageHelper | |
def self.pagify(io, template = nil) | |
io.write <<~HTML | |
<html> | |
<head> | |
<title>Dynflow Console</title> | |
<script src="jquery.js"></script> | |
<link rel="stylesheet" type="text/css" href="bootstrap.css"> | |
<link rel="stylesheet" type="text/css" href="application.css"> | |
<script src="bootstrap.js"></script> | |
<script src="run_prettify.js"></script> | |
<script src="application.js"></script> | |
</head> | |
<body> | |
HTML | |
if block_given? | |
yield io | |
else | |
io.write template | |
end | |
ensure | |
io.write '</body></html>' | |
end | |
def self.copy_assets(tmp_dir) | |
['vendor/bootstrap/js/bootstrap.js', | |
'vendor/jquery/jquery.js', | |
'vendor/jquery/jquery.js', | |
'javascripts/application.js', | |
'vendor/bootstrap/css/bootstrap.css', | |
'stylesheets/application.css'].each do |file| | |
filename = File.join(Gem::Specification.find_by_name('dynflow').gem_dir, 'web', 'assets', file) # rubocop:disable Rails/DynamicFindBy | |
FileUtils.copy_file(filename, File.join(tmp_dir, File.basename(file))) | |
end | |
end | |
def self.generate_with_index(io) | |
io.write '<div><table class="table">' | |
yield io | |
ensure | |
io.write '</table></div>' | |
end | |
def self.generate_index_entry(io, task) | |
io << <<~HTML | |
<tr> | |
<td><a href=\"#{task.id}.html\">#{task.label}</a></td> | |
<td>#{task.started_at}</td> | |
<td>#{task.duration}</td> | |
<td>#{task.state}</td> | |
<td>#{task.result}</td> | |
</tr> | |
HTML | |
end | |
end | |
def csv_export(export_filename, id_scope, task_scope) | |
CSV.open(export_filename, 'wb') do |csv| | |
csv << %w[id state type label result parent_task_id started_at ended_at duration] | |
id_scope.pluck(:id).each_slice(1000).each do |ids| | |
task_scope.where(id: ids).each do |task| | |
with_error_handling(task) do | |
csv << [task.id, task.state, task.type, task.label, task.result, | |
task.parent_task_id, task.started_at, task.ended_at, task.duration] | |
end | |
end | |
end | |
end | |
end | |
def html_export(workdir, id_scope, task_scope) | |
PageHelper.copy_assets(workdir) | |
ids = id_scope.pluck(:id) | |
renderer = TaskRender.new | |
count = 0 | |
total = ids.count | |
index = File.open(File.join(workdir, 'index.html'), 'w') | |
File.open(File.join(workdir, 'index.html'), 'w') do |index| | |
PageHelper.pagify(index) do |io| | |
PageHelper.generate_with_index(io) do |index| | |
ids.each_slice(1000).each do |ids| | |
task_scope.where(id: ids).each do |task| | |
content = with_error_handling(task) { renderer.render_task(task) } | |
if content | |
File.open(File.join(workdir, "#{task.id}.html"), 'w') { |file| PageHelper.pagify(file, content) } | |
with_error_handling(task, _('task index entry')) { PageHelper.generate_index_entry(index, task) } | |
end | |
count += 1 | |
puts "#{count}/#{total}" | |
end | |
end | |
end | |
end | |
end | |
end | |
def generate_filename(format) | |
base = "/tmp/task-export-#{Time.now.to_i}" | |
case format | |
when 'html' | |
base + '.tar.gz' | |
when 'csv' | |
base + '.csv' | |
when 'html-dir' | |
base | |
end | |
end | |
def with_error_handling(task, what = _('task')) | |
yield | |
rescue StandardError => e | |
resolution = SKIP_ERRORS ? _(', skipping') : '' | |
puts _("WARNING: %{what} failed to export%{resolution}. Additional task details below.") % { :what => what, :resolution => resolution } | |
puts task.inspect | |
unless SKIP_ERRORS | |
puts _("Re-run with SKIP_FAILED=true if you want to simply skip any tasks that fail to export.") | |
raise e | |
end | |
end | |
SKIP_ERRORS = ['true', '1', 'y', 'yes'].include? (ENV['SKIP_FAILED'] || '').downcase | |
filter = if ENV['TASK_SEARCH'].nil? && ENV['TASK_DAYS'].nil? | |
"started_at > \"#{7.days.ago.to_s(:db)}\" || " \ | |
"(result != success && started_at > \"#{60.days.ago.to_s(:db)})\"" | |
else | |
ENV['TASK_SEARCH'] || '' | |
end | |
if (days = ENV['TASK_DAYS']) | |
filter += ' && ' unless filter == '' | |
filter += "started_at > \"#{days.to_i.days.ago.to_s(:db)}\"" | |
end | |
format = ENV['TASK_FORMAT'] || 'html' | |
export_filename = ENV['TASK_FILE'] || generate_filename(format) | |
task_scope = ForemanTasks::Task.search_for(filter).with_duration.order(:started_at => :desc) | |
id_scope = task_scope.group(:id, :started_at) | |
puts _("Exporting all tasks matching filter #{filter}") | |
puts _("Gathering #{id_scope.count(:all).count} tasks.") | |
case format | |
when 'html' | |
Dir.mktmpdir('task-export') do |tmp_dir| | |
html_export(tmp_dir, id_scope, task_scope) | |
system("tar", "czf", export_filename, tmp_dir) | |
end | |
when 'html-dir' | |
FileUtils.mkdir_p(export_filename) | |
html_export(export_filename, id_scope, task_scope) | |
when 'csv' | |
csv_export(export_filename, id_scope, task_scope) | |
else | |
raise "Unkonwn export format '#{format}'" | |
end | |
puts "Created #{export_filename}" | |
end | |
end |