foreman_remote_execution 4.5.6 → 5.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (118) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ruby_ci.yml +7 -0
  3. data/.rubocop_todo.yml +1 -0
  4. data/app/controllers/api/v2/job_invocations_controller.rb +16 -1
  5. data/app/controllers/ui_job_wizard_controller.rb +16 -4
  6. data/app/graphql/mutations/job_invocations/create.rb +43 -0
  7. data/app/graphql/types/job_invocation.rb +16 -0
  8. data/app/graphql/types/job_invocation_input.rb +13 -0
  9. data/app/graphql/types/recurrence_input.rb +8 -0
  10. data/app/graphql/types/scheduling_input.rb +6 -0
  11. data/app/graphql/types/targeting_enum.rb +7 -0
  12. data/app/lib/actions/remote_execution/run_host_job.rb +6 -1
  13. data/app/lib/actions/remote_execution/run_hosts_job.rb +57 -3
  14. data/app/mailers/rex_job_mailer.rb +15 -0
  15. data/app/models/concerns/foreman_remote_execution/host_extensions.rb +8 -0
  16. data/app/models/job_invocation.rb +4 -0
  17. data/app/models/job_invocation_composer.rb +21 -13
  18. data/app/models/job_template.rb +1 -1
  19. data/app/models/remote_execution_provider.rb +17 -2
  20. data/app/models/rex_mail_notification.rb +13 -0
  21. data/app/models/targeting.rb +2 -2
  22. data/app/services/ui_notifications/remote_execution_jobs/base_job_finish.rb +2 -1
  23. data/app/views/dashboard/_latest-jobs.html.erb +21 -0
  24. data/app/views/job_invocations/refresh.js.erb +1 -0
  25. data/app/views/rex_job_mailer/job_finished.html.erb +24 -0
  26. data/app/views/rex_job_mailer/job_finished.text.erb +9 -0
  27. data/app/views/template_invocations/show.html.erb +2 -1
  28. data/config/routes.rb +1 -0
  29. data/db/migrate/20210816100932_rex_setting_category_to_dsl.rb +5 -0
  30. data/db/seeds.d/50-notification_blueprints.rb +14 -0
  31. data/db/seeds.d/95-mail_notifications.rb +24 -0
  32. data/foreman_remote_execution.gemspec +2 -4
  33. data/lib/foreman_remote_execution/engine.rb +114 -6
  34. data/lib/foreman_remote_execution/version.rb +1 -1
  35. data/package.json +6 -6
  36. data/test/functional/api/v2/job_invocations_controller_test.rb +20 -0
  37. data/test/functional/cockpit_controller_test.rb +0 -1
  38. data/test/graphql/mutations/job_invocations/create.rb +58 -0
  39. data/test/graphql/queries/job_invocation_query_test.rb +31 -0
  40. data/test/graphql/queries/job_invocations_query_test.rb +35 -0
  41. data/test/helpers/remote_execution_helper_test.rb +0 -1
  42. data/test/unit/actions/run_host_job_test.rb +21 -0
  43. data/test/unit/actions/run_hosts_job_test.rb +99 -4
  44. data/test/unit/concerns/host_extensions_test.rb +40 -7
  45. data/test/unit/input_template_renderer_test.rb +1 -89
  46. data/test/unit/job_invocation_composer_test.rb +4 -17
  47. data/test/unit/job_invocation_report_template_test.rb +16 -13
  48. data/test/unit/job_template_effective_user_test.rb +0 -4
  49. data/test/unit/remote_execution_provider_test.rb +34 -4
  50. data/test/unit/targeting_test.rb +68 -1
  51. data/webpack/JobWizard/JobWizard.js +106 -15
  52. data/webpack/JobWizard/JobWizard.scss +73 -39
  53. data/webpack/JobWizard/JobWizardConstants.js +36 -0
  54. data/webpack/JobWizard/JobWizardSelectors.js +32 -0
  55. data/webpack/JobWizard/__tests__/fixtures.js +81 -6
  56. data/webpack/JobWizard/__tests__/integration.test.js +26 -15
  57. data/webpack/JobWizard/__tests__/validation.test.js +141 -0
  58. data/webpack/JobWizard/autofill.js +38 -0
  59. data/webpack/JobWizard/index.js +7 -0
  60. data/webpack/JobWizard/steps/AdvancedFields/AdvancedFields.js +7 -4
  61. data/webpack/JobWizard/steps/AdvancedFields/DescriptionField.js +32 -9
  62. data/webpack/JobWizard/steps/AdvancedFields/__tests__/AdvancedFields.test.js +216 -12
  63. data/webpack/JobWizard/steps/AdvancedFields/__tests__/__snapshots__/AdvancedFields.test.js.snap +82 -0
  64. data/webpack/JobWizard/steps/CategoryAndTemplate/CategoryAndTemplate.js +1 -0
  65. data/webpack/JobWizard/steps/CategoryAndTemplate/index.js +3 -2
  66. data/webpack/JobWizard/steps/HostsAndInputs/HostPreviewModal.js +62 -0
  67. data/webpack/JobWizard/steps/HostsAndInputs/HostSearch.js +54 -0
  68. data/webpack/JobWizard/steps/HostsAndInputs/SelectAPI.js +33 -0
  69. data/webpack/JobWizard/steps/HostsAndInputs/SelectGQL.js +52 -0
  70. data/webpack/JobWizard/steps/HostsAndInputs/SelectedChips.js +82 -7
  71. data/webpack/JobWizard/steps/HostsAndInputs/__tests__/HostsAndInputs.test.js +151 -0
  72. data/webpack/JobWizard/steps/HostsAndInputs/__tests__/TemplateInputs.test.js +7 -4
  73. data/webpack/JobWizard/steps/HostsAndInputs/buildHostQuery.js +18 -0
  74. data/webpack/JobWizard/steps/HostsAndInputs/hostgroups.gql +8 -0
  75. data/webpack/JobWizard/steps/HostsAndInputs/hosts.gql +8 -0
  76. data/webpack/JobWizard/steps/HostsAndInputs/index.js +182 -34
  77. data/webpack/JobWizard/steps/ReviewDetails/index.js +193 -0
  78. data/webpack/JobWizard/steps/Schedule/PurposeField.js +31 -0
  79. data/webpack/JobWizard/steps/Schedule/QueryType.js +46 -43
  80. data/webpack/JobWizard/steps/Schedule/RepeatCron.js +53 -0
  81. data/webpack/JobWizard/steps/Schedule/RepeatDaily.js +37 -0
  82. data/webpack/JobWizard/steps/Schedule/RepeatHour.js +54 -0
  83. data/webpack/JobWizard/steps/Schedule/RepeatMonth.js +46 -0
  84. data/webpack/JobWizard/steps/Schedule/RepeatOn.js +95 -31
  85. data/webpack/JobWizard/steps/Schedule/RepeatWeek.js +70 -0
  86. data/webpack/JobWizard/steps/Schedule/ScheduleType.js +24 -21
  87. data/webpack/JobWizard/steps/Schedule/StartEndDates.js +78 -23
  88. data/webpack/JobWizard/steps/Schedule/__tests__/Schedule.test.js +402 -0
  89. data/webpack/JobWizard/steps/Schedule/__tests__/StartEndDates.test.js +20 -10
  90. data/webpack/JobWizard/steps/Schedule/index.js +153 -19
  91. data/webpack/JobWizard/steps/form/DateTimePicker.js +126 -0
  92. data/webpack/JobWizard/steps/form/FormHelpers.js +4 -0
  93. data/webpack/JobWizard/steps/form/Formatter.js +39 -8
  94. data/webpack/JobWizard/steps/form/NumberInput.js +3 -2
  95. data/webpack/JobWizard/steps/form/ResourceSelect.js +29 -0
  96. data/webpack/JobWizard/steps/form/SearchSelect.js +121 -0
  97. data/webpack/JobWizard/steps/form/SelectField.js +14 -3
  98. data/webpack/JobWizard/steps/form/__tests__/SelectSearch.test.js +33 -0
  99. data/webpack/JobWizard/submit.js +120 -0
  100. data/webpack/JobWizard/validation.js +53 -0
  101. data/webpack/__mocks__/foremanReact/Root/Context/ForemanContext/index.js +2 -0
  102. data/webpack/__mocks__/foremanReact/common/I18n.js +2 -0
  103. data/webpack/__mocks__/foremanReact/components/AutoComplete/AutoCompleteActions.js +1 -0
  104. data/webpack/__mocks__/foremanReact/components/AutoComplete/AutoCompleteConstants.js +1 -0
  105. data/webpack/__mocks__/foremanReact/routes/RouterSelector.js +1 -0
  106. data/webpack/helpers.js +1 -0
  107. data/webpack/react_app/components/RecentJobsCard/JobStatusIcon.js +43 -0
  108. data/webpack/react_app/components/RecentJobsCard/RecentJobsCard.js +73 -66
  109. data/webpack/react_app/components/RecentJobsCard/RecentJobsTable.js +98 -0
  110. data/webpack/react_app/components/RecentJobsCard/constants.js +11 -0
  111. data/webpack/react_app/components/RecentJobsCard/styles.scss +11 -0
  112. data/webpack/react_app/extend/fillRecentJobsCard.js +1 -1
  113. metadata +56 -23
  114. data/app/models/setting/remote_execution.rb +0 -88
  115. data/webpack/JobWizard/steps/AdvancedFields/__tests__/DescriptionField.test.js +0 -23
  116. data/webpack/JobWizard/steps/HostsAndInputs/__tests__/SelectedChips.test.js +0 -37
  117. data/webpack/JobWizard/steps/form/__tests__/Formatter.test.js.example +0 -76
  118. data/webpack/react_app/components/RecentJobsCard/styles.css +0 -15
