#!/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-2022 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"
# arguments to be passed on to internal fai-cd call
arguments="-MB"

liveos="squashfs-root/LiveOS/"
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-2022 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
}
# - - - - - - - - - - - - - - - - - - - - - - - - - -
error_space() {

    local e=$1
    shift
    local msg=$1
    echo "Not enough space left on image for the $msg" >&2
    df -h $tmp/$liveos/mounted-ext3fs >&2
    echo "You can add more space to the image by using the option -s. See man fai-cd for details." >&2
    die $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_ext3fs_image() {

    # create the folder that will be used for the squashfs.img later
    # along with it, a temporary folder is created to mount the ext3fs.img
    mkdir -p $tmp/$liveos/mounted-ext3fs

    # hide some directories before copying the nfsroot
    customize_nfsroot
    calculate_required_size

    # creates an empty file
    dd if=/dev/zero of=$tmp/$liveos/ext3fs.img bs=1MB count=$count 2>/dev/null ||
        die 9 "dd failed. Maybe no enough free space in $tmp"

    mkfs.ext4 -m1 -q -O^has_journal -F -L FAI-NFSROOT $tmp/$liveos/ext3fs.img

    # the resulting ext3fs.img is mounted
    mount -o loop,noatime $tmp/$liveos/ext3fs.img $tmp/$liveos/mounted-ext3fs

    echo "Copying the nfsroot to CD image"
    # copy the contents of the nfsroot into it
    cp -Ppr $ONFSROOT/. $tmp/$liveos/mounted-ext3fs/ || error_space $? "nfsroot"
    rm -f $tmp/$liveos/mounted-ext3fs/etc/resolv.conf

    # find $tmp/$liveos/mounted-ext3fs # for debugging
    # 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/* $tmp/$liveos/mounted-ext3fs/var/lib/fai/config/ || error_space $? "config directory"
    fi
    # add the partial mirror to the CD
    addmirror

    unhide_dirs
    umount $tmp/$liveos/mounted-ext3fs
    rmdir $tmp/$liveos/mounted-ext3fs
}
# - - - - - - - - - - - - - - - - - - - - - - - - - -
create_autodiscover_iso() {

    local compress

    # boot-only CD does not need those dracut modules
    cat <<EOF > $ONFSROOT/etc/dracut.conf.d/02-omit.conf
omit_dracutmodules+=" fs-lib livenet rootfs-block "
EOF

    if [ -n "$_OPTJ" ]; then
	compress="--xz"
    else
	compress="--zstd"
    fi
    mv $ONFSROOT/boot/initrd.img* $ONFSROOT/tmp
    hide_dirs
    mountpoint -q $ONFSROOT/proc || mount --bind /proc $ONFSROOT/proc
    TMPDIR=/tmp chroot $ONFSROOT dracut --add 'fai-autodiscover' "$compress" /boot/initrd.img-$rel $rel 2>/dev/null
    umount $ONFSROOT/proc
    unhide_dirs
    rm -f $ONFSROOT/etc/dracut.conf.d/02-omit.conf

    fai-cd $arguments -g $grub_config $isoname
    mv $ONFSROOT/tmp/initrd.img* $ONFSROOT/boot
}
# - - - - - - - - - - - - - - - - - - - - - - - - - -
create_squashfs_image() {

    # 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

    mksquashfs $tmp/squashfs-root $output_path $sqopt

    rm $tmp/$liveos/ext3fs.img
    rmdir $tmp/$liveos $tmp/squashfs-root
}
# - - - - - - - - - - - - - - - - - - - - - - - - - -
create_grub2_image() {

    mkdir -p $tmp/boot

    if [ -d $NFSROOT/usr/lib/grub/x86_64-efi ]; then
	cp $grub_config $NFSROOT/tmp/grub.cfg
	# insert date into grub menu
	perl -pi -e "s/_VERSIONSTRING_/   $isoversion     /" $NFSROOT/tmp/grub.cfg
	if [ -n "$config_space" ] || [ $configset -eq 1 ]; then
	    perl -pi -e "s|FAI_CONFIG_SRC=(.*?)\S+|FAI_CONFIG_SRC=$config_space|" $NFSROOT/tmp/grub.cfg
	fi

	TMPDIR=tmp chroot $NFSROOT grub-mkstandalone \
	    --format=x86_64-efi \
	    --output=/tmp/bootx64.efi \
	    --locales="" --fonts="" \
	    "boot/grub/grub.cfg=/tmp/grub.cfg"
	mv $NFSROOT/tmp/bootx64.efi $scratch

	dd if=/dev/zero of=$scratch/efiboot.img bs=1M count=3 2>/dev/null
	mkfs.vfat $scratch/efiboot.img
	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 $NFSROOT/usr/lib/grub/i386-pc ]; then
	TMPDIR=/tmp chroot $NFSROOT grub-mkstandalone \
	    --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 $NFSROOT/usr/lib/grub/i386-pc/cdboot.img $NFSROOT/tmp/core.img > $scratch/bios.img
	rm $NFSROOT/tmp/core.img
    else
        die 11 "No grub-pc installation found in NFSROOT. Aborting."
    fi
    cp -p $NFSROOT/boot/vmlinuz-$kernelversion $tmp/boot/vmlinuz
    cp -p $NFSROOT/boot/initrd.img-$kernelversion $tmp/boot/initrd.img
    cp -p $NFSROOT/boot/config-$kernelversion $tmp/boot/
    touch $tmp/FAI-CD
}
# - - - - - - - - - - - - - - - - - - - - - - - - - -
calculate_required_size() {
    # calculates the amount of space needed for the ext3fsimage

    [ $configset -eq 1 ] && unset FAI_CONFIGDIR
    if [ $nomirror -eq 0 ]; then
	size=$(du -sc $ONFSROOT $FAI_CONFIGDIR $mirrordir/{dists,pool} | tail -n1 | awk '{print $1}')
    else
	size=$(du -sc $ONFSROOT $FAI_CONFIGDIR $mdir | tail -n1 | awk '{print $1}')
    fi

    # not dividing by 1024 to get the exact, this allows us to get some additional space
    # add some addition space
    count=$((size/850 + space))
}

