# MAIN KICKSTART

# Author:  Linuxfabrik GmbH, Zurich, Switzerland
# Contact: info (at) linuxfabrik (dot) ch
#          https://www.linuxfabrik.ch/
# License: The Unlicense, see LICENSE file.

# This kickstart file consists of two parts.
# * The main part, which is quite minimal,
# * and a dynamic part (/tmp/dynamic.ks) that gets written during the %pre script.
#   The %pre script starts by parsing some options from the kernel cmdline and
#   detecting the RHEL version.
#   It then fills /tmp/dynamic.ks depending on these variables.

# The order of the commands is relatively compatible with the output of
# https://access.redhat.com/labs/kickstartconfig/

# See the README for more details.


# System language
lang en_US.UTF-8

# Keyboard layouts
keyboard us

# System timezone
timezone Europe/Zurich --utc

# Shutdown after installation
shutdown

# Use text mode install and CDROM installation media
text
cdrom

# This command is required when performing an unattended installation on a system with previously
# initialized disks.
zerombr

# Automatically creates partitions required by your hardware platform, eg /boot/efi or biosboot
reqpart

# Network information
network --hostname=localhost.localdomain
# note: do not set any other network options. dhcp is the default anyway, and setting them here disregards the ip and nameserver settings on the kernel command line
# network --bootproto=dhcp --device=link --activate --onboot=on

# Do not configure the X Window System
skipx

# Disable the Setup Agent on first boot
firstboot --disable

# State of SELinux on the installed system
selinux --enforcing

# Firewalling should be done later on by the admin. Required for the cloud
firewall --disable

# System services
services --disabled="kdump" --enabled="NetworkManager,sshd"

# Disable kdump by default, frees up some memory
%addon com_redhat_kdump --disable
%end

%include /tmp/dynamic.ks



%pre --logfile /tmp/kickstart.install.pre.log --erroronfail
# fedora doesn't have platform-python and rhel8 only has platform-python, so we'll have to
# detect what we have
PYTHON=$(ls /usr/libexec/platform-python /usr/bin/python3 2> /dev/null | head -n1)
cat << EOT > /tmp/pre-script.py
#!$PYTHON
import os
import re
import stat
import sys


def isblockdevice(path):
    return os.path.exists(path) and stat.S_ISBLK(os.stat(path).st_mode)


lfkeys = [
    'ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIDuYw1auj8Lo6l5Ie8H7q419pKNjD3LSDZpFLNI0KehO chris.drescher@linuxfabrik.ch',
    'ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAACAQDaWaDOEGqhZy1XD3a0IJKPvuB0wz2Yzc7Y8b2E91PPDSfi2wczUjZ9T2f8J7fw46i6O8dUFdsle8HePlVt1f6P+SPr84KIvte4sCXPGjHO7UlP+0biPl1FJbe+LU6akGVgAhc37CTn7h10COim5TpIdmPfn8A1y0Y8G3GNTVELSC4GG6rJLgme++OTkNlenH+L7KSobQE1v+MS4mRjrg+qitgPzBv1VgTWJff4d3vdEtb9zwVdzZzyXcdP5/nWH54iDaZawLsyvPXG2VDQq9bUn4CbZ+/ldrVg+yi8Y5RlSLFlbaX2XKnqf8mC3fpszXrmZ93d/idvCLDQ2ijV1FQzYLNg9nstV6VHCek1+g0u3oQ17CyRRCuxSRf3kDagO/+FMGwIliQTSX8rTx0epYzj4vUKA0nYIHXhwklUTG2PFNgJ1Nfxllqblij/PbFfoJCCp+st7/ewYYiclV6jrAg01/bqAvrdS8PW6JQpTIeuJRZhkrvCxgzDsuYE+EqTHxyBs7Uxu9D8QvgZEYiwuClRE92xPNmuEk/BDpX9s8mcXtamp57AUESRFWPFUZoDFuHRjHz56lXJ0UIfDR7XDZHumdQRt6jh7Sj0YGVN3Es9UIqka9dZRiXLdZ9q/C0IReZKtcKDSIn/xB1Kt2qAIRLpSG3IX8JmONOa1h/Gns1NKw==  markus.frei@linuxfabrik.ch',
    'ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIM5DxWzuUlSfdHHE1c4oQ2cbC0TyjXkVCuNKBJvn7TvX navid.sassan@linuxfabrik.ch',
]
lftype='minimal'
lfdisk = None
for guess in ['vda', 'sda', 'nvme0n1']:
    if isblockdevice('/dev/{}'.format(guess)):
        lfdisk = guess
        break

