net-ssh-multi 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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