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' => ''
}
--- 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