@@ -1,9 +1,6 @@
1
1
  require 'test_plugin_helper'
2
2
 
3
3
  class ForemanRemoteExecutionHostExtensionsTest < ActiveSupport::TestCase
4
- before do
5
- Setting::RemoteExecution.load_defaults
6
- end
7
4
  let(:provider) { 'SSH' }
8
5
 
9
6
  before { User.current = FactoryBot.build(:user, :admin) }
@@ -131,23 +128,23 @@ class ForemanRemoteExecutionHostExtensionsTest < ActiveSupport::TestCase
131
128
  end
132
129
 
133
130
  context 'fallback strategy' do
134
- let(:host) { FactoryBot.build(:host, :with_puppet) }
131
+ let(:host) { FactoryBot.build(:host, :with_tftp_subnet) }
135
132
 
136
133
  context 'enabled' do
137
134
  before do
138
135
  Setting[:remote_execution_fallback_proxy] = true
139
- host.puppet_proxy.features << FactoryBot.create(:feature, :ssh)
136
+ host.subnet.tftp.features << FactoryBot.create(:feature, :ssh)
140
137
  end
141
138
 
142
139
  it 'returns a fallback proxy' do
143
- host.remote_execution_proxies(provider)[:fallback].must_include host.puppet_proxy
140
+ host.remote_execution_proxies(provider)[:fallback].must_include host.subnet.tftp
144
141
  end
