sshkit 1.8.1 → 1.9.0.rc1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.travis.yml +4 -3
- data/CHANGELOG.md +39 -0
- data/CONTRIBUTING.md +10 -0
- data/EXAMPLES.md +3 -1
- data/README.md +9 -10
- data/Rakefile +7 -1
- data/lib/sshkit/all.rb +2 -1
- data/lib/sshkit/backends/abstract.rb +25 -1
- data/lib/sshkit/backends/connection_pool.rb +139 -100
- data/lib/sshkit/backends/connection_pool/cache.rb +67 -0
- data/lib/sshkit/backends/connection_pool/nil_cache.rb +11 -0
- data/lib/sshkit/backends/netssh.rb +5 -9
- data/lib/sshkit/backends/printer.rb +5 -0
- data/lib/sshkit/command.rb +5 -1
- data/lib/sshkit/command_map.rb +0 -2
- data/lib/sshkit/configuration.rb +6 -2
- data/lib/sshkit/coordinator.rb +7 -7
- data/lib/sshkit/dsl.rb +0 -2
- data/lib/sshkit/formatters/abstract.rb +3 -2
- data/lib/sshkit/formatters/pretty.rb +3 -2
- data/lib/sshkit/mapping_interaction_handler.rb +4 -3
- data/lib/sshkit/runners/parallel.rb +3 -4
- data/lib/sshkit/version.rb +1 -1
- data/sshkit.gemspec +3 -2
- data/test/functional/backends/test_local.rb +1 -1
- data/test/helper.rb +5 -8
- data/test/unit/backends/test_abstract.rb +36 -0
- data/test/unit/backends/test_connection_pool.rb +48 -49
- data/test/unit/backends/test_printer.rb +5 -5
- data/test/unit/formatters/test_custom.rb +8 -2
- data/test/unit/test_command_map.rb +9 -0
- data/test/unit/test_configuration.rb +6 -0
- data/test/unit/test_coordinator.rb +27 -1
- data/test/unit/test_dsl.rb +26 -0
- data/test/unit/test_host.rb +6 -2
- metadata +43 -31
@@ -0,0 +1,67 @@
|
|
1
|
+
# A Cache holds connections for a given key. Each connection is stored along
|
2
|
+
# with an expiration time so that its idle duration can be measured.
|
3
|
+
class SSHKit::Backend::ConnectionPool::Cache
|
4
|
+
def initialize(idle_timeout, closer)
|
5
|
+
@connections = []
|
6
|
+
@connections.extend(MonitorMixin)
|
7
|
+
@idle_timeout = idle_timeout
|
8
|
+
@closer = closer
|
9
|
+
end
|
10
|
+
|
11
|
+
# Remove and return a fresh connection from this Cache. Returns `nil` if
|
12
|
+
# the Cache is empty or if all existing connections have gone stale.
|
13
|
+
def pop
|
14
|
+
connections.synchronize do
|
15
|
+
evict
|
16
|
+
_, connection = connections.pop
|
17
|
+
connection
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
# Return a connection to this Cache.
|
22
|
+
def push(conn)
|
23
|
+
# No need to cache if the connection has already been closed.
|
24
|
+
return if closed?(conn)
|
25
|
+
|
26
|
+
connections.synchronize do
|
27
|
+
connections.push([Time.now + idle_timeout, conn])
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
# Close and remove any connections in this Cache that have been idle for
|
32
|
+
# too long.
|
33
|
+
def evict
|
34
|
+
# Peek at the first connection to see if it is still fresh. If so, we can
|
35
|
+
# return right away without needing to use `synchronize`.
|
36
|
+
first_expires_at, _connection = connections.first
|
37
|
+
return if first_expires_at.nil? || fresh?(first_expires_at)
|
38
|
+
|
39
|
+
connections.synchronize do
|
40
|
+
fresh, stale = connections.partition do |expires_at, _|
|
41
|
+
fresh?(expires_at)
|
42
|
+
end
|
43
|
+
connections.replace(fresh)
|
44
|
+
stale.each { |_, conn| closer.call(conn) }
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
# Close all connections and completely clear the cache.
|
49
|
+
def clear
|
50
|
+
connections.synchronize do
|
51
|
+
connections.map(&:last).each(&closer)
|
52
|
+
connections.clear
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
private
|
57
|
+
|
58
|
+
attr_reader :connections, :idle_timeout, :closer
|
59
|
+
|
60
|
+
def fresh?(expires_at)
|
61
|
+
expires_at > Time.now
|
62
|
+
end
|
63
|
+
|
64
|
+
def closed?(conn)
|
65
|
+
conn.respond_to?(:closed?) && conn.closed?
|
66
|
+
end
|
67
|
+
end
|
@@ -129,19 +129,15 @@ module SSHKit
|
|
129
129
|
end
|
130
130
|
end
|
131
131
|
|
132
|
-
def with_ssh
|
133
|
-
host.ssh_options =
|
134
|
-
|
132
|
+
def with_ssh(&block)
|
133
|
+
host.ssh_options = self.class.config.ssh_options.merge(host.ssh_options || {})
|
134
|
+
self.class.pool.with(
|
135
|
+
Net::SSH.method(:start),
|
135
136
|
String(host.hostname),
|
136
137
|
host.username,
|
137
138
|
host.netssh_options,
|
138
|
-
&
|
139
|
+
&block
|
139
140
|
)
|
140
|
-
begin
|
141
|
-
yield conn.connection
|
142
|
-
ensure
|
143
|
-
self.class.pool.checkin conn
|
144
|
-
end
|
145
141
|
end
|
146
142
|
|
147
143
|
end
|
data/lib/sshkit/command.rb
CHANGED
@@ -206,7 +206,11 @@ module SSHKit
|
|
206
206
|
end
|
207
207
|
|
208
208
|
def to_s
|
209
|
-
|
209
|
+
if should_map?
|
210
|
+
[SSHKit.config.command_map[command.to_sym], *Array(args)].join(' ')
|
211
|
+
else
|
212
|
+
command.to_s
|
213
|
+
end
|
210
214
|
end
|
211
215
|
|
212
216
|
private
|
data/lib/sshkit/command_map.rb
CHANGED
data/lib/sshkit/configuration.rb
CHANGED
@@ -3,7 +3,7 @@ module SSHKit
|
|
3
3
|
class Configuration
|
4
4
|
|
5
5
|
attr_accessor :umask, :output_verbosity
|
6
|
-
attr_writer :output, :backend, :default_env
|
6
|
+
attr_writer :output, :backend, :default_env, :default_runner
|
7
7
|
|
8
8
|
def output
|
9
9
|
@output ||= use_format(:pretty)
|
@@ -22,6 +22,10 @@ module SSHKit
|
|
22
22
|
@default_env ||= {}
|
23
23
|
end
|
24
24
|
|
25
|
+
def default_runner
|
26
|
+
@default_runner ||= :parallel
|
27
|
+
end
|
28
|
+
|
25
29
|
def backend
|
26
30
|
@backend ||= SSHKit::Backend::Netssh
|
27
31
|
end
|
@@ -78,7 +82,7 @@ module SSHKit
|
|
78
82
|
found = SSHKit::Formatter.constants.find do |const|
|
79
83
|
const.to_s.downcase == name
|
80
84
|
end
|
81
|
-
fail NameError,
|
85
|
+
fail NameError, %Q{Unrecognized SSHKit::Formatter "#{symbol}"} if found.nil?
|
82
86
|
SSHKit::Formatter.const_get(found)
|
83
87
|
end
|
84
88
|
|
data/lib/sshkit/coordinator.rb
CHANGED
@@ -17,7 +17,7 @@ module SSHKit
|
|
17
17
|
when :sequence then Runner::Sequential
|
18
18
|
when :groups then Runner::Group
|
19
19
|
else
|
20
|
-
|
20
|
+
options[:in]
|
21
21
|
end.new(hosts, options, &block).execute
|
22
22
|
else
|
23
23
|
Runner::Null.new(hosts, options, &block).execute
|
@@ -26,13 +26,13 @@ module SSHKit
|
|
26
26
|
|
27
27
|
private
|
28
28
|
|
29
|
-
|
30
|
-
|
31
|
-
|
29
|
+
def default_options
|
30
|
+
{ in: SSHKit.config.default_runner }
|
31
|
+
end
|
32
32
|
|
33
|
-
|
34
|
-
|
35
|
-
|
33
|
+
def resolve_hosts
|
34
|
+
@raw_hosts.collect { |rh| rh.is_a?(Host) ? rh : Host.new(rh) }.uniq
|
35
|
+
end
|
36
36
|
|
37
37
|
end
|
38
38
|
|
data/lib/sshkit/dsl.rb
CHANGED
@@ -7,12 +7,13 @@ module SSHKit
|
|
7
7
|
class Abstract
|
8
8
|
|
9
9
|
extend Forwardable
|
10
|
-
attr_reader :original_output
|
10
|
+
attr_reader :original_output, :options
|
11
11
|
def_delegators :@original_output, :read, :rewind
|
12
12
|
def_delegators :@color, :colorize
|
13
13
|
|
14
|
-
def initialize(output)
|
14
|
+
def initialize(output, options={})
|
15
15
|
@original_output = output
|
16
|
+
@options = options
|
16
17
|
@color = SSHKit::Color.new(output)
|
17
18
|
end
|
18
19
|
|
@@ -23,11 +23,12 @@ module SSHKit
|
|
23
23
|
end
|
24
24
|
|
25
25
|
def log_command_data(command, stream_type, stream_data)
|
26
|
-
color =
|
26
|
+
color = \
|
27
|
+
case stream_type
|
27
28
|
when :stdout then :green
|
28
29
|
when :stderr then :red
|
29
30
|
else raise "Unrecognised stream_type #{stream_type}, expected :stdout or :stderr"
|
30
|
-
|
31
|
+
end
|
31
32
|
write_message(Logger::DEBUG, colorize("\t#{stream_data}".chomp, color), command.uuid)
|
32
33
|
end
|
33
34
|
|
@@ -4,7 +4,8 @@ module SSHKit
|
|
4
4
|
|
5
5
|
def initialize(mapping, log_level=nil)
|
6
6
|
@log_level = log_level
|
7
|
-
@mapping_proc =
|
7
|
+
@mapping_proc = \
|
8
|
+
case mapping
|
8
9
|
when Hash
|
9
10
|
lambda do |server_output|
|
10
11
|
first_matching_key_value = mapping.find { |k, _v| k === server_output }
|
@@ -14,7 +15,7 @@ module SSHKit
|
|
14
15
|
mapping
|
15
16
|
else
|
16
17
|
raise "Unsupported mapping type: #{mapping.class} - only Hash and Proc mappings are supported"
|
17
|
-
|
18
|
+
end
|
18
19
|
end
|
19
20
|
|
20
21
|
def on_data(_command, stream_name, data, channel)
|
@@ -44,4 +45,4 @@ module SSHKit
|
|
44
45
|
|
45
46
|
end
|
46
47
|
|
47
|
-
end
|
48
|
+
end
|
@@ -6,9 +6,8 @@ module SSHKit
|
|
6
6
|
|
7
7
|
class Parallel < Abstract
|
8
8
|
def execute
|
9
|
-
threads =
|
10
|
-
|
11
|
-
threads << Thread.new(host) do |h|
|
9
|
+
threads = hosts.map do |host|
|
10
|
+
Thread.new(host) do |h|
|
12
11
|
begin
|
13
12
|
backend(h, &block).run
|
14
13
|
rescue StandardError => e
|
@@ -17,7 +16,7 @@ module SSHKit
|
|
17
16
|
end
|
18
17
|
end
|
19
18
|
end
|
20
|
-
threads.
|
19
|
+
threads.each(&:join)
|
21
20
|
end
|
22
21
|
end
|
23
22
|
|
data/lib/sshkit/version.rb
CHANGED
data/sshkit.gemspec
CHANGED
@@ -20,9 +20,10 @@ Gem::Specification.new do |gem|
|
|
20
20
|
gem.add_runtime_dependency('net-ssh', '>= 2.8.0')
|
21
21
|
gem.add_runtime_dependency('net-scp', '>= 1.1.2')
|
22
22
|
|
23
|
-
gem.add_development_dependency('minitest',
|
23
|
+
gem.add_development_dependency('minitest', '>= 5.0.0')
|
24
|
+
gem.add_development_dependency('minitest-reporters')
|
24
25
|
gem.add_development_dependency('rake')
|
25
|
-
gem.add_development_dependency('
|
26
|
+
gem.add_development_dependency('rubocop')
|
26
27
|
gem.add_development_dependency('unindent')
|
27
28
|
gem.add_development_dependency('mocha')
|
28
29
|
end
|
data/test/helper.rb
CHANGED
@@ -1,9 +1,9 @@
|
|
1
1
|
require 'rubygems'
|
2
2
|
require 'bundler/setup'
|
3
3
|
require 'tempfile'
|
4
|
-
require 'minitest/
|
4
|
+
require 'minitest/autorun'
|
5
|
+
require 'minitest/reporters'
|
5
6
|
require 'mocha/setup'
|
6
|
-
require 'turn'
|
7
7
|
require 'unindent'
|
8
8
|
require 'stringio'
|
9
9
|
require 'json'
|
@@ -14,7 +14,7 @@ require 'sshkit'
|
|
14
14
|
|
15
15
|
Dir[File.expand_path('test/support/*.rb')].each { |file| require file }
|
16
16
|
|
17
|
-
class UnitTest <
|
17
|
+
class UnitTest < Minitest::Test
|
18
18
|
|
19
19
|
def setup
|
20
20
|
SSHKit.reset_configuration!
|
@@ -27,7 +27,7 @@ class UnitTest < MiniTest::Unit::TestCase
|
|
27
27
|
end
|
28
28
|
end
|
29
29
|
|
30
|
-
class FunctionalTest <
|
30
|
+
class FunctionalTest < Minitest::Test
|
31
31
|
|
32
32
|
def setup
|
33
33
|
unless VagrantWrapper.running?
|
@@ -78,7 +78,4 @@ end
|
|
78
78
|
#
|
79
79
|
# Force colours in Autotest
|
80
80
|
#
|
81
|
-
|
82
|
-
Turn.config.format = :pretty
|
83
|
-
|
84
|
-
MiniTest::Unit.autorun
|
81
|
+
Minitest::Reporters.use! Minitest::Reporters::SpecReporter.new
|
@@ -77,6 +77,20 @@ module SSHKit
|
|
77
77
|
assert_equal "Some stdout\n ", output
|
78
78
|
end
|
79
79
|
|
80
|
+
def test_within_properly_clears
|
81
|
+
backend = ExampleBackend.new do
|
82
|
+
within 'a' do
|
83
|
+
execute :cat, 'file', :strip => false
|
84
|
+
end
|
85
|
+
|
86
|
+
execute :cat, 'file', :strip => false
|
87
|
+
end
|
88
|
+
|
89
|
+
backend.run
|
90
|
+
|
91
|
+
assert_equal '/usr/bin/env cat file', backend.executed_command.to_command
|
92
|
+
end
|
93
|
+
|
80
94
|
def test_background_logs_deprecation_warnings
|
81
95
|
deprecation_out = ''
|
82
96
|
SSHKit.config.deprecation_output = deprecation_out
|
@@ -117,6 +131,28 @@ module SSHKit
|
|
117
131
|
end
|
118
132
|
end
|
119
133
|
|
134
|
+
def test_current_refers_to_currently_executing_backend
|
135
|
+
backend = nil
|
136
|
+
current = nil
|
137
|
+
|
138
|
+
backend = ExampleBackend.new do
|
139
|
+
backend = self
|
140
|
+
current = SSHKit::Backend.current
|
141
|
+
end
|
142
|
+
backend.run
|
143
|
+
|
144
|
+
assert_equal(backend, current)
|
145
|
+
end
|
146
|
+
|
147
|
+
def test_current_is_nil_outside_of_the_block
|
148
|
+
backend = ExampleBackend.new do
|
149
|
+
# nothing
|
150
|
+
end
|
151
|
+
backend.run
|
152
|
+
|
153
|
+
assert_nil(SSHKit::Backend.current)
|
154
|
+
end
|
155
|
+
|
120
156
|
# Use a concrete ExampleBackend rather than a mock for improved assertion granularity
|
121
157
|
class ExampleBackend < Abstract
|
122
158
|
attr_writer :full_stdout
|
@@ -32,65 +32,62 @@ module SSHKit
|
|
32
32
|
|
33
33
|
def test_connection_factory_receives_args
|
34
34
|
args = %w(a b c)
|
35
|
-
conn = pool.
|
35
|
+
conn = pool.with(echo_args, *args) { |c| c }
|
36
36
|
|
37
|
-
assert_equal args, conn
|
37
|
+
assert_equal args, conn
|
38
38
|
end
|
39
39
|
|
40
40
|
def test_connections_are_not_reused_if_not_checked_in
|
41
|
-
conn1 =
|
42
|
-
conn2 =
|
41
|
+
conn1 = nil
|
42
|
+
conn2 = nil
|
43
|
+
|
44
|
+
pool.with(connect, "conn") do |yielded_conn_1|
|
45
|
+
conn1 = yielded_conn_1
|
46
|
+
conn2 = pool.with(connect, "conn") { |c| c }
|
47
|
+
end
|
43
48
|
|
44
49
|
refute_equal conn1, conn2
|
45
50
|
end
|
46
51
|
|
47
52
|
def test_connections_are_reused_if_checked_in
|
48
|
-
conn1 = pool.
|
49
|
-
pool.
|
50
|
-
conn2 = pool.checkout("conn", &connect)
|
53
|
+
conn1 = pool.with(connect, "conn") {}
|
54
|
+
conn2 = pool.with(connect, "conn") {}
|
51
55
|
|
52
56
|
assert_equal conn1, conn2
|
53
57
|
end
|
54
58
|
|
55
59
|
def test_connections_are_reused_across_threads_multiple_times
|
56
|
-
t1 = Thread.new
|
57
|
-
|
58
|
-
|
59
|
-
}.join
|
60
|
+
t1 = Thread.new do
|
61
|
+
pool.with(connect, "conn") { |c| c }
|
62
|
+
end
|
60
63
|
|
61
|
-
t2 = Thread.new
|
62
|
-
|
63
|
-
|
64
|
-
}.join
|
64
|
+
t2 = Thread.new do
|
65
|
+
pool.with(connect, "conn") { |c| c }
|
66
|
+
end
|
65
67
|
|
66
|
-
t3 = Thread.new
|
67
|
-
|
68
|
-
|
69
|
-
}.join
|
68
|
+
t3 = Thread.new do
|
69
|
+
pool.with(connect, "conn") { |c| c }
|
70
|
+
end
|
70
71
|
|
71
|
-
|
72
|
-
assert_equal t1
|
73
|
-
assert_equal t2
|
72
|
+
refute_nil t1.value
|
73
|
+
assert_equal t1.value, t2.value
|
74
|
+
assert_equal t2.value, t3.value
|
74
75
|
end
|
75
76
|
|
76
|
-
def
|
77
|
+
def test_zero_idle_timeout_disables_pooling
|
77
78
|
pool.idle_timeout = 0
|
78
79
|
|
79
|
-
conn1 = pool.
|
80
|
-
pool.
|
81
|
-
|
82
|
-
conn2 = pool.checkout("conn", &connect)
|
83
|
-
|
80
|
+
conn1 = pool.with(connect, "conn") { |c| c }
|
81
|
+
conn2 = pool.with(connect, "conn") { |c| c }
|
84
82
|
refute_equal conn1, conn2
|
85
83
|
end
|
86
84
|
|
87
85
|
def test_expired_connection_is_not_reused
|
88
86
|
pool.idle_timeout = 0.1
|
89
87
|
|
90
|
-
conn1 = pool.
|
91
|
-
pool.checkin conn1
|
88
|
+
conn1 = pool.with(connect, "conn") { |c| c }
|
92
89
|
sleep(pool.idle_timeout)
|
93
|
-
conn2 = pool.
|
90
|
+
conn2 = pool.with(connect, "conn") { |c| c }
|
94
91
|
|
95
92
|
refute_equal conn1, conn2
|
96
93
|
end
|
@@ -98,43 +95,45 @@ module SSHKit
|
|
98
95
|
def test_expired_connection_is_closed
|
99
96
|
pool.idle_timeout = 0.1
|
100
97
|
conn1 = mock
|
101
|
-
conn1.expects(:closed?).returns(false)
|
98
|
+
conn1.expects(:closed?).twice.returns(false)
|
102
99
|
conn1.expects(:close)
|
103
100
|
|
104
|
-
|
105
|
-
|
106
|
-
sleep(pool.idle_timeout)
|
107
|
-
pool.checkout("conn2"){|*args| Object.new}
|
101
|
+
pool.with(->(*) { conn1 }, "conn1") {}
|
102
|
+
# Pause to allow the background thread to wake and close the conn
|
103
|
+
sleep(5 + pool.idle_timeout)
|
108
104
|
end
|
109
105
|
|
110
106
|
def test_closed_connection_is_not_reused
|
111
|
-
conn1 = pool.
|
112
|
-
pool.
|
113
|
-
conn2 = pool.checkout("conn", &connect)
|
107
|
+
conn1 = pool.with(connect_and_close, "conn") { |c| c }
|
108
|
+
conn2 = pool.with(connect, "conn") { |c| c }
|
114
109
|
|
115
110
|
refute_equal conn1, conn2
|
116
111
|
end
|
117
112
|
|
118
113
|
def test_connections_with_different_args_are_not_reused
|
119
|
-
conn1 = pool.
|
120
|
-
pool.
|
121
|
-
conn2 = pool.checkout("conn2", &connect)
|
114
|
+
conn1 = pool.with(connect, "conn1") { |c| c }
|
115
|
+
conn2 = pool.with(connect, "conn2") { |c| c }
|
122
116
|
|
123
117
|
refute_equal conn1, conn2
|
124
118
|
end
|
125
119
|
|
126
120
|
def test_close_connections
|
127
121
|
conn1 = mock
|
128
|
-
conn1.expects(:closed?).returns(false)
|
122
|
+
conn1.expects(:closed?).twice.returns(false)
|
129
123
|
conn1.expects(:close)
|
130
|
-
entry1 = pool.checkout("conn1"){|*args| conn1 }
|
131
|
-
pool.checkin entry1
|
132
|
-
entry2 = pool.checkout("conn2", &connect)
|
133
|
-
# entry2 isn't closed if close_connections is called
|
134
124
|
|
135
|
-
|
136
|
-
|
125
|
+
conn2 = mock
|
126
|
+
conn2.expects(:closed?).returns(false)
|
127
|
+
conn2.expects(:close).never
|
128
|
+
|
129
|
+
pool.with(->(*) { conn1 }, "conn1") {}
|
137
130
|
|
131
|
+
# We are using conn2 when close_connections is called, so it should
|
132
|
+
# not be closed.
|
133
|
+
pool.with(->(*) { conn2 }, "conn2") do
|
134
|
+
pool.close_connections
|
135
|
+
end
|
136
|
+
end
|
138
137
|
end
|
139
138
|
end
|
140
139
|
end
|