Skip to content

Commit 691f12c

Browse files
Bastian Schmidtekohl
andcommitted
Fixes #35270 - Enable boot image download for iso images
* Implement fetch and extract boot image * Implement class for file extraction with isoinfo * Add capability for archive extraction * Separate logging and file writing tasks * Add additional API endpoint /tftp/boot_image/ for boot images Co-Authored-By: Ewoud Kohl van Wijngaarden <[email protected]>
1 parent a48da73 commit 691f12c

File tree

11 files changed

+148
-3
lines changed

11 files changed

+148
-3
lines changed

config/settings.d/tftp.yml.example

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
---
2-
# Can be true, false, or http/https to enable just one of the protocols
2+
# Can be true or false
33
:enabled: false
44

55
#:tftproot: /var/lib/tftpboot
@@ -13,3 +13,8 @@
1313
# Defines the default certificate action for certificate checking.
1414
# When false, the argument --no-check-certificate will be used.
1515
#:verify_server_cert: true
16+
17+
# Defines the default folder to provide boot images. It becomes important when
18+
# automating the extraction process as it is done for the Ubuntu Autoinstall
19+
# procedure.
20+
#:boot_image_root: /var/lib/foreman-proxy/tftp/boot_images

lib/proxy/archive_extract.rb

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
module Proxy
2+
class ArchiveExtract < Proxy::Util::CommandTask
3+
include Util
4+
5+
SHELL_COMMAND = 'isoinfo'
6+
7+
def initialize(image_path, file_in_image, dst_path)
8+
args = [
9+
which(SHELL_COMMAND),
10+
# Print information from Rock Ridge extensions
11+
'-R',
12+
# Filename to read ISO-9660 image from
13+
'-i', image_path.to_s,
14+
# Extract specified file to stdout
15+
'-x', file_in_image.to_s
16+
]
17+
18+
super(args, nil, dst_path)
19+
end
20+
21+
def start
22+
lock = Proxy::FileLock.try_locking(File.join(File.dirname(@output), ".#{File.basename(@output)}.lock"))
23+
if lock.nil?
24+
false
25+
else
26+
super do
27+
Proxy::FileLock.unlock(lock)
28+
File.unlink(lock)
29+
end
30+
end
31+
end
32+
end
33+
end

lib/proxy/util.rb

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,12 +13,19 @@ class CommandTask
1313
# stderr is redirected to proxy error log, stdout to proxy debug log
1414
# command can be either string or array (command + arguments)
1515
# input is passed into STDIN and must be string
16-
def initialize(command, input = nil)
16+
# output can be a string containing a file path. If this is the case,
17+
# output is not logged but written to this file.
18+
def initialize(command, input = nil, output = nil)
1719
@command = command
1820
@input = input
21+
@output = output
1922
end
2023

2124
def start(&ensured_block)
25+
@output.nil? ? spawn_logging_thread(&ensured_block) : spawn_output_thread(&ensured_block)
26+
end
27+
28+
def spawn_logging_thread(&ensured_block)
2229
# run the task in its own thread
2330
@task = Thread.new(@command, @input) do |cmd, input|
2431
status = nil
@@ -43,6 +50,26 @@ def start(&ensured_block)
4350
self
4451
end
4552

53+
def spawn_output_thread(&ensured_block)
54+
# run the task in its own thread
55+
@task = Thread.new(@command, @input, @output) do |cmd, input, file|
56+
status = nil
57+
Open3.pipeline_w(cmd, :out => file.to_s) do |stdin, thr|
58+
cmdline_string = Shellwords.escape(cmd.is_a?(Array) ? cmd.join(' ') : cmd)
59+
last_thr = thr[-1]
60+
logger.info "[#{last_thr.pid}] Started task #{cmdline_string}"
61+
stdin.write(input) if input
62+
stdin.close
63+
# call thr.value to wait for a Process::Status object.
64+
status = last_thr.value
65+
end
66+
status ? status.exitstatus : $CHILD_STATUS
67+
ensure
68+
yield if block_given?
69+
end
70+
self
71+
end
72+
4673
# wait for the task to finish and get the subprocess return code
4774
def join
4875
@task.value

lib/smart_proxy_for_testing.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
require 'proxy/dependency_injection'
1111
require 'proxy/util'
1212
require 'proxy/http_download'
13+
require 'proxy/archive_extract'
1314
require 'proxy/helpers'
1415
require 'proxy/memory_store'
1516
require 'proxy/plugin_validators'

lib/smart_proxy_main.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
require 'proxy/dependency_injection'
1515
require 'proxy/util'
1616
require 'proxy/http_download'
17+
require 'proxy/archive_extract'
1718
require 'proxy/helpers'
1819
require 'proxy/memory_store'
1920
require 'proxy/plugin_validators'

