manageiq-ssh-util 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 58c774eec4755352ba4daf38939f8ab0d35ea8bd110eb34d7b772d0fdbecb574
4
+ data.tar.gz: b5ed3ef9f3c97ff0a998816db02442f44a98f6d648444138b5ad0f3759731763
5
+ SHA512:
6
+ metadata.gz: af4a43b882154267c45a17c797c024f21aa75c6cd15316778e1d84bd159dc3dafffabfe8eca047efa1041b04b1c35479d7f114885b277533721efb1417202107
7
+ data.tar.gz: 360b674b19f26b8438d6d2d8857413ac189ef93c1e0cd3b86bc18f408d4bc1627f462667339c96fa59e5a9ce9c1cec7377634b14aefd44a492294fbb187ef057
@@ -0,0 +1,202 @@
1
+
2
+ Apache License
3
+ Version 2.0, January 2004
4
+ http://www.apache.org/licenses/
5
+
6
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
7
+
8
+ 1. Definitions.
9
+
10
+ "License" shall mean the terms and conditions for use, reproduction,
11
+ and distribution as defined by Sections 1 through 9 of this document.
12
+
13
+ "Licensor" shall mean the copyright owner or entity authorized by
14
+ the copyright owner that is granting the License.
15
+
16
+ "Legal Entity" shall mean the union of the acting entity and all
17
+ other entities that control, are controlled by, or are under common
18
+ control with that entity. For the purposes of this definition,
19
+ "control" means (i) the power, direct or indirect, to cause the
20
+ direction or management of such entity, whether by contract or
21
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
22
+ outstanding shares, or (iii) beneficial ownership of such entity.
23
+
24
+ "You" (or "Your") shall mean an individual or Legal Entity
25
+ exercising permissions granted by this License.
26
+
27
+ "Source" form shall mean the preferred form for making modifications,
28
+ including but not limited to software source code, documentation
29
+ source, and configuration files.
30
+
31
+ "Object" form shall mean any form resulting from mechanical
32
+ transformation or translation of a Source form, including but
33
+ not limited to compiled object code, generated documentation,
34
+ and conversions to other media types.
35
+
36
+ "Work" shall mean the work of authorship, whether in Source or
37
+ Object form, made available under the License, as indicated by a
38
+ copyright notice that is included in or attached to the work
39
+ (an example is provided in the Appendix below).
40
+
41
+ "Derivative Works" shall mean any work, whether in Source or Object
42
+ form, that is based on (or derived from) the Work and for which the
43
+ editorial revisions, annotations, elaborations, or other modifications
44
+ represent, as a whole, an original work of authorship. For the purposes
45
+ of this License, Derivative Works shall not include works that remain
46
+ separable from, or merely link (or bind by name) to the interfaces of,
47
+ the Work and Derivative Works thereof.
48
+
49
+ "Contribution" shall mean any work of authorship, including
50
+ the original version of the Work and any modifications or additions
51
+ to that Work or Derivative Works thereof, that is intentionally
52
+ submitted to Licensor for inclusion in the Work by the copyright owner
53
+ or by an individual or Legal Entity authorized to submit on behalf of
54
+ the copyright owner. For the purposes of this definition, "submitted"
55
+ means any form of electronic, verbal, or written communication sent
56
+ to the Licensor or its representatives, including but not limited to
57
+ communication on electronic mailing lists, source code control systems,
58
+ and issue tracking systems that are managed by, or on behalf of, the
59
+ Licensor for the purpose of discussing and improving the Work, but
60
+ excluding communication that is conspicuously marked or otherwise
61
+ designated in writing by the copyright owner as "Not a Contribution."
62
+
63
+ "Contributor" shall mean Licensor and any individual or Legal Entity
64
+ on behalf of whom a Contribution has been received by Licensor and
65
+ subsequently incorporated within the Work.
66
+
67
+ 2. Grant of Copyright License. Subject to the terms and conditions of
68
+ this License, each Contributor hereby grants to You a perpetual,
69
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
70
+ copyright license to reproduce, prepare Derivative Works of,
71
+ publicly display, publicly perform, sublicense, and distribute the
72
+ Work and such Derivative Works in Source or Object form.
73
+
74
+ 3. Grant of Patent License. Subject to the terms and conditions of
75
+ this License, each Contributor hereby grants to You a perpetual,
76
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
77
+ (except as stated in this section) patent license to make, have made,
78
+ use, offer to sell, sell, import, and otherwise transfer the Work,
79
+ where such license applies only to those patent claims licensable
80
+ by such Contributor that are necessarily infringed by their
81
+ Contribution(s) alone or by combination of their Contribution(s)
82
+ with the Work to which such Contribution(s) was submitted. If You
83
+ institute patent litigation against any entity (including a
84
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
85
+ or a Contribution incorporated within the Work constitutes direct
86
+ or contributory patent infringement, then any patent licenses
87
+ granted to You under this License for that Work shall terminate
88
+ as of the date such litigation is filed.
89
+
90
+ 4. Redistribution. You may reproduce and distribute copies of the
91
+ Work or Derivative Works thereof in any medium, with or without
92
+ modifications, and in Source or Object form, provided that You
93
+ meet the following conditions:
94
+
95
+ (a) You must give any other recipients of the Work or
96
+ Derivative Works a copy of this License; and
97
+
98
+ (b) You must cause any modified files to carry prominent notices
99
+ stating that You changed the files; and
100
+
101
+ (c) You must retain, in the Source form of any Derivative Works
102
+ that You distribute, all copyright, patent, trademark, and
103
+ attribution notices from the Source form of the Work,
104
+ excluding those notices that do not pertain to any part of
105
+ the Derivative Works; and
106
+
107
+ (d) If the Work includes a "NOTICE" text file as part of its
108
+ distribution, then any Derivative Works that You distribute must
109
+ include a readable copy of the attribution notices contained
110
+ within such NOTICE file, excluding those notices that do not
111
+ pertain to any part of the Derivative Works, in at least one
112
+ of the following places: within a NOTICE text file distributed
113
+ as part of the Derivative Works; within the Source form or
114
+ documentation, if provided along with the Derivative Works; or,
115
+ within a display generated by the Derivative Works, if and
116
+ wherever such third-party notices normally appear. The contents
117
+ of the NOTICE file are for informational purposes only and
118
+ do not modify the License. You may add Your own attribution
119
+ notices within Derivative Works that You distribute, alongside
120
+ or as an addendum to the NOTICE text from the Work, provided
121
+ that such additional attribution notices cannot be construed
122
+ as modifying the License.
123
+
124
+ You may add Your own copyright statement to Your modifications and
125
+ may provide additional or different license terms and conditions
126
+ for use, reproduction, or distribution of Your modifications, or
127
+ for any such Derivative Works as a whole, provided Your use,
128
+ reproduction, and distribution of the Work otherwise complies with
129
+ the conditions stated in this License.
130
+
131
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
132
+ any Contribution intentionally submitted for inclusion in the Work
133
+ by You to the Licensor shall be under the terms and conditions of
134
+ this License, without any additional terms or conditions.
135
+ Notwithstanding the above, nothing herein shall supersede or modify
136
+ the terms of any separate license agreement you may have executed
137
+ with Licensor regarding such Contributions.
138
+
139
+ 6. Trademarks. This License does not grant permission to use the trade
140
+ names, trademarks, service marks, or product names of the Licensor,
141
+ except as required for reasonable and customary use in describing the
142
+ origin of the Work and reproducing the content of the NOTICE file.
143
+
144
+ 7. Disclaimer of Warranty. Unless required by applicable law or
145
+ agreed to in writing, Licensor provides the Work (and each
146
+ Contributor provides its Contributions) on an "AS IS" BASIS,
147
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
148
+ implied, including, without limitation, any warranties or conditions
149
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
150
+ PARTICULAR PURPOSE. You are solely responsible for determining the
151
+ appropriateness of using or redistributing the Work and assume any
152
+ risks associated with Your exercise of permissions under this License.
153
+
154
+ 8. Limitation of Liability. In no event and under no legal theory,
155
+ whether in tort (including negligence), contract, or otherwise,
156
+ unless required by applicable law (such as deliberate and grossly
157
+ negligent acts) or agreed to in writing, shall any Contributor be
158
+ liable to You for damages, including any direct, indirect, special,
159
+ incidental, or consequential damages of any character arising as a
160
+ result of this License or out of the use or inability to use the
161
+ Work (including but not limited to damages for loss of goodwill,
162
+ work stoppage, computer failure or malfunction, or any and all
163
+ other commercial damages or losses), even if such Contributor
164
+ has been advised of the possibility of such damages.
165
+
166
+ 9. Accepting Warranty or Additional Liability. While redistributing
167
+ the Work or Derivative Works thereof, You may choose to offer,
168
+ and charge a fee for, acceptance of support, warranty, indemnity,
169
+ or other liability obligations and/or rights consistent with this
170
+ License. However, in accepting such obligations, You may act only
171
+ on Your own behalf and on Your sole responsibility, not on behalf
172
+ of any other Contributor, and only if You agree to indemnify,
173
+ defend, and hold each Contributor harmless for any liability
174
+ incurred by, or claims asserted against, such Contributor by reason
175
+ of your accepting any such warranty or additional liability.
176
+
177
+ END OF TERMS AND CONDITIONS
178
+
179
+ APPENDIX: How to apply the Apache License to your work.
180
+
181
+ To apply the Apache License to your work, attach the following
182
+ boilerplate notice, with the fields enclosed by brackets "[]"
183
+ replaced with your own identifying information. (Don't include
184
+ the brackets!) The text should be enclosed in the appropriate
185
+ comment syntax for the file format. We also recommend that a
186
+ file or class name and description of purpose be included on the
187
+ same "printed page" as the copyright notice for easier
188
+ identification within third-party archives.
189
+
190
+ Copyright [yyyy] [name of copyright owner]
191
+
192
+ Licensed under the Apache License, Version 2.0 (the "License");
193
+ you may not use this file except in compliance with the License.
194
+ You may obtain a copy of the License at
195
+
196
+ http://www.apache.org/licenses/LICENSE-2.0
197
+
198
+ Unless required by applicable law or agreed to in writing, software
199
+ distributed under the License is distributed on an "AS IS" BASIS,
200
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
201
+ See the License for the specific language governing permissions and
202
+ limitations under the License.
@@ -0,0 +1,17 @@
1
+ manageiq-ssh-util
2
+
3
+ The manageiq-ssh-util library is a wrapper library around net-ssh. Its
4
+ main benefit is that it automatically handles channels and logging for
5
+ various states when running remote commands. It also automatically handles
6
+ terminal passwords and running commands via sudo, as well as automatic
7
+ retry for host key mismatches.
8
+
9
+ Some differences with the original MiqSshUtil library include:
10
+
11
+ * The name has been changed and scoped under the ManageIQ namespace.
12
+ * The ability to override the default value for the :use_agent option.
13
+ * Bug fixes for the on_extended_data ssh channel.
14
+
15
+ For details of the original bugs (and fixes) please see https://github.com/ManageIQ/manageiq-gems-pending/pull/437.
16
+
17
+ The remaining differences are internal refactoring updates.
@@ -0,0 +1,6 @@
1
+ require "bundler/gem_tasks"
2
+
3
+ require "rspec/core/rake_task"
4
+ RSpec::Core::RakeTask.new('spec')
5
+ task :test => :spec
6
+ task :default => :spec
@@ -0,0 +1 @@
1
+ require "manageiq/ssh/util"
@@ -0,0 +1,432 @@
1
+ require 'net/ssh'
2
+ require 'net/sftp'
3
+ require 'tempfile'
4
+ require 'active_support/core_ext/object/blank'
5
+
6
+ module ManageIQ
7
+ class SSH
8
+ # Utility wrapper around the net-ssh library.
9
+ class Util
10
+ # The exit status of the ssh command.
11
+ attr_reader :status
12
+
13
+ # The name of the host provided to the constructor.
14
+ attr_reader :host
15
+
16
+ # The options hash passed to the constructor.
17
+ attr_reader :options
18
+
19
+ # The username passed to the constructor.
20
+ attr_reader :user
21
+
22
+ # Create and return a ManageIQ::SSH::Util object. A host, user and
23
+ # password must be specified.
24
+ #
25
+ # The +options+ param may contain options that are passed directly
26
+ # to the Net::SSH constructor. By default the :non_interactive option is
27
+ # set to true (meaning it will fail instead of prompting for a password),
28
+ # the :verbose level is set to :warn, and the :use_agent option is
29
+ # set to false.
30
+ #
31
+ # The :logger option is not set by default. If you do set it, you should
32
+ # NOT use an existing logger, but instead use a separate custom log.
33
+ # If the log already exists, then the option is effectively ignored. Some
34
+ # additional logging will be written to the global ManageIQ log in
35
+ # debug mode.
36
+ #
37
+ # The following local options are also supported:
38
+ #
39
+ # :passwordless_sudo - If set to true, then it is assumed that the sudo
40
+ # command does not require a password, and 'sudo' will automatically be
41
+ # prepended to your command. For sudo that requires a password, set
42
+ # the :su_user and :su_password options instead.
43
+ #
44
+ # :remember_host - Setting this to true will cause a HostKeyMismatch
45
+ # error to be rescued and retried once after recording the host and
46
+ # key in the known hosts file. By default this is false.
47
+ #
48
+ # :su_user - If set, ssh commands for that object will be executed via
49
+ # sudo. Do not use if :passwordless_sudo is set to true.
50
+ #
51
+ # :su_password - When used in conjunction with :su_user, the password sent
52
+ # to the command prompt when asked for as the result of using the su
53
+ # command. Do not use if :passwordless_sudo is set to true.
54
+ #
55
+ def initialize(host, user, password = nil, options = {})
56
+ @host = host
57
+ @user = user
58
+ @password = password
59
+
60
+ @options = {
61
+ :remember_host => false,
62
+ :verbose => :warn,
63
+ :non_interactive => true,
64
+ :use_agent => false
65
+ }.merge(options)
66
+
67
+ options[:password] = password if password
68
+
69
+ # Pull our custom keys out of the hash because the SSH initializer will complain
70
+ @remember_host = @options.delete(:remember_host)
71
+ @su_user = @options.delete(:su_user)
72
+ @su_password = @options.delete(:su_password)
73
+ @passwordless_sudo = @options.delete(:passwordless_sudo)
74
+
75
+ # Obsolete, delete if passed in
76
+ @options.delete(:authentication_prompt_delay)
77
+ end
78
+
79
+ # Returns a boolean value indicating whether or not the +remember_host+
80
+ # option is set. This tells Net::SSH to record the host and key in the
81
+ # known hosts file, so that subsequent connections will remember them.
82
+ #
83
+ def remember_host?
84
+ !!@remember_host
85
+ end
86
+
87
+ # Download the contents of the remote +from+ file to the local +to+ file. Some
88
+ # messages will be written to the global ManageIQ log in debug mode.
89
+ #
90
+ # Note that the returned data is normally a Net::SFTP::Operations::Download
91
+ # object. If you want to store the file contents in memory, pass an IO object
92
+ # as the second argument.
93
+ #
94
+ def get_file(from, to)
95
+ run_session do |ssh|
96
+ $log&.debug("#{self.class}##{__method__} - Copying file #{host}:#{from} to #{to}.")
97
+ data = ssh.sftp.download!(from, to)
98
+ $log&.debug("#{self.class}##{__method__} - Copying of #{host}:#{from} to #{to}, complete.")
99
+ return data
100
+ end
101
+ end
102
+
103
+ # Upload the contents of local file +to+ to remote location +path+. You may
104
+ # use the specified +content+ instead of the content of the local file.
105
+ #
106
+ # At least one of the +content+ or +path+ parameters must be specified or
107
+ # an error is raised.
108
+ #
109
+ def put_file(to, content = nil, path = nil)
110
+ raise ArgumentError, "Need to provide either content or path" if content.nil? && path.nil?
111
+ run_session do |ssh|
112
+ content ||= IO.binread(path)
113
+ $log&.debug("#{self.class}##{__method__} - Copying file to #{@host}:#{to}.")
114
+ ssh.sftp.file.open(to, 'wb') { |f| f.write(content) }
115
+ $log&.debug("#{self.class}##{__method__} - Copying of file to #{@host}:#{to}, complete.")
116
+ end
117
+ end
118
+
119
+ # Execute the remote +cmd+ via ssh. This is automatically handled via
120
+ # channels on the ssh session so that various states can be checked,
121
+ # stored and logged independently and asynchronously.
122
+ #
123
+ # If the :passwordless_sudo option was set to true in the constructor
124
+ # then the +cmd+ will automatically be prepended with "sudo".
125
+ #
126
+ # If specified, the data collection will stop the first time a +done_string+
127
+ # argument is encountered at the end of a line. In practice you would
128
+ # typically specify a newline character.
129
+ #
130
+ # If present, the +stdin+ argument will be sent to the underlying
131
+ # command as input for those commands that expect it, e.g. tee.
132
+ #
133
+ # If a signal is received, the command returns any sort of non-zero
134
+ # error status, or if any stderr output is encountered then an exception
135
+ # is raised.
136
+ #
137
+ def exec(cmd, done_string = nil, stdin = nil)
138
+ error_buffer = ""
139
+ output_buffer = ""
140
+ status = 0
141
+ signal = nil
142
+ header = "#{self.class}##{__method__}"
143
+
144
+ # If passwordless sudo is true then prepend every command with 'sudo'.
145
+ cmd = 'sudo ' + cmd if @passwordless_sudo
146
+
147
+ run_session do |ssh|
148
+ ssh.open_channel do |channel|
149
+ channel.exec(cmd) do |chan, success|
150
+ raise "#{header} - Could not execute command #{cmd}" unless success
151
+
152
+ $log&.debug("#{header} - Command: #{cmd} started.")
153
+
154
+ if stdin.present?
155
+ chan.send_data(stdin)
156
+ chan.eof!
157
+ end
158
+
159
+ channel.on_data do |_channel, data|
160
+ $log&.debug("#{header} - STDOUT: #{data}")
161
+ output_buffer << data
162
+ data.each_line { |l| return output_buffer if done_string == l.chomp } unless done_string.nil?
163
+ end
164
+
165
+ channel.on_extended_data do |_channel, _type, data|
166
+ $log&.debug("#{header} - STDERR: #{data}")
167
+ error_buffer << data
168
+ end
169
+
170
+ channel.on_request('exit-status') do |_channel, data|
171
+ status = data.read_long || 0
172
+ $log&.debug("#{header} - STATUS: #{status}")
173
+ end
174
+
175
+ channel.on_request('exit-signal') do |_channel, data|
176
+ signal = data.read_string
177
+ $log&.debug("#{header} - SIGNAL: #{signal}")
178
+ end
179
+
180
+ channel.on_eof do |_channel|
181
+ $log&.debug("#{header} - EOF RECEIVED")
182
+ end
183
+
184
+ channel.on_close do |_channel|
185
+ $log&.debug("#{header} - Command: #{cmd}, exit status: #{status}")
186
+ if signal.present? || status.nonzero? || error_buffer.present?
187
+ raise "#{header} - Command '#{cmd}' exited with signal #{signal}" if signal.present?
188
+ raise "#{header} - Command '#{cmd}' exited with status #{status}" if status.nonzero?
189
+ raise "#{header} - Command '#{cmd}' failed: #{error_buffer}"
190
+ end
191
+ return output_buffer
192
+ end
193
+ end # exec
194
+ end # open_channel
195
+ ssh.loop
196
+ end # run_session
197
+ end
198
+
199
+ # Execute the remote +cmd+ via ssh. This is nearly identical to the exec
200
+ # method, and is used only if the :su_user and :su_password options are
201
+ # set in the constructor.
202
+ #
203
+ # The difference between this method and the exec method are primarily in
204
+ # the underlying handling of the sudo user and sudo password parameters, i.e
205
+ # creating a PTY session and dealing with prompts. From the perspective of
206
+ # an end user they are essentially identical.
207
+ #
208
+ def suexec(cmd_str, done_string = nil, stdin = nil)
209
+ error_buffer = ""
210
+ output_buffer = ""
211
+ prompt = ""
212
+ cmd_rx = ""
213
+ status = 0
214
+ signal = nil
215
+ state = :initial
216
+ header = "#{self.class}##{__method__}"
217
+
218
+ run_session do |ssh|
219
+ temp_cmd_file(cmd_str) do |cmd|
220
+ ssh.open_channel do |channel|
221
+ # now we request a "pty" (i.e. interactive) session so we can send data back and forth if needed.
222
+ # it WILL NOT WORK without this, and it has to be done before any call to exec.
223
+ channel.request_pty(:chars_wide => 256) do |_channel, success|
224
+ raise "Could not obtain pty (i.e. an interactive ssh session)" unless success
225
+ end
226
+
227
+ channel.on_data do |channel, data|
228
+ $log&.debug("#{header} - state: [#{state.inspect}] STDOUT: [#{data.hex_dump.chomp}]")
229
+ if state == :prompt
230
+ # Detect the common prompts
231
+ # someuser@somehost ... $ rootuser@somehost ... # [someuser@somehost ...] $ [rootuser@somehost ...] #
232
+ prompt = data if data =~ /^\[*[\w\-\.]+@[\w\-\.]+.+\]*[\#\$]\s*$/
233
+ output_buffer << data
234
+ unless done_string.nil?
235
+ data.each_line { |l| return output_buffer if done_string == l.chomp }
236
+ end
237
+
238
+ if output_buffer[-prompt.length, prompt.length] == prompt
239
+ return output_buffer[0..(output_buffer.length - prompt.length)]
240
+ end
241
+ end
242
+
243
+ if state == :command_sent
244
+ cmd_rx << data
245
+ state = :prompt if cmd_rx == "#{cmd}\r\n"
246
+ end
247
+
248
+ if state == :password_sent
249
+ prompt << data.lstrip
250
+ if data.strip =~ /\#/
251
+ $log&.debug("#{header} - Superuser Prompt detected: sending command #{cmd}")
252
+ channel.send_data("#{cmd}\n")
253
+ state = :command_sent
254
+ end
255
+ end
256
+
257
+ if state == :initial
258
+ prompt << data.lstrip
259
+ if data.strip =~ /[Pp]assword:/
260
+ prompt = ""
261
+ $log&.debug("#{header} - Password Prompt detected: sending su password")
262
+ channel.send_data("#{@su_password}\n")
263
+ state = :password_sent
264
+ end
265
+ end
266
+ end
267
+
268
+ channel.on_extended_data do |_channel, _type, data|
269
+ $log&.debug("#{header} - STDERR: #{data}")
270
+ error_buffer << data
271
+ end
272
+
273
+ channel.on_request('exit-status') do |_channel, data|
274
+ status = data.read_long
275
+ $log&.debug("#{header} - STATUS: #{status}")
276
+ end
277
+
278
+ channel.on_request('exit-signal') do |_channel, data|
279
+ signal = data.read_string
280
+ $log&.debug("#{header} - SIGNAL: #{signal}")
281
+ end
282
+
283
+ channel.on_eof do |_channel|
284
+ $log&.debug("#{header} - EOF RECEIVED")
285
+ end
286
+
287
+ channel.on_close do |_channel|
288
+ error_buffer << prompt if [:initial, :password_sent].include?(state)
289
+ $log&.debug("#{header} - Command: #{cmd}, exit status: #{status}")
290
+ raise "#{header} - Command #{cmd}, exited with signal #{signal}" unless signal.nil?
291
+ unless status.zero?
292
+ raise "#{header} - Command #{cmd}, exited with status #{status}" if error_buffer.empty?
293
+ raise "#{header} - Command #{cmd} failed: #{error_buffer}, status: #{status}"
294
+ end
295
+ return output_buffer
296
+ end
297
+
298
+ $log&.debug("#{header} - Command: [#{cmd_str}] started.")
299
+ su_command = @su_user == 'root' ? "su -l\n" : "su -l #{@su_user}\n"
300
+
301
+ channel.exec(su_command) do |chan, success|
302
+ raise "#{header} - Could not execute command #{cmd}" unless success
303
+ if stdin.present?
304
+ chan.send_data(stdin)
305
+ chan.eof!
306
+ end
307
+ end
308
+ end
309
+ end
310
+ ssh.loop
311
+ end
312
+ end
313
+
314
+ # Creates a local temporary file under /var/tmp with +cmd+ as its contents.
315
+ # The tempfile name is the name of the command with "miq-" prepended and ".sh"
316
+ # appended to the end.
317
+ #
318
+ # The end result is a string meant to be run via the suexec method. For example:
319
+ #
320
+ # "chmod 700 /var/tmp/miq-foo.sh; /var/tmp/miq-foo.sh; rm -f /var/tmp/miq-foo.sh
321
+ #
322
+ def temp_cmd_file(cmd)
323
+ temp_remote_script = Tempfile.new(["miq-", ".sh"], "/var/tmp")
324
+ temp_file = temp_remote_script.path
325
+ begin
326
+ temp_remote_script.write(cmd)
327
+ temp_remote_script.close
328
+ remote_cmd = "chmod 700 #{temp_file}; #{temp_file}; rm -f #{temp_file}"
329
+ yield(remote_cmd)
330
+ ensure
331
+ temp_remote_script.close!
332
+ end
333
+ end
334
+
335
+ # Shortcut method that creates and yields a ManageIQ::SSH::Util object, with the +host+,
336
+ # +remote_user+ and +remote_password+ options passed in as the first three
337
+ # params to the constructor, while the +su_user+ and +su_password+ parameters
338
+ # automatically set the corresponding :su_user and :su_password options. The
339
+ # remaining options are passed normally.
340
+ #
341
+ # This method is functionally identical to the following code, except that it
342
+ # yields itself (and nil) and re-raises certain Net::SSH exceptions as
343
+ # ManageIQ exceptions.
344
+ #
345
+ # ManageIQ::SSH::Util.new(host, remote_user, remote_password, {:su_user => su_user, :su_password => su_password})
346
+ #
347
+ def self.shell_with_su(host, remote_user, remote_password, su_user, su_password, options = {})
348
+ options[:su_user], options[:su_password] = su_user, su_password
349
+ ssu = new(host, remote_user, remote_password, options)
350
+ yield(ssu, nil)
351
+ rescue Net::SSH::AuthenticationFailed
352
+ raise MiqException::MiqInvalidCredentialsError
353
+ rescue Net::SSH::HostKeyMismatch
354
+ raise MiqException::MiqSshUtilHostKeyMismatch
355
+ end
356
+
357
+ # Executes the provided +cmd+ using the exec or suexec method, depending on
358
+ # whether or not the :su_user option is set. The +done_string+ and +stdin+
359
+ # arguments are passed along to the appropriate method as well.
360
+ #
361
+ # In the case of suexec, escape characters are automatically removed from
362
+ # the final output.
363
+ #
364
+ #--
365
+ # The _shell argument appears to be an artifact that has been retained
366
+ # over time for reasons that aren't immediately apparent.
367
+ #
368
+ def shell_exec(cmd, done_string = nil, _shell = nil, stdin = nil)
369
+ return exec(cmd, done_string, stdin) if @su_user.nil?
370
+ ret = suexec(cmd, done_string, stdin)
371
+ # Remove escape character from the end of the line
372
+ ret.sub!(/\e$/, '')
373
+ ret
374
+ end
375
+
376
+ # Copies the remote +file_path+ to a local temporary file, and then
377
+ # yields or returns a filehandle to the local temporary file.
378
+ #--
379
+ # Presumably this method was meant for use with the SCVMM provider
380
+ # given the hardcoded name of the temporary file.
381
+ #
382
+ def file_open(file_path, perm = 'r')
383
+ if block_given?
384
+ Tempfile.open('miqscvmm') do |tf|
385
+ tf.close
386
+ get_file(file_path, tf.path)
387
+ File.open(tf.path, perm) { |f| yield(f) }
388
+ end
389
+ else
390
+ tf = Tempfile.open('miqscvmm')
391
+ tf.close
392
+ get_file(file_path, tf.path)
393
+ File.open(tf.path, perm)
394
+ end
395
+ end
396
+
397
+ # Returns whether or not the remote +filename+ exists.
398
+ #
399
+ def file_exists?(filename)
400
+ shell_exec("test -f #{filename}")
401
+ rescue
402
+ false
403
+ else
404
+ true
405
+ end
406
+
407
+ # This method creates and yields an ssh object. If the :remember_host option
408
+ # was set to true, it will record this host and key in the known hosts file
409
+ # and retry once.
410
+ #
411
+ def run_session
412
+ first_try = true
413
+
414
+ begin
415
+ Net::SSH.start(@host, @user, @options) do |ssh|
416
+ yield(ssh)
417
+ end
418
+ rescue Net::SSH::HostKeyMismatch => err
419
+ if remember_host? && first_try
420
+ # Save fingerprint and try again
421
+ first_try = false
422
+ err.remember_host!
423
+ retry
424
+ else
425
+ # Re-raise error
426
+ raise err
427
+ end
428
+ end
429
+ end
430
+ end # Util
431
+ end # SSH
432
+ end # ManageIQ
@@ -0,0 +1,8 @@
1
+ module ManageIQ
2
+ class SSH
3
+ class Util
4
+ # The version of the manageiq-ssh-util library
5
+ VERSION = '0.1.0'.freeze
6
+ end
7
+ end
8
+ end
metadata ADDED
@@ -0,0 +1,146 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: manageiq-ssh-util
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - ManageIQ Developers
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2020-01-28 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: activesupport
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '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'
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: '4.2'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '4.2'
41
+ - !ruby/object:Gem::Dependency
42
+ name: net-sftp
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '2.1'
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '2.1'
55
+ - !ruby/object:Gem::Dependency
56
+ name: codeclimate-test-reporter
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: 1.0.0
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: 1.0.0
69
+ - !ruby/object:Gem::Dependency
70
+ name: rake
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - ">="
74
+ - !ruby/object:Gem::Version
75
+ version: '0'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - ">="
81
+ - !ruby/object:Gem::Version
82
+ version: '0'
83
+ - !ruby/object:Gem::Dependency
84
+ name: rspec
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - ">="
88
+ - !ruby/object:Gem::Version
89
+ version: '0'
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - ">="
95
+ - !ruby/object:Gem::Version
96
+ version: '0'
97
+ - !ruby/object:Gem::Dependency
98
+ name: simplecov
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - ">="
102
+ - !ruby/object:Gem::Version
103
+ version: '0'
104
+ type: :development
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - ">="
109
+ - !ruby/object:Gem::Version
110
+ version: '0'
111
+ description: ManageIQ wrapper library for net-ssh
112
+ email:
113
+ executables: []
114
+ extensions: []
115
+ extra_rdoc_files: []
116
+ files:
117
+ - LICENSE.txt
118
+ - README.md
119
+ - Rakefile
120
+ - lib/manageiq-ssh-util.rb
121
+ - lib/manageiq/ssh/util.rb
122
+ - lib/manageiq/ssh/util/version.rb
123
+ homepage: https://github.com/ManageIQ/manageiq-ssh-util
124
+ licenses:
125
+ - Apache-2.0
126
+ metadata: {}
127
+ post_install_message:
128
+ rdoc_options: []
129
+ require_paths:
130
+ - lib
131
+ required_ruby_version: !ruby/object:Gem::Requirement
132
+ requirements:
133
+ - - ">="
134
+ - !ruby/object:Gem::Version
135
+ version: '0'
136
+ required_rubygems_version: !ruby/object:Gem::Requirement
137
+ requirements:
138
+ - - ">="
139
+ - !ruby/object:Gem::Version
140
+ version: '0'
141
+ requirements: []
142
+ rubygems_version: 3.0.6
143
+ signing_key:
144
+ specification_version: 4
145
+ summary: ManageIQ wrapper library for net-ssh
146
+ test_files: []