foreman_openbolt 1.2.0 → 1.3.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 (43) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +1 -0
  3. data/Rakefile +32 -0
  4. data/app/controllers/api/v2/openbolt_jobs_controller.rb +77 -0
  5. data/app/controllers/api/v2/openbolt_tasks_controller.rb +83 -0
  6. data/app/controllers/concerns/foreman_openbolt/common.rb +121 -0
  7. data/app/controllers/concerns/foreman_openbolt/jobs.rb +84 -0
  8. data/app/controllers/concerns/foreman_openbolt/tasks.rb +140 -0
  9. data/app/controllers/foreman_openbolt/task_controller.rb +12 -291
  10. data/app/lib/actions/foreman_openbolt/cleanup_proxy_artifacts.rb +4 -2
  11. data/app/lib/actions/foreman_openbolt/poll_task_status.rb +48 -29
  12. data/app/models/foreman_openbolt/task_job.rb +9 -2
  13. data/config/routes.rb +39 -4
  14. data/db/migrate/20260824000000_remove_view_smart_proxies_openbolt_permission.rb +18 -0
  15. data/lib/foreman_openbolt/engine.rb +15 -7
  16. data/lib/foreman_openbolt/version.rb +1 -1
  17. data/lib/proxy_api/openbolt.rb +64 -13
  18. data/package.json +1 -1
  19. data/test/acceptance/acceptance_helper.rb +13 -0
  20. data/test/acceptance/api_acceptance_helper.rb +109 -0
  21. data/test/acceptance/tests/api/authn_authz_test.rb +82 -0
  22. data/test/acceptance/tests/api/jobs_test.rb +77 -0
  23. data/test/acceptance/tests/api/launch_test.rb +79 -0
  24. data/test/acceptance/tests/api/proxy_tasks_test.rb +51 -0
  25. data/test/unit/controllers/api/v2/openbolt_jobs_controller_test.rb +251 -0
  26. data/test/unit/controllers/api/v2/openbolt_tasks_controller_test.rb +438 -0
  27. data/test/unit/controllers/task_controller_test.rb +197 -39
  28. data/test/unit/docker/Dockerfile +1 -0
  29. data/test/unit/lib/actions/poll_task_status_test.rb +112 -34
  30. data/test/unit/lib/proxy_api/openbolt_test.rb +121 -6
  31. data/test/unit/models/task_job_test.rb +11 -0
  32. data/webpack/src/Components/LaunchTask/__tests__/LaunchTask.test.js +44 -1
  33. data/webpack/src/Components/LaunchTask/hooks/__tests__/useOpenBoltOptions.test.js +3 -0
  34. data/webpack/src/Components/LaunchTask/hooks/__tests__/useTasksData.test.js +5 -4
  35. data/webpack/src/Components/LaunchTask/hooks/useOpenBoltOptions.js +1 -1
  36. data/webpack/src/Components/LaunchTask/hooks/useTasksData.js +4 -1
  37. data/webpack/src/Components/LaunchTask/index.js +21 -6
  38. data/webpack/src/Components/TaskExecution/__tests__/TaskExecution.test.js +1 -1
  39. data/webpack/src/Components/TaskExecution/hooks/__tests__/useJobPolling.test.js +8 -8
  40. data/webpack/src/Components/TaskExecution/hooks/useJobPolling.js +3 -3
  41. data/webpack/src/Components/TaskHistory/__tests__/TaskHistory.test.js +6 -6
  42. data/webpack/src/Components/TaskHistory/index.js +3 -3
  43. metadata +17 -4
@@ -1,24 +1,22 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'foreman/logging'
3
4
  require 'foreman_openbolt/engine'
4
5
  require 'proxy_api/openbolt'
5
6
 
6
7
  module ForemanOpenbolt
7
- class TaskController < ::ApplicationController
8
- include ::Foreman::Controller::AutoCompleteSearch
9
-
10
- # For passing to/from the UI
11
- ENCRYPTED_PLACEHOLDER = '[Use saved encrypted default]'
12
- # For saving to the database
13
- REDACTED_PLACEHOLDER = '*****'
8
+ class TaskController < ApplicationController
9
+ # Rails checks rescue_from handlers in reverse registration order (last
10
+ # registered is checked first). The StandardError catch-all must be
11
+ # registered BEFORE the includes so that the specific handlers registered
12
+ # by the other concerns' includes are checked first.
13
+ rescue_from StandardError do |error|
14
+ Foreman::Logging.exception('OpenBolt UI unexpected error', error)
15
+ render_json_error('Internal server error', :internal_server_error)
16
+ end
14
17
 
