How to get exit status with Ruby's Net::SSH library? - ruby

I have a snippet of code, simply trying to execute a script on a remote server, in the event that it fails, I'd like to make a follow-up call, imagine this:
require 'rubygems'
require 'net/ssh'
require 'etc'
server = 'localhost'
Net::SSH.start(server, Etc.getlogin) do |ssh|
puts (ssh.exec("true") ? 'Exit Success' : "Exit Failure")
puts (ssh.exec("false") ? 'Exit Success' : "Exit Failure")
end
I would expect (ignoring that stdout and stderr are printed in my contrived example) - but first line should exit with 0 which I would expect Ruby would interperate as false and display "Exit Failure" (sure, so the logic is wrong, the ternary needs to be flipped) - but the second line should exit with the opposite status, and it doesn't.
I can't even find anything in the documentation about how to do this, and I'm a little worried that I might be doing it wrong?!

I find the following way of running processes with Net::SSH much more useful. It provides you with distinct stdout and stderr, exit code and exit signal.
require 'rubygems'
require 'net/ssh'
require 'etc'
server = 'localhost'
def ssh_exec!(ssh, command)
stdout_data = ""
stderr_data = ""
exit_code = nil
exit_signal = nil
ssh.open_channel do |channel|
channel.exec(command) do |ch, success|
unless success
abort "FAILED: couldn't execute command (ssh.channel.exec)"
end
channel.on_data do |ch,data|
stdout_data+=data
end
channel.on_extended_data do |ch,type,data|
stderr_data+=data
end
channel.on_request("exit-status") do |ch,data|
exit_code = data.read_long
end
channel.on_request("exit-signal") do |ch, data|
exit_signal = data.read_long
end
end
end
ssh.loop
[stdout_data, stderr_data, exit_code, exit_signal]
end
Net::SSH.start(server, Etc.getlogin) do |ssh|
puts ssh_exec!(ssh, "true").inspect
# => ["", "", 0, nil]
puts ssh_exec!(ssh, "false").inspect
# => ["", "", 1, nil]
end
Hope this helps.

Building on the answer by flitzwald - I've monkey patched my version of this into Net::SSH (Ruby 1.9+)
class Net::SSH::Connection::Session
class CommandFailed < StandardError
end
class CommandExecutionFailed < StandardError
end
def exec_sc!(command)
stdout_data,stderr_data = "",""
exit_code,exit_signal = nil,nil
self.open_channel do |channel|
channel.exec(command) do |_, success|
raise CommandExecutionFailed, "Command \"#{command}\" was unable to execute" unless success
channel.on_data do |_,data|
stdout_data += data
end
channel.on_extended_data do |_,_,data|
stderr_data += data
end
channel.on_request("exit-status") do |_,data|
exit_code = data.read_long
end
channel.on_request("exit-signal") do |_, data|
exit_signal = data.read_long
end
end
end
self.loop
raise CommandFailed, "Command \"#{command}\" returned exit code #{exit_code}" unless exit_code == 0
{
stdout:stdout_data,
stderr:stderr_data,
exit_code:exit_code,
exit_signal:exit_signal
}
end
end

For newer versions of Net::SSH, you can just pass a status hash to Net::SSH::Connection::Session#exec:
status = {}
Net::SSH.start(hostname, user, options) do |ssh|
channel = ssh.exec(command, status: status)
channel.wait # wait for the command to actually be executed
end
puts status.inspect
# {:exit_code=>0}
By default, exec streams its output to $stdout and $stderr. You can pass a block to exec to do something different, a la:
ssh.exec(command, status: status) do |ch, stream, data|
if stream == :stdout
do_something_with_stdout(data)
else
do_something_with_stderr(data)
end
end
This works on 6.1.0 - not sure about availability for older versions. See http://net-ssh.github.io/net-ssh/Net/SSH/Connection/Session.html#method-i-exec for more details.

Related

Declaring Ruby Methods along with a menu

