# -*- encoding: utf-8 -*- # # Author:: Fletcher Nichol () # 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