foreman_remote_execution 8.0.0 → 8.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/app/controllers/job_invocations_controller.rb +1 -2
- data/app/controllers/job_templates_controller.rb +1 -1
- data/app/controllers/ui_job_wizard_controller.rb +1 -1
- data/app/helpers/job_invocations_helper.rb +0 -7
- data/app/helpers/remote_execution_helper.rb +1 -1
- data/app/lib/actions/remote_execution/proxy_action.rb +46 -0
- data/app/lib/actions/remote_execution/run_host_job.rb +38 -11
- data/app/lib/actions/remote_execution/run_hosts_job.rb +7 -6
- data/app/lib/actions/remote_execution/template_invocation_progress_logging.rb +27 -0
- data/app/models/job_invocation.rb +5 -9
- data/app/models/job_invocation_composer.rb +4 -0
- data/app/models/remote_execution_provider.rb +10 -2
- data/app/models/ssh_execution_provider.rb +1 -0
- data/app/models/template_invocation.rb +1 -0
- data/app/models/template_invocation_event.rb +11 -0
- data/app/views/job_invocations/_form.html.erb +4 -0
- data/app/views/job_invocations/new.html.erb +5 -0
- data/app/views/templates/script/package_action.erb +1 -1
- data/config/routes.rb +5 -5
- data/db/migrate/20220713095705_create_template_invocation_events.rb +17 -0
- data/db/migrate/20220822155946_add_time_to_pickup_to_job_invocation.rb +5 -0
- data/extra/cockpit/foreman-cockpit-session +303 -230
- data/extra/cockpit/foreman-cockpit.service +1 -0
- data/foreman_remote_execution.gemspec +1 -1
- data/lib/foreman_remote_execution/engine.rb +12 -7
- data/lib/foreman_remote_execution/tasks/explain_proxy_selection.rake +131 -0
- data/lib/foreman_remote_execution/version.rb +1 -1
- data/test/unit/remote_execution_provider_test.rb +22 -0
- data/webpack/JobWizard/JobWizard.js +53 -18
- data/webpack/JobWizard/JobWizard.scss +3 -0
- data/webpack/JobWizard/JobWizardConstants.js +1 -1
- data/webpack/JobWizard/JobWizardHelpers.js +15 -0
- data/webpack/JobWizard/JobWizardPageRerun.js +29 -5
- data/webpack/JobWizard/JobWizardSelectors.js +8 -2
- data/webpack/JobWizard/__tests__/JobWizardPageRerun.test.js +5 -0
- data/webpack/JobWizard/__tests__/fixtures.js +26 -2
- data/webpack/JobWizard/autofill.js +32 -10
- data/webpack/JobWizard/index.js +25 -6
- data/webpack/JobWizard/steps/AdvancedFields/AdvancedFields.js +25 -0
- data/webpack/JobWizard/steps/AdvancedFields/DescriptionField.js +12 -1
- data/webpack/JobWizard/steps/AdvancedFields/Fields.js +41 -6
- data/webpack/JobWizard/steps/AdvancedFields/__tests__/AdvancedFields.test.js +1 -1
- data/webpack/JobWizard/steps/AdvancedFields/__tests__/__snapshots__/AdvancedFields.test.js.snap +1 -1
- data/webpack/JobWizard/steps/CategoryAndTemplate/CategoryAndTemplate.js +4 -2
- data/webpack/JobWizard/steps/CategoryAndTemplate/CategoryAndTemplate.test.js +6 -2
- data/webpack/JobWizard/steps/CategoryAndTemplate/index.js +28 -20
- data/webpack/JobWizard/steps/HostsAndInputs/__tests__/TemplateInputs.test.js +32 -0
- data/webpack/JobWizard/steps/HostsAndInputs/index.js +2 -2
- data/webpack/JobWizard/steps/ReviewDetails/index.js +1 -0
- data/webpack/JobWizard/steps/form/FormHelpers.js +21 -1
- data/webpack/JobWizard/steps/form/Formatter.js +22 -6
- data/webpack/JobWizard/steps/form/ResourceSelect.js +97 -10
- data/webpack/JobWizard/steps/form/SearchSelect.js +2 -2
- data/webpack/JobWizard/steps/form/SelectField.js +4 -0
- data/webpack/JobWizard/submit.js +3 -1
- data/webpack/JobWizard/validation.js +1 -0
- data/webpack/Routes/routes.js +3 -3
- data/webpack/react_app/components/FeaturesDropdown/actions.js +23 -2
- data/webpack/react_app/components/FeaturesDropdown/index.js +2 -0
- data/webpack/react_app/components/HostKebab/KebabItems.js +1 -0
- data/webpack/react_app/components/RecentJobsCard/RecentJobsCard.js +5 -0
- data/webpack/react_app/components/RecentJobsCard/RecentJobsTable.js +51 -59
- data/webpack/react_app/extend/Fills.js +3 -3
- metadata +12 -5
@@ -0,0 +1,131 @@
|
|
1
|
+
namespace :foreman_remote_execution do
|
2
|
+
desc <<~DESC
|
3
|
+
Explains which proxies can be used for remote execution against HOST using a specified PROVIDER.
|
4
|
+
|
5
|
+
* HOST : A scoped search query to find hosts by
|
6
|
+
* PROVIDER : The PROVIDER to scope by
|
7
|
+
* FORMAT : Output format, one of verbose (or unset), json, pretty-json, csv
|
8
|
+
* FOREMAN_USER : Run as if FOREMAN_USER triggered a job (runs as anonymous_admin if unset)
|
9
|
+
* ORGANIZATION : Run in the context of ORGANIZATION (runs in any context if unset)
|
10
|
+
* LOCATION : Run in the context of LOCATION (runs in any context if unset)
|
11
|
+
DESC
|
12
|
+
task :explain_proxy_selection => ['environment'] do
|
13
|
+
options = {}
|
14
|
+
options[:host] = ENV['HOST']
|
15
|
+
options[:provider] = ENV['PROVIDER']
|
16
|
+
|
17
|
+
raise 'Environment variable HOST has to be set' unless options[:host]
|
18
|
+
raise 'Environment variable PROVIDER has to be set' unless options[:provider]
|
19
|
+
|
20
|
+
if ENV['FOREMAN_USER']
|
21
|
+
User.current = User.friendly.find(ENV['FOREMAN_USER'])
|
22
|
+
else
|
23
|
+
User.current = User.anonymous_admin
|
24
|
+
end
|
25
|
+
Location.current = Location.friendly.find(ENV['LOCATION']) if ENV['LOCATION']
|
26
|
+
Organization.current = Organization.friendly.find(ENV['ORGANIZATION']) if ENV['ORGANIZATION']
|
27
|
+
|
28
|
+
selector = ::RemoteExecutionProxySelector.new
|
29
|
+
|
30
|
+
results = Host.search_for(options[:host]).map do |host|
|
31
|
+
host_base = { :host => host }
|
32
|
+
proxies = selector.available_proxies(host, options[:provider])
|
33
|
+
determined_proxy = selector.determine_proxy(host, options[:provider])
|
34
|
+
counts = selector.instance_variable_get('@tasks')
|
35
|
+
counts.default = 0
|
36
|
+
|
37
|
+
strategies = selector.strategies.map do |strategy|
|
38
|
+
base = { :name => strategy, :enabled => !proxies[strategy].nil? }
|
39
|
+
next base if proxies[strategy].nil?
|
40
|
+
|
41
|
+
base.merge(:proxies => proxies[strategy].sort_by { |proxy| counts[proxy] }.map do |proxy|
|
42
|
+
{:proxy => proxy, :count => counts[proxy]}
|
43
|
+
end)
|
44
|
+
end
|
45
|
+
|
46
|
+
case determined_proxy
|
47
|
+
when :not_defined
|
48
|
+
settings = {
|
49
|
+
global_proxy: 'remote_execution_global_proxy',
|
50
|
+
fallback_proxy: 'remote_execution_fallback_proxy',
|
51
|
+
provider: options[:provider],
|
52
|
+
}
|
53
|
+
|
54
|
+
host_base[:detail] = _('Could not use any proxy for the %{provider} job. Consider configuring %{global_proxy}, ' +
|
55
|
+
'%{fallback_proxy} in settings') % settings
|
56
|
+
when :not_available
|
57
|
+
offline_proxies = selector.offline
|
58
|
+
settings = { :count => offline_proxies.count, :proxy_names => offline_proxies.map(&:name).join(', ') }
|
59
|
+
host_base[:detail] = n_('The only applicable proxy %{proxy_names} is down',
|
60
|
+
'All %{count} applicable proxies are down. Tried %{proxy_names}',
|
61
|
+
offline_proxies.count) % settings
|
62
|
+
else
|
63
|
+
winning_strategy = selector.strategies.find { |strategy| !proxies[strategy].empty? && proxies[strategy].include?(determined_proxy) }
|
64
|
+
end
|
65
|
+
|
66
|
+
{
|
67
|
+
:host => host,
|
68
|
+
:strategies => strategies,
|
69
|
+
:selected_proxy => determined_proxy,
|
70
|
+
:winning_strategy => winning_strategy,
|
71
|
+
}
|
72
|
+
end
|
73
|
+
|
74
|
+
case ENV['FORMAT']
|
75
|
+
when nil, 'verbose'
|
76
|
+
output_verbose(results, options[:provider])
|
77
|
+
when 'csv'
|
78
|
+
require 'csv'
|
79
|
+
output_csv(results)
|
80
|
+
when 'json'
|
81
|
+
require 'json'
|
82
|
+
puts JSON.generate(results)
|
83
|
+
when 'pretty-json'
|
84
|
+
require 'json'
|
85
|
+
puts JSON.pretty_generate(results)
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
def output_verbose(results, provider)
|
90
|
+
errors = [:not_defined, :not_available]
|
91
|
+
|
92
|
+
results.each do |host|
|
93
|
+
puts "=> Host #{host[:host]}"
|
94
|
+
host[:strategies].each do |strategy|
|
95
|
+
puts "==> Strategy #{strategy[:name]}"
|
96
|
+
unless strategy[:enabled]
|
97
|
+
puts " strategy is disabled"
|
98
|
+
puts
|
99
|
+
next
|
100
|
+
end
|
101
|
+
if strategy[:proxies].empty?
|
102
|
+
puts " no proxies available using this strategy"
|
103
|
+
else
|
104
|
+
strategy[:proxies].each_with_index do |proxy_record, i|
|
105
|
+
puts " #{i + 1}) #{proxy_record[:proxy]} - #{proxy_record[:count]} tasks"
|
106
|
+
end
|
107
|
+
end
|
108
|
+
puts
|
109
|
+
end
|
110
|
+
if errors.include? host[:selected_proxy]
|
111
|
+
puts host[:detail]
|
112
|
+
else
|
113
|
+
puts "As of now, #{provider} job would use proxy #{host[:selected_proxy]}, determined by strategy #{host[:winning_strategy]}."
|
114
|
+
end
|
115
|
+
puts
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
119
|
+
def output_csv(results)
|
120
|
+
writer = CSV.new($stdout)
|
121
|
+
writer << %w(host strategy proxy)
|
122
|
+
results.each do |host|
|
123
|
+
writer << [
|
124
|
+
host[:host].name,
|
125
|
+
host[:winning_strategy],
|
126
|
+
host[:selected_proxy],
|
127
|
+
]
|
128
|
+
end
|
129
|
+
writer.close
|
130
|
+
end
|
131
|
+
end
|
@@ -56,6 +56,28 @@ class RemoteExecutionProviderTest < ActiveSupport::TestCase
|
|
56
56
|
end
|
57
57
|
end
|
58
58
|
|
59
|
+
describe '.proxy_feature' do
|
60
|
+
# rubocop:disable Naming/ConstantName
|
61
|
+
it 'handles provider subclasses properly' do
|
62
|
+
old = ::RemoteExecutionProvider
|
63
|
+
|
64
|
+
class P2 < old
|
65
|
+
end
|
66
|
+
::RemoteExecutionProvider = P2
|
67
|
+
|
68
|
+
class CustomProvider < ::RemoteExecutionProvider
|
69
|
+
end
|
70
|
+
|
71
|
+
::RemoteExecutionProvider.register('custom', CustomProvider)
|
72
|
+
|
73
|
+
feature = CustomProvider.proxy_feature
|
74
|
+
_(feature).must_equal 'custom'
|
75
|
+
ensure
|
76
|
+
::RemoteExecutionProvider = old
|
77
|
+
end
|
78
|
+
# rubocop:enable Naming/ConstantName
|
79
|
+
end
|
80
|
+
|
59
81
|
describe '.provider_proxy_features' do
|
60
82
|
it 'returns correct values' do
|
61
83
|
RemoteExecutionProvider.stubs(:providers).returns(
|
@@ -25,6 +25,7 @@ import {
|
|
25
25
|
selectIsSubmitting,
|
26
26
|
selectRouterSearch,
|
27
27
|
selectIsLoading,
|
28
|
+
selectJobCategoriesResponse,
|
28
29
|
} from './JobWizardSelectors';
|
29
30
|
import { ScheduleType } from './steps/Schedule/ScheduleType';
|
30
31
|
import { ScheduleFuture } from './steps/Schedule/ScheduleFuture';
|
@@ -34,13 +35,18 @@ import ReviewDetails from './steps/ReviewDetails/';
|
|
34
35
|
import { useValidation } from './validation';
|
35
36
|
import { useAutoFill } from './autofill';
|
36
37
|
import { submit } from './submit';
|
38
|
+
import { generateDefaultDescription } from './JobWizardHelpers';
|
37
39
|
import './JobWizard.scss';
|
38
40
|
|
39
41
|
export const JobWizard = ({ rerunData }) => {
|
42
|
+
const jobCategoriesResponse = useSelector(selectJobCategoriesResponse);
|
40
43
|
const [jobTemplateID, setJobTemplateID] = useState(
|
41
|
-
rerunData?.template_invocations?.[0]?.template_id
|
44
|
+
rerunData?.template_invocations?.[0]?.template_id ||
|
45
|
+
jobCategoriesResponse?.default_template
|
46
|
+
);
|
47
|
+
const [category, setCategory] = useState(
|
48
|
+
rerunData?.job_category || jobCategoriesResponse?.default_category || ''
|
42
49
|
);
|
43
|
-
const [category, setCategory] = useState(rerunData?.job_category || '');
|
44
50
|
const [advancedValues, setAdvancedValues] = useState({ templateValues: {} });
|
45
51
|
const [templateValues, setTemplateValues] = useState({}); // TODO use templateValues in advanced fields - description https://github.com/theforeman/foreman_remote_execution/pull/605
|
46
52
|
const [scheduleValue, setScheduleValue] = useState(initialScheduleState);
|
@@ -56,6 +62,7 @@ export const JobWizard = ({ rerunData }) => {
|
|
56
62
|
? {
|
57
63
|
search: rerunData?.targeting?.search_query,
|
58
64
|
...rerunData.inputs,
|
65
|
+
...routerSearch,
|
59
66
|
}
|
60
67
|
: routerSearch
|
61
68
|
);
|
@@ -70,6 +77,7 @@ export const JobWizard = ({ rerunData }) => {
|
|
70
77
|
job_template: {
|
71
78
|
name,
|
72
79
|
execution_timeout_interval,
|
80
|
+
time_to_pickup,
|
73
81
|
description_format,
|
74
82
|
job_category,
|
75
83
|
},
|
@@ -78,8 +86,8 @@ export const JobWizard = ({ rerunData }) => {
|
|
78
86
|
concurrency_control = {},
|
79
87
|
},
|
80
88
|
}) => {
|
81
|
-
if (
|
82
|
-
setCategory(
|
89
|
+
if (category !== job_category) {
|
90
|
+
setCategory(job_category);
|
83
91
|
}
|
84
92
|
const advancedTemplateValues = {};
|
85
93
|
const defaultTemplateValues = {};
|
@@ -101,21 +109,19 @@ export const JobWizard = ({ rerunData }) => {
|
|
101
109
|
currentAdvancedValues[input.name] || input?.default || '';
|
102
110
|
});
|
103
111
|
}
|
104
|
-
const generateDefaultDescription = () => {
|
105
|
-
if (description_format) return description_format;
|
106
|
-
const allInputs = [...advancedInputs, ...inputs];
|
107
|
-
if (!allInputs.length) return name;
|
108
|
-
const inputsString = allInputs
|
109
|
-
.map(({ name: inputname }) => `${inputname}="%{${inputname}}"`)
|
110
|
-
.join(' ');
|
111
|
-
return `${name} with inputs ${inputsString}`;
|
112
|
-
};
|
113
112
|
return {
|
114
113
|
...currentAdvancedValues,
|
115
114
|
effectiveUserValue: effective_user?.value || '',
|
116
115
|
timeoutToKill: execution_timeout_interval || '',
|
116
|
+
timeToPickup: time_to_pickup || '',
|
117
117
|
templateValues: advancedTemplateValues,
|
118
|
-
description:
|
118
|
+
description:
|
119
|
+
generateDefaultDescription({
|
120
|
+
description_format,
|
121
|
+
advancedInputs,
|
122
|
+
inputs,
|
123
|
+
name,
|
124
|
+
}) || '',
|
119
125
|
isRandomizedOrdering: randomized_ordering,
|
120
126
|
sshUser: ssh_user || '',
|
121
127
|
timeSpan: concurrency_control.time_span || '',
|
@@ -123,6 +129,7 @@ export const JobWizard = ({ rerunData }) => {
|
|
123
129
|
};
|
124
130
|
});
|
125
131
|
},
|
132
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
126
133
|
[category.length]
|
127
134
|
);
|
128
135
|
useEffect(() => {
|
@@ -134,6 +141,9 @@ export const JobWizard = ({ rerunData }) => {
|
|
134
141
|
},
|
135
142
|
job_template: {
|
136
143
|
execution_timeout_interval: rerunData.execution_timeout_interval,
|
144
|
+
description_format: rerunData.description_format,
|
145
|
+
job_category: rerunData.job_category,
|
146
|
+
time_to_pickup: rerunData.time_to_pickup,
|
137
147
|
},
|
138
148
|
randomized_ordering: rerunData.targeting.randomized_ordering,
|
139
149
|
ssh_user: rerunData.ssh_user,
|
@@ -141,18 +151,39 @@ export const JobWizard = ({ rerunData }) => {
|
|
141
151
|
},
|
142
152
|
});
|
143
153
|
}
|
144
|
-
|
154
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
155
|
+
}, [rerunData]);
|
145
156
|
useEffect(() => {
|
146
157
|
if (jobTemplateID) {
|
147
158
|
dispatch(
|
148
159
|
get({
|
149
160
|
key: JOB_TEMPLATE,
|
150
161
|
url: `/ui_job_wizard/template/${jobTemplateID}`,
|
151
|
-
handleSuccess: rerunData
|
162
|
+
handleSuccess: rerunData
|
163
|
+
? ({
|
164
|
+
data: {
|
165
|
+
template_inputs = [],
|
166
|
+
advanced_template_inputs = [],
|
167
|
+
job_template: { name, description_format },
|
168
|
+
},
|
169
|
+
}) => {
|
170
|
+
setAdvancedValues(currentAdvancedValues => ({
|
171
|
+
...currentAdvancedValues,
|
172
|
+
description:
|
173
|
+
generateDefaultDescription({
|
174
|
+
description_format,
|
175
|
+
advancedInputs: advanced_template_inputs,
|
176
|
+
inputs: template_inputs,
|
177
|
+
name,
|
178
|
+
}) || '',
|
179
|
+
}));
|
180
|
+
}
|
181
|
+
: setDefaults,
|
152
182
|
})
|
153
183
|
);
|
154
184
|
}
|
155
|
-
|
185
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
186
|
+
}, [rerunData, jobTemplateID, dispatch]);
|
156
187
|
|
157
188
|
const [valid, setValid] = useValidation({
|
158
189
|
advancedValues,
|
@@ -187,7 +218,9 @@ export const JobWizard = ({ rerunData }) => {
|
|
187
218
|
setJobTemplate={setJobTemplateID}
|
188
219
|
category={category}
|
189
220
|
setCategory={setCategory}
|
190
|
-
isCategoryPreselected={
|
221
|
+
isCategoryPreselected={
|
222
|
+
!!rerunData || !!fills.feature || !!fills.template_id
|
223
|
+
}
|
191
224
|
/>
|
192
225
|
),
|
193
226
|
enableNext: isTemplate,
|
@@ -370,6 +403,7 @@ JobWizard.propTypes = {
|
|
370
403
|
time_span: PropTypes.number,
|
371
404
|
}),
|
372
405
|
execution_timeout_interval: PropTypes.number,
|
406
|
+
time_to_pickup: PropTypes.number,
|
373
407
|
remote_execution_feature_id: PropTypes.string,
|
374
408
|
template_invocations: PropTypes.arrayOf(
|
375
409
|
PropTypes.shape({
|
@@ -379,6 +413,7 @@ JobWizard.propTypes = {
|
|
379
413
|
})
|
380
414
|
),
|
381
415
|
inputs: PropTypes.object,
|
416
|
+
description_format: PropTypes.string,
|
382
417
|
}),
|
383
418
|
};
|
384
419
|
JobWizard.defaultProps = {
|
@@ -0,0 +1,15 @@
|
|
1
|
+
/* eslint-disable camelcase */
|
2
|
+
export const generateDefaultDescription = ({
|
3
|
+
description_format,
|
4
|
+
advancedInputs,
|
5
|
+
inputs,
|
6
|
+
name,
|
7
|
+
}) => {
|
8
|
+
if (description_format) return description_format;
|
9
|
+
const allInputs = [...advancedInputs, ...inputs];
|
10
|
+
if (!allInputs.length) return name;
|
11
|
+
const inputsString = allInputs
|
12
|
+
.map(({ name: inputname }) => `${inputname}="%{${inputname}}"`)
|
13
|
+
.join(' ');
|
14
|
+
return `${name} with inputs ${inputsString}`;
|
15
|
+
};
|
@@ -1,7 +1,15 @@
|
|
1
1
|
import React from 'react';
|
2
2
|
import PropTypes from 'prop-types';
|
3
3
|
import URI from 'urijs';
|
4
|
-
import {
|
4
|
+
import {
|
5
|
+
Alert,
|
6
|
+
Title,
|
7
|
+
Divider,
|
8
|
+
Skeleton,
|
9
|
+
Flex,
|
10
|
+
FlexItem,
|
11
|
+
Button,
|
12
|
+
} from '@patternfly/react-core';
|
5
13
|
import { sprintf, translate as __ } from 'foremanReact/common/I18n';
|
6
14
|
import { useAPI } from 'foremanReact/common/hooks/API/APIHooks';
|
7
15
|
import PageLayout from 'foremanReact/routes/common/PageLayout/PageLayout';
|
@@ -31,7 +39,7 @@ const JobWizardPageRerun = ({
|
|
31
39
|
const title = __('Run job');
|
32
40
|
const breadcrumbOptions = {
|
33
41
|
breadcrumbItems: [
|
34
|
-
{ caption: __('Jobs'), url: `/
|
42
|
+
{ caption: __('Jobs'), url: `/job_invocations` },
|
35
43
|
{ caption: title },
|
36
44
|
],
|
37
45
|
};
|
@@ -48,9 +56,25 @@ const JobWizardPageRerun = ({
|
|
48
56
|
searchable={false}
|
49
57
|
>
|
50
58
|
<React.Fragment>
|
51
|
-
<
|
52
|
-
|
53
|
-
|
59
|
+
<React.Fragment>
|
60
|
+
<Flex>
|
61
|
+
<FlexItem>
|
62
|
+
<Title headingLevel="h2" size="2xl">
|
63
|
+
{title}
|
64
|
+
</Title>
|
65
|
+
</FlexItem>
|
66
|
+
<FlexItem align={{ default: 'alignRight' }}>
|
67
|
+
<Button
|
68
|
+
variant="link"
|
69
|
+
component="a"
|
70
|
+
href={`/old/job_invocations/${id}/rerun${search}`}
|
71
|
+
>
|
72
|
+
{__('Use old form')}
|
73
|
+
</Button>
|
74
|
+
</FlexItem>
|
75
|
+
</Flex>
|
76
|
+
<Divider component="div" />
|
77
|
+
</React.Fragment>
|
54
78
|
{!status || status === STATUS.PENDING ? (
|
55
79
|
<div style={{ height: '400px' }}>
|
56
80
|
<Skeleton
|
@@ -24,11 +24,17 @@ export const filterJobTemplates = templates =>
|
|
24
24
|
export const selectJobTemplates = state =>
|
25
25
|
filterJobTemplates(selectAPIResponse(state, JOB_TEMPLATES)?.results);
|
26
26
|
|
27
|
+
export const selectJobTemplatesSearch = state =>
|
28
|
+
selectAPIResponse(state, JOB_TEMPLATES)?.search;
|
29
|
+
|
30
|
+
export const selectJobCategoriesResponse = state =>
|
31
|
+
selectAPIResponse(state, JOB_CATEGORIES) || {};
|
32
|
+
|
27
33
|
export const selectJobCategories = state =>
|
28
|
-
|
34
|
+
selectJobCategoriesResponse(state).job_categories || [];
|
29
35
|
|
30
36
|
export const selectWithKatello = state =>
|
31
|
-
|
37
|
+
selectJobCategoriesResponse(state).with_katello || false;
|
32
38
|
|
33
39
|
export const selectJobCategoriesStatus = state =>
|
34
40
|
selectAPIStatus(state, JOB_CATEGORIES);
|
@@ -64,6 +64,11 @@ describe('Job wizard fill', () => {
|
|
64
64
|
selector: 'input',
|
65
65
|
}).value
|
66
66
|
).toBe('1');
|
67
|
+
expect(
|
68
|
+
screen.getByLabelText('time to pickup', {
|
69
|
+
selector: 'input',
|
70
|
+
}).value
|
71
|
+
).toBe('25');
|
67
72
|
|
68
73
|
expect(
|
69
74
|
screen.getByLabelText('Concurrency level', {
|
@@ -1,3 +1,4 @@
|
|
1
|
+
/* eslint-disable max-lines */
|
1
2
|
import configureMockStore from 'redux-mock-store';
|
2
3
|
import hostsQuery from '../steps/HostsAndInputs/hosts.gql';
|
3
4
|
import hostgroupsQuery from '../steps/HostsAndInputs/hostgroups.gql';
|
@@ -14,6 +15,18 @@ export const jobTemplate = {
|
|
14
15
|
execution_timeout_interval: 2,
|
15
16
|
description: null,
|
16
17
|
};
|
18
|
+
export const pupptetJobTemplate = {
|
19
|
+
id: 163,
|
20
|
+
name: 'Puppet Agent Disable - Script Default',
|
21
|
+
template:
|
22
|
+
'<% if @host.operatingsystem.family == \'Debian\' -%>\nexport PATH=/opt/puppetlabs/bin:$PATH\n<% end -%>\npuppet agent --disable "<%= input("comment").present? ? input("comment") : "Disabled using Foreman Remote Execution" %> - <%= current_user %> - $(date "+%d/%m/%Y %H:%M")"',
|
23
|
+
snippet: false,
|
24
|
+
default: true,
|
25
|
+
job_category: 'Puppet',
|
26
|
+
provider_type: 'script',
|
27
|
+
execution_timeout_interval: 2,
|
28
|
+
description: null,
|
29
|
+
};
|
17
30
|
|
18
31
|
export const jobTemplates = [jobTemplate];
|
19
32
|
|
@@ -120,6 +133,7 @@ export const testSetup = (selectors, api) => {
|
|
120
133
|
selectors.selectJobCategories.mockImplementation(() => jobCategories);
|
121
134
|
selectors.selectJobTemplates.mockImplementation(() => [
|
122
135
|
jobTemplate,
|
136
|
+
pupptetJobTemplate,
|
123
137
|
{ ...jobTemplate, id: 2, name: 'template2' },
|
124
138
|
]);
|
125
139
|
selectors.selectJobTemplate.mockImplementation(() => jobTemplateResponse);
|
@@ -164,12 +178,21 @@ export const mockApi = api => {
|
|
164
178
|
} else if (action.key === 'JOB_TEMPLATE') {
|
165
179
|
handleSuccess &&
|
166
180
|
handleSuccess({
|
167
|
-
data:
|
181
|
+
data:
|
182
|
+
action.url === '/ui_job_wizard/template/163'
|
183
|
+
? { ...jobTemplateResponse, job_template: pupptetJobTemplate }
|
184
|
+
: jobTemplateResponse,
|
168
185
|
});
|
169
186
|
} else if (action.key === 'JOB_TEMPLATES') {
|
170
187
|
handleSuccess &&
|
171
188
|
handleSuccess({
|
172
|
-
data: {
|
189
|
+
data: {
|
190
|
+
results:
|
191
|
+
action.url.search() ===
|
192
|
+
'?search=job_category%3D%22Puppet%22&per_page=all'
|
193
|
+
? [pupptetJobTemplate]
|
194
|
+
: [jobTemplate],
|
195
|
+
},
|
173
196
|
});
|
174
197
|
} else if (action.key === 'HOST_IDS') {
|
175
198
|
handleSuccess &&
|
@@ -252,6 +275,7 @@ export const jobInvocation = {
|
|
252
275
|
time_span: 4,
|
253
276
|
},
|
254
277
|
execution_timeout_interval: 1,
|
278
|
+
time_to_pickup: 25,
|
255
279
|
remote_execution_feature_id: null,
|
256
280
|
template_invocations: [
|
257
281
|
{
|
@@ -17,19 +17,30 @@ export const useAutoFill = ({
|
|
17
17
|
|
18
18
|
useEffect(() => {
|
19
19
|
if (Object.keys(fills).length) {
|
20
|
-
const {
|
20
|
+
const {
|
21
|
+
'host_ids[]': hostIds,
|
22
|
+
search,
|
23
|
+
feature,
|
24
|
+
template_id: templateID,
|
25
|
+
...rest
|
26
|
+
} = { ...fills };
|
21
27
|
setFills({});
|
22
28
|
if (hostIds) {
|
29
|
+
const hostSearch = Array.isArray(hostIds)
|
30
|
+
? `id = ${hostIds.join(' or id = ')}`
|
31
|
+
: `id = ${hostIds}`;
|
23
32
|
dispatch(
|
24
33
|
get({
|
25
34
|
key: HOST_IDS,
|
26
35
|
url: '/api/hosts',
|
27
|
-
params: {
|
36
|
+
params: {
|
37
|
+
search: hostSearch,
|
38
|
+
},
|
28
39
|
handleSuccess: ({ data }) => {
|
29
40
|
setSelectedTargets(currentTargets => ({
|
30
41
|
...currentTargets,
|
31
|
-
hosts: (data.results || []).map(({ name }) => ({
|
32
|
-
id
|
42
|
+
hosts: (data.results || []).map(({ id, name }) => ({
|
43
|
+
id,
|
33
44
|
name,
|
34
45
|
})),
|
35
46
|
}));
|
@@ -37,9 +48,12 @@ export const useAutoFill = ({
|
|
37
48
|
})
|
38
49
|
);
|
39
50
|
}
|
40
|
-
if (search) {
|
51
|
+
if (search && !hostIds?.length) {
|
41
52
|
setHostsSearchQuery(search);
|
42
53
|
}
|
54
|
+
if (templateID) {
|
55
|
+
setJobTemplateID(+templateID);
|
56
|
+
}
|
43
57
|
if (feature) {
|
44
58
|
dispatch(
|
45
59
|
get({
|
@@ -56,11 +70,19 @@ export const useAutoFill = ({
|
|
56
70
|
const re = /inputs\[(?<input>.*)\]/g;
|
57
71
|
const input = re.exec(key)?.groups?.input;
|
58
72
|
if (input) {
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
73
|
+
if (typeof rest[key] === 'string') {
|
74
|
+
setTemplateValues(prev => ({ ...prev, [input]: rest[key] }));
|
75
|
+
} else {
|
76
|
+
const { value, advanced } = rest[key];
|
77
|
+
if (advanced) {
|
78
|
+
setAdvancedValues(prev => ({
|
79
|
+
...prev,
|
80
|
+
templateValues: { ...prev.templateValues, [input]: value },
|
81
|
+
}));
|
82
|
+
} else {
|
83
|
+
setTemplateValues(prev => ({ ...prev, [input]: value }));
|
84
|
+
}
|
85
|
+
}
|
64
86
|
}
|
65
87
|
});
|
66
88
|
}
|
data/webpack/JobWizard/index.js
CHANGED
@@ -1,14 +1,15 @@
|
|
1
1
|
import React from 'react';
|
2
|
-
import
|
2
|
+
import PropTypes from 'prop-types';
|
3
|
+
import { Title, Divider, Flex, FlexItem, Button } from '@patternfly/react-core';
|
3
4
|
import { translate as __ } from 'foremanReact/common/I18n';
|
4
5
|
import PageLayout from 'foremanReact/routes/common/PageLayout/PageLayout';
|
5
6
|
import { JobWizard } from './JobWizard';
|
6
7
|
|
7
|
-
const JobWizardPage = () => {
|
8
|
+
const JobWizardPage = ({ location: { search } }) => {
|
8
9
|
const title = __('Run job');
|
9
10
|
const breadcrumbOptions = {
|
10
11
|
breadcrumbItems: [
|
11
|
-
{ caption: __('Jobs'), url: `/
|
12
|
+
{ caption: __('Jobs'), url: `/job_invocations` },
|
12
13
|
{ caption: title },
|
13
14
|
],
|
14
15
|
};
|
@@ -19,9 +20,22 @@ const JobWizardPage = () => {
|
|
19
20
|
searchable={false}
|
20
21
|
>
|
21
22
|
<React.Fragment>
|
22
|
-
<
|
23
|
-
|
24
|
-
|
23
|
+
<Flex>
|
24
|
+
<FlexItem>
|
25
|
+
<Title headingLevel="h2" size="2xl">
|
26
|
+
{title}
|
27
|
+
</Title>
|
28
|
+
</FlexItem>
|
29
|
+
<FlexItem align={{ default: 'alignRight' }}>
|
30
|
+
<Button
|
31
|
+
variant="link"
|
32
|
+
component="a"
|
33
|
+
href={`/old/job_invocations/new${search}`}
|
34
|
+
>
|
35
|
+
{__('Use legacy form')}
|
36
|
+
</Button>
|
37
|
+
</FlexItem>
|
38
|
+
</Flex>
|
25
39
|
<Divider component="div" />
|
26
40
|
<JobWizard />
|
27
41
|
</React.Fragment>
|
@@ -29,4 +43,9 @@ const JobWizardPage = () => {
|
|
29
43
|
);
|
30
44
|
};
|
31
45
|
|
46
|
+
JobWizardPage.propTypes = {
|
47
|
+
location: PropTypes.shape({
|
48
|
+
search: PropTypes.string,
|
49
|
+
}).isRequired,
|
50
|
+
};
|
32
51
|
export default JobWizardPage;
|