diff -r 000000000000 -r aef8f9f4a788 lib/symphony/tasks/ssh.rb --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/lib/symphony/tasks/ssh.rb Sat May 10 17:40:36 2014 -0700 @@ -0,0 +1,166 @@ +#!/usr/bin/env ruby +# vim: set nosta noet ts=4 sw=4: + +require 'shellwords' +require 'symphony/task' unless defined?( Symphony::Task ) + + +### A base class for connecting to remote hosts, running arbitrary +### commands, and collecting output. +### +### This isn't designed to be used directly. To use this in your +### environment, you'll want to subclass it, add the behaviors +### that make sense for you, then super() back to the parent in the +### #work method. +### +### It expects the payload to contain the following keys: +### +### host: (required) The hostname to connect to +### command: (required) The command to run on the remote host +### port: (optional) The port to connect to (defaults to 22) +### opts: (optional) Explicit SSH client options +### user: (optional) The user to connect as (defaults to root) +### key: (optional) The path to an SSH private key +### +### +### Additionally, this class responds to the 'symphony_ssh' configurability +### key. Currently, you can set the 'path' argument, which is the +### full path to the local ssh binary (defaults to '/usr/bin/ssh') and +### override the default ssh user, key, and client opts. +### +### Textual output of the command is stored in the @output instance variable. +### +### +### require 'symphony' +### require 'symphony/tasks/ssh' +### +### class YourTask < Symphony::Task::SSH +### timeout 5 +### subscribe_to 'ssh.command' +### +### def work( payload, metadata ) +### status = super +### puts "Remote host said: %s" % [ @output ] +### return status.success? +### end +### end +### +class Symphony::Task::SSH < Symphony::Task + extend Configurability + config_key :symphony_ssh + + # SSH default options. + # + CONFIG_DEFAULTS = { + :path => '/usr/bin/ssh', + :opts => [ + '-e', 'none', + '-T', + '-x', + '-q', + '-o', 'CheckHostIP=no', + '-o', 'BatchMode=yes', + '-o', 'StrictHostKeyChecking=no' + ], + :user => 'root', + :key => nil + } + + # SSH "informative" stdout output that should be cleaned from the + # command output. + SSH_CLEANUP = %r/Warning: no access to tty|Thus no job control in this shell/ + + class << self + # The full path to the ssh binary. + attr_reader :path + + # A default set of ssh client options when connecting + # to remote hosts. + attr_reader :opts + + # The default user to use when connecting. If unset, 'root' is used. + attr_reader :user + + # An absolute path to a password-free ssh private key. + attr_reader :key + end + + ### Configurability API. + ### + def self::configure( config=nil ) + config = self.defaults.merge( config || {} ) + @path = config.delete( :path ) + @opts = config.delete( :opts ) + @user = config.delete( :user ) + @key = config.delete( :key ) + super + end + + + ### Perform the ssh connection, passing the command to the pipe + ### and retreiving any output from the remote end. + ### + def work( payload, metadata ) + command = payload[ 'command' ] + raise ArgumentError, "Missing required option 'command'" unless command + raise ArgumentError, "Missing required option 'host'" unless payload[ 'host' ] + + exitcode = self.open_connection( payload, metadata ) do |reader, writer| + self.log.debug "Writing command #{command}..." + writer.puts( command ) + self.log.debug " closing child's writer." + writer.close + self.log.debug " reading from child." + reader.read + end + + self.log.debug "SSH exited: %d" % [ exitcode ] + return exitcode + end + + + ######### + protected + ######### + + ### Call ssh and yield the remote IO objects to the caller, + ### cleaning up afterwards. + ### + def open_connection( payload, metadata=nil ) + raise LocalJumpError, "no block given" unless block_given? + @output = '' + + port = payload[ 'port' ] || 22 + opts = payload[ 'opts' ] || Symphony::Task::SSH.opts + user = payload[ 'user' ] || Symphony::Task::SSH.user + key = payload[ 'key' ] || Symphony::Task::SSH.key + + cmd = [] + cmd << Symphony::Task::SSH.path + cmd += Symphony::Task::SSH.opts + + cmd << '-p' << port.to_s + cmd << '-i' << key if key + cmd << '-l' << user + cmd << payload[ 'host' ] + cmd.flatten! + self.log.debug "Running SSH command with: %p" % [ Shellwords.shelljoin(cmd) ] + + parent_reader, child_writer = IO.pipe + child_reader, parent_writer = IO.pipe + + pid = spawn( *cmd, :out => child_writer, :in => child_reader, :close_others => true ) + child_writer.close + child_reader.close + + self.log.debug "Yielding back to the run block." + @output = yield( parent_reader, parent_writer ) + @output = @output.split("\n").reject{|l| l =~ SSH_CLEANUP }.join + self.log.debug " run block done." + + pid, status = Process.waitpid2( pid ) + return status + end + +end # Symphony::Task::SSH +