foreman_remote_execution 16.3.2 → 16.4.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/app/lib/proxy_api/remote_execution_ssh.rb +9 -0
- data/app/models/concerns/foreman_remote_execution/host_extensions.rb +12 -4
- data/app/models/concerns/foreman_remote_execution/smart_proxy_extensions.rb +14 -0
- data/app/views/api/v2/smart_proxies/ca_pubkey.json.rabl +1 -0
- data/db/migrate/20250606125543_add_ca_pub_key_to_smart_proxy.rb +5 -0
- data/lib/foreman_remote_execution/plugin.rb +1 -0
- data/lib/foreman_remote_execution/version.rb +1 -1
- data/test/unit/concerns/host_extensions_test.rb +43 -0
- data/webpack/JobInvocationDetail/JobInvocationHostTable.js +93 -67
- data/webpack/JobInvocationDetail/index.js +4 -5
- data/webpack/JobWizard/JobWizard.js +11 -10
- data/webpack/JobWizard/autofill.js +10 -2
- data/webpack/JobWizard/steps/AdvancedFields/__tests__/__snapshots__/AdvancedFields.test.js.snap +1 -1
- data/webpack/JobWizard/steps/CategoryAndTemplate/index.js +25 -13
- data/webpack/JobWizard/steps/HostsAndInputs/SelectAPI.js +16 -11
- data/webpack/JobWizard/steps/form/ResourceSelect.js +18 -16
- data/webpack/JobWizard/steps/form/SearchSelect.js +6 -7
- data/webpack/JobWizard/validation.js +1 -3
- data/webpack/react_app/components/TargetingHosts/index.js +49 -32
- metadata +5 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 2db78eb6979bd486194e94b58c14978db6d08c02f675a042bd9827c9101b0639
|
|
4
|
+
data.tar.gz: c9060344c28a222b93fed7f26794fde15827030ae7b3b6197a66b9f86413b656
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 478f4cc9af2467020b4498bdd757d9f3b33a5e233c65b0b941cb0eda2deacf0689077907298e111c7cc1ec13e64508c9cd33d0870df70fe8a7247dd99e9433a9
|
|
7
|
+
data.tar.gz: f2bf38fe739e5043ba595448090a4231a52ef923464eec41b470d71c8ccdf3f83c8faec3a47627c5a606601d343b42302093a3a22474f57dded90375933a506e
|
|
@@ -11,6 +11,15 @@ module ::ProxyAPI
|
|
|
11
11
|
raise ProxyException.new(url, e, N_('Unable to fetch public key'))
|
|
12
12
|
end
|
|
13
13
|
|
|
14
|
+
def ca_pubkey
|
|
15
|
+
get('ca_pubkey')&.strip
|
|
16
|
+
rescue RestClient::ResourceNotFound => e
|
|
17
|
+
Rails.logger.warn(format(N_("Unable to fetch CA public key: %{error}"), error: e.message))
|
|
18
|
+
nil
|
|
19
|
+
rescue => e
|
|
20
|
+
raise ProxyException.new(url, e, N_('Unable to fetch CA public key'))
|
|
21
|
+
end
|
|
22
|
+
|
|
14
23
|
def drop_from_known_hosts(hostname)
|
|
15
24
|
delete('known_hosts/' + hostname)
|
|
16
25
|
rescue => e
|
|
@@ -108,7 +108,12 @@ module ForemanRemoteExecution
|
|
|
108
108
|
end
|
|
109
109
|
|
|
110
110
|
def remote_execution_ssh_keys
|
|
111
|
-
|
|
111
|
+
# only include public keys from SSH proxies that don't have SSH cert verification configured
|
|
112
|
+
remote_execution_proxies(%w(SSH Script), false).values.flatten.uniq.map { |proxy| proxy.pubkey if proxy.ca_pubkey.blank? }.compact.uniq
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def remote_execution_ssh_ca_keys
|
|
116
|
+
remote_execution_proxies(%w(SSH Script), false).values.flatten.uniq.map { |proxy| proxy.ca_pubkey }.compact.uniq
|
|
112
117
|
end
|
|
113
118
|
|
|
114
119
|
def drop_execution_interface_cache
|
|
@@ -139,10 +144,13 @@ module ForemanRemoteExecution
|
|
|
139
144
|
|
|
140
145
|
def extend_host_params_hash(params)
|
|
141
146
|
keys = remote_execution_ssh_keys
|
|
147
|
+
ca_keys = remote_execution_ssh_ca_keys
|
|
142
148
|
source = 'global'
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
149
|
+
{keys: keys, ca_keys: ca_keys}.each do |key_set_name, key_set|
|
|
150
|
+
if key_set.present?
|
|
151
|
+
value, safe_value = params.fetch("remote_execution_ssh_#{key_set_name}", {}).values_at(:value, :safe_value).map { |v| [v].flatten.compact }
|
|
152
|
+
params["remote_execution_ssh_#{key_set_name}"] = {:value => value + key_set, :safe_value => safe_value + key_set, :source => source}
|
|
153
|
+
end
|
|
146
154
|
end
|
|
147
155
|
[:remote_execution_ssh_user, :remote_execution_effective_user_method,
|
|
148
156
|
:remote_execution_connect_by_ip].each do |key|
|
|
@@ -11,6 +11,10 @@ module ForemanRemoteExecution
|
|
|
11
11
|
self[:pubkey] || update_pubkey
|
|
12
12
|
end
|
|
13
13
|
|
|
14
|
+
def ca_pubkey
|
|
15
|
+
self[:ca_pubkey] || update_ca_pubkey
|
|
16
|
+
end
|
|
17
|
+
|
|
14
18
|
def update_pubkey
|
|
15
19
|
return unless has_feature?(%w(SSH Script))
|
|
16
20
|
|
|
@@ -19,6 +23,15 @@ module ForemanRemoteExecution
|
|
|
19
23
|
key
|
|
20
24
|
end
|
|
21
25
|
|
|
26
|
+
def update_ca_pubkey
|
|
27
|
+
return unless has_feature?(%w(SSH Script))
|
|
28
|
+
|
|
29
|
+
# smart proxy is not required to have a CA pubkey, in which case an empty string is returned
|
|
30
|
+
key = ::ProxyAPI::RemoteExecutionSSH.new(:url => url).ca_pubkey&.presence
|
|
31
|
+
self.update_attribute(:ca_pubkey, key)
|
|
32
|
+
key
|
|
33
|
+
end
|
|
34
|
+
|
|
22
35
|
def drop_host_from_known_hosts(host)
|
|
23
36
|
::ProxyAPI::RemoteExecutionSSH.new(:url => url).drop_from_known_hosts(host)
|
|
24
37
|
end
|
|
@@ -26,6 +39,7 @@ module ForemanRemoteExecution
|
|
|
26
39
|
def refresh
|
|
27
40
|
errors = super
|
|
28
41
|
update_pubkey
|
|
42
|
+
update_ca_pubkey
|
|
29
43
|
errors
|
|
30
44
|
end
|
|
31
45
|
end
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
attribute :ca_pubkey => :remote_execution_ca_pubkey
|
|
@@ -213,6 +213,7 @@ Foreman::Plugin.register :foreman_remote_execution do
|
|
|
213
213
|
extend_template_helpers ForemanRemoteExecution::RendererMethods
|
|
214
214
|
|
|
215
215
|
extend_rabl_template 'api/v2/smart_proxies/main', 'api/v2/smart_proxies/pubkey'
|
|
216
|
+
extend_rabl_template 'api/v2/smart_proxies/main', 'api/v2/smart_proxies/ca_pubkey'
|
|
216
217
|
extend_rabl_template 'api/v2/interfaces/main', 'api/v2/interfaces/execution_flag'
|
|
217
218
|
extend_rabl_template 'api/v2/subnets/show', 'api/v2/subnets/remote_execution_proxies'
|
|
218
219
|
extend_rabl_template 'api/v2/hosts/main', 'api/v2/host/main'
|
|
@@ -12,6 +12,7 @@ class ForemanRemoteExecutionHostExtensionsTest < ActiveSupport::TestCase
|
|
|
12
12
|
|
|
13
13
|
before do
|
|
14
14
|
SmartProxy.any_instance.stubs(:pubkey).returns(sshkey)
|
|
15
|
+
SmartProxy.any_instance.stubs(:ca_pubkey).returns(nil)
|
|
15
16
|
Setting[:remote_execution_ssh_user] = 'root'
|
|
16
17
|
Setting[:remote_execution_effective_user_method] = 'sudo'
|
|
17
18
|
end
|
|
@@ -61,6 +62,48 @@ class ForemanRemoteExecutionHostExtensionsTest < ActiveSupport::TestCase
|
|
|
61
62
|
end
|
|
62
63
|
end
|
|
63
64
|
|
|
65
|
+
describe 'has ssh CA key configured' do
|
|
66
|
+
let(:host) { FactoryBot.create(:host, :with_execution) }
|
|
67
|
+
let(:sshkey) { 'ssh-rsa AAAAB3NzaC1yc2EAAAABJQ foo@example.com' }
|
|
68
|
+
let(:ca_sshkey) { 'ssh-rsa AAAAB3NzaC1yc2EAAAABJE bar@example.com' }
|
|
69
|
+
|
|
70
|
+
before do
|
|
71
|
+
SmartProxy.any_instance.stubs(:pubkey).returns(sshkey)
|
|
72
|
+
SmartProxy.any_instance.stubs(:ca_pubkey).returns(ca_sshkey)
|
|
73
|
+
Setting[:remote_execution_ssh_user] = 'root'
|
|
74
|
+
Setting[:remote_execution_effective_user_method] = 'sudo'
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
it 'has CA ssh keys in the parameters' do
|
|
78
|
+
assert_includes host.remote_execution_ssh_ca_keys, ca_sshkey
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
it 'excludes ssh keys from proxies that have SSH CA key configured' do
|
|
82
|
+
assert_empty host.remote_execution_ssh_keys
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
it 'merges ssh CA keys from host parameters and proxies' do
|
|
86
|
+
key = 'ssh-rsa not-even-a-key something@somewhere.com'
|
|
87
|
+
host.host_parameters << FactoryBot.create(:host_parameter, :host => host, :name => 'remote_execution_ssh_ca_keys', :value => [key])
|
|
88
|
+
assert_includes host.host_param('remote_execution_ssh_ca_keys'), key
|
|
89
|
+
assert_includes host.host_param('remote_execution_ssh_ca_keys'), ca_sshkey
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
it 'has ssh CA keys in the parameters even when no user specified' do
|
|
93
|
+
FactoryBot.create(:smart_proxy, :ssh)
|
|
94
|
+
host.interfaces.first.subnet.remote_execution_proxies.clear
|
|
95
|
+
User.current = nil
|
|
96
|
+
assert_includes host.remote_execution_ssh_ca_keys, ca_sshkey
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
it 'merges ssh CA key as a string from host parameters and proxies' do
|
|
100
|
+
key = 'ssh-rsa not-even-a-key something@somewhere.com'
|
|
101
|
+
host.host_parameters << FactoryBot.create(:host_parameter, :host => host, :name => 'remote_execution_ssh_ca_keys', :value => key)
|
|
102
|
+
assert_includes host.host_param('remote_execution_ssh_ca_keys'), key
|
|
103
|
+
assert_includes host.host_param('remote_execution_ssh_ca_keys'), ca_sshkey
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
|
|
64
107
|
context 'host has multiple nics' do
|
|
65
108
|
let(:host) { FactoryBot.build(:host, :with_execution) }
|
|
66
109
|
|
|
@@ -24,7 +24,13 @@ import TableIndexPage from 'foremanReact/components/PF4/TableIndexPage/TableInde
|
|
|
24
24
|
import { getControllerSearchProps } from 'foremanReact/constants';
|
|
25
25
|
import { Icon } from 'patternfly-react';
|
|
26
26
|
import PropTypes from 'prop-types';
|
|
27
|
-
import React, {
|
|
27
|
+
import React, {
|
|
28
|
+
useEffect,
|
|
29
|
+
useMemo,
|
|
30
|
+
useState,
|
|
31
|
+
useRef,
|
|
32
|
+
useCallback,
|
|
33
|
+
} from 'react';
|
|
28
34
|
import { FormattedMessage } from 'react-intl';
|
|
29
35
|
import { useHistory } from 'react-router-dom';
|
|
30
36
|
import { useForemanSettings } from 'foremanReact/Root/Context/ForemanContext';
|
|
@@ -131,19 +137,22 @@ const JobInvocationHostTable = ({
|
|
|
131
137
|
});
|
|
132
138
|
|
|
133
139
|
// Search filter
|
|
134
|
-
const constructFilter = (
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
140
|
+
const constructFilter = useCallback(
|
|
141
|
+
(filter = initialFilter, search = urlSearchQuery) => {
|
|
142
|
+
const dropdownFilterClause =
|
|
143
|
+
filter && filter !== 'all_statuses'
|
|
144
|
+
? `job_invocation.result = ${filter}`
|
|
145
|
+
: null;
|
|
146
|
+
const parts = [dropdownFilterClause, search];
|
|
147
|
+
return parts
|
|
148
|
+
.filter(x => x)
|
|
149
|
+
.map(fragment => `(${fragment})`)
|
|
150
|
+
.join(' AND ');
|
|
151
|
+
},
|
|
152
|
+
[initialFilter, urlSearchQuery]
|
|
153
|
+
);
|
|
145
154
|
|
|
146
|
-
const handleResponse = (data, key) => {
|
|
155
|
+
const handleResponse = useCallback((data, key) => {
|
|
147
156
|
if (key === JOB_INVOCATION_HOSTS) {
|
|
148
157
|
const ids = data.data.results.map(i => i.id);
|
|
149
158
|
|
|
@@ -152,76 +161,94 @@ const JobInvocationHostTable = ({
|
|
|
152
161
|
}
|
|
153
162
|
|
|
154
163
|
setStatus(STATUS_UPPERCASE.RESOLVED);
|
|
155
|
-
};
|
|
164
|
+
}, []);
|
|
156
165
|
|
|
157
166
|
// Call hosts data with params
|
|
158
|
-
const makeApiCall = (
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
response
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
167
|
+
const makeApiCall = useCallback(
|
|
168
|
+
(requestParams, callParams = {}) => {
|
|
169
|
+
dispatch(
|
|
170
|
+
APIActions.get({
|
|
171
|
+
key: callParams.key ?? ALL_JOB_HOSTS,
|
|
172
|
+
url: callParams.url ?? `/api/job_invocations/${id}/hosts`,
|
|
173
|
+
params: requestParams,
|
|
174
|
+
handleSuccess: data => handleResponse(data, callParams.key),
|
|
175
|
+
handleError: () => setStatus(STATUS_UPPERCASE.ERROR),
|
|
176
|
+
errorToast: ({ response }) =>
|
|
177
|
+
response?.data?.error?.full_messages?.[0] || response,
|
|
178
|
+
})
|
|
179
|
+
);
|
|
180
|
+
},
|
|
181
|
+
[dispatch, id, handleResponse]
|
|
182
|
+
);
|
|
171
183
|
|
|
172
|
-
const filterApiCall =
|
|
173
|
-
|
|
184
|
+
const filterApiCall = useCallback(
|
|
185
|
+
newAPIOptions => {
|
|
186
|
+
const newParams = newAPIOptions?.params ?? newAPIOptions ?? {};
|
|
174
187
|
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
188
|
+
const filterSearch = constructFilter(
|
|
189
|
+
initialFilter,
|
|
190
|
+
newParams.search ?? urlSearchQuery
|
|
191
|
+
);
|
|
179
192
|
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
193
|
+
const finalParams = {
|
|
194
|
+
...defaultParams,
|
|
195
|
+
...newParams,
|
|
196
|
+
};
|
|
184
197
|
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
198
|
+
if (filterSearch === AWAITING_STATUS_FILTER) {
|
|
199
|
+
finalParams.awaiting = 'true';
|
|
200
|
+
} else if (filterSearch !== '') {
|
|
201
|
+
finalParams.search = filterSearch;
|
|
202
|
+
}
|
|
190
203
|
|
|
191
|
-
|
|
204
|
+
makeApiCall(finalParams, { key: JOB_INVOCATION_HOSTS });
|
|
192
205
|
|
|
193
|
-
|
|
206
|
+
const urlSearchParams = new URLSearchParams(window.location.search);
|
|
194
207
|
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
208
|
+
['page', 'per_page', 'order'].forEach(key => {
|
|
209
|
+
if (finalParams[key]) urlSearchParams.set(key, finalParams[key]);
|
|
210
|
+
});
|
|
198
211
|
|
|
199
|
-
|
|
200
|
-
|
|
212
|
+
history.push({ search: urlSearchParams.toString() });
|
|
213
|
+
},
|
|
214
|
+
[
|
|
215
|
+
initialFilter,
|
|
216
|
+
urlSearchQuery,
|
|
217
|
+
defaultParams,
|
|
218
|
+
makeApiCall,
|
|
219
|
+
history,
|
|
220
|
+
constructFilter,
|
|
221
|
+
]
|
|
222
|
+
);
|
|
201
223
|
|
|
202
224
|
// Filter change
|
|
203
|
-
const handleFilterChange =
|
|
204
|
-
|
|
205
|
-
|
|
225
|
+
const handleFilterChange = useCallback(
|
|
226
|
+
newFilter => {
|
|
227
|
+
onFilterUpdate(newFilter);
|
|
228
|
+
},
|
|
229
|
+
[onFilterUpdate]
|
|
230
|
+
);
|
|
206
231
|
|
|
207
232
|
// Effects
|
|
208
233
|
// run after mount
|
|
234
|
+
const initializedRef = useRef(false);
|
|
209
235
|
useEffect(() => {
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
236
|
+
if (!initializedRef.current) {
|
|
237
|
+
// Job Invo template load
|
|
238
|
+
makeApiCall(
|
|
239
|
+
{},
|
|
240
|
+
{
|
|
241
|
+
url: `/job_invocations/${id}/hosts`,
|
|
242
|
+
key: LIST_TEMPLATE_INVOCATIONS,
|
|
243
|
+
}
|
|
244
|
+
);
|
|
245
|
+
|
|
246
|
+
if (initialFilter === '') {
|
|
247
|
+
onFilterUpdate('all_statuses');
|
|
216
248
|
}
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
if (initialFilter === '') {
|
|
220
|
-
onFilterUpdate('all_statuses');
|
|
249
|
+
initializedRef.current = true;
|
|
221
250
|
}
|
|
222
|
-
|
|
223
|
-
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
224
|
-
}, []);
|
|
251
|
+
}, [makeApiCall, id, initialFilter, onFilterUpdate]);
|
|
225
252
|
|
|
226
253
|
useEffect(() => {
|
|
227
254
|
if (initialFilter !== '') filterApiCall();
|
|
@@ -230,8 +257,7 @@ const JobInvocationHostTable = ({
|
|
|
230
257
|
prevStatusLabel.current = statusLabel;
|
|
231
258
|
filterApiCall();
|
|
232
259
|
}
|
|
233
|
-
|
|
234
|
-
}, [initialFilter, statusLabel, id]);
|
|
260
|
+
}, [initialFilter, statusLabel, id, filterApiCall]);
|
|
235
261
|
|
|
236
262
|
const {
|
|
237
263
|
updateSearchQuery: updateSearchQueryBulk,
|
|
@@ -79,15 +79,14 @@ const JobInvocationDetailPage = ({
|
|
|
79
79
|
return () => {
|
|
80
80
|
dispatch(stopInterval(JOB_INVOCATION_KEY));
|
|
81
81
|
};
|
|
82
|
-
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
83
82
|
}, [dispatch, id, finished, autoRefresh]);
|
|
84
83
|
|
|
84
|
+
const taskId = task?.id;
|
|
85
85
|
useEffect(() => {
|
|
86
|
-
if (
|
|
87
|
-
dispatch(getTask(`${
|
|
86
|
+
if (taskId !== undefined) {
|
|
87
|
+
dispatch(getTask(`${taskId}`));
|
|
88
88
|
}
|
|
89
|
-
|
|
90
|
-
}, [dispatch, task?.id]);
|
|
89
|
+
}, [dispatch, taskId]);
|
|
91
90
|
|
|
92
91
|
const pageStatus =
|
|
93
92
|
items.id === undefined
|
|
@@ -89,9 +89,6 @@ export const JobWizard = ({ rerunData }) => {
|
|
|
89
89
|
concurrency_control = {},
|
|
90
90
|
},
|
|
91
91
|
}) => {
|
|
92
|
-
if (category !== job_category) {
|
|
93
|
-
setCategory(job_category);
|
|
94
|
-
}
|
|
95
92
|
const advancedTemplateValues = {};
|
|
96
93
|
const defaultTemplateValues = {};
|
|
97
94
|
const inputs = template_inputs;
|
|
@@ -131,8 +128,7 @@ export const JobWizard = ({ rerunData }) => {
|
|
|
131
128
|
};
|
|
132
129
|
});
|
|
133
130
|
},
|
|
134
|
-
|
|
135
|
-
[category.length]
|
|
131
|
+
[setTemplateValues, setAdvancedValues]
|
|
136
132
|
);
|
|
137
133
|
useEffect(() => {
|
|
138
134
|
if (rerunData) {
|
|
@@ -153,8 +149,7 @@ export const JobWizard = ({ rerunData }) => {
|
|
|
153
149
|
},
|
|
154
150
|
});
|
|
155
151
|
}
|
|
156
|
-
|
|
157
|
-
}, [rerunData]);
|
|
152
|
+
}, [rerunData, setDefaults]);
|
|
158
153
|
useEffect(() => {
|
|
159
154
|
if (jobTemplateID) {
|
|
160
155
|
dispatch(
|
|
@@ -199,8 +194,14 @@ export const JobWizard = ({ rerunData }) => {
|
|
|
199
194
|
})
|
|
200
195
|
);
|
|
201
196
|
}
|
|
202
|
-
|
|
203
|
-
|
|
197
|
+
}, [
|
|
198
|
+
rerunData,
|
|
199
|
+
jobTemplateID,
|
|
200
|
+
dispatch,
|
|
201
|
+
setDefaults,
|
|
202
|
+
setTemplateValues,
|
|
203
|
+
setAdvancedValues,
|
|
204
|
+
]);
|
|
204
205
|
|
|
205
206
|
const [isStartsBeforeError, setIsStartsBeforeError] = useState(false);
|
|
206
207
|
const [isStartsAtError, setIsStartsAtError] = useState(false);
|
|
@@ -514,7 +515,7 @@ JobWizard.propTypes = {
|
|
|
514
515
|
}),
|
|
515
516
|
execution_timeout_interval: PropTypes.number,
|
|
516
517
|
time_to_pickup: PropTypes.number,
|
|
517
|
-
remote_execution_feature_id: PropTypes.
|
|
518
|
+
remote_execution_feature_id: PropTypes.number,
|
|
518
519
|
template_invocations: PropTypes.arrayOf(
|
|
519
520
|
PropTypes.shape({
|
|
520
521
|
template_id: PropTypes.number,
|
|
@@ -95,6 +95,14 @@ export const useAutoFill = ({
|
|
|
95
95
|
});
|
|
96
96
|
}
|
|
97
97
|
}
|
|
98
|
-
|
|
99
|
-
|
|
98
|
+
}, [
|
|
99
|
+
fills,
|
|
100
|
+
setFills,
|
|
101
|
+
setSelectedTargets,
|
|
102
|
+
setHostsSearchQuery,
|
|
103
|
+
setJobTemplateID,
|
|
104
|
+
setTemplateValues,
|
|
105
|
+
setAdvancedValues,
|
|
106
|
+
dispatch,
|
|
107
|
+
]);
|
|
100
108
|
};
|
|
@@ -55,8 +55,13 @@ const ConnectedCategoryAndTemplate = ({
|
|
|
55
55
|
})
|
|
56
56
|
);
|
|
57
57
|
}
|
|
58
|
-
|
|
59
|
-
|
|
58
|
+
}, [
|
|
59
|
+
jobCategoriesStatus,
|
|
60
|
+
dispatch,
|
|
61
|
+
isCategoryPreselected,
|
|
62
|
+
setCategory,
|
|
63
|
+
setJobTemplate,
|
|
64
|
+
]);
|
|
60
65
|
|
|
61
66
|
const jobCategories = useSelector(selectJobCategories);
|
|
62
67
|
const jobTemplatesSearch = useSelector(selectJobTemplatesSearch);
|
|
@@ -73,22 +78,29 @@ const ConnectedCategoryAndTemplate = ({
|
|
|
73
78
|
per_page: 'all',
|
|
74
79
|
}),
|
|
75
80
|
handleSuccess: response => {
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
81
|
+
const filteredTemplates = filterJobTemplates(
|
|
82
|
+
response?.data?.results
|
|
83
|
+
);
|
|
84
|
+
setJobTemplate(current => {
|
|
85
|
+
// Check if current template is in the new category's template list.
|
|
86
|
+
// This preserves the user's selection when changing categories on rerun,
|
|
87
|
+
// preventing the category from flashing and reverting back (Issue #38899).
|
|
88
|
+
// We check the state value (current) rather than the prop to avoid race conditions.
|
|
89
|
+
if (
|
|
90
|
+
current &&
|
|
91
|
+
filteredTemplates.some(template => template.id === current)
|
|
92
|
+
) {
|
|
93
|
+
return current;
|
|
94
|
+
}
|
|
95
|
+
// Otherwise, select the first template from the new category
|
|
96
|
+
return Number(filteredTemplates[0]?.id) || null;
|
|
97
|
+
});
|
|
85
98
|
},
|
|
86
99
|
})
|
|
87
100
|
);
|
|
88
101
|
}
|
|
89
102
|
}
|
|
90
|
-
|
|
91
|
-
}, [category, dispatch]);
|
|
103
|
+
}, [category, dispatch, jobTemplatesSearch, setJobTemplate]);
|
|
92
104
|
|
|
93
105
|
const jobTemplates = useSelector(selectJobTemplates);
|
|
94
106
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import React from 'react';
|
|
1
|
+
import React, { useCallback } from 'react';
|
|
2
2
|
import { useSelector, useDispatch } from 'react-redux';
|
|
3
3
|
import URI from 'urijs';
|
|
4
4
|
import { SelectVariant } from '@patternfly/react-core/deprecated';
|
|
@@ -8,16 +8,21 @@ import { SearchSelect } from '../form/SearchSelect';
|
|
|
8
8
|
|
|
9
9
|
export const useNameSearchAPI = (apiKey, url) => {
|
|
10
10
|
const dispatch = useDispatch();
|
|
11
|
-
|
|
12
|
-
const onSearch =
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
11
|
+
|
|
12
|
+
const onSearch = useCallback(
|
|
13
|
+
search => {
|
|
14
|
+
const uri = new URI(url);
|
|
15
|
+
dispatch(
|
|
16
|
+
get({
|
|
17
|
+
key: apiKey,
|
|
18
|
+
url: uri.addSearch({
|
|
19
|
+
search: `name~"${search}"`,
|
|
20
|
+
}),
|
|
21
|
+
})
|
|
22
|
+
);
|
|
23
|
+
},
|
|
24
|
+
[dispatch, apiKey, url]
|
|
25
|
+
);
|
|
21
26
|
|
|
22
27
|
const response = useSelector(state => selectResponse(state, apiKey));
|
|
23
28
|
const isLoading = useSelector(state => selectIsLoading(state, apiKey));
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import React, { useState, useEffect } from 'react';
|
|
1
|
+
import React, { useState, useEffect, useRef, useCallback } from 'react';
|
|
2
2
|
import PropTypes from 'prop-types';
|
|
3
3
|
import {
|
|
4
4
|
Select,
|
|
@@ -25,28 +25,30 @@ export const ResourceSelect = ({
|
|
|
25
25
|
const { perPage } = useForemanSettings();
|
|
26
26
|
const maxResults = perPage;
|
|
27
27
|
const dispatch = useDispatch();
|
|
28
|
-
const
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
28
|
+
const onSearch = useCallback(
|
|
29
|
+
search => {
|
|
30
|
+
const uri = new URI(url);
|
|
31
|
+
dispatch(
|
|
32
|
+
get({
|
|
33
|
+
key: apiKey,
|
|
34
|
+
url: uri.addSearch(search),
|
|
35
|
+
})
|
|
36
|
+
);
|
|
37
|
+
},
|
|
38
|
+
[dispatch, apiKey, url]
|
|
39
|
+
);
|
|
37
40
|
|
|
38
41
|
const response = useSelector(state => selectResponse(state, apiKey));
|
|
39
42
|
const isLoading = useSelector(state => selectIsLoading(state, apiKey));
|
|
40
43
|
const [isOpen, setIsOpen] = useState(false);
|
|
41
44
|
const [typingTimeout, setTypingTimeout] = useState(null);
|
|
45
|
+
const initializedRef = useRef(false);
|
|
42
46
|
useEffect(() => {
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
47
|
+
if (!initializedRef.current) {
|
|
48
|
+
onSearch(selected ? { id: selected } : {});
|
|
49
|
+
initializedRef.current = true;
|
|
46
50
|
}
|
|
47
|
-
|
|
48
|
-
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
49
|
-
}, []);
|
|
51
|
+
}, [onSearch, selected]);
|
|
50
52
|
let selectOptions = [];
|
|
51
53
|
if (response.subtotal > maxResults) {
|
|
52
54
|
selectOptions = [
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import React, { useState, useEffect } from 'react';
|
|
1
|
+
import React, { useState, useEffect, useRef } from 'react';
|
|
2
2
|
import PropTypes from 'prop-types';
|
|
3
3
|
import {
|
|
4
4
|
Select,
|
|
@@ -25,14 +25,13 @@ export const SearchSelect = ({
|
|
|
25
25
|
const [onSearch, response, isLoading] = useNameSearch(apiKey, url);
|
|
26
26
|
const [isOpen, setIsOpen] = useState(false);
|
|
27
27
|
const [typingTimeout, setTypingTimeout] = useState(null);
|
|
28
|
+
const initializedRef = useRef(false);
|
|
28
29
|
useEffect(() => {
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
30
|
+
if (!initializedRef.current) {
|
|
31
|
+
onSearch(selected.name || '');
|
|
32
|
+
initializedRef.current = true;
|
|
32
33
|
}
|
|
33
|
-
|
|
34
|
-
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
35
|
-
}, []);
|
|
34
|
+
}, [onSearch, selected.name]);
|
|
36
35
|
let selectOptions = [];
|
|
37
36
|
if (response.subtotal > maxResults) {
|
|
38
37
|
selectOptions = [
|
|
@@ -46,8 +46,6 @@ export const useValidation = ({ advancedValues, templateValues }) => {
|
|
|
46
46
|
setValid(currValid => ({ ...currValid, advanced: false }));
|
|
47
47
|
}
|
|
48
48
|
});
|
|
49
|
-
|
|
50
|
-
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
51
|
-
}, [advancedValues, templateValues]);
|
|
49
|
+
}, [advancedValues, templateValues, templateInputs, advancedTemplateInputs]);
|
|
52
50
|
return [valid, setValid];
|
|
53
51
|
};
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import React, { useEffect, useState } from 'react';
|
|
1
|
+
import React, { useEffect, useState, useCallback, useRef } from 'react';
|
|
2
2
|
import { useSelector, useDispatch } from 'react-redux';
|
|
3
3
|
|
|
4
4
|
import { get } from 'foremanReact/redux/API';
|
|
@@ -50,38 +50,55 @@ const WrappedTargetingHosts = () => {
|
|
|
50
50
|
const [apiUrl, setApiUrl] = useState(getApiUrl(searchQuery, pagination));
|
|
51
51
|
const intervalExists = useSelector(selectIntervalExists);
|
|
52
52
|
|
|
53
|
-
const
|
|
54
|
-
const defaultPagination = { page: 1, per_page: pagination.per_page };
|
|
55
|
-
stopApiInterval();
|
|
56
|
-
|
|
57
|
-
setApiUrl(getApiUrl(buildSearchQuery(query, status), defaultPagination));
|
|
58
|
-
setSearchQuery(query);
|
|
59
|
-
setPagination(defaultPagination);
|
|
60
|
-
};
|
|
61
|
-
|
|
62
|
-
const handlePagination = args => {
|
|
63
|
-
stopApiInterval();
|
|
64
|
-
setPagination(args);
|
|
65
|
-
setApiUrl(getApiUrl(buildSearchQuery(searchQuery, statusFilter), args));
|
|
66
|
-
};
|
|
67
|
-
|
|
68
|
-
const stopApiInterval = () => {
|
|
53
|
+
const stopApiInterval = useCallback(() => {
|
|
69
54
|
if (intervalExists) {
|
|
70
55
|
dispatch(stopInterval(TARGETING_HOSTS));
|
|
71
56
|
}
|
|
72
|
-
};
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
57
|
+
}, [dispatch, intervalExists]);
|
|
58
|
+
|
|
59
|
+
// Use ref to avoid infinite loop from handleSearch depending on pagination.per_page
|
|
60
|
+
const perPageRef = useRef(pagination.per_page);
|
|
61
|
+
|
|
62
|
+
const handleSearch = useCallback(
|
|
63
|
+
(query, status) => {
|
|
64
|
+
const defaultPagination = { page: 1, per_page: perPageRef.current };
|
|
65
|
+
stopApiInterval();
|
|
66
|
+
|
|
67
|
+
setApiUrl(getApiUrl(buildSearchQuery(query, status), defaultPagination));
|
|
68
|
+
setSearchQuery(query);
|
|
69
|
+
setPagination(defaultPagination);
|
|
70
|
+
},
|
|
71
|
+
[stopApiInterval]
|
|
72
|
+
);
|
|
73
|
+
|
|
74
|
+
// Keep ref in sync with pagination
|
|
75
|
+
useEffect(() => {
|
|
76
|
+
perPageRef.current = pagination.per_page;
|
|
77
|
+
}, [pagination.per_page]);
|
|
78
|
+
|
|
79
|
+
const handlePagination = useCallback(
|
|
80
|
+
args => {
|
|
81
|
+
stopApiInterval();
|
|
82
|
+
setPagination(args);
|
|
83
|
+
setApiUrl(getApiUrl(buildSearchQuery(searchQuery, statusFilter), args));
|
|
84
|
+
},
|
|
85
|
+
[searchQuery, statusFilter, stopApiInterval]
|
|
86
|
+
);
|
|
87
|
+
|
|
88
|
+
const getData = useCallback(
|
|
89
|
+
url =>
|
|
90
|
+
withInterval(
|
|
91
|
+
get({
|
|
92
|
+
key: TARGETING_HOSTS,
|
|
93
|
+
url,
|
|
94
|
+
handleError: () => {
|
|
95
|
+
dispatch(stopInterval(TARGETING_HOSTS));
|
|
96
|
+
},
|
|
97
|
+
}),
|
|
98
|
+
1000
|
|
99
|
+
),
|
|
100
|
+
[dispatch]
|
|
101
|
+
);
|
|
85
102
|
|
|
86
103
|
useEffect(() => {
|
|
87
104
|
dispatch(getData(apiUrl));
|
|
@@ -93,11 +110,11 @@ const WrappedTargetingHosts = () => {
|
|
|
93
110
|
return () => {
|
|
94
111
|
dispatch(stopInterval(TARGETING_HOSTS));
|
|
95
112
|
};
|
|
96
|
-
}, [dispatch, apiUrl, autoRefresh]);
|
|
113
|
+
}, [dispatch, apiUrl, autoRefresh, getData]);
|
|
97
114
|
|
|
98
115
|
useEffect(() => {
|
|
99
116
|
handleSearch(searchQuery, statusFilter);
|
|
100
|
-
}, [statusFilter, searchQuery]);
|
|
117
|
+
}, [statusFilter, searchQuery, handleSearch]);
|
|
101
118
|
|
|
102
119
|
return (
|
|
103
120
|
<TargetingHostsPage
|
metadata
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: foreman_remote_execution
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 16.
|
|
4
|
+
version: 16.4.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Foreman Remote Execution team
|
|
8
8
|
bindir: bin
|
|
9
9
|
cert_chain: []
|
|
10
|
-
date:
|
|
10
|
+
date: 2026-01-06 00:00:00.000000000 Z
|
|
11
11
|
dependencies:
|
|
12
12
|
- !ruby/object:Gem::Dependency
|
|
13
13
|
name: deface
|
|
@@ -223,6 +223,7 @@ files:
|
|
|
223
223
|
- app/views/api/v2/remote_execution_features/index.json.rabl
|
|
224
224
|
- app/views/api/v2/remote_execution_features/main.json.rabl
|
|
225
225
|
- app/views/api/v2/remote_execution_features/show.json.rabl
|
|
226
|
+
- app/views/api/v2/smart_proxies/ca_pubkey.json.rabl
|
|
226
227
|
- app/views/api/v2/smart_proxies/pubkey.json.rabl
|
|
227
228
|
- app/views/api/v2/subnets/remote_execution_proxies.json.rabl
|
|
228
229
|
- app/views/api/v2/template_invocations/base.json.rabl
|
|
@@ -340,6 +341,7 @@ files:
|
|
|
340
341
|
- db/migrate/20240522093412_add_smart_proxy_id_to_template_invocation.rb
|
|
341
342
|
- db/migrate/20240522093413_migrate_smart_proxy_ids_to_template_invocations.rb
|
|
342
343
|
- db/migrate/20241126150849_remove_remote_execution_workers_pool_size.rb
|
|
344
|
+
- db/migrate/20250606125543_add_ca_pub_key_to_smart_proxy.rb
|
|
343
345
|
- db/seeds.d/100-assign_features_with_templates.rb
|
|
344
346
|
- db/seeds.d/20-permissions.rb
|
|
345
347
|
- db/seeds.d/50-notification_blueprints.rb
|
|
@@ -606,7 +608,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
606
608
|
- !ruby/object:Gem::Version
|
|
607
609
|
version: '0'
|
|
608
610
|
requirements: []
|
|
609
|
-
rubygems_version:
|
|
611
|
+
rubygems_version: 4.0.3
|
|
610
612
|
specification_version: 4
|
|
611
613
|
summary: A plugin bringing remote execution to the Foreman, completing the config
|
|
612
614
|
management functionality with remote management functionality.
|