Class: CloudFlock::Remote::SSH
- Inherits:
-
Object
- Object
- CloudFlock::Remote::SSH
- Defined in:
- lib/cloudflock/remote/ssh.rb,
lib/cloudflock/error.rb
Overview
Public: Wrap the tasks of logging into remote hosts via ssh and interacting with them through Expectr.
Examples
# Log in to remote host 'host.example.com'
shell = SSH.new(host: 'host.example.com', pass: 'examplepass')
Defined Under Namespace
Classes: ConnectionFailed, InvalidHostname, LoginFailed, RootFailed
Constant Summary collapse
- SSH_ARGUMENTS =
Public: Arguments to pass to ssh for when logging into remote hosts
%w{-o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no -o NumberOfPasswordPrompts=1 -o ConnectTimeout=15 -o ServerAliveInterval=30}.join(' ')
- PROMPT =
Public: String used to standardize and identify the shell’s prompt
'@@MIGRATE@@'
- HISTFILE =
Public: String representing the location of the history file
'/root/.migration_history'
Instance Method Summary collapse
-
#buffer ⇒ Object
Public: Provide access to the Expectr object’s output buffer.
-
#check_root ⇒ Object
Public: Check to see if the shell attached to the SSH object has superuser priveleges.
-
#clear ⇒ Object
Public: Wrap Expectr#clear_buffer!.
-
#expect(*args) ⇒ Object
Public: Wrap Expectr#expect.
-
#flush_buffer(flush) ⇒ Object
Public: Set whether or not the Expectr object will flush the internal buffer to STDOUT.
-
#get_root(password, use_sudo = false) ⇒ Object
Public: Determine if currently logged in user is the superuser and, if not, attempt to gain superuser permissions via su/sudo.
-
#initialize(args = {}) ⇒ SSH
constructor
Public: Initialize a new SSH object and automatically log in via SSH to the host with provided address/credentials.
-
#login(password = '') ⇒ Object
Public: Wrap authentication and provide passwords if requested.
-
#logout! ⇒ Object
Public: Log out of any active shells.
-
#lookup_hostname(host) ⇒ Object
Public: Verify the host passed and return network address to which it’s mapped.
-
#prompt(recoverable = false) ⇒ Object
Public: Wait for a prompt from the SSH object.
-
#puts(command = "") ⇒ Object
Public: Wrap Expectr#puts.
-
#query(tag = "SSH_TAG", command = "", recoverable = false) ⇒ Object
Public: Print a tag to a new line, followed by the output of an arbitrary command, then the tag again.
-
#send(command) ⇒ Object
Public: Wrap Expectr#send.
-
#set_timeout(timeout) ⇒ Object
Public: Set the Expectr object’s timeout.
Constructor Details
#initialize(args = {}) ⇒ SSH
Public: Initialize a new SSH object and automatically log in via SSH to the host with provided address/credentials.
args - A Hash used to specify optional arguments (default: {}):
:host - String used to specify the address to which to
connect.
:username - String containing the remote user to use.
(default: "root")
:password - String containing the password to use for the user
specified with :user. (default: "")
:port - Fixnum specifying the port to which to connect.
(default: 22)
:flush_buffer - Boolean specifying whether or not to flush output
to STDOUT. (default: false)
:timeout - Fixnum specifying the timeout for the Expectr object
to use. (default: 30)
Raises InvalidHostname if no host is specified. Raises InvalidHostname if looking up the host fails. Raises LoginFailed if logging in fails.
44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 |
# File 'lib/cloudflock/remote/ssh.rb', line 44 def initialize(args = {}) unless args[:host].kind_of?(String) && args[:host].length > 0 raise InvalidHostname, "No host specified" end begin args[:host] = lookup_hostname(args[:host]) rescue SocketError raise InvalidHostname, "Unable to look up host: #{args[:host]}" end # Set up the rest of the arguments Hash args[:username] ||= '' args[:password] ||= '' args[:flush_buffer] = false if args[:flush_buffer].nil? args[:username] ||= 'root' args[:timeout] ||= 30 args[:port] ||= 22 args[:public_key] ||= '' # Sanitize the arguments Hash args[:username].downcase! args[:username].gsub!(/[^a-z0-9_-]/, '') args[:port] = args[:port].to_i args[:password].tr!("\u0000-\u001f\u007f\u2028-\u2029", '') # Build the SSH command to send to the system command = "ssh #{SSH_ARGUMENTS}" if File.file?(File.(args[:public_key])) command << " -i #{File.(args[:public_key])}" end if args[:username].length > 0 args[:username] << '@' end command << " #{args[:username]}#{args[:host]} -p #{args[:port]}" @expect = Expectr.new(command, flush_buffer: args[:flush_buffer], timeout: args[:timeout]) raise LoginFailed unless login(args[:password]) rescue Timeout::Error, Expectr::ProcessError raise LoginFailed end |
Instance Method Details
#buffer ⇒ Object
Public: Provide access to the Expectr object’s output buffer.
Returns a String containing the buffer.
270 271 272 |
# File 'lib/cloudflock/remote/ssh.rb', line 270 def buffer @expect.buffer end |
#check_root ⇒ Object
Public: Check to see if the shell attached to the SSH object has superuser priveleges.
Returns false if root privileges are detected, true otherwise.
133 134 135 136 137 138 |
# File 'lib/cloudflock/remote/ssh.rb', line 133 def check_root @expect.clear_buffer! uid = query("UID_CHECK", command = "id") root = /uid=0\(.*$/.match(uid) root.nil? end |
#clear ⇒ Object
Public: Wrap Expectr#clear_buffer!
Returns nothing.
308 309 310 |
# File 'lib/cloudflock/remote/ssh.rb', line 308 def clear @expect.clear_buffer! end |
#expect(*args) ⇒ Object
Public: Wrap Expectr#expect.
args - Variable length list of arguments to send to Expectr#expect.
Returns a MatchData object once a match is found if no block is given. Yields the MatchData object representing the match. Raises TypeError if something other than a String or Regexp is passed. Raises Timeout::Error if a match isn’t found, unless recoverable.
301 302 303 |
# File 'lib/cloudflock/remote/ssh.rb', line 301 def expect(*args) @expect.expect(*args) end |
#flush_buffer(flush) ⇒ Object
Public: Set whether or not the Expectr object will flush the internal buffer to STDOUT.
flush - Boolean denoting whether to flush the buffer.
Returns nothing.
263 264 265 |
# File 'lib/cloudflock/remote/ssh.rb', line 263 def flush_buffer(flush) @expect.flush_buffer = flush end |
#get_root(password, use_sudo = false) ⇒ Object
Public: Determine if currently logged in user is the superuser and, if not, attempt to gain superuser permissions via su/sudo.
password - String containing password to use. use_sudo - Boolean value denoting whether to use sudo to obtain root
privileges. (default: false)
Returns nothing. Raises StandardError if we are unable to obtain root.
149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 |
# File 'lib/cloudflock/remote/ssh.rb', line 149 def get_root(password, use_sudo = false) match = nil if check_root @expect.send("sudo ") if use_sudo @expect.puts("su -") login(password) end raise RootFailed, "Unable to obtain root permissions" if check_root @expect.puts("export PS1='#{PROMPT} '") @expect.puts("export HISTFILE='#{HISTFILE}'") set_timeout(5) do while prompt(true) end end end |
#login(password = '') ⇒ Object
Public: Wrap authentication and provide passwords if requested. Upon detecting successful authentication, set the shell’s PS1 to PROMPT.
password - String containing the password to provide if requested.
(default: "")
Returns true or false indicating login success.
104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 |
# File 'lib/cloudflock/remote/ssh.rb', line 104 def login(password = '') @expect.expect(/password/i, true) do |match| @expect.puts(password) if match.to_s =~ /password/i end # Wait to get a response (e.g. prompt), then set PS1 count = 0 continue = false until continue if count == 5 @expect.kill! return false end sleep 5 @expect.clear_buffer! @expect.puts continue = @expect.expect(/./, true).to_s =~ /./ count += 1 end return false if @expect.pid == 0 @expect.puts("export PS1='#{PROMPT} '") true end |
#logout! ⇒ Object
Public: Log out of any active shells. Attempt a maximum of 10 logouts, then kill the ssh process if the Expectr object still reports a live pid.
Returns nothing.
172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 |
# File 'lib/cloudflock/remote/ssh.rb', line 172 def logout! count = 0 while @expect.pid > 0 && count < 10 count += 1 @expect.clear_buffer! @expect.puts prompt(true) @expect.puts("exit") sleep 1 end @expect.kill! rescue ArgumentError, Expectr::ProcessError # Raised if puts or kill! fails. end |
#lookup_hostname(host) ⇒ Object
Public: Verify the host passed and return network address to which it’s mapped.
host - String containing the hostname or IP to verify
Returns a String containing an IP
93 94 95 |
# File 'lib/cloudflock/remote/ssh.rb', line 93 def lookup_hostname(host) Socket.getaddrinfo(host, nil, nil, Socket::SOCK_STREAM)[0][3] end |
#prompt(recoverable = false) ⇒ Object
Public: Wait for a prompt from the SSH object.
recoverable - Boolean specifying whether the prompt is recoverable if no
match is found. (default: false)
Returns nothing.
195 196 197 198 199 200 201 |
# File 'lib/cloudflock/remote/ssh.rb', line 195 def prompt(recoverable=false) unless recoverable.kind_of?(TrueClass) || recoverable.kind_of?(FalseClass) raise ArgumentError, "Should specify true or false" end @expect.expect(PROMPT, recoverable) end |
#puts(command = "") ⇒ Object
Public: Wrap Expectr#puts.
command - String containing the data to send to the Expectr object.
(default: "")
Returns nothing.
289 290 291 |
# File 'lib/cloudflock/remote/ssh.rb', line 289 def puts(command = "") @expect.puts(command) end |
#query(tag = "SSH_TAG", command = "", recoverable = false) ⇒ Object
Public: Print a tag to a new line, followed by the output of an arbitrary command, then the tag again. Return everything between the two tags.
tag - String containing the tag to use to isolate command output.
This string must not be empty after being constrained to
/[a-zA-Z0-9_-]/. (default: "SSH_TAG")
command - Command to send to the active ssh session. recoverable - Whether a timeout should be considered recoverable or fatal.
(default: false)
Returns a MatchData object from the Expectr object. Raises ArgumentError if tag or command aren’t Strings. Raises ArgumentError if the tag String is empty.
241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 |
# File 'lib/cloudflock/remote/ssh.rb', line 241 def query(tag = "SSH_TAG", command = "", recoverable = false) raise ArgumentError unless command.kind_of?(String) && tag.kind_of?(String) tag.gsub!(/[^a-zA-Z0-9_-]/, '') raise ArgumentError, "Alphanumeric tag required" if tag.empty? @expect.send('printf "' + tag + '\n";') @expect.send(command.gsub(/[\r\n]/, ' ')) @expect.puts(';printf "\n' + tag + '\n";') match = @expect.expect(/^#{tag}.*^#{tag}/m, recoverable) prompt(recoverable) match.to_s.gsub(/^#{tag}(.*)^#{tag}$/m, '\1').strip end |
#send(command) ⇒ Object
Public: Wrap Expectr#send.
command - String containing the data to send to the Expectr object.
Returns nothing.
279 280 281 |
# File 'lib/cloudflock/remote/ssh.rb', line 279 def send(command) @expect.send(command) end |
#set_timeout(timeout) ⇒ Object
Public: Set the Expectr object’s timeout.
timeout - Fixnum containing the number of seconds which the Expectr object
should use as its timeout value, either temporarily or until set
explicitly again.
Returns nothing. Yields nothing. Raises ArgumentError if timeout is not a Fixnum.
212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 |
# File 'lib/cloudflock/remote/ssh.rb', line 212 def set_timeout(timeout) unless timeout.kind_of?(Fixnum) && timeout > 0 raise ArgumentError, "Expected an integer greater than 0" end if block_given? old_timeout = @expect.timeout @expect.timeout = timeout result = yield @expect.timeout = old_timeout return result else @expect.timeout = timeout end end |