Interactive Ruby script - ruby

I'm trying to write a ruby script that logs into a remote server, switches to another user, executes a script and answers questions to that script. Right now, I can log in but it hangs on the execution of the bash script. I'm not sure if I got the prompt part right but it's not getting to that point yet. It "hangs" on the running of the script or it just isn't printing the output of the script to the screen.
Here's what I got for now:
require 'rubygems'
require 'net/ssh'
require 'net/ssh/telnet'
s = Net::SSH::Telnet.new("Host" => "server1", "Username" => "dev", "Password" => "12345", "Prompt" => /[$%#>] \z/n)
puts s.cmd("sudo -s")
puts s.cmd("su - user1")
puts s.cmd("/opt/develop/develop-bin/start.sh")
puts s.waitfor(/Prompt/)

Here is the snippet that works for me. It is taken from the bigger library, so treat it as an example. Fill up the variables command, username, password, servername and sudoer and give a try
class AuthenticationError < StandardError; end
AUTH_METHODS = ['hostbased', 'password', 'keyboard-interactive']
MYPROMPT = SecureRandom.hex # or whatever you want
ret = ""
stderr_data = ""
Net::SSH.start(servername, username, :password => password, :auth_methods => AUTH_METHODS) do |ssh|
ssh.open_channel do |channel|
channel.on_data do |channel, data|
ret += data
raise AuthenticationError, "SUDO ACCESS DENIED" if data.inspect.include?('Sorry, try again.') || data.inspect.include?('not in sudoers')
channel.send_data(password+"\n") if data.inspect.include? MYPROMPT
sleep 0.1
end
channel.on_extended_data do |ch, type, data|
stderr_data+=data
end
channel.request_pty
channel.exec("sudo -p #{MYPROMPT} su - #{sudoer} -c '#{command}'")
channel.wait
end
end
puts ret.gsub(/^"/,'').gsub(/"$/,'')

Related

How do I concatenate a method call?

I am calling a method so:
Net::SSH.start( value, USER, :password => PASS, :keys => ["/keys/id_rsa"] ) do|ssh|
mess = ssh.sudo "password", "apt-get upgrade"
end
The method is:
class Net::SSH::Connection::Session
def sudo password, command
exec %Q%echo "#{password}" | sudo -S #{command}% do |channel, stream, data|
stdout << data if stream == :stdout
end
return stdout
end
end
Later, I send an email with the mess variable and this works fine.
However, if I want to run through an array of hosts and try to append mess to a string like results << mess, all I get is an empty variable. I don't know why this is.

How to properly implement Net::SSH port forwards

I have been trying to get port forwarding to work correctly with Net::SSH. From what I understand I need to fork out the Net::SSH session if I want to be able to use it from the same Ruby program so that the event handling loop can actually process packets being sent through the connection. However, this results in the ugliness you can see in the following:
#!/usr/bin/env ruby -w
require 'net/ssh'
require 'httparty'
require 'socket'
include Process
log = Logger.new(STDOUT)
log.level = Logger::DEBUG
local_port = 2006
child_socket, parent_socket = Socket.pair(:UNIX, :DGRAM, 0)
maxlen = 1000
hostname = "www.example.com"
pid = fork do
parent_socket.close
Net::SSH.start("hostname", "username") do |session|
session.logger = log
session.logger.sev_threshold=Logger::Severity::DEBUG
session.forward.local(local_port, hostname, 80)
child_socket.send("ready", 0)
pidi = fork do
msg = child_socket.recv(maxlen)
puts "Message from parent was: #{msg}"
exit
end
session.loop do
status = waitpid(pidi, Process::WNOHANG)
puts "Status: #{status.inspect}"
status.nil?
end
end
end
child_socket.close
puts "Message from child: #{parent_socket.recv(maxlen)}"
resp = HTTParty.post("http://localhost:#{local_port}/", :headers => { "Host" => hostname } )
# the write cannot be the last statement, otherwise the child pid could end up
# not receiving it
parent_socket.write("done")
puts resp.inspect
Can anybody show me a more elegant/better working solution to this?
I spend a lot of time trying to figure out how to correctly implement port forwarding, then I took inspiration from net/ssh/gateway library. I needed a robust solution that works after various possible connection errors. This is what I'm using now, hope it helps:
require 'net/ssh'
ssh_options = ['host', 'login', :password => 'password']
tunnel_port = 2222
begin
run_tunnel_thread = true
tunnel_mutex = Mutex.new
ssh = Net::SSH.start *ssh_options
tunnel_thread = Thread.new do
begin
while run_tunnel_thread do
tunnel_mutex.synchronize { ssh.process 0.01 }
Thread.pass
end
rescue => exc
puts "tunnel thread error: #{exc.message}"
end
end
tunnel_mutex.synchronize do
ssh.forward.local tunnel_port, 'tunnel_host', 22
end
begin
ssh_tunnel = Net::SSH.start 'localhost', 'tunnel_login', :password => 'tunnel_password', :port => tunnel_port
puts ssh_tunnel.exec! 'date'
rescue => exc
puts "tunnel connection error: #{exc.message}"
ensure
ssh_tunnel.close if ssh_tunnel
end
tunnel_mutex.synchronize do
ssh.forward.cancel_local tunnel_port
end
rescue => exc
puts "tunnel error: #{exc.message}"
ensure
run_tunnel_thread = false
tunnel_thread.join if tunnel_thread
ssh.close if ssh
end
That's just how SSH in general is. If you're offended by how ugly it looks, you should probably wrap up that functionality into a port forwarding class of some sort so that the exposed part is a lot more succinct. An interface like this, perhaps:
forwarder = PortForwarder.new(8080, 'remote.host', 80)
So I have found a slightly better implementation. It only requires a single fork but still uses a socket for the communication. It uses IO#read_nonblock for checking if a message is ready. If there isn't one, the method throws an exception, in which case the block continues to return true and the SSH session keeps serving requests. Once the parent is done with the connection it sends a message, which causes child_socket.read_nonblock(maxlen).nil? to return false, making the loop exit and therefore shutting down the SSH connection.
I feel a little better about this, so between that and #tadman's suggestion to wrap it in a port forwarding class I think it's about as good as it'll get. However, any further suggestions for improving this are most welcome.
#!/usr/bin/env ruby -w
require 'net/ssh'
require 'httparty'
require 'socket'
log = Logger.new(STDOUT)
log.level = Logger::DEBUG
local_port = 2006
child_socket, parent_socket = Socket.pair(:UNIX, :DGRAM, 0)
maxlen = 1000
hostname = "www.example.com"
pid = fork do
parent_socket.close
Net::SSH.start("ssh-tunnel-hostname", "username") do |session|
session.logger = log
session.logger.sev_threshold=Logger::Severity::DEBUG
session.forward.local(local_port, hostname, 80)
child_socket.send("ready", 0)
session.loop { child_socket.read_nonblock(maxlen).nil? rescue true }
end
end
child_socket.close
puts "Message from child: #{parent_socket.recv(maxlen)}"
resp = HTTParty.post("http://localhost:#{local_port}/", :headers => { "Host" => hostname } )
# the write cannot be the last statement, otherwise the child pid could end up
# not receiving it
parent_socket.write("done")
puts resp.inspect

Ruby Net-SFTP capture and log exceptions

I have a simple SFTP script that I am testing to connect to a server and download a file, or files, with a specific date in the file name.
I am using rufus/scheduler to start the SFTP portion of the script every X minutes to see if a new file is on the server.
It all seems to work until I intentionally force an error, such as provide incorrect login credentials. Then I want to be able to capture the exact error or exception and log it using logger. I am not getting error detail or I am not using rescue correctly:
scheduler = Rufus::Scheduler::PlainScheduler.start_new(:frequency => 3.0)
log = Logger.new('sftp.log')
log.level = Logger::INFO
begin
log.info 'starting sftp'
Net::SFTP.start(HOST, ID, :password => PW ) do |sftp|
sftp.dir.glob("./", "20120820*") do |entry|
puts entry.name
file = entry.name
success = sftp.download!(file, file)
end
end
rescue Exception => e
puts e.message # Human readable error
log.error ("SFTP exception occured: " + e.message)
end
scheduler.join
Does adding :verbose => Logger::DEBUG work ?
Net::SFTP.start(HOST, ID, :password => PW, :verbose => Logger::DEBUG ) do |sftp|

ruby net-ssh calling bash script with interactive prompts

I have a problem that I hope you can help me with
I’m trying to use ruby to ssh onto a machine and run a bash script, this part is fairly easy but the bash script requires me to entry a username and password interactively and this is where I’m stuck
So if I run the script manually I see:-
./run_file.sh
Enter username:
Enter password:
So at the enter Username prompt I have to enter the username etc
I’ve got a simple method I use to make the connection and my idea was to pass in an array made up of
[‘command to run’, ‘username’, ‘password’]
but I don’t know how to extend the Net::SSH call to respond to the prompts
# ssh conectivity method
def command_ssh(host, cmd)
require 'net/ssh'
user = LocalConfig::SSH_DETAILS[:user]
pass = LocalConfig::SSH_DETAILS[:pass]
Net::SSH.start(host, user, :password => pass, :paranoid => false, :auth_methods => ['password'], :timeout => 10 )do |ssh |
output = (ssh.exec!(cmd[0]))
return output
end
end
anyone got any ideas
managed to fix this by using the channel function, here's the method I use now
def connect(host,command)
require 'rubygems'
require 'net/ssh'
user = LocalConfig::SSH_DETAILS[:user]
pass = LocalConfig::SSH_DETAILS[:pass
o = ""
Net::SSH.start(host, user, :password => pass, :paranoid => false, :auth_methods => ['password'], :timeout => 30 )do |ssh |
channel = ssh.open_channel do |ch|
ch.exec(command) do |ch2, success|
ch2.send_data "myUserName\n"
ch2.send_data "mPassWord\n"
ch.on_data do |ch2, data|
o += data
end
end
end
channel.wait
return o.to_s
end
end

How do I display progress bars from a shell command over ssh

I've got a script thats supposed to mimic ffmpeg on my local machine, by sending the command of to a remote machine, running it there and then returning the results.
(see previous stackoverflow question.)
#!/usr/bin/env ruby
require 'rubygems'
require 'net/ssh'
require 'net/sftp'
require 'highline/import'
file = ARGV[ ARGV.index( '-i' ) + 1] if ARGV.include?( '-i' )
puts 'No input file specified' unless file;
host = "10.0.0.10"
user = "user"
prod = "new-#{file}" # product filename (call it <file>-new)
rpath = "/home/#{user}/.rffmpeg" # remote computer operating directory
rfile = "#{rpath}/#{file}" # remote filename
rprod = "#{rpath}/#{prod}" # remote product
cmd = "ffmpeg -i #{rfile} #{rprod}"# remote command, constructed
pass = ask("Password: ") { |q| q.echo = false } # password from stdin
Net::SSH.start(host, user ) do |ssh|
ssh.sftp.connect do |sftp|
# upload local 'file' to remote 'rfile'
sftp.upload!(file, rfile)
# run remote command 'cmd' to produce 'rprod'
ssh.exec!(cmd)
# download remote 'rprod' to local 'prod'
sftp.download!(rprod, prod)
end
end
now my problem is at
ssh.exec!(cmd)
I want to display the cmd's output to the local user in real-time. But making it
puts ssh.exec!(cmd)
I only get the resulting output after the command has finished running. How would I have to change the code to make this work?
On the display side of your question, you can generate an updating progress bar in Ruby using the "\r" string char. This backs you up to the beginning of the current line allowing you to re-write it. For example:
1.upto(100) { |i| sleep 0.05; print "\rPercent Complete #{i}%"}
Or if you just want a progress bar across the screen you can simply do something similar to this:
1.upto(50) { sleep 0.05; print "|"}
Also, relating to stdout, in addition to flushing output per previous example (STDOUT.flush), you can ask Ruby to automatically sync writes to an IO buffer (in this case STDOUT) with associated device writes (basically turns off internal buffering):
STDOUT.sync = true
Also, I find that sometimes flush doesn't work for me, and I use "IO.fsync" instead. For me that's mostly been related to file system work, but it's worth knowing.
From ri Net::SSH::start:
-------------------------------------------------------- Net::SSH::start
Net::SSH::start(host, user, options={}, &block) {|connection| ...}
------------------------------------------------------------------------
The standard means of starting a new SSH connection. When used with
a block, the connection will be closed when the block terminates,
otherwise the connection will just be returned. The yielded (or
returned) value will be an instance of
Net::SSH::Connection::Session (q.v.). (See also
Net::SSH::Connection::Channel and Net::SSH::Service::Forward.)
Net::SSH.start("host", "user") do |ssh|
ssh.exec! "cp /some/file /another/location"
hostname = ssh.exec!("hostname")
ssh.open_channel do |ch|
ch.exec "sudo -p 'sudo password: ' ls" do |ch, success|
abort "could not execute sudo ls" unless success
ch.on_data do |ch, data|
print data
if data =~ /sudo password: /
ch.send_data("password\n")
end
end
end
end
ssh.loop
end
So it looks like you can get more interactive by using #open_channel
Here's some example code:
user#server% cat echo.rb
#! /usr/local/bin/ruby
def putsf s
puts s
STDOUT.flush
end
putsf "hello"
5.times do
putsf gets.chomp
end
putsf "goodbye"
And on your local machine:
user#local% cat client.rb
#! /usr/local/bin/ruby
require 'rubygems'
require 'net/ssh'
words = %w{ earn more sessions by sleaving }
index = 0;
Net::SSH.start('server', 'user') do |ssh|
ssh.open_channel do |ch|
ch.exec './echo.rb' do |ch, success|
abort "could not execute ./echo.rb" unless success
ch.on_data do |ch, data|
p [:data, data]
index %= words.size
ch.send_data( words[index] + "\n" )
index += 1
end
end
end
end
user#local% ./client.rb
[:data, "hello\n"]
[:data, "earn\n"]
[:data, "more\n"]
[:data, "sessions\n"]
[:data, "by\n"]
[:data, "sleaving\n"]
[:data, "goodbye\n"]
So you can interact with a running process this way.
It's important that the running process flush its output before requesting input - otherwise, the program might hang as the channel may not have received the unflushed output.

Resources