smart_proxy_remote_execution_ssh 0.5.3 → 0.8.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 +72 -14
- data/lib/smart_proxy_remote_execution_ssh/api.rb +9 -7
- data/lib/smart_proxy_remote_execution_ssh/cockpit.rb +13 -9
- data/lib/smart_proxy_remote_execution_ssh/command_logging.rb +23 -0
- data/lib/smart_proxy_remote_execution_ssh/multiplexed_ssh_connection.rb +196 -0
- data/lib/smart_proxy_remote_execution_ssh/net_ssh_compat.rb +6 -2
- data/lib/smart_proxy_remote_execution_ssh/plugin.rb +11 -2
- data/lib/smart_proxy_remote_execution_ssh/runners/polling_script_runner.rb +17 -9
- data/lib/smart_proxy_remote_execution_ssh/runners/script_runner.rb +86 -110
- data/lib/smart_proxy_remote_execution_ssh/version.rb +1 -1
- data/lib/smart_proxy_remote_execution_ssh.rb +35 -14
- data/settings.d/remote_execution_ssh.yml.example +8 -0
- metadata +7 -5
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 1a599d3732d4f6b064a850ae8baced28354980096785a340828533f953f844c4
|
|
4
|
+
data.tar.gz: 160b80ab983f0eb7b987987191b48c19db42d3b76079b0ba6639ec831dccc6f3
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 349961aa474809225d9781bda09687eb23f00db51cca9f758a57e075fa3d19fc07b729569b064887f91bb7f07190152d6ec9cb01e8ccf48df0841e9c3f85d10e
|
|
7
|
+
data.tar.gz: 8ad40859c92f7d1cd089adf0f37bb0aaa8add745cd461c2e1ed5b484d3ae6b856ac0ee53e61c62e89d564a4ba676b22bf5f01618da970986cd60e42591ff6346
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
require 'mqtt'
|
|
2
2
|
require 'json'
|
|
3
|
+
require 'time'
|
|
3
4
|
|
|
4
5
|
module Proxy::RemoteExecution::Ssh::Actions
|
|
5
6
|
class PullScript < Proxy::Dynflow::Action::Runner
|
|
@@ -25,7 +26,8 @@ module Proxy::RemoteExecution::Ssh::Actions
|
|
|
25
26
|
otp_password = if input[:with_mqtt]
|
|
26
27
|
::Proxy::Dynflow::OtpManager.generate_otp(execution_plan_id)
|
|
27
28
|
end
|
|
28
|
-
|
|
29
|
+
|
|
30
|
+
input[:job_uuid] = job_storage.store_job(host_name, execution_plan_id, run_step_id, input[:script].tr("\r", ''))
|
|
29
31
|
output[:state] = :ready_for_pickup
|
|
30
32
|
output[:result] = []
|
|
31
33
|
mqtt_start(otp_password) if input[:with_mqtt]
|
|
@@ -40,9 +42,40 @@ module Proxy::RemoteExecution::Ssh::Actions
|
|
|
40
42
|
def process_external_event(event)
|
|
41
43
|
output[:state] = :running
|
|
42
44
|
data = event.data
|
|
45
|
+
case data['version']
|
|
46
|
+
when nil
|
|
47
|
+
process_external_unversioned(data)
|
|
48
|
+
when '1'
|
|
49
|
+
process_external_v1(data)
|
|
50
|
+
else
|
|
51
|
+
raise "Unexpected update message version '#{data['version']}'"
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def process_external_unversioned(payload)
|
|
56
|
+
continuous_output = Proxy::Dynflow::ContinuousOutput.new
|
|
57
|
+
Array(payload['output']).each { |line| continuous_output.add_output(line, payload['type']) } if payload.key?('output')
|
|
58
|
+
exit_code = payload['exit_code'].to_i if payload['exit_code']
|
|
59
|
+
process_update(Proxy::Dynflow::Runner::Update.new(continuous_output, exit_code))
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def process_external_v1(payload)
|
|
43
63
|
continuous_output = Proxy::Dynflow::ContinuousOutput.new
|
|
44
|
-
|
|
45
|
-
|
|
64
|
+
exit_code = nil
|
|
65
|
+
|
|
66
|
+
payload['updates'].each do |update|
|
|
67
|
+
time = Time.parse update['timestamp']
|
|
68
|
+
type = update['type']
|
|
69
|
+
case type
|
|
70
|
+
when 'output'
|
|
71
|
+
continuous_output.add_output(update['content'], update['stream'], time)
|
|
72
|
+
when 'exit'
|
|
73
|
+
exit_code = update['exit_code'].to_i
|
|
74
|
+
else
|
|
75
|
+
raise "Unexpected update type '#{update['type']}'"
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
|
|
46
79
|
process_update(Proxy::Dynflow::Runner::Update.new(continuous_output, exit_code))
|
|
47
80
|
end
|
|
48
81
|
|
|
@@ -56,37 +89,52 @@ module Proxy::RemoteExecution::Ssh::Actions
|
|
|
56
89
|
# Client was notified or is already running, dealing with this situation
|
|
57
90
|
# is only supported if mqtt is available
|
|
58
91
|
# Otherwise we have to wait it out
|
|
59
|
-
|
|
60
|
-
# if input[:with_mqtt]
|
|
92
|
+
mqtt_cancel if input[:with_mqtt]
|
|
61
93
|
end
|
|
62
94
|
suspend
|
|
63
95
|
end
|
|
64
96
|
|
|
65
97
|
def mqtt_start(otp_password)
|
|
66
|
-
payload =
|
|
67
|
-
|
|
68
|
-
message_id: SecureRandom.uuid,
|
|
69
|
-
version: 1,
|
|
70
|
-
sent: DateTime.now.iso8601,
|
|
71
|
-
directive: 'foreman',
|
|
98
|
+
payload = mqtt_payload_base.merge(
|
|
99
|
+
content: "#{input[:proxy_url]}/ssh/jobs/#{input[:job_uuid]}",
|
|
72
100
|
metadata: {
|
|
101
|
+
'event': 'start',
|
|
73
102
|
'job_uuid': input[:job_uuid],
|
|
74
103
|
'username': execution_plan_id,
|
|
75
104
|
'password': otp_password,
|
|
76
105
|
'return_url': "#{input[:proxy_url]}/ssh/jobs/#{input[:job_uuid]}/update",
|
|
77
106
|
},
|
|
78
|
-
|
|
79
|
-
}
|
|
107
|
+
)
|
|
80
108
|
mqtt_notify payload
|
|
81
109
|
output[:state] = :notified
|
|
82
110
|
end
|
|
83
111
|
|
|
112
|
+
def mqtt_cancel
|
|
113
|
+
cleanup
|
|
114
|
+
payload = mqtt_payload_base.merge(
|
|
115
|
+
metadata: {
|
|
116
|
+
'event': 'cancel',
|
|
117
|
+
'job_uuid': input[:job_uuid]
|
|
118
|
+
}
|
|
119
|
+
)
|
|
120
|
+
mqtt_notify payload
|
|
121
|
+
end
|
|
122
|
+
|
|
84
123
|
def mqtt_notify(payload)
|
|
85
|
-
|
|
124
|
+
with_mqtt_client do |c|
|
|
86
125
|
c.publish(mqtt_topic, JSON.dump(payload), false, 1)
|
|
87
126
|
end
|
|
88
127
|
end
|
|
89
128
|
|
|
129
|
+
def with_mqtt_client(&block)
|
|
130
|
+
MQTT::Client.connect(settings.mqtt_broker, settings.mqtt_port,
|
|
131
|
+
:ssl => settings.mqtt_tls,
|
|
132
|
+
:cert_file => ::Proxy::SETTINGS.foreman_ssl_cert || ::Proxy::SETTINGS.ssl_certificate,
|
|
133
|
+
:key_file => ::Proxy::SETTINGS.foreman_ssl_key || ::Proxy::SETTINGS.ssl_private_key,
|
|
134
|
+
:ca_file => ::Proxy::SETTINGS.foreman_ssl_ca || ::Proxy::SETTINGS.ssl_ca_file,
|
|
135
|
+
&block)
|
|
136
|
+
end
|
|
137
|
+
|
|
90
138
|
def host_name
|
|
91
139
|
alternative_names = input.fetch(:alternative_names, {})
|
|
92
140
|
|
|
@@ -106,5 +154,15 @@ module Proxy::RemoteExecution::Ssh::Actions
|
|
|
106
154
|
def job_storage
|
|
107
155
|
Proxy::RemoteExecution::Ssh.job_storage
|
|
108
156
|
end
|
|
157
|
+
|
|
158
|
+
def mqtt_payload_base
|
|
159
|
+
{
|
|
160
|
+
type: 'data',
|
|
161
|
+
message_id: SecureRandom.uuid,
|
|
162
|
+
version: 1,
|
|
163
|
+
sent: DateTime.now.iso8601,
|
|
164
|
+
directive: 'foreman'
|
|
165
|
+
}
|
|
166
|
+
end
|
|
109
167
|
end
|
|
110
168
|
end
|
|
@@ -13,14 +13,16 @@ module Proxy::RemoteExecution
|
|
|
13
13
|
File.read(Ssh.public_key_file)
|
|
14
14
|
end
|
|
15
15
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
16
|
+
if Proxy::RemoteExecution::Ssh::Plugin.settings.cockpit_integration
|
|
17
|
+
post "/session" do
|
|
18
|
+
do_authorize_any
|
|
19
|
+
session = Cockpit::Session.new(env)
|
|
20
|
+
unless session.valid?
|
|
21
|
+
return [ 400, "Invalid request: /ssh/session requires connection upgrade to 'raw'" ]
|
|
22
|
+
end
|
|
23
|
+
session.hijack!
|
|
24
|
+
101
|
|
21
25
|
end
|
|
22
|
-
session.hijack!
|
|
23
|
-
101
|
|
24
26
|
end
|
|
25
27
|
|
|
26
28
|
delete '/known_hosts/:name' do |name|
|
|
@@ -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,19 +176,22 @@ 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)
|
|
182
184
|
err_buf_raw = ''
|
|
183
|
-
readers = [buf_socket, out_buf, err_buf]
|
|
184
185
|
loop do
|
|
186
|
+
readers = [buf_socket, out_buf, err_buf].reject { |io| io.closed? }
|
|
187
|
+
writers = [buf_socket, in_buf].select { |io| io.pending_writes? }
|
|
185
188
|
# Prime the sockets for reading
|
|
186
|
-
ready_readers, ready_writers = IO.select(readers,
|
|
189
|
+
ready_readers, ready_writers = IO.select(readers, writers)
|
|
187
190
|
(ready_readers || []).each { |reader| reader.close if reader.fill.zero? }
|
|
188
191
|
|
|
189
192
|
proxy_data(out_buf, in_buf)
|
|
190
193
|
if buf_socket.closed?
|
|
191
|
-
|
|
194
|
+
connection.disconnect!
|
|
192
195
|
end
|
|
193
196
|
|
|
194
197
|
if out_buf.closed?
|
|
@@ -262,10 +265,10 @@ module Proxy::RemoteExecution
|
|
|
262
265
|
params["hostname"]
|
|
263
266
|
end
|
|
264
267
|
|
|
265
|
-
def
|
|
266
|
-
@
|
|
268
|
+
def connection
|
|
269
|
+
@connection ||= Proxy::RemoteExecution::Ssh::Runners::MultiplexedSSHConnection.new(
|
|
267
270
|
runner_params,
|
|
268
|
-
|
|
271
|
+
logger: logger
|
|
269
272
|
)
|
|
270
273
|
end
|
|
271
274
|
|
|
@@ -278,6 +281,7 @@ module Proxy::RemoteExecution
|
|
|
278
281
|
# For compatibility only
|
|
279
282
|
ret[:script] = nil
|
|
280
283
|
ret[:hostname] = host
|
|
284
|
+
ret[:id] = SecureRandom.uuid
|
|
281
285
|
ret
|
|
282
286
|
end
|
|
283
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
|
+
''
|
|
18
|
+
end
|
|
19
|
+
pm.on_stdout(&callback)
|
|
20
|
+
pm.on_stderr(&callback)
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
@@ -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)}, '/usr/bin/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
|
+
['/usr/bin/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, '/usr/bin/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 = ['/usr/bin/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
|
|
@@ -173,10 +173,14 @@ module Proxy::RemoteExecution
|
|
|
173
173
|
output.append(data)
|
|
174
174
|
end
|
|
175
175
|
|
|
176
|
+
def pending_writes?
|
|
177
|
+
output.length.positive?
|
|
178
|
+
end
|
|
179
|
+
|
|
176
180
|
# Sends as much of the pending output as possible. Returns +true+ if any
|
|
177
181
|
# data was sent, and +false+ otherwise.
|
|
178
182
|
def send_pending
|
|
179
|
-
if
|
|
183
|
+
if pending_writes?
|
|
180
184
|
sent = send(output.to_s, 0)
|
|
181
185
|
output.consume!(sent)
|
|
182
186
|
return sent.positive?
|
|
@@ -189,7 +193,7 @@ module Proxy::RemoteExecution
|
|
|
189
193
|
# buffer is empty.
|
|
190
194
|
def wait_for_pending_sends
|
|
191
195
|
send_pending
|
|
192
|
-
while
|
|
196
|
+
while pending_writes?
|
|
193
197
|
result = IO.select(nil, [self]) || next
|
|
194
198
|
next unless result[1].any?
|
|
195
199
|
|
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
module Proxy::RemoteExecution::Ssh
|
|
2
2
|
class Plugin < Proxy::Plugin
|
|
3
3
|
SSH_LOG_LEVELS = %w[debug info error fatal].freeze
|
|
4
|
-
MODES = %i[ssh async
|
|
4
|
+
MODES = %i[ssh ssh-async pull pull-mqtt].freeze
|
|
5
|
+
# Unix domain socket path length is limited to 104 (on some platforms) characters
|
|
6
|
+
# Socket path is composed of custom path (max 49 characters) + job id (37 characters)
|
|
7
|
+
# + offset(17 characters) + null terminator
|
|
8
|
+
SOCKET_PATH_MAX_LENGTH = 49
|
|
5
9
|
|
|
6
10
|
http_rackup_path File.expand_path("http_config.ru", File.expand_path("../", __FILE__))
|
|
7
11
|
https_rackup_path File.expand_path("http_config.ru", File.expand_path("../", __FILE__))
|
|
@@ -11,16 +15,21 @@ module Proxy::RemoteExecution::Ssh
|
|
|
11
15
|
:ssh_user => 'root',
|
|
12
16
|
:remote_working_dir => '/var/tmp',
|
|
13
17
|
:local_working_dir => '/var/tmp',
|
|
18
|
+
:socket_working_dir => '/var/tmp',
|
|
14
19
|
:kerberos_auth => false,
|
|
20
|
+
:cockpit_integration => true,
|
|
15
21
|
# When set to nil, makes REX use the runner's default interval
|
|
16
22
|
# :runner_refresh_interval => nil,
|
|
17
23
|
:ssh_log_level => :error,
|
|
18
24
|
:cleanup_working_dirs => true,
|
|
19
25
|
# :mqtt_broker => nil,
|
|
20
26
|
# :mqtt_port => nil,
|
|
27
|
+
# :mqtt_tls => nil,
|
|
21
28
|
:mode => :ssh
|
|
22
29
|
|
|
23
|
-
|
|
30
|
+
capability(proc { 'cockpit' if settings.cockpit_integration })
|
|
31
|
+
|
|
32
|
+
plugin :script, Proxy::RemoteExecution::Ssh::VERSION
|
|
24
33
|
after_activation do
|
|
25
34
|
require 'smart_proxy_dynflow'
|
|
26
35
|
require 'smart_proxy_remote_execution_ssh/version'
|
|
@@ -36,13 +36,12 @@ module Proxy::RemoteExecution::Ssh::Runners
|
|
|
36
36
|
def initialization_script
|
|
37
37
|
close_stdin = '</dev/null'
|
|
38
38
|
close_fds = close_stdin + ' >/dev/null 2>/dev/null'
|
|
39
|
-
main_script = "(#{@remote_script} #{close_stdin} 2>&1; echo $?>#{@base_dir}/init_exit_code) >#{@base_dir}/output"
|
|
39
|
+
main_script = "(#{@remote_script_wrapper} #{@remote_script} #{close_stdin} 2>&1; echo $?>#{@base_dir}/init_exit_code) >#{@base_dir}/output"
|
|
40
40
|
control_script_finish = "#{@control_script_path} init-script-finish"
|
|
41
41
|
<<-SCRIPT.gsub(/^ +\| /, '')
|
|
42
42
|
| export CONTROL_SCRIPT="#{@control_script_path}"
|
|
43
|
-
| #{"chown #{@user_method.effective_user}
|
|
43
|
+
| #{"chown #{@user_method.effective_user} #{@base_dir}" if @user_method.cli_command_prefix}
|
|
44
44
|
| #{@user_method.cli_command_prefix} sh -c '#{main_script}; #{control_script_finish}' #{close_fds} &
|
|
45
|
-
| echo $! > '#{@base_dir}/pid'
|
|
46
45
|
SCRIPT
|
|
47
46
|
end
|
|
48
47
|
|
|
@@ -51,18 +50,23 @@ module Proxy::RemoteExecution::Ssh::Runners
|
|
|
51
50
|
end
|
|
52
51
|
|
|
53
52
|
def refresh
|
|
54
|
-
|
|
53
|
+
@connection.establish! unless @connection.connected?
|
|
55
54
|
begin
|
|
56
|
-
|
|
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)
|
|
57
57
|
rescue StandardError => e
|
|
58
58
|
@logger.info("Error while connecting to the remote host on refresh: #{e.message}")
|
|
59
59
|
end
|
|
60
|
-
|
|
61
|
-
process_retrieved_data(output, err)
|
|
62
60
|
ensure
|
|
63
61
|
destroy_session
|
|
64
62
|
end
|
|
65
63
|
|
|
64
|
+
def kill
|
|
65
|
+
run_sync("pkill -P $(cat #{@pid_path})")
|
|
66
|
+
rescue StandardError => e
|
|
67
|
+
publish_exception('Unexpected error', e, false)
|
|
68
|
+
end
|
|
69
|
+
|
|
66
70
|
def process_retrieved_data(output, err)
|
|
67
71
|
return if output.nil? || output.empty?
|
|
68
72
|
|
|
@@ -127,11 +131,15 @@ module Proxy::RemoteExecution::Ssh::Runners
|
|
|
127
131
|
end
|
|
128
132
|
|
|
129
133
|
def cleanup
|
|
130
|
-
|
|
134
|
+
if @cleanup_working_dirs
|
|
135
|
+
ensure_remote_command("rm -rf #{remote_command_dir}",
|
|
136
|
+
publish: true,
|
|
137
|
+
error: "Unable to remove working directory #{remote_command_dir} on remote system, exit code: %{exit_code}")
|
|
138
|
+
end
|
|
131
139
|
end
|
|
132
140
|
|
|
133
141
|
def destroy_session
|
|
134
|
-
if @
|
|
142
|
+
if @connection.connected?
|
|
135
143
|
@logger.debug("Closing session with #{@ssh_user}@#{@host}")
|
|
136
144
|
close_session
|
|
137
145
|
end
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
require 'fileutils'
|
|
2
|
-
require 'smart_proxy_dynflow/runner/
|
|
2
|
+
require 'smart_proxy_dynflow/runner/process_manager_command'
|
|
3
|
+
require 'smart_proxy_dynflow/process_manager'
|
|
4
|
+
require 'smart_proxy_remote_execution_ssh/multiplexed_ssh_connection'
|
|
3
5
|
|
|
4
6
|
module Proxy::RemoteExecution::Ssh::Runners
|
|
5
7
|
class EffectiveUserMethod
|
|
@@ -12,9 +14,9 @@ module Proxy::RemoteExecution::Ssh::Runners
|
|
|
12
14
|
@password_sent = false
|
|
13
15
|
end
|
|
14
16
|
|
|
15
|
-
def on_data(received_data,
|
|
17
|
+
def on_data(received_data, io_buffer)
|
|
16
18
|
if received_data.match(login_prompt)
|
|
17
|
-
|
|
19
|
+
io_buffer.add_data(effective_user_password + "\n")
|
|
18
20
|
@password_sent = true
|
|
19
21
|
end
|
|
20
22
|
end
|
|
@@ -90,7 +92,9 @@ module Proxy::RemoteExecution::Ssh::Runners
|
|
|
90
92
|
|
|
91
93
|
# rubocop:disable Metrics/ClassLength
|
|
92
94
|
class ScriptRunner < Proxy::Dynflow::Runner::Base
|
|
93
|
-
include Proxy::Dynflow::Runner::
|
|
95
|
+
include Proxy::Dynflow::Runner::ProcessManagerCommand
|
|
96
|
+
include CommandLogging
|
|
97
|
+
|
|
94
98
|
attr_reader :execution_timeout_interval
|
|
95
99
|
|
|
96
100
|
EXPECTED_POWER_ACTION_MESSAGES = ['restart host', 'shutdown host'].freeze
|
|
@@ -102,18 +106,17 @@ module Proxy::RemoteExecution::Ssh::Runners
|
|
|
102
106
|
@script = options.fetch(:script)
|
|
103
107
|
@ssh_user = options.fetch(:ssh_user, 'root')
|
|
104
108
|
@ssh_port = options.fetch(:ssh_port, 22)
|
|
105
|
-
@ssh_password = options.fetch(:secrets, {}).fetch(:ssh_password, nil)
|
|
106
|
-
@key_passphrase = options.fetch(:secrets, {}).fetch(:key_passphrase, nil)
|
|
107
109
|
@host_public_key = options.fetch(:host_public_key, nil)
|
|
108
|
-
@verify_host = options.fetch(:verify_host, nil)
|
|
109
110
|
@execution_timeout_interval = options.fetch(:execution_timeout_interval, nil)
|
|
110
111
|
|
|
111
112
|
@client_private_key_file = settings.ssh_identity_key_file
|
|
112
113
|
@local_working_dir = options.fetch(:local_working_dir, settings.local_working_dir)
|
|
113
|
-
@remote_working_dir = options.fetch(:remote_working_dir, settings.remote_working_dir)
|
|
114
|
+
@remote_working_dir = options.fetch(:remote_working_dir, settings.remote_working_dir.shellescape)
|
|
115
|
+
@socket_working_dir = options.fetch(:socket_working_dir, settings.socket_working_dir)
|
|
114
116
|
@cleanup_working_dirs = options.fetch(:cleanup_working_dirs, settings.cleanup_working_dirs)
|
|
115
117
|
@first_execution = options.fetch(:first_execution, false)
|
|
116
118
|
@user_method = user_method
|
|
119
|
+
@options = options
|
|
117
120
|
end
|
|
118
121
|
|
|
119
122
|
def self.build(options, suspended_action:)
|
|
@@ -141,6 +144,10 @@ module Proxy::RemoteExecution::Ssh::Runners
|
|
|
141
144
|
|
|
142
145
|
def start
|
|
143
146
|
Proxy::RemoteExecution::Utils.prune_known_hosts!(@host, @ssh_port, logger) if @first_execution
|
|
147
|
+
ensure_local_directory(@socket_working_dir)
|
|
148
|
+
@connection = MultiplexedSSHConnection.new(@options.merge(:id => @id), logger: logger)
|
|
149
|
+
@connection.establish!
|
|
150
|
+
preflight_checks
|
|
144
151
|
prepare_start
|
|
145
152
|
script = initialization_script
|
|
146
153
|
logger.debug("executing script:\n#{indent_multiline(script)}")
|
|
@@ -154,32 +161,51 @@ module Proxy::RemoteExecution::Ssh::Runners
|
|
|
154
161
|
run_async(*args)
|
|
155
162
|
end
|
|
156
163
|
|
|
164
|
+
def preflight_checks
|
|
165
|
+
ensure_remote_command(cp_script_to_remote("#!/bin/sh\nexec true", 'test'),
|
|
166
|
+
error: 'Failed to execute script on remote machine, exit code: %{exit_code}.'
|
|
167
|
+
)
|
|
168
|
+
unless @user_method.is_a? NoopUserMethod
|
|
169
|
+
path = cp_script_to_remote("#!/bin/sh\nexec #{@user_method.cli_command_prefix} true", 'effective-user-test')
|
|
170
|
+
ensure_remote_command(path,
|
|
171
|
+
error: 'Failed to change to effective user, exit code: %{exit_code}',
|
|
172
|
+
tty: true,
|
|
173
|
+
user_method: @user_method,
|
|
174
|
+
close_stdin: false)
|
|
175
|
+
end
|
|
176
|
+
end
|
|
177
|
+
|
|
157
178
|
def prepare_start
|
|
158
179
|
@remote_script = cp_script_to_remote
|
|
159
180
|
@output_path = File.join(File.dirname(@remote_script), 'output')
|
|
160
181
|
@exit_code_path = File.join(File.dirname(@remote_script), 'exit_code')
|
|
182
|
+
@pid_path = File.join(File.dirname(@remote_script), 'pid')
|
|
183
|
+
@remote_script_wrapper = upload_data("echo $$ > #{@pid_path}; exec \"$@\";", File.join(File.dirname(@remote_script), 'script-wrapper'), 555)
|
|
161
184
|
end
|
|
162
185
|
|
|
163
186
|
# the script that initiates the execution
|
|
164
187
|
def initialization_script
|
|
165
188
|
su_method = @user_method.instance_of?(SuUserMethod)
|
|
166
189
|
# pipe the output to tee while capturing the exit code in a file
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
190
|
+
<<~SCRIPT
|
|
191
|
+
sh <<EOF | /usr/bin/tee #{@output_path}
|
|
192
|
+
#{@remote_script_wrapper} #{@user_method.cli_command_prefix}#{su_method ? "'#{@remote_script} < /dev/null '" : "#{@remote_script} < /dev/null"}
|
|
193
|
+
echo \\$?>#{@exit_code_path}
|
|
194
|
+
EOF
|
|
195
|
+
exit $(cat #{@exit_code_path})
|
|
170
196
|
SCRIPT
|
|
171
197
|
end
|
|
172
198
|
|
|
173
199
|
def refresh
|
|
174
|
-
return if @
|
|
200
|
+
return if @process_manager.nil?
|
|
175
201
|
super
|
|
176
202
|
ensure
|
|
177
203
|
check_expecting_disconnect
|
|
178
204
|
end
|
|
179
205
|
|
|
180
206
|
def kill
|
|
181
|
-
if @
|
|
182
|
-
run_sync("pkill -
|
|
207
|
+
if @process_manager&.started?
|
|
208
|
+
run_sync("pkill -P $(cat #{@pid_path})")
|
|
183
209
|
else
|
|
184
210
|
logger.debug('connection closed')
|
|
185
211
|
end
|
|
@@ -197,28 +223,24 @@ module Proxy::RemoteExecution::Ssh::Runners
|
|
|
197
223
|
end
|
|
198
224
|
|
|
199
225
|
def close_session
|
|
200
|
-
|
|
201
|
-
raise 'Control socket file does not exist' unless File.exist?(local_command_file("socket"))
|
|
226
|
+
raise 'Control socket file does not exist' unless File.exist?(socket_file)
|
|
202
227
|
@logger.debug("Sending exit request for session #{@ssh_user}@#{@host}")
|
|
203
|
-
|
|
204
|
-
pid, *, err = session(args, in_stream: false, out_stream: false)
|
|
205
|
-
result = read_output_debug(err)
|
|
206
|
-
Process.wait(pid)
|
|
207
|
-
result
|
|
228
|
+
@connection.disconnect!
|
|
208
229
|
end
|
|
209
230
|
|
|
210
231
|
def close
|
|
211
|
-
run_sync("rm -rf
|
|
232
|
+
run_sync("rm -rf #{remote_command_dir}") if should_cleanup?
|
|
212
233
|
rescue StandardError => e
|
|
213
234
|
publish_exception('Error when removing remote working dir', e, false)
|
|
214
235
|
ensure
|
|
215
|
-
close_session if @
|
|
236
|
+
close_session if @process_manager
|
|
216
237
|
FileUtils.rm_rf(local_command_dir) if Dir.exist?(local_command_dir) && @cleanup_working_dirs
|
|
217
238
|
end
|
|
218
239
|
|
|
219
|
-
def publish_data(data, type)
|
|
240
|
+
def publish_data(data, type, pm = nil)
|
|
241
|
+
pm ||= @process_manager
|
|
220
242
|
super(data.force_encoding('UTF-8'), type) unless @user_method.filter_password?(data)
|
|
221
|
-
@user_method.on_data(data,
|
|
243
|
+
@user_method.on_data(data, pm.stdin) if pm
|
|
222
244
|
end
|
|
223
245
|
|
|
224
246
|
private
|
|
@@ -228,94 +250,47 @@ module Proxy::RemoteExecution::Ssh::Runners
|
|
|
228
250
|
end
|
|
229
251
|
|
|
230
252
|
def should_cleanup?
|
|
231
|
-
@
|
|
232
|
-
end
|
|
233
|
-
|
|
234
|
-
# Creates session with three pipes - one for reading and two for
|
|
235
|
-
# writing. Similar to `Open3.popen3` method but without creating
|
|
236
|
-
# a separate thread to monitor it.
|
|
237
|
-
def session(args, in_stream: true, out_stream: true, err_stream: true)
|
|
238
|
-
@session = true
|
|
239
|
-
|
|
240
|
-
in_read, in_write = in_stream ? IO.pipe : '/dev/null'
|
|
241
|
-
out_read, out_write = out_stream ? IO.pipe : [nil, '/dev/null']
|
|
242
|
-
err_read, err_write = err_stream ? IO.pipe : [nil, '/dev/null']
|
|
243
|
-
command_pid = spawn(*args, :in => in_read, :out => out_write, :err => err_write)
|
|
244
|
-
in_read.close if in_stream
|
|
245
|
-
out_write.close if out_stream
|
|
246
|
-
err_write.close if err_stream
|
|
247
|
-
|
|
248
|
-
return command_pid, in_write, out_read, err_read
|
|
249
|
-
end
|
|
250
|
-
|
|
251
|
-
def ssh_options(with_pty = false)
|
|
252
|
-
ssh_options = []
|
|
253
|
-
ssh_options << "-tt" if with_pty
|
|
254
|
-
ssh_options << "-o User=#{@ssh_user}"
|
|
255
|
-
ssh_options << "-o Port=#{@ssh_port}" if @ssh_port
|
|
256
|
-
ssh_options << "-o IdentityFile=#{@client_private_key_file}" if @client_private_key_file
|
|
257
|
-
ssh_options << "-o IdentitiesOnly=yes"
|
|
258
|
-
ssh_options << "-o StrictHostKeyChecking=no"
|
|
259
|
-
ssh_options << "-o PreferredAuthentications=#{available_authentication_methods.join(',')}"
|
|
260
|
-
ssh_options << "-o UserKnownHostsFile=#{prepare_known_hosts}" if @host_public_key
|
|
261
|
-
ssh_options << "-o NumberOfPasswordPrompts=1"
|
|
262
|
-
ssh_options << "-o LogLevel=#{settings[:ssh_log_level]}"
|
|
263
|
-
ssh_options << "-o ControlMaster=auto"
|
|
264
|
-
ssh_options << "-o ControlPath=#{local_command_file("socket")}"
|
|
265
|
-
ssh_options << "-o ControlPersist=yes"
|
|
253
|
+
@process_manager && @cleanup_working_dirs
|
|
266
254
|
end
|
|
267
255
|
|
|
268
256
|
def settings
|
|
269
257
|
Proxy::RemoteExecution::Ssh::Plugin.settings
|
|
270
258
|
end
|
|
271
259
|
|
|
272
|
-
def get_args(command, with_pty = false)
|
|
273
|
-
args = []
|
|
274
|
-
args = [{'SSHPASS' => @key_passphrase}, '/usr/bin/sshpass', '-P', 'passphrase', '-e'] if @key_passphrase
|
|
275
|
-
args = [{'SSHPASS' => @ssh_password}, '/usr/bin/sshpass', '-e'] if @ssh_password
|
|
276
|
-
args += ['/usr/bin/ssh', @host, ssh_options(with_pty), command].flatten
|
|
277
|
-
end
|
|
278
|
-
|
|
279
260
|
# Initiates run of the remote command and yields the data when
|
|
280
261
|
# available. The yielding doesn't happen automatically, but as
|
|
281
262
|
# part of calling the `refresh` method.
|
|
282
263
|
def run_async(command)
|
|
283
|
-
raise 'Async command already in progress' if @started
|
|
264
|
+
raise 'Async command already in progress' if @process_manager&.started?
|
|
284
265
|
|
|
285
|
-
@started = false
|
|
286
266
|
@user_method.reset
|
|
287
|
-
|
|
288
|
-
|
|
267
|
+
cmd = @connection.command([tty_flag(true), command].flatten.compact)
|
|
268
|
+
log_command(cmd)
|
|
269
|
+
initialize_command(*cmd)
|
|
289
270
|
|
|
290
|
-
|
|
271
|
+
true
|
|
291
272
|
end
|
|
292
273
|
|
|
293
274
|
def run_started?
|
|
294
|
-
@started && @user_method.sent_all_data?
|
|
275
|
+
@process_manager&.started? && @user_method.sent_all_data?
|
|
295
276
|
end
|
|
296
277
|
|
|
297
|
-
def
|
|
298
|
-
|
|
299
|
-
debug_str = ''
|
|
300
|
-
|
|
301
|
-
if out_io
|
|
302
|
-
stdout += out_io.read until out_io.eof? rescue
|
|
303
|
-
out_io.close
|
|
304
|
-
end
|
|
305
|
-
debug_str += err_io.read until err_io.eof? rescue
|
|
306
|
-
err_io.close
|
|
307
|
-
debug_str.lines.each { |line| @logger.debug(line.strip) }
|
|
308
|
-
|
|
309
|
-
return stdout, debug_str
|
|
278
|
+
def tty_flag(tty)
|
|
279
|
+
'-tt' if tty
|
|
310
280
|
end
|
|
311
281
|
|
|
312
|
-
def run_sync(command, stdin
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
282
|
+
def run_sync(command, stdin: nil, close_stdin: true, tty: false, user_method: nil)
|
|
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)
|
|
287
|
+
pm.start!
|
|
288
|
+
unless pm.status
|
|
289
|
+
pm.stdin.io.puts(stdin) if stdin
|
|
290
|
+
pm.stdin.io.close if close_stdin
|
|
291
|
+
pm.run!
|
|
292
|
+
end
|
|
293
|
+
pm
|
|
319
294
|
end
|
|
320
295
|
|
|
321
296
|
def prepare_known_hosts
|
|
@@ -334,6 +309,10 @@ module Proxy::RemoteExecution::Ssh::Runners
|
|
|
334
309
|
File.join(ensure_local_directory(local_command_dir), filename)
|
|
335
310
|
end
|
|
336
311
|
|
|
312
|
+
def socket_file
|
|
313
|
+
File.join(ensure_local_directory(@socket_working_dir), @id)
|
|
314
|
+
end
|
|
315
|
+
|
|
337
316
|
def remote_command_dir
|
|
338
317
|
File.join(@remote_working_dir, "foreman-ssh-cmd-#{id}")
|
|
339
318
|
end
|
|
@@ -362,15 +341,13 @@ module Proxy::RemoteExecution::Ssh::Runners
|
|
|
362
341
|
# We use tee here to pipe stdin coming from ssh to a file at $path, while silencing its output
|
|
363
342
|
# This is used to write to $path with elevated permissions, solutions using cat and output redirection
|
|
364
343
|
# would not work, because the redirection would happen in the non-elevated shell.
|
|
365
|
-
command = "tee
|
|
344
|
+
command = "tee #{path} >/dev/null && chmod #{permissions} #{path}"
|
|
366
345
|
|
|
367
346
|
@logger.debug("Sending data to #{path} on remote host:\n#{data}")
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
raise "Unable to upload file to #{path} on remote system: exit code: #{status}"
|
|
373
|
-
end
|
|
347
|
+
ensure_remote_command(command,
|
|
348
|
+
stdin: data,
|
|
349
|
+
error: "Unable to upload file to #{path} on remote system, exit code: %{exit_code}"
|
|
350
|
+
)
|
|
374
351
|
|
|
375
352
|
path
|
|
376
353
|
end
|
|
@@ -382,9 +359,15 @@ module Proxy::RemoteExecution::Ssh::Runners
|
|
|
382
359
|
end
|
|
383
360
|
|
|
384
361
|
def ensure_remote_directory(path)
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
362
|
+
ensure_remote_command("mkdir -p #{path}",
|
|
363
|
+
error: "Unable to create directory #{path} on remote system, exit code: %{exit_code}"
|
|
364
|
+
)
|
|
365
|
+
end
|
|
366
|
+
|
|
367
|
+
def ensure_remote_command(cmd, error: nil, **kwargs)
|
|
368
|
+
if (pm = run_sync(cmd, **kwargs)).status != 0
|
|
369
|
+
msg = error || 'Failed to run command %{command} on remote machine, exit code: %{exit_code}'
|
|
370
|
+
raise(msg % { command: cmd, exit_code: pm.status })
|
|
388
371
|
end
|
|
389
372
|
end
|
|
390
373
|
|
|
@@ -410,13 +393,6 @@ module Proxy::RemoteExecution::Ssh::Runners
|
|
|
410
393
|
@expecting_disconnect = true
|
|
411
394
|
end
|
|
412
395
|
end
|
|
413
|
-
|
|
414
|
-
def available_authentication_methods
|
|
415
|
-
methods = %w[publickey] # Always use pubkey auth as fallback
|
|
416
|
-
methods << 'gssapi-with-mic' if settings[:kerberos_auth]
|
|
417
|
-
methods.unshift('password') if @ssh_password
|
|
418
|
-
methods
|
|
419
|
-
end
|
|
420
396
|
end
|
|
421
397
|
# rubocop:enable Metrics/ClassLength
|
|
422
398
|
end
|
|
@@ -7,22 +7,10 @@ module Proxy::RemoteExecution
|
|
|
7
7
|
module Ssh
|
|
8
8
|
class << self
|
|
9
9
|
def validate!
|
|
10
|
-
unless private_key_file
|
|
11
|
-
raise "settings for `ssh_identity_key` not set"
|
|
12
|
-
end
|
|
13
|
-
|
|
14
|
-
unless File.exist?(private_key_file)
|
|
15
|
-
raise "Ssh public key file #{private_key_file} doesn't exist.\n"\
|
|
16
|
-
"You can generate one with `ssh-keygen -t rsa -b 4096 -f #{private_key_file} -N ''`"
|
|
17
|
-
end
|
|
18
|
-
|
|
19
|
-
unless File.exist?(public_key_file)
|
|
20
|
-
raise "Ssh public key file #{public_key_file} doesn't exist"
|
|
21
|
-
end
|
|
22
|
-
|
|
23
10
|
validate_mode!
|
|
24
|
-
|
|
11
|
+
validate_ssh_settings!
|
|
25
12
|
validate_mqtt_settings!
|
|
13
|
+
validate_socket_path!
|
|
26
14
|
end
|
|
27
15
|
|
|
28
16
|
def private_key_file
|
|
@@ -60,6 +48,28 @@ module Proxy::RemoteExecution
|
|
|
60
48
|
|
|
61
49
|
raise 'mqtt_broker has to be set when pull-mqtt mode is used' if Plugin.settings.mqtt_broker.nil?
|
|
62
50
|
raise 'mqtt_port has to be set when pull-mqtt mode is used' if Plugin.settings.mqtt_port.nil?
|
|
51
|
+
|
|
52
|
+
if Plugin.settings.mqtt_tls.nil?
|
|
53
|
+
Plugin.settings.mqtt_tls = [[:foreman_ssl_cert, :ssl_certificate], [:foreman_ssl_key, :ssl_private_key], [:foreman_ssl_ca, :ssl_ca_file]].all? { |(client, server)| ::Proxy::SETTINGS[client] || ::Proxy::SETTINGS[server] }
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def validate_ssh_settings!
|
|
58
|
+
return unless requires_configured_ssh?
|
|
59
|
+
unless private_key_file
|
|
60
|
+
raise "settings for `ssh_identity_key` not set"
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
unless File.exist?(private_key_file)
|
|
64
|
+
raise "SSH public key file #{private_key_file} doesn't exist.\n"\
|
|
65
|
+
"You can generate one with `ssh-keygen -t rsa -b 4096 -f #{private_key_file} -N ''`"
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
unless File.exist?(public_key_file)
|
|
69
|
+
raise "SSH public key file #{public_key_file} doesn't exist"
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
validate_ssh_log_level!
|
|
63
73
|
end
|
|
64
74
|
|
|
65
75
|
def validate_ssh_log_level!
|
|
@@ -83,6 +93,17 @@ module Proxy::RemoteExecution
|
|
|
83
93
|
Plugin.settings.ssh_log_level = Plugin.settings.ssh_log_level.to_sym
|
|
84
94
|
end
|
|
85
95
|
|
|
96
|
+
def requires_configured_ssh?
|
|
97
|
+
%i[ssh ssh-async].include?(Plugin.settings.mode) || Plugin.settings.cockpit_integration
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def validate_socket_path!
|
|
101
|
+
return unless Plugin.settings.mode == :'ssh' || Plugin.settings.mode == :'ssh-async'
|
|
102
|
+
|
|
103
|
+
socket_path = File.expand_path(Plugin.settings.socket_working_dir)
|
|
104
|
+
raise "Socket path #{socket_path} is too long" if socket_path.length > Plugin::SOCKET_PATH_MAX_LENGTH
|
|
105
|
+
end
|
|
106
|
+
|
|
86
107
|
def job_storage
|
|
87
108
|
@job_storage ||= Proxy::RemoteExecution::Ssh::JobStorage.new
|
|
88
109
|
end
|
|
@@ -3,8 +3,11 @@
|
|
|
3
3
|
:ssh_identity_key_file: '~/.ssh/id_rsa_foreman_proxy'
|
|
4
4
|
:local_working_dir: '/var/tmp'
|
|
5
5
|
:remote_working_dir: '/var/tmp'
|
|
6
|
+
:socket_working_dir: '/var/tmp'
|
|
6
7
|
# :kerberos_auth: false
|
|
7
8
|
|
|
9
|
+
# :cockpit_integration: true
|
|
10
|
+
|
|
8
11
|
# Mode of operation, one of ssh, ssh-async, pull, pull-mqtt
|
|
9
12
|
:mode: ssh
|
|
10
13
|
|
|
@@ -24,3 +27,8 @@
|
|
|
24
27
|
# MQTT configuration, need to be set if mode is set to pull-mqtt
|
|
25
28
|
# :mqtt_broker: localhost
|
|
26
29
|
# :mqtt_port: 1883
|
|
30
|
+
|
|
31
|
+
# Use of SSL can be forced either way by explicitly setting mqtt_tls setting. If
|
|
32
|
+
# unset, SSL gets used if smart-proxy's foreman_ssl_cert, foreman_ssl_key and
|
|
33
|
+
# foreman_ssl_ca settings are set available.
|
|
34
|
+
# :mqtt_tls:
|
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.8.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:
|
|
11
|
+
date: 1980-01-01 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: bundler
|
|
@@ -114,14 +114,14 @@ dependencies:
|
|
|
114
114
|
requirements:
|
|
115
115
|
- - "~>"
|
|
116
116
|
- !ruby/object:Gem::Version
|
|
117
|
-
version: '0.
|
|
117
|
+
version: '0.8'
|
|
118
118
|
type: :runtime
|
|
119
119
|
prerelease: false
|
|
120
120
|
version_requirements: !ruby/object:Gem::Requirement
|
|
121
121
|
requirements:
|
|
122
122
|
- - "~>"
|
|
123
123
|
- !ruby/object:Gem::Version
|
|
124
|
-
version: '0.
|
|
124
|
+
version: '0.8'
|
|
125
125
|
- !ruby/object:Gem::Dependency
|
|
126
126
|
name: net-ssh
|
|
127
127
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -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
|
|
@@ -203,7 +205,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
203
205
|
- !ruby/object:Gem::Version
|
|
204
206
|
version: '0'
|
|
205
207
|
requirements: []
|
|
206
|
-
rubygems_version: 3.
|
|
208
|
+
rubygems_version: 3.2.26
|
|
207
209
|
signing_key:
|
|
208
210
|
specification_version: 4
|
|
209
211
|
summary: Ssh remote execution provider for Foreman Smart-Proxy
|