class Chef::Knife::Ssh

def configure_gateway

def configure_gateway
  if config[:ssh_gateway]
    gw_host, gw_user = config[:ssh_gateway].split("@").reverse
    gw_host, gw_port = gw_host.split(":")
    gw_opts = session_options(gw_host, gw_port, gw_user, gateway: true)
    user = gw_opts.delete(:user)
    begin
      # Try to connect with a key.

      session.via(gw_host, user, gw_opts)
    rescue Net::SSH::AuthenticationFailed
      prompt = "Enter the password for #{user}@#{gw_host}: "
      gw_opts[:password] = prompt_for_password(prompt)
      # Try again with a password.

      session.via(gw_host, user, gw_opts)
    end
  end
end

def configure_password

def configure_password
  if config.key?(:ssh_password) && config[:ssh_password].nil?
    # if we have an actual nil that means someone called "--ssh-password" with no value, so we prompt for a password

    config[:ssh_password] = get_password
  else
    # the false default of ssh_password results in a nil here

    config[:ssh_password] = get_stripped_unfrozen_value(config[:ssh_password])
  end
end

def configure_session

def configure_session
  list = config[:manual] ? @name_args[0].split(" ") : search_nodes
  if list.length == 0
    if @search_count == 0
      ui.fatal("No nodes returned from search")
    else
      ui.fatal("#{@search_count} #{@search_count > 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
  if %i{warn fatal}.include?(config[:duplicated_fqdns])
    fqdns = list.map { |v| v[0] }
    if fqdns.count != fqdns.uniq.count
      duplicated_fqdns = fqdns.uniq
      ui.send(config[:duplicated_fqdns],
        "SSH #{duplicated_fqdns.count > 1 ? "nodes are" : "node is"} " +
        "duplicated: #{duplicated_fqdns.join(",")}")
      exit 10 if config[:duplicated_fqdns] == :fatal
    end
  end
  session_from_list(list)
end

def configure_ssh_gateway_identity

def configure_ssh_gateway_identity
  config[:ssh_gateway_identity] = get_stripped_unfrozen_value(config[:ssh_gateway_identity])
end

def configure_ssh_identity_file

def configure_ssh_identity_file
  config[:ssh_identity_file] = get_stripped_unfrozen_value(config[:ssh_identity_file])
end

def configure_user

def configure_user
  config[:ssh_user] = get_stripped_unfrozen_value(config[:ssh_user] ||
                       Chef::Config[:knife][:ssh_user])
end

def cssh

def cssh
  cssh_cmd = nil
  %w{csshX cssh}.each do |cmd|
    # Unix and Mac only

    cssh_cmd = shell_out!("which #{cmd}").stdout.strip
    break
  rescue Mixlib::ShellOut::ShellCommandFailed
  end
  raise Chef::Exceptions::Exec, "no command found for cssh" unless cssh_cmd
  # pass in the consolidated identity file option to cssh(X)

  if config[:ssh_identity_file]
    cssh_cmd << " --ssh_args '-i #{File.expand_path(config[:ssh_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

def fixup_sudo(command)

def fixup_sudo(command)
  command.sub(/^sudo/, "sudo -p 'knife sudo password: '")
end

def get_password

def get_password
  @password ||= prompt_for_password
end

def get_prefix_attribute(item)

def get_prefix_attribute(item)
  # Order of precedence for prefix

  # 1) config value (cli or knife config)

  # 2) nil

  msg = "Using node attribute '%s' as the prefix: %s"
  if item["prefix"]
    Chef::Log.debug(sprintf(msg, config[:prefix_attribute], item["prefix"]))
    item["prefix"]
  else
    nil
  end
end

def get_ssh_attribute(item)

def get_ssh_attribute(item)
  # Order of precedence for ssh target

  # 1) config value (cli or knife config)

  # 2) cloud attribute

  # 3) fqdn

  msg = "Using node attribute '%s' as the ssh target: %s"
  if item["target"]
    Chef::Log.debug(sprintf(msg, config[:ssh_attribute], item["target"]))
    item["target"]
  elsif !item.dig("cloud", "public_hostname").to_s.empty?
    Chef::Log.debug(sprintf(msg, "cloud.public_hostname", item["cloud"]["public_hostname"]))
    item["cloud"]["public_hostname"]
  else
    Chef::Log.debug(sprintf(msg, "fqdn", item["fqdn"]))
    item["fqdn"]
  end
end

def get_stripped_unfrozen_value(value)

def get_stripped_unfrozen_value(value)
  return nil unless value
  value.strip
end

def interactive

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
  loop do
    command = read_line
    case command
    when "quit!"
      puts "Bye!"
      break
    when /^on (.+?); (.+)$/
      raw_list = $1.split(" ")
      server_list = []
      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

def macterm

def macterm
  begin
    require "appscript" unless defined?(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

def print_data(host, data)

def print_data(host, data)
  @buffers ||= {}
  if leftover = @buffers[host]
    @buffers[host] = nil
    print_data(host, leftover + data)
  else
    if newline_index = data.index("\n")
      line = data.slice!(0...newline_index)
      data.slice!(0)
      print_line(host, line)
      print_data(host, data)
    else
      @buffers[host] = data
    end
  end
end

def print_line(host, data)

def print_line(host, data)
  padding = @longest - host.length
  str = ui.color(host, :cyan) + (" " * (padding + 1)) + data
  ui.msg(str)
end

def prompt_for_password(prompt = "Enter your password: ")

def prompt_for_password(prompt = "Enter your password: ")
  ui.ask(prompt, echo: false)
end

def read_line

line is input.
history, unless the input is empty. Loops repeatedly until a non-empty
detects ^D and returns "exit" in that case. Adds the input to the
Present the prompt and read a single line from the console. It also
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

def reader

def reader
  Readline
end

def run

def run
  @longest = 0
  if @name_args.length < 1
    show_usage
    ui.fatal("You must specify the SEARCH QUERY.")
    exit(1)
  end
  configure_user
  configure_password
  @password = config[:ssh_password] if config[:ssh_password]
  # If a password was not given, check for SSH identity file.

  unless @password
    configure_ssh_identity_file
    configure_ssh_gateway_identity
  end
  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
    else
      ssh_command(@name_args[1..].join(" "))
    end
  session.close
  if exit_status && exit_status != 0
    exit exit_status
  else
    exit_status
  end
end

def screen

def screen
  tf = Tempfile.new("knife-ssh-screen")
  ChefConfig::PathHelper.home(".screenrc") do |screenrc_path|
    if File.exist? screenrc_path
      tf.puts("source #{screenrc_path}")
    end
  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[:ssh_identity_file]} ") if config[:ssh_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

def search_nodes

def search_nodes
  list = []
  query = Chef::Search::Query.new
  required_attributes = { fqdn: ["fqdn"], cloud: ["cloud"] }
  separator = ui.presenter.attribute_field_separator
  if config[:prefix_attribute]
    required_attributes[:prefix] = config[:prefix_attribute].split(separator)
  end
  if config[:ssh_attribute]
    required_attributes[:target] = config[:ssh_attribute].split(separator)
  end
  @search_count = 0
  query.search(:node, @name_args[0], filter_result: required_attributes, fuzz: true) do |item|
    @search_count += 1
    # we should skip the loop to next iteration if the item

    # returned by the search is nil

    next if item.nil?
    # next if we couldn't find the specified attribute in the

    # returned node object

    host = get_ssh_attribute(item)
    next if host.nil?
    prefix = get_prefix_attribute(item)
    ssh_port = item.dig("cloud", "public_ssh_port")
    srv = [host, ssh_port, prefix]
    list.push(srv)
  end
  list
end

def session

def session
  ssh_error_handler = Proc.new do |server|
    if config[:on_error]
      # Net::SSH::Multi magic to force exception to be re-raised.

      throw :go, :raise
    else
      ui.warn "Failed to connect to #{server.host} -- #{$!.class.name}: #{$!.message}"
      $!.backtrace.each { |l| Chef::Log.debug(l) }
    end
  end
  @session ||= Net::SSH::Multi.start(concurrent_connections: config[:concurrency], on_error: ssh_error_handler)
end

def session_from_list(list)

def session_from_list(list)
  list.each do |item|
    host, ssh_port, prefix = item
    prefix = host unless prefix
    Chef::Log.debug("Adding #{host}")
    session_opts = session_options(host, ssh_port, gateway: false)
    # Handle port overrides for the main connection.

    session_opts[:port] = config[:ssh_port] if config[:ssh_port]
    # Handle connection timeout

    session_opts[:timeout] = config[:ssh_timeout] if config[:ssh_timeout]
    # Handle session prefix

    session_opts[:properties] = { prefix: prefix }
    # Create the hostspec.

    hostspec = session_opts[:user] ? "#{session_opts.delete(:user)}@#{host}" : host
    # Connect a new session on the multi.

    session.use(hostspec, session_opts)
    @longest = prefix.length if prefix.length > @longest
  end
  session
end

def session_options(host, port, user = nil, gateway: false)

Returns:
  • (Hash) -

Parameters:
  • gateway (Boolean) -- Flag: host or gateway key
  • user (String) -- Optional username for this session.
  • port (String) -- SSH port for this session.
  • host (String) -- Hostname for this session.

Other tags:
    Since: - 12.5.0
def session_options(host, port, user = nil, gateway: false)
  ssh_config = Net::SSH.configuration_for(host, true)
  {}.tap do |opts|
    opts[:user] = user || config[:ssh_user] || ssh_config[:user]
    if !gateway && config[:ssh_identity_file]
      opts[:keys] = File.expand_path(config[:ssh_identity_file])
      opts[:keys_only] = true
    elsif gateway && config[:ssh_gateway_identity]
      opts[:keys] = File.expand_path(config[:ssh_gateway_identity])
      opts[:keys_only] = true
    elsif config[:ssh_password]
      opts[:password] = config[:ssh_password]
    end
    # Don't set the keys to nil if we don't have them.

    forward_agent = config[:forward_agent] || ssh_config[:forward_agent]
    opts[:forward_agent] = forward_agent unless forward_agent.nil?
    port ||= ssh_config[:port]
    opts[:port] = port unless port.nil?
    opts[:logger] = Chef::Log.with_child(subsystem: "net/ssh") if Chef::Log.level == :trace
    unless config[:host_key_verify]
      opts[:verify_host_key] = :never
      opts[:user_known_hosts_file] = "/dev/null"
    end
    if ssh_config[:keepalive]
      opts[:keepalive] = true
      opts[:keepalive_interval] = ssh_config[:keepalive_interval]
    end
    # maintain support for legacy key types / ciphers / key exchange algorithms.

    # most importantly this adds back support for DSS host keys

    # See https://github.com/net-ssh/net-ssh/pull/709

    opts[:append_all_supported_algorithms] = true
  end
end

def ssh_command(command, session_list = session)

Parameters:
  • session_list (???) -- list of sessions, one per node
  • command (String) -- the command to run
def ssh_command(command, session_list = session)
  stderr = ""
  exit_status = 0
  command = fixup_sudo(command)
  command.force_encoding("binary") if command.respond_to?(:force_encoding)
  session_list.open_channel do |chan|
    if config[:on_error] && exit_status != 0
      chan.close
    else
      if config[:pty]
        chan.request_pty do |ch, success|
          unless success
            ui.warn("Failed to obtain a PTY from #{ch.connection.host}")
            raise ArgumentError, "Request for PTY failed" if config[:require_pty]
          end
        end
      end
      chan.exec command do |ch, success|
        raise ArgumentError, "Cannot execute #{command}" unless success
        ch.on_data do |ichannel, data|
          print_data(ichannel.connection[:prefix], data)
          if /^knife sudo password: /.match?(data)
            print_data(ichannel.connection[:prefix], "\n")
            ichannel.send_data("#{get_password}\n")
          end
        end
        ch.on_extended_data do |_, _type, data|
          raise ArgumentError, "No PTY present. If a PTY is required use --require-pty" if data.eql?("sudo: no tty present and no askpass program specified\n")
          stderr += data
        end
        ch.on_request "exit-status" do |ichannel, data|
          exit_status = [exit_status, data.read_long].max
        end
      end
    end
  end
  session.loop
  exit_status
ensure
  session_list.close
end

def tmux

def tmux
  ssh_dest = lambda do |server|
    identity = "-i #{config[:ssh_identity_file]} " if config[:ssh_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..].map do |server|
        if config[:tmux_split]
          "split-window #{ssh_dest.call(server)}; tmux select-layout tiled"
        else
          "new-window -a -n '#{server.host}' #{ssh_dest.call(server)}"
        end
      end
    else
      []
    end.join(" \\; ")
  end
  tmux_name = "'knife ssh #{@name_args[0].tr(":.", "=-")}'"
  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