15
- before_action :load_smart_proxy, only: [
16
- :fetch_tasks, :reload_tasks, :fetch_openbolt_options, :launch_task
17
- ]
18
- before_action :load_openbolt_api, only: [
19
- :fetch_tasks, :reload_tasks, :fetch_openbolt_options, :launch_task
20
- ]
21
- before_action :load_task_job, only: [:job_status, :job_result]
18
+ include ForemanOpenbolt::Jobs
19
+ include ForemanOpenbolt::Tasks
22
20
 
23
21
  # React-rendered pages
24
22
  def page_launch_task
@@ -32,282 +30,5 @@ module ForemanOpenbolt
32
30
  def page_task_history
33
31
  render 'foreman_openbolt/react_page'
34
32
  end
35
-
36
- def fetch_tasks
37
- render_openbolt_api_call(:tasks)
38
- end
39
-
40
- def reload_tasks
41
- render_openbolt_api_call(:reload_tasks)
42
- end
43
-
44
- def fetch_openbolt_options
45
- options = @openbolt_api.openbolt_options
46
-
47
- # Get defaults from Foreman settings.
48
- # For encrypted settings, show the placeholder only if a non-empty value
49
- # has been saved, so the UI shows an empty field for unconfigured passwords
50
- # instead of a misleading placeholder.
51
- defaults = {}
52
- openbolt_settings.each do |setting|
53
- key = setting.name.sub(/^openbolt_/, '')
54
- if setting.encrypted?
55
- defaults[key] = ENCRYPTED_PLACEHOLDER unless setting.value.to_s.empty?
56
- elsif !setting.value.to_s.empty?
57
- defaults[key] = setting.value
58
- end
59
- end
60
-
61
- # Merge the defaults into the options metadata
62
- result = {}
63
- options.each do |name, meta|
64
- result[name] = meta.dup
65
- result[name]['default'] = defaults[name] if defaults.key?(name)
66
- end
67
-
68
- render json: result
69
- rescue ProxyAPI::ProxyException => e
70
- log_exception('fetch_openbolt_options', e)
71
- render_error("Smart Proxy error: #{e.message}", :bad_gateway)
72
- rescue StandardError => e
73
- log_exception('fetch_openbolt_options', e)
74
- render_error("Internal server error: #{e.message}", :internal_server_error)
75
- end
76
-
77
- def launch_task
78
- required_args = [:task_name, :targets]
79
- missing_args = required_args.select { |arg| params[arg].blank? }
80
-
81
- if missing_args.any?
82
- return render_error("Missing required arguments to the launch_task function: #{missing_args.join(', ')}",
83
- :bad_request)
84
- end
85
-
86
- begin
87
- task_name = params[:task_name].to_s.strip
88
- targets = params[:targets].to_s.strip
89
- task_params = params[:params] || {}
90
- options = params[:options] || {}
91
- options = merge_encrypted_defaults(options)
92
-
93
- return render_error('Task name and targets cannot be empty', :bad_request) if task_name.empty? || targets.empty?
94
-
95
- logger.info("Launching OpenBolt task '#{task_name}' on targets '#{targets}' via proxy #{@smart_proxy.name}")
96
-
97
- response = @openbolt_api.launch_task(
98
- name: task_name,
99
- targets: targets,
100
- parameters: task_params,
101
- options: options
102
- )
103
-
104
- logger.debug("Task execution response: #{response.inspect}")
105
-
106
- if response['error']
107
- error_detail = response['error'].is_a?(Hash) ? response['error']['message'] : response['error']
108
- return render_error("Task execution failed: #{error_detail}", :bad_request)
109
- end
110
- return render_error('Task execution failed: No job ID returned', :bad_request) unless response['id']
111
-
112
- metadata = @openbolt_api.tasks[task_name] || {}
113
- TaskJob.create_from_execution!(
114
- proxy: @smart_proxy,
115
- task_name: task_name,
116
- task_description: metadata['description'] || '',
117
- targets: targets.split(',').map(&:strip),
118
- parameters: task_params,
119
- options: scrub_options_for_storage(options),
120
- job_id: response['id']
121
- )
122
-
123
- # Start background polling to update status
124
- ForemanTasks.async_task(Actions::ForemanOpenbolt::PollTaskStatus,
125
- response['id'],
126
- @smart_proxy.id)
127
-
128
- render json: {
129
- job_id: response['id'],
130
- }
131
- rescue ArgumentError => e
132
- # From merge_encrypted_defaults when a user submits the encrypted
133
- # placeholder for an option that has no saved Foreman setting.
134
- log_exception('launch_task', e)
135
- render_error(e.message, :bad_request)
136
- rescue ActiveRecord::RecordInvalid => e
137
- log_exception('launch_task', e)
138
- render_error("Database error: #{e.message}", :internal_server_error)
139
- rescue ProxyAPI::ProxyException => e
140
- log_exception('launch_task', e)
141
- render_error("Smart Proxy error: #{e.message}", :bad_gateway)
142
- rescue StandardError => e
143
- log_exception('launch_task', e)
144
- render_error("Error launching task: #{e.message}", :internal_server_error)
145
- end
146
- end
147
-
148
- def job_status
149
- return render_error('Task job not found', :not_found) unless @task_job
150
-
151
- render json: {
152
- status: @task_job.status,
153
- submitted_at: @task_job.submitted_at,
154
- completed_at: @task_job.completed_at,
155
- duration: @task_job.duration,
156
- task_name: @task_job.task_name,
157
- task_description: @task_job.task_description,
158
- task_parameters: @task_job.task_parameters,
159
- targets: @task_job.targets,
160
- smart_proxy: {
161
- id: @task_job.smart_proxy_id,
162
- name: @task_job.smart_proxy&.name || '(unknown)',
163
- },
164
- }
165
- end
166
-
167
- def job_result
168
- return render_error('Task job not found', :not_found) unless @task_job
169
-
170
- render json: {
171
- status: @task_job.status,
172
- command: @task_job.command,
173
- value: @task_job.result,
174
- log: @task_job.log,
175
- }
176
- end
177
-
178
- # List of all task history
179
- def fetch_task_history
180
- per_page = [(params[:per_page] || 20).to_i, 100].min
181
- @task_history = TaskJob.includes(:smart_proxy)
182
- .recent
183
- .paginate(page: params[:page], per_page: per_page)
184
-
185
- render json: {
186
- results: @task_history.map { |job| serialize_task_job(job) },
187
- total: @task_history.total_entries,
188
- page: @task_history.current_page,
189
- per_page: @task_history.per_page,
190
- }
191
- rescue StandardError => e
192
- log_exception('fetch_task_history', e)
193
- render_error("Error loading task history: #{e.message}", :internal_server_error)
194
- end
195
-
196
- def load_smart_proxy
197
- proxy_id = params[:proxy_id]
198
- if proxy_id.blank?
199
- render_error('Smart Proxy ID is required', :bad_request)
200
- return false
201
- end
202
-
203
- return true if @smart_proxy && @smart_proxy.id.to_s == proxy_id.to_s
204
-
205
- @smart_proxy = SmartProxy.authorized(:view_smart_proxies).find_by(id: proxy_id)
206
-
207
- unless @smart_proxy
208
- render_error("Smart Proxy with ID #{proxy_id} not found or not authorized", :not_found)
209
- return false
210
- end
211
-
212
- true
213
- end
214
-
215
- def load_openbolt_api
216
- return false unless @smart_proxy
217
- return true if @openbolt_api && @openbolt_api.url == @smart_proxy.url
218
-
219
- begin
220
- @openbolt_api = ProxyAPI::Openbolt.new(url: @smart_proxy.url)
221
- rescue StandardError => e
222
- log_exception("load_openbolt_api for proxy #{@smart_proxy.name}", e)
223
- render_error("Failed to connect to Smart Proxy", :bad_gateway)
224
- return false
225
- end
226
-
227
- true
228
- end
229
-
230
- def load_task_job
231
- job_id = params[:job_id]
232
- logger.debug("load_task_job - Job ID: #{job_id}")
233
- if job_id.blank?
234
- render_error('Job ID is required', :bad_request)
235
- return false
236
- end
237
-
238
- @task_job = TaskJob.find_by(job_id: job_id)
239
- logger.debug("load_task_job - Task Job: #{@task_job.inspect}")
240
- end
241
-
242
- def openbolt_settings
243
- @openbolt_settings ||= Foreman.settings.select { |s| s.name.start_with?('openbolt_') }
244
- end
245
-
246
- def encrypted_settings
247
- openbolt_settings.select(&:encrypted?)
248
- end
249
-
250
- def merge_encrypted_defaults(options)
251
- merged = options.dup
252
- merged.each do |key, value|
253
- next unless value == ENCRYPTED_PLACEHOLDER
254
-
255
- saved = Setting["openbolt_#{key}"]
256
- if saved.nil? || saved.to_s.empty?
257
- raise ArgumentError,
258
- "No saved value for encrypted option '#{key}'. Configure it in Administer > Settings or provide a value."
259
- end
260
- merged[key] = saved
261
- end
262
- merged
263
- end
264
-
265
- def scrub_options_for_storage(options)
266
- scrubbed = options.dup
267
- encrypted_settings.each do |setting|
268
- option_name = setting.name.sub(/^openbolt_/, '')
269
- scrubbed[option_name] = REDACTED_PLACEHOLDER if scrubbed.key?(option_name)
270
- end
271
- scrubbed
272
- end
273
-
274
- def render_openbolt_api_call(method_name, **args)
275
- result = @openbolt_api.send(method_name, **args)
276
- logger.debug("OpenBolt API call #{method_name} successful for proxy #{@smart_proxy.name}")
277
- render json: result
278
- rescue ProxyAPI::ProxyException => e
279
- log_exception(method_name, e)
280
- render_error("Smart Proxy error: #{e.message}", :bad_gateway)
281
- rescue StandardError => e
282
- log_exception(method_name, e)
283
- render_error("Internal server error: #{e.message}", :internal_server_error)
284
- end
285
-
286
- def log_exception(message, exception)
287
- logger.error("#{message}: #{exception.class}: #{exception.message}")
288
- logger.error(exception.backtrace.join("\n")) if exception.backtrace
289
- end
290
-
291
- def render_error(message, status)
292
- render json: { error: message }, status: status
293
- end
294
-
295
- def serialize_task_job(task_job)
296
- {
297
- job_id: task_job.job_id,
298
- task_name: task_job.task_name,
299
- task_description: task_job.task_description,
300
- task_parameters: task_job.task_parameters,
301
- targets: task_job.targets,
302
- status: task_job.status,
303
- smart_proxy: {
304
- id: task_job.smart_proxy_id,
305
- name: task_job.smart_proxy&.name || '(unknown)',
306
- },
307
- submitted_at: task_job.submitted_at,
308
- completed_at: task_job.completed_at,
309
- duration: task_job.duration,
310
- }
311
- end
312
33
  end
