Multiple changes to help support Windows native ssh with sshscript workers. v0.4.0
authorMahlon E. Smith <mahlon@martini.nu>
Wed, 15 Jul 2020 15:35:06 -0700
changeset 19 f31d60b04f8a
parent 18 d629b9939df4
child 20 a64e6e4f8446
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' => '' }
Rakefile
lib/symphony/tasks/sshscript.rb
spec/symphony/tasks/sshscript_spec.rb
--- a/Rakefile	Thu Jul 09 15:11:45 2020 -0700
+++ b/Rakefile	Wed Jul 15 15:35:06 2020 -0700
@@ -31,7 +31,7 @@
 	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'
--- a/lib/symphony/tasks/sshscript.rb	Thu Jul 09 15:11:45 2020 -0700
+++ b/lib/symphony/tasks/sshscript.rb	Wed Jul 15 15:35:06 2020 -0700
@@ -27,7 +27,9 @@
 ###    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 @@
 	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 @@
 		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 @@
 			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 @@
 	### 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 @@
 
 
 	### 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
--- a/spec/symphony/tasks/sshscript_spec.rb	Thu Jul 09 15:11:45 2020 -0700
+++ b/spec/symphony/tasks/sshscript_spec.rb	Wed Jul 15 15:35:06 2020 -0700
@@ -20,8 +20,11 @@
 			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 @@
 			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