foreman_remote_execution 3.3.4 → 4.1.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 (57) 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 +1 -0
  4. data/app/controllers/foreman_remote_execution/concerns/api/v2/subnets_controller_extensions.rb +21 -0
  5. data/app/controllers/job_invocations_controller.rb +22 -8
  6. data/app/helpers/job_invocations_helper.rb +3 -2
  7. data/app/lib/actions/remote_execution/run_host_job.rb +1 -1
  8. data/app/lib/actions/remote_execution/run_hosts_job.rb +4 -3
  9. data/app/lib/foreman_remote_execution/renderer/scope/input.rb +35 -0
  10. data/app/models/concerns/api/v2/interfaces_controller_extensions.rb +13 -0
  11. data/app/models/concerns/foreman_remote_execution/orchestration/ssh.rb +11 -4
  12. data/app/models/job_invocation.rb +11 -4
  13. data/app/models/job_invocation_composer.rb +2 -2
  14. data/app/models/remote_execution_provider.rb +2 -2
  15. data/app/models/setting/remote_execution.rb +2 -2
  16. data/app/models/ssh_execution_provider.rb +1 -1
  17. data/app/views/api/v2/interfaces/execution_flag.json.rabl +1 -0
  18. data/app/views/api/v2/job_invocations/base.json.rabl +1 -0
  19. data/app/views/api/v2/job_invocations/main.json.rabl +1 -1
  20. data/app/views/api/v2/subnets/remote_execution_proxies.json.rabl +3 -0
  21. data/app/views/job_invocations/_form.html.erb +1 -1
  22. data/app/views/job_invocations/_tab_hosts.html.erb +1 -20
  23. data/app/views/job_invocations/_tab_overview.html.erb +13 -1
  24. data/app/views/job_invocations/show.html.erb +9 -0
  25. data/app/views/job_invocations/show.js.erb +5 -0
  26. data/app/views/job_invocations/show.json.erb +2 -1
  27. data/db/migrate/20200623073022_rename_sudo_password_to_effective_user_password.rb +34 -0
  28. data/db/seeds.d/20-permissions.rb +9 -0
  29. data/lib/foreman_remote_execution/engine.rb +19 -1
  30. data/lib/foreman_remote_execution/version.rb +1 -1
  31. data/test/functional/api/v2/job_invocations_controller_test.rb +65 -2
  32. data/test/functional/job_invocations_controller_test.rb +71 -0
  33. data/test/models/orchestration/ssh_test.rb +1 -1
  34. data/test/support/remote_execution_helper.rb +5 -0
  35. data/test/unit/actions/run_host_job_test.rb +3 -3
  36. data/test/unit/actions/run_hosts_job_test.rb +2 -2
  37. data/test/unit/job_invocation_composer_test.rb +5 -5
  38. data/test/unit/remote_execution_provider_test.rb +6 -6
  39. data/webpack/__mocks__/foremanReact/components/Pagination/PaginationWrapper.js +2 -0
  40. data/webpack/__mocks__/foremanReact/components/SearchBar.js +2 -0
  41. data/webpack/__mocks__/foremanReact/constants.js +21 -0
  42. data/webpack/__mocks__/foremanReact/redux/API/APISelectors.js +2 -0
  43. data/webpack/__mocks__/foremanReact/redux/middlewares/IntervalMiddleware/IntervalSelectors.js +1 -0
  44. data/webpack/react_app/components/TargetingHosts/TargetingHosts.js +21 -15
  45. data/webpack/react_app/components/TargetingHosts/TargetingHostsHelpers.js +10 -0
  46. data/webpack/react_app/components/TargetingHosts/TargetingHostsPage.js +62 -0
  47. data/webpack/react_app/components/TargetingHosts/TargetingHostsPage.scss +6 -0
  48. data/webpack/react_app/components/TargetingHosts/TargetingHostsSelectors.js +10 -2
  49. data/webpack/react_app/components/TargetingHosts/__tests__/TargetingHostsPage.test.js +9 -0
  50. data/webpack/react_app/components/TargetingHosts/__tests__/TargetingHostsSelectors.test.js +26 -0
  51. data/webpack/react_app/components/TargetingHosts/__tests__/__snapshots__/TargetingHosts.test.js.snap +16 -1
  52. data/webpack/react_app/components/TargetingHosts/__tests__/__snapshots__/TargetingHostsPage.test.js.snap +68 -0
  53. data/webpack/react_app/components/TargetingHosts/__tests__/__snapshots__/TargetingHostsSelectors.test.js.snap +11 -0
  54. data/webpack/react_app/components/TargetingHosts/__tests__/fixtures.js +35 -19
  55. data/webpack/react_app/components/TargetingHosts/index.js +73 -13
  56. metadata +22 -3
  57. data/webpack/react_app/components/TargetingHosts/TargetingHostsActions.js +0 -8
