class Chef::Knife::Ssh

Attributes

password[W]

Public Instance Methods

configure_attribute() click to toggle source
# File lib/chef/knife/ssh.rb, line 414
def configure_attribute
  # Setting 'knife[:ssh_attribute] = "foo"' in knife.rb => Chef::Config[:knife][:ssh_attribute] == 'foo'
  # Running 'knife ssh -a foo' => both Chef::Config[:knife][:ssh_attribute] && config[:attribute] == foo
  # Thus we can differentiate between a config file value and a command line override at this point by checking config[:attribute]
  # We can tell here if fqdn was passed from the command line, rather than being the default, by checking config[:attribute]
  # However, after here, we cannot tell these things, so we must preserve config[:attribute]
  config[:attribute_from_cli] = config[:attribute]
  config[:attribute] = (config[:attribute_from_cli] || Chef::Config[:knife][:ssh_attribute] || "fqdn").strip
end
configure_gateway() click to toggle source
# File lib/chef/knife/ssh.rb, line 129
def configure_gateway
  config[:ssh_gateway] ||= Chef::Config[:knife][:ssh_gateway]
  if config[:ssh_gateway]
    gw_host, gw_user = config[:ssh_gateway].split('@').reverse
    gw_host, gw_port = gw_host.split(':')
    gw_opts = gw_port ? { :port => gw_port } : {}

    session.via(gw_host, gw_user || config[:ssh_user], gw_opts)
  end
rescue Net::SSH::AuthenticationFailed
  user = gw_user || config[:ssh_user]
  prompt = "Enter the password for #{user}@#{gw_host}: "
  gw_opts.merge!(:password => prompt_for_password(prompt))
  session.via(gw_host, user, gw_opts)
end
configure_identity_file() click to toggle source
# File lib/chef/knife/ssh.rb, line 483
def configure_identity_file
  config[:identity_file] = get_stripped_unfrozen_value(config[:identity_file] ||
                       Chef::Config[:knife][:ssh_identity_file])
end
configure_password() click to toggle source

This is a bit overly complicated because of the way we want knife ssh to work with -P causing a password prompt for the user, but we have to be conscious that this code gets included in knife bootstrap and knife * server create as well. We want to change the semantics so that the default is false and 'nil' means -P without an argument on the command line. But the other utilities expect nil to be the default and we can't prompt in that case. So we effectively use ssh_password_ng to determine if we're coming from knife ssh or from the other utilities. The other utilties can also be patched to use ssh_password_ng easily as long they follow the convention that the default is false.

# File lib/chef/knife/ssh.rb, line 464
def configure_password
  if config.has_key?(:ssh_password_ng) && config[:ssh_password_ng].nil?
    # If the parameter is called on the command line with no value
    # it will set :ssh_password_ng = nil
    # This is where we want to trigger a prompt for password
    config[:ssh_password] = get_password
  else
    # if ssh_password_ng is false then it has not been set at all, and we may be in knife ec2 and still
    # using an old config[:ssh_password].  this is backwards compatibility.  all knife cloud plugins should
    # be updated to use ssh_password_ng with a default of false and ssh_password should be retired, (but
    # we'll still need to use the ssh_password out of knife.rb if we find that).
    ssh_password = config.has_key?(:ssh_password_ng) ? config[:ssh_password_ng] : config[:ssh_password]
    # Otherwise, the password has either been specified on the command line,
    # in knife.rb, or key based auth will be attempted
    config[:ssh_password] = get_stripped_unfrozen_value(ssh_password ||
                       Chef::Config[:knife][:ssh_password])
  end
end
configure_session() click to toggle source
# File lib/chef/knife/ssh.rb, line 145
def configure_session
  list = config[:manual] ?
         @name_args[0].split(" ") :
         search_nodes
  if list.length == 0
    if @action_nodes.length == 0
      ui.fatal("No nodes returned from search!")
    else
      ui.fatal("#{@action_nodes.length} #{@action_nodes.length > 1 ? "nodes":"node"} found, " +
               "but does not have the required attribute to establish the connection. " +
               "Try setting another attribute to open the connection using --attribute.")
    end
    exit 10
  end
  session_from_list(list)
