foreman_remote_execution 0.2.3 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (87) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +2 -0
  3. data/.rubocop.yml +7 -0
  4. data/.rubocop_todo.yml +16 -5
  5. data/.tx/config +1 -1
  6. data/app/assets/javascripts/template_invocation.js +14 -1
  7. data/app/assets/stylesheets/job_invocations.css.scss +0 -13
  8. data/app/assets/stylesheets/template_invocation.css.scss +7 -13
  9. data/app/controllers/api/v2/foreign_input_sets_controller.rb +1 -1
  10. data/app/controllers/api/v2/job_invocations_controller.rb +12 -18
  11. data/app/controllers/api/v2/remote_execution_features_controller.rb +38 -0
  12. data/app/controllers/api/v2/template_inputs_controller.rb +1 -0
  13. data/app/controllers/job_invocations_controller.rb +3 -13
  14. data/app/controllers/job_templates_controller.rb +1 -1
  15. data/app/controllers/remote_execution_features_controller.rb +19 -0
  16. data/app/helpers/concerns/foreman_remote_execution/hosts_helper_extensions.rb +1 -22
  17. data/app/helpers/remote_execution_helper.rb +30 -12
  18. data/app/lib/actions/remote_execution/helpers/live_output.rb +2 -2
  19. data/app/lib/actions/remote_execution/run_host_job.rb +7 -4
  20. data/app/lib/actions/remote_execution/run_hosts_job.rb +14 -0
  21. data/app/lib/actions/remote_execution/run_proxy_command.rb +2 -2
  22. data/app/models/concerns/foreman_remote_execution/host_extensions.rb +5 -4
  23. data/app/models/concerns/foreman_remote_execution/subnet_extensions.rb +1 -0
  24. data/app/models/input_template_renderer.rb +14 -3
  25. data/app/models/job_invocation.rb +6 -15
  26. data/app/models/job_invocation_composer.rb +131 -21
  27. data/app/models/job_template.rb +77 -22
  28. data/app/models/job_template_effective_user.rb +0 -2
  29. data/app/models/remote_execution_feature.rb +34 -0
  30. data/app/models/setting/remote_execution.rb +4 -1
  31. data/app/models/targeting.rb +2 -2
  32. data/app/models/template_input.rb +17 -13
  33. data/app/views/api/v2/remote_execution_features/base.json.rabl +3 -0
  34. data/app/views/api/v2/remote_execution_features/index.json.rabl +3 -0
  35. data/app/views/api/v2/remote_execution_features/main.json.rabl +3 -0
  36. data/app/views/api/v2/remote_execution_features/show.json.rabl +3 -0
  37. data/app/views/job_invocations/_form.html.erb +51 -40
  38. data/app/views/job_invocations/_preview_hosts_list.html.erb +1 -1
  39. data/app/views/job_invocations/_tab_hosts.html.erb +6 -3
  40. data/app/views/job_invocations/index.html.erb +11 -11
  41. data/app/views/job_templates/_custom_tabs.html.erb +4 -4
  42. data/app/views/job_templates/index.html.erb +5 -5
  43. data/app/views/overrides/nics/_execution_interface.html.erb +9 -1
  44. data/app/views/remote_execution_features/_form.html.erb +24 -0
  45. data/app/views/remote_execution_features/index.html.erb +21 -0
  46. data/app/views/remote_execution_features/show.html.erb +3 -0
  47. data/app/views/template_inputs/_form.html.erb +1 -0
  48. data/app/views/template_inputs/_invocation_form.html.erb +7 -0
  49. data/app/views/templates/package_action.erb +17 -5
  50. data/app/views/templates/power_action.erb +22 -0
  51. data/app/views/templates/puppet_run_once.erb +1 -1
  52. data/config/routes.rb +4 -0
  53. data/db/migrate/20160113162007_expand_all_template_invocations.rb +1 -1
  54. data/db/migrate/20160118124600_create_remote_execution_features.rb +14 -0
  55. data/db/migrate/20160125155108_make_job_template_name_unique.rb +12 -0
  56. data/db/migrate/20160127134031_add_advanced_to_template_input.rb +11 -0
  57. data/db/migrate/20160127162711_reword_puppet_template_description.rb +9 -0
  58. data/db/migrate/20160203104056_add_concurrency_options_to_job_invocation.rb +6 -0
  59. data/db/seeds.d/70-job_templates.rb +2 -1
  60. data/doc/plugins/div_tag.rb +1 -1
  61. data/doc/plugins/plantuml.rb +1 -1
  62. data/doc/plugins/tags.rb +7 -8
  63. data/doc/plugins/toc.rb +0 -1
  64. data/foreman_remote_execution.gemspec +1 -1
  65. data/lib/foreman_remote_execution/engine.rb +10 -2
  66. data/lib/foreman_remote_execution/version.rb +1 -1
  67. data/locale/action_names.rb +8 -0
  68. data/locale/en/foreman_remote_execution.po +767 -11
  69. data/locale/foreman_remote_execution.pot +1026 -8
  70. data/test/functional/api/v2/foreign_input_sets_controller_test.rb +1 -1
  71. data/test/functional/api/v2/job_invocations_controller_test.rb +0 -9
  72. data/test/functional/api/v2/job_templates_controller_test.rb +11 -11
  73. data/test/functional/api/v2/remote_execution_features_controller_test.rb +35 -0
  74. data/test/functional/api/v2/template_inputs_controller_test.rb +1 -1
  75. data/test/unit/actions/run_hosts_job_test.rb +48 -7
  76. data/test/unit/actions/run_proxy_command_test.rb +1 -1
  77. data/test/unit/concerns/host_extensions_test.rb +11 -0
  78. data/test/unit/input_template_renderer_test.rb +4 -4
  79. data/test/unit/job_invocation_composer_test.rb +52 -2
  80. data/test/unit/job_invocation_test.rb +1 -1
  81. data/test/unit/job_template_test.rb +126 -3
  82. data/test/unit/remote_execution_feature_test.rb +42 -0
  83. data/test/unit/targeting_test.rb +4 -4
  84. metadata +26 -7
  85. data/app/views/job_invocation_task_groups/_job_invocation_task_group.html.erb +0 -31
  86. data/app/views/unattended/snippets/_remote_execution_ssh_keys.erb +0 -18
  87. data/db/seeds.d/80-provision_templates.rb +0 -21
