foreman_remote_execution 4.4.0 → 4.5.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 (56) hide show
  1. checksums.yaml +4 -4
  2. data/app/controllers/api/v2/job_invocations_controller.rb +13 -24
  3. data/app/controllers/job_templates_controller.rb +4 -4
  4. data/app/controllers/ui_job_wizard_controller.rb +12 -0
  5. data/app/helpers/job_invocations_helper.rb +2 -2
  6. data/app/helpers/remote_execution_helper.rb +8 -8
  7. data/app/lib/actions/remote_execution/run_host_job.rb +31 -5
  8. data/app/models/concerns/foreman_remote_execution/host_extensions.rb +5 -5
  9. data/app/models/host_status/execution_status.rb +5 -5
  10. data/app/models/job_invocation.rb +23 -7
  11. data/app/models/job_invocation_composer.rb +59 -17
  12. data/app/models/ssh_execution_provider.rb +4 -4
  13. data/app/overrides/execution_interface.rb +8 -8
  14. data/app/overrides/subnet_proxies.rb +6 -6
  15. data/config/routes.rb +1 -0
  16. data/db/migrate/20180110104432_rename_template_invocation_permission.rb +1 -1
  17. data/db/migrate/20190111153330_remove_remote_execution_without_proxy_setting.rb +4 -4
  18. data/extra/cockpit/foreman-cockpit-session +6 -6
  19. data/lib/foreman_remote_execution/engine.rb +11 -6
  20. data/lib/foreman_remote_execution/version.rb +1 -1
  21. data/package.json +2 -1
  22. data/test/functional/api/v2/job_invocations_controller_test.rb +14 -1
  23. data/test/unit/job_invocation_composer_test.rb +45 -1
  24. data/webpack/JobWizard/JobWizard.js +59 -18
  25. data/webpack/JobWizard/JobWizard.scss +3 -1
  26. data/webpack/JobWizard/JobWizardConstants.js +1 -0
  27. data/webpack/JobWizard/JobWizardSelectors.js +18 -1
  28. data/webpack/JobWizard/__tests__/JobWizard.test.js +4 -11
  29. data/webpack/JobWizard/__tests__/__snapshots__/JobWizard.test.js.snap +0 -51
  30. data/webpack/JobWizard/__tests__/__snapshots__/integration.test.js.snap +43 -0
  31. data/webpack/JobWizard/__tests__/fixtures.js +26 -0
  32. data/webpack/JobWizard/__tests__/integration.test.js +156 -0
  33. data/webpack/JobWizard/steps/AdvancedFields/AdvancedFields.js +93 -0
  34. data/webpack/JobWizard/steps/AdvancedFields/Fields.js +181 -0
  35. data/webpack/JobWizard/steps/AdvancedFields/__tests__/AdvancedFields.test.js +25 -0
  36. data/webpack/JobWizard/steps/AdvancedFields/__tests__/__snapshots__/AdvancedFields.test.js.snap +249 -0
  37. data/webpack/JobWizard/steps/CategoryAndTemplate/CategoryAndTemplate.js +34 -2
  38. data/webpack/JobWizard/steps/CategoryAndTemplate/CategoryAndTemplate.test.js +10 -3
  39. data/webpack/JobWizard/steps/CategoryAndTemplate/__snapshots__/CategoryAndTemplate.test.js.snap +50 -1
  40. data/webpack/JobWizard/steps/CategoryAndTemplate/index.js +9 -1
  41. data/webpack/JobWizard/steps/form/FormHelpers.js +19 -0
  42. data/webpack/JobWizard/steps/form/GroupedSelectField.js +3 -0
  43. data/webpack/JobWizard/steps/form/SelectField.js +10 -1
  44. data/webpack/JobWizard/steps/form/__tests__/__snapshots__/GroupedSelectField.test.js.snap +1 -0
  45. data/webpack/JobWizard/steps/form/__tests__/__snapshots__/SelectField.test.js.snap +1 -0
  46. data/webpack/__mocks__/foremanReact/redux/API/APISelectors.js +21 -2
  47. data/webpack/global_index.js +5 -3
  48. data/webpack/index.js +3 -0
  49. data/webpack/react_app/components/RecentJobsCard/RecentJobsCard.js +1 -5
  50. data/webpack/react_app/components/TargetingHosts/__tests__/TargetingHostsSelectors.test.js +8 -3
  51. data/webpack/react_app/components/TargetingHosts/__tests__/__snapshots__/TargetingHostsSelectors.test.js.snap +7 -2
  52. data/webpack/react_app/extend/{fills.js → fillRecentJobsCard.js} +7 -6
  53. data/webpack/react_app/extend/fillregistrationAdvanced.js +11 -0
  54. data/webpack/react_app/extend/reducers.js +2 -1
  55. metadata +12 -4
  56. data/webpack/fills_index.js +0 -11
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 28b7b4b36385dfde5221cc7d9b9d07d95f4515dafb657507015b9436086474ca
4
- data.tar.gz: 27d3907329bcda250a9518903f069de480fda0609a53134369c2a348a31fb29a
3
+ metadata.gz: 113fb04d80f92308f55da87e148b2c30de510b5a97dda5c400bdac598d3a407f
4
+ data.tar.gz: 42a44014bbb85af6ccef059d4d716a9e840865be30735cea61fb65cd94a63d2f
5
5
  SHA512:
