8

I want to write code in Ruby witch net::ssh that run commands one by one on remote linux machine and log everything (called command, stdout and stderr on linux machine).

So I write function:

  def rs(ssh,cmds)
    cmds.each do |cmd|
      log.debug "[SSH>] #{cmd}"
      ssh.exec!(cmd) do |ch, stream, data|    
        log.debug "[SSH:#{stream}>] #{data}"            
      end  
    end
  end

For example if I want to create on remote linux new folders and file: "./verylongdirname/anotherlongdirname/a.txt", and list files in that direcotry, and find firefox there (which is stupid a little :P) so i call above procedure like that:

Net::SSH.start(host, user, :password => pass) do |ssh|  

  cmds=["mkdir verylongdirname", \                                 #1
        "cd verylongdirname; mkdir anotherlongdirname, \           #2
        "cd verylongdirname/anotherlongdirname; touch a.txt", \    #3
        "cd verylongdirname/anotherlongdirname; ls -la", \         #4
        "cd verylongdirname/anotherlongdirname; find ./ firefox"   #5 that command send error to stderr.
        ]

  rs(ssh,cmds)   # HERE we call our function

  ssh.loop
end

After run code above i will have full LOG witch informations about executions commands in line #1,#2,#3,#4,#5. The problem is that state on linux, between execude commands from cmds array, is not saved (so I must repeat "cd" statement before run proper command). And I'm not satisfy with that.

My purpose is to have cmds tables like that:

  cmds=["mkdir verylongdirname", \     #1
        "cd verylongdirname", \        
        "mkdir anotherlongdirname", \  #2
        "cd anotherlongdirname", \
        "touch a.txt", \               #3
        "ls -la", \                    #4
        "find ./ firefox"]             #5

As you see, te state between run each command is save on the linux machine (and we don't need repeat apropriate "cd" statement before run proper command). How to change "rs(ssh,cmds)" procedure to do it and LOG EVERYTHING (comand,stdout,stdin) like before?

Kamil Kiełczewski
  • 85,173
  • 29
  • 368
  • 345

4 Answers4

4

Perhaps try it with an ssh channel instead to open a remote shell. That should preserve state between your commands as the connection will be kept open:

http://net-ssh.github.com/ssh/v1/chapter-5.html

Here's also an article of doing something similar with a little bit different approach:

http://drnicwilliams.com/2006/09/22/remote-shell-with-ruby/

Edit 1:

Ok. I see what you are saying. SyncShell was removed from Net::SSH 2.0. However I found this, which looks like it does pretty much what SyncShell did:

http://net-ssh-telnet.rubyforge.org/

Example:

s = Net::SSH.start(host, user)
t = Net::SSH::Telnet.new("Session" => s, "Prompt" => %r{^myprompt :})
puts t.cmd("cd /tmp")  
puts t.cmd("ls")       # <- Lists contents of /tmp

I.e. Net::SSH::Telnet is synchronous, and preserves state, because it runs in a pty with your remote shell environment. Remember to set the correct prompt detection, otherwise Net::SSH::Telnet will appear to hang once you call it (it's trying to find the prompt).

Casper
  • 33,403
  • 4
  • 84
  • 79
  • 1. In http://drnicwilliams.com/2006/09/22/remote-shell-with-ruby/ we see: ` shell = session.shell.sync` Thad web is from 2006. Today in ruby shell.sync doesnt work. Shell is probably droped from develop net::ssh (I don't know why). There is some alternative package net-ssh-shell but is to young to use it (no much function is implement there). – Kamil Kiełczewski Jul 24 '11 at 18:19
  • 2. I read http://net-ssh.github.com/ssh/v1/chapter-5.html before. When I try to use channels with its on_data(for stdout), on_extended_data(for stderr) and send_data(for send commands one by one in loop) functions - but I can't force sequential log (to log sequential command1,stdout1,stderr1,command2,stdout2,stderr2,command3,stdout3,sterr3...). The reason is that in send_data - all commands was send before first of it be executed on remote machine (so in log we see: command1,command2,command3,...,stdout1,stderr1,stdout2,stderr2,stdout3,stderr3...) – Kamil Kiełczewski Jul 24 '11 at 18:24
  • 3. Ok - thank U very much - it's what i wan't. :) BTW: My linux prompt is "/[$%#>] \z/n" - and it is default for net/ssh/telnet package - so I don't need to use "Prompt" option. Thanks – Kamil Kiełczewski Jul 25 '11 at 06:22
2

You can use pipe instead:

require "open3"

SERVER = "..."
BASH_PATH = "/bin/bash"

BASH_REMOTE = lambda do |command|
  Open3.popen3("ssh #{SERVER} #{BASH_PATH}") do |stdin, stdout, stderr|
    stdin.puts command
    stdin.close_write
    puts "STDOUT:", stdout.read
    puts "STDERR:", stderr.read
  end
end

BASH_REMOTE["ls /"]
BASH_REMOTE["ls /no_such_file"]
Victor Moroz
  • 9,167
  • 1
  • 19
  • 23
  • That solution don't work in Windows - so it's no portable. I don't check it in linux, but thanks for your time. Maby in future someone use it. – Kamil Kiełczewski Jul 25 '11 at 06:26
2

Ok, finally with the help of @Casper i get the procedure (maby someone use it):

  # Remote command execution
  # t=net::ssh:telnet, c="command_string"
  def cmd(t,c)    
    first=true
    d=''    
    # We send command via SSH and read output piece by piece (in 'cm' variable)
    t.cmd(c) do |cm|       
      # below we cleaning up output piece (becouse it have strange chars)     
      d << cm.gsub(/\e\].*?\a/,"").gsub(/\e\[.*?m/,"").gsub(/\r/,"")     
      # when we read entire line(composed of many pieces) we write it to log
      if d =~ /(^.*?)\n(.*)$/m
        if first ; 
          # instead of the first line (which has repeated commands) we log commands 'c'
          @log.info "[SSH]>"+c; 
          first=false
        else
          @log.info "[SSH] "+$1; 
        end
        d=$2        
      end      
    end

    # We print lines that were at the end (in last piece)
    d.each_line do |l|
      @log.info "[SSH] "+l.chomp      
    end
  end

And we call it in code:

#!/usr/bin/env ruby

require 'rubygems'

require 'net/ssh'

require 'net/ssh/telnet'

require 'log4r'
...
...
...
Net::SSH.start(host, user, :password => pass) do |ssh|  
  t = Net::SSH::Telnet.new("Session" => ssh)
  cmd(t,"cd /")
  cmd(t,"ls -la")
  cmd(t,"find ./ firefox")  
end

Thanks, bye.

Kamil Kiełczewski
  • 85,173
  • 29
  • 368
  • 345
0

Here's wrapper around Net/ssh here's article http://ruby-lang.info/blog/virtual-file-system-b3g

source https://github.com/alexeypetrushin/vfs

to log all commands just overwrite the Box.bash method and add logging there

Alex Craft
  • 13,598
  • 11
  • 69
  • 133