foreman_remote_execution 8.0.0 → 8.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (65) hide show
  1. checksums.yaml +4 -4
  2. data/app/controllers/job_invocations_controller.rb +1 -2
  3. data/app/controllers/job_templates_controller.rb +1 -1
  4. data/app/controllers/ui_job_wizard_controller.rb +1 -1
  5. data/app/helpers/job_invocations_helper.rb +0 -7
  6. data/app/helpers/remote_execution_helper.rb +1 -1
  7. data/app/lib/actions/remote_execution/proxy_action.rb +46 -0
  8. data/app/lib/actions/remote_execution/run_host_job.rb +38 -11
  9. data/app/lib/actions/remote_execution/run_hosts_job.rb +7 -6
  10. data/app/lib/actions/remote_execution/template_invocation_progress_logging.rb +27 -0
  11. data/app/models/job_invocation.rb +5 -9
  12. data/app/models/job_invocation_composer.rb +4 -0
  13. data/app/models/remote_execution_provider.rb +10 -2
  14. data/app/models/ssh_execution_provider.rb +1 -0
  15. data/app/models/template_invocation.rb +1 -0
  16. data/app/models/template_invocation_event.rb +11 -0
  17. data/app/views/job_invocations/_form.html.erb +4 -0
  18. data/app/views/job_invocations/new.html.erb +5 -0
  19. data/app/views/templates/script/package_action.erb +1 -1
  20. data/config/routes.rb +5 -5
  21. data/db/migrate/20220713095705_create_template_invocation_events.rb +17 -0
  22. data/db/migrate/20220822155946_add_time_to_pickup_to_job_invocation.rb +5 -0
  23. data/extra/cockpit/foreman-cockpit-session +303 -230
  24. data/extra/cockpit/foreman-cockpit.service +1 -0
  25. data/foreman_remote_execution.gemspec +1 -1
  26. data/lib/foreman_remote_execution/engine.rb +12 -7
  27. data/lib/foreman_remote_execution/tasks/explain_proxy_selection.rake +131 -0
  28. data/lib/foreman_remote_execution/version.rb +1 -1
  29. data/test/unit/remote_execution_provider_test.rb +22 -0
  30. data/webpack/JobWizard/JobWizard.js +53 -18
  31. data/webpack/JobWizard/JobWizard.scss +3 -0
  32. data/webpack/JobWizard/JobWizardConstants.js +1 -1
  33. data/webpack/JobWizard/JobWizardHelpers.js +15 -0
  34. data/webpack/JobWizard/JobWizardPageRerun.js +29 -5
  35. data/webpack/JobWizard/JobWizardSelectors.js +8 -2
  36. data/webpack/JobWizard/__tests__/JobWizardPageRerun.test.js +5 -0
  37. data/webpack/JobWizard/__tests__/fixtures.js +26 -2
  38. data/webpack/JobWizard/autofill.js +32 -10
  39. data/webpack/JobWizard/index.js +25 -6
  40. data/webpack/JobWizard/steps/AdvancedFields/AdvancedFields.js +25 -0
  41. data/webpack/JobWizard/steps/AdvancedFields/DescriptionField.js +12 -1
  42. data/webpack/JobWizard/steps/AdvancedFields/Fields.js +41 -6
  43. data/webpack/JobWizard/steps/AdvancedFields/__tests__/AdvancedFields.test.js +1 -1
  44. data/webpack/JobWizard/steps/AdvancedFields/__tests__/__snapshots__/AdvancedFields.test.js.snap +1 -1
  45. data/webpack/JobWizard/steps/CategoryAndTemplate/CategoryAndTemplate.js +4 -2
  46. data/webpack/JobWizard/steps/CategoryAndTemplate/CategoryAndTemplate.test.js +6 -2
  47. data/webpack/JobWizard/steps/CategoryAndTemplate/index.js +28 -20
  48. data/webpack/JobWizard/steps/HostsAndInputs/__tests__/TemplateInputs.test.js +32 -0
  49. data/webpack/JobWizard/steps/HostsAndInputs/index.js +2 -2
  50. data/webpack/JobWizard/steps/ReviewDetails/index.js +1 -0
  51. data/webpack/JobWizard/steps/form/FormHelpers.js +21 -1
  52. data/webpack/JobWizard/steps/form/Formatter.js +22 -6
  53. data/webpack/JobWizard/steps/form/ResourceSelect.js +97 -10
  54. data/webpack/JobWizard/steps/form/SearchSelect.js +2 -2
  55. data/webpack/JobWizard/steps/form/SelectField.js +4 -0
  56. data/webpack/JobWizard/submit.js +3 -1
  57. data/webpack/JobWizard/validation.js +1 -0
  58. data/webpack/Routes/routes.js +3 -3
  59. data/webpack/react_app/components/FeaturesDropdown/actions.js +23 -2
  60. data/webpack/react_app/components/FeaturesDropdown/index.js +2 -0
  61. data/webpack/react_app/components/HostKebab/KebabItems.js +1 -0
  62. data/webpack/react_app/components/RecentJobsCard/RecentJobsCard.js +5 -0
  63. data/webpack/react_app/components/RecentJobsCard/RecentJobsTable.js +51 -59
  64. data/webpack/react_app/extend/Fills.js +3 -3
  65. metadata +12 -5
