foreman_remote_execution 4.4.0 → 4.5.3

Sign up to get free protection for your applications and to get access to all the features.
Files changed (83) hide show
  1. checksums.yaml +4 -4
  2. data/app/controllers/api/v2/job_invocations_controller.rb +13 -24
  3. data/app/controllers/job_invocations_controller.rb +1 -1
  4. data/app/controllers/job_templates_controller.rb +4 -4
  5. data/app/controllers/ui_job_wizard_controller.rb +19 -0
  6. data/app/helpers/job_invocations_helper.rb +2 -2
  7. data/app/helpers/remote_execution_helper.rb +13 -9
  8. data/app/lib/actions/remote_execution/run_host_job.rb +36 -6
  9. data/app/models/concerns/foreman_remote_execution/host_extensions.rb +7 -5
  10. data/app/models/concerns/foreman_remote_execution/smart_proxy_extensions.rb +6 -0
  11. data/app/models/host_proxy_invocation.rb +4 -0
  12. data/app/models/host_status/execution_status.rb +5 -5
  13. data/app/models/job_invocation.rb +31 -12
  14. data/app/models/job_invocation_composer.rb +61 -19
  15. data/app/models/remote_execution_provider.rb +1 -1
  16. data/app/models/setting/remote_execution.rb +2 -2
  17. data/app/models/ssh_execution_provider.rb +4 -4
  18. data/app/models/targeting.rb +5 -1
  19. data/app/overrides/execution_interface.rb +8 -8
  20. data/app/overrides/subnet_proxies.rb +6 -6
  21. data/app/views/job_invocations/index.html.erb +1 -1
  22. data/app/views/templates/ssh/module_action.erb +1 -0
  23. data/app/views/templates/ssh/puppet_run_once.erb +1 -0
  24. data/config/routes.rb +1 -0
  25. data/db/migrate/20180110104432_rename_template_invocation_permission.rb +1 -1
  26. data/db/migrate/20190111153330_remove_remote_execution_without_proxy_setting.rb +4 -4
  27. data/db/migrate/2021051713291621250977_add_host_proxy_invocations.rb +12 -0
  28. data/extra/cockpit/foreman-cockpit-session +6 -6
  29. data/lib/foreman_remote_execution/engine.rb +11 -8
  30. data/lib/foreman_remote_execution/version.rb +1 -1
  31. data/package.json +2 -1
  32. data/test/functional/api/v2/job_invocations_controller_test.rb +14 -1
  33. data/test/unit/job_invocation_composer_test.rb +59 -2
  34. data/test/unit/job_invocation_test.rb +1 -1
  35. data/webpack/JobWizard/JobWizard.js +80 -19
  36. data/webpack/JobWizard/JobWizard.scss +42 -1
  37. data/webpack/JobWizard/JobWizardConstants.js +11 -0
  38. data/webpack/JobWizard/JobWizardSelectors.js +27 -1
  39. data/webpack/JobWizard/__tests__/__snapshots__/integration.test.js.snap +43 -0
  40. data/webpack/JobWizard/__tests__/fixtures.js +128 -0
  41. data/webpack/JobWizard/__tests__/integration.test.js +84 -0
  42. data/webpack/JobWizard/steps/AdvancedFields/AdvancedFields.js +110 -0
  43. data/webpack/JobWizard/steps/AdvancedFields/DescriptionField.js +67 -0
  44. data/webpack/JobWizard/steps/AdvancedFields/Fields.js +195 -0
  45. data/webpack/JobWizard/steps/AdvancedFields/__tests__/AdvancedFields.test.js +144 -0
  46. data/webpack/JobWizard/steps/AdvancedFields/__tests__/DescriptionField.test.js +23 -0
  47. data/webpack/JobWizard/steps/CategoryAndTemplate/CategoryAndTemplate.js +34 -2
  48. data/webpack/JobWizard/steps/CategoryAndTemplate/CategoryAndTemplate.test.js +122 -44
  49. data/webpack/JobWizard/steps/CategoryAndTemplate/index.js +9 -1
  50. data/webpack/JobWizard/steps/Schedule/QueryType.js +48 -0
  51. data/webpack/JobWizard/steps/Schedule/RepeatOn.js +61 -0
  52. data/webpack/JobWizard/steps/Schedule/ScheduleType.js +25 -0
  53. data/webpack/JobWizard/steps/Schedule/StartEndDates.js +51 -0
  54. data/webpack/JobWizard/steps/Schedule/__tests__/StartEndDates.test.js +22 -0
  55. data/webpack/JobWizard/steps/Schedule/index.js +41 -0
  56. data/webpack/JobWizard/steps/form/FormHelpers.js +20 -0
  57. data/webpack/JobWizard/steps/form/Formatter.js +149 -0
  58. data/webpack/JobWizard/steps/form/GroupedSelectField.js +3 -0
  59. data/webpack/JobWizard/steps/form/NumberInput.js +33 -0
  60. data/webpack/JobWizard/steps/form/SelectField.js +24 -3
  61. data/webpack/JobWizard/steps/form/__tests__/Formatter.test.js.example +76 -0
  62. data/webpack/__mocks__/foremanReact/components/SearchBar.js +18 -1
  63. data/webpack/__mocks__/foremanReact/redux/API/APISelectors.js +21 -2
  64. data/webpack/global_index.js +5 -3
  65. data/webpack/index.js +3 -0
  66. data/webpack/react_app/components/RecentJobsCard/RecentJobsCard.js +1 -5
  67. data/webpack/react_app/components/TargetingHosts/__tests__/TargetingHostsSelectors.test.js +8 -3
  68. data/webpack/react_app/components/TargetingHosts/__tests__/__snapshots__/TargetingHostsPage.test.js.snap +1 -0
  69. data/webpack/react_app/components/TargetingHosts/__tests__/__snapshots__/TargetingHostsSelectors.test.js.snap +7 -2
  70. data/webpack/react_app/extend/{fills.js → fillRecentJobsCard.js} +7 -6
  71. data/webpack/react_app/extend/fillregistrationAdvanced.js +11 -0
  72. data/webpack/react_app/extend/reducers.js +2 -1
  73. metadata +24 -14
  74. data/app/models/concerns/foreman_remote_execution/orchestration/ssh.rb +0 -70
  75. data/test/models/orchestration/ssh_test.rb +0 -56
  76. data/webpack/JobWizard/__tests__/JobWizard.test.js +0 -20
  77. data/webpack/JobWizard/__tests__/__snapshots__/JobWizard.test.js.snap +0 -83
  78. data/webpack/JobWizard/steps/CategoryAndTemplate/__snapshots__/CategoryAndTemplate.test.js.snap +0 -64
  79. data/webpack/JobWizard/steps/form/__tests__/GroupedSelectField.test.js +0 -38
  80. data/webpack/JobWizard/steps/form/__tests__/SelectField.test.js +0 -23
  81. data/webpack/JobWizard/steps/form/__tests__/__snapshots__/GroupedSelectField.test.js.snap +0 -36
  82. data/webpack/JobWizard/steps/form/__tests__/__snapshots__/SelectField.test.js.snap +0 -22
  83. data/webpack/fills_index.js +0 -11
