remotus 0.2.0 → 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 +4 -4
- data/.rubocop.yml +4 -1
- data/CHANGELOG.md +17 -2
- data/Gemfile.lock +35 -34
- data/README.md +1 -1
- data/lib/remotus/auth/hash_store.rb +1 -1
- data/lib/remotus/auth.rb +32 -8
- data/lib/remotus/host_pool.rb +1 -1
- data/lib/remotus/pool.rb +5 -1
- data/lib/remotus/result.rb +1 -1
- data/lib/remotus/ssh_connection.rb +132 -106
- data/lib/remotus/version.rb +1 -1
- data/lib/remotus/winrm_connection.rb +12 -1
- data/remotus.gemspec +1 -1
- metadata +3 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: d07fd3936fbe26fea5629acc43238c698f735f3d6814f0733c3b5b81d638f340
|
|
4
|
+
data.tar.gz: fd028ed4fe407c5f4b9dc82ecdc7ec3371c4605cc19997227287df08badcaa9b
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 143e87065eb71702504a6773c74e68bc4a6efff6f08cbc8b812ed0c84db7a994719b578e4eb6a345a0f2369a11c1f4b05e1a9531a015cee345f958186ee06860
|
|
7
|
+
data.tar.gz: 54ce478b43f70a989d8103f659da7ae61723794da6ef1918eee33ff73ee1d363dfb772da8297a4b8428712f639291d481c1df3112d5d09ba1bbbb3c2fd58883b
|
data/.rubocop.yml
CHANGED
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,20 @@
|
|
|
1
1
|
## [Unreleased]
|
|
2
2
|
|
|
3
|
-
## [0.
|
|
3
|
+
## [0.3.0] - 2022-02-18
|
|
4
|
+
* Add retries to SSH SCP transactions
|
|
5
|
+
|
|
6
|
+
## [0.2.3] - 2021-05-01
|
|
7
|
+
* Resolve rexml vulnerability CVE-2021-28965
|
|
8
|
+
|
|
9
|
+
## [0.2.2] - 2021-03-23
|
|
10
|
+
* Ensure both user and password are populated before using a cached credential
|
|
4
11
|
|
|
5
|
-
-
|
|
12
|
+
## [0.2.1] - 2021-03-15
|
|
13
|
+
* Fix connection pooling metadata sharing
|
|
14
|
+
* Fix caching of pooled metadata
|
|
15
|
+
|
|
16
|
+
## [0.2.0] - 2021-03-14
|
|
17
|
+
* Add per-connection metadata support
|
|
18
|
+
|
|
19
|
+
## [0.1.0] - 2021-03-09
|
|
20
|
+
* Initial release
|
data/Gemfile.lock
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
PATH
|
|
2
2
|
remote: .
|
|
3
3
|
specs:
|
|
4
|
-
remotus (0.
|
|
4
|
+
remotus (0.3.0)
|
|
5
5
|
connection_pool (~> 2.2)
|
|
6
6
|
net-scp (~> 3.0)
|
|
7
7
|
net-ssh (~> 6.1)
|
|
@@ -13,10 +13,10 @@ GEM
|
|
|
13
13
|
specs:
|
|
14
14
|
ast (2.4.2)
|
|
15
15
|
builder (3.2.4)
|
|
16
|
-
connection_pool (2.2.
|
|
17
|
-
diff-lcs (1.
|
|
16
|
+
connection_pool (2.2.5)
|
|
17
|
+
diff-lcs (1.5.0)
|
|
18
18
|
erubi (1.10.0)
|
|
19
|
-
ffi (1.15.
|
|
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.
|
|
35
|
-
parser (3.
|
|
34
|
+
parallel (1.21.0)
|
|
35
|
+
parser (3.1.0.0)
|
|
36
36
|
ast (~> 2.4.1)
|
|
37
|
-
rainbow (3.
|
|
38
|
-
rake (13.0.
|
|
39
|
-
regexp_parser (2.
|
|
40
|
-
rexml (3.2.
|
|
41
|
-
rspec (3.
|
|
42
|
-
rspec-core (~> 3.
|
|
43
|
-
rspec-expectations (~> 3.
|
|
44
|
-
rspec-mocks (~> 3.
|
|
45
|
-
rspec-core (3.
|
|
46
|
-
rspec-support (~> 3.
|
|
47
|
-
rspec-expectations (3.
|
|
37
|
+
rainbow (3.1.1)
|
|
38
|
+
rake (13.0.6)
|
|
39
|
+
regexp_parser (2.2.1)
|
|
40
|
+
rexml (3.2.5)
|
|
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.
|
|
50
|
-
rspec-mocks (3.
|
|
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.
|
|
53
|
-
rspec-support (3.
|
|
54
|
-
rubocop (1.
|
|
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.
|
|
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.
|
|
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.
|
|
64
|
-
parser (>=
|
|
65
|
-
rubocop-rake (0.
|
|
66
|
-
rubocop
|
|
67
|
-
rubocop-rspec (2.2.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
|
-
|
|
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.
|
|
73
|
-
unicode-display_width (2.
|
|
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.
|
|
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.
|
|
105
|
+
2.2.22
|
data/README.md
CHANGED
|
@@ -15,7 +15,7 @@ module Remotus
|
|
|
15
15
|
#
|
|
16
16
|
# Retrieves a credential from the hash store
|
|
17
17
|
#
|
|
18
|
-
# @param [Remotus::SshConnection, Remotus::WinrmConnection, #host] connection
|
|
18
|
+
# @param [Remotus::SshConnection, Remotus::WinrmConnection, #host] connection associated connection
|
|
19
19
|
# @param [Hash] _options unused options hash
|
|
20
20
|
#
|
|
21
21
|
# @return [Remotus::Auth::Credential, nil] found credential or nil
|
data/lib/remotus/auth.rb
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require "remotus"
|
|
3
4
|
require "remotus/auth/credential"
|
|
4
5
|
require "remotus/auth/store"
|
|
5
6
|
require "remotus/auth/hash_store"
|
|
@@ -17,15 +18,12 @@ module Remotus
|
|
|
17
18
|
# @return [Remotus::Auth::Credential] found credential
|
|
18
19
|
#
|
|
19
20
|
def self.credential(connection, **options)
|
|
20
|
-
return
|
|
21
|
+
# Only return cached credentials that have a populated user and password, otherwise attempt retrieval
|
|
22
|
+
return cache[connection.host] if cache.key?(connection.host) && cache[connection.host].user && cache[connection.host].password
|
|
23
|
+
|
|
24
|
+
found_credential = credential_from_stores(connection, **options)
|
|
25
|
+
return found_credential if found_credential
|
|
21
26
|
|
|
22
|
-
stores.each do |store|
|
|
23
|
-
host_cred = store.credential(connection, **options)
|
|
24
|
-
if host_cred
|
|
25
|
-
cache[connection.host] = host_cred
|
|
26
|
-
return host_cred
|
|
27
|
-
end
|
|
28
|
-
end
|
|
29
27
|
raise Remotus::MissingCredential, "Could not find credential for #{connection.host} in any credential store (#{stores.join(", ")})."
|
|
30
28
|
end
|
|
31
29
|
|
|
@@ -62,5 +60,31 @@ module Remotus
|
|
|
62
60
|
def self.stores=(stores)
|
|
63
61
|
@stores = stores
|
|
64
62
|
end
|
|
63
|
+
|
|
64
|
+
class << self
|
|
65
|
+
private
|
|
66
|
+
|
|
67
|
+
#
|
|
68
|
+
# Gets authentication credentials for the given connection and options from one of the credential stores
|
|
69
|
+
#
|
|
70
|
+
# @param [Remotus::SshConnection, Remotus::WinrmConnection, #host] connection remote connection
|
|
71
|
+
# @param [Hash] options options hash
|
|
72
|
+
# options may be used by different credential stores.
|
|
73
|
+
#
|
|
74
|
+
# @return [Remotus::Auth::Credential, nil] found credential or nil if the credential cannot be found
|
|
75
|
+
#
|
|
76
|
+
def credential_from_stores(connection, **options)
|
|
77
|
+
stores.each do |store|
|
|
78
|
+
Remotus.logger.debug { "Gathering #{connection.host} credentials from #{store} credential store" }
|
|
79
|
+
host_cred = store.credential(connection, **options)
|
|
80
|
+
next unless host_cred
|
|
81
|
+
|
|
82
|
+
Remotus.logger.debug { "#{connection.host} credentials found in #{store} credential store" }
|
|
83
|
+
cache[connection.host] = host_cred
|
|
84
|
+
return host_cred
|
|
85
|
+
end
|
|
86
|
+
nil
|
|
87
|
+
end
|
|
88
|
+
end
|
|
65
89
|
end
|
|
66
90
|
end
|
data/lib/remotus/host_pool.rb
CHANGED
|
@@ -57,7 +57,7 @@ module Remotus
|
|
|
57
57
|
connection_class = Object.const_get("Remotus::#{@proto.capitalize}Connection")
|
|
58
58
|
port ||= connection_class::REMOTE_PORT
|
|
59
59
|
|
|
60
|
-
@pool = ConnectionPool.new(size: size, timeout: timeout) { connection_class.new(host, port) }
|
|
60
|
+
@pool = ConnectionPool.new(size: size, timeout: timeout) { connection_class.new(host, port, host_pool: self) }
|
|
61
61
|
@size = size.to_i
|
|
62
62
|
@timeout = timeout.to_i
|
|
63
63
|
@expiration_time = Time.now + timeout
|
data/lib/remotus/pool.rb
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
require "remotus"
|
|
4
4
|
require "remotus/host_pool"
|
|
5
|
+
require "remotus/core_ext/string"
|
|
5
6
|
|
|
6
7
|
module Remotus
|
|
7
8
|
# Class representing a connection pool containing many host-specific pools
|
|
@@ -144,9 +145,12 @@ module Remotus
|
|
|
144
145
|
return false unless pool[host]
|
|
145
146
|
|
|
146
147
|
options.each do |k, v|
|
|
148
|
+
k = k.to_s.to_method_name
|
|
149
|
+
|
|
147
150
|
Remotus.logger.debug { "Checking if option #{k} => #{v} has changed" }
|
|
148
151
|
|
|
149
|
-
|
|
152
|
+
# If any of the options passed are new, assume a change has occurred
|
|
153
|
+
return true unless pool[host].respond_to?(k.to_sym)
|
|
150
154
|
|
|
151
155
|
host_value = pool[host].send(k.to_sym)
|
|
152
156
|
|
data/lib/remotus/result.rb
CHANGED
|
@@ -66,7 +66,7 @@ module Remotus
|
|
|
66
66
|
return unless error?(accepted_exit_codes)
|
|
67
67
|
|
|
68
68
|
raise Remotus::ResultError, "Error encountered executing #{@command}! Exit code #{@exit_code} was returned "\
|
|
69
|
-
|
|
69
|
+
"while a value in #{accepted_exit_codes} was expected.\n#{output}"
|
|
70
70
|
end
|
|
71
71
|
|
|
72
72
|
#
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require "forwardable"
|
|
3
4
|
require "remotus"
|
|
4
5
|
require "remotus/result"
|
|
5
6
|
require "remotus/auth"
|
|
@@ -9,6 +10,8 @@ require "net/ssh"
|
|
|
9
10
|
module Remotus
|
|
10
11
|
# Class representing an SSH connection to a host
|
|
11
12
|
class SshConnection
|
|
13
|
+
extend Forwardable
|
|
14
|
+
|
|
12
15
|
# Standard SSH remote port
|
|
13
16
|
REMOTE_PORT = 22
|
|
14
17
|
|
|
@@ -24,16 +27,24 @@ module Remotus
|
|
|
24
27
|
# @return [String] host hostname
|
|
25
28
|
attr_reader :host
|
|
26
29
|
|
|
30
|
+
# @return [Remotus::HostPool] host_pool associated host pool
|
|
31
|
+
attr_reader :host_pool
|
|
32
|
+
|
|
33
|
+
# Delegate metadata methods to associated host pool
|
|
34
|
+
def_delegators :@host_pool, :[], :[]=
|
|
35
|
+
|
|
27
36
|
#
|
|
28
37
|
# Creates an SshConnection
|
|
29
38
|
#
|
|
30
39
|
# @param [String] host hostname
|
|
31
40
|
# @param [Integer] port remote port
|
|
41
|
+
# @param [Remotus::HostPool] host_pool associated host pool
|
|
32
42
|
#
|
|
33
|
-
def initialize(host, port = REMOTE_PORT)
|
|
43
|
+
def initialize(host, port = REMOTE_PORT, host_pool: nil)
|
|
34
44
|
Remotus.logger.debug { "Creating SshConnection #{object_id} for #{host}" }
|
|
35
45
|
@host = host
|
|
36
46
|
@port = port
|
|
47
|
+
@host_pool = host_pool
|
|
37
48
|
end
|
|
38
49
|
|
|
39
50
|
#
|
|
@@ -102,7 +113,7 @@ module Remotus
|
|
|
102
113
|
# @param [Hash] options command options
|
|
103
114
|
# @option options [Boolean] :sudo whether to run the command with sudo (defaults to false)
|
|
104
115
|
# @option options [Boolean] :pty whether to allocate a terminal (defaults to false)
|
|
105
|
-
# @option options [Integer] :retries number of times to retry a closed connection (defaults to
|
|
116
|
+
# @option options [Integer] :retries number of times to retry a closed connection (defaults to 2)
|
|
106
117
|
# @option options [String] :input stdin input to provide to the command
|
|
107
118
|
# @option options [Array<Integer>] :accepted_exit_codes array of acceptable exit codes (defaults to [0])
|
|
108
119
|
# only used if :on_error or :on_success are set
|
|
@@ -130,121 +141,88 @@ module Remotus
|
|
|
130
141
|
# Refer to the command by object_id throughout the log to avoid logging sensitive data
|
|
131
142
|
Remotus.logger.debug { "Preparing to run command #{command.object_id} on #{@host}" }
|
|
132
143
|
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
# If password was nil, raise an exception
|
|
140
|
-
raise Remotus::MissingSudoPassword, "#{host} credential does not have a password specified" if input.start_with?("\n")
|
|
141
|
-
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}"
|
|
142
150
|
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
# Open an SSH channel to the host
|
|
148
|
-
channel_handle = connection.open_channel do |channel|
|
|
149
|
-
# Execute the command
|
|
150
|
-
if pty
|
|
151
|
-
Remotus.logger.debug { "Requesting pty for command #{command.object_id}" }
|
|
152
|
-
channel.request_pty do |ch, success|
|
|
153
|
-
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
|
|
154
154
|
|
|
155
|
-
|
|
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)
|
|
156
172
|
end
|
|
157
|
-
else
|
|
158
|
-
Remotus.logger.debug { "Executing command #{command.object_id}" }
|
|
159
|
-
channel.exec(ssh_command)
|
|
160
|
-
end
|
|
161
173
|
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
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
|
|
168
180
|
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
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)
|
|
175
192
|
end
|
|
176
|
-
stdout << data
|
|
177
|
-
output << data
|
|
178
|
-
options[:on_stdout].call(ch, data) if options[:on_stdout].respond_to?(:call)
|
|
179
|
-
options[:on_output].call(ch, data) if options[:on_output].respond_to?(:call)
|
|
180
|
-
end
|
|
181
193
|
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
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
|
|
189
201
|
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
202
|
+
# Process exit status/code
|
|
203
|
+
channel.on_request("exit-status") do |_, data|
|
|
204
|
+
exit_code = data.read_long
|
|
205
|
+
end
|
|
193
206
|
end
|
|
194
|
-
end
|
|
195
207
|
|
|
196
|
-
|
|
197
|
-
|
|
208
|
+
# Block until the command has completed execution
|
|
209
|
+
channel_handle.wait
|
|
198
210
|
|
|
199
|
-
|
|
200
|
-
|
|
211
|
+
Remotus.logger.debug { "Generating result for command #{command.object_id}" }
|
|
212
|
+
result = Remotus::Result.new(command, stdout, stderr, output, exit_code)
|
|
201
213
|
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
# Perform success, error, and completion callbacks
|
|
208
|
-
options[:on_success].call(result) if options[:on_success].respond_to?(:call) && result.success?(accepted_exit_codes)
|
|
209
|
-
options[:on_error].call(result) if options[:on_error].respond_to?(:call) && result.error?(accepted_exit_codes)
|
|
210
|
-
options[:on_complete].call(result) if options[:on_complete].respond_to?(:call)
|
|
211
|
-
|
|
212
|
-
result
|
|
213
|
-
rescue Remotus::AuthenticationError => e
|
|
214
|
-
# Re-raise exception if the retry count is exceeded
|
|
215
|
-
Remotus.logger.debug do
|
|
216
|
-
"Sudo authentication failed for command #{command.object_id}, retrying with #{retries} attempt#{retries.abs == 1 ? "" : "s"} remaining..."
|
|
217
|
-
end
|
|
218
|
-
retries -= 1
|
|
219
|
-
raise 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
|
|
220
218
|
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
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)
|
|
224
223
|
|
|
225
|
-
|
|
226
|
-
rescue Net::SSH::AuthenticationFailed => e
|
|
227
|
-
# Attempt to update the user password and retry
|
|
228
|
-
Remotus.logger.debug do
|
|
229
|
-
"SSH authentication failed for command #{command.object_id}, retrying with #{retries} attempt#{retries.abs == 1 ? "" : "s"} remaining..."
|
|
224
|
+
result
|
|
230
225
|
end
|
|
231
|
-
retries -= 1
|
|
232
|
-
raise Remotus::AuthenticationError, e.to_s if retries.negative?
|
|
233
|
-
|
|
234
|
-
# Remove user password to force credential store update on next retry
|
|
235
|
-
Remotus.logger.debug { "Removing current credential for #{@host} to force credential retrieval." }
|
|
236
|
-
Remotus::Auth.cache.delete(@host)
|
|
237
|
-
|
|
238
|
-
retry
|
|
239
|
-
rescue IOError => e
|
|
240
|
-
# Re-raise exception if it is not a closed stream error or if the retry count is exceeded
|
|
241
|
-
Remotus.logger.debug do
|
|
242
|
-
"IOError (#{e}) encountered for command #{command.object_id}, retrying with #{retries} attempt#{retries.abs == 1 ? "" : "s"} remaining..."
|
|
243
|
-
end
|
|
244
|
-
retries -= 1
|
|
245
|
-
raise if e.to_s != "closed stream" || retries.negative?
|
|
246
|
-
|
|
247
|
-
retry
|
|
248
226
|
end
|
|
249
227
|
|
|
250
228
|
#
|
|
@@ -256,7 +234,7 @@ module Remotus
|
|
|
256
234
|
# @param [Hash] options command options
|
|
257
235
|
# @option options [Boolean] :sudo whether to run the script with sudo (defaults to false)
|
|
258
236
|
# @option options [Boolean] :pty whether to allocate a terminal (defaults to false)
|
|
259
|
-
# @option options [Integer] :retries number of times to retry a closed connection (defaults to
|
|
237
|
+
# @option options [Integer] :retries number of times to retry a closed connection (defaults to 2)
|
|
260
238
|
# @option options [String] :input stdin input to provide to the command
|
|
261
239
|
# @option options [Array<Integer>] :accepted_exit_codes array of acceptable exit codes (defaults to [0])
|
|
262
240
|
# only used if :on_error or :on_success are set
|
|
@@ -286,6 +264,7 @@ module Remotus
|
|
|
286
264
|
# @option options [String] :owner file owner ("oracle")
|
|
287
265
|
# @option options [String] :group file group ("dba")
|
|
288
266
|
# @option options [String] :mode file mode ("0640")
|
|
267
|
+
# @option options [Integer] :retries number of times to retry a closed connection (defaults to 2)
|
|
289
268
|
#
|
|
290
269
|
# @return [String] remote path
|
|
291
270
|
#
|
|
@@ -296,7 +275,11 @@ module Remotus
|
|
|
296
275
|
sudo_upload(local_path, remote_path, options)
|
|
297
276
|
else
|
|
298
277
|
permission_cmd = permission_cmds(remote_path, options[:owner], options[:group], options[:mode])
|
|
299
|
-
|
|
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
|
+
|
|
300
283
|
run(permission_cmd).error! unless permission_cmd.empty?
|
|
301
284
|
end
|
|
302
285
|
|
|
@@ -311,6 +294,7 @@ module Remotus
|
|
|
311
294
|
# if local_path is nil, the file's content will be returned
|
|
312
295
|
# @param [Hash] options download options
|
|
313
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)
|
|
314
298
|
#
|
|
315
299
|
# @return [String] local path or file content (if local_path is nil)
|
|
316
300
|
#
|
|
@@ -332,7 +316,12 @@ module Remotus
|
|
|
332
316
|
end
|
|
333
317
|
|
|
334
318
|
Remotus.logger.debug { "Downloading file from #{@host}:#{remote_path}" }
|
|
335
|
-
|
|
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
|
|
336
325
|
|
|
337
326
|
# Return the file content if that is desired
|
|
338
327
|
local_path.nil? ? result : local_path
|
|
@@ -361,6 +350,39 @@ module Remotus
|
|
|
361
350
|
|
|
362
351
|
private
|
|
363
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
|
+
|
|
364
386
|
#
|
|
365
387
|
# Whether to restart the current SSH connection
|
|
366
388
|
#
|
|
@@ -402,13 +424,17 @@ module Remotus
|
|
|
402
424
|
# @option options [String] :owner file owner ("oracle")
|
|
403
425
|
# @option options [String] :group file group ("dba")
|
|
404
426
|
# @option options [String] :mode file mode ("0640")
|
|
427
|
+
# @option options [Integer] :retries number of times to retry a closed connection (defaults to 2)
|
|
405
428
|
#
|
|
406
429
|
def sudo_upload(local_path, remote_path, options = {})
|
|
407
430
|
# Must first upload the file to an accessible directory for the login user
|
|
408
431
|
user_remote_path = sudo_remote_file_path(remote_path)
|
|
409
432
|
Remotus.logger.debug { "Sudo enabled, uploading file to #{user_remote_path}" }
|
|
410
433
|
permission_cmd = permission_cmds(user_remote_path, options[:owner], options[:group], options[:mode])
|
|
411
|
-
|
|
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
|
|
412
438
|
|
|
413
439
|
# Set permissions and move the file to the correct destination
|
|
414
440
|
move_cmd = "/bin/mv -f '#{user_remote_path}' '#{remote_path}'"
|
data/lib/remotus/version.rb
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require "forwardable"
|
|
3
4
|
require "remotus"
|
|
4
5
|
require "remotus/result"
|
|
5
6
|
require "remotus/auth"
|
|
@@ -9,6 +10,8 @@ require "winrm-fs"
|
|
|
9
10
|
module Remotus
|
|
10
11
|
# Class representing a WinRM connection to a host
|
|
11
12
|
class WinrmConnection
|
|
13
|
+
extend Forwardable
|
|
14
|
+
|
|
12
15
|
# Standard WinRM remote port
|
|
13
16
|
REMOTE_PORT = 5985
|
|
14
17
|
|
|
@@ -18,15 +21,23 @@ module Remotus
|
|
|
18
21
|
# @return [String] host hostname
|
|
19
22
|
attr_reader :host
|
|
20
23
|
|
|
24
|
+
# @return [Remotus::HostPool] host_pool associated host pool
|
|
25
|
+
attr_reader :host_pool
|
|
26
|
+
|
|
27
|
+
# Delegate metadata methods to associated host pool
|
|
28
|
+
def_delegators :@host_pool, :[], :[]=
|
|
29
|
+
|
|
21
30
|
#
|
|
22
31
|
# Creates a WinrmConnection
|
|
23
32
|
#
|
|
24
33
|
# @param [String] host hostname
|
|
25
34
|
# @param [Integer] port remote port
|
|
35
|
+
# @param [Remotus::HostPool] host_pool associated host pool
|
|
26
36
|
#
|
|
27
|
-
def initialize(host, port = REMOTE_PORT)
|
|
37
|
+
def initialize(host, port = REMOTE_PORT, host_pool: nil)
|
|
28
38
|
@host = host
|
|
29
39
|
@port = port
|
|
40
|
+
@host_pool = host_pool
|
|
30
41
|
end
|
|
31
42
|
|
|
32
43
|
#
|
data/remotus.gemspec
CHANGED
|
@@ -12,7 +12,7 @@ Gem::Specification.new do |spec|
|
|
|
12
12
|
spec.description = "Ruby gem for connecting to remote systems seamlessly via WinRM or SSH."
|
|
13
13
|
spec.homepage = "https://github.com/wheatevo/remotus"
|
|
14
14
|
spec.license = "MIT"
|
|
15
|
-
spec.required_ruby_version = Gem::Requirement.new(">= 2.
|
|
15
|
+
spec.required_ruby_version = Gem::Requirement.new(">= 2.5.0")
|
|
16
16
|
|
|
17
17
|
spec.metadata["homepage_uri"] = spec.homepage
|
|
18
18
|
spec.metadata["source_code_uri"] = "https://github.com/wheatevo/remotus"
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: remotus
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.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:
|
|
11
|
+
date: 2022-02-18 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: connection_pool
|
|
@@ -213,7 +213,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
|
213
213
|
requirements:
|
|
214
214
|
- - ">="
|
|
215
215
|
- !ruby/object:Gem::Version
|
|
216
|
-
version: 2.
|
|
216
|
+
version: 2.5.0
|
|
217
217
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
218
218
|
requirements:
|
|
219
219
|
- - ">="
|