@@ -0,0 +1,131 @@
1
+ namespace :foreman_remote_execution do
2
+ desc <<~DESC
3
+ Explains which proxies can be used for remote execution against HOST using a specified PROVIDER.
4
+
5
+ * HOST : A scoped search query to find hosts by
6
+ * PROVIDER : The PROVIDER to scope by
7
+ * FORMAT : Output format, one of verbose (or unset), json, pretty-json, csv
8
+ * FOREMAN_USER : Run as if FOREMAN_USER triggered a job (runs as anonymous_admin if unset)
9
+ * ORGANIZATION : Run in the context of ORGANIZATION (runs in any context if unset)
10
+ * LOCATION : Run in the context of LOCATION (runs in any context if unset)
11
+ DESC
12
+ task :explain_proxy_selection => ['environment'] do
13
+ options = {}
14
+ options[:host] = ENV['HOST']
15
+ options[:provider] = ENV['PROVIDER']
16
+
17
+ raise 'Environment variable HOST has to be set' unless options[:host]
18
+ raise 'Environment variable PROVIDER has to be set' unless options[:provider]
19
+
20
+ if ENV['FOREMAN_USER']
21
+ User.current = User.friendly.find(ENV['FOREMAN_USER'])
22
+ else
23
+ User.current = User.anonymous_admin
24
+ end
25
+ Location.current = Location.friendly.find(ENV['LOCATION']) if ENV['LOCATION']
26
+ Organization.current = Organization.friendly.find(ENV['ORGANIZATION']) if ENV['ORGANIZATION']
27
+
28
+ selector = ::RemoteExecutionProxySelector.new
29
+
30
+ results = Host.search_for(options[:host]).map do |host|
31
+ host_base = { :host => host }
32
+ proxies = selector.available_proxies(host, options[:provider])
33
+ determined_proxy = selector.determine_proxy(host, options[:provider])
34
+ counts = selector.instance_variable_get('@tasks')
35
+ counts.default = 0
36
+
37
+ strategies = selector.strategies.map do |strategy|
38
+ base = { :name => strategy, :enabled => !proxies[strategy].nil? }
39
+ next base if proxies[strategy].nil?
40
+
41
+ base.merge(:proxies => proxies[strategy].sort_by { |proxy| counts[proxy] }.map do |proxy|
42
+ {:proxy => proxy, :count => counts[proxy]}
43
+ end)
44
+ end
45
+
46
+ case determined_proxy
47
+ when :not_defined
48
+ settings = {
49
+ global_proxy: 'remote_execution_global_proxy',
50
+ fallback_proxy: 'remote_execution_fallback_proxy',
51
+ provider: options[:provider],
52
+ }
53
+
54
+ host_base[:detail] = _('Could not use any proxy for the %{provider} job. Consider configuring %{global_proxy}, ' +
55
+ '%{fallback_proxy} in settings') % settings
56
+ when :not_available
57
+ offline_proxies = selector.offline
58
+ settings = { :count => offline_proxies.count, :proxy_names => offline_proxies.map(&:name).join(', ') }
59
+ host_base[:detail] = n_('The only applicable proxy %{proxy_names} is down',
60
+ 'All %{count} applicable proxies are down. Tried %{proxy_names}',
61
+ offline_proxies.count) % settings
62
+ else
63
+ winning_strategy = selector.strategies.find { |strategy| !proxies[strategy].empty? && proxies[strategy].include?(determined_proxy) }
64
+ end
65
+
66
+ {
67
+ :host => host,
68
+ :strategies => strategies,
69
+ :selected_proxy => determined_proxy,
70
+ :winning_strategy => winning_strategy,
71
+ }
72
+ end
73
+
74
+ case ENV['FORMAT']
75
+ when nil, 'verbose'
76
+ output_verbose(results, options[:provider])
77
+ when 'csv'
78
+ require 'csv'
79
+ output_csv(results)
80
+ when 'json'
81
+ require 'json'
82
+ puts JSON.generate(results)
83
+ when 'pretty-json'
84
+ require 'json'
85
+ puts JSON.pretty_generate(results)
86
+ end
87
+ end
88
+
89
+ def output_verbose(results, provider)
90
+ errors = [:not_defined, :not_available]
91
+
92
+ results.each do |host|
93
+ puts "=> Host #{host[:host]}"
94
+ host[:strategies].each do |strategy|
95
+ puts "==> Strategy #{strategy[:name]}"
96
+ unless strategy[:enabled]
97
+ puts " strategy is disabled"
98
+ puts
99
+ next
100
+ end
101
+ if strategy[:proxies].empty?
102
+ puts " no proxies available using this strategy"
103
+ else
104
+ strategy[:proxies].each_with_index do |proxy_record, i|
105
+ puts " #{i + 1}) #{proxy_record[:proxy]} - #{proxy_record[:count]} tasks"
106
+ end
107
+ end
108
+ puts
109
+ end
110
+ if errors.include? host[:selected_proxy]
111
+ puts host[:detail]
112
+ else
113
+ puts "As of now, #{provider} job would use proxy #{host[:selected_proxy]}, determined by strategy #{host[:winning_strategy]}."
114
+ end
115
+ puts
116
+ end
117
+ end
118
+
119
+ def output_csv(results)
120
+ writer = CSV.new($stdout)
121
+ writer << %w(host strategy proxy)
122
+ results.each do |host|
123
+ writer << [
124
+ host[:host].name,
125
+ host[:winning_strategy],
126
+ host[:selected_proxy],
127
+ ]
128
+ end
129
+ writer.close
130
+ end
131
+ end
@@ -1,3 +1,3 @@
1
1
  module ForemanRemoteExecution
