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 +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
|