end
configure_user() click to toggle source
# File lib/chef/knife/ssh.rb, line 453
def configure_user
  config[:ssh_user] = get_stripped_unfrozen_value(config[:ssh_user] ||
                       Chef::Config[:knife][:ssh_user])
end
cssh() click to toggle source
# File lib/chef/knife/ssh.rb, line 424
def cssh
  cssh_cmd = nil
  %w[csshX cssh].each do |cmd|
    begin
      # Unix and Mac only
      cssh_cmd = shell_out!("which #{cmd}").stdout.strip
      break
    rescue Mixlib::ShellOut::ShellCommandFailed
    end
  end
  raise Chef::Exceptions::Exec, "no command found for cssh" unless cssh_cmd

  # pass in the consolidated itentity file option to cssh(X)
  if config[:identity_file]
    cssh_cmd << " --ssh_args '-i #{File.expand_path(config[:identity_file])}'"
  end

  session.servers_for.each do |server|
    cssh_cmd << " #{server.user ? "#{server.user}@#{server.host}" : server.host}"
  end
  Chef::Log.debug("starting cssh session with command: #{cssh_cmd}")
  exec(cssh_cmd)
end
extract_nested_value(data_structure, path_spec) click to toggle source
# File lib/chef/knife/ssh.rb, line 488
def extract_nested_value(data_structure, path_spec)
  ui.presenter.extract_nested_value(data_structure, path_spec)