313
34
  end
@@ -1,5 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'foreman/logging'
4
+ require 'proxy_api/openbolt'
5
+
3
6
  module Actions
4
7
  module ForemanOpenbolt
5
8
  class CleanupProxyArtifacts < Actions::EntryAction
@@ -19,8 +22,7 @@ module Actions
19
22
  Rails.logger.debug { "Cleaned up artifacts for job #{input[:job_id]} on proxy #{proxy.name}: #{response}" }
20
23
  rescue StandardError => e
21
24
  # Don't fail the action if cleanup fails - it's not critical
22
- Rails.logger.error("Failed to cleanup artifacts for job #{input[:job_id]}: #{e.class}: #{e.message}")
23
- Rails.logger.error(e.backtrace.join("\n")) if e.backtrace
25
+ ::Foreman::Logging.exception("Failed to cleanup artifacts for job #{input[:job_id]}", e)
24
26
  end
25
27
 
26
28
  def rescue_strategy
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'proxy_api/openbolt'
4
+
3
5
  # This is a Dynflow action that polls the status of an OpenBolt task job
4
6
  # from the Smart Proxy. It periodically checks the job status until it is
5
7
  # completed, then fetches the final results.
@@ -42,11 +44,20 @@ module Actions
42
44
  log("Polling finished for OpenBolt job #{input[:job_id]}")
