lib/symphony/tasks/sshscript.rb
changeset 11 ffa70066522c
parent 4 3972315383b3
child 19 f31d60b04f8a
equal deleted inserted replaced
10:7013280e62fa 11:ffa70066522c
     1 #!/usr/bin/env ruby
     1 #!/usr/bin/env ruby
     2 # vim: set nosta noet ts=4 sw=4:
     2 # vim: set nosta noet ts=4 sw=4:
     3 
     3 
       
     4 require 'securerandom'
     4 require 'net/ssh'
     5 require 'net/ssh'
     5 require 'net/sftp'
     6 require 'net/sftp'
     6 require 'tmpdir'
       
     7 require 'inversion'
     7 require 'inversion'
     8 require 'symphony'
     8 require 'symphony'
     9 require 'symphony/task'
     9 require 'symphony/task'
       
    10 require 'symphony/tasks/ssh'
    10 
    11 
    11 
    12 
    12 ### A base class for connecting to a remote host, then uploading and
    13 ### A base class for connecting to a remote host, then uploading and
    13 ### executing an Inversion templated script.
    14 ### executing an Inversion templated script.
    14 ###
    15 ###
    24 ###    port:       (optional) The port to connect to (defaults to 22)
    25 ###    port:       (optional) The port to connect to (defaults to 22)
    25 ###    user:       (optional) The user to connect as (defaults to root)
    26 ###    user:       (optional) The user to connect as (defaults to root)
    26 ###    key:        (optional) The path to an SSH private key
    27 ###    key:        (optional) The path to an SSH private key
    27 ###    attributes: (optional) Additional data to attach to the template
    28 ###    attributes: (optional) Additional data to attach to the template
    28 ###    nocleanup:  (optional) Leave the remote script after execution? (default to false)
    29 ###    nocleanup:  (optional) Leave the remote script after execution? (default to false)
       
    30 ###    tempdir:    (optional) The destination temp directory.  (defaults to /tmp)
    29 ###
    31 ###
    30 ###
    32 ###
    31 ### Additionally, this class responds to the 'symphony_ssh' configurability
    33 ### Additionally, this class responds to the 'symphony.ssh' configurability
    32 ### key.  Currently, you can override the default ssh user and private key.
    34 ### key.  Currently, you can override the default ssh user and private key.
    33 ###
    35 ###
    34 ### Textual output of the command is stored in the @output instance variable.
    36 ### Textual output of the command is stored in the @output instance variable.
    35 ###
    37 ###
    36 ###
    38 ###
    47 ###            return status.success?
    49 ###            return status.success?
    48 ###        end
    50 ###        end
    49 ###    end
    51 ###    end
    50 ###
    52 ###
    51 class Symphony::Task::SSHScript < Symphony::Task
    53 class Symphony::Task::SSHScript < Symphony::Task
    52 	extend Configurability
       
    53 	config_key :symphony_ssh
       
    54 
    54 
    55 	# Template config
    55 	# Template config
    56 	#
    56 	#
    57 	TEMPLATE_OPTS = {
    57 	TEMPLATE_OPTS = {
    58 		:ignore_unknown_tags => false,
    58 		ignore_unknown_tags: false,
    59 		:on_render_error     => :propagate,
    59 		on_render_error:     :propagate,
    60 		:strip_tag_lines     => true
    60 		strip_tag_lines:     true
    61 	}
    61 	}
    62 
    62 
    63 	# The defaults to use when connecting via SSH
    63 	# The defaults to use when connecting via SSH
    64 	#
    64 	#
    65 	DEFAULT_SSH_OPTIONS = {
    65 	DEFAULT_SSH_OPTIONS = {
    66 		:auth_methods            => [ 'publickey' ],
    66 		auth_methods:            [ 'publickey' ],
    67 		:compression             => true,
    67 		compression:             true,
    68 		:config                  => false,
    68 		config:                  false,
    69 		:keys_only               => true,
    69 		keys_only:               true,
    70 		:paranoid                => false,
    70 		verify_host_key:         :never,
    71 		:global_known_hosts_file => '/dev/null',
    71 		global_known_hosts_file: '/dev/null',
    72 		:user_known_hosts_file   => '/dev/null'
    72 		user_known_hosts_file:   '/dev/null'
    73 	}
    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 = Symphony::Task::SSHScript.defaults.merge( config || {} )
       
    94 		@user = config.delete( :user )
       
    95 		@key  = config.delete( :key )
       
    96 		super
       
    97 	end
       
    98 
    74 
    99 
    75 
   100 	### Perform the ssh connection, render the template, send it, and
    76 	### Perform the ssh connection, render the template, send it, and
   101 	### execute it.
    77 	### execute it.
   102 	###
    78 	###
   103 	def work( payload, metadata )
    79 	def work( payload, metadata )
   104 		template   = payload[ 'template' ]
    80 		template   = payload[ 'template' ]
   105 		attributes = payload[ 'attributes' ] || {}
    81 		attributes = payload[ 'attributes' ] || {}
   106 		port       = payload[ 'port' ]    || 22
    82 		port       = payload[ 'port' ]    || 22
   107 		user       = payload[ 'user' ]    || Symphony::Task::SSHScript.user
    83 		user       = payload[ 'user' ]    || Symphony::Task::SSH.user
   108 		key        = payload[ 'key'  ]    || Symphony::Task::SSHScript.key
    84 		key        = payload[ 'key'  ]    || Symphony::Task::SSH.key
   109 		nocleanup  = payload[ 'nocleanup' ]
    85 		nocleanup  = payload[ 'nocleanup' ]
       
    86 		tempdir    = payload[ 'tempdir' ] || '/tmp'
   110 
    87 
   111 		raise ArgumentError, "Missing required option 'template'" unless template
    88 		raise ArgumentError, "Missing required option 'template'" unless template
   112 		raise ArgumentError, "Missing required option 'host'"    unless payload[ 'host' ]
    89 		raise ArgumentError, "Missing required option 'host'"     unless payload[ 'host' ]
   113 
    90 
   114 		remote_filename = self.make_remote_filename( template )
    91 		remote_filename = self.make_remote_filename( template, tempdir )
   115 		source = self.generate_script( template, attributes )
    92 		source = self.generate_script( template, attributes )
   116 
    93 
   117 		ssh_options = DEFAULT_SSH_OPTIONS.merge( :port => port, :keys => [key] )
    94 		ssh_options = DEFAULT_SSH_OPTIONS.merge( port: port, keys: Array(key) )
   118 		ssh_options.merge!(
    95 		ssh_options.merge!(
   119 			:logger  => Loggability[ Net::SSH ],
    96 			logger: Loggability[ Net::SSH ],
   120 			:verbose => :debug
    97 			verbose: :debug
   121 		) if payload[ 'debug' ]
    98 		) if payload[ 'debug' ]
   122 
    99 
   123 		Net::SSH.start( payload['host'], user, ssh_options ) do |conn|
   100 		Net::SSH.start( payload['host'], user, ssh_options ) do |conn|
   124 			self.log.debug "Uploading script (%d bytes) to %s:%s." %
   101 			self.log.debug "Uploading script (%d bytes) to %s:%s." %
   125 				[ source.bytesize, payload['host'], remote_filename ]
   102 				[ source.bytesize, payload['host'], remote_filename ]
   139 	#########
   116 	#########
   140 
   117 
   141 	### Generate a unique filename for the script on the remote host,
   118 	### Generate a unique filename for the script on the remote host,
   142 	### based on +template+ name.
   119 	### based on +template+ name.
   143 	###
   120 	###
   144 	def make_remote_filename( template )
   121 	def make_remote_filename( template, tempdir="/tmp" )
   145 		basename = File.basename( template, File.extname(template) )
   122 		basename = File.basename( template, File.extname(template) )
   146 		tmpname  = Dir::Tmpname.make_tmpname( basename, rand(10000) )
   123 		tmpname  = "%s/%s-%s" % [
       
   124 			tempdir,
       
   125 			basename,
       
   126 			SecureRandom.hex( 6 )
       
   127 		]
   147 
   128 
   148 		return "/tmp/#{tmpname}"
   129 		return tmpname
   149 	end
   130 	end
   150 
   131 
   151 
   132 
   152 	### Generate a script by loading the script +template+, populating it with
   133 	### Generate a script by loading the script +template+, populating it with
   153 	### +attributes+, and returning the rendered output.
   134 	### +attributes+, and returning the rendered output.