Skip to content

Commit

Permalink
Fixes #35270 - Enable boot image download for iso images
Browse files Browse the repository at this point in the history
* Implement fetch and extract boot image
* Implement class for file extraction with isoinfo
* Add capability for archive extraction
* Separate logging and file writing tasks

Co-Authored-By: Ewoud Kohl van Wijngaarden <[email protected]>
  • Loading branch information
bastian-src and ekohl committed Sep 28, 2022
1 parent a48da73 commit 4f38afe
Show file tree
Hide file tree
Showing 7 changed files with 116 additions and 1 deletion.
33 changes: 33 additions & 0 deletions lib/proxy/archive_extract.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
module Proxy
class ArchiveExtract < Proxy::Util::CommandTask
include Util

SHELL_COMMAND = 'isoinfo'

def initialize(image_path, file_in_image, dst_path)
args = [
which(SHELL_COMMAND),
# Print information from Rock Ridge extensions
'-R',
# Filename to read ISO-9660 image from
'-i', image_path.to_s,
# Extract specified file to stdout
'-x', file_in_image.to_s
]

super(args, nil, dst_path)
end

def start
lock = Proxy::FileLock.try_locking(File.join(File.dirname(@output), ".#{File.basename(@output)}.lock"))
if lock.nil?
false
else
super do
Proxy::FileLock.unlock(lock)
File.unlink(lock)
end
end
end
end
end
29 changes: 28 additions & 1 deletion lib/proxy/util.rb
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,19 @@ class CommandTask
# stderr is redirected to proxy error log, stdout to proxy debug log
# command can be either string or array (command + arguments)
# input is passed into STDIN and must be string
def initialize(command, input = nil)
# output can be a string containing a file path. If this is the case,
# output is not logged but written to this file.
def initialize(command, input = nil, output = nil)
@command = command
@input = input
@output = output
end

def start(&ensured_block)
@output.nil? ? spawn_logging_thread(&ensured_block) : spawn_output_thread(&ensured_block)
end

def spawn_logging_thread(&ensured_block)
# run the task in its own thread
@task = Thread.new(@command, @input) do |cmd, input|
status = nil
Expand All @@ -43,6 +50,26 @@ def start(&ensured_block)
self
end

def spawn_output_thread(&ensured_block)
# run the task in its own thread
@task = Thread.new(@command, @input, @output) do |cmd, input, file|
status = nil
Open3.pipeline_w(cmd, :out => file.to_s) do |stdin, thr|
cmdline_string = Shellwords.escape(cmd.is_a?(Array) ? cmd.join(' ') : cmd)
last_thr = thr[-1]
logger.info "[#{last_thr.pid}] Started task #{cmdline_string}"
stdin.write(input) if input
stdin.close
# call thr.value to wait for a Process::Status object.
status = last_thr.value
end
status ? status.exitstatus : $CHILD_STATUS
ensure
yield if block_given?
end
self
end

# wait for the task to finish and get the subprocess return code
def join
@task.value
Expand Down
1 change: 1 addition & 0 deletions lib/smart_proxy_main.rb
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
require 'proxy/dependency_injection'
require 'proxy/util'
require 'proxy/http_download'
require 'proxy/archive_extract'
require 'proxy/helpers'
require 'proxy/memory_store'
require 'proxy/plugin_validators'
Expand Down
37 changes: 37 additions & 0 deletions modules/tftp/server.rb
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,43 @@ def pxeconfig_file(mac)
end
end

def self.fetch_boot_image(image_dst, url, files)
base_path = Pathname.new("/var/www/html/pub/installation_media")
image_path = Pathname.new(File.expand_path(image_dst, base_path)).cleanpath

# Verify image_dst does not contain ".." (switch folder)
if image_path.expand_path.relative_path_from(base_path).to_s.start_with?('..')
raise "File to extract from image contains up-directory: #{image_dst}"
end

extr_image_dir = image_path.sub_ext('')

image_path.parent.mkpath
download_task = choose_protocol_and_fetch(url, image_path)
# Wait for concurrent processes
raise "Cannot download boot image at the moment - is another process downloading it already?" if download_task.is_a?(FalseClass)
# wait for download completion
download_task.join

files.each do |file|
file_path = Pathname.new file
extr_file_path = Pathname.new(File.join(extr_image_dir, file_path)).cleanpath

# Verify file does not contain ".." (switch folder)
if extr_file_path.expand_path.relative_path_from(extr_image_dir).to_s.start_with?('..')
raise "File to extract from image contains up-directory: #{file_path}"
end

# Create destination directory
extr_file_path.parent.mkpath
# extract iso
unless File.exist? extr_file_path
extract_task = ::Proxy::ArchiveExtract.new(image_path, file_path, extr_file_path).start
raise "TFTP image file extraction error: #{file_path}" unless extract_task.join == 0
end
end
end

def self.fetch_boot_file(dst, src)
filename = boot_filename(dst, src)
destination = Pathname.new(File.expand_path(filename, Proxy::TFTP::Plugin.settings.tftproot)).cleanpath
Expand Down
8 changes: 8 additions & 0 deletions modules/tftp/tftp_api.rb
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,14 @@ def create_default(variant)
end
end

post "/fetch_boot_image" do
log_halt(400, "TFTP: Wrong input parameters given.") unless [params[:path], params[:url], params[:files]].all?

log_halt(500, "TFTP: Failed to fetch boot file: ") do
Proxy::TFTP.fetch_boot_image(params[:path], params[:url], params[:files])
end
end

post "/fetch_boot_file" do
log_halt(400, "TFTP: Failed to fetch boot file: ") { Proxy::TFTP.fetch_boot_file(params[:prefix], params[:path]) }
end
Expand Down
3 changes: 3 additions & 0 deletions modules/tftp/tftp_plugin.rb
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@ class Plugin < ::Proxy::Plugin
:verify_server_cert => true
validate :verify_server_cert, boolean: true

# Expose automatic iso handling capability
capability -> { which(Proxy::ArchiveExtract.SHELL_COMMAND) ? 'extraction' : nil }

expose_setting :tftp_servername
end
end
6 changes: 6 additions & 0 deletions test/tftp/tftp_api_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,12 @@ def test_api_can_fetch_boot_file
assert last_response.ok?
end

def test_api_can_fetch_boot_image
Proxy::TFTP.expects(:fetch_boot_image).with('some/image.iso', 'http://localhost/file.iso').returns(true)
post "/fetch_boot_image", :path => 'some/image.iso', :url => 'http://localhost/file.iso'
assert last_response.ok?
end

def test_api_can_get_servername
Proxy::TFTP::Plugin.settings.stubs(:tftp_servername).returns("servername")
result = get "/serverName"
Expand Down

0 comments on commit 4f38afe

Please sign in to comment.