foreman_remote_execution 14.1.0 → 14.1.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (45) hide show
  1. checksums.yaml +4 -4
  2. data/app/models/job_template.rb +1 -0
  3. data/app/views/template_invocations/show.js.erb +1 -1
  4. data/db/migrate/20240312133027_extend_template_invocation_events.rb +9 -0
  5. data/lib/foreman_remote_execution/engine.rb +1 -1
  6. data/lib/foreman_remote_execution/version.rb +1 -1
  7. data/lib/tasks/foreman_remote_execution_tasks.rake +3 -0
  8. data/test/benchmark/run_hosts_job_benchmark.rb +70 -0
  9. data/test/benchmark/targeting_benchmark.rb +31 -0
  10. data/test/factories/foreman_remote_execution_factories.rb +147 -0
  11. data/test/functional/api/v2/foreign_input_sets_controller_test.rb +58 -0
  12. data/test/functional/api/v2/job_invocations_controller_test.rb +446 -0
  13. data/test/functional/api/v2/job_templates_controller_test.rb +110 -0
  14. data/test/functional/api/v2/registration_controller_test.rb +73 -0
  15. data/test/functional/api/v2/remote_execution_features_controller_test.rb +34 -0
  16. data/test/functional/api/v2/template_invocations_controller_test.rb +33 -0
  17. data/test/functional/cockpit_controller_test.rb +16 -0
  18. data/test/functional/job_invocations_controller_test.rb +132 -0
  19. data/test/functional/job_templates_controller_test.rb +31 -0
  20. data/test/functional/ui_job_wizard_controller_test.rb +16 -0
  21. data/test/graphql/mutations/job_invocations/create_test.rb +58 -0
  22. data/test/graphql/queries/job_invocation_query_test.rb +31 -0
  23. data/test/graphql/queries/job_invocations_query_test.rb +35 -0
  24. data/test/helpers/remote_execution_helper_test.rb +46 -0
  25. data/test/support/remote_execution_helper.rb +5 -0
  26. data/test/test_plugin_helper.rb +9 -0
  27. data/test/unit/actions/run_host_job_test.rb +115 -0
  28. data/test/unit/actions/run_hosts_job_test.rb +214 -0
  29. data/test/unit/api_params_test.rb +25 -0
  30. data/test/unit/concerns/foreman_tasks_cleaner_extensions_test.rb +29 -0
  31. data/test/unit/concerns/host_extensions_test.rb +219 -0
  32. data/test/unit/concerns/nic_extensions_test.rb +9 -0
  33. data/test/unit/execution_task_status_mapper_test.rb +92 -0
  34. data/test/unit/input_template_renderer_test.rb +503 -0
  35. data/test/unit/job_invocation_composer_test.rb +974 -0
  36. data/test/unit/job_invocation_report_template_test.rb +60 -0
  37. data/test/unit/job_invocation_test.rb +232 -0
  38. data/test/unit/job_template_effective_user_test.rb +37 -0
  39. data/test/unit/job_template_test.rb +316 -0
  40. data/test/unit/remote_execution_feature_test.rb +86 -0
  41. data/test/unit/remote_execution_provider_test.rb +298 -0
  42. data/test/unit/renderer_scope_input_test.rb +49 -0
  43. data/test/unit/targeting_test.rb +206 -0
  44. data/test/unit/template_invocation_input_value_test.rb +38 -0
  45. metadata +39 -2