6
- metadata.gz: 9bfc761d4d8343a7fce864c3155cea9d4a3c67ff42b60359901e8be0f890ddfc63f536e3288133877a448cbee367063a990619a1b85007ab8f0433f4acd917b6
7
- data.tar.gz: 05b1af5c16a8c0d542b9876f86810e8ca2c44dbe4165ff10eccf08eb1172edacc94e68025e486324c52648be0b1cfaf2e85ceb552755428a5917163c28febfad
6
+ metadata.gz: 6321f8795de48743746748a71b3a64d4c75de23f55bfe0e79bfd660a54ecd2b5522f10554fa96c16d11350a6800457b47ae0a82fec7f905142f9d062103b3ce6
7
+ data.tar.gz: 41962649438f42c27845b6e7b6694cff343c4f44e8b7771534a25127458ed2b9a15b404e319e00958f36f81a0d139733820873dd5a2771cb5efeaaebbbfa7223
@@ -80,18 +80,15 @@ module Api
80
80
  api :POST, '/job_invocations/', N_('Create a job invocation')
81
81
  param_group :job_invocation, :as => :create
82
82
  def create
83
- if job_invocation_params[:feature].present?
84
- composer = composer_for_feature
85
- else
86
- validate_template
87
- composer = JobInvocationComposer.from_api_params(
88
- job_invocation_params
89
- )
90
- end
83
+ composer = JobInvocationComposer.from_api_params(
84
+ job_invocation_params
85
+ )
91
86
  composer.trigger!
92
87
  @job_invocation = composer.job_invocation
93
88
  @hosts = @job_invocation.targeting.hosts
94
89
  process_response @job_invocation
90
+ rescue JobInvocationComposer::JobTemplateNotFound, JobInvocationComposer::FeatureNotFound => e
91
+ not_found(error: { message: e.message })
95
92
  end
96
93
 
97
94
  api :GET, '/job_invocations/:id/hosts/:host_id', N_('Get output for a host')
@@ -128,7 +125,7 @@ module Api
128
125
  render :json => { :cancelled => result, :id => @job_invocation.id }
129
126
  else
130
127
  render :json => { :message => _('The job could not be cancelled.') },
131
- :status => :unprocessable_entity
128
+ :status => :unprocessable_entity
132
129
  end
133
130
  end
134
131
 
@@ -143,7 +140,7 @@ module Api
143
140
  process_response @job_invocation
