cloudflock 0.6.1 → 0.7.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.
Files changed (52) hide show
  1. checksums.yaml +15 -0
  2. data/bin/cloudflock +7 -1
  3. data/bin/cloudflock-files +2 -14
  4. data/bin/cloudflock-profile +3 -15
  5. data/bin/cloudflock-servers +3 -22
  6. data/bin/cloudflock.default +3 -22
  7. data/lib/cloudflock/app/common/cleanup/unix.rb +23 -0
  8. data/lib/cloudflock/app/common/cleanup.rb +107 -0
  9. data/lib/cloudflock/app/common/exclusions/unix/centos.rb +18 -0
  10. data/lib/cloudflock/app/common/exclusions/unix/redhat.rb +18 -0
  11. data/lib/cloudflock/app/common/exclusions/unix.rb +58 -0
  12. data/lib/cloudflock/app/common/exclusions.rb +57 -0
  13. data/lib/cloudflock/app/common/platform_action.rb +59 -0
  14. data/lib/cloudflock/app/common/rackspace.rb +63 -0
  15. data/lib/cloudflock/app/common/servers.rb +673 -0
  16. data/lib/cloudflock/app/files-migrate.rb +246 -0
  17. data/lib/cloudflock/app/server-migrate.rb +327 -0
  18. data/lib/cloudflock/app/server-profile.rb +130 -0
  19. data/lib/cloudflock/app.rb +87 -0
  20. data/lib/cloudflock/error.rb +6 -19
  21. data/lib/cloudflock/errstr.rb +31 -0
  22. data/lib/cloudflock/remote/files.rb +82 -22
  23. data/lib/cloudflock/remote/ssh.rb +234 -278
  24. data/lib/cloudflock/target/servers/platform.rb +92 -115
  25. data/lib/cloudflock/target/servers/profile.rb +331 -340
  26. data/lib/cloudflock/task/server-profile.rb +651 -0
  27. data/lib/cloudflock.rb +6 -8
  28. metadata +49 -68
  29. data/lib/cloudflock/interface/cli/app/common/servers.rb +0 -128
  30. data/lib/cloudflock/interface/cli/app/files.rb +0 -179
  31. data/lib/cloudflock/interface/cli/app/servers/migrate.rb +0 -491
  32. data/lib/cloudflock/interface/cli/app/servers/profile.rb +0 -88
  33. data/lib/cloudflock/interface/cli/app/servers.rb +0 -2
  34. data/lib/cloudflock/interface/cli/console.rb +0 -213
  35. data/lib/cloudflock/interface/cli/opts/servers.rb +0 -20
  36. data/lib/cloudflock/interface/cli/opts.rb +0 -87
  37. data/lib/cloudflock/interface/cli.rb +0 -15
  38. data/lib/cloudflock/target/servers/data/exceptions/base.txt +0 -44
  39. data/lib/cloudflock/target/servers/data/exceptions/platform/amazon.txt +0 -10
  40. data/lib/cloudflock/target/servers/data/exceptions/platform/centos.txt +0 -7
  41. data/lib/cloudflock/target/servers/data/exceptions/platform/debian.txt +0 -0
  42. data/lib/cloudflock/target/servers/data/exceptions/platform/redhat.txt +0 -7
  43. data/lib/cloudflock/target/servers/data/exceptions/platform/suse.txt +0 -1
  44. data/lib/cloudflock/target/servers/data/post-migration/chroot/base.txt +0 -1
  45. data/lib/cloudflock/target/servers/data/post-migration/chroot/platform/amazon.txt +0 -19
  46. data/lib/cloudflock/target/servers/data/post-migration/pre/base.txt +0 -3
  47. data/lib/cloudflock/target/servers/data/post-migration/pre/platform/amazon.txt +0 -4
  48. data/lib/cloudflock/target/servers/migrate.rb +0 -466
  49. data/lib/cloudflock/target/servers/platform/v1.rb +0 -97
  50. data/lib/cloudflock/target/servers/platform/v2.rb +0 -93
  51. data/lib/cloudflock/target/servers.rb +0 -5
  52. data/lib/cloudflock/version.rb +0 -3
@@ -1,311 +1,267 @@
1
1
  require 'cloudflock'
2
- require 'expectr'
2
+ require 'timeout'
3
+ require 'net/ssh'
3
4
  require 'socket'
