foreman_remote_execution 3.3.7 → 4.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.github/workflows/ci.yml +1 -1
- data/app/controllers/job_invocations_controller.rb +22 -8
- data/app/lib/actions/remote_execution/run_host_job.rb +1 -1
- data/app/lib/actions/remote_execution/run_hosts_job.rb +1 -1
- data/app/lib/foreman_remote_execution/renderer/scope/input.rb +35 -0
- data/app/models/job_invocation.rb +6 -3
- data/app/models/job_invocation_composer.rb +2 -2
- data/app/models/remote_execution_provider.rb +2 -2
- data/app/models/setting/remote_execution.rb +2 -2
- data/app/models/ssh_execution_provider.rb +1 -1
- data/app/views/job_invocations/_form.html.erb +1 -1
- data/app/views/job_invocations/_tab_hosts.html.erb +1 -20
- data/app/views/job_invocations/show.html.erb +3 -0
- data/app/views/job_invocations/show.json.erb +2 -1
- data/db/migrate/20200623073022_rename_sudo_password_to_effective_user_password.rb +34 -0
- data/db/seeds.d/20-permissions.rb +9 -0
- data/lib/foreman_remote_execution/engine.rb +4 -10
- data/lib/foreman_remote_execution/version.rb +1 -1
- data/test/functional/api/v2/job_invocations_controller_test.rb +64 -1
- data/test/functional/job_invocations_controller_test.rb +71 -0
- data/test/support/remote_execution_helper.rb +5 -0
- data/test/unit/actions/run_host_job_test.rb +3 -3
- data/test/unit/actions/run_hosts_job_test.rb +1 -1
- data/test/unit/job_invocation_composer_test.rb +5 -5
- data/test/unit/remote_execution_provider_test.rb +6 -6
- data/webpack/__mocks__/foremanReact/components/Pagination/PaginationWrapper.js +2 -0
- data/webpack/__mocks__/foremanReact/components/SearchBar.js +2 -0
- data/webpack/__mocks__/foremanReact/constants.js +21 -0
- data/webpack/__mocks__/foremanReact/redux/API/APISelectors.js +2 -0
- data/webpack/__mocks__/foremanReact/redux/middlewares/IntervalMiddleware/IntervalSelectors.js +1 -0
- data/webpack/react_app/components/TargetingHosts/TargetingHosts.js +21 -15
- data/webpack/react_app/components/TargetingHosts/TargetingHostsHelpers.js +10 -0
- data/webpack/react_app/components/TargetingHosts/TargetingHostsPage.js +62 -0
- data/webpack/react_app/components/TargetingHosts/TargetingHostsPage.scss +6 -0
- data/webpack/react_app/components/TargetingHosts/TargetingHostsSelectors.js +10 -2
- data/webpack/react_app/components/TargetingHosts/__tests__/TargetingHostsPage.test.js +9 -0
- data/webpack/react_app/components/TargetingHosts/__tests__/TargetingHostsSelectors.test.js +26 -0
- data/webpack/react_app/components/TargetingHosts/__tests__/__snapshots__/TargetingHosts.test.js.snap +17 -2
- data/webpack/react_app/components/TargetingHosts/__tests__/__snapshots__/TargetingHostsPage.test.js.snap +68 -0
- data/webpack/react_app/components/TargetingHosts/__tests__/__snapshots__/TargetingHostsSelectors.test.js.snap +11 -0
- data/webpack/react_app/components/TargetingHosts/__tests__/fixtures.js +35 -19
- data/webpack/react_app/components/TargetingHosts/index.js +73 -13
- metadata +17 -3
- data/webpack/react_app/components/TargetingHosts/TargetingHostsActions.js +0 -8
@@ -1,6 +1,7 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
require 'test_plugin_helper'
|
4
|
+
require_relative '../support/remote_execution_helper'
|
4
5
|
|
5
6
|
class JobInvocationsControllerTest < ActionController::TestCase
|
6
7
|
test 'should parse inputs coming from the URL params' do
|
@@ -58,4 +59,74 @@ class JobInvocationsControllerTest < ActionController::TestCase
|
|
58
59
|
post :new, params: params, session: set_session_user
|
59
60
|
assert_response :success
|
60
61
|
end
|
62
|
+
|
63
|
+
context 'restricted access' do
|
64
|
+
setup do
|
65
|
+
@admin = users(:admin)
|
66
|
+
@user = FactoryBot.create(:user, mail: 'test23@test.foreman.com', admin: false)
|
67
|
+
@invocation = FactoryBot.create(:job_invocation, :with_template, :with_task)
|
68
|
+
@invocation2 = FactoryBot.create(:job_invocation, :with_template, :with_task)
|
69
|
+
|
70
|
+
@invocation.task.update(user: @admin)
|
71
|
+
@invocation2.task.update(user: @user)
|
72
|
+
|
73
|
+
setup_user 'view', 'hosts', nil, @user
|
74
|
+
setup_user 'view', 'job_invocations', 'user = current_user', @user
|
75
|
+
setup_user 'create', 'job_invocations', 'user = current_user', @user
|
76
|
+
setup_user 'cancel', 'job_invocations', 'user = current_user', @user
|
77
|
+
end
|
78
|
+
|
79
|
+
context 'without user filter' do
|
80
|
+
test '#index' do
|
81
|
+
get :index, session: prepare_user(@admin)
|
82
|
+
assert_response :success
|
83
|
+
assert 2, assigns(:job_invocations).size
|
84
|
+
end
|
85
|
+
|
86
|
+
test '#show' do
|
87
|
+
get :show, params: { id: @invocation2.id }, session: prepare_user(@admin)
|
88
|
+
assert_response :success
|
89
|
+
end
|
90
|
+
|
91
|
+
test '#rerun' do
|
92
|
+
get :rerun, params: { id: @invocation2.id }, session: prepare_user(@admin)
|
93
|
+
assert_response :success
|
94
|
+
end
|
95
|
+
|
96
|
+
test '#cancel' do
|
97
|
+
ForemanTasks::Task.any_instance.expects(:cancel).returns(true)
|
98
|
+
post :cancel, params: { id: @invocation2.id }, session: prepare_user(@admin)
|
99
|
+
assert_response :redirect
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
context 'with user filter' do
|
104
|
+
test '#index' do
|
105
|
+
get :index, session: prepare_user(@user)
|
106
|
+
assert_response :success
|
107
|
+
assert_equal 1, assigns(:job_invocations).size
|
108
|
+
assert_equal @invocation2, assigns(:job_invocations)[0]
|
109
|
+
end
|
110
|
+
|
111
|
+
test '#show' do
|
112
|
+
get :show, params: { id: @invocation.id }, session: prepare_user(@user)
|
113
|
+
assert_response :not_found
|
114
|
+
end
|
115
|
+
|
116
|
+
test '#rerun' do
|
117
|
+
get :rerun, params: { id: @invocation.id }, session: prepare_user(@user)
|
118
|
+
assert_response :not_found
|
119
|
+
end
|
120
|
+
|
121
|
+
test 'cancel' do
|
122
|
+
post :cancel, params: { id: @invocation.id }, session: prepare_user(@user)
|
123
|
+
assert_response :not_found
|
124
|
+
end
|
125
|
+
end
|
126
|
+
end
|
127
|
+
|
128
|
+
def prepare_user(user)
|
129
|
+
User.current = user
|
130
|
+
set_session_user(user)
|
131
|
+
end
|
61
132
|
end
|
@@ -12,7 +12,7 @@ module ForemanRemoteExecution
|
|
12
12
|
let(:provider) do
|
13
13
|
provider = ::SSHExecutionProvider
|
14
14
|
provider.expects(:ssh_password).with(host).returns('sshpass')
|
15
|
-
provider.expects(:
|
15
|
+
provider.expects(:effective_user_password).with(host).returns('sudopass')
|
16
16
|
provider.expects(:ssh_key_passphrase).with(host).returns('keypass')
|
17
17
|
provider
|
18
18
|
end
|
@@ -21,7 +21,7 @@ module ForemanRemoteExecution
|
|
21
21
|
secrets = subject.secrets(host, job_invocation, provider)
|
22
22
|
|
23
23
|
assert_equal 'sshpass', secrets[:ssh_password]
|
24
|
-
assert_equal 'sudopass', secrets[:
|
24
|
+
assert_equal 'sudopass', secrets[:effective_user_password]
|
25
25
|
assert_equal 'keypass', secrets[:key_passphrase]
|
26
26
|
end
|
27
27
|
|
@@ -31,7 +31,7 @@ module ForemanRemoteExecution
|
|
31
31
|
secrets = subject.secrets(host, job_invocation, provider)
|
32
32
|
|
33
33
|
assert_equal 'jobsshpass', secrets[:ssh_password]
|
34
|
-
assert_equal 'sudopass', secrets[:
|
34
|
+
assert_equal 'sudopass', secrets[:effective_user_password]
|
35
35
|
assert_equal 'jobkeypass', secrets[:key_passphrase]
|
36
36
|
end
|
37
37
|
end
|
@@ -14,7 +14,7 @@ module ForemanRemoteExecution
|
|
14
14
|
invocation.description = 'Some short description'
|
15
15
|
invocation.password = 'changeme'
|
16
16
|
invocation.key_passphrase = 'changemetoo'
|
17
|
-
invocation.
|
17
|
+
invocation.effective_user_password = 'sudopassword'
|
18
18
|
invocation.save
|
19
19
|
end
|
20
20
|
end
|
@@ -523,15 +523,15 @@ class JobInvocationComposerTest < ActiveSupport::TestCase
|
|
523
523
|
end
|
524
524
|
end
|
525
525
|
|
526
|
-
describe '#
|
527
|
-
let(:
|
526
|
+
describe '#effective_user_password' do
|
527
|
+
let(:effective_user_password) { 'password' }
|
528
528
|
let(:params) do
|
529
|
-
{ :job_invocation => { :
|
529
|
+
{ :job_invocation => { :effective_user_password => effective_user_password }}
|
530
530
|
end
|
531
531
|
|
532
|
-
it 'sets the
|
532
|
+
it 'sets the effective_user_password password properly' do
|
533
533
|
composer
|
534
|
-
_(composer.job_invocation.
|
534
|
+
_(composer.job_invocation.effective_user_password).must_equal effective_user_password
|
535
535
|
end
|
536
536
|
end
|
537
537
|
|
@@ -78,12 +78,12 @@ class RemoteExecutionProviderTest < ActiveSupport::TestCase
|
|
78
78
|
end
|
79
79
|
end
|
80
80
|
|
81
|
-
describe '
|
82
|
-
it 'uses the
|
83
|
-
host.params['
|
84
|
-
host.host_parameters << FactoryBot.create(:host_parameter, :host => host, :name => '
|
85
|
-
assert_not proxy_options.key?(:
|
86
|
-
_(secrets[:
|
81
|
+
describe 'effective user password' do
|
82
|
+
it 'uses the remote_execution_effective_user_password on the host param' do
|
83
|
+
host.params['remote_execution_effective_user_password'] = 'mypassword'
|
84
|
+
host.host_parameters << FactoryBot.create(:host_parameter, :host => host, :name => 'remote_execution_effective_user_password', :value => 'mypassword')
|
85
|
+
assert_not proxy_options.key?(:effective_user_password)
|
86
|
+
_(secrets[:effective_user_password]).must_equal 'mypassword'
|
87
87
|
end
|
88
88
|
end
|
89
89
|
|
@@ -1,3 +1,24 @@
|
|
1
1
|
export const STATUS = {
|
2
|
+
PENDING: 'PENDING',
|
3
|
+
RESOLVED: 'RESOLVED',
|
2
4
|
ERROR: 'ERROR',
|
3
5
|
};
|
6
|
+
|
7
|
+
export const getControllerSearchProps = (
|
8
|
+
controller,
|
9
|
+
id = 'searchBar',
|
10
|
+
canCreate = true
|
11
|
+
) => ({
|
12
|
+
controller,
|
13
|
+
autocomplete: {
|
14
|
+
id,
|
15
|
+
searchQuery: '',
|
16
|
+
url: `${controller}/auto_complete_search`,
|
17
|
+
useKeyShortcuts: true,
|
18
|
+
},
|
19
|
+
bookmarks: {
|
20
|
+
url: '/api/bookmarks',
|
21
|
+
canCreate,
|
22
|
+
documentationUrl: `4.1.5Searching`,
|
23
|
+
},
|
24
|
+
});
|
@@ -0,0 +1 @@
|
|
1
|
+
export const selectDoesIntervalExist = () => false;
|
@@ -5,8 +5,8 @@ import { LoadingState, Alert } from 'patternfly-react';
|
|
5
5
|
import { STATUS } from 'foremanReact/constants';
|
6
6
|
import HostItem from './components/HostItem';
|
7
7
|
|
8
|
-
const TargetingHosts = ({
|
9
|
-
if (
|
8
|
+
const TargetingHosts = ({ apiStatus, items }) => {
|
9
|
+
if (apiStatus === STATUS.ERROR) {
|
10
10
|
return (
|
11
11
|
<Alert type="error">
|
12
12
|
{__(
|
@@ -16,8 +16,24 @@ const TargetingHosts = ({ status, items }) => {
|
|
16
16
|
);
|
17
17
|
}
|
18
18
|
|
19
|
+
const tableBodyRows = items.length ? (
|
20
|
+
items.map(({ name, link, status, actions }) => (
|
21
|
+
<HostItem
|
22
|
+
key={name}
|
23
|
+
name={name}
|
24
|
+
link={link}
|
25
|
+
status={status}
|
26
|
+
actions={actions}
|
27
|
+
/>
|
28
|
+
))
|
29
|
+
) : (
|
30
|
+
<tr>
|
31
|
+
<td colSpan="3">{__('No hosts found.')}</td>
|
32
|
+
</tr>
|
33
|
+
);
|
34
|
+
|
19
35
|
return (
|
20
|
-
<LoadingState loading={!items.length &&
|
36
|
+
<LoadingState loading={!items.length && apiStatus === STATUS.PENDING}>
|
21
37
|
<div>
|
22
38
|
<table className="table table-bordered table-striped table-hover">
|
23
39
|
<thead>
|
@@ -27,17 +43,7 @@ const TargetingHosts = ({ status, items }) => {
|
|
27
43
|
<th>{__('Actions')}</th>
|
28
44
|
</tr>
|
29
45
|
</thead>
|
30
|
-
<tbody>
|
31
|
-
{items.map(host => (
|
32
|
-
<HostItem
|
33
|
-
key={host.name}
|
34
|
-
name={host.name}
|
35
|
-
link={host.link}
|
36
|
-
status={host.status}
|
37
|
-
actions={host.actions}
|
38
|
-
/>
|
39
|
-
))}
|
40
|
-
</tbody>
|
46
|
+
<tbody>{tableBodyRows}</tbody>
|
41
47
|
</table>
|
42
48
|
</div>
|
43
49
|
</LoadingState>
|
@@ -45,7 +51,7 @@ const TargetingHosts = ({ status, items }) => {
|
|
45
51
|
};
|
46
52
|
|
47
53
|
TargetingHosts.propTypes = {
|
48
|
-
|
54
|
+
apiStatus: PropTypes.string.isRequired,
|
49
55
|
items: PropTypes.array.isRequired,
|
50
56
|
};
|
51
57
|
|
@@ -0,0 +1,10 @@
|
|
1
|
+
import { getURI } from 'foremanReact/common/urlHelpers';
|
2
|
+
|
3
|
+
export const getApiUrl = (searchQuery, pagination) => {
|
4
|
+
const baseUrl = getURI()
|
5
|
+
.search('')
|
6
|
+
.addQuery('page', pagination.page)
|
7
|
+
.addQuery('per_page', pagination.perPage);
|
8
|
+
|
9
|
+
return searchQuery ? baseUrl.addQuery('search', searchQuery) : baseUrl;
|
10
|
+
};
|
@@ -0,0 +1,62 @@
|
|
1
|
+
import React from 'react';
|
2
|
+
import PropTypes from 'prop-types';
|
3
|
+
import { Grid } from 'patternfly-react';
|
4
|
+
|
5
|
+
import SearchBar from 'foremanReact/components/SearchBar';
|
6
|
+
import Pagination from 'foremanReact/components/Pagination/PaginationWrapper';
|
7
|
+
import { getControllerSearchProps } from 'foremanReact/constants';
|
8
|
+
|
9
|
+
import TargetingHosts from './TargetingHosts';
|
10
|
+
import './TargetingHostsPage.scss';
|
11
|
+
|
12
|
+
const TargetingHostsPage = ({
|
13
|
+
handleSearch,
|
14
|
+
searchQuery,
|
15
|
+
apiStatus,
|
16
|
+
items,
|
17
|
+
totalHosts,
|
18
|
+
pagination,
|
19
|
+
handlePagination,
|
20
|
+
}) => (
|
21
|
+
<div id="targeting_hosts">
|
22
|
+
<Grid.Row>
|
23
|
+
<Grid.Col md={6} className="title_filter">
|
24
|
+
<SearchBar
|
25
|
+
onSearch={query => handleSearch(query)}
|
26
|
+
data={{
|
27
|
+
...getControllerSearchProps('hosts'),
|
28
|
+
autocomplete: {
|
29
|
+
id: 'targeting_hosts_search',
|
30
|
+
searchQuery,
|
31
|
+
url: '/hosts/auto_complete_search',
|
32
|
+
useKeyShortcuts: true,
|
33
|
+
},
|
34
|
+
bookmarks: {},
|
35
|
+
}}
|
36
|
+
/>
|
37
|
+
</Grid.Col>
|
38
|
+
</Grid.Row>
|
39
|
+
<br />
|
40
|
+
<TargetingHosts apiStatus={apiStatus} items={items} />
|
41
|
+
<Pagination
|
42
|
+
viewType="list"
|
43
|
+
itemCount={totalHosts}
|
44
|
+
pagination={pagination}
|
45
|
+
onChange={args => handlePagination(args)}
|
46
|
+
dropdownButtonId="targeting-hosts-pagination-dropdown"
|
47
|
+
className="targeting-hosts-pagination"
|
48
|
+
/>
|
49
|
+
</div>
|
50
|
+
);
|
51
|
+
|
52
|
+
TargetingHostsPage.propTypes = {
|
53
|
+
handleSearch: PropTypes.func.isRequired,
|
54
|
+
searchQuery: PropTypes.string.isRequired,
|
55
|
+
apiStatus: PropTypes.string.isRequired,
|
56
|
+
items: PropTypes.array.isRequired,
|
57
|
+
totalHosts: PropTypes.number.isRequired,
|
58
|
+
pagination: PropTypes.object.isRequired,
|
59
|
+
handlePagination: PropTypes.func.isRequired,
|
60
|
+
};
|
61
|
+
|
62
|
+
export default TargetingHostsPage;
|
@@ -2,11 +2,19 @@ import {
|
|
2
2
|
selectAPIStatus,
|
3
3
|
selectAPIResponse,
|
4
4
|
} from 'foremanReact/redux/API/APISelectors';
|
5
|
+
import { selectDoesIntervalExist } from 'foremanReact/redux/middlewares/IntervalMiddleware/IntervalSelectors';
|
6
|
+
|
5
7
|
import { TARGETING_HOSTS } from './TargetingHostsConsts';
|
6
8
|
|
7
9
|
export const selectItems = state =>
|
8
10
|
selectAPIResponse(state, TARGETING_HOSTS).hosts || [];
|
9
11
|
|
10
12
|
export const selectAutoRefresh = state =>
|
11
|
-
selectAPIResponse(state, TARGETING_HOSTS).autoRefresh;
|
12
|
-
|
13
|
+
selectAPIResponse(state, TARGETING_HOSTS).autoRefresh || '';
|
14
|
+
|
15
|
+
export const selectApiStatus = state => selectAPIStatus(state, TARGETING_HOSTS);
|
16
|
+
export const selectTotalHosts = state =>
|
17
|
+
selectAPIResponse(state, TARGETING_HOSTS).total_hosts || 0;
|
18
|
+
|
19
|
+
export const selectIntervalExists = state =>
|
20
|
+
selectDoesIntervalExist(state, TARGETING_HOSTS);
|
@@ -0,0 +1,9 @@
|
|
1
|
+
import { testComponentSnapshotsWithFixtures } from '@theforeman/test';
|
2
|
+
import TargetingHostsPage from '../TargetingHostsPage';
|
3
|
+
import { TargetingHostsPageFixtures } from './fixtures';
|
4
|
+
|
5
|
+
describe('TargetingHostsPage', () =>
|
6
|
+
testComponentSnapshotsWithFixtures(
|
7
|
+
TargetingHostsPage,
|
8
|
+
TargetingHostsPageFixtures
|
9
|
+
));
|
@@ -0,0 +1,26 @@
|
|
1
|
+
import { testSelectorsSnapshotWithFixtures } from '@theforeman/test';
|
2
|
+
|
3
|
+
import {
|
4
|
+
selectItems,
|
5
|
+
selectAutoRefresh,
|
6
|
+
selectApiStatus,
|
7
|
+
selectTotalHosts,
|
8
|
+
selectIntervalExists,
|
9
|
+
} from '../TargetingHostsSelectors';
|
10
|
+
|
11
|
+
const state = {
|
12
|
+
hosts: [],
|
13
|
+
autoRefresh: 'true',
|
14
|
+
total_hosts: 0,
|
15
|
+
};
|
16
|
+
|
17
|
+
const fixtures = {
|
18
|
+
'should return hosts': () => selectItems(state),
|
19
|
+
'should return autoRefresh': () => selectAutoRefresh(state),
|
20
|
+
'should return apiStatus': () => selectApiStatus(state),
|
21
|
+
'should return totalHosts': () => selectTotalHosts(state),
|
22
|
+
'should return intervalExists': () => selectIntervalExists(state),
|
23
|
+
};
|
24
|
+
|
25
|
+
describe('TargetingHostsSelectors', () =>
|
26
|
+
testSelectorsSnapshotWithFixtures(fixtures));
|
data/webpack/react_app/components/TargetingHosts/__tests__/__snapshots__/TargetingHosts.test.js.snap
CHANGED
@@ -33,6 +33,13 @@ exports[`TargetingHosts renders 1`] = `
|
|
33
33
|
name="host"
|
34
34
|
status="success"
|
35
35
|
/>
|
36
|
+
<HostItem
|
37
|
+
actions={Array []}
|
38
|
+
key="host2"
|
39
|
+
link="/link2"
|
40
|
+
name="host2"
|
41
|
+
status="success"
|
42
|
+
/>
|
36
43
|
</tbody>
|
37
44
|
</table>
|
38
45
|
</div>
|
@@ -52,7 +59,7 @@ exports[`TargetingHosts renders with error 1`] = `
|
|
52
59
|
exports[`TargetingHosts renders with loading 1`] = `
|
53
60
|
<LoadingState
|
54
61
|
additionalClasses=""
|
55
|
-
loading={
|
62
|
+
loading={true}
|
56
63
|
loadingText="Loading"
|
57
64
|
size="lg"
|
58
65
|
timeout={300}
|
@@ -74,7 +81,15 @@ exports[`TargetingHosts renders with loading 1`] = `
|
|
74
81
|
</th>
|
75
82
|
</tr>
|
76
83
|
</thead>
|
77
|
-
<tbody
|
84
|
+
<tbody>
|
85
|
+
<tr>
|
86
|
+
<td
|
87
|
+
colSpan="3"
|
88
|
+
>
|
89
|
+
No hosts found.
|
90
|
+
</td>
|
91
|
+
</tr>
|
92
|
+
</tbody>
|
78
93
|
</table>
|
79
94
|
</div>
|
80
95
|
</LoadingState>
|