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.
Files changed (42) hide show
  1. checksums.yaml +4 -4
  2. data/app/lib/actions/entry_action.rb +8 -4
  3. data/app/lib/actions/helpers/lock.rb +11 -5
  4. data/app/lib/actions/middleware/keep_current_request_id.rb +4 -1
  5. data/app/lib/actions/middleware/keep_current_user.rb +11 -1
  6. data/app/lib/actions/observable_action.rb +80 -0
  7. data/app/models/foreman_tasks/concerns/action_subject.rb +0 -6
  8. data/app/models/foreman_tasks/link.rb +60 -0
  9. data/app/models/foreman_tasks/lock.rb +30 -128
  10. data/app/models/foreman_tasks/task.rb +20 -7
  11. data/app/models/foreman_tasks/task/dynflow_task.rb +3 -8
  12. data/app/models/foreman_tasks/task/search.rb +6 -6
  13. data/app/views/foreman_tasks/api/locks/show.json.rabl +4 -0
  14. data/app/views/foreman_tasks/api/tasks/details.json.rabl +5 -3
  15. data/app/views/foreman_tasks/tasks/_lock_card.html.erb +10 -0
  16. data/db/migrate/20181206123910_create_foreman_tasks_links.foreman_tasks.rb +26 -0
  17. data/db/migrate/20181206124952_migrate_non_exclusive_locks_to_links.foreman_tasks.rb +14 -0
  18. data/db/migrate/20181206131436_drop_old_locks.foreman_tasks.rb +20 -0
  19. data/db/migrate/20181206131627_make_locks_exclusive.foreman_tasks.rb +25 -0
  20. data/lib/foreman_tasks/cleaner.rb +10 -0
  21. data/lib/foreman_tasks/engine.rb +5 -2
  22. data/lib/foreman_tasks/tasks/export_tasks.rake +2 -2
  23. data/lib/foreman_tasks/version.rb +1 -1
  24. data/package.json +6 -6
  25. data/test/controllers/tasks_controller_test.rb +1 -1
  26. data/test/unit/actions/action_with_sub_plans_test.rb +5 -2
  27. data/test/unit/cleaner_test.rb +4 -4
  28. data/test/unit/locking_test.rb +85 -0
  29. data/webpack/ForemanTasks/Components/TaskDetails/Components/Locks.js +2 -2
  30. data/webpack/ForemanTasks/Components/TaskDetails/Components/TaskInfo.js +3 -2
  31. data/webpack/ForemanTasks/Components/TaskDetails/Components/__tests__/__snapshots__/Locks.test.js.snap +4 -4
  32. data/webpack/ForemanTasks/Components/TaskDetails/Components/__tests__/__snapshots__/TaskInfo.test.js.snap +2 -0
  33. data/webpack/ForemanTasks/Components/TaskDetails/TaskDetails.js +4 -1
  34. data/webpack/ForemanTasks/Components/TaskDetails/TaskDetails.scss +5 -1
  35. data/webpack/ForemanTasks/Components/TaskDetails/TaskDetailsSelectors.js +3 -0
  36. data/webpack/ForemanTasks/Components/TaskDetails/index.js +2 -0
  37. data/webpack/ForemanTasks/Components/TasksTable/TasksTablePage.scss +4 -3
  38. data/webpack/ForemanTasks/Components/TasksTable/__tests__/__snapshots__/TasksTablePage.test.js.snap +2 -2
  39. data/webpack/ForemanTasks/Components/TasksTable/formatters/__test__/__snapshots__/actionNameCellFormatter.test.js.snap +2 -3
  40. data/webpack/ForemanTasks/Components/TasksTable/formatters/actionNameCellFormatter.js +2 -3
  41. metadata +12 -4
  42. data/test/unit/lock_test.rb +0 -22
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 29e160b9773db8ef5b9f8cb9e26011b957578caccf2d56bc6a57852c5553df9d
4
- data.tar.gz: 863c9436e788ede17b6e2a80aae49b259f96a1372c80a004ed66e70409957435
3
+ metadata.gz: bded348bac199b96bf0c1bf458c0785cdc233892acc1a5d06a940a8c5b28309b
4
+ data.tar.gz: f90eaf365ebfb191b45ab12b4693c7253d829ffcf0c57192e8bb4c1fa3e51986
5
5
  SHA512:
6
- metadata.gz: 4140dea3980dcbbc2d140b3e83dc23458a53ff6be35f5012feb8d3bd87c24ed45e98eb98a19aaf7228c1ca620f4e91e57236cc4c17e36754bd1cbe65dd1b3249
7
- data.tar.gz: e2836f3e5061587913504a69509e21fab8e640f002cf026641d8948e64f830c8abf1aa4b7d72e9a9c64f49c47116ac94be299367b6fba9d44689d410f1805cb3
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 == :exclusive
38
- exclusive_lock!(resource)
39
- elsif resource_locks == :link
39
+ if resource_locks == :link
40
40
  link!(resource)
41
41
  else
42
- lock!(resource, resource_locks)
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.exclusive!(resource, task.id)
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, *lock_names)
12
- phase! Dynflow::Action::Plan
13
- ::ForemanTasks::Lock.lock!(resource, task.id, *lock_names.flatten)
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::Lock.link!(resource, task.id)
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
- restore_current_request_id { pass(*args) }
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
@@ -3,12 +3,6 @@ module ForemanTasks
3
3
  module ActionSubject
4
4
  extend ActiveSupport::Concern
5
5
 
6
- module ClassMethods
7
- def available_locks
8
- [:read, :write]
9
- end
10
- end
11
-
12
6
  def action_input_key
13
7
  self.class.name.demodulize.underscore
14
8
  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
- scope :active, -> { joins(:task).where('foreman_tasks_tasks.state != ?', :stopped) }
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
- raise LockConflict.new(self, colliding_locks) unless available?
32
+ if (locks = colliding_locks).any?
33
+ raise LockConflict.new(self, locks)
34
+ end
45
35
  end
46
36
 
47
- # returns true if it's possible to aquire this kind of lock
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.active.where(Lock.arel_table[:task_id].not_in(task_ids))
56
- colliding_locks_scope = colliding_locks_scope.where(name: name,
57
- resource_id: resource_id,
58
- resource_type: resource_type)
59
- unless exclusive?
60
- colliding_locks_scope = colliding_locks_scope.where(:exclusive => true)
61
- end
62
- colliding_locks_scope
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
- def exclusive!(resource, uuid)
71
- build_exclusive_locks(resource, uuid).each(&:save!)
72
- end
73
-
74
- def exclusive?(resource)
75
- build_exclusive_locks(resource).all?(&:available?)
76
- end
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
- def locked?(resource, uuid, *lock_names)
98
- !lockable?(resource, uuid, *lock_names)
67
+ # See #exclusive!
68
+ def lock!(resource, task, *_lock_names)
69
+ exclusive!(resource, task)
99
70
  end
100
71
 
101
- def colliding_locks(resource, uuid, *lock_names)
102
- build_locks(resource, lock_names, uuid)
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 all_lock_names(resource, include_links = false)
124
- lock_names = []
125
- if resource.class.respond_to?(:available_locks) &&
126
- resource.class.available_locks.any?
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