-
Notifications
You must be signed in to change notification settings - Fork 93
/
job_invocation_composer.rb
563 lines (479 loc) · 19.2 KB
/
job_invocation_composer.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
class JobInvocationComposer
class UiParams
attr_reader :ui_params
def initialize(ui_params)
@ui_params = ui_params
end
def params
{ :job_category => job_invocation_base[:job_category],
:targeting => targeting(ui_params.fetch(:targeting, {})),
:triggering => triggering,
:host_ids => ui_params[:host_ids],
:remote_execution_feature_id => job_invocation_base[:remote_execution_feature_id],
:description_format => job_invocation_base[:description_format],
:password => blank_to_nil(job_invocation_base[:password]),
:key_passphrase => blank_to_nil(job_invocation_base[:key_passphrase]),
:sudo_password => blank_to_nil(job_invocation_base[:sudo_password]),
:concurrency_control => concurrency_control_params,
:execution_timeout_interval => execution_timeout_interval,
:template_invocations => template_invocations_params }.with_indifferent_access
end
def job_invocation_base
ui_params.fetch(:job_invocation, {})
end
def providers_base
job_invocation_base.fetch(:providers, {})
end
def execution_timeout_interval
providers_base.values.map do |provider|
id = provider[:job_template_id]
provider.fetch(:job_templates, {}).fetch(id, {})[:execution_timeout_interval]
end.first
end
def blank_to_nil(thing)
thing.blank? ? nil : thing
end
# TODO: Fix this comment
# parses params to get job templates in form of id => attributes for selected job templates, e.g.
# {
# "459" => {},
# "454" => {
# "input_values" => {
# "2" => {
# "value" => ""
# },
# "5" => {
# "value" => ""
# }
# }
# }
# }
def template_invocations_params
providers_base.values.map do |template_params|
template_base = template_params.fetch(:job_templates, {}).fetch(template_params[:job_template_id], {}).dup.with_indifferent_access
template_base[:template_id] = template_params[:job_template_id]
input_values_params = template_base.fetch(:input_values, {})
template_base[:input_values] = input_values_params.map do |id, values|
values.merge(:template_input_id => id)
end
template_base
end
end
def concurrency_control_params
{
:time_span => job_invocation_base[:time_span],
:level => job_invocation_base[:concurrency_level]
}
end
def triggering
return {} unless ui_params.key?(:triggering)
trig = ui_params[:triggering]
keys = (1..5).map { |i| "end_time(#{i}i)" }
return trig unless trig.key?(:end_time) && trig[:end_time].keys == keys
trig.merge(:end_time => Time.local(*trig[:end_time].values_at(*keys)))
end
def targeting(targeting_params)
targeting_params.merge(:user_id => User.current.id)
end
end
class ApiParams
attr_reader :api_params
def initialize(api_params)
@api_params = api_params
end
def params
{ :job_category => template.job_category,
:targeting => targeting_params,
:triggering => triggering_params,
:description_format => api_params[:description_format],
:remote_execution_feature_id => api_params[:remote_execution_feature_id],
:concurrency_control => concurrency_control_params,
:execution_timeout_interval => api_params[:execution_timeout_interval] || template.execution_timeout_interval,
:template_invocations => template_invocations_params }.with_indifferent_access
end
def targeting_params
raise ::Foreman::Exception, _('Cannot specify both bookmark_id and search_query') if api_params[:bookmark_id] && api_params[:search_query]
api_params.slice(:targeting_type, :bookmark_id, :search_query).merge(:user_id => User.current.id)
end
def triggering_params
raise ::Foreman::Exception, _('Cannot specify both recurrence and scheduling') if api_params[:recurrence].present? && api_params[:scheduling].present?
if api_params[:recurrence].present?
{
:mode => :recurring,
:cronline => api_params[:recurrence][:cron_line],
:end_time => format_datetime(api_params[:recurrence][:end_time]),
:input_type => :cronline,
:max_iteration => api_params[:recurrence][:max_iteration]
}
elsif api_params[:scheduling].present?
{
:mode => :future,
:start_at_raw => format_datetime(api_params[:scheduling][:start_at]),
:start_before_raw => format_datetime(api_params[:scheduling][:start_before]),
:end_time_limited => api_params[:scheduling][:start_before] ? true : false
}
else
{}
end
end
def concurrency_control_params
{
:level => api_params.fetch(:concurrency_control, {})[:concurrency_level],
:time_span => api_params.fetch(:concurrency_control, {})[:time_span]
}
end
def template_invocations_params
template_invocation_params = { :template_id => template.id, :effective_user => api_params[:effective_user] }
template_invocation_params[:input_values] = api_params.fetch(:inputs, {}).to_h.map do |name, value|
input = template.template_inputs_with_foreign.find { |i| i.name == name }
unless input
raise ::Foreman::Exception, _('Unknown input %{input_name} for template %{template_name}') %
{ :input_name => name, :template_name => template.name }
end
{ :template_input_id => input.id, :value => value }
end
[template_invocation_params]
end
def template
@template ||= JobTemplate.authorized(:view_job_templates).find(api_params[:job_template_id])
end
private
def format_datetime(datetime)
return datetime if datetime.blank?
Time.parse(datetime).strftime('%Y-%m-%d %H:%M')
end
end
class ParamsFromJobInvocation
attr_reader :job_invocation
def initialize(job_invocation, params = {})
@job_invocation = job_invocation
if params[:host_ids]
@host_ids = params[:host_ids]
elsif params[:failed_only]
@host_ids = job_invocation.failed_host_ids
end
end
def params
{ :job_category => job_invocation.job_category,
:targeting => targeting_params,
:triggering => triggering_params,
:description_format => job_invocation.description_format,
:concurrency_control => concurrency_control_params,
:execution_timeout_interval => job_invocation.execution_timeout_interval,
:remote_execution_feature_id => job_invocation.remote_execution_feature_id,
:template_invocations => template_invocations_params,
:reruns => job_invocation.id }.with_indifferent_access
end
private
def concurrency_control_params
{
:level => job_invocation.concurrency_level,
:time_span => job_invocation.time_span
}
end
def targeting_params
base = { :user_id => User.current.id }
if @host_ids
search_query = @host_ids.empty? ? 'name ^ ()' : Targeting.build_query_from_hosts(@host_ids)
base.merge(:search_query => search_query, :targeting_type => job_invocation.targeting.targeting_type)
else
base.merge job_invocation.targeting.attributes.slice('search_query', 'bookmark_id', 'targeting_type')
end
end
def template_invocations_params
job_invocation.pattern_template_invocations.map do |template_invocation|
params = template_invocation.attributes.slice('template_id', 'effective_user')
params['input_values'] = template_invocation.input_values.map { |v| v.attributes.slice('template_input_id', 'value') }
params
end
end
def triggering_params
ForemanTasks::Triggering.new_from_params.attributes.slice('mode', 'start_at', 'start_before')
end
end
class ParamsForFeature
attr_reader :feature_label, :feature, :provided_inputs
def initialize(feature_label, hosts, provided_inputs = {})
@feature = RemoteExecutionFeature.feature(feature_label)
@provided_inputs = provided_inputs
if hosts.is_a? Bookmark
@host_bookmark = hosts
elsif hosts.is_a? Host::Base
@host_objects = [hosts]
elsif hosts.is_a? Array
@host_objects = hosts.map do |id|
Host::Managed.authorized.friendly.find(id)
end
elsif hosts.is_a? String
@host_scoped_search = hosts
else
@host_objects = hosts
end
end
def params
{ :job_category => job_template.job_category,
:targeting => targeting_params,
:triggering => {},
:concurrency_control => {},
:remote_execution_feature_id => @feature.id,
:template_invocations => template_invocations_params }.with_indifferent_access
end
private
def targeting_params
ret = {}
ret['targeting_type'] = Targeting::STATIC_TYPE
ret['search_query'] = @host_scoped_search if @host_scoped_search
ret['search_query'] = Targeting.build_query_from_hosts(@host_objects) if @host_objects
ret['bookmark_id'] = @host_bookmark.id if @host_bookmark
ret['user_id'] = User.current.id
ret
end
def template_invocations_params
[ { 'template_id' => job_template.id,
'input_values' => input_values_params } ]
end
def input_values_params
return {} if @provided_inputs.blank?
@provided_inputs.map do |key, value|
input = job_template.template_inputs_with_foreign.find { |i| i.name == key.to_s }
unless input
raise Foreman::Exception.new(N_('Feature input %{input_name} not defined in template %{template_name}'),
:input_name => key, :template_name => job_template.name)
end
{ 'template_input_id' => input.id, 'value' => value }
end
end
def job_template
unless feature.job_template
raise Foreman::Exception.new(N_('No template mapped to feature %{feature_name}'),
:feature_name => feature.name)
end
template = JobTemplate.authorized(:view_job_templates).find_by(id: feature.job_template_id)
unless template
raise Foreman::Exception.new(N_('The template %{template_name} mapped to feature %{feature_name} is not accessible by the user'),
:template_name => template.name,
:feature_name => feature.name)
end
template
end
end
attr_accessor :params, :job_invocation, :host_ids, :search_query
attr_reader :reruns
delegate :job_category, :remote_execution_feature_id, :pattern_template_invocations, :template_invocations, :targeting, :triggering, :to => :job_invocation
def initialize(params, set_defaults = false)
@params = params
@set_defaults = set_defaults
@job_invocation = JobInvocation.new
@job_invocation.task_group = JobInvocationTaskGroup.new
@reruns = params[:reruns]
compose
@host_ids = validate_host_ids(params[:host_ids])
@search_query = job_invocation.targeting.search_query if job_invocation.targeting.bookmark_id.blank?
end
def self.from_job_invocation(job_invocation, params = {})
self.new(ParamsFromJobInvocation.new(job_invocation, params).params)
end
def self.from_ui_params(ui_params)
self.new(UiParams.new(ui_params).params, true)
end
def self.from_api_params(api_params)
self.new(ApiParams.new(api_params).params)
end
def self.for_feature(feature_label, hosts, provided_inputs = {})
self.new(ParamsForFeature.new(feature_label, hosts, provided_inputs).params)
end
# rubocop:disable Metrics/AbcSize
def compose
job_invocation.job_category = validate_job_category(params[:job_category])
job_invocation.job_category ||= available_job_categories.first if @set_defaults
job_invocation.remote_execution_feature_id = params[:remote_execution_feature_id]
job_invocation.targeting = build_targeting
job_invocation.triggering = build_triggering
job_invocation.pattern_template_invocations = build_template_invocations
job_invocation.description_format = params[:description_format]
job_invocation.time_span = params[:concurrency_control][:time_span].to_i if params[:concurrency_control][:time_span].present?
job_invocation.concurrency_level = params[:concurrency_control][:level].to_i if params[:concurrency_control][:level].present?
job_invocation.execution_timeout_interval = params[:execution_timeout_interval]
job_invocation.password = params[:password]
job_invocation.key_passphrase = params[:key_passphrase]
job_invocation.sudo_password = params[:sudo_password]
job_invocation.job_category = nil unless rerun_possible?
self
end
# rubocop:enable Metrics/AbcSize
def trigger(raise_on_error = false)
generate_description
if raise_on_error
save!
else
return false unless save
end
triggering.trigger(::Actions::RemoteExecution::RunHostsJob, job_invocation)
end
def trigger!
trigger(true)
end
def valid?
targeting.valid? & job_invocation.valid? & !pattern_template_invocations.map(&:valid?).include?(false) &
triggering.valid?
end
def save
valid? && job_invocation.save
end
def save!
if valid?
job_invocation.save!
else
raise job_invocation.flattened_validation_exception
end
end
def available_templates
JobTemplate.authorized(:view_job_templates).where(:snippet => false)
end
def available_templates_for(job_category)
available_templates.where(:job_category => job_category)
end
def available_job_categories
available_templates.reorder(:job_category).group(:job_category).pluck(:job_category)
end
def available_provider_types
available_templates_for(job_category).reorder(:provider_type).group(:provider_type).pluck(:provider_type)
end
def available_template_inputs
TemplateInput.where(:template_id => job_template_ids.empty? ? available_templates_for(job_category).map(&:id) : job_template_ids)
end
def needs_provider_type_selection?
available_provider_types.size > 1
end
def displayed_provider_types
# TODO available_provider_types based on targets
available_provider_types
end
def templates_for_provider(provider_type)
available_templates_for(job_category).select { |t| t.provider_type == provider_type }
end
def selected_job_templates
available_templates_for(job_category).where(:id => job_template_ids)
end
def preselected_template_for_provider(provider_type)
(templates_for_provider(provider_type) & selected_job_templates).first
end
def displayed_search_query
if @search_query.present?
@search_query
elsif host_ids.present?
Targeting.build_query_from_hosts(host_ids)
elsif targeting.bookmark_id
if (bookmark = available_bookmarks.find_by(:id => targeting.bookmark_id))
bookmark.query
else
''
end
else
''
end
end
def available_bookmarks
Bookmark.authorized(:view_bookmarks).my_bookmarks.where(:controller => ['hosts', 'dashboard'])
end
def targeted_hosts
if displayed_search_query.blank?
Host.where('1 = 0')
else
Host.authorized(Targeting::RESOLVE_PERMISSION, Host).search_for(displayed_search_query)
end
end
def targeted_hosts_count
targeted_hosts.count
rescue
0
end
def template_invocation(job_template)
pattern_template_invocations.find { |invocation| invocation.template == job_template }
end
def template_invocation_input_value_for(job_template, input)
invocations = pattern_template_invocations
default = TemplateInvocationInputValue.new
if (invocation = invocations.detect { |i| i.template_id == job_template.id })
invocation.input_values.detect { |iv| iv.template_input_id == input.id } || default
else
default
end
end
def job_template_ids
job_invocation.pattern_template_invocations.map(&:template_id)
end
def rerun_possible?
!(@reruns && job_invocation.pattern_template_invocations.empty?)
end
private
# builds input values for a given templates id based on params
# omits inputs that belongs to unavailable templates
def build_input_values_for(template_invocation, job_template_base)
template_invocation.input_values = job_template_base.fetch('input_values', {}).map do |attributes|
input = template_invocation.template.template_inputs_with_foreign.find { |i| i.id.to_s == attributes[:template_input_id].to_s }
input ? input.template_invocation_input_values.build(attributes) : nil
end.compact
end
def build_targeting
# if bookmark was used we compare it to search query,
# when it's the same, we delete the query since it is used from bookmark
# when no bookmark is set we store the query
bookmark_id = params[:targeting][:bookmark_id]
bookmark = available_bookmarks.find_by(:id => bookmark_id)
query = params[:targeting][:search_query]
if bookmark.present? && query.present?
if query.strip == bookmark.query.strip
query = nil
else
bookmark_id = nil
end
elsif query.present?
query = params[:targeting][:search_query]
bookmark_id = nil
end
Targeting.new(
:bookmark_id => bookmark_id,
:targeting_type => params[:targeting][:targeting_type],
:search_query => query
) { |t| t.user_id = params[:targeting][:user_id] }
end
def build_triggering
::ForemanTasks::Triggering.new_from_params(params[:triggering])
end
def build_template_invocations
valid_template_ids = validate_job_template_ids(params[:template_invocations].map { |t| t[:template_id] })
params[:template_invocations].select { |t| valid_template_ids.include?(t[:template_id].to_i) }.map do |template_invocation_params|
template_invocation = job_invocation.pattern_template_invocations.build(:template_id => template_invocation_params[:template_id],
:effective_user => build_effective_user(template_invocation_params))
build_input_values_for(template_invocation, template_invocation_params)
template_invocation
end
end
def generate_description
unless job_invocation.description_format
template = job_invocation.pattern_template_invocations.first.try(:template)
job_invocation.description_format = template.generate_description_format if template
end
job_invocation.generate_description if job_invocation.description.blank?
end
def build_effective_user(template_invocation_params)
job_template = available_templates.find(template_invocation_params[:template_id])
if job_template.effective_user.overridable? && template_invocation_params[:effective_user].present?
template_invocation_params[:effective_user]
else
job_template.effective_user.compute_value
end
end
# returns nil if user can't see any job template with such name
# existing job_category string otherwise
def validate_job_category(name)
available_job_categories.include?(name) ? name : nil
end
def validate_job_template_ids(ids)
available_templates_for(job_category).where(:id => ids).pluck(:id)
end
def validate_host_ids(ids)
Host.authorized(Targeting::RESOLVE_PERMISSION, Host).where(:id => ids).pluck(:id)
end
end