foreman-tasks 3.0.6 → 4.0.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.
- 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
|