144
141
  else
145
142
  render :json => { :error => _('Could not rerun job %{id} because its template could not be found') % { :id => composer.reruns } },
146
- :status => :not_found
143
+ :status => :not_found
147
144
  end
148
145
  end
149
146
 
@@ -190,19 +187,19 @@ module Api
190
187
  not_found({ :error => { :message => (_("Host with id '%{id}' was not found") % { :id => params['host_id'] }) } })
191
188
  end
192
189
 
193
- def validate_template
194
- JobTemplate.authorized(:view_job_templates).find(job_invocation_params['job_template_id'])
195
- rescue ActiveRecord::RecordNotFound
196
- not_found({ :error => { :message => (_("Template with id '%{id}' was not found") % { :id => job_invocation_params['job_template_id'] }) } })
197
- end
198
-
199
190
  def job_invocation_params
200
191
  return @job_invocation_params if @job_invocation_params.present?
201
192
 
202
193
  job_invocation_params = params.fetch(:job_invocation, {}).dup
194
+
195
+ if job_invocation_params[:feature].present? && job_invocation_params[:job_template_id].present?
196
+ raise _("Only one of feature or job_template_id can be specified")
197
+ end
198
+
203
199
  if job_invocation_params.key?(:ssh)
204
200
  job_invocation_params.merge!(job_invocation_params.delete(:ssh).permit(:effective_user))
205
201
  end
202
+
206
203
  job_invocation_params[:inputs] ||= {}
207
204
  job_invocation_params[:inputs].permit!
208
205
  permit_provider_inputs job_invocation_params
@@ -214,14 +211,6 @@ module Api
214
211
  providers.each { |provider| invocation_params[provider.provider_input_namespace]&.permit! }
215
212
  end
216
213
 
217
- def composer_for_feature
218
- JobInvocationComposer.for_feature(
219
- job_invocation_params[:feature],
220
- job_invocation_params[:host_ids],
221
- job_invocation_params[:inputs].to_hash
222
- )
223
- end
224
-
225
214
  def output_lines_since(task, time)
226
215
  since = time.to_f if time.present?
227
216
  line_sets = task.main_action.live_output
@@ -24,10 +24,10 @@ class JobTemplatesController < ::TemplatesController
24
24
  render :plain => output
25
25
  else
26
26
  render status: :not_acceptable,
27
- plain: _(
28
- 'Problem with previewing the template: %{error}. Note that you must save template input changes before you try to preview it.' %
29
- {:error => renderer.error_message}
30
- )
27
+ plain: _(
28
+ 'Problem with previewing the template: %{error}. Note that you must save template input changes before you try to preview it.' %
29
+ {:error => renderer.error_message}
30
+ )
31
31
  end
32
32
  end
33
33
 
@@ -8,6 +8,18 @@ class UiJobWizardController < ::Api::V2::BaseController
8
8
  render :json => {:job_categories =>job_categories}
9
9
  end
10
10
 
11
+ def template
12
+ job_template = JobTemplate.authorized.find(params[:id])
13
+ render :json => {
14
+ :job_template => job_template,
15
+ :effective_user => job_template.effective_user,
16
+ }
17
+ end
18
+
19
+ def resource_name(nested_resource = nil)
20
+ nested_resource || 'job_template'
21
+ end
22
+
11
23
  def resource_class
12
24
  JobTemplate
13
25
  end
@@ -34,8 +34,8 @@ module JobInvocationsHelper
34
34
  hosts.map do |host|
35
35
  collapsed_preview(host) +
36
36
  render(:partial => 'job_invocations/user_input',
37
- :locals => { :template_invocation => template_invocation,
38
- :target => host })
37
+ :locals => { :template_invocation => template_invocation,
38
+ :target => host })
39
39
  end.reduce(:+)
40
40
  end
41
41
 
@@ -27,19 +27,19 @@ module RemoteExecutionHelper
27
27
 