@@ -2,11 +2,11 @@ module Actions
2
2
  module RemoteExecution
3
3
  module Helpers
4
4
  module LiveOutput
5
- def exception_to_output(context, exception, timestamp = Time.now)
5
+ def exception_to_output(context, exception, timestamp = Time.now.getlocal)
6
6
  format_output(context + ": #{exception.class} - #{exception.message}", 'debug', timestamp)
7
7
  end
8
8
 
9
- def format_output(message, type = 'debug', timestamp = Time.now)
9
+ def format_output(message, type = 'debug', timestamp = Time.now.getlocal)
10
10
  { 'output_type' => type,
11
11
  'output' => message,
12
12
  'timestamp' => timestamp.to_f }
@@ -43,7 +43,7 @@ module Actions
43
43
  host = Host.find(input[:host][:id])
44
44
  host.refresh_statuses
45
45
  rescue => e
46
- Foreman::Logging.exception "Could not update execution status for #{input[:host][:name]}", e
46
+ ::Foreman::Logging.exception "Could not update execution status for #{input[:host][:name]}", e
47
47
  end
48
48
 
49
49
  def humanized_output
@@ -59,10 +59,13 @@ module Actions
59
59
  end
60
60
  end
61
61
 
62
+ def humanized_input
63
+ N_('%{description} on %{host}') % { :host => input[:host].try(:[], :name),
64
+ :description => input[:description].try(:capitalize) || input[:job_category] }
65
+ end
66
+
62
67
  def humanized_name
63
- _('%{description} on %{host}') % { :job_category => input[:job_category],
64
- :host => input[:host][:name],
65
- :description => input[:description].try(:capitalize) || input[:job_category] }
68
+ N_('Remote action:')
66
69
  end
67
70
 
68
71
  def find_ip_or_hostname(host)
@@ -12,6 +12,7 @@ module Actions
12
12
  end
13
13
 
14
14
  def plan(job_invocation, locked = false)
15
+ set_up_concurrency_control job_invocation
15
16
  job_invocation.task_group.save! if job_invocation.task_group.try(:new_record?)
16
17
  task.add_missing_task_groups(job_invocation.task_group) if job_invocation.task_group
17
18
  action_subject(job_invocation) unless locked
