lib/bolt/shell/bash.rb in bolt-3.1.0 vs lib/bolt/shell/bash.rb in bolt-3.3.0

- old
+ new

@@ -10,11 +10,10 @@ def initialize(target, conn) super @run_as = nil - @sudo_id = SecureRandom.uuid @sudo_password = @target.options['sudo-password'] || @target.password end def provided_features @@ -52,23 +51,39 @@ end end def download(source, destination, options = {}) running_as(options[:run_as]) do - # Target OS may be either Unix or Windows. Without knowing the target OS before-hand - # we can't assume whether the path separator is '/' or '\'. Assume we're connecting - # to a target with Unix and then check if the path exists after downloading. download = File.join(destination, Bolt::Util.unix_basename(source)) - conn.download_file(source, destination, download) + # If using run-as, the file is copied to a tmpdir and chowned to the + # connecting user. This is a workaround for limitations in net-ssh that + # only allow for downloading files as the connecting user, which is a + # problem for users who cannot connect to targets as the root user. + # This temporary copy should *always* be deleted. + if run_as + with_tmpdir(force_cleanup: true) do |dir| + tmpfile = File.join(dir.to_s, Bolt::Util.unix_basename(source)) - # If the download path doesn't exist, then the file was likely downloaded from Windows - # using a source path with backslashes (e.g. 'C:\Users\Administrator\foo'). The file - # should be saved to the expected location, so update the download path assuming a - # Windows basename so the result shows the correct local path. - unless File.exist?(download) - download = File.join(destination, Bolt::Util.windows_basename(source)) + result = execute(['cp', '-r', source, dir.to_s], sudoable: true) + + if result.exit_code != 0 + message = "Could not copy file '#{source}' to temporary directory '#{dir}': #{result.stderr.string}" + raise Bolt::Node::FileError.new(message, 'CP_ERROR') + end + + # We need to force the chown, otherwise this will just return + # without doing anything since the chown user is the same as the + # connecting user. + dir.chown(conn.user, force: true) + + conn.download_file(tmpfile, destination, download) + end + # If not using run-as, we can skip creating a temporary copy and just + # download the file directly. + else + conn.download_file(source, destination, download) end Bolt::Result.for_download(target, source, destination, download) end end @@ -143,11 +158,14 @@ remote_task_path = write_executable(dir, wrapper, 'wrapper.sh') end dir.chown(run_as) - execute_options[:stdin] = stdin + # Don't pass parameters on stdin if using a tty, as the parameters are + # already part of the wrapper script. + execute_options[:stdin] = stdin unless stdin && target.options['tty'] + execute_options[:sudoable] = true if run_as output = execute(remote_task_path, **execute_options) end Bolt::Result.for_task(target, output.stdout.string, output.stderr.string, @@ -289,16 +307,16 @@ remote_path end # A helper to create and delete a tmpdir on the remote system. Yields the # directory name. - def with_tmpdir + def with_tmpdir(force_cleanup: false) dir = make_tmpdir yield dir ensure if dir - if target.options['cleanup'] + if target.options['cleanup'] || force_cleanup dir.delete else Bolt::Logger.warn("skip_cleanup", "Skipping cleanup of tmpdir #{dir}") end end @@ -388,18 +406,27 @@ # True while the process is running or waiting for IO input while t.alive? # See if we can read from out or err, or write to in ready_read, ready_write, = select(read_streams.keys, write_stream, nil, timeout) - # Read from out and err ready_read&.each do |stream| + stream_name = stream == out ? 'out' : 'err' # Check for sudo prompt - read_streams[stream] << if use_sudo - check_sudo(stream, inp, options[:stdin]) - else - stream.readpartial(CHUNK_SIZE) - end + to_print = if use_sudo + check_sudo(stream, inp, options[:stdin]) + else + stream.readpartial(CHUNK_SIZE) + end + + if !to_print.chomp.empty? && @stream_logger + formatted = to_print.lines.map do |msg| + "[#{@target.safe_name}] #{stream_name}: #{msg.chomp}" + end.join("\n") + @stream_logger.warn(formatted) + end + + read_streams[stream] << to_print rescue EOFError end # select will either return an empty array if there are no # writable streams or nil if no IO object is available before the @@ -443,10 +470,10 @@ case result_output.exit_code when 0 @logger.trace { "Command `#{command_str}` returned successfully" } when 126 - msg = "\n\nThis may be caused by the default tmpdir being mounted "\ + msg = "\n\nThis might be caused by the default tmpdir being mounted "\ "using 'noexec'. See http://pup.pt/task-failure for details and workarounds." result_output.stderr << msg @logger.trace { "Command #{command_str} failed with exit code #{result_output.exit_code}" } else @logger.trace { "Command #{command_str} failed with exit code #{result_output.exit_code}" }