Class: Nucleon::Util::SSH

Inherits:
Core show all
Defined in:
lib/core/util/ssh.rb

Defined Under Namespace

Classes: Keypair

Constant Summary collapse

@@key_path =

User key home

nil
@@sessions =

SSH Execution interface

{}
@@auth =
{}
@@session_lock =
Mutex.new

Instance Attribute Summary

Attributes inherited from Core

#logger, #ui

Class Method Summary collapse

Methods inherited from Core

#initialize, #initialized?, logger, ui, ui_group, #ui_group

Methods included from Mixin::Colors

#black, #blue, #cyan, #green, #grey, #purple, #red, #yellow

Methods inherited from Config

#[], #[]=, #append, #array, array, #clear, #defaults, #delete, #empty?, ensure, #export, filter, #filter, #get, #get_array, #get_hash, #has_key?, #hash, hash, #import, #init, init, init_flat, #initialize, #keys, #prepend, #set, #string, string, string_map, #string_map, symbol, #symbol, #symbol_array, symbol_map, #symbol_map, test, #test

Methods included from Mixin::ConfigOptions

#all_options, #clear_options, #contexts, #get_options, #set_options

Methods included from Mixin::ConfigCollection

#all_properties, #clear_properties, #delete_property, #get_property, #save_properties, #set_property

Constructor Details

This class inherits a constructor from Nucleon::Core

Class Method Details

.close(hostname = nil, user = nil) ⇒ Object




220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
# File 'lib/core/util/ssh.rb', line 220

def self.close(hostname = nil, user = nil)
  if hostname && user.nil? # Assume we entered a session id
    if @@sessions.has_key?(hostname)
      @@sessions[hostname].close
      @@sessions.delete(hostname)
    end

  elsif hostname && user # Generate session id from args
    session_id = session_id(hostname, user)

    if @@sessions.has_key?(session_id)
      @@sessions[session_id].close
      @@sessions.delete(session_id)
    end

  else # Close all connections
    @@sessions.keys.each do |id|
      @@sessions[id].close
      @@sessions.delete(id)
    end
  end
end

.close_session(hostname, user) ⇒ Object




206
207
208
209
210
211
212
213
214
215
216
# File 'lib/core/util/ssh.rb', line 206

def self.close_session(hostname, user)
  session_id = session_id(hostname, user)

  if @@sessions.has_key?(session_id)
    begin # Don't care about errors here
      @@sessions[session_id].close
    rescue
    end
    @@sessions.delete(session_id)
  end
end

.download(hostname, user, remote_path, local_path, options = {}) ⇒ Object




299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
# File 'lib/core/util/ssh.rb', line 299

def self.download(hostname, user, remote_path, local_path, options = {})
  config = Config.ensure(options)

  require 'net/scp'

  # Accepted options:
  # * :recursive - the +remote+ parameter refers to a remote directory, which
  # should be downloaded to a new directory named +local+ on the local
  # machine.
  # * :preserve - the atime and mtime of the file should be preserved.
  # * :verbose - the process should result in verbose output on the server
  # end (useful for debugging).
  #
  config.init(:recursive, true)
  config.init(:preserve, true)
  config.init(:verbose, true)

  blocking = config.delete(:blocking, true)

  session(hostname, user) do |ssh|
    if blocking
      ssh.scp.download!(remote_path, local_path, config.export) do |ch, name, received, total|
        yield(name, received, total) if block_given?
      end
    else
      ssh.scp.download(remote_path, local_path, config.export)
    end
  end
end

.exec(hostname, user, commands) ⇒ Object




245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
# File 'lib/core/util/ssh.rb', line 245

def self.exec(hostname, user, commands)
  results = []

  begin
    session(hostname, user) do |ssh|
      Data.array(commands).each do |command|
        command = command.flatten.join(' ') if command.is_a?(Array)
        command = command.to_s
        result  = Shell::Result.new(command)

        logger.info(">> running SSH: #{command}")

        ssh.open_channel do |ssh_channel|
          ssh_channel.exec(command) do |channel, success|
            unless success
              raise "Could not execute command: #{command.inspect}"
            end

            channel.on_data do |ch, data|
              data = yield(:output, command, data) if block_given?
              result.append_output(data)
            end

            channel.on_extended_data do |ch, type, data|
              data = yield(:error, command, data) if block_given?
              result.append_errors(data)
            end

            channel.on_request('exit-status') do |ch, data|
              result.status = data.read_long
            end

            channel.on_request('exit-signal') do |ch, data|
              result.status = 255
            end
          end
        end
        logger.warn("`#{command}` messages: #{result.errors}") if result.errors.length > 0
        logger.warn("`#{command}` status: #{result.status}") unless result.status == 0

        results << result
        ssh.loop
      end
    end
  rescue Net::SSH::HostKeyMismatch => error
    error.remember_host!
    sleep 0.2
    retry
  end
  results
end

