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.
- checksums.yaml +4 -4
- data/.github/workflows/ruby_ci.yml +7 -0
- data/.rubocop_todo.yml +1 -0
- data/app/controllers/api/v2/job_invocations_controller.rb +16 -1
- data/app/controllers/ui_job_wizard_controller.rb +16 -4
- data/app/graphql/mutations/job_invocations/create.rb +43 -0
- data/app/graphql/types/job_invocation.rb +16 -0
- data/app/graphql/types/job_invocation_input.rb +13 -0
- data/app/graphql/types/recurrence_input.rb +8 -0
- data/app/graphql/types/scheduling_input.rb +6 -0
- data/app/graphql/types/targeting_enum.rb +7 -0
- data/app/lib/actions/remote_execution/run_host_job.rb +6 -1
- data/app/lib/actions/remote_execution/run_hosts_job.rb +57 -3
- data/app/mailers/rex_job_mailer.rb +15 -0
- data/app/models/concerns/foreman_remote_execution/host_extensions.rb +8 -0
- data/app/models/job_invocation.rb +4 -0
- data/app/models/job_invocation_composer.rb +21 -13
- data/app/models/job_template.rb +1 -1
- data/app/models/remote_execution_provider.rb +17 -2
- data/app/models/rex_mail_notification.rb +13 -0
- data/app/models/targeting.rb +2 -2
- data/app/services/ui_notifications/remote_execution_jobs/base_job_finish.rb +2 -1
- data/app/views/dashboard/_latest-jobs.html.erb +21 -0
- data/app/views/job_invocations/refresh.js.erb +1 -0
- data/app/views/rex_job_mailer/job_finished.html.erb +24 -0
- data/app/views/rex_job_mailer/job_finished.text.erb +9 -0
- data/app/views/template_invocations/show.html.erb +2 -1
- data/config/routes.rb +1 -0
- data/db/migrate/20210816100932_rex_setting_category_to_dsl.rb +5 -0
- data/db/seeds.d/50-notification_blueprints.rb +14 -0
- data/db/seeds.d/95-mail_notifications.rb +24 -0
- data/foreman_remote_execution.gemspec +2 -4
- data/lib/foreman_remote_execution/engine.rb +114 -6
- data/lib/foreman_remote_execution/version.rb +1 -1
- data/package.json +6 -6
- data/test/functional/api/v2/job_invocations_controller_test.rb +20 -0
- data/test/functional/cockpit_controller_test.rb +0 -1
- data/test/graphql/mutations/job_invocations/create.rb +58 -0
- data/test/graphql/queries/job_invocation_query_test.rb +31 -0
- data/test/graphql/queries/job_invocations_query_test.rb +35 -0
- data/test/helpers/remote_execution_helper_test.rb +0 -1
- data/test/unit/actions/run_host_job_test.rb +21 -0
- data/test/unit/actions/run_hosts_job_test.rb +99 -4
- data/test/unit/concerns/host_extensions_test.rb +40 -7
- data/test/unit/input_template_renderer_test.rb +1 -89
- data/test/unit/job_invocation_composer_test.rb +4 -17
- data/test/unit/job_invocation_report_template_test.rb +16 -13
- data/test/unit/job_template_effective_user_test.rb +0 -4
- data/test/unit/remote_execution_provider_test.rb +34 -4
- data/test/unit/targeting_test.rb +68 -1
- data/webpack/JobWizard/JobWizard.js +106 -15
- data/webpack/JobWizard/JobWizard.scss +73 -39
- data/webpack/JobWizard/JobWizardConstants.js +36 -0
- data/webpack/JobWizard/JobWizardSelectors.js +32 -0
- data/webpack/JobWizard/__tests__/fixtures.js +81 -6
- data/webpack/JobWizard/__tests__/integration.test.js +26 -15
- data/webpack/JobWizard/__tests__/validation.test.js +141 -0
- data/webpack/JobWizard/autofill.js +38 -0
- data/webpack/JobWizard/index.js +7 -0
- data/webpack/JobWizard/steps/AdvancedFields/AdvancedFields.js +7 -4
- data/webpack/JobWizard/steps/AdvancedFields/DescriptionField.js +32 -9
- data/webpack/JobWizard/steps/AdvancedFields/__tests__/AdvancedFields.test.js +216 -12
- data/webpack/JobWizard/steps/AdvancedFields/__tests__/__snapshots__/AdvancedFields.test.js.snap +82 -0
- data/webpack/JobWizard/steps/CategoryAndTemplate/CategoryAndTemplate.js +1 -0
- data/webpack/JobWizard/steps/CategoryAndTemplate/index.js +3 -2
- data/webpack/JobWizard/steps/HostsAndInputs/HostPreviewModal.js +62 -0
- data/webpack/JobWizard/steps/HostsAndInputs/HostSearch.js +54 -0
- data/webpack/JobWizard/steps/HostsAndInputs/SelectAPI.js +33 -0
- data/webpack/JobWizard/steps/HostsAndInputs/SelectGQL.js +52 -0
- data/webpack/JobWizard/steps/HostsAndInputs/SelectedChips.js +82 -7
- data/webpack/JobWizard/steps/HostsAndInputs/__tests__/HostsAndInputs.test.js +151 -0
- data/webpack/JobWizard/steps/HostsAndInputs/__tests__/TemplateInputs.test.js +7 -4
- data/webpack/JobWizard/steps/HostsAndInputs/buildHostQuery.js +18 -0
- data/webpack/JobWizard/steps/HostsAndInputs/hostgroups.gql +8 -0
- data/webpack/JobWizard/steps/HostsAndInputs/hosts.gql +8 -0
- data/webpack/JobWizard/steps/HostsAndInputs/index.js +182 -34
- data/webpack/JobWizard/steps/ReviewDetails/index.js +193 -0
- data/webpack/JobWizard/steps/Schedule/PurposeField.js +31 -0
- data/webpack/JobWizard/steps/Schedule/QueryType.js +46 -43
- data/webpack/JobWizard/steps/Schedule/RepeatCron.js +53 -0
- data/webpack/JobWizard/steps/Schedule/RepeatDaily.js +37 -0
- data/webpack/JobWizard/steps/Schedule/RepeatHour.js +54 -0
- data/webpack/JobWizard/steps/Schedule/RepeatMonth.js +46 -0
- data/webpack/JobWizard/steps/Schedule/RepeatOn.js +95 -31
- data/webpack/JobWizard/steps/Schedule/RepeatWeek.js +70 -0
- data/webpack/JobWizard/steps/Schedule/ScheduleType.js +24 -21
- data/webpack/JobWizard/steps/Schedule/StartEndDates.js +78 -23
- data/webpack/JobWizard/steps/Schedule/__tests__/Schedule.test.js +402 -0
- data/webpack/JobWizard/steps/Schedule/__tests__/StartEndDates.test.js +20 -10
- data/webpack/JobWizard/steps/Schedule/index.js +153 -19
- data/webpack/JobWizard/steps/form/DateTimePicker.js +126 -0
- data/webpack/JobWizard/steps/form/FormHelpers.js +4 -0
- data/webpack/JobWizard/steps/form/Formatter.js +39 -8
- data/webpack/JobWizard/steps/form/NumberInput.js +3 -2
- data/webpack/JobWizard/steps/form/ResourceSelect.js +29 -0
- data/webpack/JobWizard/steps/form/SearchSelect.js +121 -0
- data/webpack/JobWizard/steps/form/SelectField.js +14 -3
- data/webpack/JobWizard/steps/form/__tests__/SelectSearch.test.js +33 -0
- data/webpack/JobWizard/submit.js +120 -0
- data/webpack/JobWizard/validation.js +53 -0
- data/webpack/__mocks__/foremanReact/Root/Context/ForemanContext/index.js +2 -0
- data/webpack/__mocks__/foremanReact/common/I18n.js +2 -0
- data/webpack/__mocks__/foremanReact/components/AutoComplete/AutoCompleteActions.js +1 -0
- data/webpack/__mocks__/foremanReact/components/AutoComplete/AutoCompleteConstants.js +1 -0
- data/webpack/__mocks__/foremanReact/routes/RouterSelector.js +1 -0
- data/webpack/helpers.js +1 -0
- data/webpack/react_app/components/RecentJobsCard/JobStatusIcon.js +43 -0
- data/webpack/react_app/components/RecentJobsCard/RecentJobsCard.js +73 -66
- data/webpack/react_app/components/RecentJobsCard/RecentJobsTable.js +98 -0
- data/webpack/react_app/components/RecentJobsCard/constants.js +11 -0
- data/webpack/react_app/components/RecentJobsCard/styles.scss +11 -0
- data/webpack/react_app/extend/fillRecentJobsCard.js +1 -1
- metadata +56 -23
- data/app/models/setting/remote_execution.rb +0 -88
- data/webpack/JobWizard/steps/AdvancedFields/__tests__/DescriptionField.test.js +0 -23
- data/webpack/JobWizard/steps/HostsAndInputs/__tests__/SelectedChips.test.js +0 -37
- data/webpack/JobWizard/steps/form/__tests__/Formatter.test.js.example +0 -76
- 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, :
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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(:
|
|
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
|
-
|
|
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 =
|
|
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"
|
|
32
|
-
{ 'output_type' => 'stdout', 'output' => "output"
|
|
33
|
-
{ 'output_type' => '
|
|
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)
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
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
|
data/test/unit/targeting_test.rb
CHANGED
|
@@ -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 {
|
|
10
|
-
|
|
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 [
|
|
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:
|
|
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
|
|
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
|
-
|
|
95
|
-
|
|
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
|
-
|
|
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:
|
|
119
|
-
|
|
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:
|
|
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:
|
|
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
|
};
|