foreman-tasks 5.1.1 → 5.2.2
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.
- checksums.yaml +4 -4
- data/.rubocop.yml +0 -4
- data/.rubocop_todo.yml +0 -2
- data/README.md +8 -6
- data/app/controllers/foreman_tasks/tasks_controller.rb +18 -18
- data/app/graphql/mutations/recurring_logics/cancel.rb +27 -0
- data/app/graphql/types/recurring_logic.rb +2 -0
- data/app/lib/actions/proxy_action.rb +2 -12
- data/app/lib/actions/trigger_proxy_batch.rb +2 -1
- data/app/models/foreman_tasks/remote_task.rb +6 -21
- data/app/views/foreman_tasks/task_groups/recurring_logic_task_groups/_recurring_logic_task_group.html.erb +4 -0
- data/extra/foreman-tasks-cleanup.sh +19 -2
- data/extra/foreman-tasks-export.sh +7 -3
- data/foreman-tasks.gemspec +1 -3
- data/lib/foreman_tasks/engine.rb +2 -0
- data/lib/foreman_tasks/tasks/export_tasks.rake +43 -15
- data/lib/foreman_tasks/version.rb +1 -1
- 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 +1 -1
- data/test/factories/recurring_logic_factory.rb +7 -1
- data/test/graphql/mutations/recurring_logics/cancel_mutation_test.rb +66 -0
- data/test/support/dummy_proxy_action.rb +6 -0
- data/test/unit/actions/proxy_action_test.rb +11 -11
- data/test/unit/remote_task_test.rb +21 -3
- metadata +6 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 882587333f9ebd555b6bd0ca6adf6c9e75789fce2e84cbbc2e3af0943cdf0051
|
4
|
+
data.tar.gz: 1db069d66971fed8af95856a2d582b1320c02c48fddec68f9f9a1061abc6965c
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: b204e43fe719216b44377fa0eb69654a404dfc7d53fbd5ed803efca3f5dd649d2dc901abd1b29b4048c1f99f7a37bd1b94c5c349ae9844e08ae95bd86a942b35
|
7
|
+
data.tar.gz: 90c69b77188a55d8d993a6e098945448dce1ff720697def8ea1914ba2c03fe2ed7efec30b4a7e33224afa9b32d3b9f1c8eb4fd37cdee37566321e69d67d78b1a
|
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
|
@@ -4,6 +4,8 @@ module ForemanTasks
|
|
4
4
|
include Foreman::Controller::CsvResponder
|
5
5
|
include ForemanTasks::FindTasksCommon
|
6
6
|
|
7
|
+
before_action :find_dynflow_task, only: [:unlock, :force_unlock, :cancel, :cancel_step, :resume]
|
8
|
+
|
7
9
|
def show
|
8
10
|
@task = resource_base.find(params[:id])
|
9
11
|
render :layout => !request.xhr?
|
@@ -31,8 +33,7 @@ module ForemanTasks
|
|
31
33
|
end
|
32
34
|
|
33
35
|
def cancel_step
|
34
|
-
|
35
|
-
result = ForemanTasks.dynflow.world.event(task.external_id, params[:step_id].to_i, ::Dynflow::Action::Cancellable::Cancel).wait
|
36
|
+
result = ForemanTasks.dynflow.world.event(@dynflow_task.external_id, params[:step_id].to_i, ::Dynflow::Action::Cancellable::Cancel).wait
|
36
37
|
if result.rejected?
|
37
38
|
render json: { error: result.reason }, status: :bad_request
|
38
39
|
else
|
@@ -41,8 +42,7 @@ module ForemanTasks
|
|
41
42
|
end
|
42
43
|
|
43
44
|
def cancel
|
44
|
-
|
45
|
-
if task.cancel
|
45
|
+
if @dynflow_task.cancel
|
46
46
|
render json: { statusText: 'OK' }
|
47
47
|
else
|
48
48
|
render json: {}, status: :bad_request
|
@@ -50,19 +50,17 @@ module ForemanTasks
|
|
50
50
|
end
|
51
51
|
|
52
52
|
def abort
|
53
|
-
|
54
|
-
if task.abort
|
53
|
+
if @dynflow_task.abort
|
55
54
|
flash[:info] = _('Trying to abort the task')
|
56
55
|
else
|
57
56
|
flash[:warning] = _('The task cannot be aborted at the moment.')
|
58
57
|
end
|
59
|
-
redirect_back(:fallback_location => foreman_tasks_task_path(
|
58
|
+
redirect_back(:fallback_location => foreman_tasks_task_path(@dynflow_task))
|
60
59
|
end
|
61
60
|
|
62
61
|
def resume
|
63
|
-
|
64
|
-
|
65
|
-
ForemanTasks.dynflow.world.execute(task.execution_plan.id)
|
62
|
+
if @dynflow_task.resumable?
|
63
|
+
ForemanTasks.dynflow.world.execute(@dynflow_task.execution_plan.id)
|
66
64
|
render json: { statusText: 'OK' }
|
67
65
|
else
|
68
66
|
render json: {}, status: :bad_request
|
@@ -70,10 +68,8 @@ module ForemanTasks
|
|
70
68
|
end
|
71
69
|
|
72
70
|
def unlock
|
73
|
-
|
74
|
-
|
75
|
-
task.state = :stopped
|
76
|
-
task.save!
|
71
|
+
if @dynflow_task.paused?
|
72
|
+
unlock_task(@dynflow_task)
|
77
73
|
render json: { statusText: 'OK' }
|
78
74
|
else
|
79
75
|
render json: {}, status: :bad_request
|
@@ -81,9 +77,7 @@ module ForemanTasks
|
|
81
77
|
end
|
82
78
|
|
83
79
|
def force_unlock
|
84
|
-
|
85
|
-
task.state = :stopped
|
86
|
-
task.save!
|
80
|
+
unlock_task(@dynflow_task)
|
87
81
|
render json: { statusText: 'OK' }
|
88
82
|
end
|
89
83
|
|
@@ -98,6 +92,12 @@ module ForemanTasks
|
|
98
92
|
|
99
93
|
private
|
100
94
|
|
95
|
+
def unlock_task(task)
|
96
|
+
task.state = :stopped
|
97
|
+
task.locks.destroy_all
|
98
|
+
task.save!
|
99
|
+
end
|
100
|
+
|
101
101
|
def respond_with_tasks(scope)
|
102
102
|
@tasks = filter(scope, paginate: false).with_duration
|
103
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'])
|
@@ -123,7 +123,7 @@ module ForemanTasks
|
|
123
123
|
end
|
124
124
|
|
125
125
|
def find_dynflow_task
|
126
|
-
resource_scope.where(:type => 'ForemanTasks::Task::DynflowTask').find(params[:id])
|
126
|
+
@dynflow_task = resource_scope.where(:type => 'ForemanTasks::Task::DynflowTask').find(params[:id])
|
127
127
|
end
|
128
128
|
|
129
129
|
def filter(scope, paginate: true)
|
@@ -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
|
@@ -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
|
@@ -40,7 +40,8 @@ module Actions
|
|
40
40
|
end
|
41
41
|
output[:planned_count] += batch.size
|
42
42
|
rescue => e
|
43
|
-
action_logger.warn "Could not trigger task on the smart proxy
|
43
|
+
action_logger.warn "Could not trigger task on the smart proxy"
|
44
|
+
action_logger.warn e
|
44
45
|
batch.each { |remote_task| remote_task.update_from_batch_trigger({}) }
|
45
46
|
output[:failed_count] += batch.size
|
46
47
|
end
|
@@ -16,9 +16,10 @@ 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
|
-
logger.warn "Could not trigger task on the smart proxy
|
21
|
+
logger.warn "Could not trigger task on the smart proxy"
|
22
|
+
logger.warn e
|
22
23
|
{}
|
23
24
|
end
|
24
25
|
update_from_batch_trigger(response)
|
@@ -26,33 +27,17 @@ module ForemanTasks
|
|
26
27
|
end
|
27
28
|
|
28
29
|
def self.batch_trigger(operation, remote_tasks)
|
29
|
-
remote_tasks.group_by(&:proxy_url).
|
30
|
+
remote_tasks.group_by(&:proxy_url).each_value do |group|
|
30
31
|
input_hash = group.reduce({}) do |acc, remote_task|
|
31
32
|
acc.merge(remote_task.execution_plan_id => { :action_input => remote_task.proxy_input,
|
32
33
|
:action_class => remote_task.proxy_action_name })
|
33
34
|
end
|
34
|
-
|
35
|
+
results = group.first.proxy.launch_tasks(operation, input_hash)
|
36
|
+
group.each { |remote_task| remote_task.update_from_batch_trigger results[remote_task.execution_plan_id] }
|
35
37
|
end
|
36
38
|
remote_tasks
|
37
39
|
end
|
38
40
|
|
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
41
|
def update_from_batch_trigger(data)
|
57
42
|
if data['result'] == 'success'
|
58
43
|
self.remote_task_id = data['task_id']
|
@@ -40,4 +40,8 @@
|
|
40
40
|
<th><%= N_("Purpose") %></th>
|
41
41
|
<td><%= recurring_logic.purpose %></td>
|
42
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>
|
43
47
|
</table>
|
@@ -21,6 +21,21 @@ function build_rake() {
|
|
21
21
|
echo
|
22
22
|
}
|
23
23
|
|
24
|
+
function incorrect_usage() {
|
25
|
+
echo "$1" >&2
|
26
|
+
echo
|
27
|
+
usage
|
28
|
+
|
29
|
+
exit 1
|
30
|
+
}
|
31
|
+
|
32
|
+
function validate_options!() {
|
33
|
+
if [ -z "$TASK_SEARCH" ]; then
|
34
|
+
[ -n "$AFTER" ] && incorrect_usage "Error: -a|--after cannot be used without -s|--search"
|
35
|
+
[ -n "$STATES" ] && incorrect_usage "Error: -S|--states cannot be used without -s|--search"
|
36
|
+
fi
|
37
|
+
}
|
38
|
+
|
24
39
|
function usage() {
|
25
40
|
cat << EOF
|
26
41
|
Usage: $PROGNAME [script_options...] [options...]
|
@@ -43,8 +58,8 @@ EOF
|
|
43
58
|
echo Cleanup options:
|
44
59
|
cat <<EOF | column -s\& -t
|
45
60
|
-B|--batch-size BATCH_SIZE & process tasks in batches of BATCH_SIZE, 1000 by default
|
46
|
-
-S|--states STATES & operate on tasks in STATES, comma separated list of states, set to all to operate on tasks in any state
|
47
|
-
-a|--after AGE & operate on tasks older than AGE. Expected format is a number followed by the time unit (s,h,m,y), such as '10d' for 10 days
|
61
|
+
-S|--states STATES & operate on tasks in STATES, comma separated list of states, set to all to operate on tasks in any state. Has to be used together with -s|--search
|
62
|
+
-a|--after AGE & operate on tasks older than AGE. Expected format is a number followed by the time unit (s,h,m,y), such as '10d' for 10 days. Has to be used together with -s|--search
|
48
63
|
-b|--backup & backup deleted tasks
|
49
64
|
-n|--noop & do a dry run, print what would be done
|
50
65
|
-s|--search QUERY & use QUERY in scoped search format to match tasks to delete
|
@@ -119,6 +134,8 @@ while true; do
|
|
119
134
|
shift
|
120
135
|
done
|
121
136
|
|
137
|
+
validate_options!
|
138
|
+
|
122
139
|
if [ "$EXECUTE" -eq 1 ]; then
|
123
140
|
build_rake | sh
|
124
141
|
else
|
@@ -14,7 +14,7 @@ function die() {
|
|
14
14
|
function build_rake() {
|
15
15
|
echo -n "$RAKE_COMMAND "
|
16
16
|
echo -n 'foreman_tasks:export_tasks '
|
17
|
-
for env in TASK_SEARCH TASK_FILE TASK_FORMAT TASK_DAYS; do
|
17
|
+
for env in TASK_SEARCH TASK_FILE TASK_FORMAT TASK_DAYS SKIP_FAILED; do
|
18
18
|
local value="${!env}"
|
19
19
|
[ -n "${value}" ] && echo -n "${env}=$(printf '%q' "$value") "
|
20
20
|
done
|
@@ -46,11 +46,12 @@ EOF
|
|
46
46
|
-f|--format FORMAT & export tasks in FORMAT, one of html, html-dir, csv
|
47
47
|
-o|--output FILE & export tasks into FILE, a random file will be used if not provided
|
48
48
|
-s|--search QUERY & use QUERY in scoped search format to match tasks to export
|
49
|
+
-S|--skip-failed & skip tasks that fail to export
|
49
50
|
EOF
|
50
51
|
}
|
51
52
|
|
52
|
-
SHORTOPTS="d:Ehs:o:f:"
|
53
|
-
LONGOPTS="days:,execute,help,search:,output:,format
|
53
|
+
SHORTOPTS="d:Ehs:o:f:S"
|
54
|
+
LONGOPTS="days:,execute,help,search:,output:,format:,skip-failed"
|
54
55
|
|
55
56
|
ARGS=$(getopt -s bash \
|
56
57
|
--options $SHORTOPTS \
|
@@ -96,6 +97,9 @@ while true; do
|
|
96
97
|
-E|--execute)
|
97
98
|
EXECUTE=1
|
98
99
|
;;
|
100
|
+
-S|--skip-failed)
|
101
|
+
SKIP_FAILED=1
|
102
|
+
;;
|
99
103
|
\?)
|
100
104
|
die 1 "Invalid option: -$OPTARG"
|
101
105
|
;;
|
data/foreman-tasks.gemspec
CHANGED
@@ -20,9 +20,7 @@ same resource. It also optionally provides Dynflow infrastructure for using it f
|
|
20
20
|
DESC
|
21
21
|
|
22
22
|
s.files = `git ls-files`.split("\n").reject do |file|
|
23
|
-
file.end_with?("test.rake")
|
24
|
-
file.start_with?('lib/foreman_tasks_core') ||
|
25
|
-
file == 'foreman-tasks-core.gemspec'
|
23
|
+
file.end_with?("test.rake")
|
26
24
|
end
|
27
25
|
|
28
26
|
s.test_files = `git ls-files test`.split("\n")
|
data/lib/foreman_tasks/engine.rb
CHANGED
@@ -70,6 +70,8 @@ module ForemanTasks
|
|
70
70
|
register_graphql_query_field :recurring_logic, '::Types::RecurringLogic', :record_field
|
71
71
|
register_graphql_query_field :recurring_logics, '::Types::RecurringLogic', :collection_field
|
72
72
|
|
73
|
+
register_graphql_mutation_field :cancel_recurring_logic, ::Mutations::RecurringLogics::Cancel
|
74
|
+
|
73
75
|
logger :dynflow, :enabled => true
|
74
76
|
logger :action, :enabled => true
|
75
77
|
|
@@ -14,6 +14,7 @@ namespace :foreman_tasks do
|
|
14
14
|
* TASK_FILE : file to export to
|
15
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
|
+
* SKIP_FAILED : skip tasks that fail to export (true or false[default])
|
17
18
|
|
18
19
|
If TASK_SEARCH is not defined, it defaults to all tasks in the past 7 days and
|
19
20
|
all unsuccessful tasks in the past 60 days. The default TASK_FORMAT is html
|
@@ -240,30 +241,42 @@ namespace :foreman_tasks do
|
|
240
241
|
end
|
241
242
|
end
|
242
243
|
|
243
|
-
def csv_export(export_filename,
|
244
|
+
def csv_export(export_filename, id_scope, task_scope)
|
244
245
|
CSV.open(export_filename, 'wb') do |csv|
|
245
246
|
csv << %w[id state type label result parent_task_id started_at ended_at duration]
|
246
|
-
|
247
|
-
|
248
|
-
|
247
|
+
id_scope.pluck(:id).each_slice(1000).each do |ids|
|
248
|
+
task_scope.where(id: ids).each do |task|
|
249
|
+
with_error_handling(task) do
|
250
|
+
csv << [task.id, task.state, task.type, task.label, task.result,
|
251
|
+
task.parent_task_id, task.started_at, task.ended_at, task.duration]
|
252
|
+
end
|
253
|
+
end
|
249
254
|
end
|
250
255
|
end
|
251
256
|
end
|
252
257
|
|
253
|
-
def html_export(workdir,
|
258
|
+
def html_export(workdir, id_scope, task_scope)
|
254
259
|
PageHelper.copy_assets(workdir)
|
255
260
|
|
261
|
+
ids = id_scope.pluck(:id)
|
256
262
|
renderer = TaskRender.new
|
257
|
-
|
263
|
+
count = 0
|
264
|
+
total = ids.count
|
258
265
|
index = File.open(File.join(workdir, 'index.html'), 'w')
|
259
266
|
|
260
267
|
File.open(File.join(workdir, 'index.html'), 'w') do |index|
|
261
268
|
PageHelper.pagify(index) do |io|
|
262
269
|
PageHelper.generate_with_index(io) do |index|
|
263
|
-
|
264
|
-
|
265
|
-
|
266
|
-
|
270
|
+
ids.each_slice(1000).each do |ids|
|
271
|
+
task_scope.where(id: ids).each do |task|
|
272
|
+
content = with_error_handling(task) { renderer.render_task(task) }
|
273
|
+
if content
|
274
|
+
File.open(File.join(workdir, "#{task.id}.html"), 'w') { |file| PageHelper.pagify(file, content) }
|
275
|
+
with_error_handling(task, _('task index entry')) { PageHelper.generate_index_entry(index, task) }
|
276
|
+
end
|
277
|
+
count += 1
|
278
|
+
puts "#{count}/#{total}"
|
279
|
+
end
|
267
280
|
end
|
268
281
|
end
|
269
282
|
end
|
@@ -282,6 +295,20 @@ namespace :foreman_tasks do
|
|
282
295
|
end
|
283
296
|
end
|
284
297
|
|
298
|
+
def with_error_handling(task, what = _('task'))
|
299
|
+
yield
|
300
|
+
rescue StandardError => e
|
301
|
+
resolution = SKIP_ERRORS ? _(', skipping') : ''
|
302
|
+
puts _("WARNING: %{what} failed to export%{resolution}. Additional task details below.") % { :what => what, :resolution => resolution }
|
303
|
+
puts task.inspect
|
304
|
+
unless SKIP_ERRORS
|
305
|
+
puts _("Re-run with SKIP_FAILED=true if you want to simply skip any tasks that fail to export.")
|
306
|
+
raise e
|
307
|
+
end
|
308
|
+
end
|
309
|
+
|
310
|
+
SKIP_ERRORS = ['true', '1', 'y', 'yes'].include? (ENV['SKIP_FAILED'] || '').downcase
|
311
|
+
|
285
312
|
filter = if ENV['TASK_SEARCH'].nil? && ENV['TASK_DAYS'].nil?
|
286
313
|
"started_at > \"#{7.days.ago.to_s(:db)}\" || " \
|
287
314
|
"(result != success && started_at > \"#{60.days.ago.to_s(:db)})\""
|
@@ -297,21 +324,22 @@ namespace :foreman_tasks do
|
|
297
324
|
format = ENV['TASK_FORMAT'] || 'html'
|
298
325
|
export_filename = ENV['TASK_FILE'] || generate_filename(format)
|
299
326
|
|
300
|
-
|
327
|
+
task_scope = ForemanTasks::Task.search_for(filter).with_duration.order(:started_at => :desc)
|
328
|
+
id_scope = task_scope.group(:id, :started_at)
|
301
329
|
|
302
330
|
puts _("Exporting all tasks matching filter #{filter}")
|
303
|
-
puts _("Gathering #{
|
331
|
+
puts _("Gathering #{id_scope.count(:all).count} tasks.")
|
304
332
|
case format
|
305
333
|
when 'html'
|
306
334
|
Dir.mktmpdir('task-export') do |tmp_dir|
|
307
|
-
html_export(tmp_dir,
|
335
|
+
html_export(tmp_dir, id_scope, task_scope)
|
308
336
|
system("tar", "czf", export_filename, tmp_dir)
|
309
337
|
end
|
310
338
|
when 'html-dir'
|
311
339
|
FileUtils.mkdir_p(export_filename)
|
312
|
-
html_export(export_filename,
|
340
|
+
html_export(export_filename, id_scope, task_scope)
|
313
341
|
when 'csv'
|
314
|
-
csv_export(export_filename,
|
342
|
+
csv_export(export_filename, id_scope, task_scope)
|
315
343
|
else
|
316
344
|
raise "Unkonwn export format '#{format}'"
|
317
345
|
end
|
Binary file
|
Binary file
|
Binary file
|
data/package.json
CHANGED
@@ -23,20 +23,18 @@
|
|
23
23
|
"url": "http://projects.theforeman.org/projects/foreman-tasks/issues"
|
24
24
|
},
|
25
25
|
"peerDependencies": {
|
26
|
-
"@theforeman/vendor": "
|
26
|
+
"@theforeman/vendor": "^8.15.0"
|
27
27
|
},
|
28
28
|
"dependencies": {
|
29
|
-
"c3": "^0.4.11"
|
30
|
-
"humanize-duration": "^3.20.1",
|
31
|
-
"react-intl": "^2.8.0"
|
29
|
+
"c3": "^0.4.11"
|
32
30
|
},
|
33
31
|
"devDependencies": {
|
34
32
|
"@babel/core": "^7.7.0",
|
35
|
-
"@theforeman/builder": "^
|
36
|
-
"@theforeman/eslint-plugin-foreman": "
|
37
|
-
"@theforeman/stories": "^
|
38
|
-
"@theforeman/test": "^
|
39
|
-
"@theforeman/vendor-dev": "^
|
33
|
+
"@theforeman/builder": "^8.15.0",
|
34
|
+
"@theforeman/eslint-plugin-foreman": "^8.15.0",
|
35
|
+
"@theforeman/stories": "^8.15.0",
|
36
|
+
"@theforeman/test": "^8.15.0",
|
37
|
+
"@theforeman/vendor-dev": "^8.15.0",
|
40
38
|
"babel-eslint": "^10.0.3",
|
41
39
|
"eslint": "^6.7.2",
|
42
40
|
"jed": "^1.1.1",
|
@@ -192,7 +192,7 @@ module ForemanTasks
|
|
192
192
|
_(task.state).must_equal 'running'
|
193
193
|
_(task.result).must_equal 'pending'
|
194
194
|
|
195
|
-
callback = Support::DummyProxyAction.proxy.log[:trigger_task].first[1][:callback]
|
195
|
+
callback = Support::DummyProxyAction.proxy.log[:trigger_task].first[1][:action_input][:callback]
|
196
196
|
post :callback, params: { 'callback' => callback, 'data' => { 'result' => 'success' } }
|
197
197
|
triggered.finished.wait(5)
|
198
198
|
|
@@ -1,8 +1,14 @@
|
|
1
1
|
FactoryBot.define do
|
2
2
|
factory :recurring_logic, :class => ForemanTasks::RecurringLogic do
|
3
3
|
cron_line { '* * * * *' }
|
4
|
-
|
4
|
+
association :task_group
|
5
5
|
end
|
6
6
|
|
7
|
+
factory :task_group, :class => ::ForemanTasks::TaskGroup do
|
8
|
+
type { "ForemanTasks::TaskGroups::RecurringLogicTaskGroup" }
|
9
|
+
end
|
7
10
|
factory :recurring_logic_task_group, :class => ::ForemanTasks::TaskGroups::RecurringLogicTaskGroup
|
11
|
+
factory :task_group_member, :class => ::ForemanTasks::TaskGroupMember do
|
12
|
+
association :task_group, :task
|
13
|
+
end
|
8
14
|
end
|
@@ -0,0 +1,66 @@
|
|
1
|
+
require 'foreman_tasks_test_helper'
|
2
|
+
|
3
|
+
module Mutations
|
4
|
+
module RecurringLogics
|
5
|
+
class CancelMutationTest < ActiveSupport::TestCase
|
6
|
+
setup do
|
7
|
+
@task = FactoryBot.create(:dynflow_task, state: 'pending')
|
8
|
+
@task_group = FactoryBot.create(:recurring_logic_task_group)
|
9
|
+
@task_group_member = FactoryBot.create(:task_group_member, task: @task, task_group: @task_group)
|
10
|
+
@recurring_logic = FactoryBot.create(:recurring_logic, task_group: @task_group)
|
11
|
+
@id = Foreman::GlobalId.for(@recurring_logic)
|
12
|
+
@variables = { id: @id }
|
13
|
+
@query =
|
14
|
+
<<-GRAPHQL
|
15
|
+
mutation CancelRecurringLogic($id:ID!) {
|
16
|
+
cancelRecurringLogic(input: { id: $id }){
|
17
|
+
recurringLogic {
|
18
|
+
id
|
19
|
+
state
|
20
|
+
cronLine
|
21
|
+
}
|
22
|
+
errors {
|
23
|
+
message
|
24
|
+
path
|
25
|
+
}
|
26
|
+
}
|
27
|
+
}
|
28
|
+
GRAPHQL
|
29
|
+
end
|
30
|
+
|
31
|
+
context 'as admin' do
|
32
|
+
setup do
|
33
|
+
@context = { current_user: FactoryBot.create(:user, :admin) }
|
34
|
+
end
|
35
|
+
|
36
|
+
test 'should cancel recurring logic' do
|
37
|
+
assert_not_equal 'cancelled', @recurring_logic.state
|
38
|
+
result = ForemanGraphqlSchema.execute(@query, variables: @variables, context: @context)
|
39
|
+
assert_empty result['errors']
|
40
|
+
assert_empty result['data']['cancelRecurringLogic']['errors']
|
41
|
+
assert_equal 'cancelled', result['data']['cancelRecurringLogic']['recurringLogic']['state']
|
42
|
+
@recurring_logic.reload
|
43
|
+
assert_equal 'cancelled', @recurring_logic.state
|
44
|
+
end
|
45
|
+
|
46
|
+
test 'should handle errors on execution plan load failure' do
|
47
|
+
invalid_plan = ::Dynflow::ExecutionPlan::InvalidPlan.new(StandardError.new('This is a failure'), 'xyz', 'test-label', 'invalid')
|
48
|
+
::Dynflow::Persistence.any_instance.stubs(:load_execution_plan).returns(invalid_plan)
|
49
|
+
result = ForemanGraphqlSchema.execute(@query, variables: @variables, context: @context)
|
50
|
+
assert_equal "There has been an error when canceling one of the tasks: This is a failure", result['data']['cancelRecurringLogic']['errors'].first['message']
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
context 'as viewer' do
|
55
|
+
setup do
|
56
|
+
@context = { current_user: setup_user('view', 'recurring_logics') }
|
57
|
+
end
|
58
|
+
|
59
|
+
test 'should not allow to cancel recurring logic' do
|
60
|
+
result = ForemanGraphqlSchema.execute(@query, variables: @variables, context: @context)
|
61
|
+
assert_includes result['errors'].first['message'], 'Unauthorized.'
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
@@ -36,6 +36,12 @@ module Support
|
|
36
36
|
def statuses
|
37
37
|
{ version: DummyProxyVersion.new('1.21.0') }
|
38
38
|
end
|
39
|
+
|
40
|
+
def launch_tasks(operation, args = {})
|
41
|
+
@log[:trigger_task] << [operation, args]
|
42
|
+
@task_triggered.fulfill(true)
|
43
|
+
{ 'task_id' => @uuid, 'result' => 'success' }
|
44
|
+
end
|
39
45
|
end
|
40
46
|
|
41
47
|
class ProxySelector < ::ForemanTasks::ProxySelector
|
@@ -11,12 +11,11 @@ module ForemanTasks
|
|
11
11
|
let(:batch_triggering) { false }
|
12
12
|
|
13
13
|
before do
|
14
|
-
Support::DummyProxyAction.any_instance.stubs(:with_batch_triggering?).returns(batch_triggering)
|
15
14
|
Support::DummyProxyAction.reset
|
16
15
|
RemoteTask.any_instance.stubs(:proxy).returns(Support::DummyProxyAction.proxy)
|
17
16
|
Setting.stubs(:[]).with('foreman_tasks_proxy_action_retry_interval')
|
18
17
|
Setting.stubs(:[]).with('foreman_tasks_proxy_action_retry_count')
|
19
|
-
Setting.stubs(:[]).with('foreman_tasks_proxy_batch_trigger')
|
18
|
+
Setting.stubs(:[]).with('foreman_tasks_proxy_batch_trigger').returns(batch_triggering)
|
20
19
|
@action = create_and_plan_action(Support::DummyProxyAction,
|
21
20
|
Support::DummyProxyAction.proxy,
|
22
21
|
'Proxy::DummyAction',
|
@@ -29,17 +28,18 @@ module ForemanTasks
|
|
29
28
|
describe 'first run' do
|
30
29
|
it 'triggers the corresponding action on the proxy' do
|
31
30
|
proxy_call = Support::DummyProxyAction.proxy.log[:trigger_task].first
|
32
|
-
expected_call = ['
|
33
|
-
{
|
34
|
-
|
35
|
-
'
|
31
|
+
expected_call = ['single',
|
32
|
+
{ :action_class => 'Proxy::DummyAction',
|
33
|
+
:action_input =>
|
34
|
+
{ 'foo' => 'bar',
|
35
|
+
'secrets' => secrets,
|
36
|
+
'connection_options' =>
|
36
37
|
{ 'retry_interval' => 15, 'retry_count' => 4,
|
37
38
|
'proxy_batch_triggering' => batch_triggering },
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
'callback' => { 'task_id' => Support::DummyProxyAction.proxy.uuid, 'step_id' => @action.run_step_id } }]
|
39
|
+
'use_batch_triggering' => batch_triggering,
|
40
|
+
'proxy_url' => 'proxy.example.com',
|
41
|
+
'proxy_action_name' => 'Proxy::DummyAction',
|
42
|
+
'callback' => { 'task_id' => Support::DummyProxyAction.proxy.uuid, 'step_id' => @action.run_step_id } } }]
|
43
43
|
_(proxy_call).must_equal(expected_call)
|
44
44
|
end
|
45
45
|
|
@@ -29,12 +29,30 @@ module ForemanTasks
|
|
29
29
|
end
|
30
30
|
end
|
31
31
|
|
32
|
-
it '
|
32
|
+
it 'honors the batches with multiple proxies' do
|
33
|
+
remote_task = remote_tasks.last
|
34
|
+
remote_task.proxy_url = 'something else'
|
35
|
+
|
36
|
+
results = remote_tasks.reduce({}) do |acc, cur|
|
37
|
+
acc.merge(cur.execution_plan_id.to_s => { 'task_id' => cur.id + 5, 'result' => 'success' })
|
38
|
+
end
|
39
|
+
other_results = { remote_task.execution_plan_id => results.delete(remote_task.execution_plan_id) }
|
40
|
+
|
33
41
|
fake_proxy = mock
|
34
|
-
fake_proxy.expects(:launch_tasks).
|
42
|
+
fake_proxy.expects(:launch_tasks).returns(results)
|
43
|
+
|
44
|
+
another_fake_proxy = mock
|
45
|
+
another_fake_proxy.expects(:launch_tasks).returns(other_results)
|
46
|
+
|
35
47
|
remote_tasks.first.expects(:proxy).returns(fake_proxy)
|
36
|
-
remote_tasks.
|
48
|
+
remote_tasks.last.expects(:proxy).returns(another_fake_proxy)
|
49
|
+
|
37
50
|
RemoteTask.batch_trigger('a_operation', remote_tasks)
|
51
|
+
remote_tasks.each do |remote_task|
|
52
|
+
remote_task.reload
|
53
|
+
_(remote_task.state).must_equal 'triggered'
|
54
|
+
_(remote_task.remote_task_id).must_equal((remote_task.id + 5).to_s)
|
55
|
+
end
|
38
56
|
end
|
39
57
|
end
|
40
58
|
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: foreman-tasks
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 5.
|
4
|
+
version: 5.2.2
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Ivan Nečas
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 1980-01-01 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: dynflow
|
@@ -133,6 +133,7 @@ files:
|
|
133
133
|
- app/controllers/foreman_tasks/react_controller.rb
|
134
134
|
- app/controllers/foreman_tasks/recurring_logics_controller.rb
|
135
135
|
- app/controllers/foreman_tasks/tasks_controller.rb
|
136
|
+
- app/graphql/mutations/recurring_logics/cancel.rb
|
136
137
|
- app/graphql/types/recurring_logic.rb
|
137
138
|
- app/graphql/types/task.rb
|
138
139
|
- app/graphql/types/triggering.rb
|
@@ -304,6 +305,7 @@ files:
|
|
304
305
|
- test/factories/task_factory.rb
|
305
306
|
- test/factories/triggering_factory.rb
|
306
307
|
- test/foreman_tasks_test_helper.rb
|
308
|
+
- test/graphql/mutations/recurring_logics/cancel_mutation_test.rb
|
307
309
|
- test/graphql/queries/recurring_logic_test.rb
|
308
310
|
- test/graphql/queries/recurring_logics_query_test.rb
|
309
311
|
- test/graphql/queries/task_query_test.rb
|
@@ -605,7 +607,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
605
607
|
- !ruby/object:Gem::Version
|
606
608
|
version: '0'
|
607
609
|
requirements: []
|
608
|
-
rubygems_version: 3.
|
610
|
+
rubygems_version: 3.2.26
|
609
611
|
signing_key:
|
610
612
|
specification_version: 4
|
611
613
|
summary: Foreman plugin for showing tasks information for resources and users
|
@@ -618,6 +620,7 @@ test_files:
|
|
618
620
|
- test/factories/task_factory.rb
|
619
621
|
- test/factories/triggering_factory.rb
|
620
622
|
- test/foreman_tasks_test_helper.rb
|
623
|
+
- test/graphql/mutations/recurring_logics/cancel_mutation_test.rb
|
621
624
|
- test/graphql/queries/recurring_logic_test.rb
|
622
625
|
- test/graphql/queries/recurring_logics_query_test.rb
|
623
626
|
- test/graphql/queries/task_query_test.rb
|