@@ -34,6 +35,11 @@ module Actions
34
35
  end
35
36
  end
36
37
 
38
+ def set_up_concurrency_control(invocation)
39
+ limit_concurrency_level invocation.concurrency_level unless invocation.concurrency_level.nil?
40
+ distribute_over_time invocation.time_span unless invocation.time_span.nil?
41
+ end
42
+
37
43
  def rescue_strategy
38
44
  ::Dynflow::Action::Rescue::Skip
39
45
  end
@@ -42,6 +48,14 @@ module Actions
42
48
  super unless event == Dynflow::Action::Skip
43
49
  end
44
50
 
51
+ def humanized_input
52
+ input[:job_invocation][:description]
53
+ end
54
+
55
+ def humanized_name
56
+ '%s:' % _(super)
57
+ end
58
+
45
59
  private
46
60
 
47
61
  def determine_proxy(template_invocation, host, load_balancer)
@@ -62,7 +62,7 @@ module Actions
62
62
  proxy_data = proxy.status_of_task(output[:proxy_task_id])['actions'].detect { |action| action['class'] == proxy_action_name }
63
63
  proxy_data.fetch('output', {}).fetch('result', [])
64
64
  rescue => e
65
- ::Foreman::Logging.exception("Failed to load data for task #{task.id} from proxy #{ input[:proxy_url] }", e)
65
+ ::Foreman::Logging.exception("Failed to load data for task #{task.id} from proxy #{input[:proxy_url]}", e)
66
66
  [exception_to_output(_('Error loading data from proxy'), e)]
67
67
  end
68
68
 
@@ -85,7 +85,7 @@ module Actions
85
85
 
86
86
  def final_timestamp(records)
87
87
  return task.ended_at if records.blank?
88
- records.last.fetch('timestamp', task.ended_at) + 1
88
+ records.last.fetch('timestamp', task.ended_at).to_f + 1
89
89
  end
90
90
 
91
91
  def proxy_result
@@ -12,7 +12,7 @@ module ForemanRemoteExecution
12
12
  has_many :template_invocations, :dependent => :destroy, :foreign_key => 'host_id'
13
13
  has_one :execution_status_object, :class_name => 'HostStatus::ExecutionStatus', :foreign_key => 'host_id'
14
14
 
15
- scoped_search :in => :execution_status_object, :on => :status, :rename => :'execution_status',
15
+ scoped_search :in => :execution_status_object, :on => :status, :rename => :execution_status,
16
16
  :complete_value => { :ok => HostStatus::ExecutionStatus::OK, :error => HostStatus::ExecutionStatus::ERROR }
17
17
  end
18
18
 
@@ -38,7 +38,7 @@ module ForemanRemoteExecution
38
38
  get_interface_by_flag(:execution)
39
39
  end
40
40
 
41
- def remote_execution_proxies(provider)
41
+ def remote_execution_proxies(provider, authorized = true)
42
42
  proxies = {}
43
43
  proxies[:subnet] = execution_interface.subnet.remote_execution_proxies.with_features(provider) if execution_interface && execution_interface.subnet
44
44
  proxies[:fallback] = smart_proxies.with_features(provider) if Setting[:remote_execution_fallback_proxy]
@@ -50,14 +50,15 @@ module ForemanRemoteExecution
50
50
  proxy_scope = ::SmartProxy
51
51
  end
52
52
 
53
- proxies[:global] = proxy_scope.authorized.with_features(provider)
53
+ proxy_scope = proxy_scope.authorized if authorized
54
+ proxies[:global] = proxy_scope.with_features(provider)
54
55
  end
55
56
 
56
57
  proxies
57
58
  end
58
59
 
59
60
  def remote_execution_ssh_keys
60
- remote_execution_proxies('SSH').values.flatten.uniq.map { |proxy| proxy.pubkey }.compact.uniq
61
+ remote_execution_proxies('SSH', false).values.flatten.uniq.map { |proxy| proxy.pubkey }.compact.uniq
61
62
  end
62
63
 
63
64
  def drop_execution_interface_cache
@@ -5,6 +5,7 @@ module ForemanRemoteExecution
5
5
  included do
6
6
  has_many :target_remote_execution_proxies, :as => :target
7
7
  has_many :remote_execution_proxies, :dependent => :destroy, :through => :target_remote_execution_proxies
