class Furnish::Provisioner::SSH

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

Attributes

ips[R]

a stored list of the IPs dealt with by this provisioner

log_output[RW]

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

merge_output[RW]

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

mute_output[RW]

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

output[R]

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

paranoid[RW]

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.

password[RW]

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

private_key[RW]

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

private_key_path[RW]

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

provision_wait[RW]

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

require_pty[RW]

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

shutdown_command[RW]

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

startup_command[RW]

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

stdin[RW]

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

success[RW]

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

username[RW]

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

Public Class Methods

new(args) click to toggle source

Construct the SSH provisioner.

Requirements
Calls superclass method
# 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

Public Instance Methods

check_auth_args() click to toggle source

Predicate for determining requirements for ::new.

# 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_args() click to toggle source

Predicate for determining requirements for ::new.

# 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_pty() click to toggle source

Predicate for determining requirements for ::new.

# 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) click to toggle source

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

# 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
noop() click to toggle source

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

# 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
report() click to toggle source

Outputs the commands if they exist.

# 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
run_ssh_provision(provision_command) click to toggle source

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.

# 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={}) click to toggle source

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

# 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
ssh(host, cmd) click to toggle source

Performs the actual connection and execution.

# 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_options() click to toggle source

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

# 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={}) click to toggle source

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.

# File lib/furnish/provisioners/ssh.rb, line 445
def startup(args={})
  @ips = args[:ips]
  run_ssh_provision(startup_command)
end