|
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 |