print('Linuxfabrik Kickstart: Set kernel cmdline arguments')
with open('/proc/cmdline', 'r') as file:
    cmdline = file.read()
    for item in cmdline.split(" "):
        if item.split("=")[0] == "lftype":
            lftype=item.split("=")[1].strip()
        elif item.split("=")[0] == "lfdisk":
            lfdisk=item.split("=")[1].strip()

if not lfdisk:
    print('Linuxfabrik Kickstart: Lfdisk is not given and no disk to install to could be guessed. Aborting...')
    sys.exit(1)

print('Linuxfabrik Kickstart: Lftype: {}, lfdisk: {}'.format(lftype, lfdisk))

print('Linuxfabrik Kickstart: Set bootloader')
# * console=ttyS0,115200n8:
#   required for cloud images.
#   see https://docs.openstack.org/image-guide/openstack-images.html#ensure-image-writes-boot-log-to-console
# * no_timer_check:
#   see https://opendev.org/openstack/diskimage-builder/commit/1ec93f43a8736171cb78382fd6184f03c001771b
# * net.ifnames=0:
#   for cloud images, 'eth0' _is_ the predictable device name
# * audit=1 audit_backlog_limit=8192:
#   required by CIS
# * vsyscall=none:
#   due to https://www.stigviewer.com/stig/red_hat_enterprise_linux_8/2020-11-25/finding/V-230278
bootloader_cis = 'bootloader --location=mbr --boot-drive={} --append="audit=1 audit_backlog_limit=8192 vsyscall=none"'.format(lfdisk)
bootloader_cloud = 'bootloader --location=mbr --boot-drive={} --append="console=tty0 console=ttyS0,115200n8 no_timer_check net.ifnames=0 vsyscall=none"'.format(lfdisk)
bootloader_cloud_cis = 'bootloader --location=mbr --boot-drive={} --append="console=tty0 console=ttyS0,115200n8 no_timer_check net.ifnames=0 audit=1 audit_backlog_limit=8192 vsyscall=none"'.format(lfdisk)
bootloader_minimal = 'bootloader --location=mbr --boot-drive={} --append="vsyscall=none"'.format(lfdisk)

print('Linuxfabrik Kickstart: Set packages')
packages = [
    '@core',
    '-plymouth', # No need for plymouth. Also means anaconda won't put rhgb/quiet on kernel command line
]
packages_cis = []
packages_cloud = [
    'cloud-init',
    'cloud-utils-growpart',
    'dhcp-client',
    'qemu-guest-agent',
    'rng-tools',
    'tuned',
    '-aic94xx-firmware',
    '-alsa-firmware',
    '-alsa-lib',
    '-alsa-tools-firmware',
    '-biosdevname',
    '-iprutils',
    '-ivtv-firmware',
    '-iwl100-firmware',
    '-iwl1000-firmware',
    '-iwl105-firmware',
    '-iwl135-firmware',
    '-iwl2000-firmware',
    '-iwl2030-firmware',
    '-iwl3160-firmware',
    '-iwl3945-firmware',
    '-iwl4965-firmware',
    '-iwl5000-firmware',
    '-iwl5150-firmware',
    '-iwl6000-firmware',
    '-iwl6000g2a-firmware',
    '-iwl6000g2b-firmware',
    '-iwl6050-firmware',
    '-iwl7260-firmware',
    '-langpacks-*',
    '-langpacks-en',
    '-libertas-sd8686-firmware',
    '-libertas-sd8787-firmware',
    '-libertas-usb8388-firmware',
]
packages_cloud_cis = packages_cloud
packages_minimal = []

