Skip to content

Instantly share code, notes, and snippets.

@ianballou
Created November 11, 2025 18:35
Show Gist options
  • Select an option

  • Save ianballou/9e38948de7b4e9064e6777cacfccd9a4 to your computer and use it in GitHub Desktop.

Select an option

Save ianballou/9e38948de7b4e9064e6777cacfccd9a4 to your computer and use it in GitHub Desktop.
Default Foreman kickstart provisioning template for image mode machines
<%#
kind: provision
name: Kickstart Default bootc
model: ProvisioningTemplate
oses:
- AlmaLinux
- CentOS
- CentOS_Stream
- Fedora
- RedHat
- Rocky
description: |
Provisioning template for kickstart based OSTree/Bootc container installations.
The output is fetched by Anaconda installer during the network based installation.
This template accepts the following parameters:
- ostreecontainer: string (required) - The container image URL (e.g., quay.io/exampleos/foo:latest)
- lang: string (default="en_US.UTF-8")
- selinux-mode: string (default="enforcing")
- keyboard: string (default="us")
- time-zone: string (default="UTC")
- http-proxy: string (default="")
- http-proxy-port: string (default="")
- ntp-pools: array (default=undef)
- ntp-server: string (default=undef)
- bootloader-append: string (default="nofb quiet splash=quiet")
- disable-firewall: boolean (default=false)
- kdump-options: string (default=undef)
- use_graphical_installer: boolean (default=false)
Reference links:
- https://access.redhat.com/documentation/en-us/red_hat_enterprise_linux/9/html/installing_rhel_using_an_image/kickstart-commands-and-options-reference_installing-rhel-using-an-image
-%>
<%
rhel_compatible = @host.operatingsystem.family == 'Redhat' && @host.operatingsystem.name != 'Fedora'
is_fedora = @host.operatingsystem.name == 'Fedora'
os_major = @host.operatingsystem.major.to_i
os_minor = @host.operatingsystem.minor.to_i
realm_compatible = (@host.operatingsystem.name == 'Fedora' && os_major >= 20) || (rhel_compatible && os_major >= 7)
# safemode renderer does not support unary negation
proxy_uri = host_param('http-proxy') ? "http://#{host_param('http-proxy')}:#{host_param('http-proxy-port')}" : nil
proxy_string = proxy_uri ? " --proxy=#{proxy_uri}" : ''
section_end = (rhel_compatible && os_major <= 5) ? '' : '%end'
iface = @host.provision_interface
use_rhsm = (@host.operatingsystem.name == 'RedHat' || @host.operatingsystem.name == 'RHEL') && os_major >= 9
ostreecontainer = host_param('ostreecontainer')
unless ostreecontainer.present?
raise "Host parameter 'ostreecontainer' is required for this template."
end
-%>
# This kickstart file was rendered from the Foreman provisioning template "<%= @template_name %>".
# for <%= @host %> running <%= @host.operatingsystem.name %> <%= os_major %> <%= @arch %>
# Organization: <%= @host.organization %>
# Location: <%= @host.location %>
<%
if plugin_present?('katello')
-%>
# Lifecycle environment: <%= @host.single_lifecycle_environment %>
# Content View: <%= @host.single_content_view %>
# Content Source: <%= @host.content_source %>
<% end -%>
<% if (is_fedora && os_major < 29) || (rhel_compatible && os_major <= 7) -%>
install
<% end -%>
lang <%= host_param('lang') || 'en_US.UTF-8' %>
selinux --<%= host_param('selinux-mode') || host_param('selinux') || 'enforcing' %>
keyboard <%= host_param('keyboard') || 'us' %>
<%
# start with provisioning interface, then other non-bond non-bridge interfaces and the bonds + bridges at the end
@host.interfaces.reject{ |iface| iface.bmc? }.sort_by { |iface| (iface.bond? || iface.bridge?) ? 0 : iface.provision? ? 20 : 10 }.each do |iface|
-%>
<%= snippet(
'kickstart_network_interface',
variables: {
iface: iface,
host: @host,
static: @static,
static6: @static6
}
) -%>
<%
end
-%>
ostreecontainer --url <%= ostreecontainer %> <%= proxy_string %>
rootpw --iscrypted <%= root_pass %>
#<%# Firewall -%>
#<% if host_param_true?('disable-firewall') -%>
#firewall --disable
#<% else -%>
#firewall --<%= os_major >= 6 ? 'service=' : '' %>ssh
#<% end -%>
#<%# Kdump -%>
#<% if host_param('kdump-options').present? -%>
#%addon com_redhat_kdump <%= host_param('kdump-options') %>
#%end
#<% end -%>
<%# Timezone + NTP -%>
<% if rhel_compatible && os_major < 9 -%>
timezone --utc <%= host_param('time-zone') || 'UTC' %> <%= host_param('ntp-server') ? "--ntpservers #{host_param('ntp-server')}" : '' %>
<% else -%>
timezone --utc <%= host_param('time-zone') || 'UTC' %>
<% if host_param('ntp-pools') -%>
<% host_param('ntp-pools').each do |ntppool| -%>
timesource --ntp-pool <%= ntppool %>
<% end -%>
<% elsif host_param('ntp-server') -%>
timesource --ntp-server <%= host_param('ntp-server') %>
<% end -%>
<% end -%>
<% if rhel_compatible -%>
services --disabled gpm,sendmail,cups,pcmcia,isdn,rawdevices,hpoj,bluetooth,openibd,avahi-daemon,avahi-dnsconfd,hidd,hplip,pcscd
<% end -%>
<% if realm_compatible && @host.realm && @host.realm.realm_type == 'Active Directory' -%>
# One-time password will be requested at install time. Otherwise, $HOST[OTP] is used as a placeholder value.
realm join --one-time-password='<%= @host.otp || "$HOST[OTP]" %>' <%= @host.realm %>
<% end -%>
<% if @host.operatingsystem.name == 'Fedora' && os_major <= 16 -%>
# Bootloader exception for Fedora 16:
bootloader --append="<%= host_param('bootloader-append') || 'nofb quiet splash=quiet' %> <%= ks_console %>" <%= grub_pass %>
part biosboot --fstype=biosboot --size=1
<% else -%>
bootloader --location=mbr --append="<%= host_param('bootloader-append') || 'nofb quiet splash=quiet' %>" <%= grub_pass %>
<% if os_major == 5 -%>
key --skip
<% end -%>
<% end -%>
<% if @dynamic -%>
%include /tmp/diskpart.cfg
<% else -%>
<%= @host.diskLayout %>
<% end -%>
<%= snippet('kickstart_rhsm') if use_rhsm -%>
<% if host_param_true?('use_graphical_installer') -%>
graphical
<% else -%>
skipx
text
<% end -%>
reboot<% if host_param_true?('install_reboot_kexec') %> --kexec<% end %>
<% if @dynamic -%>
%pre --log=/tmp/install.pre.dynamic.log
<%= snippet_if_exists(template_name + " custom pre") %>
<%= @host.diskLayout %>
<%= section_end %>
<% end -%>
%post --nochroot
exec < /dev/tty3 > /dev/tty3
chvt 3
(
<% if host_param_false?('no-resolv-override') -%>
cp -va /etc/resolv.conf /mnt/sysimage/etc/resolv.conf
<% end -%>
<%= snippet_if_exists(template_name + " custom postnochroot") -%>
chvt 1
) 2>&1 | tee /mnt/sysimage/root/install.postnochroot.log
<%= section_end %>
<%#
Main post script, if it fails the last post is still executed.
%>
%post
exec < /dev/tty3 > /dev/tty3
chvt 3
(
logger "Starting anaconda <%= @host %> postinstall"
<%= snippet('remote_execution_ssh_keys') %>
<%= snippet "blacklist_kernel_modules" %>
<%= snippet('ansible_provisioning_callback') %>
<%= snippet 'efibootmgr_netboot' %>
<%= snippet_if_exists(template_name + " custom post") %>
touch /tmp/foreman_built
<% if host_param_true?('use_graphical_installer') -%>
chvt 6
<% else -%>
chvt 1
<% end -%>
) 2>&1 | tee /root/install.post.log
<%= section_end %>
# copy %pre log files into chroot
%post --nochroot
cp -vf /tmp/*.pre.*.log /mnt/sysimage/root/
<%= section_end %>
<%#
The last post section halts Anaconda to prevent endless loop in case HTTP request fails
%>
<% if (is_fedora && os_major < 20) || (rhel_compatible && os_major < 7) -%>
%post
<% else -%>
%post --erroronfail --log=/root/install-callhome.post.log
<% end -%>
<%= snippet 'eject_cdrom' -%>
if test -f /tmp/foreman_built; then
echo "calling home: build is done!"
<%= indent(2, skip1: true, skip_content: 'EOF') { snippet('built', :variables => { :endpoint => 'built', :method => 'POST', :body_file => '/root/install.post.log' }) } -%>
else
echo "calling home: build failed!"
<%= indent(2, skip1: true, skip_content: 'EOF') { snippet('built', :variables => { :endpoint => 'failed', :method => 'POST', :body_file => '/root/install.post.log' }) } -%>
fi
sync
<%= section_end %>
@ianballou
Copy link
Author

Also requires the following patch to link the RHEL repository to the installation without the Anaconda url command :

diff --git a/app/views/unattended/provisioning_templates/snippet/kickstart_kernel_options.erb b/app/views/unattended/provisioning_templates/snippet/kickstart_kernel_options.erb
index 0e878d93b..2b0b6401e 100644
--- a/app/views/unattended/provisioning_templates/snippet/kickstart_kernel_options.erb
+++ b/app/views/unattended/provisioning_templates/snippet/kickstart_kernel_options.erb
@@ -128,6 +128,9 @@ description: |
 
   if host_param('kickstart_liveimg')
     options.push("inst.stage2=#{medium_uri}")
+  elsif host_param('ostreecontainer')
+    # Bootc installations need inst.stage2 for Anaconda installer runtime
+    options.push("inst.stage2=#{medium_uri}")
   end
 
   # S390x architecture has a different stage two image:

@ianballou
Copy link
Author

Note that the firewall section is commented out - to use it, you theoretically just need to ensure the firewalld RPM is installed in your container image.

@ianballou
Copy link
Author

Here is a shortened version of the KS template. The firewall setting is back because I built firewalld into my container image (quay.io/iballou/rhel10-squid) and pushed it up. Note that it is still missing subscription registration - for some reason I keep hitting an error: [Errno -2] Name or service not known.

<%#
kind: provision
name: Kickstart Default bootc - Trimmed
model: ProvisioningTemplate
oses:
- AlmaLinux
- CentOS
- CentOS_Stream
- Fedora
- RedHat
- Rocky
-%>
<%
  # Variable setup for post scripts and container URL
  ostreecontainer = host_param('ostreecontainer')
-%>
# This kickstart file was rendered from the Foreman provisioning template "<%= @template_name %>".

lang <%= host_param('lang') || 'en_US.UTF-8' %>
selinux --<%= host_param('selinux-mode') || 'enforcing' %>
keyboard <%= host_param('keyboard') || 'us' %>

<%
# Network setup is essential for pulling the container and reporting to Foreman
@host.interfaces.reject{ |iface| iface.bmc? }.sort_by { |iface| (iface.bond? || iface.bridge?) ? 0 : iface.provision? ? 20 : 10 }.each do |iface|
-%>
<%= snippet(
      'kickstart_network_interface',
      variables: {
        iface: iface,
        host: @host,
        static: @static,
        static6: @static6
      }
    ) -%>
<%
end
-%>

# --- Core Installation ---
# 1. Set the container image as the installation source
ostreecontainer --url <%= ostreecontainer %>

# 2. Set the root password to make the system login-able
rootpw --iscrypted <%= root_pass %>

# 3. Allow SSH for remote login
firewall --service=ssh

# --- Time ---
timezone --utc <%= host_param('time-zone') || 'UTC' %>
<% if host_param('ntp-pools') -%>
<% host_param('ntp-pools').each do |ntppool| -%>
timesource --ntp-pool <%= ntppool %>
<% end -%>
<% elsif host_param('ntp-server') -%>
timesource --ntp-server <%= host_param('ntp-server') %>
<% end -%>

# --- Bootloader and Partitioning ---
# This assumes you are assigning a partition table in Foreman.
# For a modern bootc install, you might replace the bootloader/diskLayout
# sections with: autopart --type=bootc
bootloader --location=mbr --append="<%= host_param('bootloader-append') || 'nofb quiet splash=quiet' %>" <%= grub_pass %>
<%= @host.diskLayout %>

# --- Finalize ---
text
skipx
reboot

# --- Post-Install Scripts ---
# This section injects SSH keys for Foreman and signals the build is done.
%post
exec < /dev/tty3 > /dev/tty3
chvt 3
(
<%= snippet('remote_execution_ssh_keys') %>
touch /tmp/foreman_built
chvt 1
) 2>&1 | tee /root/install.post.log
%end

<%#
The last post section tells Foreman the build is complete.
%>
%post --erroronfail --log=/root/install-callhome.post.log
if test -f /tmp/foreman_built; then
  echo "calling home: build is done!"
  <%= indent(2, skip1: true, skip_content: 'EOF') { snippet('built', :variables => { :endpoint => 'built', :method => 'POST', :body_file => '/root/install.post.log' }) } -%>
else
  echo "calling home: build failed!"
  <%= indent(2, skip1: true, skip_content: 'EOF') { snippet('built', :variables => { :endpoint => 'failed', :method => 'POST', :body_file => '/root/install.post.log' }) } -%>
fi
sync
%end

@ianballou
Copy link
Author

The following template is working with subscription-manager registration. The key here was that DNS wasn't set up correctly in the system environment:

<%#
kind: provision
name: Kickstart Default bootc - Trimmed
model: ProvisioningTemplate
oses:
- AlmaLinux
- CentOS
- CentOS_Stream
- Fedora
- RedHat
- Rocky
-%>
<%
  # Variable setup for post scripts and container URL
  ostreecontainer = host_param('ostreecontainer')
-%>
# This kickstart file was rendered from the Foreman provisioning template "<%= @template_name %>".

lang <%= host_param('lang') || 'en_US.UTF-8' %>
selinux --<%= host_param('selinux-mode') || 'enforcing' %>
keyboard <%= host_param('keyboard') || 'us' %>

<%
# Network setup is essential for pulling the container and reporting to Foreman
@host.interfaces.reject{ |iface| iface.bmc? }.sort_by { |iface| (iface.bond? || iface.bridge?) ? 0 : iface.provision? ? 20 : 10 }.each do |iface|
-%>
<%= snippet(
      'kickstart_network_interface',
      variables: {
        iface: iface,
        host: @host,
        static: @static,
        static6: @static6
      }
    ) -%>
<%
end
-%>

# --- Core Installation ---
# 1. Set the container image as the installation source
ostreecontainer --url <%= ostreecontainer %>

# 2. Set the root password to make the system login-able
rootpw --iscrypted <%= root_pass %>

# 3. Allow SSH for remote login
firewall --service=ssh

# --- Time ---
timezone --utc <%= host_param('time-zone') || 'UTC' %>
<% if host_param('ntp-pools') -%>
<% host_param('ntp-pools').each do |ntppool| -%>
timesource --ntp-pool <%= ntppool %>
<% end -%>
<% elsif host_param('ntp-server') -%>
timesource --ntp-server <%= host_param('ntp-server') %>
<% end -%>

# --- Bootloader and Partitioning ---
# This assumes you are assigning a partition table in Foreman.
# For a modern bootc install, you might replace the bootloader/diskLayout
# sections with: autopart --type=bootc
bootloader --location=mbr --append="<%= host_param('bootloader-append') || 'nofb quiet splash=quiet' %>" <%= grub_pass %>
<%= @host.diskLayout %>

# --- Finalize ---
text
skipx
reboot

# --- Post-Install Scripts ---


%post --nochroot
exec < /dev/tty3 > /dev/tty3
chvt 3
(
<% if host_param_false?('no-resolv-override') -%>
cp -va /etc/resolv.conf /mnt/sysimage/etc/resolv.conf
<% end -%>

# Hack to try fixing DNS

echo "search example.com" > /mnt/sysimage/etc/resolv.conf
echo "nameserver 192.168.73.1" >> /mnt/sysimage/etc/resolv.conf

chvt 1
) 2>&1 | tee /mnt/sysimage/root/install.postnochroot.log
%end


<%#
  This section injects SSH keys for Foreman, registers the system,
  and signals the build is done.
%>
%post
exec < /dev/tty3 > /dev/tty3
chvt 3
(
# Ensure DNS resolution works in chroot environment
cat > /etc/resolv.conf << 'EOF'
search example.com
nameserver 192.168.73.1
EOF

echo "=== DNS Configuration ==="
cat /etc/resolv.conf
echo "=== Testing DNS Resolution ==="
nslookup cdn.redhat.com || echo "WARNING: Cannot resolve cdn.redhat.com"
echo "=========================="

<%= snippet 'redhat_register' -%>
<%= snippet('remote_execution_ssh_keys') %>
touch /tmp/foreman_built
chvt 1
) 2>&1 | tee /root/install.post.log
%end

<%#
The last post section tells Foreman the build is complete.
%>
%post --erroronfail --log=/root/install-callhome.post.log
if test -f /tmp/foreman_built; then
  echo "calling home: build is done!"
  <%= indent(2, skip1: true, skip_content: 'EOF') { snippet('built', :variables => { :endpoint => 'built', :method => 'POST', :body_body_file => '/root/install.post.log' }) } -%>
else
  echo "calling home: build failed!"
  <%= indent(2, skip1: true, skip_content: 'EOF') { snippet('built', :variables => { :endpoint => 'failed', :method => 'POST', :body_body_file => '/root/install.post.log' }) } -%>
fi
sync
%end

@ianballou
Copy link
Author

Latest KS template with /etc/resolv in chroot from the Foreman host:

<%#
kind: provision
name: Kickstart Default bootc - Trimmed
model: ProvisioningTemplate
oses:
- AlmaLinux
- CentOS
- CentOS_Stream
- Fedora
- RedHat
- Rocky
-%>
<%
  # Variable setup for post scripts and container URL
  ostreecontainer = host_param('ostreecontainer')
-%>
# This kickstart file was rendered from the Foreman provisioning template "<%= @template_name %>".

lang <%= host_param('lang') || 'en_US.UTF-8' %>
selinux --<%= host_param('selinux-mode') || 'enforcing' %>
keyboard <%= host_param('keyboard') || 'us' %>

<%
# Network setup is essential for pulling the container and reporting to Foreman
@host.interfaces.reject{ |iface| iface.bmc? }.sort_by { |iface| (iface.bond? || iface.bridge?) ? 0 : iface.provision? ? 20 : 10 }.each do |iface|
-%>
<%= snippet(
      'kickstart_network_interface',
      variables: {
        iface: iface,
        host: @host,
        static: @static,
        static6: @static6
      }
    ) -%>
<%
end
-%>

# --- Core Installation ---
# 1. Set the container image as the installation source
ostreecontainer --url <%= ostreecontainer %>

# 2. Set the root password to make the system login-able
rootpw --iscrypted <%= root_pass %>

# 3. Allow SSH for remote login
firewall --service=ssh

# --- Time ---
timezone --utc <%= host_param('time-zone') || 'UTC' %>
<% if host_param('ntp-pools') -%>
<% host_param('ntp-pools').each do |ntppool| -%>
timesource --ntp-pool <%= ntppool %>
<% end -%>
<% elsif host_param('ntp-server') -%>
timesource --ntp-server <%= host_param('ntp-server') %>
<% end -%>

# --- Bootloader and Partitioning ---
# This assumes you are assigning a partition table in Foreman.
# For a modern bootc install, you might replace the bootloader/diskLayout
# sections with: autopart --type=bootc
bootloader --location=mbr --append="<%= host_param('bootloader-append') || 'nofb quiet splash=quiet' %>" <%= grub_pass %>
<%= @host.diskLayout %>

# --- Finalize ---
text
skipx
reboot

# --- Post-Install Scripts ---


%post --nochroot
exec < /dev/tty3 > /dev/tty3
chvt 3
(
<% if host_param_false?('no-resolv-override') -%>
# Copy DNS configuration from installation environment
cp -va /etc/resolv.conf /mnt/sysimage/etc/resolv.conf
<% end -%>

chvt 1
) 2>&1 | tee /mnt/sysimage/root/install.postnochroot.log
%end


<%#
  This section injects SSH keys for Foreman, registers the system,
  and signals the build is done.
%>
%post
exec < /dev/tty3 > /dev/tty4
chvt 3
(
# DNS should already be configured from %post --nochroot copy
# But verify and recreate if missing (bootc containers may not have it)
if [ ! -f /etc/resolv.conf ] || [ ! -s /etc/resolv.conf ]; then
  echo "WARNING: /etc/resolv.conf missing or empty in chroot, recreating from Foreman configuration"
  cat > /etc/resolv.conf << 'RESOLV_EOF'
<% if @host.domain -%>
search <%= @host.domain.name %>
<% end -%>
<% [@host.subnet.dns_primary, @host.subnet.dns_secondary].compact.each do |nameserver| -%>
nameserver <%= nameserver %>
<% end -%>
RESOLV_EOF
fi

echo "=== DNS Configuration ==="
cat /etc/resolv.conf
echo "=== Testing DNS Resolution ==="
nslookup cdn.redhat.com || echo "WARNING: Cannot resolve cdn.redhat.com"
echo "=========================="

<%= snippet 'redhat_register' -%>
<%= snippet('remote_execution_ssh_keys') %>
touch /tmp/foreman_built
chvt 1
) 2>&1 | tee /root/install.post.log
%end

<%#
The last post section tells Foreman the build is complete.
%>
%post --erroronfail --log=/root/install-callhome.post.log
if test -f /tmp/foreman_built; then
  echo "calling home: build is done!"
  <%= indent(2, skip1: true, skip_content: 'EOF') { snippet('built', :variables => { :endpoint => 'built', :method => 'POST', :body_body_file => '/root/install.post.log' }) } -%>
else
  echo "calling home: build failed!"
  <%= indent(2, skip1: true, skip_content: 'EOF') { snippet('built', :variables => { :endpoint => 'failed', :method => 'POST', :body_body_file => '/root/install.post.log' }) } -%>
fi
sync
%end

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment