Multiple changes to help support Windows native ssh with sshscript workers.
Defaults are to assume a non-hostile posix environment.

 - Don't hardcode the tempdir separator character.
 - Allow overrides in the payload of any exposed net-ssh options.
 - Allow setting of the "delete" command for script cleanup (ie, 'del')
 - Allow running the script via a specific interpreter.

An example payload to make this work with Windows native ssh/powershell
to execute a ruby script:

	payload = {
		'compression' => false,
		'delete_cmd'  => 'del',
		'run_binary'  => 'ruby',
		'tempdir'     => ''
	}
3 files changed, 62 insertions(+), 14 deletions(-)

M Rakefile
M lib/symphony/tasks/sshscript.rb
M spec/symphony/tasks/sshscript_spec.rb
M Rakefile +1 -1
@@ 31,7 31,7 @@ spec = Gem::Specification.new do |s|
 	s.platform     = Gem::Platform::RUBY
 	s.summary      = "Base classes for using Symphony with ssh."
 	s.name         = 'symphony-ssh'
-	s.version      = '0.3.0'
+	s.version      = '0.4.0'
 	s.license      = 'BSD-3-Clause'
 	s.has_rdoc     = true
 	s.require_path = 'lib'

          
M lib/symphony/tasks/sshscript.rb +27 -12
@@ 27,7 27,9 @@ require 'symphony/tasks/ssh'
 ###    key:        (optional) The path to an SSH private key
 ###    attributes: (optional) Additional data to attach to the template
 ###    nocleanup:  (optional) Leave the remote script after execution? (default to false)
-###    tempdir:    (optional) The destination temp directory.  (defaults to /tmp)
+###    delete_cmd: (optional) The command to delete the remote script.  (default to 'rm')
+###    run_binary: (optional) Windows doesn't allow direct execution of scripts, this is prefixed to the remote command if present.
+###    tempdir:    (optional) The destination temp directory.  (defaults to /tmp/, needs to include the separator character)
 ###
 ###
 ### Additionally, this class responds to the 'symphony.ssh' configurability

          
@@ 79,11 81,9 @@ class Symphony::Task::SSHScript < Sympho
 	def work( payload, metadata )
 		template   = payload[ 'template' ]
 		attributes = payload[ 'attributes' ] || {}
-		port       = payload[ 'port' ]    || 22
 		user       = payload[ 'user' ]    || Symphony::Task::SSH.user
 		key        = payload[ 'key'  ]    || Symphony::Task::SSH.key
-		nocleanup  = payload[ 'nocleanup' ]
-		tempdir    = payload[ 'tempdir' ] || '/tmp'
+		tempdir    = payload[ 'tempdir' ] || '/tmp/'
 
 		raise ArgumentError, "Missing required option 'template'" unless template
 		raise ArgumentError, "Missing required option 'host'"     unless payload[ 'host' ]

          
@@ 91,7 91,17 @@ class Symphony::Task::SSHScript < Sympho
 		remote_filename = self.make_remote_filename( template, tempdir )
 		source = self.generate_script( template, attributes )
 
-		ssh_options = DEFAULT_SSH_OPTIONS.merge( port: port, keys: Array(key) )
+		# Map any configuration parameters in the payload to ssh
+		# options, for potential per-message behavior overrides.
+		ssh_opts_override = payload.
+			slice( *DEFAULT_SSH_OPTIONS.keys.map( &:to_s ) ).
+			transform_keys{|k| k.to_sym }
+
+		ssh_options = DEFAULT_SSH_OPTIONS.dup.merge!(
+			ssh_opts_override,
+			port: payload[ 'port' ] || 22,
+			keys: Array( key )
+		)
 		ssh_options.merge!(
 			logger: Loggability[ Net::SSH ],
 			verbose: :debug

          
@@ 103,7 113,7 @@ class Symphony::Task::SSHScript < Sympho
 			self.upload_script( conn, source, remote_filename )
 			self.log.debug "  done with the upload."
 
-			self.run_script( conn, remote_filename, nocleanup )
+			self.run_script( conn, remote_filename, payload )
 			self.log.debug "Output was:\n#{@output}"
 		end
 

          
@@ 118,9 128,9 @@ class Symphony::Task::SSHScript < Sympho
 	### Generate a unique filename for the script on the remote host,
 	### based on +template+ name.
 	###
-	def make_remote_filename( template, tempdir="/tmp" )
+	def make_remote_filename( template, tempdir="/tmp/" )
 		basename = File.basename( template, File.extname(template) )
-		tmpname  = "%s/%s-%s" % [
+		tmpname  = "%s%s-%s" % [
 			tempdir,
 			basename,
 			SecureRandom.hex( 6 )

          
@@ 153,11 163,16 @@ class Symphony::Task::SSHScript < Sympho
 
 
 	### Run the +remote_filename+ via the ssh +conn+.  The script
-	### will be deleted automatically unless +nocleanup+ is true.
+	### will be deleted automatically unless +nocleanup+ is set
+	### in the payload.
 	###
-	def run_script( conn, remote_filename, nocleanup=false )
-		@output = conn.exec!( remote_filename )
-		conn.exec!( "rm #{remote_filename}" ) unless nocleanup
+	def run_script( conn, remote_filename, payload )
+		delete_cmd = payload[ 'delete_cmd' ] || 'rm'
+		command    = remote_filename
+		command    = "%s %s" % [ payload['run_binary'], remote_filename ] if payload[ 'run_binary' ]
+
+		@output = conn.exec!( command )
+		conn.exec!( "#{delete_cmd} #{remote_filename}" ) unless payload[ 'nocleanup' ]
 	end
 
 end # Symphony::Task::SSHScript

          
M spec/symphony/tasks/sshscript_spec.rb +34 -1
@@ 20,8 20,11 @@ context Symphony::Task::SSHScript do
 			tmpname = instance.send( :make_remote_filename, "fancy-script.tmpl" )
 			expect( tmpname ).to match( %r|^/tmp/fancy-script-[[:xdigit:]]{6}| )
 
-			tmpname = instance.send( :make_remote_filename, "fancy-script.tmpl", "/var/tmp" )
+			tmpname = instance.send( :make_remote_filename, "fancy-script.tmpl", "/var/tmp/" )
 			expect( tmpname ).to match( %r|/var/tmp/fancy-script-[[:xdigit:]]{6}| )
+
+			tmpname = instance.send( :make_remote_filename, "fancy-script.tmpl", '' )
+			expect( tmpname ).to match( %r|fancy-script-[[:xdigit:]]{6}| )
 		end
 	end
 

          
@@ 108,6 111,36 @@ context Symphony::Task::SSHScript do
 			instance.work( payload, {} )
 		end
 
+		it "can override how it cleans the remote script up" do
+			payload[ 'delete_cmd' ] = 'del'
+
+			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 receive( :exec! ).with( "del /tmp/script_temp" )
+
+			expect( Net::SSH ).to receive( :start ).
+				with( 'example.com', 'symphony', opts ).and_yield( conn )
+
+			instance.work( payload, {} )
+		end
+
+		it "can run the script with a specific interpreter" do
+			payload[ 'run_binary' ] = 'ruby'
+
+			conn = double( :ssh_connection )
+			expect( instance ).to receive( :upload_script ).
+				with( conn, "Hi there, !", "/tmp/script_temp" )
+			expect( conn ).to receive( :exec! ).with( "ruby /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