2
- VERSION = '8.0.0'.freeze
2
+ VERSION = '8.1.0'.freeze
3
3
  end
@@ -56,6 +56,28 @@ class RemoteExecutionProviderTest < ActiveSupport::TestCase
56
56
  end
57
57
  end
58
58
 
59
+ describe '.proxy_feature' do
60
+ # rubocop:disable Naming/ConstantName
61
+ it 'handles provider subclasses properly' do
62
+ old = ::RemoteExecutionProvider
63
+
64
+ class P2 < old
65
+ end
66
+ ::RemoteExecutionProvider = P2
67
+
68
+ class CustomProvider < ::RemoteExecutionProvider
69
+ end
70
+
71
+ ::RemoteExecutionProvider.register('custom', CustomProvider)
72
+
73
+ feature = CustomProvider.proxy_feature
74
+ _(feature).must_equal 'custom'
75
+ ensure
76
+ ::RemoteExecutionProvider = old
77
+ end
78
+ # rubocop:enable Naming/ConstantName
79
+ end
80
+
59
81
  describe '.provider_proxy_features' do
60
82
  it 'returns correct values' do
61
83
  RemoteExecutionProvider.stubs(:providers).returns(
@@ -25,6 +25,7 @@ import {
25
25
  selectIsSubmitting,
26
26
  selectRouterSearch,
27
27
  selectIsLoading,
28
+ selectJobCategoriesResponse,
28
29
  } from './JobWizardSelectors';
29
30
  import { ScheduleType } from './steps/Schedule/ScheduleType';
30
31
  import { ScheduleFuture } from './steps/Schedule/ScheduleFuture';
@@ -34,13 +35,18 @@ import ReviewDetails from './steps/ReviewDetails/';
34
35
  import { useValidation } from './validation';
35
36
  import { useAutoFill } from './autofill';
36
37
  import { submit } from './submit';
38
+ import { generateDefaultDescription } from './JobWizardHelpers';
37
39
  import './JobWizard.scss';
38
40
 
39
41
  export const JobWizard = ({ rerunData }) => {
42
+ const jobCategoriesResponse = useSelector(selectJobCategoriesResponse);
40
43
  const [jobTemplateID, setJobTemplateID] = useState(
41
- rerunData?.template_invocations?.[0]?.template_id
44
+ rerunData?.template_invocations?.[0]?.template_id ||
45
+ jobCategoriesResponse?.default_template
46
+ );
47
+ const [category, setCategory] = useState(
48
+ rerunData?.job_category || jobCategoriesResponse?.default_category || ''
42
49
  );
43
- const [category, setCategory] = useState(rerunData?.job_category || '');
44
50
  const [advancedValues, setAdvancedValues] = useState({ templateValues: {} });
45
51
  const [templateValues, setTemplateValues] = useState({}); // TODO use templateValues in advanced fields - description https://github.com/theforeman/foreman_remote_execution/pull/605
46
52
  const [scheduleValue, setScheduleValue] = useState(initialScheduleState);
@@ -56,6 +62,7 @@ export const JobWizard = ({ rerunData }) => {
56
62
  ? {
57
63
  search: rerunData?.targeting?.search_query,
58
64
  ...rerunData.inputs,
65
+ ...routerSearch,
59
66
  }
60
67
  : routerSearch
61
68
  );
@@ -70,6 +77,7 @@ export const JobWizard = ({ rerunData }) => {
70
77
  job_template: {
71
78
  name,
72
79
  execution_timeout_interval,
80
+ time_to_pickup,
73
81
  description_format,
74
82
  job_category,
75
83
  },
@@ -78,8 +86,8 @@ export const JobWizard = ({ rerunData }) => {
78
86
  concurrency_control = {},
79
87
  },
80
88
  }) => {
81
- if (!category.length) {
82
- setCategory(current => (current.length ? current : job_category));
89
+ if (category !== job_category) {
90
+ setCategory(job_category);
83
91
  }
84
92
  const advancedTemplateValues = {};
85
93
  const defaultTemplateValues = {};
@@ -101,21 +109,19 @@ export const JobWizard = ({ rerunData }) => {
101
109
  currentAdvancedValues[input.name] || input?.default || '';
102
110
  });
103
111
  }
104
- const generateDefaultDescription = () => {
105
- if (description_format) return description_format;
106
- const allInputs = [...advancedInputs, ...inputs];
107
- if (!allInputs.length) return name;
108
- const inputsString = allInputs
109
- .map(({ name: inputname }) => `${inputname}="%{${inputname}}"`)
110
- .join(' ');
111
- return `${name} with inputs ${inputsString}`;
112
- };
113
112
  return {
114
113
  ...currentAdvancedValues,
115
114
  effectiveUserValue: effective_user?.value || '',
116
115
  timeoutToKill: execution_timeout_interval || '',
116
+ timeToPickup: time_to_pickup || '',
117
117
  templateValues: advancedTemplateValues,
118
- description: generateDefaultDescription() || '',
118
+ description:
119
+ generateDefaultDescription({
120
+ description_format,
121
+ advancedInputs,
122
+ inputs,
123
+ name,
124
+ }) || '',
119
125
  isRandomizedOrdering: randomized_ordering,
120
126
  sshUser: ssh_user || '',
121
127
  timeSpan: concurrency_control.time_span || '',
@@ -123,6 +129,7 @@ export const JobWizard = ({ rerunData }) => {
123
129
  };
124
130
  });
