class Nucleon::Util::SSH

Public Class Methods

close(hostname = nil, user = nil) click to toggle source
    # File lib/core/util/ssh.rb
220 def self.close(hostname = nil, user = nil)
221   if hostname && user.nil? # Assume we entered a session id
222     if @@sessions.has_key?(hostname)
223       @@sessions[hostname].close
224       @@sessions.delete(hostname)
225     end
226 
227   elsif hostname && user # Generate session id from args
228     session_id = session_id(hostname, user)
229 
230     if @@sessions.has_key?(session_id)
231       @@sessions[session_id].close
232       @@sessions.delete(session_id)
233     end
234 
235   else # Close all connections
236     @@sessions.keys.each do |id|
237       @@sessions[id].close
238       @@sessions.delete(id)
239     end
240   end
241 end
close_session(hostname, user) click to toggle source
    # File lib/core/util/ssh.rb
206 def self.close_session(hostname, user)
207   session_id = session_id(hostname, user)
208 
209   if @@sessions.has_key?(session_id)
210     begin # Don't care about errors here
211       @@sessions[session_id].close
212     rescue
213     end
214     @@sessions.delete(session_id)
215   end
216 end
download(hostname, user, remote_path, local_path, options = {}) { |name, received, total| ... } click to toggle source
    # File lib/core/util/ssh.rb
299 def self.download(hostname, user, remote_path, local_path, options = {})
300   config = Config.ensure(options)
301 
302   require 'net/scp'
303 
304   # Accepted options:
305   # * :recursive - the +remote+ parameter refers to a remote directory, which
306   # should be downloaded to a new directory named +local+ on the local
307   # machine.
308   # * :preserve - the atime and mtime of the file should be preserved.
309   # * :verbose - the process should result in verbose output on the server
310   # end (useful for debugging).
311   #
312   config.init(:recursive, true)
313   config.init(:preserve, true)
314   config.init(:verbose, true)
315 
316   blocking = config.delete(:blocking, true)
317 
318   session(hostname, user) do |ssh|
319     if blocking
320       ssh.scp.download!(remote_path, local_path, config.export) do |ch, name, received, total|
321         yield(name, received, total) if block_given?
322       end
323     else
324       ssh.scp.download(remote_path, local_path, config.export)
325     end
326   end
327 end
exec(hostname, user, commands) { |:output, command, data| ... } click to toggle source
    # File lib/core/util/ssh.rb
245 def self.exec(hostname, user, commands)
246   results = []
247 
248   begin
249     session(hostname, user) do |ssh|
250       Data.array(commands).each do |command|
251         command = command.flatten.join(' ') if command.is_a?(Array)
252         command = command.to_s
253         result  = Shell::Result.new(command)
254 
255         logger.info(">> running SSH: #{command}")
256 
257         ssh.open_channel do |ssh_channel|
258           ssh_channel.exec(command) do |channel, success|
259             unless success
260               raise "Could not execute command: #{command.inspect}"
261             end
262 
263             channel.on_data do |ch, data|
264               data = yield(:output, command, data) if block_given?
265               result.append_output(data)
266             end
267 
268             channel.on_extended_data do |ch, type, data|
269               data = yield(:error, command, data) if block_given?
270               result.append_errors(data)
271             end
272 
273             channel.on_request('exit-status') do |ch, data|
274               result.status = data.read_long
275             end
276 
277             channel.on_request('exit-signal') do |ch, data|
278               result.status = 255
279             end
280           end
281         end
282         logger.warn("`#{command}` messages: #{result.errors}") if result.errors.length > 0
283         logger.warn("`#{command}` status: #{result.status}") unless result.status == 0
284 
285         results << result
286         ssh.loop
287       end
288     end
289   rescue Net::SSH::HostKeyMismatch => error
290     error.remember_host!
291     sleep 0.2
292     retry
293   end
294   results
295 end
generate(options = {}) click to toggle source
   # File lib/core/util/ssh.rb
30 def self.generate(options = {})
31   config = Config.ensure(options)
32 
33   private_key  = config.get(:private_key, nil)
34   original_key = nil
35   key_comment  = config.get(:comment, '')
36   passphrase   = config.get(:passphrase, nil)
37   force        = config.get(:force, false)
38 
39   if private_key.nil?
40     key_type    = config.get(:type, "RSA")
41     key_bits    = config.get(:bits, 2048)
42 
43     key_data = SSHKey.generate({
44       :type       => key_type,
45       :bits       => key_bits,
46       :comment    => key_comment,
47       :passphrase => passphrase
48     })
49     is_new = true
50 
51   else
52     if private_key.include?('PRIVATE KEY')
53       original_key = private_key
54     else
55       original_key = Disk.read(private_key)
56     end
57 
58     if original_key
59       encrypted = original_key.include?('ENCRYPTED')
60       key_data  = SSHKey.new(original_key, {
61         :comment    => key_comment,
62         :passphrase => passphrase
63       }) if force || ! encrypted || passphrase
64     end
65     is_new = false
66   end
67 
68   return nil unless key_data && ! key_data.ssh_public_key.empty?
69   Keypair.new(key_data, is_new, original_key, passphrase)
70 end
init_session(hostname, user, port = 22, private_key = nil, options = {}) click to toggle source
    # File lib/core/util/ssh.rb