@@ -0,0 +1,84 @@
1
+ import React from 'react';
2
+ import { Provider } from 'react-redux';
3
+ import { mount } from '@theforeman/test';
4
+ import { render, fireEvent, screen, act } from '@testing-library/react';
5
+ import * as api from 'foremanReact/redux/API';
6
+ import { JobWizard } from '../JobWizard';
7
+ import * as selectors from '../JobWizardSelectors';
8
+ import {
9
+ testSetup,
10
+ mockApi,
11
+ jobCategories,
12
+ jobTemplateResponse as jobTemplate,
13
+ } from './fixtures';
14
+
15
+ const store = testSetup(selectors, api);
16
+
17
+ selectors.selectJobTemplate.mockImplementation(() => {});
18
+
19
+ api.get.mockImplementation(({ handleSuccess, ...action }) => {
20
+ if (action.key === 'JOB_CATEGORIES') {
21
+ handleSuccess && handleSuccess({ data: { job_categories: jobCategories } });
22
+ }
23
+ return { type: 'get', ...action };
24
+ });
25
+ describe('Job wizard fill', () => {
26
+ it('should select template', async () => {
27
+ const wrapper = mount(
28
+ <Provider store={store}>
29
+ <JobWizard advancedValues={{}} setAdvancedValues={jest.fn()} />
30
+ </Provider>
31
+ );
32
+ expect(wrapper.find('.pf-c-wizard__nav-link.pf-m-disabled')).toHaveLength(
33
+ 4
34
+ );
35
+ selectors.selectJobCategoriesStatus.mockImplementation(() => 'RESOLVED');
36
+ expect(store.getActions()).toMatchSnapshot('initial');
37
+
38
+ selectors.selectJobTemplate.mockImplementation(() => jobTemplate);
39
+ wrapper.find('.pf-c-button.pf-c-select__toggle-button').simulate('click');
40
+ await act(async () => {
41
+ await wrapper
42
+ .find('.pf-c-select__menu-item')
43
+ .first()
44
+ .simulate('click');
45
+ await wrapper.update();
46
+ });
47
+ expect(store.getActions().slice(-1)).toMatchSnapshot('select template');
48
+ expect(wrapper.find('.pf-c-wizard__nav-link.pf-m-disabled')).toHaveLength(
49
+ 0
50
+ );
51
+ });
52
+
53
+ it('have all steps', async () => {
54
+ selectors.selectJobCategoriesStatus.mockImplementation(() => null);
55
+ selectors.selectJobTemplate.mockRestore();
56
+ selectors.selectJobTemplates.mockRestore();
57
+ selectors.selectJobCategories.mockRestore();
58
+ mockApi(api);
59
+
60
+ render(
61
+ <Provider store={store}>
62
+ <JobWizard />
63
+ </Provider>
64
+ );
65
+ const steps = [
66
+ 'Target Hosts',
67
+ 'Advanced Fields',
68
+ 'Schedule',
69
+ 'Review Details',
70
+ 'Category and Template',
71
+ ];
72
+ // eslint-disable-next-line no-unused-vars
73
+ for await (const step of steps) {
74
+ const stepSelector = screen.getByText(step);
75
+ const stepTitle = screen.getAllByText(step);
76
+ expect(stepTitle).toHaveLength(1);
77
+ await act(async () => {
78
+ await fireEvent.click(stepSelector);
79
+ });
80
+ const stepTitles = screen.getAllByText(step);
81
+ expect(stepTitles).toHaveLength(3);
82
+ }
83
+ });
84
+ });
@@ -0,0 +1,110 @@
1
+ import React from 'react';
2
+ import PropTypes from 'prop-types';
3
+ import { useSelector } from 'react-redux';
4
+ import { Title, Form } from '@patternfly/react-core';
5
+ import { translate as __ } from 'foremanReact/common/I18n';
6
+ import {
7
+ selectEffectiveUser,
8
+ selectAdvancedTemplateInputs,
9
+ selectTemplateInputs,
10
+ } from '../../JobWizardSelectors';
11
+ import {
12
+ EffectiveUserField,
13
+ TimeoutToKillField,
14
+ PasswordField,
15
+ KeyPassphraseField,
16
+ EffectiveUserPasswordField,
17
+ ConcurrencyLevelField,
18
+ TimeSpanLevelField,
19
+ TemplateInputsFields,
20
+ } from './Fields';
21
+ import { DescriptionField } from './DescriptionField';
22
+
23
+ export const AdvancedFields = ({ advancedValues, setAdvancedValues }) => {
24
+ const effectiveUser = useSelector(selectEffectiveUser);
25
+ const advancedTemplateInputs = useSelector(selectAdvancedTemplateInputs);
26
+ const templateInputs = useSelector(selectTemplateInputs);
27
+ return (
28
+ <>
29
+ <Title headingLevel="h2" className="advanced-fields-title">
30
+ {__('Advanced Fields')}
31
+ </Title>
32
+ <Form id="advanced-fields-job-template" autoComplete="off">
33
+ <TemplateInputsFields
34
+ inputs={advancedTemplateInputs}
35
+ value={advancedValues.templateValues}
36
+ setValue={newValue => setAdvancedValues({ templateValues: newValue })}
37
+ />
38
+ {effectiveUser?.overridable && (
39
+ <EffectiveUserField
40
+ value={advancedValues.effectiveUserValue}
41
+ setValue={newValue =>
42
+ setAdvancedValues({
43
+ effectiveUserValue: newValue,
44
+ })
45
+ }
46
+ />
47
+ )}
48
+ <DescriptionField
49
+ inputs={templateInputs}
50
+ value={advancedValues.description}
51
+ setValue={newValue => setAdvancedValues({ description: newValue })}
52
+ />
53
+ <TimeoutToKillField
54
+ value={advancedValues.timeoutToKill}
55
+ setValue={newValue =>
56
+ setAdvancedValues({
57
+ timeoutToKill: newValue,
58
+ })
59
+ }
60
+ />
61
+ <PasswordField
62
+ value={advancedValues.password}
63
+ setValue={newValue =>
64
+ setAdvancedValues({
65
+ password: newValue,
66
+ })
67
+ }
68
+ />
69
+ <KeyPassphraseField
70
+ value={advancedValues.keyPassphrase}
71
+ setValue={newValue =>
72
+ setAdvancedValues({
73
+ keyPassphrase: newValue,
74
+ })
75
+ }
76
+ />
77
+ <EffectiveUserPasswordField
78
+ value={advancedValues.effectiveUserPassword}
79
+ setValue={newValue =>
80
+ setAdvancedValues({
81
+ effectiveUserPassword: newValue,
82
+ })
83
+ }
84
+ />
85
+ <ConcurrencyLevelField
86
+ value={advancedValues.concurrencyLevel}
87
+ setValue={newValue =>
88
+ setAdvancedValues({
89
+ concurrencyLevel: newValue,
90
+ })
91
+ }
92
+ />
93
+ <TimeSpanLevelField
94
+ value={advancedValues.timeSpan}
95
+ setValue={newValue =>
96
+ setAdvancedValues({
97
+ timeSpan: newValue,
98
+ })
99
+ }
100
+ />
101
+ </Form>
102
+ </>
103
+ );
104
+ };
105
+
106
+ AdvancedFields.propTypes = {
107
+ advancedValues: PropTypes.object.isRequired,
108
+ setAdvancedValues: PropTypes.func.isRequired,
109
+ };
110
+ export default AdvancedFields;
@@ -0,0 +1,67 @@
1
+ import React, { useState } from 'react';
2
+ import PropTypes from 'prop-types';
3
+ import { FormGroup, TextInput, Button } from '@patternfly/react-core';
4
+ import { translate as __ } from 'foremanReact/common/I18n';
5
+
6
+ export const DescriptionField = ({ inputs, value, setValue }) => {
7
+ const generateDesc = () => {
8
+ let newDesc = value;
9
+ if (value) {
10
+ const re = new RegExp('%\\{([^\\}]+)\\}', 'gm');
11
+ const results = [...newDesc.matchAll(re)].map(result => ({
12
+ name: result[1],
13
+ text: result[0],
14
+ }));
15
+ results.forEach(result => {
16
+ newDesc = newDesc.replace(
17
+ result.text,
18
+ // TODO: Replace with the value of the input from Target Hosts step
19
+ inputs.find(input => input.name === result.name)?.name || result.text
20
+ );
21
+ });
22
+ }
23
+ return newDesc;
24
+ };
25
+ const [generatedDesc, setGeneratedDesc] = useState(generateDesc());
26
+ const [isPreview, setIsPreview] = useState(true);
27
+
28
+ const togglePreview = () => {
29
+ setGeneratedDesc(generateDesc());
30
+ setIsPreview(v => !v);
31
+ };
32
+
33
+ return (
34
+ <FormGroup
35
+ label={__('Description')}
36
+ fieldId="description"
37
+ helperText={
38
+ <Button variant="link" isInline onClick={togglePreview}>
39
+ {isPreview
40
+ ? __('Edit job description template')
41
+ : __('Preview job description')}
42
+ </Button>
43
+ }
44
+ >
45
+ {isPreview ? (
46
+ <TextInput id="description-preview" value={generatedDesc} isDisabled />
47
+ ) : (
48
+ <TextInput
49
+ type="text"
50
+ autoComplete="description"
51
+ id="description"
52
+ value={value}
53
+ onChange={newValue => setValue(newValue)}
54
+ />
55
+ )}
56
+ </FormGroup>
57
+ );
58
+ };
59
+
60
+ DescriptionField.propTypes = {
61
+ inputs: PropTypes.array.isRequired,
62
+ value: PropTypes.string,
63
+ setValue: PropTypes.func.isRequired,
64
+ };
65
+ DescriptionField.defaultProps = {
66
+ value: '',
67
+ };
@@ -0,0 +1,195 @@
1
+ import React from 'react';
2
+ import PropTypes from 'prop-types';
3
+ import { FormGroup, TextInput } from '@patternfly/react-core';
4
+ import { translate as __ } from 'foremanReact/common/I18n';
5
+ import { helpLabel } from '../form/FormHelpers';
6
+ import { formatter } from '../form/Formatter';
7
+ import { NumberInput } from '../form/NumberInput';
8
+
9
+ export const EffectiveUserField = ({ value, setValue }) => (
10
+ <FormGroup
11
+ label={__('Effective user')}
12
+ labelIcon={helpLabel(
13
+ __(
14
+ 'A user to be used for executing the script. If it differs from the SSH user, su or sudo is used to switch the accounts.'
15
+ ),
16
+ 'effective-user'
17
+ )}
18
+ fieldId="effective-user"
19
+ >
20
+ <TextInput
21
+ autoComplete="effective-user"
22
+ id="effective-user"
23
+ type="text"
24
+ value={value}
25
+ onChange={newValue => setValue(newValue)}
26
+ />
27
+ </FormGroup>
28
+ );
29
+
30
+ export const TimeoutToKillField = ({ value, setValue }) => (
31
+ <NumberInput
32
+ formProps={{
33
+ label: __('Timeout to kill'),
34
+ labelIcon: helpLabel(
35
+ __(
36
+ 'Time in seconds from the start on the remote host after which the job should be killed.'
37
+ ),
38
+ 'timeout-to-kill'
39
+ ),
40
+ fieldId: 'timeout-to-kill',
41
+ }}
42
+ inputProps={{
43
+ value,
44
+ placeholder: __('For example: 1, 2, 3, 4, 5...'),
45
+ autoComplete: 'timeout-to-kill',
46
+ id: 'timeout-to-kill',
47
+ onChange: newValue => setValue(newValue),
48
+ }}
49
+ />
50
+ );
51
+
52
+ export const PasswordField = ({ value, setValue }) => (
53
+ <FormGroup
54
+ label={__('Password')}
55
+ labelIcon={helpLabel(
56
+ __(
57
+ 'Password is stored encrypted in DB until the job finishes. For future or recurring executions, it is removed after the last execution.'
58
+ ),
59
+ 'password'
60
+ )}
61
+ fieldId="job-password"
62
+ >
63
+ <TextInput
64
+ autoComplete="new-password" // to prevent firefox from autofilling the user password
65
+ id="job-password"
66
+ type="password"
67
+ placeholder="*****"
68
+ value={value}
69
+ onChange={newValue => setValue(newValue)}
70
+ />
71
+ </FormGroup>
72
+ );
73
+
74
+ export const KeyPassphraseField = ({ value, setValue }) => (
75
+ <FormGroup
76
+ label={__('Private key passphrase')}
77
+ labelIcon={helpLabel(
78
+ __(
79
+ 'Key passphrase is only applicable for SSH provider. Other providers ignore this field. Passphrase is stored encrypted in DB until the job finishes. For future or recurring executions, it is removed after the last execution.'
80
+ ),
81
+ 'key-passphrase'
82
+ )}
83
+ fieldId="key-passphrase"
84
+ >
85
+ <TextInput
86
+ autoComplete="key-passphrase"
87
+ id="key-passphrase"
88
+ type="password"
89
+ placeholder="*****"
90
+ value={value}
91
+ onChange={newValue => setValue(newValue)}
92
+ />
93
+ </FormGroup>
94
+ );
95
+
96
+ export const EffectiveUserPasswordField = ({ value, setValue }) => (
97
+ <FormGroup
98
+ label={__('Effective user password')}
99
+ labelIcon={helpLabel(
100
+ __(
101
+ 'Effective user password is only applicable for SSH provider. Other providers ignore this field. Password is stored encrypted in DB until the job finishes. For future or recurring executions, it is removed after the last execution.'
102
+ ),
103
+ 'effective-user-password'
104
+ )}
105
+ fieldId="effective-user-password"
106
+ >
107
+ <TextInput
108
+ autoComplete="effective-user-password"
109
+ id="effective-user-password"
110
+ type="password"
111
+ placeholder="*****"
112
+ value={value}
113
+ onChange={newValue => setValue(newValue)}
114
+ />
115
+ </FormGroup>
116
+ );
117
+
118
+ export const ConcurrencyLevelField = ({ value, setValue }) => (
119
+ <NumberInput
120
+ formProps={{
121
+ label: __('Concurrency level'),
122
+ labelIcon: helpLabel(
123
+ __(
124
+ 'Run at most N tasks at a time. If this is set and proxy batch triggering is enabled, then tasks are triggered on the smart proxy in batches of size 1.'
125
+ ),
126
+ 'concurrency-level'
127
+ ),
128
+ fieldId: 'concurrency-level',
129
+ }}
130
+ inputProps={{
131
+ min: 1,
132
+ autoComplete: 'concurrency-level',
133
+ id: 'concurrency-level',
134
+ placeholder: __('For example: 1, 2, 3, 4, 5...'),
135
+ value,
136
+ onChange: newValue => setValue(newValue),
137
+ }}
138
+ />
139
+ );
140
+
141
+ export const TimeSpanLevelField = ({ value, setValue }) => (
142
+ <NumberInput
143
+ formProps={{
144
+ label: __('Time span'),
145
+ labelIcon: helpLabel(
146
+ __(
147
+ 'Distribute execution over N seconds. If this is set and proxy batch triggering is enabled, then tasks are triggered on the smart proxy in batches of size 1.'
148
+ ),
149
+ 'time-span'
150
+ ),
151
+ fieldId: 'time-span',
152
+ }}
153
+ inputProps={{
154
+ min: 1,
155
+ autoComplete: 'time-span',
156
+ id: 'time-span',
157
+ placeholder: __('For example: 1, 2, 3, 4, 5...'),
158
+ value,
159
+ onChange: newValue => setValue(newValue),
160
+ }}
161
+ />
162
+ );
163
+
164
+ export const TemplateInputsFields = ({ inputs, value, setValue }) => (
165
+ <>{inputs?.map(input => formatter(input, value, setValue))}</>
166
+ );
167
+ EffectiveUserField.propTypes = {
168
+ value: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
169
+ setValue: PropTypes.func.isRequired,
170
+ };
171
+ EffectiveUserField.defaultProps = {
172
+ value: '',
173
+ };
174
+
175
+ TimeoutToKillField.propTypes = EffectiveUserField.propTypes;
176
+ TimeoutToKillField.defaultProps = EffectiveUserField.defaultProps;
177
+ PasswordField.propTypes = EffectiveUserField.propTypes;
178
+ PasswordField.defaultProps = EffectiveUserField.defaultProps;
179
+ KeyPassphraseField.propTypes = EffectiveUserField.propTypes;
180
+ KeyPassphraseField.defaultProps = EffectiveUserField.defaultProps;
181
+ EffectiveUserPasswordField.propTypes = EffectiveUserField.propTypes;
182
+ EffectiveUserPasswordField.defaultProps = EffectiveUserField.defaultProps;
183
+ ConcurrencyLevelField.propTypes = EffectiveUserField.propTypes;
184
+ ConcurrencyLevelField.defaultProps = EffectiveUserField.defaultProps;
185
+ TimeSpanLevelField.propTypes = EffectiveUserField.propTypes;
186
+ TimeSpanLevelField.defaultProps = EffectiveUserField.defaultProps;
187
+ TemplateInputsFields.propTypes = {
188
+ inputs: PropTypes.array.isRequired,
189
+ value: PropTypes.object,
190
+ setValue: PropTypes.func.isRequired,
191
+ };
192
+
193
+ TemplateInputsFields.defaultProps = {
194
+ value: {},
195
+ };