remotus 0.2.2 → 0.4.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 +4 -4
- data/.rubocop.yml +4 -1
- data/CHANGELOG.md +9 -0
- data/Gemfile.lock +43 -36
- data/README.md +4 -1
- data/lib/remotus/core_ext/elevated.rb +35 -0
- data/lib/remotus/core_ext/string.rb +5 -1
- data/lib/remotus/result.rb +1 -1
- data/lib/remotus/ssh_connection.rb +120 -105
- data/lib/remotus/version.rb +1 -1
- data/lib/remotus/winrm_connection.rb +23 -9
- data/remotus.gemspec +2 -1
- metadata +18 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 34953ce4b1c254e31a40b7b3ff3795124fe98b712722862a35813cceba686759
|
|
4
|
+
data.tar.gz: 3d342087b707b3078cedf38038b3ba21c1a1db6ed7c7d189f5912d9cd5adf4e7
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: c884fbb30a585899e840ab9e3dddd3b3d6ac052d4b10745bc9c582f9e67ca445aa9ef98af7ac8cbba551cc6594f23a9583d3f54cfba6202f3b8595804bd60641
|
|
7
|
+
data.tar.gz: 166f8cb4e3e633a79f1f28ddba9a3ff8793cc2927873bdd349e035870273c81ad6a9688f37d2f871413f43451604f64a06631e0813f8e111f7d36c701a168b05
|
data/.rubocop.yml
CHANGED
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,14 @@
|
|
|
1
1
|
## [Unreleased]
|
|
2
2
|
|
|
3
|
+
## [0.4.0] - 2022-06-02
|
|
4
|
+
* Added winrm-elevated gem to solve wirnrm AuthenticationError
|
|
5
|
+
|
|
6
|
+
## [0.3.0] - 2022-02-18
|
|
7
|
+
* Add retries to SSH SCP transactions
|
|
8
|
+
|
|
9
|
+
## [0.2.3] - 2021-05-01
|
|
10
|
+
* Resolve rexml vulnerability CVE-2021-28965
|
|
11
|
+
|
|
3
12
|
## [0.2.2] - 2021-03-23
|
|
4
13
|
* Ensure both user and password are populated before using a cached credential
|
|
5
14
|
|
data/Gemfile.lock
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
PATH
|
|
2
2
|
remote: .
|
|
3
3
|
specs:
|
|
4
|
-
remotus (0.
|
|
4
|
+
remotus (0.4.0)
|
|
5
5
|
connection_pool (~> 2.2)
|
|
6
6
|
net-scp (~> 3.0)
|
|
7
7
|
net-ssh (~> 6.1)
|
|
8
8
|
winrm (~> 2.3)
|
|
9
|
+
winrm-elevated (~> 1.2)
|
|
9
10
|
winrm-fs (~> 1.3)
|
|
10
11
|
|
|
11
12
|
GEM
|
|
@@ -13,17 +14,18 @@ GEM
|
|
|
13
14
|
specs:
|
|
14
15
|
ast (2.4.2)
|
|
15
16
|
builder (3.2.4)
|
|
16
|
-
connection_pool (2.2.
|
|
17
|
-
diff-lcs (1.
|
|
17
|
+
connection_pool (2.2.5)
|
|
18
|
+
diff-lcs (1.5.0)
|
|
18
19
|
erubi (1.10.0)
|
|
19
|
-
ffi (1.15.
|
|
20
|
+
ffi (1.15.5)
|
|
20
21
|
gssapi (1.3.1)
|
|
21
22
|
ffi (>= 1.0.1)
|
|
22
|
-
gyoku (1.
|
|
23
|
+
gyoku (1.4.0)
|
|
23
24
|
builder (>= 2.1.2)
|
|
25
|
+
rexml (~> 3.0)
|
|
24
26
|
httpclient (2.8.3)
|
|
25
27
|
little-plugger (1.1.4)
|
|
26
|
-
logging (2.3.
|
|
28
|
+
logging (2.3.1)
|
|
27
29
|
little-plugger (~> 1.1)
|
|
28
30
|
multi_json (~> 1.14)
|
|
29
31
|
multi_json (1.15.0)
|
|
@@ -31,46 +33,46 @@ GEM
|
|
|
31
33
|
net-ssh (>= 2.6.5, < 7.0.0)
|
|
32
34
|
net-ssh (6.1.0)
|
|
33
35
|
nori (2.6.0)
|
|
34
|
-
parallel (1.
|
|
35
|
-
parser (3.
|
|
36
|
+
parallel (1.21.0)
|
|
37
|
+
parser (3.1.0.0)
|
|
36
38
|
ast (~> 2.4.1)
|
|
37
|
-
rainbow (3.
|
|
38
|
-
rake (13.0.
|
|
39
|
-
regexp_parser (2.
|
|
40
|
-
rexml (3.2.
|
|
41
|
-
rspec (3.
|
|
42
|
-
rspec-core (~> 3.
|
|
43
|
-
rspec-expectations (~> 3.
|
|
44
|
-
rspec-mocks (~> 3.
|
|
45
|
-
rspec-core (3.
|
|
46
|
-
rspec-support (~> 3.
|
|
47
|
-
rspec-expectations (3.
|
|
39
|
+
rainbow (3.1.1)
|
|
40
|
+
rake (13.0.6)
|
|
41
|
+
regexp_parser (2.2.1)
|
|
42
|
+
rexml (3.2.5)
|
|
43
|
+
rspec (3.11.0)
|
|
44
|
+
rspec-core (~> 3.11.0)
|
|
45
|
+
rspec-expectations (~> 3.11.0)
|
|
46
|
+
rspec-mocks (~> 3.11.0)
|
|
47
|
+
rspec-core (3.11.0)
|
|
48
|
+
rspec-support (~> 3.11.0)
|
|
49
|
+
rspec-expectations (3.11.0)
|
|
48
50
|
diff-lcs (>= 1.2.0, < 2.0)
|
|
49
|
-
rspec-support (~> 3.
|
|
50
|
-
rspec-mocks (3.
|
|
51
|
+
rspec-support (~> 3.11.0)
|
|
52
|
+
rspec-mocks (3.11.0)
|
|
51
53
|
diff-lcs (>= 1.2.0, < 2.0)
|
|
52
|
-
rspec-support (~> 3.
|
|
53
|
-
rspec-support (3.
|
|
54
|
-
rubocop (1.
|
|
54
|
+
rspec-support (~> 3.11.0)
|
|
55
|
+
rspec-support (3.11.0)
|
|
56
|
+
rubocop (1.25.1)
|
|
55
57
|
parallel (~> 1.10)
|
|
56
|
-
parser (>= 3.
|
|
58
|
+
parser (>= 3.1.0.0)
|
|
57
59
|
rainbow (>= 2.2.2, < 4.0)
|
|
58
60
|
regexp_parser (>= 1.8, < 3.0)
|
|
59
61
|
rexml
|
|
60
|
-
rubocop-ast (>= 1.
|
|
62
|
+
rubocop-ast (>= 1.15.1, < 2.0)
|
|
61
63
|
ruby-progressbar (~> 1.7)
|
|
62
64
|
unicode-display_width (>= 1.4.0, < 3.0)
|
|
63
|
-
rubocop-ast (1.
|
|
64
|
-
parser (>=
|
|
65
|
-
rubocop-rake (0.
|
|
66
|
-
rubocop
|
|
67
|
-
rubocop-rspec (2.2.0)
|
|
65
|
+
rubocop-ast (1.15.2)
|
|
66
|
+
parser (>= 3.0.1.1)
|
|
67
|
+
rubocop-rake (0.6.0)
|
|
68
68
|
rubocop (~> 1.0)
|
|
69
|
-
|
|
69
|
+
rubocop-rspec (2.8.0)
|
|
70
|
+
rubocop (~> 1.19)
|
|
70
71
|
ruby-progressbar (1.11.0)
|
|
71
72
|
rubyntlm (0.6.3)
|
|
72
|
-
rubyzip (2.3.
|
|
73
|
-
unicode-display_width (2.
|
|
73
|
+
rubyzip (2.3.2)
|
|
74
|
+
unicode-display_width (2.1.0)
|
|
75
|
+
webrick (1.7.0)
|
|
74
76
|
winrm (2.3.6)
|
|
75
77
|
builder (>= 2.1.2)
|
|
76
78
|
erubi (~> 1.8)
|
|
@@ -80,12 +82,17 @@ GEM
|
|
|
80
82
|
logging (>= 1.6.1, < 3.0)
|
|
81
83
|
nori (~> 2.0)
|
|
82
84
|
rubyntlm (~> 0.6.0, >= 0.6.3)
|
|
85
|
+
winrm-elevated (1.2.3)
|
|
86
|
+
erubi (~> 1.8)
|
|
87
|
+
winrm (~> 2.0)
|
|
88
|
+
winrm-fs (~> 1.0)
|
|
83
89
|
winrm-fs (1.3.5)
|
|
84
90
|
erubi (~> 1.8)
|
|
85
91
|
logging (>= 1.6.1, < 3.0)
|
|
86
92
|
rubyzip (~> 2.0)
|
|
87
93
|
winrm (~> 2.0)
|
|
88
|
-
yard (0.9.
|
|
94
|
+
yard (0.9.27)
|
|
95
|
+
webrick (~> 1.7.0)
|
|
89
96
|
|
|
90
97
|
PLATFORMS
|
|
91
98
|
ruby
|
|
@@ -101,4 +108,4 @@ DEPENDENCIES
|
|
|
101
108
|
yard (~> 0.9)
|
|
102
109
|
|
|
103
110
|
BUNDLED WITH
|
|
104
|
-
2.2.
|
|
111
|
+
2.2.22
|
data/README.md
CHANGED
|
@@ -54,6 +54,9 @@ result.exit_code
|
|
|
54
54
|
# Run a command on the remote host with sudo (Linux only, requires password to be specified)
|
|
55
55
|
result = connection.run("ls /root", sudo: true)
|
|
56
56
|
|
|
57
|
+
# Run a command on the remote host with elevated shell privilege
|
|
58
|
+
result = connection.run("ipconfig", shell: :elevated)
|
|
59
|
+
|
|
57
60
|
# Run a script on the remote host
|
|
58
61
|
connection.run_script("/local/script.sh", "/remote/path/script.sh")
|
|
59
62
|
|
|
@@ -87,7 +90,7 @@ require "remotus"
|
|
|
87
90
|
|
|
88
91
|
class SimpleStore < Remotus::Auth::Store
|
|
89
92
|
def credential(connection, **options)
|
|
90
|
-
"#{connection.host}_password"
|
|
93
|
+
Remotus::Auth::Credential.new('user', "#{connection.host}_password")
|
|
91
94
|
end
|
|
92
95
|
end
|
|
93
96
|
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "winrm-elevated"
|
|
4
|
+
|
|
5
|
+
module Remotus
|
|
6
|
+
# Core Ruby extensions
|
|
7
|
+
module CoreExt
|
|
8
|
+
# WinRM Elevated extension module
|
|
9
|
+
module Elevated
|
|
10
|
+
unless method_defined?(:connection_opts)
|
|
11
|
+
#
|
|
12
|
+
# Returns a hash for the connection options from the interal
|
|
13
|
+
# WinRM::Shells::Powershell object
|
|
14
|
+
#
|
|
15
|
+
# @return [Hash] internal WinRM::Shells::Powershell connection options
|
|
16
|
+
#
|
|
17
|
+
def connection_opts
|
|
18
|
+
@shell.connection_opts
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# @api private
|
|
26
|
+
# Main WinRM module
|
|
27
|
+
module WinRM
|
|
28
|
+
# Shells module (contains PowerShell, Elevated, etc.)
|
|
29
|
+
module Shells
|
|
30
|
+
# Elevated PowerShell class from winrm-elevated
|
|
31
|
+
class Elevated
|
|
32
|
+
include Remotus::CoreExt::Elevated
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
data/lib/remotus/result.rb
CHANGED
|
@@ -66,7 +66,7 @@ module Remotus
|
|
|
66
66
|
return unless error?(accepted_exit_codes)
|
|
67
67
|
|
|
68
68
|
raise Remotus::ResultError, "Error encountered executing #{@command}! Exit code #{@exit_code} was returned "\
|
|
69
|
-
|
|
69
|
+
"while a value in #{accepted_exit_codes} was expected.\n#{output}"
|
|
70
70
|
end
|
|
71
71
|
|
|
72
72
|
#
|
|
@@ -113,7 +113,7 @@ module Remotus
|
|
|
113
113
|
# @param [Hash] options command options
|
|
114
114
|
# @option options [Boolean] :sudo whether to run the command with sudo (defaults to false)
|
|
115
115
|
# @option options [Boolean] :pty whether to allocate a terminal (defaults to false)
|
|
116
|
-
# @option options [Integer] :retries number of times to retry a closed connection (defaults to
|
|
116
|
+
# @option options [Integer] :retries number of times to retry a closed connection (defaults to 2)
|
|
117
117
|
# @option options [String] :input stdin input to provide to the command
|
|
118
118
|
# @option options [Array<Integer>] :accepted_exit_codes array of acceptable exit codes (defaults to [0])
|
|
119
119
|
# only used if :on_error or :on_success are set
|
|
@@ -141,121 +141,88 @@ module Remotus
|
|
|
141
141
|
# Refer to the command by object_id throughout the log to avoid logging sensitive data
|
|
142
142
|
Remotus.logger.debug { "Preparing to run command #{command.object_id} on #{@host}" }
|
|
143
143
|
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
# If password was nil, raise an exception
|
|
151
|
-
raise Remotus::MissingSudoPassword, "#{host} credential does not have a password specified" if input.start_with?("\n")
|
|
152
|
-
end
|
|
144
|
+
with_retries(command, retries) do
|
|
145
|
+
# Handle sudo
|
|
146
|
+
if options[:sudo]
|
|
147
|
+
Remotus.logger.debug { "Sudo is enabled for command #{command.object_id}" }
|
|
148
|
+
ssh_command = "sudo -p '' -S sh -c '#{command.gsub("'", "'\"'\"'")}'"
|
|
149
|
+
input = "#{Remotus::Auth.credential(self).password}\n#{input}"
|
|
153
150
|
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
# Open an SSH channel to the host
|
|
159
|
-
channel_handle = connection.open_channel do |channel|
|
|
160
|
-
# Execute the command
|
|
161
|
-
if pty
|
|
162
|
-
Remotus.logger.debug { "Requesting pty for command #{command.object_id}" }
|
|
163
|
-
channel.request_pty do |ch, success|
|
|
164
|
-
raise Remotus::PtyError, "could not obtain pty" unless success
|
|
151
|
+
# If password was nil, raise an exception
|
|
152
|
+
raise Remotus::MissingSudoPassword, "#{host} credential does not have a password specified" if input.start_with?("\n")
|
|
153
|
+
end
|
|
165
154
|
|
|
166
|
-
|
|
155
|
+
# Allocate a terminal if specified
|
|
156
|
+
pty = options[:pty] || false
|
|
157
|
+
skip_first_output = pty && options[:sudo]
|
|
158
|
+
|
|
159
|
+
# Open an SSH channel to the host
|
|
160
|
+
channel_handle = connection.open_channel do |channel|
|
|
161
|
+
# Execute the command
|
|
162
|
+
if pty
|
|
163
|
+
Remotus.logger.debug { "Requesting pty for command #{command.object_id}" }
|
|
164
|
+
channel.request_pty do |ch, success|
|
|
165
|
+
raise Remotus::PtyError, "could not obtain pty" unless success
|
|
166
|
+
|
|
167
|
+
ch.exec(ssh_command)
|
|
168
|
+
end
|
|
169
|
+
else
|
|
170
|
+
Remotus.logger.debug { "Executing command #{command.object_id}" }
|
|
171
|
+
channel.exec(ssh_command)
|
|
167
172
|
end
|
|
168
|
-
else
|
|
169
|
-
Remotus.logger.debug { "Executing command #{command.object_id}" }
|
|
170
|
-
channel.exec(ssh_command)
|
|
171
|
-
end
|
|
172
173
|
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
174
|
+
# Provide input
|
|
175
|
+
unless input.empty?
|
|
176
|
+
Remotus.logger.debug { "Sending input for command #{command.object_id}" }
|
|
177
|
+
channel.send_data input
|
|
178
|
+
channel.eof!
|
|
179
|
+
end
|
|
179
180
|
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
181
|
+
# Process stdout
|
|
182
|
+
channel.on_data do |ch, data|
|
|
183
|
+
# Skip the first iteration if sudo and pty is enabled to avoid outputting the sudo password
|
|
184
|
+
if skip_first_output
|
|
185
|
+
skip_first_output = false
|
|
186
|
+
next
|
|
187
|
+
end
|
|
188
|
+
stdout << data
|
|
189
|
+
output << data
|
|
190
|
+
options[:on_stdout].call(ch, data) if options[:on_stdout].respond_to?(:call)
|
|
191
|
+
options[:on_output].call(ch, data) if options[:on_output].respond_to?(:call)
|
|
186
192
|
end
|
|
187
|
-
stdout << data
|
|
188
|
-
output << data
|
|
189
|
-
options[:on_stdout].call(ch, data) if options[:on_stdout].respond_to?(:call)
|
|
190
|
-
options[:on_output].call(ch, data) if options[:on_output].respond_to?(:call)
|
|
191
|
-
end
|
|
192
193
|
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
194
|
+
# Process stderr
|
|
195
|
+
channel.on_extended_data do |ch, _, data|
|
|
196
|
+
stderr << data
|
|
197
|
+
output << data
|
|
198
|
+
options[:on_stderr].call(ch, data) if options[:on_stderr].respond_to?(:call)
|
|
199
|
+
options[:on_output].call(ch, data) if options[:on_output].respond_to?(:call)
|
|
200
|
+
end
|
|
200
201
|
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
202
|
+
# Process exit status/code
|
|
203
|
+
channel.on_request("exit-status") do |_, data|
|
|
204
|
+
exit_code = data.read_long
|
|
205
|
+
end
|
|
204
206
|
end
|
|
205
|
-
end
|
|
206
|
-
|
|
207
|
-
# Block until the command has completed execution
|
|
208
|
-
channel_handle.wait
|
|
209
|
-
|
|
210
|
-
Remotus.logger.debug { "Generating result for command #{command.object_id}" }
|
|
211
|
-
result = Remotus::Result.new(command, stdout, stderr, output, exit_code)
|
|
212
207
|
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
raise Remotus::AuthenticationError, "Could not authenticate to sudo as #{Remotus::Auth.credential(self).user}"
|
|
216
|
-
end
|
|
208
|
+
# Block until the command has completed execution
|
|
209
|
+
channel_handle.wait
|
|
217
210
|
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
options[:on_error].call(result) if options[:on_error].respond_to?(:call) && result.error?(accepted_exit_codes)
|
|
221
|
-
options[:on_complete].call(result) if options[:on_complete].respond_to?(:call)
|
|
211
|
+
Remotus.logger.debug { "Generating result for command #{command.object_id}" }
|
|
212
|
+
result = Remotus::Result.new(command, stdout, stderr, output, exit_code)
|
|
222
213
|
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
"Sudo authentication failed for command #{command.object_id}, retrying with #{retries} attempt#{retries.abs == 1 ? "" : "s"} remaining..."
|
|
228
|
-
end
|
|
229
|
-
retries -= 1
|
|
230
|
-
raise if retries.negative?
|
|
231
|
-
|
|
232
|
-
# Remove user password to force credential store update on next retry
|
|
233
|
-
Remotus.logger.debug { "Removing current credential for #{@host} to force credential retrieval." }
|
|
234
|
-
Remotus::Auth.cache.delete(@host)
|
|
235
|
-
|
|
236
|
-
retry
|
|
237
|
-
rescue Net::SSH::AuthenticationFailed => e
|
|
238
|
-
# Attempt to update the user password and retry
|
|
239
|
-
Remotus.logger.debug do
|
|
240
|
-
"SSH authentication failed for command #{command.object_id}, retrying with #{retries} attempt#{retries.abs == 1 ? "" : "s"} remaining..."
|
|
241
|
-
end
|
|
242
|
-
retries -= 1
|
|
243
|
-
raise Remotus::AuthenticationError, e.to_s if retries.negative?
|
|
214
|
+
# If we are using sudo and experience an authentication failure, raise an exception
|
|
215
|
+
if options[:sudo] && result.error? && !result.stderr.empty? && result.stderr.match?(/^sudo: \d+ incorrect password attempts?$/)
|
|
216
|
+
raise Remotus::AuthenticationError, "Could not authenticate to sudo as #{Remotus::Auth.credential(self).user}"
|
|
217
|
+
end
|
|
244
218
|
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
219
|
+
# Perform success, error, and completion callbacks
|
|
220
|
+
options[:on_success].call(result) if options[:on_success].respond_to?(:call) && result.success?(accepted_exit_codes)
|
|
221
|
+
options[:on_error].call(result) if options[:on_error].respond_to?(:call) && result.error?(accepted_exit_codes)
|
|
222
|
+
options[:on_complete].call(result) if options[:on_complete].respond_to?(:call)
|
|
248
223
|
|
|
249
|
-
|
|
250
|
-
rescue IOError => e
|
|
251
|
-
# Re-raise exception if it is not a closed stream error or if the retry count is exceeded
|
|
252
|
-
Remotus.logger.debug do
|
|
253
|
-
"IOError (#{e}) encountered for command #{command.object_id}, retrying with #{retries} attempt#{retries.abs == 1 ? "" : "s"} remaining..."
|
|
224
|
+
result
|
|
254
225
|
end
|
|
255
|
-
retries -= 1
|
|
256
|
-
raise if e.to_s != "closed stream" || retries.negative?
|
|
257
|
-
|
|
258
|
-
retry
|
|
259
226
|
end
|
|
260
227
|
|
|
261
228
|
#
|
|
@@ -267,7 +234,7 @@ module Remotus
|
|
|
267
234
|
# @param [Hash] options command options
|
|
268
235
|
# @option options [Boolean] :sudo whether to run the script with sudo (defaults to false)
|
|
269
236
|
# @option options [Boolean] :pty whether to allocate a terminal (defaults to false)
|
|
270
|
-
# @option options [Integer] :retries number of times to retry a closed connection (defaults to
|
|
237
|
+
# @option options [Integer] :retries number of times to retry a closed connection (defaults to 2)
|
|
271
238
|
# @option options [String] :input stdin input to provide to the command
|
|
272
239
|
# @option options [Array<Integer>] :accepted_exit_codes array of acceptable exit codes (defaults to [0])
|
|
273
240
|
# only used if :on_error or :on_success are set
|
|
@@ -297,6 +264,7 @@ module Remotus
|
|
|
297
264
|
# @option options [String] :owner file owner ("oracle")
|
|
298
265
|
# @option options [String] :group file group ("dba")
|
|
299
266
|
# @option options [String] :mode file mode ("0640")
|
|
267
|
+
# @option options [Integer] :retries number of times to retry a closed connection (defaults to 2)
|
|
300
268
|
#
|
|
301
269
|
# @return [String] remote path
|
|
302
270
|
#
|
|
@@ -307,7 +275,11 @@ module Remotus
|
|
|
307
275
|
sudo_upload(local_path, remote_path, options)
|
|
308
276
|
else
|
|
309
277
|
permission_cmd = permission_cmds(remote_path, options[:owner], options[:group], options[:mode])
|
|
310
|
-
|
|
278
|
+
|
|
279
|
+
with_retries("Upload #{local_path} to #{remote_path}", options[:retries] || DEFAULT_RETRIES) do
|
|
280
|
+
connection.scp.upload!(local_path, remote_path, options)
|
|
281
|
+
end
|
|
282
|
+
|
|
311
283
|
run(permission_cmd).error! unless permission_cmd.empty?
|
|
312
284
|
end
|
|
313
285
|
|
|
@@ -322,6 +294,7 @@ module Remotus
|
|
|
322
294
|
# if local_path is nil, the file's content will be returned
|
|
323
295
|
# @param [Hash] options download options
|
|
324
296
|
# @option options [Boolean] :sudo whether to run the download with sudo (defaults to false)
|
|
297
|
+
# @option options [Integer] :retries number of times to retry a closed connection (defaults to 2)
|
|
325
298
|
#
|
|
326
299
|
# @return [String] local path or file content (if local_path is nil)
|
|
327
300
|
#
|
|
@@ -343,7 +316,12 @@ module Remotus
|
|
|
343
316
|
end
|
|
344
317
|
|
|
345
318
|
Remotus.logger.debug { "Downloading file from #{@host}:#{remote_path}" }
|
|
346
|
-
|
|
319
|
+
|
|
320
|
+
result = nil
|
|
321
|
+
|
|
322
|
+
with_retries("Download #{remote_path} to #{local_path}", options[:retries] || DEFAULT_RETRIES) do
|
|
323
|
+
result = connection.scp.download!(remote_path, local_path, options)
|
|
324
|
+
end
|
|
347
325
|
|
|
348
326
|
# Return the file content if that is desired
|
|
349
327
|
local_path.nil? ? result : local_path
|
|
@@ -372,6 +350,39 @@ module Remotus
|
|
|
372
350
|
|
|
373
351
|
private
|
|
374
352
|
|
|
353
|
+
#
|
|
354
|
+
# Wraps one or many SSH commands to provide exception handling and retry support
|
|
355
|
+
# to a given block
|
|
356
|
+
#
|
|
357
|
+
# @param [String] command command to be run or command description
|
|
358
|
+
# @param [Integer] retries number of retries
|
|
359
|
+
#
|
|
360
|
+
def with_retries(command, retries)
|
|
361
|
+
yield if block_given?
|
|
362
|
+
rescue Remotus::AuthenticationError, Net::SSH::AuthenticationFailed => e
|
|
363
|
+
# Re-raise exception if the retry count is exceeded
|
|
364
|
+
Remotus.logger.debug do
|
|
365
|
+
"Sudo authentication failed for command #{command.object_id}, retrying with #{retries} attempt#{retries.abs == 1 ? "" : "s"} remaining..."
|
|
366
|
+
end
|
|
367
|
+
retries -= 1
|
|
368
|
+
raise Remotus::AuthenticationError, e.to_s if retries.negative?
|
|
369
|
+
|
|
370
|
+
# Remove user password to force credential store update on next retry
|
|
371
|
+
Remotus.logger.debug { "Removing current credential for #{@host} to force credential retrieval." }
|
|
372
|
+
Remotus::Auth.cache.delete(@host)
|
|
373
|
+
|
|
374
|
+
retry
|
|
375
|
+
rescue IOError => e
|
|
376
|
+
# Re-raise exception if it is not a closed stream error or if the retry count is exceeded
|
|
377
|
+
Remotus.logger.debug do
|
|
378
|
+
"IOError (#{e}) encountered for command #{command.object_id}, retrying with #{retries} attempt#{retries.abs == 1 ? "" : "s"} remaining..."
|
|
379
|
+
end
|
|
380
|
+
retries -= 1
|
|
381
|
+
raise if e.to_s != "closed stream" || retries.negative?
|
|
382
|
+
|
|
383
|
+
retry
|
|
384
|
+
end
|
|
385
|
+
|
|
375
386
|
#
|
|
376
387
|
# Whether to restart the current SSH connection
|
|
377
388
|
#
|
|
@@ -413,13 +424,17 @@ module Remotus
|
|
|
413
424
|
# @option options [String] :owner file owner ("oracle")
|
|
414
425
|
# @option options [String] :group file group ("dba")
|
|
415
426
|
# @option options [String] :mode file mode ("0640")
|
|
427
|
+
# @option options [Integer] :retries number of times to retry a closed connection (defaults to 2)
|
|
416
428
|
#
|
|
417
429
|
def sudo_upload(local_path, remote_path, options = {})
|
|
418
430
|
# Must first upload the file to an accessible directory for the login user
|
|
419
431
|
user_remote_path = sudo_remote_file_path(remote_path)
|
|
420
432
|
Remotus.logger.debug { "Sudo enabled, uploading file to #{user_remote_path}" }
|
|
421
433
|
permission_cmd = permission_cmds(user_remote_path, options[:owner], options[:group], options[:mode])
|
|
422
|
-
|
|
434
|
+
|
|
435
|
+
with_retries("Upload #{local_path} to #{user_remote_path}", options[:retries] || DEFAULT_RETRIES) do
|
|
436
|
+
connection.scp.upload!(local_path, user_remote_path, options)
|
|
437
|
+
end
|
|
423
438
|
|
|
424
439
|
# Set permissions and move the file to the correct destination
|
|
425
440
|
move_cmd = "/bin/mv -f '#{user_remote_path}' '#{remote_path}'"
|
data/lib/remotus/version.rb
CHANGED
|
@@ -4,7 +4,9 @@ require "forwardable"
|
|
|
4
4
|
require "remotus"
|
|
5
5
|
require "remotus/result"
|
|
6
6
|
require "remotus/auth"
|
|
7
|
+
require "remotus/core_ext/elevated"
|
|
7
8
|
require "winrm"
|
|
9
|
+
require "winrm-elevated"
|
|
8
10
|
require "winrm-fs"
|
|
9
11
|
|
|
10
12
|
module Remotus
|
|
@@ -21,6 +23,9 @@ module Remotus
|
|
|
21
23
|
# @return [String] host hostname
|
|
22
24
|
attr_reader :host
|
|
23
25
|
|
|
26
|
+
# @return [String] shell type
|
|
27
|
+
attr_reader :shell
|
|
28
|
+
|
|
24
29
|
# @return [Remotus::HostPool] host_pool associated host pool
|
|
25
30
|
attr_reader :host_pool
|
|
26
31
|
|
|
@@ -38,6 +43,7 @@ module Remotus
|
|
|
38
43
|
@host = host
|
|
39
44
|
@port = port
|
|
40
45
|
@host_pool = host_pool
|
|
46
|
+
@shell = :powershell
|
|
41
47
|
end
|
|
42
48
|
|
|
43
49
|
#
|
|
@@ -69,14 +75,17 @@ module Remotus
|
|
|
69
75
|
|
|
70
76
|
#
|
|
71
77
|
# Retrieves/creates the WinRM shell connection for the host
|
|
78
|
+
#
|
|
79
|
+
# @param [symbol] shell connection shell type, defaults to :powershell
|
|
72
80
|
# If the connection already exists, the existing connection will be retrieved
|
|
73
81
|
#
|
|
74
|
-
# @return [WinRM::Shells::Powershell] remote connection
|
|
82
|
+
# @return [WinRM::Shells::Powershell, WinRM::Shells::Elevated] remote connection
|
|
75
83
|
#
|
|
76
|
-
def connection
|
|
77
|
-
return @connection unless restart_connection?
|
|
84
|
+
def connection(shell = :powershell)
|
|
85
|
+
return @connection unless restart_connection?(shell: shell)
|
|
78
86
|
|
|
79
|
-
@
|
|
87
|
+
@shell = shell
|
|
88
|
+
@connection = base_connection(reload: true).shell(@shell)
|
|
80
89
|
end
|
|
81
90
|
|
|
82
91
|
#
|
|
@@ -93,13 +102,14 @@ module Remotus
|
|
|
93
102
|
#
|
|
94
103
|
# @param [String] command command to run
|
|
95
104
|
# @param [Array] args command arguments
|
|
96
|
-
# @param [Hash]
|
|
105
|
+
# @param [Hash] options command options
|
|
106
|
+
# @option options [Symbol] :shell shell type to use for the connection
|
|
97
107
|
#
|
|
98
108
|
# @return [Remotus::Result] result describing the stdout, stderr, and exit status of the command
|
|
99
109
|
#
|
|
100
|
-
def run(command, *args, **
|
|
110
|
+
def run(command, *args, **options)
|
|
101
111
|
command = "#{command}#{args.empty? ? "" : " "}#{args.join(" ")}"
|
|
102
|
-
run_result = connection.run(command)
|
|
112
|
+
run_result = options[:shell].nil? ? connection.run(command) : connection(options[:shell]).run(command)
|
|
103
113
|
Remotus::Result.new(command, run_result.stdout, run_result.stderr, run_result.output, run_result.exitcode)
|
|
104
114
|
rescue WinRM::WinRMAuthorizationError => e
|
|
105
115
|
raise Remotus::AuthenticationError, e.to_s
|
|
@@ -171,7 +181,7 @@ module Remotus
|
|
|
171
181
|
# @return [Boolean] whether to restart the current base connection
|
|
172
182
|
#
|
|
173
183
|
def restart_base_connection?
|
|
174
|
-
return restart_connection? if @connection
|
|
184
|
+
return restart_connection?(shell: @shell) if @connection
|
|
175
185
|
return true unless @base_connection
|
|
176
186
|
return true if @host != @base_connection.instance_values["connection_opts"][:endpoint].scan(%r{//(.*):}).flatten.first
|
|
177
187
|
return true if Remotus::Auth.credential(self).user != @base_connection.instance_values["connection_opts"][:user]
|
|
@@ -183,10 +193,14 @@ module Remotus
|
|
|
183
193
|
#
|
|
184
194
|
# Whether to restart the current WinRM connection
|
|
185
195
|
#
|
|
196
|
+
# @param [Hash] options restart connection options
|
|
197
|
+
# @option options [Symbol] :shell shell type to use for the connection
|
|
198
|
+
#
|
|
186
199
|
# @return [Boolean] whether to restart the current connection
|
|
187
200
|
#
|
|
188
|
-
def restart_connection?
|
|
201
|
+
def restart_connection?(**options)
|
|
189
202
|
return true unless @connection
|
|
203
|
+
return true if shell && !options[:shell].casecmp?(@shell)
|
|
190
204
|
return true if @host != @connection.connection_opts[:endpoint].scan(%r{//(.*):}).flatten.first
|
|
191
205
|
return true if Remotus::Auth.credential(self).user != @connection.connection_opts[:user]
|
|
192
206
|
return true if Remotus::Auth.credential(self).password != @connection.connection_opts[:password]
|
data/remotus.gemspec
CHANGED
|
@@ -12,7 +12,7 @@ Gem::Specification.new do |spec|
|
|
|
12
12
|
spec.description = "Ruby gem for connecting to remote systems seamlessly via WinRM or SSH."
|
|
13
13
|
spec.homepage = "https://github.com/wheatevo/remotus"
|
|
14
14
|
spec.license = "MIT"
|
|
15
|
-
spec.required_ruby_version = Gem::Requirement.new(">= 2.
|
|
15
|
+
spec.required_ruby_version = Gem::Requirement.new(">= 2.5.0")
|
|
16
16
|
|
|
17
17
|
spec.metadata["homepage_uri"] = spec.homepage
|
|
18
18
|
spec.metadata["source_code_uri"] = "https://github.com/wheatevo/remotus"
|
|
@@ -33,6 +33,7 @@ Gem::Specification.new do |spec|
|
|
|
33
33
|
spec.add_dependency "net-scp", "~> 3.0"
|
|
34
34
|
spec.add_dependency "net-ssh", "~> 6.1"
|
|
35
35
|
spec.add_dependency "winrm", "~> 2.3"
|
|
36
|
+
spec.add_dependency "winrm-elevated", "~> 1.2"
|
|
36
37
|
spec.add_dependency "winrm-fs", "~> 1.3"
|
|
37
38
|
|
|
38
39
|
# Development dependencies
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: remotus
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.4.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Matthew Newell
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: exe
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date:
|
|
11
|
+
date: 2022-06-09 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: connection_pool
|
|
@@ -66,6 +66,20 @@ dependencies:
|
|
|
66
66
|
- - "~>"
|
|
67
67
|
- !ruby/object:Gem::Version
|
|
68
68
|
version: '2.3'
|
|
69
|
+
- !ruby/object:Gem::Dependency
|
|
70
|
+
name: winrm-elevated
|
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
|
72
|
+
requirements:
|
|
73
|
+
- - "~>"
|
|
74
|
+
- !ruby/object:Gem::Version
|
|
75
|
+
version: '1.2'
|
|
76
|
+
type: :runtime
|
|
77
|
+
prerelease: false
|
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
79
|
+
requirements:
|
|
80
|
+
- - "~>"
|
|
81
|
+
- !ruby/object:Gem::Version
|
|
82
|
+
version: '1.2'
|
|
69
83
|
- !ruby/object:Gem::Dependency
|
|
70
84
|
name: winrm-fs
|
|
71
85
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -188,6 +202,7 @@ files:
|
|
|
188
202
|
- lib/remotus/auth/credential.rb
|
|
189
203
|
- lib/remotus/auth/hash_store.rb
|
|
190
204
|
- lib/remotus/auth/store.rb
|
|
205
|
+
- lib/remotus/core_ext/elevated.rb
|
|
191
206
|
- lib/remotus/core_ext/string.rb
|
|
192
207
|
- lib/remotus/host_pool.rb
|
|
193
208
|
- lib/remotus/logger.rb
|
|
@@ -213,7 +228,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
|
213
228
|
requirements:
|
|
214
229
|
- - ">="
|
|
215
230
|
- !ruby/object:Gem::Version
|
|
216
|
-
version: 2.
|
|
231
|
+
version: 2.5.0
|
|
217
232
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
218
233
|
requirements:
|
|
219
234
|
- - ">="
|