foreman-tasks 3.0.6 → 4.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/app/lib/actions/entry_action.rb +8 -4
- data/app/lib/actions/helpers/lock.rb +11 -5
- data/app/lib/actions/middleware/keep_current_request_id.rb +4 -1
- data/app/lib/actions/middleware/keep_current_user.rb +11 -1
- data/app/lib/actions/observable_action.rb +80 -0
- data/app/models/foreman_tasks/concerns/action_subject.rb +0 -6
- data/app/models/foreman_tasks/link.rb +60 -0
- data/app/models/foreman_tasks/lock.rb +30 -128
- data/app/models/foreman_tasks/task.rb +20 -7
- data/app/models/foreman_tasks/task/dynflow_task.rb +3 -8
- data/app/models/foreman_tasks/task/search.rb +6 -6
- data/app/views/foreman_tasks/api/locks/show.json.rabl +4 -0
- data/app/views/foreman_tasks/api/tasks/details.json.rabl +5 -3
- data/app/views/foreman_tasks/tasks/_lock_card.html.erb +10 -0
- data/db/migrate/20181206123910_create_foreman_tasks_links.foreman_tasks.rb +26 -0
- data/db/migrate/20181206124952_migrate_non_exclusive_locks_to_links.foreman_tasks.rb +14 -0
- data/db/migrate/20181206131436_drop_old_locks.foreman_tasks.rb +20 -0
- data/db/migrate/20181206131627_make_locks_exclusive.foreman_tasks.rb +25 -0
- data/lib/foreman_tasks/cleaner.rb +10 -0
- data/lib/foreman_tasks/engine.rb +5 -2
- data/lib/foreman_tasks/tasks/export_tasks.rake +2 -2
- data/lib/foreman_tasks/version.rb +1 -1
- data/package.json +6 -6
- data/test/controllers/tasks_controller_test.rb +1 -1
- data/test/unit/actions/action_with_sub_plans_test.rb +5 -2
- data/test/unit/cleaner_test.rb +4 -4
- data/test/unit/locking_test.rb +85 -0
- data/webpack/ForemanTasks/Components/TaskDetails/Components/Locks.js +2 -2
- data/webpack/ForemanTasks/Components/TaskDetails/Components/TaskInfo.js +3 -2
- data/webpack/ForemanTasks/Components/TaskDetails/Components/__tests__/__snapshots__/Locks.test.js.snap +4 -4
- data/webpack/ForemanTasks/Components/TaskDetails/Components/__tests__/__snapshots__/TaskInfo.test.js.snap +2 -0
- data/webpack/ForemanTasks/Components/TaskDetails/TaskDetails.js +4 -1
- data/webpack/ForemanTasks/Components/TaskDetails/TaskDetails.scss +5 -1
- data/webpack/ForemanTasks/Components/TaskDetails/TaskDetailsSelectors.js +3 -0
- data/webpack/ForemanTasks/Components/TaskDetails/index.js +2 -0
- data/webpack/ForemanTasks/Components/TasksTable/TasksTablePage.scss +4 -3
- data/webpack/ForemanTasks/Components/TasksTable/__tests__/__snapshots__/TasksTablePage.test.js.snap +2 -2
- data/webpack/ForemanTasks/Components/TasksTable/formatters/__test__/__snapshots__/actionNameCellFormatter.test.js.snap +2 -3
- data/webpack/ForemanTasks/Components/TasksTable/formatters/actionNameCellFormatter.js +2 -3
- metadata +12 -4
- data/test/unit/lock_test.rb +0 -22
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: bded348bac199b96bf0c1bf458c0785cdc233892acc1a5d06a940a8c5b28309b
|
4
|
+
data.tar.gz: f90eaf365ebfb191b45ab12b4693c7253d829ffcf0c57192e8bb4c1fa3e51986
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 41d60c3a188d66c0f7f3e9982c72de16f323d458529e2b90a16e8c023733acaedcf94a09ae30e94e54a66f4303f0b904a06d54c33b6db2af3acf49669c8b8b0f
|
7
|
+
data.tar.gz: b0ec169bf37128b471263c189b839416d8ae572a69732d3a9d4dcb64d4e2cdf2aef53d423cb2a7eef1c5f50f0756d332066dc9a244788f2b8c05643e1ca73b0f
|
@@ -3,6 +3,8 @@ module Actions
|
|
3
3
|
include Helpers::ArgsSerialization
|
4
4
|
include Helpers::Lock
|
5
5
|
|
6
|
+
execution_plan_hooks.use :drop_all_locks!, :on => :stopped
|
7
|
+
|
6
8
|
# what locks to use on the resource? All by default, can be overriden.
|
7
9
|
# It might one or more locks available for the resource. This following
|
8
10
|
# special values are supported as well:
|
@@ -34,12 +36,10 @@ module Actions
|
|
34
36
|
input.update serialize_args(resource, *resource.all_related_resources, *additional_args)
|
35
37
|
|
36
38
|
if resource.is_a? ActiveRecord::Base
|
37
|
-
if resource_locks == :
|
38
|
-
exclusive_lock!(resource)
|
39
|
-
elsif resource_locks == :link
|
39
|
+
if resource_locks == :link
|
40
40
|
link!(resource)
|
41
41
|
else
|
42
|
-
|
42
|
+
exclusive_lock!(resource)
|
43
43
|
end
|
44
44
|
end
|
45
45
|
end
|
@@ -63,5 +63,9 @@ module Actions
|
|
63
63
|
def self.serializer_class
|
64
64
|
Serializers::ActiveRecordSerializer
|
65
65
|
end
|
66
|
+
|
67
|
+
def drop_all_locks!(_execution_plan)
|
68
|
+
ForemanTasks::Lock.where(:task_id => task.id).destroy_all
|
69
|
+
end
|
66
70
|
end
|
67
71
|
end
|
@@ -4,19 +4,25 @@ module Actions
|
|
4
4
|
# @see Lock.exclusive!
|
5
5
|
def exclusive_lock!(resource)
|
6
6
|
phase! Dynflow::Action::Plan
|
7
|
-
::ForemanTasks::Lock.
|
7
|
+
parent_lock = ::ForemanTasks::Lock.for_resource(resource).where(:task_id => task.self_and_parents.map(&:id)).first
|
8
|
+
if parent_lock
|
9
|
+
ForemanTasks::Link.link_resource_and_related!(resource, task)
|
10
|
+
parent_lock
|
11
|
+
else
|
12
|
+
::ForemanTasks::Lock.exclusive!(resource, task)
|
13
|
+
end
|
8
14
|
end
|
9
15
|
|
10
16
|
# @see Lock.lock!
|
11
|
-
def lock!(resource, *
|
12
|
-
|
13
|
-
|
17
|
+
def lock!(resource, *_lock_names)
|
18
|
+
Foreman::Deprecation.deprecation_warning('2.4', 'locking in foreman-tasks was reworked, please use a combination of exclusive_lock! and link! instead.')
|
19
|
+
exclusive_lock!(resource)
|
14
20
|
end
|
15
21
|
|
16
22
|
# @see Lock.link!
|
17
23
|
def link!(resource)
|
18
24
|
phase! Dynflow::Action::Plan
|
19
|
-
::ForemanTasks::
|
25
|
+
::ForemanTasks::Link.link!(resource, task)
|
20
26
|
end
|
21
27
|
end
|
22
28
|
end
|
@@ -21,7 +21,10 @@ module Actions
|
|
21
21
|
|
22
22
|
# Run all execution plan lifecycle hooks as the original request_id
|
23
23
|
def hook(*args)
|
24
|
-
|
24
|
+
store_current_request_id if !action.input.key?(:current_request_id) && ::Logging.mdc['request']
|
25
|
+
restore_current_request_id do
|
26
|
+
pass(*args)
|
27
|
+
end
|
25
28
|
end
|
26
29
|
|
27
30
|
private
|
@@ -16,9 +16,19 @@ module Actions
|
|
16
16
|
end
|
17
17
|
|
18
18
|
def finalize
|
19
|
+
current_id = User.current.try(:id)
|
20
|
+
saved_id = action.input[:current_user_id]
|
21
|
+
if User.current && saved_id && current_id != saved_id
|
22
|
+
Foreman::Deprecation.deprecation_warning('2.5', 'relying on per-step setting of current user in finalize phase')
|
23
|
+
end
|
24
|
+
|
19
25
|
restore_curent_user { pass }
|
20
26
|
end
|
21
27
|
|
28
|
+
def finalize_phase(execution_plan, *args)
|
29
|
+
restore_curent_user(execution_plan.entry_action) { pass(execution_plan, *args) }
|
30
|
+
end
|
31
|
+
|
22
32
|
# Run all execution plan lifecycle hooks as the original user
|
23
33
|
def hook(*args)
|
24
34
|
restore_curent_user { pass(*args) }
|
@@ -38,7 +48,7 @@ module Actions
|
|
38
48
|
action.input[:current_user_id] = User.current.try(:id)
|
39
49
|
end
|
40
50
|
|
41
|
-
def restore_curent_user
|
51
|
+
def restore_curent_user(action = self.action)
|
42
52
|
old_user = User.current
|
43
53
|
User.current = User.unscoped.find(action.input[:current_user_id]) if action.input[:current_user_id].present?
|
44
54
|
yield
|
@@ -0,0 +1,80 @@
|
|
1
|
+
module Actions
|
2
|
+
# Examples:
|
3
|
+
|
4
|
+
# # Action A which emits an event when it successfully finishes.
|
5
|
+
# class A
|
6
|
+
# include ::Actions::ObservableAction
|
7
|
+
# # ... rest ...
|
8
|
+
# end
|
9
|
+
|
10
|
+
# # Action B which emits an event when it successfully finishes or fails.
|
11
|
+
# class B
|
12
|
+
# include ::Actions::ObservableAction
|
13
|
+
#
|
14
|
+
# execution_plan_hooks.use :emit_event_failure, :on => [:failure]
|
15
|
+
#
|
16
|
+
# def self.event_names
|
17
|
+
# super + [event_name_base + '_' + event_name_suffix(:failure)]
|
18
|
+
# end
|
19
|
+
#
|
20
|
+
# def emit_event_failure(plan)
|
21
|
+
# emit_event(plan, :failure)
|
22
|
+
# end
|
23
|
+
# # ... rest ...
|
24
|
+
# end
|
25
|
+
module ObservableAction
|
26
|
+
module ClassMethods
|
27
|
+
def event_name_suffix(hook)
|
28
|
+
case hook
|
29
|
+
when :success
|
30
|
+
'succeeded'
|
31
|
+
when :failure
|
32
|
+
'failed'
|
33
|
+
else
|
34
|
+
hook
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
def event_names
|
39
|
+
[event_name_base + '_' + event_name_suffix(:success)]
|
40
|
+
end
|
41
|
+
|
42
|
+
def namespaced_event_names
|
43
|
+
event_names.map { |e| ::Foreman::Observable.event_name_for(e) }
|
44
|
+
end
|
45
|
+
|
46
|
+
def event_name_base
|
47
|
+
to_s.underscore.tr('/', '.')
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
def self.included(base)
|
52
|
+
base.extend ClassMethods
|
53
|
+
base.include ::Foreman::Observable
|
54
|
+
base.execution_plan_hooks.use :emit_event, :on => :success
|
55
|
+
end
|
56
|
+
|
57
|
+
def emit_event(execution_plan, hook = :success)
|
58
|
+
return unless root_action?
|
59
|
+
|
60
|
+
trigger_hook "#{self.class.event_name_base}_#{self.class.event_name_suffix(hook)}",
|
61
|
+
payload: event_payload(execution_plan)
|
62
|
+
end
|
63
|
+
|
64
|
+
def event_payload(_execution_plan)
|
65
|
+
{ object: self }
|
66
|
+
end
|
67
|
+
|
68
|
+
extend ApipieDSL::Module
|
69
|
+
|
70
|
+
apipie :class, "An common ancestor action for observable actions" do
|
71
|
+
name 'Actions::ObservableAction'
|
72
|
+
refs 'Actions::ObservableAction'
|
73
|
+
sections only: %w[webhooks]
|
74
|
+
property :task, object_of: 'Task', desc: 'Returns the task to which this action belongs'
|
75
|
+
end
|
76
|
+
class Jail < Safemode::Jail
|
77
|
+
allow :task
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
@@ -0,0 +1,60 @@
|
|
1
|
+
module ForemanTasks
|
2
|
+
class Link < ApplicationRecord
|
3
|
+
belongs_to :task
|
4
|
+
|
5
|
+
belongs_to :resource, polymorphic: true
|
6
|
+
|
7
|
+
scope :for_resource, ->(resource) { where(:resource => resource) }
|
8
|
+
|
9
|
+
validates :task_id, :resource_id, :resource_type, presence: true
|
10
|
+
|
11
|
+
class << self
|
12
|
+
# Assigns the resource to the task to easily track the task in context of
|
13
|
+
# the resource. This doesn't prevent other actions to lock the resource
|
14
|
+
# and should be used only for actions that tolerate other actions to be
|
15
|
+
# performed on the resource. Usually, this shouldn't needed to be done
|
16
|
+
# through the action directly, because the lock should assign it's parent
|
17
|
+
# objects to the action recursively (using +related_resources+ method in model
|
18
|
+
# objects)
|
19
|
+
def link!(resource, task)
|
20
|
+
link = build(resource, task)
|
21
|
+
link.save!
|
22
|
+
link
|
23
|
+
end
|
24
|
+
|
25
|
+
# Records the information about the user that triggered the task
|
26
|
+
def owner!(user, task)
|
27
|
+
link!(user, task)
|
28
|
+
end
|
29
|
+
|
30
|
+
def link_resource_and_related!(resource, task)
|
31
|
+
link!(resource, task)
|
32
|
+
build_related_links(resource, task).each(&:save!)
|
33
|
+
end
|
34
|
+
|
35
|
+
def build_related_links(resource, task)
|
36
|
+
related_resources(resource).map do |related_resource|
|
37
|
+
build(related_resource, task)
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
private
|
42
|
+
|
43
|
+
def build(resource, task)
|
44
|
+
find_or_initialize_by(task_id: task.id,
|
45
|
+
resource_type: resource.class.name,
|
46
|
+
resource_id: resource.id)
|
47
|
+
end
|
48
|
+
|
49
|
+
# recursively search for related resources of the resource (using
|
50
|
+
# the +related_resources+ method, avoiding the cycles
|
51
|
+
def related_resources(resource)
|
52
|
+
if resource.respond_to?(:all_related_resources)
|
53
|
+
resource.all_related_resources
|
54
|
+
else
|
55
|
+
[]
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
@@ -1,14 +1,5 @@
|
|
1
1
|
module ForemanTasks
|
2
2
|
class Lock < ApplicationRecord
|
3
|
-
LINK_LOCK_NAME = :link_resource
|
4
|
-
OWNER_LOCK_NAME = :task_owner
|
5
|
-
|
6
|
-
# not really intended to be created in database, but it's used for
|
7
|
-
# explicitly stating that the all the locks for resource should be used
|
8
|
-
ALL_LOCK_NAME = :all
|
9
|
-
|
10
|
-
RESERVED_LOCK_NAMES = [LINK_LOCK_NAME, OWNER_LOCK_NAME, ALL_LOCK_NAME].freeze
|
11
|
-
|
12
3
|
class LockConflict < StandardError
|
13
4
|
attr_reader :required_lock, :conflicting_locks
|
14
5
|
def initialize(required_lock, conflicting_locks)
|
@@ -16,7 +7,6 @@ module ForemanTasks
|
|
16
7
|
| #{_('Required lock is already taken by other running tasks.')}
|
17
8
|
| #{_('Please inspect their state, fix their errors and resume them.')}
|
18
9
|
|
|
19
|
-
| #{_('Required lock: %s') % required_lock.name}
|
20
10
|
| #{_('Conflicts with tasks:')}
|
21
11
|
HEADER
|
22
12
|
url_helpers = Rails.application.routes.url_helpers
|
@@ -36,30 +26,28 @@ module ForemanTasks
|
|
36
26
|
|
37
27
|
belongs_to :resource, polymorphic: true
|
38
28
|
|
39
|
-
|
40
|
-
|
41
|
-
validates :task_id, :name, :resource_id, :resource_type, presence: true
|
29
|
+
validates :task_id, :resource_id, :resource_type, presence: true
|
42
30
|
|
43
31
|
validate do
|
44
|
-
|
32
|
+
if (locks = colliding_locks).any?
|
33
|
+
raise LockConflict.new(self, locks)
|
34
|
+
end
|
45
35
|
end
|
46
36
|
|
47
|
-
|
48
|
-
def available?
|
49
|
-
!colliding_locks.exists?
|
50
|
-
end
|
37
|
+
scope :for_resource, ->(resource) { where(:resource => resource) }
|
51
38
|
|
52
39
|
# returns a scope of the locks colliding with this one
|
53
40
|
def colliding_locks
|
54
41
|
task_ids = task.self_and_parents.map(&:id)
|
55
|
-
colliding_locks_scope = Lock.
|
56
|
-
colliding_locks_scope
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
42
|
+
colliding_locks_scope = Lock.where(Lock.arel_table[:task_id].not_in(task_ids))
|
43
|
+
colliding_locks_scope.where(resource_id: resource_id,
|
44
|
+
resource_type: resource_type)
|
45
|
+
end
|
46
|
+
|
47
|
+
def save!
|
48
|
+
super
|
49
|
+
rescue ActiveRecord::RecordNotUnique
|
50
|
+
raise LockConflict.new(self, colliding_locks)
|
63
51
|
end
|
64
52
|
|
65
53
|
class << self
|
@@ -67,116 +55,30 @@ module ForemanTasks
|
|
67
55
|
# No other task related to the resource is not allowed (even not-locking ones)
|
68
56
|
# A typical usecase is resource deletion, where it's good idea to make sure
|
69
57
|
# nothing else related to the resource is really running.
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
# Locks the resource so that no other task can lock it while running.
|
79
|
-
# Other not-locking tasks are tolerated.
|
80
|
-
#
|
81
|
-
# The lock names allow to specify what locks should be activated. It has to
|
82
|
-
# be a subset of names defined in model's class available_locks method
|
83
|
-
#
|
84
|
-
# When no lock name is specified, the resource is locked against all the available
|
85
|
-
# locks.
|
86
|
-
#
|
87
|
-
# It also looks at +related_resources+ method of the resource to calcuate all
|
88
|
-
# the related resources (recursively) and links the task to them as well.
|
89
|
-
def lock!(resource, uuid, *lock_names)
|
90
|
-
build_locks(resource, lock_names, uuid).each(&:save!)
|
91
|
-
end
|
92
|
-
|
93
|
-
def lockable?(resource, uuid, *lock_names)
|
94
|
-
build_locks(resource, lock_names, uuid).all?(&:available?)
|
58
|
+
# It also creates a Link between the task and the resource and between the task
|
59
|
+
# and all related resources.
|
60
|
+
def exclusive!(resource, task)
|
61
|
+
lock = build(resource, task)
|
62
|
+
lock.save!
|
63
|
+
ForemanTasks::Link.link_resource_and_related!(resource, task)
|
64
|
+
lock
|
95
65
|
end
|
96
66
|
|
97
|
-
|
98
|
-
|
67
|
+
# See #exclusive!
|
68
|
+
def lock!(resource, task, *_lock_names)
|
69
|
+
exclusive!(resource, task)
|
99
70
|
end
|
100
71
|
|
101
|
-
def colliding_locks(resource,
|
102
|
-
|
103
|
-
.inject([]) { |collisions, lock| collisions.concat lock.colliding_locks.to_a }
|
104
|
-
end
|
105
|
-
|
106
|
-
# Assigns the resource to the task to easily track the task in context of
|
107
|
-
# the resource. This doesn't prevent other actions to lock the resource
|
108
|
-
# and should be used only for actions that tolerate other actions to be
|
109
|
-
# performed on the resource. Usually, this shouldn't needed to be done
|
110
|
-
# through the action directly, because the lock should assign it's parrent
|
111
|
-
# objects to the action srecursively (using +related_resources+ method in model
|
112
|
-
# objects)
|
113
|
-
def link!(resource, uuid)
|
114
|
-
build_link(resource, uuid).save!
|
115
|
-
end
|
116
|
-
|
117
|
-
def link?(resource, uuid)
|
118
|
-
build_link(resource, uuid).available?
|
72
|
+
def colliding_locks(resource, task, *_lock_names)
|
73
|
+
build(resource, task).colliding_locks.to_a
|
119
74
|
end
|
120
75
|
|
121
76
|
private
|
122
77
|
|
123
|
-
def
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
lock_names.concat(resource.class.available_locks)
|
128
|
-
else
|
129
|
-
raise "The resource #{resource.class.name} doesn't define any available lock"
|
130
|
-
end
|
131
|
-
if lock_names.any? { |lock_name| RESERVED_LOCK_NAMES.include?(lock_name) }
|
132
|
-
raise "Lock name #{lock_name} is reserved"
|
133
|
-
end
|
134
|
-
lock_names.concat([LINK_LOCK_NAME, OWNER_LOCK_NAME]) if include_links
|
135
|
-
lock_names
|
136
|
-
end
|
137
|
-
|
138
|
-
def build_exclusive_locks(resource, uuid = nil)
|
139
|
-
build_locks(resource, all_lock_names(resource, true), uuid)
|
140
|
-
end
|
141
|
-
|
142
|
-
def build_locks(resource, lock_names, uuid = nil)
|
143
|
-
locks = []
|
144
|
-
if lock_names.empty? || lock_names == [:all]
|
145
|
-
lock_names = all_lock_names(resource)
|
146
|
-
end
|
147
|
-
lock_names.map do |lock_name|
|
148
|
-
locks << build(uuid, resource, lock_name, true)
|
149
|
-
end
|
150
|
-
locks.concat(build_links(resource, uuid))
|
151
|
-
locks
|
152
|
-
end
|
153
|
-
|
154
|
-
def build_links(resource, uuid = nil)
|
155
|
-
related_resources(resource).map do |related_resource|
|
156
|
-
build_link(related_resource, uuid)
|
157
|
-
end
|
158
|
-
end
|
159
|
-
|
160
|
-
def build_link(resource, uuid = nil)
|
161
|
-
build(uuid, resource, LINK_LOCK_NAME, false)
|
162
|
-
end
|
163
|
-
|
164
|
-
def build(uuid, resource, lock_name, exclusive)
|
165
|
-
new(task_id: uuid,
|
166
|
-
name: lock_name,
|
167
|
-
resource_type: resource.class.name,
|
168
|
-
resource_id: resource.id,
|
169
|
-
exclusive: !!exclusive)
|
170
|
-
end
|
171
|
-
|
172
|
-
# recursively search for related resources of the resource (using
|
173
|
-
# the +related_resources+ method, avoiding the cycles
|
174
|
-
def related_resources(resource)
|
175
|
-
if resource.respond_to?(:all_related_resources)
|
176
|
-
resource.all_related_resources
|
177
|
-
else
|
178
|
-
[]
|
179
|
-
end
|
78
|
+
def build(resource, task)
|
79
|
+
find_or_initialize_by(task_id: task.id,
|
80
|
+
resource_type: resource.class.name,
|
81
|
+
resource_id: resource.id)
|
180
82
|
end
|
181
83
|
end
|
182
84
|
end
|