remotus 0.3.0 → 0.5.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.github/workflows/main.yml +5 -4
- data/.rubocop.yml +1 -0
- data/.ruby-version +1 -0
- data/CHANGELOG.md +6 -1
- data/Gemfile.lock +33 -22
- data/README.md +12 -0
- data/docker/Dockerfile +16 -0
- data/docker/entrypoint.sh +3 -0
- data/docker/ssh_integration_script.sh +8 -0
- data/docker/ssh_integration_spec.rb +84 -0
- data/docker-compose.yml +10 -0
- data/lib/remotus/core_ext/elevated.rb +35 -0
- data/lib/remotus/core_ext/string.rb +5 -1
- data/lib/remotus/host_pool.rb +7 -0
- data/lib/remotus/result.rb +1 -1
- data/lib/remotus/ssh_connection.rb +97 -17
- data/lib/remotus/version.rb +1 -1
- data/lib/remotus/winrm_connection.rb +23 -9
- data/remotus.gemspec +2 -0
- metadata +38 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: b375e9a93879a30b113991e9b653e602f30d970743925527af92a964298d7a27
|
4
|
+
data.tar.gz: 97bab3f384551a63e665d173e9b90330f03d3e8b56e6e89a9c216b18e37c2a66
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 6b3ded8d0ceb3c4b36736e875ca9ed80a85d77d388d1fb26f5a9659350622230c4f17efaee017e7765ddc081cd0ba52e767af960514a54c4061f667b38f1933c
|
7
|
+
data.tar.gz: 53aaf5d724c91a5639ce5f7928c6049b5912c21a523f2d1e5697191fb2511611e384fa5ce14fe92a53affaa8e707e42f5a5539ce83c93a16549bc1ac0fb3e5e5
|
data/.github/workflows/main.yml
CHANGED
@@ -6,21 +6,22 @@ jobs:
|
|
6
6
|
build:
|
7
7
|
runs-on: ubuntu-latest
|
8
8
|
steps:
|
9
|
-
- uses: actions/checkout@
|
9
|
+
- uses: actions/checkout@v3
|
10
10
|
- name: Set up Ruby
|
11
11
|
uses: ruby/setup-ruby@v1
|
12
12
|
with:
|
13
|
-
ruby-version:
|
13
|
+
ruby-version: 3.0.4
|
14
14
|
- name: Run unit tests and code linting
|
15
15
|
run: |
|
16
|
-
gem install bundler -v 2.2.
|
16
|
+
gem install bundler -v 2.2.33
|
17
17
|
bundle install
|
18
18
|
bundle exec rake
|
19
|
+
bundle exec rspec docker
|
19
20
|
bundle exec yard
|
20
21
|
|
21
22
|
- name: Deploy documentation
|
22
23
|
if: github.ref == 'refs/heads/main'
|
23
|
-
uses: crazy-max/ghaction-github-pages@
|
24
|
+
uses: crazy-max/ghaction-github-pages@v3.0.0
|
24
25
|
with:
|
25
26
|
target_branch: gh-pages
|
26
27
|
build_dir: doc
|
data/.rubocop.yml
CHANGED
data/.ruby-version
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
3.0.4
|
data/CHANGELOG.md
CHANGED
@@ -1,4 +1,9 @@
|
|
1
|
-
## [
|
1
|
+
## [0.5.0] - 2022-09-21
|
2
|
+
* Ensure port argument is respected in `Remotus::SshConnection`
|
3
|
+
* Add SSH gateway support
|
4
|
+
|
5
|
+
## [0.4.0] - 2022-06-02
|
6
|
+
* Added winrm-elevated gem to solve wirnrm AuthenticationError
|
2
7
|
|
3
8
|
## [0.3.0] - 2022-02-18
|
4
9
|
* Add retries to SSH SCP transactions
|
data/Gemfile.lock
CHANGED
@@ -1,11 +1,13 @@
|
|
1
1
|
PATH
|
2
2
|
remote: .
|
3
3
|
specs:
|
4
|
-
remotus (0.
|
4
|
+
remotus (0.5.0)
|
5
5
|
connection_pool (~> 2.2)
|
6
6
|
net-scp (~> 3.0)
|
7
7
|
net-ssh (~> 6.1)
|
8
|
+
net-ssh-gateway (~> 2.0)
|
8
9
|
winrm (~> 2.3)
|
10
|
+
winrm-elevated (~> 1.2)
|
9
11
|
winrm-fs (~> 1.3)
|
10
12
|
|
11
13
|
GEM
|
@@ -13,30 +15,34 @@ GEM
|
|
13
15
|
specs:
|
14
16
|
ast (2.4.2)
|
15
17
|
builder (3.2.4)
|
16
|
-
connection_pool (2.
|
18
|
+
connection_pool (2.3.0)
|
17
19
|
diff-lcs (1.5.0)
|
18
|
-
erubi (1.
|
20
|
+
erubi (1.11.0)
|
19
21
|
ffi (1.15.5)
|
20
22
|
gssapi (1.3.1)
|
21
23
|
ffi (>= 1.0.1)
|
22
|
-
gyoku (1.
|
24
|
+
gyoku (1.4.0)
|
23
25
|
builder (>= 2.1.2)
|
26
|
+
rexml (~> 3.0)
|
24
27
|
httpclient (2.8.3)
|
28
|
+
json (2.6.2)
|
25
29
|
little-plugger (1.1.4)
|
26
|
-
logging (2.3.
|
30
|
+
logging (2.3.1)
|
27
31
|
little-plugger (~> 1.1)
|
28
32
|
multi_json (~> 1.14)
|
29
33
|
multi_json (1.15.0)
|
30
34
|
net-scp (3.0.0)
|
31
35
|
net-ssh (>= 2.6.5, < 7.0.0)
|
32
36
|
net-ssh (6.1.0)
|
37
|
+
net-ssh-gateway (2.0.0)
|
38
|
+
net-ssh (>= 4.0.0)
|
33
39
|
nori (2.6.0)
|
34
|
-
parallel (1.
|
35
|
-
parser (3.1.
|
40
|
+
parallel (1.22.1)
|
41
|
+
parser (3.1.2.1)
|
36
42
|
ast (~> 2.4.1)
|
37
43
|
rainbow (3.1.1)
|
38
44
|
rake (13.0.6)
|
39
|
-
regexp_parser (2.
|
45
|
+
regexp_parser (2.5.0)
|
40
46
|
rexml (3.2.5)
|
41
47
|
rspec (3.11.0)
|
42
48
|
rspec-core (~> 3.11.0)
|
@@ -44,32 +50,33 @@ GEM
|
|
44
50
|
rspec-mocks (~> 3.11.0)
|
45
51
|
rspec-core (3.11.0)
|
46
52
|
rspec-support (~> 3.11.0)
|
47
|
-
rspec-expectations (3.11.
|
53
|
+
rspec-expectations (3.11.1)
|
48
54
|
diff-lcs (>= 1.2.0, < 2.0)
|
49
55
|
rspec-support (~> 3.11.0)
|
50
|
-
rspec-mocks (3.11.
|
56
|
+
rspec-mocks (3.11.1)
|
51
57
|
diff-lcs (>= 1.2.0, < 2.0)
|
52
58
|
rspec-support (~> 3.11.0)
|
53
|
-
rspec-support (3.11.
|
54
|
-
rubocop (1.
|
59
|
+
rspec-support (3.11.1)
|
60
|
+
rubocop (1.36.0)
|
61
|
+
json (~> 2.3)
|
55
62
|
parallel (~> 1.10)
|
56
|
-
parser (>= 3.1.
|
63
|
+
parser (>= 3.1.2.1)
|
57
64
|
rainbow (>= 2.2.2, < 4.0)
|
58
65
|
regexp_parser (>= 1.8, < 3.0)
|
59
|
-
rexml
|
60
|
-
rubocop-ast (>= 1.
|
66
|
+
rexml (>= 3.2.5, < 4.0)
|
67
|
+
rubocop-ast (>= 1.20.1, < 2.0)
|
61
68
|
ruby-progressbar (~> 1.7)
|
62
69
|
unicode-display_width (>= 1.4.0, < 3.0)
|
63
|
-
rubocop-ast (1.
|
64
|
-
parser (>= 3.
|
70
|
+
rubocop-ast (1.21.0)
|
71
|
+
parser (>= 3.1.1.0)
|
65
72
|
rubocop-rake (0.6.0)
|
66
73
|
rubocop (~> 1.0)
|
67
|
-
rubocop-rspec (2.
|
68
|
-
rubocop (~> 1.
|
74
|
+
rubocop-rspec (2.13.1)
|
75
|
+
rubocop (~> 1.33)
|
69
76
|
ruby-progressbar (1.11.0)
|
70
77
|
rubyntlm (0.6.3)
|
71
78
|
rubyzip (2.3.2)
|
72
|
-
unicode-display_width (2.
|
79
|
+
unicode-display_width (2.3.0)
|
73
80
|
webrick (1.7.0)
|
74
81
|
winrm (2.3.6)
|
75
82
|
builder (>= 2.1.2)
|
@@ -80,12 +87,16 @@ GEM
|
|
80
87
|
logging (>= 1.6.1, < 3.0)
|
81
88
|
nori (~> 2.0)
|
82
89
|
rubyntlm (~> 0.6.0, >= 0.6.3)
|
90
|
+
winrm-elevated (1.2.3)
|
91
|
+
erubi (~> 1.8)
|
92
|
+
winrm (~> 2.0)
|
93
|
+
winrm-fs (~> 1.0)
|
83
94
|
winrm-fs (1.3.5)
|
84
95
|
erubi (~> 1.8)
|
85
96
|
logging (>= 1.6.1, < 3.0)
|
86
97
|
rubyzip (~> 2.0)
|
87
98
|
winrm (~> 2.0)
|
88
|
-
yard (0.9.
|
99
|
+
yard (0.9.28)
|
89
100
|
webrick (~> 1.7.0)
|
90
101
|
|
91
102
|
PLATFORMS
|
@@ -102,4 +113,4 @@ DEPENDENCIES
|
|
102
113
|
yard (~> 0.9)
|
103
114
|
|
104
115
|
BUNDLED WITH
|
105
|
-
2.2.
|
116
|
+
2.2.33
|
data/README.md
CHANGED
@@ -30,6 +30,15 @@ connection = Remotus.connect("remotehost.local", proto: :ssh, port: 2222)
|
|
30
30
|
# Initialize a new connection pool to remotehost.local with a defined protocol and port and arbitrary metadata
|
31
31
|
connection = Remotus.connect("remotehost.local", proto: :ssh, port: 2222, company: "Test Corp", location: "Oslo")
|
32
32
|
|
33
|
+
# Initialize a new connection pool to remotehost.local via a gateway host named gateway.local
|
34
|
+
connection = Remotus.connect("remotehost.local", gateway_host: "gateway.local")
|
35
|
+
|
36
|
+
# Initialize a new connection pool to remotehost.local via a gateway host named gateway.local with a defined protocol and port
|
37
|
+
connection = Remotus.connect("remotehost.local", proto: :ssh, port: 2222, gateway_host: "gateway.local", gateway_port: 2222)
|
38
|
+
|
39
|
+
# Initialize a new connection pool to remotehost.local via a gateway host named gateway.local with arbitrary metadata
|
40
|
+
connection = Remotus.connect("remotehost.local", company: "Test Corp", gateway_host: "gateway.local", gateway_metadata: { company: "Other Corp" })
|
41
|
+
|
33
42
|
# Create a credential for the new connection pool
|
34
43
|
connection.credential = Remotus::Auth::Credential.new("username", "password")
|
35
44
|
|
@@ -54,6 +63,9 @@ result.exit_code
|
|
54
63
|
# Run a command on the remote host with sudo (Linux only, requires password to be specified)
|
55
64
|
result = connection.run("ls /root", sudo: true)
|
56
65
|
|
66
|
+
# Run a command on the remote host with elevated shell privilege
|
67
|
+
result = connection.run("ipconfig", shell: :elevated)
|
68
|
+
|
57
69
|
# Run a script on the remote host
|
58
70
|
connection.run_script("/local/script.sh", "/remote/path/script.sh")
|
59
71
|
|
data/docker/Dockerfile
ADDED
@@ -0,0 +1,16 @@
|
|
1
|
+
FROM alpine:latest
|
2
|
+
|
3
|
+
COPY entrypoint.sh /
|
4
|
+
|
5
|
+
RUN apk add --update --no-cache openssh && \
|
6
|
+
sed -i '/GatewayPorts.*/d' /etc/ssh/sshd_config && \
|
7
|
+
echo 'GatewayPorts yes' >> /etc/ssh/sshd_config && \
|
8
|
+
sed -i '/AllowTcpForwarding.*/d' /etc/ssh/sshd_config && \
|
9
|
+
echo 'AllowTcpForwarding yes' >> /etc/ssh/sshd_config && \
|
10
|
+
echo 'PasswordAuthentication yes' >> /etc/ssh/sshd_config && \
|
11
|
+
adduser -h /home/testuser -s /bin/sh -D testuser && \
|
12
|
+
echo 'testuser:testuser' | chpasswd
|
13
|
+
|
14
|
+
ENTRYPOINT ["/entrypoint.sh"]
|
15
|
+
|
16
|
+
EXPOSE 22
|
@@ -0,0 +1,84 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Assumes docker-compose is available
|
4
|
+
|
5
|
+
RSpec.describe "Remotus::SshConnection Integration Tests" do
|
6
|
+
before(:all) do
|
7
|
+
# docker-compose startup
|
8
|
+
`docker-compose -f "docker-compose.yml" up -d --build`
|
9
|
+
|
10
|
+
# Set up Remotus credentials based on Dockerfile
|
11
|
+
Remotus::Auth.cache["localhost"] = Remotus::Auth::Credential.new("testuser", "testuser")
|
12
|
+
Remotus::Auth.cache["target"] = Remotus::Auth::Credential.new("testuser", "testuser")
|
13
|
+
|
14
|
+
# Allow time for sshd to start up
|
15
|
+
sleep 1
|
16
|
+
end
|
17
|
+
|
18
|
+
after(:all) do
|
19
|
+
# docker-compose cleanup
|
20
|
+
`docker-compose -f "docker-compose.yml" down -v`
|
21
|
+
end
|
22
|
+
|
23
|
+
let(:test_script) { ::File.join(__dir__, "ssh_integration_script.sh") }
|
24
|
+
let(:test_script_dest) { ::File.join("/home/testuser", ::File.basename(test_script)) }
|
25
|
+
|
26
|
+
context "when a gateway and inaccessible target host exist" do
|
27
|
+
let(:gateway_hostname) { `docker ps | grep remotus_gateway`.split.first }
|
28
|
+
let(:target_hostname) { `docker ps | grep remotus_target`.split.first }
|
29
|
+
let(:gateway_connection) { Remotus.connect("localhost", proto: :ssh, port: 2222) }
|
30
|
+
let(:target_connection) { Remotus.connect("target", proto: :ssh, port: 22, gateway_host: "localhost", gateway_port: 2222) }
|
31
|
+
|
32
|
+
it "Connects to the gateway host successfully" do
|
33
|
+
expect(gateway_connection.run("hostname").stdout.chomp).to eq(gateway_hostname)
|
34
|
+
end
|
35
|
+
|
36
|
+
it "Can run a script against the gateway host successfully" do
|
37
|
+
result = gateway_connection.run_script(test_script, test_script_dest)
|
38
|
+
expect(result.stdout).to eq("success")
|
39
|
+
expect(result.success?).to eq(true)
|
40
|
+
end
|
41
|
+
|
42
|
+
it "Can upload a file to the gateway host" do
|
43
|
+
result = gateway_connection.upload(test_script, "/home/testuser/upload_test")
|
44
|
+
expect(result).to eq("/home/testuser/upload_test")
|
45
|
+
expect(gateway_connection.file_exist?("/home/testuser/upload_test")).to eq(true)
|
46
|
+
end
|
47
|
+
|
48
|
+
it "Can download a file from the gateway host" do
|
49
|
+
gateway_connection.run('echo -n "test" > /home/testuser/download_test')
|
50
|
+
result = gateway_connection.download("/home/testuser/download_test")
|
51
|
+
expect(result).to eq("test")
|
52
|
+
end
|
53
|
+
|
54
|
+
it "Can check if a file exists on the gateway host" do
|
55
|
+
expect(gateway_connection.file_exist?("/home/testuser")).to eq(true)
|
56
|
+
end
|
57
|
+
|
58
|
+
it "Connects to the target host successfully" do
|
59
|
+
expect(target_connection.run("hostname").stdout.chomp).to eq(target_hostname)
|
60
|
+
end
|
61
|
+
|
62
|
+
it "Can run a script against the target host successfully" do
|
63
|
+
result = target_connection.run_script(test_script, test_script_dest)
|
64
|
+
expect(result.stdout).to eq("success")
|
65
|
+
expect(result.success?).to eq(true)
|
66
|
+
end
|
67
|
+
|
68
|
+
it "Can upload a file to the target host" do
|
69
|
+
result = target_connection.upload(test_script, "/home/testuser/upload_test")
|
70
|
+
expect(result).to eq("/home/testuser/upload_test")
|
71
|
+
expect(target_connection.file_exist?("/home/testuser/upload_test")).to eq(true)
|
72
|
+
end
|
73
|
+
|
74
|
+
it "Can download a file from the target host" do
|
75
|
+
target_connection.run('echo -n "test" > /home/testuser/download_test')
|
76
|
+
result = target_connection.download("/home/testuser/download_test")
|
77
|
+
expect(result).to eq("test")
|
78
|
+
end
|
79
|
+
|
80
|
+
it "Can check if a file exists on the target host" do
|
81
|
+
expect(target_connection.file_exist?("/home/testuser")).to eq(true)
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
data/docker-compose.yml
ADDED
@@ -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
|
data/lib/remotus/host_pool.rb
CHANGED
@@ -42,6 +42,13 @@ module Remotus
|
|
42
42
|
# @param [Hash] metadata metadata for this connection. Useful for providing additional information to various authentication stores
|
43
43
|
# should be specified using snake_case symbol keys. If keys are not snake_case, they will be converted.
|
44
44
|
#
|
45
|
+
# To configure a connection gateway, the following metadata entries can be provided to the host pool:
|
46
|
+
# :gateway_host
|
47
|
+
# :gateway_port
|
48
|
+
# :gateway_metadata
|
49
|
+
#
|
50
|
+
# These function similarly to the host, port, and host_pool metadata fields.
|
51
|
+
#
|
45
52
|
def initialize(host, size: DEFAULT_POOL_SIZE, timeout: DEFAULT_EXPIRATION_SECONDS, port: nil, proto: nil, **metadata)
|
46
53
|
Remotus.logger.debug { "Creating host pool for #{host}" }
|
47
54
|
|
data/lib/remotus/result.rb
CHANGED
@@ -65,7 +65,7 @@ module Remotus
|
|
65
65
|
def error!(accepted_exit_codes = [0])
|
66
66
|
return unless error?(accepted_exit_codes)
|
67
67
|
|
68
|
-
raise Remotus::ResultError, "Error encountered executing #{@command}! Exit code #{@exit_code} was returned "\
|
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
|
|
@@ -6,6 +6,7 @@ require "remotus/result"
|
|
6
6
|
require "remotus/auth"
|
7
7
|
require "net/scp"
|
8
8
|
require "net/ssh"
|
9
|
+
require "net/ssh/gateway"
|
9
10
|
|
10
11
|
module Remotus
|
11
12
|
# Class representing an SSH connection to a host
|
@@ -21,6 +22,9 @@ module Remotus
|
|
21
22
|
# Number of default retries
|
22
23
|
DEFAULT_RETRIES = 2
|
23
24
|
|
25
|
+
# Base options for new SSH connections
|
26
|
+
BASE_CONNECT_OPTIONS = { non_interactive: true, keepalive: true, keepalive_interval: KEEPALIVE_INTERVAL }.freeze
|
27
|
+
|
24
28
|
# @return [Integer] Remote port
|
25
29
|
attr_reader :port
|
26
30
|
|
@@ -33,12 +37,50 @@ module Remotus
|
|
33
37
|
# Delegate metadata methods to associated host pool
|
34
38
|
def_delegators :@host_pool, :[], :[]=
|
35
39
|
|
40
|
+
# Internal gateway connection class to allow for the host and metadata to be pulled for the gateway
|
41
|
+
# by authentication credentials
|
42
|
+
class GatewayConnection
|
43
|
+
extend Forwardable
|
44
|
+
|
45
|
+
# @return [String] host gateway hostname
|
46
|
+
attr_reader :host
|
47
|
+
|
48
|
+
# @return [Integer] Remote port
|
49
|
+
attr_reader :port
|
50
|
+
|
51
|
+
# @return [Net::SSH::Gateway] connection gateway connection
|
52
|
+
attr_accessor :connection
|
53
|
+
|
54
|
+
# Delegate metadata methods to associated hash
|
55
|
+
def_delegators :@metadata, :[], :[]=
|
56
|
+
|
57
|
+
#
|
58
|
+
# Creates a GatewayConnection
|
59
|
+
#
|
60
|
+
# @param [String] host hostname
|
61
|
+
# @param [Integer] port remote port
|
62
|
+
# @param [Hash] metadata associated metadata for this gateway
|
63
|
+
#
|
64
|
+
def initialize(host, port = REMOTE_PORT, metadata = {})
|
65
|
+
@host = host
|
66
|
+
@port = port
|
67
|
+
@metadata = metadata
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
36
71
|
#
|
37
72
|
# Creates an SshConnection
|
38
73
|
#
|
39
74
|
# @param [String] host hostname
|
40
75
|
# @param [Integer] port remote port
|
41
76
|
# @param [Remotus::HostPool] host_pool associated host pool
|
77
|
+
# To configure the gateway, the following metadata
|
78
|
+
# entries can be provided to the host pool:
|
79
|
+
# :gateway_host
|
80
|
+
# :gateway_port
|
81
|
+
# :gateway_metadata
|
82
|
+
#
|
83
|
+
# These function similarly to the host, port, and host_pool metadata fields.
|
42
84
|
#
|
43
85
|
def initialize(host, port = REMOTE_PORT, host_pool: nil)
|
44
86
|
Remotus.logger.debug { "Creating SshConnection #{object_id} for #{host}" }
|
@@ -77,23 +119,32 @@ module Remotus
|
|
77
119
|
def connection
|
78
120
|
return @connection unless restart_connection?
|
79
121
|
|
80
|
-
|
122
|
+
target_cred = Remotus::Auth.credential(self)
|
123
|
+
|
124
|
+
Remotus.logger.debug { "Initializing SSH connection to #{target_cred.user}@#{@host}:#{@port}" }
|
81
125
|
|
82
|
-
|
126
|
+
target_options = BASE_CONNECT_OPTIONS.dup
|
127
|
+
target_options[:password] = target_cred.password if target_cred.password
|
128
|
+
target_options[:keys] = [target_cred.private_key] if target_cred.private_key
|
129
|
+
target_options[:key_data] = [target_cred.private_key_data] if target_cred.private_key_data
|
130
|
+
target_options[:port] = @port || REMOTE_PORT
|
83
131
|
|
84
|
-
|
85
|
-
|
86
|
-
|
132
|
+
if via_gateway?
|
133
|
+
@gateway = GatewayConnection.new(@host_pool[:gateway_host], @host_pool[:gateway_port], @host_pool[:gateway_metadata])
|
134
|
+
gateway_cred = Remotus::Auth.credential(@gateway)
|
135
|
+
gateway_options = BASE_CONNECT_OPTIONS.dup
|
136
|
+
gateway_options[:port] = @gateway.port || REMOTE_PORT
|
137
|
+
gateway_options[:password] = gateway_cred.password if gateway_cred.password
|
138
|
+
gateway_options[:keys] = [gateway_cred.private_key] if gateway_cred.private_key
|
139
|
+
gateway_options[:key_data] = [gateway_cred.private_key_data] if gateway_cred.private_key_data
|
87
140
|
|
88
|
-
|
89
|
-
options[:keys] = [private_key_path] if private_key_path
|
90
|
-
options[:key_data] = [private_key_data] if private_key_data
|
141
|
+
Remotus.logger.debug { "Initializing SSH gateway connection to #{gateway_cred.user}@#{@gateway.host}:#{gateway_options[:port]}" }
|
91
142
|
|
92
|
-
|
93
|
-
@host,
|
94
|
-
|
95
|
-
**
|
96
|
-
|
143
|
+
@gateway.connection = Net::SSH::Gateway.new(@gateway.host, gateway_cred.user, **gateway_options)
|
144
|
+
@gateway.connection.ssh(@host, target_cred.user, **target_options)
|
145
|
+
else
|
146
|
+
@connection = Net::SSH.start(@host, target_cred.user, **target_options)
|
147
|
+
end
|
97
148
|
end
|
98
149
|
|
99
150
|
#
|
@@ -392,10 +443,30 @@ module Remotus
|
|
392
443
|
return true unless @connection
|
393
444
|
return true if @connection.closed?
|
394
445
|
return true if @host != @connection.host
|
395
|
-
|
396
|
-
|
397
|
-
|
398
|
-
return true if
|
446
|
+
|
447
|
+
target_cred = Remotus::Auth.credential(self)
|
448
|
+
|
449
|
+
return true if target_cred.user != @connection.options[:user]
|
450
|
+
return true if target_cred.password != @connection.options[:password]
|
451
|
+
return true if Array(target_cred.private_key) != Array(@connection.options[:keys])
|
452
|
+
return true if Array(target_cred.private_key_data) != Array(@connection.options[:key_data])
|
453
|
+
|
454
|
+
# Perform gateway checks
|
455
|
+
if via_gateway?
|
456
|
+
return true unless @gateway&.connection&.active?
|
457
|
+
|
458
|
+
gateway_session = @gateway.connection.instance_variable_get(:@session)
|
459
|
+
|
460
|
+
return true if gateway_session.closed?
|
461
|
+
return true if @host_pool[:gateway_host] != gateway_session.host
|
462
|
+
|
463
|
+
gateway_cred = Remotus::Auth.credential(@gateway)
|
464
|
+
|
465
|
+
return true if gateway_cred.user != @connection.options[:user]
|
466
|
+
return true if gateway_cred.password != @connection.options[:password]
|
467
|
+
return true if Array(gateway_cred.private_key) != Array(@connection.options[:keys])
|
468
|
+
return true if Array(gateway_cred.private_key_data) != Array(@connection.options[:key_data])
|
469
|
+
end
|
399
470
|
|
400
471
|
false
|
401
472
|
end
|
@@ -469,5 +540,14 @@ module Remotus
|
|
469
540
|
Remotus.logger.debug { "Generated permission commands #{cmds}" }
|
470
541
|
cmds
|
471
542
|
end
|
543
|
+
|
544
|
+
#
|
545
|
+
# Whether connecting via an SSH gateway
|
546
|
+
#
|
547
|
+
# @return [Boolean] true if using a gateway, false otherwise
|
548
|
+
#
|
549
|
+
def via_gateway?
|
550
|
+
host_pool && host_pool[:gateway_host]
|
551
|
+
end
|
472
552
|
end
|
473
553
|
end
|
data/lib/remotus/version.rb
CHANGED
@@ -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
|
-
@
|
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]
|
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, **
|
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
@@ -32,7 +32,9 @@ Gem::Specification.new do |spec|
|
|
32
32
|
spec.add_dependency "connection_pool", "~> 2.2"
|
33
33
|
spec.add_dependency "net-scp", "~> 3.0"
|
34
34
|
spec.add_dependency "net-ssh", "~> 6.1"
|
35
|
+
spec.add_dependency "net-ssh-gateway", "~> 2.0"
|
35
36
|
spec.add_dependency "winrm", "~> 2.3"
|
37
|
+
spec.add_dependency "winrm-elevated", "~> 1.2"
|
36
38
|
spec.add_dependency "winrm-fs", "~> 1.3"
|
37
39
|
|
38
40
|
# 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.
|
4
|
+
version: 0.5.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: 2022-
|
11
|
+
date: 2022-09-20 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: connection_pool
|
@@ -52,6 +52,20 @@ dependencies:
|
|
52
52
|
- - "~>"
|
53
53
|
- !ruby/object:Gem::Version
|
54
54
|
version: '6.1'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: net-ssh-gateway
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - "~>"
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '2.0'
|
62
|
+
type: :runtime
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - "~>"
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '2.0'
|
55
69
|
- !ruby/object:Gem::Dependency
|
56
70
|
name: winrm
|
57
71
|
requirement: !ruby/object:Gem::Requirement
|
@@ -66,6 +80,20 @@ dependencies:
|
|
66
80
|
- - "~>"
|
67
81
|
- !ruby/object:Gem::Version
|
68
82
|
version: '2.3'
|
83
|
+
- !ruby/object:Gem::Dependency
|
84
|
+
name: winrm-elevated
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
86
|
+
requirements:
|
87
|
+
- - "~>"
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: '1.2'
|
90
|
+
type: :runtime
|
91
|
+
prerelease: false
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
93
|
+
requirements:
|
94
|
+
- - "~>"
|
95
|
+
- !ruby/object:Gem::Version
|
96
|
+
version: '1.2'
|
69
97
|
- !ruby/object:Gem::Dependency
|
70
98
|
name: winrm-fs
|
71
99
|
requirement: !ruby/object:Gem::Requirement
|
@@ -175,6 +203,7 @@ files:
|
|
175
203
|
- ".gitignore"
|
176
204
|
- ".rspec"
|
177
205
|
- ".rubocop.yml"
|
206
|
+
- ".ruby-version"
|
178
207
|
- CHANGELOG.md
|
179
208
|
- Gemfile
|
180
209
|
- Gemfile.lock
|
@@ -183,11 +212,17 @@ files:
|
|
183
212
|
- Rakefile
|
184
213
|
- bin/console
|
185
214
|
- bin/setup
|
215
|
+
- docker-compose.yml
|
216
|
+
- docker/Dockerfile
|
217
|
+
- docker/entrypoint.sh
|
218
|
+
- docker/ssh_integration_script.sh
|
219
|
+
- docker/ssh_integration_spec.rb
|
186
220
|
- lib/remotus.rb
|
187
221
|
- lib/remotus/auth.rb
|
188
222
|
- lib/remotus/auth/credential.rb
|
189
223
|
- lib/remotus/auth/hash_store.rb
|
190
224
|
- lib/remotus/auth/store.rb
|
225
|
+
- lib/remotus/core_ext/elevated.rb
|
191
226
|
- lib/remotus/core_ext/string.rb
|
192
227
|
- lib/remotus/host_pool.rb
|
193
228
|
- lib/remotus/logger.rb
|
@@ -220,7 +255,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
220
255
|
- !ruby/object:Gem::Version
|
221
256
|
version: '0'
|
222
257
|
requirements: []
|
223
|
-
rubygems_version: 3.
|
258
|
+
rubygems_version: 3.2.33
|
224
259
|
signing_key:
|
225
260
|
specification_version: 4
|
226
261
|
summary: Ruby gem for connecting to remote systems seamlessly via WinRM or SSH.
|