foreman-tasks 4.1.5 → 5.2.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.github/workflows/ruby_tests.yml +0 -1
- data/.rubocop.yml +0 -4
- data/.rubocop_todo.yml +0 -2
- data/README.md +8 -6
- data/app/assets/javascripts/foreman_tasks/trigger_form.js +7 -0
- data/app/controllers/foreman_tasks/api/tasks_controller.rb +2 -2
- data/app/controllers/foreman_tasks/tasks_controller.rb +2 -2
- data/app/graphql/mutations/recurring_logics/cancel.rb +27 -0
- data/app/graphql/types/recurring_logic.rb +21 -0
- data/app/graphql/types/task.rb +25 -0
- data/app/graphql/types/triggering.rb +16 -0
- data/app/helpers/foreman_tasks/foreman_tasks_helper.rb +4 -1
- data/app/lib/actions/helpers/with_continuous_output.rb +1 -1
- data/app/lib/actions/proxy_action.rb +2 -12
- data/app/lib/actions/trigger_proxy_batch.rb +79 -0
- data/app/models/foreman_tasks/recurring_logic.rb +10 -0
- data/app/models/foreman_tasks/remote_task.rb +3 -19
- data/app/models/foreman_tasks/task.rb +29 -0
- data/app/models/foreman_tasks/triggering.rb +14 -4
- data/app/views/foreman_tasks/api/tasks/show.json.rabl +1 -1
- data/app/views/foreman_tasks/layouts/react.html.erb +0 -1
- data/app/views/foreman_tasks/recurring_logics/index.html.erb +4 -2
- data/app/views/foreman_tasks/task_groups/recurring_logic_task_groups/_recurring_logic_task_group.html.erb +8 -0
- data/db/migrate/20210720115251_add_purpose_to_recurring_logic.rb +6 -0
- data/extra/foreman-tasks-cleanup.sh +127 -0
- data/extra/foreman-tasks-export.sh +121 -0
- data/foreman-tasks.gemspec +1 -4
- data/lib/foreman_tasks/continuous_output.rb +50 -0
- data/lib/foreman_tasks/engine.rb +8 -15
- data/lib/foreman_tasks/tasks/export_tasks.rake +29 -8
- data/lib/foreman_tasks/version.rb +1 -1
- data/lib/foreman_tasks.rb +2 -5
- data/locale/fr/LC_MESSAGES/foreman_tasks.mo +0 -0
- data/locale/ja/LC_MESSAGES/foreman_tasks.mo +0 -0
- data/locale/zh_CN/LC_MESSAGES/foreman_tasks.mo +0 -0
- data/package.json +7 -9
- data/test/controllers/api/tasks_controller_test.rb +19 -1
- data/test/controllers/tasks_controller_test.rb +19 -0
- data/test/factories/recurring_logic_factory.rb +7 -1
- data/test/graphql/mutations/recurring_logics/cancel_mutation_test.rb +66 -0
- data/test/graphql/queries/recurring_logic_test.rb +28 -0
- data/test/graphql/queries/recurring_logics_query_test.rb +30 -0
- data/test/graphql/queries/task_query_test.rb +33 -0
- data/test/graphql/queries/tasks_query_test.rb +31 -0
- data/test/support/dummy_proxy_action.rb +6 -0
- data/test/unit/actions/proxy_action_test.rb +11 -11
- data/test/unit/actions/trigger_proxy_batch_test.rb +59 -0
- data/test/unit/remote_task_test.rb +0 -8
- data/test/unit/task_test.rb +39 -8
- data/test/unit/triggering_test.rb +22 -0
- metadata +23 -26
- data/test/core/unit/dispatcher_test.rb +0 -43
- data/test/core/unit/runner_test.rb +0 -116
- data/test/core/unit/task_launcher_test.rb +0 -56
- data/test/foreman_tasks_core_test_helper.rb +0 -4
- data/test/unit/otp_manager_test.rb +0 -77
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: fa2189368cc093a6ca613ca1b91add99bc3689b2cb22cd0c04a3718821b5f27f
|
4
|
+
data.tar.gz: f7e82294bbbb76fe6576b6c2de9e55c16342962f711a8e5c9070787be304a87c
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: f20e5fba4dbc2401a4fed1f5eb68961ccc7095071fc309316ad14392b9209141e96aa2372eb7bb5db0e7f829ef08727ff2e232e53de86baa8385183ddb722252
|
7
|
+
data.tar.gz: 534ac74d1c8010ac25330562a6bdb713beacc413d2c681ee72f865727741cfb17e04d9dda2c0ea8ab7bde2f7f2e08f54fa4266e25686ca9d1f38c5309b24b94c
|
data/.rubocop.yml
CHANGED
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: [
|
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
|
158
|
-
tasks can
|
159
|
-
|
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
|
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.
|
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 = ::
|
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
|
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] &&
|
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.
|
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
|
-
|
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 =>
|
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.
|
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>
|