foreman-tasks 4.1.5 → 5.2.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 (57) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ruby_tests.yml +0 -1
  3. data/.rubocop.yml +0 -4
  4. data/.rubocop_todo.yml +0 -2
  5. data/README.md +8 -6
  6. data/app/assets/javascripts/foreman_tasks/trigger_form.js +7 -0
  7. data/app/controllers/foreman_tasks/api/tasks_controller.rb +2 -2
  8. data/app/controllers/foreman_tasks/tasks_controller.rb +2 -2
  9. data/app/graphql/mutations/recurring_logics/cancel.rb +27 -0
  10. data/app/graphql/types/recurring_logic.rb +21 -0
  11. data/app/graphql/types/task.rb +25 -0
  12. data/app/graphql/types/triggering.rb +16 -0
  13. data/app/helpers/foreman_tasks/foreman_tasks_helper.rb +4 -1
  14. data/app/lib/actions/helpers/with_continuous_output.rb +1 -1
  15. data/app/lib/actions/proxy_action.rb +2 -12
  16. data/app/lib/actions/trigger_proxy_batch.rb +79 -0
  17. data/app/models/foreman_tasks/recurring_logic.rb +10 -0
  18. data/app/models/foreman_tasks/remote_task.rb +3 -19
  19. data/app/models/foreman_tasks/task.rb +29 -0
  20. data/app/models/foreman_tasks/triggering.rb +14 -4
  21. data/app/views/foreman_tasks/api/tasks/show.json.rabl +1 -1
  22. data/app/views/foreman_tasks/layouts/react.html.erb +0 -1
  23. data/app/views/foreman_tasks/recurring_logics/index.html.erb +4 -2
  24. data/app/views/foreman_tasks/task_groups/recurring_logic_task_groups/_recurring_logic_task_group.html.erb +8 -0
  25. data/db/migrate/20210720115251_add_purpose_to_recurring_logic.rb +6 -0
  26. data/extra/foreman-tasks-cleanup.sh +127 -0
  27. data/extra/foreman-tasks-export.sh +121 -0
  28. data/foreman-tasks.gemspec +1 -4
  29. data/lib/foreman_tasks/continuous_output.rb +50 -0
  30. data/lib/foreman_tasks/engine.rb +8 -15
  31. data/lib/foreman_tasks/tasks/export_tasks.rake +29 -8
  32. data/lib/foreman_tasks/version.rb +1 -1
  33. data/lib/foreman_tasks.rb +2 -5
  34. data/locale/fr/LC_MESSAGES/foreman_tasks.mo +0 -0
  35. data/locale/ja/LC_MESSAGES/foreman_tasks.mo +0 -0
  36. data/locale/zh_CN/LC_MESSAGES/foreman_tasks.mo +0 -0
  37. data/package.json +7 -9
  38. data/test/controllers/api/tasks_controller_test.rb +19 -1
  39. data/test/controllers/tasks_controller_test.rb +19 -0
  40. data/test/factories/recurring_logic_factory.rb +7 -1
  41. data/test/graphql/mutations/recurring_logics/cancel_mutation_test.rb +66 -0
  42. data/test/graphql/queries/recurring_logic_test.rb +28 -0
  43. data/test/graphql/queries/recurring_logics_query_test.rb +30 -0
  44. data/test/graphql/queries/task_query_test.rb +33 -0
  45. data/test/graphql/queries/tasks_query_test.rb +31 -0
  46. data/test/support/dummy_proxy_action.rb +6 -0
  47. data/test/unit/actions/proxy_action_test.rb +11 -11
  48. data/test/unit/actions/trigger_proxy_batch_test.rb +59 -0
  49. data/test/unit/remote_task_test.rb +0 -8
  50. data/test/unit/task_test.rb +39 -8
  51. data/test/unit/triggering_test.rb +22 -0
  52. metadata +23 -26
  53. data/test/core/unit/dispatcher_test.rb +0 -43
  54. data/test/core/unit/runner_test.rb +0 -116
  55. data/test/core/unit/task_launcher_test.rb +0 -56
  56. data/test/foreman_tasks_core_test_helper.rb +0 -4
  57. data/test/unit/otp_manager_test.rb +0 -77
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 6602d0bac71ecbcf69cecc38a00ab4ef145832318576aadcda9a1eda9fed543b
4
- data.tar.gz: 4b04eef7b87ba5542fd89576671e2bcf90d81ebad475adcc797dfc916bc30f1a
3
+ metadata.gz: fa2189368cc093a6ca613ca1b91add99bc3689b2cb22cd0c04a3718821b5f27f
4
+ data.tar.gz: f7e82294bbbb76fe6576b6c2de9e55c16342962f711a8e5c9070787be304a87c
5
5
  SHA512:
