# frozen_string_literal: true require 'open3' module Bolt module Transport class SSH < Simple class ExecConnection attr_reader :user, :target def initialize(target) raise Bolt::ValidationError, "Target #{target.safe_name} does not have a host" unless target.host @target = target ssh_config = Net::SSH::Config.for(target.host) @user = @target.user || ssh_config[:user] || Etc.getlogin @logger = Logging.logger[self] end # This is used to verify we can connect to targets with `connected?` def connect cmd = build_ssh_command('exit') _, err, stat = Open3.capture3(*cmd) unless stat.success? raise Bolt::Node::ConnectError.new( "Failed to connect to #{@target.safe_name}: #{err}", 'CONNECT_ERROR' ) end end def disconnect; end def shell Bolt::Shell::Bash.new(@target, self) end def userhost "#{@user}@#{@target.host}" end def ssh_opts cmd = [] # BatchMode is SSH's noninteractive option: if key authentication # fails it will error out instead of falling back to password prompt cmd += %w[-o BatchMode=yes] cmd += %W[-o Port=#{@target.port}] if @target.port if @target.transport_config.key?('host-key-check') hkc = @target.transport_config['host-key-check'] ? 'yes' : 'no' cmd += %W[-o StrictHostKeyChecking=#{hkc}] end if (key = target.transport_config['private-key']) cmd += ['-i', key] end cmd end def build_ssh_command(command) ssh_conf = @target.transport_config['ssh-command'] || 'ssh' ssh_cmd = Array(ssh_conf) ssh_cmd += ssh_opts ssh_cmd << userhost ssh_cmd << command end def upload_file(source, dest) @logger.trace { "Uploading #{source} to #{dest}" } unless source.is_a?(StringIO) cp_conf = @target.transport_config['copy-command'] || ["scp", "-r"] cp_cmd = Array(cp_conf) cp_cmd += ssh_opts _, err, stat = if source.is_a?(StringIO) Tempfile.create(File.basename(dest)) do |f| f.write(source.read) f.close cp_cmd << f.path cp_cmd << "#{userhost}:#{Shellwords.escape(dest)}" Open3.capture3(*cp_cmd) end else cp_cmd << source cp_cmd << "#{userhost}:#{Shellwords.escape(dest)}" Open3.capture3(*cp_cmd) end if stat.success? @logger.trace "Successfully uploaded #{source} to #{dest}" else message = "Could not copy file to #{dest}: #{err}" raise Bolt::Node::FileError.new(message, 'COPY_ERROR') end end def download_file(source, dest, _download) @logger.trace { "Downloading #{userhost}:#{source} to #{dest}" } FileUtils.mkdir_p(dest) cp_conf = @target.transport_config['copy-command'] || ["scp", "-r"] cp_cmd = Array(cp_conf) cp_cmd += ssh_opts cp_cmd << "#{userhost}:#{Shellwords.escape(source)}" cp_cmd << dest _, err, stat = Open3.capture3(*cp_cmd) if stat.success? @logger.trace "Successfully downloaded #{userhost}:#{source} to #{dest}" else message = "Could not copy file to #{dest}: #{err}" raise Bolt::Node::FileError.new(message, 'COPY_ERROR') end end def execute(command) cmd_array = build_ssh_command(command) Open3.popen3(*cmd_array) end # This is used by the Bash shell to decide whether to `cd` before # executing commands as a run-as user def reset_cwd? true end end end end end