.generate(options = {}) ⇒ Object


Instance generators



30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
# File 'lib/core/util/ssh.rb', line 30

def self.generate(options = {})
  config = Config.ensure(options)

  private_key  = config.get(:private_key, nil)
  original_key = nil
  key_comment  = config.get(:comment, '')
  passphrase   = config.get(:passphrase, nil)
  force        = config.get(:force, false)

  if private_key.nil?
    key_type    = config.get(:type, "RSA")
    key_bits    = config.get(:bits, 2048)

    key_data = SSHKey.generate({
      :type       => key_type,
      :bits       => key_bits,
      :comment    => key_comment,
      :passphrase => passphrase
    })
    is_new = true

  else
    if private_key.include?('PRIVATE KEY')
      original_key = private_key
    else
      original_key = Disk.read(private_key)
    end

    if original_key
      encrypted = original_key.include?('ENCRYPTED')
      key_data  = SSHKey.new(original_key, {
        :comment    => key_comment,
        :passphrase => passphrase
      }) if force || ! encrypted || passphrase
    end
    is_new = false
  end

  return nil unless key_data && ! key_data.ssh_public_key.empty?
  Keypair.new(key_data, is_new, original_key, passphrase)
end

.init_session(hostname, user, port = 22, private_key = nil, options = {}) ⇒ Object



200
201
202
# File 'lib/core/util/ssh.rb', line 200

def self.init_session(hostname, user, port = 22, private_key = nil, options = {})
  session(hostname, user, port, private_key, true, options)
end

.key_path(ssh_user = nil) ⇒ Object




13
14
15
16
17
18
19
20
21
22
23
24
25
# File 'lib/core/util/ssh.rb', line 13

def self.key_path(ssh_user = nil)
  unless @@key_path
    if ssh_user
      home_path = "/home/#{ssh_user}"
    else
      home_path = ( ENV['USER'] == 'root' ? '/root' : ENV['HOME'] ) # In case we are using sudo
    end
    @@key_path = File.join(home_path, '.ssh')

    FileUtils.mkdir(@@key_path) unless File.directory?(@@key_path)
  end
  @@key_path
end

.session(hostname, user, port = 22, private_key = nil, reset = false, options = {}) {|| ... } ⇒ Object


Yields:

  • ()


157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
# File 'lib/core/util/ssh.rb', line 157

def self.session(hostname, user, port = 22, private_key = nil, reset = false, options = {})
  require 'net/ssh'

  session_id  = session_id(hostname, user)

  @@session_lock.synchronize do
    config      = Config.ensure(options)

    ssh_options = Config.new({
      :user_known_hosts_file => [ File.join(key_path, 'known_hosts'), File.join(key_path, 'known_hosts2') ],
      :auth_methods          => [ 'publickey', 'password' ],
      :paranoid              => :very
    }, {}, true, false).import(Util::Data.subset(config, config.keys - [ :keypair, :key_dir, :key_name, :reset_conn ]))

    if private_key
      auth_id = [ session_id, private_key ].join('_')

      if (config[:reset_conn] || ! @@auth[auth_id]) && keypair = unlock_private_key(private_key, config)
        @@auth[auth_id] = keypair
      end
      config[:keypair] = @@auth[auth_id] # Reset so caller can access updated keypair

      if @@auth[auth_id].is_a?(String)
        ssh_options[:keys_only] = false
        ssh_options[:keys]      = [ @@auth[auth_id] ]
      else
        ssh_options[:keys_only] = true
        ssh_options[:key_data]  = [ @@auth[auth_id].private_key ]
      end
    else
      ssh_options[:password] = config[:password] if config[:password]
    end

    ssh_options[:port] = port

    if reset || ! @@sessions.has_key?(session_id)
      @@sessions[session_id] = Net::SSH.start(hostname, user, ssh_options.export)
    end
  end
  yield(@@sessions[session_id]) if block_given? && @@sessions[session_id]
  @@sessions[session_id]
end

.session_id(hostname, user) ⇒ Object




151
152
153
# File 'lib/core/util/ssh.rb', line 151

def self.session_id(hostname, user)
  "#{hostname}-#{user}"
end

.terminal(hostname, user, options = {}) ⇒ Object

Inspired by vagrant ssh implementation

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



373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
# File 'lib/core/util/ssh.rb', line 373