6
- metadata.gz: 96c669db27581e60652ece6e49fe9bfa1db1d1313b0c89a9a8c9124c897d2c73b9bb7ab5db47045a26651cabdb745779cfcdad45375ea606190774b00544bd8e
7
- data.tar.gz: cb9100d6621dbda92e4c25c5e87df96ee2fefd6de1dc556ce44031ee45732cd545671624a51864e38d8c5a177b302d843f897d98de7f4f7b68d5484c84bdf7f4
6
+ metadata.gz: f20e5fba4dbc2401a4fed1f5eb68961ccc7095071fc309316ad14392b9209141e96aa2372eb7bb5db0e7f829ef08727ff2e232e53de86baa8385183ddb722252
7
+ data.tar.gz: 534ac74d1c8010ac25330562a6bdb713beacc413d2c681ee72f865727741cfb17e04d9dda2c0ea8ab7bde2f7f2e08f54fa4266e25686ca9d1f38c5309b24b94c
@@ -72,5 +72,4 @@ jobs:
72
72
  - name: Run plugin tests
73
73
  run: |
74
74
  bundle exec rake test:foreman_tasks
75
- bundle exec rake test:foreman_tasks_core
76
75
  bundle exec rake test TEST="test/unit/foreman/access_permissions_test.rb"
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
+ };
@@ -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
@@ -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
@@ -0,0 +1,21 @@
1
+ module Types
2
+ class RecurringLogic < Types::BaseObject
3
+ description 'A Recurring Logic'
4
+ model_class ::ForemanTasks::RecurringLogic
5
+
6
+ include ::Types::Concerns::MetaField
7
+
8
+ global_id_field :id
9
+ field :cron_line, String
10
+ field :end_time, GraphQL::Types::ISO8601DateTime
11
+ field :max_iteration, Integer
12
+ field :iteration, Integer
13
+ field :state, String
14
+ field :purpose, String
15
+ belongs_to :triggering, Types::Triggering
16
+
17
+ def self.graphql_definition
18
+ super.tap { |type| type.instance_variable_set(:@name, 'ForemanTasks::RecurringLogic') }
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,25 @@
1
+ module Types
2
+ class Task < Types::BaseObject
3
+ description 'A Task'
4
+ model_class ::ForemanTasks::Task
5
+
6
+ global_id_field :id
7
+ field :type, String
8
+ field :label, String
9
+ field :started_at, GraphQL::Types::ISO8601DateTime
10
+ field :ended_at, GraphQL::Types::ISO8601DateTime
11
+ field :state, String
12
+ field :result, String
13
+ field :external_id, String
14
+ field :parent_task_id, String
15
+ field :start_at, GraphQL::Types::ISO8601DateTime
16
+ field :start_before, GraphQL::Types::ISO8601DateTime
17
+ field :action, String
18
+ field :user_id, Integer
19
+ field :state_updated_at, GraphQL::Types::ISO8601DateTime
20
+
21
+ def self.graphql_definition
22
+ super.tap { |type| type.instance_variable_set(:@name, 'ForemanTasks::Task') }
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,16 @@
1
+ module Types
2
+ class Triggering < Types::BaseObject
3
+ description 'A Task Triggering'
4
+ model_class ::ForemanTasks::Triggering
5
+
6
+ global_id_field :id
7
+ field :mode, String
8
+ field :start_at, GraphQL::Types::ISO8601DateTime
9
+ field :start_before, GraphQL::Types::ISO8601DateTime
10
+ field :recurring_logic, Types::RecurringLogic
11
+
12
+ def self.graphql_definition
13
+ super.tap { |type| type.instance_variable_set(:@name, 'ForemanTasks::Triggering') }
14
+ end
15
+ end
16
+ end
@@ -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
@@ -8,7 +8,7 @@ module Actions
8
8
  end
9
9
 
10
10
  def continuous_output
11
- continuous_output = ::ForemanTasksCore::ContinuousOutput.new
11
+ continuous_output = ::ForemanTasks::ContinuousOutput.new
12
12
  continuous_output_providers.each do |continous_output_provider|
13
13
  continous_output_provider.fill_continuous_output(continuous_output)
14
14
  end
@@ -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
@@ -4,6 +4,8 @@ module ForemanTasks
4
4
  class RecurringLogic < ApplicationRecord
5
5
  include Authorizable
6
6
 
7
+ graphql_type '::Types::RecurringLogic'
8
+
7
9
  belongs_to :task_group
8
10
  belongs_to :triggering
9
11
 
@@ -15,6 +17,9 @@ module ForemanTasks
15
17
  scoped_search :on => :iteration, :complete_value => false
16
18
  scoped_search :on => :cron_line, :complete_value => true
17
19
  scoped_search :on => :state, :complete_value => true
