foreman_remote_execution 3.3.7 → 4.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (45) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ci.yml +1 -1
  3. data/app/controllers/job_invocations_controller.rb +22 -8
  4. data/app/lib/actions/remote_execution/run_host_job.rb +1 -1
  5. data/app/lib/actions/remote_execution/run_hosts_job.rb +1 -1
  6. data/app/lib/foreman_remote_execution/renderer/scope/input.rb +35 -0
  7. data/app/models/job_invocation.rb +6 -3
  8. data/app/models/job_invocation_composer.rb +2 -2
  9. data/app/models/remote_execution_provider.rb +2 -2
  10. data/app/models/setting/remote_execution.rb +2 -2
  11. data/app/models/ssh_execution_provider.rb +1 -1
  12. data/app/views/job_invocations/_form.html.erb +1 -1
  13. data/app/views/job_invocations/_tab_hosts.html.erb +1 -20
  14. data/app/views/job_invocations/show.html.erb +3 -0
  15. data/app/views/job_invocations/show.json.erb +2 -1
  16. data/db/migrate/20200623073022_rename_sudo_password_to_effective_user_password.rb +34 -0
  17. data/db/seeds.d/20-permissions.rb +9 -0
  18. data/lib/foreman_remote_execution/engine.rb +4 -10
  19. data/lib/foreman_remote_execution/version.rb +1 -1
  20. data/test/functional/api/v2/job_invocations_controller_test.rb +64 -1
  21. data/test/functional/job_invocations_controller_test.rb +71 -0
  22. data/test/support/remote_execution_helper.rb +5 -0
  23. data/test/unit/actions/run_host_job_test.rb +3 -3
  24. data/test/unit/actions/run_hosts_job_test.rb +1 -1
  25. data/test/unit/job_invocation_composer_test.rb +5 -5
  26. data/test/unit/remote_execution_provider_test.rb +6 -6
  27. data/webpack/__mocks__/foremanReact/components/Pagination/PaginationWrapper.js +2 -0
  28. data/webpack/__mocks__/foremanReact/components/SearchBar.js +2 -0
  29. data/webpack/__mocks__/foremanReact/constants.js +21 -0
  30. data/webpack/__mocks__/foremanReact/redux/API/APISelectors.js +2 -0
  31. data/webpack/__mocks__/foremanReact/redux/middlewares/IntervalMiddleware/IntervalSelectors.js +1 -0
  32. data/webpack/react_app/components/TargetingHosts/TargetingHosts.js +21 -15
  33. data/webpack/react_app/components/TargetingHosts/TargetingHostsHelpers.js +10 -0
  34. data/webpack/react_app/components/TargetingHosts/TargetingHostsPage.js +62 -0
  35. data/webpack/react_app/components/TargetingHosts/TargetingHostsPage.scss +6 -0
  36. data/webpack/react_app/components/TargetingHosts/TargetingHostsSelectors.js +10 -2
  37. data/webpack/react_app/components/TargetingHosts/__tests__/TargetingHostsPage.test.js +9 -0
  38. data/webpack/react_app/components/TargetingHosts/__tests__/TargetingHostsSelectors.test.js +26 -0
  39. data/webpack/react_app/components/TargetingHosts/__tests__/__snapshots__/TargetingHosts.test.js.snap +17 -2
  40. data/webpack/react_app/components/TargetingHosts/__tests__/__snapshots__/TargetingHostsPage.test.js.snap +68 -0
  41. data/webpack/react_app/components/TargetingHosts/__tests__/__snapshots__/TargetingHostsSelectors.test.js.snap +11 -0
  42. data/webpack/react_app/components/TargetingHosts/__tests__/fixtures.js +35 -19
  43. data/webpack/react_app/components/TargetingHosts/index.js +73 -13
  44. metadata +17 -3
  45. 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
@@ -0,0 +1,5 @@
1
+ module RemoteExecutionHelper
2
+ def job_invocation_task_buttons(task)
3
+ return []
4
+ end
5
+ 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(:sudo_password).with(host).returns('sudopass')
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[:sudo_password]
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[:sudo_password]
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.sudo_password = 'sudopassword'
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 '#sudo_password' do
527
- let(:sudo_password) { 'password' }
526
+ describe '#effective_user_password' do
527
+ let(:effective_user_password) { 'password' }
528
528
  let(:params) do
529
- { :job_invocation => { :sudo_password => sudo_password }}
529
+ { :job_invocation => { :effective_user_password => effective_user_password }}
530
530
  end
531
531
 
532
- it 'sets the sudo password properly' do
532
+ it 'sets the effective_user_password password properly' do
533
533
  composer
534
- _(composer.job_invocation.sudo_password).must_equal sudo_password
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 'sudo password' do
82
- it 'uses the remote_execution_sudo_password on the host param' do
83
- host.params['remote_execution_sudo_password'] = 'mypassword'
84
- host.host_parameters << FactoryBot.create(:host_parameter, :host => host, :name => 'remote_execution_sudo_password', :value => 'mypassword')
85
- assert_not proxy_options.key?(:sudo_password)
86
- _(secrets[:sudo_password]).must_equal 'mypassword'
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
 
@@ -0,0 +1,2 @@
1
+ const PaginationWrapper = () => jest.fn();
2
+ export default PaginationWrapper;
@@ -0,0 +1,2 @@
1
+ const SearchBar = () => jest.fn();
2
+ export default SearchBar;
@@ -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,2 @@
1
+ export const selectAPIStatus = () => 'RESOLVED';
2
+ export const selectAPIResponse = state => state;
@@ -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 = ({ status, items }) => {
9
- if (status === STATUS.ERROR) {
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 && status === STATUS.PENDING}>
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
- status: PropTypes.string.isRequired,
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;
@@ -0,0 +1,6 @@
1
+ .targeting-hosts-pagination {
2
+ margin-top: -7px;
3
+ }
4
+ #targeting_hosts {
5
+ min-height: 350px;
6
+ }
@@ -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
- export const selectStatus = state => selectAPIStatus(state, TARGETING_HOSTS);
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));
@@ -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={false}
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>