foreman-tasks 5.0.0 → 5.1.0

Sign up to get free protection for your applications and to get access to all the features.
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