125
131
  },
132
+ // eslint-disable-next-line react-hooks/exhaustive-deps
126
133
  [category.length]
127
134
  );
128
135
  useEffect(() => {
@@ -134,6 +141,9 @@ export const JobWizard = ({ rerunData }) => {
134
141
  },
135
142
  job_template: {
136
143
  execution_timeout_interval: rerunData.execution_timeout_interval,
144
+ description_format: rerunData.description_format,
145
+ job_category: rerunData.job_category,
146
+ time_to_pickup: rerunData.time_to_pickup,
137
147
  },
138
148
  randomized_ordering: rerunData.targeting.randomized_ordering,
139
149
  ssh_user: rerunData.ssh_user,
@@ -141,18 +151,39 @@ export const JobWizard = ({ rerunData }) => {
141
151
  },
142
152
  });
143
153
  }
144
- }, [rerunData, setDefaults]);
154
+ // eslint-disable-next-line react-hooks/exhaustive-deps
155
+ }, [rerunData]);
145
156
  useEffect(() => {
146
157
  if (jobTemplateID) {
147
158
  dispatch(
148
159
  get({
149
160
  key: JOB_TEMPLATE,
150
161
  url: `/ui_job_wizard/template/${jobTemplateID}`,
151
- handleSuccess: rerunData ? () => {} : setDefaults,
162
+ handleSuccess: rerunData
163
+ ? ({
164
+ data: {
165
+ template_inputs = [],
166
+ advanced_template_inputs = [],
167
+ job_template: { name, description_format },
168
+ },
169
+ }) => {
170
+ setAdvancedValues(currentAdvancedValues => ({
171
+ ...currentAdvancedValues,
172
+ description:
173
+ generateDefaultDescription({
174
+ description_format,
175
+ advancedInputs: advanced_template_inputs,
176
+ inputs: template_inputs,
177
+ name,
178
+ }) || '',
179
+ }));
180
+ }
181
+ : setDefaults,
152
182
  })
153
183
  );
154
184
  }
155
- }, [rerunData, jobTemplateID, setDefaults, dispatch]);
185
+ // eslint-disable-next-line react-hooks/exhaustive-deps
186
+ }, [rerunData, jobTemplateID, dispatch]);
156
187
 