200 def self.init_session(hostname, user, port = 22, private_key = nil, options = {})
201   session(hostname, user, port, private_key, true, options)
202 end
key_path(ssh_user = nil) click to toggle source
   # File lib/core/util/ssh.rb
13 def self.key_path(ssh_user = nil)
14   unless @@key_path
15     if ssh_user
16       home_path = "/home/#{ssh_user}"
17     else
18       home_path = ( ENV['USER'] == 'root' ? '/root' : ENV['HOME'] ) # In case we are using sudo
19     end
20     @@key_path = File.join(home_path, '.ssh')
21 
22     FileUtils.mkdir(@@key_path) unless File.directory?(@@key_path)
23   end
24   @@key_path
25 end
session(hostname, user, port = 22, private_key = nil, reset = false, options = {}) { |sessions| ... } click to toggle source
    # File lib/core/util/ssh.rb
157 def self.session(hostname, user, port = 22, private_key = nil, reset = false, options = {})
158   require 'net/ssh'
159 
160   session_id  = session_id(hostname, user)
161 
162   @@session_lock.synchronize do
163     config      = Config.ensure(options)
164 
165     ssh_options = Config.new({
166       :user_known_hosts_file => [ File.join(key_path, 'known_hosts'), File.join(key_path, 'known_hosts2') ],
167       :auth_methods          => [ 'publickey', 'password' ],
168       :paranoid              => :very
169     }, {}, true, false).import(Util::Data.subset(config, config.keys - [ :keypair, :key_dir, :key_name, :reset_conn ]))
170 
171     if private_key
172       auth_id = [ session_id, private_key ].join('_')
173 
174       if (config[:reset_conn] || ! @@auth[auth_id]) && keypair = unlock_private_key(private_key, config)
175         @@auth[auth_id] = keypair
176       end
177       config[:keypair] = @@auth[auth_id] # Reset so caller can access updated keypair
178 
179       if @@auth[auth_id].is_a?(String)
180         ssh_options[:keys_only] = false
181         ssh_options[:keys]      = [ @@auth[auth_id] ]
182       else
183         ssh_options[:keys_only] = true
184         ssh_options[:key_data]  = [ @@auth[auth_id].private_key ]
185       end
186     else
187       ssh_options[:password] = config[:password] if config[:password]
188     end
189 
190     ssh_options[:port] = port
191 
192     if reset || ! @@sessions.has_key?(session_id)
193       @@sessions[session_id] = Net::SSH.start(hostname, user, ssh_options.export)
194     end
195   end
196   yield(@@sessions[session_id]) if block_given? && @@sessions[session_id]
197   @@sessions[session_id]
198 end
session_id(hostname, user) click to toggle source
    # File lib/core/util/ssh.rb
151 def self.session_id(hostname, user)
152   "#{hostname}-#{user}"
153 end
terminal(hostname, user, options = {}) click to toggle source

Inspired by vagrant ssh implementation

See: github.com/mitchellh/vagrant/blob/master/lib/vagrant/util/ssh.rb

    # File lib/core/util/ssh.rb
373 def self.terminal(hostname, user, options = {})
374   config   = Config.ensure(options)
375   ssh_path = nucleon_locate("ssh")
376 
377   raise Errors::SSHUnavailable unless ssh_path
378 
379   port         = config.get(:port, 22)
380   private_keys = config.get(:private_keys, File.join(ENV['HOME'], '.ssh', 'id_rsa'))
381   key_dir      = config.get(:key_dir, nil)
382   key_name     = config.get(:key_name, 'default')
383 
384   command_options = [
385     "#{user}@#{hostname}",
386     "-p", port.to_s,
387     "-o", "Compression=yes",
388     "-o", "DSAAuthentication=yes",
389     "-o", "LogLevel=FATAL",
390     "-o", "StrictHostKeyChecking=no",
391     "-o", "UserKnownHostsFile=/dev/null",
392     "-o", "IdentitiesOnly=yes"
393   ]
394 
395   Util::Data.array(private_keys).each do |private_key|
396     unless ENV['NUCLEON_NO_SSH_KEY_SAVE'] || key_dir.nil?
397       keypair = unlock_private_key(private_key, {
398         :key_dir  => key_dir,
399         :key_name => key_name
400       })
401       private_key = keypair.private_key_file(key_dir, key_name) if keypair
402     end
403     command_options += [ "-i", File.expand_path(private_key) ]
404   end
405 
406   if config.get(:forward_x11, false)
407     command_options += [
408       "-o", "ForwardX11=yes",
409       "-o", "ForwardX11Trusted=yes"
410     ]
411   end
412 
413   command_options += [ "-o", "ProxyCommand=#{config[:proxy_command]}" ] if config.get(:proxy_command, false)
414   command_options += [ "-o", "ForwardAgent=yes" ] if config.get(:forward_agent, false)
415 
416   command_options.concat(Util::Data.array(config[:extra_args])) if config.get(:extra_args, false)
417 
418   #---
419 
420   logger.info("Executing SSH in subprocess: #{command_options.inspect}")
421 
422   process = ChildProcess.build('ssh', *command_options)
423   process.io.inherit!
424 
425   process.start
426   process.wait
427   process.exit_code
428 end
unlock_private_key(private_key, options = {}) click to toggle source
    # File lib/core/util/ssh.rb
