Class: Furnish::Provisioner::SSH

Inherits:
API
  • Object
show all
Includes:
Logger::Mixins
Defined in:
lib/furnish/provisioners/ssh.rb

Overview

Provisioner to execute SSH commands on remote targets during startup and shutdown. See ::new for construction requirements.

Please see Net::SSH::Verifiers::NoSaveStrict and #paranoid about how host keys are handled in a surprising way.

Startup
  • requires:

    • ips (Set<String>)

      list of IP addresses to target

  • yields:

    • ssh_exit_statuses (Hash<String, Integer>)

      IP -> exit status map

    • ssh_output (Hash<String, String>)

      IP -> command output map

Shutdown
  • accepts:

    • ips (Set<String>)

      list of IP addresses to target

  • yields:

    • ssh_exit_statuses (Hash<String, Integer>)

      IP -> exit status map

    • ssh_output (Hash<String, String>)

      IP -> command output map

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(args) ⇒ SSH

Construct the SSH provisioner.

Requirements
  • #username must be provided.

  • #password, #private_key, or #private_key_path must be provided, but only one of them.

  • #startup_command or #shutdown_command must be provided. You may provide both.

  • #stdin and #require_pty cannot be provided together.



226
227
228
229
230
231
232
233
234
235
# File 'lib/furnish/provisioners/ssh.rb', line 226

def initialize(args)
  super
  check_auth_args
  check_command_args
  check_stdin_pty

  @paranoid         = args.has_key?(:paranoid) ? args[:paranoid] : :no_save_strict
  @provision_wait ||= 300
  @success        ||= Set[0]
end

Instance Attribute Details

#ipsObject (readonly)

a stored list of the IPs dealt with by this provisioner



206
207
208
# File 'lib/furnish/provisioners/ssh.rb', line 206

def ips
  @ips
end

#outputObject (readonly)

a hash of ip -> output, accessible after provision. overwritten on both startup and shutdown.



210
211
212
# File 'lib/furnish/provisioners/ssh.rb', line 210

def output
  @output
end

Instance Method Details

#check_auth_argsObject

Predicate for determining requirements for ::new.



258
259
260
261
262
263
264
265
266
267
268
269
270
# File 'lib/furnish/provisioners/ssh.rb', line 258

def check_auth_args
  unless username
    raise ArgumentError, "username must be provided"
  end

  unless password or private_key or private_key_path
    raise ArgumentError, "password, private_key, or private_key_path must be provided"
  end

  if [password, private_key, private_key_path].compact.count > 1
    raise ArgumentError, "You may only supply one of password, private_key, or private_key_path."
  end
end

#check_command_argsObject

Predicate for determining requirements for ::new.



249
250
251
252
253
# File 'lib/furnish/provisioners/ssh.rb', line 249

def check_command_args
  unless startup_command or shutdown_command
    raise ArgumentError, "startup_command or shutdown_command must be provided at minimum."
  end
end

#check_stdin_ptyObject

Predicate for determining requirements for ::new.



240
241
242
243
244
# File 'lib/furnish/provisioners/ssh.rb', line 240

def check_stdin_pty
  if stdin and require_pty
    raise ArgumentError, "stdin and require_pty are incompatible -- if used together, will hang the provision."
  end
end

#log(host, output) ⇒ Object

Checks #log_output and logs the output with the host if set.



275
276
277
278
279
280
281
282
# File 'lib/furnish/provisioners/ssh.rb', line 275

def log(host, output)
  if log_output
    if_debug do
      print "[#{host}] #{output}"
      flush
    end
  end
end

#log_outputObject

:attr: log_output

If true, will send all output to the furnish logger. Use #merge_output to get stderr as well.



171
172
# File 'lib/furnish/provisioners/ssh.rb', line 171

furnish_property :log_output,
"If true, will send all output to the furnish logger. Use merge_output to get stderr as well."

#merge_outputObject

:attr: merge_output

If true, will merge stdout and stderr for purposes of output.



162
163
# File 'lib/furnish/provisioners/ssh.rb', line 162

furnish_property :merge_output,
"If true, will merge stdout and stderr for purposes of output."

#mute_outputObject

:attr: mute_output

If true, output will not be stored or relayed. Useful for commands which will perform lots of output. log_output is not affected.“



192
193
# File 'lib/furnish/provisioners/ssh.rb', line 192

furnish_property :mute_output,
"If true, output will not be stored or relayed. Useful for commands which will perform lots of output. log_output is not affected."

#noopObject

What happens when we can’t execute something (e.g., because of a missing command). Just some boilerplate values.



