Proper way of dealing with input prompts in Capistrano? - shell

What is the correct way of dealing with input prompts that are triggered by commands I run through Capistrano?
One example is the iptables-persistent package I install using aptitude. Despite the --no-gui flag, a prompt still comes up asking me to confirm how I want things configured.
Is there a way to pass parameters through command line to avoid such prompts?

I found and was able to implement this very helpful handle_command_with_input method from:
https://github.com/nesquena/cap-recipes/blob/master/lib/cap_recipes/tasks/utilities.rb
def handle_command_with_input(local_run_method, shell_command, input_query, response=nil)
send(local_run_method, shell_command, {:pty => true}) do |channel, stream, data|
if data =~ input_query
if response
logger.info "#{data} #{"*"*(rand(10)+5)}", channel[:host]
channel.send_data "#{response}\n"
else
logger.info data, channel[:host]
response = ::Capistrano::CLI.password_prompt "#{data}"
channel.send_data "#{response}\n"
end
else
logger.info data, channel[:host]
end
end
end
None of the code is mine. Gracias a Nesquena.

Related

Ruby and Websockets

I'm trying to create an asynchronous connection to a device that broadcasts packets when something changes, like a door is opened. I want to process the feedback in realtime to the control system. I also need to try to do this with the standard library due to the limitations on some controllers.
Right now I've been using cURL inside ruby to keep a connection open and reconnect if it disconnects. That has worked fine, but on macOS Big Sur after a few days terminal stops working due to the requests. I have not been able to figure out why.
I've rewritten most of my script to use net/http instead of cURL, but I can't figure out keeping a connection open and then real-time sending data to another function.
cURL Ruby Code:
def httpBcast(cmd, url, header,valuesListID,valuesListFID)
fullCommand = "curl -s -k -X #{cmd} '#{url}' #{header}"
loop do
begin
PTY.spawn( fullCommand ) do |stdout, stdin, pid|
begin
# Send Output to Savant
stdout.each { |line| parseBcast(line,valuesListID,valuesListFID)}
rescue Errno::EIO
puts "Errno:EIO error, but this probably just means " +
"that the process has finished giving output"
end
end
rescue PTY::ChildExited
puts "The child process exited!"
end
end
end
def parseBcast(msg='',valuesListID,valuesListFID)
if msg.start_with?("data:")
msg.gsub!(/\n+/, '')
msg.gsub!(/\s+/, "")
msg.gsub!("data:","")
msg.to_json
msgP = JSON.parse(msg)
if valuesListFID.include?(msgP['result']['deviceId'])
id = valuesListFID.index(msgP['result']['deviceId'])
id +=1
else
id = "Device Doesn't Exist"
end
msgP['result']['deviceId'] = id
send_savant msgP.to_json
end
end
Any guidance anyone can offer would be most appreciated.
Turns out the vendor needed to make a firmware update. All Good Now.

How to use a conditional within a Ruby expect script

I'm playing with expect in Ruby but I'm a little lost as to how I can branch my code based on the behavior of a device I am logging into. How could I do say foo.run if I get the correct prompt below > but run foo.fail if I do not? Even further, how can I evaluate all of the text that comes back between entering the password and receiving the > prompt? Is there a way to look at all text that the device prints somehow?
def device_test(password)
$expect_verbose = true
PTY.spawn("ssh my-router") do |reader, writer, pid|
reader.expect("password:")
writer.puts(password)
reader.expect(">")
puts "logged in"
sleep(15)
end
end
It appears that the expect method can only look for a single pattern (unlike the Tcl expect library where you can look for multiple patterns simultaneously).
It looks like you'll have to pass a "timeout" parameter and check the return value:
if reader.expect(">", 2)
puts "foo.run"
else
# did not see ">" within 2 seconds
puts "foo.fail
end

Make Net:SSH update returned data packets/chunks in exec block more often

