foreman_remote_execution 4.6.0 → 5.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.github/workflows/ruby_ci.yml +7 -0
- data/.rubocop_todo.yml +1 -0
- data/app/controllers/api/v2/job_invocations_controller.rb +16 -1
- data/app/controllers/job_invocations_controller.rb +1 -1
- data/app/controllers/ui_job_wizard_controller.rb +21 -2
- data/app/graphql/mutations/job_invocations/create.rb +43 -0
- data/app/graphql/types/job_invocation.rb +16 -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 +5 -1
- data/app/helpers/remote_execution_helper.rb +9 -3
- data/app/lib/actions/remote_execution/run_host_job.rb +10 -1
- data/app/lib/actions/remote_execution/run_hosts_job.rb +58 -4
- data/app/mailers/rex_job_mailer.rb +15 -0
- data/app/models/concerns/foreman_remote_execution/host_extensions.rb +10 -0
- data/app/models/concerns/foreman_remote_execution/smart_proxy_extensions.rb +6 -0
- data/app/models/host_proxy_invocation.rb +4 -0
- data/app/models/host_status/execution_status.rb +3 -3
- data/app/models/job_invocation.rb +12 -5
- data/app/models/job_invocation_composer.rb +25 -17
- data/app/models/job_template.rb +1 -1
- data/app/models/remote_execution_feature.rb +5 -1
- data/app/models/remote_execution_provider.rb +18 -2
- data/app/models/rex_mail_notification.rb +13 -0
- data/app/models/targeting.rb +7 -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/index.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 +2 -1
- data/app/views/templates/ssh/module_action.erb +1 -0
- data/app/views/templates/ssh/power_action.erb +2 -0
- data/app/views/templates/ssh/puppet_run_once.erb +1 -0
- data/config/routes.rb +1 -0
- data/db/migrate/2021051713291621250977_add_host_proxy_invocations.rb +12 -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 +2 -3
- data/lib/foreman_remote_execution/engine.rb +114 -8
- 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/graphql/queries/job_invocation_query_test.rb +31 -0
- data/test/graphql/queries/job_invocations_query_test.rb +35 -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 +40 -7
- data/test/unit/input_template_renderer_test.rb +1 -89
- data/test/unit/job_invocation_composer_test.rb +18 -18
- data/test/unit/job_invocation_report_template_test.rb +16 -13
- data/test/unit/job_invocation_test.rb +1 -1
- 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 +158 -24
- data/webpack/JobWizard/JobWizard.scss +93 -1
- data/webpack/JobWizard/JobWizardConstants.js +54 -0
- data/webpack/JobWizard/JobWizardSelectors.js +41 -0
- data/webpack/JobWizard/__tests__/fixtures.js +188 -3
- data/webpack/JobWizard/__tests__/integration.test.js +41 -106
- 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 +41 -10
- data/webpack/JobWizard/steps/AdvancedFields/DescriptionField.js +90 -0
- data/webpack/JobWizard/steps/AdvancedFields/Fields.js +116 -55
- data/webpack/JobWizard/steps/AdvancedFields/__tests__/AdvancedFields.test.js +354 -16
- data/webpack/JobWizard/steps/AdvancedFields/__tests__/__snapshots__/AdvancedFields.test.js.snap +79 -246
- data/webpack/JobWizard/steps/CategoryAndTemplate/CategoryAndTemplate.js +5 -2
- data/webpack/JobWizard/steps/CategoryAndTemplate/CategoryAndTemplate.test.js +123 -51
- 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 +51 -0
- 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 +125 -0
- data/webpack/JobWizard/steps/Schedule/RepeatWeek.js +70 -0
- data/webpack/JobWizard/steps/Schedule/ScheduleType.js +28 -0
- data/webpack/JobWizard/steps/Schedule/StartEndDates.js +106 -0
- data/webpack/JobWizard/steps/Schedule/__tests__/Schedule.test.js +402 -0
- data/webpack/JobWizard/steps/Schedule/__tests__/StartEndDates.test.js +32 -0
- data/webpack/JobWizard/steps/Schedule/index.js +178 -0
- data/webpack/JobWizard/steps/form/DateTimePicker.js +126 -0
- data/webpack/JobWizard/steps/form/FormHelpers.js +5 -0
- data/webpack/JobWizard/steps/form/Formatter.js +181 -0
- data/webpack/JobWizard/steps/form/NumberInput.js +36 -0
- 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 +28 -5
- 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/components/SearchBar.js +18 -1
- data/webpack/__mocks__/foremanReact/routes/RouterSelector.js +1 -0
- data/webpack/helpers.js +1 -0
- data/webpack/react_app/components/RecentJobsCard/JobStatusIcon.js +43 -0
- data/webpack/react_app/components/RecentJobsCard/RecentJobsCard.js +73 -66
- data/webpack/react_app/components/RecentJobsCard/RecentJobsTable.js +98 -0
- data/webpack/react_app/components/RecentJobsCard/constants.js +11 -0
- data/webpack/react_app/components/RecentJobsCard/styles.scss +11 -0
- data/webpack/react_app/components/TargetingHosts/__tests__/__snapshots__/TargetingHostsPage.test.js.snap +1 -0
- data/webpack/react_app/extend/fillRecentJobsCard.js +1 -1
- metadata +71 -16
- data/app/models/concerns/foreman_remote_execution/orchestration/ssh.rb +0 -70
- data/app/models/setting/remote_execution.rb +0 -88
- data/test/models/orchestration/ssh_test.rb +0 -56
- data/webpack/JobWizard/__tests__/JobWizard.test.js +0 -13
- data/webpack/JobWizard/__tests__/__snapshots__/JobWizard.test.js.snap +0 -32
- data/webpack/JobWizard/steps/CategoryAndTemplate/__snapshots__/CategoryAndTemplate.test.js.snap +0 -113
- data/webpack/JobWizard/steps/form/__tests__/GroupedSelectField.test.js +0 -38
- data/webpack/JobWizard/steps/form/__tests__/SelectField.test.js +0 -23
- data/webpack/JobWizard/steps/form/__tests__/__snapshots__/GroupedSelectField.test.js.snap +0 -37
- data/webpack/JobWizard/steps/form/__tests__/__snapshots__/SelectField.test.js.snap +0 -23
- 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:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: cd91f2539393453fe888022502baba8ad7597da50682a4d87a8cf78dcaaebbfa
|
|
4
|
+
data.tar.gz: 2c97670fa1b3555f9660ea8adee03e48ae83779aff3324d977eae65a70b86956
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 7fbc26b3be07128f7dd015db03b8e46d34faee8f13f8b7e7f3d6ff54f360afd70cdd6f4143862695342591be55cfed9a74f315ae77ef5231cd20bee5f9bf8565
|
|
7
|
+
data.tar.gz: 851d5fd53efb928e8a6ce15468af83bc992b98265bc289f23fed310834e1c83753113e981a673f89e3183efaa7d6f76ec270f06f1f41072f79c045947a78b6c8
|
|
@@ -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] ||= {}
|
|
@@ -67,7 +67,7 @@ class JobInvocationsController < ApplicationController
|
|
|
67
67
|
end
|
|
68
68
|
|
|
69
69
|
def index
|
|
70
|
-
@job_invocations = resource_base_search_and_page.
|
|
70
|
+
@job_invocations = resource_base_search_and_page.preload(:task, :targeting).order('job_invocations.id DESC')
|
|
71
71
|
end
|
|
72
72
|
|
|
73
73
|
# refreshes the form
|
|
@@ -1,25 +1,37 @@
|
|
|
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
|
|
12
13
|
job_template = JobTemplate.authorized.find(params[:id])
|
|
14
|
+
advanced_template_inputs, template_inputs = map_template_inputs(job_template.template_inputs_with_foreign).partition { |x| x["advanced"] }
|
|
13
15
|
render :json => {
|
|
14
16
|
:job_template => job_template,
|
|
15
17
|
:effective_user => job_template.effective_user,
|
|
18
|
+
:template_inputs => template_inputs,
|
|
19
|
+
:advanced_template_inputs => advanced_template_inputs,
|
|
16
20
|
}
|
|
17
21
|
end
|
|
18
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
|
+
|
|
19
27
|
def resource_name(nested_resource = nil)
|
|
20
28
|
nested_resource || 'job_template'
|
|
21
29
|
end
|
|
22
30
|
|
|
31
|
+
def with_katello
|
|
32
|
+
!!defined?(::Katello)
|
|
33
|
+
end
|
|
34
|
+
|
|
23
35
|
def resource_class
|
|
24
36
|
JobTemplate
|
|
25
37
|
end
|
|
@@ -27,4 +39,11 @@ class UiJobWizardController < ::Api::V2::BaseController
|
|
|
27
39
|
def action_permission
|
|
28
40
|
:view_job_templates
|
|
29
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
|
|
30
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
|
|
@@ -7,7 +7,9 @@ module ForemanRemoteExecution
|
|
|
7
7
|
end
|
|
8
8
|
|
|
9
9
|
def multiple_actions
|
|
10
|
-
|
|
10
|
+
res = super
|
|
11
|
+
res += [ [_('Schedule Remote Job'), new_job_invocation_path, false] ] if authorized_for(controller: :job_invocations, action: :new)
|
|
12
|
+
res
|
|
11
13
|
end
|
|
12
14
|
|
|
13
15
|
def schedule_job_multi_button(*args)
|
|
@@ -21,12 +23,14 @@ module ForemanRemoteExecution
|
|
|
21
23
|
end
|
|
22
24
|
|
|
23
25
|
def rex_host_features(*args)
|
|
26
|
+
return unless authorized_for(controller: :job_invocations, action: :create)
|
|
24
27
|
RemoteExecutionFeature.with_host_action_button.order(:label).map do |feature|
|
|
25
28
|
link_to(_('%s') % feature.name, job_invocations_path(:host_ids => [args.first.id], :feature => feature.label), :method => :post)
|
|
26
29
|
end
|
|
27
30
|
end
|
|
28
31
|
|
|
29
32
|
def schedule_job_button(*args)
|
|
33
|
+
return unless authorized_for(controller: :job_invocations, action: :new)
|
|
30
34
|
link_to(_('Schedule Remote Job'), new_job_invocation_path(:host_ids => [args.first.id]), :id => :run_button, :class => 'btn btn-default')
|
|
31
35
|
end
|
|
32
36
|
|
|
@@ -7,6 +7,10 @@ module RemoteExecutionHelper
|
|
|
7
7
|
@job_hosts_authorizer ||= Authorizer.new(User.current, :collection => @hosts)
|
|
8
8
|
end
|
|
9
9
|
|
|
10
|
+
def host_tasks_authorizer
|
|
11
|
+
@host_tasks_authorizer ||= Authorizer.new(User.current, :collection => @job_invocation.sub_tasks)
|
|
12
|
+
end
|
|
13
|
+
|
|
10
14
|
def host_counter(label, count)
|
|
11
15
|
content_tag(:div, :class => 'host_counter') do
|
|
12
16
|
content_tag(:div, label, :class => 'header') + content_tag(:div, count.to_s, :class => 'count')
|
|
@@ -36,7 +40,7 @@ module RemoteExecutionHelper
|
|
|
36
40
|
'data-method': 'get', id: "#{host.name}-actions-rerun" } }
|
|
37
41
|
end
|
|
38
42
|
|
|
39
|
-
if host_task.present? && authorized_for(hash_for_foreman_tasks_task_path(host_task).merge(auth_object: host_task, permission: :view_foreman_tasks))
|
|
43
|
+
if host_task.present? && authorized_for(hash_for_foreman_tasks_task_path(host_task).merge(auth_object: host_task, permission: :view_foreman_tasks, authorizer: host_tasks_authorizer))
|
|
40
44
|
links << { title: _('Host task'),
|
|
41
45
|
action: { href: foreman_tasks_task_path(host_task),
|
|
42
46
|
'data-method': 'get', id: "#{host.name}-actions-task" } }
|
|
@@ -109,12 +113,14 @@ module RemoteExecutionHelper
|
|
|
109
113
|
:class => 'btn btn-danger',
|
|
110
114
|
:title => _('Try to cancel the job on a host'),
|
|
111
115
|
:disabled => !task.cancellable?,
|
|
112
|
-
:method => :post
|
|
116
|
+
:method => :post,
|
|
117
|
+
:remote => true)
|
|
113
118
|
buttons << link_to(_('Abort Job'), abort_foreman_tasks_task_path(task),
|
|
114
119
|
:class => 'btn btn-danger',
|
|
115
120
|
:title => _('Try to abort the job on a host without waiting for its result'),
|
|
116
121
|
:disabled => !task.cancellable?,
|
|
117
|
-
:method => :post
|
|
122
|
+
:method => :post,
|
|
123
|
+
:remote => true)
|
|
118
124
|
end
|
|
119
125
|
buttons
|
|
120
126
|
end
|
|
@@ -47,12 +47,17 @@ module Actions
|
|
|
47
47
|
script = renderer.render
|
|
48
48
|
raise _('Failed rendering template: %s') % renderer.error_message unless script
|
|
49
49
|
|
|
50
|
+
first_execution = host.executed_through_proxies.where(:id => proxy.id).none?
|
|
51
|
+
host.executed_through_proxies << proxy if first_execution
|
|
52
|
+
|
|
50
53
|
additional_options = { :hostname => provider.find_ip_or_hostname(host),
|
|
51
54
|
:script => script,
|
|
52
55
|
:execution_timeout_interval => job_invocation.execution_timeout_interval,
|
|
53
56
|
:secrets => secrets(host, job_invocation, provider),
|
|
54
57
|
:use_batch_triggering => true,
|
|
55
|
-
:use_concurrency_control => options[:use_concurrency_control]
|
|
58
|
+
:use_concurrency_control => options[:use_concurrency_control],
|
|
59
|
+
:first_execution => first_execution,
|
|
60
|
+
:alternative_names => provider.alternative_names(host) }
|
|
56
61
|
action_options = provider.proxy_command_options(template_invocation, host)
|
|
57
62
|
.merge(additional_options)
|
|
58
63
|
|
|
@@ -191,6 +196,10 @@ module Actions
|
|
|
191
196
|
def verify_permissions(host, template_invocation)
|
|
192
197
|
raise _('User can not execute job on host %s') % host.name unless User.current.can?(:view_hosts, host)
|
|
193
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
|
|
194
203
|
|
|
195
204
|
# we don't want to load all template_invocations to verify so we construct Authorizer object manually and set
|
|
196
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,12 +106,13 @@ 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
|
|
|
73
113
|
def total_count
|
|
74
114
|
# For compatibility with already existing tasks
|
|
75
|
-
return output[:total_count] unless output.has_key?(:host_count) || task.pending?
|
|
115
|
+
return output[:total_count] || hosts.count unless output.has_key?(:host_count) || task.pending?
|
|
76
116
|
|
|
77
117
|
output[:host_count] || hosts.count
|
|
78
118
|
end
|
|
@@ -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
|
|
@@ -6,6 +6,8 @@ module ForemanRemoteExecution
|
|
|
6
6
|
has_many :template_invocations, :dependent => :destroy, :foreign_key => 'host_id'
|
|
7
7
|
has_one :execution_status_object, :class_name => 'HostStatus::ExecutionStatus', :foreign_key => 'host_id', :dependent => :destroy
|
|
8
8
|
has_many :run_host_job_tasks, :through => :template_invocations
|
|
9
|
+
has_many :host_proxy_invocations, :foreign_key => 'host_id', :dependent => :destroy
|
|
10
|
+
has_many :executed_through_proxies, :through => :host_proxy_invocations, :source => 'smart_proxy'
|
|
9
11
|
|
|
10
12
|
scoped_search :relation => :run_host_job_tasks, :on => :result, :rename => 'job_invocation.result',
|
|
11
13
|
:ext_method => :search_by_job_invocation,
|
|
@@ -18,6 +20,14 @@ module ForemanRemoteExecution
|
|
|
18
20
|
scoped_search :relation => :execution_status_object, :on => :status, :rename => :execution_status,
|
|
19
21
|
:complete_value => { :ok => HostStatus::ExecutionStatus::OK, :error => HostStatus::ExecutionStatus::ERROR }
|
|
20
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
|
+
|
|
21
31
|
def search_by_job_invocation(key, operator, value)
|
|
22
32
|
if key == 'job_invocation.result'
|
|
23
33
|
operator = operator == '=' ? 'IN' : 'NOT IN'
|
|
@@ -64,11 +64,11 @@ class HostStatus::ExecutionStatus < HostStatus::Status
|
|
|
64
64
|
|
|
65
65
|
case status
|
|
66
66
|
when OK
|
|
67
|
-
[ "foreman_tasks_tasks.state = 'stopped' AND result = 'success'" ]
|
|
67
|
+
[ "foreman_tasks_tasks.state = 'stopped' AND foreman_tasks_tasks.result = 'success'" ]
|
|
68
68
|
when CANCELLED
|
|
69
|
-
[ "foreman_tasks_tasks.state = 'stopped' AND result = 'cancelled'" ]
|
|
69
|
+
[ "foreman_tasks_tasks.state = 'stopped' AND foreman_tasks_tasks.result = 'cancelled'" ]
|
|
70
70
|
when ERROR
|
|
71
|
-
[ "foreman_tasks_tasks.state = 'stopped' AND (result = 'error' OR result = 'warning')" ]
|
|
71
|
+
[ "foreman_tasks_tasks.state = 'stopped' AND (foreman_tasks_tasks.result = 'error' OR foreman_tasks_tasks.result = 'warning')" ]
|
|
72
72
|
when QUEUED
|
|
73
73
|
[ "foreman_tasks_tasks.state = 'scheduled' OR foreman_tasks_tasks.state IS NULL" ]
|
|
74
74
|
when RUNNING
|
|
@@ -65,7 +65,7 @@ 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
|
-
|
|
68
|
+
scoped_search :relation => :recurring_logic, :on => 'purpose', :rename => 'recurring_logic.purpose'
|
|
69
69
|
|
|
70
70
|
scoped_search :relation => :recurring_logic, :on => 'id', :rename => 'recurring_logic.id'
|
|
71
71
|
|
|
@@ -75,6 +75,8 @@ class JobInvocation < ApplicationRecord
|
|
|
75
75
|
|
|
76
76
|
default_scope -> { order('job_invocations.id DESC') }
|
|
77
77
|
|
|
78
|
+
scope :latest_jobs, -> { unscoped.joins(:task).order('foreman_tasks_tasks.start_at DESC').authorized(:view_job_invocations).limit(5) }
|
|
79
|
+
|
|
78
80
|
validates_lengths_from_database :only => [:description]
|
|
79
81
|
|
|
80
82
|
attr_accessor :start_before, :description_format
|
|
@@ -99,7 +101,7 @@ class JobInvocation < ApplicationRecord
|
|
|
99
101
|
def self.search_by_status(key, operator, value)
|
|
100
102
|
conditions = HostStatus::ExecutionStatus::ExecutionTaskStatusMapper.sql_conditions_for(value)
|
|
101
103
|
conditions[0] = "NOT (#{conditions[0]})" if operator == '<>'
|
|
102
|
-
{ :conditions => sanitize_sql_for_conditions(conditions), :
|
|
104
|
+
{ :conditions => sanitize_sql_for_conditions(conditions), :joins => :task }
|
|
103
105
|
end
|
|
104
106
|
|
|
105
107
|
def self.search_by_recurring_logic(key, operator, value)
|
|
@@ -193,11 +195,16 @@ class JobInvocation < ApplicationRecord
|
|
|
193
195
|
end
|
|
194
196
|
|
|
195
197
|
def total_hosts_count
|
|
198
|
+
count = _('N/A')
|
|
199
|
+
|
|
196
200
|
if targeting.resolved?
|
|
197
|
-
task&.main_action
|
|
198
|
-
|
|
199
|
-
|
|
201
|
+
count = if task&.main_action.respond_to?(:total_count)
|
|
202
|
+
task.main_action.total_count
|
|
203
|
+
else
|
|
204
|
+
targeting.hosts.count
|
|
205
|
+
end
|
|
200
206
|
end
|
|
207
|
+
count
|
|
201
208
|
end
|
|
202
209
|
|
|
203
210
|
def pattern_template_invocation_for_host(host)
|