modules/tftp/http_config.ru

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
11
require 'tftp/tftp_api'
2+
require 'tftp/tftp_bootimage_api'
23

34
map "/tftp" do
45
run Proxy::TFTP::Api
56
end
7+
8+
map "/tftp/boot_image" do
9+
run Proxy::TFTP::BootimageApi
10+
end

modules/tftp/server.rb

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,43 @@ def pxeconfig_file(mac)
150150
end
151151
end
152152

153+
def self.fetch_boot_image(image_dst, url, files)
154+
base_path = Pathname.new(Proxy::TFTP::Plugin.settings.boot_image_root).cleanpath
155+
image_path = Pathname.new(File.expand_path(image_dst, base_path)).cleanpath
156+
157+
# Verify image_dst does not contain ".." (switch folder)
158+
if image_path.expand_path.relative_path_from(base_path).to_s.start_with?('..')
159+
raise "File to extract from image contains up-directory: #{image_dst}"
160+
end
161+
162+
extr_image_dir = image_path.sub_ext('')
163+
164+
image_path.parent.mkpath
165+
download_task = choose_protocol_and_fetch(url, image_path)
166+
# Wait for concurrent processes
167+
raise "Cannot download boot image at the moment - is another process downloading it already?" if download_task.is_a?(FalseClass)
168+
# wait for download completion
169+
download_task.join
170+
171+
files.each do |file|
172+
file_path = Pathname.new file
173+
extr_file_path = Pathname.new(File.join(extr_image_dir, file_path)).cleanpath
174+
175+
# Verify file does not contain ".." (switch folder)
176+
if extr_file_path.expand_path.relative_path_from(extr_image_dir).to_s.start_with?('..')
177+
raise "File to extract from image contains up-directory: #{file_path}"
178+
end
179+
180+
# Create destination directory
181+
extr_file_path.parent.mkpath
182+
# extract iso
183+
unless File.exist? extr_file_path
184+
extract_task = ::Proxy::ArchiveExtract.new(image_path, file_path, extr_file_path).start
185+
raise "TFTP image file extraction error: #{file_path}" unless extract_task.join == 0
186+
end
187+
end
188+
end
189+
153190
def self.fetch_boot_file(dst, src)
154191
filename = boot_filename(dst, src)
155192
destination = Pathname.new(File.expand_path(filename, Proxy::TFTP::Plugin.settings.tftproot)).cleanpath

modules/tftp/tftp_api.rb

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,14 @@ def create_default(variant)
3434
end
3535
end
3636

37+
post "/fetch_boot_image" do
38+
log_halt(400, "TFTP: Wrong input parameters given.") unless [params[:path], params[:url], params[:files]].all?
39+
40+
log_halt(500, "TFTP: Failed to fetch boot file: ") do
41+
Proxy::TFTP.fetch_boot_image(params[:path], params[:url], params[:files])
42+
end
43+
end
44+
3745
post "/fetch_boot_file" do
3846
log_halt(400, "TFTP: Failed to fetch boot file: ") { Proxy::TFTP.fetch_boot_file(params[:prefix], params[:path]) }
3947
end

modules/tftp/tftp_bootimage_api.rb

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
module Proxy::TFTP
2+
class BootimageApi < ::Sinatra::Base
3+
helpers ::Proxy::Helpers
4+
5+
get "/*" do
6+
file = Pathname.new(params[:splat].first).cleanpath
7+
root = Pathname.new(Proxy::TFTP::Plugin.settings.boot_image_root).expand_path.cleanpath
8+
joined_path = File.join(root, file)
9+
log_halt(404, "Not found") unless File.exist?(joined_path)
10+
real_file = Pathname.new(joined_path).realpath
11+
log_halt(403, "Invalid or empty path") unless real_file.fnmatch?("#{root}/**")
12+
log_halt(403, "Directory listing not allowed") if File.directory?(real_file)
13+
log_halt(503, "Not a regular file") unless File.file?(real_file)
14+
send_file real_file
15+
end
16+
end
17+
end
18+

modules/tftp/tftp_plugin.rb

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,13 @@ class Plugin < ::Proxy::Plugin
66

77
default_settings :tftproot => '/var/lib/tftpboot',
88
:tftp_connect_timeout => 10,
9-
:verify_server_cert => true
9+
:verify_server_cert => true,
10+
:boot_image_root => '/var/lib/foreman-proxy/tftp/boot_images'
1011
validate :verify_server_cert, boolean: true
1112

13+
# Expose automatic iso handling capability
14+
capability -> { which(Proxy::ArchiveExtract.SHELL_COMMAND) ? 'extraction' : nil }
15+
1216
expose_setting :tftp_servername
1317
end
1418
end

0 commit comments

Comments
 (0)