Project

General

Profile

Download (9.69 KB) Statistics
| Branch: | Tag: | Revision:
class UnattendedController < ApplicationController
include Foreman::Renderer

layout false

# We dont require any of these methods for provisioning
skip_before_action :require_login, :session_expiry, :update_activity_time, :set_taxonomy, :authorize, :unless => Proc.new { preview? }

# Allow HTTP POST methods without CSRF
skip_before_action :verify_authenticity_token

before_action :set_admin_user, :unless => Proc.new { preview? }
# We want to find out our requesting host
before_action :get_host_details, :except => [:hostgroup_template, :built, :failed]
before_action :get_built_host_details, :only => [:built, :failed]
before_action :allowed_to_install?, :except => :hostgroup_template
before_action :handle_realm, :if => Proc.new { params[:kind] == 'provision' }
# load "helper" variables to be available in the templates
before_action :load_template_vars, :only => :host_template
# all of our requests should be returned in text/plain
after_action :set_content_type

# maximum size of built/failed request body accepted to prevent DoS (in bytes)
MAX_BUILT_BODY = 65535

def built
logger.info "#{controller_name}: #{@host.name} is built!"
# clear possible previous errors
@host.build_errors = nil
update_ip if Setting[:update_ip_from_built_request]
head(@host.built ? :created : :conflict)
end

def failed
return if preview? || !@host.build
logger.warn "#{controller_name}: #{@host.name} build failed!"
@host.build_errors = request.body.read(MAX_BUILT_BODY)&.encode('utf-8', invalid: :replace, undef: :replace, replace: '_')
body_length = @host.build_errors.try(:size) || 0
@host.build_errors += "\n\nOutput trimmed\n" if body_length >= MAX_BUILT_BODY
logger.warn { "Log lines from the OS installer:\n#{@host.build_errors}" }
head(@host.built ? :created : :conflict)
end

def hostgroup_template
return head(:not_found) unless (params.has_key?("id") && params.has_key?(:hostgroup))

template = ProvisioningTemplate.find_by_name(params['id'].to_s)
@host = Hostgroup.find_by_title(params['hostgroup'].to_s)
return head(:not_found) unless template && @host

load_template_vars if template.template_kind.name == 'provision'
safe_render template.template
end

# Generate an action for each template kind
# i.e. /unattended/provision will render the provisioning template for the requesting host
def host_template
return head(:not_found) unless params[:kind].present?
render_template params[:kind]
end

protected

def require_ssl?
return super if params.key?(:spoof) || params.key?(:hostname)
false
end

private

def preview?
params.key?(:spoof) || params.key?(:hostname)
end

def render_custom_error(status, error_message, params)
logger.error error_message % params
# add a comment character (works with Red Hat and Debian systems) to avoid parsing errors
render(:plain => '# ' + _(error_message) % params, :status => status, :content_type => 'text/plain')
end

def render_template(type)
# Compatibility with older URLs
type = 'iPXE' if type == 'gPXE'

if (config = @host.provisioning_template({ :kind => type }))
safe_render config
else
error_message = N_("unable to find %{type} template for %{host} running %{os}")
render_custom_error(:not_found, error_message, {:type => type, :host => @host.name, :os => @host.operatingsystem})
end
end

# lookup for a host based on the ip address and if possible by a mac address(as sent by anaconda)
# if the host was found than its record will be in @host
# if the host doesn't exists, it will return 404 and the requested method will not be reached.
def get_host_details
@host = find_host_by_spoof || find_host_by_token
@host ||= find_host_by_ip_or_mac unless token_from_params.present?
verify_valid_host_token
verify_found_host
end

def get_built_host_details
@host = find_host_by_spoof || find_built_host_by_token
@host ||= find_host_by_ip_or_mac unless token_from_params.present?
verify_found_host
end

def verify_valid_host_token
return unless @host&.token_expired?
render_custom_error(
:precondition_failed,
N_('%{controller}: provisioning token for host %{host} expired'),
{ :controller => controller_name, :host => @host.name }
)
end

def verify_found_host
logger.debug "Found #{@host}" unless host_not_found?(@host) || host_os_is_missing?(@host) || host_os_family_is_missing?(@host)
end

def value_missing?(value, error_message, error_type, custom_error_parameters = {})
return false if value
render_custom_error(error_type, error_message, custom_error_parameters)
true
end

def host_not_found?(a_host)
value_missing?(a_host, N_("%{controller}: unable to find a host that matches the request from %{addr}"),
:not_found, :controller => controller_name, :addr => request.env['REMOTE_ADDR'])
end

