foreman-tasks 5.0.0 → 5.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. checksums.yaml +4 -4
  2. data/app/controllers/foreman_tasks/api/tasks_controller.rb +4 -4
  3. data/app/controllers/foreman_tasks/tasks_controller.rb +5 -4
  4. data/app/graphql/types/recurring_logic.rb +1 -0
  5. data/app/helpers/foreman_tasks/foreman_tasks_helper.rb +4 -1
  6. data/app/lib/actions/middleware/watch_delegated_proxy_sub_tasks.rb +2 -6
  7. data/app/lib/actions/trigger_proxy_batch.rb +79 -0
  8. data/app/models/foreman_tasks/recurring_logic.rb +8 -0
  9. data/app/models/foreman_tasks/task/dynflow_task.rb +8 -3
  10. data/app/models/foreman_tasks/task.rb +1 -0
  11. data/app/models/foreman_tasks/triggering.rb +12 -4
  12. data/app/views/foreman_tasks/api/tasks/show.json.rabl +1 -1
  13. data/app/views/foreman_tasks/layouts/react.html.erb +0 -1
  14. data/app/views/foreman_tasks/recurring_logics/index.html.erb +4 -2
  15. data/app/views/foreman_tasks/task_groups/recurring_logic_task_groups/_recurring_logic_task_group.html.erb +4 -0
  16. data/db/migrate/20210720115251_add_purpose_to_recurring_logic.rb +6 -0
  17. data/extra/foreman-tasks-cleanup.sh +127 -0
  18. data/extra/foreman-tasks-export.sh +117 -0
  19. data/lib/foreman_tasks/tasks/export_tasks.rake +92 -47
  20. data/lib/foreman_tasks/version.rb +1 -1
  21. data/test/controllers/api/tasks_controller_test.rb +29 -0
  22. data/test/controllers/tasks_controller_test.rb +19 -0
  23. data/test/unit/actions/trigger_proxy_batch_test.rb +59 -0
  24. data/test/unit/triggering_test.rb +22 -0
  25. data/webpack/ForemanTasks/Components/TaskActions/TaskActionHelpers.js +11 -4
  26. data/webpack/ForemanTasks/Components/TaskActions/TaskActionHelpers.test.js +27 -5
  27. data/webpack/ForemanTasks/Components/TasksTable/TasksTable.js +8 -0
  28. data/webpack/ForemanTasks/Components/TasksTable/TasksTableActions.js +6 -1
  29. data/webpack/ForemanTasks/Components/TasksTable/TasksTableHelpers.js +2 -1
  30. data/webpack/ForemanTasks/Components/TasksTable/TasksTablePage.js +22 -11
  31. data/webpack/ForemanTasks/Components/TasksTable/TasksTableReducer.js +17 -16
  32. data/webpack/ForemanTasks/Components/TasksTable/TasksTableSelectors.js +3 -0
  33. data/webpack/ForemanTasks/Components/TasksTable/__tests__/TasksTableHelpers.test.js +1 -1
  34. data/webpack/ForemanTasks/Components/TasksTable/__tests__/TasksTableReducer.test.js +3 -1
  35. data/webpack/ForemanTasks/Components/TasksTable/__tests__/__snapshots__/TasksTablePage.test.js.snap +12 -2
  36. data/webpack/ForemanTasks/Components/TasksTable/__tests__/__snapshots__/TasksTableReducer.test.js.snap +5 -0
  37. data/webpack/ForemanTasks/Components/TasksTable/formatters/__test__/__snapshots__/selectionHeaderCellFormatter.test.js.snap +1 -0
  38. data/webpack/ForemanTasks/Components/TasksTable/formatters/__test__/selectionHeaderCellFormatter.test.js +1 -1
  39. data/webpack/ForemanTasks/Components/TasksTable/formatters/selectionHeaderCellFormatter.js +1 -0
  40. data/webpack/ForemanTasks/Components/TasksTable/index.js +2 -0
  41. metadata +8 -5
  42. data/app/services/foreman_tasks/dashboard_table_filter.rb +0 -56
  43. data/test/unit/dashboard_table_filter_test.rb +0 -77