20
+ scoped_search :on => :purpose, :complete_value => true
21
+
22
+ validate :valid_purpose
18
23
 
19
24
  before_create do
20
25
  task_group.save
@@ -167,6 +172,7 @@ module ForemanTasks
167
172
  ::ForemanTasks::RecurringLogic.new_from_cronline(cronline).tap do |manager|
168
173
  manager.end_time = triggering.end_time if triggering.end_time_limited.present?
169
174
  manager.max_iteration = triggering.max_iteration if triggering.max_iteration.present?
175
+ manager.purpose = triggering.purpose if triggering.purpose.present?
170
176
  manager.triggering = triggering
171
177
  end
172
178
  end
@@ -187,5 +193,9 @@ module ForemanTasks
187
193
  end
188
194
  hash.select { |key, _| allowed_keys.include? key }
189
195
  end
196
+
197
+ def valid_purpose?
198
+ !(purpose.present? && self.class.where(:purpose => purpose, :state => %w[active disabled]).any?)
199
+ end
190
200
  end
191
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']
@@ -5,6 +5,8 @@ module ForemanTasks
5
5
  include Authorizable
6
6
  extend Search
7
7
 
8
+ graphql_type '::Types::Task'
9
+
8
10
  def check_permissions_after_save
9
11
  # there's no create_tasks permission, tasks are created as a result of internal actions, in such case we
10
12
  # don't do authorization, that should have been performed on wrapping action level
@@ -76,6 +78,7 @@ module ForemanTasks
76
78
  :"foreman_tasks_links.resource_type" => resource.class.name)
77
79
  end)
78
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") }
79
82
 
80
83
  apipie :class, "A class representing #{model_name.human} object" do
81
84
  name 'Task'
@@ -255,6 +258,32 @@ module ForemanTasks
255
258
  main_action.continuous_output.raw_outputs
256
259
  end
257
260
 
261
+ def self.latest_tasks_by_resource_ids(label, resource_type, resource_ids)
262
+ tasks = arel_table
263
+ links = ForemanTasks::Link.arel_table
264
+ started_at = tasks[:started_at]
265
+ resource_id = links[:resource_id]
266
+
267
+ base_combined_table = tasks
268
+ .join(links).on(tasks[:id].eq(links[:task_id]))
269
+ .where(tasks[:label].eq(label)
270
+ .and(links[:resource_type].eq(resource_type))
271
+ .and(links[:resource_id].in(resource_ids)))
272
+
273
+ grouped = base_combined_table.project(
274
+ started_at.maximum.as('started_at_max'),
275
+ resource_id
276
+ ).group(resource_id).order(resource_id).as('grouped')
277
+
278
+ max_per_resource_id = tasks
279
+ .join(links).on(tasks[:id].eq(links[:task_id]))
280
+ .join(grouped).on(grouped[:started_at_max].eq(started_at).and(grouped[:resource_id].eq(resource_id)))
281
+ .distinct
282
+ .project(tasks[Arel.star], grouped[:resource_id])
283
+
284
+ find_by_sql(max_per_resource_id.to_sql).index_by(&:resource_id)
285
+ end
286
+
258
287
  protected
259
288
 
260
289
  def generate_id
@@ -2,9 +2,11 @@ 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
+ graphql_type '::Types::Triggering'
9
+
8
10
  before_save do
9
11
  if future?
10
12
  parse_start_at!
@@ -28,7 +30,7 @@ module ForemanTasks
28
30
  validates :input_type, :if => :recurring?,
29
31
  :inclusion => { :in => ALLOWED_INPUT_TYPES,
30
32
  :message => _('%{value} is not allowed input type') }
31
- 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) },
32
34
  :message => _('%{value} is wrong format') }
33
35
  validates :start_before_raw, format: { :with => TIME_REGEXP, :if => :future?,
34
36
  :message => _('%{value} is wrong format'), :allow_blank => true }
@@ -68,7 +70,7 @@ module ForemanTasks
68
70
  delay_options,
69
71
  *args
70
72
  when :recurring
71
- recurring_logic.start(action, *args)
73
+ recurring_logic.start_after(action, delay_options[:start_at] || Time.zone.now, *args)
72
74
  end
73
75
  end
74
76
 
@@ -92,7 +94,13 @@ module ForemanTasks
92
94
  end
93
95
 
94
96
  def parse_start_at!
95
- 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'))
96
104
  end
97
105
 
98
106
  def parse_start_before!
@@ -102,7 +110,9 @@ module ForemanTasks
102
110
  private
103
111
 
104
112
  def can_start_recurring
113
+ parse_start_at
105
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?
106
116
  errors.add(:cronline, _('%s is not valid format of cron line') % cronline) unless recurring_logic.valid_cronline?
107
117
  end
108
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