43
45
  end
44
46
 
45
- def extract_proxy_error(response)
46
- error_value = response&.dig('error')
47
- return nil if error_value.nil? || (error_value.respond_to?(:empty?) && error_value.empty?)
47
+ def mark_exception!(task_job, reason)
48
+ previous_status = task_job.status
49
+ task_job.update!(status: 'exception')
50
+ rescue StandardError => e
51
+ log(
52
+ "Could not mark TaskJob #{task_job.job_id} as exception after " \
53
+ "#{reason}: #{e.class}: #{e.message}. Row remains " \
54
+ "in '#{previous_status}' state and will not be re-polled.", :error
55
+ )
56
+ log(e.backtrace.join("\n"), :error) if e.backtrace
57
+ end
48
58
 
49
- error_value.is_a?(Hash) ? error_value['message'] || error_value.to_s : error_value.to_s
59
+ def completed_status?(status)
60
+ ::ForemanOpenbolt::TaskJob::COMPLETED_STATUSES.include?(status)
50
61
  end
51
62
 
52
63
  def poll_and_reschedule
@@ -69,7 +80,7 @@ module Actions
69
80
  proxy = ::SmartProxy.find_by(id: input[:proxy_id])
70
81
  unless proxy
71
82
  log("Smart Proxy with ID #{input[:proxy_id]} not found for OpenBolt job #{job_id}", :error)
