remotus 0.1.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 +7 -0
- data/.github/workflows/main.yml +35 -0
- data/.gitignore +11 -0
- data/.rspec +3 -0
- data/.rubocop.yml +41 -0
- data/CHANGELOG.md +5 -0
- data/Gemfile +6 -0
- data/Gemfile.lock +103 -0
- data/LICENSE.txt +21 -0
- data/README.md +112 -0
- data/Rakefile +12 -0
- data/bin/console +15 -0
- data/bin/setup +8 -0
- data/lib/remotus.rb +142 -0
- data/lib/remotus/auth.rb +66 -0
- data/lib/remotus/auth/credential.rb +145 -0
- data/lib/remotus/auth/hash_store.rb +56 -0
- data/lib/remotus/auth/store.rb +43 -0
- data/lib/remotus/host_pool.rb +181 -0
- data/lib/remotus/logger.rb +9 -0
- data/lib/remotus/pool.rb +189 -0
- data/lib/remotus/result.rb +83 -0
- data/lib/remotus/ssh_connection.rb +447 -0
- data/lib/remotus/version.rb +6 -0
- data/lib/remotus/winrm_connection.rb +186 -0
- data/remotus.gemspec +45 -0
- metadata +226 -0
data/lib/remotus/auth.rb
ADDED
@@ -0,0 +1,66 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "remotus/auth/credential"
|
4
|
+
require "remotus/auth/store"
|
5
|
+
require "remotus/auth/hash_store"
|
6
|
+
|
7
|
+
module Remotus
|
8
|
+
# Module containing remote authentication classes and modules
|
9
|
+
module Auth
|
10
|
+
#
|
11
|
+
# Gets authentication credentials for the given connection and options
|
12
|
+
#
|
13
|
+
# @param [Remotus::SshConnection, Remotus::WinrmConnection, #host] connection remote connection
|
14
|
+
# @param [Hash] options options hash
|
15
|
+
# options may be used by different credential stores.
|
16
|
+
#
|
17
|
+
# @return [Remotus::Auth::Credential] found credential
|
18
|
+
#
|
19
|
+
def self.credential(connection, **options)
|
20
|
+
return cache[connection.host] if cache.key?(connection.host)
|
21
|
+
|
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
|
+
raise Remotus::MissingCredential, "Could not find credential for #{connection.host} in any credential store (#{stores.join(", ")})."
|
30
|
+
end
|
31
|
+
|
32
|
+
#
|
33
|
+
# Gets the credential cache
|
34
|
+
#
|
35
|
+
# @return [Hash{String => Remotus::Auth::Credential}] credential cache with hostname keys
|
36
|
+
#
|
37
|
+
def self.cache
|
38
|
+
@cache ||= {}
|
39
|
+
end
|
40
|
+
|
41
|
+
#
|
42
|
+
# Clears all entries in the credential cache
|
43
|
+
#
|
44
|
+
def self.clear_cache
|
45
|
+
@cache = {}
|
46
|
+
end
|
47
|
+
|
48
|
+
#
|
49
|
+
# Gets the list of associated credential stores
|
50
|
+
#
|
51
|
+
# @return [Array<Remotus::Auth::Store>] credential stores
|
52
|
+
#
|
53
|
+
def self.stores
|
54
|
+
@stores ||= []
|
55
|
+
end
|
56
|
+
|
57
|
+
#
|
58
|
+
# Sets the list of associated credential stores
|
59
|
+
#
|
60
|
+
# @param [Array<Remotus::Auth::Store>] stores credential stores
|
61
|
+
#
|
62
|
+
def self.stores=(stores)
|
63
|
+
@stores = stores
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
@@ -0,0 +1,145 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "openssl"
|
4
|
+
|
5
|
+
module Remotus
|
6
|
+
module Auth
|
7
|
+
# Authentication credential
|
8
|
+
class Credential
|
9
|
+
# @return [String] gets or sets user
|
10
|
+
attr_accessor :user
|
11
|
+
|
12
|
+
# @return [String] gets or sets private key path
|
13
|
+
attr_accessor :private_key
|
14
|
+
|
15
|
+
#
|
16
|
+
# Generates a new credential from a hash
|
17
|
+
#
|
18
|
+
# @param [Hash] hash hash with :user, :password, :private_key, and :private_key_data keys
|
19
|
+
# @option hash [String] :user user name
|
20
|
+
# @option hash [String] :password user password
|
21
|
+
# @option hash [String] :private_key private key path
|
22
|
+
# @option hash [String] :private_key_data private key data as a string
|
23
|
+
#
|
24
|
+
# @return [Remotus::Auth::Credential] newly initialized credential
|
25
|
+
#
|
26
|
+
def self.from_hash(hash)
|
27
|
+
Credential.new(
|
28
|
+
hash[:user],
|
29
|
+
hash[:password],
|
30
|
+
private_key: hash[:private_key],
|
31
|
+
private_key_data: hash[:private_key_data]
|
32
|
+
)
|
33
|
+
end
|
34
|
+
|
35
|
+
#
|
36
|
+
# Creates a new instance of a Remotus::Auth::Credential
|
37
|
+
#
|
38
|
+
# @param [String] user user name
|
39
|
+
# @param [String] password user password
|
40
|
+
# @param [Hash] options options hash
|
41
|
+
# @option options [String] :private_key private key path
|
42
|
+
# @option options [String] :private_key_data private key data as a string
|
43
|
+
#
|
44
|
+
def initialize(user, password = nil, **options)
|
45
|
+
@user = user
|
46
|
+
@crypt_info = { password: {}, private_key_data: {} }
|
47
|
+
@private_key = options[:private_key]
|
48
|
+
self.password = password
|
49
|
+
self.private_key_data = options[:private_key_data]
|
50
|
+
end
|
51
|
+
|
52
|
+
#
|
53
|
+
# Retrieved decrypted password
|
54
|
+
#
|
55
|
+
# @return [String, nil] decrypted password or nil if unset
|
56
|
+
#
|
57
|
+
def password
|
58
|
+
return unless @password
|
59
|
+
|
60
|
+
decrypt(@password, :password)
|
61
|
+
end
|
62
|
+
|
63
|
+
#
|
64
|
+
# Sets password
|
65
|
+
#
|
66
|
+
# @param [String] password new password
|
67
|
+
#
|
68
|
+
def password=(password)
|
69
|
+
@password = password ? encrypt(password.to_s, :password) : nil
|
70
|
+
end
|
71
|
+
|
72
|
+
#
|
73
|
+
# Retrieves decrypted private key data
|
74
|
+
#
|
75
|
+
# @return [String, nil] decrypted private key data or nil if unset
|
76
|
+
#
|
77
|
+
def private_key_data
|
78
|
+
return unless @private_key_data
|
79
|
+
|
80
|
+
decrypt(@private_key_data, :private_key_data)
|
81
|
+
end
|
82
|
+
|
83
|
+
#
|
84
|
+
# Sets private key data
|
85
|
+
#
|
86
|
+
# @param [String] private_key_data private key data
|
87
|
+
#
|
88
|
+
def private_key_data=(private_key_data)
|
89
|
+
@private_key_data = private_key_data ? encrypt(private_key_data.to_s, :private_key_data) : nil
|
90
|
+
end
|
91
|
+
|
92
|
+
#
|
93
|
+
# Converts credential to a string
|
94
|
+
#
|
95
|
+
# @return [String] Credential represented as a string
|
96
|
+
#
|
97
|
+
def to_s
|
98
|
+
"user: #{@user}"
|
99
|
+
end
|
100
|
+
|
101
|
+
#
|
102
|
+
# Inspects credential
|
103
|
+
#
|
104
|
+
# @return [String] Credential represented as an inspection string
|
105
|
+
#
|
106
|
+
def inspect
|
107
|
+
"#{self.class.name}: (#{self})"
|
108
|
+
end
|
109
|
+
|
110
|
+
private
|
111
|
+
|
112
|
+
#
|
113
|
+
# Encrypts string data
|
114
|
+
#
|
115
|
+
# @param [String] data data to encrypt
|
116
|
+
# @param [Symbol] crypt_key key in @crypt_info to store the key and iv for decryption
|
117
|
+
#
|
118
|
+
# @return [Object] encrypted data
|
119
|
+
#
|
120
|
+
def encrypt(data, crypt_key)
|
121
|
+
cipher = OpenSSL::Cipher.new("aes-256-cbc")
|
122
|
+
cipher.encrypt
|
123
|
+
@crypt_info[crypt_key][:key] = cipher.random_key
|
124
|
+
@crypt_info[crypt_key][:iv] = cipher.random_iv
|
125
|
+
cipher.update(data) + cipher.final
|
126
|
+
end
|
127
|
+
|
128
|
+
#
|
129
|
+
# Decrypts data to a string
|
130
|
+
#
|
131
|
+
# @param [Object] data encrypted data
|
132
|
+
# @param [Symbol] crypt_key key in @crypt_info containing the key and iv for decryption
|
133
|
+
#
|
134
|
+
# @return [String] decrypted string
|
135
|
+
#
|
136
|
+
def decrypt(data, crypt_key)
|
137
|
+
decipher = OpenSSL::Cipher.new("aes-256-cbc")
|
138
|
+
decipher.decrypt
|
139
|
+
decipher.key = @crypt_info[crypt_key][:key]
|
140
|
+
decipher.iv = @crypt_info[crypt_key][:iv]
|
141
|
+
decipher.update(data) + decipher.final
|
142
|
+
end
|
143
|
+
end
|
144
|
+
end
|
145
|
+
end
|
@@ -0,0 +1,56 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Remotus
|
4
|
+
module Auth
|
5
|
+
# Hash-based authentication store that requires credentials to be added manually
|
6
|
+
class HashStore < Store
|
7
|
+
#
|
8
|
+
# Creates the HashStore
|
9
|
+
#
|
10
|
+
def initialize
|
11
|
+
super
|
12
|
+
@store = {}
|
13
|
+
end
|
14
|
+
|
15
|
+
#
|
16
|
+
# Retrieves a credential from the hash store
|
17
|
+
#
|
18
|
+
# @param [Remotus::SshConnection, Remotus::WinrmConnection, #host] connection <description>
|
19
|
+
# @param [Hash] _options unused options hash
|
20
|
+
#
|
21
|
+
# @return [Remotus::Auth::Credential, nil] found credential or nil
|
22
|
+
#
|
23
|
+
def credential(connection, **_options)
|
24
|
+
@store[connection.host.downcase]
|
25
|
+
end
|
26
|
+
|
27
|
+
#
|
28
|
+
# Adds a credential to the store for a given connection
|
29
|
+
#
|
30
|
+
# @param [Remotus::SshConnection, Remotus::WinrmConnection, #host] connection associated connection
|
31
|
+
# @param [Remotus::Auth::Credential] credential new credential
|
32
|
+
#
|
33
|
+
def add(connection, credential)
|
34
|
+
@store[connection.host.downcase] = credential
|
35
|
+
end
|
36
|
+
|
37
|
+
#
|
38
|
+
# Removes a credential from the store for a given connection
|
39
|
+
#
|
40
|
+
# @param [Remotus::SshConnection, Remotus::WinrmConnection, #host] connection associated connection
|
41
|
+
#
|
42
|
+
def remove(connection)
|
43
|
+
@store.delete(connection.host.downcase)
|
44
|
+
end
|
45
|
+
|
46
|
+
#
|
47
|
+
# String representation of the hash store
|
48
|
+
#
|
49
|
+
# @return [String] string representation of the hash store
|
50
|
+
#
|
51
|
+
def to_s
|
52
|
+
"HashStore"
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Remotus
|
4
|
+
module Auth
|
5
|
+
# Authentication store base class
|
6
|
+
class Store
|
7
|
+
#
|
8
|
+
# Base method fo retrieving a credential from the hash store.
|
9
|
+
# This must be overridden in derived classes or it will raise an exception.
|
10
|
+
#
|
11
|
+
# @param [Remotus::SshConnection, Remotus::WinrmConnection, #host] _connection unused associated connection
|
12
|
+
# @param [Hash] _options unused options hash
|
13
|
+
#
|
14
|
+
def credential(_connection, **_options)
|
15
|
+
raise Remotus::MissingOverride, "credential method not implemented in credential store #{self.class}"
|
16
|
+
end
|
17
|
+
|
18
|
+
#
|
19
|
+
# Gets the user for a given connection and options
|
20
|
+
#
|
21
|
+
# @param [Remotus::SshConnection, Remotus::WinrmConnection, #host] connection associated connection
|
22
|
+
# @param [Hash] options options hash
|
23
|
+
#
|
24
|
+
# @return [String] user
|
25
|
+
#
|
26
|
+
def user(connection, **options)
|
27
|
+
credential(connection, **options)&.user
|
28
|
+
end
|
29
|
+
|
30
|
+
#
|
31
|
+
# Gets the password for a given connection and options
|
32
|
+
#
|
33
|
+
# @param [Remotus::SshConnection, Remotus::WinrmConnection, #host] connection associated connection
|
34
|
+
# @param [Hash] options options hash
|
35
|
+
#
|
36
|
+
# @return [String] password
|
37
|
+
#
|
38
|
+
def password(connection, **options)
|
39
|
+
credential(connection, **options)&.password
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
@@ -0,0 +1,181 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "remotus"
|
4
|
+
require "remotus/auth"
|
5
|
+
require "remotus/ssh_connection"
|
6
|
+
require "remotus/winrm_connection"
|
7
|
+
require "connection_pool"
|
8
|
+
|
9
|
+
module Remotus
|
10
|
+
# Class representing a remote connection pool (SSH or WinRM) for a single host
|
11
|
+
class HostPool
|
12
|
+
# @return [Time] when the host pool will expire
|
13
|
+
attr_reader :expiration_time
|
14
|
+
|
15
|
+
# @return [Integer] size of the host connection pool
|
16
|
+
attr_reader :size
|
17
|
+
|
18
|
+
# @return [Integer] Number of seconds to wait for a connection from the pool
|
19
|
+
attr_reader :timeout
|
20
|
+
|
21
|
+
# @return [Symbol] host pool protocol (:ssh or :winrm)
|
22
|
+
attr_reader :proto
|
23
|
+
|
24
|
+
# @return [String] host pool remote host
|
25
|
+
attr_reader :host
|
26
|
+
|
27
|
+
# Number of seconds to wait for a connection from the pool by default
|
28
|
+
DEFAULT_EXPIRATION_SECONDS = 600
|
29
|
+
|
30
|
+
# Default size of the connection pool
|
31
|
+
DEFAULT_POOL_SIZE = 2
|
32
|
+
|
33
|
+
#
|
34
|
+
# Creates a host pool for a specific host
|
35
|
+
#
|
36
|
+
# @param [String] host hostname
|
37
|
+
# @param [Integer] size number of connections in the pool (optional)
|
38
|
+
# @param [Integer] timeout amount of time to wait for a connection from the pool (optional)
|
39
|
+
# @param [Integer] port port to use for the connection
|
40
|
+
# @param [Symbol] proto protocol to use for the connection (:winrm, :ssh), must be specified if port is specified
|
41
|
+
#
|
42
|
+
def initialize(host, size: DEFAULT_POOL_SIZE, timeout: DEFAULT_EXPIRATION_SECONDS, port: nil, proto: nil)
|
43
|
+
Remotus.logger.debug { "Creating host pool for #{host}" }
|
44
|
+
|
45
|
+
@host = host
|
46
|
+
@proto = proto || Remotus.host_type(host)
|
47
|
+
|
48
|
+
raise Remotus::HostTypeDeterminationError, "Could not determine whether to use SSH or WinRM for #{host}" unless @proto
|
49
|
+
|
50
|
+
connection_class = Object.const_get("Remotus::#{@proto.capitalize}Connection")
|
51
|
+
port ||= connection_class::REMOTE_PORT
|
52
|
+
|
53
|
+
@pool = ConnectionPool.new(size: size, timeout: timeout) { connection_class.new(host, port) }
|
54
|
+
@size = size.to_i
|
55
|
+
@timeout = timeout.to_i
|
56
|
+
@expiration_time = Time.now + timeout
|
57
|
+
end
|
58
|
+
|
59
|
+
#
|
60
|
+
# Whether the pool is currently expired
|
61
|
+
#
|
62
|
+
# @return [Boolean] whether pool is expired
|
63
|
+
#
|
64
|
+
def expired?
|
65
|
+
Time.now > @expiration_time
|
66
|
+
end
|
67
|
+
|
68
|
+
#
|
69
|
+
# Force immediate expiration of the pool
|
70
|
+
#
|
71
|
+
def expire
|
72
|
+
Remotus.logger.debug { "Expiring #{@proto} host pool #{object_id} (#{@host})" }
|
73
|
+
@expiration_time = Time.now
|
74
|
+
end
|
75
|
+
|
76
|
+
#
|
77
|
+
# Provides an SSH or WinRM connection to a given block of code
|
78
|
+
#
|
79
|
+
# @example Run a command over SSH or WinRM using a pooled connection
|
80
|
+
# pool.with { |c| c.run("ls") }
|
81
|
+
#
|
82
|
+
# @param [Hash] options options hash
|
83
|
+
# @option options [Integer] :timeout amount of time to wait for a connection if none is available
|
84
|
+
#
|
85
|
+
# @return [Object] return value of the provided block
|
86
|
+
#
|
87
|
+
def with(**options, &block)
|
88
|
+
# Update expiration time since the pool has been used
|
89
|
+
@expiration_time = Time.now + (@timeout + options[:timeout].to_i)
|
90
|
+
Remotus.logger.debug { "Updating #{@proto} host pool #{object_id} (#{@host}) expiration time to #{@expiration_time}" }
|
91
|
+
|
92
|
+
# Run the provided block against the connection
|
93
|
+
Remotus.logger.debug { "Running block in #{@proto} host pool #{object_id} (#{@host})" }
|
94
|
+
@pool.with(options, &block)
|
95
|
+
end
|
96
|
+
|
97
|
+
#
|
98
|
+
# Gets remote host connection port
|
99
|
+
# @see Remotus::SshConnection#port
|
100
|
+
# @see Remotus::WinrmConnection#port
|
101
|
+
#
|
102
|
+
def port
|
103
|
+
Remotus.logger.debug { "Getting port from #{@proto} host pool #{object_id} (#{@host})" }
|
104
|
+
with(&:port)
|
105
|
+
end
|
106
|
+
|
107
|
+
#
|
108
|
+
# Checks if connection port is open on the remote host
|
109
|
+
# @see Remotus::SshConnection#port_open?
|
110
|
+
# @see Remotus::WinrmConnection#port_open?
|
111
|
+
#
|
112
|
+
def port_open?
|
113
|
+
Remotus.logger.debug { "Checking if port is open from #{@proto} host pool #{object_id} (#{@host})" }
|
114
|
+
with(&:port_open?)
|
115
|
+
end
|
116
|
+
|
117
|
+
#
|
118
|
+
# Runs command on the remote host
|
119
|
+
# @see Remotus::SshConnection#run
|
120
|
+
# @see Remotus::WinrmConnection#run
|
121
|
+
#
|
122
|
+
def run(command, *args, **options)
|
123
|
+
with { |c| c.run(command, *args, **options) }
|
124
|
+
end
|
125
|
+
|
126
|
+
#
|
127
|
+
# Runs script on the remote host
|
128
|
+
# @see Remotus::SshConnection#run_script
|
129
|
+
# @see Remotus::WinrmConnection#run_script
|
130
|
+
#
|
131
|
+
def run_script(local_path, remote_path, *args, **options)
|
132
|
+
with { |c| c.run_script(local_path, remote_path, *args, **options) }
|
133
|
+
end
|
134
|
+
|
135
|
+
#
|
136
|
+
# Uploads file to the remote host
|
137
|
+
# @see Remotus::SshConnection#upload
|
138
|
+
# @see Remotus::WinrmConnection#upload
|
139
|
+
#
|
140
|
+
def upload(local_path, remote_path, **options)
|
141
|
+
with { |c| c.upload(local_path, remote_path, **options) }
|
142
|
+
end
|
143
|
+
|
144
|
+
#
|
145
|
+
# Downloads file from the remote host
|
146
|
+
# @see Remotus::SshConnection#download
|
147
|
+
# @see Remotus::WinrmConnection#download
|
148
|
+
#
|
149
|
+
def download(remote_path, local_path = nil, **options)
|
150
|
+
with { |c| c.download(remote_path, local_path, **options) }
|
151
|
+
end
|
152
|
+
|
153
|
+
#
|
154
|
+
# Checks if file exists on the remote host
|
155
|
+
# @see Remotus::SshConnection#file_exist?
|
156
|
+
# @see Remotus::WinrmConnection#file_exist?
|
157
|
+
#
|
158
|
+
def file_exist?(remote_path, **options)
|
159
|
+
with { |c| c.file_exist?(remote_path, **options) }
|
160
|
+
end
|
161
|
+
|
162
|
+
#
|
163
|
+
# Gets the current host credential (if any)
|
164
|
+
# @see Remotus::Auth#credential
|
165
|
+
#
|
166
|
+
def credential(**options)
|
167
|
+
with { |c| Remotus::Auth.credential(c, **options) }
|
168
|
+
end
|
169
|
+
|
170
|
+
#
|
171
|
+
# Sets the current host credential
|
172
|
+
#
|
173
|
+
# @param [Remotus::Auth::Credential, Hash] credential new credential
|
174
|
+
#
|
175
|
+
def credential=(credential)
|
176
|
+
# If the credential is a hash, transform it prior to setting it
|
177
|
+
credential = Remotus::Auth::Credential.from_hash(credential) unless credential.is_a?(Remotus::Auth::Credential)
|
178
|
+
Remotus::Auth.cache[host] = credential
|
179
|
+
end
|
180
|
+
end
|
181
|
+
end
|