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
|
@@ -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],
|
|
@@ -209,7 +217,7 @@ class JobInvocationComposer
|
|
|
209
217
|
def format_datetime(datetime)
|
|
210
218
|
return datetime if datetime.blank?
|
|
211
219
|
|
|
212
|
-
Time.parse(datetime).strftime('%Y-%m-%d %H:%M')
|
|
220
|
+
Time.parse(datetime).utc.strftime('%Y-%m-%d %H:%M')
|
|
213
221
|
end
|
|
214
222
|
end
|
|
215
223
|
|
|
@@ -296,7 +304,7 @@ class JobInvocationComposer
|
|
|
296
304
|
attr_reader :feature_label, :feature, :provided_inputs
|
|
297
305
|
|
|
298
306
|
def initialize(feature_label, hosts, provided_inputs = {})
|
|
299
|
-
@feature = RemoteExecutionFeature.feature(feature_label)
|
|
307
|
+
@feature = RemoteExecutionFeature.feature!(feature_label)
|
|
300
308
|
@provided_inputs = provided_inputs
|
|
301
309
|
translator = HostIdsTranslator.new(hosts)
|
|
302
310
|
@host_bookmark = translator.bookmark
|
|
@@ -405,7 +413,7 @@ class JobInvocationComposer
|
|
|
405
413
|
job_invocation.effective_user_password = params[:effective_user_password]
|
|
406
414
|
|
|
407
415
|
if @reruns && job_invocation.targeting.static?
|
|
408
|
-
job_invocation.targeting.
|
|
416
|
+
job_invocation.targeting.assign_host_ids(JobInvocation.find(@reruns).targeting.host_ids)
|
|
409
417
|
job_invocation.targeting.mark_resolved!
|
|
410
418
|
end
|
|
411
419
|
|
|
@@ -513,14 +521,14 @@ class JobInvocationComposer
|
|
|
513
521
|
end
|
|
514
522
|
|
|
515
523
|
def available_bookmarks
|
|
516
|
-
Bookmark.
|
|
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
|
|
|
@@ -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?
|
data/app/models/job_template.rb
CHANGED
|
@@ -20,7 +20,11 @@ class RemoteExecutionFeature < ApplicationRecord
|
|
|
20
20
|
end
|
|
21
21
|
|
|
22
22
|
def self.feature(label)
|
|
23
|
-
self.find_by(label: label)
|
|
23
|
+
self.find_by(label: label)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def self.feature!(label)
|
|
27
|
+
feature(label) || raise(::Foreman::Exception.new(N_('Unknown remote execution feature %s'), label))
|
|
24
28
|
end
|
|
25
29
|
|
|
26
30
|
def self.register(label, name, options = {})
|
|
@@ -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,11 +44,15 @@ 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
|
+
self.assign_host_ids(host_ids)
|
|
50
|
+
self.save(:validate => false)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def assign_host_ids(host_ids)
|
|
49
54
|
# this can be optimized even more, by introducing bulk insert
|
|
50
55
|
self.targeting_hosts.build(host_ids.map { |id| { :host_id => id } })
|
|
51
|
-
self.save(:validate => false)
|
|
52
56
|
end
|
|
53
57
|
|
|
54
58
|
def dynamic?
|
|
@@ -62,7 +66,7 @@ class Targeting < ApplicationRecord
|
|
|
62
66
|
def self.build_query_from_hosts(ids)
|
|
63
67
|
return '' if ids.empty?
|
|
64
68
|
|
|
65
|
-
hosts = Host.where(:id => ids).distinct.pluck(:name)
|
|
69
|
+
hosts = Host.execution_scope.where(:id => ids).distinct.pluck(:name)
|
|
66
70
|
"name ^ (#{hosts.join(', ')})"
|
|
67
71
|
end
|
|
68
72
|
|
|
@@ -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 %>
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
<% @job_invocations.each do |invocation| %>
|
|
22
22
|
<tr>
|
|
23
23
|
<td class="text_warp"><%= link_to_if_authorized invocation_description(invocation), hash_for_job_invocation_path(invocation).merge(:auth_object => invocation, :permission => :view_job_invocations, :authorizer => authorizer) %></td>
|
|
24
|
-
<td><%= trunc_with_tooltip(invocation
|
|
24
|
+
<td><%= trunc_with_tooltip(invocation.targeting.search_query, 15) %></td>
|
|
25
25
|
<td><%= link_to_invocation_task_if_authorized(invocation) %></td>
|
|
26
26
|
<td><%= invocation_result(invocation, :success_count) %></td>
|
|
27
27
|
<td><%= invocation_result(invocation, :failed_count) %></td>
|
|
@@ -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
|
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
class AddHostProxyInvocations < ActiveRecord::Migration[6.0]
|
|
2
|
+
def change
|
|
3
|
+
# rubocop:disable Rails/CreateTableWithTimestamps
|
|
4
|
+
create_table :host_proxy_invocations do |t|
|
|
5
|
+
t.references :host, :null => false
|
|
6
|
+
t.references :smart_proxy, :null => false
|
|
7
|
+
end
|
|
8
|
+
# rubocop:enable Rails/CreateTableWithTimestamps
|
|
9
|
+
|
|
10
|
+
add_index :host_proxy_invocations, [:host_id, :smart_proxy_id], unique: true
|
|
11
|
+
end
|
|
12
|
+
end
|
|
@@ -13,6 +13,20 @@ blueprints = [
|
|
|
13
13
|
],
|
|
14
14
|
},
|
|
15
15
|
},
|
|
16
|
+
{
|
|
17
|
+
group: N_('Jobs'),
|
|
18
|
+
name: 'rex_job_failed',
|
|
19
|
+
message: N_("A job '%{subject}' has failed"),
|
|
20
|
+
level: 'error',
|
|
21
|
+
actions:
|
|
22
|
+
{
|
|
23
|
+
links:
|
|
24
|
+
[
|
|
25
|
+
path_method: :job_invocation_path,
|
|
26
|
+
title: N_('Job Details'),
|
|
27
|
+
],
|
|
28
|
+
},
|
|
29
|
+
},
|
|
16
30
|
]
|
|
17
31
|
|
|
18
32
|
blueprints.each { |blueprint| UINotifications::Seed.new(blueprint).configure }
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
N_('Remote execution job')
|
|
2
|
+
|
|
3
|
+
notifications = [
|
|
4
|
+
{
|
|
5
|
+
:name => 'remote_execution_job',
|
|
6
|
+
:description => N_('A notification when a job finishes'),
|
|
7
|
+
:mailer => 'RexJobMailer',
|
|
8
|
+
:method => 'job_finished',
|
|
9
|
+
:subscription_type => 'alert',
|
|
10
|
+
},
|
|
11
|
+
]
|
|
12
|
+
|
|
13
|
+
notifications.each do |notification|
|
|
14
|
+
if (mail = RexMailNotification.find_by(name: notification[:name]))
|
|
15
|
+
mail.attributes = notification
|
|
16
|
+
mail.save! if mail.changed?
|
|
17
|
+
else
|
|
18
|
+
created_notification = RexMailNotification.create(notification)
|
|
19
|
+
if created_notification.nil? || created_notification.errors.any?
|
|
20
|
+
raise ::Foreman::Exception.new(N_("Unable to create mail notification: %s"),
|
|
21
|
+
format_errors(created_notification))
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
@@ -16,8 +16,7 @@ Gem::Specification.new do |s|
|
|
|
16
16
|
'management functionality with remote management functionality.'
|
|
17
17
|
|
|
18
18
|
s.files = `git ls-files`.split("\n").reject do |file|
|
|
19
|
-
file.start_with?('scripts'
|
|
20
|
-
file == 'foreman_remote_execution_core.gemspec'
|
|
19
|
+
file.start_with?('scripts')
|
|
21
20
|
end
|
|
22
21
|
|
|
23
22
|
s.test_files = `git ls-files test`.split("\n")
|
|
@@ -25,7 +24,7 @@ Gem::Specification.new do |s|
|
|
|
25
24
|
|
|
26
25
|
s.add_dependency 'deface'
|
|
27
26
|
s.add_dependency 'dynflow', '>= 1.0.2', '< 2.0.0'
|
|
28
|
-
s.add_dependency 'foreman-tasks', '>=
|
|
27
|
+
s.add_dependency 'foreman-tasks', '>= 5.1.0'
|
|
29
28
|
|
|
30
29
|
s.add_development_dependency 'factory_bot_rails', '~> 4.8.0'
|
|
31
30
|
s.add_development_dependency 'rdoc'
|
|
@@ -19,10 +19,6 @@ module ForemanRemoteExecution
|
|
|
19
19
|
end
|
|
20
20
|
assets_to_precompile += %w(foreman_remote_execution/foreman_remote_execution.css)
|
|
21
21
|
|
|
22
|
-
initializer 'foreman_remote_execution.load_default_settings', :before => :load_config_initializers do
|
|
23
|
-
require_dependency File.expand_path('../../../app/models/setting/remote_execution.rb', __FILE__) if (Setting.table_exists? rescue(false))
|
|
24
|
-
end
|
|
25
|
-
|
|
26
22
|
# Add any db migrations
|
|
27
23
|
initializer 'foreman_remote_execution.load_app_instance_data' do |app|
|
|
28
24
|
ForemanRemoteExecution::Engine.paths['db/migrate'].existent.each do |path|
|
|
@@ -51,7 +47,7 @@ module ForemanRemoteExecution
|
|
|
51
47
|
|
|
52
48
|
initializer 'foreman_remote_execution.register_plugin', before: :finisher_hook do |_app|
|
|
53
49
|
Foreman::Plugin.register :foreman_remote_execution do
|
|
54
|
-
requires_foreman '>=
|
|
50
|
+
requires_foreman '>= 3.1'
|
|
55
51
|
register_global_js_file 'global'
|
|
56
52
|
|
|
57
53
|
apipie_documented_controllers ["#{ForemanRemoteExecution::Engine.root}/app/controllers/api/v2/*.rb"]
|
|
@@ -61,13 +57,114 @@ module ForemanRemoteExecution
|
|
|
61
57
|
automatic_assets(false)
|
|
62
58
|
precompile_assets(*assets_to_precompile)
|
|
63
59
|
|
|
60
|
+
# Add settings to a Remote Execution category
|
|
61
|
+
settings do
|
|
62
|
+
category :remote_execution, N_('Remote Execution') do
|
|
63
|
+
setting 'remote_execution_fallback_proxy',
|
|
64
|
+
type: :boolean,
|
|
65
|
+
description: N_('Search the host for any proxy with Remote Execution, useful when the host has no subnet or the subnet does not have an execution proxy'),
|
|
66
|
+
default: false,
|
|
67
|
+
full_name: N_('Fallback to Any Proxy')
|
|
68
|
+
setting 'remote_execution_global_proxy',
|
|
69
|
+
type: :boolean,
|
|
70
|
+
description: N_('Search for remote execution proxy outside of the proxies assigned to the host. The search will be limited to the host\'s organization and location.'),
|
|
71
|
+
default: true,
|
|
72
|
+
full_name: N_('Enable Global Proxy')
|
|
73
|
+
setting 'remote_execution_ssh_user',
|
|
74
|
+
type: :string,
|
|
75
|
+
description: N_('Default user to use for SSH. You may override per host by setting a parameter called remote_execution_ssh_user.'),
|
|
76
|
+
default: 'root',
|
|
77
|
+
full_name: N_('SSH User')
|
|
78
|
+
setting 'remote_execution_effective_user',
|
|
79
|
+
type: :string,
|
|
80
|
+
description: N_('Default user to use for executing the script. If the user differs from the SSH user, su or sudo is used to switch the user.'),
|
|
81
|
+
default: 'root',
|
|
82
|
+
full_name: N_('Efffective User')
|
|
83
|
+
setting 'remote_execution_effective_user_method',
|
|
84
|
+
type: :string,
|
|
85
|
+
description: N_('What command should be used to switch to the effective user. One of %s') % SSHExecutionProvider::EFFECTIVE_USER_METHODS.inspect,
|
|
86
|
+
default: 'sudo',
|
|
87
|
+
full_name: N_('Effective User Method'),
|
|
88
|
+
collection: proc { Hash[SSHExecutionProvider::EFFECTIVE_USER_METHODS.map { |method| [method, method] }] }
|
|
89
|
+
setting 'remote_execution_effective_user_password',
|
|
90
|
+
type: :string,
|
|
91
|
+
description: N_('Effective user password'),
|
|
92
|
+
default: '',
|
|
93
|
+
full_name: N_('Effective user password'),
|
|
94
|
+
encrypted: true
|
|
95
|
+
setting 'remote_execution_sync_templates',
|
|
96
|
+
type: :boolean,
|
|
97
|
+
description: N_('Whether we should sync templates from disk when running db:seed.'),
|
|
98
|
+
default: true,
|
|
99
|
+
full_name: N_('Sync Job Templates')
|
|
100
|
+
setting 'remote_execution_ssh_port',
|
|
101
|
+
type: :integer,
|
|
102
|
+
description: N_('Port to use for SSH communication. Default port 22. You may override per host by setting a parameter called remote_execution_ssh_port.'),
|
|
103
|
+
default: 22,
|
|
104
|
+
full_name: N_('SSH Port')
|
|
105
|
+
setting 'remote_execution_connect_by_ip',
|
|
106
|
+
type: :boolean,
|
|
107
|
+
description: N_('Should the ip addresses on host interfaces be preferred over the fqdn? '\
|
|
108
|
+
'It is useful when DNS not resolving the fqdns properly. You may override this per host by setting a parameter called remote_execution_connect_by_ip. '\
|
|
109
|
+
'For dual-stacked hosts you should consider the remote_execution_connect_by_ip_prefer_ipv6 setting'),
|
|
110
|
+
default: false,
|
|
111
|
+
full_name: N_('Connect by IP')
|
|
112
|
+
setting 'remote_execution_connect_by_ip_prefer_ipv6',
|
|
113
|
+
type: :boolean,
|
|
114
|
+
description: N_('When connecting using ip address, should the IPv6 addresses be preferred? '\
|
|
115
|
+
'If no IPv6 address is set, it falls back to IPv4 automatically. You may override this per host by setting a parameter called remote_execution_connect_by_ip_prefer_ipv6. '\
|
|
116
|
+
'By default and for compatibility, IPv4 will be preferred over IPv6 by default'),
|
|
117
|
+
default: false,
|
|
118
|
+
full_name: N_('Prefer IPv6 over IPv4')
|
|
119
|
+
setting 'remote_execution_ssh_password',
|
|
120
|
+
type: :string,
|
|
121
|
+
description: N_('Default password to use for SSH. You may override per host by setting a parameter called remote_execution_ssh_password'),
|
|
122
|
+
default: nil,
|
|
123
|
+
full_name: N_('Default SSH password'),
|
|
124
|
+
encrypted: true
|
|
125
|
+
setting 'remote_execution_ssh_key_passphrase',
|
|
126
|
+
type: :string,
|
|
127
|
+
description: N_('Default key passphrase to use for SSH. You may override per host by setting a parameter called remote_execution_ssh_key_passphrase'),
|
|
128
|
+
default: nil,
|
|
129
|
+
full_name: N_('Default SSH key passphrase'),
|
|
130
|
+
encrypted: true
|
|
131
|
+
setting 'remote_execution_workers_pool_size',
|
|
132
|
+
type: :integer,
|
|
133
|
+
description: N_('Amount of workers in the pool to handle the execution of the remote execution jobs. Restart of the dynflowd/foreman-tasks service is required.'),
|
|
134
|
+
default: 5,
|
|
135
|
+
full_name: N_('Workers pool size')
|
|
136
|
+
setting 'remote_execution_cleanup_working_dirs',
|
|
137
|
+
type: :boolean,
|
|
138
|
+
description: N_('When enabled, working directories will be removed after task completion. You may override this per host by setting a parameter called remote_execution_cleanup_working_dirs.'),
|
|
139
|
+
default: true,
|
|
140
|
+
full_name: N_('Cleanup working directories')
|
|
141
|
+
setting 'remote_execution_cockpit_url',
|
|
142
|
+
type: :string,
|
|
143
|
+
description: N_('Where to find the Cockpit instance for the Web Console button. By default, no button is shown.'),
|
|
144
|
+
default: nil,
|
|
145
|
+
full_name: N_('Cockpit URL')
|
|
146
|
+
setting 'remote_execution_form_job_template',
|
|
147
|
+
type: :string,
|
|
148
|
+
description: N_('Choose a job template that is pre-selected in job invocation form'),
|
|
149
|
+
default: 'Run Command - SSH Default',
|
|
150
|
+
full_name: N_('Form Job Template'),
|
|
151
|
+
collection: proc { Hash[JobTemplate.unscoped.map { |template| [template.name, template.name] }] }
|
|
152
|
+
setting 'remote_execution_job_invocation_report_template',
|
|
153
|
+
type: :string,
|
|
154
|
+
description: N_('Select a report template used for generating a report for a particular remote execution job'),
|
|
155
|
+
default: 'Jobs - Invocation report template',
|
|
156
|
+
full_name: N_('Job Invocation Report Template'),
|
|
157
|
+
collection: proc { ForemanRemoteExecution.job_invocation_report_templates_select }
|
|
158
|
+
end
|
|
159
|
+
end
|
|
160
|
+
|
|
64
161
|
# Add permissions
|
|
65
162
|
security_block :foreman_remote_execution do
|
|
66
163
|
permission :view_job_templates, { :job_templates => [:index, :show, :revision, :auto_complete_search, :auto_complete_job_category, :preview, :export],
|
|
67
164
|
:'api/v2/job_templates' => [:index, :show, :revision, :export],
|
|
68
165
|
:'api/v2/template_inputs' => [:index, :show],
|
|
69
166
|
:'api/v2/foreign_input_sets' => [:index, :show],
|
|
70
|
-
:ui_job_wizard => [:categories, :template]}, :resource_type => 'JobTemplate'
|
|
167
|
+
:ui_job_wizard => [:categories, :template, :resources]}, :resource_type => 'JobTemplate'
|
|
71
168
|
permission :create_job_templates, { :job_templates => [:new, :create, :clone_template, :import],
|
|
72
169
|
:'api/v2/job_templates' => [:create, :clone, :import] }, :resource_type => 'JobTemplate'
|
|
73
170
|
permission :edit_job_templates, { :job_templates => [:edit, :update],
|
|
@@ -86,6 +183,7 @@ module ForemanRemoteExecution
|
|
|
86
183
|
permission :view_template_invocations, { :template_invocations => [:show],
|
|
87
184
|
'api/v2/template_invocations' => [:template_invocations] }, :resource_type => 'TemplateInvocation'
|
|
88
185
|
permission :create_template_invocations, {}, :resource_type => 'TemplateInvocation'
|
|
186
|
+
permission :execute_jobs_on_infrastructure_hosts, {}, :resource_type => 'JobInvocation'
|
|
89
187
|
permission :cancel_job_invocations, { :job_invocations => [:cancel], 'api/v2/job_invocations' => [:cancel] }, :resource_type => 'JobInvocation'
|
|
90
188
|
# this permissions grants user to get auto completion hints when setting up filters
|
|
91
189
|
permission :filter_autocompletion_for_template_invocation, { :template_invocations => [ :auto_complete_search, :index ] },
|
|
@@ -146,12 +244,18 @@ module ForemanRemoteExecution
|
|
|
146
244
|
register_custom_status HostStatus::ExecutionStatus
|
|
147
245
|
# add dashboard widget
|
|
148
246
|
# widget 'foreman_remote_execution_widget', name: N_('Foreman plugin template widget'), sizex: 4, sizey: 1
|
|
247
|
+
widget 'dashboard/latest-jobs', :name => N_('Latest Jobs'), :sizex => 6, :sizey => 1
|
|
149
248
|
|
|
150
249
|
parameter_filter Subnet, :remote_execution_proxies, :remote_execution_proxy_ids => []
|
|
151
250
|
parameter_filter Nic::Interface do |ctx|
|
|
152
251
|
ctx.permit :execution
|
|
153
252
|
end
|
|
154
253
|
|
|
254
|
+
register_graphql_query_field :job_invocations, '::Types::JobInvocation', :collection_field
|
|
255
|
+
register_graphql_query_field :job_invocation, '::Types::JobInvocation', :record_field
|
|
256
|
+
|
|
257
|
+
register_graphql_mutation_field :create_job_invocation, ::Mutations::JobInvocations::Create
|
|
258
|
+
|
|
155
259
|
extend_template_helpers ForemanRemoteExecution::RendererMethods
|
|
156
260
|
|
|
157
261
|
extend_rabl_template 'api/v2/smart_proxies/main', 'api/v2/smart_proxies/pubkey'
|
|
@@ -201,12 +305,10 @@ module ForemanRemoteExecution
|
|
|
201
305
|
|
|
202
306
|
Host::Managed.prepend ForemanRemoteExecution::HostExtensions
|
|
203
307
|
Host::Managed.include ForemanTasks::Concerns::HostActionSubject
|
|
204
|
-
Host::Managed.include ForemanRemoteExecution::Orchestration::SSH
|
|
205
308
|
|
|
206
309
|
(Nic::Base.descendants + [Nic::Base]).each do |klass|
|
|
207
310
|
klass.send(:include, ForemanRemoteExecution::NicExtensions)
|
|
208
311
|
end
|
|
209
|
-
Nic::Managed.include ForemanRemoteExecution::Orchestration::SSH
|
|
210
312
|
|
|
211
313
|
Bookmark.include ForemanRemoteExecution::BookmarkExtensions
|
|
212
314
|
HostsHelper.prepend ForemanRemoteExecution::HostsHelperExtensions
|
|
@@ -239,6 +341,10 @@ module ForemanRemoteExecution
|
|
|
239
341
|
end
|
|
240
342
|
end
|
|
241
343
|
|
|
344
|
+
def self.job_invocation_report_templates_select
|
|
345
|
+
Hash[ReportTemplate.unscoped.joins(:template_inputs).where(template_inputs: TemplateInput.where(name: 'job_id')).map { |template| [template.name, template.name] }]
|
|
346
|
+
end
|
|
347
|
+
|
|
242
348
|
def self.register_rex_feature
|
|
243
349
|
RemoteExecutionFeature.register(
|
|
244
350
|
:puppet_run_host,
|