28
28
  if authorized_for(hash_for_host_path(host).merge(auth_object: host, permission: :view_hosts, authorizer: job_hosts_authorizer))
29
29
  links << { title: _('Host detail'),
30
- action: { href: host_path(host), 'data-method': 'get', id: "#{host.name}-actions-detail" } }
30
+ action: { href: host_path(host), 'data-method': 'get', id: "#{host.name}-actions-detail" } }
31
31
  end
32
32
 
33
33
  if authorized_for(hash_for_rerun_job_invocation_path(id: job_invocation, host_ids: [ host.id ], authorizer: job_hosts_authorizer))
34
34
  links << { title: (_('Rerun on %s') % host.name),
35
- action: { href: rerun_job_invocation_path(job_invocation, host_ids: [ host.id ]),
36
- 'data-method': 'get', id: "#{host.name}-actions-rerun" } }
35
+ action: { href: rerun_job_invocation_path(job_invocation, host_ids: [ host.id ]),
36
+ 'data-method': 'get', id: "#{host.name}-actions-rerun" } }
37
37
  end
38
38
 
39
39
  if host_task.present? && authorized_for(hash_for_foreman_tasks_task_path(host_task).merge(auth_object: host_task, permission: :view_foreman_tasks))
40
40
  links << { title: _('Host task'),
41
- action: { href: foreman_tasks_task_path(host_task),
42
- 'data-method': 'get', id: "#{host.name}-actions-task" } }
41
+ action: { href: foreman_tasks_task_path(host_task),
42
+ 'data-method': 'get', id: "#{host.name}-actions-task" } }
43
43
  end
44
44
 
45
45
  links
@@ -159,11 +159,11 @@ module RemoteExecutionHelper
159
159
  content_tag :pre, preview
160
160
  elsif target.nil?
161
161
  alert :text => _('Could not render the preview because no host matches the search query.'),
162
- :class => 'alert alert-block alert-warning base',
163
- :close => false
162
+ :class => 'alert alert-block alert-warning base',
163
+ :close => false
164
164
  else
165
165
  alert :class => 'alert-block alert-danger base in fade has-error',
166
- :text => renderer.error_message.html_safe # rubocop:disable Rails/OutputSafety
166
+ :text => renderer.error_message.html_safe # rubocop:disable Rails/OutputSafety
167
167
  end
168
168
  end
169
169
 
@@ -5,6 +5,8 @@ module Actions
5
5
  include ::Actions::Helpers::WithDelegatedAction
6
6
  include ::Actions::ObservableAction
7
7
 
8
+ execution_plan_hooks.use :emit_feature_event, :on => :success
9
+
8
10
  middleware.do_not_use Dynflow::Middleware::Common::Transaction
9
11
  middleware.use Actions::Middleware::HideSecrets
10
12
 
@@ -17,7 +19,12 @@ module Actions
17
19
  end
18
20
 
19
21
  def plan(job_invocation, host, template_invocation, proxy_selector = ::RemoteExecutionProxySelector.new, options = {})
20
- action_subject(host, :job_category => job_invocation.job_category, :description => job_invocation.description, :job_invocation_id => job_invocation.id)
22
+ features = template_invocation.template.remote_execution_features.pluck(:label).uniq
23
+ action_subject(host,
24
+ :job_category => job_invocation.job_category,
25
+ :description => job_invocation.description,
26
+ :job_invocation_id => job_invocation.id,
27
+ :job_features => features)
21
28
 
22
29
  template_invocation.host_id = host.id
23
30
  template_invocation.run_host_job_task_id = task.id
@@ -58,6 +65,22 @@ module Actions
58
65
  check_exit_status
59
66
  end
60
67
 
