foreman_remote_execution_core 1.0.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
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: f4434d9a0811c522641e6dfa5fbd36478501084c
|
4
|
+
data.tar.gz: dc04023dbcd536cf25aa9adb5c73721fdf86ee17
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 02477dee68e14b9ac64987f59e9df36cdd4a7b5e9705a9ba83c4564f234f59d65a9980ec5d65873c06e496876f98d7d16515ca3062173d94d913d5085e1cfe63
|
7
|
+
data.tar.gz: 4e0fcc9ce65277675f15e8283523a118a12a03abbecdf72247573f4de44df12f04c5cf1c707575b2aa4331ee438e82efa5db4cc6aa57e1192be1db3cc9c7dead
|
@@ -0,0 +1,18 @@
|
|
1
|
+
require 'foreman_tasks_core'
|
2
|
+
|
3
|
+
module ForemanRemoteExecutionCore
|
4
|
+
extend ForemanTasksCore::SettingsLoader
|
5
|
+
register_settings([:remote_execution_ssh, :smart_proxy_remote_execution_ssh_core],
|
6
|
+
:ssh_identity_key_file => '~/.ssh/id_rsa_foreman_proxy',
|
7
|
+
:ssh_user => 'root',
|
8
|
+
:remote_working_dir => '/var/tmp',
|
9
|
+
:local_working_dir => '/var/tmp')
|
10
|
+
|
11
|
+
if ForemanTasksCore.dynflow_present?
|
12
|
+
require 'foreman_tasks_core/runner'
|
13
|
+
require 'foreman_remote_execution_core/script_runner'
|
14
|
+
require 'foreman_remote_execution_core/actions'
|
15
|
+
end
|
16
|
+
|
17
|
+
require 'foreman_remote_execution_core/version'
|
18
|
+
end
|
@@ -0,0 +1,266 @@
|
|
1
|
+
require 'net/ssh'
|
2
|
+
require 'net/scp'
|
3
|
+
|
4
|
+
module ForemanRemoteExecutionCore
|
5
|
+
class ScriptRunner < ForemanTasksCore::Runner::Base
|
6
|
+
EXPECTED_POWER_ACTION_MESSAGES = ["restart host", "shutdown host"]
|
7
|
+
|
8
|
+
def initialize(options)
|
9
|
+
super()
|
10
|
+
@host = options.fetch(:hostname)
|
11
|
+
@script = options.fetch(:script)
|
12
|
+
@ssh_user = options.fetch(:ssh_user, 'root')
|
13
|
+
@ssh_port = options.fetch(:ssh_port, 22)
|
14
|
+
@effective_user = options.fetch(:effective_user, nil)
|
15
|
+
@effective_user_method = options.fetch(:effective_user_method, 'sudo')
|
16
|
+
@host_public_key = options.fetch(:host_public_key, nil)
|
17
|
+
@verify_host = options.fetch(:verify_host, nil)
|
18
|
+
|
19
|
+
@client_private_key_file = settings.fetch(:ssh_identity_key_file)
|
20
|
+
@local_working_dir = options.fetch(:local_working_dir, settings.fetch(:local_working_dir))
|
21
|
+
@remote_working_dir = options.fetch(:remote_working_dir, settings.fetch(:remote_working_dir))
|
22
|
+
end
|
23
|
+
|
24
|
+
def start
|
25
|
+
remote_script = cp_script_to_remote
|
26
|
+
output_path = File.join(File.dirname(remote_script), 'output')
|
27
|
+
|
28
|
+
# pipe the output to tee while capturing the exit code
|
29
|
+
script = <<-SCRIPT
|
30
|
+
exec 4>&1
|
31
|
+
exit_code=`((#{su_prefix}#{remote_script}; echo $?>&3 ) | /usr/bin/tee #{output_path} ) 3>&1 >&4`
|
32
|
+
exec 4>&-
|
33
|
+
exit $exit_code
|
34
|
+
SCRIPT
|
35
|
+
logger.debug("executing script:\n#{script.lines.map { |line| " | #{line}" }.join}")
|
36
|
+
run_async(script)
|
37
|
+
rescue => e
|
38
|
+
logger.error("error while initalizing command #{e.class} #{e.message}:\n #{e.backtrace.join("\n")}")
|
39
|
+
publish_exception("Error initializing command", e)
|
40
|
+
end
|
41
|
+
|
42
|
+
def refresh
|
43
|
+
return if @session.nil?
|
44
|
+
with_retries do
|
45
|
+
with_disconnect_handling do
|
46
|
+
@session.process(0)
|
47
|
+
end
|
48
|
+
end
|
49
|
+
ensure
|
50
|
+
check_expecting_disconnect
|
51
|
+
end
|
52
|
+
|
53
|
+
def kill
|
54
|
+
if @session
|
55
|
+
run_sync("pkill -f #{remote_command_file('script')}")
|
56
|
+
else
|
57
|
+
logger.debug("connection closed")
|
58
|
+
end
|
59
|
+
rescue => e
|
60
|
+
publish_exception("Unexpected error", e, false)
|
61
|
+
end
|
62
|
+
|
63
|
+
def with_retries
|
64
|
+
tries = 0
|
65
|
+
begin
|
66
|
+
yield
|
67
|
+
rescue => e
|
68
|
+
logger.error("Unexpected error: #{e.class} #{e.message}\n #{e.backtrace.join("\n")}")
|
69
|
+
tries += 1
|
70
|
+
if tries <= MAX_PROCESS_RETRIES
|
71
|
+
logger.error('Retrying')
|
72
|
+
retry
|
73
|
+
else
|
74
|
+
publish_exception("Unexpected error", e)
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
def with_disconnect_handling
|
80
|
+
yield
|
81
|
+
rescue Net::SSH::Disconnect => e
|
82
|
+
@session.shutdown!
|
83
|
+
check_expecting_disconnect
|
84
|
+
if @expecting_disconnect
|
85
|
+
publish_exit_status(0)
|
86
|
+
else
|
87
|
+
publish_exception("Unexpected disconnect", e)
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
def close
|
92
|
+
@session.close if @session && !@session.closed?
|
93
|
+
end
|
94
|
+
|
95
|
+
private
|
96
|
+
|
97
|
+
def session
|
98
|
+
@session ||= begin
|
99
|
+
@logger.debug("opening session to #{@ssh_user}@#{@host}")
|
100
|
+
Net::SSH.start(@host, @ssh_user, ssh_options)
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
def ssh_options
|
105
|
+
ssh_options = {}
|
106
|
+
ssh_options[:port] = @ssh_port if @ssh_port
|
107
|
+
ssh_options[:keys] = [@client_private_key_file] if @client_private_key_file
|
108
|
+
ssh_options[:user_known_hosts_file] = @known_hosts_file if @known_hosts_file
|
109
|
+
ssh_options[:keys_only] = true
|
110
|
+
# if the host public key is contained in the known_hosts_file,
|
111
|
+
# verify it, otherwise, if missing, import it and continue
|
112
|
+
ssh_options[:paranoid] = true
|
113
|
+
ssh_options[:auth_methods] = ["publickey"]
|
114
|
+
ssh_options[:user_known_hosts_file] = prepare_known_hosts if @host_public_key
|
115
|
+
return ssh_options
|
116
|
+
end
|
117
|
+
|
118
|
+
def settings
|
119
|
+
ForemanRemoteExecutionCore.settings
|
120
|
+
end
|
121
|
+
|
122
|
+
# Initiates run of the remote command and yields the data when
|
123
|
+
# available. The yielding doesn't happen automatically, but as
|
124
|
+
# part of calling the `refresh` method.
|
125
|
+
def run_async(command)
|
126
|
+
raise "Async command already in progress" if @started
|
127
|
+
@started = false
|
128
|
+
session.open_channel do |channel|
|
129
|
+
channel.request_pty
|
130
|
+
channel.on_data { |ch, data| publish_data(data, 'stdout') }
|
131
|
+
channel.on_extended_data { |ch, type, data| publish_data(data, 'stderr') }
|
132
|
+
# standard exit of the command
|
133
|
+
channel.on_request("exit-status") { |ch, data| publish_exit_status(data.read_long) }
|
134
|
+
# on signal: sending the signal value (such as 'TERM')
|
135
|
+
channel.on_request("exit-signal") do |ch, data|
|
136
|
+
publish_exit_status(data.read_string)
|
137
|
+
ch.close
|
138
|
+
# wait for the channel to finish so that we know at the end
|
139
|
+
# that the session is inactive
|
140
|
+
ch.wait
|
141
|
+
end
|
142
|
+
channel.exec(command) do |ch, success|
|
143
|
+
@started = true
|
144
|
+
raise("Error initializing command") unless success
|
145
|
+
end
|
146
|
+
end
|
147
|
+
session.process(0) until @started
|
148
|
+
return true
|
149
|
+
end
|
150
|
+
|
151
|
+
def run_sync(command)
|
152
|
+
output = ""
|
153
|
+
exit_status = nil
|
154
|
+
channel = session.open_channel do |ch|
|
155
|
+
ch.on_data { |data| output.concat(data) }
|
156
|
+
ch.on_extended_data { |_, _, data| output.concat(data) }
|
157
|
+
ch.on_request("exit-status") { |_, data| exit_status = data.read_long }
|
158
|
+
# on signal: sending the signal value (such as 'TERM')
|
159
|
+
ch.on_request("exit-signal") do |_, data|
|
160
|
+
exit_status = data.read_string
|
161
|
+
ch.close
|
162
|
+
ch.wait
|
163
|
+
end
|
164
|
+
ch.exec command do |_, success|
|
165
|
+
raise "could not execute command" unless success
|
166
|
+
end
|
167
|
+
end
|
168
|
+
channel.wait
|
169
|
+
return exit_status, output
|
170
|
+
end
|
171
|
+
|
172
|
+
def su_prefix
|
173
|
+
return if @effective_user.nil? || @effective_user == @ssh_user
|
174
|
+
case @effective_user_method
|
175
|
+
when 'sudo'
|
176
|
+
"sudo -n -u #{@effective_user} "
|
177
|
+
when 'su'
|
178
|
+
"su - #{@effective_user} -c "
|
179
|
+
else
|
180
|
+
raise "effective_user_method ''#{@effective_user_method}'' not supported"
|
181
|
+
end
|
182
|
+
end
|
183
|
+
|
184
|
+
def prepare_known_hosts
|
185
|
+
path = local_command_file('known_hosts')
|
186
|
+
if @host_public_key
|
187
|
+
write_command_file_locally('known_hosts', "#{@host} #{@host_public_key}")
|
188
|
+
end
|
189
|
+
return path
|
190
|
+
end
|
191
|
+
|
192
|
+
def local_command_dir
|
193
|
+
File.join(@local_working_dir, 'foreman-proxy', "foreman-ssh-cmd-#{@id}")
|
194
|
+
end
|
195
|
+
|
196
|
+
def local_command_file(filename)
|
197
|
+
File.join(local_command_dir, filename)
|
198
|
+
end
|
199
|
+
|
200
|
+
def remote_command_dir
|
201
|
+
File.join(@remote_working_dir, "foreman-ssh-cmd-#{id}")
|
202
|
+
end
|
203
|
+
|
204
|
+
def remote_command_file(filename)
|
205
|
+
File.join(remote_command_dir, filename)
|
206
|
+
end
|
207
|
+
|
208
|
+
def ensure_local_directory(path)
|
209
|
+
if File.exist?(path)
|
210
|
+
raise "#{path} expected to be a directory" unless File.directory?(path)
|
211
|
+
else
|
212
|
+
FileUtils.mkdir_p(path)
|
213
|
+
end
|
214
|
+
return path
|
215
|
+
end
|
216
|
+
|
217
|
+
def cp_script_to_remote
|
218
|
+
local_script_file = write_command_file_locally('script', sanitize_script(@script))
|
219
|
+
File.chmod(0555, local_script_file)
|
220
|
+
remote_script_file = remote_command_file('script')
|
221
|
+
upload_file(local_script_file, remote_script_file)
|
222
|
+
return remote_script_file
|
223
|
+
end
|
224
|
+
|
225
|
+
def upload_file(local_path, remote_path)
|
226
|
+
ensure_remote_directory(File.dirname(remote_path))
|
227
|
+
scp = Net::SCP.new(session)
|
228
|
+
upload_channel = scp.upload(local_path, remote_path)
|
229
|
+
upload_channel.wait
|
230
|
+
ensure
|
231
|
+
if upload_channel
|
232
|
+
upload_channel.close
|
233
|
+
upload_channel.wait
|
234
|
+
end
|
235
|
+
end
|
236
|
+
|
237
|
+
def ensure_remote_directory(path)
|
238
|
+
exit_code, output = run_sync("mkdir -p #{path}")
|
239
|
+
if exit_code != 0
|
240
|
+
raise "Unable to create directory on remote system #{path}: exit code: #{exit_code}\n #{output}"
|
241
|
+
end
|
242
|
+
end
|
243
|
+
|
244
|
+
def sanitize_script(script)
|
245
|
+
script.tr("\r", '')
|
246
|
+
end
|
247
|
+
|
248
|
+
def write_command_file_locally(filename, content)
|
249
|
+
path = local_command_file(filename)
|
250
|
+
ensure_local_directory(File.dirname(path))
|
251
|
+
File.write(path, content)
|
252
|
+
return path
|
253
|
+
end
|
254
|
+
|
255
|
+
# when a remote server disconnects, it's hard to tell if it was on purpose (when calling reboot)
|
256
|
+
# or it's an error. When it's expected, we expect the script to produce 'restart host' as
|
257
|
+
# its last command output
|
258
|
+
def check_expecting_disconnect
|
259
|
+
last_output = @continuous_output.raw_outputs.find { |d| d['output_type'] == 'stdout' }
|
260
|
+
return unless last_output
|
261
|
+
if EXPECTED_POWER_ACTION_MESSAGES.any? { |message| last_output['output'] =~ /^#{message}/ }
|
262
|
+
@expecting_disconnect = true
|
263
|
+
end
|
264
|
+
end
|
265
|
+
end
|
266
|
+
end
|
metadata
ADDED
@@ -0,0 +1,91 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: foreman_remote_execution_core
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 1.0.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Ivan Nečas
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2016-09-08 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: foreman-tasks-core
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: 0.1.0
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: 0.1.0
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: net-ssh
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ">="
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '0'
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - ">="
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '0'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: net-scp
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - ">="
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '0'
|
48
|
+
type: :runtime
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - ">="
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '0'
|
55
|
+
description: |2
|
56
|
+
Ssh remote execution provider code sharable between Foreman and Foreman-Proxy
|
57
|
+
email:
|
58
|
+
- inecas@redhat.com
|
59
|
+
executables: []
|
60
|
+
extensions: []
|
61
|
+
extra_rdoc_files: []
|
62
|
+
files:
|
63
|
+
- lib/foreman_remote_execution_core.rb
|
64
|
+
- lib/foreman_remote_execution_core/actions.rb
|
65
|
+
- lib/foreman_remote_execution_core/script_runner.rb
|
66
|
+
- lib/foreman_remote_execution_core/version.rb
|
67
|
+
homepage: https://github.com/theforeman/foreman_remote_execution
|
68
|
+
licenses:
|
69
|
+
- GPL-3
|
70
|
+
metadata: {}
|
71
|
+
post_install_message:
|
72
|
+
rdoc_options: []
|
73
|
+
require_paths:
|
74
|
+
- lib
|
75
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
76
|
+
requirements:
|
77
|
+
- - ">="
|
78
|
+
- !ruby/object:Gem::Version
|
79
|
+
version: '0'
|
80
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
81
|
+
requirements:
|
82
|
+
- - ">="
|
83
|
+
- !ruby/object:Gem::Version
|
84
|
+
version: '0'
|
85
|
+
requirements: []
|
86
|
+
rubyforge_project:
|
87
|
+
rubygems_version: 2.4.5
|
88
|
+
signing_key:
|
89
|
+
specification_version: 4
|
90
|
+
summary: Foreman remote execution - core bits
|
91
|
+
test_files: []
|