def self.terminal(hostname, user, options = {})
  config   = Config.ensure(options)
  ssh_path = nucleon_locate("ssh")

  raise Errors::SSHUnavailable unless ssh_path

  port         = config.get(:port, 22)
  private_keys = config.get(:private_keys, File.join(ENV['HOME'], '.ssh', 'id_rsa'))
  key_dir      = config.get(:key_dir, nil)
  key_name     = config.get(:key_name, 'default')

  command_options = [
    "#{user}@#{hostname}",
    "-p", port.to_s,
    "-o", "Compression=yes",
    "-o", "DSAAuthentication=yes",
    "-o", "LogLevel=FATAL",
    "-o", "StrictHostKeyChecking=no",
    "-o", "UserKnownHostsFile=/dev/null",
    "-o", "IdentitiesOnly=yes"
  ]

  Util::Data.array(private_keys).each do |private_key|
    unless ENV['NUCLEON_NO_SSH_KEY_SAVE'] || key_dir.nil?
      keypair = unlock_private_key(private_key, {
        :key_dir  => key_dir,
        :key_name => key_name
      })
      private_key = keypair.private_key_file(key_dir, key_name) if keypair
    end
    command_options += [ "-i", File.expand_path(private_key) ]
  end

  if config.get(:forward_x11, false)
    command_options += [
      "-o", "ForwardX11=yes",
      "-o", "ForwardX11Trusted=yes"
    ]
  end

  command_options += [ "-o", "ProxyCommand=#{config[:proxy_command]}" ] if config.get(:proxy_command, false)
  command_options += [ "-o", "ForwardAgent=yes" ] if config.get(:forward_agent, false)

  command_options.concat(Util::Data.array(config[:extra_args])) if config.get(:extra_args, false)

  #---

  logger.info("Executing SSH in subprocess: #{command_options.inspect}")

  process = ChildProcess.build('ssh', *command_options)
  process.io.inherit!

  process.start
  process.wait
  process.exit_code
end

.unlock_private_key(private_key, options = {}) ⇒ Object


SSH utilities



433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
# File 'lib/core/util/ssh.rb', line 433

def self.unlock_private_key(private_key, options = {})
  require 'net/ssh'

  config   = Config.ensure(options)
  keypair  = config.get(:keypair, nil)
  key_dir  = config.get(:key_dir, nil)
  key_name = config.get(:key_name, 'default')
  no_file  = ENV['NUCLEON_NO_SSH_KEY_SAVE']

  password    = nil
  tmp_key_dir = nil

  if private_key
    keypair = nil if keypair && ! keypair.private_key

    unless no_file
      if key_dir && key_name && File.exists?(private_key) && loaded_private_key = Util::Disk.read(private_key)
        FileUtils.mkdir_p(key_dir)

        loaded_private_key =~ /BEGIN\s+([A-Z]+)\s+/

        local_key_type = $1
        local_key_name = Keypair.render(local_key_type, key_name)
        local_key_path = File.join(key_dir, local_key_name)

        keypair = generate({ :private_key => local_key_path }) if File.exists?(local_key_path)
      end
    end

    unless keypair
      key_manager_logger       = ::Logger.new(STDERR)
      key_manager_logger.level = ::Logger::FATAL
      key_manager              = Net::SSH::Authentication::KeyManager.new(key_manager_logger)

      key_manager.each_identity do |identity|
        if identity.comment == private_key
          # Feed the key to the system password manager if it exists
          keypair = private_key
        end
      end
      key_manager.finish

      until keypair
        keypair = generate({
          :private_key => private_key,
          :passphrase  => password
        })
        password = ui.ask("Enter passphrase for #{private_key}: ", { :echo => false }) unless keypair
      end

      unless no_file
        if key_dir && key_name && ! keypair.is_a?(String)
          key_files = keypair.store(key_dir, key_name, false)

          if key_files && File.exists?(key_files[:private_key])
            keypair = generate({ :private_key => key_files[:private_key] })
          end
        end
      end
    end
  end
  keypair
end

.upload(hostname, user, local_path, remote_path, options = {}) ⇒ Object




331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
# File 'lib/core/util/ssh.rb', line 331

def self.upload(hostname, user, local_path, remote_path, options = {})
  config = Config.ensure(options)

  require 'net/scp'

  # Accepted options:
  # * :recursive - the +local+ parameter refers to a local directory, which
  # should be uploaded to a new directory named +remote+ on the remote
  # server.
  # * :preserve - the atime and mtime of the file should be preserved.
  # * :verbose - the process should result in verbose output on the server
  # end (useful for debugging).
  # * :chunk_size - the size of each "chunk" that should be sent. Defaults
  # to 2048. Changing this value may improve throughput at the expense
  # of decreasing interactivity.
  #
  config.init(:recursive, true)
  config.init(:preserve, true)
  config.init(:verbose, true)
  config.init(:chunk_size, 2048)

  blocking = config.delete(:blocking, true)

  session(hostname, user) do |ssh|
    if blocking
      ssh.scp.upload!(local_path, remote_path, config.export) do |ch, name, sent, total|
        yield(name, sent, total) if block_given?
      end
    else
      ssh.scp.upload(local_path, remote_path, config.export)
    end
  end
end

.valid?(public_ssh_key) ⇒ Boolean


Checks

Returns:

  • (Boolean)


75
76
77
# File 'lib/core/util/ssh.rb', line 75

def self.valid?(public_ssh_key)
  SSHKey.valid_ssh_public_key?(public_ssh_key)
end