def host_os_is_missing?(a_host)
value_missing?(a_host.operatingsystem, N_("%{controller}: %{host}'s operating system is missing"),
:conflict, :controller => controller_name, :host => a_host.name)
end

def host_os_family_is_missing?(a_host)
value_missing?(a_host.operatingsystem.family, N_("%{controller}: %{host}'s operating system %{os} has no OS family"),
:conflict, :controller => controller_name, :host => a_host.name, :os => a_host.operatingsystem.fullname)
end

def find_host_by_spoof
host = Host.authorized('view_hosts').joins(:primary_interface).where("#{Nic::Base.table_name}.ip" => params['spoof']).first if params['spoof'].present?
host ||= Host.authorized('view_hosts').find_by_name(params['hostname']) if params['hostname'].present?
@spoof = host.present?
host
end

def find_host_by_token
token = token_from_params
return nil if token.nil?
Host.for_token(token).first
end

def find_built_host_by_token
token = token_from_params
return nil if token.nil?
Host.for_token_when_built(token).first
end

def token_from_params
token = params[:token]
return nil if token.blank?
# Quirk: ZTP requires the .slax suffix
if (result = token.match(/^([a-z0-9-]+)(.slax)$/i))
return result[1]
end
token
end

def find_host_by_ip_or_mac
# try to find host based on our client ip address
ip = ip_from_request_env

# in case we got back multiple ips (see #1619)
ip = ip.split(',').first

# search for a mac address in any of the RHN provisioning headers
# this section is kickstart only relevant
mac_list = []
if request.env['HTTP_X_RHN_PROVISIONING_MAC_0'].present?
begin
request.env.keys.each do |header|
mac_list << request.env[header].split[1].strip.downcase if header =~ /^HTTP_X_RHN_PROVISIONING_MAC_/
end
rescue => e
Foreman::Logging.exception("unknown RHN_PROVISIONING header", e)
mac_list = []
end
end

if params.key?(:mac)
mac_list << params[:mac].strip.downcase
end

# we try to match first based on the MAC, falling back to the IP
candidates = Host.joins(:provision_interface).where(mac_list.empty? ? {:nics => {:ip => ip}} : ["lower(nics.mac) IN (?)", mac_list]).order(:created_at)
logger.warn("Multiple hosts found with #{ip} or #{mac_list}, picking up the most recent") if candidates.count > 1
host = candidates.last
# host is readonly because of association so we reload it if we find it
host ? Host.find(host.id) : nil
end

def allowed_to_install?
(@host.build || @spoof || Setting[:access_unattended_without_build]) ? true : head(:method_not_allowed)
end

# Reset realm OTP. This is run as a before_action for provisioning templates.
def handle_realm
# We don't do anything if we are in spoof mode.
return true if @spoof

# This should terminate the before_action and the action. We return a HTTP
# error so the installer knows something is wrong. This is tested with
# Anaconda, but maybe Suninstall will choke on it.
render(:plain => _("Failed to get a new realm OTP. Terminating the build!"), :status => :internal_server_error) unless @host.handle_realm
end

def set_content_type
response.headers['Content-Type'] = 'text/plain'
end

# This method updates the IP held by Foreman from the incoming request.
# Useful on unmanaged DHCP systems, with token-based installs where Foreman
# doesn't know the IP in advance (and has been given a fake one just to make
# the form save)
def update_ip
ip = ip_from_request_env
logger.debug "Built notice from #{ip}, current host ip is #{@host.ip}, updating" if @host.ip != ip

# @host has been changed even if the save fails, so we have to change it back
old_ip = @host.ip
@host.ip = old_ip unless @host.update({'ip' => ip})
end

def ip_from_request_env
ip = request.env['REMOTE_ADDR']

# check if someone is asking on behalf of another system (load balance etc)
if request.env['HTTP_X_FORWARDED_FOR'].present? && (ip =~ Regexp.new(Setting[:remote_addr]))
ip = request.env['HTTP_X_FORWARDED_FOR']
end

ip
end

def safe_render(template)
if template.is_a?(String)
@unsafe_template_content = template
@template_name = 'Unnamed'
elsif template.is_a?(ProvisioningTemplate)
@unsafe_template_content = template.template
@template_name = template.name
else
raise "unknown template"
end

begin
render :inline => "<%= unattended_render(@unsafe_template_content, @template_name).html_safe %>"
rescue => error
msg = _("There was an error rendering the %s template: ") % @template_name
Foreman::Logging.exception(msg, error)
render :plain => msg + error.message, :status => :internal_server_error
end
end
end
(55-55/58)