Class: BarkestSsh::SecureShell
- Inherits:
-
Object
- Object
- BarkestSsh::SecureShell
- Defined in:
- lib/barkest_ssh/secure_shell.rb
Overview
The SecureShell class is used to run an SSH session with a local or remote host.
This is a wrapper for Net::SSH that starts a shell session and executes a block. All of the output from the session is cached for later use as needed.
Constant Summary collapse
- ShellError =
An error occurring within the SecureShell class aside from argument errors.
Class.new(StandardError)
- ConnectionClosed =
An exception raised when a command requiring a connection is attempted after the connection has been closed.
Class.new(ShellError)
- FailedToRequestPTY =
An exception raised when the SSH session fails to request a PTY.
Class.new(ShellError)
- FailedToStartShell =
An exception raised when the SSH session fails to start a shell.
Class.new(ShellError)
- FailedToExecute =
An exception raised when the SSH shell session fails to execute.
Class.new(ShellError)
- LongSilence =
An exception raised when the shell session is silent too long.
Class.new(ShellError)
- NonZeroExitCode =
A command exited with a non-zero status.
Class.new(ShellError)
Instance Method Summary collapse
-
#combined_output ⇒ Object
Gets both the standard output and error output from the session.
-
#download(remote_file, local_file) ⇒ Object
Uses SFTP to download a single file from the host.
-
#exec(command, options = {}, &block) ⇒ Object
Executes a command during the shell session.
-
#exec_ignore(command, &block) ⇒ Object
Wrapper for
exec
that will ignore non-zero exit codes. -
#exec_raise(command, &block) ⇒ Object
Wrapper for
exec
that will raise an error on non-zero exit codes. -
#initialize(options = {}, &block) ⇒ SecureShell
constructor
Creates a SecureShell session and executes the provided block.
-
#last_exit_code ⇒ Object
Gets the last exit code.
-
#read_file(remote_file) ⇒ Object
Uses SFTP to read the contents of a single file.
-
#stderr ⇒ Object
Gets the error output from the session.
-
#stdout ⇒ Object
Gets the standard output from the session.
-
#sudo_exec(command, options = {}, &block) ⇒ Object
Executes a command using
sudo
during the shell session. -
#sudo_exec_ignore(command, &block) ⇒ Object
Wrapper for
sudo_exec
that will ignore non-zero exit codes. -
#sudo_exec_raise(command, &block) ⇒ Object
Wrapper for
sudo_exec
that will raise an error on non-zero exit codes. -
#upload(local_file, remote_file) ⇒ Object
Uses SFTP to upload a single file to the host.
-
#write_file(remote_file, data) ⇒ Object
Uses SFTP to write data to a single file.
Constructor Details
#initialize(options = {}, &block) ⇒ SecureShell
Creates a SecureShell session and executes the provided block.
You must provide a code block to run within the shell session, the session is closed before this returns.
Valid options:
-
host
The name or IP address of the host to connect to. Defaults to ‘localhost’. -
port
The port on the host to connect to. Defaults to 22. -
user
The user to login with. -
password
The password to login with. -
prompt
The prompt used to determine when processes finish execution. Defaults to ‘~~#’, but if that doesn’t work for some reason because it is valid output from one or more commands, you can change it to something else. It must be unique and cannot contain certain characters. The characters you should avoid are !, $, , /, “, and ‘ because no attempt is made to escape them and the resulting prompt can very easily become something else entirely. If they are provided, they will be replaced to protect the shell from getting stuck. -
silence_wait
The number of seconds to wait when the shell is not sending back data to send a newline. This can help battle background tasks burying the prompt, but it might not play nice with long-running foreground tasks. The default is 5 seconds, if you notice problems, set this to a higher value, or 0 to disable. During extended silence, the first time this value elapses, the shell will send the newline, the second time the shell will error out. -
replace_cr
The string to replace stand-alone CR characters with. The default is an empty string (ie - remove them). You may also want to replace with a LF character instead, which is the behavior taken when a CR+ LF sequence is encountered. A space followed by a standalone CR is treated differently since these seem to occur when the terminal ouput wraps. In these cases, the SPACE + CR sequence is simply removed. -
retrieve_exit_code
Version 1.1.10 introduces support for grabbing the exit code from the last command and then performing an action. The default value is true, but if you set this to false then the shell will not retrieve the exit codes automatically. -
on_non_zero_exit_code
If the exit code is non-zero, the default behavior (to remain compatible with prior versions) is to ignore the exit code. You can also set this to :raise_error to raise the NonZeroExitCode error. -
filter_password
As a convenience, if this is set to true (the default), then any text matching the configured password will be replaced with a series of asterisks in the output.
SecureShell.new(
host: '10.10.10.10',
user: 'somebody',
password: 'super-secret'
) do |shell|
shell.exec('cd /usr/local/bin')
user_bin_files = shell.exec('ls -A1').split('\n')
@app_is_installed = user_bin_files.include?('my_app')
end
97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 |
# File 'lib/barkest_ssh/secure_shell.rb', line 97 def initialize( = {}, &block) ||= {} @options = { host: [:host] || 'localhost', port: [:port] || 22, user: [:user], password: [:password], prompt: ([:prompt].to_s.strip == '') ? '~~#' : [:prompt], silence_wait: ([:silence_wait] || 5), replace_cr: [:replace_cr].to_s, retrieve_exit_code: [:retrieve_exit_code].nil? ? true : [:retrieve_exit_code], on_non_zero_exit_code: [:on_non_zero_exit_code] ? [:on_non_zero_exit_code].to_s.to_sym : :ignore, filter_password: [:filter_password].nil? ? true : [:filter_password], } raise ArgumentError.new('Missing block.') unless block_given? raise ArgumentError.new('Missing host.') if @options[:host].to_s.strip == '' raise ArgumentError.new('Missing user.') if @options[:user].to_s.strip == '' raise ArgumentError.new('Missing password.') if @options[:password].to_s.strip == '' raise ArgumentError.new('Missing prompt.') if @options[:prompt].to_s.strip == '' raise ArgumentError.new('Invalid option for on_non_zero_exit_code.') unless [:ignore, :raise_error].include?(@options[:on_non_zero_exit_code]) @options[:prompt] = @options[:prompt] .gsub('!', '#') .gsub('$', '#') .gsub('\\', '.') .gsub('/', '.') .gsub('"', '-') .gsub('\'', '-') executed = false @last_exit_code = 0 @sftp = nil Net::SSH.start( @options[:host], @options[:user], password: @options[:password], port: @options[:port], non_interactive: true, ) do |ssh| @ssh = ssh ssh.open_channel do |ssh_channel| ssh_channel.request_pty do |pty_channel, pty_success| raise FailedToRequestPTY.new('Failed to request PTY.') unless pty_success pty_channel.send_channel_request('shell') do |_, shell_success| raise FailedToStartShell.new('Failed to start shell.') unless shell_success # cache the channel pointer and start buffering the input. @channel = pty_channel buffer_input # give the shell a chance to catch up and initialize fully. sleep 0.25 # set the shell prompt so that we can determine when processes end. # does not work with background processes since we are looking for # the shell to send us this when it is ready for more input. # a background process can easily bury the prompt and then we are stuck in a loop. exec "PS1=\"#{@options[:prompt]}\"" block.call(self) executed = true # send the exit command and remove the channel pointer. quit @channel = nil end end ssh_channel.wait end end @ssh = nil if @sftp @sftp.session.close @sftp = nil end # remove the cached user and password. .delete(:user) .delete(:password) raise FailedToExecute.new('Failed to execute shell.') unless executed end |
Instance Method Details
#combined_output ⇒ Object
Gets both the standard output and error output from the session.
The prompts will be included in the combined output. There is no attempt to differentiate error output from standard output.
This is essentially the definitive log for the session.
All line endings are converted to LF characters, so you will not encounter or need to search for CRLF or CR sequences.
386 387 388 |
# File 'lib/barkest_ssh/secure_shell.rb', line 386 def combined_output @stdcomb || '' end |
#download(remote_file, local_file) ⇒ Object
Uses SFTP to download a single file from the host.
329 330 331 332 |
# File 'lib/barkest_ssh/secure_shell.rb', line 329 def download(remote_file, local_file) raise ConnectionClosed.new('Connection is closed.') unless @ssh sftp.download!(remote_file, local_file) end |
#exec(command, options = {}, &block) ⇒ Object
Executes a command during the shell session.
If called outside of the new
block, this will raise an error.
The command
is the command to execute in the shell.
The options
parameter can include the following keys.
-
The :on_non_zero_exit_code option can be :default, :ignore, or :raise_error.
If provided, the block
is a chunk of code that will be processed every time the shell receives output from the program. If the block returns a string, the string will be sent to the shell. This can be used to monitor processes or monitor and interact with processes. The block
is optional.
shell.exec('sudo -p "password:" nginx restart') do |data,type|
return 'super-secret' if /password:$/.match(data)
nil
end
224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 |
# File 'lib/barkest_ssh/secure_shell.rb', line 224 def exec(command, ={}, &block) raise ConnectionClosed.new('Connection is closed.') unless @channel = { on_non_zero_exit_code: :default }.merge( || {}) [:on_non_zero_exit_code] = @options[:on_non_zero_exit_code] if [:on_non_zero_exit_code] == :default push_buffer # store the current buffer and start a fresh buffer # buffer while also passing data to the supplied block. if block_given? buffer_input( &block ) end # send the command and wait for the prompt to return. @channel.send_data command + "\n" wait_for_prompt # return buffering to normal. if block_given? buffer_input end # get the output from the command, minus the trailing prompt. ret = command_output(command) # restore the original buffer and merge the output from the command. pop_merge_buffer if @options[:retrieve_exit_code] # get the exit code for the command. push_buffer retrieve_command = 'echo $?' @channel.send_data retrieve_command + "\n" wait_for_prompt @last_exit_code = command_output(retrieve_command).strip.to_i # restore the original buffer and discard the output from this command. pop_discard_buffer # if we are expected to raise an error, do so. if [:on_non_zero_exit_code] == :raise_error raise NonZeroExitCode.new("Exit code was #{@last_exit_code}.") unless @last_exit_code == 0 end end ret end |
#exec_ignore(command, &block) ⇒ Object
Wrapper for exec
that will ignore non-zero exit codes.
194 195 196 |
# File 'lib/barkest_ssh/secure_shell.rb', line 194 def exec_ignore(command, &block) exec command, on_non_zero_exit_code: :ignore, &block end |
#exec_raise(command, &block) ⇒ Object
Wrapper for exec
that will raise an error on non-zero exit codes.
200 201 202 |
# File 'lib/barkest_ssh/secure_shell.rb', line 200 def exec_raise(command, &block) exec command, on_non_zero_exit_code: :raise_error, &block end |
#last_exit_code ⇒ Object
Gets the last exit code.
188 189 190 |
# File 'lib/barkest_ssh/secure_shell.rb', line 188 def last_exit_code @last_exit_code || 0 end |
#read_file(remote_file) ⇒ Object
Uses SFTP to read the contents of a single file.
Returns the contents of the file.
338 339 340 341 |
# File 'lib/barkest_ssh/secure_shell.rb', line 338 def read_file(remote_file) raise ConnectionClosed.new('Connection is closed.') unless @ssh sftp.download!(remote_file) end |
#stderr ⇒ Object
Gets the error output from the session.
All line endings are converted to LF characters, so you will not encounter or need to search for CRLF or CR sequences.
371 372 373 |
# File 'lib/barkest_ssh/secure_shell.rb', line 371 def stderr @stderr || '' end |
#stdout ⇒ Object
Gets the standard output from the session.
The prompts are stripped from the standard ouput as they are encountered. So this will be a list of commands with their output.
All line endings are converted to LF characters, so you will not encounter or need to search for CRLF or CR sequences.
361 362 363 |
# File 'lib/barkest_ssh/secure_shell.rb', line 361 def stdout @stdout || '' end |
#sudo_exec(command, options = {}, &block) ⇒ Object
Executes a command using sudo
during the shell session.
This is a wrapper around exec
that attempts to run the command as root. It provides the configured user’s password if/when prompted.
See exec
for more information.
293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 |
# File 'lib/barkest_ssh/secure_shell.rb', line 293 def sudo_exec(command, = {}, &block) sudo_prompt = '[sp:' sudo_match = /(\r|\n)\[sp\:$/ sudo_strip = /\[sp\:\n/ ret = exec("sudo -p \"#{sudo_prompt}\" bash -c \"#{command.gsub('"', '\\"')}\"", ) do |data,type| test_data = data.to_s desired_length = sudo_prompt.length + 1 # prefix a NL before the prompt. # pull from the current stdout to get the full test data, but only if we received some new data. if test_data.length > 0 && test_data.length < desired_length test_data = stdout[-desired_length..-1].to_s end if sudo_match.match(test_data) @options[:password] else if block block.call(data, type) else nil end end end # remove the sudo prompts. ret.gsub(sudo_strip, '') end |
#sudo_exec_ignore(command, &block) ⇒ Object
Wrapper for sudo_exec
that will ignore non-zero exit codes.
276 277 278 |
# File 'lib/barkest_ssh/secure_shell.rb', line 276 def sudo_exec_ignore(command, &block) sudo_exec command, on_non_zero_exit_code: :ignore, &block end |
#sudo_exec_raise(command, &block) ⇒ Object
Wrapper for sudo_exec
that will raise an error on non-zero exit codes.
282 283 284 |
# File 'lib/barkest_ssh/secure_shell.rb', line 282 def sudo_exec_raise(command, &block) sudo_exec command, on_non_zero_exit_code: :raise_error, &block end |
#upload(local_file, remote_file) ⇒ Object
Uses SFTP to upload a single file to the host.
322 323 324 325 |
# File 'lib/barkest_ssh/secure_shell.rb', line 322 def upload(local_file, remote_file) raise ConnectionClosed.new('Connection is closed.') unless @ssh sftp.upload!(local_file, remote_file) end |
#write_file(remote_file, data) ⇒ Object
Uses SFTP to write data to a single file.
345 346 347 348 349 350 |
# File 'lib/barkest_ssh/secure_shell.rb', line 345 def write_file(remote_file, data) raise ConnectionClosed.new('Connection is closed.') unless @ssh sftp.file.open(remote_file, 'w') do |f| f.write data end end |