net-ssh-multi 1.0.0

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.
@@ -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