foreman_remote_execution 1.1.1 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/app/lib/actions/remote_execution/run_host_job.rb +74 -24
- data/app/lib/actions/remote_execution/run_hosts_job.rb +2 -20
- data/app/models/host_status/execution_status.rb +1 -1
- data/app/models/setting/remote_execution.rb +4 -0
- data/app/services/remote_execution_proxy_selector.rb +5 -0
- data/foreman_remote_execution.gemspec +2 -1
- data/foreman_remote_execution_core.gemspec +23 -0
- data/lib/foreman_remote_execution/engine.rb +2 -0
- data/lib/foreman_remote_execution/version.rb +1 -1
- data/lib/foreman_remote_execution_core.rb +18 -0
- data/lib/foreman_remote_execution_core/actions.rb +11 -0
- data/lib/foreman_remote_execution_core/script_runner.rb +266 -0
- data/lib/foreman_remote_execution_core/version.rb +3 -0
- metadata +26 -13
- data/app/lib/actions/remote_execution/helpers/live_output.rb +0 -17
- data/app/lib/actions/remote_execution/run_proxy_command.rb +0 -96
- data/app/services/proxy_load_balancer.rb +0 -33
- data/test/unit/actions/run_proxy_command_test.rb +0 -134
- data/test/unit/proxy_load_balancer_test.rb +0 -27
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 8ca71ec8c1f3b330d327966298dea09c81a21378
|
4
|
+
data.tar.gz: a86edc45eaebca0fa3acfeecf38115fa6596d199
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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,
|
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
|
-
|
28
|
-
|
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
|
33
|
-
|
34
|
-
settings = { :global_proxy
|
35
|
-
:fallback_proxy => 'remote_execution_fallback_proxy'
|
36
|
-
|
37
|
-
|
38
|
-
|
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
|
-
|
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
|
-
|
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
|
58
|
-
|
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
|
-
|
63
|
-
|
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
|
-
|
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
|
-
|
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'),
|
@@ -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 '
|
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
|
@@ -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,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
|
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.
|
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-
|
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.
|
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.
|
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/
|
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
|