145
142
  end
146
143
 
147
144
  context 'disabled' do
148
145
  before do
149
146
  Setting[:remote_execution_fallback_proxy] = false
150
- host.puppet_proxy.features << FactoryBot.create(:feature, :ssh)
147
+ host.subnet.tftp.features << FactoryBot.create(:feature, :ssh)
151
148
  end
152
149
 
153
150
  it 'returns no proxy' do
@@ -182,4 +179,40 @@ class ForemanRemoteExecutionHostExtensionsTest < ActiveSupport::TestCase
182
179
  end
183
180
  end
184
181
  end
182
+
183
+ describe '#execution_scope' do
184
+ let(:host) { FactoryBot.create(:host) }
185
+ let(:infra_host) { FactoryBot.create(:host, :with_infrastructure_facet) }
186
+
187
+ before do
188
+ host
189
+ infra_host
190
+ end
191
+
192
+ context 'without infrastructure host permission' do
193
+ it 'omits the infrastructure host' do
194
+ setup_user('view', 'hosts')
195
+
196
+ hosts = ::Host::Managed.execution_scope
197
+ hosts.must_include host
198
+ hosts.wont_include infra_host
199
+ end
200
+ end
201
+
202
+ context 'with infrastructure host permission' do
203
+ it 'finds the host as admin' do
204
+ assert User.current.admin?
205
+ hosts = ::Host::Managed.execution_scope
206
+ hosts.must_include host
207
+ hosts.must_include infra_host
208
+ end
209
+
210
+ it 'finds the host as user with needed permissions' do
211
+ setup_user('execute_jobs_on', 'infrastructure_hosts')
212
+ hosts = ::Host::Managed.execution_scope
213
+ hosts.must_include host
214
+ hosts.must_include infra_host
215
+ end
216
+ end
217
+ end
185
218
  end
@@ -446,8 +446,7 @@ class InputTemplateRendererTest < ActiveSupport::TestCase
446
446
  before { User.current = FactoryBot.build(:user, :admin) }
447
447
  after { User.current = nil }
448
448
 
449
- let(:environment) { FactoryBot.create(:environment) }
450
- before { renderer.host = FactoryBot.create(:host, :environment => environment) }
449
+ before { renderer.host = FactoryBot.create(:host) }
451
450
 
452
451
  describe 'rendering' do
453
452
  it 'can\'t render the content without host since we don\'t have variable value in classification' do
@@ -497,91 +496,4 @@ class InputTemplateRendererTest < ActiveSupport::TestCase
497
496
  end
498
497
  end
499
498
  end