157
188
  const [valid, setValid] = useValidation({
158
189
  advancedValues,
@@ -187,7 +218,9 @@ export const JobWizard = ({ rerunData }) => {
187
218
  setJobTemplate={setJobTemplateID}
188
219
  category={category}
189
220
  setCategory={setCategory}
190
- isCategoryPreselected={!!rerunData || !!fills.feature}
221
+ isCategoryPreselected={
222
+ !!rerunData || !!fills.feature || !!fills.template_id
223
+ }
191
224
  />
192
225
  ),
193
226
  enableNext: isTemplate,
@@ -370,6 +403,7 @@ JobWizard.propTypes = {
370
403
  time_span: PropTypes.number,
371
404
  }),
372
405
  execution_timeout_interval: PropTypes.number,
406
+ time_to_pickup: PropTypes.number,
373
407
  remote_execution_feature_id: PropTypes.string,
374
408
  template_invocations: PropTypes.arrayOf(
375
409
  PropTypes.shape({
@@ -379,6 +413,7 @@ JobWizard.propTypes = {
379
413
  })
380
414
  ),
381
415
  inputs: PropTypes.object,
416
+ description_format: PropTypes.string,
382
417
  }),
383
418
  };
384
419
  JobWizard.defaultProps = {
@@ -145,6 +145,9 @@
145
145
  .pf-c-radio__body {
146
146
  font-size: var(--pf-c-radio__label--FontSize);
147
147
  }
148
+ .reset-default{
149
+ padding-bottom: 0;
150
+ }
148
151
  }
149
152
 