5
+ require 'thread'
4
6
 
5
- # Public: Wrap the tasks of logging into remote hosts via ssh and interacting
6
- # with them through Expectr.
7
- #
8
- # Examples
9
- #
10
- # # Log in to remote host 'host.example.com'
11
- # shell = SSH.new(host: 'host.example.com', pass: 'examplepass')
12
- class CloudFlock::Remote::SSH
13
- # Public: Arguments to pass to ssh for when logging into remote hosts
14
- SSH_ARGUMENTS = %w{-o UserKnownHostsFile=/dev/null -o
15
- StrictHostKeyChecking=no -o NumberOfPasswordPrompts=1 -o
16
- ConnectTimeout=15 -o ServerAliveInterval=30}.join(' ')
17
-
18
- # Public: String used to standardize and identify the shell's prompt
19
- PROMPT = '@@MIGRATE@@'
20
-
21
- # Public: String representing the location of the history file
22
- HISTFILE = '/root/.migration_history'
23
-
24
- # Public: Initialize a new SSH object and automatically log in via SSH to
25
- # the host with provided address/credentials.
7
+ module CloudFlock; module Remote
8
+ # The SSH Class wraps the tasks of logging into a host and interacting with
9
+ # it via SSH.
26
10
  #
27
- # args - A Hash used to specify optional arguments (default: {}):
28
- # :host - String used to specify the address to which to
29
- # connect.
30
- # :username - String containing the remote user to use.
31
- # (default: "root")
32
- # :password - String containing the password to use for the user
33
- # specified with :user. (default: "")
34
- # :port - Fixnum specifying the port to which to connect.
35
- # (default: 22)
36
- # :flush_buffer - Boolean specifying whether or not to flush output
37
- # to STDOUT. (default: false)
38
- # :timeout - Fixnum specifying the timeout for the Expectr object
39
- # to use. (default: 30)
11
+ # Examples
40
12
  #
41
- # Raises InvalidHostname if no host is specified.
42
- # Raises InvalidHostname if looking up the host fails.
43
- # Raises LoginFailed if logging in fails.
44
- def initialize(args = {})
45
- unless args[:host].kind_of?(String) && args[:host].length > 0
46
- raise InvalidHostname, "No host specified"
13
+ # # Log into root@host.example.com
14
+ # shell = SSH.new(host: 'host.example.com', pass: 'examplepass')
15
+ # shell.puts 'ls'
16
+ class SSH
17
+ # Public: String containing arguments to pass to ssh(1)
18
+ SSH_ARGUMENTS = '-o UserKnownHostsFile=/dev/null ' \
19
+ '-o StrictHostKeyChecking=no ' \
20
+ '-o NumberOfPasswordPrompts=1 ' \
21
+ '-o ConnectTimeout=15 ' \
22
+ '-o ServerAliveInterval=30'
23
+
24
+ # Public: Prompt to be set on a host upon successful login.
25
+ PROMPT = '@@CLOUDFLOCK@@'
26
+
27
+ # Public: Absolute path to the history file to use.
28
+ HISTFILE = '/root/.cloudflock_history'
29
+
30
+ # Internal: Default arguments to be used for SSH session initialization.
31
+ DEFAULT_ARGS = {username: '', password: '', ssh_key: '', port: 22}
32
+
33
+ # Internal: Default settings for calls to #batch.
34
+ DEFAULT_BATCH_ARGS = { timeout: 30, recoverable: true }
35
+
36
+ attr_reader :options
37
+
38
+ # Public: Create a new SSH object and log in to the specified host via ssh.
39
+ #
40
+ # args - Hash containing arguments relating to the SSH session. (default
41
+ # defined in DEFAULT_ARGS):
42
+ # :hostname - String containing the address of the remote
43
+ # host.
44
+ # :username - String containing the remote user with which to
45
+ # log in. (default: '')
46
+ # :password - String containing the password with which to log
47
+ # in. (default: '')
48
+ # :port - Fixnum specifying the port to which to connect.
49
+ # (default: 22)
50
+ # :ssh_key - String containing the path to an ssh private
51
+ # key. (default: '')
52
+ # :key_passphrase - The passphrase for the ssh key if applicable.
53
+ # (default: '')
54
+ #
55
+ # Raises InvalidHostname if host lookup fails.
56
+ # Raises LoginFailed if logging into the host fails.
57
+ def initialize(args = {})
58
+ @options = sanitize_arguments(DEFAULT_ARGS.merge(args))
59
+ start_session
47
60
  end
