foreman_remote_execution 4.3.0 → 4.5.1

Sign up to get free protection for your applications and to get access to all the features.
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
+ };