72
- task_job.update!(status: 'exception')
83
+ mark_exception!(task_job, 'proxy not found')
73
84
  finish
74
85
  return
75
86
  end
@@ -77,51 +88,59 @@ module Actions
77
88
  begin
78
89
  api = ::ProxyAPI::Openbolt.new(url: proxy.url)
79
90
 
80
- # Fetch current status
91
+ # Fetch current status. ProxyAPI::Openbolt raises ProxyReportedError
92
+ # for a 200 + {"error": ...} result, which is handled in the rescue. Transport
93
+ # failures raise plain ProxyException and fall through to the retry loop.
81
94
  status_result = api.job_status(job_id: job_id)
82
95
 
83
- proxy_error = extract_proxy_error(status_result)
84
- if proxy_error
85
- log("Proxy returned error for job #{job_id}: #{proxy_error}", :error)
86
- task_job.update!(status: 'exception')
87
- finish
88
- return
89
- end
90
-
91
96
  unless status_result&.dig('status')
92
97
  log("Proxy returned response without status for job #{job_id}: #{status_result.inspect}", :error)
93
- task_job.update!(status: 'exception')
98
+ mark_exception!(task_job, 'missing status in proxy response')
94
99
  finish
95
100
  return
96
101
  end
97
102
 
98
- input[:retry_count] = 0
99
103
  new_status = status_result['status']
100
- if new_status == task_job.status
101
- log("Poll for OpenBolt job #{job_id}: status=#{new_status}")
102
- else
103
- previous_status = task_job.status
104
- task_job.update!(status: new_status)
105
- log("OpenBolt job #{job_id} status changed from '#{previous_status}' to '#{new_status}'", :info)
106
- end
107
104
 
108
- # If completed, fetch full results
109
- if task_job.completed?
105
+ # Fetch the result before updating the completed status. Writing
106
+ # status first would strand the job on a transient result fetch
107
+ # failure, since the next poll would see completed? and finish
108
+ # resultless. retry_count is deliberately not reset here so a
109
+ # persistently failing result endpoint still hits the retry limit.
110
+ if completed_status?(new_status)
110
111
  result = api.job_result(job_id: job_id)
112
+
113
+ task_job.update_from_proxy_result!(result) if result.present?
114
+ # Fallback for a blank or statusless result body, so the row
115
+ # never finishes stuck as running.
116
+ task_job.update!(status: new_status) unless task_job.completed?
117
+
111
118
  if result.present?
112
- task_job.update_from_proxy_result!(result)
113
119
  log("OpenBolt job #{job_id} completed with status '#{task_job.status}'", :info)
114
120
  else
