foreman_remote_execution 1.1.1 → 1.2.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 4b2960d7df5f74d0bb9455ea4f024b1d31b9a68f
4
- data.tar.gz: 53d254b4f949852d53403d6ac15f6093211a3c79
3
+ metadata.gz: 8ca71ec8c1f3b330d327966298dea09c81a21378
4
+ data.tar.gz: a86edc45eaebca0fa3acfeecf38115fa6596d199
5
5
  SHA512:
6
- metadata.gz: 720aaba4a13126df8deea7d0566aa154da1b5bde179fecd51cd072f634f37f0c189f2d7942c665dba93b5078cb3872477735ac628d09ca035c394306acbe109d
7
- data.tar.gz: 4d57da51e9a0a8c651c778b5b0be52cc9c2159db07b532be6b2e4e1246f8d20b1b27f22217fdc028ea4dd180092963bcfec5da3e7b21f32a94629cf3e0599b5d
6
+ metadata.gz: db8e9e87cf16680859499d1f910f318b3ffba7e47bd180e30b5e43a25bebe04ef08a44b4225b519c73b312c4033d3b93dfababb80999ea04c667a0dbdd527d6f
7
+ data.tar.gz: 0d4b6974e4172fff1feee3a861cad6a14fb10788d1fb1a1dc8486328e94029681f3fbc9070394f706a434f6047dc2840f947f09422853d9faa551404698bd3ed
@@ -1,15 +1,16 @@
1
1
  module Actions
2
2
  module RemoteExecution
3
3
  class RunHostJob < Actions::EntryAction
4
+ include ::Actions::Helpers::WithContinuousOutput
5
+ include ::Actions::Helpers::WithDelegatedAction
4
6
 
5
7
  middleware.do_not_use Dynflow::Middleware::Common::Transaction
6
- include Actions::RemoteExecution::Helpers::LiveOutput
7
8
 
8
9
  def resource_locks
9
10
  :link
10
11
  end
11
12
 
12
- def plan(job_invocation, host, template_invocation, proxy, options = {})
13
+ def plan(job_invocation, host, template_invocation, proxy_selector = ::RemoteExecutionProxySelector.new, options = {})
13
14
  action_subject(host, :job_category => job_invocation.job_category, :description => job_invocation.description)
14
15
 
15
16
  template_invocation.host_id = host.id
@@ -24,18 +25,21 @@ module Actions
24
25
 
25
26
  raise _('Could not use any template used in the job invocation') if template_invocation.blank?
26
27
 
27
- if proxy.blank?
28
- offline_proxies = options.fetch(:offline_proxies, [])
28
+ provider = template_invocation.template.provider_type.to_s
29
+ proxy = proxy_selector.determine_proxy(host, provider)
30
+ if proxy == :not_available
31
+ offline_proxies = proxy_selector.offline
29
32
  settings = { :count => offline_proxies.count, :proxy_names => offline_proxies.map(&:name).join(', ') }
30
33
  raise n_('The only applicable proxy %{proxy_names} is down',
31
34
  'All %{count} applicable proxies are down. Tried %{proxy_names}',
32
- offline_proxies.count) % settings unless offline_proxies.empty?
33
-
34
- settings = { :global_proxy => 'remote_execution_global_proxy',
35
- :fallback_proxy => 'remote_execution_fallback_proxy' }
36
-
37
- raise _('Could not use any proxy. Consider configuring %{global_proxy} ' +
38
- 'or %{fallback_proxy} in settings') % settings
35
+ offline_proxies.count) % settings
36
+ elsif proxy == :not_defined && !Setting['remote_execution_without_proxy']
37
+ settings = { :global_proxy => 'remote_execution_global_proxy',
38
+ :fallback_proxy => 'remote_execution_fallback_proxy',
39
+ :no_proxy => 'remote_execution_no_proxy' }
40
+
41
+ raise _('Could not use any proxy. Consider configuring %{global_proxy}, ' +
42
+ '%{fallback_proxy} or %{no_proxy} in settings') % settings
39
43
  end
40
44
 
41
45
  renderer = InputTemplateRenderer.new(template_invocation.template, host, template_invocation)
@@ -43,28 +47,24 @@ module Actions
43
47
  raise _('Failed rendering template: %s') % renderer.error_message unless script
44
48
 
45
49
  provider = template_invocation.template.provider
46
- plan_action(RunProxyCommand, proxy, hostname, script, provider.proxy_command_options(template_invocation, host))
50
+ action_options = provider.proxy_command_options(template_invocation, host).merge(:hostname => hostname, :script => script)
51
+ plan_delegated_action(proxy, ForemanRemoteExecutionCore::Actions::RunScript, action_options)
47
52
  plan_self
48
53
  end
49
54
 
50
55
  def finalize(*args)
51
- host = Host.find(input[:host][:id])
52
- host.refresh_statuses
53
- rescue => e
54
- ::Foreman::Logging.exception "Could not update execution status for #{input[:host][:name]}", e
56
+ check_exit_status
55
57
  end
56
58
 
57
- def humanized_output
58
- live_output.map { |line| line['output'].chomp }.join("\n")
59
+ def check_exit_status
60
+ if delegated_output[:exit_status].to_s != '0'
61
+ error! _('Playbook execution failed')
62
+ end
59
63
  end
