Skip to content

Commit

Permalink
Fixes #35269 - Support system image download for installation media
Browse files Browse the repository at this point in the history
* Include proxy.fetch_system_image
* Add system_image_path variable for template reference
* Adapt PXELinux template
* Add tftp_http_port setting
* Add tftp.setTFTPBootFiles test scenarios
  • Loading branch information
bastian-src committed Jun 20, 2023
1 parent cb88310 commit edd0437
Show file tree
Hide file tree
Showing 8 changed files with 150 additions and 9 deletions.
51 changes: 47 additions & 4 deletions app/models/concerns/orchestration/tftp.rb
Original file line number Diff line number Diff line change
Expand Up @@ -111,10 +111,25 @@ def setTFTPBootFiles
logger.info "Fetching required TFTP boot files for #{host.name}"
valid = []

host.operatingsystem.pxe_files(host.medium_provider).each do |bootfile_info|
bootfile_info.each do |prefix, path|
valid << each_unique_feasible_tftp_proxy do |proxy|
proxy.fetch_boot_file(:prefix => prefix.to_s, :path => path)
# Check host.medium_provider path for iso image
is_image_path = File.extname(host.medium_uri.to_s).downcase.end_with?(".iso")

valid << each_unique_feasible_tftp_proxy do |proxy|
bootfiles = host.operatingsystem.pxe_files(host.medium_provider)
if is_image_path
max_request_timeout = Setting[:proxy_request_timeout]
image_url = host.medium_uri.to_s
tftp_files = pxe_files_from_bootfiles(bootfiles, image_url)
tftp_dst = pxe_dst_from_bootfiles(bootfiles)
image_dst = host.operatingsystem.system_image_path(host.medium_provider, true, false)
image_status = poll_fetch_system_image(proxy, image_url, image_dst, tftp_files, tftp_dst, max_request_timeout)
logger.error "Timeout fetching system image #{image_url}. See smart proxy log for details." unless image_status
image_status
else
bootfiles.each do |bootfile_info|
bootfile_info.each do |prefix, path|
proxy.fetch_boot_file(:prefix => prefix.to_s, :path => path)
end
end
end
end
Expand Down Expand Up @@ -197,4 +212,32 @@ def each_unique_feasible_tftp_proxy
end
results.all?
end

# Extract pxe files from bootfiles
def pxe_files_from_bootfiles(bootfiles, image_url)
pxe_files = []
bootfiles.each { |pxe_url| pxe_files.append(pxe_url.values.first.delete_prefix(image_url)) }
pxe_files
end

# Extract pxe destination file name from bootfiles
def pxe_dst_from_bootfiles(bootfiles)
bootfiles.first.keys.first
end

def poll_fetch_system_image(proxy, image_url, image_dst, tftp_files, tftp_dst, max_request_time)
retries = poll_system_image_retries
pause_time = max_request_time / poll_system_image_retries
request_status = proxy.fetch_system_image(:url => image_url, :path => image_dst, :files => tftp_files, :tftp_path => tftp_dst)
until retries <= 0 || request_status == 200
sleep(pause_time)
request_status = proxy.fetch_system_image(:url => image_url, :path => image_dst, :files => tftp_files, :tftp_path => tftp_dst)
retries -= 1
end
request_status == 200
end

def poll_system_image_retries
10
end
end
23 changes: 22 additions & 1 deletion app/models/operatingsystem.rb
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ class Operatingsystem < ApplicationRecord
property :password_hash, String, desc: 'Encrypted hash of the operating system password'
end
class Jail < Safemode::Jail
allow :id, :name, :major, :minor, :family, :to_s, :==, :release, :release_name, :kernel, :initrd, :pxe_type, :boot_files_uri, :password_hash, :mediumpath, :bootfile
allow :id, :name, :major, :minor, :family, :to_s, :==, :release, :release_name, :kernel, :initrd, :pxe_type, :boot_files_uri, :password_hash, :mediumpath, :bootfile, :system_image_path
end

def self.title_name
Expand Down Expand Up @@ -236,6 +236,27 @@ def bootfile(medium_provider, type)
pxe_prefix(medium_provider) + "-" + pxe_file_names(medium_provider)[type.to_sym]
end

