foreman_remote_execution_core 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
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: []