68
+ def self.feature_job_event_name(label, suffix = :success)
69
+ ::Foreman::Observable.event_name_for("#{::Actions::RemoteExecution::RunHostJob.event_name_base}_#{label}_#{::Actions::RemoteExecution::RunHostJob.event_name_suffix(suffix)}")
70
+ end
71
+
72
+ def emit_feature_event(execution_plan, hook = :success)
73
+ return unless root_action?
74
+
75
+ payload = event_payload(execution_plan)
76
+ if input["job_features"]&.any?
77
+ input['job_features'].each do |feature|
78
+ name = "#{self.class.event_name_base}_#{feature}_#{self.class.event_name_suffix(hook)}"
79
+ trigger_hook name, payload: payload
80
+ end
81
+ end
82
+ end
83
+
61
84
  def secrets(host, job_invocation, provider)
62
85
  job_secrets = { :ssh_password => job_invocation.password,
63
86
  :key_passphrase => job_invocation.key_passphrase,
@@ -78,7 +101,7 @@ module Actions
78
101
 
79
102
  def humanized_input
80
103
  N_('%{description} on %{host}') % { :host => input[:host].try(:[], :name),
81
- :description => input[:description].try(:capitalize) || input[:job_category] }
104
+ :description => input[:description].try(:capitalize) || input[:job_category] }
82
105
  end
83
106
 
84
107
  def humanized_name
@@ -186,10 +209,13 @@ module Actions
186
209
  'All %{count} applicable proxies are down. Tried %{proxy_names}',
187
210
  offline_proxies.count) % settings
188
211
  elsif proxy == :not_defined
189
- settings = { :global_proxy => 'remote_execution_global_proxy',
190
- :fallback_proxy => 'remote_execution_fallback_proxy' }
212
+ settings = {
213
+ global_proxy: 'remote_execution_global_proxy',
214
+ fallback_proxy: 'remote_execution_fallback_proxy',
215
+ provider: provider,
216
+ }
191
217
 