@@ -0,0 +1,974 @@
1
+ require 'test_plugin_helper'
2
+ RemoteExecutionProvider.register(:Ansible, OpenStruct)
3
+ RemoteExecutionProvider.register(:Mcollective, OpenStruct)
4
+
5
+ class JobInvocationComposerTest < ActiveSupport::TestCase
6
+ before do
7
+ setup_user('create', 'template_invocations')
8
+ setup_user('view', 'job_templates', 'name ~ trying*')
9
+ setup_user('create', 'job_templates', 'name ~ trying*')
10
+ setup_user('view', 'job_invocations')
11
+ setup_user('create', 'job_invocations')
12
+ setup_user('view', 'bookmarks')
13
+ setup_user('create', 'bookmarks')
14
+ setup_user('edit', 'bookmarks')
15
+ setup_user('view', 'hosts')
16
+ setup_user('create', 'hosts')
17
+ end
18
+
19
+ class AnsibleInputs < RemoteExecutionProvider
20
+ class << self
21
+ def provider_input_namespace
22
+ :ansible
23
+ end
24
+
25
+ def provider_inputs
26
+ [
27
+ ForemanRemoteExecution::ProviderInput.new(name: 'tags', label: 'Tags', value: 'fooo', value_type: 'plain'),
28
+ ForemanRemoteExecution::ProviderInput.new(name: 'tags_flag', label: 'Tags Flag', value: '--tags', options: "--tags\n--skip-tags"),
29
+ ]
30
+ end
31
+ end
32
+ end
33
+ RemoteExecutionProvider.register(:AnsibleInputs, AnsibleInputs)
34
+
35
+ let(:trying_job_template_1) { FactoryBot.create(:job_template, :job_category => 'trying_job_template_1', :name => 'trying1', :provider_type => 'SSH') }
36
+ let(:trying_job_template_2) { FactoryBot.create(:job_template, :job_category => 'trying_job_template_2', :name => 'trying2', :provider_type => 'Mcollective') }
37
+ let(:trying_job_template_3) { FactoryBot.create(:job_template, :job_category => 'trying_job_template_1', :name => 'trying3', :provider_type => 'SSH') }
38
+ let(:trying_job_template_5) { FactoryBot.create(:job_template, :job_category => 'trying_job_template_5', :name => 'trying5', :provider_type => 'SSH') }
39
+ let(:unauthorized_job_template_1) { FactoryBot.create(:job_template, :job_category => 'trying_job_template_1', :name => 'unauth1', :provider_type => 'SSH') }
40
+ let(:unauthorized_job_template_2) { FactoryBot.create(:job_template, :job_category => 'unauthorized_job_template_2', :name => 'unauth2', :provider_type => 'Ansible') }
41
+
42
+ let(:provider_inputs_job_template) { FactoryBot.create(:job_template, :job_category => 'trying_test_inputs', :name => 'trying provider inputs', :provider_type => 'AnsibleInputs') }
43
+
44
+ let(:input1) { FactoryBot.create(:template_input, :template => trying_job_template_1, :input_type => 'user') }
45
+ let(:input2) { FactoryBot.create(:template_input, :template => trying_job_template_3, :input_type => 'user') }
46
+ let(:input3) { FactoryBot.create(:template_input, :template => trying_job_template_1, :input_type => 'user', :required => true) }
47
+ let(:input4) { FactoryBot.create(:template_input, :template => provider_inputs_job_template, :input_type => 'user') }
48
+ let(:input5) { FactoryBot.create(:template_input, :template => trying_job_template_5, :input_type => 'user', :default => 'value') }
49
+ let(:unauthorized_input1) { FactoryBot.create(:template_input, :template => unauthorized_job_template_1, :input_type => 'user') }
50
+
51
+ let(:ansible_params) { { } }
52
+ let(:ssh_params) { { } }
53
+ let(:mcollective_params) { { } }
54
+ let(:providers_params) { { :providers => { :ansible => ansible_params, :ssh => ssh_params, :mcollective => mcollective_params } } }
55
+
56
+ context 'with general new invocation and empty params' do
57
+ let(:params) { {} }
58
+ let(:composer) { JobInvocationComposer.from_ui_params(params) }
59
+
60
+ describe '#available_templates' do
61
+ it 'obeys authorization' do
62
+ composer # lazy load composer before stubbing
63
+ JobTemplate.expects(:authorized).with(:view_job_templates).returns(JobTemplate.where({}))
64
+ composer.available_templates
65
+ end
66
+ end
67
+
68
+ context 'job templates exist' do
69
+ before do
70
+ trying_job_template_1
71
+ trying_job_template_2
72
+ trying_job_template_3
73
+ unauthorized_job_template_1
74
+ unauthorized_job_template_2
75
+ end
76
+
77
+ describe '#available_templates_for(job_category)' do
78
+ it 'find the templates only for a given job name' do
79
+ results = composer.available_templates_for(trying_job_template_1.job_category)
80
+ assert_includes results, trying_job_template_1
81
+ refute_includes results, trying_job_template_2
82
+ end
83
+
84
+ it 'it respects view permissions' do
85
+ results = composer.available_templates_for(trying_job_template_1.job_category)
86
+ refute_includes results, unauthorized_job_template_1
87
+ end
88
+ end
89
+
90
+ describe '#available_job_categories' do
91
+ let(:job_categories) { composer.available_job_categories }
92
+
93
+ it 'find only job names that user is granted to view' do
94
+ assert_includes job_categories, trying_job_template_1.job_category
95
+ assert_includes job_categories, trying_job_template_2.job_category
96
+ refute_includes job_categories, unauthorized_job_template_2.job_category
97
+ end
98
+
99
+ it 'every job name is listed just once' do
100
+ assert_equal job_categories.uniq, job_categories
101
+ end
102
+ end
103
+
104
+ describe '#available_provider_types' do
105
+ let(:provider_types) { composer.available_provider_types }
106
+
107
+ it 'finds only providers which user is granted to view' do
108
+ composer.job_invocation.job_category = 'trying_job_template_1'
109
+ assert_includes provider_types, 'SSH'
110
+ refute_includes provider_types, 'Mcollective'
111
+ refute_includes provider_types, 'Ansible'
112
+ end
113
+
114
+ it 'every provider type is listed just once' do
115
+ assert_equal provider_types.uniq, provider_types
116
+ end
117
+ end
118
+
119
+ describe '#available_template_inputs' do
120
+ before do
121
+ input1
122
+ input2
123
+ unauthorized_input1
124
+ end
125
+
126
+ it 'returns only authorized inputs based on templates' do
127
+ assert_includes composer.available_template_inputs, input1
128
+ assert_includes composer.available_template_inputs, input2
129
+ refute_includes composer.available_template_inputs, unauthorized_input1
130
+ end
131
+
132
+ context 'params contains job template ids' do
133
+ let(:ssh_params) { { :job_template_id => trying_job_template_1.id.to_s } }
134
+ let(:ansible_params) { { :job_template_id => '' } }
135
+ let(:mcollective_params) { { :job_template_id => '' } }
136
+ let(:params) { { :job_invocation => providers_params }.with_indifferent_access }
137
+
138
+ it 'finds the inputs only specified job templates' do
139
+ assert_includes composer.available_template_inputs, input1
140
+ refute_includes composer.available_template_inputs, input2
141
+ refute_includes composer.available_template_inputs, unauthorized_input1
142
+ end
143
+ end
144
+ end
145
+
146
+ describe '#needs_provider_type_selection?' do
147
+ it 'returns true if there are more than one providers respecting authorization' do
148
+ composer.stubs(:available_provider_types => [ 'SSH', 'Ansible' ])
149
+ assert composer.needs_provider_type_selection?
150
+ end
151
+
152
+ it 'returns false if there is one provider' do
153
+ composer.stubs(:available_provider_types => [ 'SSH' ])
154
+ refute composer.needs_provider_type_selection?
155
+ end
156
+ end
157
+
158
+ describe '#displayed_provider_types' do
159
+ # nothing to test yet
160
+ end
161
+
162
+ describe '#templates_for_provider(provider_type)' do
163
+ it 'returns all templates for a given provider respecting template permissions' do
164
+ trying_job_template_4 = FactoryBot.create(:job_template, :job_category => 'trying_job_template_1', :name => 'trying4', :provider_type => 'Ansible')
165
+ result = composer.templates_for_provider('SSH')
166
+ assert_includes result, trying_job_template_1
167
+ assert_includes result, trying_job_template_3
168
+ refute_includes result, unauthorized_job_template_1
169
+ refute_includes result, trying_job_template_4
170
+
171
+ result = composer.templates_for_provider('Ansible')
172
+ refute_includes result, trying_job_template_1
173
+ refute_includes result, trying_job_template_3
174
+ refute_includes result, unauthorized_job_template_2
175
+ assert_includes result, trying_job_template_4
176
+ end
177
+ end
178
+
179
+ describe '#rerun_possible?' do
180
+ it 'is true when not rerunning' do
181
+ assert_predicate composer, :rerun_possible?
182
+ end
183
+
184
+ it 'is true when rerunning with pattern tempalte invocations' do
185
+ composer.expects(:reruns).returns(1)
186
+ composer.job_invocation.expects(:pattern_template_invocations).returns([1])
187
+ assert_predicate composer, :rerun_possible?
188
+ end
189
+
190
+ it 'is false when rerunning without pattern template invocations' do
191
+ composer.expects(:reruns).returns(1)
192
+ composer.job_invocation.expects(:pattern_template_invocations).returns([])
193
+ refute_predicate composer, :rerun_possible?
194
+ end
195
+ end
196
+
197
+ describe '#selected_job_templates' do
198
+ it 'returns no template if none was selected through params' do
199
+ assert_empty composer.selected_job_templates
200
+ end
201
+
202
+ context 'extra unavailable templates id were selected' do
203
+ let(:unauthorized) { FactoryBot.create(:job_template, :job_category => 'trying_job_template_1', :name => 'unauth3', :provider_type => 'Ansible') }
204
+ let(:mcollective_authorized) { FactoryBot.create(:job_template, :job_category => 'trying_job_template_1', :name => 'trying4', :provider_type => 'Mcollective') }
205
+ let(:ssh_params) { { :job_template_id => trying_job_template_1.id.to_s } }
206
+ let(:ansible_params) { { :job_template_id => unauthorized.id.to_s } }
207
+ let(:mcollective_params) { { :job_template_id => mcollective_authorized.id.to_s } }
208
+ let(:params) { { :job_invocation => providers_params }.with_indifferent_access }
209
+
210
+ it 'ignores unauthorized template' do
211
+ unauthorized # make sure unautorized exists
212
+ refute_includes composer.selected_job_templates, unauthorized
213
+ end
214
+
215
+ it 'contains only authorized template specified in params' do
216
+ mcollective_authorized # make sure mcollective_authorized exists
217
+ assert_includes composer.selected_job_templates, trying_job_template_1
218
+ assert_includes composer.selected_job_templates, mcollective_authorized
219
+ refute_includes composer.selected_job_templates, trying_job_template_3
220
+ end
221
+ end
222
+ end
223
+
224
+ describe '#preselected_template_for_provider(provider_type)' do
225
+ context 'none template was selected through params' do
226
+ it 'returns nil' do
227
+ assert_nil composer.preselected_template_for_provider('SSH')
228
+ end
229
+ end
230
+
231
+ context 'available template was selected for a specified provider through params' do
232
+ let(:ssh_params) { { :job_template_id => trying_job_template_1.id.to_s } }
233
+ let(:params) { { :job_invocation => providers_params }.with_indifferent_access }
234
+
235
+ it 'returns the selected template because it is available for provider' do
236
+ assert_equal trying_job_template_1, composer.preselected_template_for_provider('SSH')
237
+ end
238
+ end
239
+ end
240
+
241
+ describe '#pattern template_invocations' do
242
+ let(:ssh_params) do
243
+ { :job_template_id => trying_job_template_1.id.to_s,
244
+ :job_templates => {
245
+ trying_job_template_1.id.to_s => {
246
+ :input_values => { input1.id.to_s => { :value => 'value1' }, unauthorized_input1.id.to_s => { :value => 'dropped' } },
247
+ },
248
+ }}
249
+ end
250
+ let(:params) { { :job_invocation => { :providers => { :ssh => ssh_params } } }.with_indifferent_access }
251
+ let(:invocations) { composer.pattern_template_invocations }
252
+
253
+ it 'builds pattern template invocations based on passed params and it filters out wrong inputs' do
254
+ assert_equal 1, invocations.size
255
+ assert_equal 1, invocations.first.input_values.size
256
+ assert_equal 'value1', invocations.first.input_values.first.value
257
+ end
258
+ end
259
+
260
+ describe '#effective_user' do
261
+ let(:ssh_params) do
262
+ { :job_template_id => trying_job_template_1.id.to_s,
263
+ :job_templates => {
264
+ trying_job_template_1.id.to_s => {
265
+ :effective_user => invocation_effective_user,
266
+ },
267
+ }}
268
+ end
269
+ let(:params) { { :job_invocation => { :providers => { :ssh => ssh_params } } }.with_indifferent_access }
270
+ let(:template_invocation) do
271
+ trying_job_template_1.effective_user.update(:overridable => overridable, :value => 'template user')
272
+ composer.pattern_template_invocations.first
273
+ end
274
+
275
+ context 'when overridable and provided' do
276
+ let(:overridable) { true }
277
+ let(:invocation_effective_user) { 'invocation user' }
278
+
279
+ it 'takes the value from the template invocation' do
280
+ assert_equal 'invocation user', template_invocation.effective_user
281
+ end
282
+ end
283
+
284
+ context 'when overridable and not provided' do
285
+ let(:overridable) { true }
286
+ let(:invocation_effective_user) { '' }
287
+
288
+ it 'takes the value from the job template' do
289
+ assert_equal 'template user', template_invocation.effective_user
290
+ end
291
+ end
292
+
293
+ context 'when not overridable and provided' do
294
+ let(:overridable) { false }
295
+ let(:invocation_effective_user) { 'invocation user' }
296
+
297
+ it 'takes the value from the job template' do
298
+ assert_equal 'template user', template_invocation.effective_user
299
+ end
300
+ end
301
+ end
302
+
303
+ describe '#displayed_search_query' do
304
+ it 'is empty by default' do
305
+ assert_empty composer.displayed_search_query
306
+ end
307
+
308
+ let(:host) { FactoryBot.create(:host) }
309
+ let(:bookmark) { Bookmark.create!(:query => 'b', :name => 'bookmark', :public => true, :controller => 'hosts') }
310
+
311
+ context 'all targetings parameters are present' do
312
+ let(:params) { { :targeting => { :search_query => 'a', :bookmark_id => bookmark.id }, :host_ids => [ host.id ] }.with_indifferent_access }
313
+
314
+ it 'explicit search query has highest priority' do
315
+ assert_equal 'a', composer.displayed_search_query
316
+ end
317
+ end
318
+
319
+ context 'host ids and bookmark are present' do
320
+ let(:params) { { :targeting => { :bookmark_id => bookmark.id }, :host_ids => [ host.id ] }.with_indifferent_access }
321
+
322
+ it 'hosts will be used instead of a bookmark' do
323
+ assert_includes composer.displayed_search_query, host.name
324
+ end
325
+ end
326
+
327
+ context 'bookmark is present' do
328
+ let(:params) { { :targeting => { :bookmark_id => bookmark.id } }.with_indifferent_access }
329
+
330
+ it 'bookmark query is used if it is available for the user' do
331
+ bookmark.update_attribute :public, false
332
+ assert_equal bookmark.query, composer.displayed_search_query
333
+ end
334
+
335
+ it 'bookmark query is used if the bookmark is public' do
336
+ bookmark.owner = nil
337
+ bookmark.save(:validate => false) # skip validations so owner remains nil
338
+ assert_equal bookmark.query, composer.displayed_search_query
339
+ end
340
+
341
+ it 'empty search is returned if bookmark is not owned by the user and is not public' do
342
+ bookmark.public = false
343
+ bookmark.owner = nil
344
+ bookmark.save(:validate => false) # skip validations so owner remains nil
345
+ assert_empty composer.displayed_search_query
346
+ end
347
+ end
348
+ end
349
+
350
+ describe '#available_bookmarks' do
351
+ context 'there are hostgroups and hosts bookmark' do
352
+ let(:hostgroups) { Bookmark.create(:name => 'hostgroups', :query => 'name = x', :controller => 'hostgroups') }
353
+ let(:hosts) { Bookmark.create(:name => 'hosts', :query => 'name = x', :controller => 'hosts') }
354
+ let(:dashboard) { Bookmark.create(:name => 'dashboard', :query => 'name = x', :controller => 'dashboard') }
355
+
356
+ it 'finds only host related bookmarks' do
357
+ hosts
358
+ dashboard
359
+ hostgroups
360
+ assert_includes composer.available_bookmarks, hosts
361
+ assert_includes composer.available_bookmarks, dashboard
362
+ refute_includes composer.available_bookmarks, hostgroups
363
+ end
364
+ end
365
+ end
366
+
367
+ describe '#targeted_hosts_count' do
368
+ let(:host) { FactoryBot.create(:host) }
369
+
370
+ it 'obeys authorization' do
371
+ fake_scope = mock
372
+ composer.stubs(:displayed_search_query => "name = #{host.name}")
373
+ Host.expects(:execution_scope).returns(fake_scope)
374
+ fake_scope.expects(:authorized).with(:view_hosts, Host).returns(Host.where({}))
375
+ composer.targeted_hosts_count
376
+ end
377
+
378
+ it 'searches hosts based on displayed_search_query' do
379
+ composer.stubs(:displayed_search_query => "name = #{host.name}")
380
+ assert_equal 1, composer.targeted_hosts_count
381
+ end
382
+
383
+ it 'returns 0 for queries with syntax errors' do
384
+ composer.stubs(:displayed_search_query => 'name = ')
385
+ assert_equal 0, composer.targeted_hosts_count
386
+ end
387
+
388
+ it 'returns 0 when no query is present' do
389
+ composer.stubs(:displayed_search_query => '')
390
+ assert_equal 0, composer.targeted_hosts_count
391
+ end
392
+ end
393
+
394
+ describe '#input_value_for(input)' do
395
+ let(:value1) { composer.input_value_for(input1) }
396
+ it 'returns new empty input value if there is no invocation' do
397
+ assert value1.new_record?
398
+ assert_empty value1.value
399
+ end
400
+
401
+ context 'there are invocations without input values for a given input' do
402
+ let(:ssh_params) do
403
+ { :job_template_id => trying_job_template_1.id.to_s,
404
+ :job_templates => {
405
+ trying_job_template_1.id.to_s => {
406
+ :input_values => { },
407
+ },
408
+ } }
409
+ end
410
+ let(:params) { { :job_invocation => { :providers => { :ssh => ssh_params } } }.with_indifferent_access }
411
+
412
+ it 'returns new empty input value' do
413
+ assert value1.new_record?
414
+ assert_empty value1.value
415
+ end
416
+ end
417
+
418
+ context 'there are invocations with input values for a given input' do
419
+ let(:ssh_params) do
420
+ { :job_template_id => trying_job_template_1.id.to_s,
421
+ :job_templates => {
422
+ trying_job_template_1.id.to_s => {
423
+ :input_values => { input1.id.to_s => { :value => 'value1' } },
424
+ },
425
+ } }
426
+ end
427
+ let(:params) { { :job_invocation => { :providers => { :ssh => ssh_params } } }.with_indifferent_access }
428
+
429
+ it 'finds the value among template invocations' do
430
+ assert_equal 'value1', value1.value
431
+ end
432
+ end
433
+ end
434
+
435
+ describe '#valid?' do
436
+ let(:host) { FactoryBot.create(:host) }
437
+ let(:ssh_params) do
438
+ { :job_template_id => trying_job_template_1.id.to_s,
439
+ :job_templates => {
440
+ trying_job_template_1.id.to_s => {
441
+ :input_values => { input1.id.to_s => { :value => 'value1' } },
442
+ },
443
+ } }
444
+ end
445
+
446
+ let(:params) do
447
+ { :job_invocation => { :providers => { :ssh => ssh_params } }, :targeting => { :search_query => "name = #{host.name}" } }.with_indifferent_access
448
+ end
449
+
450
+ it 'validates all associated objects even if some of the is invalid' do
451
+ composer
452
+ composer.job_invocation.expects(:valid?).returns(false)
453
+ composer.targeting.expects(:valid?).returns(false)
454
+ composer.pattern_template_invocations.each { |invocation| invocation.expects(:valid?).returns(false) }
455
+ assert_not composer.valid?
456
+ end
457
+ end
458
+
459
+ describe 'concurrency control' do
460
+
461
+ describe 'with concurrency control set' do
462
+ let(:params) do
463
+ { :job_invocation => { :providers => { :ssh => ssh_params }, :concurrency_level => '5' } }.with_indifferent_access
464
+ end
465
+
466
+ it 'accepts the concurrency options' do
467
+ assert_equal 5, composer.job_invocation.concurrency_level
468
+ end
469
+ end
470
+
471
+ it 'can be disabled' do
472
+ assert_nil composer.job_invocation.concurrency_level
473
+ end
474
+ end
475
+
476
+ describe 'triggering' do
477
+ let(:params) do
478
+ { :job_invocation => { :providers => { :ssh => ssh_params } }, :triggering => { :mode => 'future', :end_time=> {"end_time(3i)": 1, "end_time(2i)": 2, "end_time(1i)": 3, "end_time(4i)": 4, "end_time(5i)": 5} }}.with_indifferent_access
479
+ end
480
+
481
+ it 'accepts the triggering params' do
482
+ assert_equal :future, composer.job_invocation.triggering.mode
483
+ end
484
+
485
+ it 'formats the triggering end time when its unordered' do
486
+ assert_equal Time.local(3,2,1,4,5), composer.job_invocation.triggering.end_time
487
+ end
488
+ end
489
+
490
+ describe '#save' do
491
+ it 'triggers save on job_invocation if it is valid' do
492
+ composer.stubs(:valid? => true)
493
+ composer.job_invocation.expects(:save)
494
+ composer.save
495
+ end
496
+
497
+ it 'does not trigger save on job_invocation if it is invalid' do
498
+ composer.stubs(:valid? => false)
499
+ composer.job_invocation.expects(:save).never
500
+ composer.save
501
+ end
502
+ end
503
+
504
+ describe '#job_category' do
505
+ it 'triggers job_category on job_invocation' do
506
+ composer
507
+ composer.job_invocation.expects(:job_category)
508
+ composer.job_category
509
+ end
510
+ end
511
+
512
+ describe '#password' do
513
+ let(:password) { 'changeme' }
514
+ let(:params) do
515
+ { :job_invocation => { :password => password }}.with_indifferent_access
516
+ end
517
+
518
+ it 'sets the password properly' do
519
+ composer
520
+ assert_equal password, composer.job_invocation.password
521
+ end
522
+ end
523
+
524
+ describe '#key_passphrase' do
525
+ let(:key_passphrase) { 'changeme' }
526
+ let(:params) do
527
+ { :job_invocation => { :key_passphrase => key_passphrase }}
528
+ end
529
+
530
+ it 'sets the key passphrase properly' do
531
+ composer
532
+ assert_equal key_passphrase, composer.job_invocation.key_passphrase
533
+ end
534
+ end
535
+
536
+ describe '#effective_user_password' do
537
+ let(:effective_user_password) { 'password' }
538
+ let(:params) do
539
+ { :job_invocation => { :effective_user_password => effective_user_password }}
540
+ end
541
+
542
+ it 'sets the effective_user_password password properly' do
543
+ composer
544
+ assert_equal effective_user_password, composer.job_invocation.effective_user_password
545
+ end
546
+ end
547
+
548
+ describe '#targeting' do
549
+ it 'triggers targeting on job_invocation' do
550
+ composer
551
+ composer.job_invocation.expects(:targeting)
552
+ composer.targeting
553
+ end
554
+ end
555
+
556
+ describe '#compose_from_invocation(existing_invocation)' do
557
+ let(:host) { FactoryBot.create(:host) }
558
+ let(:ssh_params) do
559
+ { :job_template_id => trying_job_template_1.id.to_s,
560
+ :job_templates => {
561
+ trying_job_template_1.id.to_s => {
562
+ :input_values => { input1.id.to_s => { :value => 'value1' } },
563
+ },
564
+ } }
565
+ end
566
+ let(:params) do
567
+ {
568
+ :job_invocation => {
569
+ :providers => { :ssh => ssh_params },
570
+ :concurrency_level => 5,
571
+ },
572
+ :targeting => {
573
+ :search_query => "name = #{host.name}",
574
+ :targeting_type => Targeting::STATIC_TYPE,
575
+ },
576
+ }.with_indifferent_access
577
+ end
578
+ let(:existing) { composer.job_invocation }
579
+ let(:new_composer) { JobInvocationComposer.from_job_invocation(composer.job_invocation) }
580
+
581
+ before do
582
+ composer.save
583
+ end
584
+
585
+ it 'sets the same job name' do
586
+ assert_equal existing.job_category, new_composer.job_category
587
+ end
588
+
589
+ it 'accepts additional host ids' do
590
+ new_composer = JobInvocationComposer.from_job_invocation(composer.job_invocation, { :host_ids => [host.id] })
591
+ assert_equal "name ^ (#{host.name})", new_composer.search_query
592
+ end
593
+
594
+ it 'builds new targeting object which keeps search query' do
595
+ refute_equal existing.targeting, new_composer.targeting
596
+ assert_equal existing.targeting.search_query, new_composer.search_query
597
+ end
598
+
599
+ it 'keeps job template ids' do
600
+ assert_equal existing.pattern_template_invocations.map(&:template_id), new_composer.job_template_ids
601
+ end
602
+
603
+ it 'keeps template invocations and their values' do
604
+ assert_equal existing.pattern_template_invocations.size, new_composer.pattern_template_invocations.size
605
+ end
606
+
607
+ it 'sets the same concurrency control options' do
608
+ assert_equal existing.concurrency_level, new_composer.job_invocation.concurrency_level
609
+ end
610
+
611
+ end
612
+ end
613
+ end
614
+
615
+ describe '.from_api_params' do
616
+ let(:composer) do
617
+ JobInvocationComposer.from_api_params(params)
618
+ end
619
+ let(:bookmark) { bookmarks(:one) }
620
+
621
+ context 'with targeting from bookmark' do
622
+
623
+ before do
624
+ [trying_job_template_1, trying_job_template_3] # mentioning templates we want to have initialized in the test
625
+ end
626
+
627
+ let(:params) do
628
+ { :job_category => trying_job_template_1.job_category,
629
+ :job_template_id => trying_job_template_1.id,
630
+ :targeting_type => 'static_query',
631
+ :bookmark_id => bookmark.id }
632
+ end
633
+
634
+ it 'creates invocation with a bookmark' do
635
+ assert composer.save!
636
+ assert_equal bookmark, composer.job_invocation.targeting.bookmark
637
+ assert_equal composer.job_invocation.targeting.user, User.current
638
+ assert_not_empty composer.job_invocation.pattern_template_invocations
639
+ end
640
+ end
641
+
642
+ context 'with targeting from search query' do
643
+ let(:params) do
644
+ { :job_category => trying_job_template_1.job_category,
645
+ :job_template_id => trying_job_template_1.id,
646
+ :targeting_type => 'static_query',
647
+ :search_query => 'some hosts' }
648
+ end
649
+
650
+ it 'creates invocation with a search query' do
651
+ assert composer.save!
652
+ assert_equal 'some hosts', composer.job_invocation.targeting.search_query
653
+ assert_not_empty composer.job_invocation.pattern_template_invocations
654
+ end
655
+ end
656
+
657
+ context 'with with inputs' do
658
+ let(:params) do
659
+ { :job_category => trying_job_template_5.job_category,
660
+ :job_template_id => trying_job_template_5.id,
661
+ :targeting_type => 'static_query',
662
+ :search_query => 'some hosts',
663
+ :inputs => {input5.name => 'some_value'}}
664
+ end
665
+
666
+ it 'finds the inputs by name' do
667
+ assert composer.save!
668
+ values = composer.pattern_template_invocations.first.input_values
669
+ assert_equal 1, values.count
670
+ assert_equal 'some_value', values.first.value
671
+ end
672
+
673
+ it 'can be forced to be empty' do
674
+ params[:inputs] = {input5.name => ''}
675
+ assert composer.save!
676
+ values = composer.pattern_template_invocations.first.input_values
677
+ assert_equal 1, values.count
678
+ assert_equal '', values.first.value
679
+ end
680
+ end
681
+
682
+ context 'with inputs and default values' do
683
+ let(:params) do
684
+ { :job_category => trying_job_template_5.job_category,
685
+ :job_template_id => trying_job_template_5.id,
686
+ :targeting_type => 'static_query',
687
+ :search_query => 'some hosts',
688
+ :inputs => {}}
689
+ end
690
+
691
+ it 'uses the default input values' do
692
+ input5 # Force the factory to be materialized
693
+ assert composer.save!
694
+ assert_equal 1, composer.pattern_template_invocations.first.input_values.collect.count
695
+ end
696
+ end
697
+
698
+ context 'with provider inputs' do
699
+ let(:params) do
700
+ { :job_category => provider_inputs_job_template.job_category,
701
+ :job_template_id => provider_inputs_job_template.id,
702
+ :targeting_type => 'static_query',
703
+ :search_query => 'some hosts',
704
+ :inputs => { input4.name => 'some_value' },
705
+ :ansible => { 'tags' => 'bar', 'tags_flag' => '--skip-tags' } }
706
+ end
707
+
708
+ it 'detects provider inputs' do
709
+ assert composer.save!
710
+ scope = composer.job_invocation.pattern_template_invocations.first.provider_input_values
711
+ tags = scope.find_by :name => 'tags'
712
+ flags = scope.find_by :name => 'tags_flag'
713
+ assert_equal 'bar', tags.value
714
+ assert_equal '--skip-tags', flags.value
715
+ end
716
+ end
717
+
718
+ context 'with effective user' do
719
+ let(:params) do
720
+ { :job_category => trying_job_template_1.job_category,
721
+ :job_template_id => trying_job_template_1.id,
722
+ :effective_user => 'invocation user',
723
+ :targeting_type => 'static_query',
724
+ :search_query => 'some hosts',
725
+ :inputs => {input1.name => 'some_value'}}
726
+ end
727
+
728
+ let(:template_invocation) { composer.job_invocation.pattern_template_invocations.first }
729
+
730
+ it 'sets the effective user based on the input' do
731
+ assert composer.save!
732
+ assert_equal 'invocation user', template_invocation.effective_user
733
+ end
734
+ end
735
+
736
+ context 'with concurrency_control' do
737
+ let(:level) { 5 }
738
+ let(:params) do
739
+ { :job_category => trying_job_template_1.job_category,
740
+ :job_template_id => trying_job_template_1.id,
741
+ :concurrency_control => {
742
+ :concurrency_level => level,
743
+ },
744
+ :targeting_type => 'static_query',
745
+ :search_query => 'some hosts',
746
+ :inputs => { input1.name => 'some_value' } }
747
+ end
748
+
749
+ it 'sets the concurrency level based on the input' do
750
+ assert composer.save!
751
+ assert_equal level, composer.job_invocation.concurrency_level
752
+ end
753
+ end
754
+
755
+ context 'with rex feature defined' do
756
+ let(:feature) { FactoryBot.create(:remote_execution_feature) }
757
+ let(:params) do
758
+ { :job_category => trying_job_template_1.job_category,
759
+ :job_template_id => trying_job_template_1.id,
760
+ :remote_execution_feature_id => feature.id,
761
+ :targeting_type => 'static_query',
762
+ :search_query => 'some hosts',
763
+ :inputs => { input1.name => 'some_value' } }
764
+ end
765
+
766
+ it 'sets the remote execution feature based on the input' do
767
+ assert composer.save!
768
+ assert_equal feature, composer.job_invocation.remote_execution_feature
769
+ end
770
+
771
+ it 'sets the remote execution_feature id based on `feature` param' do
772
+ params[:remote_execution_feature_id] = nil
773
+ params[:feature] = feature.label
774
+ params[:job_template_id] = trying_job_template_1.id
775
+ refute_equal feature.job_template, trying_job_template_1
776
+
777
+ assert composer.save!
778
+ assert_equal feature, composer.job_invocation.remote_execution_feature
779
+ end
780
+ end
781
+
782
+ context 'with invalid targeting' do
783
+ let(:params) do
784
+ { :job_category => trying_job_template_1.job_category,
785
+ :job_template_id => trying_job_template_1.id,
786
+ :targeting_type => 'fake',
787
+ :search_query => 'some hosts',
788
+ :inputs => {input1.name => 'some_value'}}
789
+ end
790
+
791
+ it 'handles errors' do
792
+ assert_raises(ActiveRecord::RecordNotSaved) do
793
+ composer.save!
794
+ end
795
+ end
796
+ end
797
+
798
+ context 'with invalid bookmark and search query' do
799
+ let(:params) do
800
+ { :job_category => trying_job_template_1.job_category,
801
+ :job_template_id => trying_job_template_1.id,
802
+ :targeting_type => 'static_query',
803
+ :search_query => 'some hosts',
804
+ :bookmark_id => bookmark.id,
805
+ :inputs => {input1.name => 'some_value'}}
806
+ end
807
+
808
+ it 'handles errors' do
809
+ assert_raises(Foreman::Exception) do
810
+ JobInvocationComposer.from_api_params(params)
811
+ end
812
+ end
813
+ end
814
+
815
+ context 'with invalid inputs' do
816
+ let(:params) do
817
+ { :job_category => trying_job_template_1.job_category,
818
+ :job_template_id => trying_job_template_1.id,
819
+ :targeting_type => 'static_query',
820
+ :search_query => 'some hosts',
821
+ :inputs => {input3.name => nil}}
822
+ end
823
+
824
+ it 'handles errors' do
825
+ error = assert_raises(ActiveRecord::RecordNotSaved) do
826
+ composer.save!
827
+ end
828
+ assert_includes error.message, "Template #{trying_job_template_1.name}: Input #{input3.name.downcase}: Value can't be blank"
829
+ end
830
+ end
831
+
832
+ context 'with empty values for non-required inputs' do
833
+ let(:params) do
834
+ { :job_category => trying_job_template_1.job_category,
835
+ :job_template_id => trying_job_template_1.id,
836
+ :targeting_type => 'static_query',
837
+ :search_query => 'some hosts',
838
+ :inputs => {input3.name => 'some value'}}
839
+ end
840
+
841
+ it 'accepts the params' do
842
+ composer.save!
843
+ assert_not composer.job_invocation.new_record?
844
+ end
845
+ end
846
+
847
+ context 'with missing required inputs' do
848
+ let(:params) do
849
+ { :job_category => trying_job_template_1.job_category,
850
+ :job_template_id => trying_job_template_1.id,
851
+ :targeting_type => 'static_query',
852
+ :search_query => 'some hosts',
853
+ :inputs => {input1.name => 'some_value'}}
854
+ end
855
+
856
+ it 'handles errors' do
857
+ assert_predicate input3, :required
858
+
859
+ error = assert_raises(ActiveRecord::RecordNotSaved) do
860
+ composer.save!
861
+ end
862
+
863
+ assert_includes error.message, "Template #{trying_job_template_1.name}: Not all required inputs have values. Missing inputs: #{input3.name}"
864
+ end
865
+ end
866
+ end
867
+
868
+ describe '#from_job_invocation' do
869
+ let(:job_invocation) do
870
+ as_admin { FactoryBot.create(:job_invocation, :with_template, :with_task) }
871
+ end
872
+
873
+ before do
874
+ job_invocation.targeting.host_ids = job_invocation.template_invocations_host_ids
875
+ end
876
+
877
+ it 'marks targeting as resolved if static' do
878
+ created = JobInvocationComposer.from_job_invocation(job_invocation).job_invocation
879
+ assert created.targeting.resolved?
880
+ created.targeting.save
881
+ created.targeting.reload
882
+ assert_equal job_invocation.template_invocations_host_ids, created.targeting.targeting_hosts.pluck(:host_id)
883
+ end
884
+
885
+ it 'takes randomized_ordering from the original job invocation when rerunning failed' do
886
+ job_invocation.targeting.randomized_ordering = true
887
+ job_invocation.targeting.save!
888
+ host_ids = job_invocation.targeting.hosts.pluck(:id)
889
+ composer = JobInvocationComposer.from_job_invocation(job_invocation, :host_ids => host_ids)
890
+ assert composer.job_invocation.targeting.randomized_ordering
891
+ end
892
+
893
+ it 'works with invalid hosts' do
894
+ host = job_invocation.targeting.hosts.first
895
+ ::Host::Managed.any_instance.stubs(:valid?).returns(false)
896
+ composer = JobInvocationComposer.from_job_invocation(job_invocation, {})
897
+ targeting = composer.compose.job_invocation.targeting
898
+ targeting.save!
899
+ targeting.reload
900
+ assert targeting.valid?
901
+ assert_equal targeting.hosts.pluck(:id), [host.id]
902
+ end
903
+ end
904
+
905
+ describe '.for_feature' do
906
+ let(:feature) { FactoryBot.create(:remote_execution_feature, job_template: trying_job_template_1) }
907
+ let(:host) { FactoryBot.create(:host) }
908
+ let(:bookmark) { Bookmark.create!(:query => 'b', :name => 'bookmark', :public => true, :controller => 'hosts') }
909
+
910
+ context 'specifying hosts' do
911
+ it 'takes a bookmarked search' do
912
+ composer = JobInvocationComposer.for_feature(feature.label, bookmark, {})
913
+ assert_equal bookmark.id, composer.params['targeting']['bookmark_id']
914
+ end
915
+
916
+ it 'takes an array of host ids' do
917
+ composer = JobInvocationComposer.for_feature(feature.label, [host.id], {})
918
+ assert_match(/#{host.name}/, composer.params['targeting']['search_query'])
919
+ end
920
+
921
+ it 'takes a single host object' do
922
+ composer = JobInvocationComposer.for_feature(feature.label, host, {})
923
+ assert_match(/#{host.name}/, composer.params['targeting']['search_query'])
924
+ end
925
+
926
+ it 'takes an array of host FQDNs' do
927
+ composer = JobInvocationComposer.for_feature(feature.label, [host.fqdn], {})
928
+ assert_match(/#{host.name}/, composer.params['targeting']['search_query'])
929
+ end
930
+
931
+ it 'takes a search query string' do
932
+ composer = JobInvocationComposer.for_feature(feature.label, 'host.example.com', {})
933
+ assert_equal 'host.example.com', composer.search_query
934
+ end
935
+ end
936
+ end
937
+
938
+ describe '#resolve_job_category and #resolve job_templates' do
939
+ let(:setting_template) { as_admin { FactoryBot.create(:job_template, :name => 'trying setting', :job_category => 'fluff') } }
940
+ let(:other_template) { as_admin { FactoryBot.create(:job_template, :name => 'trying something', :job_category => 'fluff') } }
941
+ let(:second_template) { as_admin { FactoryBot.create(:job_template, :name => 'second template', :job_category => 'fluff') } }
942
+ let(:params) { { :host_ids => nil, :targeting => { :targeting_type => "static_query", :bookmark_id => nil }, :job_template_id => setting_template.id } }
943
+ let(:composer) { JobInvocationComposer.from_api_params(params) }
944
+
945
+ context 'with template in setting present' do
946
+ before do
947
+ Setting[:remote_execution_form_job_template] = setting_template.name
948
+ end
949
+
950
+ it 'should resolve category to the setting value' do
951
+ assert_equal setting_template.job_category, composer.resolve_job_category('foo')
952
+ end
953
+
954
+ it 'should resolve template to the setting value' do
955
+ assert_equal setting_template, composer.resolve_job_template([other_template, setting_template])
956
+ end
957
+
958
+ it 'should respect provider templates when resolving templates' do
959
+ assert_equal other_template, composer.resolve_job_template([other_template])
960
+ end
961
+ end
962
+
963
+ context 'with template in setting absent' do
964
+ it 'should resolve category to the default value' do
965
+ category = 'foo'
966
+ assert_equal category, composer.resolve_job_category(category)
967
+ end
968
+
969
+ it 'should resolve template to the first in category' do
970
+ assert_equal other_template, composer.resolve_job_template([other_template, second_template])
971
+ end
972
+ end
973
+ end
974
+ end