# class Chef::Knife::Ssh

### Attributes

password[W]

### Public Instance Methods

configure_attribute() click to toggle source

    # File lib/chef/knife/ssh.rb, line 409
    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[:override_attribute] = config[:attribute] || Chef::Config[:knife][:ssh_attribute]
      config[:attribute] = (Chef::Config[:knife][:ssh_attribute] ||
                            config[:attribute] ||
                            "fqdn").strip
    end

configure_gateway() click to toggle source

    # File lib/chef/knife/ssh.rb, line 128
    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 475
    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 456
    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 144
    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 445
    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 421
    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

      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 480
    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 224
    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 276
    def get_password
      @password ||= prompt_for_password
    end

get_stripped_unfrozen_value(value) click to toggle source

    # File lib/chef/knife/ssh.rb, line 440
    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 309
    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 385
    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

    # File lib/chef/knife/ssh.rb, line 228
    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

print_line(host, data) click to toggle source

    # File lib/chef/knife/ssh.rb, line 245
    def print_line(host, data)
      padding = @longest - host.length
      str = ui.color(host, :cyan) + (" " * (padding + 1)) + data
      ui.msg(str)
    end

prompt_for_password(prompt = "Enter your password: ") click to toggle source

    # File lib/chef/knife/ssh.rb, line 280
    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 288
    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 305
    def reader
      Readline
    end

run() click to toggle source

    # File lib/chef/knife/ssh.rb, line 484
    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 338
    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 105
    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 190
    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 251
    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 356
    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