I've been fooling around with Ruby lately, so i decided to write an sftp client.
require 'net/sftp'
require 'ostruct'
require 'optparse'
class Sftp
def parse(arguments)
ARGV << "-h" if ARGV.empty?
#options = OpenStruct.new
args = OptionParser.new do |args|
args.banner = "Usage: #{__FILE__} [options]"
args.on("--host HOST", String,
) do |host|
#options.host = host
end
args.on("--username USERNAME", String,
) do |username|
#options.username = username
end
args.on("--password PASSWORD", String,
) do |password|
#options.password = password
end
args.on("--port=PORT", Integer,
) do |port|
#options.port = port
end
args.on("--mkdir=MAKE DIRECTORY", String,
) do |mkdir|
#options.mkdir = mkdir
end
args.on("--rmdir=REMOVE DIRECTORY", String,
) do |rmdir|
#options.rmdir = rmdir
end
args.on("-h", "--help", "Show help and exit") do
puts args
exit
end
end
begin
args.parse!(arguments)
rescue OptionParser::MissingArgument => error
puts "[!] ".red + error.message.bold
exit
rescue OptionParser::InvalidOption => error
puts "[!] ".red + error.message.bold
exit
end
def connect
Net::SFTP.start(#options.host, #options.username, :password => #options.password, :port => #options.port) do |sftp|
sftp.mkdir(#options.mkdir)
puts "Creating Directory: #{#options.mkdir}"
sftp.rmdir(#options.rmdir)
puts "Deleting Directory: #{#options.rmdir}"
end
end
end
def run(arguments)
parse(arguments)
connect
end
end
sftp = Sftp.new
sftp.run(ARGV)
I want these two commands to be separated. For example when i pass
the argument mkdir I just want only this to run and if I want to run rmdir again I just wanna run only this command.
It has to do with methods, but I can't find a proper solution. And I'm really rusty.
Any recommendation?
A very simple approach could be to check if the required value is set before running the command, and skip the command if the value is not set.
def connect
Net::SFTP.start(#options.host, #options.username, password: #options.password, port: #options.port) do |sftp|
if #options.mkdir
sftp.mkdir(#options.mkdir)
puts "Creating Directory: #{#options.mkdir}"
end
if #options.rmdir
sftp.rmdir(#options.rmdir)
puts "Deleting Directory: #{#options.rmdir}"
end
end
end

How do we test that ruby's Timeout.timeout using RSpec?

Example Snippet:
require "tty-prompt"
require "timeout"
def prompt_with_timeout
prompt = TTY::Prompt.new
timeout_duration = 60
response = ""
Timeout.timeout(timeout_duration, Timeout::Error) do
response = prompt.yes?("Do you agree?")
rescue Timeout::Error => e
puts "Prompt timed out!"
exit 1
end
if response == "yes" || response == "y"
# do something
else
# do something else
end
end
How would we test the above functionality using RSpec?
I am trying to test the above Timeout functionality using RSpec. I want to write a RSpec test that captures the message from the rescue block.
Use test doubles. RSpec also provides matcher for testing output to stdout declaratively, so smth. like the following should suffice hopefully:
let(:prompt) { instance_double("TTY::Prompt") }
before do
allow(TTY::Prompt).to receive(:new).and_return(prompt)
allow(prompt).to receive(:yes?).and_raise(Timeout::Error)
end
specify do
expect { prompt_with_timeout }.to output("Prompt timed out!").to_stdout
end
First of all, I don't really know what you're trying to do and why you would want to test it. But I've tried to come up with a solution for your example.
I would put the duration in a constant and stub it with rspec so you don't have to wait 60 seconds to run a simple test.
It would look something like this:
require "tty-prompt"
require "timeout"
require 'rspec'
TIMEOUT_DURATION = 60
def prompt_with_timeout
prompt = TTY::Prompt.new
response = ""
Timeout.timeout(TIMEOUT_DURATION, Timeout::Error) do
response = prompt.yes?("Do you agree?")
rescue Timeout::Error => e
puts "Prompt timed out!"
exit 1
end
if response == "yes" || response == "y"
# do something
else
# do something else
end
end
describe "Prompt with timeout" do
it "returns the correct message on timeout" do
stub_const("TIMEOUT_DURATION", 0.1)
expect(prompt_with_timeout).to eq("Prompt timed out!")
end
end
RSpec::Core::Runner.run([$__FILE__])

ruby net::ssh does not print stdout data on channel.on_data

I wanted to run a remote command with ruby's net::ssh and was hoping to print the output stdout but i don't see anything printed at the below code at channel.on_data
see test code:
Net::SSH.start('testhost', "root", :timeout => 10) do |ssh|
ssh.open_channel do |channel|
channel.exec('ls') do |_, success|
unless success
puts "NOT SUCCEESS:! "
end
channel.on_data do |_, data|
puts "DATAAAAAA:! " # ======> Why am i not getting to here?? <=======
end
channel.on_extended_data do |_, _, data|
puts "EXTENDED!!!"
end
channel.on_request("exit-status") do |_, data|
puts "EXIT!!!! "
end
channel.on_close do |ch|
puts "channel is closing!"
end
end
end
end
and the output is:
channel is closing!
why don't i get into the block on_data? I want to grab the stdout.
note that i know the client code is able to ssh to the remote server because when I asked the command to be ls > ls.log I saw that ls.log on target host.
Note that opening a channel is asynchronous, so you have to wait for the channel to do anything meaningful, otherwise you are closing the connection too soon.
Try this:
Net::SSH.start('test', "root", :timeout => 10) do |ssh|
ch = ssh.open_channel do |channel|
channel.exec('ls') do |_, success|
unless success
puts "Error"
end
channel.on_data do |_, data|
puts data
end
channel.on_extended_data do |_, _, data|
puts data
end
channel.on_request("exit-status") do |_, data|
puts "Exit"
end
channel.on_close do |ch|
puts "Closing!"
end
end
end
ch.wait
end

Getting output from Ruby Net::SSH sessions

I've got a larger script that basically uses the same type of code below. The script runs and functions but I don't get output to the screen. How do I get the output of the script that is being executed remotely to show up on the screen I'm running the ruby script from?
#!/usr/bin/ruby
#
require 'rubygems'
require 'net/ssh'
require 'pty'
if ENV['USER'] == 'root'
raise "You can't run this as root"
end
Net::SSH.start(server01, testuser) do |ssh|
ssh.open_channel do |channel|
channel.on_request "exit-status" do |channel, data|
$exit_status = data.read_long
end
channel.on_data do |channel, data|
data
end
channel.request_pty do |channel, data|
channel.exec("sudo -s")
channel.send_data("/tmp/scripts/test.sh\n")
channel.send_data("exit\n")
end
end
end
puts "DONE"
change
channel.on_data do |channel, data|
data
end
to
channel.on_data do |channel, data|
puts data
end
the data mentioned is the response from the server
and add
channel.send_channel_request 'shell' do |ch, success|
if success
puts 'user shell started successfully'
else
puts 'could not start user shell'
end
end

ruby shoes ssh connection

I am trying my hands on shoes but got stuck. I am trying to connect to a remote computer using ssh and issue a command, got it working in cli but it is a no go for me to get it working on shoes.
This might be a simple thing but new as I am I can't get past it.
Here is what my code looks like atm
Shoes.setup do
gem 'net-ssh'
end
require "rubygems"
require "net/ssh"
Shoes.app do
button "Connect" do
append Net::SSH.start( '192.168.100.127', 'fox', :password => "xxxxxx" ) do
|ssh_connection|
ssh_connection.open_channel do |channel|
channel.on_data do |ch, data|
puts data
channel.exec "ls -la" do |ch, success|
para success
if success then
alert "uploaded"
else
alert "Fail"
end
end
end
end
end
end
end
Your code is trying to receive data first which is not the case usually. Remove the on_data:
Shoes.app do
button "Connect" do
append Net::SSH.start( '192.168.100.127', 'fox', :password => "xxxxxx" ) do |ssh_connection|
ssh_connection.open_channel do |channel|
channel.exec "ls -la" do |ch, success|
para success
if success then
alert "uploaded"
else
alert "Fail"
end
end
end
end
end
end

Resources