115
- log("No result returned from proxy for completed OpenBolt job #{job_id}", :error)
121
+ log("No result returned from proxy for completed OpenBolt job #{job_id}; recorded status '#{task_job.status}'", :error)
116
122
  end
117
123
  finish
118
124
  return
119
125
  end
120
126
 
121
- # Still running, schedule next poll in 5 seconds
127
+ # Still running. A good status read clears the retry counter.
128
+ input[:retry_count] = 0
129
+ if new_status == task_job.status
130
+ log("Poll for OpenBolt job #{job_id}: status=#{new_status}")
131
+ else
132
+ previous_status = task_job.status
133
+ task_job.update!(status: new_status)
134
+ log("OpenBolt job #{job_id} status changed from '#{previous_status}' to '#{new_status}'", :info)
135
+ end
136
+
122
137
  suspend do |suspended_action|
123
138
  world.clock.ping(suspended_action, POLL_INTERVAL.from_now.to_time, :poll)
124
139
  end
140
+ rescue ::ProxyAPI::Openbolt::ProxyReportedError => e
141
+ log("Proxy reported error for job #{job_id}: #{e.message}", :error)
142
+ mark_exception!(task_job, 'proxy-reported error')
143
+ finish
125
144
  rescue StandardError => e
126
145
  exception("Error polling task status for job #{job_id}", e)
127
146
 
@@ -130,7 +149,7 @@ module Actions
130
149
 
131
150
  if retry_count > RETRY_LIMIT
132
151
  log("Polling gave up for job #{job_id} after #{retry_count} attempts", :error)
133
- task_job.update!(status: 'exception')
152
+ mark_exception!(task_job, "retry limit exceeded (#{retry_count} attempts)")
134
153
  finish
135
154
  return
136
155
  end
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'foreman/logging'
4
+
3
5
  module ForemanOpenbolt
4
6
  class TaskJob < ApplicationRecord
5
7
  self.table_name = 'openbolt_task_jobs'
@@ -44,6 +46,10 @@ module ForemanOpenbolt
44
46
  )
45
47
  end
46
48
 
49
+ def kind
50
+ 'task'
51
+ end
52
+
47
53
  def completed?
48
54
  status.in?(COMPLETED_STATUSES)
49
55
  end
@@ -65,6 +71,8 @@ module ForemanOpenbolt
65
71
  end
66
72
 
67
73
  transaction do
74
+ # status and command guard on presence so blanks never clobber good
75
+ # values; result and log use key? because empty values are legitimate.
68
76
  self.status = proxy_result['status'] if proxy_result['status'].present?
69
77
  self.command = proxy_result['command'] if proxy_result['command'].present?
70
78
  self.result = proxy_result['value'] if proxy_result.key?('value')
@@ -102,8 +110,7 @@ module ForemanOpenbolt
102
110
  job_id)
103
111
  Rails.logger.debug { "Scheduled cleanup for job #{job_id} on proxy #{smart_proxy_id}" }
104
112
  rescue StandardError => e
105
- Rails.logger.error("Failed to schedule cleanup for job #{job_id}: #{e.class}: #{e.message}")
106
- Rails.logger.error(e.backtrace.join("\n")) if e.backtrace
113
+ Foreman::Logging.exception("Failed to schedule cleanup for job #{job_id}", e)
107
114
  end
108
115
  end
109
116
  end
data/config/routes.rb CHANGED
@@ -7,17 +7,52 @@ ForemanOpenbolt::Engine.routes.draw do
7
7
  get 'page_task_history', to: 'task#page_task_history', as: :page_task_history
8
8
 
9
9
  # API endpoints
10
- get 'fetch_tasks', to: 'task#fetch_tasks'
11
- get 'reload_tasks', to: 'task#reload_tasks'
12
- get 'fetch_openbolt_options', to: 'task#fetch_openbolt_options'
10
+ get 'fetch_tasks', to: 'task#tasks'
11
+ post 'reload_tasks', to: 'task#reload_tasks'
12
+ get 'fetch_openbolt_options', to: 'task#task_options'
13
13
  post 'launch_task', to: 'task#launch_task'
14
14
  get 'job_status', to: 'task#job_status'
15
15
  get 'job_result', to: 'task#job_result'
16
16
 
17
17
  # Task job management endpoints