I have a ruby script on a remote server that I'm running via Net:SSH on my local pc.
The remote script takes a few minutes to run and outputs it's progress to stdout.
The problem I have is the block in my exec command only gets called when the packet/chunk is full.
So I get the progress all in one hit about each minute.
Here is some cut down examples that illustrate my problem:
Server Script:
(0.999).each do |i|
puts i
sleep 1
end
puts 1000
Local Script:
Net::SSH.start('ip.v.4.addr', 'user', :keys => ['my_key']) do |ssh|
ssh.exec("ruby count_to_1000.rb") do |ch, stream, data|
puts data if stream == :stdout
end
ssh.loop(1)
end
Is there any way from the remote script to force the sending of the packet/chunk?
Or is there a way to set a limit of say a second (or n bits) before it's flushed? (within Net:SSH)
Thanks for all your help!
Try flush:
http://www.ruby-doc.org/core-2.1.5/IO.html#method-i-flush
(0.999).each do |i|
puts i
STDOUT.flush
sleep 1
end
Or sync:
http://www.ruby-doc.org/core-2.1.5/IO.html#method-i-sync
STDOUT.sync = true
(0.999).each do |i|
puts i
sleep 1
end
(Untested, btw. Maybe they need to be used on the client-side instead, or on some other IO stream. But those are the two methods that immediately come to mind.)
In my test setup this works as expected (tested with localhost). However, there might be some issues with the STDOUT flush.
You can try to to write to STDOUT in stead of using puts (I have heard that there is some difference that I don't really understand).
Thus, you can on your server use:
(0.999).each do |i|
STDOUT.puts i
sleep 1
end
STDOUT.puts 1000
#You could possibly also use "STDOUT.write 1000", but it will not append a newline, like puts does.
If that does not work, then you can try to force-flush the STDOUT by using STDOUT.flush(). I believe the same can be achieved by writing an empty string to STDOUT, but I am not 1000% sure.
It might also happen that the exec command actually waits for the entire process to terminate for some reason(I was not able to figure out from the docs). In which case, you won't be able to achieve what you want. Then you can consider setting up websockets, use DRB, or some other means to pass the data.

Exposing console apps to the web with Ruby

I'm looking to expose an interactive command line program via JSON or another RPC style service using Ruby. I've found a couple tricks to do this, but im missing something when redirecting the output and input.
One method at least on linux is to redirect the stdin and stdout to a file then read and write to that file asynchronously with file reads and writes. Another method ive been trying after googling around was to use open4. Here is the code I wrote so far, but its getting stuck after reading a few lines from standard output.
require "open4"
include Open4
status = popen4("./srcds_run -console -game tf +map ctf_2fort -maxplayers 6") do |pid, stdin, stdout, stderr|
puts "PID #{pid}"
lines=""
while (line=stdout.gets)
lines+=line
puts line
end
while (line=stderr.gets)
lines+=line
puts line
end
end
Any help on this or some insight would be appreciated!
What I would recommend is using Xinetd (or similar) to run the command on some socket and then using the ruby network code. One of the problems you've already run into in your code here is that your two while loops are sequential, which can cause problems.
Another trick you might try is to re-direct stderr to stdout in your command, so that your program only has to read the stdout. Something like this:
popen4("./srcds_run -console -game tf +map ctf_2fort -maxplayers 6 2>&1")
The other benefit of this is that you get all the messages/errors in the order they happen during the program run.
EDIT
Your should consider integrating with AnyTerm. You can then either expose AnyTerm directly e.g. via Apache mod_proxy, or have your Rails controller act as the reverse proxy (handling authentication/session validation, then playing back controller.request minus any cookies to localhost:<AnyTerm-daemon-port>, and sending back as a response whatever AnyTerm replies with.)
class ConsoleController < ApplicationController
# AnyTerm speaks via HTTP POST only
def update
# validate session
...
# forward request to AnyTerm
response = Net::HTTP.post_form(URI.parse('http://localhost:#{AnyTermPort}/', request.params))
headers['Content-Type'] = response['Content-Type']
render_text response.body, response.status
end
Otherwise, you'd need to use IO::Select or IO::read_noblock to know when data is available to be read (from either network or subprocess) so you don't deadlock. See this too. Also check that either your Rails is used in a multi-threaded environment or that your Ruby version is not affected by this IO::Select bug.
You can start with something along these lines:
status = POpen4::popen4("ping localhost") do |stdout, stderr, stdin, pid|
puts "PID #{pid}"
# our buffers
stdout_lines=""
stderr_lines=""
begin
loop do
# check whether stdout, stderr or both are
# ready to be read from without blocking
IO.select([stdout,stderr]).flatten.compact.each { |io|
# stdout, if ready, goes to stdout_lines
stdout_lines += io.readpartial(1024) if io.fileno == stdout.fileno
# stderr, if ready, goes to stdout_lines
stderr_lines += io.readpartial(1024) if io.fileno == stderr.fileno
}
break if stdout.closed? && stderr.closed?
# if we acumulated any complete lines (\n-terminated)
# in either stdout/err_lines, output them now
stdout_lines.sub!(/.*\n/m) { puts $& ; '' }
stderr_lines.sub!(/.*\n/m) { puts $& ; '' }
end
rescue EOFError
puts "Done"
end
end
To also handle stdin, change to:
IO.select([stdout,stderr],[stdin]).flatten.compact.each { |io|
# program ready to get stdin? do we have anything for it?
if io.fileno == stdin.fileno && <got data from client?>
<write a small chunk from client to stdin>
end
# stdout, if ready, goes to stdout_lines

Respond to a SSH prompt before first command with ruby and Net::SSH

I'm trying to connect, using Net::SSH, to a server that immediately after
login executes a script that requires input from user. The user has to enter "1" or "2" and will receive some data via in the terminal afterwards.
My problem is that, although I am able to connect, I can not figure out a way to send "1\n" to the server and to receive the output.
The following code stops at "INFO -- net.ssh.connection.session[80906b74]: channel_open_confirmation: 0 0 0 32768".
Using channel.exec( "1\n" ) instead of channel.send_data unsurprisingly does not work either.
Net::SSH.start('host', 'user', :password => "pass", :auth_methods => ["password"], :verbose => :debug) do |session|
session.open_channel do |channel|
channel.on_data do |ch, data|
STDOUT.print data
end
channel.send_data( "1\n")
end
session.loop
end
Any ideas, anyone?
Thanks in advance
Can you verify that your send_data call is happening after you get the prompt from the remote server? Try constructing a channel.on_data block around your send_data call so that you can verify that you get the expected prompt from the server before you send a response.
You might not want to be using exec here. From the docs for Net::SSH::Connection::Channel:
Sends a channel request asking that
the given command be invoked.
You are wanting to send a text string to reply to a prompt, not invoke a command. The docs show exec being used to send full CLI commands like "ls -l /home".
Instead, send_data is probably what you want. The docs show it used to send arbitrary text such as channel.send_data("the password\n"). Note, however, this sentence in the docs:
Note that it does not immediately send
the data across the channel, but
instead merely appends the given data
to the channel‘s output buffer,
preparatory to being packaged up and
sent out the next time the connection
is accepting data.
You might want to take a look at channel.request_pty. It appears to be designed for interaction with a console-based application.
If you are trying to (in essence) script an SSH session that you would normally do manually, you may find it easier to use an expect-like interface (for example, a gem like sshExpect might be worth a try).
Thank you all for the pointers. I have been able to put my finger on the problem – besides using channel.request_pty it was also necessary to request a shell. The following finally works as expected:
Net::SSH.start('host', 'user', :password => "pass", :auth_methods => ["password"]) do |session|
session.open_channel do |channel|
channel.request_pty do |ch, success|
raise "Error requesting pty" unless success
ch.send_channel_request("shell") do |ch, success|
raise "Error opening shell" unless success
end
end
channel.on_data do |ch, data|
STDOUT.print data
end
channel.on_extended_data do |ch, type, data|
STDOUT.print "Error: #{data}\n"
end
channel.send_data( "1\n" )
session.loop
end
end
I'm not terribly familiar with the Net::SSH libs so I can't help with that per-se but it sounds like you could achieve what you want using Capistrano.
For example I have a capistrano task which connects to a remote server, runs a command which expects input and then continues. Capistrano takes care of the remote i/o. Maybe that could be a solution for you?
Hope it helps!
If I execute "1\n" in a shell the reply I get is: bash: 1: command not found
If I execute echo "1" I get: 1
Are you sure you want to try to execute the text you are sending? Maybe you were looking for something like:
output = ""
Net::SSH.start('host', 'user', :password => "pass") do |ssh|
output = ssh.exec! "echo 1"
end
puts output
I'm not proficient with that lib, but on SSH you can open multiple channels. Maybe the server only responds to the first default channel and if you open another one you get a fresh shell.

Resources