60
64
 
61
65
  def live_output
62
- proxy_command_action = planned_actions(RunProxyCommand).first
63
- if proxy_command_action
64
- proxy_command_action.live_output
65
- else
66
- execution_plan.errors.map { |e| exception_to_output(_('Failed to initialize command'), e) }
67
- end
66
+ continuous_output.sort!
67
+ continuous_output.raw_outputs
68
68
  end
69
69
 
70
70
  def humanized_input
@@ -76,6 +76,43 @@ module Actions
76
76
  N_('Remote action:')
77
77
  end
78
78
 
79
+ def finalize
80
+ if exit_status.to_s != '0'
81
+ error! _('Playbook execution failed')
82
+ end
83
+ end
84
+
85
+ def rescue_strategy
86
+ ::Dynflow::Action::Rescue::Fail
87
+ end
88
+
89
+ def humanized_output
90
+ continuous_output.humanize
91
+ end
92
+
93
+ def continuous_output_providers
94
+ super << self
95
+ end
96
+
97
+ def fill_continuous_output(continuous_output)
98
+ fill_planning_errors_to_continuous_output(continuous_output) unless exit_status
99
+ delegated_output.fetch('result', []).each do |raw_output|
100
+ continuous_output.add_raw_output(raw_output)
101
+ end
102
+ final_timestamp = (continuous_output.last_timestamp || task.ended_at).to_f + 1
103
+ if exit_status
104
+ continuous_output.add_output(_('Exit status: %s') % exit_status, 'stdout', final_timestamp)
105
+ elsif run_step && run_step.error
106
+ continuous_output.add_ouput(_('Job finished with error') + ": #{run_step.error.exception_class} - #{run_step.error.message}", 'debug', final_timestamp)
107
+ end
108
+ rescue => e
109
+ continuous_output.add_exception(_('Error loading data from proxy'), e)
110
+ end
111
+
112
+ def exit_status
113
+ delegated_output[:exit_status]
114
+ end
115
+
79
116
  def find_ip_or_hostname(host)
80
117
  %w(execution primary provision).each do |flag|
81
118
  interface = host.send(flag + '_interface')
@@ -91,6 +128,19 @@ module Actions
91
128
 
92
129
  private
93
130
 
131
+ def delegated_output
132
+ if input[:delegated_action_id]
133
+ super
134
+ elsif phase?(Present)
135
+ # for compatibility with old actions
136
+ if old_action = all_planned_actions.first
137
+ old_action.output.fetch('proxy_output', {})
138
+ else
139
+ {}
140
+ end
141
+ end
142
+ end
143
+
94
144
  def verify_permissions(host, template_invocation)
95
145
  raise _('User can not execute job on host %s') % host.name unless User.current.can?(:view_hosts, host)
96
146
  raise _('User can not execute this job template') unless User.current.can?(:view_job_templates, template_invocation.template)
@@ -23,16 +23,14 @@ module Actions
23
23
 
24
24
  def create_sub_plans
25
25
  job_invocation = JobInvocation.find(input[:job_invocation_id])
26
- load_balancer = ProxyLoadBalancer.new
26
+ proxy_selector = RemoteExecutionProxySelector.new
27
27
 
28
28
  # composer creates just "pattern" for template_invocations because target is evaluated
29
29
  # during actual run (here) so we build template invocations from these patterns
30
30
  job_invocation.targeting.hosts.map do |host|
31
31
  template_invocation = job_invocation.pattern_template_invocation_for_host(host).deep_clone
32
32
  template_invocation.host_id = host.id
33
- proxy = determine_proxy(template_invocation, host, load_balancer)
34
- trigger(RunHostJob, job_invocation, host, template_invocation, proxy,
35
- :offline_proxies => load_balancer.offline)
33
+ trigger(RunHostJob, job_invocation, host, template_invocation, proxy_selector)
36
34
  end
37
35
  end
38
36
 
@@ -56,22 +54,6 @@ module Actions
56
54
  def humanized_name
57
55
  '%s:' % _(super)
58
56
  end
59
-
60
- private
61
-
62
- def determine_proxy(template_invocation, host, load_balancer)
63
- provider = template_invocation.template.provider_type.to_s
64
- host_proxies = host.remote_execution_proxies(provider)
65
- strategies = [:subnet, :fallback, :global]
66
- proxy = nil
67
-
68
- strategies.each do |strategy|
69
- proxy = load_balancer.next(host_proxies[strategy]) if host_proxies[strategy].present?
70
- break if proxy
71
- end
72
-
73
- proxy
74
- end
75
57
  end
76
58
  end
77
59
  end
@@ -10,7 +10,7 @@ class HostStatus::ExecutionStatus < HostStatus::Status
10
10
  # mapping to string representation
11
11
  STATUS_NAMES = { OK => 'succeeded', ERROR => 'failed', QUEUED => 'queued', RUNNING => 'running' }
12
12
 
13
- def relevant?
13
+ def relevant?(*args)
14
14
  execution_tasks.present?
15
15
  end
16
16
 
@@ -14,6 +14,10 @@ class Setting::RemoteExecution < Setting
14
14
  "If locations or organizations are enabled, the search will be limited to the host's " +
15
15
  'organization or location.'),
16
16
  true),
