#!/bin/bash

#*********************************************************************
#
# fai-cd -- make a fai CD, a bootable CD that performs the FAI
#
# This script is part of FAI (Fully Automatic Installation)
# (c) 2004-2026 by Thomas Lange, lange@cs.uni-koeln.de
# Universitaet zu Koeln
# Support for grub2 added by Sebastian Hetze, s.hetze@linux-ag.de
# with major support from Michael Prokop, prokop@grml-solutions.com
# dracut/squashfs support by Kerim Gueney, gueney@informatik.uni-koeln.de
# based on a script called make-fai-bootcd by Niall Young <niall@holbytla.org>
#
#*********************************************************************
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
# General Public License for more details.
#
# A copy of the GNU General Public License is available as
# `/usr/share/common-licences/GPL' in the Debian GNU/Linux distribution
# or on the World Wide Web at http://www.gnu.org/copyleft/gpl.html.  You
# can also obtain it by writing to the Free Software Foundation, Inc.,
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
#*********************************************************************

set -e

# last die exit code 22

count=0
space=0
config_space=""
configset=0
forceremoval=0
burn=0
bootonly=0
squash_only=0
nomirror=0
hidevartmp=0
rel=0
target=0
autodiscover=0
vname="FAI_CD"

hidedirs="/usr/share/locale /usr/share/doc /var/lib/apt /var/lib/aptitude /var/cache/debconf /var/cache/apt /var/cache/man /usr/share/man /var/lib/dpkg/info /media/mirror/aptcache "

# we need FAI_CONFIGDIR, NFSROOT

# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
usage() {

    cat <<-EOF
	fai-cd Copyright (C) 2004-2026 Thomas Lange

	Usage: fai-cd [OPTIONS] -m MIRRORDIR ISONAME
	Usage: fai-cd [OPTIONS] -B ISONAME
	Usage: fai-cd [OPTIONS] -S IMAGEFILE
	Create a FAI CD, a bootable CD that performs the FAI or a squashfs boot image.
	Read the man pages pages fai-cd(8) for more information.
EOF
exit 0
}
# - - - - - - - - - - - - - - - - - - - - - - - - - -
die() {

    local e=$1   # first parameter is the exit code
    shift

    echo -e "ERROR: $*" >&2   # print error message
    exit $e
}
# - - - - - - - - - - - - - - - - - - - - - - - - - -
check_programs() {

    local msg
    [ $squash_only -eq 0 ] && ! command -v xorriso >&/dev/null && msg+="xorriso not found. Please install package."
    command -v mksquashfs >&/dev/null || msg+="\nmksquashfs not found. Please install the package squashfs-tools."
    command -v mkfs.vfat  >&/dev/null || msg+="\nmkfs.vfat not found. Please install the package dosfstools."
    command -v mcopy      >&/dev/null || msg+="\nmcopy not found. Please install the package mtools."

    if [ -n "$msg" ]; then
	die 8 "$msg"
    fi
}
# - - - - - - - - - - - - - - - - - - - - - - - - - -
create_new_nfsroot() {

    mkdir -p $tmp/cow/work $tmp/cow/rw $NEW

    mount -t overlay -o lowerdir=$ONFSROOT,upperdir=$tmp/cow/rw,workdir=$tmp/cow/work,default_permissions overlay $NEW

    customize_nfsroot $NEW
    rm -f $NEW/etc/resolv.conf
}
# - - - - - - - - - - - - - - - - - - - - - - - - - -
create_autodiscover_iso() {

    # boot-only CD does not need some dracut modules

    local compress
    local rel
    rel=$(ls $NEW/lib/modules/|head -1)
    local target=$NEW/lib/modules/$rel/kernel

    if [ -n "$_OPTJ" ]; then
	compress="--xz"
    else
	compress="--zstd"
    fi
    rm $NEW/boot/initrd.img*
    mountpoint -q $NEW/proc || mount --bind /proc $NEW/proc
    mountpoint -q $NEW/sys  || mount --bind /sys  $NEW/sys
    TMPDIR=/tmp chroot $NEW dracut -o ' fs-lib livenet rootfs-block ' -a 'overlayfs bash fai-autodiscover' "$compress" /boot/initrd.img-$rel $rel 2>/dev/null
    umount $NEW/proc
    umount $NEW/sys
    customize_nfsroot $target
}
# - - - - - - - - - - - - - - - - - - - - - - - - - -
create_squashfs_image() {

    local output_path

    # this is where the squashfs.img needs to be placed
    mkdir -p $tmp/LiveOS

    # create the squashfs.img
    output_path=$tmp/LiveOS/squashfs.img

    if [ $squash_only -eq 1 ]; then
        output_path=$isoname
    fi

    echo "Make squashfs file system"
    mksquashfs $NEW $output_path $sqopt -xattrs-exclude '^system.posix_acl_access|system.posix_acl_default'
}
# - - - - - - - - - - - - - - - - - - - - - - - - - -
prepare_grub_cfg() {

    # prepare grub.cfg
    cp $grub_config $NEW/tmp/grub.cfg
    # insert date into grub menu
    perl -pi -e "s/_VERSIONSTRING_/   $isoversion     /" $NEW/tmp/grub.cfg
    if [ -n "$config_space" ] || [ $configset -eq 1 ]; then
	perl -pi -e "s|FAI_CONFIG_SRC=(.*?)\S+|FAI_CONFIG_SRC=$config_space|" $NEW/tmp/grub.cfg
    fi
    provide_memtest_boot_option
}
# - - - - - - - - - - - - - - - - - - - - - - - - - -
copy_kernel_initrd() {

    # copy kernel and initrd
    cp -p $NEW/boot/vmlinuz-$kernelversion $tmp/boot/vmlinuz
    cp -p $NEW/boot/initrd.img-$kernelversion $tmp/boot/initrd.img
    cp -p $NEW/boot/config-$kernelversion $tmp/boot/
    rm -f $NEW/tmp/grub.cfg
    echo "$isoversion" > $tmp/FAI-CD
}
# - - - - - - - - - - - - - - - - - - - - - - - - - -
create_grub2_image_arm64() {

    mkdir -p $tmp/boot/grub
    if [ -d $NEW/usr/lib/grub/arm64-efi ]; then

	prepare_grub_cfg

	# create arch.efi and copy to vfat file system
	TMPDIR=tmp chroot $NEW grub-mkstandalone \
	    --format=arm64-efi \
	    --output=/tmp/grubaa64.efi \
	    --locales="" \
	    "boot/grub/grub.cfg=/tmp/grub.cfg"
	mv $NEW/tmp/grubaa64.efi $scratch/BOOTAA64.EFI

	mkfs.vfat -C $scratch/efiboot.img 6000 >/dev/null
	mmd -i $scratch/efiboot.img EFI EFI/boot
	mcopy -i $scratch/efiboot.img $scratch/BOOTAA64.EFI ::EFI/boot/
    else
        die 11 "No grub-efi-arm64-bin installation found in NFSROOT. Aborting."
    fi
    copy_kernel_initrd
}
# - - - - - - - - - - - - - - - - - - - - - - - - - -
create_grub2_image_x86() {

    mkdir -p $tmp/boot/grub

    if [ -d $NEW/usr/lib/grub/x86_64-efi ]; then

	prepare_grub_cfg

	# create arch.efi and copy to vfat file system
	TMPDIR=tmp chroot $NEW grub-mkstandalone \
	    --format=x86_64-efi \
	    --output=/tmp/bootx64.efi \
	    --locales="" \
	    "boot/grub/grub.cfg=/tmp/grub.cfg"
	mv $NEW/tmp/bootx64.efi $scratch

	mkfs.vfat -C $scratch/efiboot.img 6000 >/dev/null
	mmd -i $scratch/efiboot.img efi efi/boot
	mcopy -i $scratch/efiboot.img $scratch/bootx64.efi ::efi/boot/
    else
        die 11 "No grub-efi-amd64-bin installation found in NFSROOT. Aborting."
    fi
    if [ -d $NEW/usr/lib/grub/i386-pc ]; then
        cp -a $NEW/usr/lib/grub/i386-pc $NEW/tmp/grub
        echo -e "ext2\niso9660\ntar\n" > $NEW/tmp/grub/fs.lst
	TMPDIR=/tmp chroot $NEW grub-mkstandalone \
            --directory=/tmp/grub \
	    --format=i386-pc \
	    --output=/tmp/core.img \
	    --locales="" --fonts="" \
	    --install-modules="linux normal iso9660 biosdisk memdisk search ls echo test chain msdospart part_msdos part_gpt minicmd ext2 keystatus all_video font sleep gfxterm regexp" \
	    --modules="linux normal iso9660 biosdisk search" \
	    "boot/grub/grub.cfg=/tmp/grub.cfg"
	cat $NEW/usr/lib/grub/i386-pc/cdboot.img $NEW/tmp/core.img > $scratch/bios.img
	rm -rf $NEW/tmp/core.img $NEW/tmp/grub
    else
        die 11 "No grub-pc installation found in NFSROOT. Aborting."
    fi
    copy_kernel_initrd
}
# - - - - - - - - - - - - - - - - - - - - - - - - - -
provide_memtest_boot_option() {

    if [ $bootonly -eq 1 ]; then
	return
    fi

    if [ -f $NEW/boot/memtest86+x64.efi ] || [ -f $NEW/boot/memtest86+x64.bin ]; then
        cp -p $NEW/boot/memtest86+x64* $tmp/boot
        echo "Adding memtest86+ to grub menu"
    else
        return 0
    fi

    cat >> $NEW/tmp/grub.cfg <<'EOF'

    if [ "$grub_platform" == efi -a "$grub_cpu" == x86_64 ]; then
       menuentry "Memory test (memtest86+)" --unrestricted {
         search --set=root --file /FAI-CD
         linux /boot/memtest86+x64.efi
       }
    fi
    if [ "$grub_platform" == pc ]; then
       menuentry "Memory test (memtest86+)" --unrestricted {
         search --set=root --file /FAI-CD
         linux /boot/memtest86+x64.bin
       }
    fi
EOF
}
# - - - - - - - - - - - - - - - - - - - - - - - - - -
customize_nfsroot() {

    # hide/remove some dirs to save space and make the CD image smaller
    local path=$1
    local d

    for d in $hidedirs; do
        if [ -d $path/$d ]; then
	    [ "$debug" ] && echo "hiding $d"
	    rm -rf $path/$d/*
        fi
    done
}
# - - - - - - - - - - - - - - - - - - - - - - - - - -
cleanup_liveos_mounts() {

    mountpoint -q $NEW && umount $NEW
    rm -rf $tmp $scratch
}
# - - - - - - - - - - - - - - - - - - - - - - - - - -
mkiso_x86() {

    echo "Writing FAI CD-ROM image to $isoname. This may need some time."
    xorriso -report_about HINT -as mkisofs -iso-level 3 \
	-iso_mbr_part_type 00 \
        -full-iso9660-filenames \
	-volid "$vname" -appid "$aname" \
	-eltorito-boot boot/grub/bios.img  \
	-no-emul-boot -boot-load-size 4 -boot-info-table \
	--eltorito-catalog boot/grub/boot.cat \
	--grub2-boot-info \
	--grub2-mbr $ONFSROOT/usr/lib/grub/i386-pc/boot_hybrid.img \
	-eltorito-alt-boot -e EFI/efiboot.img -no-emul-boot \
	-append_partition 2 0xef $scratch/efiboot.img \
	--exclude $tmp/cow \
	-o $isoname -graft-points \
	--sort-weight 0 / --sort-weight 1 /boot \
	"$tmp" \
	/boot/grub/bios.img=$scratch/bios.img \
	/EFI/efiboot.img=$scratch/efiboot.img || die 12 "xorriso failed."

    echo -n "ISO image size and filename: "; du -h $isoname
}
# - - - - - - - - - - - - - - - - - - - - - - - - - -
mkiso_arm64() {

    echo "Writing FAI CD-ROM image to $isoname. This may need some time."
    cp $scratch/efiboot.img $tmp/boot/grub/efi.img
    xorriso -as mkisofs -r \
        -volid "$vname" -appid "$aname" \
        -J -joliet-long \
        -e boot/grub/efi.img -no-emul-boot \
        --exclude $tmp/cow \
        -append_partition 2 0xef $scratch/efiboot.img \
        -partition_cyl_align all \
        "$tmp" \
        -o $isoname \
        || die 12 "xorriso failed."

    echo -n "ISO image size and filename: "; du -h $isoname
}
# - - - - - - - - - - - - - - - - - - - - - - - - - -
addmirror() {

    [ $nomirror -eq 1 ] && return

    mkdir $NEW/media/mirror

    echo "Copying the mirror to CD image"
    cp -Pr $mirrordir/{dists,pool} $NEW/media/mirror 2>/dev/null || die $? "Not enough space left on image for the mirror"

    # create the sources.list for the CD
    cat > $NEW/etc/apt/sources.list <<EOF
# mirror location for fai CD, file generated by fai-cd
EOF

    dists=$(find $mirrordir -name "Packages*" | grep binary | sed 's/binary-.*//' | \
         sed "s#$mirrordir/*dists/##" | xargs -r -n 1 dirname | sort | uniq )
    [ -z "$dists" ] && die 19 "No suitable Packages file found in mirror."

    for i in $dists ; do
        comp=$(find $mirrordir/dists/$i -maxdepth 2 -type d -name "binary-*" | \
        sed -e "s#$mirrordir/*dists/$i/##" -e 's#/binary-.*##' | uniq | tr '\n' " ")
        echo "deb [trusted=yes] file:/media/mirror $i $comp" >> $NEW/etc/apt/sources.list
    done
}
# - - - - - - - - - - - - - - - - - - - - - - - - - -
add_config_space() {

    # copy config space into nfsroot-copy unless -d is given
    if [ $configset -eq 0 ]; then
        echo "Copying the config space to CD image"
        cp -Ppr $FAI_CONFIGDIR/. $NEW/var/lib/fai/config/ || die $? "Not enough space left on image for the config directory"
    fi
}
# - - - - - - - - - - - - - - - - - - - - - - - - - -
burniso() {

    wodim -v -eject $isoname
}
# - - - - - - - - - - - - - - - - - - - - - - - - - -
# main program

