foreman_remote_execution 4.3.0 → 4.5.1

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 (103) hide show
  1. checksums.yaml +4 -4
  2. data/app/controllers/api/v2/job_invocations_controller.rb +27 -22
  3. data/app/controllers/foreman_remote_execution/concerns/api/v2/registration_commands_controller_extensions.rb +19 -0
  4. data/app/controllers/job_invocations_controller.rb +1 -1
  5. data/app/controllers/job_templates_controller.rb +4 -4
  6. data/app/controllers/ui_job_wizard_controller.rb +12 -0
  7. data/app/helpers/job_invocations_helper.rb +2 -2
  8. data/app/helpers/remote_execution_helper.rb +35 -8
  9. data/app/lib/actions/remote_execution/run_host_job.rb +37 -7
  10. data/app/lib/foreman_remote_execution/provider_input.rb +29 -0
  11. data/app/lib/foreman_remote_execution/renderer/scope/input.rb +1 -0
  12. data/app/models/concerns/foreman_remote_execution/host_extensions.rb +7 -5
  13. data/app/models/concerns/foreman_remote_execution/smart_proxy_extensions.rb +6 -0
  14. data/app/models/host_proxy_invocation.rb +4 -0
  15. data/app/models/host_status/execution_status.rb +5 -5
  16. data/app/models/invocation_provider_input_value.rb +12 -0
  17. data/app/models/job_invocation.rb +35 -12
  18. data/app/models/job_invocation_composer.rb +74 -19
  19. data/app/models/remote_execution_provider.rb +18 -3
  20. data/app/models/setting/remote_execution.rb +11 -1
  21. data/app/models/ssh_execution_provider.rb +4 -4
  22. data/app/models/targeting.rb +5 -1
  23. data/app/models/template_invocation.rb +2 -0
  24. data/app/overrides/execution_interface.rb +8 -8
  25. data/app/overrides/subnet_proxies.rb +6 -6
  26. data/app/services/renderer_methods.rb +12 -0
  27. data/app/views/job_invocations/_form.html.erb +8 -0
  28. data/app/views/job_invocations/index.html.erb +1 -1
  29. data/config/routes.rb +1 -0
  30. data/db/migrate/20180110104432_rename_template_invocation_permission.rb +1 -1
  31. data/db/migrate/20190111153330_remove_remote_execution_without_proxy_setting.rb +4 -4
  32. data/db/migrate/20210312074713_add_provider_inputs.rb +10 -0
  33. data/db/migrate/2021051713291621250977_add_host_proxy_invocations.rb +12 -0
  34. data/extra/cockpit/foreman-cockpit-session +6 -6
  35. data/foreman_remote_execution.gemspec +1 -1
  36. data/lib/foreman_remote_execution/engine.rb +14 -12
  37. data/lib/foreman_remote_execution/version.rb +1 -1
  38. data/lib/tasks/foreman_remote_execution_tasks.rake +1 -18
  39. data/locale/action_names.rb +1 -0
  40. data/locale/de/foreman_remote_execution.po +77 -27
  41. data/locale/en/foreman_remote_execution.po +77 -27
  42. data/locale/en_GB/foreman_remote_execution.po +77 -27
  43. data/locale/es/foreman_remote_execution.po +77 -27
  44. data/locale/foreman_remote_execution.pot +241 -163
  45. data/locale/fr/foreman_remote_execution.po +77 -27
  46. data/locale/ja/foreman_remote_execution.po +77 -27
  47. data/locale/ko/foreman_remote_execution.po +77 -27
  48. data/locale/pt_BR/foreman_remote_execution.po +77 -27
  49. data/locale/ru/foreman_remote_execution.po +77 -27
  50. data/locale/zh_CN/foreman_remote_execution.po +77 -27
  51. data/locale/zh_TW/foreman_remote_execution.po +77 -27
  52. data/package.json +4 -2
  53. data/test/functional/api/v2/job_invocations_controller_test.rb +14 -1
  54. data/test/helpers/remote_execution_helper_test.rb +16 -0
  55. data/test/unit/job_invocation_composer_test.rb +100 -3
  56. data/test/unit/job_invocation_report_template_test.rb +57 -0
  57. data/test/unit/job_invocation_test.rb +1 -1
  58. data/webpack/JobWizard/JobWizard.js +75 -11
  59. data/webpack/JobWizard/JobWizard.scss +14 -0
  60. data/webpack/JobWizard/JobWizardConstants.js +6 -0
  61. data/webpack/JobWizard/JobWizardSelectors.js +38 -0
  62. data/webpack/JobWizard/__tests__/JobWizard.test.js +13 -0
  63. data/webpack/JobWizard/__tests__/__snapshots__/JobWizard.test.js.snap +32 -0
  64. data/webpack/JobWizard/__tests__/__snapshots__/integration.test.js.snap +43 -0
  65. data/webpack/JobWizard/__tests__/fixtures.js +26 -0
  66. data/webpack/JobWizard/__tests__/integration.test.js +156 -0
  67. data/webpack/JobWizard/steps/AdvancedFields/AdvancedFields.js +93 -0
  68. data/webpack/JobWizard/steps/AdvancedFields/Fields.js +181 -0
  69. data/webpack/JobWizard/steps/AdvancedFields/__tests__/AdvancedFields.test.js +25 -0
  70. data/webpack/JobWizard/steps/AdvancedFields/__tests__/__snapshots__/AdvancedFields.test.js.snap +249 -0
  71. data/webpack/JobWizard/steps/CategoryAndTemplate/CategoryAndTemplate.js +109 -0
  72. data/webpack/JobWizard/steps/CategoryAndTemplate/CategoryAndTemplate.test.js +52 -0
  73. data/webpack/JobWizard/steps/CategoryAndTemplate/__snapshots__/CategoryAndTemplate.test.js.snap +113 -0
  74. data/webpack/JobWizard/steps/CategoryAndTemplate/index.js +94 -0
  75. data/webpack/JobWizard/steps/form/FormHelpers.js +19 -0
  76. data/webpack/JobWizard/steps/form/GroupedSelectField.js +91 -0
  77. data/webpack/JobWizard/steps/form/SelectField.js +48 -0
  78. data/webpack/JobWizard/steps/form/__tests__/GroupedSelectField.test.js +38 -0
  79. data/webpack/JobWizard/steps/form/__tests__/SelectField.test.js +23 -0
  80. data/webpack/JobWizard/steps/form/__tests__/__snapshots__/GroupedSelectField.test.js.snap +37 -0
  81. data/webpack/JobWizard/steps/form/__tests__/__snapshots__/SelectField.test.js.snap +23 -0
  82. data/webpack/__mocks__/foremanReact/common/helpers.js +1 -0
  83. data/webpack/__mocks__/foremanReact/redux/API/APISelectors.js +21 -2
  84. data/webpack/__mocks__/foremanReact/redux/API/index.js +5 -0
  85. data/webpack/__mocks__/foremanReact/routes/common/PageLayout/PageLayout.js +10 -0
  86. data/webpack/global_index.js +6 -0
  87. data/webpack/index.js +3 -4
  88. data/webpack/react_app/components/RecentJobsCard/RecentJobsCard.js +83 -0
  89. data/webpack/react_app/components/RecentJobsCard/constants.js +1 -0
  90. data/webpack/react_app/components/RecentJobsCard/index.js +1 -0
  91. data/webpack/react_app/components/RecentJobsCard/styles.css +15 -0
  92. data/webpack/react_app/components/RegistrationExtension/RexInterface.js +50 -0
  93. data/webpack/react_app/components/RegistrationExtension/__tests__/RexInterface.test.js +9 -0
  94. data/webpack/react_app/components/RegistrationExtension/__tests__/__snapshots__/RexInterface.test.js.snap +35 -0
  95. data/webpack/react_app/components/TargetingHosts/__tests__/TargetingHostsSelectors.test.js +8 -3
  96. data/webpack/react_app/components/TargetingHosts/__tests__/__snapshots__/TargetingHostsSelectors.test.js.snap +7 -2
  97. data/webpack/react_app/extend/fillRecentJobsCard.js +11 -0
  98. data/webpack/react_app/extend/fillregistrationAdvanced.js +11 -0
  99. data/webpack/react_app/extend/reducers.js +5 -0
  100. metadata +49 -8
  101. data/app/models/concerns/foreman_remote_execution/orchestration/ssh.rb +0 -70
  102. data/app/views/api/v2/registration/_form.html.erb +0 -12
  103. data/test/models/orchestration/ssh_test.rb +0 -56