8
+ attr_accessible :remote_execution_proxies, :remote_execution_proxy_ids
8
9
  end
9
10
  end
10
11
  end
@@ -2,6 +2,9 @@ class InputTemplateRenderer
2
2
  class UndefinedInput < ::Foreman::Exception
3
3
  end
4
4
 
5
+ class RenderError < ::Foreman::Exception
6
+ end
7
+
5
8
  include UnattendedHelper
6
9
 
7
10
  attr_accessor :template, :host, :invocation, :input_values, :error_message
@@ -22,7 +25,7 @@ class InputTemplateRenderer
22
25
 
23
26
  def render
24
27
  @template.validate_unique_inputs!
25
- render_safe(@template.template, ::Foreman::Renderer::ALLOWED_HELPERS + [ :input, :render_template ], :host => @host)
28
+ render_safe(@template.template, ::Foreman::Renderer::ALLOWED_HELPERS + [ :input, :render_template, :preview?, :render_error ], :host => @host)
26
29
  rescue => e
27
30
  self.error_message ||= _('error during rendering: %s') % e.message
28
31
  Rails.logger.debug e.to_s + "\n" + e.backtrace.join("\n")
@@ -48,10 +51,14 @@ class InputTemplateRenderer
48
51
  end
49
52
  end
50
53
 
54
+ def render_error(message)
55
+ raise RenderError.new(message)
56
+ end
57
+
51
58
  def render_template(template_name, input_values = {}, options = {})
52
59
  options.assert_valid_keys(:with_foreign_input_set)
53
60
  with_foreign_input_set = options.fetch(:with_foreign_input_set, true)
54
- template = JobTemplate.authorized(:view_job_templates).where(:name => template_name).first
61
+ template = JobTemplate.authorized(:view_job_templates).find_by(:name => template_name)
55
62
  unless template
56
63
  self.error_message = _('included template \'%s\' not found') % template_name
57
64
  raise error_message
@@ -69,8 +76,12 @@ class InputTemplateRenderer
69
76
  end
70
77
  end
71
78
 
79
+ def preview?
80
+ !!@preview
81
+ end
82
+
72
83
  def foreign_input_set_values(target_template, overrides = {})
73
- input_set = @template.foreign_input_sets.where(:target_template_id => target_template).first
84
+ input_set = @template.foreign_input_sets.find_by(:target_template_id => target_template)
74
85
  return overrides if input_set.nil?
75
86
 
76
87
  inputs_to_generate = input_set.inputs.map(&:name) - overrides.keys.map(&:to_s)
@@ -10,13 +10,8 @@ class JobInvocation < ActiveRecord::Base
10
10
 
11
11
  belongs_to :targeting, :dependent => :destroy
12
12
  has_many :all_template_invocations, :inverse_of => :job_invocation, :dependent => :destroy, :class_name => 'TemplateInvocation'
13
- if Rails::VERSION::MAJOR >= 4
14
- has_many :template_invocations, -> { where('host_id IS NOT NULL') }, :inverse_of => :job_invocation
15
- has_many :pattern_template_invocations, -> { where('host_id IS NULL') }, :inverse_of => :job_invocation, :class_name => 'TemplateInvocation'
16
- else
17
- has_many :template_invocations, :conditions => 'host_id IS NOT NULL', :inverse_of => :job_invocation
18
- has_many :pattern_template_invocations, :conditions => 'host_id IS NULL', :inverse_of => :job_invocation, :class_name => 'TemplateInvocation'
19
- end
13
+ has_many :template_invocations, -> { where('host_id IS NOT NULL') }, :inverse_of => :job_invocation
14
+ has_many :pattern_template_invocations, -> { where('host_id IS NULL') }, :inverse_of => :job_invocation, :class_name => 'TemplateInvocation'
20
15
 
21
16
  validates :targeting, :presence => true
22
17
  validates :job_category, :presence => true
@@ -48,11 +43,7 @@ class JobInvocation < ActiveRecord::Base
48
43
  belongs_to :triggering, :class_name => 'ForemanTasks::Triggering'
49
44
  has_one :recurring_logic, :through => :triggering, :class_name => 'ForemanTasks::RecurringLogic'
50
45
 
