sshkit 1.18.0 → 1.23.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.docker/Dockerfile +6 -0
- data/.docker/ubuntu_setup.sh +22 -0
- data/.github/dependabot.yml +16 -0
- data/.github/release-drafter.yml +25 -0
- data/.github/workflows/ci.yml +98 -0
- data/.github/workflows/push.yml +12 -0
- data/.gitignore +0 -1
- data/.rubocop.yml +63 -0
- data/.rubocop_todo.yml +630 -0
- data/CHANGELOG.md +1 -769
- data/CONTRIBUTING.md +2 -2
- data/Dangerfile +1 -1
- data/EXAMPLES.md +35 -13
- data/Gemfile +0 -12
- data/README.md +35 -17
- data/RELEASING.md +4 -5
- data/Rakefile +3 -10
- data/docker-compose.yml +8 -0
- data/examples/simple_connection.rb +9 -0
- data/lib/sshkit/backends/abstract.rb +9 -6
- data/lib/sshkit/backends/connection_pool/cache.rb +12 -5
- data/lib/sshkit/backends/connection_pool.rb +8 -4
- data/lib/sshkit/backends/netssh/known_hosts.rb +8 -8
- data/lib/sshkit/backends/netssh/scp_transfer.rb +26 -0
- data/lib/sshkit/backends/netssh/sftp_transfer.rb +46 -0
- data/lib/sshkit/backends/netssh.rb +38 -8
- data/lib/sshkit/command.rb +18 -13
- data/lib/sshkit/deprecation_logger.rb +2 -0
- data/lib/sshkit/host.rb +31 -4
- data/lib/sshkit/version.rb +1 -1
- data/sshkit.gemspec +6 -1
- data/test/functional/backends/netssh_transfer_tests.rb +83 -0
- data/test/functional/backends/test_local.rb +18 -0
- data/test/functional/backends/test_netssh.rb +24 -79
- data/test/functional/backends/test_netssh_scp.rb +23 -0
- data/test/functional/backends/test_netssh_sftp.rb +23 -0
- data/test/helper.rb +5 -43
- data/test/support/docker_wrapper.rb +71 -0
- data/test/unit/backends/test_abstract.rb +13 -1
- data/test/unit/backends/test_netssh.rb +55 -0
- data/test/unit/formatters/test_pretty.rb +1 -1
- data/test/unit/test_command.rb +32 -7
- data/test/unit/test_command_map.rb +8 -8
- data/test/unit/test_deprecation_logger.rb +1 -1
- data/test/unit/test_host.rb +44 -0
- metadata +58 -18
- data/.travis.yml +0 -14
- data/Vagrantfile +0 -15
- data/test/boxes.json +0 -17
- data/test/functional/test_ssh_server_comes_up_for_functional_tests.rb +0 -24
- data/test/support/vagrant_wrapper.rb +0 -55
data/CONTRIBUTING.md
CHANGED
@@ -24,8 +24,8 @@ using unsupported features.
|
|
24
24
|
|
25
25
|
## Tests
|
26
26
|
|
27
|
-
SSHKit has a unit test suite and a functional test suite. Some functional tests run
|
28
|
-
[
|
27
|
+
SSHKit has a unit test suite and a functional test suite. Some functional tests run using
|
28
|
+
[Docker](https://docs.docker.com/get-docker/). If possible, you should make sure that the
|
29
29
|
tests pass for each commit by running `rake` in the sshkit directory. This is in case we
|
30
30
|
need to cherry pick commits or rebase. You should ensure the tests pass, (preferably on
|
31
31
|
the minimum and maximum ruby version), before creating a PR.
|
data/Dangerfile
CHANGED
@@ -1 +1 @@
|
|
1
|
-
danger.import_dangerfile(github: "capistrano/danger")
|
1
|
+
danger.import_dangerfile(github: "capistrano/danger", branch: "no-changelog")
|
data/EXAMPLES.md
CHANGED
@@ -121,9 +121,6 @@ on hosts do |host|
|
|
121
121
|
end
|
122
122
|
```
|
123
123
|
|
124
|
-
**Note:** The `upload!()` method doesn't honor the values of `as()` etc, this
|
125
|
-
will be improved as the library matures, but we're not there yet.
|
126
|
-
|
127
124
|
## Upload a file from a stream
|
128
125
|
|
129
126
|
```ruby
|
@@ -148,9 +145,6 @@ end
|
|
148
145
|
This spares one from having to figure out the correct escaping sequences for
|
149
146
|
something like "echo(:cat, '...?...', '> /etc/sudoers.d/yolo')".
|
150
147
|
|
151
|
-
**Note:** The `upload!()` method doesn't honor the values of `within()`, `as()`
|
152
|
-
etc, this will be improved as the library matures, but we're not there yet.
|
153
|
-
|
154
148
|
## Upload a directory of files
|
155
149
|
|
156
150
|
```ruby
|
@@ -160,7 +154,25 @@ end
|
|
160
154
|
```
|
161
155
|
|
162
156
|
In this case the `recursive: true` option mirrors the same options which are
|
163
|
-
available to [`Net::
|
157
|
+
available to [`Net::SCP`](https://github.com/net-ssh/net-scp) and
|
158
|
+
[`Net::SFTP`](https://github.com/net-ssh/net-sftp).
|
159
|
+
|
160
|
+
## Set the upload/download method (SCP or SFTP).
|
161
|
+
|
162
|
+
SSHKit can use SCP or SFTP for file transfers. The default is SCP, but this can be changed to SFTP per host:
|
163
|
+
|
164
|
+
```ruby
|
165
|
+
host = SSHKit::Host.new('user@example.com')
|
166
|
+
host.transfer_method = :sftp
|
167
|
+
```
|
168
|
+
|
169
|
+
or globally:
|
170
|
+
|
171
|
+
```ruby
|
172
|
+
SSHKit::Backend::Netssh.configure do |ssh|
|
173
|
+
ssh.transfer_method = :sftp
|
174
|
+
end
|
175
|
+
```
|
164
176
|
|
165
177
|
## Setting global SSH options
|
166
178
|
|
@@ -171,6 +183,7 @@ individual hosts:
|
|
171
183
|
SSHKit::Backend::Netssh.configure do |ssh|
|
172
184
|
ssh.connection_timeout = 30
|
173
185
|
ssh.ssh_options = {
|
186
|
+
user: 'adifferentuser',
|
174
187
|
keys: %w(/home/user/.ssh/id_rsa),
|
175
188
|
forward_agent: false,
|
176
189
|
auth_methods: %w(publickey password)
|
@@ -234,16 +247,16 @@ end
|
|
234
247
|
|
235
248
|
```ruby
|
236
249
|
# The default format is pretty, which outputs colored text
|
237
|
-
SSHKit.config.
|
250
|
+
SSHKit.config.use_format :pretty
|
238
251
|
|
239
252
|
# Text with no coloring
|
240
|
-
SSHKit.config.
|
253
|
+
SSHKit.config.use_format :simpletext
|
241
254
|
|
242
255
|
# Red / Green dots for each completed step
|
243
|
-
SSHKit.config.
|
256
|
+
SSHKit.config.use_format :dot
|
244
257
|
|
245
258
|
# No output
|
246
|
-
SSHKit.config.
|
259
|
+
SSHKit.config.use_format :blackhole
|
247
260
|
```
|
248
261
|
|
249
262
|
## Implement a dirt-simple formatter class
|
@@ -253,7 +266,7 @@ module SSHKit
|
|
253
266
|
module Formatter
|
254
267
|
class MyFormatter < SSHKit::Formatter::Abstract
|
255
268
|
def write(obj)
|
256
|
-
|
269
|
+
if obj.is_a? SSHKit::Command
|
257
270
|
# Do something here, see the SSHKit::Command documentation
|
258
271
|
end
|
259
272
|
end
|
@@ -262,7 +275,7 @@ module SSHKit
|
|
262
275
|
end
|
263
276
|
|
264
277
|
# If your formatter is defined in the SSHKit::Formatter module configure with the format option:
|
265
|
-
SSHKit.config.
|
278
|
+
SSHKit.config.use_format :myformatter
|
266
279
|
|
267
280
|
# Or configure the output directly
|
268
281
|
SSHKit.config.output = MyFormatter.new($stdout)
|
@@ -338,6 +351,15 @@ end
|
|
338
351
|
This will resolve the `example.com` hostname into a `SSHKit::Host` object, and
|
339
352
|
try to pull up the correct configuration for it.
|
340
353
|
|
354
|
+
## Connect to a host on a port different than 22
|
355
|
+
|
356
|
+
If your ssh server is running on a port different than 22, you can change this is
|
357
|
+
shown:
|
358
|
+
|
359
|
+
```ruby
|
360
|
+
on('example.com', {port: 1234}) do
|
361
|
+
end
|
362
|
+
```
|
341
363
|
|
342
364
|
## Run a command without it being command-mapped
|
343
365
|
|
data/Gemfile
CHANGED
@@ -2,18 +2,6 @@ source 'https://rubygems.org'
|
|
2
2
|
|
3
3
|
gemspec
|
4
4
|
|
5
|
-
platforms :rbx do
|
6
|
-
gem 'rubysl', '~> 2.0'
|
7
|
-
gem 'json'
|
8
|
-
end
|
9
|
-
|
10
|
-
# Chandler requires Ruby >= 2.1.0, but depending on the Travis environment,
|
11
|
-
# we may not meet that requirement. Only include the chandler gem if the Ruby
|
12
|
-
# requirement is met. (Chandler is used only for `rake release`; see Rakefile.)
|
13
|
-
if Gem::Requirement.new('>= 2.1.0').satisfied_by?(Gem::Version.new(RUBY_VERSION))
|
14
|
-
gem 'chandler', '>= 0.1.1'
|
15
|
-
end
|
16
|
-
|
17
5
|
# public_suffix 3+ requires ruby 2.1+
|
18
6
|
if Gem::Requirement.new('< 2.1').satisfied_by?(Gem::Version.new(RUBY_VERSION))
|
19
7
|
gem 'public_suffix', '< 3'
|
data/README.md
CHANGED
@@ -4,31 +4,33 @@
|
|
4
4
|
more servers.
|
5
5
|
|
6
6
|
[![Gem Version](https://badge.fury.io/rb/sshkit.svg)](https://rubygems.org/gems/sshkit)
|
7
|
-
[![Build Status](https://
|
7
|
+
[![Build Status](https://github.com/capistrano/sshkit/actions/workflows/ci.yml/badge.svg)](https://github.com/capistrano/sshkit/actions/workflows/ci.yml)
|
8
8
|
|
9
|
-
##
|
9
|
+
## Example
|
10
10
|
|
11
|
-
|
11
|
+
- Connect to 2 servers
|
12
|
+
- Execute commands as `deploy` user with `RAILS_ENV=production`
|
13
|
+
- Execute commands in serial (default is `:parallel`)
|
12
14
|
|
13
15
|
```ruby
|
14
16
|
require 'sshkit'
|
15
17
|
require 'sshkit/dsl'
|
16
18
|
include SSHKit::DSL
|
17
19
|
|
18
|
-
on
|
20
|
+
on ["1.example.com", "2.example.com"], in: :sequence do |host|
|
21
|
+
puts "Now executing on #{host}"
|
19
22
|
within "/opt/sites/example.com" do
|
20
23
|
as :deploy do
|
21
|
-
with
|
22
|
-
rake
|
23
|
-
runner "S3::Sync.notify"
|
24
|
-
execute :node, "socket_server.js"
|
24
|
+
with RAILS_ENV: 'production' do
|
25
|
+
execute :rake, "assets:precompile"
|
26
|
+
execute :rails, "runner", "S3::Sync.notify"
|
25
27
|
end
|
26
28
|
end
|
27
29
|
end
|
28
30
|
end
|
29
31
|
```
|
30
32
|
|
31
|
-
|
33
|
+
Many other examples are in [EXAMPLES.md](EXAMPLES.md).
|
32
34
|
|
33
35
|
## Basic usage
|
34
36
|
|
@@ -37,7 +39,7 @@ You can pass one or more hosts as parameters; this runs commands via SSH. Altern
|
|
37
39
|
pass `:local` to run commands locally. By default SSKit will run the commands on all hosts in
|
38
40
|
parallel.
|
39
41
|
|
40
|
-
|
42
|
+
### Running commands
|
41
43
|
|
42
44
|
All backends support the `execute(*args)`, `test(*args)` & `capture(*args)` methods
|
43
45
|
for executing a command. You can call any of these methods in the context of an `on()`
|
@@ -63,19 +65,20 @@ end
|
|
63
65
|
By default the `capture` methods strips whitespace. If you need to preserve whitespace
|
64
66
|
you can pass the `strip: false` option: `capture(:ls, '-l', strip: false)`
|
65
67
|
|
66
|
-
|
68
|
+
### Transferring files
|
67
69
|
|
68
70
|
All backends also support the `upload!` and `download!` methods for transferring files.
|
69
|
-
For the remote backend, the file is transferred with scp
|
71
|
+
For the remote backend, the file is transferred with scp by default, but sftp is also
|
72
|
+
supported. See [EXAMPLES.md](EXAMPLES.md) for details.
|
70
73
|
|
71
74
|
```ruby
|
72
75
|
on '1.example.com' do
|
73
76
|
upload! 'some_local_file.txt', '/home/some_user/somewhere'
|
74
|
-
download! '/home/some_user/some_remote_file.txt', 'somewhere_local', :
|
77
|
+
download! '/home/some_user/some_remote_file.txt', 'somewhere_local', log_percent: 25
|
75
78
|
end
|
76
79
|
```
|
77
80
|
|
78
|
-
|
81
|
+
### Users, working directories, environment variables and umask
|
79
82
|
|
80
83
|
When running commands, you can tell SSHKit to set up the context for those
|
81
84
|
commands using the following methods:
|
@@ -113,6 +116,11 @@ the raised error.
|
|
113
116
|
Helpers such as `runner()` and `rake()` which expand to `execute(:rails, "runner", ...)` and
|
114
117
|
`execute(:rake, ...)` are convenience helpers for Ruby, and Rails based apps.
|
115
118
|
|
119
|
+
### Verbosity / Silence
|
120
|
+
|
121
|
+
- raise verbosity of a command: `execute "echo DEAD", verbosity: :ERROR`
|
122
|
+
- hide a command from output: `execute "echo HIDDEN", verbosity: :DEBUG`
|
123
|
+
|
116
124
|
## Parallel
|
117
125
|
|
118
126
|
Notice on the `on()` call the `in: :sequence` option, the following will do
|
@@ -567,10 +575,20 @@ In order to do special gymnastics with SSH, tunneling, aliasing, complex options
|
|
567
575
|
|
568
576
|
These system level files are the preferred way of setting up tunneling and proxies because the system implementations of these things are faster and better than the Ruby implementations you would get if you were to configure them through Net::SSH. In cases where it's not possible (Windows?), it should be possible to make use of the Net::SSH APIs to setup tunnels and proxy commands before deferring control to Capistrano/SSHKit..
|
569
577
|
|
570
|
-
##
|
578
|
+
## Proxying
|
571
579
|
|
572
|
-
|
580
|
+
To connect to the target host via a jump/bastion host, use a `Net::SSH::Proxy::Jump`
|
573
581
|
|
574
|
-
|
582
|
+
```ruby
|
583
|
+
host = SSHKit::Host.new(
|
584
|
+
hostname: 'target.host.com',
|
585
|
+
ssh_options: { proxy: Net::SSH::Proxy::Jump.new("proxy.bar.com") }
|
586
|
+
)
|
587
|
+
on [host] do
|
588
|
+
execute :echo, '1'
|
589
|
+
end
|
590
|
+
```
|
591
|
+
|
592
|
+
## SSHKit Related Blog Posts
|
575
593
|
|
576
594
|
[Embedded Capistrano with SSHKit](http://ryandoyle.net/posts/embedded-capistrano-with-sshkit/)
|
data/RELEASING.md
CHANGED
@@ -5,14 +5,13 @@
|
|
5
5
|
* You must have commit rights to the SSHKit repository.
|
6
6
|
* You must have push rights for the sshkit gem on rubygems.org.
|
7
7
|
* You must be using Ruby >= 2.1.0.
|
8
|
-
* Your `~/.netrc` must be configured with your GitHub credentials, [as explained here](https://github.com/mattbrictson/chandler#2-configure-netrc).
|
9
8
|
|
10
9
|
## How to release
|
11
10
|
|
12
11
|
1. Run `bundle install` to make sure that you have all the gems necessary for testing and releasing.
|
13
|
-
2. **Ensure the tests are passing by running `rake test`.** If functional tests fail, ensure you have [
|
12
|
+
2. **Ensure the tests are passing by running `rake test`.** If functional tests fail, ensure you have [Docker installed](https://docs.docker.com/get-docker/) and running.
|
14
13
|
3. Determine which would be the correct next version number according to [semver](http://semver.org/).
|
15
14
|
4. Update the version in `./lib/sshkit/version.rb`.
|
16
|
-
5.
|
17
|
-
6.
|
18
|
-
7.
|
15
|
+
5. Commit the `version.rb` change with a message like "Preparing vX.Y.Z"
|
16
|
+
6. Run `rake release`; this will tag, push to GitHub, and publish to rubygems.org
|
17
|
+
7. Update the draft release on the [GitHub releases page](https://github.com/capistrano/sshkit/releases) to point to the new tag and publish the release
|
data/Rakefile
CHANGED
@@ -21,19 +21,12 @@ namespace :test do
|
|
21
21
|
|
22
22
|
end
|
23
23
|
|
24
|
-
Rake::Task["test:functional"].enhance do
|
25
|
-
warn "Remember there are still some VMs running, kill them with `vagrant halt` if you are finished using them."
|
26
|
-
end
|
27
|
-
|
28
24
|
desc 'Run RuboCop lint checks'
|
29
25
|
RuboCop::RakeTask.new(:lint) do |task|
|
30
26
|
task.options = ['--lint']
|
31
27
|
end
|
32
28
|
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
# We assume that the person doing `rake release` has Ruby >= 2.1.
|
37
|
-
require "chandler/tasks"
|
38
|
-
Rake.application.invoke_task("chandler:push")
|
29
|
+
Rake::Task["release"].enhance do
|
30
|
+
puts "Don't forget to publish the release on GitHub!"
|
31
|
+
system "open https://github.com/capistrano/sshkit/releases"
|
39
32
|
end
|
data/docker-compose.yml
ADDED
@@ -1,3 +1,5 @@
|
|
1
|
+
require 'shellwords'
|
2
|
+
|
1
3
|
module SSHKit
|
2
4
|
|
3
5
|
module Backend
|
@@ -80,13 +82,14 @@ module SSHKit
|
|
80
82
|
|
81
83
|
def within(directory, &_block)
|
82
84
|
(@pwd ||= []).push directory.to_s
|
85
|
+
escaped = Command.shellescape_except_tilde(pwd_path)
|
83
86
|
execute <<-EOTEST, verbosity: Logger::DEBUG
|
84
|
-
if test ! -d #{
|
85
|
-
then echo "Directory does not exist '#{
|
87
|
+
if test ! -d #{escaped}
|
88
|
+
then echo "Directory does not exist '#{escaped}'" 1>&2
|
86
89
|
false
|
87
90
|
fi
|
88
|
-
|
89
|
-
|
91
|
+
EOTEST
|
92
|
+
yield
|
90
93
|
ensure
|
91
94
|
@pwd.pop
|
92
95
|
end
|
@@ -108,8 +111,8 @@ module SSHKit
|
|
108
111
|
@group = nil
|
109
112
|
end
|
110
113
|
execute <<-EOTEST, verbosity: Logger::DEBUG
|
111
|
-
if ! sudo -u #{@user} whoami > /dev/null
|
112
|
-
then echo "You cannot switch to user '#{@user}' using sudo, please check the sudoers file" 1>&2
|
114
|
+
if ! sudo -u #{@user.to_s.shellescape} whoami > /dev/null
|
115
|
+
then echo "You cannot switch to user '#{@user.to_s.shellescape}' using sudo, please check the sudoers file" 1>&2
|
113
116
|
false
|
114
117
|
fi
|
115
118
|
EOTEST
|
@@ -36,12 +36,12 @@ class SSHKit::Backend::ConnectionPool::Cache
|
|
36
36
|
def evict
|
37
37
|
# Peek at the first connection to see if it is still fresh. If so, we can
|
38
38
|
# return right away without needing to use `synchronize`.
|
39
|
-
first_expires_at,
|
40
|
-
return if first_expires_at.nil? || fresh?(first_expires_at)
|
39
|
+
first_expires_at, _first_conn = connections.first
|
40
|
+
return if (first_expires_at.nil? || fresh?(first_expires_at))
|
41
41
|
|
42
42
|
connections.synchronize do
|
43
|
-
fresh, stale = connections.partition do |expires_at,
|
44
|
-
fresh?(expires_at)
|
43
|
+
fresh, stale = connections.partition do |expires_at, conn|
|
44
|
+
fresh?(expires_at) && !closed?(conn)
|
45
45
|
end
|
46
46
|
connections.replace(fresh)
|
47
47
|
stale.each { |_, conn| closer.call(conn) }
|
@@ -71,6 +71,13 @@ class SSHKit::Backend::ConnectionPool::Cache
|
|
71
71
|
end
|
72
72
|
|
73
73
|
def closed?(conn)
|
74
|
-
conn.respond_to?(:closed?) && conn.closed?
|
74
|
+
return true if conn.respond_to?(:closed?) && conn.closed?
|
75
|
+
# test if connection is alive
|
76
|
+
conn.process(0) if conn.respond_to?(:process)
|
77
|
+
return false
|
78
|
+
rescue IOError => e
|
79
|
+
# connection is closed by server
|
80
|
+
return true if e.message == 'closed stream'
|
81
|
+
raise
|
75
82
|
end
|
76
83
|
end
|
@@ -21,7 +21,7 @@ end
|
|
21
21
|
|
22
22
|
# The ConnectionPool caches connections and allows them to be reused, so long as
|
23
23
|
# the reuse happens within the `idle_timeout` period. Timed out connections are
|
24
|
-
# closed, forcing a new connection to be used in that case.
|
24
|
+
# eventually closed, forcing a new connection to be used in that case.
|
25
25
|
#
|
26
26
|
# Additionally, a background thread is started to check for abandoned
|
27
27
|
# connections that have timed out without any attempt at being reused. These
|
@@ -46,7 +46,11 @@ class SSHKit::Backend::ConnectionPool
|
|
46
46
|
@caches = {}
|
47
47
|
@caches.extend(MonitorMixin)
|
48
48
|
@timed_out_connections = Queue.new
|
49
|
-
|
49
|
+
|
50
|
+
# Spin up eviction loop only if caching is enabled
|
51
|
+
if cache_enabled?
|
52
|
+
Thread.new { run_eviction_loop }
|
53
|
+
end
|
50
54
|
end
|
51
55
|
|
52
56
|
# Creates a new connection or reuses a cached connection (if possible) and
|
@@ -118,9 +122,9 @@ class SSHKit::Backend::ConnectionPool
|
|
118
122
|
# Update cache key with changed args to prevent cache miss
|
119
123
|
def update_key_if_args_changed(cache, args)
|
120
124
|
new_key = cache_key_for_connection_args(args)
|
121
|
-
return if cache.same_key?(new_key)
|
122
125
|
|
123
126
|
caches.synchronize do
|
127
|
+
return if cache.same_key?(new_key)
|
124
128
|
caches[new_key] = caches.delete(cache.key)
|
125
129
|
cache.key = new_key
|
126
130
|
end
|
@@ -133,7 +137,7 @@ class SSHKit::Backend::ConnectionPool
|
|
133
137
|
process_deferred_close
|
134
138
|
|
135
139
|
# Periodically sweep all Caches to evict stale connections
|
136
|
-
sleep(
|
140
|
+
sleep(5)
|
137
141
|
caches.values.each(&:evict)
|
138
142
|
end
|
139
143
|
end
|
@@ -1,3 +1,5 @@
|
|
1
|
+
require "base64"
|
2
|
+
|
1
3
|
module SSHKit
|
2
4
|
|
3
5
|
module Backend
|
@@ -5,12 +7,11 @@ module SSHKit
|
|
5
7
|
class Netssh < Abstract
|
6
8
|
|
7
9
|
class KnownHostsKeys
|
8
|
-
include Mutex_m
|
9
|
-
|
10
10
|
def initialize(path)
|
11
11
|
super()
|
12
12
|
@path = File.expand_path(path)
|
13
13
|
@hosts_keys = nil
|
14
|
+
@mutex = Mutex.new
|
14
15
|
end
|
15
16
|
|
16
17
|
def keys_for(hostlist)
|
@@ -44,7 +45,7 @@ module SSHKit
|
|
44
45
|
end
|
45
46
|
|
46
47
|
def parse_file
|
47
|
-
synchronize do
|
48
|
+
@mutex.synchronize do
|
48
49
|
return if hosts_keys && hosts_hashes
|
49
50
|
|
50
51
|
unless File.readable?(path)
|
@@ -110,11 +111,10 @@ module SSHKit
|
|
110
111
|
end
|
111
112
|
|
112
113
|
class KnownHosts
|
113
|
-
include Mutex_m
|
114
|
-
|
115
114
|
def initialize
|
116
115
|
super()
|
117
116
|
@files = {}
|
117
|
+
@mutex = Mutex.new
|
118
118
|
end
|
119
119
|
|
120
120
|
def search_for(host, options = {})
|
@@ -126,13 +126,13 @@ module SSHKit
|
|
126
126
|
|
127
127
|
def add(*args)
|
128
128
|
::Net::SSH::KnownHosts.add(*args)
|
129
|
-
synchronize { @files = {} }
|
129
|
+
@mutex.synchronize { @files = {} }
|
130
130
|
end
|
131
131
|
|
132
132
|
private
|
133
133
|
|
134
134
|
def known_hosts_file(path)
|
135
|
-
@files[path] || synchronize { @files[path] ||= KnownHostsKeys.new(path) }
|
135
|
+
@files[path] || @mutex.synchronize { @files[path] ||= KnownHostsKeys.new(path) }
|
136
136
|
end
|
137
137
|
end
|
138
138
|
|
@@ -140,4 +140,4 @@ module SSHKit
|
|
140
140
|
|
141
141
|
end
|
142
142
|
|
143
|
-
end
|
143
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
require "net/scp"
|
2
|
+
|
3
|
+
module SSHKit
|
4
|
+
module Backend
|
5
|
+
class Netssh < Abstract
|
6
|
+
class ScpTransfer
|
7
|
+
def initialize(ssh, summarizer)
|
8
|
+
@ssh = ssh
|
9
|
+
@summarizer = summarizer
|
10
|
+
end
|
11
|
+
|
12
|
+
def upload!(local, remote, options)
|
13
|
+
ssh.scp.upload!(local, remote, options, &summarizer)
|
14
|
+
end
|
15
|
+
|
16
|
+
def download!(remote, local, options)
|
17
|
+
ssh.scp.download!(remote, local, options, &summarizer)
|
18
|
+
end
|
19
|
+
|
20
|
+
private
|
21
|
+
|
22
|
+
attr_reader :ssh, :summarizer
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
require "net/sftp"
|
2
|
+
|
3
|
+
module SSHKit
|
4
|
+
module Backend
|
5
|
+
class Netssh < Abstract
|
6
|
+
class SftpTransfer
|
7
|
+
def initialize(ssh, summarizer)
|
8
|
+
@ssh = ssh
|
9
|
+
@summarizer = summarizer
|
10
|
+
end
|
11
|
+
|
12
|
+
def upload!(local, remote, options)
|
13
|
+
options = { progress: self }.merge(options || {})
|
14
|
+
ssh.sftp.connect!
|
15
|
+
ssh.sftp.upload!(local, remote, options)
|
16
|
+
ensure
|
17
|
+
ssh.sftp.close_channel
|
18
|
+
end
|
19
|
+
|
20
|
+
def download!(remote, local, options)
|
21
|
+
options = { progress: self }.merge(options || {})
|
22
|
+
destination = local ? local : StringIO.new.tap { |io| io.set_encoding('BINARY') }
|
23
|
+
|
24
|
+
ssh.sftp.connect!
|
25
|
+
ssh.sftp.download!(remote, destination, options)
|
26
|
+
local ? true : destination.string
|
27
|
+
ensure
|
28
|
+
ssh.sftp.close_channel
|
29
|
+
end
|
30
|
+
|
31
|
+
def on_get(download, entry, offset, data)
|
32
|
+
entry.size ||= download.sftp.file.open(entry.remote) { |file| file.stat.size }
|
33
|
+
summarizer.call(nil, entry.remote, offset + data.bytesize, entry.size)
|
34
|
+
end
|
35
|
+
|
36
|
+
def on_put(_upload, file, offset, data)
|
37
|
+
summarizer.call(nil, file.local, offset + data.bytesize, file.size)
|
38
|
+
end
|
39
|
+
|
40
|
+
private
|
41
|
+
|
42
|
+
attr_reader :ssh, :summarizer
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
@@ -1,8 +1,6 @@
|
|
1
1
|
require 'English'
|
2
2
|
require 'strscan'
|
3
|
-
require 'mutex_m'
|
4
3
|
require 'net/ssh'
|
5
|
-
require 'net/scp'
|
6
4
|
|
7
5
|
module Net
|
8
6
|
module SSH
|
@@ -23,10 +21,27 @@ module SSHKit
|
|
23
21
|
module Backend
|
24
22
|
|
25
23
|
class Netssh < Abstract
|
24
|
+
def self.assert_valid_transfer_method!(method)
|
25
|
+
return if [:scp, :sftp].include?(method)
|
26
|
+
|
27
|
+
raise ArgumentError, "#{method.inspect} is not a valid transfer method. Supported methods are :scp, :sftp."
|
28
|
+
end
|
29
|
+
|
26
30
|
class Configuration
|
27
31
|
attr_accessor :connection_timeout, :pty
|
32
|
+
attr_reader :transfer_method
|
28
33
|
attr_writer :ssh_options
|
29
34
|
|
35
|
+
def initialize
|
36
|
+
self.transfer_method = :scp
|
37
|
+
end
|
38
|
+
|
39
|
+
def transfer_method=(method)
|
40
|
+
Netssh.assert_valid_transfer_method!(method)
|
41
|
+
|
42
|
+
@transfer_method = method
|
43
|
+
end
|
44
|
+
|
30
45
|
def ssh_options
|
31
46
|
default_options.merge(@ssh_options ||= {})
|
32
47
|
end
|
@@ -64,16 +79,16 @@ module SSHKit
|
|
64
79
|
def upload!(local, remote, options = {})
|
65
80
|
summarizer = transfer_summarizer('Uploading', options)
|
66
81
|
remote = File.join(pwd_path, remote) unless remote.to_s.start_with?("/") || pwd_path.nil?
|
67
|
-
|
68
|
-
|
82
|
+
with_transfer(summarizer) do |transfer|
|
83
|
+
transfer.upload!(local, remote, options)
|
69
84
|
end
|
70
85
|
end
|
71
86
|
|
72
87
|
def download!(remote, local=nil, options = {})
|
73
88
|
summarizer = transfer_summarizer('Downloading', options)
|
74
89
|
remote = File.join(pwd_path, remote) unless remote.to_s.start_with?("/") || pwd_path.nil?
|
75
|
-
|
76
|
-
|
90
|
+
with_transfer(summarizer) do |transfer|
|
91
|
+
transfer.download!(remote, local, options)
|
77
92
|
end
|
78
93
|
end
|
79
94
|
|
@@ -105,11 +120,12 @@ module SSHKit
|
|
105
120
|
last_percentage = nil
|
106
121
|
proc do |_ch, name, transferred, total|
|
107
122
|
percentage = (transferred.to_f * 100 / total.to_f)
|
108
|
-
unless percentage.nan?
|
123
|
+
unless percentage.nan? || percentage.infinite?
|
109
124
|
message = "#{action} #{name} #{percentage.round(2)}%"
|
110
125
|
percentage_r = (percentage / log_percent).truncate * log_percent
|
111
126
|
if percentage_r > 0 && (last_name != name || last_percentage != percentage_r)
|
112
|
-
|
127
|
+
verbosity = (options[:verbosity] || :INFO).downcase # TODO: ideally reuse command.rb logic
|
128
|
+
public_send verbosity, message
|
113
129
|
last_name = name
|
114
130
|
last_percentage = percentage_r
|
115
131
|
else
|
@@ -182,6 +198,20 @@ module SSHKit
|
|
182
198
|
)
|
183
199
|
end
|
184
200
|
|
201
|
+
def with_transfer(summarizer)
|
202
|
+
transfer_method = host.transfer_method || self.class.config.transfer_method
|
203
|
+
transfer_class = if transfer_method == :sftp
|
204
|
+
require_relative "netssh/sftp_transfer"
|
205
|
+
SftpTransfer
|
206
|
+
else
|
207
|
+
require_relative "netssh/scp_transfer"
|
208
|
+
ScpTransfer
|
209
|
+
end
|
210
|
+
|
211
|
+
with_ssh do |ssh|
|
212
|
+
yield(transfer_class.new(ssh, summarizer))
|
213
|
+
end
|
214
|
+
end
|
185
215
|
end
|
186
216
|
end
|
187
217
|
|