foreman_remote_execution 1.5.1 → 1.5.2
Sign up to get free protection for your applications and to get access to all the features.
- 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
|