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.
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