18
- get 'fetch_task_history', to: 'task#fetch_task_history'
18
+ get 'fetch_task_history', to: 'task#jobs'
19
19
  end
20
20
 
21
21
  Foreman::Application.routes.draw do
22
22
  mount ForemanOpenbolt::Engine, at: '/foreman_openbolt'
23
+
24
+ namespace :api, defaults: { format: 'json' } do
25
+ scope '(:apiv)',
26
+ module: :v2,
27
+ defaults: { apiv: 'v2' },
28
+ apiv: /v2/,
29
+ constraints: ApiConstraints.new(version: 2, default: true) do
30
+ scope '/openbolt', as: 'openbolt' do
31
+ # Proxy-scoped task operations
32
+ resources :smart_proxies, only: [] do
33
+ resources :tasks, only: [], controller: 'openbolt_tasks' do
34
+ collection do
35
+ get '/', action: :tasks
36
+ post :reload, action: :reload_tasks
37
+ get :options, action: :task_options
38
+ end
39
+ end
40
+ end
41
+
42
+ # Launch is kind-specific. Plans will add POST 'launch/plan' later.
43
+ post 'launch/task', to: 'openbolt_tasks#launch_task', as: 'launch_task'
44
+
45
+ # Job queries are kind-agnostic. Reads come from the Foreman DB, populated by PollTaskStatus.
46
+ resources :jobs, only: [], controller: 'openbolt_jobs', param: :job_id do
47
+ collection do
48
+ get '/', action: :jobs
49
+ end
50
+ member do
51
+ get :status, action: :job_status
52
+ get :result, action: :job_result
53
+ end
54
+ end
55
+ end
56
+ end
57
+ end
23
58
  end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Foreman never deletes Permission rows for permissions a plugin no longer
4
+ # registers, so upgrades would keep an orphaned view_smart_proxies_openbolt row.
5
+ # Drop it. The default roles keep proxy visibility through the core
6
+ # view_smart_proxies permission. If for some reason a custom role had the old
7
+ # permission applied and not view_smart_proxies, it would lose the smart proxy
8
+ # view permission and this can be easily fixed. But it's very unlikely anyone
9
+ # has a custom role configured like this.
10
+ class RemoveViewSmartProxiesOpenboltPermission < ActiveRecord::Migration[7.0]
11
+ def up
12
+ Permission.where(name: 'view_smart_proxies_openbolt').destroy_all
13
+ end
14
+
15
+ def down
16
+ # The plugin no longer registers the permission so there's nothing to restore.
17
+ end
18
+ end
@@ -238,15 +238,23 @@ module ForemanOpenbolt
238
238
 
239
239
  security_block :foreman_openbolt do
240
240
  permission :execute_openbolt,
241
- { :'foreman_openbolt/task' => [
242
- :page_launch_task, :page_task_execution, :page_task_history,
243
- :fetch_tasks, :reload_tasks, :fetch_openbolt_options,
244
- :launch_task, :job_status, :job_result, :fetch_task_history, :show
245
- ] }
246
- permission :view_smart_proxies_openbolt, :smart_proxies => [:index, :show], :resource_type => 'SmartProxy'
241
+ {
242
+ :'foreman_openbolt/task' => [
243
+ :page_launch_task, :page_task_execution, :page_task_history,
244
+ :tasks, :reload_tasks, :task_options,
245
+ :launch_task, :job_status, :job_result, :jobs
246
+ ],
247
+ :'api/v2/openbolt_tasks' => [
248
+ :tasks, :reload_tasks, :task_options, :launch_task
249
+ ],
250
+ :'api/v2/openbolt_jobs' => [
251
+ :jobs, :job_status, :job_result
252
+ ],
253
+ }
247
254
  end
248
255
 
249
- role 'OpenBolt Executor', [:execute_openbolt]
256
+ role 'OpenBolt Executor', [:execute_openbolt, :view_smart_proxies],
257
+ 'Role granting permissions to list and launch OpenBolt actions through a smart proxy and to view the resulting jobs'
250
258
  add_all_permissions_to_default_roles
251
259
 
252
260
  sub_menu :top_menu, :openbolt,
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ForemanOpenbolt
4
- VERSION = '1.2.0'
4
+ VERSION = '1.3.0'
5
5
  end