foreman-tasks 5.0.0 → 5.2.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (61) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +0 -4
  3. data/.rubocop_todo.yml +0 -2
  4. data/README.md +8 -6
  5. data/app/assets/javascripts/foreman_tasks/trigger_form.js +7 -0
  6. data/app/controllers/foreman_tasks/api/tasks_controller.rb +4 -4
  7. data/app/controllers/foreman_tasks/tasks_controller.rb +5 -4
  8. data/app/graphql/mutations/recurring_logics/cancel.rb +27 -0
  9. data/app/graphql/types/recurring_logic.rb +3 -0
  10. data/app/helpers/foreman_tasks/foreman_tasks_helper.rb +4 -1
  11. data/app/lib/actions/middleware/watch_delegated_proxy_sub_tasks.rb +2 -6
  12. data/app/lib/actions/proxy_action.rb +2 -12
  13. data/app/lib/actions/trigger_proxy_batch.rb +79 -0
  14. data/app/models/foreman_tasks/recurring_logic.rb +8 -0
  15. data/app/models/foreman_tasks/remote_task.rb +3 -19
  16. data/app/models/foreman_tasks/task/dynflow_task.rb +8 -3
  17. data/app/models/foreman_tasks/task.rb +1 -0
  18. data/app/models/foreman_tasks/triggering.rb +12 -4
  19. data/app/views/foreman_tasks/api/tasks/show.json.rabl +1 -1
  20. data/app/views/foreman_tasks/layouts/react.html.erb +0 -1
  21. data/app/views/foreman_tasks/recurring_logics/index.html.erb +4 -2
  22. data/app/views/foreman_tasks/task_groups/recurring_logic_task_groups/_recurring_logic_task_group.html.erb +8 -0
  23. data/db/migrate/20210720115251_add_purpose_to_recurring_logic.rb +6 -0
  24. data/extra/foreman-tasks-cleanup.sh +144 -0
  25. data/extra/foreman-tasks-export.sh +121 -0
  26. data/foreman-tasks.gemspec +1 -3
  27. data/lib/foreman_tasks/engine.rb +2 -0
  28. data/lib/foreman_tasks/tasks/export_tasks.rake +112 -47
  29. data/lib/foreman_tasks/version.rb +1 -1
  30. data/locale/fr/LC_MESSAGES/foreman_tasks.mo +0 -0
  31. data/locale/ja/LC_MESSAGES/foreman_tasks.mo +0 -0
  32. data/locale/zh_CN/LC_MESSAGES/foreman_tasks.mo +0 -0
  33. data/package.json +7 -9
  34. data/test/controllers/api/tasks_controller_test.rb +30 -1
  35. data/test/controllers/tasks_controller_test.rb +19 -0
  36. data/test/factories/recurring_logic_factory.rb +7 -1
  37. data/test/graphql/mutations/recurring_logics/cancel_mutation_test.rb +66 -0
  38. data/test/support/dummy_proxy_action.rb +6 -0
  39. data/test/unit/actions/proxy_action_test.rb +11 -11
  40. data/test/unit/actions/trigger_proxy_batch_test.rb +59 -0
  41. data/test/unit/remote_task_test.rb +0 -8
  42. data/test/unit/triggering_test.rb +22 -0
  43. data/webpack/ForemanTasks/Components/TaskActions/TaskActionHelpers.js +11 -4
  44. data/webpack/ForemanTasks/Components/TaskActions/TaskActionHelpers.test.js +27 -5
  45. data/webpack/ForemanTasks/Components/TasksTable/TasksTable.js +8 -0
  46. data/webpack/ForemanTasks/Components/TasksTable/TasksTableActions.js +6 -1
  47. data/webpack/ForemanTasks/Components/TasksTable/TasksTableHelpers.js +2 -1
  48. data/webpack/ForemanTasks/Components/TasksTable/TasksTablePage.js +22 -11
  49. data/webpack/ForemanTasks/Components/TasksTable/TasksTableReducer.js +17 -16
  50. data/webpack/ForemanTasks/Components/TasksTable/TasksTableSelectors.js +3 -0
  51. data/webpack/ForemanTasks/Components/TasksTable/__tests__/TasksTableHelpers.test.js +1 -1
  52. data/webpack/ForemanTasks/Components/TasksTable/__tests__/TasksTableReducer.test.js +3 -1
  53. data/webpack/ForemanTasks/Components/TasksTable/__tests__/__snapshots__/TasksTablePage.test.js.snap +12 -2
  54. data/webpack/ForemanTasks/Components/TasksTable/__tests__/__snapshots__/TasksTableReducer.test.js.snap +5 -0
  55. data/webpack/ForemanTasks/Components/TasksTable/formatters/__test__/__snapshots__/selectionHeaderCellFormatter.test.js.snap +1 -0
  56. data/webpack/ForemanTasks/Components/TasksTable/formatters/__test__/selectionHeaderCellFormatter.test.js +1 -1
  57. data/webpack/ForemanTasks/Components/TasksTable/formatters/selectionHeaderCellFormatter.js +1 -0
  58. data/webpack/ForemanTasks/Components/TasksTable/index.js +2 -0
  59. metadata +11 -5
  60. data/app/services/foreman_tasks/dashboard_table_filter.rb +0 -56
  61. data/test/unit/dashboard_table_filter_test.rb +0 -77
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 3e48966ae6049eb85b3051151b7d15d48beb91a55b3580525fe114a3e045ad0f
4
- data.tar.gz: 93da446f348f1a27600a79223ba4daab0e5e83417db711919392cfab2eedf57a
3
+ metadata.gz: affb6b8dabd35bf7600c2c76aa6f129dab3d43cbddc24120d89bbe898355adf2
4
+ data.tar.gz: 57e68fca3c4e837f5ccb5073c8c8459dbda2e98975c2211d4f583daa478ebbf0
5
5
  SHA512:
