foreman_remote_execution 4.5.6 → 5.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (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
  };