150
153
  .job-wizard-alert.pf-c-alert.pf-m-warning {
@@ -64,7 +64,7 @@ export const dataName = {
64
64
  };
65
65
  export const HOSTS_TO_PREVIEW_AMOUNT = 20;
66
66
 
67
- export const DEBOUNCE_HOST_COUNT = 700;
67
+ export const DEBOUNCE_API = 1500;
68
68
  export const HOST_IDS = 'HOST_IDS';
69
69
  export const REX_FEATURE = 'REX_FEATURE';
70
70
 
@@ -0,0 +1,15 @@
1
+ /* eslint-disable camelcase */
2
+ export const generateDefaultDescription = ({
3
+ description_format,
4
+ advancedInputs,
5
+ inputs,
6
+ name,
7
+ }) => {
8
+ if (description_format) return description_format;
9
+ const allInputs = [...advancedInputs, ...inputs];
10
+ if (!allInputs.length) return name;
11
+ const inputsString = allInputs
12
+ .map(({ name: inputname }) => `${inputname}="%{${inputname}}"`)
13
+ .join(' ');
14
+ return `${name} with inputs ${inputsString}`;
15
+ };
@@ -1,7 +1,15 @@
1
1
  import React from 'react';
2
2
  import PropTypes from 'prop-types';
3
3
  import URI from 'urijs';
4
- import { Alert, Title, Divider, Skeleton } from '@patternfly/react-core';
4
+ import {
5
+ Alert,
6
+ Title,
7
+ Divider,
8
+ Skeleton,
9
+ Flex,
10
+ FlexItem,
11
+ Button,
12
+ } from '@patternfly/react-core';
5
13
  import { sprintf, translate as __ } from 'foremanReact/common/I18n';
6
14
  import { useAPI } from 'foremanReact/common/hooks/API/APIHooks';
7
15
  import PageLayout from 'foremanReact/routes/common/PageLayout/PageLayout';
@@ -31,7 +39,7 @@ const JobWizardPageRerun = ({
31
39
  const title = __('Run job');
32
40
  const breadcrumbOptions = {
33
41
  breadcrumbItems: [
34
- { caption: __('Jobs'), url: `/jobs` },
42
+ { caption: __('Jobs'), url: `/job_invocations` },
35
43
  { caption: title },
36
44
  ],
37
45
  };
@@ -48,9 +56,25 @@ const JobWizardPageRerun = ({
48
56
  searchable={false}
49
57
  >
50
58
  <React.Fragment>
51
- <Title headingLevel="h2" size="2xl">
52
- {title}
53
- </Title>
59
+ <React.Fragment>
60
+ <Flex>
61
+ <FlexItem>
62
+ <Title headingLevel="h2" size="2xl">
63
+ {title}
64
+ </Title>
65
+ </FlexItem>
66
+ <FlexItem align={{ default: 'alignRight' }}>
67
+ <Button
68
+ variant="link"
69
+ component="a"
70
+ href={`/old/job_invocations/${id}/rerun${search}`}
71
+ >
72
+ {__('Use old form')}
73
+ </Button>
74
+ </FlexItem>
75
+ </Flex>
76
+ <Divider component="div" />
77
+ </React.Fragment>
54
78
  {!status || status === STATUS.PENDING ? (
55
79
  <div style={{ height: '400px' }}>
56
80
  <Skeleton
@@ -24,11 +24,17 @@ export const filterJobTemplates = templates =>
24
24
  export const selectJobTemplates = state =>
25
25
  filterJobTemplates(selectAPIResponse(state, JOB_TEMPLATES)?.results);
26
26
 
27
+ export const selectJobTemplatesSearch = state =>
28
+ selectAPIResponse(state, JOB_TEMPLATES)?.search;
29
+
30
+ export const selectJobCategoriesResponse = state =>
31
+ selectAPIResponse(state, JOB_CATEGORIES) || {};
32
+
27
33
  export const selectJobCategories = state =>
28
- selectAPIResponse(state, JOB_CATEGORIES).job_categories || [];
34
+ selectJobCategoriesResponse(state).job_categories || [];
29
35
 
30
36
  export const selectWithKatello = state =>
31
- selectAPIResponse(state, JOB_CATEGORIES).with_katello || false;
37
+ selectJobCategoriesResponse(state).with_katello || false;
32
38
 
33
39
  export const selectJobCategoriesStatus = state =>
34
40
  selectAPIStatus(state, JOB_CATEGORIES);
@@ -64,6 +64,11 @@ describe('Job wizard fill', () => {
64
64
  selector: 'input',
65
65
  }).value
66
66
  ).toBe('1');
67
+ expect(
68
+ screen.getByLabelText('time to pickup', {
69
+ selector: 'input',
70
+ }).value
71
+ ).toBe('25');
67
72
 
68
73
  expect(
69
74
  screen.getByLabelText('Concurrency level', {
@@ -1,3 +1,4 @@
1
+ /* eslint-disable max-lines */
1
2
  import configureMockStore from 'redux-mock-store';
2
3
  import hostsQuery from '../steps/HostsAndInputs/hosts.gql';
3
4
  import hostgroupsQuery from '../steps/HostsAndInputs/hostgroups.gql';
@@ -14,6 +15,18 @@ export const jobTemplate = {
14
15
  execution_timeout_interval: 2,
15
16
  description: null,
16
17
  };
18
+ export const pupptetJobTemplate = {
19
+ id: 163,
20
+ name: 'Puppet Agent Disable - Script Default',
21
+ template:
22
+ '<% if @host.operatingsystem.family == \'Debian\' -%>\nexport PATH=/opt/puppetlabs/bin:$PATH\n<% end -%>\npuppet agent --disable "<%= input("comment").present? ? input("comment") : "Disabled using Foreman Remote Execution" %> - <%= current_user %> - $(date "+%d/%m/%Y %H:%M")"',
23
+ snippet: false,
24
+ default: true,
25
+ job_category: 'Puppet',
26
+ provider_type: 'script',
27
+ execution_timeout_interval: 2,
28
+ description: null,
29
+ };
17
30
 
18
31
  export const jobTemplates = [jobTemplate];
19
32
 
@@ -120,6 +133,7 @@ export const testSetup = (selectors, api) => {
120
133
  selectors.selectJobCategories.mockImplementation(() => jobCategories);
121
134
  selectors.selectJobTemplates.mockImplementation(() => [
122
135
  jobTemplate,
136
+ pupptetJobTemplate,
123
137
  { ...jobTemplate, id: 2, name: 'template2' },
124
138
  ]);
125
139
  selectors.selectJobTemplate.mockImplementation(() => jobTemplateResponse);
@@ -164,12 +178,21 @@ export const mockApi = api => {
164
178
  } else if (action.key === 'JOB_TEMPLATE') {
165
179
  handleSuccess &&
166
180
  handleSuccess({
167
- data: jobTemplateResponse,
181
+ data:
182
+ action.url === '/ui_job_wizard/template/163'
183
+ ? { ...jobTemplateResponse, job_template: pupptetJobTemplate }
184
+ : jobTemplateResponse,
168
185
  });
169
186
  } else if (action.key === 'JOB_TEMPLATES') {
170
187
  handleSuccess &&
171
188
  handleSuccess({
172
- data: { results: [jobTemplate] },
189
+ data: {
190
+ results:
191
+ action.url.search() ===
192
+ '?search=job_category%3D%22Puppet%22&per_page=all'
193
+ ? [pupptetJobTemplate]
194
+ : [jobTemplate],
195
+ },
173
196
  });
174
197
  } else if (action.key === 'HOST_IDS') {
175
198
  handleSuccess &&
@@ -252,6 +275,7 @@ export const jobInvocation = {
252
275
  time_span: 4,
253
276
  },
254
277
  execution_timeout_interval: 1,
278
+ time_to_pickup: 25,
255
279
  remote_execution_feature_id: null,
256
280
  template_invocations: [
257
281
  {
@@ -17,19 +17,30 @@ export const useAutoFill = ({
17
17
 
18
18
  useEffect(() => {
19
19
  if (Object.keys(fills).length) {
20
- const { 'host_ids[]': hostIds, search, feature, ...rest } = { ...fills };
20
+ const {
21
+ 'host_ids[]': hostIds,
22
+ search,
23
+ feature,
24
+ template_id: templateID,
25
+ ...rest
26
+ } = { ...fills };
21
27
  setFills({});
22
28
  if (hostIds) {
29
+ const hostSearch = Array.isArray(hostIds)
30
+ ? `id = ${hostIds.join(' or id = ')}`
31
+ : `id = ${hostIds}`;
23
32
  dispatch(
24
33
  get({
25
34
  key: HOST_IDS,
26
35
  url: '/api/hosts',
27
- params: { search: `id = ${hostIds.join(' or id = ')}` },
36
+ params: {
37
+ search: hostSearch,
38
+ },
28
39
  handleSuccess: ({ data }) => {
29
40
  setSelectedTargets(currentTargets => ({
30
41
  ...currentTargets,
31
- hosts: (data.results || []).map(({ name }) => ({
32
- id: name,
42
+ hosts: (data.results || []).map(({ id, name }) => ({
43
+ id,
33
44
  name,
34
45
  })),
35
46
  }));
@@ -37,9 +48,12 @@ export const useAutoFill = ({
37
48
  })
38
49
  );
39
50
  }
40
- if (search) {
51
+ if (search && !hostIds?.length) {
41
52
  setHostsSearchQuery(search);
42
53
  }
54
+ if (templateID) {
55
+ setJobTemplateID(+templateID);
56
+ }
43
57
  if (feature) {
44
58
  dispatch(
45
59
  get({
@@ -56,11 +70,19 @@ export const useAutoFill = ({
56
70
  const re = /inputs\[(?<input>.*)\]/g;
57
71
  const input = re.exec(key)?.groups?.input;
58
72
  if (input) {
59
- setTemplateValues(prev => ({ ...prev, [input]: rest[key] }));
60
- setAdvancedValues(prev => ({
61
- ...prev,
62
- templateValues: { ...prev.templateValues, [input]: rest[key] },
63
- }));
73
+ if (typeof rest[key] === 'string') {
74
+ setTemplateValues(prev => ({ ...prev, [input]: rest[key] }));
75
+ } else {
76
+ const { value, advanced } = rest[key];
77
+ if (advanced) {
78
+ setAdvancedValues(prev => ({
79
+ ...prev,
80
+ templateValues: { ...prev.templateValues, [input]: value },
81
+ }));
82
+ } else {
83
+ setTemplateValues(prev => ({ ...prev, [input]: value }));
84
+ }
85
+ }
64
86
  }
65
87
  });
66
88
  }
@@ -1,14 +1,15 @@
1
1
  import React from 'react';
2
- import { Title, Divider } from '@patternfly/react-core';
2
+ import PropTypes from 'prop-types';
3
+ import { Title, Divider, Flex, FlexItem, Button } from '@patternfly/react-core';
3
4
  import { translate as __ } from 'foremanReact/common/I18n';
4
5
  import PageLayout from 'foremanReact/routes/common/PageLayout/PageLayout';
5
6
  import { JobWizard } from './JobWizard';
6
7
 
7
- const JobWizardPage = () => {
8
+ const JobWizardPage = ({ location: { search } }) => {
8
9
  const title = __('Run job');
9
10
  const breadcrumbOptions = {
10
11
  breadcrumbItems: [
11
- { caption: __('Jobs'), url: `/jobs` },
12
+ { caption: __('Jobs'), url: `/job_invocations` },
12
13
  { caption: title },
13
14
  ],
14
15
  };
@@ -19,9 +20,22 @@ const JobWizardPage = () => {
19
20
  searchable={false}
20
21
  >
21
22
  <React.Fragment>
22
- <Title headingLevel="h2" size="2xl">
23
- {title}
24
- </Title>
23
+ <Flex>
24
+ <FlexItem>
25
+ <Title headingLevel="h2" size="2xl">
26
+ {title}
27
+ </Title>
28
+ </FlexItem>
29
+ <FlexItem align={{ default: 'alignRight' }}>
30
+ <Button
31
+ variant="link"
32
+ component="a"
33
+ href={`/old/job_invocations/new${search}`}
34
+ >
35
+ {__('Use legacy form')}
36
+ </Button>
37
+ </FlexItem>
38
+ </Flex>
25
39
  <Divider component="div" />
26
40
  <JobWizard />
27
41
  </React.Fragment>
@@ -29,4 +43,9 @@ const JobWizardPage = () => {
29
43
  );
30
44
  };
31
45
 
46
+ JobWizardPage.propTypes = {
47
+ location: PropTypes.shape({
48
+ search: PropTypes.string,
49
+ }).isRequired,
50
+ };
32
51
  export default JobWizardPage;