@@ -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,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>
@@ -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>
@@ -0,0 +1,68 @@
1
+ // Jest Snapshot v1, https://goo.gl/fbAQLP
2
+
3
+ exports[`TargetingHostsPage renders 1`] = `
4
+ <div
5
+ id="targeting_hosts"
6
+ >
7
+ <Row
8
+ bsClass="row"
9
+ componentClass="div"
10
+ >
11
+ <Col
12
+ bsClass="col"
13
+ className="title_filter"
14
+ componentClass="div"
15
+ md={6}
16
+ >
17
+ <SearchBar
18
+ data={
19
+ Object {
20
+ "autocomplete": Object {
21
+ "id": "targeting_hosts_search",
22
+ "searchQuery": "",
23
+ "url": "/hosts/auto_complete_search",
24
+ "useKeyShortcuts": true,
25
+ },
26
+ "bookmarks": Object {},
27
+ "controller": "hosts",
28
+ }
29
+ }
30
+ onSearch={[Function]}
31
+ />
32
+ </Col>
33
+ </Row>
34
+ <br />
35
+ <TargetingHosts
36
+ apiStatus="RESOLVED"
37
+ items={
38
+ Array [
39
+ Object {
40
+ "actions": Array [],
41
+ "link": "/link",
42
+ "name": "host",
43
+ "status": "success",
44
+ },
45
+ Object {
46
+ "actions": Array [],
47
+ "link": "/link2",
48
+ "name": "host2",
49
+ "status": "success",
50
+ },
51
+ ]
52
+ }
53
+ />
54
+ <PaginationWrapper
55
+ className="targeting-hosts-pagination"
56
+ dropdownButtonId="targeting-hosts-pagination-dropdown"
57
+ itemCount={1}
58
+ onChange={[Function]}
59
+ pagination={
60
+ Object {
61
+ "page": 1,
62
+ "perPage": 20,
63
+ }
64
+ }
65
+ viewType="list"
66
+ />
67
+ </div>
68
+ `;
@@ -0,0 +1,11 @@
1
+ // Jest Snapshot v1, https://goo.gl/fbAQLP
2
+
3
+ exports[`TargetingHostsSelectors should return apiStatus 1`] = `"RESOLVED"`;
4
+
5
+ exports[`TargetingHostsSelectors should return autoRefresh 1`] = `"true"`;
6
+
7
+ exports[`TargetingHostsSelectors should return hosts 1`] = `Array []`;
8
+
9
+ exports[`TargetingHostsSelectors should return intervalExists 1`] = `false`;
10
+
11
+ exports[`TargetingHostsSelectors should return totalHosts 1`] = `0`;
@@ -1,3 +1,18 @@
1
+ const items = [
2
+ {
3
+ name: 'host',
4
+ link: '/link',
5
+ status: 'success',
6
+ actions: [],
7
+ },
8
+ {
9
+ name: 'host2',
10
+ link: '/link2',
11
+ status: 'success',
12
+ actions: [],
13
+ },
14
+ ];
15
+
1
16
  export const HostItemFixtures = {
2
17
  renders: {
3
18
  name: 'Host1',
@@ -15,29 +30,30 @@ export const HostStatusFixtures = {
15
30
 
16
31
  export const TargetingHostsFixtures = {
17
32
  renders: {
18
- status: '',
19
- items: [
20
- {
21
- name: 'host',
22
- link: '/link',
23
- status: 'success',
24
- actions: [],
25
- },
26
- ],
33
+ apiStatus: 'RESOLVED',
34
+ items,
27
35
  },
28
36
  'renders with error': {
29
- status: 'ERROR',
30
- items: [
31
- {
32
- name: 'host',
33
- link: '/link',
34
- status: 'success',
35
- actions: [],
36
- },
37
- ],
37
+ apiStatus: 'ERROR',
38
+ items,
38
39
  },
39
40
  'renders with loading': {
40
- status: '',
41
+ apiStatus: 'PENDING',
41
42
  items: [],
42
43
  },
43
44
  };
45
+
46
+ export const TargetingHostsPageFixtures = {
47
+ renders: {
48
+ handleSearch: () => {},
49
+ searchQuery: '',
50
+ apiStatus: 'RESOLVED',
51
+ items,
52
+ totalHosts: 1,
53
+ pagination: {
54
+ page: 1,
55
+ perPage: 20,
56
+ },
57
+ handlePagination: () => {},
58
+ },
59
+ };
@@ -1,37 +1,97 @@
1
- import React, { useEffect } from 'react';
1
+ import React, { useEffect, useState } from 'react';
2
2
  import { useSelector, useDispatch } from 'react-redux';
3
- import { stopInterval } from 'foremanReact/redux/middlewares/IntervalMiddleware';
4
- import TargetingHosts from './TargetingHosts';
3
+
4
+ import { get } from 'foremanReact/redux/API';
5
+ import { useForemanSettings } from 'foremanReact/Root/Context/ForemanContext';
6
+ import {
7
+ withInterval,
8
+ stopInterval,
9
+ } from 'foremanReact/redux/middlewares/IntervalMiddleware';
5
10
 
6
11
  import {
7
12
  selectItems,
8
- selectStatus,
13
+ selectApiStatus,
9
14
  selectAutoRefresh,
15
+ selectTotalHosts,
16
+ selectIntervalExists,
10
17
  } from './TargetingHostsSelectors';
11
- import { getData } from './TargetingHostsActions';
18
+ import { getApiUrl } from './TargetingHostsHelpers';
12
19
  import { TARGETING_HOSTS } from './TargetingHostsConsts';
20
+ import TargetingHostsPage from './TargetingHostsPage';
13
21
 
14
22
  const WrappedTargetingHosts = () => {
15
23
  const dispatch = useDispatch();
24
+ const { perPage, perPageOptions } = useForemanSettings();
25
+
16
26
  const autoRefresh = useSelector(selectAutoRefresh);
17
27
  const items = useSelector(selectItems);
18
- const status = useSelector(selectStatus);
28
+ const apiStatus = useSelector(selectApiStatus);
29
+ const totalHosts = useSelector(selectTotalHosts);
30
+ const [searchQuery, setSearchQuery] = useState('');
31
+ const [pagination, setPagination] = useState({
32
+ page: 1,
33
+ perPage,
34
+ perPageOptions,
35
+ });
36
+ const [apiUrl, setApiUrl] = useState(getApiUrl(searchQuery, pagination));
37
+ const intervalExists = useSelector(selectIntervalExists);
19
38
 
20
- useEffect(() => {
21
- dispatch(getData());
39
+ const handleSearch = query => {
40
+ const defaultPagination = { page: 1, perPage: pagination.perPage };
41
+ stopApiInterval();
22
42
 
23
- return () => {
43
+ setApiUrl(getApiUrl(query, defaultPagination));
44
+ setSearchQuery(query);
45
+ setPagination(defaultPagination);
46
+ };
47
+
48
+ const handlePagination = args => {
49
+ stopApiInterval();
50
+ setPagination(args);
51
+ setApiUrl(getApiUrl(searchQuery, args));
52
+ };
53
+
54
+ const stopApiInterval = () => {
55
+ if (intervalExists) {
24
56
  dispatch(stopInterval(TARGETING_HOSTS));
25
- };
26
- }, [dispatch]);
57
+ }
58
+ };
59
+
60
+ const getData = url =>
61
+ withInterval(
62
+ get({
63
+ key: TARGETING_HOSTS,
64
+ url,
65
+ handleError: () => {
66
+ dispatch(stopInterval(TARGETING_HOSTS));
67
+ },
68
+ }),
69
+ 1000
70
+ );
27
71
 
28
72
  useEffect(() => {
73
+ dispatch(getData(apiUrl));
74
+
29
75
  if (autoRefresh === 'false') {
30
76
  dispatch(stopInterval(TARGETING_HOSTS));
31
77
  }
32
- }, [autoRefresh, dispatch]);
33
78
 
34
- return <TargetingHosts status={status} items={items} />;
79
+ return () => {
80
+ dispatch(stopInterval(TARGETING_HOSTS));
81
+ };
82
+ }, [dispatch, apiUrl, autoRefresh]);
83
+
84
+ return (
85
+ <TargetingHostsPage
86
+ handleSearch={handleSearch}
87
+ searchQuery={searchQuery}
88
+ apiStatus={apiStatus}
89
+ items={items}
90
+ totalHosts={totalHosts}
91
+ pagination={pagination}
92
+ handlePagination={handlePagination}
93
+ />
94
+ );
35
95
  };
36
96
 
37
97
  export default WrappedTargetingHosts;