provide_memtest_boot_option() {

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

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

    cat >> $tmp/boot/grub/grub.cfg <<EOF

menuentry "Memory test (memtest86+)" --unrestricted {
    linux16 /boot/memtest86+.bin
}
EOF
}
# - - - - - - - - - - - - - - - - - - - - - - - - - -
hide_dirs() {

# hide some dirs to save space and make the CD image smaller
    local d

    mkdir -p $tmp/empty
    for d in $hidedirs; do
        if [ -d $target/$d ]; then
            [ "$debug" ] && echo "hiding $d"
            mount --bind $tmp/empty $target/$d
        fi
    done
}

customize_nfsroot() {

    # hide some dirs to save space and make the CD image smaller
    local d

    mkdir -p $tmp/empty
    for d in $hidedirs; do
        if [ -d $ONFSROOT/$d ]; then
	    [ "$debug" ] && echo "hiding $d"
	    mount --bind $tmp/empty $ONFSROOT/$d
        fi
    done
}
# - - - - - - - - - - - - - - - - - - - - - - - - - -
unhide_dirs() {

    set +e
    for d in $hidedirs; do
	if [ $autodiscover -eq 1 ]; then
	    if [ -d $target/$d ]; then
		[ "$debug" ] && echo "disclosing $d"
		umount  $target/$d 2>/dev/null
            fi
	else
            if [ -d $ONFSROOT/$d ]; then
		[ "$debug" ] && echo "disclosing $d"
		umount $ONFSROOT/$d 2>/dev/null
            fi
	fi
    done
    set -e
}
# - - - - - - - - - - - - - - - - - - - - - - - - - -
cleanup_liveos_mounts() {

    mountpoint -q $tmp/$liveos/mounted-ext3fs && umount $tmp/$liveos/mounted-ext3fs
    unhide_dirs
    rm -rf $tmp $scratch
    if [ $autodiscover -eq 1 ]; then
        local initrdfiles=($ONFSROOT/tmp/initrd.img*)
        if [[ -e $initrdfiles ]]; then
	    mv "${initrdfiles[@]}" $ONFSROOT/boot 2>/dev/null
        fi
    fi
}
# - - - - - - - - - - - - - - - - - - - - - - - - - -
mkiso() {

    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 $NFSROOT/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 \
	-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

    unhide_dirs
}
# - - - - - - - - - - - - - - - - - - - - - - - - - -
addmirror() {

    [ $nomirror -eq 1 ] && return

    mkdir $tmp/$liveos/mounted-ext3fs/media/mirror

    echo "Copying the mirror to CD image"
    cp -Pr $mirrordir/{dists,pool} $tmp/$liveos/mounted-ext3fs/media/mirror 2>/dev/null || error_space $? "mirror"

    # create the sources.list for the CD
    cat > $tmp/$liveos/mounted-ext3fs/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" >> $tmp/squashfs-root/LiveOS/mounted-ext3fs/etc/apt/sources.list
    done

}
# - - - - - - - - - - - - - - - - - - - - - - - - - -
burniso() {

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

sqopt="-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" ; arguments+=" -C $cdir";;
        e)  hidevartmp=1 ;;
        f)  forceremoval=1 ;;
        h)  usage ;;
        H)  hidedirs="" ;;
        g)  grub_config="$OPTARG" ;;
        J)  _OPTJ=1; sqopt="-comp xz" ; arguments+=" -J";;
        M)  nomirror=1 ;;
        m)  mirrordir="$OPTARG" ;;
        n)  setnfsroot="$OPTARG" ;;
        b)  burn=1 ; arguments+=" -b";;
        s)  space="$OPTARG" ;;
        S)  squash_only=1 ;;
        B)  bootonly=1 ;;
        V)  vname="$OPTARG" ;;
	d)  config_space="$OPTARG"; configset=1 ; arguments+=" -d $config_space";;
        ?)  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
    drivers/net/wireless
    drivers/block/drbd
    drivers/hwmon
    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
    "
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
[ X$verbose = X1 ] && echo "Using configuration files from $cfdir"
. $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=$NFSROOT # save original nfsroot

if [ $autodiscover -eq 1 ]; then
    rel=$(ls $ONFSROOT/lib/modules/|head -1)
    target=$ONFSROOT/lib/modules/$rel/kernel
fi

[ -d "$NFSROOT/etc/fai" ] || die 10 "Please create NFSROOT by calling fai-make-nfsroot or fai-setup."

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

# if -g is a file name, add prefix

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

trap "cleanup_liveos_mounts" EXIT ERR

if [ $bootonly -eq 0 ]; then
create_ext3fs_image
create_squashfs_image
fi

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

[ $squash_only -eq 1 ] && exit 0


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

create_grub2_image

provide_memtest_boot_option

mkiso

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