smart_proxy_remote_execution_ssh 0.7.3 → 0.9.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/lib/smart_proxy_remote_execution_ssh/actions/pull_script.rb +34 -2
- data/lib/smart_proxy_remote_execution_ssh/api.rb +1 -0
- data/lib/smart_proxy_remote_execution_ssh/cockpit.rb +10 -7
- data/lib/smart_proxy_remote_execution_ssh/command_logging.rb +23 -0
- data/lib/smart_proxy_remote_execution_ssh/job_storage.rb +4 -2
- data/lib/smart_proxy_remote_execution_ssh/multiplexed_ssh_connection.rb +196 -0
- data/lib/smart_proxy_remote_execution_ssh/plugin.rb +2 -1
- data/lib/smart_proxy_remote_execution_ssh/runners/polling_script_runner.rb +3 -3
- data/lib/smart_proxy_remote_execution_ssh/runners/script_runner.rb +19 -62
- data/lib/smart_proxy_remote_execution_ssh/version.rb +1 -1
- data/settings.d/remote_execution_ssh.yml.example +4 -0
- metadata +4 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: f28e2eb48888626dad82eb56e7a5309da15d584e6d4c53eaf648e5ddfc6f3d1e
|
4
|
+
data.tar.gz: a5b604d80264dc78b461e1792100ecd6fc9a4a71791b8df5b064bd80a32f6920
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 3231a3d465b180d1b8b325fd9fc341c009b566452946ee56837f9f37e6502d7095d1a213bc5b4b13078b9d54e5bd592f5430aea502ecdee93edab15901440815
|
7
|
+
data.tar.gz: 62e77d3cf6d37c90b2e6e8e48ad8b2080186a50fd1d3633b38743a099e0897b9b1e4d4dafaf7b02b1855dcd51036d418333c156438c3b20e5fa42feac93ded17
|
@@ -5,6 +5,8 @@ require 'time'
|
|
5
5
|
module Proxy::RemoteExecution::Ssh::Actions
|
6
6
|
class PullScript < Proxy::Dynflow::Action::Runner
|
7
7
|
JobDelivered = Class.new
|
8
|
+
PickupTimeout = Class.new
|
9
|
+
ResendNotification = Class.new
|
8
10
|
|
9
11
|
execution_plan_hooks.use :cleanup, :on => :stopped
|
10
12
|
|
@@ -17,9 +19,20 @@ module Proxy::RemoteExecution::Ssh::Actions
|
|
17
19
|
if event == JobDelivered
|
18
20
|
output[:state] = :delivered
|
19
21
|
suspend
|
22
|
+
elsif event == PickupTimeout
|
23
|
+
process_pickup_timeout
|
24
|
+
elsif event == ResendNotification
|
25
|
+
if input[:with_mqtt] && %w(ready_for_pickup notified).include?(output[:state])
|
26
|
+
schedule_mqtt_resend
|
27
|
+
mqtt_start(::Proxy::Dynflow::OtpManager.passwords[execution_plan_id])
|
28
|
+
end
|
29
|
+
suspend
|
20
30
|
else
|
21
31
|
super
|
22
32
|
end
|
33
|
+
rescue => e
|
34
|
+
action_logger.error(e)
|
35
|
+
process_update(Proxy::Dynflow::Runner::Update.encode_exception('Proxy error', e))
|
23
36
|
end
|
24
37
|
|
25
38
|
def init_run
|
@@ -27,10 +40,15 @@ module Proxy::RemoteExecution::Ssh::Actions
|
|
27
40
|
::Proxy::Dynflow::OtpManager.generate_otp(execution_plan_id)
|
28
41
|
end
|
29
42
|
|
30
|
-
input[:
|
43
|
+
plan_event(PickupTimeout, input[:time_to_pickup], optional: true) if input[:time_to_pickup]
|
44
|
+
|
45
|
+
input[:job_uuid] = job_storage.store_job(host_name, execution_plan_id, run_step_id, input[:script].tr("\r", ''), effective_user: input[:effective_user])
|
31
46
|
output[:state] = :ready_for_pickup
|
32
47
|
output[:result] = []
|
33
|
-
|
48
|
+
if input[:with_mqtt]
|
49
|
+
schedule_mqtt_resend
|
50
|
+
mqtt_start(otp_password)
|
51
|
+
end
|
34
52
|
suspend
|
35
53
|
end
|
36
54
|
|
@@ -103,6 +121,8 @@ module Proxy::RemoteExecution::Ssh::Actions
|
|
103
121
|
'username': execution_plan_id,
|
104
122
|
'password': otp_password,
|
105
123
|
'return_url': "#{input[:proxy_url]}/ssh/jobs/#{input[:job_uuid]}/update",
|
124
|
+
'version': 'v1',
|
125
|
+
'effective_user': input[:effective_user]
|
106
126
|
},
|
107
127
|
)
|
108
128
|
mqtt_notify payload
|
@@ -164,5 +184,17 @@ module Proxy::RemoteExecution::Ssh::Actions
|
|
164
184
|
directive: 'foreman'
|
165
185
|
}
|
166
186
|
end
|
187
|
+
|
188
|
+
def process_pickup_timeout
|
189
|
+
if output[:state] != :delivered
|
190
|
+
raise "The job was not picked up in time"
|
191
|
+
else
|
192
|
+
suspend
|
193
|
+
end
|
194
|
+
end
|
195
|
+
|
196
|
+
def schedule_mqtt_resend
|
197
|
+
plan_event(ResendNotification, settings[:mqtt_resend_interval], optional: true)
|
198
|
+
end
|
167
199
|
end
|
168
200
|
end
|
@@ -1,5 +1,6 @@
|
|
1
1
|
require 'smart_proxy_remote_execution_ssh/net_ssh_compat'
|
2
2
|
require 'forwardable'
|
3
|
+
require 'securerandom'
|
3
4
|
|
4
5
|
module Proxy::RemoteExecution
|
5
6
|
module Cockpit
|
@@ -164,9 +165,8 @@ module Proxy::RemoteExecution
|
|
164
165
|
out_read, out_write = IO.pipe
|
165
166
|
err_read, err_write = IO.pipe
|
166
167
|
|
167
|
-
|
168
|
-
|
169
|
-
pid = spawn(*script_runner.send(:get_args, command), :in => in_read, :out => out_write, :err => err_write)
|
168
|
+
connection.establish!
|
169
|
+
pid = spawn(*connection.command(command), :in => in_read, :out => out_write, :err => err_write)
|
170
170
|
[in_read, out_write, err_write].each(&:close)
|
171
171
|
|
172
172
|
send_start
|
@@ -176,6 +176,8 @@ module Proxy::RemoteExecution
|
|
176
176
|
in_buf = MiniSSLBufferedSocket.new(in_write)
|
177
177
|
|
178
178
|
inner_system_ssh_loop out_buf, err_buf, in_buf, pid
|
179
|
+
ensure
|
180
|
+
connection.disconnect!
|
179
181
|
end
|
180
182
|
|
181
183
|
def inner_system_ssh_loop(out_buf, err_buf, in_buf, pid)
|
@@ -189,7 +191,7 @@ module Proxy::RemoteExecution
|
|
189
191
|
|
190
192
|
proxy_data(out_buf, in_buf)
|
191
193
|
if buf_socket.closed?
|
192
|
-
|
194
|
+
connection.disconnect!
|
193
195
|
end
|
194
196
|
|
195
197
|
if out_buf.closed?
|
@@ -263,10 +265,10 @@ module Proxy::RemoteExecution
|
|
263
265
|
params["hostname"]
|
264
266
|
end
|
265
267
|
|
266
|
-
def
|
267
|
-
@
|
268
|
+
def connection
|
269
|
+
@connection ||= Proxy::RemoteExecution::Ssh::Runners::MultiplexedSSHConnection.new(
|
268
270
|
runner_params,
|
269
|
-
|
271
|
+
logger: logger
|
270
272
|
)
|
271
273
|
end
|
272
274
|
|
@@ -279,6 +281,7 @@ module Proxy::RemoteExecution
|
|
279
281
|
# For compatibility only
|
280
282
|
ret[:script] = nil
|
281
283
|
ret[:hostname] = host
|
284
|
+
ret[:id] = SecureRandom.uuid
|
282
285
|
ret
|
283
286
|
end
|
284
287
|
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
# lib/command_logging.rb
|
2
|
+
|
3
|
+
module Proxy::RemoteExecution::Ssh::Runners
|
4
|
+
module CommandLogging
|
5
|
+
def log_command(command, label: "Running")
|
6
|
+
command = command.join(' ')
|
7
|
+
label = "#{label}: " if label
|
8
|
+
logger.debug("#{label}#{command}")
|
9
|
+
end
|
10
|
+
|
11
|
+
def set_pm_debug_logging(pm, capture: false, user_method: nil)
|
12
|
+
callback = proc do |data|
|
13
|
+
data.each_line do |line|
|
14
|
+
logger.debug(line.chomp) if user_method.nil? || !user_method.filter_password?(line)
|
15
|
+
user_method.on_data(data, pm.stdin) if user_method
|
16
|
+
end
|
17
|
+
data
|
18
|
+
end
|
19
|
+
pm.on_stdout(&callback)
|
20
|
+
pm.on_stderr(&callback)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -11,6 +11,7 @@ module Proxy::RemoteExecution::Ssh
|
|
11
11
|
String :hostname, null: false, index: true
|
12
12
|
String :execution_plan_uuid, fixed: true, size: 36, null: false, index: true
|
13
13
|
Integer :run_step_id, null: false
|
14
|
+
String :effective_user
|
14
15
|
String :job, text: true
|
15
16
|
end
|
16
17
|
end
|
@@ -24,13 +25,14 @@ module Proxy::RemoteExecution::Ssh
|
|
24
25
|
.select_map(:uuid)
|
25
26
|
end
|
26
27
|
|
27
|
-
def store_job(hostname, execution_plan_uuid, run_step_id, job, uuid: SecureRandom.uuid, timestamp: Time.now.utc)
|
28
|
+
def store_job(hostname, execution_plan_uuid, run_step_id, job, uuid: SecureRandom.uuid, timestamp: Time.now.utc, effective_user: nil)
|
28
29
|
jobs.insert(timestamp: timestamp,
|
29
30
|
uuid: uuid,
|
30
31
|
hostname: hostname,
|
31
32
|
execution_plan_uuid: execution_plan_uuid,
|
32
33
|
run_step_id: run_step_id,
|
33
|
-
job: job
|
34
|
+
job: job,
|
35
|
+
effective_user: effective_user)
|
34
36
|
uuid
|
35
37
|
end
|
36
38
|
|
@@ -0,0 +1,196 @@
|
|
1
|
+
require 'smart_proxy_remote_execution_ssh/command_logging'
|
2
|
+
|
3
|
+
module Proxy::RemoteExecution::Ssh::Runners
|
4
|
+
class SensitiveString
|
5
|
+
def initialize(value, mask: '*****')
|
6
|
+
@value = value
|
7
|
+
@mask = mask
|
8
|
+
end
|
9
|
+
|
10
|
+
def inspect
|
11
|
+
'"' + to_s + '"'
|
12
|
+
end
|
13
|
+
|
14
|
+
def to_s
|
15
|
+
@mask
|
16
|
+
end
|
17
|
+
|
18
|
+
def to_str
|
19
|
+
@value
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
class AuthenticationMethod
|
24
|
+
attr_reader :name
|
25
|
+
def initialize(name, prompt: nil, password: nil)
|
26
|
+
@name = name
|
27
|
+
@prompt = prompt
|
28
|
+
@password = password
|
29
|
+
end
|
30
|
+
|
31
|
+
def ssh_command_prefix
|
32
|
+
return [] unless @password
|
33
|
+
|
34
|
+
prompt = ['-P', @prompt] if @prompt
|
35
|
+
[{'SSHPASS' => SensitiveString.new(@password)}, 'sshpass', '-e', prompt].compact
|
36
|
+
end
|
37
|
+
|
38
|
+
def ssh_options
|
39
|
+
["-o PreferredAuthentications=#{name}",
|
40
|
+
"-o NumberOfPasswordPrompts=#{@password ? 1 : 0}"]
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
class MultiplexedSSHConnection
|
45
|
+
include CommandLogging
|
46
|
+
|
47
|
+
attr_reader :logger
|
48
|
+
def initialize(options, logger:)
|
49
|
+
@logger = logger
|
50
|
+
|
51
|
+
@id = options.fetch(:id)
|
52
|
+
@host = options.fetch(:hostname)
|
53
|
+
@script = options.fetch(:script)
|
54
|
+
@ssh_user = options.fetch(:ssh_user, 'root')
|
55
|
+
@ssh_port = options.fetch(:ssh_port, 22)
|
56
|
+
@ssh_password = options.fetch(:secrets, {}).fetch(:ssh_password, nil)
|
57
|
+
@key_passphrase = options.fetch(:secrets, {}).fetch(:key_passphrase, nil)
|
58
|
+
@host_public_key = options.fetch(:host_public_key, nil)
|
59
|
+
@verify_host = options.fetch(:verify_host, nil)
|
60
|
+
@client_private_key_file = settings.ssh_identity_key_file
|
61
|
+
|
62
|
+
@local_working_dir = options.fetch(:local_working_dir, settings.local_working_dir)
|
63
|
+
@socket_working_dir = options.fetch(:socket_working_dir, settings.socket_working_dir)
|
64
|
+
@socket = nil
|
65
|
+
end
|
66
|
+
|
67
|
+
def establish!
|
68
|
+
@available_auth_methods ||= available_authentication_methods
|
69
|
+
method = @available_auth_methods.find do |method|
|
70
|
+
if try_auth_method(method)
|
71
|
+
@available_auth_methods.unshift(method).uniq!
|
72
|
+
true
|
73
|
+
end
|
74
|
+
end
|
75
|
+
method || raise("Could not establish connection to remote host using any available authentication method, tried #{@available_auth_methods.map(&:name).join(', ')}")
|
76
|
+
end
|
77
|
+
|
78
|
+
def disconnect!
|
79
|
+
return unless connected?
|
80
|
+
|
81
|
+
cmd = command(%w[-O exit])
|
82
|
+
log_command(cmd, label: "Closing shared connection")
|
83
|
+
pm = Proxy::Dynflow::ProcessManager.new(cmd)
|
84
|
+
set_pm_debug_logging(pm)
|
85
|
+
pm.run!
|
86
|
+
@socket = nil
|
87
|
+
end
|
88
|
+
|
89
|
+
def connected?
|
90
|
+
!@socket.nil?
|
91
|
+
end
|
92
|
+
|
93
|
+
def command(cmd)
|
94
|
+
raise "Cannot build command to run over multiplexed connection without having an established connection" unless connected?
|
95
|
+
|
96
|
+
['ssh', reuse_ssh_options, cmd].flatten
|
97
|
+
end
|
98
|
+
|
99
|
+
private
|
100
|
+
|
101
|
+
def try_auth_method(method)
|
102
|
+
# running "ssh -f -N" instead of "ssh true" would be cleaner, but ssh
|
103
|
+
# does not close its stderr which trips up the process manager which
|
104
|
+
# expects all FDs to be closed
|
105
|
+
|
106
|
+
full_command = [method.ssh_command_prefix, 'ssh', establish_ssh_options, method.ssh_options, @host, 'true'].flatten
|
107
|
+
log_command(full_command)
|
108
|
+
pm = Proxy::Dynflow::ProcessManager.new(full_command)
|
109
|
+
pm.start!
|
110
|
+
if pm.status
|
111
|
+
raise pm.stderr.to_s
|
112
|
+
else
|
113
|
+
set_pm_debug_logging(pm)
|
114
|
+
pm.stdin.io.close
|
115
|
+
pm.run!
|
116
|
+
end
|
117
|
+
|
118
|
+
if pm.status.zero?
|
119
|
+
logger.debug("Established connection using authentication method #{method.name}")
|
120
|
+
@socket = socket_file
|
121
|
+
true
|
122
|
+
else
|
123
|
+
logger.debug("Failed to establish connection using authentication method #{method.name}")
|
124
|
+
false
|
125
|
+
end
|
126
|
+
end
|
127
|
+
|
128
|
+
def settings
|
129
|
+
Proxy::RemoteExecution::Ssh::Plugin.settings
|
130
|
+
end
|
131
|
+
|
132
|
+
def available_authentication_methods
|
133
|
+
methods = []
|
134
|
+
methods << AuthenticationMethod.new('password', password: @ssh_password) if @ssh_password
|
135
|
+
if verify_key_passphrase
|
136
|
+
methods << AuthenticationMethod.new('publickey', password: @key_passphrase, prompt: 'passphrase')
|
137
|
+
end
|
138
|
+
methods << AuthenticationMethod.new('gssapi-with-mic') if settings[:kerberos_auth]
|
139
|
+
raise "There are no available authentication methods" if methods.empty?
|
140
|
+
methods
|
141
|
+
end
|
142
|
+
|
143
|
+
def establish_ssh_options
|
144
|
+
return @establish_ssh_options if @establish_ssh_options
|
145
|
+
ssh_options = []
|
146
|
+
ssh_options << "-o User=#{@ssh_user}"
|
147
|
+
ssh_options << "-o Port=#{@ssh_port}" if @ssh_port
|
148
|
+
ssh_options << "-o IdentityFile=#{@client_private_key_file}" if @client_private_key_file
|
149
|
+
ssh_options << "-o IdentitiesOnly=yes"
|
150
|
+
ssh_options << "-o StrictHostKeyChecking=no"
|
151
|
+
ssh_options << "-o UserKnownHostsFile=#{prepare_known_hosts}" if @host_public_key
|
152
|
+
ssh_options << "-o LogLevel=#{ssh_log_level(true)}"
|
153
|
+
ssh_options << "-o ControlMaster=auto"
|
154
|
+
ssh_options << "-o ControlPath=#{socket_file}"
|
155
|
+
ssh_options << "-o ControlPersist=yes"
|
156
|
+
ssh_options << "-o ProxyCommand=none"
|
157
|
+
@establish_ssh_options = ssh_options
|
158
|
+
end
|
159
|
+
|
160
|
+
def reuse_ssh_options
|
161
|
+
["-o", "ControlPath=#{@socket}", "-o", "LogLevel=#{ssh_log_level(false)}", @host]
|
162
|
+
end
|
163
|
+
|
164
|
+
def socket_file
|
165
|
+
File.join(@socket_working_dir, @id)
|
166
|
+
end
|
167
|
+
|
168
|
+
def verify_key_passphrase
|
169
|
+
command = ['ssh-keygen', '-y', '-f', File.expand_path(@client_private_key_file)]
|
170
|
+
log_command(command, label: "Checking if private key has passphrase")
|
171
|
+
pm = Proxy::Dynflow::ProcessManager.new(command)
|
172
|
+
pm.start!
|
173
|
+
|
174
|
+
raise pm.stderr.to_s if pm.status
|
175
|
+
|
176
|
+
pm.stdin.io.close
|
177
|
+
pm.run!
|
178
|
+
|
179
|
+
if pm.status.zero?
|
180
|
+
logger.debug("Private key is not protected with a passphrase")
|
181
|
+
@key_passphrase = nil
|
182
|
+
else
|
183
|
+
logger.debug("Private key is protected with a passphrase")
|
184
|
+
end
|
185
|
+
|
186
|
+
return true if pm.status.zero? || @key_passphrase
|
187
|
+
|
188
|
+
logger.debug("Private key is protected with a passphrase, but no passphrase was provided")
|
189
|
+
false
|
190
|
+
end
|
191
|
+
|
192
|
+
def ssh_log_level(new_connection)
|
193
|
+
new_connection ? settings[:ssh_log_level] : 'quiet'
|
194
|
+
end
|
195
|
+
end
|
196
|
+
end
|
@@ -50,13 +50,13 @@ module Proxy::RemoteExecution::Ssh::Runners
|
|
50
50
|
end
|
51
51
|
|
52
52
|
def refresh
|
53
|
+
@connection.establish! unless @connection.connected?
|
53
54
|
begin
|
54
55
|
pm = run_sync("#{@user_method.cli_command_prefix} #{@retrieval_script}")
|
56
|
+
process_retrieved_data(pm.stdout.to_s.chomp, pm.stderr.to_s.chomp)
|
55
57
|
rescue StandardError => e
|
56
58
|
@logger.info("Error while connecting to the remote host on refresh: #{e.message}")
|
57
59
|
end
|
58
|
-
|
59
|
-
process_retrieved_data(pm.stdout.to_s.chomp, pm.stderr.to_s.chomp)
|
60
60
|
ensure
|
61
61
|
destroy_session
|
62
62
|
end
|
@@ -138,7 +138,7 @@ module Proxy::RemoteExecution::Ssh::Runners
|
|
138
138
|
end
|
139
139
|
|
140
140
|
def destroy_session
|
141
|
-
if @
|
141
|
+
if @connection.connected?
|
142
142
|
@logger.debug("Closing session with #{@ssh_user}@#{@host}")
|
143
143
|
close_session
|
144
144
|
end
|
@@ -1,6 +1,7 @@
|
|
1
1
|
require 'fileutils'
|
2
2
|
require 'smart_proxy_dynflow/runner/process_manager_command'
|
3
3
|
require 'smart_proxy_dynflow/process_manager'
|
4
|
+
require 'smart_proxy_remote_execution_ssh/multiplexed_ssh_connection'
|
4
5
|
|
5
6
|
module Proxy::RemoteExecution::Ssh::Runners
|
6
7
|
class EffectiveUserMethod
|
@@ -92,6 +93,8 @@ module Proxy::RemoteExecution::Ssh::Runners
|
|
92
93
|
# rubocop:disable Metrics/ClassLength
|
93
94
|
class ScriptRunner < Proxy::Dynflow::Runner::Base
|
94
95
|
include Proxy::Dynflow::Runner::ProcessManagerCommand
|
96
|
+
include CommandLogging
|
97
|
+
|
95
98
|
attr_reader :execution_timeout_interval
|
96
99
|
|
97
100
|
EXPECTED_POWER_ACTION_MESSAGES = ['restart host', 'shutdown host'].freeze
|
@@ -103,10 +106,7 @@ module Proxy::RemoteExecution::Ssh::Runners
|
|
103
106
|
@script = options.fetch(:script)
|
104
107
|
@ssh_user = options.fetch(:ssh_user, 'root')
|
105
108
|
@ssh_port = options.fetch(:ssh_port, 22)
|
106
|
-
@ssh_password = options.fetch(:secrets, {}).fetch(:ssh_password, nil)
|
107
|
-
@key_passphrase = options.fetch(:secrets, {}).fetch(:key_passphrase, nil)
|
108
109
|
@host_public_key = options.fetch(:host_public_key, nil)
|
109
|
-
@verify_host = options.fetch(:verify_host, nil)
|
110
110
|
@execution_timeout_interval = options.fetch(:execution_timeout_interval, nil)
|
111
111
|
|
112
112
|
@client_private_key_file = settings.ssh_identity_key_file
|
@@ -116,6 +116,7 @@ module Proxy::RemoteExecution::Ssh::Runners
|
|
116
116
|
@cleanup_working_dirs = options.fetch(:cleanup_working_dirs, settings.cleanup_working_dirs)
|
117
117
|
@first_execution = options.fetch(:first_execution, false)
|
118
118
|
@user_method = user_method
|
119
|
+
@options = options
|
119
120
|
end
|
120
121
|
|
121
122
|
def self.build(options, suspended_action:)
|
@@ -143,7 +144,9 @@ module Proxy::RemoteExecution::Ssh::Runners
|
|
143
144
|
|
144
145
|
def start
|
145
146
|
Proxy::RemoteExecution::Utils.prune_known_hosts!(@host, @ssh_port, logger) if @first_execution
|
146
|
-
|
147
|
+
ensure_local_directory(@socket_working_dir)
|
148
|
+
@connection = MultiplexedSSHConnection.new(@options.merge(:id => @id), logger: logger)
|
149
|
+
@connection.establish!
|
147
150
|
preflight_checks
|
148
151
|
prepare_start
|
149
152
|
script = initialization_script
|
@@ -172,16 +175,6 @@ module Proxy::RemoteExecution::Ssh::Runners
|
|
172
175
|
end
|
173
176
|
end
|
174
177
|
|
175
|
-
def establish_connection
|
176
|
-
# run_sync ['-f', '-N'] would be cleaner, but ssh does not close its
|
177
|
-
# stderr which trips up the process manager which expects all FDs to be
|
178
|
-
# closed
|
179
|
-
ensure_remote_command(
|
180
|
-
'true',
|
181
|
-
error: 'Failed to establish connection to remote host, exit code: %{exit_code}'
|
182
|
-
)
|
183
|
-
end
|
184
|
-
|
185
178
|
def prepare_start
|
186
179
|
@remote_script = cp_script_to_remote
|
187
180
|
@output_path = File.join(File.dirname(@remote_script), 'output')
|
@@ -232,11 +225,7 @@ module Proxy::RemoteExecution::Ssh::Runners
|
|
232
225
|
def close_session
|
233
226
|
raise 'Control socket file does not exist' unless File.exist?(socket_file)
|
234
227
|
@logger.debug("Sending exit request for session #{@ssh_user}@#{@host}")
|
235
|
-
|
236
|
-
pm = Proxy::Dynflow::ProcessManager.new(args)
|
237
|
-
pm.on_stdout { |data| @logger.debug "[close_session]: #{data.chomp}"; data }
|
238
|
-
pm.on_stderr { |data| @logger.debug "[close_session]: #{data.chomp}"; data }
|
239
|
-
pm.run!
|
228
|
+
@connection.disconnect!
|
240
229
|
end
|
241
230
|
|
242
231
|
def close
|
@@ -264,35 +253,10 @@ module Proxy::RemoteExecution::Ssh::Runners
|
|
264
253
|
@process_manager && @cleanup_working_dirs
|
265
254
|
end
|
266
255
|
|
267
|
-
def ssh_options(with_pty = false, quiet: false)
|
268
|
-
ssh_options = []
|
269
|
-
ssh_options << "-tt" if with_pty
|
270
|
-
ssh_options << "-o User=#{@ssh_user}"
|
271
|
-
ssh_options << "-o Port=#{@ssh_port}" if @ssh_port
|
272
|
-
ssh_options << "-o IdentityFile=#{@client_private_key_file}" if @client_private_key_file
|
273
|
-
ssh_options << "-o IdentitiesOnly=yes"
|
274
|
-
ssh_options << "-o StrictHostKeyChecking=no"
|
275
|
-
ssh_options << "-o PreferredAuthentications=#{available_authentication_methods.join(',')}"
|
276
|
-
ssh_options << "-o UserKnownHostsFile=#{prepare_known_hosts}" if @host_public_key
|
277
|
-
ssh_options << "-o NumberOfPasswordPrompts=1"
|
278
|
-
ssh_options << "-o LogLevel=#{quiet ? 'quiet' : settings[:ssh_log_level]}"
|
279
|
-
ssh_options << "-o ControlMaster=auto"
|
280
|
-
ssh_options << "-o ControlPath=#{socket_file}"
|
281
|
-
ssh_options << "-o ControlPersist=yes"
|
282
|
-
ssh_options << "-o ProxyCommand=none"
|
283
|
-
end
|
284
|
-
|
285
256
|
def settings
|
286
257
|
Proxy::RemoteExecution::Ssh::Plugin.settings
|
287
258
|
end
|
288
259
|
|
289
|
-
def get_args(command, with_pty = false, quiet: false)
|
290
|
-
args = []
|
291
|
-
args = [{'SSHPASS' => @key_passphrase}, '/usr/bin/sshpass', '-P', 'passphrase', '-e'] if @key_passphrase
|
292
|
-
args = [{'SSHPASS' => @ssh_password}, '/usr/bin/sshpass', '-e'] if @ssh_password
|
293
|
-
args += ['/usr/bin/ssh', @host, ssh_options(with_pty, quiet: quiet), command].flatten
|
294
|
-
end
|
295
|
-
|
296
260
|
# Initiates run of the remote command and yields the data when
|
297
261
|
# available. The yielding doesn't happen automatically, but as
|
298
262
|
# part of calling the `refresh` method.
|
@@ -300,7 +264,9 @@ module Proxy::RemoteExecution::Ssh::Runners
|
|
300
264
|
raise 'Async command already in progress' if @process_manager&.started?
|
301
265
|
|
302
266
|
@user_method.reset
|
303
|
-
|
267
|
+
cmd = @connection.command([tty_flag(true), command].flatten.compact)
|
268
|
+
log_command(cmd)
|
269
|
+
initialize_command(*cmd)
|
304
270
|
|
305
271
|
true
|
306
272
|
end
|
@@ -309,17 +275,15 @@ module Proxy::RemoteExecution::Ssh::Runners
|
|
309
275
|
@process_manager&.started? && @user_method.sent_all_data?
|
310
276
|
end
|
311
277
|
|
278
|
+
def tty_flag(tty)
|
279
|
+
'-tt' if tty
|
280
|
+
end
|
281
|
+
|
312
282
|
def run_sync(command, stdin: nil, close_stdin: true, tty: false, user_method: nil)
|
313
|
-
|
314
|
-
|
315
|
-
|
316
|
-
|
317
|
-
user_method.on_data(data, pm.stdin) if user_method
|
318
|
-
end
|
319
|
-
data
|
320
|
-
end
|
321
|
-
pm.on_stdout(&callback)
|
322
|
-
pm.on_stderr(&callback)
|
283
|
+
cmd = @connection.command([tty_flag(tty), command].flatten.compact)
|
284
|
+
log_command(cmd)
|
285
|
+
pm = Proxy::Dynflow::ProcessManager.new(cmd)
|
286
|
+
set_pm_debug_logging(pm, user_method: user_method)
|
323
287
|
pm.start!
|
324
288
|
unless pm.status
|
325
289
|
pm.stdin.io.puts(stdin) if stdin
|
@@ -429,13 +393,6 @@ module Proxy::RemoteExecution::Ssh::Runners
|
|
429
393
|
@expecting_disconnect = true
|
430
394
|
end
|
431
395
|
end
|
432
|
-
|
433
|
-
def available_authentication_methods
|
434
|
-
methods = %w[publickey] # Always use pubkey auth as fallback
|
435
|
-
methods << 'gssapi-with-mic' if settings[:kerberos_auth]
|
436
|
-
methods.unshift('password') if @ssh_password
|
437
|
-
methods
|
438
|
-
end
|
439
396
|
end
|
440
397
|
# rubocop:enable Metrics/ClassLength
|
441
398
|
end
|
@@ -32,3 +32,7 @@
|
|
32
32
|
# unset, SSL gets used if smart-proxy's foreman_ssl_cert, foreman_ssl_key and
|
33
33
|
# foreman_ssl_ca settings are set available.
|
34
34
|
# :mqtt_tls:
|
35
|
+
|
36
|
+
# The notification is sent over mqtt every $mqtt_resend_interval seconds, until
|
37
|
+
# the job is picked up by the host or cancelled
|
38
|
+
# :mqtt_resend_interval: 900
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: smart_proxy_remote_execution_ssh
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.9.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Ivan Nečas
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2022-
|
11
|
+
date: 2022-11-14 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: bundler
|
@@ -170,10 +170,12 @@ files:
|
|
170
170
|
- lib/smart_proxy_remote_execution_ssh/async_scripts/control.sh
|
171
171
|
- lib/smart_proxy_remote_execution_ssh/async_scripts/retrieve.sh
|
172
172
|
- lib/smart_proxy_remote_execution_ssh/cockpit.rb
|
173
|
+
- lib/smart_proxy_remote_execution_ssh/command_logging.rb
|
173
174
|
- lib/smart_proxy_remote_execution_ssh/dispatcher.rb
|
174
175
|
- lib/smart_proxy_remote_execution_ssh/http_config.ru
|
175
176
|
- lib/smart_proxy_remote_execution_ssh/job_storage.rb
|
176
177
|
- lib/smart_proxy_remote_execution_ssh/log_filter.rb
|
178
|
+
- lib/smart_proxy_remote_execution_ssh/multiplexed_ssh_connection.rb
|
177
179
|
- lib/smart_proxy_remote_execution_ssh/net_ssh_compat.rb
|
178
180
|
- lib/smart_proxy_remote_execution_ssh/plugin.rb
|
179
181
|
- lib/smart_proxy_remote_execution_ssh/runners.rb
|