apipie :method, 'Returns path to system image based on given medium provider' do
required :medium_provider, 'MediumProviders::Provider', 'Medium provider responsible to provide location of installation medium for a given entity (host or host group)'
# optional :include_suffix, Boolean, 'Include the .iso file suffix if true (defaults true)'
# optional :include_base_path, Boolean, 'Include the Smart Proxy base address if true (defaults true)'
returns String, 'Path to the system image file'
end
def system_image_path(medium_provider, include_suffix = true, include_base_path = true)
unless medium_provider.is_a? MediumProviders::Provider
raise Foreman::Exception.new(N_('Please provide a medium provider. It can be found as @medium_provider in templates, or Foreman::Plugin.medium_providers_registry.find_provider(host)'))
end
include_base_path ? base_path = system_image_base_path : base_path = ""
include_suffix ? suffix = ".iso" : suffix = ""

"#{base_path}#{name.downcase}/#{medium_provider.unique_id}#{suffix}"
end

# Base path for system_image url
def system_image_base_path
"/tftp/system_image/"
end

# Does this OS family support a build variant that is constructed from a prebuilt archive
def supports_image
false
Expand Down
12 changes: 11 additions & 1 deletion app/models/smart_proxy.rb
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,14 @@ def setting(feature, setting)
smart_proxy_feature_by_name(feature).try(:settings).try(:[], setting)
end

def tftp_http_port
setting(:TFTP, 'http_port')
end

def tftp_http_port!
tftp_http_port || raise(::Foreman::Exception.new(N_("HTTP boot requires proxy with httpboot feature and http_port exposed setting")))
end

def httpboot_http_port
setting(:HTTPBoot, 'http_port')
end
Expand Down Expand Up @@ -202,12 +210,14 @@ def get_features
sections only: %w[all additional]
prop_group :basic_model_props, ApplicationRecord, meta: { friendly_name: 'Smart Proxy' }
property :hostname, String, desc: 'Returns name of the host with proxy'
property :tftp_http_port, Integer, desc: 'Returns proxy port for TFTP boot images'
property :tftp_http_port!, Integer, desc: 'Same as tftp_http_port, but raises Foreman::Exception if no port is set'
property :httpboot_http_port, Integer, desc: 'Returns proxy port for HTTP boot'
property :httpboot_http_port!, Integer, desc: 'Same as httpboot_http_port, but raises Foreman::Exception if no port is set'
property :httpboot_https_port, Integer, desc: 'Returns proxy port for HTTPS boot'
property :httpboot_https_port!, Integer, desc: 'Same as httpboot_https_port, but raises Foreman::Exception if no port is set'
end
class Jail < ::Safemode::Jail
allow :id, :name, :hostname, :httpboot_http_port, :httpboot_https_port, :httpboot_http_port!, :httpboot_https_port!, :url
allow :id, :name, :hostname, :tftp_http_port, :httpboot_http_port, :httpboot_https_port, :tftp_http_port!, :httpboot_http_port!, :httpboot_https_port!, :url
end
end
1 change: 1 addition & 0 deletions app/services/foreman/renderer/configuration.rb
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,7 @@ class Configuration
:static,
:template_name,
:xen,
:system_image_path,
]

