foreman_remote_execution 4.6.0 → 5.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (143) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ruby_ci.yml +7 -0
  3. data/.rubocop_todo.yml +1 -0
  4. data/app/controllers/api/v2/job_invocations_controller.rb +16 -1
  5. data/app/controllers/job_invocations_controller.rb +1 -1
  6. data/app/controllers/ui_job_wizard_controller.rb +21 -2
  7. data/app/graphql/mutations/job_invocations/create.rb +43 -0
  8. data/app/graphql/types/job_invocation.rb +16 -0
  9. data/app/graphql/types/job_invocation_input.rb +13 -0
  10. data/app/graphql/types/recurrence_input.rb +8 -0
  11. data/app/graphql/types/scheduling_input.rb +6 -0
  12. data/app/graphql/types/targeting_enum.rb +7 -0
  13. data/app/helpers/concerns/foreman_remote_execution/hosts_helper_extensions.rb +5 -1
  14. data/app/helpers/remote_execution_helper.rb +9 -3
  15. data/app/lib/actions/remote_execution/run_host_job.rb +10 -1
  16. data/app/lib/actions/remote_execution/run_hosts_job.rb +58 -4
  17. data/app/mailers/rex_job_mailer.rb +15 -0
  18. data/app/models/concerns/foreman_remote_execution/host_extensions.rb +10 -0
  19. data/app/models/concerns/foreman_remote_execution/smart_proxy_extensions.rb +6 -0
  20. data/app/models/host_proxy_invocation.rb +4 -0
  21. data/app/models/host_status/execution_status.rb +3 -3
  22. data/app/models/job_invocation.rb +12 -5
  23. data/app/models/job_invocation_composer.rb +25 -17
  24. data/app/models/job_template.rb +1 -1
  25. data/app/models/remote_execution_feature.rb +5 -1
  26. data/app/models/remote_execution_provider.rb +18 -2
  27. data/app/models/rex_mail_notification.rb +13 -0
  28. data/app/models/targeting.rb +7 -3
  29. data/app/services/ui_notifications/remote_execution_jobs/base_job_finish.rb +2 -1
  30. data/app/views/dashboard/_latest-jobs.html.erb +21 -0
  31. data/app/views/job_invocations/index.html.erb +1 -1
  32. data/app/views/job_invocations/refresh.js.erb +1 -0
  33. data/app/views/rex_job_mailer/job_finished.html.erb +24 -0
  34. data/app/views/rex_job_mailer/job_finished.text.erb +9 -0
  35. data/app/views/template_invocations/show.html.erb +2 -1
  36. data/app/views/templates/ssh/module_action.erb +1 -0
  37. data/app/views/templates/ssh/power_action.erb +2 -0
  38. data/app/views/templates/ssh/puppet_run_once.erb +1 -0
  39. data/config/routes.rb +1 -0
  40. data/db/migrate/2021051713291621250977_add_host_proxy_invocations.rb +12 -0
  41. data/db/migrate/20210816100932_rex_setting_category_to_dsl.rb +5 -0
  42. data/db/seeds.d/50-notification_blueprints.rb +14 -0
  43. data/db/seeds.d/95-mail_notifications.rb +24 -0
  44. data/foreman_remote_execution.gemspec +2 -3
  45. data/lib/foreman_remote_execution/engine.rb +114 -8
  46. data/lib/foreman_remote_execution/version.rb +1 -1
  47. data/package.json +9 -7
  48. data/test/functional/api/v2/job_invocations_controller_test.rb +20 -0
  49. data/test/functional/cockpit_controller_test.rb +0 -1
  50. data/test/graphql/mutations/job_invocations/create.rb +58 -0
  51. data/test/graphql/queries/job_invocation_query_test.rb +31 -0
  52. data/test/graphql/queries/job_invocations_query_test.rb +35 -0
  53. data/test/helpers/remote_execution_helper_test.rb +0 -1
  54. data/test/unit/actions/run_host_job_test.rb +21 -0
  55. data/test/unit/actions/run_hosts_job_test.rb +99 -4
  56. data/test/unit/concerns/host_extensions_test.rb +40 -7
  57. data/test/unit/input_template_renderer_test.rb +1 -89
  58. data/test/unit/job_invocation_composer_test.rb +18 -18
  59. data/test/unit/job_invocation_report_template_test.rb +16 -13
  60. data/test/unit/job_invocation_test.rb +1 -1
  61. data/test/unit/job_template_effective_user_test.rb +0 -4
  62. data/test/unit/remote_execution_provider_test.rb +46 -4
  63. data/test/unit/targeting_test.rb +68 -1
  64. data/webpack/JobWizard/JobWizard.js +158 -24
  65. data/webpack/JobWizard/JobWizard.scss +93 -1
  66. data/webpack/JobWizard/JobWizardConstants.js +54 -0
  67. data/webpack/JobWizard/JobWizardSelectors.js +41 -0
  68. data/webpack/JobWizard/__tests__/fixtures.js +188 -3
  69. data/webpack/JobWizard/__tests__/integration.test.js +41 -106
  70. data/webpack/JobWizard/__tests__/validation.test.js +141 -0
  71. data/webpack/JobWizard/autofill.js +38 -0
  72. data/webpack/JobWizard/index.js +7 -0
  73. data/webpack/JobWizard/steps/AdvancedFields/AdvancedFields.js +41 -10
  74. data/webpack/JobWizard/steps/AdvancedFields/DescriptionField.js +90 -0
  75. data/webpack/JobWizard/steps/AdvancedFields/Fields.js +116 -55
  76. data/webpack/JobWizard/steps/AdvancedFields/__tests__/AdvancedFields.test.js +354 -16
  77. data/webpack/JobWizard/steps/AdvancedFields/__tests__/__snapshots__/AdvancedFields.test.js.snap +79 -246
  78. data/webpack/JobWizard/steps/CategoryAndTemplate/CategoryAndTemplate.js +5 -2
  79. data/webpack/JobWizard/steps/CategoryAndTemplate/CategoryAndTemplate.test.js +123 -51
  80. data/webpack/JobWizard/steps/CategoryAndTemplate/index.js +3 -2
  81. data/webpack/JobWizard/steps/HostsAndInputs/HostPreviewModal.js +62 -0
  82. data/webpack/JobWizard/steps/HostsAndInputs/HostSearch.js +54 -0
  83. data/webpack/JobWizard/steps/HostsAndInputs/SelectAPI.js +33 -0
  84. data/webpack/JobWizard/steps/HostsAndInputs/SelectGQL.js +52 -0
  85. data/webpack/JobWizard/steps/HostsAndInputs/SelectedChips.js +100 -0
  86. data/webpack/JobWizard/steps/HostsAndInputs/TemplateInputs.js +23 -0
  87. data/webpack/JobWizard/steps/HostsAndInputs/__tests__/HostsAndInputs.test.js +151 -0
  88. data/webpack/JobWizard/steps/HostsAndInputs/__tests__/TemplateInputs.test.js +53 -0
  89. data/webpack/JobWizard/steps/HostsAndInputs/buildHostQuery.js +18 -0
  90. data/webpack/JobWizard/steps/HostsAndInputs/hostgroups.gql +8 -0
  91. data/webpack/JobWizard/steps/HostsAndInputs/hosts.gql +8 -0
  92. data/webpack/JobWizard/steps/HostsAndInputs/index.js +214 -0
  93. data/webpack/JobWizard/steps/ReviewDetails/index.js +193 -0
  94. data/webpack/JobWizard/steps/Schedule/PurposeField.js +31 -0
  95. data/webpack/JobWizard/steps/Schedule/QueryType.js +51 -0
  96. data/webpack/JobWizard/steps/Schedule/RepeatCron.js +53 -0
  97. data/webpack/JobWizard/steps/Schedule/RepeatDaily.js +37 -0
  98. data/webpack/JobWizard/steps/Schedule/RepeatHour.js +54 -0
  99. data/webpack/JobWizard/steps/Schedule/RepeatMonth.js +46 -0
  100. data/webpack/JobWizard/steps/Schedule/RepeatOn.js +125 -0
  101. data/webpack/JobWizard/steps/Schedule/RepeatWeek.js +70 -0
  102. data/webpack/JobWizard/steps/Schedule/ScheduleType.js +28 -0
  103. data/webpack/JobWizard/steps/Schedule/StartEndDates.js +106 -0
  104. data/webpack/JobWizard/steps/Schedule/__tests__/Schedule.test.js +402 -0
  105. data/webpack/JobWizard/steps/Schedule/__tests__/StartEndDates.test.js +32 -0
  106. data/webpack/JobWizard/steps/Schedule/index.js +178 -0
  107. data/webpack/JobWizard/steps/form/DateTimePicker.js +126 -0
  108. data/webpack/JobWizard/steps/form/FormHelpers.js +5 -0
  109. data/webpack/JobWizard/steps/form/Formatter.js +181 -0
  110. data/webpack/JobWizard/steps/form/NumberInput.js +36 -0
  111. data/webpack/JobWizard/steps/form/ResourceSelect.js +29 -0
  112. data/webpack/JobWizard/steps/form/SearchSelect.js +121 -0
  113. data/webpack/JobWizard/steps/form/SelectField.js +28 -5
  114. data/webpack/JobWizard/steps/form/WizardTitle.js +14 -0
  115. data/webpack/JobWizard/steps/form/__tests__/SelectSearch.test.js +33 -0
  116. data/webpack/JobWizard/submit.js +120 -0
  117. data/webpack/JobWizard/validation.js +53 -0
  118. data/webpack/__mocks__/foremanReact/Root/Context/ForemanContext/index.js +2 -0
  119. data/webpack/__mocks__/foremanReact/common/I18n.js +2 -0
  120. data/webpack/__mocks__/foremanReact/components/AutoComplete/AutoCompleteActions.js +1 -0
  121. data/webpack/__mocks__/foremanReact/components/AutoComplete/AutoCompleteConstants.js +1 -0
  122. data/webpack/__mocks__/foremanReact/components/SearchBar.js +18 -1
  123. data/webpack/__mocks__/foremanReact/routes/RouterSelector.js +1 -0
  124. data/webpack/helpers.js +1 -0
  125. data/webpack/react_app/components/RecentJobsCard/JobStatusIcon.js +43 -0
  126. data/webpack/react_app/components/RecentJobsCard/RecentJobsCard.js +73 -66
  127. data/webpack/react_app/components/RecentJobsCard/RecentJobsTable.js +98 -0
  128. data/webpack/react_app/components/RecentJobsCard/constants.js +11 -0
  129. data/webpack/react_app/components/RecentJobsCard/styles.scss +11 -0
  130. data/webpack/react_app/components/TargetingHosts/__tests__/__snapshots__/TargetingHostsPage.test.js.snap +1 -0
  131. data/webpack/react_app/extend/fillRecentJobsCard.js +1 -1
  132. metadata +71 -16
  133. data/app/models/concerns/foreman_remote_execution/orchestration/ssh.rb +0 -70
  134. data/app/models/setting/remote_execution.rb +0 -88
  135. data/test/models/orchestration/ssh_test.rb +0 -56
  136. data/webpack/JobWizard/__tests__/JobWizard.test.js +0 -13
  137. data/webpack/JobWizard/__tests__/__snapshots__/JobWizard.test.js.snap +0 -32
  138. data/webpack/JobWizard/steps/CategoryAndTemplate/__snapshots__/CategoryAndTemplate.test.js.snap +0 -113
  139. data/webpack/JobWizard/steps/form/__tests__/GroupedSelectField.test.js +0 -38
  140. data/webpack/JobWizard/steps/form/__tests__/SelectField.test.js +0 -23
  141. data/webpack/JobWizard/steps/form/__tests__/__snapshots__/GroupedSelectField.test.js.snap +0 -37
  142. data/webpack/JobWizard/steps/form/__tests__/__snapshots__/SelectField.test.js.snap +0 -23
  143. data/webpack/react_app/components/RecentJobsCard/styles.css +0 -15