@@ -0,0 +1,117 @@
1
+ #!/bin/bash
2
+
3
+ PROGNAME="$0"
4
+ EXECUTE=0
5
+ RAKE_COMMAND="${RAKE_COMMAND:-"foreman-rake"}"
6
+
7
+ function die() {
8
+ local code="$1"
9
+ local message="$2"
10
+ echo "$message" >&2
11
+ exit $code
12
+ }
13
+
14
+ function build_rake() {
15
+ echo -n "$RAKE_COMMAND "
16
+ echo -n 'foreman_tasks:export_tasks '
17
+ for env in TASK_SEARCH TASK_FILE TASK_FORMAT TASK_DAYS; do
18
+ local value="${!env}"
19
+ [ -n "${value}" ] && echo -n "${env}=$(printf '%q' "$value") "
20
+ done
21
+ echo
22
+ }
23
+
24
+ function usage() {
25
+ cat << EOF
26
+ Usage: $PROGNAME [options]
27
+
28
+ An interface script for setting environment variables properly
29
+ for foreman-tasks:export_tasks rake task. By default only prints
30
+ the command to run, with -E|--execute flag performs the export.
31
+
32
+ Environment variables:
33
+ RAKE_COMMAND: can be used to redefine path to rake, by default foreman-rake
34
+
35
+ Script options:
36
+ EOF
37
+ cat <<EOF | column -s\& -t
38
+ -E|--execute & execute the created rake command
39
+ -h|--help & show this output
40
+ EOF
41
+
42
+ echo
43
+ echo Export options:
44
+ cat <<EOF | column -s\& -t
45
+ -d|--days DAYS & export only tasks started within the last DAYS days
46
+ -f|--format FORMAT & export tasks in FORMAT, one of html, html-dir, csv
47
+ -o|--output FILE & export tasks into FILE, a random file will be used if not provided
48
+ -s|--search QUERY & use QUERY in scoped search format to match tasks to export
49
+ EOF
50
+ }
51
+
52
+ SHORTOPTS="d:Ehs:o:f:"
53
+ LONGOPTS="days:,execute,help,search:,output:,format:"
54
+
55
+ ARGS=$(getopt -s bash \
56
+ --options $SHORTOPTS \
57
+ --longoptions $LONGOPTS \
58
+ --name $PROGNAME \
59
+ -- "$@" )
60
+
61
+ if [ $? -gt 0 ]; then
62
+ die 1 "getopt failed"
63
+ fi
64
+
65
+ eval set -- "$ARGS"
66
+
67
+ while true; do
68
+ case $1 in
69
+ -d|--days)
70
+ shift
71
+ TASK_DAYS="$1"
72
+ ;;
73
+ -s|--search)
74
+ shift
75
+ TASK_SEARCH="$1"
76
+ ;;
77
+ -o|--output)
78
+ shift
79
+ TASK_FILE="$1"
80
+ ;;
81
+ -f|--format)
82
+ case "$2" in
83
+ "html" | "html-dir" | "csv")
84
+ shift
85
+ TASK_FORMAT="$1"
86
+ ;;
87
+ *)
88
+ die 1 "Value for $1 must be one of html, csv. Given $2"
89
+ ;;
90
+ esac
91
+ ;;
92
+ -h|--help)
93
+ usage
94
+ exit 0
95
+ ;;
96
+ -E|--execute)
97
+ EXECUTE=1
98
+ ;;
99
+ \?)
100
+ die 1 "Invalid option: -$OPTARG"
101
+ ;;
102
+ --)
103
+ ;;
104
+ *)
105
+ [ -n "$1" ] || break
106
+ die 1 "Unaccepted parameter: $1"
107
+ shift
108
+ ;;
109
+ esac
110
+ shift
111
+ done
112
+
113
+ if [ "$EXECUTE" -eq 1 ]; then
114
+ build_rake | sh
115
+ else
116
+ build_rake
117
+ fi
@@ -12,7 +12,7 @@ namespace :foreman_tasks do
12
12
 