500
-
501
- context 'renderer for template with puppet parameter input used' do
502
- let(:template) { FactoryBot.build(:job_template, :template => 'echo "This is WebServer with nginx <%= input("nginx_version") -%>" > /etc/motd') }
503
- let(:renderer) { InputTemplateRenderer.new(template) }
504
-
505
- context 'with matching input defined' do
506
- before do
507
- renderer.template.template_inputs<< FactoryBot.build(:template_input,
508
- :name => 'nginx_version',
509
- :input_type => 'puppet_parameter',
510
- :puppet_parameter_name => 'version',
511
- :puppet_class_name => 'nginx')
512
- end
513
- let(:result) { renderer.render }
514
-
515
- describe 'rendering' do
516
- it 'can\'t render the content without host since we don\'t have host so no classification' do
517
- assert_not result
518
- end
519
-
520
- it 'registers an error' do
521
- result # let is lazy
522
- _(renderer.error_message).wont_be_nil
523
- _(renderer.error_message).wont_be_empty
524
- end
525
-
526
- context 'with host specified' do
527
- let(:environment) { FactoryBot.create(:environment) }
528
- before { renderer.host = FactoryBot.create(:host, :environment => environment) }
529
-
530
- describe 'rendering' do
531
- it 'can\'t render the content without host since we don\'t have puppet parameter in classification' do
532
- assert_not result
533
- end
534
-
535
- it 'registers an error' do
536
- result # let is lazy
537
- _(renderer.error_message).wont_be_nil
538
- _(renderer.error_message).wont_be_empty
539
- end
540
- end
541
-
542
- describe 'preview' do
543
- it 'should render preview' do
544
- _(renderer.preview).must_equal 'echo "This is WebServer with nginx $PUPPET_PARAMETER_INPUT[nginx_version]" > /etc/motd'
545
- end
546
- end
547
-
548
- context 'with existing puppet parameter with matching override' do
549
- let(:puppet_class) do
550
- puppetclass = FactoryBot.create(:puppetclass, :environments => [environment], :name => 'nginx')
551
- puppetclass.update_attribute(:hosts, [renderer.host])
552
- puppetclass
553
- end
554
- let(:lookup_key) do
555
- FactoryBot.create(:puppetclass_lookup_key, :as_smart_class_param,
556
- :key => 'version',
557
- :puppetclass => puppet_class,
558
- :path => 'fqdn',
559
- :override => true,
560
- :overrides => {"fqdn=#{renderer.host.fqdn}" => '1.4.7'})
561
- end
562
-
563
- describe 'rendering' do
564
- it 'renders the value from puppet parameter' do
565
- lookup_key
566
- _(result).must_equal 'echo "This is WebServer with nginx 1.4.7" > /etc/motd'
567
- end
568
- end
569
-
570
- describe 'preview' do
571
- it 'should render preview' do
572
- lookup_key
573
- _(renderer.preview).must_equal 'echo "This is WebServer with nginx 1.4.7" > /etc/motd'
574
- end
575
- end
576
- end
577
- end
578
-
579
- describe 'preview' do
580
- it 'should render preview' do
581
- _(renderer.preview).must_equal 'echo "This is WebServer with nginx $PUPPET_PARAMETER_INPUT[nginx_version]" > /etc/motd'
582
- end
583
- end
584
- end
585
- end
586
- end
587
499
  end
@@ -270,10 +270,6 @@ class JobInvocationComposerTest < ActiveSupport::TestCase
270
270
  composer.pattern_template_invocations.first
271
271
  end
272
272
 
273
- before do
274
- Setting::RemoteExecution.load_defaults
275
- end
276
-
277
273
  context 'when overridable and provided' do
278
274
  let(:overridable) { true }
279
275
  let(:invocation_effective_user) { 'invocation user' }
@@ -350,12 +346,6 @@ class JobInvocationComposerTest < ActiveSupport::TestCase
350
346
  end
351
347
 
352
348
  describe '#available_bookmarks' do
353
- it 'obeys authorization' do
354
- composer
355
- Bookmark.expects(:authorized).with(:view_bookmarks).returns(Bookmark.where({}))
356
- composer.available_bookmarks
357
- end
358
-
359
349
  context 'there are hostgroups and hosts bookmark' do
360
350
  let(:hostgroups) { Bookmark.create(:name => 'hostgroups', :query => 'name = x', :controller => 'hostgroups') }
361
351
  let(:hosts) { Bookmark.create(:name => 'hosts', :query => 'name = x', :controller => 'hosts') }
@@ -376,8 +366,10 @@ class JobInvocationComposerTest < ActiveSupport::TestCase
376
366
  let(:host) { FactoryBot.create(:host) }
377
367
 
378
368
  it 'obeys authorization' do
369
+ fake_scope = mock
379
370
  composer.stubs(:displayed_search_query => "name = #{host.name}")
380
- Host.expects(:authorized).with(:view_hosts, Host).returns(Host.where({}))
371
+ Host.expects(:execution_scope).returns(fake_scope)
372
+ fake_scope.expects(:authorized).with(:view_hosts, Host).returns(Host.where({}))
381
373
  composer.targeted_hosts_count
382
374
  end
383
375
 
@@ -931,12 +923,7 @@ class JobInvocationComposerTest < ActiveSupport::TestCase
931
923
 
932
924
  context 'with template in setting present' do
933
925
  before do
934
- FactoryBot.create(
935
- :setting,
936
- :name => 'remote_execution_form_job_template',
937
- :category => 'Setting::RemoteExecution',
938
- :value => setting_template.name
939
- )
926
+ Setting[:remote_execution_form_job_template] = setting_template.name
940
927
  end
941
928
 
942
929
  it 'should resolve category to the setting value' do
@@ -19,7 +19,7 @@ class JobReportTemplateTest < ActiveSupport::TestCase
19
19
  it 'in settings includes only report templates with job_id input' do
20
20
  FactoryBot.create(:report_template, name: 'Template 1')
