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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: d07fd3936fbe26fea5629acc43238c698f735f3d6814f0733c3b5b81d638f340
4
- data.tar.gz: fd028ed4fe407c5f4b9dc82ecdc7ec3371c4605cc19997227287df08badcaa9b
3
+ metadata.gz: b375e9a93879a30b113991e9b653e602f30d970743925527af92a964298d7a27
4
+ data.tar.gz: 97bab3f384551a63e665d173e9b90330f03d3e8b56e6e89a9c216b18e37c2a66
5
5
  SHA512:
6
- metadata.gz: 143e87065eb71702504a6773c74e68bc4a6efff6f08cbc8b812ed0c84db7a994719b578e4eb6a345a0f2369a11c1f4b05e1a9531a015cee345f958186ee06860
7
- data.tar.gz: 54ce478b43f70a989d8103f659da7ae61723794da6ef1918eee33ff73ee1d363dfb772da8297a4b8428712f639291d481c1df3112d5d09ba1bbbb3c2fd58883b
6
+ metadata.gz: 6b3ded8d0ceb3c4b36736e875ca9ed80a85d77d388d1fb26f5a9659350622230c4f17efaee017e7765ddc081cd0ba52e767af960514a54c4061f667b38f1933c
7
+ data.tar.gz: 53aaf5d724c91a5639ce5f7928c6049b5912c21a523f2d1e5697191fb2511611e384fa5ce14fe92a53affaa8e707e42f5a5539ce83c93a16549bc1ac0fb3e5e5
@@ -6,21 +6,22 @@ jobs:
6
6
  build:
7
7
  runs-on: ubuntu-latest
8
8
  steps:
9
- - uses: actions/checkout@v2
9
+ - uses: actions/checkout@v3
10
10
  - name: Set up Ruby
11
11
  uses: ruby/setup-ruby@v1
12
12
  with:
13
- ruby-version: 2.7.2
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.9
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@v2.2.0
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
@@ -19,6 +19,7 @@ Metrics/ClassLength:
19
19
  Metrics/BlockLength:
20
20
  Exclude:
21
21
  - spec/**/*.rb
22
+ - docker/**/*.rb
22
23
  - remotus.gemspec
23
24
  - lib/remotus/ssh_connection.rb
24
25
 
data/.ruby-version ADDED
@@ -0,0 +1 @@
1
+ 3.0.4
data/CHANGELOG.md CHANGED
@@ -1,4 +1,9 @@
1
- ## [Unreleased]
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.3.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.2.5)
18
+ connection_pool (2.3.0)
17
19
  diff-lcs (1.5.0)
18
- erubi (1.10.0)
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.3.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.0)
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.21.0)
35
- parser (3.1.0.0)
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.2.1)
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.0)
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.0)
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.0)
54
- rubocop (1.25.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.0.0)
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.15.1, < 2.0)
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.15.2)
64
- parser (>= 3.0.1.1)
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.8.0)
68
- rubocop (~> 1.19)
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.1.0)
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.27)
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.22
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,3 @@
1
+ #!/bin/sh
2
+ ssh-keygen -A
3
+ exec /usr/sbin/sshd -D -e "$@"
@@ -0,0 +1,8 @@
1
+ #!/bin/sh
2
+
3
+ # Script for use with the ssh_integration_spec.rb to validate
4
+ # SSH script functionality.
5
+
6
+ echo -n 'test complete' > /home/testuser/script_file
7
+
8
+ echo -n 'success'
@@ -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
@@ -0,0 +1,10 @@
1
+ version: "3.9"
2
+ services:
3
+ # Accessible gateway host
4
+ gateway:
5
+ build: docker
6
+ ports:
7
+ - "2222:22"
8
+ # Inaccessible gateway target host
9
+ target:
10
+ build: docker
@@ -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
@@ -24,4 +24,8 @@ module Remotus
24
24
  end
25
25
  end
26
26
 
27
- String.include(Remotus::CoreExt::String)
27
+ # @api private
28
+ # Core ruby string class
29
+ class String
30
+ include Remotus::CoreExt::String
31
+ end
@@ -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
 
@@ -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
- Remotus.logger.debug { "Initializing SSH connection to #{Remotus::Auth.credential(self).user}@#{@host}:#{@port}" }
122
+ target_cred = Remotus::Auth.credential(self)
123
+
124
+ Remotus.logger.debug { "Initializing SSH connection to #{target_cred.user}@#{@host}:#{@port}" }
81
125
 
82
- options = { non_interactive: true, keepalive: true, keepalive_interval: KEEPALIVE_INTERVAL }
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
- password = Remotus::Auth.credential(self).password
85
- private_key_path = Remotus::Auth.credential(self).private_key
86
- private_key_data = Remotus::Auth.credential(self).private_key_data
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
- options[:password] = password if password
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
- @connection = Net::SSH.start(
93
- @host,
94
- Remotus::Auth.credential(self).user,
95
- **options
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
- return true if Remotus::Auth.credential(self).user != @connection.options[:user]
396
- return true if Remotus::Auth.credential(self).password != @connection.options[:password]
397
- return true if Array(Remotus::Auth.credential(self).private_key) != Array(@connection.options[:keys])
398
- return true if Array(Remotus::Auth.credential(self).private_key_data) != Array(@connection.options[:key_data])
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
@@ -2,5 +2,5 @@
2
2
 
3
3
  module Remotus
4
4
  # Remotus gem version
5
- VERSION = "0.3.0"
5
+ VERSION = "0.5.0"
6
6
  end
@@ -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
- @connection = base_connection(reload: true).shell(:powershell)
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] _options unused command options
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, **_options)
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.3.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-02-18 00:00:00.000000000 Z
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.1.4
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.