13
13
  * TASK_SEARCH : scoped search filter (example: 'label = "Actions::Foreman::Host::ImportFacts"')
14
14
  * TASK_FILE : file to export to
15
- * TASK_FORMAT : format to use for the export (either html or csv)
15
+ * TASK_FORMAT : format to use for the export (either html, html-dir or csv)
16
16
  * TASK_DAYS : number of days to go back
17
17
 
18
18
  If TASK_SEARCH is not defined, it defaults to all tasks in the past 7 days and
@@ -185,23 +185,27 @@ namespace :foreman_tasks do
185
185
  end
186
186
 
187
187
  class PageHelper
188
- def self.pagify(template)
189
- pre = <<-HTML
190
- <html>
191
- <head>
192
- <title>Dynflow Console</title>
193
- <script src="jquery.js"></script>
194
- <link rel="stylesheet" type="text/css" href="bootstrap.css">
195
- <link rel="stylesheet" type="text/css" href="application.css">
196
- <script src="bootstrap.js"></script>
197
- <script src="run_prettify.js"></script>
198
- <script src="application.js"></script>
199
- </head>
200
- <body>
201
- #{template}
202
- <body>
203
- </html>
188
+ def self.pagify(io, template = nil)
189
+ io.write <<~HTML
190
+ <html>
191
+ <head>
192
+ <title>Dynflow Console</title>
193
+ <script src="jquery.js"></script>
194
+ <link rel="stylesheet" type="text/css" href="bootstrap.css">
195
+ <link rel="stylesheet" type="text/css" href="application.css">
196
+ <script src="bootstrap.js"></script>
197
+ <script src="run_prettify.js"></script>
198
+ <script src="application.js"></script>
199
+ </head>
200
+ <body>
204
201
  HTML
202
+ if block_given?
203
+ yield io
204
+ else
205
+ io.write template
206
+ end
207
+ ensure
208
+ io.write '</body></html>'
205
209
  end
206
210
 
207
211
  def self.copy_assets(tmp_dir)
@@ -216,13 +220,65 @@ namespace :foreman_tasks do
216
220
  end
217
221
  end
218
222
 
219
- def self.generate_index(tasks)
220
- html = '<div><table class="table">'
221
- tasks.order('started_at desc').all.each do |task|
222
- html << "<tr><td><a href=\"#{task.id}.html\">#{task.label}</a></td><td>#{task.started_at}</td>\
223
- <td>#{task.state}</td><td>#{task.result}</td></tr>"
223
+ def self.generate_with_index(io)
224
+ io.write '<div><table class="table">'
225
+ yield io
226
+ ensure
227
+ io.write '</table></div>'
228
+ end
229
+
230
+ def self.generate_index_entry(io, task)
231
+ io << <<~HTML
232
+ <tr>
233
+ <td><a href=\"#{task.id}.html\">#{task.label}</a></td>
234
+ <td>#{task.started_at}</td>
235
+ <td>#{task.duration}</td>
236
+ <td>#{task.state}</td>
237
+ <td>#{task.result}</td>
238
+ </tr>
239
+ HTML
240
+ end
241
+ end
242
+
243
+ def csv_export(export_filename, tasks)
244
+ CSV.open(export_filename, 'wb') do |csv|
245
+ csv << %w[id state type label result parent_task_id started_at ended_at duration]
246
+ tasks.find_each do |task|
247
+ csv << [task.id, task.state, task.type, task.label, task.result,
248
+ task.parent_task_id, task.started_at, task.ended_at, task.duration]
224
249
  end
