foreman_remote_execution 4.7.0 → 5.0.2
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.
- checksums.yaml +4 -4
- data/.rubocop_todo.yml +1 -0
- data/app/controllers/api/v2/job_invocations_controller.rb +16 -1
- data/app/controllers/ui_job_wizard_controller.rb +16 -4
- data/app/graphql/mutations/job_invocations/create.rb +43 -0
- data/app/graphql/types/job_invocation_input.rb +13 -0
- data/app/graphql/types/recurrence_input.rb +8 -0
- data/app/graphql/types/scheduling_input.rb +6 -0
- data/app/graphql/types/targeting_enum.rb +7 -0
- data/app/helpers/concerns/foreman_remote_execution/hosts_helper_extensions.rb +20 -9
- data/app/helpers/remote_execution_helper.rb +1 -1
- data/app/lib/actions/remote_execution/run_host_job.rb +6 -1
- data/app/lib/actions/remote_execution/run_hosts_job.rb +57 -3
- data/app/mailers/rex_job_mailer.rb +15 -0
- data/app/models/concerns/foreman_remote_execution/host_extensions.rb +12 -0
- data/app/models/job_invocation.rb +4 -0
- data/app/models/job_invocation_composer.rb +21 -13
- data/app/models/remote_execution_provider.rb +18 -2
- data/app/models/rex_mail_notification.rb +13 -0
- data/app/models/targeting.rb +3 -3
- data/app/services/ui_notifications/remote_execution_jobs/base_job_finish.rb +2 -1
- data/app/views/dashboard/_latest-jobs.html.erb +21 -0
- data/app/views/job_invocations/_preview_hosts_list.html.erb +1 -1
- data/app/views/job_invocations/refresh.js.erb +1 -0
- data/app/views/rex_job_mailer/job_finished.html.erb +24 -0
- data/app/views/rex_job_mailer/job_finished.text.erb +9 -0
- data/app/views/template_invocations/show.html.erb +3 -2
- data/config/routes.rb +1 -0
- data/db/migrate/20210816100932_rex_setting_category_to_dsl.rb +5 -0
- data/db/seeds.d/50-notification_blueprints.rb +14 -0
- data/db/seeds.d/95-mail_notifications.rb +24 -0
- data/foreman_remote_execution.gemspec +1 -1
- data/lib/foreman_remote_execution/engine.rb +116 -7
- data/lib/foreman_remote_execution/version.rb +1 -1
- data/package.json +9 -7
- data/test/functional/api/v2/job_invocations_controller_test.rb +20 -0
- data/test/functional/cockpit_controller_test.rb +0 -1
- data/test/graphql/mutations/job_invocations/create.rb +58 -0
- data/test/helpers/remote_execution_helper_test.rb +0 -1
- data/test/unit/actions/run_host_job_test.rb +21 -0
- data/test/unit/actions/run_hosts_job_test.rb +99 -4
- data/test/unit/concerns/host_extensions_test.rb +36 -3
- data/test/unit/job_invocation_composer_test.rb +3 -5
- data/test/unit/job_invocation_report_template_test.rb +16 -13
- data/test/unit/job_template_effective_user_test.rb +0 -4
- data/test/unit/remote_execution_provider_test.rb +46 -4
- data/test/unit/targeting_test.rb +68 -1
- data/webpack/JobWizard/JobWizard.js +142 -28
- data/webpack/JobWizard/JobWizard.scss +86 -33
- data/webpack/JobWizard/JobWizardConstants.js +44 -0
- data/webpack/JobWizard/JobWizardSelectors.js +32 -0
- data/webpack/JobWizard/__tests__/fixtures.js +89 -6
- data/webpack/JobWizard/__tests__/integration.test.js +29 -22
- data/webpack/JobWizard/__tests__/validation.test.js +141 -0
- data/webpack/JobWizard/autofill.js +38 -0
- data/webpack/JobWizard/index.js +7 -0
- data/webpack/JobWizard/steps/AdvancedFields/AdvancedFields.js +23 -9
- data/webpack/JobWizard/steps/AdvancedFields/DescriptionField.js +32 -9
- data/webpack/JobWizard/steps/AdvancedFields/Fields.js +48 -1
- data/webpack/JobWizard/steps/AdvancedFields/__tests__/AdvancedFields.test.js +242 -23
- data/webpack/JobWizard/steps/AdvancedFields/__tests__/__snapshots__/AdvancedFields.test.js.snap +82 -0
- data/webpack/JobWizard/steps/CategoryAndTemplate/CategoryAndTemplate.js +5 -2
- data/webpack/JobWizard/steps/CategoryAndTemplate/CategoryAndTemplate.test.js +3 -2
- data/webpack/JobWizard/steps/CategoryAndTemplate/index.js +3 -2
- data/webpack/JobWizard/steps/HostsAndInputs/HostPreviewModal.js +62 -0
- data/webpack/JobWizard/steps/HostsAndInputs/HostSearch.js +54 -0
- data/webpack/JobWizard/steps/HostsAndInputs/SelectAPI.js +33 -0
- data/webpack/JobWizard/steps/HostsAndInputs/SelectGQL.js +52 -0
- data/webpack/JobWizard/steps/HostsAndInputs/SelectedChips.js +100 -0
- data/webpack/JobWizard/steps/HostsAndInputs/TemplateInputs.js +23 -0
- data/webpack/JobWizard/steps/HostsAndInputs/__tests__/HostsAndInputs.test.js +151 -0
- data/webpack/JobWizard/steps/HostsAndInputs/__tests__/TemplateInputs.test.js +53 -0
- data/webpack/JobWizard/steps/HostsAndInputs/buildHostQuery.js +18 -0
- data/webpack/JobWizard/steps/HostsAndInputs/hostgroups.gql +8 -0
- data/webpack/JobWizard/steps/HostsAndInputs/hosts.gql +8 -0
- data/webpack/JobWizard/steps/HostsAndInputs/index.js +214 -0
- data/webpack/JobWizard/steps/ReviewDetails/index.js +193 -0
- data/webpack/JobWizard/steps/Schedule/PurposeField.js +31 -0
- data/webpack/JobWizard/steps/Schedule/QueryType.js +46 -43
- data/webpack/JobWizard/steps/Schedule/RepeatCron.js +53 -0
- data/webpack/JobWizard/steps/Schedule/RepeatDaily.js +37 -0
- data/webpack/JobWizard/steps/Schedule/RepeatHour.js +54 -0
- data/webpack/JobWizard/steps/Schedule/RepeatMonth.js +46 -0
- data/webpack/JobWizard/steps/Schedule/RepeatOn.js +95 -31
- data/webpack/JobWizard/steps/Schedule/RepeatWeek.js +70 -0
- data/webpack/JobWizard/steps/Schedule/ScheduleType.js +24 -21
- data/webpack/JobWizard/steps/Schedule/StartEndDates.js +78 -23
- data/webpack/JobWizard/steps/Schedule/__tests__/Schedule.test.js +402 -0
- data/webpack/JobWizard/steps/Schedule/__tests__/StartEndDates.test.js +20 -10
- data/webpack/JobWizard/steps/Schedule/index.js +166 -29
- data/webpack/JobWizard/steps/form/DateTimePicker.js +126 -0
- data/webpack/JobWizard/steps/form/FormHelpers.js +4 -0
- data/webpack/JobWizard/steps/form/Formatter.js +49 -17
- data/webpack/JobWizard/steps/form/NumberInput.js +5 -2
- data/webpack/JobWizard/steps/form/ResourceSelect.js +29 -0
- data/webpack/JobWizard/steps/form/SearchSelect.js +121 -0
- data/webpack/JobWizard/steps/form/SelectField.js +14 -3
- data/webpack/JobWizard/steps/form/WizardTitle.js +14 -0
- data/webpack/JobWizard/steps/form/__tests__/SelectSearch.test.js +33 -0
- data/webpack/JobWizard/submit.js +120 -0
- data/webpack/JobWizard/validation.js +53 -0
- data/webpack/__mocks__/foremanReact/Root/Context/ForemanContext/index.js +2 -0
- data/webpack/__mocks__/foremanReact/common/I18n.js +2 -0
- data/webpack/__mocks__/foremanReact/components/AutoComplete/AutoCompleteActions.js +1 -0
- data/webpack/__mocks__/foremanReact/components/AutoComplete/AutoCompleteConstants.js +1 -0
- data/webpack/__mocks__/foremanReact/routes/RouterSelector.js +1 -0
- data/webpack/helpers.js +1 -0
- data/webpack/react_app/components/RecentJobsCard/RecentJobsCard.js +2 -1
- metadata +53 -7
- data/app/models/setting/remote_execution.rb +0 -88
- data/webpack/JobWizard/steps/AdvancedFields/__tests__/DescriptionField.test.js +0 -23
- data/webpack/JobWizard/steps/form/__tests__/Formatter.test.js.example +0 -76
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 06e8f7e73f607c4b3d5e465f6eb095c8f0352377f1237e32385e1a4dd3d64db4
|
|
4
|
+
data.tar.gz: 6e2a1f2cf43e68d8442a6de224333cbb3712351e8439d25a211505091f16d60e
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: c7d4f6804de17435657aab34bf6801a06433eae56e766913503cdfcdae7f0cdc330afd68c46de54b4ef6bc8d617df7f757b2420c92e167c44a94aafaab229f6d
|
|
7
|
+
data.tar.gz: dc42410db0ffe4e28009b370c677b4c37ae4ceccdbc2565f85f920e662d75bea49e2152ebd7385f5eaf4411289afea165d73946750b7a9ab732de1215bfc9615
|
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 <
|
|
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
|
|
27
|
-
|
|
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,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
|
|
@@ -8,7 +8,7 @@ module ForemanRemoteExecution
|
|
|
8
8
|
|
|
9
9
|
def multiple_actions
|
|
10
10
|
res = super
|
|
11
|
-
res += [ [_('Schedule Remote Job'), new_job_invocation_path, false] ] if
|
|
11
|
+
res += [ [_('Schedule Remote Job'), new_job_invocation_path, false] ] if can_schedule_jobs?
|
|
12
12
|
res
|
|
13
13
|
end
|
|
14
14
|
|
|
@@ -22,20 +22,20 @@ module ForemanRemoteExecution
|
|
|
22
22
|
end
|
|
23
23
|
end
|
|
24
24
|
|
|
25
|
-
def rex_host_features(*
|
|
26
|
-
return unless
|
|
25
|
+
def rex_host_features(host, *_rest)
|
|
26
|
+
return [] unless can_execute_on_host?(host)
|
|
27
27
|
RemoteExecutionFeature.with_host_action_button.order(:label).map do |feature|
|
|
28
|
-
link_to(_('%s') % feature.name, job_invocations_path(:host_ids => [
|
|
28
|
+
link_to(_('%s') % feature.name, job_invocations_path(:host_ids => [host.id], :feature => feature.label), :method => :post)
|
|
29
29
|
end
|
|
30
30
|
end
|
|
31
31
|
|
|
32
|
-
def schedule_job_button(*
|
|
33
|
-
return unless
|
|
34
|
-
link_to(_('Schedule Remote Job'), new_job_invocation_path(:host_ids => [
|
|
32
|
+
def schedule_job_button(host, *_rest)
|
|
33
|
+
return unless can_execute_on_host?(host)
|
|
34
|
+
link_to(_('Schedule Remote Job'), new_job_invocation_path(:host_ids => [host.id]), :id => :run_button, :class => 'btn btn-default')
|
|
35
35
|
end
|
|
36
36
|
|
|
37
37
|
def web_console_button(host, *args)
|
|
38
|
-
return
|
|
38
|
+
return if !authorized_for(permission: 'cockpit_hosts', auth_object: host) || !can_execute_on_infrastructure_host?(host)
|
|
39
39
|
|
|
40
40
|
url = SSHExecutionProvider.cockpit_url_for_host(host.name)
|
|
41
41
|
url ? link_to(_('Web Console'), url, :class => 'btn btn-default', :id => :'web-console-button', :target => '_new') : nil
|
|
@@ -46,6 +46,17 @@ module ForemanRemoteExecution
|
|
|
46
46
|
button_group(web_console_button(*args)))
|
|
47
47
|
super(*args)
|
|
48
48
|
end
|
|
49
|
-
end
|
|
50
49
|
|
|
50
|
+
def can_schedule_jobs?
|
|
51
|
+
authorized_for(controller: :job_invocations, action: :create)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def can_execute_on_host?(host)
|
|
55
|
+
can_schedule_jobs? && can_execute_on_infrastructure_host?(host)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def can_execute_on_infrastructure_host?(host)
|
|
59
|
+
!host.infrastructure_host? || User.current.can?(:execute_jobs_on_infrastructure_hosts)
|
|
60
|
+
end
|
|
61
|
+
end
|
|
51
62
|
end
|
|
@@ -31,7 +31,7 @@ module RemoteExecutionHelper
|
|
|
31
31
|
|
|
32
32
|
if authorized_for(hash_for_host_path(host).merge(auth_object: host, permission: :view_hosts, authorizer: job_hosts_authorizer))
|
|
33
33
|
links << { title: _('Host detail'),
|
|
34
|
-
action: { href:
|
|
34
|
+
action: { href: current_host_details_path(host), 'data-method': 'get', id: "#{host.name}-actions-detail" } }
|
|
35
35
|
end
|
|
36
36
|
|
|
37
37
|
if authorized_for(hash_for_rerun_job_invocation_path(id: job_invocation, host_ids: [ host.id ], authorizer: job_hosts_authorizer))
|
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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'
|
|
@@ -113,6 +121,10 @@ module ForemanRemoteExecution
|
|
|
113
121
|
@cached_rex_host_params_hash = nil
|
|
114
122
|
end
|
|
115
123
|
|
|
124
|
+
def infrastructure_host?
|
|
125
|
+
infrastructure_facet&.foreman_instance || infrastructure_facet&.smart_proxy_id
|
|
126
|
+
end
|
|
127
|
+
|
|
116
128
|
private
|
|
117
129
|
|
|
118
130
|
def extend_host_params_hash(params)
|
|
@@ -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
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
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],
|
|
@@ -520,7 +528,7 @@ class JobInvocationComposer
|
|
|
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
|
|
|
@@ -636,7 +644,7 @@ class JobInvocationComposer
|
|
|
636
644
|
setting_value = Setting['remote_execution_form_job_template']
|
|
637
645
|
return default_value unless setting_value
|
|
638
646
|
|
|
639
|
-
form_template = JobTemplate.find_by :name => setting_value
|
|
647
|
+
form_template = JobTemplate.authorized(:view_job_templates).find_by :name => setting_value
|
|
640
648
|
return default_value unless form_template
|
|
641
649
|
|
|
642
650
|
if block_given?
|
|
@@ -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
|
|
|
@@ -89,7 +96,8 @@ class RemoteExecutionProvider
|
|
|
89
96
|
end
|
|
90
97
|
|
|
91
98
|
def host_setting(host, setting)
|
|
92
|
-
host.host_param(setting.to_s)
|
|
99
|
+
param_value = host.host_param(setting.to_s)
|
|
100
|
+
param_value.nil? ? Setting[setting] : param_value
|
|
93
101
|
end
|
|
94
102
|
|
|
95
103
|
def ssh_password(_host)
|
|
@@ -114,6 +122,10 @@ class RemoteExecutionProvider
|
|
|
114
122
|
'Proxy::RemoteExecution::Ssh::Actions::RunScript'
|
|
115
123
|
end
|
|
116
124
|
|
|
125
|
+
def proxy_batch_size
|
|
126
|
+
Setting['foreman_tasks_proxy_batch_size']
|
|
127
|
+
end
|
|
128
|
+
|
|
117
129
|
# Return a specific proxy selector to use for running a given template
|
|
118
130
|
# Returns either nil to use the default selector or an instance of a (sub)class of ::ForemanTasks::ProxySelector
|
|
119
131
|
def required_proxy_selector_for(template)
|
|
@@ -123,5 +135,9 @@ class RemoteExecutionProvider
|
|
|
123
135
|
::DefaultProxyProxySelector.new
|
|
124
136
|
end
|
|
125
137
|
end
|
|
138
|
+
|
|
139
|
+
def alternative_names(host)
|
|
140
|
+
{ :fqdn => find_fqdn(effective_interfaces(host)) }
|
|
141
|
+
end
|
|
126
142
|
end
|
|
127
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
|
data/app/models/targeting.rb
CHANGED
|
@@ -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,8 +66,8 @@ 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)
|
|
70
|
-
"name ^ (#{hosts.join(', ')})"
|
|
69
|
+
hosts = Host.execution_scope.where(:id => ids).distinct.pluck(:name)
|
|
70
|
+
hosts.any? ? "name ^ (#{hosts.join(', ')})" : ''
|
|
71
71
|
end
|
|
72
72
|
|
|
73
73
|
def resolved?
|
|
@@ -20,7 +20,8 @@ module UINotifications
|
|
|
20
20
|
end
|
|
21
21
|
|
|
22
22
|
def blueprint
|
|
23
|
-
|
|
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 %>
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
<% if @hosts.any? -%>
|
|
8
8
|
<ul>
|
|
9
9
|
<% @hosts.each do |host| -%>
|
|
10
|
-
<li><%= link_to h(host.name),
|
|
10
|
+
<li><%= link_to h(host.name), current_host_details_path(host), :target => '_blank' %></li>
|
|
11
11
|
<% end -%>
|
|
12
12
|
|
|
13
13
|
<% if @additional > 0 -%>
|