remotus 0.2.3 → 0.3.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: 150c4d88d234d85c6f8d0c82765b52a139235a8cadf14ba50a247369ebaf260d
4
- data.tar.gz: 81199c5d7906ebba92197738ca31e1c7ff40c1cc6fc37eef0685fb38d1247446
3
+ metadata.gz: d07fd3936fbe26fea5629acc43238c698f735f3d6814f0733c3b5b81d638f340
4
+ data.tar.gz: fd028ed4fe407c5f4b9dc82ecdc7ec3371c4605cc19997227287df08badcaa9b
5
5
  SHA512:
6
- metadata.gz: 5ec542e6af06160c5ce0e0457e8bb5b4db18b57de01c90fbf729988790d5b394da0f004ee8fb6c257ee65eb3074b234184a61a1188efb6223968f1b2defe39e4
7
- data.tar.gz: de814cdd4ee721ce521112cc82fe6d0f53ba622289914793f335872ae4a9785acac076cc5ce1a928c07d52d95d2aac9641c62f654efcaf86d30a72d0df394010
6
+ metadata.gz: 143e87065eb71702504a6773c74e68bc4a6efff6f08cbc8b812ed0c84db7a994719b578e4eb6a345a0f2369a11c1f4b05e1a9531a015cee345f958186ee06860
7
+ data.tar.gz: 54ce478b43f70a989d8103f659da7ae61723794da6ef1918eee33ff73ee1d363dfb772da8297a4b8428712f639291d481c1df3112d5d09ba1bbbb3c2fd58883b
data/.rubocop.yml CHANGED
@@ -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,8 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [0.3.0] - 2022-02-18
4
+ * Add retries to SSH SCP transactions
5
+
3
6
  ## [0.2.3] - 2021-05-01
4
7
  * Resolve rexml vulnerability CVE-2021-28965
5
8
 
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- remotus (0.2.3)
4
+ remotus (0.3.0)
5
5
  connection_pool (~> 2.2)
6
6
  net-scp (~> 3.0)
7
7
  net-ssh (~> 6.1)
@@ -14,9 +14,9 @@ GEM
14
14
  ast (2.4.2)
15
15
  builder (3.2.4)
16
16
  connection_pool (2.2.5)
17
- diff-lcs (1.4.4)
17
+ diff-lcs (1.5.0)
18
18
  erubi (1.10.0)
19
- ffi (1.15.0)
19
+ ffi (1.15.5)
20
20
  gssapi (1.3.1)
21
21
  ffi (>= 1.0.1)
22
22
  gyoku (1.3.1)
@@ -31,46 +31,46 @@ GEM
31
31
  net-ssh (>= 2.6.5, < 7.0.0)
32
32
  net-ssh (6.1.0)
33
33
  nori (2.6.0)
34
- parallel (1.20.1)
35
- parser (3.0.1.0)
34
+ parallel (1.21.0)
35
+ parser (3.1.0.0)
36
36
  ast (~> 2.4.1)
37
- rainbow (3.0.0)
38
- rake (13.0.3)
39
- regexp_parser (2.1.1)
37
+ rainbow (3.1.1)
38
+ rake (13.0.6)
39
+ regexp_parser (2.2.1)
40
40
  rexml (3.2.5)
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)
41
+ rspec (3.11.0)
42
+ rspec-core (~> 3.11.0)
43
+ rspec-expectations (~> 3.11.0)
44
+ rspec-mocks (~> 3.11.0)
45
+ rspec-core (3.11.0)
46
+ rspec-support (~> 3.11.0)
47
+ rspec-expectations (3.11.0)
48
48
  diff-lcs (>= 1.2.0, < 2.0)
49
- rspec-support (~> 3.10.0)
50
- rspec-mocks (3.10.2)
49
+ rspec-support (~> 3.11.0)
50
+ rspec-mocks (3.11.0)
51
51
  diff-lcs (>= 1.2.0, < 2.0)
52
- rspec-support (~> 3.10.0)
53
- rspec-support (3.10.2)
54
- rubocop (1.13.0)
52
+ rspec-support (~> 3.11.0)
53
+ rspec-support (3.11.0)
54
+ rubocop (1.25.1)
55
55
  parallel (~> 1.10)
56
- parser (>= 3.0.0.0)
56
+ parser (>= 3.1.0.0)
57
57
  rainbow (>= 2.2.2, < 4.0)
58
58
  regexp_parser (>= 1.8, < 3.0)
59
59
  rexml
60
- rubocop-ast (>= 1.2.0, < 2.0)
60
+ rubocop-ast (>= 1.15.1, < 2.0)
61
61
  ruby-progressbar (~> 1.7)
62
62
  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.3.0)
63
+ rubocop-ast (1.15.2)
64
+ parser (>= 3.0.1.1)
65
+ rubocop-rake (0.6.0)
68
66
  rubocop (~> 1.0)
69
- rubocop-ast (>= 1.1.0)
67
+ rubocop-rspec (2.8.0)
68
+ rubocop (~> 1.19)
70
69
  ruby-progressbar (1.11.0)
71
70
  rubyntlm (0.6.3)
72
- rubyzip (2.3.0)
73
- unicode-display_width (2.0.0)
71
+ rubyzip (2.3.2)
72
+ unicode-display_width (2.1.0)
73
+ webrick (1.7.0)
74
74
  winrm (2.3.6)
75
75
  builder (>= 2.1.2)
76
76
  erubi (~> 1.8)
@@ -85,7 +85,8 @@ GEM
85
85
  logging (>= 1.6.1, < 3.0)
86
86
  rubyzip (~> 2.0)
87
87
  winrm (~> 2.0)
88
- yard (0.9.26)
88
+ yard (0.9.27)
89
+ webrick (~> 1.7.0)
89
90
 
90
91
  PLATFORMS
91
92
  ruby
@@ -101,4 +102,4 @@ DEPENDENCIES
101
102
  yard (~> 0.9)
102
103
 
103
104
  BUNDLED WITH
104
- 2.2.14
105
+ 2.2.22
data/README.md CHANGED
@@ -87,7 +87,7 @@ require "remotus"
87
87
 
88
88
  class SimpleStore < Remotus::Auth::Store
89
89
  def credential(connection, **options)
90
- "#{connection.host}_password"
90
+ Remotus::Auth::Credential.new('user', "#{connection.host}_password")
91
91
  end
92
92
  end
93
93
 
@@ -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.3"
5
+ VERSION = "0.3.0"
6
6
  end
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.3
4
+ version: 0.3.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-05-01 00:00:00.000000000 Z
11
+ date: 2022-02-18 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: connection_pool