225
- html << '</table></div>'
250
+ end
251
+ end
252
+
253
+ def html_export(workdir, tasks)
254
+ PageHelper.copy_assets(workdir)
255
+
256
+ renderer = TaskRender.new
257
+ total = tasks.count(:all)
258
+ index = File.open(File.join(workdir, 'index.html'), 'w')
259
+
260
+ File.open(File.join(workdir, 'index.html'), 'w') do |index|
261
+ PageHelper.pagify(index) do |io|
262
+ PageHelper.generate_with_index(io) do |index|
263
+ tasks.find_each.each_with_index do |task, count|
264
+ File.open(File.join(workdir, "#{task.id}.html"), 'w') { |file| PageHelper.pagify(file, renderer.render_task(task)) }
265
+ PageHelper.generate_index_entry(index, task)
266
+ puts "#{count + 1}/#{total}"
267
+ end
268
+ end
269
+ end
270
+ end
271
+ end
272
+
273
+ def generate_filename(format)
274
+ base = "/tmp/task-export-#{Time.now.to_i}"
275
+ case format
276
+ when 'html'
277
+ base + '.tar.gz'
278
+ when 'csv'
279
+ base + '.csv'
280
+ when 'html-dir'
281
+ base
226
282
  end
227
283
  end
228
284
 
@@ -239,36 +295,25 @@ namespace :foreman_tasks do
239
295
  end
240
296
 
241
297
  format = ENV['TASK_FORMAT'] || 'html'
242
- export_filename = ENV['TASK_FILE'] || "/tmp/task-export-#{Time.now.to_i}.#{format == 'csv' ? 'csv' : 'tar.gz'}"
298
+ export_filename = ENV['TASK_FILE'] || generate_filename(format)
243
299
 
244
- tasks = ForemanTasks::Task.search_for(filter)
300
+ tasks = ForemanTasks::Task.search_for(filter).order(:started_at => :desc).with_duration.distinct
245
301
 
246
302
  puts _("Exporting all tasks matching filter #{filter}")
247
- puts _("Gathering #{tasks.count} tasks.")
248
- if format == 'html'
303
+ puts _("Gathering #{tasks.count(:all)} tasks.")
304
+ case format
305
+ when 'html'
249
306
  Dir.mktmpdir('task-export') do |tmp_dir|
250
- PageHelper.copy_assets(tmp_dir)
251
-
252
- renderer = TaskRender.new
253
- total = tasks.count
254
-
255
- tasks.find_each.with_index do |task, count|
256
- File.open(File.join(tmp_dir, "#{task.id}.html"), 'w') { |file| file.write(PageHelper.pagify(renderer.render_task(task))) }
257
- puts "#{count + 1}/#{total}"
258
- end
259
-
260
- File.open(File.join(tmp_dir, 'index.html'), 'w') { |file| file.write(PageHelper.pagify(PageHelper.generate_index(tasks))) }
261
-
307
+ html_export(tmp_dir, tasks)
262
308
  system("tar", "czf", export_filename, tmp_dir)
263
309
  end
264
- elsif format == 'csv'
265
- CSV.open(export_filename, 'wb') do |csv|
266
- csv << %w[id state type label result parent_task_id started_at ended_at]
267
- tasks.find_each do |task|
268
- csv << [task.id, task.state, task.type, task.label, task.result,
269
- task.parent_task_id, task.started_at, task.ended_at]
270
- end
271
- end
310
+ when 'html-dir'
311
+ FileUtils.mkdir_p(export_filename)
312
+ html_export(export_filename, tasks)
313
+ when 'csv'
314
+ csv_export(export_filename, tasks)
315
+ else
316
+ raise "Unkonwn export format '#{format}'"
272
317
  end
273
318
 
274
319
  puts "Created #{export_filename}"
@@ -1,3 +1,3 @@
1
1
  module ForemanTasks
2
- VERSION = '5.0.0'.freeze
2
+ VERSION = '5.1.0'.freeze
3
3
  end
@@ -68,6 +68,17 @@ module ForemanTasks
68
68
  data = JSON.parse(response.body)
69
69
  _(data[0]['results'][0]['id']).must_equal task.id
70
70
  end
