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,11 @@
1
+ require 'foreman_tasks_core/shareable_action'
2
+
3
+ module ForemanRemoteExecutionCore
4
+ module Actions
5
+ class RunScript < ForemanTasksCore::Runner::Action
6
+ def initiate_runner
7
+ ScriptRunner.new(input)
8
+ end
9
+ end
10
+ end
11
+ 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
@@ -0,0 +1,3 @@
1
+ module ForemanRemoteExecutionCore
2
+ VERSION = '1.0.0'
3
+ 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: []