21
21
  job_invocation_template
22
- templates = Setting::RemoteExecution.job_invocation_report_templates_select
22
+ templates = ForemanRemoteExecution.job_invocation_report_templates_select
23
23
 
24
24
  assert_include templates, 'Job Invocation Report Template'
25
25
  end
@@ -28,15 +28,16 @@ class JobReportTemplateTest < ActiveSupport::TestCase
28
28
  describe 'task reporting' do
29
29
  let(:fake_outputs) do
30
30
  [
31
- { 'output_type' => 'stderr', 'output' => "error", 'timestamp' => Time.new(2020, 12, 1, 0, 0, 0).utc },
32
- { 'output_type' => 'stdout', 'output' => "output", 'timestamp' => Time.new(2020, 12, 1, 0, 0, 0).utc },
33
- { 'output_type' => 'stdebug', 'output' => "debug", 'timestamp' => Time.new(2020, 12, 1, 0, 0, 0).utc },
31
+ { 'output_type' => 'stderr', 'output' => "error" },
32
+ { 'output_type' => 'stdout', 'output' => "output" },
33
+ { 'output_type' => 'debug', 'output' => "debug" },
34
34
  ]
35
35
  end
36
- let(:fake_task) { FakeTask.new(result: 'success', action_continuous_output: fake_outputs) }
36
+ let(:fake_task) { FakeTask.new(result: 'success', action_continuous_output: fake_outputs, :ended_at => Time.new(2020, 12, 1, 0, 0, 0).utc) }
37
+ let(:job_invocation) { FactoryBot.create(:job_invocation, :with_task) }
38
+ let(:host) { job_invocation.template_invocations.first.host }
37
39
 
38
40
  it 'should render task outputs' do
39
- job_invocation = FactoryBot.create(:job_invocation, :with_task)
40
41
  JobInvocation.any_instance.expects(:sub_task_for_host).returns(fake_task)
41
42
 
42
43
  input = job_invocation_template.template_inputs.first
@@ -44,13 +45,15 @@ class JobReportTemplateTest < ActiveSupport::TestCase
44
45
  result = ReportComposer.new(composer_params).render
45
46
 
46
47
  # parsing the CSV result
47
- CSV.parse(result.strip, headers: true).each_with_index do |row, i|
48
- row_hash = row.to_h
49
- assert_equal 'success', row_hash['result']
50
- assert_equal fake_outputs[i]['output_type'], row_hash['type']
51
- assert_equal fake_outputs[i]['output'], row_hash['message']
52
- assert_kind_of Time, Time.zone.parse(row_hash['time']), 'Parsing of time column failed'
53
- end
48
+ rows = CSV.parse(result.strip, headers: true)
49
+ assert_equal 1, rows.count
50
+ row = rows.first
51
+ assert_equal host.name, row['Host']
52
+ assert_equal 'success', row['Result']
53
+ assert_equal 'error', row['stderr']
54
+ assert_equal 'output', row['stdout']
55
+ assert_equal 'debug', row['debug']
56
+ assert_kind_of Time, Time.zone.parse(row['Finished']), 'Parsing of time column failed'
54
57
  end
55
58
  end
56
59
  end
@@ -4,10 +4,6 @@ class JobTemplateEffectiveUserTest < ActiveSupport::TestCase
4
4
  let(:job_template) { FactoryBot.build(:job_template, :job_category => '') }
5
5
  let(:effective_user) { job_template.effective_user }
6
6
 
7
- before do
8
- Setting::RemoteExecution.load_defaults
9
- end
10
-
11
7
  describe 'by default' do
12
8
  it 'is overridable' do
13
9
  assert effective_user.overridable?
@@ -66,10 +66,6 @@ class RemoteExecutionProviderTest < ActiveSupport::TestCase
66
66
  before { User.current = FactoryBot.build(:user, :admin) }
67
67
  after { User.current = nil }
68
68
 
69
- before do
70
- Setting::RemoteExecution.load_defaults
71
- end
72
-
73
69
  let(:job_invocation) { FactoryBot.create(:job_invocation, :with_template) }
74
70
  let(:template_invocation) { job_invocation.pattern_template_invocations.first }
75
71
  let(:host) { FactoryBot.create(:host) }
@@ -212,6 +208,40 @@ class RemoteExecutionProviderTest < ActiveSupport::TestCase
212
208
  host.interfaces.each(&:save)
213
209
  host.reload
214
210
  SSHExecutionProvider.find_ip_or_hostname(host).must_equal execution_interface.ip