6
- metadata.gz: 740ff700f75e43154068d3838611dc7c7306d7f5da91e3d2d41a6fce5ea0ccde84bd5f027545f24874b8fb112d9848505ae2a535386ee2f356852695dacd61a2
7
- data.tar.gz: 8f61c3c868af4b7c3d3173b728858537c72ccf695e60b63971ec19f8a675a6ad9c19ecdd3dedb2549e2eb27b7cde62a2be67ead4d737bb43b774301abb303228
6
+ metadata.gz: 117021731b7da694348b5b29564a3662b35a6de175b056db4ede99707ccace227f5815ace3adb9caee3e094aeba2fd9a0b722ad71c67c9405b8d2a97f33c16d1
7
+ data.tar.gz: 95a05ed5983f03aa8db0233d5f8c2dce7031eeae15f79ecb1fa936212920017da59d629a28908fde3469c3b7652c8ee4ae68f89b0aef4229152fad4f509283fc
data/.rubocop.yml CHANGED
@@ -46,10 +46,6 @@ Style/SymbolArray:
46
46
  Style/FormatString:
47
47
  Enabled: false
48
48
 
49
- Rails/Present:
50
- Exclude:
51
- - lib/foreman_tasks_core/**/*
52
-
53
49
  Rails/FilePath:
54
50
  Enabled: false
55
51
 
data/.rubocop_todo.yml CHANGED
@@ -11,7 +11,6 @@
11
11
  # Include: **/*.gemspec
12
12
  Gemspec/RequiredRubyVersion:
13
13
  Exclude:
14
- - 'foreman-tasks-core.gemspec'
15
14
  - 'foreman-tasks.gemspec'
16
15
 
17
16
  # Offense count: 1
@@ -37,7 +36,6 @@ Naming/MemoizedInstanceVariableName:
37
36
  Exclude:
38
37
  - 'app/controllers/foreman_tasks/recurring_logics_controller.rb'
39
38
  - 'app/lib/actions/recurring_action.rb'
40
- - 'lib/foreman_tasks_core/otp_manager.rb'
41
39
 
42
40
  # Offense count: 11
43
41
  # Configuration parameters: MinNameLength, AllowNamesEndingInNumbers, AllowedNames, ForbiddenNames.
data/README.md CHANGED
@@ -6,7 +6,8 @@ happening/happened in your Foreman instance. A framework for asynchronous tasks
6
6
 