sqopt="-quiet -comp zstd  -Xcompression-level 5"

# Parse commandline options
while getopts "AekfhHg:Jbs:SBMn:m:V:c:C:d:" opt ; do
    case "$opt" in
	A)  autodiscover=1 ;;
        c)  csdir="$OPTARG" ;;
        C)  cdir="$OPTARG"
            echo "Using configuration files from $cdir";;
        e)  hidevartmp=1 ;;
        f)  forceremoval=1 ;;
        h)  usage ;;
        H)  hidedirs="" ;;
        g)  grub_config="$OPTARG" ;;
        J)  _OPTJ=1; sqopt="-quiet -comp xz";;
        M)  nomirror=1 ;;
        m)  mirrordir="$OPTARG" ;;
        n)  setnfsroot="$OPTARG" ;;
        b)  burn=1;;
        s)  echo "WARNING. Option -s is ignored" > /dev/stderr ;;
        S)  squash_only=1 ;;
        B)  bootonly=1 ;;
        V)  vname="$OPTARG" ;;
	d)  config_space="$OPTARG"; configset=1 ;;
        ?)  usage ;;
    esac
done
shift $((OPTIND - 1))
isoname=$1


if [ $autodiscover -eq 1 ]; then
    nomirror=1
    bootonly=1
    hidedirs="
    sound
    net/ceph
    fs/ceph
    fs/ocfs2
    fs/btrfs
    fs/xfs
    fs/bcachefs
    drivers/net/wireless
    drivers/block/drbd
    drivers/hwmon
    drivers/iio
    drivers/staging/lustre
    drivers/scsi
    drivers/ata
    drivers/target
    drivers/pcmcia
    drivers/mmc
    drivers/media
    drivers/infiniband
    drivers/isdn
    drivers/video
    drivers/mtd
    drivers/md
    drivers/gpu/drm
    "