433 def self.unlock_private_key(private_key, options = {})
434   require 'net/ssh'
435 
436   config   = Config.ensure(options)
437   keypair  = config.get(:keypair, nil)
438   key_dir  = config.get(:key_dir, nil)
439   key_name = config.get(:key_name, 'default')
440   no_file  = ENV['NUCLEON_NO_SSH_KEY_SAVE']
441 
442   password    = nil
443   tmp_key_dir = nil
444 
445   if private_key
446     keypair = nil if keypair && ! keypair.private_key
447 
448     unless no_file
449       if key_dir && key_name && File.exists?(private_key) && loaded_private_key = Util::Disk.read(private_key)
450         FileUtils.mkdir_p(key_dir)
451 
452         loaded_private_key =~ /BEGIN\s+([A-Z]+)\s+/
453 
454         local_key_type = $1
455         local_key_name = Keypair.render(local_key_type, key_name)
456         local_key_path = File.join(key_dir, local_key_name)
457 
458         keypair = generate({ :private_key => local_key_path }) if File.exists?(local_key_path)
459       end
460     end
461 
462     unless keypair
463       key_manager_logger       = ::Logger.new(STDERR)
464       key_manager_logger.level = ::Logger::FATAL
465       key_manager              = Net::SSH::Authentication::KeyManager.new(key_manager_logger)
466 
467       key_manager.each_identity do |identity|
468         if identity.comment == private_key
469           # Feed the key to the system password manager if it exists
470           keypair = private_key
471         end
472       end
473       key_manager.finish
474 
475       until keypair
476         keypair = generate({
477           :private_key => private_key,
478           :passphrase  => password
479         })
480         password = ui.ask("Enter passphrase for #{private_key}: ", { :echo => false }) unless keypair
481       end
482 
483       unless no_file
484         if key_dir && key_name && ! keypair.is_a?(String)
485           key_files = keypair.store(key_dir, key_name, false)
486 
487           if key_files && File.exists?(key_files[:private_key])
488             keypair = generate({ :private_key => key_files[:private_key] })
489           end
490         end
491       end
492     end
493   end
494   keypair
495 end
upload(hostname, user, local_path, remote_path, options = {}) { |name, sent, total| ... } click to toggle source
    # File lib/core/util/ssh.rb
331 def self.upload(hostname, user, local_path, remote_path, options = {})
332   config = Config.ensure(options)
333 
334   require 'net/scp'
335 
336   # Accepted options:
337   # * :recursive - the +local+ parameter refers to a local directory, which
338   # should be uploaded to a new directory named +remote+ on the remote
339   # server.
340   # * :preserve - the atime and mtime of the file should be preserved.
341   # * :verbose - the process should result in verbose output on the server
342   # end (useful for debugging).
343   # * :chunk_size - the size of each "chunk" that should be sent. Defaults
344   # to 2048. Changing this value may improve throughput at the expense
345   # of decreasing interactivity.
346   #
347   config.init(:recursive, true)
348   config.init(:preserve, true)
349   config.init(:verbose, true)
350   config.init(:chunk_size, 2048)
351 
352   blocking = config.delete(:blocking, true)
353 
354   session(hostname, user) do |ssh|
355     if blocking
356       ssh.scp.upload!(local_path, remote_path, config.export) do |ch, name, sent, total|
357         yield(name, sent, total) if block_given?
358       end
359     else
360       ssh.scp.upload(local_path, remote_path, config.export)
361     end
362   end
363 end
valid?(public_ssh_key) click to toggle source
   # File lib/core/util/ssh.rb
75 def self.valid?(public_ssh_key)
76   SSHKey.valid_ssh_public_key?(public_ssh_key)
77 end