71
+
72
+ it 'can search for a specific resource' do
73
+ org = FactoryBot.create(:organization)
74
+ task = FactoryBot.create(:task_with_links, resource_id: org.id, resource_type: 'Organization')
75
+
76
+ post :bulk_search, params: { :searches => [{ :type => 'resource', :resource_id => org.id, :resource_type => 'Organization' }] }
77
+
78
+ assert_response :success
79
+ data = JSON.parse(response.body)
80
+ _(data[0]['results'][0]['id']).must_equal task.id
81
+ end
71
82
  end
72
83
 
73
84
  describe 'GET /api/tasks/show' do
@@ -91,6 +102,24 @@ module ForemanTasks
91
102
  session: set_session_user(User.current)
92
103
  assert_response :not_found
93
104
  end
105
+
106
+ it 'shows duration column' do
107
+ task = ForemanTasks::Task.with_duration.find(FactoryBot.create(:dynflow_task).id)
108
+ get :show, params: { id: task.id }, session: set_session_user
109
+ assert_response :success
110
+ data = JSON.parse(response.body)
111
+ _(data['duration']).must_equal task.duration
112
+ end
113
+ end
114
+
115
+ describe 'GET /api/tasks/index' do
116
+ it 'shows duration column' do
117
+ task = ForemanTasks::Task.with_duration.find(FactoryBot.create(:dynflow_task).id)
118
+ get :index, session: set_session_user
119
+ assert_response :success
120
+ data = JSON.parse(response.body)
121
+ _(data['results'][0]['duration']).must_equal task.duration
122
+ end
94
123
  end
95
124
 
96
125
  describe 'GET /api/tasks/summary' do
@@ -91,6 +91,15 @@ module ForemanTasks
91
91
  end
92
92
  end
93
93
 
94
+ describe 'index' do
95
+ it 'shows duration column' do
96
+ task = ForemanTasks::Task.with_duration.find(FactoryBot.create(:some_task).id)
97
+ get(:index, params: {}, session: set_session_user)
98
+ assert_response :success
99
+ assert_include response.body.lines[1], task.duration
100
+ end
101
+ end
102
+
94
103
  describe 'sub_tasks' do
95
104
  it 'does not allow user without permissions to see task details' do
96
105
  setup_user('view', 'foreman_tasks', 'owner.id = current_user')
@@ -109,6 +118,16 @@ module ForemanTasks
109
118
  assert_equal 2, response.body.lines.size
110
119
  assert_include response.body.lines[1], 'Child action'
111
120
  end
121
+
122
+ it 'shows duration column' do
123
+ parent = ForemanTasks::Task.find(FactoryBot.create(:some_task).id)
124
+ child = ForemanTasks::Task.with_duration.find(FactoryBot.create(:some_task).id)
125
+ child.parent_task_id = parent.id
126
+ child.save!
127
+ get(:sub_tasks, params: { id: parent.id }, session: set_session_user)
128
+ assert_response :success
129
+ assert_include response.body.lines[1], child.duration
130
+ end
112
131
  end
113
132
 
114
133
  describe 'taxonomy scoping' do
