foreman_remote_execution 1.5.1 → 1.5.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 +5 -5
- data/app/controllers/api/v2/job_invocations_controller.rb +1 -1
- data/app/helpers/remote_execution_helper.rb +2 -0
- data/app/lib/actions/remote_execution/run_host_job.rb +14 -1
- data/app/lib/actions/remote_execution/run_hosts_job.rb +6 -2
- data/app/models/concerns/foreman_remote_execution/host_extensions.rb +7 -3
- data/app/models/host_status/execution_status.rb +5 -1
- data/app/models/job_invocation.rb +4 -3
- data/app/models/job_invocation_composer.rb +2 -0
- data/app/models/job_template.rb +21 -25
- data/app/models/remote_execution_provider.rb +10 -1
- data/app/models/setting/remote_execution.rb +13 -4
- data/app/models/ssh_execution_provider.rb +3 -1
- data/app/views/job_invocations/_form.html.erb +1 -0
- data/app/views/job_invocations/show.html.erb +3 -1
- data/app/views/job_templates/edit.html.erb +13 -0
- data/app/views/job_templates/index.html.erb +1 -1
- data/db/migrate/20180411160809_add_sudo_password_to_job_invocation.rb +5 -0
- data/foreman_remote_execution.gemspec +2 -2
- data/lib/foreman_remote_execution/engine.rb +3 -0
- data/lib/foreman_remote_execution/version.rb +1 -1
- data/test/functional/api/v2/job_invocations_controller_test.rb +1 -1
- data/test/unit/actions/run_host_job_test.rb +63 -0
- data/test/unit/actions/run_hosts_job_test.rb +1 -0
- data/test/unit/concerns/host_extensions_test.rb +4 -4
- data/test/unit/job_invocation_composer_test.rb +12 -0
- data/test/unit/remote_execution_provider_test.rb +28 -3
- metadata +10 -10
- data/app/models/job_template_importer.rb +0 -36
- data/test/unit/job_template_importer_test.rb +0 -66
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
|
-
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: cee9f09b684c8a829284e5eef226b75d241e91a3622fbad2f1626ff75855c812
|
4
|
+
data.tar.gz: 160de6767048860228ab7007394d548d937d757d8f19f3cd401e1257b96950ad
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: c4c7c5c3a2d9173defa65b8a9fa81de541698345a623cf870f76f70a6c83792f8ecf9a70939f103fb78b0ef5fc4e8dd763bfd541df46f965bfbe532aa1cee9e8
|
7
|
+
data.tar.gz: 9311306fda1292ff48856e6f3c05808c61f89154f07491071f9e71bafbf089b13f18ef128749ad0c8726005d4f8d0658bc551ecacd3960d069fdce16387b485d
|
@@ -77,7 +77,7 @@ module Api
|
|
77
77
|
param :host_id, :identifier, :required => true
|
78
78
|
param :since, String, :required => false
|
79
79
|
def output
|
80
|
-
if @nested_obj.task.
|
80
|
+
if @nested_obj.task.scheduled?
|
81
81
|
render :json => { :refresh => true, :output => [], :delayed => true, :start_at => @nested_obj.task.start_at }
|
82
82
|
return
|
83
83
|
end
|
@@ -140,6 +140,8 @@ module RemoteExecutionHelper
|
|
140
140
|
options = { :unknown_string => 'N/A' }.merge(options)
|
141
141
|
if invocation.queued?
|
142
142
|
options[:unknown_string]
|
143
|
+
elsif options[:output_key] == :total_count
|
144
|
+
invocation.total_hosts_count
|
143
145
|
else
|
144
146
|
(invocation.task.try(:output) || {}).fetch(options[:output_key], options[:unknown_string])
|
145
147
|
end
|
@@ -7,6 +7,10 @@ module Actions
|
|
7
7
|
middleware.do_not_use Dynflow::Middleware::Common::Transaction
|
8
8
|
middleware.use Actions::Middleware::HideSecrets
|
9
9
|
|
10
|
+
def queue
|
11
|
+
ForemanRemoteExecution::DYNFLOW_QUEUE
|
12
|
+
end
|
13
|
+
|
10
14
|
def resource_locks
|
11
15
|
:link
|
12
16
|
end
|
@@ -35,7 +39,8 @@ module Actions
|
|
35
39
|
provider = template_invocation.template.provider
|
36
40
|
|
37
41
|
secrets = { :ssh_password => job_invocation.password || provider.ssh_password(host),
|
38
|
-
:key_passphrase => job_invocation.key_passphrase || provider.ssh_key_passphrase(host)
|
42
|
+
:key_passphrase => job_invocation.key_passphrase || provider.ssh_key_passphrase(host),
|
43
|
+
:sudo_password => job_invocation.sudo_password || provider.sudo_password(host) }
|
39
44
|
|
40
45
|
additional_options = { :hostname => provider.find_ip_or_hostname(host),
|
41
46
|
:script => script,
|
@@ -49,6 +54,7 @@ module Actions
|
|
49
54
|
end
|
50
55
|
|
51
56
|
def finalize(*args)
|
57
|
+
update_host_status
|
52
58
|
check_exit_status
|
53
59
|
end
|
54
60
|
|
@@ -112,6 +118,13 @@ module Actions
|
|
112
118
|
|
113
119
|
private
|
114
120
|
|
121
|
+
def update_host_status
|
122
|
+
host = Host.find(input[:host][:id])
|
123
|
+
status = (host.execution_status_object ||= HostStatus::ExecutionStatus.new)
|
124
|
+
status.status = exit_status.zero? ? HostStatus::ExecutionStatus::OK : HostStatus::ExecutionStatus::ERROR
|
125
|
+
status.save!
|
126
|
+
end
|
127
|
+
|
115
128
|
def delegated_output
|
116
129
|
if input[:delegated_action_id]
|
117
130
|
super
|
@@ -4,9 +4,13 @@ module Actions
|
|
4
4
|
|
5
5
|
include Dynflow::Action::WithBulkSubPlans
|
6
6
|
include Dynflow::Action::WithPollingSubPlans
|
7
|
+
include Actions::RecurringAction
|
7
8
|
|
8
9
|
middleware.use Actions::Middleware::BindJobInvocation
|
9
|
-
|
10
|
+
|
11
|
+
def queue
|
12
|
+
ForemanRemoteExecution::DYNFLOW_QUEUE
|
13
|
+
end
|
10
14
|
|
11
15
|
def delay(delay_options, job_invocation)
|
12
16
|
task.add_missing_task_groups(job_invocation.task_group)
|
@@ -38,7 +42,7 @@ module Actions
|
|
38
42
|
end
|
39
43
|
|
40
44
|
def finalize
|
41
|
-
job_invocation.password = job_invocation.key_passphrase = nil
|
45
|
+
job_invocation.password = job_invocation.key_passphrase = job_invocation.sudo_password = nil
|
42
46
|
job_invocation.save!
|
43
47
|
|
44
48
|
# creating the success notification should be the very last thing this tasks do
|
@@ -45,13 +45,17 @@ module ForemanRemoteExecution
|
|
45
45
|
@execution_status_label ||= get_status(HostStatus::ExecutionStatus).to_label(options)
|
46
46
|
end
|
47
47
|
|
48
|
-
def
|
48
|
+
def host_params_hash
|
49
49
|
params = super
|
50
50
|
keys = remote_execution_ssh_keys
|
51
|
-
|
51
|
+
source = 'global'
|
52
|
+
if keys.present?
|
53
|
+
params['remote_execution_ssh_keys'] = {:value => keys, :safe_value => keys, :source => source}
|
54
|
+
end
|
52
55
|
[:remote_execution_ssh_user, :remote_execution_effective_user_method,
|
53
56
|
:remote_execution_connect_by_ip].each do |key|
|
54
|
-
|
57
|
+
value = Setting[key]
|
58
|
+
params[key.to_s] = {:value => value, :safe_value => value, :source => source} unless params.key?(key.to_s)
|
55
59
|
end
|
56
60
|
params
|
57
61
|
end
|
@@ -15,7 +15,11 @@ class HostStatus::ExecutionStatus < HostStatus::Status
|
|
15
15
|
end
|
16
16
|
|
17
17
|
def to_status(options = {})
|
18
|
-
|
18
|
+
if self.new_record?
|
19
|
+
ExecutionTaskStatusMapper.new(last_stopped_task).status
|
20
|
+
else
|
21
|
+
self.status
|
22
|
+
end
|
19
23
|
end
|
20
24
|
|
21
25
|
def to_global(options = {})
|
@@ -1,9 +1,9 @@
|
|
1
1
|
class JobInvocation < ApplicationRecord
|
2
|
+
audited :except => [:task_id, :targeting_id, :task_group_id, :triggering_id]
|
3
|
+
|
2
4
|
include Authorizable
|
3
5
|
include Encryptable
|
4
6
|
|
5
|
-
audited :except => [ :task_id, :targeting_id, :task_group_id, :triggering_id ]
|
6
|
-
|
7
7
|
include ForemanRemoteExecution::ErrorsFlattener
|
8
8
|
FLATTENED_ERRORS_MAPPING = {
|
9
9
|
:pattern_template_invocations => lambda do |template_invocation|
|
@@ -68,7 +68,7 @@ class JobInvocation < ApplicationRecord
|
|
68
68
|
|
69
69
|
delegate :start_at, :to => :task, :allow_nil => true
|
70
70
|
|
71
|
-
encrypts :password, :key_passphrase
|
71
|
+
encrypts :password, :key_passphrase, :sudo_password
|
72
72
|
|
73
73
|
def self.search_by_status(key, operator, value)
|
74
74
|
conditions = HostStatus::ExecutionStatus::ExecutionTaskStatusMapper.sql_conditions_for(value)
|
@@ -137,6 +137,7 @@ class JobInvocation < ApplicationRecord
|
|
137
137
|
invocation.pattern_template_invocations = self.pattern_template_invocations.map(&:deep_clone)
|
138
138
|
invocation.password = self.password
|
139
139
|
invocation.key_passphrase = self.key_passphrase
|
140
|
+
invocation.sudo_password = self.sudo_password
|
140
141
|
end
|
141
142
|
end
|
142
143
|
|
@@ -15,6 +15,7 @@ class JobInvocationComposer
|
|
15
15
|
:description_format => job_invocation_base[:description_format],
|
16
16
|
:password => blank_to_nil(job_invocation_base[:password]),
|
17
17
|
:key_passphrase => blank_to_nil(job_invocation_base[:key_passphrase]),
|
18
|
+
:sudo_password => blank_to_nil(job_invocation_base[:sudo_password]),
|
18
19
|
:concurrency_control => concurrency_control_params,
|
19
20
|
:execution_timeout_interval => execution_timeout_interval,
|
20
21
|
:template_invocations => template_invocations_params }.with_indifferent_access
|
@@ -335,6 +336,7 @@ class JobInvocationComposer
|
|
335
336
|
job_invocation.execution_timeout_interval = params[:execution_timeout_interval]
|
336
337
|
job_invocation.password = params[:password]
|
337
338
|
job_invocation.key_passphrase = params[:key_passphrase]
|
339
|
+
job_invocation.sudo_password = params[:sudo_password]
|
338
340
|
|
339
341
|
self
|
340
342
|
end
|
data/app/models/job_template.rb
CHANGED
@@ -1,4 +1,5 @@
|
|
1
1
|
class JobTemplate < ::Template
|
2
|
+
audited
|
2
3
|
include ::Exportable
|
3
4
|
|
4
5
|
class NonUniqueInputsError < Foreman::Exception
|
@@ -12,7 +13,6 @@ class JobTemplate < ::Template
|
|
12
13
|
friendly_id :name
|
13
14
|
include Parameterizable::ByIdName
|
14
15
|
|
15
|
-
audited :allow_mass_assignment => true
|
16
16
|
has_many :audits, :as => :auditable, :class_name => Audited.audit_class.name, :dependent => :nullify
|
17
17
|
has_many :all_template_invocations, :dependent => :destroy, :foreign_key => 'template_id', :class_name => 'TemplateInvocation'
|
18
18
|
has_many :template_invocations, -> { where('host_id IS NOT NULL') }, :foreign_key => 'template_id'
|
@@ -66,7 +66,7 @@ class JobTemplate < ::Template
|
|
66
66
|
# Import a template from ERB, with YAML metadata in the first comment. It
|
67
67
|
# will overwrite (sync) an existing template if options[:update] is true.
|
68
68
|
def import_raw(contents, options = {})
|
69
|
-
metadata = parse_metadata(contents)
|
69
|
+
metadata = Template.parse_metadata(contents)
|
70
70
|
import_parsed(metadata['name'], contents, metadata, options)
|
71
71
|
end
|
72
72
|
|
@@ -76,34 +76,17 @@ class JobTemplate < ::Template
|
|
76
76
|
template
|
77
77
|
end
|
78
78
|
|
79
|
-
|
80
|
-
def import!(name, text, metadata, force = false)
|
81
|
-
metadata = metadata.dup
|
82
|
-
metadata.delete('associate')
|
83
|
-
JobTemplateImporter.import!(name, text, metadata)
|
84
|
-
end
|
85
|
-
|
86
|
-
# rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
|
87
|
-
def import_parsed(name, text, metadata, options = {})
|
79
|
+
def import_parsed(name, text, _metadata, options = {})
|
88
80
|
transaction do
|
89
|
-
return if metadata.blank? || metadata.delete('kind') != 'job_template' ||
|
90
|
-
(metadata.key?('model') && metadata.delete('model') != self.to_s)
|
91
|
-
metadata['name'] = name
|
92
81
|
# Don't look for existing if we should always create a new template
|
93
82
|
existing = self.find_by(:name => name) unless options.delete(:build_new)
|
94
83
|
# Don't update if the template already exists, unless we're told to
|
95
84
|
return if !options.delete(:update) && existing
|
96
85
|
|
97
|
-
template = existing || self.new
|
98
|
-
template.
|
99
|
-
template.sync_foreign_input_sets(metadata.delete('foreign_input_sets'))
|
100
|
-
template.sync_feature(metadata.delete('feature'))
|
101
|
-
template.locked = false if options.delete(:force)
|
102
|
-
template.assign_attributes(metadata.merge(:template => text.gsub(/<%\#.+?.-?%>\n?/m, '').strip).merge(options))
|
103
|
-
template.assign_taxonomies if template.new_record?
|
86
|
+
template = existing || self.new(:name => name)
|
87
|
+
template.import_without_save(text, options)
|
104
88
|
template
|
105
89
|
end
|
106
|
-
# rubocop:enable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
|
107
90
|
end
|
108
91
|
end
|
109
92
|
|
@@ -215,9 +198,22 @@ class JobTemplate < ::Template
|
|
215
198
|
end
|
216
199
|
end
|
217
200
|
|
218
|
-
def
|
219
|
-
|
220
|
-
|
201
|
+
def import_custom_data(options)
|
202
|
+
sync_inputs(@importing_metadata['template_inputs'])
|
203
|
+
sync_foreign_input_sets(@importing_metadata['foreign_input_sets'])
|
204
|
+
sync_feature(@importing_metadata['feature'])
|
205
|
+
|
206
|
+
%w(job_category description_format provider_type).each do |attribute|
|
207
|
+
value = @importing_metadata[attribute]
|
208
|
+
self.public_send "#{attribute}=", value if @importing_metadata.key?(attribute)
|
209
|
+
end
|
210
|
+
|
211
|
+
# this should be moved to core but meanwhile we support default attribute here
|
212
|
+
# see http://projects.theforeman.org/issues/23426 for more details
|
213
|
+
self.default = options[:default] unless options[:default].nil?
|
214
|
+
|
215
|
+
# job templates have too long metadata, we remove them on parsing until it's stored in separate attribute
|
216
|
+
self.template = self.template.gsub(/<%\#.+?.-?%>\n?/m, '').strip
|
221
217
|
end
|
222
218
|
|
223
219
|
private
|
@@ -44,6 +44,15 @@ class RemoteExecutionProvider
|
|
44
44
|
method
|
45
45
|
end
|
46
46
|
|
47
|
+
def cleanup_working_dirs?(host)
|
48
|
+
setting = host_setting(host, :remote_execution_cleanup_working_dirs)
|
49
|
+
[true, 'true', 'True', 'TRUE', '1'].include?(setting)
|
50
|
+
end
|
51
|
+
|
52
|
+
def sudo_password(host)
|
53
|
+
host_setting(host, :remote_execution_sudo_password)
|
54
|
+
end
|
55
|
+
|
47
56
|
def effective_interfaces(host)
|
48
57
|
interfaces = []
|
49
58
|
%w(execution primary provision).map do |flag|
|
@@ -70,7 +79,7 @@ class RemoteExecutionProvider
|
|
70
79
|
end
|
71
80
|
|
72
81
|
def host_setting(host, setting)
|
73
|
-
host.
|
82
|
+
host.host_param(setting.to_s) || Setting[setting]
|
74
83
|
end
|
75
84
|
|
76
85
|
def ssh_password(_host) end
|
@@ -1,8 +1,8 @@
|
|
1
1
|
class Setting::RemoteExecution < Setting
|
2
2
|
|
3
|
-
::Setting::BLANK_ATTRS.concat %w{remote_execution_ssh_password remote_execution_ssh_key_passphrase}
|
3
|
+
::Setting::BLANK_ATTRS.concat %w{remote_execution_ssh_password remote_execution_ssh_key_passphrase remote_execution_sudo_password}
|
4
4
|
|
5
|
-
# rubocop:disable Metrics/MethodLength
|
5
|
+
# rubocop:disable Metrics/MethodLength,Metrics/AbcSize
|
6
6
|
def self.load_defaults
|
7
7
|
# Check the table exists
|
8
8
|
return unless super
|
@@ -39,6 +39,7 @@ class Setting::RemoteExecution < Setting
|
|
39
39
|
N_('Effective User Method'),
|
40
40
|
nil,
|
41
41
|
{ :collection => proc { Hash[SSHExecutionProvider::EFFECTIVE_USER_METHODS.map { |method| [method, method] }] } }),
|
42
|
+
self.set('remote_execution_sudo_password', N_("Sudo password"), '', N_("Sudo password"), nil, {:encrypted => true}),
|
42
43
|
self.set('remote_execution_sync_templates',
|
43
44
|
N_('Whether we should sync templates from disk when running db:seed.'),
|
44
45
|
true,
|
@@ -63,12 +64,20 @@ class Setting::RemoteExecution < Setting
|
|
63
64
|
nil,
|
64
65
|
N_('Default SSH key passphrase'),
|
65
66
|
nil,
|
66
|
-
{ :encrypted => true })
|
67
|
+
{ :encrypted => true }),
|
68
|
+
self.set('remote_execution_workers_pool_size',
|
69
|
+
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.'),
|
70
|
+
5,
|
71
|
+
N_('Workers pool size')),
|
72
|
+
self.set('remote_execution_cleanup_working_dirs',
|
73
|
+
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.'),
|
74
|
+
true,
|
75
|
+
N_('Cleanup working directories'))
|
67
76
|
].each { |s| self.create! s.update(:category => 'Setting::RemoteExecution') }
|
68
77
|
end
|
69
78
|
|
70
79
|
true
|
71
80
|
end
|
72
81
|
# rubocop:enable AbcSize
|
73
|
-
# rubocop:enable Metrics/MethodLength
|
82
|
+
# rubocop:enable Metrics/MethodLength,Metrics/AbcSize
|
74
83
|
end
|
@@ -4,6 +4,8 @@ class SSHExecutionProvider < RemoteExecutionProvider
|
|
4
4
|
super.merge(:ssh_user => ssh_user(host),
|
5
5
|
:effective_user => effective_user(template_invocation),
|
6
6
|
:effective_user_method => effective_user_method(host),
|
7
|
+
:cleanup_working_dirs => cleanup_working_dirs?(host),
|
8
|
+
:sudo_password => sudo_password(host),
|
7
9
|
:ssh_port => ssh_port(host))
|
8
10
|
end
|
9
11
|
|
@@ -26,7 +28,7 @@ class SSHExecutionProvider < RemoteExecutionProvider
|
|
26
28
|
private
|
27
29
|
|
28
30
|
def ssh_user(host)
|
29
|
-
host.
|
31
|
+
host.host_param('remote_execution_ssh_user')
|
30
32
|
end
|
31
33
|
|
32
34
|
def ssh_port(host)
|
@@ -93,6 +93,7 @@
|
|
93
93
|
<div class="advanced hidden">
|
94
94
|
<%= password_f f, :password, :placeholder => '*****', :label => _('Password'), :label_help => N_('Password is stored encrypted in DB until the job finishes. For future or recurring executions, it is removed after the last execution.') %>
|
95
95
|
<%= password_f f, :key_passphrase, :placeholder => '*****', :label => _('Private key passphrase'), :label_help => N_('Key passhprase is only applicable for SSH provider. Other providers ignore this field. <br> Passphrase is stored encrypted in DB until the job finishes. For future or recurring executions, it is removed after the last execution.') %>
|
96
|
+
<%= password_f f, :sudo_password, :placeholder => '*****', :label => _('Sudo password'), :label_help => N_('Sudo password is only applicable for SSH provider. Other providers ignore this field. <br> Password is stored encrypted in DB until the job finishes. For future or recurring executions, it is removed after the last execution.') %>
|
96
97
|
</div>
|
97
98
|
|
98
99
|
<div class="advanced hidden">
|
@@ -1,7 +1,9 @@
|
|
1
1
|
<% title @job_invocation.description, trunc_with_tooltip(@job_invocation.description, 120) %>
|
2
2
|
<% stylesheet 'foreman_remote_execution/job_invocations' %>
|
3
3
|
<% javascript 'charts', 'foreman_remote_execution/template_invocation' %>
|
4
|
-
<%= javascript_include_tag *webpack_asset_paths('
|
4
|
+
<%= javascript_include_tag *webpack_asset_paths('foreman_remote_execution', :extension => 'js'), "data-turbolinks-track" => true, 'defer' => 'defer' %>
|
5
|
+
|
6
|
+
<%= breadcrumbs name_field: 'description' %>
|
5
7
|
|
6
8
|
<% if @job_invocation.task %>
|
7
9
|
<% title_actions(button_group(job_invocation_task_buttons(@job_invocation.task))) %>
|
@@ -1,6 +1,19 @@
|
|
1
1
|
<%= javascript 'lookup_keys' %>
|
2
2
|
<%= javascript 'foreman_remote_execution/template_input' %>
|
3
3
|
|
4
|
+
<%= breadcrumbs(
|
5
|
+
items: [
|
6
|
+
{
|
7
|
+
caption: _('Job Templates'),
|
8
|
+
url: url_for(job_templates_path)
|
9
|
+
},
|
10
|
+
{
|
11
|
+
caption: _('Edit %s' % @template.to_label)
|
12
|
+
}
|
13
|
+
]
|
14
|
+
)
|
15
|
+
%>
|
16
|
+
|
4
17
|
<% title _("Edit Job Template") %>
|
5
18
|
|
6
19
|
<%= render :partial => 'form' %>
|
@@ -7,7 +7,7 @@
|
|
7
7
|
link_to_function(_('Import'), 'show_import_job_template_modal();', :class => 'btn btn-default'),
|
8
8
|
new_link(_("New Job Template"))) %>
|
9
9
|
|
10
|
-
<table class="<%= table_css_classes('table-
|
10
|
+
<table class="<%= table_css_classes('table-fixed') %>">
|
11
11
|
<thead>
|
12
12
|
<tr>
|
13
13
|
<th class="col-md-6"><%= sort :name, :as => s_("JobTemplate|Name") %></th>
|
@@ -24,9 +24,9 @@ Gem::Specification.new do |s|
|
|
24
24
|
s.extra_rdoc_files = Dir['README*', 'LICENSE']
|
25
25
|
|
26
26
|
s.add_dependency 'deface'
|
27
|
-
s.add_dependency 'dynflow', '>= 1.0.
|
27
|
+
s.add_dependency 'dynflow', '>= 1.0.1', '< 2.0.0'
|
28
28
|
s.add_dependency 'foreman_remote_execution_core'
|
29
|
-
s.add_dependency 'foreman-tasks', '~> 0.
|
29
|
+
s.add_dependency 'foreman-tasks', '~> 0.13'
|
30
30
|
|
31
31
|
s.add_development_dependency 'factory_bot_rails', '~> 4.8.0'
|
32
32
|
s.add_development_dependency 'rubocop'
|
@@ -1,6 +1,8 @@
|
|
1
1
|
require 'foreman_remote_execution_core'
|
2
2
|
|
3
3
|
module ForemanRemoteExecution
|
4
|
+
DYNFLOW_QUEUE = :remote_execution
|
5
|
+
|
4
6
|
class Engine < ::Rails::Engine
|
5
7
|
engine_name 'foreman_remote_execution'
|
6
8
|
|
@@ -26,6 +28,7 @@ module ForemanRemoteExecution
|
|
26
28
|
|
27
29
|
initializer 'foreman_remote_execution.require_dynflow', :before => 'foreman_tasks.initialize_dynflow' do |app|
|
28
30
|
ForemanTasks.dynflow.require!
|
31
|
+
ForemanTasks.dynflow.config.queues.add(DYNFLOW_QUEUE, :pool_size => Setting['remote_execution_workers_pool_size']) if Setting.table_exists? rescue(false)
|
29
32
|
ForemanTasks.dynflow.config.eager_load_paths << File.join(ForemanRemoteExecution::Engine.root, 'app/lib/actions')
|
30
33
|
end
|
31
34
|
|
@@ -98,7 +98,7 @@ module Api
|
|
98
98
|
|
99
99
|
test 'should provide output for delayed task' do
|
100
100
|
host = @invocation.template_invocations_hosts.first
|
101
|
-
ForemanTasks::Task.any_instance.expects(:
|
101
|
+
ForemanTasks::Task.any_instance.expects(:scheduled?).returns(true)
|
102
102
|
get :output, params: { :job_invocation_id => @invocation.id, :host_id => host.id }
|
103
103
|
result = ActiveSupport::JSON.decode(@response.body)
|
104
104
|
assert_equal result['delayed'], true
|
@@ -0,0 +1,63 @@
|
|
1
|
+
require 'test_plugin_helper'
|
2
|
+
|
3
|
+
module ForemanRemoteExecution
|
4
|
+
class RunHostJobTest < ActiveSupport::TestCase
|
5
|
+
include Dynflow::Testing
|
6
|
+
|
7
|
+
subject { create_action(Actions::RemoteExecution::RunHostJob) }
|
8
|
+
let(:host) { FactoryBot.create(:host, :with_execution) }
|
9
|
+
|
10
|
+
before do
|
11
|
+
subject.stubs(:input).returns({ host: { id: host.id } })
|
12
|
+
Host.expects(:find).with(host.id).returns(host)
|
13
|
+
end
|
14
|
+
|
15
|
+
describe '#finalize' do
|
16
|
+
describe 'updates the host status' do
|
17
|
+
before do
|
18
|
+
subject.expects(:check_exit_status).returns(nil)
|
19
|
+
end
|
20
|
+
|
21
|
+
context 'with stubbed status' do
|
22
|
+
let(:stub_status) do
|
23
|
+
status = HostStatus::ExecutionStatus.new
|
24
|
+
status.stubs(:save!).returns(true)
|
25
|
+
status
|
26
|
+
end
|
27
|
+
|
28
|
+
before do
|
29
|
+
host.expects(:execution_status_object).returns(stub_status)
|
30
|
+
end
|
31
|
+
|
32
|
+
context 'exit_status is 0' do
|
33
|
+
it 'updates the host status to OK' do
|
34
|
+
subject.stubs(:exit_status).returns(0)
|
35
|
+
stub_status.expects(:"status=").with(HostStatus::ExecutionStatus::OK)
|
36
|
+
subject.finalize
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
context 'exit_status is NOT 0' do
|
41
|
+
it 'updates the host status to ERROR' do
|
42
|
+
subject.stubs(:exit_status).returns(1)
|
43
|
+
stub_status.expects(:"status=").with(HostStatus::ExecutionStatus::ERROR)
|
44
|
+
subject.finalize
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
context 'host has no execution status yet' do
|
50
|
+
before do
|
51
|
+
assert_nil host.execution_status_object
|
52
|
+
subject.stubs(:exit_status).returns(0)
|
53
|
+
end
|
54
|
+
|
55
|
+
it 'creates a new status' do
|
56
|
+
subject.finalize
|
57
|
+
refute_nil host.execution_status_object
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
@@ -20,21 +20,21 @@ class ForemanRemoteExecutionHostExtensionsTest < ActiveSupport::TestCase
|
|
20
20
|
end
|
21
21
|
|
22
22
|
it 'has ssh user in the parameters' do
|
23
|
-
host.
|
23
|
+
host.host_param('remote_execution_ssh_user').must_equal Setting[:remote_execution_ssh_user]
|
24
24
|
end
|
25
25
|
|
26
26
|
it 'can override ssh user' do
|
27
27
|
host.host_parameters << FactoryBot.create(:host_parameter, :host => host, :name => 'remote_execution_ssh_user', :value => 'amy')
|
28
|
-
host.
|
28
|
+
host.host_param('remote_execution_ssh_user').must_equal 'amy'
|
29
29
|
end
|
30
30
|
|
31
31
|
it 'has effective user method in the parameters' do
|
32
|
-
host.
|
32
|
+
host.host_param('remote_execution_effective_user_method').must_equal Setting[:remote_execution_effective_user_method]
|
33
33
|
end
|
34
34
|
|
35
35
|
it 'can override effective user method' do
|
36
36
|
host.host_parameters << FactoryBot.create(:host_parameter, :host => host, :name => 'remote_execution_effective_user_method', :value => 'su')
|
37
|
-
host.
|
37
|
+
host.host_param('remote_execution_effective_user_method').must_equal 'su'
|
38
38
|
end
|
39
39
|
|
40
40
|
it 'has ssh keys in the parameters' do
|
@@ -501,6 +501,18 @@ class JobInvocationComposerTest < ActiveSupport::TestCase
|
|
501
501
|
end
|
502
502
|
end
|
503
503
|
|
504
|
+
describe '#sudo_password' do
|
505
|
+
let(:sudo_password) { 'password' }
|
506
|
+
let(:params) do
|
507
|
+
{ :job_invocation => { :sudo_password => sudo_password }}
|
508
|
+
end
|
509
|
+
|
510
|
+
it 'sets the sudo password properly' do
|
511
|
+
composer
|
512
|
+
composer.job_invocation.sudo_password.must_equal sudo_password
|
513
|
+
end
|
514
|
+
end
|
515
|
+
|
504
516
|
describe '#targeting' do
|
505
517
|
it 'triggers targeting on job_invocation' do
|
506
518
|
composer
|
@@ -72,15 +72,21 @@ class RemoteExecutionProviderTest < ActiveSupport::TestCase
|
|
72
72
|
|
73
73
|
describe 'ssh user' do
|
74
74
|
it 'uses the remote_execution_ssh_user on the host param' do
|
75
|
-
host.params['remote_execution_ssh_user'] = 'my user'
|
76
75
|
host.host_parameters << FactoryBot.create(:host_parameter, :host => host, :name => 'remote_execution_ssh_user', :value => 'my user')
|
77
76
|
proxy_options[:ssh_user].must_equal 'my user'
|
78
77
|
end
|
79
78
|
end
|
80
79
|
|
80
|
+
describe 'sudo password' do
|
81
|
+
it 'uses the remote_execution_sudo_password on the host param' do
|
82
|
+
host.params['remote_execution_sudo_password'] = 'mypassword'
|
83
|
+
host.host_parameters << FactoryBot.create(:host_parameter, :host => host, :name => 'remote_execution_sudo_password', :value => 'mypassword')
|
84
|
+
proxy_options[:sudo_password].must_equal 'mypassword'
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
81
88
|
describe 'sudo' do
|
82
89
|
it 'uses the remote_execution_ssh_user on the host param' do
|
83
|
-
host.params['remote_execution_effective_user_method'] = 'sudo'
|
84
90
|
method_param = FactoryBot.create(:host_parameter, :host => host, :name => 'remote_execution_effective_user_method', :value => 'sudo')
|
85
91
|
host.host_parameters << method_param
|
86
92
|
proxy_options[:effective_user_method].must_equal 'sudo'
|
@@ -112,6 +118,25 @@ class RemoteExecutionProviderTest < ActiveSupport::TestCase
|
|
112
118
|
end
|
113
119
|
end
|
114
120
|
|
121
|
+
describe 'cleanup working directories setting' do
|
122
|
+
before do
|
123
|
+
Setting[:remote_execution_cleanup_working_dirs] = false
|
124
|
+
end
|
125
|
+
|
126
|
+
it 'updates the value via settings' do
|
127
|
+
proxy_options[:cleanup_working_dirs].must_equal false
|
128
|
+
end
|
129
|
+
end
|
130
|
+
|
131
|
+
describe 'cleanup working directories from parameters' do
|
132
|
+
it 'takes the value from host parameters' do
|
133
|
+
host.params['remote_execution_cleanup_working_dirs'] = 'false'
|
134
|
+
host.host_parameters << FactoryBot.build(:host_parameter, :name => 'remote_execution_cleanup_working_dirs', :value => 'false')
|
135
|
+
host.clear_host_parameters_cache!
|
136
|
+
proxy_options[:cleanup_working_dirs].must_equal false
|
137
|
+
end
|
138
|
+
end
|
139
|
+
|
115
140
|
describe '#find_ip_or_hostname' do
|
116
141
|
let(:host) do
|
117
142
|
FactoryBot.create(:host) do |host|
|
@@ -139,7 +164,7 @@ class RemoteExecutionProviderTest < ActiveSupport::TestCase
|
|
139
164
|
end
|
140
165
|
|
141
166
|
it 'gets ip from flagged interfaces' do
|
142
|
-
host.
|
167
|
+
host.host_params['remote_execution_connect_by_ip'] = true
|
143
168
|
# no ip address set on relevant interface - fallback to fqdn
|
144
169
|
SSHExecutionProvider.find_ip_or_hostname(host).must_equal 'somehost.somedomain.org'
|
145
170
|
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: foreman_remote_execution
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.5.
|
4
|
+
version: 1.5.2
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Foreman Remote Execution team
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2018-
|
11
|
+
date: 2018-05-16 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: deface
|
@@ -30,7 +30,7 @@ dependencies:
|
|
30
30
|
requirements:
|
31
31
|
- - ">="
|
32
32
|
- !ruby/object:Gem::Version
|
33
|
-
version: 1.0.
|
33
|
+
version: 1.0.1
|
34
34
|
- - "<"
|
35
35
|
- !ruby/object:Gem::Version
|
36
36
|
version: 2.0.0
|
@@ -40,7 +40,7 @@ dependencies:
|
|
40
40
|
requirements:
|
41
41
|
- - ">="
|
42
42
|
- !ruby/object:Gem::Version
|
43
|
-
version: 1.0.
|
43
|
+
version: 1.0.1
|
44
44
|
- - "<"
|
45
45
|
- !ruby/object:Gem::Version
|
46
46
|
version: 2.0.0
|
@@ -64,14 +64,14 @@ dependencies:
|
|
64
64
|
requirements:
|
65
65
|
- - "~>"
|
66
66
|
- !ruby/object:Gem::Version
|
67
|
-
version: '0.
|
67
|
+
version: '0.13'
|
68
68
|
type: :runtime
|
69
69
|
prerelease: false
|
70
70
|
version_requirements: !ruby/object:Gem::Requirement
|
71
71
|
requirements:
|
72
72
|
- - "~>"
|
73
73
|
- !ruby/object:Gem::Version
|
74
|
-
version: '0.
|
74
|
+
version: '0.13'
|
75
75
|
- !ruby/object:Gem::Dependency
|
76
76
|
name: factory_bot_rails
|
77
77
|
requirement: !ruby/object:Gem::Requirement
|
@@ -189,7 +189,6 @@ files:
|
|
189
189
|
- app/models/job_invocation_task_group.rb
|
190
190
|
- app/models/job_template.rb
|
191
191
|
- app/models/job_template_effective_user.rb
|
192
|
-
- app/models/job_template_importer.rb
|
193
192
|
- app/models/remote_execution_feature.rb
|
194
193
|
- app/models/remote_execution_provider.rb
|
195
194
|
- app/models/setting/remote_execution.rb
|
@@ -318,6 +317,7 @@ files:
|
|
318
317
|
- db/migrate/20180202072115_add_notification_builder_to_remote_execution_feature.rb
|
319
318
|
- db/migrate/20180202123215_add_feature_id_to_job_invocation.rb
|
320
319
|
- db/migrate/20180226095631_change_task_id_to_uuid.rb
|
320
|
+
- db/migrate/20180411160809_add_sudo_password_to_job_invocation.rb
|
321
321
|
- db/seeds.d/50-notification_blueprints.rb
|
322
322
|
- db/seeds.d/60-ssh_proxy_feature.rb
|
323
323
|
- db/seeds.d/70-job_templates.rb
|
@@ -364,6 +364,7 @@ files:
|
|
364
364
|
- test/functional/api/v2/template_inputs_controller_test.rb
|
365
365
|
- test/functional/job_invocations_controller_test.rb
|
366
366
|
- test/test_plugin_helper.rb
|
367
|
+
- test/unit/actions/run_host_job_test.rb
|
367
368
|
- test/unit/actions/run_hosts_job_test.rb
|
368
369
|
- test/unit/concerns/foreman_tasks_cleaner_extensions_test.rb
|
369
370
|
- test/unit/concerns/host_extensions_test.rb
|
@@ -373,7 +374,6 @@ files:
|
|
373
374
|
- test/unit/job_invocation_composer_test.rb
|
374
375
|
- test/unit/job_invocation_test.rb
|
375
376
|
- test/unit/job_template_effective_user_test.rb
|
376
|
-
- test/unit/job_template_importer_test.rb
|
377
377
|
- test/unit/job_template_test.rb
|
378
378
|
- test/unit/remote_execution_feature_test.rb
|
379
379
|
- test/unit/remote_execution_provider_test.rb
|
@@ -410,7 +410,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
410
410
|
version: '0'
|
411
411
|
requirements: []
|
412
412
|
rubyforge_project:
|
413
|
-
rubygems_version: 2.
|
413
|
+
rubygems_version: 2.7.3
|
414
414
|
signing_key:
|
415
415
|
specification_version: 4
|
416
416
|
summary: A plugin bringing remote execution to the Foreman, completing the config
|
@@ -426,6 +426,7 @@ test_files:
|
|
426
426
|
- test/functional/api/v2/template_inputs_controller_test.rb
|
427
427
|
- test/functional/job_invocations_controller_test.rb
|
428
428
|
- test/test_plugin_helper.rb
|
429
|
+
- test/unit/actions/run_host_job_test.rb
|
429
430
|
- test/unit/actions/run_hosts_job_test.rb
|
430
431
|
- test/unit/concerns/foreman_tasks_cleaner_extensions_test.rb
|
431
432
|
- test/unit/concerns/host_extensions_test.rb
|
@@ -435,7 +436,6 @@ test_files:
|
|
435
436
|
- test/unit/job_invocation_composer_test.rb
|
436
437
|
- test/unit/job_invocation_test.rb
|
437
438
|
- test/unit/job_template_effective_user_test.rb
|
438
|
-
- test/unit/job_template_importer_test.rb
|
439
439
|
- test/unit/job_template_test.rb
|
440
440
|
- test/unit/remote_execution_feature_test.rb
|
441
441
|
- test/unit/remote_execution_provider_test.rb
|
@@ -1,36 +0,0 @@
|
|
1
|
-
# This class is a shim to handle the importing of templates via the
|
2
|
-
# foreman_templates plugin. It expects a method like
|
3
|
-
# def import(name, text, metadata)
|
4
|
-
# but REx already has an import! method, so this class provides the
|
5
|
-
# translation layer.
|
6
|
-
|
7
|
-
class JobTemplateImporter
|
8
|
-
def self.import!(name, text, metadata, force = false)
|
9
|
-
skip = skip_locked(name, force)
|
10
|
-
return skip if skip
|
11
|
-
|
12
|
-
template = JobTemplate.import_parsed(name, text, metadata, :update => true, :force => force)
|
13
|
-
c_or_u = template.new_record? ? 'Created' : 'Updated'
|
14
|
-
|
15
|
-
result = " #{c_or_u} Template #{id_string template}:#{name}"
|
16
|
-
{ :old => template.template_was,
|
17
|
-
:new => template.template,
|
18
|
-
:status => template.save,
|
19
|
-
:result => result}
|
20
|
-
end
|
21
|
-
|
22
|
-
def self.skip_locked(name, force)
|
23
|
-
template = JobTemplate.find_by :name => name
|
24
|
-
|
25
|
-
if template && template.locked? && !template.new_record? && !force
|
26
|
-
{ :old => template.template_was,
|
27
|
-
:new => template.template,
|
28
|
-
:status => false,
|
29
|
-
:result => "Skipping Template #{id_string template}:#{name} - template is locked" }
|
30
|
-
end
|
31
|
-
end
|
32
|
-
|
33
|
-
def self.id_string(template)
|
34
|
-
template ? template.id.to_s : ''
|
35
|
-
end
|
36
|
-
end
|
@@ -1,66 +0,0 @@
|
|
1
|
-
require 'test_plugin_helper'
|
2
|
-
|
3
|
-
class JobTemplateImporterTest < ActiveSupport::TestCase
|
4
|
-
context 'importing a new template' do
|
5
|
-
# JobTemplate tests handle most of this, we just check that the shim
|
6
|
-
# correctly loads a template returns a hash
|
7
|
-
let(:remote_execution_feature) do
|
8
|
-
FactoryBot.create(:remote_execution_feature)
|
9
|
-
end
|
10
|
-
|
11
|
-
let(:result) do
|
12
|
-
name = 'Community Service Restart'
|
13
|
-
metadata = {
|
14
|
-
'model' => 'JobTemplate',
|
15
|
-
'kind' => 'job_template',
|
16
|
-
'name' => 'Service Restart',
|
17
|
-
'job_category' => 'Service Restart',
|
18
|
-
'provider_type' => 'SSH',
|
19
|
-
'feature' => remote_execution_feature.label,
|
20
|
-
'template_inputs' => [
|
21
|
-
{ 'name' => 'service_name', 'input_type' => 'user', 'required' => true },
|
22
|
-
{ 'name' => 'verbose', 'input_type' => 'user' }
|
23
|
-
]
|
24
|
-
}
|
25
|
-
text = <<-END_TEMPLATE.strip_heredoc
|
26
|
-
<%#
|
27
|
-
#{YAML.dump(metadata)}
|
28
|
-
%>
|
29
|
-
|
30
|
-
service <%= input("service_name") %> restart
|
31
|
-
END_TEMPLATE
|
32
|
-
|
33
|
-
JobTemplateImporter.import!(name, text, metadata)
|
34
|
-
end
|
35
|
-
|
36
|
-
let(:template) { JobTemplate.find_by name: 'Community Service Restart' }
|
37
|
-
|
38
|
-
it 'returns a valid foreman_templates hash' do
|
39
|
-
result[:status].must_equal true
|
40
|
-
result[:result].must_equal ' Created Template :Community Service Restart'
|
41
|
-
result[:old].must_be_nil
|
42
|
-
result[:new].must_equal template.template.squish
|
43
|
-
end
|
44
|
-
end
|
45
|
-
|
46
|
-
context 'updating locked template' do
|
47
|
-
it 'does not update locked template' do
|
48
|
-
name = 'Locked job template'
|
49
|
-
template = FactoryBot.create(:job_template, :locked => true, :name => name)
|
50
|
-
res = JobTemplateImporter.import!(name, 'some text', 'metadata')
|
51
|
-
assert_equal "Skipping Template #{template.id}:#{template.name} - template is locked", res[:result]
|
52
|
-
end
|
53
|
-
|
54
|
-
it 'updates locked template' do
|
55
|
-
name = 'Locked job template again'
|
56
|
-
metadata = {
|
57
|
-
'model' => 'JobTemplate',
|
58
|
-
'kind' => 'job_template',
|
59
|
-
'name' => name
|
60
|
-
}
|
61
|
-
template = FactoryBot.create(:job_template, :locked => true, :name => name)
|
62
|
-
res = JobTemplateImporter.import!(name, 'some text', metadata, true)
|
63
|
-
assert_equal " Updated Template #{template.id}:Locked job template again", res[:result]
|
64
|
-
end
|
65
|
-
end
|
66
|
-
end
|