print('Linuxfabrik Kickstart: Set partitioning schema')
part_cis = [
    'logvol /              --fstype="xfs"  --size=4096   --vgname=rl --name=root',
    'logvol /home          --fstype="xfs"  --size=1024   --vgname=rl --name=home          --fsoptions="nodev,nosuid"',
    'logvol /tmp           --fstype="xfs"  --size=1024   --vgname=rl --name=tmp           --fsoptions="nodev,noexec,nosuid"',
    'logvol /var           --fstype="xfs"  --size=4096   --vgname=rl --name=var           --fsoptions="nodev,nosuid"',
    'logvol /var/log       --fstype="xfs"  --size=2048   --vgname=rl --name=var_log       --fsoptions="nodev,noexec,nosuid"',
    'logvol /var/log/audit --fstype="xfs"  --size=512    --vgname=rl --name=var_log_audit --fsoptions="nodev,noexec,nosuid"',
    'logvol /var/tmp       --fstype="xfs"  --size=1024   --vgname=rl --name=var_tmp       --fsoptions="nodev,noexec,nosuid"',
    'logvol swap           --fstype="swap" --recommended --vgname=rl --name=swap',
]
part_cloud = [
    'logvol /              --fstype="xfs"  --size=1024   --vgname=rl --name=root --grow',
    'logvol swap           --fstype="swap" --recommended --vgname=rl --name=swap',
]
part_cloud_cis = part_cis
part_minimal = part_cloud

print('Linuxfabrik Kickstart: Define users')
users_cis = [
    {
        'name': 'linuxfabrik',
        'cmd': 'user --name=linuxfabrik --groups=wheel --password=password --plaintext',
        'keys': lfkeys,
    },
    {
        'name': 'root',
        'cmd': 'rootpw --plaintext --lock password',
        'keys': [],
    },
]
users_cloud = [
    {
        'name': 'linuxfabrik',
        'cmd': 'user --name=linuxfabrik --groups=wheel --lock',
        'keys': [],
    },
    {
        'name': 'root',
        'cmd': 'rootpw --plaintext --lock password',
        'keys': [],
    },
]
users_cloud_cis = users_cloud
users_minimal = users_cis

print('Linuxfabrik Kickstart: Create post scripts')
post_cis = '''
# allow password-less sudo
cat > /etc/sudoers.d/linuxfabrik << EOF
%linuxfabrik ALL=(ALL) NOPASSWD: ALL
EOF
'''

