smart_proxy_remote_execution_ssh 0.5.2 → 0.7.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/plugin.rb +10 -1
- data/lib/smart_proxy_remote_execution_ssh/runners/polling_script_runner.rb +15 -6
- data/lib/smart_proxy_remote_execution_ssh/runners/script_runner.rb +100 -82
- 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 +5 -5
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: e58a285a16b7d9a173ee4c29639b3367517c5d84f7979bdc84ab2c502a51edb1
|
4
|
+
data.tar.gz: 57892ef63dc0a6bd156ae6fef562b8a4096a7a3cd6a2a03828b4598b1b1d37c7
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 89cc41ef35aa11210f322afc3b7ae35aa269cc4ed8fcef8faff11e32ac37fe765cc083d0bdef2b9c58a54f96be5c8224c5916b85cbb393040f6e715816bf51b7
|
7
|
+
data.tar.gz: ec608d3b666443e6afbb97d28f1014f459d746d65adcf0e1f0cd43d168961e05884f8d3e393814f594527a294d6ac56d2af432509b8ecb99cfcf0df7101f501b
|
@@ -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|
|
@@ -2,6 +2,10 @@ module Proxy::RemoteExecution::Ssh
|
|
2
2
|
class Plugin < Proxy::Plugin
|
3
3
|
SSH_LOG_LEVELS = %w[debug info error fatal].freeze
|
4
4
|
MODES = %i[ssh async-ssh 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,12 +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} #{@base_dir}" if @user_method.cli_command_prefix}
|
43
44
|
| #{@user_method.cli_command_prefix} sh -c '#{main_script}; #{control_script_finish}' #{close_fds} &
|
44
|
-
| echo $! > '#{@base_dir}/pid'
|
45
45
|
SCRIPT
|
46
46
|
end
|
47
47
|
|
@@ -50,18 +50,23 @@ module Proxy::RemoteExecution::Ssh::Runners
|
|
50
50
|
end
|
51
51
|
|
52
52
|
def refresh
|
53
|
-
err = output = nil
|
54
53
|
begin
|
55
|
-
|
54
|
+
pm = run_sync("#{@user_method.cli_command_prefix} #{@retrieval_script}")
|
56
55
|
rescue StandardError => e
|
57
56
|
@logger.info("Error while connecting to the remote host on refresh: #{e.message}")
|
58
57
|
end
|
59
58
|
|
60
|
-
process_retrieved_data(
|
59
|
+
process_retrieved_data(pm.stdout.to_s.chomp, pm.stderr.to_s.chomp)
|
61
60
|
ensure
|
62
61
|
destroy_session
|
63
62
|
end
|
64
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
|
+
|
65
70
|
def process_retrieved_data(output, err)
|
66
71
|
return if output.nil? || output.empty?
|
67
72
|
|
@@ -126,7 +131,11 @@ module Proxy::RemoteExecution::Ssh::Runners
|
|
126
131
|
end
|
127
132
|
|
128
133
|
def cleanup
|
129
|
-
|
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
|
130
139
|
end
|
131
140
|
|
132
141
|
def destroy_session
|
@@ -1,5 +1,6 @@
|
|
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'
|
3
4
|
|
4
5
|
module Proxy::RemoteExecution::Ssh::Runners
|
5
6
|
class EffectiveUserMethod
|
@@ -12,9 +13,9 @@ module Proxy::RemoteExecution::Ssh::Runners
|
|
12
13
|
@password_sent = false
|
13
14
|
end
|
14
15
|
|
15
|
-
def on_data(received_data,
|
16
|
+
def on_data(received_data, io_buffer)
|
16
17
|
if received_data.match(login_prompt)
|
17
|
-
|
18
|
+
io_buffer.add_data(effective_user_password + "\n")
|
18
19
|
@password_sent = true
|
19
20
|
end
|
20
21
|
end
|
@@ -90,7 +91,7 @@ module Proxy::RemoteExecution::Ssh::Runners
|
|
90
91
|
|
91
92
|
# rubocop:disable Metrics/ClassLength
|
92
93
|
class ScriptRunner < Proxy::Dynflow::Runner::Base
|
93
|
-
include Proxy::Dynflow::Runner::
|
94
|
+
include Proxy::Dynflow::Runner::ProcessManagerCommand
|
94
95
|
attr_reader :execution_timeout_interval
|
95
96
|
|
96
97
|
EXPECTED_POWER_ACTION_MESSAGES = ['restart host', 'shutdown host'].freeze
|
@@ -110,7 +111,8 @@ module Proxy::RemoteExecution::Ssh::Runners
|
|
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
|
@@ -141,6 +143,8 @@ module Proxy::RemoteExecution::Ssh::Runners
|
|
141
143
|
|
142
144
|
def start
|
143
145
|
Proxy::RemoteExecution::Utils.prune_known_hosts!(@host, @ssh_port, logger) if @first_execution
|
146
|
+
establish_connection
|
147
|
+
preflight_checks
|
144
148
|
prepare_start
|
145
149
|
script = initialization_script
|
146
150
|
logger.debug("executing script:\n#{indent_multiline(script)}")
|
@@ -154,32 +158,61 @@ module Proxy::RemoteExecution::Ssh::Runners
|
|
154
158
|
run_async(*args)
|
155
159
|
end
|
156
160
|
|
161
|
+
def preflight_checks
|
162
|
+
ensure_remote_command(cp_script_to_remote("#!/bin/sh\nexec true", 'test'),
|
163
|
+
error: 'Failed to execute script on remote machine, exit code: %{exit_code}.'
|
164
|
+
)
|
165
|
+
unless @user_method.is_a? NoopUserMethod
|
166
|
+
path = cp_script_to_remote("#!/bin/sh\nexec #{@user_method.cli_command_prefix} true", 'effective-user-test')
|
167
|
+
ensure_remote_command(path,
|
168
|
+
error: 'Failed to change to effective user, exit code: %{exit_code}',
|
169
|
+
tty: true,
|
170
|
+
user_method: @user_method,
|
171
|
+
close_stdin: false)
|
172
|
+
end
|
173
|
+
end
|
174
|
+
|
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
|
+
|
157
185
|
def prepare_start
|
158
186
|
@remote_script = cp_script_to_remote
|
159
187
|
@output_path = File.join(File.dirname(@remote_script), 'output')
|
160
188
|
@exit_code_path = File.join(File.dirname(@remote_script), 'exit_code')
|
189
|
+
@pid_path = File.join(File.dirname(@remote_script), 'pid')
|
190
|
+
@remote_script_wrapper = upload_data("echo $$ > #{@pid_path}; exec \"$@\";", File.join(File.dirname(@remote_script), 'script-wrapper'), 555)
|
161
191
|
end
|
162
192
|
|
163
193
|
# the script that initiates the execution
|
164
194
|
def initialization_script
|
165
195
|
su_method = @user_method.instance_of?(SuUserMethod)
|
166
196
|
# pipe the output to tee while capturing the exit code in a file
|
167
|
-
|
168
|
-
|
169
|
-
|
197
|
+
<<~SCRIPT
|
198
|
+
sh <<EOF | /usr/bin/tee #{@output_path}
|
199
|
+
#{@remote_script_wrapper} #{@user_method.cli_command_prefix}#{su_method ? "'#{@remote_script} < /dev/null '" : "#{@remote_script} < /dev/null"}
|
200
|
+
echo \\$?>#{@exit_code_path}
|
201
|
+
EOF
|
202
|
+
exit $(cat #{@exit_code_path})
|
170
203
|
SCRIPT
|
171
204
|
end
|
172
205
|
|
173
206
|
def refresh
|
174
|
-
return if @
|
207
|
+
return if @process_manager.nil?
|
175
208
|
super
|
176
209
|
ensure
|
177
210
|
check_expecting_disconnect
|
178
211
|
end
|
179
212
|
|
180
213
|
def kill
|
181
|
-
if @
|
182
|
-
run_sync("pkill -
|
214
|
+
if @process_manager&.started?
|
215
|
+
run_sync("pkill -P $(cat #{@pid_path})")
|
183
216
|
else
|
184
217
|
logger.debug('connection closed')
|
185
218
|
end
|
@@ -197,28 +230,28 @@ module Proxy::RemoteExecution::Ssh::Runners
|
|
197
230
|
end
|
198
231
|
|
199
232
|
def close_session
|
200
|
-
|
201
|
-
raise 'Control socket file does not exist' unless File.exist?(local_command_file("socket"))
|
233
|
+
raise 'Control socket file does not exist' unless File.exist?(socket_file)
|
202
234
|
@logger.debug("Sending exit request for session #{@ssh_user}@#{@host}")
|
203
|
-
args = ['/usr/bin/ssh', @host, "-o", "
|
204
|
-
|
205
|
-
|
206
|
-
|
207
|
-
|
235
|
+
args = ['/usr/bin/ssh', @host, "-o", "ControlPath=#{socket_file}", "-O", "exit"].flatten
|
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!
|
208
240
|
end
|
209
241
|
|
210
242
|
def close
|
211
|
-
run_sync("rm -rf
|
243
|
+
run_sync("rm -rf #{remote_command_dir}") if should_cleanup?
|
212
244
|
rescue StandardError => e
|
213
245
|
publish_exception('Error when removing remote working dir', e, false)
|
214
246
|
ensure
|
215
|
-
close_session if @
|
247
|
+
close_session if @process_manager
|
216
248
|
FileUtils.rm_rf(local_command_dir) if Dir.exist?(local_command_dir) && @cleanup_working_dirs
|
217
249
|
end
|
218
250
|
|
219
|
-
def publish_data(data, type)
|
251
|
+
def publish_data(data, type, pm = nil)
|
252
|
+
pm ||= @process_manager
|
220
253
|
super(data.force_encoding('UTF-8'), type) unless @user_method.filter_password?(data)
|
221
|
-
@user_method.on_data(data,
|
254
|
+
@user_method.on_data(data, pm.stdin) if pm
|
222
255
|
end
|
223
256
|
|
224
257
|
private
|
@@ -228,27 +261,10 @@ module Proxy::RemoteExecution::Ssh::Runners
|
|
228
261
|
end
|
229
262
|
|
230
263
|
def should_cleanup?
|
231
|
-
@
|
264
|
+
@process_manager && @cleanup_working_dirs
|
232
265
|
end
|
233
266
|
|
234
|
-
|
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)
|
267
|
+
def ssh_options(with_pty = false, quiet: false)
|
252
268
|
ssh_options = []
|
253
269
|
ssh_options << "-tt" if with_pty
|
254
270
|
ssh_options << "-o User=#{@ssh_user}"
|
@@ -259,9 +275,9 @@ module Proxy::RemoteExecution::Ssh::Runners
|
|
259
275
|
ssh_options << "-o PreferredAuthentications=#{available_authentication_methods.join(',')}"
|
260
276
|
ssh_options << "-o UserKnownHostsFile=#{prepare_known_hosts}" if @host_public_key
|
261
277
|
ssh_options << "-o NumberOfPasswordPrompts=1"
|
262
|
-
ssh_options << "-o LogLevel=#{settings[:ssh_log_level]}"
|
278
|
+
ssh_options << "-o LogLevel=#{quiet ? 'quiet' : settings[:ssh_log_level]}"
|
263
279
|
ssh_options << "-o ControlMaster=auto"
|
264
|
-
ssh_options << "-o ControlPath=#{
|
280
|
+
ssh_options << "-o ControlPath=#{socket_file}"
|
265
281
|
ssh_options << "-o ControlPersist=yes"
|
266
282
|
end
|
267
283
|
|
@@ -269,53 +285,47 @@ module Proxy::RemoteExecution::Ssh::Runners
|
|
269
285
|
Proxy::RemoteExecution::Ssh::Plugin.settings
|
270
286
|
end
|
271
287
|
|
272
|
-
def get_args(command, with_pty = false)
|
288
|
+
def get_args(command, with_pty = false, quiet: false)
|
273
289
|
args = []
|
274
290
|
args = [{'SSHPASS' => @key_passphrase}, '/usr/bin/sshpass', '-P', 'passphrase', '-e'] if @key_passphrase
|
275
291
|
args = [{'SSHPASS' => @ssh_password}, '/usr/bin/sshpass', '-e'] if @ssh_password
|
276
|
-
args += ['/usr/bin/ssh', @host, ssh_options(with_pty), command].flatten
|
292
|
+
args += ['/usr/bin/ssh', @host, ssh_options(with_pty, quiet: quiet), command].flatten
|
277
293
|
end
|
278
294
|
|
279
295
|
# Initiates run of the remote command and yields the data when
|
280
296
|
# available. The yielding doesn't happen automatically, but as
|
281
297
|
# part of calling the `refresh` method.
|
282
298
|
def run_async(command)
|
283
|
-
raise 'Async command already in progress' if @started
|
299
|
+
raise 'Async command already in progress' if @process_manager&.started?
|
284
300
|
|
285
|
-
@started = false
|
286
301
|
@user_method.reset
|
287
|
-
|
288
|
-
@started = true
|
302
|
+
initialize_command(*get_args(command, true, quiet: true))
|
289
303
|
|
290
|
-
|
304
|
+
true
|
291
305
|
end
|
292
306
|
|
293
307
|
def run_started?
|
294
|
-
@started && @user_method.sent_all_data?
|
308
|
+
@process_manager&.started? && @user_method.sent_all_data?
|
295
309
|
end
|
296
310
|
|
297
|
-
def
|
298
|
-
|
299
|
-
|
300
|
-
|
301
|
-
|
302
|
-
|
303
|
-
|
311
|
+
def run_sync(command, stdin: nil, close_stdin: true, tty: false, user_method: nil)
|
312
|
+
pm = Proxy::Dynflow::ProcessManager.new(get_args(command, tty))
|
313
|
+
callback = proc do |data|
|
314
|
+
data.each_line do |line|
|
315
|
+
logger.debug(line.chomp) if user_method.nil? || !user_method.filter_password?(line)
|
316
|
+
user_method.on_data(data, pm.stdin) if user_method
|
317
|
+
end
|
318
|
+
''
|
304
319
|
end
|
305
|
-
|
306
|
-
|
307
|
-
|
308
|
-
|
309
|
-
|
310
|
-
|
311
|
-
|
312
|
-
|
313
|
-
|
314
|
-
tx.puts(stdin) unless stdin.nil?
|
315
|
-
tx.close
|
316
|
-
stdout, stderr = read_output_debug(err, rx)
|
317
|
-
exit_status = Process.wait2(pid)[1].exitstatus
|
318
|
-
return exit_status, stdout, stderr
|
320
|
+
pm.on_stdout(&callback)
|
321
|
+
pm.on_stderr(&callback)
|
322
|
+
pm.start!
|
323
|
+
unless pm.status
|
324
|
+
pm.stdin.io.puts(stdin) if stdin
|
325
|
+
pm.stdin.io.close if close_stdin
|
326
|
+
pm.run!
|
327
|
+
end
|
328
|
+
pm
|
319
329
|
end
|
320
330
|
|
321
331
|
def prepare_known_hosts
|
@@ -334,6 +344,10 @@ module Proxy::RemoteExecution::Ssh::Runners
|
|
334
344
|
File.join(ensure_local_directory(local_command_dir), filename)
|
335
345
|
end
|
336
346
|
|
347
|
+
def socket_file
|
348
|
+
File.join(ensure_local_directory(@socket_working_dir), @id)
|
349
|
+
end
|
350
|
+
|
337
351
|
def remote_command_dir
|
338
352
|
File.join(@remote_working_dir, "foreman-ssh-cmd-#{id}")
|
339
353
|
end
|
@@ -362,15 +376,13 @@ module Proxy::RemoteExecution::Ssh::Runners
|
|
362
376
|
# We use tee here to pipe stdin coming from ssh to a file at $path, while silencing its output
|
363
377
|
# This is used to write to $path with elevated permissions, solutions using cat and output redirection
|
364
378
|
# would not work, because the redirection would happen in the non-elevated shell.
|
365
|
-
command = "tee
|
379
|
+
command = "tee #{path} >/dev/null && chmod #{permissions} #{path}"
|
366
380
|
|
367
381
|
@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
|
382
|
+
ensure_remote_command(command,
|
383
|
+
stdin: data,
|
384
|
+
error: "Unable to upload file to #{path} on remote system, exit code: %{exit_code}"
|
385
|
+
)
|
374
386
|
|
375
387
|
path
|
376
388
|
end
|
@@ -382,9 +394,15 @@ module Proxy::RemoteExecution::Ssh::Runners
|
|
382
394
|
end
|
383
395
|
|
384
396
|
def ensure_remote_directory(path)
|
385
|
-
|
386
|
-
|
387
|
-
|
397
|
+
ensure_remote_command("mkdir -p #{path}",
|
398
|
+
error: "Unable to create directory #{path} on remote system, exit code: %{exit_code}"
|
399
|
+
)
|
400
|
+
end
|
401
|
+
|
402
|
+
def ensure_remote_command(cmd, error: nil, **kwargs)
|
403
|
+
if (pm = run_sync(cmd, **kwargs)).status != 0
|
404
|
+
msg = error || 'Failed to run command %{command} on remote machine, exit code: %{exit_code}'
|
405
|
+
raise(msg % { command: cmd, exit_code: pm.status })
|
388
406
|
end
|
389
407
|
end
|
390
408
|
|
@@ -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.7.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-05-13 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
|
@@ -203,7 +203,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
203
203
|
- !ruby/object:Gem::Version
|
204
204
|
version: '0'
|
205
205
|
requirements: []
|
206
|
-
rubygems_version: 3.1.
|
206
|
+
rubygems_version: 3.1.4
|
207
207
|
signing_key:
|
208
208
|
specification_version: 4
|
209
209
|
summary: Ssh remote execution provider for Foreman Smart-Proxy
|