lib/symphony/tasks/ssh.rb
changeset 0 aef8f9f4a788
child 3 62196065e9ea
equal deleted inserted replaced
-1:000000000000 0:aef8f9f4a788
       
     1 #!/usr/bin/env ruby
       
     2 # vim: set nosta noet ts=4 sw=4:
       
     3 
       
     4 require 'shellwords'
       
     5 require 'symphony/task' unless defined?( Symphony::Task )
       
     6 
       
     7 
       
     8 ### A base class for connecting to remote hosts, running arbitrary
       
     9 ### commands, and collecting output.
       
    10 ###
       
    11 ### This isn't designed to be used directly.  To use this in your
       
    12 ### environment, you'll want to subclass it, add the behaviors
       
    13 ### that make sense for you, then super() back to the parent in the
       
    14 ### #work method.
       
    15 ###
       
    16 ### It expects the payload to contain the following keys:
       
    17 ###
       
    18 ###    host:    (required) The hostname to connect to
       
    19 ###    command: (required) The command to run on the remote host
       
    20 ###    port:    (optional) The port to connect to (defaults to 22)
       
    21 ###    opts:    (optional) Explicit SSH client options
       
    22 ###    user:    (optional) The user to connect as (defaults to root)
       
    23 ###    key:     (optional) The path to an SSH private key
       
    24 ###
       
    25 ###
       
    26 ### Additionally, this class responds to the 'symphony_ssh' configurability
       
    27 ### key.  Currently, you can set the 'path' argument, which is the
       
    28 ### full path to the local ssh binary (defaults to '/usr/bin/ssh') and
       
    29 ### override the default ssh user, key, and client opts.
       
    30 ###
       
    31 ### Textual output of the command is stored in the @output instance variable.
       
    32 ###
       
    33 ###
       
    34 ###    require 'symphony'
       
    35 ###    require 'symphony/tasks/ssh'
       
    36 ###
       
    37 ###    class YourTask < Symphony::Task::SSH
       
    38 ###        timeout 5
       
    39 ###        subscribe_to 'ssh.command'
       
    40 ###
       
    41 ###        def work( payload, metadata )
       
    42 ###            status = super
       
    43 ###            puts "Remote host said: %s" % [ @output ]
       
    44 ###            return status.success?
       
    45 ###        end
       
    46 ###    end
       
    47 ###
       
    48 class Symphony::Task::SSH < Symphony::Task
       
    49 	extend Configurability
       
    50 	config_key :symphony_ssh
       
    51 
       
    52 	# SSH default options.
       
    53 	#
       
    54 	CONFIG_DEFAULTS = {
       
    55 		:path => '/usr/bin/ssh',
       
    56 		:opts => [
       
    57 			'-e', 'none',
       
    58 			'-T',
       
    59 			'-x',
       
    60 			'-q',
       
    61 			'-o', 'CheckHostIP=no',
       
    62 			'-o', 'BatchMode=yes',
       
    63 			'-o', 'StrictHostKeyChecking=no'
       
    64 		],
       
    65 		:user => 'root',
       
    66 		:key  => nil
       
    67 	}
       
    68 
       
    69 	# SSH "informative" stdout output that should be cleaned from the
       
    70 	# command output.
       
    71 	SSH_CLEANUP = %r/Warning: no access to tty|Thus no job control in this shell/
       
    72 
       
    73 	class << self
       
    74 		# The full path to the ssh binary.
       
    75 		attr_reader :path
       
    76 
       
    77 		# A default set of ssh client options when connecting
       
    78 		# to remote hosts.
       
    79 		attr_reader :opts
       
    80 
       
    81 		# The default user to use when connecting.  If unset, 'root' is used.
       
    82 		attr_reader :user
       
    83 
       
    84 		# An absolute path to a password-free ssh private key.
       
    85 		attr_reader :key
       
    86 	end
       
    87 
       
    88 	### Configurability API.
       
    89 	###
       
    90 	def self::configure( config=nil )
       
    91 		config = self.defaults.merge( config || {} )
       
    92 		@path  = config.delete( :path )
       
    93 		@opts  = config.delete( :opts )
       
    94 		@user  = config.delete( :user )
       
    95 		@key   = config.delete( :key )
       
    96 		super
       
    97 	end
       
    98 
       
    99 
       
   100 	### Perform the ssh connection, passing the command to the pipe
       
   101 	### and retreiving any output from the remote end.
       
   102 	###
       
   103 	def work( payload, metadata )
       
   104 		command = payload[ 'command' ]
       
   105 		raise ArgumentError, "Missing required option 'command'" unless command
       
   106 		raise ArgumentError, "Missing required option 'host'"    unless payload[ 'host' ]
       
   107 
       
   108 		exitcode = self.open_connection( payload, metadata ) do |reader, writer|
       
   109 			self.log.debug "Writing command #{command}..."
       
   110 			writer.puts( command )
       
   111 			self.log.debug "  closing child's writer."
       
   112 			writer.close
       
   113 			self.log.debug "  reading from child."
       
   114 			reader.read
       
   115 		end
       
   116 
       
   117 		self.log.debug "SSH exited: %d" % [ exitcode ]
       
   118 		return exitcode
       
   119 	end
       
   120 
       
   121 
       
   122 	#########
       
   123 	protected
       
   124 	#########
       
   125 
       
   126 	### Call ssh and yield the remote IO objects to the caller,
       
   127 	### cleaning up afterwards.
       
   128 	###
       
   129 	def open_connection( payload, metadata=nil )
       
   130 		raise LocalJumpError, "no block given" unless block_given?
       
   131 		@output = ''
       
   132 
       
   133 		port = payload[ 'port' ] || 22
       
   134 		opts = payload[ 'opts' ] || Symphony::Task::SSH.opts
       
   135 		user = payload[ 'user' ] || Symphony::Task::SSH.user
       
   136 		key  = payload[ 'key'  ] || Symphony::Task::SSH.key
       
   137 
       
   138 		cmd = []
       
   139 		cmd << Symphony::Task::SSH.path
       
   140 		cmd += Symphony::Task::SSH.opts
       
   141 
       
   142 		cmd << '-p' << port.to_s
       
   143 		cmd << '-i' << key if key
       
   144 		cmd << '-l' << user
       
   145 		cmd << payload[ 'host' ]
       
   146 		cmd.flatten!
       
   147 		self.log.debug "Running SSH command with: %p" % [ Shellwords.shelljoin(cmd) ]
       
   148 
       
   149 		parent_reader, child_writer = IO.pipe
       
   150 		child_reader, parent_writer = IO.pipe
       
   151 
       
   152 		pid = spawn( *cmd, :out => child_writer, :in => child_reader, :close_others => true )
       
   153 		child_writer.close
       
   154 		child_reader.close
       
   155 
       
   156 		self.log.debug "Yielding back to the run block."
       
   157 		@output = yield( parent_reader, parent_writer )
       
   158 		@output = @output.split("\n").reject{|l| l =~ SSH_CLEANUP }.join
       
   159 		self.log.debug "  run block done."
       
   160 
       
   161 		pid, status = Process.waitpid2( pid )
       
   162 		return status
       
   163 	end
       
   164 
       
   165 end # Symphony::Task::SSH
       
   166