211
+
212
+ # there is an execution interface with both IPv6 and IPv4: IPv4 is being preferred over IPv6 by default
213
+ execution_interface = FactoryBot.build(:nic_managed,
214
+ flags.merge(:execution => true, :ip => '10.0.0.4', :ip6 => 'fd00::4'))
215
+ host.interfaces = [execution_interface]
216
+ host.interfaces.each(&:save)
217
+ host.reload
218
+ SSHExecutionProvider.find_ip_or_hostname(host).must_equal execution_interface.ip
219
+ end
220
+
221
+ it 'gets ipv6 from flagged interfaces with IPv6 preference' do
222
+ host.host_params['remote_execution_connect_by_ip_prefer_ipv6'] = true
223
+ host.host_params['remote_execution_connect_by_ip'] = true
224
+
225
+ # there is an execution interface with both IPv6 and IPv4: IPv6 is being preferred over IPv4 by host parameter configuration
226
+ execution_interface = FactoryBot.build(:nic_managed,
227
+ flags.merge(:execution => true, :ip => '10.0.0.4', :ip6 => 'fd00::4'))
228
+ host.interfaces = [execution_interface]
229
+ host.interfaces.each(&:save)
230
+ host.reload
231
+ SSHExecutionProvider.find_ip_or_hostname(host).must_equal execution_interface.ip6
232
+ end
233
+
234
+ it 'gets ipv6 from flagged interfaces with IPv4 preference but without IPv4 address' do
235
+ host.host_params['remote_execution_connect_by_ip_prefer_ipv6'] = false
236
+ host.host_params['remote_execution_connect_by_ip'] = true
237
+
238
+ # there is an execution interface with both IPv6 and IPv4: IPv6 is being preferred over IPv4 by host parameter configuration
239
+ execution_interface = FactoryBot.build(:nic_managed,
240
+ flags.merge(:execution => true, :ip => nil, :ip6 => 'fd00::4'))
241
+ host.interfaces = [execution_interface]
242
+ host.interfaces.each(&:save)
243
+ host.reload
244
+ SSHExecutionProvider.find_ip_or_hostname(host).must_equal execution_interface.ip6
215
245
  end
216
246
  end
217
247
  end
@@ -77,24 +77,73 @@ class TargetingTest < ActiveSupport::TestCase
77
77
  it { _(targeting.reload.hosts).must_be_empty }
78
78
  end
79
79
 
80
+ describe '#resolve_hosts!' do
81
+ let(:second_host) { FactoryBot.create(:host) }
82
+ let(:infra_host) { FactoryBot.create(:host, :with_infrastructure_facet) }
83
+ let(:targeting) { FactoryBot.build(:targeting) }
84
+
85
+ before do
86
+ host
87
+ second_host
88
+ infra_host
89
+ end
90
+
91
+ context 'with infrastructure host permission' do
92
+ before do
93
+ setup_user('view', 'hosts')
94
+ setup_user('execute_jobs_on', 'infrastructure_hosts')
95
+ end
96
+
97
+ it 'resolves all hosts' do
98
+ hosts = [host, second_host, infra_host]
99
+ targeting.search_query = "name ^ (#{hosts.map(&:name).join(',')})"
100
+ targeting.user = User.current
101
+ targeting.resolve_hosts!
102
+
103
+ targeting.hosts.must_include host
104
+ targeting.hosts.must_include second_host
105
+ targeting.hosts.must_include infra_host
106
+ end
107
+ end
108
+
109
+ context 'without infrastructure host permission' do
110
+ before { setup_user('view', 'hosts') }
111
+
112
+ it 'ignores infrastructure hosts' do
113
+ hosts = [host, second_host, infra_host]
114
+ targeting.search_query = "name ^ (#{hosts.map(&:name).join(',')})"
115
+ targeting.user = User.current
116
+ targeting.resolve_hosts!
117
+
118
+ targeting.hosts.must_include host
119
+ targeting.hosts.must_include second_host
120
+ targeting.hosts.wont_include infra_host
121
+ end
122
+ end
123
+ end
124
+
80
125
  describe '#build_query_from_hosts(ids)' do
81
126
  let(:second_host) { FactoryBot.create(:host) }
127
+ let(:infra_host) { FactoryBot.create(:host, :with_infrastructure_facet) }
82
128
 
83
129
  before do
84
130
  host
85
131
  second_host
132
+ infra_host
86
133
  end
87
134
 
88
135
  context 'for two hosts' do
89
- let(:query) { Targeting.build_query_from_hosts([ host.id, second_host.id ]) }
136
+ let(:query) { Targeting.build_query_from_hosts([ host.id, second_host.id, infra_host.id ]) }
90
137
 
91
138
  it 'builds query using host names joining inside ^' do
92
139
  _(query).must_include host.name
93
140
  _(query).must_include second_host.name
141
+ _(query).must_include infra_host.name
94
142
  _(query).must_include 'name ^'
95
143
 
96
144
  Host.search_for(query).must_include host
97
145
  Host.search_for(query).must_include second_host
146
+ Host.search_for(query).must_include infra_host
98
147
  end
99
148
  end
100
149
 
@@ -105,6 +154,7 @@ class TargetingTest < ActiveSupport::TestCase
105
154
  _(query).must_equal "name ^ (#{host.name})"
106
155
  Host.search_for(query).must_include host