192
- raise _('Could not use any proxy. Consider configuring %{global_proxy}, ' +
218
+ raise _('Could not use any proxy for the %{provider} job. Consider configuring %{global_proxy}, ' +
193
219
  '%{fallback_proxy} in settings') % settings
194
220
  end
195
221
  proxy
@@ -8,15 +8,15 @@ module ForemanRemoteExecution
8
8
  has_many :run_host_job_tasks, :through => :template_invocations
9
9
 
10
10
  scoped_search :relation => :run_host_job_tasks, :on => :result, :rename => 'job_invocation.result',
11
- :ext_method => :search_by_job_invocation,
12
- :only_explicit => true,
13
- :complete_value => TemplateInvocation::TaskResultMap::REVERSE_MAP
11
+ :ext_method => :search_by_job_invocation,
12
+ :only_explicit => true,
13
+ :complete_value => TemplateInvocation::TaskResultMap::REVERSE_MAP
14
14
 
15
15
  scoped_search :relation => :template_invocations, :on => :job_invocation_id,
16
- :rename => 'job_invocation.id', :only_explicit => true, :ext_method => :search_by_job_invocation
16
+ :rename => 'job_invocation.id', :only_explicit => true, :ext_method => :search_by_job_invocation
17
17
 
18
18
  scoped_search :relation => :execution_status_object, :on => :status, :rename => :execution_status,
19
- :complete_value => { :ok => HostStatus::ExecutionStatus::OK, :error => HostStatus::ExecutionStatus::ERROR }
19
+ :complete_value => { :ok => HostStatus::ExecutionStatus::OK, :error => HostStatus::ExecutionStatus::ERROR }
20
20
 
21
21
  def search_by_job_invocation(key, operator, value)
22
22
  if key == 'job_invocation.result'
@@ -64,15 +64,15 @@ class HostStatus::ExecutionStatus < HostStatus::Status
64
64
 
65
65
  case status
66
66
  when OK
67
- [ "state = 'stopped' AND result = 'success'" ]
67
+ [ "foreman_tasks_tasks.state = 'stopped' AND result = 'success'" ]
68
68
  when CANCELLED
69
- [ "state = 'stopped' AND result = 'cancelled'" ]
69
+ [ "foreman_tasks_tasks.state = 'stopped' AND result = 'cancelled'" ]
70
70
  when ERROR
71
- [ "state = 'stopped' AND (result = 'error' OR result = 'warning')" ]
71
+ [ "foreman_tasks_tasks.state = 'stopped' AND (result = 'error' OR result = 'warning')" ]
72
72
  when QUEUED
73
- [ "state = 'scheduled' OR state IS NULL" ]
73
+ [ "foreman_tasks_tasks.state = 'scheduled' OR foreman_tasks_tasks.state IS NULL" ]
74
74
  when RUNNING
75
- [ "state <> 'stopped'" ]
75
+ [ "foreman_tasks_tasks.state <> 'stopped'" ]
76
76
  else
77
77
  [ '1 = 0' ]
78
78
  end
@@ -15,8 +15,9 @@ class JobInvocation < ApplicationRecord
15
15
 
16
16
  belongs_to :targeting, :dependent => :destroy
17
17
  has_many :all_template_invocations, :inverse_of => :job_invocation, :dependent => :destroy, :class_name => 'TemplateInvocation'
18
- has_many :template_invocations, -> { where('host_id IS NOT NULL') }, :inverse_of => :job_invocation
19
- has_many :pattern_template_invocations, -> { where('host_id IS NULL') }, :inverse_of => :job_invocation, :class_name => 'TemplateInvocation'
18
+ has_many :template_invocations, -> { where('template_invocations.host_id IS NOT NULL') }, :inverse_of => :job_invocation
19
+ has_many :pattern_template_invocations, -> { where('template_invocations.host_id IS NULL') }, :inverse_of => :job_invocation, :class_name => 'TemplateInvocation'
20
+ has_many :pattern_templates, :through => :pattern_template_invocations, :source => :template
20
21
 
21
22
  validates :targeting, :presence => true
22
23
  validates :job_category, :presence => true
@@ -44,26 +45,33 @@ class JobInvocation < ApplicationRecord
44
45
  :source => 'run_host_job_task'
45
46
  has_one :user, through: :task
46
47
  scoped_search relation: :user, on: :login, rename: 'user', complete_value: true,
47
- value_translation: ->(value) { value == 'current_user' ? User.current.login : value },
48
- special_values: [:current_user], aliases: ['owner'], :only_explicit => true
48
+ value_translation: ->(value) { value == 'current_user' ? User.current.login : value },
49
+ special_values: [:current_user], aliases: ['owner'], :only_explicit => true
49
50
  scoped_search :relation => :task, :on => :started_at, :rename => 'started_at', :complete_value => true
50
51
  scoped_search :relation => :task, :on => :start_at, :rename => 'start_at', :complete_value => true
51
52
  scoped_search :relation => :task, :on => :ended_at, :rename => 'ended_at', :complete_value => true
52
53
  scoped_search :relation => :task, :on => :state, :rename => 'status', :ext_method => :search_by_status,
53
- :only_explicit => true, :complete_value => Hash[HostStatus::ExecutionStatus::STATUS_NAMES.values.map { |v| [v, v] }]
54
+ :only_explicit => true, :complete_value => Hash[HostStatus::ExecutionStatus::STATUS_NAMES.values.map { |v| [v, v] }]
54
55
 
55
56
  belongs_to :triggering, :class_name => 'ForemanTasks::Triggering'
56
57
  has_one :recurring_logic, :through => :triggering, :class_name => 'ForemanTasks::RecurringLogic'
57
58
 
58
59
  belongs_to :remote_execution_feature
59
60
 
61
+ has_many :targeted_hosts, :through => :targeting, :source => :hosts
62
+ scoped_search :on => 'targeted_host_id', :rename => 'targeted_host_id', :operators => ['= '],
63
+ :complete_value => false, :only_explicit => true, :ext_method => :search_by_targeted_host
64
+
65
+ scoped_search :on => 'pattern_template_name', :rename => 'pattern_template_name', :operators => ['= '],
66
+ :complete_value => false, :only_explicit => true, :ext_method => :search_by_pattern_template
67
+
60
68
  scope :with_task, -> { references(:task) }
61
69
 
62
70
  scoped_search :relation => :recurring_logic, :on => 'id', :rename => 'recurring_logic.id'
63
71
 
64
72
  scoped_search :relation => :recurring_logic, :on => 'id', :rename => 'recurring',
65
- :ext_method => :search_by_recurring_logic, :only_explicit => true,
66
- :complete_value => { :true => true, :false => false }
73
+ :ext_method => :search_by_recurring_logic, :only_explicit => true,
74
+ :complete_value => { :true => true, :false => false }
67
75
 
68
76
  default_scope -> { order('job_invocations.id DESC') }
69
77
 
@@ -80,6 +88,14 @@ class JobInvocation < ApplicationRecord
80
88
  allow :sub_task_for_host, :template_invocations_hosts
81
89
  end
82
90
 
91
+ def self.search_by_targeted_host(key, operator, value)
92
+ { :conditions => sanitize_sql_for_conditions(["hosts.id = ?", value]), :joins => :targeted_hosts }
93
+ end
94
+
95
+ def self.search_by_pattern_template(key, operator, value)
96
+ { :conditions => sanitize_sql_for_conditions(["templates.name = ?", value]), :joins => :pattern_templates }
97
+ end
98
+
83
99
  def self.search_by_status(key, operator, value)
84
100
  conditions = HostStatus::ExecutionStatus::ExecutionTaskStatusMapper.sql_conditions_for(value)
85
101
  conditions[0] = "NOT (#{conditions[0]})" if operator == '<>'
@@ -1,4 +1,7 @@
1
1
  class JobInvocationComposer
2
+ class JobTemplateNotFound < StandardError; end
3
+
4
+ class FeatureNotFound < StandardError; end
2
5
 
3
6
  class UiParams
4
7
  attr_reader :ui_params
@@ -100,6 +103,17 @@ class JobInvocationComposer
100
103
 
101
104
  def initialize(api_params)
102
105
  @api_params = api_params
106
+
107
+ if api_params[:feature]
108
+ # set a default targeting type for backward compatibility
109
+ # when `for_feature` was used by the API it automatically set a default
110
+ api_params[:targeting_type] = Targeting::STATIC_TYPE
111
+ end
112
+
113
+ if api_params[:search_query].blank? && api_params[:host_ids].present?
114
+ translator = HostIdsTranslator.new(api_params[:host_ids])
115
+ api_params[:search_query] = translator.scoped_search
116
+ end
103
117
  end
104
118
 
105
119
  def params
@@ -107,12 +121,16 @@ class JobInvocationComposer
107
121
  :targeting => targeting_params,
108
122
  :triggering => triggering_params,
109
123
  :description_format => api_params[:description_format],
110
- :remote_execution_feature_id => api_params[:remote_execution_feature_id],
124
+ :remote_execution_feature_id => remote_execution_feature_id,
111
125
  :concurrency_control => concurrency_control_params,
112
126
  :execution_timeout_interval => api_params[:execution_timeout_interval] || template.execution_timeout_interval,
113
127
  :template_invocations => template_invocations_params }.with_indifferent_access