51
- if Rails::VERSION::MAJOR >= 4
52
- scope :with_task, -> { references(:task) }
53
- else
54
- scope :with_task, -> { joins('LEFT JOIN foreman_tasks_tasks ON foreman_tasks_tasks.id = job_invocations.task_id') }
55
- end
46
+ scope :with_task, -> { references(:task) }
56
47
 
57
48
  scoped_search :in => :recurring_logic, :on => 'id', :rename => 'recurring_logic.id', :auto_complete => true
58
49
 
@@ -112,7 +103,7 @@ class JobInvocation < ActiveRecord::Base
112
103
  end
113
104
 
114
105
  def to_action_input
115
- { :id => id, :name => job_category }
106
+ { :id => id, :name => job_category, :description => description }
116
107
  end
117
108
 
118
109
  def failed_template_invocation_tasks
@@ -152,7 +143,7 @@ class JobInvocation < ActiveRecord::Base
152
143
  end
153
144
 
154
145
  def sub_task_for_host(host)
155
- template_invocations.where(:host => host.id).first.try(:run_host_job_task)
146
+ template_invocations.find_by(:host => host.id).try(:run_host_job_task)
156
147
  end
157
148
 
158
149
  def output(host)
@@ -165,7 +156,7 @@ class JobInvocation < ActiveRecord::Base
165
156
  template_invocation = pattern_template_invocations.first
166
157
  input_names = template_invocation.template.template_inputs_with_foreign(&:name)
167
158
  hash_base = Hash.new { |hash, key| hash[key] = "%{#{key}}" }
168
- input_hash = hash_base.merge Hash[input_names.zip(template_invocation.input_values.pluck(:value))]
159
+ input_hash = hash_base.merge Hash[input_names.zip(template_invocation.input_values.map(&:value))]
169
160
  input_hash.update(:job_category => job_category)
170
161
  input_hash.update(:template_name => template_invocation.template.name)
171
162
  description_format.scan(key_re) { |key| input_hash[key.first] }
@@ -12,6 +12,7 @@ class JobInvocationComposer
12
12
  :triggering => ui_params.fetch(:triggering, {}),
13
13
  :host_ids => ui_params[:host_ids],
14
14
  :description_format => job_invocation_base[:description_format],
15
+ :concurrency_control => concurrency_control_params,
15
16
  :template_invocations => template_invocations_params }.with_indifferent_access
16
17
  end
17
18
 
@@ -49,6 +50,13 @@ class JobInvocationComposer
49
50
  template_base
50
51
  end
51
52
  end
53
+
54
+ def concurrency_control_params
55
+ {
56
+ :time_span => job_invocation_base[:time_span],
57
+ :level => job_invocation_base[:concurrency_level]
58
+ }
59
+ end
52
60
  end
53
61
 
54
62
  class ApiParams
@@ -59,17 +67,14 @@ class JobInvocationComposer
59
67
  end
60
68
 
61
69
  def params
62
- { :job_category => job_category,
70
+ { :job_category => template.job_category,
63
71
  :targeting => targeting_params,
64
72
  :triggering => triggering_params,
65
- :description_format => api_params[:description_format] || template_with_default.generate_description_format,
73
+ :description_format => api_params[:description_format],
74
+ :concurrency_control => concurrency_control_params,
66
75
  :template_invocations => template_invocations_params }.with_indifferent_access
67
76
  end
68
77
 
69
- def job_category
70
- api_params[:job_category]
71
- end
72
-
73
78
  def targeting_params
74
79
  raise ::Foreman::Exception, _('Cannot specify both bookmark_id and search_query') if api_params[:bookmark_id] && api_params[:search_query]
75
80
  api_params.slice(:targeting_type, :bookmark_id, :search_query).merge(:user_id => User.current.id)
@@ -98,8 +103,14 @@ class JobInvocationComposer
98
103
  end
99
104
  end
100
105
 
106
+ def concurrency_control_params
107
+ {
108
+ :level => api_params.fetch(:concurrency_control, {})[:concurrency_level],
109
+ :time_span => api_params.fetch(:concurrency_control, {})[:time_span]
110
+ }
111
+ end
112
+
101
113
  def template_invocations_params
102
- template = template_with_default
103
114
  template_invocation_params = { :template_id => template.id, :effective_user => api_params[:effective_user] }
104
115
  template_invocation_params[:input_values] = api_params.fetch(:inputs, {}).map do |name, value|
105
116
  input = template.template_inputs_with_foreign.find { |i| i.name == name }
@@ -112,13 +123,8 @@ class JobInvocationComposer
112
123
  [template_invocation_params]
113
124
  end
114
125
 
115
- def template_with_default
116
- template = nil
117
- templates = JobTemplate.authorized(:view_job_templates).where(:job_category => job_category)
118
- template = templates.find(api_params[:job_template_id]) if api_params[:job_template_id]
119
- template ||= templates.first if templates.count == 1
120
- raise ::Foreman::Exception, _('Cannot determine the template to be used of job %s') % job_category unless template
121
- return template
126
+ def template
127
+ @template ||= JobTemplate.authorized(:view_job_templates).find(api_params[:job_template_id])
122
128
  end
123
129
 
124
130
  private
@@ -141,11 +147,18 @@ class JobInvocationComposer
141
147
  :targeting => targeting_params,
142
148
  :triggering => triggering_params,
143
149
  :description_format => job_invocation.description_format,
150
+ :concurrency_control => concurrency_control_params,
144
151
  :template_invocations => template_invocations_params }.with_indifferent_access