107
156
  Host.search_for(query).wont_include second_host
157
+ Host.search_for(query).wont_include infra_host
108
158
  end
109
159
  end
110
160
 
@@ -114,6 +164,23 @@ class TargetingTest < ActiveSupport::TestCase
114
164
  it 'builds query to find all hosts' do
115
165
  Host.search_for(query).must_include host
116
166
  Host.search_for(query).must_include second_host
167
+ Host.search_for(query).must_include infra_host
168
+ end
169
+ end
170
+
171
+ context 'without infrastructure host permission' do
172
+ before { User.current = nil }
173
+
174
+ it 'ignores the infrastructure host' do
175
+ query = Targeting.build_query_from_hosts([host.id, second_host.id, infra_host.id])
176
+ _(query).must_include host.name
177
+ _(query).must_include second_host.name
178
+ _(query).wont_include infra_host.name
179
+ _(query).must_include 'name ^'
180
+
181
+ Host.search_for(query).must_include host
182
+ Host.search_for(query).must_include second_host
183
+ Host.search_for(query).wont_include infra_host
117
184
  end
118
185
  end
119
186
  end
@@ -4,20 +4,43 @@ import { useDispatch, useSelector } from 'react-redux';
4
4
  import { Wizard } from '@patternfly/react-core';
5
5
  import { get } from 'foremanReact/redux/API';
6
6
  import history from 'foremanReact/history';
7
+
8
+ import {
9
+ useForemanOrganization,
10
+ useForemanLocation,
11
+ } from 'foremanReact/Root/Context/ForemanContext';
7
12
  import CategoryAndTemplate from './steps/CategoryAndTemplate/';
8
13
  import { AdvancedFields } from './steps/AdvancedFields/AdvancedFields';
9
- import { JOB_TEMPLATE, WIZARD_TITLES } from './JobWizardConstants';
10
- import { selectTemplateError } from './JobWizardSelectors';
14
+ import {
15
+ JOB_TEMPLATE,
16
+ WIZARD_TITLES,
17
+ initialScheduleState,
18
+ } from './JobWizardConstants';
19
+ import {
20
+ selectTemplateError,
21
+ selectJobTemplate,
22
+ selectIsSubmitting,
23
+ } from './JobWizardSelectors';
11
24
  import Schedule from './steps/Schedule/';
12
25
  import HostsAndInputs from './steps/HostsAndInputs/';
26
+ import ReviewDetails from './steps/ReviewDetails/';
27
+ import { useValidation } from './validation';
28
+ import { useAutoFill } from './autofill';
29
+ import { submit } from './submit';
13
30
  import './JobWizard.scss';
14
31
 
15
32
  export const JobWizard = () => {
16
33
  const [jobTemplateID, setJobTemplateID] = useState(null);
17
34
  const [category, setCategory] = useState('');
18
- const [advancedValues, setAdvancedValues] = useState({});
35
+ const [advancedValues, setAdvancedValues] = useState({ templateValues: {} });
19
36
  const [templateValues, setTemplateValues] = useState({}); // TODO use templateValues in advanced fields - description https://github.com/theforeman/foreman_remote_execution/pull/605
20
- const [selectedHosts, setSelectedHosts] = useState(['host1', 'host2']);
37
+ const [scheduleValue, setScheduleValue] = useState(initialScheduleState);
38
+ const [selectedTargets, setSelectedTargets] = useState({
39
+ hosts: [],
40
+ hostCollections: [],
41
+ hostGroups: [],
42
+ });
43
+ const [hostsSearchQuery, setHostsSearchQuery] = useState('');
21
44
  const dispatch = useDispatch();
22
45
 
23
46
  const setDefaults = useCallback(
@@ -26,7 +49,7 @@ export const JobWizard = () => {
26
49
  template_inputs,
27
50
  advanced_template_inputs,
28
51
  effective_user,
29
- job_template: { execution_timeout_interval, description_format },
52
+ job_template: { name, execution_timeout_interval, description_format },
30
53
  },
31
54
  }) => {
32
55
  const advancedTemplateValues = {};
@@ -47,12 +70,21 @@ export const JobWizard = () => {
47
70
  advancedTemplateValues[input.name] = input?.default || '';
48
71
  });
49
72
  }
73
+ const generateDefaultDescription = () => {
74
+ if (description_format) return description_format;
75
+ const allInputs = [...advancedInputs, ...inputs];
76
+ if (!allInputs.length) return name;
77
+ const inputsString = allInputs
78
+ .map(({ name: inputname }) => `${inputname}="%{${inputname}}"`)
79
+ .join(' ');
80
+ return `${name} with inputs ${inputsString}`;
81
+ };
50
82
  return {
51
83
  ...currentAdvancedValues,
52
84
  effectiveUserValue: effective_user?.value || '',
53
85
  timeoutToKill: execution_timeout_interval || '',
54
86
  templateValues: advancedTemplateValues,
55
- description: description_format || '',
87
+ description: generateDefaultDescription() || '',
56
88
  isRandomizedOrdering: false,
57
89
  };