DEFAULT_ALLOWED_GLOBAL_SETTINGS = [
Expand Down
3 changes: 2 additions & 1 deletion app/services/foreman/renderer/scope/variables/base.rb
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ def self.included(base)
delegate :diskLayout, :disk_layout_source, :medium, :architecture, :ptable, :use_image, :arch,
:image_file, :default_image_file, to: :host, allow_nil: true
delegate :mediumpath, :additional_media, :supports_image, :major, :preseed_path, :preseed_server,
:xen, :kernel, :initrd, to: :operatingsystem, allow_nil: true
:xen, :kernel, :initrd, :system_image_path, to: :operatingsystem, allow_nil: true
delegate :name, to: :architecture, allow_nil: true, prefix: true
delegate :content, to: :disk_layout_source, allow_nil: true, prefix: true

Expand Down Expand Up @@ -97,6 +97,7 @@ def xenserver_attributes

def pxe_config
return unless @medium_provider
@system_image_path = system_image_path(@medium_provider)
@kernel = kernel(@medium_provider)
@initrd = initrd(@medium_provider)
@kernel_uri, @initrd_uri = operatingsystem.boot_files_uri(@medium_provider)
Expand Down
16 changes: 16 additions & 0 deletions app/services/proxy_api/tftp.rb
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,22 @@ def fetch_boot_file(args)
raise ProxyException.new(url, e, N_("Unable to fetch TFTP boot file"))
end

# Requests that the proxy downloads and extracts an image from the media's source
# [+args+] : Hash containing
# :url => String containing the URL of the image to download
# :path => String containing the location on the smart proxy to store the image
# :files => Array of Strings containing boot file paths to extract from the image
# :tftp_path => String containing the location within the TFTP tree to store the files
# Returns : Integer response status
def fetch_system_image(args)
response = post(args, "fetch_system_image")
response.code
rescue RestClient::Locked
423
rescue => e
raise ProxyException.new(url, e, N_("Unable to fetch and extract TFTP system image"))
end

# returns the TFTP boot server for this proxy
def bootServer
if (response = parse(get("serverName"))) && response["serverName"].present?
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,20 @@ test_on:
mac = @host.provision_interface.mac
subnet4 = iface.subnet
subnet6 = iface.subnet6
image_path = @preseed_path.sub(/\/?$/, '.iso')
userdata_option = "ds=nocloud-net;s=http://#{foreman_request_addr}/userdata/#{mac ? mac + '/' : ''}"
options = []

if @preseed_path.downcase.end_with?('.iso')
# Ubuntu image download is handled by the Smart Proxy
image_path = @system_image_path
tftp = @host.subnet.tftp
image_host = "#{tftp}:#{tftp.tftp_http_port}"
else
# Ubuntu image was downloaded and extracted manually
image_path = @preseed_path.sub(/\/?$/, '.iso')
image_host = foreman_request_addr.split(':').first
end

if host_param('blacklist')
options << host_param('blacklist').split(',').collect{|x| "#{x.strip}.blacklist=yes"}.join(' ')
end
Expand All @@ -39,7 +49,7 @@ test_on:
options << 'ramdisk_size=1500000'
options << 'fsck.mode=skip'
options << 'autoinstall'
options << "url=http://#{@preseed_server}#{image_path}"
options << "url=http://#{image_host}#{image_path}"
options << 'cloud-config-url=/dev/null'
if @add_userdata_quotes
options << "\"#{userdata_option}\""
Expand Down
39 changes: 39 additions & 0 deletions test/models/orchestration/tftp_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -360,4 +360,43 @@ class TFTPOrchestrationTest < ActiveSupport::TestCase
assert_equal expected.strip, template
assert h.build
end

describe "test #setTFTPBootFiles" do
setup do
@host = FactoryBot.build_stubbed(:host, :managed, :with_tftp_dual_stack_orchestration, :build => true)
arch = FactoryBot.build_stubbed(:architecture)
@medium = FactoryBot.build_stubbed(:medium, :name => 'Great OS Local', :path => 'http://my-example.com/official/great/os.iso')
@host.architecture = arch
@host.operatingsystem = FactoryBot.build_stubbed(:debian7_0, :media => [@medium], :architectures => [arch])
@host.medium = @medium
end

test "should download system image" do
ProxyAPI::TFTP.any_instance.stubs(:fetch_system_image).returns(200)
Nic::Managed.any_instance.expects(:poll_fetch_system_image).once
Nic::Managed.any_instance.expects(:poll_fetch_system_image).returns(true)
@host.provision_interface.send(:setTFTPBootFiles)
end

test "should fail system image download due to timeout" do
ProxyAPI::TFTP.any_instance.stubs(:fetch_system_image).returns(202)
Nic::Managed.any_instance.expects(:poll_fetch_system_image).once
Nic::Managed.any_instance.expects(:poll_fetch_system_image).returns(true)
@host.provision_interface.send(:setTFTPBootFiles)
end

test "should fail system image download due to timeout locked server" do
ProxyAPI::TFTP.any_instance.stubs(:fetch_system_image).returns(402)
Nic::Managed.any_instance.expects(:poll_fetch_system_image).once
Nic::Managed.any_instance.expects(:poll_fetch_system_image).returns(false)
@host.provision_interface.send(:setTFTPBootFiles)
end

test "should fail system image download due to proxy error" do
ProxyAPI::TFTP.any_instance.stubs(:fetch_system_image).returns(501)
Nic::Managed.any_instance.expects(:poll_fetch_system_image).once
Nic::Managed.any_instance.expects(:poll_fetch_system_image).returns(false)
@host.provision_interface.send(:setTFTPBootFiles)
end
end
end

0 comments on commit edd0437

Please sign in to comment.