adrift on a cosmic ocean

Writings on various topics (mostly technical) from Oliver Hookins and Angela Collins. We have lived in Berlin since 2009, have two kids, and have far too little time to really justify having a blog.

CI and Vagrant SSH sessions

Posted by Oliver on the 8th of May, 2011 in category Tech
Tagged with: cipuppetrubyvagrant

If you saw my talk in Amsterdam or read my slides (or even work with me, ) you would know that one of my stages of Puppet module testing is compile tests using Puppet Faces (at least, a pre-release version of it since we started using it a while ago). I'm just putting the finishing touches on the system so that it uses dynamically provisioned VMs using Vagrant and one of the slightly fiddly bits is actually running stuff inside the VMs - you'd think this would be a pretty large use-case in terms of what Vagrant is for, but I'm not sure how much success is being had with it in reality.

Vagrant wraps around the Ruby Net::SSH library to provide SSH access into its VMs (which just use VirtualBox's port forwarding, since all of the VMs are run on a NATed private network on the host). You can easily issue vagrant ssh from within a VM root and it will just work. You can even issue the secret vagrant ssh -e 'some command' line to run a command within the shell, but it will wrap around Net::SSH::Connection::Session.exec! which is completely synchronous (probably what you want) and saves up all the output until it has the exit code when the process has terminated (probably not what you want). Especially when the process takes a few minutes, and you are watching Jenkins' job console output, you want to see what is going on.

Unfortunately Vagrant's libraries don't expose the full richness of Net::SSH, but even if they did you wouldn't be much better off. Net::SSH gives you connection sessions in which you can issue multiple commands synchronously (as Vagrant typically does), or multiple commands asynchronously - and basically this equates to "assume they will run in parallel, in no particular order". There is also no direct handling of output and return codes - you need to set up callbacks for these. What this all amounts to is a bit of hackery just to get line-by-line output for our Jenkins job, and capture the return codes of each command properly.

#!/usr/bin/env ruby                                                                     
require 'rubygems'                                                                      
require 'net/ssh'                                                                       

def ssh_command(session, cmd)
  session.open_channel do |channel|
    channel.exec(cmd) do |ch, success|                                                  
      ch.on_data do |ch2, data|
        $stdout.puts "STDOUT: #{data}"                                                  
      end
      ch.on_extended_data do |ch2, type, data|                                          
        $stderr.puts "STDERR: #{data}"                                                  
      end
      ch.on_request "exit-status" do |ch2, data|                                        
        return data.read_long                                                           
      end                                                                               
    end                                                                                 
  end
  session.loop                                                                          
end                                                                                     

Net::SSH.start 'localhost', 'vagrant' do |session|
  ['echo foo >&2','sleep 5','echo bar','/bin/false','echo baz'].each do |command|       
    if (rc = ssh_command(session,command)) != 0                                         
      puts "ERROR: #{command}"                                                          
      puts "RC: #{rc}"                                                                  
      session.close                                                                     
      exit(rc)                                                                          
    end                                                                                 
  end                                                                                   
end

What this should give you is:

$ ./ssh_test.rb 
STDERR: foo
STDOUT: bar
ERROR: /bin/false
RC: 1
$ echo $?

Vagrant 0.7.3 sets up a read-only accessor to the Net::SSH::Connection::Session object contained within the Vagrant::SSH object so it's easy to just hook in to that and set up the code above to get slightly more flexible SSH access to the VM for our CI tasks:

commands = ['some','list','of','commands']

env = Vagrant::Environment.new(:cwd => VMDIR)
env.primary_vm.ssh.execute do |ssh|
  commands.each do |c|
    if (rc = ssh_command(ssh.session,c)) != 0
      puts "ERROR: #{c}"
      puts "RC: #{rc}"
      ssh.session.close
      exit(rc)                                    
    end
  end
end
© 2010-2018 Oliver Hookins and Angela Collins