48
61
 
49
- begin
50
- args[:host] = lookup_hostname(args[:host])
51
- rescue SocketError
52
- raise InvalidHostname, "Unable to look up host: #{args[:host]}"
62
+ # Public: Return the hostname of the host.
63
+ #
64
+ # Returns a String.
65
+ def hostname
66
+ options[:hostname]
53
67
  end
54
68
 
55
- # Set up the rest of the arguments Hash
56
- args[:username] ||= ''
57
- args[:password] ||= ''
58
- args[:flush_buffer] = false if args[:flush_buffer].nil?
59
- args[:username] ||= 'root'
60
- args[:timeout] ||= 30
61
- args[:port] ||= 22
62
- args[:public_key] ||= ''
63
-
64
- # Sanitize the arguments Hash
65
- args[:username].downcase!
66
- args[:username].gsub!(/[^a-z0-9_-]/, '')
67
- args[:port] = args[:port].to_i
68
- args[:password].tr!("\u0000-\u001f\u007f\u2028-\u2029", '')
69
-
70
- # Build the SSH command to send to the system
71
- command = "ssh #{SSH_ARGUMENTS}"
72
- if File.file?(File.expand_path(args[:public_key]))
73
- command << " -i #{File.expand_path(args[:public_key])}"
74
- end
75
- if args[:username].length > 0
76
- args[:username] << '@'
77
- end
78
- command << " #{args[:username]}#{args[:host]} -p #{args[:port]}"
79
- @expect = Expectr.new(command, flush_buffer: args[:flush_buffer],
80
- timeout: args[:timeout])
69
+ # Public: Open a channel and execute an arbitrary command, returning any
70
+ # data returned over the channel.
71
+ #
72
+ # command - Command to be executed.
73
+ # timeout - Number of seconds to allow the command to run before
74
+ # terminating the channel and returning any buffer returned
75
+ # so far. A value of 0 or nil will result in no timeout.
76
+ # (default: 30)
77
+ # recoverable - Whether a Timeout should be considered acceptable.
78
+ # (default: false)
79
+ # send_data - Array containing data to be sent to across the channel
80
+ # after the command has been run. (default: [])
81
+ #
82
+ # Returns a String.
83
+ # Raises Timeout::Error if timeout is reached.
84
+ def query(command, timeout = 30, recoverable = false, send_data = [])
85
+ buffer = ''
86
+ running = 0
87
+
88
+ channel = @ssh.open_channel do |channel|
89
+ channel.request_pty
90
+
91
+ channel.exec(command) do |ch, success|
92
+ ch.on_data { |_, data| buffer << data }
93
+ ch.on_extended_data { |_, _, data| buffer << data }
94
+
95
+ ch.send_data(send_data.join + "\n")
96
+ end
97
+ end
81
98
 
82
- raise LoginFailed unless login(args[:password])
83
- rescue Timeout::Error, Expectr::ProcessError
84
- raise LoginFailed
85
- end
99
+ Timeout::timeout(timeout) { channel.wait }
86
100
 
87
- # Public: Verify the host passed and return network address to which it's
88
- # mapped.
89
- #
90
- # host - String containing the hostname or IP to verify
91
- #
92
- # Returns a String containing an IP
93
- def lookup_hostname(host)
94
- Socket.getaddrinfo(host, nil, nil, Socket::SOCK_STREAM)[0][3]
95
- end
101
+ buffer.strip
102
+ rescue Timeout::Error
103
+ raise unless recoverable
104
+ channel.close
105
+ buffer.strip
106
+ rescue EOFError
107
+ start_session
108
+ retry
109
+ end
96
110
 
