remotus 0.2.3 → 0.3.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: 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