manageiq-ssh-util 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/LICENSE.txt +202 -0
- data/README.md +17 -0
- data/Rakefile +6 -0
- data/lib/manageiq-ssh-util.rb +1 -0
- data/lib/manageiq/ssh/util.rb +432 -0
- data/lib/manageiq/ssh/util/version.rb +8 -0
- metadata +146 -0
checksums.yaml
ADDED
@@ -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
|
data/LICENSE.txt
ADDED
@@ -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.
|
data/README.md
ADDED
@@ -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.
|
data/Rakefile
ADDED
@@ -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
|
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: []
|