post_cloud = '''
# setup systemd to boot to the right runlevel
echo "Setting default runlevel to multiuser text mode"
rm -f /etc/systemd/system/default.target
ln -s /lib/systemd/system/multi-user.target /etc/systemd/system/default.target

# this is installed by default but we don't need it in virt
# Commenting out the following for =1234504
# rpm works just fine for removing this, no idea why yum can't cope
echo "Removing linux-firmware package"
rpm --erase linux-firmware

# Remove firewalld; was supposed to be optional in F18+, but is pulled in
# in install/image building.
echo "Removing firewalld"
# FIXME! clean_requirements_on_remove is the default with yum, but may
# not work when package was installed by Anaconda instead of command line.
# Also -- check if this is still even needed with new anaconda -- disabled
# firewall should _not_ pull in this package.
# yum -C -y remove "firewalld*" --setopt="clean_requirements_on_remove=1"
yum --cacheonly -y erase "firewalld*"

# Another one needed at install time but not after that, and it pulls
# in some unneeded deps (like, newt and slang)
echo "Removing authconfig"
yum --cacheonly -y erase authconfig

echo "Removing avahi"
yum --cacheonly -y remove avahi\\*

echo "Installing cloud-init cloud-utils-growpart rng-tools tuned"
# these tools always fail to install in the %packages section, so try this here
yum -y install cloud-init cloud-utils-growpart rng-tools tuned

# Since the scriptlets from yum install are running in chroot, they can't do a daemon-reload
# ("Running in chroot, ignoring command 'daemon-reload'"), so a "systemctl enable --now" would
# fail. "systemctl enable" is enough here.
systemctl enable cloud-init
systemctl enable cloud-init-local
systemctl enable cloud-config
systemctl enable cloud-final
systemctl enable rngd

echo "Getty fixes"
# although we want console output going to the serial console, we don't
# actually have the opportunity to login there. FIX.
# we don't really need to auto-spawn _any_ gettys.
sed --in-place 's/^#NAutoVTs=.*/NAutoVTs=0/' /etc/systemd/logind.conf

echo "Network fixes"
# initscripts don't like this file to be missing.
# and https://bugzilla.redhat.com/show_bug.cgi?id=1204612
cat > /etc/sysconfig/network << EOF
NETWORKING=yes
NOZEROCONF=yes
DEVTIMEOUT=10
EOF

echo "Truncate /etc/resolv.conf"
# Anaconda is writing an /etc/resolv.conf from the install environment.
# The system should start out with an empty file, otherwise cloud-init
# will try to use this information and may error:
# https://bugs.launchpad.net/cloud-init/+bug/1670052
truncate --size=0 /etc/resolv.conf

echo "Disable predictable network interface names"
# For cloud images, 'eth0' _is_ the predictable device name, since
# we don't want to be tied to specific virtual (!) hardware
rm -f /etc/udev/rules.d/70*
ln -s /dev/null /etc/udev/rules.d/80-net-name-slot.rules

echo 'Set generic localhost names (/etc/hosts)'
cat > /etc/hosts << EOF
127.0.0.1   localhost localhost.localdomain localhost4 localhost4.localdomain4
::1         localhost localhost.localdomain localhost6 localhost6.localdomain6

EOF

# Because memory is scarce resource in most cloud/virt environments,
# and because this impedes forensics, we are differing from the Fedora
# default of having /tmp on tmpfs.
# However, there is no need to disable the mount, as we are already overwriting the options
# for /tmp above.
# Masking the mount now would lead to a missing /tmp.
# echo "Disabling tmpfs for /tmp."
# systemctl mask tmp.mount

echo 'Set tuned profile to virtual-guest'
echo 'virtual-guest' > /etc/tuned/active_profile

echo 'Make sure firstboot does not start'
echo 'RUN_FIRSTBOOT=NO' > /etc/sysconfig/firstboot

echo 'Adjust sudoers for the linuxfabrik user'
cat > /etc/sudoers.d/linuxfabrik << EOF
%linuxfabrik ALL=(ALL) NOPASSWD: ALL
EOF
sed --in-place 's/name: cloud-user/name: linuxfabrik/g' /etc/cloud/cloud.cfg

echo 'Cleaning up old yum repodata'
yum clean all

echo 'Increase DHCP client retry/timeouts'
# change dhcp client retry/timeouts to resolve =6866
cat  >> /etc/dhcp/dhclient.conf << EOF

timeout 300;
retry 60;
EOF

echo 'Adjust console output in /etc/default/grub and regenerate grub config'
# If init script messages need to be seen on the serial console as well, it should be made
# the primary by swapping the order of the console parameters:
sed --in-place 's/console=ttyS0,115200n8 console=tty0/console=tty0 console=ttyS0,115200n8/' /etc/default/grub
if [ -d /sys/firmware/efi/ ]; then
    grub_config_file=$(find /boot/efi -name grub.cfg)
else
    grub_config_file=$(find /boot/ -name grub.cfg)
fi
if [ -z "$grub_config_file" ]; then
    echo 'Could not find a grub.cfg in /boot. Skipping grub2-mkconfig'
else
    grub2-mkconfig --output=$grub_config_file
fi

echo "Removing random-seed so it's not the same in every image"
rm -f /var/lib/systemd/random-seed

echo 'Remove /etc/machine-id'
# https://git.resf.org/sig_core/kickstarts/src/branch/r8/Rocky-8-GenericCloud-LVM.ks
cat /dev/null > /etc/machine-id

echo 'Remove various other log / cache files'
rm -f /root/.wget-hsts
rm -rf /root/anaconda-ks.cfg
rm -rf /root/install.log
rm -rf /root/install.log.syslog
rm -rf /var/lib/yum/*
rm -rf /var/log/anaconda*
rm -rf /var/log/yum.log
rm -rf /var/tmp/*
find /var/log -type f -exec truncate --size=0 {} \\;
export HISTSIZE=0
cat /dev/null > ~/.bash_history
history -c

echo 'Fixing SELinux contexts'
touch /var/log/cron
touch /var/log/boot.log
mkdir -p /var/cache/yum
# ignore return code because UEFI systems with vfat filesystems
# that don't support selinux will give us errors like
# "Could not set context for /boot/efi/EFI/BOOT/BOOTX64.EFI:  Operation not supported"
/usr/sbin/fixfiles -R -a restore || true
'''

post_cloud_cis = post_cloud
post_minimal = post_cis