433
434
435
436
437
438
# File 'lib/furnish/provisioners/ssh.rb', line 433

def noop
  exit_statuses = Hash[ips.map { |ip| [ip, 0] }]
  @output       = Hash[ips.map { |ip| [ip, ""] }]

  return({ :ssh_exit_statuses => exit_statuses, :ssh_output => output })
end

#paranoidObject

:attr: paranoid

Maps to Net::SSH.start’s :paranoid option. If :no_save_strict is assigned (the default), will use our Net::SSH::Verifiers::NoSaveStrict verifier which will not attempt to save any host keys that we do not recognize. Any that do exist however will be checked appropriately.



183
184
# File 'lib/furnish/provisioners/ssh.rb', line 183

furnish_property :paranoid,
"Maps to Net::SSH.start's :paranoid option, used for host key validation. Use :no_save_strict (the default) to get something similar to :strict that doesn't save on an unknown key."

#passwordObject

:attr: password

Password to use when authenticating. Optional, but this or #private_key or #private_key_path must be provided.



93
94
95
# File 'lib/furnish/provisioners/ssh.rb', line 93

furnish_property :password,
"Password to use when authenticating. Optional, but this or private_key or private_key_path must be provided.",
String

#private_keyObject

:attr: private_key

Private key to use when authenticating. Optional, but this or #password or #private_key_path must be provided.



103
104
105
# File 'lib/furnish/provisioners/ssh.rb', line 103

furnish_property :private_key,
"Private key to use when authenticating. Optional, but this or password or private_key_path must be provided.",
String

#private_key_pathObject

:attr: private_key_path

Path to file on disk containing the private key to use when authenticating. Optional, but this or #password or #private_key must be provided.



114
115
116
# File 'lib/furnish/provisioners/ssh.rb', line 114

furnish_property :private_key_path,
"Path to file on disk containing the private key to use when authenticating. Optional, but this or password or private_key must be provided.",
String

#provision_waitObject

:attr: provision_wait

How long to wait before giving up on SSH returning. Default is 300 seconds. Fractional values OK.



74
75
76
# File 'lib/furnish/provisioners/ssh.rb', line 74

furnish_property :provision_wait,
"How long to wait before giving up on SSH returning. Default is 300 seconds. Fractional values OK.",
Numeric

#reportObject

Outputs the commands if they exist.



467
468
469
470
471
472
473
474
475
476
477
478
479
# File 'lib/furnish/provisioners/ssh.rb', line 467

def report
  a = []

  if startup_command
    a.push("startup: '#{startup_command}'")
  end

  if shutdown_command
    a.push("shutdown: '#{shutdown_command}'")
  end

  return a
end

#require_ptyObject

:attr: require_pty

If true, attempts to allocate a pty after connecting. If this fails, fails the provision. Default is false. Cannot be used with #stdin.



144
145
# File 'lib/furnish/provisioners/ssh.rb', line 144

furnish_property :require_pty,
"If true, attempts to allocate a pty after connecting. If this fails, fails the provision. Default is false. Cannot be used with stdin."

#run_ssh_provision(provision_command) ⇒ Object

Runs multiple ssh commands in threads, monitors those threads and stuffs status information. Will return #noop unless a command is provided. Called by #startup and #shutdown.



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
# File 'lib/furnish/provisioners/ssh.rb', line 384

def run_ssh_provision(provision_command)
  unless provision_command
    return noop
  end

  #
  # XXX sorry for the ugly. creates a IP => Thread map for tracking return values.
  #
  thread_map = Hash[
    ips.map do |ip|
      [
        ip,
        Thread.new do
          ssh(ip, provision_command)
        end
      ]
    end
  ]

  # FIXME see TODO about output handling
  exit_statuses = { }
  @output       = { }

  begin
    Timeout.timeout(provision_wait) do
      thread_map.each do |ip, thr|
        result = thr.value # exception will happen here.

        output[ip]        = mute_output ? "" : result[:stdout]
        exit_statuses[ip] = result[:exit_status]
      end
    end
  rescue TimeoutError
    thread_map.values.each { |t| t.kill if t.alive? }
    raise "timeout reached waiting for hosts '#{ips.join(', ')}'"
  end

  if exit_statuses.values.all? { |x| success.any? { |c| x == c } }
    return({ :ssh_exit_statuses => exit_statuses, :ssh_output => output })
  else
    # FIXME log
    return false
  end
end

#shutdown(args = {}) ⇒ Object

Deprovision: run the command. If no ips are provided from a previous provisioner, use the IPs gathered during startup.



454
455
456
457
458
459
460
461
462
# File 'lib/furnish/provisioners/ssh.rb', line 454