17
+ self.set('remote_execution_without_proxy',
18
+ N_('When enabled, the remote execution will try to run the commands directly, when no
19
+ proxy with remote execution feature is configured for the host.'),
20
+ false),
17
21
  self.set('remote_execution_ssh_user',
18
22
  N_('Default user to use for SSH. You may override per host by setting a parameter called remote_execution_ssh_user.'),
19
23
  'root'),
@@ -0,0 +1,5 @@
1
+ class RemoteExecutionProxySelector < ::ForemanTasks::ProxySelector
2
+ def available_proxies(host, provider)
3
+ host.remote_execution_proxies(provider)
4
+ end
5
+ end
@@ -20,7 +20,8 @@ Gem::Specification.new do |s|
20
20
 
21
21
  s.add_dependency 'deface'
22
22
  s.add_dependency 'dynflow', '~> 0.8.10'
23
- s.add_dependency 'foreman-tasks', '0.8.0'
23
+ s.add_dependency 'foreman_remote_execution_core'
24
+ s.add_dependency 'foreman-tasks', '~> 0.8.1'
24
25
 
25
26
  s.add_development_dependency 'rubocop'
26
27
  s.add_development_dependency 'rdoc'
@@ -0,0 +1,23 @@
1
+ # -*- coding: utf-8 -*-
2
+ require File.expand_path('../lib/foreman_remote_execution_core/version', __FILE__)
3
+ require 'date'
4
+
5
+ Gem::Specification.new do |s|
6
+ s.name = 'foreman_remote_execution_core'
7
+ s.version = ForemanRemoteExecutionCore::VERSION
8
+ s.authors = ['Ivan Nečas']
9
+ s.email = ['inecas@redhat.com']
10
+ s.homepage = 'https://github.com/theforeman/foreman_remote_execution'
11
+ s.summary = 'Foreman remote execution - core bits'
12
+ s.description = <<DESC
13
+ Ssh remote execution provider code sharable between Foreman and Foreman-Proxy
14
+ DESC
15
+ s.licenses = ['GPL-3']
16
+
17
+ s.files = Dir['lib/foreman_remote_execution_core/**/*'] +
18
+ ['lib/foreman_remote_execution_core.rb']
19
+
20
+ s.add_runtime_dependency('foreman-tasks-core', '~> 0.1.0')
21
+ s.add_runtime_dependency('net-ssh')
22
+ s.add_runtime_dependency('net-scp')
23
+ end
@@ -1,3 +1,5 @@
1
+ require 'foreman_remote_execution_core'
2
+
1
3
  module ForemanRemoteExecution
2
4
  class Engine < ::Rails::Engine
3
5
  engine_name 'foreman_remote_execution'
@@ -1,3 +1,3 @@
1
1
  module ForemanRemoteExecution
2
- VERSION = '1.1.1'
2
+ VERSION = '1.2.0'
3
3
  end
@@ -0,0 +1,18 @@
1
+ require 'foreman_tasks_core'
2
+
3
+ module ForemanRemoteExecutionCore
4
+ extend ForemanTasksCore::SettingsLoader
5
+ register_settings([:remote_execution_ssh, :smart_proxy_remote_execution_ssh_core],
6
+ :ssh_identity_key_file => '~/.ssh/id_rsa_foreman_proxy',
7
+ :ssh_user => 'root',
8
+ :remote_working_dir => '/var/tmp',
9
+ :local_working_dir => '/var/tmp')
10
+
11
+ if ForemanTasksCore.dynflow_present?
12
+ require 'foreman_tasks_core/runner'
13
+ require 'foreman_remote_execution_core/script_runner'
14
+ require 'foreman_remote_execution_core/actions'
15
+ end
16
+
17
+ require 'foreman_remote_execution_core/version'
18
+ end
@@ -0,0 +1,11 @@
1
+ require 'foreman_tasks_core/shareable_action'
2
+
3
+ module ForemanRemoteExecutionCore
4
+ module Actions
5
+ class RunScript < ForemanTasksCore::Runner::Action
6
+ def initiate_runner
7
+ ScriptRunner.new(input)
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,266 @@
1
+ require 'net/ssh'
2
+ require 'net/scp'
3
+
4
+ module ForemanRemoteExecutionCore
5
+ class ScriptRunner < ForemanTasksCore::Runner::Base
6
+ EXPECTED_POWER_ACTION_MESSAGES = ["restart host", "shutdown host"]
7
+
8
+ def initialize(options)
9
+ super()
10
+ @host = options.fetch(:hostname)
11
+ @script = options.fetch(:script)
12
+ @ssh_user = options.fetch(:ssh_user, 'root')
13
+ @ssh_port = options.fetch(:ssh_port, 22)
14
+ @effective_user = options.fetch(:effective_user, nil)
15
+ @effective_user_method = options.fetch(:effective_user_method, 'sudo')
16
+ @host_public_key = options.fetch(:host_public_key, nil)
17
+ @verify_host = options.fetch(:verify_host, nil)
18
+
19
+ @client_private_key_file = settings.fetch(:ssh_identity_key_file)
20
+ @local_working_dir = options.fetch(:local_working_dir, settings.fetch(:local_working_dir))
21
+ @remote_working_dir = options.fetch(:remote_working_dir, settings.fetch(:remote_working_dir))
22
+ end
23
+
24
+ def start
25
+ remote_script = cp_script_to_remote
26
+ output_path = File.join(File.dirname(remote_script), 'output')
27
+
28
+ # pipe the output to tee while capturing the exit code
29
+ script = <<-SCRIPT
30
+ exec 4>&1
31
+ exit_code=`((#{su_prefix}#{remote_script}; echo $?>&3 ) | /usr/bin/tee #{output_path} ) 3>&1 >&4`
32
+ exec 4>&-
33
+ exit $exit_code
34
+ SCRIPT
35
+ logger.debug("executing script:\n#{script.lines.map { |line| " | #{line}" }.join}")
36
+ run_async(script)
37
+ rescue => e
38
+ logger.error("error while initalizing command #{e.class} #{e.message}:\n #{e.backtrace.join("\n")}")
39
+ publish_exception("Error initializing command", e)
40
+ end
41
+
42
+ def refresh
43
+ return if @session.nil?
44
+ with_retries do
45
+ with_disconnect_handling do
46
+ @session.process(0)
47
+ end
48
+ end
49
+ ensure
50
+ check_expecting_disconnect
51
+ end
52
+
53
+ def kill
54
+ if @session
55
+ run_sync("pkill -f #{remote_command_file('script')}")
56
+ else
57
+ logger.debug("connection closed")
58
+ end
59
+ rescue => e
60
+ publish_exception("Unexpected error", e, false)
61
+ end
62
+
63
+ def with_retries
64
+ tries = 0
65
+ begin
66
+ yield
67
+ rescue => e
68
+ logger.error("Unexpected error: #{e.class} #{e.message}\n #{e.backtrace.join("\n")}")
69
+ tries += 1
70
+ if tries <= MAX_PROCESS_RETRIES
71
+ logger.error('Retrying')
72
+ retry
73
+ else
74
+ publish_exception("Unexpected error", e)
75
+ end
76
+ end
77
+ end
78
+
79
+ def with_disconnect_handling
80
+ yield
81
+ rescue Net::SSH::Disconnect => e
82
+ @session.shutdown!
83
+ check_expecting_disconnect
84
+ if @expecting_disconnect
85
+ publish_exit_status(0)
86
+ else
87
+ publish_exception("Unexpected disconnect", e)
88
+ end
89
+ end
90
+
91
+ def close
92
+ @session.close if @session && !@session.closed?
93
+ end
94
+
95
+ private
96
+
97
+ def session
98
+ @session ||= begin
99
+ @logger.debug("opening session to #{@ssh_user}@#{@host}")
100
+ Net::SSH.start(@host, @ssh_user, ssh_options)
101
+ end
102
+ end
103
+
104
+ def ssh_options
105
+ ssh_options = {}
106
+ ssh_options[:port] = @ssh_port if @ssh_port
107
+ ssh_options[:keys] = [@client_private_key_file] if @client_private_key_file
108
+ ssh_options[:user_known_hosts_file] = @known_hosts_file if @known_hosts_file
109
+ ssh_options[:keys_only] = true
110
+ # if the host public key is contained in the known_hosts_file,
111
+ # verify it, otherwise, if missing, import it and continue
112
+ ssh_options[:paranoid] = true
113
+ ssh_options[:auth_methods] = ["publickey"]
114
+ ssh_options[:user_known_hosts_file] = prepare_known_hosts if @host_public_key
115
+ return ssh_options
116
+ end
117
+
118
+ def settings
119
+ ForemanRemoteExecutionCore.settings
120
+ end
121
+
122
+ # Initiates run of the remote command and yields the data when
123
+ # available. The yielding doesn't happen automatically, but as
124
+ # part of calling the `refresh` method.
125
+ def run_async(command)
126
+ raise "Async command already in progress" if @started
127
+ @started = false
128
+ session.open_channel do |channel|
129
+ channel.request_pty
130
+ channel.on_data { |ch, data| publish_data(data, 'stdout') }
131
+ channel.on_extended_data { |ch, type, data| publish_data(data, 'stderr') }
132
+ # standard exit of the command
133
+ channel.on_request("exit-status") { |ch, data| publish_exit_status(data.read_long) }
134
+ # on signal: sending the signal value (such as 'TERM')
135
+ channel.on_request("exit-signal") do |ch, data|
136
+ publish_exit_status(data.read_string)
137
+ ch.close
138
+ # wait for the channel to finish so that we know at the end
139
+ # that the session is inactive
140
+ ch.wait
141
+ end
142
+ channel.exec(command) do |ch, success|
143
+ @started = true
144
+ raise("Error initializing command") unless success
145
+ end
146
+ end
147
+ session.process(0) until @started
148
+ return true
149
+ end
150
+
151
+ def run_sync(command)
152
+ output = ""
153
+ exit_status = nil
154
+ channel = session.open_channel do |ch|
155
+ ch.on_data { |data| output.concat(data) }
156
+ ch.on_extended_data { |_, _, data| output.concat(data) }
157
+ ch.on_request("exit-status") { |_, data| exit_status = data.read_long }
158
+ # on signal: sending the signal value (such as 'TERM')
159
+ ch.on_request("exit-signal") do |_, data|
160
+ exit_status = data.read_string
161
+ ch.close
162
+ ch.wait
163
+ end
164
+ ch.exec command do |_, success|
165
+ raise "could not execute command" unless success
166
+ end
167
+ end
168
+ channel.wait
169
+ return exit_status, output
170
+ end
171
+
172
+ def su_prefix
173
+ return if @effective_user.nil? || @effective_user == @ssh_user
174
+ case @effective_user_method
175
+ when 'sudo'
176
+ "sudo -n -u #{@effective_user} "
177
+ when 'su'
178
+ "su - #{@effective_user} -c "
179
+ else
180
+ raise "effective_user_method ''#{@effective_user_method}'' not supported"
181
+ end
182
+ end
183
+
184
+ def prepare_known_hosts
185
+ path = local_command_file('known_hosts')
186
+ if @host_public_key
187
+ write_command_file_locally('known_hosts', "#{@host} #{@host_public_key}")
188
+ end
189
+ return path
190
+ end
191
+
192
+ def local_command_dir
193
+ File.join(@local_working_dir, 'foreman-proxy', "foreman-ssh-cmd-#{@id}")
194
+ end
195
+
196
+ def local_command_file(filename)
197
+ File.join(local_command_dir, filename)
198
+ end
199
+
200
+ def remote_command_dir
201
+ File.join(@remote_working_dir, "foreman-ssh-cmd-#{id}")
202
+ end
203
+
204
+ def remote_command_file(filename)
205
+ File.join(remote_command_dir, filename)
206
+ end
207
+
208
+ def ensure_local_directory(path)
209
+ if File.exist?(path)
210
+ raise "#{path} expected to be a directory" unless File.directory?(path)
211
+ else
212
+ FileUtils.mkdir_p(path)
213
+ end
214
+ return path
215
+ end
216
+
217
+ def cp_script_to_remote
218
+ local_script_file = write_command_file_locally('script', sanitize_script(@script))
219
+ File.chmod(0555, local_script_file)
220
+ remote_script_file = remote_command_file('script')
221
+ upload_file(local_script_file, remote_script_file)
222
+ return remote_script_file
223
+ end
224
+
225
+ def upload_file(local_path, remote_path)
226
+ ensure_remote_directory(File.dirname(remote_path))
227
+ scp = Net::SCP.new(session)
228
+ upload_channel = scp.upload(local_path, remote_path)
229
+ upload_channel.wait
230
+ ensure
231
+ if upload_channel
232
+ upload_channel.close
233
+ upload_channel.wait
234
+ end
235
+ end
236
+
237
+ def ensure_remote_directory(path)
238
+ exit_code, output = run_sync("mkdir -p #{path}")
239
+ if exit_code != 0
240
+ raise "Unable to create directory on remote system #{path}: exit code: #{exit_code}\n #{output}"
241
+ end
242
+ end
243
+
244
+ def sanitize_script(script)
245
+ script.tr("\r", '')
246
+ end
247
+
248
+ def write_command_file_locally(filename, content)
249
+ path = local_command_file(filename)
250
+ ensure_local_directory(File.dirname(path))
251
+ File.write(path, content)
252
+ return path
253
+ end
254
+
255
+ # when a remote server disconnects, it's hard to tell if it was on purpose (when calling reboot)
256
+ # or it's an error. When it's expected, we expect the script to produce 'restart host' as
257
+ # its last command output
258
+ def check_expecting_disconnect
259
+ last_output = @continuous_output.raw_outputs.find { |d| d['output_type'] == 'stdout' }
260
+ return unless last_output
261
+ if EXPECTED_POWER_ACTION_MESSAGES.any? { |message| last_output['output'] =~ /^#{message}/ }
262
+ @expecting_disconnect = true
263
+ end
264
+ end
265
+ end
266
+ end
@@ -0,0 +1,3 @@
1
+ module ForemanRemoteExecutionCore
2
+ VERSION = '1.0.0'
3
+ end
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.1.1
4
+ version: 1.2.0
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: 2016-09-07 00:00:00.000000000 Z
11
+ date: 2016-09-08 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: deface
@@ -38,20 +38,34 @@ dependencies:
38
38
  - - "~>"
39
39
  - !ruby/object:Gem::Version
40
40
  version: 0.8.10
41
+ - !ruby/object:Gem::Dependency
42
+ name: foreman_remote_execution_core
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
41
55
  - !ruby/object:Gem::Dependency
42
56
  name: foreman-tasks
43
57
  requirement: !ruby/object:Gem::Requirement
44
58
  requirements:
45
- - - '='
59
+ - - "~>"
46
60
  - !ruby/object:Gem::Version
47
- version: 0.8.0
61
+ version: 0.8.1
48
62
  type: :runtime
49
63
  prerelease: false
50
64
  version_requirements: !ruby/object:Gem::Requirement
51
65
  requirements:
52
- - - '='
66
+ - - "~>"
53
67
  - !ruby/object:Gem::Version
54
- version: 0.8.0
68
+ version: 0.8.1
55
69
  - !ruby/object:Gem::Dependency
56
70
  name: rubocop
57
71
  requirement: !ruby/object:Gem::Requirement
@@ -160,10 +174,8 @@ files:
160
174
  - app/helpers/concerns/foreman_remote_execution/job_templates_extensions.rb
161
175
  - app/helpers/remote_execution_helper.rb
162
176
  - app/lib/actions/middleware/bind_job_invocation.rb
163
- - app/lib/actions/remote_execution/helpers/live_output.rb
164
177
  - app/lib/actions/remote_execution/run_host_job.rb
165
178
  - app/lib/actions/remote_execution/run_hosts_job.rb
166
- - app/lib/actions/remote_execution/run_proxy_command.rb
167
179
  - app/lib/proxy_api/remote_execution_ssh.rb
168
180
  - app/mailers/.gitkeep
169
181
  - app/models/concerns/foreman_remote_execution/bookmark_extensions.rb
@@ -198,7 +210,7 @@ files:
198
210
  - app/models/template_invocation_input_value.rb
199
211
  - app/overrides/execution_interface.rb
200
212
  - app/overrides/subnet_proxies.rb
201
- - app/services/proxy_load_balancer.rb
213
+ - app/services/remote_execution_proxy_selector.rb
202
214
  - app/views/api/v2/foreign_input_sets/base.json.rabl
203
215
  - app/views/api/v2/foreign_input_sets/create.json.rabl
204
216
  - app/views/api/v2/foreign_input_sets/index.json.rabl
@@ -339,9 +351,14 @@ files:
339
351
  - doc/source/static/js/jquery.tocify.min.js
340
352
  - doc/source/static/js/scroll.js
341
353
  - foreman_remote_execution.gemspec
354
+ - foreman_remote_execution_core.gemspec
342
355
  - lib/foreman_remote_execution.rb
343
356
  - lib/foreman_remote_execution/engine.rb
344
357
  - lib/foreman_remote_execution/version.rb
358
+ - lib/foreman_remote_execution_core.rb
359
+ - lib/foreman_remote_execution_core/actions.rb
360
+ - lib/foreman_remote_execution_core/script_runner.rb
361
+ - lib/foreman_remote_execution_core/version.rb
345
362
  - lib/tasks/foreman_remote_execution_tasks.rake
346
363
  - locale/Makefile
347
364
  - locale/action_names.rb
@@ -378,7 +395,6 @@ files:
378
395
  - test/test_plugin_helper.rb
379
396
  - test/unit/actions/run_host_job_test.rb
380
397
  - test/unit/actions/run_hosts_job_test.rb
381
- - test/unit/actions/run_proxy_command_test.rb
382
398
  - test/unit/concerns/exportable_test.rb
383
399
  - test/unit/concerns/host_extensions_test.rb
384
400
  - test/unit/concerns/nic_extensions_test.rb
@@ -388,7 +404,6 @@ files:
388
404
  - test/unit/job_invocation_test.rb
389
405
  - test/unit/job_template_effective_user_test.rb
390
406
  - test/unit/job_template_test.rb
391
- - test/unit/proxy_load_balancer_test.rb
392
407
  - test/unit/remote_execution_feature_test.rb
393
408
  - test/unit/remote_execution_provider_test.rb
394
409
  - test/unit/targeting_test.rb
@@ -428,7 +443,6 @@ test_files:
428
443
  - test/test_plugin_helper.rb
429
444
  - test/unit/actions/run_host_job_test.rb
430
445
  - test/unit/actions/run_hosts_job_test.rb
431
- - test/unit/actions/run_proxy_command_test.rb
432
446
  - test/unit/concerns/exportable_test.rb
433
447
  - test/unit/concerns/host_extensions_test.rb
434
448
  - test/unit/concerns/nic_extensions_test.rb
@@ -438,7 +452,6 @@ test_files:
438
452
  - test/unit/job_invocation_test.rb
439
453
  - test/unit/job_template_effective_user_test.rb
440
454
  - test/unit/job_template_test.rb
441
- - test/unit/proxy_load_balancer_test.rb
442
455
  - test/unit/remote_execution_feature_test.rb
443
456
  - test/unit/remote_execution_provider_test.rb
444
457
  - test/unit/targeting_test.rb
@@ -1,17 +0,0 @@
1
- module Actions
2
- module RemoteExecution
3
- module Helpers
4
- module LiveOutput
5
- def exception_to_output(context, exception, timestamp = Time.now.getlocal)
6
- format_output(context + ": #{exception.class} - #{exception.message}", 'debug', timestamp)
7
- end
8
-
9
- def format_output(message, type = 'debug', timestamp = Time.now.getlocal)
10
- { 'output_type' => type,
11
- 'output' => message,
12
- 'timestamp' => timestamp.to_f }
13
- end
14
- end
15
- end
16
- end
17
- end
@@ -1,96 +0,0 @@
1
- module Actions
2
- module RemoteExecution
3
- class RunProxyCommand < Actions::ProxyAction
4
-
5
- include ::Dynflow::Action::Cancellable
6
- include Actions::RemoteExecution::Helpers::LiveOutput
7
-
8
- def plan(proxy, hostname, script, options = {})
9
- options = { :effective_user => nil }.merge(options)
10
- super(proxy, options.merge(:hostname => hostname, :script => script))
11
- end
12
-
13
- def proxy_action_name
14
- 'Proxy::RemoteExecution::Ssh::CommandAction'
15
- end
16
-
17
- def on_data(data)
18
- if data[:result] == 'initialization_error'
19
- handle_connection_exception(data[:metadata][:exception_class]
20
- .constantize
21
- .new(data[:metadata][:exception_message]))
22
- else
23
- super(data)
24
- error! _('Script execution failed') if failed_run?
25
- end
26
- end
27
-
28
-
29
- def rescue_strategy
30
- ::Dynflow::Action::Rescue::Skip
31
- end
32
-
33
- def failed_run?
34
- output[:result] == 'initialization_error' ||
35
- (exit_status && proxy_output[:exit_status] != 0)
36
- end
37
-
38
- def exit_status
39
- proxy_output && proxy_output[:exit_status]
40
- end
41
-
42
- def live_output
43
- records = connection_messages
44
- if !task.pending?
45
- records.concat(finalized_output)
46
- else
47
- records.concat(current_proxy_output)
48
- end
49
- records.sort_by { |record| record['timestamp'].to_f }
50
- end
51
-
52
- private
53
-
54
- def connection_messages
55
- metadata.fetch(:failed_proxy_tasks, []).map do |failure_data|
56
- format_output(_('Initialization error: %s') % "#{failure_data[:exception_class]} - #{failure_data[:exception_message]}", 'debug', failure_data[:timestamp])
57
- end
58
- end
59
-
60
- def current_proxy_output
61
- return [] unless output[:proxy_task_id]
62
- proxy_data = proxy.status_of_task(output[:proxy_task_id])['actions'].detect { |action| action['class'] == proxy_action_name }
63
- proxy_data.fetch('output', {}).fetch('result', [])
64
- rescue => e
65
- ::Foreman::Logging.exception("Failed to load data for task #{task.id} from proxy #{input[:proxy_url]}", e)
66
- [exception_to_output(_('Error loading data from proxy'), e)]
67
- end
68
-
69
- def finalized_output
70
- records = []
71
-
72
- if proxy_result.present?
73
- records.concat(proxy_result)
74
- else
75
- records << format_output(_('No output'))
76
- end
77
-
78
- if exit_status
79
- records << format_output(_('Exit status: %s') % exit_status, 'stdout', final_timestamp(records))
80
- elsif run_step && run_step.error
81
- records << format_output(_('Job finished with error') + ": #{run_step.error.exception_class} - #{run_step.error.message}", 'debug', final_timestamp(records))
82
- end
83
- return records
84
- end
85
-
86
- def final_timestamp(records)
87
- return task.ended_at if records.blank?
88
- records.last.fetch('timestamp', task.ended_at).to_f + 1
89
- end
90
-
91
- def proxy_result
92
- self.output.fetch(:proxy_output, {}).fetch(:result, []) || []
93
- end
94
- end
95
- end
96
- end
@@ -1,33 +0,0 @@
1
- class ProxyLoadBalancer
2
-
3
- attr_reader :offline
4
-
5
- def initialize
6
- @tasks = {}
7
- @offline = []
8
- end
9
-
10
- # Get the least loaded proxy from the given list of proxies
11
- def next(proxies)
12
- exclude = @tasks.keys + @offline
13
- @tasks.merge!(get_counts(proxies - exclude))
14
- next_proxy = @tasks.select { |proxy, _| proxies.include?(proxy) }.min_by { |_, job_count| job_count }.try(:first)
15
- @tasks[next_proxy] += 1 if next_proxy.present?
16
- next_proxy
17
- end
18
-
19
- private
20
-
21
- def get_counts(proxies)
22
- proxies.inject({}) do |result, proxy|
23
- begin
24
- proxy_api = ProxyAPI::ForemanDynflow::DynflowProxy.new(:url => proxy.url)
25
- result[proxy] = proxy_api.tasks_count('running')
26
- rescue => e
27
- @offline << proxy
28
- Foreman::Logging.exception "Could not fetch task counts from #{proxy}, skipped.", e
29
- end
30
- result
31
- end
32
- end
33
- end
@@ -1,134 +0,0 @@
1
- require 'test_plugin_helper'
2
-
3
- module ForemanRemoteExecution
4
- class RunProxyCommandTest < ActiveSupport::TestCase
5
- include Dynflow::Testing
6
-
7
- let(:host) { FactoryGirl.build(:host, :with_execution) }
8
- let(:proxy) { host.remote_execution_proxies('SSH')[:subnet].first }
9
- let(:hostname) { 'myhost.example.com' }
10
- let(:script) { 'ping -c 5 redhat.com' }
11
- let(:connection_options) { { 'retry_interval' => 15, 'retry_count' => 4, 'timeout' => 60 } }
12
- let(:action) do
13
- create_and_plan_action(Actions::RemoteExecution::RunProxyCommand, proxy, host.name, script)
14
- end
15
- let(:timestamp) { 1_443_194_805.9192207 }
16
-
17
- it 'plans for running the command action on server' do
18
- assert_run_phase action, { :hostname => host.name,
19
- :script => script,
20
- :proxy_url => proxy.url,
21
- :effective_user => nil,
22
- :connection_options => connection_options }
23
- end
24
-
25
- it 'sends to command to ssh provider' do
26
- action.proxy_action_name.must_equal 'Proxy::RemoteExecution::Ssh::CommandAction'
27
- end
28
-
29
- it "doesn't block on failure" do
30
- action.rescue_strategy.must_equal ::Dynflow::Action::Rescue::Skip
31
- end
32
-
33
- describe '#live_output' do
34
- let(:task) { ForemanTasks::Task.new }
35
-
36
- let(:action) do
37
- planned_action = create_and_plan_action(Actions::RemoteExecution::RunProxyCommand, proxy, hostname, script)
38
- create_action_presentation(Actions::RemoteExecution::RunProxyCommand).tap do |action|
39
- action.stubs(:task).returns(task)
40
- action.stubs(:proxy).returns(ProxyAPI::ForemanDynflow::DynflowProxy.new(:url => proxy.url))
41
- action.instance_variable_set('@input', planned_action.input)
42
- action.output.merge!(:proxy_task_id => '123')
43
- end
44
- end
45
-
46
- let(:live_output) do
47
- action.live_output
48
- end
49
-
50
- describe 'when the task is finished' do
51
- before do
52
- task.state = 'stopped'
53
- task.ended_at = Time.at(timestamp + 1).utc
54
- end
55
-
56
- describe 'the task finished sucessfully' do
57
- before do
58
- action.output.merge!(:proxy_output => { :result => [{ 'output_type' => 'stdout', 'output' => 'Hello', 'timestamp' => timestamp}],
59
- :exit_status => 0 })
60
- end
61
-
62
- it "doesn't fetch data from proxy anymore" do
63
- action.proxy.expects(:status_of_task).never
64
- live_output.size.must_equal 2
65
- live_output[0]['output_type'].must_equal 'stdout'
66
- live_output[0]['output'].must_equal 'Hello'
67
- live_output[0]['timestamp'].must_be_kind_of Float
68
- live_output[1]['output_type'].must_equal 'stdout'
69
- live_output[1]['output'].must_equal 'Exit status: 0'
70
- live_output[1]['timestamp'].must_be_kind_of Float
71
- end
72
- end
73
-
74
- describe 'there was not output data from proxy' do
75
- before do
76
- action.output.merge!(:proxy_output => {})
77
- end
78
-
79
- it "doesn't fetch data from proxy anymore" do
80
- action.proxy.expects(:status_of_task).never
81
- live_output.size.must_equal 1
82
- live_output[0]['output_type'].must_equal 'debug'
83
- live_output[0]['output'].must_equal 'No output'
84
- live_output[0]['timestamp'].must_be_kind_of Float
85
- end
86
- end
87
- end
88
-
89
- describe 'when something went wrong while fetching the data' do
90
- before do
91
- action.proxy.expects(:status_of_task).raises('Something went wrong')
92
- end
93
-
94
- it 'reports the failure as part of the live output' do
95
- live_output.size.must_equal 1
96
- live_output.first['output_type'].must_equal 'debug'
97
- live_output.first['output'].must_equal 'Error loading data from proxy: RuntimeError - Something went wrong'
98
- live_output.first['timestamp'].must_be_kind_of Float
99
- end
100
- end
101
-
102
- describe 'when there was some connection error while running the command' do
103
- before do
104
- action.output.merge!(:proxy_task_id => nil,
105
- :metadata => { :failed_proxy_tasks => [action.send(:format_exception, RuntimeError.new('Connection error'))]})
106
- end
107
-
108
- it 'reports the failure as part of the live output' do
109
- live_output.size.must_equal 1
110
- live_output.first['output_type'].must_equal 'debug'
111
- live_output.first['output'].must_equal 'Initialization error: RuntimeError - Connection error'
112
- live_output.first['timestamp'].must_be_kind_of Float
113
- end
114
- end
115
-
116
- describe 'when proxy returns valid data' do
117
- before do
118
- action.proxy.expects(:status_of_task).returns('actions' =>
119
- [{ 'class' => 'Proxy::RemoteExecution::Ssh::CommandAction',
120
- 'output' => { 'result' => [ { 'output_type' => 'stdout',
121
- 'output' => 'Hello',
122
- 'timestamp' => timestamp }]}}])
123
- end
124
-
125
- it 'reports the failure as part of the live output' do
126
- live_output.size.must_equal 1
127
- live_output.first['output_type'].must_equal 'stdout'
128
- live_output.first['output'].must_equal 'Hello'
129
- live_output.first['timestamp'].must_be_kind_of Float
130
- end
131
- end
132
- end
133
- end
134
- end
@@ -1,27 +0,0 @@
1
- require 'test_plugin_helper'
2
-
3
- describe ProxyLoadBalancer do
4
- let(:load_balancer) { ProxyLoadBalancer.new }
5
-
6
- before do
7
- ProxyAPI::ForemanDynflow::DynflowProxy.any_instance.stubs(:tasks_count).returns(0)
8
- end
9
-
10
- it 'load balances' do
11
- count = 3
12
- ProxyAPI::ForemanDynflow::DynflowProxy.any_instance.expects(:tasks_count).raises.then.times(count - 1).returns(0)
13
- proxies = FactoryGirl.create_list(:smart_proxy, count, :ssh)
14
-
15
- available = proxies.reduce([]) do |found, _|
16
- found << load_balancer.next(proxies)
17
- end
18
-
19
- available.count.must_equal count
20
- available.uniq.count.must_equal count - 1
21
- load_balancer.offline.count.must_equal 1
22
- end
23
-
24
- it 'returns nil for if no proxy is available' do
25
- load_balancer.next([]).must_be_nil
26
- end
27
- end