114
128
  end
115
129
 
130
+ def remote_execution_feature_id
131
+ feature&.id || api_params[:remote_execution_feature_id]
132
+ end
133
+
116
134
  def targeting_params
117
135
  raise ::Foreman::Exception, _('Cannot specify both bookmark_id and search_query') if api_params[:bookmark_id] && api_params[:search_query]
118
136
 
@@ -170,8 +188,20 @@ class JobInvocationComposer
170
188
  inputs.select { |key, value| provider_input_names.include? key }.map { |key, value| { :name => key, :value => value } }
171
189
  end
172
190
 
191
+ def feature
192
+ @feature ||= RemoteExecutionFeature.feature(api_params[:feature]) if api_params[:feature]
193
+ rescue => e
194
+ raise(FeatureNotFound, e.message)
195
+ end
196
+
197
+ def job_template_id
198
+ feature&.job_template_id || api_params[:job_template_id]
199
+ end
200
+
173
201
  def template
174
- @template ||= JobTemplate.authorized(:view_job_templates).find(api_params[:job_template_id])
202
+ @template ||= JobTemplate.authorized(:view_job_templates).find(job_template_id)
203
+ rescue ActiveRecord::RecordNotFound
204
+ raise(JobTemplateNotFound, _("Template with id '%{id}' was not found") % { id: job_template_id })
175
205
  end