fi

[ $nomirror -eq 1 -a -n "$mirrordir" ]  && die 6 "Do not use -M and -m at the same time."
[ "$#" -eq 0 ]        && die 2 "Please specify the output file for the ISO image."
if [ $bootonly -eq 0 -a $nomirror -eq 0 ]; then
    [ -z "$mirrordir" ]   && die 4 "Please specify the directory of your mirror using -m"
    [ -d "$mirrordir" ]   || die 5 "$mirrordir is not a directory"
    [ -z "$(ls $mirrordir)" ] && die 18 "No mirror found in $mirrordir. Empty directory."
fi
[ -f "$isoname" ]     && [ $forceremoval -eq 1 ] && rm $isoname
[ -f "$isoname" ]     && die 3 "Outputfile $isoname already exists. Please remove it or use -f."

if [ $bootonly -eq 0 ]; then
    [ $(id -u) != "0" ]   && die 9 "Run this program as root."
fi

check_programs

# use FAI_ETC_DIR from environment variable
if [ -n "$FAI_ETC_DIR" -a -z "$cdir" ]; then
    echo "Using environment variable \$FAI_ETC_DIR."
    cfdir=$FAI_ETC_DIR
fi
[ -n "$cdir" ] && cfdir=$cdir
: ${cfdir:=/etc/fai}
cfdir=$(readlink -f $cfdir) # canonicalize path
if [ ! -d "$cfdir" ]; then
    echo "$cfdir is not a directory" >&2
    exit 6
