foreman_remote_execution 3.3.5 → 4.2.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.
Files changed (66) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ci.yml +1 -1
  3. data/app/controllers/api/v2/job_invocations_controller.rb +20 -2
  4. data/app/controllers/foreman_remote_execution/concerns/api/v2/registration_controller_extensions.rb +26 -0
  5. data/app/controllers/foreman_remote_execution/concerns/api/v2/subnets_controller_extensions.rb +21 -0
  6. data/app/controllers/job_invocations_controller.rb +22 -8
  7. data/app/controllers/job_templates_controller.rb +1 -1
  8. data/app/helpers/job_invocations_helper.rb +3 -2
  9. data/app/lib/actions/remote_execution/run_host_job.rb +1 -1
  10. data/app/lib/actions/remote_execution/run_hosts_job.rb +12 -3
  11. data/app/lib/foreman_remote_execution/renderer/scope/input.rb +35 -0
  12. data/app/models/concerns/api/v2/interfaces_controller_extensions.rb +13 -0
  13. data/app/models/concerns/foreman_remote_execution/host_extensions.rb +38 -14
  14. data/app/models/job_invocation.rb +12 -4
  15. data/app/models/job_invocation_composer.rb +2 -2
  16. data/app/models/remote_execution_feature.rb +5 -2
  17. data/app/models/remote_execution_provider.rb +8 -3
  18. data/app/models/setting/remote_execution.rb +2 -2
  19. data/app/models/ssh_execution_provider.rb +1 -1
  20. data/app/services/remote_execution_proxy_selector.rb +3 -0
  21. data/app/views/api/v2/interfaces/execution_flag.json.rabl +1 -0
  22. data/app/views/api/v2/job_invocations/base.json.rabl +1 -0
  23. data/app/views/api/v2/job_invocations/main.json.rabl +2 -2
  24. data/app/views/api/v2/registration/_form.html.erb +12 -0
  25. data/app/views/api/v2/subnets/remote_execution_proxies.json.rabl +3 -0
  26. data/app/views/job_invocations/_form.html.erb +1 -1
  27. data/app/views/job_invocations/_tab_hosts.html.erb +1 -20
  28. data/app/views/job_invocations/_tab_overview.html.erb +13 -1
  29. data/app/views/job_invocations/show.html.erb +3 -0
  30. data/app/views/job_invocations/show.json.erb +2 -1
  31. data/app/views/template_invocations/_output_line_set.html.erb +1 -1
  32. data/app/views/templates/ssh/package_action.erb +1 -0
  33. data/config/routes.rb +1 -0
  34. data/db/migrate/20200623073022_rename_sudo_password_to_effective_user_password.rb +34 -0
  35. data/db/migrate/20200820122057_add_proxy_selector_override_to_remote_execution_feature.rb +5 -0
  36. data/db/seeds.d/20-permissions.rb +9 -0
  37. data/lib/foreman_remote_execution/engine.rb +28 -2
  38. data/lib/foreman_remote_execution/version.rb +1 -1
  39. data/package.json +6 -6
  40. data/test/functional/api/v2/job_invocations_controller_test.rb +84 -2
  41. data/test/functional/api/v2/registration_controller_test.rb +82 -0
  42. data/test/functional/job_invocations_controller_test.rb +71 -0
  43. data/test/support/remote_execution_helper.rb +5 -0
  44. data/test/unit/actions/run_host_job_test.rb +3 -3
  45. data/test/unit/actions/run_hosts_job_test.rb +3 -2
  46. data/test/unit/job_invocation_composer_test.rb +5 -5
  47. data/test/unit/remote_execution_provider_test.rb +6 -6
  48. data/webpack/__mocks__/foremanReact/components/Pagination/PaginationWrapper.js +2 -0
  49. data/webpack/__mocks__/foremanReact/components/SearchBar.js +2 -0
  50. data/webpack/__mocks__/foremanReact/constants.js +21 -0
  51. data/webpack/__mocks__/foremanReact/redux/API/APISelectors.js +2 -0
  52. data/webpack/__mocks__/foremanReact/redux/middlewares/IntervalMiddleware/IntervalSelectors.js +1 -0
  53. data/webpack/react_app/components/TargetingHosts/TargetingHosts.js +25 -15
  54. data/webpack/react_app/components/TargetingHosts/TargetingHostsHelpers.js +10 -0
  55. data/webpack/react_app/components/TargetingHosts/TargetingHostsPage.js +66 -0
  56. data/webpack/react_app/components/TargetingHosts/TargetingHostsPage.scss +6 -0
  57. data/webpack/react_app/components/TargetingHosts/TargetingHostsSelectors.js +10 -2
  58. data/webpack/react_app/components/TargetingHosts/__tests__/TargetingHostsPage.test.js +9 -0
  59. data/webpack/react_app/components/TargetingHosts/__tests__/TargetingHostsSelectors.test.js +26 -0
  60. data/webpack/react_app/components/TargetingHosts/__tests__/__snapshots__/TargetingHosts.test.js.snap +16 -1
  61. data/webpack/react_app/components/TargetingHosts/__tests__/__snapshots__/TargetingHostsPage.test.js.snap +68 -0
  62. data/webpack/react_app/components/TargetingHosts/__tests__/__snapshots__/TargetingHostsSelectors.test.js.snap +11 -0
  63. data/webpack/react_app/components/TargetingHosts/__tests__/fixtures.js +35 -19
  64. data/webpack/react_app/components/TargetingHosts/index.js +73 -13
  65. metadata +30 -7
  66. data/webpack/react_app/components/TargetingHosts/TargetingHostsActions.js +0 -8
