# HG changeset patch # User Mahlon E. Smith # Date 1594852506 25200 # Node ID f31d60b04f8a3fb6f1b85842a7bea757a8f8ab5a # Parent d629b9939df4f505a41fdda2576b18a537d30dc4 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' => '' } diff -r d629b9939df4 -r f31d60b04f8a Rakefile --- 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' diff -r d629b9939df4 -r f31d60b04f8a lib/symphony/tasks/sshscript.rb --- 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 diff -r d629b9939df4 -r f31d60b04f8a spec/symphony/tasks/sshscript_spec.rb --- 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