97
- # Public: Wrap authentication and provide passwords if requested. Upon
98
- # detecting successful authentication, set the shell's PS1 to PROMPT.
99
- #
100
- # password - String containing the password to provide if requested.
101
- # (default: "")
102
- #
103
- # Returns true or false indicating login success.
104
- def login(password = '')
105
- @expect.expect(/password/i, true) do |match|
106
- @expect.puts(password) if match.to_s =~ /password/i
111
+ # Public: Call query on a list of commands, allowing optional timeout and
112
+ # recoverable settings per command.
113
+ #
114
+ # commands - Array containing Hashes containing at minimum a command key.
115
+ # Hash should follow the following specification:
116
+ # command - Command to execute.
117
+ # timeout - Timeout to specify for the call to #query.
118
+ # (default: 30)
119
+ # recoverable - Whether the call should be considered
120
+ # recoverable if the timeout is reached.
121
+ # (default: true)
122
+ #
123
+ # Returns an Array containing results of each command.
124
+ def batch(commands)
125
+ commands.map! { |c| DEFAULT_BATCH_ARGS.merge(c) }
126
+ commands.map { |c| query(c[:command], c[:timeout], c[:recoverable]) }
107
127
  end
108
128
 
109
- # Wait to get a response (e.g. prompt), then set PS1
110
- count = 0
111
- continue = false
112
- until continue
113
- if count == 5
114
- @expect.kill!
115
- return false
129
+ # Public: Wrap query, guaranteeing that the user performing the given
130
+ # command is root.
131
+ #
132
+ # command - Command to be executed.
133
+ # timeout - Number of seconds to allow the command to run before
134
+ # terminating the channel and returning any buffer returned
135
+ # so far. A value of 0 or nil will result in no timeout.
136
+ # (default: 30)
137
+ # recoverable - Whether a Timeout should be considered acceptable.
138
+ # (default: false)
139
+ #
140
+ # Returns a String.
141
+ # Raises Timeout::Error if timeout is reached.
142
+ def as_root(command, timeout = 30, recoverable = false)
143
+ return query(command, timeout, recoverable) if root?
144
+
145
+ priv = 'su -'
146
+ priv = 'sudo ' + priv if options[:sudo]
147
+ uid = ['id;logout||exit']
148
+ command = ["#{command};logout||exit"]
149
+
150
+ passwordless = query(priv, timeout, true, uid)
151
+
152
+ unless /uid=0/.match(passwordless)
153
+ command.unshift(options[:root_password] + "\n")
116
154
  end
117
155
 
118
- sleep 5
119
- @expect.clear_buffer!
120
- @expect.puts
121
- continue = @expect.expect(/./, true).to_s =~ /./
122
- count += 1
156
+ buffer = query(priv, timeout, recoverable, command)
157
+ cleanup = Regexp.new("^#{Regexp::escape(command.join)}\r\n")
158
+ buffer.gsub(cleanup, '')
123
159
  end
