foreman_remote_execution 4.5.6 → 5.0.0

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 (118) 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/ui_job_wizard_controller.rb +16 -4
  6. data/app/graphql/mutations/job_invocations/create.rb +43 -0
  7. data/app/graphql/types/job_invocation.rb +16 -0
  8. data/app/graphql/types/job_invocation_input.rb +13 -0
  9. data/app/graphql/types/recurrence_input.rb +8 -0
  10. data/app/graphql/types/scheduling_input.rb +6 -0
  11. data/app/graphql/types/targeting_enum.rb +7 -0
  12. data/app/lib/actions/remote_execution/run_host_job.rb +6 -1
  13. data/app/lib/actions/remote_execution/run_hosts_job.rb +57 -3
  14. data/app/mailers/rex_job_mailer.rb +15 -0
  15. data/app/models/concerns/foreman_remote_execution/host_extensions.rb +8 -0
  16. data/app/models/job_invocation.rb +4 -0
  17. data/app/models/job_invocation_composer.rb +21 -13
  18. data/app/models/job_template.rb +1 -1
  19. data/app/models/remote_execution_provider.rb +17 -2
  20. data/app/models/rex_mail_notification.rb +13 -0
  21. data/app/models/targeting.rb +2 -2
  22. data/app/services/ui_notifications/remote_execution_jobs/base_job_finish.rb +2 -1
  23. data/app/views/dashboard/_latest-jobs.html.erb +21 -0
  24. data/app/views/job_invocations/refresh.js.erb +1 -0
  25. data/app/views/rex_job_mailer/job_finished.html.erb +24 -0
  26. data/app/views/rex_job_mailer/job_finished.text.erb +9 -0
  27. data/app/views/template_invocations/show.html.erb +2 -1
  28. data/config/routes.rb +1 -0
  29. data/db/migrate/20210816100932_rex_setting_category_to_dsl.rb +5 -0
  30. data/db/seeds.d/50-notification_blueprints.rb +14 -0
  31. data/db/seeds.d/95-mail_notifications.rb +24 -0
  32. data/foreman_remote_execution.gemspec +2 -4
  33. data/lib/foreman_remote_execution/engine.rb +114 -6
  34. data/lib/foreman_remote_execution/version.rb +1 -1
  35. data/package.json +6 -6
  36. data/test/functional/api/v2/job_invocations_controller_test.rb +20 -0
  37. data/test/functional/cockpit_controller_test.rb +0 -1
  38. data/test/graphql/mutations/job_invocations/create.rb +58 -0
  39. data/test/graphql/queries/job_invocation_query_test.rb +31 -0
  40. data/test/graphql/queries/job_invocations_query_test.rb +35 -0
  41. data/test/helpers/remote_execution_helper_test.rb +0 -1
  42. data/test/unit/actions/run_host_job_test.rb +21 -0
  43. data/test/unit/actions/run_hosts_job_test.rb +99 -4
  44. data/test/unit/concerns/host_extensions_test.rb +40 -7
  45. data/test/unit/input_template_renderer_test.rb +1 -89
  46. data/test/unit/job_invocation_composer_test.rb +4 -17
  47. data/test/unit/job_invocation_report_template_test.rb +16 -13
  48. data/test/unit/job_template_effective_user_test.rb +0 -4
  49. data/test/unit/remote_execution_provider_test.rb +34 -4
  50. data/test/unit/targeting_test.rb +68 -1
  51. data/webpack/JobWizard/JobWizard.js +106 -15
  52. data/webpack/JobWizard/JobWizard.scss +73 -39
  53. data/webpack/JobWizard/JobWizardConstants.js +36 -0
  54. data/webpack/JobWizard/JobWizardSelectors.js +32 -0
  55. data/webpack/JobWizard/__tests__/fixtures.js +81 -6
  56. data/webpack/JobWizard/__tests__/integration.test.js +26 -15
  57. data/webpack/JobWizard/__tests__/validation.test.js +141 -0
  58. data/webpack/JobWizard/autofill.js +38 -0
  59. data/webpack/JobWizard/index.js +7 -0
  60. data/webpack/JobWizard/steps/AdvancedFields/AdvancedFields.js +7 -4
  61. data/webpack/JobWizard/steps/AdvancedFields/DescriptionField.js +32 -9
  62. data/webpack/JobWizard/steps/AdvancedFields/__tests__/AdvancedFields.test.js +216 -12
  63. data/webpack/JobWizard/steps/AdvancedFields/__tests__/__snapshots__/AdvancedFields.test.js.snap +82 -0
  64. data/webpack/JobWizard/steps/CategoryAndTemplate/CategoryAndTemplate.js +1 -0
  65. data/webpack/JobWizard/steps/CategoryAndTemplate/index.js +3 -2
  66. data/webpack/JobWizard/steps/HostsAndInputs/HostPreviewModal.js +62 -0
  67. data/webpack/JobWizard/steps/HostsAndInputs/HostSearch.js +54 -0
  68. data/webpack/JobWizard/steps/HostsAndInputs/SelectAPI.js +33 -0
  69. data/webpack/JobWizard/steps/HostsAndInputs/SelectGQL.js +52 -0
  70. data/webpack/JobWizard/steps/HostsAndInputs/SelectedChips.js +82 -7
  71. data/webpack/JobWizard/steps/HostsAndInputs/__tests__/HostsAndInputs.test.js +151 -0
  72. data/webpack/JobWizard/steps/HostsAndInputs/__tests__/TemplateInputs.test.js +7 -4
  73. data/webpack/JobWizard/steps/HostsAndInputs/buildHostQuery.js +18 -0
  74. data/webpack/JobWizard/steps/HostsAndInputs/hostgroups.gql +8 -0
  75. data/webpack/JobWizard/steps/HostsAndInputs/hosts.gql +8 -0
  76. data/webpack/JobWizard/steps/HostsAndInputs/index.js +182 -34
  77. data/webpack/JobWizard/steps/ReviewDetails/index.js +193 -0
  78. data/webpack/JobWizard/steps/Schedule/PurposeField.js +31 -0
  79. data/webpack/JobWizard/steps/Schedule/QueryType.js +46 -43
  80. data/webpack/JobWizard/steps/Schedule/RepeatCron.js +53 -0
  81. data/webpack/JobWizard/steps/Schedule/RepeatDaily.js +37 -0
  82. data/webpack/JobWizard/steps/Schedule/RepeatHour.js +54 -0
  83. data/webpack/JobWizard/steps/Schedule/RepeatMonth.js +46 -0
  84. data/webpack/JobWizard/steps/Schedule/RepeatOn.js +95 -31
  85. data/webpack/JobWizard/steps/Schedule/RepeatWeek.js +70 -0
  86. data/webpack/JobWizard/steps/Schedule/ScheduleType.js +24 -21
  87. data/webpack/JobWizard/steps/Schedule/StartEndDates.js +78 -23
  88. data/webpack/JobWizard/steps/Schedule/__tests__/Schedule.test.js +402 -0
  89. data/webpack/JobWizard/steps/Schedule/__tests__/StartEndDates.test.js +20 -10
  90. data/webpack/JobWizard/steps/Schedule/index.js +153 -19
  91. data/webpack/JobWizard/steps/form/DateTimePicker.js +126 -0
  92. data/webpack/JobWizard/steps/form/FormHelpers.js +4 -0
  93. data/webpack/JobWizard/steps/form/Formatter.js +39 -8
  94. data/webpack/JobWizard/steps/form/NumberInput.js +3 -2
  95. data/webpack/JobWizard/steps/form/ResourceSelect.js +29 -0
  96. data/webpack/JobWizard/steps/form/SearchSelect.js +121 -0
  97. data/webpack/JobWizard/steps/form/SelectField.js +14 -3
  98. data/webpack/JobWizard/steps/form/__tests__/SelectSearch.test.js +33 -0
  99. data/webpack/JobWizard/submit.js +120 -0
  100. data/webpack/JobWizard/validation.js +53 -0
  101. data/webpack/__mocks__/foremanReact/Root/Context/ForemanContext/index.js +2 -0
  102. data/webpack/__mocks__/foremanReact/common/I18n.js +2 -0
  103. data/webpack/__mocks__/foremanReact/components/AutoComplete/AutoCompleteActions.js +1 -0
  104. data/webpack/__mocks__/foremanReact/components/AutoComplete/AutoCompleteConstants.js +1 -0
  105. data/webpack/__mocks__/foremanReact/routes/RouterSelector.js +1 -0
  106. data/webpack/helpers.js +1 -0
  107. data/webpack/react_app/components/RecentJobsCard/JobStatusIcon.js +43 -0
  108. data/webpack/react_app/components/RecentJobsCard/RecentJobsCard.js +73 -66
  109. data/webpack/react_app/components/RecentJobsCard/RecentJobsTable.js +98 -0
  110. data/webpack/react_app/components/RecentJobsCard/constants.js +11 -0
  111. data/webpack/react_app/components/RecentJobsCard/styles.scss +11 -0
  112. data/webpack/react_app/extend/fillRecentJobsCard.js +1 -1
  113. metadata +56 -23
  114. data/app/models/setting/remote_execution.rb +0 -88
  115. data/webpack/JobWizard/steps/AdvancedFields/__tests__/DescriptionField.test.js +0 -23
  116. data/webpack/JobWizard/steps/HostsAndInputs/__tests__/SelectedChips.test.js +0 -37
  117. data/webpack/JobWizard/steps/form/__tests__/Formatter.test.js.example +0 -76
  118. data/webpack/react_app/components/RecentJobsCard/styles.css +0 -15
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 0f14506c337ce696f794a63b0c30b59f6d301edc4d55b9d654e2b5360215beb9
4
- data.tar.gz: 0b9d86b4a422b84d507dfd977dea5dd9f2dc8b30e464b3e4383c8ea421e0615b
3
+ metadata.gz: 66efbd042b1d607eeaa4e72b07452ac00a4e68d28bf09e1fc496f26cae25f1e4
4
+ data.tar.gz: 124621a113adab399e259ece92caad11fdf35da117da77dc512fc9e31e6ec66a
5
5
  SHA512:
