foreman_remote_execution 3.3.6 → 4.2.1

Sign up to get free protection for your applications and to get access to all the features.
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 +3 -3
  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 +1 -1
  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 +83 -1
  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 +17 -2
  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 +26 -3
  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 && 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,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;