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
a stored list of the IPs dealt with by this provisioner
If true, will send all output to the furnish logger. Use merge_output
to get stderr as well.
If true, will merge stdout and stderr for purposes of output.
If true, output will not be stored or relayed. Useful for commands which will perform lots of output. log_output
is not affected.“
a hash of ip -> output, accessible after provision. overwritten on both startup and shutdown.
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 to use when authenticating. Optional, but this or private_key
or private_key_path
must be provided.
Private key to use when authenticating. Optional, but this or password
or private_key_path
must be provided.
Path to file on disk containing the private key to use when authenticating. Optional, but this or password
or private_key
must be provided.
How long to wait before giving up on SSH
returning. Default is 300 seconds. Fractional values OK.
If true, attempts to allocate a pty after connecting. If this fails, fails the provision. Default is false. Cannot be used with stdin
.
The command to run on each remote host when this provisioner runs shutdown. Either startup_command
or shutdown_command
must be provided.
The command to run on each remote host when this provisioner runs startup. Either startup_command
or shutdown_command
must be provided.
If a string is provided, will be provided to the executing command as standard input. Cannot be used with require_pty
.
If non-nil, exit statuses that are in the set will be considered successes. Default is to only treat 0 as a success.
Username which to SSH
in as. Required, no default.
Public Class Methods
Construct the SSH
provisioner.
- Requirements
-
username
must be provided. -
password
,private_key
, orprivate_key_path
must be provided, but only one of them. -
startup_command
orshutdown_command
must be provided. You may provide both. -
stdin
andrequire_pty
cannot be provided together.
-
# 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
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
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
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
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
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
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
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
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
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
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
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