@@ -0,0 +1,82 @@
1
+ require 'test_plugin_helper'
2
+
3
+ module Api
4
+ module V2
5
+ # Tests for the extra methods to play roles on a Host
6
+ class RegistrationControllerTest < ActionController::TestCase
7
+ describe 'host registration' do
8
+ let(:organization) { FactoryBot.create(:organization) }
9
+ let(:tax_location) { FactoryBot.create(:location) }
10
+ let(:template_kind) { template_kinds(:registration) }
11
+ let(:registration_template) do
12
+ FactoryBot.create(
13
+ :provisioning_template,
14
+ template_kind: template_kind,
15
+ template: 'template content <%= @host.name %>',
16
+ locations: [tax_location],
17
+ organizations: [organization]
18
+ )
19
+ end
20
+ let(:os) do
21
+ FactoryBot.create(
22
+ :operatingsystem,
23
+ :with_associations,
24
+ family: 'Redhat',
25
+ provisioning_templates: [
26
+ registration_template,
27
+ ]
28
+ )
29
+ end
30
+
31
+ let(:host_params) do
32
+ { host: { name: 'centos-test.example.com',
33
+ managed: false, build: false,
34
+ organization_id: organization.id,
35
+ location_id: tax_location.id,
36
+ operatingsystem_id: os.id } }
37
+ end
38
+
39
+ setup do
40
+ FactoryBot.create(
41
+ :os_default_template,
42
+ template_kind: template_kind,
43
+ provisioning_template: registration_template,
44
+ operatingsystem: os
45
+ )
46
+ end
47
+
48
+ describe 'remote_execution_interface' do
49
+ setup do
50
+ @host = Host.create(host_params[:host])
51
+ @interface0 = FactoryBot.create(:nic_managed, host: @host, identifier: 'dummy0', execution: false)
52
+ end
53
+
54
+ test 'with existing interface' do
55
+ params = host_params.merge(remote_execution_interface: @interface0.identifier)
56
+
57
+ post :host, params: params, session: set_session_user
58
+ assert_response :success
59
+ assert @interface0.reload.execution
60
+ end
61
+
62
+ test 'with not-existing interface' do
63
+ params = host_params.merge(remote_execution_interface: 'dummy999')
64
+
65
+ post :host, params: params, session: set_session_user
66
+ assert_response :not_found
67
+ end
68
+
69
+ test 'with multiple interfaces' do
70
+ interface1 = FactoryBot.create(:nic_managed, host: @host, identifier: 'dummy1', execution: false)
71
+ params = host_params.merge(remote_execution_interface: interface1.identifier)
72
+
73
+ post :host, params: params, session: set_session_user
74
+ assert_response :success
75
+ refute @interface0.reload.execution
76
+ assert interface1.reload.execution
77
+ end
78
+ end
79
+ end
80
+ end
81
+ end
82
+ end
@@ -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
@@ -24,6 +24,7 @@ module ForemanRemoteExecution
24
24
  OpenStruct.new(:id => uuid).tap do |o|
25
25
  o.stubs(:add_missing_task_groups)
26
26
  o.stubs(:task_groups).returns([])
27
+ o.stubs(:pending?).returns(true)
27
28
  end
28
29
  end
29
30
  let(:action) do
@@ -73,7 +74,7 @@ module ForemanRemoteExecution
73
74
  end
74
75
 
75
76
  it 'triggers the RunHostJob actions on the resolved hosts in run phase' do
76
- planned.expects(:output).returns(:planned_count => 0)
77
+ planned.expects(:output).at_most(5).returns(:planned_count => 0)
77
78
  planned.expects(:trigger).with { |*args| args[0] == Actions::RemoteExecution::RunHostJob }
78
79
  planned.create_sub_plans
79
80
  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}>
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,8 +51,12 @@ const TargetingHosts = ({ status, items }) => {
45
51
  };
46
52
 
47
53
  TargetingHosts.propTypes = {
48
- status: PropTypes.string.isRequired,
54
+ apiStatus: PropTypes.string,
49
55
  items: PropTypes.array.isRequired,
50
56
  };
51
57
 
58
+ TargetingHosts.defaultProps = {
59
+ apiStatus: null,
60
+ };
61
+
52
62
  export default TargetingHosts;
@@ -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,66 @@
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,
56
+ items: PropTypes.array.isRequired,
57
+ totalHosts: PropTypes.number.isRequired,
58
+ pagination: PropTypes.object.isRequired,
59
+ handlePagination: PropTypes.func.isRequired,
60
+ };
61
+
62
+ TargetingHostsPage.defaultProps = {
63
+ apiStatus: null,
64
+ };
65
+
66
+ export default TargetingHostsPage;