# HG changeset patch # User Mahlon E. Smith # Date 1400017101 25200 # Node ID 3972315383b38a1d6d95cc6ed333039c3b269a58 # Parent 62196065e9ea4e8bba3d4c91621f35efb59215b6 Small cleanups, add tests. diff -r 62196065e9ea -r 3972315383b3 .pryrc --- a/.pryrc Mon May 12 16:34:12 2014 -0700 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,14 +0,0 @@ -#!/usr/bin/ruby -*- ruby -*- - -require 'pathname' - -begin - $LOAD_PATH.unshift( Pathname(__FILE__).dirname + 'lib' ) - require 'symphony' - require 'symphony/metronome' - -rescue => e - $stderr.puts "Ack! Libraries failed to load: #{e.message}\n\t" + - e.backtrace.join( "\n\t" ) -end - diff -r 62196065e9ea -r 3972315383b3 .rvm.gems --- a/.rvm.gems Mon May 12 16:34:12 2014 -0700 +++ b/.rvm.gems Tue May 13 14:38:21 2014 -0700 @@ -1,3 +1,4 @@ +configurability -v2.1.2 inversion -v0.12.3 net-sftp -v2.1.2 net-ssh -v2.9.0 @@ -9,4 +10,5 @@ rspec-expectations -v3.0.0.beta2 rspec-mocks -v3.0.0.beta2 rspec-support -v3.0.0.beta2 +simplecov -v0.7.1 symphony -v0.6.0 diff -r 62196065e9ea -r 3972315383b3 lib/symphony/tasks/ssh.rb --- a/lib/symphony/tasks/ssh.rb Mon May 12 16:34:12 2014 -0700 +++ b/lib/symphony/tasks/ssh.rb Tue May 13 14:38:21 2014 -0700 @@ -149,7 +149,7 @@ parent_reader, child_writer = IO.pipe child_reader, parent_writer = IO.pipe - pid = spawn( *cmd, :out => child_writer, :in => child_reader, :close_others => true ) + pid = Process.spawn( *cmd, :out => child_writer, :in => child_reader, :close_others => true ) child_writer.close child_reader.close diff -r 62196065e9ea -r 3972315383b3 lib/symphony/tasks/sshscript.rb --- a/lib/symphony/tasks/sshscript.rb Mon May 12 16:34:12 2014 -0700 +++ b/lib/symphony/tasks/sshscript.rb Tue May 13 14:38:21 2014 -0700 @@ -108,7 +108,7 @@ key = payload[ 'key' ] || Symphony::Task::SSHScript.key nocleanup = payload[ 'nocleanup' ] - raise ArgumentError, "Missing required option 'command'" unless template + raise ArgumentError, "Missing required option 'template'" unless template raise ArgumentError, "Missing required option 'host'" unless payload[ 'host' ] remote_filename = self.make_remote_filename( template ) diff -r 62196065e9ea -r 3972315383b3 spec/fake_ssh --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/spec/fake_ssh Tue May 13 14:38:21 2014 -0700 @@ -0,0 +1,12 @@ +#!/usr/bin/env ruby +# +# Rather than mocking out all of spawn() and its pipes, test end-to-end. + +print <<-EOF +Warning: no access to tty; +Thus no job control in this shell. +Hi there! +EOF + +exit 0 + diff -r 62196065e9ea -r 3972315383b3 spec/symphony/tasks/.placeholder diff -r 62196065e9ea -r 3972315383b3 spec/symphony/tasks/ssh_spec.rb --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/spec/symphony/tasks/ssh_spec.rb Tue May 13 14:38:21 2014 -0700 @@ -0,0 +1,64 @@ + +require_relative '../../helpers' +require 'symphony/tasks/ssh' + +context Symphony::Task::SSH do + let( :ssh ) { (Pathname( __FILE__ ).dirname.parent.parent + 'fake_ssh').realpath } + + before( :each ) do + described_class.configure( + path: ssh.to_s, + key: '/tmp/sekrit.rsa', + user: 'symphony' + ) + end + + it_should_behave_like "an object with Configurability" + + describe 'subclassed' do + let( :instance ) { Class.new(described_class).new('queue') } + let( :payload ) { + { 'command' => 'woohoo', 'host' => 'example.com' } + } + + it "aborts if there is no command in the payload" do + expect { + instance.work( {}, {} ) + }.to raise_exception( ArgumentError, /missing required option 'command'/i ) + end + + it "aborts if there is no host in the payload" do + expect { + instance.work({ 'command' => 'boop' }, {} ) + }.to raise_exception( ArgumentError, /missing required option 'host'/i ) + end + + it "builds the proper command line" do + pipe = double( :fake_pipes ).as_null_object + allow( IO ).to receive( :pipe ).and_return([ pipe, pipe ]) + + args = [ + '-p', '22', '-i', '/tmp/sekrit.rsa', '-l', 'symphony', 'example.com' + ] + + expect( Process ).to receive( :spawn ).with( + *[ ssh.to_s, described_class.opts, args ].flatten, + :out => pipe, :in => pipe, :close_others => true + ).and_return( 12 ) + + expect( Process ).to receive( :waitpid2 ).with( 12 ).and_return([ 12, 1 ]) + + code = instance.work( payload, {} ) + expect( code ).to eq( 1 ) + end + + it "execs and captures output" do + code = instance.work( payload, {} ) + expect( code ).to eq( 0 ) + + output = instance.instance_variable_get( :@output ) + expect( output ).to eq( 'Hi there!' ) + end + end +end + diff -r 62196065e9ea -r 3972315383b3 spec/symphony/tasks/sshscript_spec.rb --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/spec/symphony/tasks/sshscript_spec.rb Tue May 13 14:38:21 2014 -0700 @@ -0,0 +1,129 @@ + +require_relative '../../helpers' +require 'symphony/tasks/sshscript' + +context Symphony::Task::SSHScript do + + before( :each ) do + described_class.configure( + key: '/tmp/sekrit.rsa', + user: 'symphony' + ) + end + + it_should_behave_like "an object with Configurability" + + describe 'subclassed' do + let( :instance ) { Class.new(described_class).new('queue') } + let( :payload ) { + { 'template' => 'script', 'host' => 'example.com' } + } + let( :opts ) { + opts = described_class::DEFAULT_SSH_OPTIONS + opts.merge!( + :port => 22, + :keys => ['/tmp/sekrit.rsa'] + ) + opts + } + let( :template ) { Inversion::Template.new("Hi there, !") } + + before( :each ) do + allow( Inversion::Template ).to receive( :load ).and_return( template ) + allow( Dir::Tmpname ).to receive( :make_tmpname ).and_return( "script_temp" ) + end + + it "aborts if there is no template in the payload" do + expect { + instance.work( {}, {} ) + }.to raise_exception( ArgumentError, /missing required option 'template'/i ) + end + + it "aborts if there is no host in the payload" do + expect { + instance.work({ 'template' => 'boop' }, {} ) + }.to raise_exception( ArgumentError, /missing required option 'host'/i ) + end + + it "adds debugging output if specified in the payload" do + payload[ 'debug' ] = true + + options = opts.dup + options.merge!( + :logger => Loggability[ Net::SSH ], + :verbose => :debug + ) + + expect( Net::SSH ).to receive( :start ).with( 'example.com', 'symphony', options ) + instance.work( payload, {} ) + end + + it "attaches attributes to the scripts from the payload" do + payload[ 'attributes' ] = { :name => 'Handsome' } + + conn = double( :ssh_connection ) + expect( instance ).to receive( :upload_script ). + with( conn, "Hi there, Handsome!", "/tmp/script_temp" ) + expect( conn ).to receive( :exec! ).with( "/tmp/script_temp" ) + expect( conn ).to receive( :exec! ).with( "rm /tmp/script_temp" ) + + expect( Net::SSH ).to receive( :start ). + with( 'example.com', 'symphony', opts ).and_yield( conn ) + + instance.work( payload, {} ) + end + + it "uploads the file and sets it executable" do + conn = double( :ssh_connection ) + sftp = double( :sftp_connection ) + file = double( :remote_file_obj ) + fh = double( :remote_filehandle ) + + expect( conn ).to receive( :sftp ).and_return( sftp ) + expect( sftp ).to receive( :file ).and_return( file ) + + expect( file ).to receive( :open ). + with( "/tmp/script_temp", "w", 0755 ).and_yield( fh ) + expect( fh ).to receive( :print ).with( "Hi there, !" ) + + expect( conn ).to receive( :exec! ).with( "/tmp/script_temp" ) + expect( conn ).to receive( :exec! ).with( "rm /tmp/script_temp" ) + + expect( Net::SSH ).to receive( :start ). + with( 'example.com', 'symphony', opts ).and_yield( conn ) + + instance.work( payload, {} ) + end + + it "leaves the remote script in place if asked" do + payload[ 'nocleanup' ] = true + + conn = double( :ssh_connection ) + expect( instance ).to receive( :upload_script ). + with( conn, "Hi there, !", "/tmp/script_temp" ) + expect( conn ).to receive( :exec! ).with( "/tmp/script_temp" ) + expect( conn ).to_not receive( :exec! ).with( "rm /tmp/script_temp" ) + + expect( Net::SSH ).to receive( :start ). + with( 'example.com', 'symphony', opts ).and_yield( conn ) + + instance.work( payload, {} ) + end + + it "remembers the output of the remote script" do + conn = double( :ssh_connection ) + expect( instance ).to receive( :upload_script ). + with( conn, "Hi there, !", "/tmp/script_temp" ) + expect( conn ).to receive( :exec! ).with( "/tmp/script_temp" ).and_return( "Hi there, !" ) + expect( conn ).to receive( :exec! ).with( "rm /tmp/script_temp" ) + + expect( Net::SSH ).to receive( :start ). + with( 'example.com', 'symphony', opts ).and_yield( conn ) + + instance.work( payload, {} ) + output = instance.instance_variable_get( :@output ) + expect( output ).to eq( 'Hi there, !' ) + end + end +end +