@@ -0,0 +1,59 @@
1
+ require 'foreman_tasks_test_helper'
2
+
3
+ module ForemanTasks
4
+ class TriggerProxyBatchTest < ActiveSupport::TestCase
5
+ describe Actions::TriggerProxyBatch do
6
+ include ::Dynflow::Testing
7
+
8
+ let(:batch_size) { 20 }
9
+ let(:total_count) { 100 }
10
+ let(:action) { create_and_plan_action(Actions::TriggerProxyBatch, total_count: total_count, batch_size: batch_size) }
11
+ let(:triggered) { run_action(action) }
12
+
13
+ describe 'triggering' do
14
+ it 'doesnt run anything on trigger' do
15
+ Actions::TriggerProxyBatch.any_instance.expects(:trigger_remote_tasks_batch).never
16
+ _(triggered.state).must_equal :suspended
17
+ _(triggered.output[:planned_count]).must_equal 0
18
+ end
19
+
20
+ it 'triggers remote tasks on TriggerNextBatch' do
21
+ Actions::TriggerProxyBatch.any_instance.expects(:trigger_remote_tasks_batch).once
22
+ run_action(triggered, Actions::TriggerProxyBatch::TriggerNextBatch[1])
23
+ end
24
+
25
+ it 'triggers remote tasks on TriggerNextBatch defined number of times' do
26
+ Actions::TriggerProxyBatch.any_instance.expects(:trigger_remote_tasks_batch).twice
27
+ run_action(triggered, Actions::TriggerProxyBatch::TriggerNextBatch[2])
28
+ end
29
+
30
+ it 'triggers the last batch on resume' do
31
+ Actions::TriggerProxyBatch.any_instance.expects(:trigger_remote_tasks_batch).once
32
+ triggered.output[:planned_count] = ((total_count - 1) / batch_size) * batch_size
33
+ run_action(triggered)
34
+ end
35
+ end
36
+
37
+ describe '#trigger_remote_tasks_batch' do
38
+ let(:proxy_operation_name) { 'ansible_runner' }
39
+ let(:grouped_remote_batch) { Array.new(batch_size).map { |i| mock("RemoteTask#{i}") } }
40
+ let(:remote_tasks) do
41
+ m = mock('RemoteTaskARScope')
42
+ m.stubs(pending: m, order: m)
43
+ m.stubs(group_by: { proxy_operation_name => grouped_remote_batch })
44
+ m
45
+ end
46
+
47
+ it 'fetches batch_size of tasks and triggers them' do
48
+ remote_tasks.expects(:first).with(batch_size).returns(remote_tasks)
49
+ remote_tasks.expects(:size).returns(batch_size)
50
+ triggered.expects(:remote_tasks).returns(remote_tasks)
51
+ ForemanTasks::RemoteTask.expects(:batch_trigger).with(proxy_operation_name, grouped_remote_batch)
52
+
53
+ triggered.trigger_remote_tasks_batch
54
+ _(triggered.output[:planned_count]).must_equal(batch_size)
55
+ end
56
+ end
57
+ end
58
+ end
59
+ end
@@ -19,6 +19,28 @@ class TriggeringTest < ActiveSupport::TestCase
19
19
  triggering.recurring_logic.stubs(:valid?).returns(false)
20
20
  _(triggering).wont_be :valid?
21
21
  end
22
+
23
+ it 'is valid when recurring logic has purpose' do
24
+ logic = FactoryBot.build(:recurring_logic, :purpose => 'test', :state => 'active')
25
+ triggering = FactoryBot.build(:triggering, :recurring_logic => logic, :mode => :recurring, :input_type => :cronline, :cronline => '* * * * *')
26
+ _(triggering).must_be :valid?
27
+ end
28
+
29
+ it 'is invalid when recurring logic with given purpose exists' do
30
+ FactoryBot.create(:recurring_logic, :purpose => 'test', :state => 'active')
31
+ logic = FactoryBot.build(:recurring_logic, :purpose => 'test', :state => 'active')
32
+ triggering = FactoryBot.build(:triggering, :recurring_logic => logic, :mode => :recurring, :input_type => :cronline, :cronline => '* * * * *')
33
+ _(triggering).wont_be :valid?
34
+ end
35
+
36
+ it 'is valid when recurring logic with given purpose exists and is not active or disabled' do
37
+ ['finished', 'cancelled', 'failed'].each do |item|
38
+ FactoryBot.create(:recurring_logic, :purpose => 'test', :state => item)
39
+ end
40
+ logic = FactoryBot.build(:recurring_logic, :purpose => 'test')
41
+ triggering = FactoryBot.build(:triggering, :recurring_logic => logic, :mode => :recurring, :input_type => :cronline, :cronline => '* * * * *')
42
+ _(triggering).must_be :valid?
43
+ end
22
44
  end
23
45
 
24
46
  it 'cannot have mode set to arbitrary value' do