def shutdown(args={})
  # XXX use the IPs we got during startup if we didn't get a new set.
  if args[:ips] and !args[:ips].empty?
    @ips = args[:ips]
  end

  return false if !ips or ips.empty?
  run_ssh_provision(shutdown_command)
end

#shutdown_commandObject

:attr: shutdown_command

The command to run on each remote host when this provisioner runs shutdown. Either #startup_command or #shutdown_command must be provided.



134
135
136
# File 'lib/furnish/provisioners/ssh.rb', line 134

furnish_property :shutdown_command,
"The command to run on each remote host when this provisioner runs shutdown. Either startup_command or shutdown_command must be provided.",
String

#ssh(host, cmd) ⇒ Object

Performs the actual connection and execution.



313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
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
364
365
366
367
368
369
370
371
372
373
374
375
376
377
# File 'lib/furnish/provisioners/ssh.rb', line 313

def ssh(host, cmd)
  ret = {
    :exit_status => 0,
    :stdout      => "",
    :stderr      => ""
  }

  Net::SSH.start(host, username, ssh_options) do |ssh|
    ssh.open_channel do |ch|
      if stdin
        ch.send_data(stdin)
        ch.eof!
      end

      if require_pty
        ch.request_pty do |ch, success|
          unless success
            raise "The use_sudo setting requires a PTY, and your SSH is rejecting our attempt to get one."
          end
        end
      end

      ch.on_open_failed do |ch, code, desc|
        raise "Connection Error to #{username}@#{host}: #{desc}"
      end

      ch.exec(cmd) do |ch, success|
        unless success
          raise "Could not execute command '#{cmd}' on #{username}@#{host}"
        end

        if merge_output
          ch.on_data do |ch, data|
            log(host, data)
            ret[:stdout] << data
          end

          ch.on_extended_data do |ch, type, data|
            if type == 1
              log(host, data)
              ret[:stdout] << data
            end
          end
        else
          ch.on_data do |ch, data|
            log(host, data)
            ret[:stdout] << data
          end

          ch.on_extended_data do |ch, type, data|
            ret[:stderr] << data if type == 1
          end
        end

        ch.on_request("exit-status") do |ch, data|
          ret[:exit_status] = data.read_long
        end
      end
    end

    ssh.loop
  end

  return ret
end

#ssh_optionsObject

Constructs the proper hash for Net::SSH.start options.



287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
# File 'lib/furnish/provisioners/ssh.rb', line 287

def ssh_options
  opts = {
    :config     => false,
    :keys_only  => private_key_path || private_key
  }

  if password
    opts[:password] = password
  elsif private_key
    opts[:key_data] = [private_key]
  elsif private_key_path
    opts[:keys] = private_key_path
  end

  opts[:paranoid] = paranoid

  if opts[:paranoid] == :no_save_strict
    opts[:paranoid] = Net::SSH::Verifiers::NoSaveStrict.new
  end

  return opts
end

#startup(args = {}) ⇒ Object

Provision: run the command on all hosts and return the status from #run_ssh_provision. Will stuff the ips if passed regardless, so they can be used for #shutdown when nothing is expected to run in #startup.



445
446
447
448
# File 'lib/furnish/provisioners/ssh.rb', line 445

def startup(args={})
  @ips = args[:ips]
  run_ssh_provision(startup_command)
end

#startup_commandObject

:attr: startup_command

The command to run on each remote host when this provisioner runs startup. Either #startup_command or #shutdown_command must be provided.



124
125
126
# File 'lib/furnish/provisioners/ssh.rb', line 124

furnish_property :startup_command,
"The command to run on each remote host when this provisioner runs startup. Either startup_command or shutdown_command must be provided.",
String

#stdinObject

:attr: stdin

If a string is provided, will be provided to the executing command as standard input. Cannot be used with #require_pty.



153
154
155
# File 'lib/furnish/provisioners/ssh.rb', line 153

furnish_property :stdin,
"If a string is provided, will be provided to the executing command as standard input. Cannot be used with require_pty.",
String

#successObject

:attr: success

If non-nil, exit statuses that are in the set will be considered successes. Default is to only treat 0 as a success.



201
202
203
# File 'lib/furnish/provisioners/ssh.rb', line 201

furnish_property :success,
"If non-nil, exit statuses that are in the set will be considered successes.",
Set

#usernameObject

:attr: username

Username which to SSH in as. Required, no default.



83
84
85
# File 'lib/furnish/provisioners/ssh.rb', line 83

furnish_property :username,
"Username which to SSH in as. Required, no default.",
String