@@ -0,0 +1,90 @@
1
+ import React, { useState, useEffect, useCallback } from 'react';
2
+ import { useSelector } from 'react-redux';
3
+ import PropTypes from 'prop-types';
4
+ import { FormGroup, TextInput, Tooltip, Button } from '@patternfly/react-core';
5
+ import { translate as __ } from 'foremanReact/common/I18n';
6
+ import {
7
+ selectTemplateInputs,
8
+ selectAdvancedTemplateInputs,
9
+ } from '../../JobWizardSelectors';
10
+
11
+ export const DescriptionField = ({ inputValues, value, setValue }) => {
12
+ const inputs = [
13
+ ...useSelector(selectTemplateInputs),
14
+ ...useSelector(selectAdvancedTemplateInputs),
15
+ ].map(input => input.name);
16
+ const generateDesc = useCallback(() => {
17
+ let newDesc = value;
18
+ if (value) {
19
+ const re = new RegExp('%\\{([^\\}]+)\\}', 'gm');
20
+ const results = [...newDesc.matchAll(re)].map(result => ({
21
+ name: result[1],
22
+ text: result[0],
23
+ }));
24
+ results.forEach(result => {
25
+ newDesc = newDesc.replace(
26
+ result.text,
27
+ inputValues[result.name] ||
28
+ (inputs.includes(result.name) ? '' : result.text)
29
+ );
30
+ });
31
+ }
32
+ return newDesc;
33
+ }, [inputs, value, inputValues]);
34
+ const [generatedDesc, setGeneratedDesc] = useState(generateDesc());
35
+ const [isPreview, setIsPreview] = useState(true);
36
+
37
+ useEffect(() => {
38
+ setGeneratedDesc(generateDesc());
39
+ }, [generateDesc]);
40
+ const togglePreview = () => {
41
+ setGeneratedDesc(generateDesc());
42
+ setIsPreview(v => !v);
43
+ };
44
+
45
+ return (
46
+ <FormGroup
47
+ label={__('Description')}
48
+ fieldId="description"
49
+ helperText={
50
+ <Button variant="link" isInline onClick={togglePreview}>
51
+ {isPreview
52
+ ? __('Edit job description template')
53
+ : __('Preview job description')}
54
+ </Button>
55
+ }
56
+ >
57
+ {isPreview ? (
58
+ <Tooltip content={generatedDesc}>
59
+ <div>
60
+ {/* div wrapper so the tooltip will be shown in chrome */}
61
+ <TextInput
62
+ aria-label="description preview"
63
+ id="description-preview"
64
+ value={generatedDesc}
65
+ isDisabled
66
+ />
67
+ </div>
68
+ </Tooltip>
69
+ ) : (
70
+ <TextInput
71
+ aria-label="description edit"
72
+ type="text"
73
+ autoComplete="description"
74
+ id="description"
75
+ value={value}
76
+ onChange={newValue => setValue(newValue)}
77
+ />
78
+ )}
79
+ </FormGroup>
80
+ );
81
+ };
82
+
83
+ DescriptionField.propTypes = {
84
+ inputValues: PropTypes.object.isRequired,
85
+ value: PropTypes.string,
86
+ setValue: PropTypes.func.isRequired,
87
+ };
88
+ DescriptionField.defaultProps = {
89
+ value: '',
90
+ };
@@ -1,8 +1,10 @@
1
1
  import React from 'react';