7
7
  * Website: [TheForeman.org](http://theforeman.org)
8
8
  * ServerFault tag: [Foreman](http://serverfault.com/questions/tagged/foreman)
9
- * Issues: [foreman-tasks Redmine](http://projects.theforeman.org/projects/foreman-tasks)
9
+ * Issues: [Foreman-tasks Redmine](http://projects.theforeman.org/projects/foreman-tasks)
10
+ * Manual: [Foreman-tasks Manual](https://www.theforeman.org/plugins/foreman_tasks/0.8/index.html)
10
11
  * Wiki: [Foreman wiki](http://projects.theforeman.org/projects/foreman/wiki/About)
11
12
  * Community and support: #theforeman for general support, #theforeman-dev for development chat in [Freenode](irc.freenode.net)
12
13
  * Mailing lists:
@@ -25,6 +26,7 @@ happening/happened in your Foreman instance. A framework for asynchronous tasks
25
26
  | >= 1.22 | ~> 0.15.0 |
26
27
  | >= 2.0 | ~> 1.0.0 |
27
28
  | >= 2.1 | ~> 2.0.0 |
29
+ | >= 2.6 | ~> 5.2.0 |
28
30
 
29
31
  Installation
30
32
  ------------
@@ -154,9 +156,9 @@ rails root directory. See `-h` for more details and options
154
156
  Tasks cleanup
155
157
  -------------
156
158
 
157
- Although, the history of tasks has an auditing value, some kinds of
158
- tasks can grow up in number quite soon. Therefore there is a mechanism
159
- how to clean the tasks, using a rake command. When running without
159
+ Although the history of tasks has an auditing value, some kinds of
160
+ tasks can rapidly increase. Therefore, there is a mechanism for
161
+ cleaning up the tasks using a rake command. When running without
160
162
  any arguments, the tasks are deleted based on the default parameters
161
163
  defined in the code.
162
164
 
@@ -179,7 +181,7 @@ override the default configuration inside the configuration
179
181
  ```
180
182
  :foreman-tasks:
181
183
  :cleanup:
182
- # the period after witch to delete all the tasks (by default all tasks are not being deleted after some period)
184
+ # the period after which to delete all the tasks (by default, all tasks are not deleted after some period)
183
185
  :after: 365d
184
186
  # per action settings to override the default defined in the actions (cleanup_after method)
185
187
  :actions:
@@ -194,7 +196,7 @@ to specify the search criteria for the cleanup manually:
194
196
  * `TASK_SEARCH`: scoped search filter (example: 'label =
195
197
  "Actions::Foreman::Host::ImportFacts"')
196
198
  * `AFTER`: delete tasks created after `AFTER` period. Expected format
197
- is a number followed by the time unit (`s`, `h`, `m`, `y`), such as
199
+ is a number followed by the time unit (`s`, `h`, `d`, `m`, `y`), such as
198
200
  `10d` for 10 days (applicable only when the `TASK_SEARCH` option is
199
201
  specified)
200
202
  * `STATES`: comma separated list of task states to touch with the
@@ -8,6 +8,7 @@ function trigger_form_selector_binds(form_name, form_object_name) {
8
8
  form.find('fieldset.trigger_mode_form#trigger_mode_' + type).hide();
9
9
  });
10
10
  form.find('fieldset.trigger_mode_form#trigger_mode_' + $(this).val()).show();
11
+ disable_hidden_start_field($(this).val(), form)
11
12
  });
12
13
 
13
14
  input_type_selector.on('change', function () {
@@ -39,3 +40,9 @@ function trigger_form_selector_binds(form_name, form_object_name) {
39
40
  };
40
41
  });
41
42
  };
43
+
44
+ function disable_hidden_start_field(clicked, form) {
45
+ ["future", "recurring"].forEach(function(type) {
46
+ form.find('.trigger_mode_form#trigger_mode_' + type + ' #triggering_start_at_raw').prop('disabled', type !== clicked);
47
+ });
48
+ };
@@ -192,7 +192,7 @@ module ForemanTasks
192
192
  params[:order] = "#{ordering_params[:sort_by]} #{ordering_params[:sort_order]}"
193
193
  end
194
194
  params[:order] ||= 'started_at DESC'
195
- @tasks = DashboardTableFilter.new(resource_scope_for_index, params).scope.order(params[:order].to_s)
195
+ @tasks = resource_scope_for_index.order(params[:order].to_s)
196
196
  end
197
197
 
198
198
  def search_options
@@ -260,7 +260,7 @@ module ForemanTasks
260
260
  raise BadRequest,
261
261
  _('Resource search_params requires resource_type and resource_id to be specified')
262
262
  end
263
- scope.joins(:locks).where(foreman_tasks_locks:
263
+ scope.joins(:links).where(foreman_tasks_links:
264
264
  { resource_type: search_params[:resource_type],
265
265
  resource_id: search_params[:resource_id] })
266
266
  when 'task'
@@ -320,7 +320,7 @@ module ForemanTasks
320
320
  end
321
321
 
322
322
  def find_task
323
- @task = resource_scope.find(params[:id])
323
+ @task = resource_scope.with_duration.find(params[:id])
324
324
  end
325
325
 
326
326
  def resource_scope(_options = {})
@@ -330,7 +330,7 @@ module ForemanTasks
330
330
  end
331
331
 
332
332
  def resource_scope_for_index(*args)
333
- super.select("DISTINCT foreman_tasks_tasks.*, coalesce(ended_at, current_timestamp) - coalesce(coalesce(started_at, ended_at), current_timestamp) as duration")
333
+ super.with_duration.distinct
334
334
  end
335
335
 
336
336
  def controller_permission
@@ -99,8 +99,8 @@ module ForemanTasks
99
99
  private
100
100
 
101
101
  def respond_with_tasks(scope)
102
- @tasks = filter(scope, paginate: false)
103
- csv_response(@tasks, [:id, :action, :state, :result, 'started_at.in_time_zone', 'ended_at.in_time_zone', :username], ['Id', 'Action', 'State', 'Result', 'Started At', 'Ended At', 'User'])
102
+ @tasks = filter(scope, paginate: false).with_duration
103
+ csv_response(@tasks, [:id, :action, :state, :result, 'started_at.in_time_zone', 'ended_at.in_time_zone', :duration, :username], ['Id', 'Action', 'State', 'Result', 'Started At', 'Ended At', 'Duration', 'User'])
104
104
  end
105
105
 
106
106
  def controller_permission
@@ -127,8 +127,9 @@ module ForemanTasks
127
127
  end
128
128
 
129
129
  def filter(scope, paginate: true)
130
- scope = DashboardTableFilter.new(scope, params).scope
131
- scope = scope.search_for(search_query, order: params[:order])
130
+ search = current_taxonomy_search
131
+ search = [search, params[:search]].select(&:present?).join(' AND ')
132
+ scope = scope.search_for(search, order: params[:order])
132
133
  scope = scope.paginate(page: params[:page], per_page: params[:per_page]) if paginate
133
134
  scope.distinct
134
135
  end
@@ -0,0 +1,27 @@
1
+ module Mutations
2
+ module RecurringLogics
3
+ class Cancel < BaseMutation
4
+ graphql_name 'CancelRecurringLogic'
5
+ description 'Cancels recurring logic and all its active tasks'
6
+ resource_class ::ForemanTasks::RecurringLogic
7
+
8
+ argument :id, ID, required: true
9
+
10
+ field :errors, [Types::AttributeError], null: false
11
+ field :recurring_logic, Types::RecurringLogic, null: true
12
+
13
+ def resolve(id:)
14
+ recurring_logic = load_object_by(id: id)
15
+ authorize!(recurring_logic, :edit)
16
+ task_errors = []
17
+ begin
18
+ recurring_logic.cancel
19
+ rescue => e
20
+ task_errors = [{ path: ['tasks'], message: "There has been an error when canceling one of the tasks: #{e}" }]
21
+ end
22
+ errors = recurring_logic.errors.any? ? map_errors_to_path(recurring_logic) : []
23
+ { recurring_logic: recurring_logic, errors: (errors + task_errors) }
24
+ end
25
+ end
26
+ end
27
+ end
@@ -3,12 +3,15 @@ module Types
3
3
  description 'A Recurring Logic'
4
4
  model_class ::ForemanTasks::RecurringLogic
5
5
 
6
+ include ::Types::Concerns::MetaField
7
+
6
8
  global_id_field :id
7
9
  field :cron_line, String
8
10
  field :end_time, GraphQL::Types::ISO8601DateTime
9
11
  field :max_iteration, Integer
10
12
  field :iteration, Integer
11
13
  field :state, String
14
+ field :purpose, String
12
15
  belongs_to :triggering, Types::Triggering
13
16
 
14
17
  def self.graphql_definition
@@ -141,7 +141,10 @@ module ForemanTasks
141
141
  weekly_fieldset(f, triggering),
142
142
  time_picker_fieldset(f, triggering),
143
143
  ]
144
-
144
+ tags << text_f(f, :start_at_raw, :label => _('Start at'), :placeholder => 'YYYY-mm-dd HH:MM')
145
+ tags << text_f(f, :purpose,
146
+ :label => _('Purpose'),
147
+ :label_help => N_('A special label for tracking a recurring job. There can be only one active job with a given purpose at a time.'))
145
148
  content_tag(:fieldset, nil, :id => 'trigger_mode_recurring', :class => "trigger_mode_form #{'hidden' unless triggering.recurring?}") do
146
149
  tags.join.html_safe
147
150
  end
@@ -20,9 +20,7 @@ module Actions
20
20
  private
21
21
 
22
22
  def set_clock
23
- action.world.clock.ping action.send(:suspended_action),
24
- POLL_INTERVAL,
25
- CheckOnProxyActions
23
+ action.plan_event(CheckOnProxyActions, POLL_INTERVAL, optional: true)
26
24
  end
27
25
 
28
26
  def check_triggered
@@ -44,9 +42,7 @@ module Actions
44
42
 
45
43
  def notify(event, tasks)
46
44
  tasks.each do |task|
47
- action.world.event task.execution_plan_id,
48
- task.step_id,
49
- event
45
+ action.plan_event(event, execution_plan_id: task.execution_plan_id, step_id: task.step_id)
50
46
  end
51
47
  end
52
48
 
@@ -29,10 +29,10 @@ module Actions
29
29
  default_connection_options.each do |key, value|
30
30
  options[:connection_options][key] = value unless options[:connection_options].key?(key)
31
31
  end
32
- plan_self(options.merge(:proxy_url => proxy.url, :proxy_action_name => klass.to_s, :proxy_version => proxy_version(proxy)))
32
+ plan_self(options.merge(:proxy_url => proxy.url, :proxy_action_name => klass.to_s))
33
33
  # Just saving the RemoteTask is enough when using batch triggering
34
34
  # It will be picked up by the ProxyBatchTriggering middleware
35
- if input[:use_batch_triggering] && with_batch_triggering?(input[:proxy_version])
35
+ if input[:use_batch_triggering] && input.dig(:connection_options, :proxy_batch_triggering)
36
36
  prepare_remote_task.save!
37
37
  end
38
38
  end
@@ -193,11 +193,6 @@ module Actions
193
193
  :proxy_batch_triggering => Setting['foreman_tasks_proxy_batch_trigger'] || false }
194
194
  end
195
195
 
196
- def with_batch_triggering?(proxy_version)
197
- ((proxy_version[:major] == 1 && proxy_version[:minor] > 20) || proxy_version[:major] > 1) &&
198
- input.fetch(:connection_options, {}).fetch(:proxy_batch_triggering, false)
199
- end
200
-
201
196
  def clean_remote_task(*_args)
202
197
  remote_task.destroy! if remote_task
203
198
  end
@@ -222,11 +217,6 @@ module Actions
222
217
  .try(:fetch, 'output', {}) || {}
223
218
  end
224
219
 
225
- def proxy_version(proxy)
226
- match = proxy.statuses[:version].version['version'].match(/(\d+)\.(\d+)\.(\d+)/)
227
- { :major => match[1].to_i, :minor => match[2].to_i, :patch => match[3].to_i }
228
- end
229
-
230
220
  def failed_proxy_tasks
231
221
  metadata[:failed_proxy_tasks] ||= []
232
222
  end
@@ -0,0 +1,79 @@
1
+ module Actions
2
+ # This action plans proxy tasks in batches.
3
+ # It needs to be manually notified about the next batch being available by sending a TriggerNextBatch event.
4
+ #
5
+ # The ProxyAction needs to be planned with `:use_batch_triggering => true` to activate the feature
6
+ class TriggerProxyBatch < Base
7
+ TriggerNextBatch = Algebrick.type do
8
+ fields! batches: Integer
9
+ end
10
+ TriggerLastBatch = Algebrick.atom
11
+
12
+ def run(event = nil)
13
+ case event
14
+ when nil
15
+ if output[:planned_count]
16
+ check_finish
17
+ else
18
+ init_counts and suspend
19
+ end
20
+ when TriggerNextBatch
21
+ trigger_remote_tasks_batches(event.batches) and suspend
22
+ when TriggerLastBatch
23
+ trigger_remote_tasks_batch and on_finish
24
+ when ::Dynflow::Action::Skip
25
+ # do nothing
26
+ end
27
+ end
28
+
29
+ def trigger_remote_tasks_batches(amount = 1)
30
+ amount.times { trigger_remote_tasks_batch }
31
+ end
32
+
33
+ def trigger_remote_tasks_batch
34
+ # Find the tasks in batches, order them by proxy_url so we get all tasks
35
+ # to a certain proxy "close to each other"
36
+ batch = remote_tasks.pending.order(:proxy_url, :id).first(batch_size)
37
+ # Group the tasks by operation, in theory there should be only one operation
38
+ batch.group_by(&:operation).each do |operation, group|
39
+ ForemanTasks::RemoteTask.batch_trigger(operation, group)
40
+ end
41
+ output[:planned_count] += batch.size
42
+ rescue => e
43
+ action_logger.warn "Could not trigger task on the smart proxy: #{e.message}"
44
+ batch.each { |remote_task| remote_task.update_from_batch_trigger({}) }
45
+ output[:failed_count] += batch.size
46
+ end
47
+
48
+ def init_counts
49
+ output[:planned_count] = 0
50
+ output[:failed_count] = 0
51
+ end
52
+
53
+ def check_finish
54
+ if output[:planned_count] + output[:failed_count] + batch_size >= input[:total_count]
55
+ trigger_remote_tasks_batch and on_finish
56
+ else
57
+ suspend
58
+ end
59
+ end
60
+
61
+ def done?
62
+ output[:planned_count] + output[:failed_count] >= input[:total_count]
63
+ end
64
+
65
+ def remote_tasks
66
+ task.remote_sub_tasks
67
+ end
68
+
69
+ def on_finish
70
+ # nothing for now
71
+ end
72
+
73
+ private
74
+
75
+ def batch_size
76
+ input[:batch_size] || Setting['foreman_tasks_proxy_batch_size']
77
+ end
78
+ end
79
+ end
@@ -17,6 +17,9 @@ module ForemanTasks
17
17
  scoped_search :on => :iteration, :complete_value => false
18
18
  scoped_search :on => :cron_line, :complete_value => true
19
19
  scoped_search :on => :state, :complete_value => true
20
+ scoped_search :on => :purpose, :complete_value => true
21
+
22
+ validate :valid_purpose
20
23
 
21
24
  before_create do
22
25
  task_group.save
@@ -169,6 +172,7 @@ module ForemanTasks
169
172
  ::ForemanTasks::RecurringLogic.new_from_cronline(cronline).tap do |manager|
170
173
  manager.end_time = triggering.end_time if triggering.end_time_limited.present?
171
174
  manager.max_iteration = triggering.max_iteration if triggering.max_iteration.present?
175
+ manager.purpose = triggering.purpose if triggering.purpose.present?
172
176
  manager.triggering = triggering
173
177
  end
174
178
  end
@@ -189,5 +193,9 @@ module ForemanTasks
189
193
  end
190
194
  hash.select { |key, _| allowed_keys.include? key }
191
195
  end
196
+
197
+ def valid_purpose?
198
+ !(purpose.present? && self.class.where(:purpose => purpose, :state => %w[active disabled]).any?)
199
+ end
192
200
  end
193
201
  end
@@ -16,7 +16,7 @@ module ForemanTasks
16
16
  # Triggers a task on the proxy "the old way"
17
17
  def trigger(proxy_action_name, input)
18
18
  response = begin
19
- proxy.trigger_task(proxy_action_name, input).merge('result' => 'success')
19
+ proxy.launch_tasks('single', :action_class => proxy_action_name, :action_input => input)
20
20
  rescue RestClient::Exception => e
21
21
  logger.warn "Could not trigger task on the smart proxy: #{e.message}"
22
22
  {}
@@ -31,28 +31,12 @@ module ForemanTasks
31
31
  acc.merge(remote_task.execution_plan_id => { :action_input => remote_task.proxy_input,
32
32
  :action_class => remote_task.proxy_action_name })
33
33
  end
34
- safe_batch_trigger(operation, group, input_hash)
34
+ results = remote_tasks.first.proxy.launch_tasks(operation, input_hash)
35
+ remote_tasks.each { |remote_task| remote_task.update_from_batch_trigger results[remote_task.execution_plan_id] }
35
36
  end
36
37
  remote_tasks
37
38
  end
38
39
 
39
- # Attempt to trigger the tasks using the new API and fall back to the old one
40
- # if it fails
41
- def self.safe_batch_trigger(operation, remote_tasks, input_hash)
42
- results = remote_tasks.first.proxy.launch_tasks(operation, input_hash)
43
- remote_tasks.each { |remote_task| remote_task.update_from_batch_trigger results[remote_task.execution_plan_id] }
44
- rescue RestClient::NotFound
45
- fallback_batch_trigger remote_tasks, input_hash
46
- end
47
-
48
- # Trigger the tasks one-by-one using the old API
49
- def self.fallback_batch_trigger(remote_tasks, input_hash)
50
- remote_tasks.each do |remote_task|
51
- task_data = input_hash[remote_task.execution_plan_id]
52
- remote_task.trigger(task_data[:action_class], task_data[:action_input])
53
- end
54
- end
55
-
56
40
  def update_from_batch_trigger(data)
57
41
  if data['result'] == 'success'
58
42
  self.remote_task_id = data['task_id']
@@ -140,12 +140,17 @@ module ForemanTasks
140
140
 
141
141
  def main_action
142
142
  return @main_action if defined?(@main_action)
143
+
144
+ @main_action = execution_plan && execution_plan.root_plan_step.try(:action, execution_plan)
143
145
  if active_job?
144
146
  job_data = active_job_data
145
- @main_action = active_job_action(job_data['job_class'], job_data['arguments'])
146
- else
147
- @main_action = execution_plan && execution_plan.root_plan_step.try(:action, execution_plan)
147
+ begin
148
+ @main_action = active_job_action(job_data['job_class'], job_data['arguments'])
149
+ rescue => e
150
+ Foreman::Logging.exception("Failed to load ActiveJob for task #{id}", e, :logger => 'foreman-tasks')
151
+ end
148
152
  end
153
+ @main_action
149
154
  end
150
155
 
151
156
  # The class for ActiveJob jobs in Dynflow, JobWrapper is not expected to
@@ -78,6 +78,7 @@ module ForemanTasks
78
78
  :"foreman_tasks_links.resource_type" => resource.class.name)
79
79
  end)
80
80
  scope :for_action_types, (->(action_types) { where('foreman_tasks_tasks.label IN (?)', Array(action_types)) })
81
+ scope :with_duration, -> { select("foreman_tasks_tasks.*, coalesce(ended_at, current_timestamp) - coalesce(coalesce(started_at, ended_at), current_timestamp) as duration") }
81
82
 
82
83
  apipie :class, "A class representing #{model_name.human} object" do
83
84
  name 'Task'
@@ -2,7 +2,7 @@ module ForemanTasks
2
2
  class Triggering < ApplicationRecord
3
3
  PARAMS = [:start_at_raw, :start_before_raw, :max_iteration, :input_type,
4
4
  :cronline, :days, :days_of_week, :time, :end_time_limited,
5
- :end_time].freeze
5
+ :end_time, :purpose].freeze
6
6
  attr_accessor(*PARAMS)
7
7
 
8
8
  graphql_type '::Types::Triggering'
@@ -30,7 +30,7 @@ module ForemanTasks
30
30
  validates :input_type, :if => :recurring?,
31
31
  :inclusion => { :in => ALLOWED_INPUT_TYPES,
32
32
  :message => _('%{value} is not allowed input type') }
33
- validates :start_at_raw, format: { :with => TIME_REGEXP, :if => :future?,
33
+ validates :start_at_raw, format: { :with => TIME_REGEXP, :if => ->(triggering) { triggering.future? || (triggering.recurring? && triggering.start_at_raw) },
34
34
  :message => _('%{value} is wrong format') }
35
35
  validates :start_before_raw, format: { :with => TIME_REGEXP, :if => :future?,
36
36
  :message => _('%{value} is wrong format'), :allow_blank => true }
@@ -70,7 +70,7 @@ module ForemanTasks
70
70
  delay_options,
71
71
  *args
72
72
  when :recurring
73
- recurring_logic.start(action, *args)
73
+ recurring_logic.start_after(action, delay_options[:start_at] || Time.zone.now, *args)
74
74
  end
75
75
  end
76
76
 
@@ -94,7 +94,13 @@ module ForemanTasks
94
94
  end
95
95
 
96
96
  def parse_start_at!
97
- self.start_at ||= Time.zone.parse(start_at_raw)
97
+ self.start_at ||= Time.zone.parse(start_at_raw) if start_at_raw.present?
98
+ end
99
+
100
+ def parse_start_at
101
+ self.start_at ||= Time.zone.parse(start_at_raw) if start_at_raw.present?
102
+ rescue ArgumentError
103
+ errors.add(:start_at, _('is not a valid format'))
98
104
  end
99
105
 
100
106
  def parse_start_before!
@@ -104,7 +110,9 @@ module ForemanTasks
104
110
  private
105
111
 
106
112
  def can_start_recurring
113
+ parse_start_at
107
114
  errors.add(:input_type, _('No task could be started')) unless recurring_logic.valid?
115
+ errors.add(:purpose, _('Active or disabled recurring logic with purpose %s already exists') % recurring_logic.purpose) unless recurring_logic.valid_purpose?
108
116
  errors.add(:cronline, _('%s is not valid format of cron line') % cronline) unless recurring_logic.valid_cronline?
109
117
  end
110
118
 
@@ -3,6 +3,6 @@ object @task if @task
3
3
  extends 'api/v2/layouts/permissions'
4
4
 
5
5
  attributes :id, :label, :pending, :action
6
- attributes :username, :started_at, :ended_at, :state, :result, :progress
6
+ attributes :username, :started_at, :ended_at, :duration, :state, :result, :progress
7
7
  attributes :input, :output, :humanized, :cli_example, :start_at
8
8
  node(:available_actions) { |t| { cancellable: t.execution_plan&.cancellable?, resumable: t.resumable? } }
@@ -6,7 +6,6 @@
6
6
  <% end %>
7
7
 
8
8
  <% content_for(:content) do %>
9
- <%= notifications %>
10
9
  <div id="organization-id" data-id="<%= Organization.current.id if Organization.current %>" ></div>
11
10
  <div id="user-id" data-id="<%= User.current.id if User.current %>" ></div>
12
11
  <%= react_component('ForemanTasks') %>
@@ -8,9 +8,9 @@
8
8
  <% if authorized_for(:permission => :edit_recurring_logics, :auth_object => @recurring_logics) %>
9
9
  <% title_actions link_to(_('Clear Cancelled'),
10
10
  clear_cancelled_foreman_tasks_recurring_logics_path,
11
- class: ['btn', 'btn-sm', 'btn-danger'],
11
+ class: ['btn', 'btn-sm', 'btn-danger'],
12
12
  :'data-toggle' => "modal",
13
- :'data-target' => "#clear_modal")
13
+ :'data-target' => "#clear_modal")
14
14
  %>
15
15
 
16
16
  <div class="modal fade" id="clear_modal" tabindex="-1" role="dialog" aria-labelledby="Deploy" aria-hidden="true">
@@ -47,6 +47,7 @@
47
47
  <th><%= N_("Iteration limit") %></th>
48
48
  <th><%= N_("Repeat until") %></th>
49
49
  <th><%= N_("State") %></th>
50
+ <th><%= N_("Purpose") %></th>
50
51
  <th/>
51
52
  </thead>
52
53
  <% @recurring_logics.each do |recurring_logic| %>
@@ -61,6 +62,7 @@
61
62
  <td><%= format_recurring_logic_limit recurring_logic.max_iteration %></td>
62
63
  <td><%= format_recurring_logic_limit recurring_logic.end_time.try(:in_time_zone) %></td>
63
64
  <td><%= recurring_logic_state(recurring_logic) %></td>
65
+ <td><%= recurring_logic.purpose %></td>
64
66
  <td><%= recurring_logic_action_buttons(recurring_logic) %></td>
65
67
  </tr>
66
68
  <% end %>
@@ -36,4 +36,12 @@
36
36
  <th><%= N_("State") %></th>
37
37
  <td><%= recurring_logic_state(recurring_logic) %></td>
38
38
  </tr>
39
+ <tr>
40
+ <th><%= N_("Purpose") %></th>
41
+ <td><%= recurring_logic.purpose %></td>
42
+ </tr>
43
+ <tr>
44
+ <th><%= N_("Task count") %></th>
45
+ <td><%= link_to(task_group.tasks.count, foreman_tasks_tasks_url(:search => "task_group.id = #{task_group.id}")) %></td>
46
+ </tr>
39
47
  </table>
@@ -0,0 +1,6 @@
1
+ class AddPurposeToRecurringLogic < ActiveRecord::Migration[6.0]
2
+ def change
3
+ add_column :foreman_tasks_recurring_logics, :purpose, :string
4
+ add_index :foreman_tasks_recurring_logics, :purpose, unique: true, where: "state IN ('active', 'disabled')"
5
+ end
6
+ end