6
- metadata.gz: 250ec14568078576a42664e40e2d96b974282765e6cfcffca8ec58bdff9e72c8948f2e28e7ba5fdafe36fb45457e2f38c03bd3c39842ba0bebaa303970ea5f09
7
- data.tar.gz: 4f6469e5ad8535b7996d1a8c78d266edbba4c03e1e3eb9b52e0588c653351e153b87b4d6daaf082e039d7af47207fdf5e3e6267920cde4d2a233c4e11ac5ca57
6
+ metadata.gz: 04ed33fb87e3b0830501f97b95ab62c129897e8510909b47163559854bb0b2b792ccab4035c5fc606a6a8d8e5ff8c084fcfd2109331dff8b4e7a6eac23c86825
7
+ data.tar.gz: bca23be551bd1d8169c28a5a4e2d71079ea42cae3a11d6d289f40c9c862a2e7307b6c6e569ba79be7e743f729ef58a3ef72b027ee8735e9c448e4672a105db4b
@@ -71,3 +71,10 @@ jobs:
71
71
  run: |
72
72
  bundle exec rake test:foreman_remote_execution
73
73
  bundle exec rake test TEST="test/unit/foreman/access_permissions_test.rb"
74
+ - name: 'Upload logs'
75
+ uses: actions/upload-artifact@v2
76
+ if: failure()
77
+ with:
78
+ name: logs
79
+ path: log/*.log
80
+ retention-days: 5
data/.rubocop_todo.yml CHANGED
@@ -220,6 +220,7 @@ Naming/FileName:
220
220
  - 'db/seeds.d/60-ssh_proxy_feature.rb'
221
221
  - 'db/seeds.d/70-job_templates.rb'
222
222
  - 'db/seeds.d/90-bookmarks.rb'
223
+ - 'db/seeds.d/95-mail_notifications.rb'
223
224
 
224
225
  # Offense count: 1
225
226
  # Configuration parameters: ForbiddenDelimiters.
@@ -41,12 +41,18 @@ module Api
41
41
  param :effective_user, String,
42
42
  :required => false,
43
43
  :desc => N_('What user should be used to run the script (using sudo-like mechanisms). Defaults to a template parameter or global setting.')
44
+ param :effective_user_password, String,
45
+ :required => false,
46
+ :desc => N_('Set password for effective user (using sudo-like mechanisms)')
44
47
  end
48
+ param :password, String, :required => false, :desc => N_('Set SSH password')
49
+ param :key_passphrase, String, :required => false, :desc => N_('Set SSH key passphrase')
45
50
 
46
51
  param :recurrence, Hash, :desc => N_('Create a recurring job') do
47
52
  param :cron_line, String, :required => false, :desc => N_('How often the job should occur, in the cron format')
48
53
  param :max_iteration, :number, :required => false, :desc => N_('Repeat a maximum of N times')
49
54
  param :end_time, DateTime, :required => false, :desc => N_('Perform no more executions after this time')
55
+ param :purpose, String, :required => false, :desc => N_('Designation of a special purpose')
50
56
  end
51
57
 
52
58
  param :scheduling, Hash, :desc => N_('Schedule the job to start at a later time') do
@@ -162,6 +168,15 @@ module Api
162
168
  render :json => { :outputs => outputs }
163
169
  end
164
170
 
171
+ def resource_name(resource = controller_name)
172
+ case resource
173
+ when 'organization', 'location'
174
+ nil
175
+ else
176
+ 'job_invocation'
177
+ end
178
+ end
179
+
165
180
  private
166
181
 
167
182
  def allowed_nested_id
@@ -197,7 +212,7 @@ module Api
197
212
  end
198
213
 
199
214
  if job_invocation_params.key?(:ssh)
200
- job_invocation_params.merge!(job_invocation_params.delete(:ssh).permit(:effective_user))
215
+ job_invocation_params.merge!(job_invocation_params.delete(:ssh).permit(:effective_user, :effective_user_password))
201
216
  end
202
217
 
203
218
  job_invocation_params[:inputs] ||= {}
@@ -1,11 +1,12 @@
1
- class UiJobWizardController < ::Api::V2::BaseController
1
+ class UiJobWizardController < ApplicationController
2
+ include FiltersHelper
2
3
  def categories
3
4
  job_categories = resource_scope
4
5
  .search_for("job_category ~ \"#{params[:search]}\"")
5
6
  .select(:job_category).distinct
6
7
  .reorder(:job_category)
7
8
  .pluck(:job_category)
8
- render :json => {:job_categories =>job_categories}
9
+ render :json => {:job_categories =>job_categories, :with_katello => with_katello}
9
10
  end
10
11
 
11
12
  def template
@@ -19,12 +20,16 @@ class UiJobWizardController < ::Api::V2::BaseController
19
20
  }
20
21
  end
21
22
 
23
+ def map_template_inputs(template_inputs_with_foreign)
24
+ template_inputs_with_foreign.map { |input| input.attributes.merge({:resource_type_tableize => input.resource_type&.tableize }) }
25
+ end
26
+
22
27
  def resource_name(nested_resource = nil)
23
28
  nested_resource || 'job_template'
24
29
  end
25
30
 
26
- def map_template_inputs(template_inputs_with_foreign)
27
- template_inputs_with_foreign.map { |input| input.attributes.merge({:resource_type => input.resource_type&.tableize }) }
31
+ def with_katello
32
+ !!defined?(::Katello)
28
33
  end
29
34
 
30
35
  def resource_class
@@ -34,4 +39,11 @@ class UiJobWizardController < ::Api::V2::BaseController
34
39
  def action_permission
35
40
  :view_job_templates
36
41
  end
42
+
43
+ def resources
44
+ resource_type = params[:resource]
45
+ resource_list = resource_type.constantize.authorized("view_#{resource_type.underscore.pluralize}").all.map { |r| {:name => r.to_s, :id => r.id } }.select { |v| v[:name] =~ /#{params[:name]}/ }
46
+ render :json => { :results =>
47
+ resource_list.sort_by { |r| r[:name] }.take(100), :subtotal => resource_list.count}
48
+ end
37
49
  end
@@ -0,0 +1,43 @@
1
+ module Mutations
2
+ module JobInvocations
3
+ class Create < ::Mutations::CreateMutation
4
+ description 'Create a new job invocation'
5
+ graphql_name 'CreateJobInvocationMutation'
6
+
7
+ argument :job_invocation, ::Types::JobInvocationInput, required: true
8
+ field :job_invocation, ::Types::JobInvocation, 'The new job invocation', null: true
9
+
10
+ def resolve(params)
11
+ begin
12
+ composer = JobInvocationComposer.from_api_params(params[:job_invocation].to_h)
13
+ job_invocation = composer.job_invocation
14
+ authorize!(job_invocation, :create)
15
+ errors = []
16
+ composer.trigger!
17
+ rescue ActiveRecord::RecordNotSaved
18
+ errors = map_all_errors(job_invocation)
19
+ rescue JobInvocationComposer::JobTemplateNotFound, JobInvocationComposer::FeatureNotFound => e
20
+ errors = [{ path => ['job_template'], :message => e.message }]
21
+ end
22
+
23
+ {
24
+ result_key => job_invocation,
25
+ :errors => errors,
26
+ }
27
+ end
28
+
29
+ def map_all_errors(job_invocation)
30
+ map_errors_to_path(job_invocation) + map_errors('triggering', job_invocation.triggering) + map_errors('targeting', job_invocation.targeting)
31
+ end
32
+
33
+ def map_errors(attr_name, resource)
34
+ resource.errors.map do |attribute, message|
35
+ {
36
+ path: [attr_name, attribute.to_s.camelize(:lower)],
37
+ message: message,
38
+ }
39
+ end
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,16 @@
1
+ module Types
2
+ class JobInvocation < BaseObject
3
+ description 'A Job Invocation'
4
+
5
+ global_id_field :id
6
+ field :job_category, String
7
+ field :description, String
8
+ field :time_span, Integer
9
+ field :start_at, GraphQL::Types::ISO8601DateTime
10
+ field :status_label, String
11
+
12
+ belongs_to :triggering, Types::Triggering
13
+ belongs_to :task, Types::Task
14
+ field :recurring_logic, Types::RecurringLogic
15
+ end
16
+ end
@@ -0,0 +1,13 @@
1
+ module Types
2
+ class JobInvocationInput < BaseInputObject
3
+ argument :job_category, String, required: false
4
+ argument :job_template_id, Integer, required: false
5
+ argument :feature, String, required: false
6
+ argument :targeting_type, ::Types::TargetingEnum, required: false
7
+ argument :recurrence, ::Types::RecurrenceInput, required: false
8
+ argument :scheduling, ::Types::SchedulingInput, required: false
9
+ argument :search_query, String, required: false
10
+ argument :host_ids, [Integer], required: false
11
+ argument :inputs, ::Types::RawJson, required: false
12
+ end
13
+ end
@@ -0,0 +1,8 @@
1
+ module Types
2
+ class RecurrenceInput < BaseInputObject
3
+ argument :cron_line, String, required: false
4
+ argument :max_iteration, Integer, required: false
5
+ argument :end_time, String, required: false
6
+ argument :purpose, String, required: false
7
+ end
8
+ end
@@ -0,0 +1,6 @@
1
+ module Types
2
+ class SchedulingInput < BaseInputObject
3
+ argument :start_at, String, required: false
4
+ argument :start_before, String, required: false
5
+ end
6
+ end
@@ -0,0 +1,7 @@
1
+ module Types
2
+ class TargetingEnum < Types::BaseEnum
3
+ Targeting::TYPES.each_key do |key|
4
+ value key
5
+ end
6
+ end
7
+ end
@@ -56,7 +56,8 @@ module Actions
56
56
  :secrets => secrets(host, job_invocation, provider),
57
57
  :use_batch_triggering => true,
58
58
  :use_concurrency_control => options[:use_concurrency_control],
59
- :first_execution => first_execution }
59
+ :first_execution => first_execution,
60
+ :alternative_names => provider.alternative_names(host) }
60
61
  action_options = provider.proxy_command_options(template_invocation, host)
61
62
  .merge(additional_options)
62
63
 
@@ -195,6 +196,10 @@ module Actions
195
196
  def verify_permissions(host, template_invocation)
196
197
  raise _('User can not execute job on host %s') % host.name unless User.current.can?(:view_hosts, host)
197
198
  raise _('User can not execute this job template') unless User.current.can?(:view_job_templates, template_invocation.template)
199
+ infra_facet = host.infrastructure_facet
200
+ if (infra_facet&.foreman_instance || infra_facet&.smart_proxy_id) && !User.current.can?(:execute_jobs_on_infrastructure_hosts)
201
+ raise _('User can not execute job on infrastructure host %s') % host.name
202
+ end
198
203
 
199
204
  # we don't want to load all template_invocations to verify so we construct Authorizer object manually and set
200
205
  # the base collection to current template
@@ -9,7 +9,9 @@ module Actions
9
9
  middleware.use Actions::Middleware::BindJobInvocation
10
10
  middleware.use Actions::Middleware::RecurringLogic
11
11
  middleware.use Actions::Middleware::WatchDelegatedProxySubTasks
12
- middleware.use Actions::Middleware::ProxyBatchTriggering
12
+
13
+ execution_plan_hooks.use :notify_on_success, :on => :success
14
+ execution_plan_hooks.use :notify_on_failure, :on => :failure
13
15
 
14
16
  class CheckOnProxyActions; end
15
17
 
@@ -32,6 +34,10 @@ module Actions
32
34
  set_up_concurrency_control job_invocation
33
35
  input.update(:job_category => job_invocation.job_category)
34
36
  plan_self(:job_invocation_id => job_invocation.id)
37
+ provider = job_invocation.pattern_template_invocations.first&.template&.provider
38
+ input[:proxy_batch_size] ||= provider&.proxy_batch_size || Setting['foreman_tasks_proxy_batch_size']
39
+ trigger_action = plan_action(Actions::TriggerProxyBatch, batch_size: proxy_batch_size, total_count: hosts.count)
40
+ input[:trigger_run_step_id] = trigger_action.run_step_id
35
41
  end
36
42
 
37
43
  def create_sub_plans
@@ -46,14 +52,47 @@ module Actions
46
52
  end
47
53
  end
48
54
 
55
+ def spawn_plans
56
+ super
57
+ ensure
58
+ trigger_remote_batch
59
+ end
60
+
61
+ def trigger_remote_batch
62
+ batches_ready = (output[:planned_count] - output[:remote_triggered_count]) / proxy_batch_size
63
+ return unless batches_ready > 0
64
+
65
+ plan_event(Actions::TriggerProxyBatch::TriggerNextBatch[batches_ready], nil, step_id: input[:trigger_run_step_id])
66
+ output[:remote_triggered_count] += proxy_batch_size * batches_ready
67
+ end
68
+
69
+ def on_planning_finished
70
+ plan_event(Actions::TriggerProxyBatch::TriggerLastBatch, nil, step_id: input[:trigger_run_step_id])
71
+ super
72
+ end
73
+
49
74
  def finalize
50
75
  job_invocation.password = job_invocation.key_passphrase = job_invocation.effective_user_password = nil
51
76
  job_invocation.save!
52
77
 
53
78
  Rails.logger.debug "cleaning cache for keys that begin with 'job_invocation_#{job_invocation.id}'"
54
79
  Rails.cache.delete_matched(/\A#{JobInvocation::CACHE_PREFIX}_#{job_invocation.id}/)
55
- # creating the success notification should be the very last thing this tasks do
80
+ end
81
+
82
+ def notify_on_success(_plan)
56
83
  job_invocation.build_notification.deliver!
84
+
85
+ if [RexMailNotification::SUCCEEDED_JOBS, RexMailNotification::ALL_JOBS].include?(mail_notification_preference&.interval)
86
+ RexJobMailer.job_finished(job_invocation, subject: _("REX job has succeeded - %s") % job_invocation.to_s).deliver_now
87
+ end
88
+ end
89
+
90
+ def notify_on_failure(_plan)
91
+ job_invocation.build_notification.deliver!
92
+
93
+ if [RexMailNotification::FAILED_JOBS, RexMailNotification::ALL_JOBS].include?(mail_notification_preference&.interval)
94
+ RexJobMailer.job_finished(job_invocation, subject: _("REX job has failed - %s") % job_invocation.to_s).deliver_now
95
+ end
57
96
  end
58
97
 
59
98
  def job_invocation
@@ -67,6 +106,7 @@ module Actions
67
106
 
68
107
  def initiate
69
108
  output[:host_count] = total_count
109
+ output[:remote_triggered_count] = 0
70
110
  super
71
111
  end
72
112
 
@@ -94,7 +134,11 @@ module Actions
94
134
  end
95
135
 
96
136
  def run(event = nil)
97
- super unless event == Dynflow::Action::Skip
137
+ if event == Dynflow::Action::Skip
138
+ plan_event(Dynflow::Action::Skip, nil, step_id: input[:trigger_run_step_id])
139
+ else
140
+ super
141
+ end
98
142
  end
99
143
 
100
144
  def humanized_input
@@ -104,6 +148,16 @@ module Actions
104
148
  def humanized_name
105
149
  '%s:' % _(super)
106
150
  end
151
+
152
+ def proxy_batch_size
153
+ input[:proxy_batch_size]
154
+ end
155
+
156
+ private
157
+
158
+ def mail_notification_preference
159
+ UserMailNotification.where(mail_notification_id: RexMailNotification.first, user_id: User.current.id).first
160
+ end
107
161
  end
108
162
  end
109
163
  end
@@ -0,0 +1,15 @@
1
+ class RexJobMailer < ApplicationMailer
2
+ add_template_helper(ApplicationHelper)
3
+
4
+ def job_finished(job, opts = {})
5
+ @job = job
6
+ @subject = opts[:subject] || _('REX job has finished - %s') % @job.to_s
7
+
8
+ if @job.user.nil?
9
+ Rails.logger.warn 'Job user no longer exist, skipping email notification'
10
+ return
11
+ end
12
+
13
+ mail(to: @job.user.mail, subject: @subject)
14
+ end
15
+ end
@@ -20,6 +20,14 @@ module ForemanRemoteExecution
20
20
  scoped_search :relation => :execution_status_object, :on => :status, :rename => :execution_status,
21
21
  :complete_value => { :ok => HostStatus::ExecutionStatus::OK, :error => HostStatus::ExecutionStatus::ERROR }
22
22
 
23
+ scope :execution_scope, lambda {
24
+ if User.current&.can?('execute_jobs_on_infrastructure_hosts')
25
+ self
26
+ else
27
+ search_for('not (infrastructure_facet.foreman = true or set? infrastructure_facet.smart_proxy_id)')
28
+ end
29
+ }
30
+
23
31
  def search_by_job_invocation(key, operator, value)
24
32
  if key == 'job_invocation.result'
25
33
  operator = operator == '=' ? 'IN' : 'NOT IN'
@@ -65,6 +65,8 @@ class JobInvocation < ApplicationRecord
65
65
  scoped_search :on => 'pattern_template_name', :rename => 'pattern_template_name', :operators => ['= '],
66
66
  :complete_value => false, :only_explicit => true, :ext_method => :search_by_pattern_template
67
67
 
68
+ scoped_search :relation => :recurring_logic, :on => 'purpose', :rename => 'recurring_logic.purpose'
69
+
68
70
  scoped_search :relation => :recurring_logic, :on => 'id', :rename => 'recurring_logic.id'
69
71
 
70
72
  scoped_search :relation => :recurring_logic, :on => 'id', :rename => 'recurring',
@@ -73,6 +75,8 @@ class JobInvocation < ApplicationRecord
73
75
 
74
76
  default_scope -> { order('job_invocations.id DESC') }
75
77
 
78
+ scope :latest_jobs, -> { unscoped.joins(:task).order('foreman_tasks_tasks.start_at DESC').authorized(:view_job_invocations).limit(5) }
79
+
76
80
  validates_lengths_from_database :only => [:description]
77
81
 
78
82
  attr_accessor :start_before, :description_format
@@ -121,7 +121,10 @@ class JobInvocationComposer
121
121
  :targeting => targeting_params,
122
122
  :triggering => triggering_params,
123
123
  :description_format => api_params[:description_format],
124
+ :password => api_params[:password],
124
125
  :remote_execution_feature_id => remote_execution_feature_id,
126
+ :effective_user_password => api_params[:effective_user_password],
127
+ :key_passphrase => api_params[:key_passphrase],
125
128
  :concurrency_control => concurrency_control_params,
126
129
  :execution_timeout_interval => api_params[:execution_timeout_interval] || template.execution_timeout_interval,
127
130
  :template_invocations => template_invocations_params }.with_indifferent_access
@@ -138,17 +141,11 @@ class JobInvocationComposer
138
141
  end
139
142
 
140
143
  def triggering_params
141
- raise ::Foreman::Exception, _('Cannot specify both recurrence and scheduling') if api_params[:recurrence].present? && api_params[:scheduling].present?
142
-
143
- if api_params[:recurrence].present?
144
- {
145
- :mode => :recurring,
146
- :cronline => api_params[:recurrence][:cron_line],
147
- :end_time => format_datetime(api_params[:recurrence][:end_time]),
148
- :input_type => :cronline,
149
- :max_iteration => api_params[:recurrence][:max_iteration],
150
- }
151
- elsif api_params[:scheduling].present?
144
+ if api_params[:recurrence].present? && api_params[:scheduling].present?
145
+ recurring_mode_params.merge :start_at_raw => format_datetime(api_params[:scheduling][:start_at])
146
+ elsif api_params[:recurrence].present? && api_params[:scheduling].empty?
147
+ recurring_mode_params
148
+ elsif api_params[:recurrence].empty? && api_params[:scheduling].present?
152
149
  {
153
150
  :mode => :future,
154
151
  :start_at_raw => format_datetime(api_params[:scheduling][:start_at]),
@@ -160,6 +157,17 @@ class JobInvocationComposer
160
157
  end
161
158
  end
162
159
 
160
+ def recurring_mode_params
161
+ {
162
+ :mode => :recurring,
163
+ :cronline => api_params[:recurrence][:cron_line],
164
+ :end_time => format_datetime(api_params[:recurrence][:end_time]),
165
+ :input_type => :cronline,
166
+ :max_iteration => api_params[:recurrence][:max_iteration],
167
+ :purpose => api_params[:recurrence][:purpose],
168
+ }
169
+ end
170
+
163
171
  def concurrency_control_params
164
172
  {
165
173
  :level => api_params.fetch(:concurrency_control, {})[:concurrency_level],
@@ -513,14 +521,14 @@ class JobInvocationComposer
513
521
  end
514
522
 
515
523
  def available_bookmarks
516
- Bookmark.authorized(:view_bookmarks).my_bookmarks.where(:controller => ['hosts', 'dashboard'])
524
+ Bookmark.my_bookmarks.where(:controller => ['hosts', 'dashboard'])
517
525
  end
518
526
 
519
527
  def targeted_hosts
520
528
  if displayed_search_query.blank?
521
529
  Host.where('1 = 0')
522
530
  else
523
- Host.authorized(Targeting::RESOLVE_PERMISSION, Host).search_for(displayed_search_query)
531
+ Host.execution_scope.authorized(Targeting::RESOLVE_PERMISSION, Host).search_for(displayed_search_query)
524
532
  end
525
533
  end
526
534
 
@@ -83,7 +83,7 @@ class JobTemplate < ::Template
83
83
  end
84
84
 
85
85
  def acceptable_template_input_types
86
- [ :user, :fact, :variable, :puppet_parameter ]
86
+ [ :user, :fact, :variable ]
87
87
  end
88
88
 
89
89
  def default_render_scope_class
@@ -80,7 +80,14 @@ class RemoteExecutionProvider
80
80
 
81
81
  def find_ip(host, interfaces)
82
82
  if host_setting(host, :remote_execution_connect_by_ip)
83
- interfaces.find { |i| i.ip.present? }.try(:ip)
83
+ ip4_address = interfaces.find { |i| i.ip.present? }.try(:ip)
84
+ ip6_address = interfaces.find { |i| i.ip6.present? }.try(:ip6)
85
+
86
+ if host_setting(host, :remote_execution_connect_by_ip_prefer_ipv6)
87
+ ip6_address || ip4_address
88
+ else
89
+ ip4_address || ip6_address
90
+ end
84
91
  end
85
92
  end
86
93
 
@@ -112,7 +119,11 @@ class RemoteExecutionProvider
112
119
  end
113
120
 
114
121
  def proxy_action_class
115
- 'ForemanRemoteExecutionCore::Actions::RunScript'
122
+ 'Proxy::RemoteExecution::Ssh::Actions::RunScript'
123
+ end
124
+
125
+ def proxy_batch_size
126
+ Setting['foreman_tasks_proxy_batch_size']
116
127
  end
117
128
 
118
129
  # Return a specific proxy selector to use for running a given template
@@ -124,5 +135,9 @@ class RemoteExecutionProvider
124
135
  ::DefaultProxyProxySelector.new
125
136
  end
126
137
  end
138
+
139
+ def alternative_names(host)
140
+ { :fqdn => find_fqdn(effective_interfaces(host)) }
141
+ end
127
142
  end
128
143
  end
@@ -0,0 +1,13 @@
1
+ class RexMailNotification < MailNotification
2
+ FAILED_JOBS = N_("Subscribe to my failed jobs")
3
+ SUCCEEDED_JOBS = N_("Subscribe to my succeeded jobs")
4
+ ALL_JOBS = N_("Subscribe to all my jobs")
5
+
6
+ def subscription_options
7
+ [
8
+ FAILED_JOBS,
9
+ SUCCEEDED_JOBS,
10
+ ALL_JOBS,
11
+ ]
12
+ end
13
+ end
@@ -44,7 +44,7 @@ class Targeting < ApplicationRecord
44
44
  self.validate!
45
45
  # avoid validation of hosts objects - they will be loaded for no reason.
46
46
  # pluck(:id) returns duplicate results for HostCollections
47
- host_ids = User.as(user.login) { Host.authorized(RESOLVE_PERMISSION, Host).search_for(search_query).order(:name, :id).pluck(:id).uniq }
47
+ host_ids = User.as(user.login) { Host.execution_scope.authorized(RESOLVE_PERMISSION, Host).search_for(search_query).order(:name, :id).pluck(:id).uniq }
48
48
  host_ids.shuffle!(random: Random.new) if randomized_ordering
49
49
  self.assign_host_ids(host_ids)
50
50
  self.save(:validate => false)
@@ -66,7 +66,7 @@ class Targeting < ApplicationRecord
66
66
  def self.build_query_from_hosts(ids)
67
67
  return '' if ids.empty?
68
68
 
69
- hosts = Host.where(:id => ids).distinct.pluck(:name)
69
+ hosts = Host.execution_scope.where(:id => ids).distinct.pluck(:name)
70
70
  "name ^ (#{hosts.join(', ')})"
71
71
  end
72
72
 
@@ -20,7 +20,8 @@ module UINotifications
20
20
  end
21
21
 
22
22
  def blueprint
23
- @blueprint ||= NotificationBlueprint.unscoped.find_by(:name => 'rex_job_succeeded')
23
+ blueprint = @subject.status == HostStatus::ExecutionStatus::ERROR ? 'rex_job_failed' : 'rex_job_succeeded'
24
+ @blueprint ||= NotificationBlueprint.unscoped.find_by(:name => blueprint)
24
25
  end
25
26
 
26
27
  def message
@@ -0,0 +1,21 @@
1
+ <h4 class="header">
2
+ <%= link_to _("Latest Jobs"), job_invocations_path(:order=>'start_at DESC') %>
3
+ </h4>
4
+ <% if JobInvocation.latest_jobs.any? %>
5
+ <table class="<%= table_css_classes('table-fixed') %>">
6
+ <tr>
7
+ <th class="col-md-5"><%= _("Name") %></th>
8
+ <th class="col-md-2"><%= _("State") %></th>
9
+ <th class="col-md-3"><%= _("Started") %></th>
10
+ </tr>
11
+ <% JobInvocation.latest_jobs.each do |invocation| %>
12
+ <tr>
13
+ <td class="ellipsis"><%= link_to_if_authorized invocation_description(invocation), hash_for_job_invocation_path(invocation).merge(:auth_object => invocation, :permission => :view_job_invocations, :authorizer => authorizer) %></td>
14
+ <td><%= link_to_invocation_task_if_authorized(invocation) %></td>
15
+ <td><%= time_in_words_span(invocation.start_at) %></td>
16
+ </tr>
17
+ <% end %>
18
+ </table>
19
+ <% else %>
20
+ <p class="ca"><%= _("No jobs available") %></p>
21
+ <% end %>
@@ -1,3 +1,4 @@
1
1
  $('form#job_invocation_form').replaceWith("<%=j render :partial => 'form' %>");
2
2
  $('#job_invocation_form').find('a[rel="popover"]').popover();
3
3
  $('#job_invocation_form select:not(.without_select2)').select2({ allowClear: true });
4
+ $('div.tooltip').remove();
@@ -0,0 +1,24 @@
1
+ <p>
2
+ <%= _("A job '%{job_name}' has %{status} at %{time}") % {
3
+ :job_name => @job.to_s, :status => @job.status_label, :time => date_time_absolute_value(@job.task.ended_at)
4
+ } %>
5
+ </p>
6
+
7
+ <div class="dashboard">
8
+ <table>
9
+ <tr>
10
+ <td width="18%" class="hosts-rows"><b><%= _("Job result") %></b></td>
11
+ <td width="82%" class="hosts-rows"><%= @job.status_label %></td>
12
+ </tr>
13
+ <tr>
14
+ <td width="18%" class="hosts-rows"><b><%= _("Total hosts") %></b></td>
15
+ <td width="82%" class="hosts-rows"><%= @job.total_hosts_count %></td>
16
+ </tr>
17
+ <tr>
18
+ <td width="18%" class="hosts-rows"><b><%= _("Failed hosts") %></b></td>
19
+ <td width="82%" class="hosts-rows"><%= @job.failed_hosts.count %></td>
20
+ </tr>
21
+ </table>
22
+ </div>
23
+
24
+ <%= link_to 'More details', job_invocation_url(@job) %>
@@ -0,0 +1,9 @@
1
+ <%= _("A job '%{job_name}' has %{status} at %{time}") % {
2
+ :job_name => @job.to_s, :status => @job.status_label, :time => date_time_absolute_value(@job.task.ended_at)
3
+ } %>
4
+
5
+ <%= _('Job result') %>: <%= @job.status_label %>
6
+ <%= _('Total hosts') %>: <%= @job.total_hosts_count %>
7
+ <%= _('Failed hosts') %>: <%= @job.failed_hosts.count %>
8
+
9
+ <%= _('See more details at %s') % job_invocation_url(@job) %>
@@ -18,7 +18,8 @@ end
18
18
 
19
19
  <div id="title_action">
20
20
  <div class="btn-toolbar pull-right">
21
- <%= link_to(_('Back to Job'), job_invocation_path(@template_invocation.job_invocation), :class => 'btn btn-default') %>
21
+ <%= button_group(link_to(_('Back to Job'), job_invocation_path(@template_invocation.job_invocation), :class => 'btn btn-default'),
22
+ (link_to(_('Rerun'), rerun_job_invocation_path(@template_invocation.job_invocation, :host_ids => [ @host.id ]), :class => 'btn btn-default') if authorized_for(:permission => :create_job_invocations))) %>
22
23
  <%= button_group(link_to_function(_('Toggle command'), '$("div.preview").toggle()', :class => 'btn btn-default'),
23
24
  link_to_function(_('Toggle STDERR'), '$("div.line.stderr").toggle()', :class => 'btn btn-default'),
24
25
  link_to_function(_('Toggle STDOUT'), '$("div.line.stdout").toggle()', :class => 'btn btn-default'),
data/config/routes.rb CHANGED
@@ -45,6 +45,7 @@ Rails.application.routes.draw do
45
45
  get 'cockpit/redirect', to: 'cockpit#redirect'
46
46
  get 'ui_job_wizard/categories', to: 'ui_job_wizard#categories'
47
47
  get 'ui_job_wizard/template/:id', to: 'ui_job_wizard#template'
48
+ get 'ui_job_wizard/resources', to: 'ui_job_wizard#resources'
48
49
 
49
50
  match '/experimental/job_wizard', to: 'react#index', :via => [:get]
50
51