fnichol/knife-server

View on GitHub
lib/knife/server/ssh.rb

Summary

Maintainability
A
0 mins
Test Coverage
# -*- encoding: utf-8 -*-
#
# Author:: Fletcher Nichol (<fnichol@nichol.ca>)
# Copyright:: Copyright (c) 2012 Fletcher Nichol
# License:: Apache License, Version 2.0
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#

require "net/ssh"

module Knife
  module Server
    # Communicates with an SSH node.
    class SSH
      DEFAULT_OPTIONS = { :user => "root", :port => "22" }.freeze
      USER_SWITCH_COMMAND =
        %{sudo USER=root HOME="$(getent passwd root | cut -d : -f 6)"}.freeze

      def initialize(params)
        options = DEFAULT_OPTIONS.merge(params)

        @host = options.delete(:host)
        @user = options.delete(:user)
        @options = options
      end

      def exec!(cmd)
        result = ""
        exit_code = nil
        Net::SSH.start(@host, @user, @options) do |session|
          exit_code = ssh_session(session, full_cmd(cmd), result)
        end
        if exit_code != 0
          raise "SSH exited with code #{exit_code} for [#{full_cmd(cmd)}]"
        end
        result
      end

      def full_cmd(cmd)
        if @user == "root"
          cmd
        else
          [USER_SWITCH_COMMAND, %{bash -c '#{cmd}'}].join(" ")
        end
      end

      def ssh_session(session, cmd, result)
        exit_code = nil
        session.open_channel do |channel|

          channel.request_pty

          channel.exec(cmd) do |_ch, _success|

            channel.on_data do |_ch, data|
              result << data
            end

            channel.on_extended_data do |_ch, _type, data|
              result << data
            end

            channel.on_request("exit-status") do |_ch, data|
              exit_code = data.read_long
            end
          end
        end

        session.loop
        exit_code
      end

      # runs a script on the target host by passing it to the stdin of a sh
      # process. returns stdout and the exit status. does not care about stderr.
      def run_script(content)
        user_switch = ""

        unless @user == "root"
          user_switch = USER_SWITCH_COMMAND
        end

        wrapper = <<-EOF
        if [ -e /dev/fd/0 ]
        then
          #{user_switch} /bin/sh /dev/fd/0
        elif [ -e /dev/stdin ]
        then
          #{user_switch} /bin/sh /dev/stdin
        else
          echo "Cannot find method of communicating with the shell via stdin"
          exit 1
        fi
        EOF

        exec_ssh(wrapper, content)
      end

      def exec_ssh(wrapper, content) # rubocop:disable Metrics/MethodLength
        result = ""
        exit_status = nil

        Net::SSH.start(@host, @user, @options) do |ssh|
          ssh.open_channel do |ch|
            ch.on_open_failed do |_, _, desc|
              raise "Connection Error to #{ip}: #{desc}"
            end

            ch.exec(wrapper) do |channel, _, _|
              # spit out the shell script and close stdin so sh can do its magic
              channel.send_data(content)
              channel.eof!

              # then we just wait for sweet, sweet output
              channel.on_data do |_, data|
                result << data
              end

              channel.on_request("exit-status") do |_, data|
                exit_status = data.read_long
              end
            end

            ch.wait
          end

          ssh.loop
        end

        [result, exit_status]
      end
    end
  end
end