end
fixup_sudo(command) click to toggle source
# File lib/chef/knife/ssh.rb, line 229
def fixup_sudo(command)
  command.sub(/^sudo/, 'sudo -p \knife sudo password: \')
end
get_password() click to toggle source
# File lib/chef/knife/ssh.rb, line 281
def get_password
  @password ||= prompt_for_password
end
get_stripped_unfrozen_value(value) click to toggle source
# File lib/chef/knife/ssh.rb, line 448
def get_stripped_unfrozen_value(value)
  return nil if value.nil?
  value.strip
end
interactive() click to toggle source
# File lib/chef/knife/ssh.rb, line 314
def interactive
  puts "Connected to #{ui.list(session.servers_for.collect { |s| ui.color(s.host, :cyan) }, :inline, " and ")}"
  puts
  puts "To run a command on a list of servers, do:"
  puts "  on SERVER1 SERVER2 SERVER3; COMMAND"
  puts "  Example: on latte foamy; echo foobar"
  puts
  puts "To exit interactive mode, use 'quit!'"
  puts
  while 1
    command = read_line
    case command
    when 'quit!'
      puts 'Bye!'
      break
    when /^on (.+?); (.+)$/
      raw_list = $1.split(" ")
      server_list = Array.new
      session.servers.each do |session_server|
        server_list << session_server if raw_list.include?(session_server.host)
      end
      command = $2
      ssh_command(command, session.on(*server_list))
    else
      ssh_command(command)
    end
  end
end
macterm() click to toggle source
# File lib/chef/knife/ssh.rb, line 390
def macterm
  begin
    require 'appscript'
  rescue LoadError
    STDERR.puts "you need the rb-appscript gem to use knife ssh macterm. `(sudo) gem install rb-appscript` to install"
    raise
  end

  Appscript.app("/Applications/Utilities/Terminal.app").windows.first.activate
  Appscript.app("System Events").application_processes["Terminal.app"].keystroke("n", :using=>:command_down)
  term = Appscript.app('Terminal')
  window = term.windows.first.get

  (session.servers_for.size - 1).times do |i|
    window.activate
    Appscript.app("System Events").application_processes["Terminal.app"].keystroke("t", :using=>:command_down)
  end

  session.servers_for.each_with_index do |server, tab_number|
    cmd = "unset PROMPT_COMMAND; echo -e \"\\033]0;#{server.host}\\007\"; ssh #{server.user ? "#{server.user}@#{server.host}" : server.host}"
    Appscript.app('Terminal').do_script(cmd, :in => window.tabs[tab_number + 1].get)
  end
end
print_data(host, data) click to toggle source
print_line(host, data) click to toggle source
prompt_for_password(prompt = "Enter your password: ") click to toggle source
# File lib/chef/knife/ssh.rb, line 285
def prompt_for_password(prompt = "Enter your password: ")
  ui.ask(prompt) { |q| q.echo = false }
end
read_line() click to toggle source

Present the prompt and read a single line from the console. It also detects ^D and returns “exit” in that case. Adds the input to the history, unless the input is empty. Loops repeatedly until a non-empty line is input.

# File lib/chef/knife/ssh.rb, line 293
def read_line
  loop do
    command = reader.readline("#{ui.color('knife-ssh>', :bold)} ", true)

    if command.nil?
      command = "exit"
      puts(command)
    else
      command.strip!
    end

    unless command.empty?
      return command
    end
  end
end
reader() click to toggle source
# File lib/chef/knife/ssh.rb, line 310
def reader
  Readline
end
run() click to toggle source
# File lib/chef/knife/ssh.rb, line 492
def run
  extend Chef::Mixin::Command

  @longest = 0

  configure_attribute
  configure_user
  configure_password
  configure_identity_file
  configure_gateway
  configure_session

  exit_status =
  case @name_args[1]
  when "interactive"
    interactive
  when "screen"
    screen
  when "tmux"
    tmux
  when "macterm"
    macterm
  when "cssh"
    cssh
  when "csshx"
    Chef::Log.warn("knife ssh csshx will be deprecated in a future release")
    Chef::Log.warn("please use knife ssh cssh instead")
    cssh
  else
    ssh_command(@name_args[1..-1].join(" "))
  end

  session.close
  if exit_status != 0
    exit exit_status
  else
    exit_status
  end
end
screen() click to toggle source
# File lib/chef/knife/ssh.rb, line 343
def screen
  tf = Tempfile.new("knife-ssh-screen")
  if File.exist? "#{ENV["HOME"]}/.screenrc"
    tf.puts("source #{ENV["HOME"]}/.screenrc")
  end
  tf.puts("caption always '%-Lw%{= BW}%50>%n%f* %t%{-}%+Lw%<'")
  tf.puts("hardstatus alwayslastline 'knife ssh #{@name_args[0]}'")
  window = 0
  session.servers_for.each do |server|
    tf.print("screen -t \"#{server.host}\" #{window} ssh ")
    tf.print("-i #{config[:identity_file]} ") if config[:identity_file]
    server.user ? tf.puts("#{server.user}@#{server.host}") : tf.puts(server.host)
    window += 1
  end
  tf.close
  exec("screen -c #{tf.path}")
end
session() click to toggle source
# File lib/chef/knife/ssh.rb, line 106
def session
  config[:on_error] ||= :skip
  ssh_error_handler = Proc.new do |server|
    if config[:manual]
      node_name = server.host
    else
      @action_nodes.each do |n|
        node_name = n if format_for_display(n)[config[:attribute]] == server.host
      end
    end
    case config[:on_error]
    when :skip
      ui.warn "Failed to connect to #{server.host} -- #{$!.class.name}: #{$!.message}"
      $!.backtrace.each { |l| Chef::Log.debug(l) }
    when :raise
      #Net::SSH::Multi magic to force exception to be re-raised.
      throw :go, :raise
    end
  end

  @session ||= Net::SSH::Multi.start(:concurrent_connections => config[:concurrency], :on_error => ssh_error_handler)
end
session_from_list(list) click to toggle source
# File lib/chef/knife/ssh.rb, line 195
def session_from_list(list)
  list.each do |item|
    host, ssh_port = item
    Chef::Log.debug("Adding #{host}")
    session_opts = {}

    ssh_config = Net::SSH.configuration_for(host)

    # Chef::Config[:knife][:ssh_user] is parsed in #configure_user and written to config[:ssh_user]
    user = config[:ssh_user] || ssh_config[:user]
    hostspec = user ? "#{user}@#{host}" : host
    session_opts[:keys] = File.expand_path(config[:identity_file]) if config[:identity_file]
    session_opts[:keys_only] = true if config[:identity_file]
    session_opts[:password] = config[:ssh_password] if config[:ssh_password]
    session_opts[:forward_agent] = config[:forward_agent]
    session_opts[:port] = config[:ssh_port] ||
                          ssh_port || # Use cloud port if available
                          Chef::Config[:knife][:ssh_port] ||
                          ssh_config[:port]
    session_opts[:logger] = Chef::Log.logger if Chef::Log.level == :debug

    if !config[:host_key_verify]
      session_opts[:paranoid] = false
      session_opts[:user_known_hosts_file] = "/dev/null"
    end

    session.use(hostspec, session_opts)

    @longest = host.length if host.length > @longest
  end

  session
end
ssh_command(command, subsession=nil) click to toggle source
# File lib/chef/knife/ssh.rb, line 256
def ssh_command(command, subsession=nil)
  exit_status = 0
  subsession ||= session
  command = fixup_sudo(command)
  command.force_encoding('binary') if command.respond_to?(:force_encoding)
  subsession.open_channel do |ch|
    ch.request_pty
    ch.exec command do |ch, success|
      raise ArgumentError, "Cannot execute #{command}" unless success
      ch.on_data do |ichannel, data|
        print_data(ichannel[:host], data)
        if data =~ /^knife sudo password: /
          print_data(ichannel[:host], "\n")
          ichannel.send_data("#{get_password}\n")
        end
      end
      ch.on_request "exit-status" do |ichannel, data|
        exit_status = [exit_status, data.read_long].max
      end
    end
  end
  session.loop
  exit_status
end
tmux() click to toggle source
# File lib/chef/knife/ssh.rb, line 361
def tmux
  ssh_dest = lambda do |server|
    identity = "-i #{config[:identity_file]} " if config[:identity_file]
    prefix = server.user ? "#{server.user}@" : ""
    "'ssh #{identity}#{prefix}#{server.host}'"
  end

  new_window_cmds = lambda do
    if session.servers_for.size > 1
      [""] + session.servers_for[1..-1].map do |server|
        "new-window -a -n '#{server.host}' #{ssh_dest.call(server)}"
      end
    else
      []
    end.join(" \\; ")
  end

  tmux_name = "'knife ssh #{@name_args[0].gsub(/:/,'=')}'"
  begin
    server = session.servers_for.first
    cmd = ["tmux new-session -d -s #{tmux_name}",
           "-n '#{server.host}'", ssh_dest.call(server),
           new_window_cmds.call].join(" ")
    shell_out!(cmd)
    exec("tmux attach-session -t #{tmux_name}")
  rescue Chef::Exceptions::Exec
  end
end

Private Instance Methods

search_nodes() click to toggle source
# File lib/chef/knife/ssh.rb, line 162
def search_nodes
  list = Array.new
  query = Chef::Search::Query.new
  @action_nodes = query.search(:node, @name_args[0])[0]
  @action_nodes.each do |item|
    # we should skip the loop to next iteration if the item
    # returned by the search is nil
    next if item.nil?
    # if a command line attribute was not passed, and we have a
    # cloud public_hostname, use that.  see #configure_attribute
    # for the source of config[:attribute] and
    # config[:attribute_from_cli]
    if config[:attribute_from_cli]
      Chef::Log.debug("Using node attribute '#{config[:attribute_from_cli]}' from the command line as the ssh target")
      host = extract_nested_value(item, config[:attribute_from_cli])
    elsif item[:cloud] && item[:cloud][:public_hostname]
      Chef::Log.debug("Using node attribute 'cloud[:public_hostname]' automatically as the ssh target")
      host = item[:cloud][:public_hostname]
    else
      # ssh attribute from a configuration file or the default will land here
      Chef::Log.debug("Using node attribute '#{config[:attribute]}' as the ssh target")
      host = extract_nested_value(item, config[:attribute])
    end
    # next if we couldn't find the specified attribute in the
    # returned node object
    next if host.nil?
    ssh_port = item[:cloud].nil? ? nil : item[:cloud][:public_ssh_port]
    srv = [host, ssh_port]
    list.push(srv)
  end
  list
end