net-ssh-multi 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- data/CHANGELOG.rdoc +13 -0
- data/Manifest +23 -0
- data/README.rdoc +87 -0
- data/Rakefile +28 -0
- data/lib/net/ssh/multi.rb +71 -0
- data/lib/net/ssh/multi/channel.rb +216 -0
- data/lib/net/ssh/multi/channel_proxy.rb +50 -0
- data/lib/net/ssh/multi/dynamic_server.rb +71 -0
- data/lib/net/ssh/multi/pending_connection.rb +112 -0
- data/lib/net/ssh/multi/server.rb +229 -0
- data/lib/net/ssh/multi/server_list.rb +80 -0
- data/lib/net/ssh/multi/session.rb +546 -0
- data/lib/net/ssh/multi/session_actions.rb +153 -0
- data/lib/net/ssh/multi/subsession.rb +48 -0
- data/lib/net/ssh/multi/version.rb +21 -0
- data/net-ssh-multi.gemspec +59 -0
- data/setup.rb +1585 -0
- data/test/channel_test.rb +152 -0
- data/test/common.rb +2 -0
- data/test/multi_test.rb +20 -0
- data/test/server_test.rb +256 -0
- data/test/session_actions_test.rb +128 -0
- data/test/session_test.rb +201 -0
- data/test/test_all.rb +3 -0
- metadata +93 -0
@@ -0,0 +1,71 @@
|
|
1
|
+
require 'net/ssh/multi/server'
|
2
|
+
|
3
|
+
module Net; module SSH; module Multi
|
4
|
+
|
5
|
+
# Represents a lazily evaluated collection of servers. This will usually be
|
6
|
+
# created via Net::SSH::Multi::Session#use(&block), and is useful for creating
|
7
|
+
# server definitions where the name or address of the servers are not known
|
8
|
+
# until run-time.
|
9
|
+
#
|
10
|
+
# session.use { lookup_ip_address_of_server }
|
11
|
+
#
|
12
|
+
# This prevents +lookup_ip_address_of_server+ from being invoked unless the
|
13
|
+
# server is actually needed, at which point it is invoked and the result
|
14
|
+
# cached.
|
15
|
+
#
|
16
|
+
# The callback should return either +nil+ (in which case no new servers are
|
17
|
+
# instantiated), a String (representing a connection specification), an
|
18
|
+
# array of Strings, or an array of Net::SSH::Multi::Server instances.
|
19
|
+
class DynamicServer
|
20
|
+
# The Net::SSH::Multi::Session instance that owns this dynamic server record.
|
21
|
+
attr_reader :master
|
22
|
+
|
23
|
+
# The Proc object to call to evaluate the server(s)
|
24
|
+
attr_reader :callback
|
25
|
+
|
26
|
+
# The hash of options that will be used to initialize the server records.
|
27
|
+
attr_reader :options
|
28
|
+
|
29
|
+
# Create a new DynamicServer record, owned by the given Net::SSH::Multi::Session
|
30
|
+
# +master+, with the given hash of +options+, and using the given Proc +callback+
|
31
|
+
# to lazily evaluate the actual server instances.
|
32
|
+
def initialize(master, options, callback)
|
33
|
+
@master, @options, @callback = master, options, callback
|
34
|
+
@servers = nil
|
35
|
+
end
|
36
|
+
|
37
|
+
# Returns the value for the given +key+ in the :properties hash of the
|
38
|
+
# +options+. If no :properties hash exists in +options+, this returns +nil+.
|
39
|
+
def [](key)
|
40
|
+
(options[:properties] ||= {})[key]
|
41
|
+
end
|
42
|
+
|
43
|
+
# Sets the given key/value pair in the +:properties+ key in the options
|
44
|
+
# hash. If the options hash has no :properties key, it will be created.
|
45
|
+
def []=(key, value)
|
46
|
+
(options[:properties] ||= {})[key] = value
|
47
|
+
end
|
48
|
+
|
49
|
+
# Iterates over every instantiated server record in this dynamic server.
|
50
|
+
# If the servers have not yet been instantiated, this does nothing (e.g.,
|
51
|
+
# it does _not_ automatically invoke #evaluate!).
|
52
|
+
def each
|
53
|
+
(@servers || []).each { |server| yield server }
|
54
|
+
end
|
55
|
+
|
56
|
+
# Evaluates the callback and instantiates the servers, memoizing the result.
|
57
|
+
# Subsequent calls to #evaluate! will simply return the cached list of
|
58
|
+
# servers.
|
59
|
+
def evaluate!
|
60
|
+
@servers ||= Array(callback[options]).map do |server|
|
61
|
+
case server
|
62
|
+
when String then Net::SSH::Multi::Server.new(master, server, options)
|
63
|
+
else server
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
alias to_ary evaluate!
|
69
|
+
end
|
70
|
+
|
71
|
+
end; end; end
|
@@ -0,0 +1,112 @@
|
|
1
|
+
require 'net/ssh/multi/channel_proxy'
|
2
|
+
|
3
|
+
module Net; module SSH; module Multi
|
4
|
+
|
5
|
+
# A PendingConnection instance mimics a Net::SSH::Connection::Session instance,
|
6
|
+
# without actually being an open connection to a server. It is used by
|
7
|
+
# Net::SSH::Multi::Session when a concurrent connection limit is in effect,
|
8
|
+
# so that a server can hang on to a "connection" that isn't really a connection.
|
9
|
+
#
|
10
|
+
# Any requests against this connection (like #open_channel or #send_global_request)
|
11
|
+
# are not actually sent, but are added to a list of recordings. When the real
|
12
|
+
# session is opened and replaces this pending connection, all recorded actions
|
13
|
+
# will be replayed against that session.
|
14
|
+
#
|
15
|
+
# You'll never need to initialize one of these directly, and (if all goes well!)
|
16
|
+
# should never even notice that one of these is in use. Net::SSH::Multi::Session
|
17
|
+
# will instantiate these as needed, and only when there is a concurrent
|
18
|
+
# connection limit.
|
19
|
+
class PendingConnection
|
20
|
+
# Represents a #open_channel action.
|
21
|
+
class ChannelOpenRecording #:nodoc:
|
22
|
+
attr_reader :type, :extras, :channel
|
23
|
+
|
24
|
+
def initialize(type, extras, channel)
|
25
|
+
@type, @extras, @channel = type, extras, channel
|
26
|
+
end
|
27
|
+
|
28
|
+
def replay_on(session)
|
29
|
+
real_channel = session.open_channel(type, *extras, &channel.on_confirm)
|
30
|
+
channel.delegate_to(real_channel)
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
# Represents a #send_global_request action.
|
35
|
+
class SendGlobalRequestRecording #:nodoc:
|
36
|
+
attr_reader :type, :extra, :callback
|
37
|
+
|
38
|
+
def initialize(type, extra, callback)
|
39
|
+
@type, @extra, @callback = type, extra, callback
|
40
|
+
end
|
41
|
+
|
42
|
+
def replay_on(session)
|
43
|
+
session.send_global_request(type, *extra, &callback)
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
# The Net::SSH::Multi::Server object that "owns" this pending connection.
|
48
|
+
attr_reader :server
|
49
|
+
|
50
|
+
# Instantiates a new pending connection for the given Net::SSH::Multi::Server
|
51
|
+
# object.
|
52
|
+
def initialize(server)
|
53
|
+
@server = server
|
54
|
+
@recordings = []
|
55
|
+
end
|
56
|
+
|
57
|
+
# Instructs the pending session to replay all of its recordings against the
|
58
|
+
# given +session+, and to then replace itself with the given session.
|
59
|
+
def replace_with(session)
|
60
|
+
@recordings.each { |recording| recording.replay_on(session) }
|
61
|
+
@server.replace_session(session)
|
62
|
+
end
|
63
|
+
|
64
|
+
# Records that a channel open request has been made, and returns a new
|
65
|
+
# Net::SSH::Multi::ChannelProxy object to represent the (as yet unopened)
|
66
|
+
# channel.
|
67
|
+
def open_channel(type="session", *extras, &on_confirm)
|
68
|
+
channel = ChannelProxy.new(&on_confirm)
|
69
|
+
@recordings << ChannelOpenRecording.new(type, extras, channel)
|
70
|
+
return channel
|
71
|
+
end
|
72
|
+
|
73
|
+
# Records that a global request has been made. The request is not actually
|
74
|
+
# sent, and won't be until #replace_with is called.
|
75
|
+
def send_global_request(type, *extra, &callback)
|
76
|
+
@recordings << SendGlobalRequestRecording.new(type, extra, callback)
|
77
|
+
self
|
78
|
+
end
|
79
|
+
|
80
|
+
# Always returns +true+, so that the pending connection looks active until
|
81
|
+
# it can be truly opened and replaced with a real connection.
|
82
|
+
def busy?(include_invisible=false)
|
83
|
+
true
|
84
|
+
end
|
85
|
+
|
86
|
+
# Does nothing, except to make a pending connection quack like a real connection.
|
87
|
+
def close
|
88
|
+
self
|
89
|
+
end
|
90
|
+
|
91
|
+
# Returns an empty array, since a pending connection cannot have any real channels.
|
92
|
+
def channels
|
93
|
+
[]
|
94
|
+
end
|
95
|
+
|
96
|
+
# Returns +true+, and does nothing else.
|
97
|
+
def preprocess
|
98
|
+
true
|
99
|
+
end
|
100
|
+
|
101
|
+
# Returns +true+, and does nothing else.
|
102
|
+
def postprocess(readers, writers)
|
103
|
+
true
|
104
|
+
end
|
105
|
+
|
106
|
+
# Returns an empty hash, since a pending connection has no real listeners.
|
107
|
+
def listeners
|
108
|
+
{}
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
end; end; end
|
@@ -0,0 +1,229 @@
|
|
1
|
+
require 'net/ssh'
|
2
|
+
|
3
|
+
module Net; module SSH; module Multi
|
4
|
+
# Encapsulates the connection information for a single remote server, as well
|
5
|
+
# as the Net::SSH session corresponding to that information. You'll rarely
|
6
|
+
# need to instantiate one of these directly: instead, you should use
|
7
|
+
# Net::SSH::Multi::Session#use.
|
8
|
+
class Server
|
9
|
+
include Comparable
|
10
|
+
|
11
|
+
# The Net::SSH::Multi::Session instance that manages this server instance.
|
12
|
+
attr_reader :master
|
13
|
+
|
14
|
+
# The host name (or IP address) of the server to connect to.
|
15
|
+
attr_reader :host
|
16
|
+
|
17
|
+
# The user name to use when logging into the server.
|
18
|
+
attr_reader :user
|
19
|
+
|
20
|
+
# The Hash of additional options to pass to Net::SSH when connecting
|
21
|
+
# (including things like :password, and so forth).
|
22
|
+
attr_reader :options
|
23
|
+
|
24
|
+
# The Net::SSH::Gateway instance to use to establish the connection. Will
|
25
|
+
# be +nil+ if the connection should be established without a gateway.
|
26
|
+
attr_reader :gateway
|
27
|
+
|
28
|
+
# Creates a new Server instance with the given connection information. The
|
29
|
+
# +master+ argument must be a reference to the Net::SSH::Multi::Session
|
30
|
+
# instance that will manage this server reference. The +options+ hash must
|
31
|
+
# conform to the options described for Net::SSH::start, with two additions:
|
32
|
+
#
|
33
|
+
# * :via => a Net::SSH::Gateway instance to use when establishing a
|
34
|
+
# connection to this server.
|
35
|
+
# * :user => the name of the user to use when logging into this server.
|
36
|
+
#
|
37
|
+
# The +host+ argument may include the username and port number, in which
|
38
|
+
# case those values take precedence over similar values given in the +options+:
|
39
|
+
#
|
40
|
+
# server = Net::SSH::Multi::Server.new(session, 'user@host:1234')
|
41
|
+
# puts server.user #-> user
|
42
|
+
# puts server.port #-> 1234
|
43
|
+
def initialize(master, host, options={})
|
44
|
+
@master = master
|
45
|
+
@options = options.dup
|
46
|
+
|
47
|
+
@user, @host, port = host.match(/^(?:([^;,:=]+)@|)(.*?)(?::(\d+)|)$/)[1,3]
|
48
|
+
|
49
|
+
user_opt, port_opt = @options.delete(:user), @options.delete(:port)
|
50
|
+
|
51
|
+
@user = @user || user_opt || master.default_user
|
52
|
+
port ||= port_opt
|
53
|
+
|
54
|
+
@options[:port] = port.to_i if port
|
55
|
+
|
56
|
+
@gateway = @options.delete(:via)
|
57
|
+
@failed = false
|
58
|
+
end
|
59
|
+
|
60
|
+
# Returns the value of the server property with the given +key+. Server
|
61
|
+
# properties are described via the +:properties+ key in the options hash
|
62
|
+
# when defining the Server.
|
63
|
+
def [](key)
|
64
|
+
(options[:properties] || {})[key]
|
65
|
+
end
|
66
|
+
|
67
|
+
# Sets the given key/value pair in the +:properties+ key in the options
|
68
|
+
# hash. If the options hash has no :properties key, it will be created.
|
69
|
+
def []=(key, value)
|
70
|
+
(options[:properties] ||= {})[key] = value
|
71
|
+
end
|
72
|
+
|
73
|
+
# Returns the port number to use for this connection.
|
74
|
+
def port
|
75
|
+
options[:port] || 22
|
76
|
+
end
|
77
|
+
|
78
|
+
# Gives server definitions a sort order, and allows comparison.
|
79
|
+
def <=>(server)
|
80
|
+
[host, port, user] <=> [server.host, server.port, server.user]
|
81
|
+
end
|
82
|
+
|
83
|
+
alias :eql? :==
|
84
|
+
|
85
|
+
# Generates a +Fixnum+ hash value for this object. This function has the
|
86
|
+
# property that +a.eql?(b)+ implies +a.hash == b.hash+. The
|
87
|
+
# hash value is used by class +Hash+. Any hash value that exceeds the
|
88
|
+
# capacity of a +Fixnum+ will be truncated before being used.
|
89
|
+
def hash
|
90
|
+
@hash ||= [host, user, port].hash
|
91
|
+
end
|
92
|
+
|
93
|
+
# Returns a human-readable representation of this server instance.
|
94
|
+
def to_s
|
95
|
+
@to_s ||= begin
|
96
|
+
s = "#{user}@#{host}"
|
97
|
+
s << ":#{options[:port]}" if options[:port]
|
98
|
+
s
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
# Returns a human-readable representation of this server instance.
|
103
|
+
def inspect
|
104
|
+
@inspect ||= "#<%s:0x%x %s>" % [self.class.name, object_id, to_s]
|
105
|
+
end
|
106
|
+
|
107
|
+
# Returns +true+ if this server has ever failed a connection attempt.
|
108
|
+
def failed?
|
109
|
+
@failed
|
110
|
+
end
|
111
|
+
|
112
|
+
# Indicates (by default) that this server has just failed a connection
|
113
|
+
# attempt. If +flag+ is false, this can be used to reset the failed flag
|
114
|
+
# so that a retry may be attempted.
|
115
|
+
def fail!(flag=true)
|
116
|
+
@failed = flag
|
117
|
+
end
|
118
|
+
|
119
|
+
# Returns the Net::SSH session object for this server. If +require_session+
|
120
|
+
# is false and the session has not previously been created, this will
|
121
|
+
# return +nil+. If +require_session+ is true, the session will be instantiated
|
122
|
+
# if it has not already been instantiated, via the +gateway+ if one is
|
123
|
+
# given, or directly (via Net::SSH::start) otherwise.
|
124
|
+
#
|
125
|
+
# if server.session.nil?
|
126
|
+
# puts "connecting..."
|
127
|
+
# server.session(true)
|
128
|
+
# end
|
129
|
+
#
|
130
|
+
# Note that the sessions returned by this are "enhanced" slightly, to make
|
131
|
+
# them easier to deal with in a multi-session environment: they have a
|
132
|
+
# :server property automatically set on them, that refers to this object
|
133
|
+
# (the Server instance that spawned them).
|
134
|
+
#
|
135
|
+
# assert_equal server, server.session[:server]
|
136
|
+
def session(require_session=false)
|
137
|
+
return @session if @session || !require_session
|
138
|
+
@session ||= master.next_session(self)
|
139
|
+
end
|
140
|
+
|
141
|
+
# Returns +true+ if the session has been opened, and the session is currently
|
142
|
+
# busy (as defined by Net::SSH::Connection::Session#busy?).
|
143
|
+
def busy?(include_invisible=false)
|
144
|
+
session && session.busy?(include_invisible)
|
145
|
+
end
|
146
|
+
|
147
|
+
# Closes this server's session. If the session has not yet been opened,
|
148
|
+
# this does nothing.
|
149
|
+
def close
|
150
|
+
session.close if session
|
151
|
+
ensure
|
152
|
+
master.server_closed(self) if session
|
153
|
+
@session = nil
|
154
|
+
end
|
155
|
+
|
156
|
+
public # but not published, e.g., these are used internally only...
|
157
|
+
|
158
|
+
# Indicate that the session currently in use by this server instance
|
159
|
+
# should be replaced by the given +session+ argument. This is used when
|
160
|
+
# a pending session has been "realized". Note that this does not
|
161
|
+
# actually replace the session--see #update_session! for that.
|
162
|
+
def replace_session(session) #:nodoc:
|
163
|
+
@ready_session = session
|
164
|
+
end
|
165
|
+
|
166
|
+
# If a new session has been made ready (see #replace_session), this
|
167
|
+
# will replace the current session with the "ready" session. This
|
168
|
+
# method is called from the event loop to ensure that sessions are
|
169
|
+
# swapped in at the appropriate point (instead of in the middle of an
|
170
|
+
# event poll).
|
171
|
+
def update_session! #:nodoc:
|
172
|
+
if @ready_session
|
173
|
+
@session, @ready_session = @ready_session, nil
|
174
|
+
end
|
175
|
+
end
|
176
|
+
|
177
|
+
# Returns a new session object based on this server's connection
|
178
|
+
# criteria. Note that this will not associate the session with the
|
179
|
+
# server, and should not be called directly; it is called internally
|
180
|
+
# from Net::SSH::Multi::Session when a new session is required.
|
181
|
+
def new_session #:nodoc:
|
182
|
+
session = if gateway
|
183
|
+
gateway.ssh(host, user, options)
|
184
|
+
else
|
185
|
+
Net::SSH.start(host, user, options)
|
186
|
+
end
|
187
|
+
|
188
|
+
session[:server] = self
|
189
|
+
session
|
190
|
+
rescue Net::SSH::AuthenticationFailed => error
|
191
|
+
raise Net::SSH::AuthenticationFailed.new("#{error.message}@#{host}")
|
192
|
+
end
|
193
|
+
|
194
|
+
# Closes all open channels on this server's session. If the session has
|
195
|
+
# not yet been opened, this does nothing.
|
196
|
+
def close_channels #:nodoc:
|
197
|
+
session.channels.each { |id, channel| channel.close } if session
|
198
|
+
end
|
199
|
+
|
200
|
+
# Runs the session's preprocess action, if the session has been opened.
|
201
|
+
def preprocess #:nodoc:
|
202
|
+
session.preprocess if session
|
203
|
+
end
|
204
|
+
|
205
|
+
# Returns all registered readers on the session, or an empty array if the
|
206
|
+
# session is not open.
|
207
|
+
def readers #:nodoc:
|
208
|
+
return [] unless session
|
209
|
+
session.listeners.keys.reject { |io| io.closed? }
|
210
|
+
end
|
211
|
+
|
212
|
+
# Returns all registered and pending writers on the session, or an empty
|
213
|
+
# array if the session is not open.
|
214
|
+
def writers #:nodoc:
|
215
|
+
readers.select do |io|
|
216
|
+
io.respond_to?(:pending_write?) && io.pending_write?
|
217
|
+
end
|
218
|
+
end
|
219
|
+
|
220
|
+
# Runs the post-process action on the session, if the session has been
|
221
|
+
# opened. Only the +readers+ and +writers+ that actually belong to this
|
222
|
+
# session will be postprocessed by this server.
|
223
|
+
def postprocess(readers, writers) #:nodoc:
|
224
|
+
return true unless session
|
225
|
+
listeners = session.listeners.keys
|
226
|
+
session.postprocess(listeners & readers, listeners & writers)
|
227
|
+
end
|
228
|
+
end
|
229
|
+
end; end; end
|
@@ -0,0 +1,80 @@
|
|
1
|
+
require 'net/ssh/multi/server'
|
2
|
+
require 'net/ssh/multi/dynamic_server'
|
3
|
+
|
4
|
+
module Net; module SSH; module Multi
|
5
|
+
|
6
|
+
# Encapsulates a list of server objects, both dynamic (Net::SSH::Multi::DynamicServer)
|
7
|
+
# and static (Net::SSH::Multi::Server). It attempts to make it transparent whether
|
8
|
+
# a dynamic server set has been evaluated or not. Note that a ServerList is
|
9
|
+
# NOT an Array, though it is Enumerable.
|
10
|
+
class ServerList
|
11
|
+
include Enumerable
|
12
|
+
|
13
|
+
# Create a new ServerList that wraps the given server list. Duplicate entries
|
14
|
+
# will be discarded.
|
15
|
+
def initialize(list=[])
|
16
|
+
@list = list.uniq
|
17
|
+
end
|
18
|
+
|
19
|
+
# Adds the given server to the list, and returns the argument. If an
|
20
|
+
# identical server definition already exists in the collection, the
|
21
|
+
# argument is _not_ added, and the existing server record is returned
|
22
|
+
# instead.
|
23
|
+
def add(server)
|
24
|
+
index = @list.index(server)
|
25
|
+
if index
|
26
|
+
server = @list[index]
|
27
|
+
else
|
28
|
+
@list.push(server)
|
29
|
+
end
|
30
|
+
server
|
31
|
+
end
|
32
|
+
|
33
|
+
# Adds an array (or otherwise Enumerable list) of servers to this list, by
|
34
|
+
# calling #add for each argument. Returns +self+.
|
35
|
+
def concat(servers)
|
36
|
+
servers.each { |server| add(server) }
|
37
|
+
self
|
38
|
+
end
|
39
|
+
|
40
|
+
# Iterates over each distinct server record in the collection. This will
|
41
|
+
# correctly iterate over server records instantiated by a DynamicServer
|
42
|
+
# as well, but only if the dynamic server has been "evaluated" (see
|
43
|
+
# Net::SSH::Multi::DynamicServer#evaluate!).
|
44
|
+
def each
|
45
|
+
@list.each do |server|
|
46
|
+
case server
|
47
|
+
when Server then yield server
|
48
|
+
when DynamicServer then server.each { |item| yield item }
|
49
|
+
else raise ArgumentError, "server list contains non-server: #{server.class}"
|
50
|
+
end
|
51
|
+
end
|
52
|
+
self
|
53
|
+
end
|
54
|
+
|
55
|
+
# Works exactly as Enumerable#select, but returns the result as a new
|
56
|
+
# ServerList instance.
|
57
|
+
def select
|
58
|
+
subset = @list.select { |i| yield i }
|
59
|
+
ServerList.new(subset)
|
60
|
+
end
|
61
|
+
|
62
|
+
# Returns an array of all servers in the list, with dynamic server records
|
63
|
+
# expanded. The result is an array of distinct server records (duplicates
|
64
|
+
# are removed from the result).
|
65
|
+
def flatten
|
66
|
+
result = @list.inject([]) do |aggregator, server|
|
67
|
+
case server
|
68
|
+
when Server then aggregator.push(server)
|
69
|
+
when DynamicServer then aggregator.concat(server)
|
70
|
+
else raise ArgumentError, "server list contains non-server: #{server.class}"
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
result.uniq
|
75
|
+
end
|
76
|
+
|
77
|
+
alias to_ary flatten
|
78
|
+
end
|
79
|
+
|
80
|
+
end; end; end
|