145
152
  end
146
153
 
147
154
  private
148
155
 
156
+ def concurrency_control_params
157
+ {
158
+ :level => job_invocation.concurrency_level,
159
+ :time_span => job_invocation.time_span
160
+ }
161
+ end
149
162
 
150
163
  def targeting_params
151
164
  job_invocation.targeting.attributes.slice('search_query', 'bookmark_id', 'user_id', 'targeting_type')
@@ -164,6 +177,75 @@ class JobInvocationComposer
164
177
  end
165
178
  end
166
179
 
180
+ class ParamsForFeature
181
+ attr_reader :feature_label, :feature, :provided_inputs
182
+
183
+ def initialize(feature_label, hosts, provided_inputs = {})
184
+ @feature = RemoteExecutionFeature.feature(feature_label)
185
+ @provided_inputs = provided_inputs
186
+ if hosts.is_a? Bookmark
187
+ @host_bookmark = hosts
188
+ elsif hosts.is_a? Host::Base
189
+ @host_objects = [hosts]
190
+ elsif hosts.is_a? String
191
+ @host_scoped_search = hosts
192
+ else
193
+ @host_objects = hosts
194
+ end
195
+ end
196
+
197
+ def params
198
+ { :job_category => job_template.job_category,
199
+ :targeting => targeting_params,
200
+ :triggering => {},
201
+ :concurrency_control => {},
202
+ :template_invocations => template_invocations_params }.with_indifferent_access
203
+ end
204
+
205
+ private
206
+
207
+ def targeting_params
208
+ ret = {}
209
+ ret['targeting_type'] = Targeting::STATIC_TYPE
210
+ ret['search_query'] = @host_scoped_search if @host_scoped_search
211
+ ret['search_query'] = Targeting.build_query_from_hosts(@host_objects) if @host_objects
212
+ ret['bookmark_id'] = @host_bookmark.id if @host_bookmark
213
+ ret['user_id'] = User.current.id
214
+ ret
215
+ end
216
+
217
+ def template_invocations_params
218
+ [ { 'template_id' => job_template.id,
219
+ 'input_values' => input_values_params } ]
220
+ end
221
+
222
+ def input_values_params
223
+ @provided_inputs.map do |key, value|
224
+ input = job_template.template_inputs_with_foreign.find { |i| i.name == key.to_s }
225
+ unless input
226
+ raise Foreman::Exception.new(N_('Feature input %{input_name} not defined in template %{template_name}'),
227
+ :input_name => key, :template_name => job_template.name)
228
+ end
229
+ { 'template_input_id' => input.id, 'value' => value }
230
+ end
231
+ end
232
+
233
+ def job_template
234
+ unless feature.job_template
235
+ raise Foreman::Exception.new(N_('No template mapped to feature %{feature_name}'),
236
+ :feature_name => feature.name)
237
+ end
238
+ template = JobTemplate.authorized(:view_job_templates).find_by_id(feature.job_template_id)
239
+
240
+ unless template
241
+ raise Foreman::Exception.new(N_('The template %{template_name} mapped to feature %{feature_name} is not accessible by the user'),
242
+ :template_name => template.name,
243
+ :feature_name => feature.name)
244
+ end
245
+ template
246
+ end
247
+ end
248
+
167
249
  attr_accessor :params, :job_invocation, :host_ids, :search_query
