lib/symphony/tasks/sshscript.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 'net/ssh'
       
     5 require 'net/sftp'
       
     6 require 'tmpdir'
       
     7 require 'inversion'
       
     8 require 'symphony'
       
     9 require 'symphony/task'
       
    10 
       
    11 
       
    12 ### A base class for connecting to a remote host, then uploading and
       
    13 ### executing an Inversion templated script.
       
    14 ###
       
    15 ### This isn't designed to be used directly.  To use this in your
       
    16 ### environment, you'll want to subclass it, add the behaviors
       
    17 ### that make sense for you, then super() back to the parent in the
       
    18 ### #work method.
       
    19 ###
       
    20 ### It expects the payload to contain the following keys:
       
    21 ###
       
    22 ###    host:       (required) The hostname to connect to
       
    23 ###    template:   (required) A path to the Inversion templated script
       
    24 ###    port:       (optional) The port to connect to (defaults to 22)
       
    25 ###    user:       (optional) The user to connect as (defaults to root)
       
    26 ###    key:        (optional) The path to an SSH private key
       
    27 ###    attributes: (optional) Additional data to attach to the template
       
    28 ###    nocleanup:  (optional) Leave the remote script after execution? (default to false)
       
    29 ###
       
    30 ###
       
    31 ### Additionally, this class responds to the 'symphony_ssh' configurability
       
    32 ### key.  Currently, you can override the default ssh user and private key.
       
    33 ###
       
    34 ### Textual output of the command is stored in the @output instance variable.
       
    35 ###
       
    36 ###
       
    37 ###    require 'symphony'
       
    38 ###    require 'symphony/tasks/sshscript'
       
    39 ###
       
    40 ###    class YourTask < Symphony::Task::SSHScript
       
    41 ###        timeout 30
       
    42 ###        subscribe_to 'ssh.script.*'
       
    43 ###
       
    44 ###        def work( payload, metadata )
       
    45 ###            status = super
       
    46 ###            puts "Remote script said: %s" % [ @output ]
       
    47 ###            return status.success?
       
    48 ###        end
       
    49 ###    end
       
    50 ###
       
    51 class Symphony::Task::SSHScript < Symphony::Task
       
    52 	extend Configurability
       
    53 	config_key :symphony_ssh
       
    54 
       
    55 	# Template config
       
    56 	#
       
    57 	TEMPLATE_OPTS = {
       
    58 		:ignore_unknown_tags => false,
       
    59 		:on_render_error     => :propagate,
       
    60 		:strip_tag_lines     => true
       
    61 	}
       
    62 
       
    63 	# The defaults to use when connecting via SSH
       
    64 	#
       
    65 	DEFAULT_SSH_OPTIONS = {
       
    66 		:auth_methods            => [ 'publickey' ],
       
    67 		:compression             => true,
       
    68 		:config                  => false,
       
    69 		:keys_only               => true,
       
    70 		:paranoid                => false,
       
    71 		:global_known_hosts_file => '/dev/null',
       
    72 		:user_known_hosts_file   => '/dev/null'
       
    73 	}
       
    74 
       
    75 	# SSH default options.
       
    76 	#
       
    77 	CONFIG_DEFAULTS = {
       
    78 		:user => 'root',
       
    79 		:key  => nil
       
    80 	}
       
    81 
       
    82 	class << self
       
    83 		# The default user to use when connecting.  If unset, 'root' is used.
       
    84 		attr_reader :user
       
    85 
       
    86 		# An absolute path to a password-free ssh private key.
       
    87 		attr_reader :key
       
    88 	end
       
    89 
       
    90 	### Configurability API.
       
    91 	###
       
    92 	def self::configure( config=nil )
       
    93 		config = self.defaults.merge( config || {} )
       
    94 		@user = config.delete( :user )
       
    95 		@key  = config.delete( :key )
       
    96 		super
       
    97 	end
       
    98 
       
    99 
       
   100 	### Perform the ssh connection, render the template, send it, and
       
   101 	### execute it.
       
   102 	###
       
   103 	def work( payload, metadata )
       
   104 		template   = payload[ 'template' ]
       
   105 		attributes = payload[ 'attributes' ] || {}
       
   106 		port       = payload[ 'port' ]    || 22
       
   107 		user       = payload[ 'user' ]    || Symphony::Task::SSHScript.user
       
   108 		key        = payload[ 'key'  ]    || Symphony::Task::SSHScript.key
       
   109 		nocleanup  = payload[ 'nocleanup' ]
       
   110 
       
   111 		raise ArgumentError, "Missing required option 'command'" unless template
       
   112 		raise ArgumentError, "Missing required option 'host'"    unless payload[ 'host' ]
       
   113 
       
   114 		remote_filename = self.make_remote_filename( template )
       
   115 		source = self.generate_script( template, attributes )
       
   116 
       
   117 		ssh_options = DEFAULT_SSH_OPTIONS.merge( :port => port, :keys => [key] )
       
   118 		ssh_options.merge!(
       
   119 			:logger  => Loggability[ Net::SSH ],
       
   120 			:verbose => :debug
       
   121 		) if payload[ 'debug' ]
       
   122 
       
   123 		Net::SSH.start( payload['host'], user, ssh_options ) do |conn|
       
   124 			self.log.debug "Uploading script (%d bytes) to %s:%s." %
       
   125 				[ source.bytesize, payload['host'], remote_filename ]
       
   126 			self.upload_script( conn, source, remote_filename )
       
   127 			self.log.debug "  done with the upload."
       
   128 
       
   129 			self.run_script( conn, remote_filename, nocleanup )
       
   130 			self.log.debug "Output was:\n#{@output}"
       
   131 		end
       
   132 
       
   133 		return true
       
   134 	end
       
   135 
       
   136 
       
   137 	#########
       
   138 	protected
       
   139 	#########
       
   140 
       
   141 	### Generate a unique filename for the script on the remote host,
       
   142 	### based on +template+ name.
       
   143 	###
       
   144 	def make_remote_filename( template )
       
   145 		basename = File.basename( template, File.extname(template) )
       
   146 		tmpname  = Dir::Tmpname.make_tmpname( basename, rand(10000) )
       
   147 
       
   148 		return "/tmp/#{tmpname}"
       
   149 	end
       
   150 
       
   151 
       
   152 	### Generate a script by loading the script +template+, populating it with
       
   153 	### +attributes+, and returning the rendered output.
       
   154 	###
       
   155 	def generate_script( template, attributes )
       
   156 		tmpl = Inversion::Template.load( template, TEMPLATE_OPTS )
       
   157 		tmpl.attributes.merge!( attributes )
       
   158 		tmpl.task = self
       
   159 
       
   160 		return tmpl.render
       
   161 	end
       
   162 
       
   163 
       
   164 	### Upload the templated +source+ via the ssh +conn+ to an
       
   165 	### executable file named +remote_filename+.
       
   166 	###
       
   167 	def upload_script( conn, source, remote_filename )
       
   168 		conn.sftp.file.open( remote_filename, "w", 0755 ) do |fh|
       
   169 			fh.print( source )
       
   170 		end
       
   171 	end
       
   172 
       
   173 
       
   174 	### Run the +remote_filename+ via the ssh +conn+.  The script
       
   175 	### will be deleted automatically unless +nocleanup+ is true.
       
   176 	###
       
   177 	def run_script( conn, remote_filename, nocleanup=false )
       
   178 		@output = conn.exec!( remote_filename )
       
   179 		conn.exec!( "rm #{remote_filename}" ) unless nocleanup
       
   180 	end
       
   181 
       
   182 end # Symphony::Task::SSHScript
       
   183