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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 46da915bc63af444b4bed497e154bec84c780f20bb92be17618f22444fd26889
4
- data.tar.gz: 43f29167c6c4a9c264208f8c2454278b884edb78ea596cb00d1439db05a846ea
3
+ metadata.gz: 34953ce4b1c254e31a40b7b3ff3795124fe98b712722862a35813cceba686759
4
+ data.tar.gz: 3d342087b707b3078cedf38038b3ba21c1a1db6ed7c7d189f5912d9cd5adf4e7
5
5
  SHA512:
6
- metadata.gz: e4421747e84af73d3c2648430ccb4717b9ecb01eef6044560a1d28e101f1f2082d357c4d1827c7921aace92b513123c4097419a587d31425d62d3ccbbf0e6d2f
7
- data.tar.gz: 40dffbc5f8fa8db04fd9a803d73d3396ce31084da733b653c6636de73eb5e03c644f130b1f51baa6672f94955fb2a5b29fcd6def0b5ec3c8cb59afbc5319453c
6
+ metadata.gz: c884fbb30a585899e840ab9e3dddd3b3d6ac052d4b10745bc9c582f9e67ca445aa9ef98af7ac8cbba551cc6594f23a9583d3f54cfba6202f3b8595804bd60641
7
+ data.tar.gz: 166f8cb4e3e633a79f1f28ddba9a3ff8793cc2927873bdd349e035870273c81ad6a9688f37d2f871413f43451604f64a06631e0813f8e111f7d36c701a168b05
data/.rubocop.yml CHANGED
@@ -1,5 +1,5 @@
1
1
  AllCops:
2
- TargetRubyVersion: 2.4
2
+ TargetRubyVersion: 2.5
3
3
  NewCops: enable
4
4
 
5
5
  Style/StringLiterals:
@@ -42,3 +42,6 @@ Metrics/PerceivedComplexity:
42
42
 
43
43
  Metrics/ParameterLists:
44
44
  Max: 6
45
+
46
+ Gemspec/RequireMFA:
47
+ Enabled: false
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.2.2)
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.3)
17
- diff-lcs (1.4.4)
17
+ connection_pool (2.2.5)
18
+ diff-lcs (1.5.0)
18
19
  erubi (1.10.0)
19
- ffi (1.15.0)
20
+ ffi (1.15.5)
20
21
  gssapi (1.3.1)
21
22
  ffi (>= 1.0.1)
22
- gyoku (1.3.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.0)
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.20.1)
35
- parser (3.0.0.0)
36
+ parallel (1.21.0)
37
+ parser (3.1.0.0)
36
38
  ast (~> 2.4.1)
37
- rainbow (3.0.0)
38
- rake (13.0.3)
39
- regexp_parser (2.1.1)
40
- rexml (3.2.4)
41
- rspec (3.10.0)
42
- rspec-core (~> 3.10.0)
43
- rspec-expectations (~> 3.10.0)
44
- rspec-mocks (~> 3.10.0)
45
- rspec-core (3.10.1)
46
- rspec-support (~> 3.10.0)
47
- rspec-expectations (3.10.1)
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.10.0)
50
- rspec-mocks (3.10.2)
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.10.0)
53
- rspec-support (3.10.2)
54
- rubocop (1.11.0)
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.0.0.0)
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.2.0, < 2.0)
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.4.1)
64
- parser (>= 2.7.1.5)
65
- rubocop-rake (0.5.1)
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
- rubocop-ast (>= 1.1.0)
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.0)
73
- unicode-display_width (2.0.0)
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.26)
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.14
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
@@ -24,4 +24,8 @@ module Remotus
24
24
  end
25
25
  end
26
26
 
27
- String.include(Remotus::CoreExt::String)
27
+ # @api private
28
+ # Core ruby string class
29
+ class String
30
+ include Remotus::CoreExt::String
31
+ end
@@ -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
- "while a value in #{accepted_exit_codes} was expected.\n#{output}"
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 1)
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
- # Handle sudo
145
- if options[:sudo]
146
- Remotus.logger.debug { "Sudo is enabled for command #{command.object_id}" }
147
- ssh_command = "sudo -p '' -S sh -c '#{command.gsub("'", "'\"'\"'")}'"
148
- input = "#{Remotus::Auth.credential(self).password}\n#{input}"
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
- # Allocate a terminal if specified
155
- pty = options[:pty] || false
156
- skip_first_output = pty && options[:sudo]
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
- ch.exec(ssh_command)
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
- # Provide input
174
- unless input.empty?
175
- Remotus.logger.debug { "Sending input for command #{command.object_id}" }
176
- channel.send_data input
177
- channel.eof!
178
- end
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
- # Process stdout
181
- channel.on_data do |ch, data|
182
- # Skip the first iteration if sudo and pty is enabled to avoid outputting the sudo password
183
- if skip_first_output
184
- skip_first_output = false
185
- next
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
- # Process stderr
194
- channel.on_extended_data do |ch, _, data|
195
- stderr << data
196
- output << data
197
- options[:on_stderr].call(ch, data) if options[:on_stderr].respond_to?(:call)
198
- options[:on_output].call(ch, data) if options[:on_output].respond_to?(:call)
199
- end
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
- # Process exit status/code
202
- channel.on_request("exit-status") do |_, data|
203
- exit_code = data.read_long
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
- # If we are using sudo and experience an authentication failure, raise an exception
214
- if options[:sudo] && result.error? && !result.stderr.empty? && result.stderr.match?(/^sudo: \d+ incorrect password attempts?$/)
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
- # Perform success, error, and completion callbacks
219
- options[:on_success].call(result) if options[:on_success].respond_to?(:call) && result.success?(accepted_exit_codes)
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
- result
224
- rescue Remotus::AuthenticationError => e
225
- # Re-raise exception if the retry count is exceeded
226
- Remotus.logger.debug do
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
- # Remove user password to force credential store update on next retry
246
- Remotus.logger.debug { "Removing current credential for #{@host} to force credential retrieval." }
247
- Remotus::Auth.cache.delete(@host)
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
- retry
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 1)
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
- connection.scp.upload!(local_path, remote_path, options)
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
- result = connection.scp.download!(remote_path, local_path, options)
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
- connection.scp.upload!(local_path, user_remote_path, options)
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}'"
@@ -2,5 +2,5 @@
2
2
 
3
3
  module Remotus
4
4
  # Remotus gem version
5
- VERSION = "0.2.2"
5
+ VERSION = "0.4.0"
6
6
  end
@@ -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
- @connection = base_connection(reload: true).shell(:powershell)
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] _options unused command options
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, **_options)
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.4.0")
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.2.2
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: 2021-03-23 00:00:00.000000000 Z
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.4.0
231
+ version: 2.5.0
217
232
  required_rubygems_version: !ruby/object:Gem::Requirement
218
233
  requirements:
219
234
  - - ">="