@@ -0,0 +1,109 @@
1
+ import React from 'react';
2
+ import PropTypes from 'prop-types';
3
+ import { Title, Text, TextVariants, Form, Alert } from '@patternfly/react-core';
4
+ import { translate as __ } from 'foremanReact/common/I18n';
5
+ import { SelectField } from '../form/SelectField';
6
+ import { GroupedSelectField } from '../form/GroupedSelectField';
7
+
8
+ export const CategoryAndTemplate = ({
9
+ jobCategories,
10
+ jobTemplates,
11
+ setJobTemplate,
12
+ selectedTemplateID,
13
+ selectedCategory,
14
+ setCategory,
15
+ errors,
16
+ }) => {
17
+ const templatesGroups = {};
18
+ jobTemplates.forEach(template => {
19
+ if (templatesGroups[template.provider_type]?.options)
20
+ templatesGroups[template.provider_type].options.push({
21
+ label: template.name,
22
+ value: template.id,
23
+ });
24
+ else
25
+ templatesGroups[template.provider_type] = {
26
+ options: [{ label: template.name, value: template.id }],
27
+ groupLabel: template.provider_type,
28
+ };
29
+ });
30
+
31
+ const selectedTemplate = jobTemplates.find(
32
+ template => template.id === selectedTemplateID
33
+ )?.name;
34
+
35
+ const onSelectCategory = newCategory => {
36
+ setCategory(newCategory);
37
+ setJobTemplate(null);
38
+ };
39
+ const { categoryError, allTemplatesError, templateError } = errors;
40
+ const isError = !!(categoryError || allTemplatesError || templateError);
41
+ return (
42
+ <>
43
+ <Title headingLevel="h2">{__('Category and Template')}</Title>
44
+ <Text component={TextVariants.p}>{__('All fields are required.')}</Text>
45
+ <Form>
46
+ <SelectField
47
+ label={__('Job category')}
48
+ fieldId="job_category"
49
+ options={jobCategories}
50
+ setValue={onSelectCategory}
51
+ value={selectedCategory}
52
+ placeholderText={categoryError ? __('Error') : ''}
53
+ isDisabled={!!categoryError}
54
+ />
55
+ <GroupedSelectField
56
+ label={__('Job template')}
57
+ fieldId="job_template"
58
+ groups={Object.values(templatesGroups)}
59
+ setSelected={setJobTemplate}
60
+ selected={selectedTemplate}
61
+ isDisabled={!!(categoryError || allTemplatesError)}
62
+ placeholderText={allTemplatesError ? __('Error') : ''}
63
+ />
64
+ {isError && (
65
+ <Alert variant="danger" title={__('Errors:')}>
66
+ {categoryError && (
67
+ <span>
68
+ {__('Categories list failed with:')} {categoryError}
69
+ </span>
70
+ )}
71
+ {allTemplatesError && (
72
+ <span>
73
+ {__('Templates list failed with:')} {allTemplatesError}
74
+ </span>
75
+ )}
76
+ {templateError && (
77
+ <span>
78
+ {__('Template failed with:')} {templateError}
79
+ </span>
80
+ )}
81
+ </Alert>
82
+ )}
83
+ </Form>
84
+ </>
85
+ );
86
+ };
87
+
88
+ CategoryAndTemplate.propTypes = {
89
+ jobCategories: PropTypes.array,
90
+ jobTemplates: PropTypes.array,
91
+ setJobTemplate: PropTypes.func.isRequired,
92
+ selectedTemplateID: PropTypes.number,
93
+ setCategory: PropTypes.func.isRequired,
94
+ selectedCategory: PropTypes.string,
95
+ errors: PropTypes.shape({
96
+ categoryError: PropTypes.string,
97
+ allTemplatesError: PropTypes.string,
98
+ templateError: PropTypes.string,
99
+ }),
100
+ };
101
+ CategoryAndTemplate.defaultProps = {
102
+ jobCategories: [],
103
+ jobTemplates: [],
104
+ selectedTemplateID: null,
105
+ selectedCategory: null,
106
+ errors: {},
107
+ };
108
+
109
+ export default CategoryAndTemplate;
@@ -0,0 +1,52 @@
1
+ import { testComponentSnapshotsWithFixtures } from '@theforeman/test';
2
+ import { CategoryAndTemplate } from './CategoryAndTemplate';
3
+
4
+ const baseProps = {
5
+ setJobTemplate: jest.fn(),
6
+ selectedTemplateID: 190,
7
+ setCategory: jest.fn(),
8
+ };
9
+ const fixtures = {
10
+ 'renders with props': {
11
+ ...baseProps,
12
+ jobCategories: [
13
+ 'Commands',
14
+ 'Ansible Playbook',
15
+ 'Ansible Galaxy',
16
+ 'Ansible Roles Installation',
17
+ ],
18
+ jobTemplates: [
19
+ {
20
+ id: 190,
21
+ name: 'ab Run Command - SSH Default clone',
22
+ job_category: 'Commands',
23
+ provider_type: 'SSH',
24
+ snippet: false,
25
+ },
26
+ {
27
+ id: 168,
28
+ name: 'Ansible Roles - Ansible Default',
29
+ job_category: 'Ansible Playbook',
30
+ provider_type: 'Ansible',
31
+ snippet: false,
32
+ },
33
+ {
34
+ id: 170,
35
+ name: 'Ansible Roles - Install from git',
36
+ job_category: 'Ansible Roles Installation',
37
+ provider_type: 'Ansible',
38
+ snippet: false,
39
+ },
40
+ ],
41
+ selectedCategory: 'I am a category',
42
+ },
43
+ 'render with error': {
44
+ ...baseProps,
45
+ errors: { allTemplatesError: 'I have an error' },
46
+ },
47
+ };
48
+
49
+ describe('CategoryAndTemplate', () => {
50
+ describe('rendering', () =>
51
+ testComponentSnapshotsWithFixtures(CategoryAndTemplate, fixtures));
52
+ });
@@ -0,0 +1,113 @@
1
+ // Jest Snapshot v1, https://goo.gl/fbAQLP
2
+
3
+ exports[`CategoryAndTemplate rendering render with error 1`] = `
4
+ <Fragment>
5
+ <Title
6
+ headingLevel="h2"
7
+ >
8
+ Category and Template
9
+ </Title>
10
+ <Text
11
+ component="p"
12
+ >
13
+ All fields are required.
14
+ </Text>
15
+ <Form>
16
+ <SelectField
17
+ fieldId="job_category"
18
+ isDisabled={false}
19
+ label="Job category"
20
+ options={Array []}
21
+ placeholderText=""
22
+ setValue={[Function]}
23
+ value={null}
24
+ />
25
+ <GroupedSelectField
26
+ fieldId="job_template"
27
+ groups={Array []}
28
+ isDisabled={true}
29
+ label="Job template"
30
+ placeholderText="Error"
31
+ selected={null}
32
+ setSelected={[MockFunction]}
33
+ />
34
+ <Alert
35
+ title="Errors:"
36
+ variant="danger"
37
+ >
38
+ <span>
39
+ Templates list failed with:
40
+
41
+ I have an error
42
+ </span>
43
+ </Alert>
44
+ </Form>
45
+ </Fragment>
46
+ `;
47
+
48
+ exports[`CategoryAndTemplate rendering renders with props 1`] = `
49
+ <Fragment>
50
+ <Title
51
+ headingLevel="h2"
52
+ >
53
+ Category and Template
54
+ </Title>
55
+ <Text
56
+ component="p"
57
+ >
58
+ All fields are required.
59
+ </Text>
60
+ <Form>
61
+ <SelectField
62
+ fieldId="job_category"
63
+ isDisabled={false}
64
+ label="Job category"
65
+ options={
66
+ Array [
67
+ "Commands",
68
+ "Ansible Playbook",
69
+ "Ansible Galaxy",
70
+ "Ansible Roles Installation",
71
+ ]
72
+ }
73
+ placeholderText=""
74
+ setValue={[Function]}
75
+ value="I am a category"
76
+ />
77
+ <GroupedSelectField
78
+ fieldId="job_template"
79
+ groups={
80
+ Array [
81
+ Object {
82
+ "groupLabel": "SSH",
83
+ "options": Array [
84
+ Object {
85
+ "label": "ab Run Command - SSH Default clone",
86
+ "value": 190,
87
+ },
88
+ ],
89
+ },
90
+ Object {
91
+ "groupLabel": "Ansible",
92
+ "options": Array [
93
+ Object {
94
+ "label": "Ansible Roles - Ansible Default",
95
+ "value": 168,
96
+ },
97
+ Object {
98
+ "label": "Ansible Roles - Install from git",
99
+ "value": 170,
100
+ },
101
+ ],
102
+ },
103
+ ]
104
+ }
105
+ isDisabled={false}
106
+ label="Job template"
107
+ placeholderText=""
108
+ selected="ab Run Command - SSH Default clone"
109
+ setSelected={[MockFunction]}
110
+ />
111
+ </Form>
112
+ </Fragment>
113
+ `;
@@ -0,0 +1,94 @@
1
+ import React, { useEffect } from 'react';
2
+ import { useSelector, useDispatch } from 'react-redux';
3
+ import PropTypes from 'prop-types';
4
+ import URI from 'urijs';
5
+ import { get } from 'foremanReact/redux/API';
6
+ import {
7
+ selectJobCategories,
8
+ selectJobTemplates,
9
+ selectJobCategoriesStatus,
10
+ filterJobTemplates,
11
+ selectCategoryError,
12
+ selectAllTemplatesError,
13
+ selectTemplateError,
14
+ } from '../../JobWizardSelectors';
15
+ import { CategoryAndTemplate } from './CategoryAndTemplate';
16
+
17
+ import {
18
+ JOB_TEMPLATES,
19
+ JOB_CATEGORIES,
20
+ templatesUrl,
21
+ } from '../../JobWizardConstants';
22
+
23
+ const ConnectedCategoryAndTemplate = ({
24
+ jobTemplate,
25
+ setJobTemplate,
26
+ category,
27
+ setCategory,
28
+ }) => {
29
+ const dispatch = useDispatch();
30
+
31
+ const jobCategoriesStatus = useSelector(selectJobCategoriesStatus);
32
+ useEffect(() => {
33
+ if (!jobCategoriesStatus) {
34
+ // Don't reload categories if they are already loaded
35
+ dispatch(
36
+ get({
37
+ key: JOB_CATEGORIES,
38
+ url: '/ui_job_wizard/categories',
39
+ handleSuccess: response =>
40
+ setCategory(response.data.job_categories[0] || ''),
41
+ })
42
+ );
43
+ }
44
+ }, [jobCategoriesStatus, dispatch, setCategory]);
45
+
46
+ const jobCategories = useSelector(selectJobCategories);
47
+ useEffect(() => {
48
+ if (category) {
49
+ const templatesUrlObject = new URI(templatesUrl);
50
+ dispatch(
51
+ get({
52
+ key: JOB_TEMPLATES,
53
+ url: templatesUrlObject.addSearch({
54
+ search: `job_category="${category}"`,
55
+ per_page: 'all',
56
+ }),
57
+ handleSuccess: response =>
58
+ setJobTemplate(
59
+ Number(filterJobTemplates(response?.data?.results)[0]?.id) || null
60
+ ),
61
+ })
62
+ );
63
+ }
64
+ }, [category, dispatch, setJobTemplate]);
65
+
66
+ const jobTemplates = useSelector(selectJobTemplates);
67
+
68
+ const errors = {
69
+ categoryError: useSelector(selectCategoryError),
70
+ allTemplatesError: useSelector(selectAllTemplatesError),
71
+ templateError: useSelector(selectTemplateError),
72
+ };
73
+ return (
74
+ <CategoryAndTemplate
75
+ jobTemplates={jobTemplates}
76
+ jobCategories={jobCategories}
77
+ setJobTemplate={setJobTemplate}
78
+ selectedTemplateID={jobTemplate}
79
+ setCategory={setCategory}
80
+ selectedCategory={category}
81
+ errors={errors}
82
+ />
83
+ );
84
+ };
85
+
86
+ ConnectedCategoryAndTemplate.propTypes = {
87
+ jobTemplate: PropTypes.number,
88
+ setJobTemplate: PropTypes.func.isRequired,
89
+ category: PropTypes.string.isRequired,
90
+ setCategory: PropTypes.func.isRequired,
91
+ };
92
+ ConnectedCategoryAndTemplate.defaultProps = { jobTemplate: null };
93
+
94
+ export default ConnectedCategoryAndTemplate;
@@ -0,0 +1,19 @@
1
+ import React from 'react';
2
+ import { Popover } from '@patternfly/react-core';
3
+ import { HelpIcon } from '@patternfly/react-icons';
4
+ import { translate as __ } from 'foremanReact/common/I18n';
5
+
6
+ export const helpLabel = (text, id) => {
7
+ if (!text) return null;
8
+ return (
9
+ <Popover id={`${id}-help`} bodyContent={text} aria-label="help-text">
10
+ <button
11
+ aria-label={__('open-help-tooltip-button')}
12
+ onClick={e => e.preventDefault()}
13
+ className="pf-c-form__group-label-help"
14
+ >
15
+ <HelpIcon noVerticalAlign />
16
+ </button>
17
+ </Popover>
18
+ );
19
+ };
@@ -0,0 +1,91 @@
1
+ import React, { useState } from 'react';
2
+ import PropTypes from 'prop-types';
3
+ import {
4
+ SelectOption,
5
+ Select,
6
+ SelectGroup,
7
+ SelectVariant,
8
+ FormGroup,
9
+ } from '@patternfly/react-core';
10
+
11
+ export const GroupedSelectField = ({
12
+ label,
13
+ fieldId,
14
+ groups,
15
+ selected,
16
+ setSelected,
17
+ ...props
18
+ }) => {
19
+ const [isOpen, setIsOpen] = useState(false);
20
+ const onSelect = selection => {
21
+ setIsOpen(false);
22
+ setSelected(selection);
23
+ };
24
+
25
+ const onClear = () => {
26
+ onSelect(null);
27
+ };
28
+
29
+ const options = groups.map((group, groupIndex) => (
30
+ <SelectGroup key={groupIndex} label={group.groupLabel}>
31
+ {group.options.map((option, optionIndex) => (
32
+ <SelectOption
33
+ key={optionIndex}
34
+ value={option.label}
35
+ onClick={() => onSelect(option.value)}
36
+ />
37
+ ))}
38
+ </SelectGroup>
39
+ ));
40
+
41
+ const onFilter = evt => {
42
+ const textInput = evt?.target?.value || '';
43
+ if (textInput === '') {
44
+ return options;
45
+ }
46
+ return options
47
+ .map(group => {
48
+ const filteredGroup = React.cloneElement(group, {
49
+ children: group.props.children.filter(item =>
50
+ item.props.value.toLowerCase().includes(textInput.toLowerCase())
51
+ ),
52
+ });
53
+ if (filteredGroup.props.children.length > 0) return filteredGroup;
54
+ return null;
55
+ })
56
+ .filter(newGroup => newGroup);
57
+ };
58
+
59
+ return (
60
+ <FormGroup label={label} fieldId={fieldId}>
61
+ <Select
62
+ isGrouped
63
+ variant={SelectVariant.typeahead}
64
+ onToggle={setIsOpen}
65
+ onFilter={onFilter}
66
+ isOpen={isOpen}
67
+ onSelect={() => null}
68
+ selections={selected}
69
+ className="without_select2"
70
+ onClear={onClear}
71
+ menuAppendTo={() => document.body}
72
+ {...props}
73
+ >
74
+ {options}
75
+ </Select>
76
+ </FormGroup>
77
+ );
78
+ };
79
+
80
+ GroupedSelectField.propTypes = {
81
+ label: PropTypes.string.isRequired,
82
+ fieldId: PropTypes.string.isRequired,
83
+ groups: PropTypes.array,
84
+ selected: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
85
+ setSelected: PropTypes.func.isRequired,
86
+ };
87
+
88
+ GroupedSelectField.defaultProps = {
89
+ groups: [],
90
+ selected: null,
91
+ };