foreman_remote_execution 4.6.0 → 5.0.1
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/job_invocations_controller.rb +1 -1
- data/app/controllers/ui_job_wizard_controller.rb +21 -2
- 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/helpers/concerns/foreman_remote_execution/hosts_helper_extensions.rb +5 -1
- data/app/helpers/remote_execution_helper.rb +9 -3
- data/app/lib/actions/remote_execution/run_host_job.rb +10 -1
- data/app/lib/actions/remote_execution/run_hosts_job.rb +58 -4
- data/app/mailers/rex_job_mailer.rb +15 -0
- data/app/models/concerns/foreman_remote_execution/host_extensions.rb +10 -0
- data/app/models/concerns/foreman_remote_execution/smart_proxy_extensions.rb +6 -0
- data/app/models/host_proxy_invocation.rb +4 -0
- data/app/models/host_status/execution_status.rb +3 -3
- data/app/models/job_invocation.rb +12 -5
- data/app/models/job_invocation_composer.rb +25 -17
- data/app/models/job_template.rb +1 -1
- data/app/models/remote_execution_feature.rb +5 -1
- data/app/models/remote_execution_provider.rb +18 -2
- data/app/models/rex_mail_notification.rb +13 -0
- data/app/models/targeting.rb +7 -3
- 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/index.html.erb +1 -1
- 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/app/views/templates/ssh/module_action.erb +1 -0
- data/app/views/templates/ssh/power_action.erb +2 -0
- data/app/views/templates/ssh/puppet_run_once.erb +1 -0
- data/config/routes.rb +1 -0
- data/db/migrate/2021051713291621250977_add_host_proxy_invocations.rb +12 -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 -3
- data/lib/foreman_remote_execution/engine.rb +114 -8
- 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/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 +18 -18
- data/test/unit/job_invocation_report_template_test.rb +16 -13
- data/test/unit/job_invocation_test.rb +1 -1
- 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 +68 -1
- data/webpack/JobWizard/JobWizard.js +158 -24
- data/webpack/JobWizard/JobWizard.scss +93 -1
- data/webpack/JobWizard/JobWizardConstants.js +54 -0
- data/webpack/JobWizard/JobWizardSelectors.js +41 -0
- data/webpack/JobWizard/__tests__/fixtures.js +188 -3
- data/webpack/JobWizard/__tests__/integration.test.js +41 -106
- 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 +41 -10
- data/webpack/JobWizard/steps/AdvancedFields/DescriptionField.js +90 -0
- data/webpack/JobWizard/steps/AdvancedFields/Fields.js +116 -55
- data/webpack/JobWizard/steps/AdvancedFields/__tests__/AdvancedFields.test.js +354 -16
- data/webpack/JobWizard/steps/AdvancedFields/__tests__/__snapshots__/AdvancedFields.test.js.snap +79 -246
- data/webpack/JobWizard/steps/CategoryAndTemplate/CategoryAndTemplate.js +5 -2
- data/webpack/JobWizard/steps/CategoryAndTemplate/CategoryAndTemplate.test.js +123 -51
- 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 +51 -0
- 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 +125 -0
- data/webpack/JobWizard/steps/Schedule/RepeatWeek.js +70 -0
- data/webpack/JobWizard/steps/Schedule/ScheduleType.js +28 -0
- data/webpack/JobWizard/steps/Schedule/StartEndDates.js +106 -0
- data/webpack/JobWizard/steps/Schedule/__tests__/Schedule.test.js +402 -0
- data/webpack/JobWizard/steps/Schedule/__tests__/StartEndDates.test.js +32 -0
- data/webpack/JobWizard/steps/Schedule/index.js +178 -0
- data/webpack/JobWizard/steps/form/DateTimePicker.js +126 -0
- data/webpack/JobWizard/steps/form/FormHelpers.js +5 -0
- data/webpack/JobWizard/steps/form/Formatter.js +181 -0
- data/webpack/JobWizard/steps/form/NumberInput.js +36 -0
- 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 +28 -5
- 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/components/SearchBar.js +18 -1
- 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/components/TargetingHosts/__tests__/__snapshots__/TargetingHostsPage.test.js.snap +1 -0
- data/webpack/react_app/extend/fillRecentJobsCard.js +1 -1
- metadata +71 -16
- data/app/models/concerns/foreman_remote_execution/orchestration/ssh.rb +0 -70
- data/app/models/setting/remote_execution.rb +0 -88
- data/test/models/orchestration/ssh_test.rb +0 -56
- data/webpack/JobWizard/__tests__/JobWizard.test.js +0 -13
- data/webpack/JobWizard/__tests__/__snapshots__/JobWizard.test.js.snap +0 -32
- data/webpack/JobWizard/steps/CategoryAndTemplate/__snapshots__/CategoryAndTemplate.test.js.snap +0 -113
- data/webpack/JobWizard/steps/form/__tests__/GroupedSelectField.test.js +0 -38
- data/webpack/JobWizard/steps/form/__tests__/SelectField.test.js +0 -23
- data/webpack/JobWizard/steps/form/__tests__/__snapshots__/GroupedSelectField.test.js.snap +0 -37
- data/webpack/JobWizard/steps/form/__tests__/__snapshots__/SelectField.test.js.snap +0 -23
- data/webpack/react_app/components/RecentJobsCard/styles.css +0 -15
|
@@ -10,7 +10,7 @@ class JobInvocationTest < ActiveSupport::TestCase
|
|
|
10
10
|
end
|
|
11
11
|
|
|
12
12
|
it 'is able to perform search through job invocations' do
|
|
13
|
-
found_jobs = JobInvocation.search_for(%{job_category = "#{job_invocation.job_category}"}).paginate(:page => 1).
|
|
13
|
+
found_jobs = JobInvocation.search_for(%{job_category = "#{job_invocation.job_category}"}).paginate(:page => 1).order('job_invocations.id DESC')
|
|
14
14
|
_(found_jobs).must_equal [job_invocation]
|
|
15
15
|
end
|
|
16
16
|
|
|
@@ -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
|
@@ -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
|
|
@@ -1,28 +1,96 @@
|
|
|
1
|
+
/* eslint-disable camelcase */
|
|
1
2
|
import React, { useState, useEffect, useCallback } from 'react';
|
|
2
3
|
import { useDispatch, useSelector } from 'react-redux';
|
|
3
4
|
import { Wizard } from '@patternfly/react-core';
|
|
4
5
|
import { get } from 'foremanReact/redux/API';
|
|
5
|
-
import { translate as __ } from 'foremanReact/common/I18n';
|
|
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';
|
|
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';
|
|
11
30
|
import './JobWizard.scss';
|
|
12
31
|
|
|
13
32
|
export const JobWizard = () => {
|
|
14
33
|
const [jobTemplateID, setJobTemplateID] = useState(null);
|
|
15
34
|
const [category, setCategory] = useState('');
|
|
16
|
-
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('');
|
|
17
44
|
const dispatch = useDispatch();
|
|
18
45
|
|
|
19
|
-
const setDefaults = useCallback(
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
46
|
+
const setDefaults = useCallback(
|
|
47
|
+
({
|
|
48
|
+
data: {
|
|
49
|
+
template_inputs,
|
|
50
|
+
advanced_template_inputs,
|
|
51
|
+
effective_user,
|
|
52
|
+
job_template: { name, execution_timeout_interval, description_format },
|
|
53
|
+
},
|
|
54
|
+
}) => {
|
|
55
|
+
const advancedTemplateValues = {};
|
|
56
|
+
const defaultTemplateValues = {};
|
|
57
|
+
const inputs = template_inputs;
|
|
58
|
+
const advancedInputs = advanced_template_inputs;
|
|
59
|
+
if (inputs) {
|
|
60
|
+
setTemplateValues(() => {
|
|
61
|
+
inputs.forEach(input => {
|
|
62
|
+
defaultTemplateValues[input.name] = input?.default || '';
|
|
63
|
+
});
|
|
64
|
+
return defaultTemplateValues;
|
|
65
|
+
});
|
|
66
|
+
}
|
|
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
|
+
});
|
|
91
|
+
},
|
|
92
|
+
[]
|
|
93
|
+
);
|
|
26
94
|
useEffect(() => {
|
|
27
95
|
if (jobTemplateID) {
|
|
28
96
|
dispatch(
|
|
@@ -35,11 +103,23 @@ export const JobWizard = () => {
|
|
|
35
103
|
}
|
|
36
104
|
}, [jobTemplateID, setDefaults, dispatch]);
|
|
37
105
|
|
|
106
|
+
const [valid, setValid] = useValidation({
|
|
107
|
+
advancedValues,
|
|
108
|
+
templateValues,
|
|
109
|
+
});
|
|
110
|
+
useAutoFill({
|
|
111
|
+
setSelectedTargets,
|
|
112
|
+
setHostsSearchQuery,
|
|
113
|
+
});
|
|
38
114
|
const templateError = !!useSelector(selectTemplateError);
|
|
39
|
-
const
|
|
115
|
+
const templateResponse = useSelector(selectJobTemplate);
|
|
116
|
+
const isSubmitting = useSelector(selectIsSubmitting);
|
|
117
|
+
const isTemplate =
|
|
118
|
+
!templateError && !!jobTemplateID && templateResponse.job_template;
|
|
119
|
+
|
|
40
120
|
const steps = [
|
|
41
121
|
{
|
|
42
|
-
name:
|
|
122
|
+
name: WIZARD_TITLES.categoryAndTemplate,
|
|
43
123
|
component: (
|
|
44
124
|
<CategoryAndTemplate
|
|
45
125
|
jobTemplate={jobTemplateID}
|
|
@@ -48,14 +128,25 @@ export const JobWizard = () => {
|
|
|
48
128
|
setCategory={setCategory}
|
|
49
129
|
/>
|
|
50
130
|
),
|
|
131
|
+
enableNext: isTemplate,
|
|
51
132
|
},
|
|
52
133
|
{
|
|
53
|
-
name:
|
|
54
|
-
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
|
+
),
|
|
55
145
|
canJumpTo: isTemplate,
|
|
146
|
+
enableNext: isTemplate && valid.hostsAndInputs,
|
|
56
147
|
},
|
|
57
148
|
{
|
|
58
|
-
name:
|
|
149
|
+
name: WIZARD_TITLES.advanced,
|
|
59
150
|
component: (
|
|
60
151
|
<AdvancedFields
|
|
61
152
|
advancedValues={advancedValues}
|
|
@@ -65,23 +156,53 @@ export const JobWizard = () => {
|
|
|
65
156
|
...newValues,
|
|
66
157
|
}));
|
|
67
158
|
}}
|
|
68
|
-
|
|
159
|
+
templateValues={templateValues}
|
|
69
160
|
/>
|
|
70
161
|
),
|
|
71
|
-
canJumpTo: isTemplate,
|
|
162
|
+
canJumpTo: isTemplate && valid.hostsAndInputs,
|
|
163
|
+
enableNext: isTemplate && valid.hostsAndInputs && valid.advanced,
|
|
72
164
|
},
|
|
73
165
|
{
|
|
74
|
-
name:
|
|
75
|
-
component:
|
|
76
|
-
|
|
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,
|
|
77
179
|
},
|
|
78
180
|
{
|
|
79
|
-
name:
|
|
80
|
-
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
|
+
),
|
|
81
193
|
nextButtonText: 'Run',
|
|
82
|
-
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,
|
|
83
202
|
},
|
|
84
203
|
];
|
|
204
|
+
const location = useForemanLocation();
|
|
205
|
+
const organization = useForemanOrganization();
|
|
85
206
|
return (
|
|
86
207
|
<Wizard
|
|
87
208
|
onClose={() => history.goBack()}
|
|
@@ -89,6 +210,19 @@ export const JobWizard = () => {
|
|
|
89
210
|
steps={steps}
|
|
90
211
|
height="100%"
|
|
91
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
|
+
}}
|
|
92
226
|
/>
|
|
93
227
|
);
|
|
94
228
|
};
|
|
@@ -1,14 +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
|
}
|
|
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;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
input[type='radio'],
|
|
56
|
+
input[type='checkbox'] {
|
|
57
|
+
margin: 0;
|
|
58
|
+
}
|
|
59
|
+
.schedule-tab {
|
|
60
|
+
.advanced-scheduling-button {
|
|
61
|
+
text-align: start;
|
|
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
|
+
}
|
|
13
105
|
}
|
|
14
106
|
}
|
|
@@ -1,6 +1,60 @@
|
|
|
1
|
+
import { translate as __ } from 'foremanReact/common/I18n';
|
|
1
2
|
import { foremanUrl } from 'foremanReact/common/helpers';
|
|
2
3
|
|
|
3
4
|
export const JOB_TEMPLATES = 'JOB_TEMPLATES';
|
|
4
5
|
export const JOB_CATEGORIES = 'JOB_CATEGORIES';
|
|
5
6
|
export const JOB_TEMPLATE = 'JOB_TEMPLATE';
|
|
7
|
+
export const JOB_INVOCATION = 'JOB_INVOCATION';
|
|
6
8
|
export const templatesUrl = foremanUrl('/api/v2/job_templates');
|
|
9
|
+
|
|
10
|
+
export const repeatTypes = {
|
|
11
|
+
noRepeat: __('Does not repeat'),
|
|
12
|
+
cronline: __('Cronline'),
|
|
13
|
+
monthly: __('Monthly'),
|
|
14
|
+
weekly: __('Weekly'),
|
|
15
|
+
daily: __('Daily'),
|
|
16
|
+
hourly: __('Hourly'),
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export const WIZARD_TITLES = {
|
|
20
|
+
categoryAndTemplate: __('Category and Template'),
|
|
21
|
+
hostsAndInputs: __('Target hosts and inputs'),
|
|
22
|
+
advanced: __('Advanced Fields'),
|
|
23
|
+
schedule: __('Schedule'),
|
|
24
|
+
review: __('Review Details'),
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export const initialScheduleState = {
|
|
28
|
+
repeatType: repeatTypes.noRepeat,
|
|
29
|
+
repeatAmount: '',
|
|
30
|
+
repeatData: {},
|
|
31
|
+
startsAt: '',
|
|
32
|
+
startsBefore: '',
|
|
33
|
+
ends: '',
|
|
34
|
+
isFuture: false,
|
|
35
|
+
isNeverEnds: false,
|
|
36
|
+
isTypeStatic: true,
|
|
37
|
+
purpose: '',
|
|
38
|
+
};
|
|
39
|
+
export const HOSTS_API = 'HOSTS_API';
|
|
40
|
+
export const HOSTS = 'HOSTS';
|
|
41
|
+
export const HOST_COLLECTIONS = 'HOST_COLLECTIONS';
|
|
42
|
+
export const HOST_GROUPS = 'HOST_GROUPS';
|
|
43
|
+
export const hostMethods = {
|
|
44
|
+
hosts: __('Hosts'),
|
|
45
|
+
hostCollections: __('Host collections'),
|
|
46
|
+
hostGroups: __('Host groups'),
|
|
47
|
+
searchQuery: __('Search query'),
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
export const hostQuerySearchID = 'searchBar'; // until https://projects.theforeman.org/issues/33737 is used
|
|
51
|
+
export const hostsController = 'hosts';
|
|
52
|
+
|
|
53
|
+
export const dataName = {
|
|
54
|
+
[HOSTS]: 'hosts',
|
|
55
|
+
[HOST_GROUPS]: 'hostgroups',
|
|
56
|
+
};
|
|
57
|
+
export const HOSTS_TO_PREVIEW_AMOUNT = 20;
|
|
58
|
+
|
|
59
|
+
export const DEBOUNCE_HOST_COUNT = 700;
|
|
60
|
+
export const HOST_IDS = 'HOST_IDS';
|
|
@@ -1,13 +1,18 @@
|
|
|
1
|
+
import URI from 'urijs';
|
|
1
2
|
import {
|
|
2
3
|
selectAPIResponse,
|
|
3
4
|
selectAPIStatus,
|
|
4
5
|
selectAPIErrorMessage,
|
|
5
6
|
} from 'foremanReact/redux/API/APISelectors';
|
|
7
|
+
import { STATUS } from 'foremanReact/constants';
|
|
8
|
+
import { selectRouterLocation } from 'foremanReact/routes/RouterSelector';
|
|
6
9
|
|
|
7
10
|
import {
|
|
8
11
|
JOB_TEMPLATES,
|
|
9
12
|
JOB_CATEGORIES,
|
|
10
13
|
JOB_TEMPLATE,
|
|
14
|
+
HOSTS_API,
|
|
15
|
+
JOB_INVOCATION,
|
|
11
16
|
} from './JobWizardConstants';
|
|
12
17
|
|
|
13
18
|
export const selectJobTemplatesStatus = state =>
|
|
@@ -22,6 +27,9 @@ export const selectJobTemplates = state =>
|
|
|
22
27
|
export const selectJobCategories = state =>
|
|
23
28
|
selectAPIResponse(state, JOB_CATEGORIES).job_categories || [];
|
|
24
29
|
|
|
30
|
+
export const selectWithKatello = state =>
|
|
31
|
+
selectAPIResponse(state, JOB_CATEGORIES).with_katello || false;
|
|
32
|
+
|
|
25
33
|
export const selectJobCategoriesStatus = state =>
|
|
26
34
|
selectAPIStatus(state, JOB_CATEGORIES);
|
|
27
35
|
|
|
@@ -36,3 +44,36 @@ export const selectTemplateError = state =>
|
|
|
36
44
|
|
|
37
45
|
export const selectJobTemplate = state =>
|
|
38
46
|
selectAPIResponse(state, JOB_TEMPLATE);
|
|
47
|
+
|
|
48
|
+
export const selectEffectiveUser = state =>
|
|
49
|
+
selectAPIResponse(state, JOB_TEMPLATE).effective_user;
|
|
50
|
+
|
|
51
|
+
export const selectAdvancedTemplateInputs = state =>
|
|
52
|
+
selectAPIResponse(state, JOB_TEMPLATE).advanced_template_inputs || [];
|
|
53
|
+
|
|
54
|
+
export const selectTemplateInputs = state =>
|
|
55
|
+
selectAPIResponse(state, JOB_TEMPLATE).template_inputs || [];
|
|
56
|
+
|
|
57
|
+
export const selectHostCount = state =>
|
|
58
|
+
selectAPIResponse(state, HOSTS_API).subtotal || 0;
|
|
59
|
+
|
|
60
|
+
export const selectHosts = state =>
|
|
61
|
+
(selectAPIResponse(state, HOSTS_API).results || []).map(host => host.name);
|
|
62
|
+
|
|
63
|
+
export const selectIsLoadingHosts = state =>
|
|
64
|
+
!selectAPIStatus(state, HOSTS_API) ||
|
|
65
|
+
selectAPIStatus(state, HOSTS_API) === STATUS.PENDING;
|
|
66
|
+
|
|
67
|
+
export const selectResponse = selectAPIResponse;
|
|
68
|
+
|
|
69
|
+
export const selectIsLoading = (state, key) =>
|
|
70
|
+
selectAPIStatus(state, key) === STATUS.PENDING;
|
|
71
|
+
|
|
72
|
+
export const selectIsSubmitting = state =>
|
|
73
|
+
selectAPIStatus(state, JOB_INVOCATION) === STATUS.PENDING ||
|
|
74
|
+
selectAPIStatus(state, JOB_INVOCATION) === STATUS.RESOLVED;
|
|
75
|
+
|
|
76
|
+
export const selectRouterSearch = state => {
|
|
77
|
+
const { search } = selectRouterLocation(state);
|
|
78
|
+
return URI.parseQuery(search);
|
|
79
|
+
};
|