176
206
 
177
207
  private
@@ -239,25 +269,38 @@ class JobInvocationComposer
239
269
  end
240
270
  end
241
271
 
272
+ class HostIdsTranslator
273
+ attr_reader :bookmark, :hosts, :scoped_search, :host_ids
274
+
275
+ def initialize(input)
276
+ case input
277
+ when Bookmark
278
+ @bookmark = input
279
+ when Host::Base
280
+ @hosts = [input]
281
+ when Array
282
+ @hosts = input.map do |id|
283
+ Host::Managed.authorized.friendly.find(id)
284
+ end
285
+ when String
286
+ @scoped_search = input
287
+ else
288
+ @hosts = input
289
+ end
290
+
291
+ @scoped_search ||= Targeting.build_query_from_hosts(hosts.map(&:id)) if @hosts
292
+ end
293
+ end
294
+
242
295
  class ParamsForFeature
243
296
  attr_reader :feature_label, :feature, :provided_inputs
244
297
 
245
298
  def initialize(feature_label, hosts, provided_inputs = {})
246
299
  @feature = RemoteExecutionFeature.feature(feature_label)
247
300
  @provided_inputs = provided_inputs
248
- if hosts.is_a? Bookmark
249
- @host_bookmark = hosts
250
- elsif hosts.is_a? Host::Base
251
- @host_objects = [hosts]
252
- elsif hosts.is_a? Array
253
- @host_objects = hosts.map do |id|
254
- Host::Managed.authorized.friendly.find(id)
255
- end
256
- elsif hosts.is_a? String
257
- @host_scoped_search = hosts
258
- else
259
- @host_objects = hosts
260
- end
301
+ translator = HostIdsTranslator.new(hosts)
302
+ @host_bookmark = translator.bookmark
303
+ @host_scoped_search = translator.scoped_search
261
304
  end
262
305
 
263
306
  def params
@@ -275,7 +318,6 @@ class JobInvocationComposer
275
318
  ret = {}
276
319
  ret['targeting_type'] = Targeting::STATIC_TYPE
277
320
  ret['search_query'] = @host_scoped_search if @host_scoped_search
278
- ret['search_query'] = Targeting.build_query_from_hosts(@host_objects) if @host_objects
279
321
  ret['bookmark_id'] = @host_bookmark.id if @host_bookmark
280
322
  ret['user_id'] = User.current.id
281
323
  ret
@@ -553,7 +595,7 @@ class JobInvocationComposer
553
595
 
554
596
  params[:template_invocations].select { |t| valid_template_ids.include?(t[:template_id].to_i) }.map do |template_invocation_params|
555
597
  template_invocation = job_invocation.pattern_template_invocations.build(:template_id => template_invocation_params[:template_id],
556
- :effective_user => build_effective_user(template_invocation_params))
598
+ :effective_user => build_effective_user(template_invocation_params))
557
599
  build_input_values_for(template_invocation, template_invocation_params)
558
600
  template_invocation
559
601
  end