2
2
  import PropTypes from 'prop-types';
3
- import { FormGroup, TextInput } from '@patternfly/react-core';
3
+ import { FormGroup, TextInput, Radio } from '@patternfly/react-core';
4
4
  import { translate as __ } from 'foremanReact/common/I18n';
5
5
  import { helpLabel } from '../form/FormHelpers';
6
+ import { formatter } from '../form/Formatter';
7
+ import { NumberInput } from '../form/NumberInput';
6
8
 
7
9
  export const EffectiveUserField = ({ value, setValue }) => (
8
10
  <FormGroup
@@ -16,6 +18,7 @@ export const EffectiveUserField = ({ value, setValue }) => (
16
18
  fieldId="effective-user"
17
19
  >
18
20
  <TextInput
21
+ aria-label="effective user"
19
22
  autoComplete="effective-user"
20
23
  id="effective-user"
21
24
  type="text"
@@ -26,25 +29,25 @@ export const EffectiveUserField = ({ value, setValue }) => (
26
29
  );
27
30
 
28
31
  export const TimeoutToKillField = ({ value, setValue }) => (
29
- <FormGroup
30
- label={__('Timeout to kill')}
31
- labelIcon={helpLabel(
32
- __(
33
- 'Time in seconds from the start on the remote host after which the job should be killed.'
32
+ <NumberInput
33
+ formProps={{
34
+ label: __('Timeout to kill'),
35
+ labelIcon: helpLabel(
36
+ __(
37
+ 'Time in seconds from the start on the remote host after which the job should be killed.'
38
+ ),
39
+ 'timeout-to-kill'
34
40
  ),
35
- 'timeout-to-kill'
36
- )}
37
- fieldId="timeout-to-kill"
38
- >
39
- <TextInput
40
- type="number"
41
- value={value}
42
- placeholder={__('For example: 1, 2, 3, 4, 5...')}
43
- autoComplete="timeout-to-kill"
44
- id="timeout-to-kill"
45
- onChange={newValue => setValue(newValue)}
46
- />
47
- </FormGroup>
41
+ fieldId: 'timeout-to-kill',
42
+ }}
43
+ inputProps={{
44
+ value,
45
+ placeholder: __('For example: 1, 2, 3, 4, 5...'),
46
+ autoComplete: 'timeout-to-kill',
47
+ id: 'timeout-to-kill',
48
+ onChange: newValue => setValue(newValue),
49
+ }}
50
+ />
48
51
  );
49
52
 
50
53
  export const PasswordField = ({ value, setValue }) => (
@@ -56,11 +59,12 @@ export const PasswordField = ({ value, setValue }) => (
56
59
  ),
57
60
  'password'
58
61
  )}
59
- fieldId="password"
62
+ fieldId="job-password"
60
63
  >
61
64
  <TextInput
62
- autoComplete="password"
63
- id="password"
65
+ aria-label="job password"
66
+ autoComplete="new-password" // to prevent firefox from autofilling the user password
67
+ id="job-password"
64
68
  type="password"
65
69
  placeholder="*****"
66
70
  value={value}
@@ -81,6 +85,7 @@ export const KeyPassphraseField = ({ value, setValue }) => (
81
85
  fieldId="key-passphrase"
82
86
  >
83
87
  <TextInput
88
+ aria-label="key passphrase"
84
89
  autoComplete="key-passphrase"
85
90
  id="key-passphrase"
86
91
  type="password"
@@ -103,6 +108,7 @@ export const EffectiveUserPasswordField = ({ value, setValue }) => (
103
108
  fieldId="effective-user-password"
104
109
  >
105
110
  <TextInput
111
+ aria-label="effective userpassword"
106
112
  autoComplete="effective-user-password"
107
113
  id="effective-user-password"
108
114
  type="password"
@@ -114,51 +120,89 @@ export const EffectiveUserPasswordField = ({ value, setValue }) => (
114
120
  );
115
121
 
116
122
  export const ConcurrencyLevelField = ({ value, setValue }) => (
117
- <FormGroup
118
- label={__('Concurrency level')}
119
- labelIcon={helpLabel(
120
- __(
121
- '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.'
123
+ <NumberInput
124
+ formProps={{
125
+ label: __('Concurrency level'),
126
+ labelIcon: helpLabel(
127
+ __(
128
+ '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.'
129
+ ),
130
+ 'concurrency-level'
122
131
  ),
123
- 'concurrency-level'
124
- )}
125
- fieldId="concurrency-level"
126
- >
127
- <TextInput
128
- min={1}
129
- type="number"
130
- autoComplete="concurrency-level"
131
- id="concurrency-level"
132
- placeholder={__('For example: 1, 2, 3, 4, 5...')}
133
- value={value}
134
- onChange={newValue => setValue(newValue)}
135
- />
136
- </FormGroup>
132
+ fieldId: 'concurrency-level',
133
+ }}
134
+ inputProps={{
135
+ min: 1,
136
+ autoComplete: 'concurrency-level',
137
+ id: 'concurrency-level',
138
+ placeholder: __('For example: 1, 2, 3, 4, 5...'),
139
+ value,
140
+ onChange: newValue => setValue(newValue),
141
+ }}
142
+ />
137
143
  );
138
144
 
139
145
  export const TimeSpanLevelField = ({ value, setValue }) => (
146
+ <NumberInput
147
+ formProps={{
148
+ label: __('Time span'),
149
+ labelIcon: helpLabel(
150
+ __(
151
+ '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.'
152
+ ),
153
+ 'time-span'
154
+ ),
155
+ fieldId: 'time-span',
156
+ }}
157
+ inputProps={{
158
+ min: 1,
159
+ autoComplete: 'time-span',
160
+ id: 'time-span',
161
+ placeholder: __('For example: 1, 2, 3, 4, 5...'),
162
+ value,
163
+ onChange: newValue => setValue(newValue),
164
+ }}
165
+ />
166
+ );
167
+
168
+ export const ExecutionOrderingField = ({ isRandomizedOrdering, setValue }) => (
140
169
  <FormGroup
141
- label={__('Time span')}
170
+ label={__('Execution ordering')}
171
+ fieldId="schedule-type"
142
172
  labelIcon={helpLabel(
143
- __(
144
- '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.'
145
- ),
146
- 'time-span'
173
+ <div
174
+ dangerouslySetInnerHTML={{
175
+ __html: __(
176
+ 'Execution ordering determines whether the jobs should be executed on hosts in alphabetical order or in randomized order.<br><ul><li><b>Ordered</b> - executes the jobs on hosts in alphabetical order</li><li><b>Randomized</b> - randomizes the order in which jobs are executed on hosts</li></ul>'
177
+ ),
178
+ }}
179
+ />,
180
+ 'effective-user-password'
147
181
  )}
148
- fieldId="time-span"
182
+ isInline
149
183
  >
150
- <TextInput
151
- min={1}
152
- type="number"
153
- autoComplete="time-span"
154
- id="time-span"
155
- placeholder={__('For example: 1, 2, 3, 4, 5...')}
156
- value={value}
157
- onChange={newValue => setValue(newValue)}
184
+ <Radio
185
+ aria-label="execution order alphabetical"
186
+ isChecked={!isRandomizedOrdering}
187
+ name="execution-order"
188
+ onChange={() => setValue(false)}
189
+ id="execution-order-alphabetical"
190
+ label={__('Alphabetical')}
191
+ />
192
+ <Radio
193
+ aria-label="execution order randomized"
194
+ isChecked={isRandomizedOrdering}
195
+ name="execution-order"
196
+ onChange={() => setValue(true)}
197
+ id="execution-order-randomized"
198
+ label={__('Randomized')}
158
199
  />
159
200
  </FormGroup>
160
201
  );
161
202
 
203
+ export const TemplateInputsFields = ({ inputs, value, setValue }) => (
204
+ <>{inputs?.map(input => formatter(input, value, setValue))}</>
205
+ );
162
206
  EffectiveUserField.propTypes = {
163
207
  value: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
164
208
  setValue: PropTypes.func.isRequired,
@@ -179,3 +223,20 @@ ConcurrencyLevelField.propTypes = EffectiveUserField.propTypes;
179
223
  ConcurrencyLevelField.defaultProps = EffectiveUserField.defaultProps;
180
224
  TimeSpanLevelField.propTypes = EffectiveUserField.propTypes;
181
225
  TimeSpanLevelField.defaultProps = EffectiveUserField.defaultProps;
226
+ ExecutionOrderingField.propTypes = {
227
+ isRandomizedOrdering: PropTypes.bool,
228
+ setValue: PropTypes.func.isRequired,
229
+ };
230
+ ExecutionOrderingField.defaultProps = {
231
+ isRandomizedOrdering: false,
232
+ };
233
+
234
+ TemplateInputsFields.propTypes = {
235
+ inputs: PropTypes.array.isRequired,
236
+ value: PropTypes.object,
237
+ setValue: PropTypes.func.isRequired,
238
+ };
239
+
240
+ TemplateInputsFields.defaultProps = {
241
+ value: {},
242
+ };