168
250
  delegate :job_category, :pattern_template_invocations, :template_invocations, :targeting, :triggering, :to => :job_invocation
169
251
 
@@ -190,6 +272,10 @@ class JobInvocationComposer
190
272
  self.new(ApiParams.new(api_params).params)
191
273
  end
192
274
 
275
+ def self.for_feature(feature_label, hosts, provided_inputs = {})
276
+ self.new(ParamsForFeature.new(feature_label, hosts, provided_inputs).params)
277
+ end
278
+
193
279
  def compose
194
280
  job_invocation.job_category = validate_job_category(params[:job_category])
195
281
  job_invocation.job_category ||= available_job_categories.first if @set_defaults
@@ -197,10 +283,26 @@ class JobInvocationComposer
197
283
  job_invocation.triggering = build_triggering
198
284
  job_invocation.pattern_template_invocations = build_template_invocations
199
285
  job_invocation.description_format = params[:description_format]
286
+ job_invocation.time_span = params[:concurrency_control][:time_span].to_i unless params[:concurrency_control][:time_span].blank?
287
+ job_invocation.concurrency_level = params[:concurrency_control][:level].to_i unless params[:concurrency_control][:level].blank?
200
288
 
201
289
  self
202
290
  end
203
291
 
292
+ def trigger(raise_on_error = false)
293
+ generate_description
294
+ if raise_on_error
295
+ save!
296
+ else
297
+ return false unless save
298
+ end
299
+ triggering.trigger(::Actions::RemoteExecution::RunHostsJob, job_invocation)
300
+ end
301
+
302
+ def trigger!
303
+ trigger(true)
304
+ end
305
+
204
306
  def valid?
205
307
  targeting.valid? & job_invocation.valid? & !pattern_template_invocations.map(&:valid?).include?(false)
206
308
  end
@@ -262,9 +364,9 @@ class JobInvocationComposer
262
364
  if @search_query.present?
263
365
  @search_query
264
366
  elsif host_ids.present?
265
- targeting.build_query_from_hosts(host_ids)
367
+ Targeting.build_query_from_hosts(host_ids)
266
368
  elsif targeting.bookmark_id
267
- if (bookmark = available_bookmarks.where(:id => targeting.bookmark_id).first)
369
+ if (bookmark = available_bookmarks.find_by(:id => targeting.bookmark_id))
268
370
  bookmark.query
269
371
  else
270
372
  ''
@@ -326,7 +428,7 @@ class JobInvocationComposer
326
428
  # when it's the same, we delete the query since it is used from bookmark
327
429
  # when no bookmark is set we store the query
328
430
  bookmark_id = params[:targeting][:bookmark_id]
329
- bookmark = available_bookmarks.where(:id => bookmark_id).first
431
+ bookmark = available_bookmarks.find_by(:id => bookmark_id)
330
432
  query = params[:targeting][:search_query]
331
433
  if bookmark.present? && query.present?
332
434
  if query.strip == bookmark.query.strip
@@ -340,10 +442,10 @@ class JobInvocationComposer
340
442
  end
341
443
 
342
444
  Targeting.new(
343
- :bookmark_id => bookmark_id,
344
- :targeting_type => params[:targeting][:targeting_type],
345
- :search_query => query
346
- ) { |t| t.user_id = params[:targeting][:user_id] }
445
+ :bookmark_id => bookmark_id,
446
+ :targeting_type => params[:targeting][:targeting_type],
447
+ :search_query => query
448
+ ) { |t| t.user_id = params[:targeting][:user_id] }
347
449
  end
348
450
 
349
451
  def build_triggering
@@ -361,6 +463,14 @@ class JobInvocationComposer
361
463
  end
362
464
  end
363
465
 
466
+ def generate_description
467
+ unless job_invocation.description_format
468
+ template = job_invocation.pattern_template_invocations.first.try(:template)
469
+ job_invocation.description_format = template.generate_description_format if template
470
+ end
471
+ job_invocation.generate_description! if job_invocation.description.blank?
472
+ end
473
+
364
474
  def build_effective_user(template_invocation_params)
365
475
  job_template = available_templates.find(template_invocation_params[:template_id])
366
476
  if job_template.effective_user.overridable? && template_invocation_params[:effective_user].present?