124
- return false if @expect.pid == 0
125
- @expect.puts("export PS1='#{PROMPT} '")
126
- true
127
- end
128
-
129
- # Public: Check to see if the shell attached to the SSH object has superuser
130
- # priveleges.
131
- #
132
- # Returns false if root privileges are detected, true otherwise.
133
- def check_root
134
- @expect.clear_buffer!
135
- uid = query("UID_CHECK", command = "id")
136
- root = /uid=0\(.*$/.match(uid)
137
- root.nil?
138
- end
139
160
 
140
- # Public: Determine if currently logged in user is the superuser and, if
141
- # not, attempt to gain superuser permissions via su/sudo.
142
- #
143
- # password - String containing password to use.
144
- # use_sudo - Boolean value denoting whether to use sudo to obtain root
145
- # privileges. (default: false)
146
- #
147
- # Returns nothing.
148
- # Raises StandardError if we are unable to obtain root.
149
- def get_root(password, use_sudo = false)
150
- match = nil
151
- if check_root
152
- @expect.send("sudo ") if use_sudo
153
- @expect.puts("su -")
154
- login(password)
161
+ # Public: Terminate the active ssh session.
162
+ #
163
+ # Returns nothing.
164
+ def logout!
165
+ @ssh.close
166
+ @ssh = nil
155
167
  end
156
168
 
157
- raise RootFailed, "Unable to obtain root permissions" if check_root
169
+ private
158
170
 
159
- @expect.puts("export PS1='#{PROMPT} '")
160
- @expect.puts("export HISTFILE='#{HISTFILE}'")
161
-
162
- set_timeout(5) do
163
- while prompt(true)
164
- end
171
+ # Private: Indicates whether the logged-in user is root.
172
+ #
173
+ # Returns true if the current user is UID 0, false otherwise.
174
+ def root?
175
+ @uid || @uid = fetch_uid
165
176
  end
166
- end
167
177
 
168
- # Public: Log out of any active shells. Attempt a maximum of 10 logouts,
169
- # then kill the ssh process if the Expectr object still reports a live pid.
170
- #
171
- # Returns nothing.
172
- def logout!
173
- count = 0
174
- while @expect.pid > 0 && count < 10
175
- count += 1
176
- @expect.clear_buffer!
177
- @expect.puts
178
- prompt(true)
179
- @expect.puts("exit")
180
-
181
- sleep 1
178
+ # Private: Fetch the uid of the logged in user, return true if the uid is
179
+ # zero.
180
+ #
181
+ # Returns true if the current user is UID 0, false otherwise.
182
+ def fetch_uid
183
+ uid = query('id')
184
+ return false unless uid.gsub!(/.*uid=(\d+).*/, '\\1').to_i == 0
185
+ true
182
186
  end
183
187
 
184
- @expect.kill!
185
- rescue ArgumentError, Expectr::ProcessError
186
- # Raised if puts or kill! fails.
187
- end
188
+ # Internal: Sanitize arguments to be used
189
+ #
190
+ # args - Hash containing arguments relating to the SSH session:
191
+ # :host - String containing the address of the remote
192
+ # host.
193
+ # :username - String containing the remote user with which to
194
+ # log in. (default: '')
195
+ # :password - String containing the password with which to log
196
+ # in. (default: '')
197
+ # :port - Fixnum specifying the port to which to connect.
198
+ # (default: 22)
199
+ # :ssh_key - String containing the path to an ssh private
200
+ # key. (default: '')
201
+ # :key_passphrase - The passphrase for the ssh key if applicable.
202
+ # (default: '')
203
+ #
204
+ # Returns a Hash containing sanitized arguments suitable for passing to
205
+ # Net::SSH. Not that #filter_ssh_arguments should be called to guarantee
206
+ # inappropriate options are not passed to Net::SSH::start.
207
+ def sanitize_arguments(args)
208
+ # Resolve the host to an IP
209
+ args[:host] = lookup_hostname(args[:hostname])
210
+ args[:port] = args[:port].to_i
211
+
212
+ # Username should be lowercase, alphanumeric only.
213
+ args[:username].downcase!
214
+ args[:username].gsub!(/[^a-z0-9_-]/, '')
215
+
216
+ # Remove control characters from password
217
+ args[:password].tr!("\u0000-\u001f\u007f\u2028-\u2029", '')
218
+
219
+ # Read in ssh key data if able.
220
+ if File.file?(args[:ssh_key])
221
+ key_path = File.expand_path(args[:ssh_key].to_s)
222
+ args[:key_data] = File.read(key_path)
223
+ end
188
224
 
189
- # Public: Wait for a prompt from the SSH object.
190
- #
191
- # recoverable - Boolean specifying whether the prompt is recoverable if no
192
- # match is found. (default: false)
193
- #
194
- # Returns nothing.
195
- def prompt(recoverable=false)
196
- unless recoverable.kind_of?(TrueClass) || recoverable.kind_of?(FalseClass)
197
- raise ArgumentError, "Should specify true or false"
225
+ args
226
+ rescue Errno::EACCES
227
+ args
198
228
  end
199
229
 
200
- @expect.expect(PROMPT, recoverable)
201
- end
202
-
203
- # Public: Set the Expectr object's timeout.
204
- #
205
- # timeout - Fixnum containing the number of seconds which the Expectr object
206
- # should use as its timeout value, either temporarily or until set
207
- # explicitly again.
208
- #
209
- # Returns nothing.
210
- # Yields nothing.
211
- # Raises ArgumentError if timeout is not a Fixnum.
212
- def set_timeout(timeout)
213
- unless timeout.kind_of?(Fixnum) && timeout > 0
214
- raise ArgumentError, "Expected an integer greater than 0"
230
+ # Internal: Filter all arguments not suitable to be passed to
231
+ # Net::SSH::start.
232
+ #
233
+ # args - Hash containing arguments to filter.
234
+ #
235
+ # Returns a Hash with only the keys :port, :password, :key_data and
236
+ # :key_passphrase defined.
237
+ def filter_ssh_options(args)
238
+ valid_arguments = [:port, :password, :key_data, :key_passphrase]
239
+ args.select { |opt| valid_arguments.include? opt }
215
240
  end
216
241
 
217
- if block_given?
218
- old_timeout = @expect.timeout
219
- @expect.timeout = timeout
220
- result = yield
221
- @expect.timeout = old_timeout
222
- return result
223
- else
224
- @expect.timeout = timeout
242
+ # Internal: Resolve the hostname provided and return the network address to
243
+ # which it maps.
244
+ #
245
+ # host - String containing the hostname or IP to verify.
246
+ #
247
+ # Returns a String containing an IP.
248
+ #
249
+ # Raises InvalidHostname if address information cannot be obtained for
250
+ # the specified host.
251
+ def lookup_hostname(host)
252
+ Socket.getaddrinfo(host, nil, nil, Socket::SOCK_STREAM)[0][3]
253
+ rescue SocketError
254
+ raise(InvalidHostname, Errstr::INVALID_HOST % host)
225
255
  end
226
- end
227
-
228
- # Public: Print a tag to a new line, followed by the output of an arbitrary
229
- # command, then the tag again. Return everything between the two tags.
230
- #
231
- # tag - String containing the tag to use to isolate command output.
232
- # This string must not be empty after being constrained to
233
- # /[a-zA-Z0-9_-]/. (default: "SSH_TAG")
234
- # command - Command to send to the active ssh session.
235
- # recoverable - Whether a timeout should be considered recoverable or fatal.
236
- # (default: false)
237
- #
238
- # Returns a MatchData object from the Expectr object.
239
- # Raises ArgumentError if tag or command aren't Strings.
240
- # Raises ArgumentError if the tag String is empty.
241
- def query(tag = "SSH_TAG", command = "", recoverable = false)
242
- raise ArgumentError unless command.kind_of?(String) && tag.kind_of?(String)
243
-
244
- tag.gsub!(/[^a-zA-Z0-9_-]/, '')
245
- raise ArgumentError, "Alphanumeric tag required" if tag.empty?
246
-
247
- @expect.send('printf "' + tag + '\n";')
248
- @expect.send(command.gsub(/[\r\n]/, ' '))
249
- @expect.puts(';printf "\n' + tag + '\n";')
250
-
251
- match = @expect.expect(/^#{tag}.*^#{tag}/m, recoverable)
252
- prompt(recoverable)
253
-
254
- match.to_s.gsub(/^#{tag}(.*)^#{tag}$/m, '\1').strip
255
- end
256
-
257
- # Public: Set whether or not the Expectr object will flush the internal
258
- # buffer to STDOUT.
259
- #
260
- # flush - Boolean denoting whether to flush the buffer.
261
- #
262
- # Returns nothing.
263
- def flush_buffer(flush)
264
- @expect.flush_buffer = flush
265
- end
266
-
267
- # Public: Provide access to the Expectr object's output buffer.
268
- #
269
- # Returns a String containing the buffer.
270
- def buffer
271
- @expect.buffer
272
- end
273
-
274
- # Public: Wrap Expectr#send.
275
- #
276
- # command - String containing the data to send to the Expectr object.
277
- #
278
- # Returns nothing.
279
- def send(command)
280
- @expect.send(command)
281
- end
282
256
 
283
- # Public: Wrap Expectr#puts.
284
- #
285
- # command - String containing the data to send to the Expectr object.
286
- # (default: "")
287
- #
288
- # Returns nothing.
289
- def puts(command = "")
290
- @expect.puts(command)
291
- end
292
-
293
- # Public: Wrap Expectr#expect.
294
- #
295
- # args - Variable length list of arguments to send to Expectr#expect.
296
- #
297
- # Returns a MatchData object once a match is found if no block is given.
298
- # Yields the MatchData object representing the match.
299
- # Raises TypeError if something other than a String or Regexp is passed.
300
- # Raises Timeout::Error if a match isn't found, unless recoverable.
301
- def expect(*args)
302
- @expect.expect(*args)
303
- end
304
-
305
- # Public: Wrap Expectr#clear_buffer!
306
- #
307
- # Returns nothing.
308
- def clear
309
- @expect.clear_buffer!
257
+ # Internal: Start an SSH session.
258
+ #
259
+ # Sets @ssh.
260
+ #
261
+ # Returns nothing.
262
+ def start_session
263
+ ssh_opts = filter_ssh_options(options)
264
+ @ssh = Net::SSH.start(options[:hostname], options[:username], ssh_opts)
265
+ end
310
266
  end
311
- end
267
+ end; end