Skip to content

Commit

Permalink
Fixes #35269 - Support system image download as 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 custom timeout for tftp requests
* Add tftp_http_port setting
  • Loading branch information
bastian-src committed Nov 14, 2022
1 parent d1b18e0 commit 6d95afd
Show file tree
Hide file tree
Showing 8 changed files with 102 additions and 18 deletions.
36 changes: 33 additions & 3 deletions app/models/concerns/orchestration/tftp.rb
Original file line number Diff line number Diff line change
Expand Up @@ -111,13 +111,30 @@ 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|
# Check host.medium_provider path for iso image
prefetch_image = 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)
# fetch iso image if given
if prefetch_image
host_url = host.medium_uri.to_s
file_paths = pxe_url_to_path(bootfiles, host_url)
system_image_path = host.operatingsystem.system_image_path(host.medium_provider, host, true, false)
proxy.fetch_system_image(:url => host_url, :path => system_image_path, :files => file_paths)
end

host.operatingsystem.pxe_files(host.medium_provider).each do |bootfile_info|
bootfile_info.each do |prefix, path|
# change path in case of iso download
if prefetch_image
path = pxe_replace_proxy_addr(path, host, proxy)
end
proxy.fetch_boot_file(:prefix => prefix.to_s, :path => path)
end
end
end

failure _("Failed to fetch boot files") unless valid.all?
valid.all?
end
Expand Down Expand Up @@ -197,4 +214,17 @@ def each_unique_feasible_tftp_proxy
end
results.all?
end

def pxe_url_to_path(pxe_urls, host_url)
pxe_paths = []
pxe_urls.each {|pxe_url| pxe_paths.append(pxe_url.values.first.delete_prefix(host_url))}
pxe_paths
end

def pxe_replace_proxy_addr(url, host, proxy)
proxy_path = host.operatingsystem.system_image_path(host.medium_provider, host, false)
proxy_url = "http://#{URI.parse(proxy.url).host}/#{proxy_path}"
url.sub(host.medium_uri.to_s, proxy_url)
end

end
22 changes: 21 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,26 @@ def bootfile(medium_provider, type)
pxe_prefix(medium_provider) + "-" + pxe_file_names(medium_provider)[type.to_sym]
end

apipie :method, 'Returns path to boot image based on given medium provider and (optional) host' do
required :medium_provider, 'MediumProviders::Provider', 'Medium provider responsible to provide location of installation medium for a given entity (host or host group)'
optional :host, 'Host::Managed', 'A specific host which can set custom a boot image path'
returns String, 'Path to the boot image file'
end
def system_image_path(medium_provider, host = nil, 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 @@ -100,6 +100,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, host)
@kernel = kernel(@medium_provider)
@initrd = initrd(@medium_provider)
@kernel_uri, @initrd_uri = operatingsystem.boot_files_uri(@medium_provider)
Expand Down
24 changes: 14 additions & 10 deletions app/services/proxy_api/resource.rb
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,12 @@ def initialize(args)

attr_reader :connect_params

def resource
def resource(timeout = nil)
# Required in order to ability to mock the resource
unless timeout.nil?
custom_params = connect_params.merge(timeout: timeout)
return RestClient::Resource.new(url, custom_params)
end
@resource ||= RestClient::Resource.new(url, connect_params)
end

Expand Down Expand Up @@ -65,7 +69,7 @@ def parse(response)
end

# Perform GET operation on the supplied path
def get(path = nil, payload = {})
def get(path = nil, payload = {}, timeout = nil)
query = payload.delete(:query)
Foreman::Deprecation.deprecation_warning("3.3", "passing additional headers to ProxyApi resource GET action") unless payload.empty?
final_uri = path || ""
Expand All @@ -78,39 +82,39 @@ def get(path = nil, payload = {})
telemetry_duration_histogram(:proxy_api_duration, :ms, method: 'get') do
# This ensures that an extra "/" is not generated
if path
resource[final_uri].get payload
resource(timeout)[final_uri].get payload
else
resource.get payload
resource(timeout).get payload
end
end
end
end

# Perform POST operation with the supplied payload on the supplied path
def post(payload, path = "")
def post(payload, path = "", timeout = nil)
logger.debug("POST request payload: #{payload}")
with_logger do
telemetry_duration_histogram(:proxy_api_duration, :ms, method: 'post') do
resource[path].post payload
resource(timeout)[path].post payload
end
end
end

# Perform PUT operation with the supplied payload on the supplied path
def put(payload, path = "")
def put(payload, path = "", timeout = nil)
logger.debug("PUT request payload: #{payload}")
with_logger do
telemetry_duration_histogram(:proxy_api_duration, :ms, method: 'put') do
resource[path].put payload
resource(timeout)[path].put payload
end
end
end

# Perform DELETE operation on the supplied path
def delete(path)
def delete(path, timeout = nil)
with_logger do
telemetry_duration_histogram(:proxy_api_duration, :ms, method: 'delete') do
resource[path].delete
resource(timeout)[path].delete
end
end
end
Expand Down
11 changes: 11 additions & 0 deletions app/services/proxy_api/tftp.rb
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,17 @@ 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
# :path => String containing the location on the smart proxy to store the image
# :url => String containing the URL of the image to download
# Returns : Boolean status
def fetch_system_image(args)
parse(post(args, "fetch_system_image", 180)) # Set 180 seconds timeout for large image download
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 @@ -42,7 +42,14 @@ test_on:

options << "locale=#{host_param('lang') || 'en_US'}"
options = options.join(' ')
image_path = @preseed_path.sub(/\/?$/, '.iso')
if @preseed_path.downcase.end_with?('.iso')
image_path = @system_image_path
tftp = @host.subnet.tftp
image_host = "#{tftp}:#{tftp.tftp_http_port}"
else
image_path = @preseed_path.sub(/\/?$/, '.iso')
image_host = foreman_request_addr.split(':').first
end
-%>
#
# WARNING
Expand All @@ -55,6 +62,6 @@ DEFAULT linux cloud-init autoinstall
LABEL linux cloud-init autoinstall
KERNEL <%= @kernel %>
INITRD <%= @initrd %>
APPEND url=http://<%= @preseed_server %><%= image_path %> autoinstall ds=nocloud-net;s=http://<%= foreman_request_addr %>/userdata/ root=/dev/ram0 ramdisk_size=1500000 fsck.mode=skip <%= options %>
APPEND url=http://<%= image_host %><%= image_path %> autoinstall ds=nocloud-net;s=http://<%= foreman_request_addr %>/userdata/ root=/dev/ram0 ramdisk_size=1500000 fsck.mode=skip <%= options %>
<%= snippet_if_exists(template_name + " custom menu") %>

0 comments on commit 6d95afd

Please sign in to comment.