fi
. $cfdir/fai.conf
. $cfdir/nfsroot.conf
: ${FAI:=/var/lib/fai/config} # default value

if [ -n "$setnfsroot" ]; then
   NFSROOT=$setnfsroot # override by -n
fi
if [ -n "$csdir" ]; then
    FAI_CONFIGDIR=$csdir # override by -c
fi
ONFSROOT=$(readlink -f $NFSROOT) # save original nfsroot

[ -f "$NFSROOT/bin/dracut" ] || die 10 "Please create a NFSROOT that includes dracut."

[ $hidevartmp -eq 1 ] && hidedirs+="/var/tmp"

if [ -z "$grub_config" ]; then
    grub_config="$cfdir/grub.cfg" # set default if undefined
    [ $autodiscover -eq 1 ] && grub_config="$cfdir/grub.cfg.autodiscover" # set default if undefined
fi

# if grub_config contains / do not change it, else add prefix $cfdir
echo $grub_config | grep -q '/' || grub_config="$cfdir/$grub_config"
[ -f "$grub_config" ] || die 13 "Grub menu file $grub_config not found."

if [ $bootonly -eq 0 -o $configset -eq 1 ]; then
    [ -z "$FAI_CONFIGDIR" ]  && die 14 "Variable \$FAI_CONFIGDIR not set."
    [ -d $FAI_CONFIGDIR ] || die 15 "Can't find config space $FAI_CONFIGDIR."
    [ -d $FAI_CONFIGDIR/class ] || die 16 "Config space $FAI_CONFIGDIR seems to be empty."
fi

tmp=$(mktemp -t -d fai-cd.XXXXXX) || exit 13
scratch=$(mktemp -t -d scratch.XXXXXX) || exit 13
NEW=$tmp/nfsroot

trap "cleanup_liveos_mounts" EXIT ERR

create_new_nfsroot
if [ $bootonly -eq 0 ]; then
    addmirror
    add_config_space
    create_squashfs_image
fi

if [ $autodiscover -eq 1 ]; then
    create_autodiscover_iso
fi

[ $squash_only -eq 1 ] && exit 0

kernelversion=$(ls -tr $NEW/boot/vmlinu?-* | tail -1 | sed -e 's#.*/vmlinuz-##')
faiversion=$(dpkg --root=$NEW -l fai-client 2>/dev/null |awk '/fai-client/ {print $3}')
isoversion=$(printf "FAI %-6s build $(date '+%Y %b %d - %R')" "$faiversion")
aname="Fully Automatic Installation by Thomas Lange, $isoversion"

case $(uname -m) in
    x86_64)
        create_grub2_image_x86
        umount $NEW
        rmdir $NEW
        mkiso_x86
        ;;
    aarch64)
        create_grub2_image_arm64
        umount $NEW
        rmdir $NEW
        mkiso_arm64
        ;;
esac

[ "$burn" -eq 1 ] && burniso
exit 0