58
90
  });
@@ -71,8 +103,20 @@ export const JobWizard = () => {
71
103
  }
72
104
  }, [jobTemplateID, setDefaults, dispatch]);
73
105
 
106
+ const [valid, setValid] = useValidation({
107
+ advancedValues,
108
+ templateValues,
109
+ });
110
+ useAutoFill({
111
+ setSelectedTargets,
112
+ setHostsSearchQuery,
113
+ });
74
114
  const templateError = !!useSelector(selectTemplateError);
75
- const isTemplate = !templateError && !!jobTemplateID;
115
+ const templateResponse = useSelector(selectJobTemplate);
116
+ const isSubmitting = useSelector(selectIsSubmitting);
117
+ const isTemplate =
118
+ !templateError && !!jobTemplateID && templateResponse.job_template;
119
+
76
120
  const steps = [
77
121
  {
78
122
  name: WIZARD_TITLES.categoryAndTemplate,
@@ -84,6 +128,7 @@ export const JobWizard = () => {
84
128
  setCategory={setCategory}
85
129
  />
86
130
  ),
131
+ enableNext: isTemplate,
87
132
  },
88
133
  {
89
134
  name: WIZARD_TITLES.hostsAndInputs,
@@ -91,11 +136,14 @@ export const JobWizard = () => {
91
136
  <HostsAndInputs
92
137
  templateValues={templateValues}
93
138
  setTemplateValues={setTemplateValues}
94
- selectedHosts={selectedHosts}
95
- setSelectedHosts={setSelectedHosts}
139
+ selected={selectedTargets}
140
+ setSelected={setSelectedTargets}
141
+ hostsSearchQuery={hostsSearchQuery}
142
+ setHostsSearchQuery={setHostsSearchQuery}
96
143
  />
97
144
  ),
98
145
  canJumpTo: isTemplate,
146
+ enableNext: isTemplate && valid.hostsAndInputs,
99
147
  },
100
148
  {
101
149
  name: WIZARD_TITLES.advanced,
@@ -108,23 +156,53 @@ export const JobWizard = () => {
108
156
  ...newValues,
109
157
  }));
110
158
  }}
111
- jobTemplateID={jobTemplateID}
159
+ templateValues={templateValues}
112
160
  />
113
161
  ),
114
- canJumpTo: isTemplate,
162
+ canJumpTo: isTemplate && valid.hostsAndInputs,
163
+ enableNext: isTemplate && valid.hostsAndInputs && valid.advanced,
115
164
  },
116
165
  {
117
166
  name: WIZARD_TITLES.schedule,
118
- component: <Schedule />,
119
- canJumpTo: isTemplate,
167
+ component: (
168
+ <Schedule
169
+ scheduleValue={scheduleValue}
170
+ setScheduleValue={setScheduleValue}
171
+ setValid={newValue => {
172
+ setValid(currentValid => ({ ...currentValid, schedule: newValue }));
173
+ }}
174
+ />
175
+ ),
176
+ canJumpTo: isTemplate && valid.hostsAndInputs && valid.advanced,
177
+ enableNext:
178
+ isTemplate && valid.hostsAndInputs && valid.advanced && valid.schedule,
120
179
  },
121
180
  {
122
181
  name: WIZARD_TITLES.review,
123
- component: <p>Review Details</p>,
182
+ component: (
183
+ <ReviewDetails
184
+ jobCategory={category}
185
+ jobTemplateID={jobTemplateID}
186
+ advancedValues={advancedValues}
187
+ scheduleValue={scheduleValue}
188
+ templateValues={templateValues}
189
+ selectedTargets={selectedTargets}
190
+ hostsSearchQuery={hostsSearchQuery}
191
+ />
192
+ ),
124
193
  nextButtonText: 'Run',
125
- canJumpTo: isTemplate,
194
+ canJumpTo:
195
+ isTemplate && valid.hostsAndInputs && valid.advanced && valid.schedule,
196
+ enableNext:
197
+ isTemplate &&
198
+ valid.hostsAndInputs &&
199
+ valid.advanced &&
200
+ valid.schedule &&
201
+ !isSubmitting,
126
202
  },
127
203
  ];
204
+ const location = useForemanLocation();
205
+ const organization = useForemanOrganization();
128
206
  return (
129
207
  <Wizard
130
208
  onClose={() => history.goBack()}
@@ -132,6 +210,19 @@ export const JobWizard = () => {
132
210
  steps={steps}
133
211
  height="100%"
134
212
  className="job-wizard"
213
+ onSave={() => {
214
+ submit({
215
+ jobTemplateID,
216
+ templateValues,
217
+ advancedValues,
218
+ scheduleValue,
219
+ dispatch,
220
+ selectedTargets,
221
+ hostsSearchQuery,
222
+ location,
223
+ organization,
224
+ });
225
+ }}
135
226
  />
136
227
  );
137
228
  };