print('Linuxfabrik Kickstart: Act on cmdargs & discovery for {}'.format(lftype))
if lftype == 'cloud':
    bootloader = bootloader_cloud
    packages += packages_cloud
    part = part_cloud
    post = post_cloud
    users = users_cloud

elif lftype == 'cloud-cis':
    bootloader = bootloader_cloud_cis
    packages += packages_cloud_cis
    part = part_cloud_cis
    post = post_cloud_cis
    users = users_cloud_cis

elif lftype == 'cis':
    bootloader = bootloader_cis
    packages += packages_cis
    part = part_cis
    post = post_cis
    users = users_cis

elif lftype == 'minimal':
    bootloader = bootloader_minimal
    packages += packages_minimal
    part = part_minimal
    post = post_minimal
    users = users_minimal

# version specifics
with open('/etc/redhat-release') as file:
    rhel_version = int(re.findall('[0-9]{1,2}', file.read())[0])

print('Linuxfabrik Kickstart: Detected OS version {}'.format(rhel_version))
if rhel_version == 7:
    try:
        packages.remove('dhcp-client') # does not exist on RHEL7
    except ValueError:
        pass # means it is already removed

print('Linuxfabrik Kickstart: Write dynamic.ks')
dynamic = []
keypost = []

dynamic.append('# System users')
for user in users:
    dynamic.append(user['cmd'])
    for key in user['keys']:
        if rhel_version == 7: # sshkey only exists for RHEL8+. instead manually add the keys
            if user['name'] == 'root':
                keypost.append('mkdir -m0700 /root/.ssh')
                keypost.append('echo "{}" >> /root/.ssh/authorized_keys'.format(key))
                keypost.append('restorecon -F /root/.ssh/authorized_keys')
            else:
                keypost.append('mkdir -m0700 /home/{}/.ssh'.format(user['name']))
                keypost.append('echo "{}" >> "/home/{}/.ssh/authorized_keys"'.format(key, user['name']))
                keypost.append('chown -R {}: /home/{}/.ssh'.format(user['name'], user['name']))
                keypost.append('restorecon -F "/home/{}/.ssh/authorized_keys"'.format(user['name']))
        else:
            dynamic.append('sshkey --user={} "{}"'.format(user['name'], key))

dynamic.append('# Only touch $lfdisk. This setting is also respected by zerombr (confirmed on RHEL8 and 9)')
dynamic.append('ignoredisk --only-use={}'.format(lfdisk))

dynamic.append('# System bootloader configuration')
dynamic.append(bootloader)

dynamic.append('# Do not remove any partitions, but initializes a disk (or disks) by creating a default disk label')
dynamic.append('clearpart --all --drives={} --initlabel --disklabel gpt'.format(lfdisk))

dynamic.append('# Partitioning')
dynamic.append('volgroup rl --pesize=4096 pv.0')
dynamic.append('part pv.0 --fstype=lvmpv --ondisk={} --size=1 --grow'.format(lfdisk))
dynamic.append('part /boot --fstype=xfs --ondisk={} --size=1024 --asprimary'.format(lfdisk))
dynamic.extend(part)

dynamic.append('%packages')
dynamic.extend(packages)
dynamic.append('%end')

dynamic.append('%post --erroronfail')
dynamic.append(post)
dynamic.append('%end\n')

# actually write to file
with open('/tmp/dynamic.ks', 'w') as file:
    file.write('\n'.join(dynamic))

if rhel_version == 7:
    with open('/usr/share/anaconda/post-scripts/70-install-ssh-keys.ks', 'a') as file:
        file.write('%post\n')
        file.write('\n'.join(keypost))
        file.write('%end\n')
print('Linuxfabrik Kickstart: Wrote dynamic kickstart to /tmp/dynamic.ks')
EOT
$PYTHON /tmp/pre-script.py
%end



%post --nochroot
root_mount=$(awk '/rl-root/{print $2}' /proc/mounts)
echo "Copying /tmp/dynamic.ks to $root_mount/root/"
cp /tmp/dynamic.ks $root_mount/root/
[ -f /usr/share/anaconda/post-scripts/70-install-ssh-keys.ks ] && cp /usr/share/anaconda/post-scripts/70-install-ssh-keys.ks $root_mount/root/
%end