foreman_remote_execution 4.7.0 → 5.1.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/.rubocop_todo.yml +1 -0
- data/Gemfile +1 -1
- 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_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 +8 -1
- data/app/lib/actions/remote_execution/run_hosts_job.rb +57 -3
- data/app/lib/foreman_remote_execution/renderer/scope/input.rb +1 -1
- 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 +6 -0
- data/app/models/job_invocation_composer.rb +21 -13
- data/app/models/job_template.rb +3 -1
- data/app/models/remote_execution_provider.rb +18 -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/job_templates/_custom_tabs.html.erb +4 -9
- 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 +9 -2
- 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 +1 -1
- data/lib/foreman_remote_execution/engine.rb +111 -6
- data/lib/foreman_remote_execution/version.rb +1 -1
- data/package.json +9 -7
- 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/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 +36 -3
- data/test/unit/job_invocation_composer_test.rb +3 -5
- data/test/unit/job_invocation_report_template_test.rb +17 -14
- data/test/unit/job_template_effective_user_test.rb +0 -4
- data/test/unit/remote_execution_provider_test.rb +46 -4
- data/test/unit/targeting_test.rb +69 -2
- data/webpack/JobWizard/JobWizard.js +142 -28
- data/webpack/JobWizard/JobWizard.scss +86 -33
- data/webpack/JobWizard/JobWizardConstants.js +44 -0
- data/webpack/JobWizard/JobWizardSelectors.js +32 -0
- data/webpack/JobWizard/__tests__/fixtures.js +89 -6
- data/webpack/JobWizard/__tests__/integration.test.js +29 -22
- 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 +23 -9
- data/webpack/JobWizard/steps/AdvancedFields/DescriptionField.js +32 -9
- data/webpack/JobWizard/steps/AdvancedFields/Fields.js +48 -1
- data/webpack/JobWizard/steps/AdvancedFields/__tests__/AdvancedFields.test.js +242 -23
- data/webpack/JobWizard/steps/AdvancedFields/__tests__/__snapshots__/AdvancedFields.test.js.snap +82 -0
- data/webpack/JobWizard/steps/CategoryAndTemplate/CategoryAndTemplate.js +5 -2
- data/webpack/JobWizard/steps/CategoryAndTemplate/CategoryAndTemplate.test.js +3 -2
- 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 +100 -0
- data/webpack/JobWizard/steps/HostsAndInputs/TemplateInputs.js +23 -0
- data/webpack/JobWizard/steps/HostsAndInputs/__tests__/HostsAndInputs.test.js +151 -0
- data/webpack/JobWizard/steps/HostsAndInputs/__tests__/TemplateInputs.test.js +53 -0
- 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 +214 -0
- 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 +166 -29
- 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 +49 -17
- data/webpack/JobWizard/steps/form/NumberInput.js +5 -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/WizardTitle.js +14 -0
- 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/RecentJobsCard.js +2 -1
- metadata +53 -7
- data/app/models/setting/remote_execution.rb +0 -88
- data/webpack/JobWizard/steps/AdvancedFields/__tests__/DescriptionField.test.js +0 -23
- data/webpack/JobWizard/steps/form/__tests__/Formatter.test.js.example +0 -76
|
@@ -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' }
|
|
@@ -370,8 +366,10 @@ class JobInvocationComposerTest < ActiveSupport::TestCase
|
|
|
370
366
|
let(:host) { FactoryBot.create(:host) }
|
|
371
367
|
|
|
372
368
|
it 'obeys authorization' do
|
|
369
|
+
fake_scope = mock
|
|
373
370
|
composer.stubs(:displayed_search_query => "name = #{host.name}")
|
|
374
|
-
Host.expects(:
|
|
371
|
+
Host.expects(:execution_scope).returns(fake_scope)
|
|
372
|
+
fake_scope.expects(:authorized).with(:view_hosts, Host).returns(Host.where({}))
|
|
375
373
|
composer.targeted_hosts_count
|
|
376
374
|
end
|
|
377
375
|
|
|
@@ -9,7 +9,7 @@ class JobReportTemplateTest < ActiveSupport::TestCase
|
|
|
9
9
|
|
|
10
10
|
context 'with valid job invocation report template' do
|
|
11
11
|
let(:job_invocation_template) do
|
|
12
|
-
file_path = File.read(File.expand_path(Rails.root + "app/views/unattended/report_templates/
|
|
12
|
+
file_path = File.read(File.expand_path(Rails.root + "app/views/unattended/report_templates/job_invocation_-_report_template.erb"))
|
|
13
13
|
template = ReportTemplate.import_without_save("Job Invocation Report Template", file_path)
|
|
14
14
|
template.save!
|
|
15
15
|
template
|
|
@@ -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?
|
|
@@ -50,14 +50,22 @@ class RemoteExecutionProviderTest < ActiveSupport::TestCase
|
|
|
50
50
|
end
|
|
51
51
|
end
|
|
52
52
|
|
|
53
|
+
describe '.host_setting' do
|
|
54
|
+
let(:host) { FactoryBot.create(:host) }
|
|
55
|
+
|
|
56
|
+
it 'honors falsey values set as a host parameter' do
|
|
57
|
+
key = 'remote_execution_connect_by_ip'
|
|
58
|
+
Setting[key] = true
|
|
59
|
+
host.parameters << HostParameter.new(name: key, value: false)
|
|
60
|
+
|
|
61
|
+
refute RemoteExecutionProvider.host_setting(host, key)
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
|
|
53
65
|
describe SSHExecutionProvider do
|
|
54
66
|
before { User.current = FactoryBot.build(:user, :admin) }
|
|
55
67
|
after { User.current = nil }
|
|
56
68
|
|
|
57
|
-
before do
|
|
58
|
-
Setting::RemoteExecution.load_defaults
|
|
59
|
-
end
|
|
60
|
-
|
|
61
69
|
let(:job_invocation) { FactoryBot.create(:job_invocation, :with_template) }
|
|
62
70
|
let(:template_invocation) { job_invocation.pattern_template_invocations.first }
|
|
63
71
|
let(:host) { FactoryBot.create(:host) }
|
|
@@ -200,6 +208,40 @@ class RemoteExecutionProviderTest < ActiveSupport::TestCase
|
|
|
200
208
|
host.interfaces.each(&:save)
|
|
201
209
|
host.reload
|
|
202
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
|
|
203
245
|
end
|
|
204
246
|
end
|
|
205
247
|
end
|
data/test/unit/targeting_test.rb
CHANGED
|
@@ -61,7 +61,7 @@ class TargetingTest < ActiveSupport::TestCase
|
|
|
61
61
|
users(:one).destroy
|
|
62
62
|
end
|
|
63
63
|
|
|
64
|
-
it {
|
|
64
|
+
it { assert_nil targeting.reload.user }
|
|
65
65
|
it do
|
|
66
66
|
-> { targeting.resolve_hosts! }.must_raise(Foreman::Exception)
|
|
67
67
|
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
|
|
@@ -3,43 +3,91 @@ import React, { useState, useEffect, useCallback } from 'react';
|
|
|
3
3
|
import { useDispatch, useSelector } from 'react-redux';
|
|
4
4
|
import { Wizard } from '@patternfly/react-core';
|
|
5
5
|
import { get } from 'foremanReact/redux/API';
|
|
6
|
-
import { translate as __ } from 'foremanReact/common/I18n';
|
|
7
6
|
import history from 'foremanReact/history';
|
|
7
|
+
|
|
8
|
+
import {
|
|
9
|
+
useForemanOrganization,
|
|
10
|
+
useForemanLocation,
|
|
11
|
+
} from 'foremanReact/Root/Context/ForemanContext';
|
|
8
12
|
import CategoryAndTemplate from './steps/CategoryAndTemplate/';
|
|
9
13
|
import { AdvancedFields } from './steps/AdvancedFields/AdvancedFields';
|
|
10
|
-
import {
|
|
11
|
-
|
|
14
|
+
import {
|
|
15
|
+
JOB_TEMPLATE,
|
|
16
|
+
WIZARD_TITLES,
|
|
17
|
+
initialScheduleState,
|
|
18
|
+
} from './JobWizardConstants';
|
|
19
|
+
import {
|
|
20
|
+
selectTemplateError,
|
|
21
|
+
selectJobTemplate,
|
|
22
|
+
selectIsSubmitting,
|
|
23
|
+
} from './JobWizardSelectors';
|
|
12
24
|
import Schedule from './steps/Schedule/';
|
|
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: {} });
|
|
36
|
+
const [templateValues, setTemplateValues] = useState({}); // TODO use templateValues in advanced fields - description https://github.com/theforeman/foreman_remote_execution/pull/605
|
|
37
|
+
const [scheduleValue, setScheduleValue] = useState(initialScheduleState);
|
|
38
|
+
const [selectedTargets, setSelectedTargets] = useState({
|
|
39
|
+
hosts: [],
|
|
40
|
+
hostCollections: [],
|
|
41
|
+
hostGroups: [],
|
|
42
|
+
});
|
|
43
|
+
const [hostsSearchQuery, setHostsSearchQuery] = useState('');
|
|
19
44
|
const dispatch = useDispatch();
|
|
20
45
|
|
|
21
46
|
const setDefaults = useCallback(
|
|
22
47
|
({
|
|
23
48
|
data: {
|
|
49
|
+
template_inputs,
|
|
24
50
|
advanced_template_inputs,
|
|
25
51
|
effective_user,
|
|
26
|
-
job_template: {
|
|
52
|
+
job_template: { name, execution_timeout_interval, description_format },
|
|
27
53
|
},
|
|
28
54
|
}) => {
|
|
29
55
|
const advancedTemplateValues = {};
|
|
56
|
+
const defaultTemplateValues = {};
|
|
57
|
+
const inputs = template_inputs;
|
|
30
58
|
const advancedInputs = advanced_template_inputs;
|
|
31
|
-
if (
|
|
32
|
-
|
|
33
|
-
|
|
59
|
+
if (inputs) {
|
|
60
|
+
setTemplateValues(() => {
|
|
61
|
+
inputs.forEach(input => {
|
|
62
|
+
defaultTemplateValues[input.name] = input?.default || '';
|
|
63
|
+
});
|
|
64
|
+
return defaultTemplateValues;
|
|
34
65
|
});
|
|
35
66
|
}
|
|
36
|
-
setAdvancedValues(currentAdvancedValues =>
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
67
|
+
setAdvancedValues(currentAdvancedValues => {
|
|
68
|
+
if (advancedInputs) {
|
|
69
|
+
advancedInputs.forEach(input => {
|
|
70
|
+
advancedTemplateValues[input.name] = input?.default || '';
|
|
71
|
+
});
|
|
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
|
+
};
|
|
82
|
+
return {
|
|
83
|
+
...currentAdvancedValues,
|
|
84
|
+
effectiveUserValue: effective_user?.value || '',
|
|
85
|
+
timeoutToKill: execution_timeout_interval || '',
|
|
86
|
+
templateValues: advancedTemplateValues,
|
|
87
|
+
description: generateDefaultDescription() || '',
|
|
88
|
+
isRandomizedOrdering: false,
|
|
89
|
+
};
|
|
90
|
+
});
|
|
43
91
|
},
|
|
44
92
|
[]
|
|
45
93
|
);
|
|
@@ -55,11 +103,23 @@ export const JobWizard = () => {
|
|
|
55
103
|
}
|
|
56
104
|
}, [jobTemplateID, setDefaults, dispatch]);
|
|
57
105
|
|
|
106
|
+
const [valid, setValid] = useValidation({
|
|
107
|
+
advancedValues,
|
|
108
|
+
templateValues,
|
|
109
|
+
});
|
|
110
|
+
useAutoFill({
|
|
111
|
+
setSelectedTargets,
|
|
112
|
+
setHostsSearchQuery,
|
|
113
|
+
});
|
|
58
114
|
const templateError = !!useSelector(selectTemplateError);
|
|
59
|
-
const
|
|
115
|
+
const templateResponse = useSelector(selectJobTemplate);
|
|
116
|
+
const isSubmitting = useSelector(selectIsSubmitting);
|
|
117
|
+
const isTemplate =
|
|
118
|
+
!templateError && !!jobTemplateID && templateResponse.job_template;
|
|
119
|
+
|
|
60
120
|
const steps = [
|
|
61
121
|
{
|
|
62
|
-
name:
|
|
122
|
+
name: WIZARD_TITLES.categoryAndTemplate,
|
|
63
123
|
component: (
|
|
64
124
|
<CategoryAndTemplate
|
|
65
125
|
jobTemplate={jobTemplateID}
|
|
@@ -68,14 +128,25 @@ export const JobWizard = () => {
|
|
|
68
128
|
setCategory={setCategory}
|
|
69
129
|
/>
|
|
70
130
|
),
|
|
131
|
+
enableNext: isTemplate,
|
|
71
132
|
},
|
|
72
133
|
{
|
|
73
|
-
name:
|
|
74
|
-
component:
|
|
134
|
+
name: WIZARD_TITLES.hostsAndInputs,
|
|
135
|
+
component: (
|
|
136
|
+
<HostsAndInputs
|
|
137
|
+
templateValues={templateValues}
|
|
138
|
+
setTemplateValues={setTemplateValues}
|
|
139
|
+
selected={selectedTargets}
|
|
140
|
+
setSelected={setSelectedTargets}
|
|
141
|
+
hostsSearchQuery={hostsSearchQuery}
|
|
142
|
+
setHostsSearchQuery={setHostsSearchQuery}
|
|
143
|
+
/>
|
|
144
|
+
),
|
|
75
145
|
canJumpTo: isTemplate,
|
|
146
|
+
enableNext: isTemplate && valid.hostsAndInputs,
|
|
76
147
|
},
|
|
77
148
|
{
|
|
78
|
-
name:
|
|
149
|
+
name: WIZARD_TITLES.advanced,
|
|
79
150
|
component: (
|
|
80
151
|
<AdvancedFields
|
|
81
152
|
advancedValues={advancedValues}
|
|
@@ -85,23 +156,53 @@ export const JobWizard = () => {
|
|
|
85
156
|
...newValues,
|
|
86
157
|
}));
|
|
87
158
|
}}
|
|
88
|
-
|
|
159
|
+
templateValues={templateValues}
|
|
89
160
|
/>
|
|
90
161
|
),
|
|
91
|
-
canJumpTo: isTemplate,
|
|
162
|
+
canJumpTo: isTemplate && valid.hostsAndInputs,
|
|
163
|
+
enableNext: isTemplate && valid.hostsAndInputs && valid.advanced,
|
|
92
164
|
},
|
|
93
165
|
{
|
|
94
|
-
name:
|
|
95
|
-
component:
|
|
96
|
-
|
|
166
|
+
name: WIZARD_TITLES.schedule,
|
|
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,
|
|
97
179
|
},
|
|
98
180
|
{
|
|
99
|
-
name:
|
|
100
|
-
component:
|
|
181
|
+
name: WIZARD_TITLES.review,
|
|
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
|
+
),
|
|
101
193
|
nextButtonText: 'Run',
|
|
102
|
-
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,
|
|
103
202
|
},
|
|
104
203
|
];
|
|
204
|
+
const location = useForemanLocation();
|
|
205
|
+
const organization = useForemanOrganization();
|
|
105
206
|
return (
|
|
106
207
|
<Wizard
|
|
107
208
|
onClose={() => history.goBack()}
|
|
@@ -109,6 +210,19 @@ export const JobWizard = () => {
|
|
|
109
210
|
steps={steps}
|
|
110
211
|
height="100%"
|
|
111
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
|
+
}}
|
|
112
226
|
/>
|
|
113
227
|
);
|
|
114
228
|
};
|
|
@@ -1,53 +1,106 @@
|
|
|
1
1
|
.job-wizard {
|
|
2
|
+
.wizard-title {
|
|
3
|
+
margin-bottom: 25px;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
.pf-c-wizard__nav.pf-m-expanded {
|
|
7
|
+
z-index: calc(
|
|
8
|
+
var(--pf-c-wizard__footer--ZIndex) + 2
|
|
9
|
+
); // So the small screen navigation can be shown above the select box
|
|
10
|
+
}
|
|
11
|
+
|
|
2
12
|
.pf-c-wizard__main {
|
|
13
|
+
overflow: visible;
|
|
3
14
|
z-index: calc(
|
|
4
15
|
var(--pf-c-wizard__footer--ZIndex) + 1
|
|
5
16
|
); // So the select box can be shown above the wizard footer
|
|
6
17
|
}
|
|
7
|
-
|
|
18
|
+
.pf-c-wizard__nav {
|
|
19
|
+
z-index: calc(
|
|
20
|
+
var(--pf-c-wizard__footer--ZIndex) + 2
|
|
21
|
+
); // So the navigation box can be shown above the wizard body
|
|
22
|
+
}
|
|
8
23
|
.pf-c-wizard__main-body {
|
|
9
24
|
max-width: 500px;
|
|
10
25
|
.advanced-fields-title {
|
|
11
26
|
margin-bottom: 10px;
|
|
12
27
|
}
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
font-size: 16px;
|
|
38
|
-
}
|
|
39
|
-
}
|
|
28
|
+
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
.gray-text {
|
|
32
|
+
color: var(--pf-global--Color--dark-200);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
.target-hosts-and-inputs {
|
|
36
|
+
.hosts-chip-group {
|
|
37
|
+
margin-top: 8px;
|
|
38
|
+
float: left;
|
|
39
|
+
clear: left;
|
|
40
|
+
display: block;
|
|
41
|
+
}
|
|
42
|
+
.clear-chips {
|
|
43
|
+
margin-top: 8px;
|
|
44
|
+
}
|
|
45
|
+
.pf-c-select__toggle-typeahead {
|
|
46
|
+
border: 0px;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
.target-method-select {
|
|
50
|
+
.pf-c-select__toggle-wrapper {
|
|
51
|
+
flex-wrap: nowrap;
|
|
40
52
|
}
|
|
41
53
|
}
|
|
42
54
|
}
|
|
43
|
-
|
|
55
|
+
input[type='radio'],
|
|
56
|
+
input[type='checkbox'] {
|
|
57
|
+
margin: 0;
|
|
58
|
+
}
|
|
44
59
|
.schedule-tab {
|
|
45
|
-
input[type='radio'],
|
|
46
|
-
input[type='checkbox'] {
|
|
47
|
-
margin: 0;
|
|
48
|
-
}
|
|
49
60
|
.advanced-scheduling-button {
|
|
50
61
|
text-align: start;
|
|
51
62
|
}
|
|
63
|
+
#repeat-on-weekly {
|
|
64
|
+
display: grid;
|
|
65
|
+
grid-template-columns: repeat(7, 1fr);
|
|
66
|
+
}
|
|
67
|
+
.pf-l-grid {
|
|
68
|
+
gap: var(--pf-c-form--GridGap);
|
|
69
|
+
}
|
|
70
|
+
#repeat-on-hourly {
|
|
71
|
+
max-height: 300px;
|
|
72
|
+
overflow: scroll;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
.pf-c-date-picker {
|
|
77
|
+
vertical-align: top;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
.time-picker {
|
|
81
|
+
width: 150px;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
input[type='radio'],
|
|
85
|
+
input[type='checkbox'] {
|
|
86
|
+
// overwriting bootstrap/_forms.scss margin: 4px 0 0;
|
|
87
|
+
margin: 0;
|
|
88
|
+
}
|
|
89
|
+
textarea {
|
|
90
|
+
min-height: 40px;
|
|
91
|
+
min-width: 100px;
|
|
92
|
+
}
|
|
93
|
+
#host-selection {
|
|
94
|
+
width: 500px;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
.pf-c-modal-box {
|
|
98
|
+
width: auto;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
.review-details {
|
|
102
|
+
.advanced-fields {
|
|
103
|
+
margin-left: 10px;
|
|
104
|
+
}
|
|
52
105
|
}
|
|
53
106
|
}
|