remotus 0.2.2 → 0.4.0

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