#!/bin/sh

# Copyright (c) 2022 pangea.org AssociaciĆ³ Pangea - Coordinadora ComunicaciĆ³ per a la CooperaciĆ³
# SPDX-License-Identifier: AGPL-3.0-or-later
# Description: This program attaches workbench-script to a ISO

# debug
set -x
# exit on failure
set -e
# fail and exit when it cannot substitute a variable
set -u

# inspired from Ander in https://code.ungleich.ch/ungleich-public/cdist/issues/4
# this is a way to reuse a function used inside and outside of chroot
# this function is used both in shell and chroot
decide_if_update_str="$(cat <<END
decide_if_update() {
        if [ ! -d /var/lib/apt/lists ] \
                   || [ -n "\$( find /etc/apt -newer /var/lib/apt/lists )" ] \
                   || [ ! -f /var/cache/apt/pkgcache.bin ] \
                   || [ "\$( stat --format %Y /var/cache/apt/pkgcache.bin )" -lt "\$( date +%s -d '-1 day' )" ]
        then
                if [ -d /var/lib/apt/lists ]; then
                        \${SUDO} touch /var/lib/apt/lists
                fi
                apt_opts="-o Acquire::AllowReleaseInfoChange::Suite=true -o Acquire::AllowReleaseInfoChange::Version=true"
                # apt update could have problems such as key expirations, proceed anyway
                \${SUDO} apt-get "\${apt_opts}" update || true
        fi
}
END
)"

create_iso() {
        # Copy kernel and initramfs
        vmlinuz="$(ls -1v "${ISO_PATH}"/chroot/boot/vmlinuz-* | tail -n 1)"
        initrd="$(ls -1v "${ISO_PATH}"/chroot/boot/initrd.img-* | tail -n 1)"
        ${SUDO} cp ${vmlinuz} "${ISO_PATH}"/staging/live/vmlinuz
        ${SUDO} cp ${initrd} "${ISO_PATH}"/staging/live/initrd
        # Creating ISO
        iso_path=""${ISO_PATH}"/${iso_name}.iso"

        # 0x14 is FAT16 Hidden FAT16 <32, this is the only format detected in windows10 automatically when using a persistent volume of 10 MB
        ${SUDO} xorrisofs \
                -verbose \
                -iso-level 3 \
                -o "${iso_path}" \
                -full-iso9660-filenames \
                -volid "${iso_name}" \
                -isohybrid-mbr /usr/lib/ISOLINUX/isohdpfx.bin \
                -eltorito-boot \
                isolinux/isolinux.bin \
                -no-emul-boot \
                -boot-load-size 4 \
                -boot-info-table \
                --eltorito-catalog isolinux/isolinux.cat \
                -eltorito-alt-boot \
                -e /EFI/boot/efiboot.img \
                -no-emul-boot \
                -isohybrid-gpt-basdat \
                -append_partition 2 0xef "${ISO_PATH}"/staging/EFI/boot/efiboot.img \
                -append_partition 3 0x14 "${rw_img_path}" \
                "${ISO_PATH}/staging"

        printf "\n\n  Image generated in ${iso_path}\n\n"
}

isolinux_boot() {
        isolinuxcfg_str="$(cat <<END
UI vesamenu.c32

MENU TITLE Boot Menu
DEFAULT linux
TIMEOUT 10
MENU RESOLUTION 640 480
MENU COLOR border       30;44   #40ffffff #a0000000 std
MENU COLOR title        1;36;44 #9033ccff #a0000000 std
MENU COLOR sel          7;37;40 #e0ffffff #20ffffff all
MENU COLOR unsel        37;44   #50ffffff #a0000000 std
MENU COLOR help         37;40   #c0ffffff #a0000000 std
MENU COLOR timeout_msg  37;40   #80ffffff #00000000 std
MENU COLOR timeout      1;37;40 #c0ffffff #00000000 std
MENU COLOR msg07        37;40   #90ffffff #a0000000 std
MENU COLOR tabmsg       31;40   #30ffffff #00000000 std

LABEL linux
  MENU LABEL workbench
  MENU DEFAULT
  KERNEL /live/vmlinuz
  APPEND initrd=/live/initrd boot=live net.ifnames=0 biosdevname=0 persistence

LABEL linux
  MENU LABEL workbench (nomodeset)
  MENU DEFAULT
  KERNEL /live/vmlinuz
  APPEND initrd=/live/initrd boot=live net.ifnames=0 biosdevname=0 persistence nomodeset
END
)"
        #   TIMEOUT 60 means 6 seconds :)
        ${SUDO} tee "${ISO_PATH}/staging/isolinux/isolinux.cfg" <<EOF
${isolinuxcfg_str}
EOF
        ${SUDO} cp /usr/lib/ISOLINUX/isolinux.bin "${ISO_PATH}/staging/isolinux/"
        ${SUDO} cp /usr/lib/syslinux/modules/bios/* "${ISO_PATH}/staging/isolinux/"
}

grub_boot() {
        grubcfg_str="$(cat <<END
search --set=root --file /${iso_name}

set default="0"
set timeout=1

# If X has issues finding screens, experiment with/without nomodeset.

menuentry "workbench" {
    linux (\$root)/live/vmlinuz boot=live net.ifnames=0 biosdevname=0 persistence
    initrd (\$root)/live/initrd
}

menuentry "workbench (nomodeset)" {
    linux (\$root)/live/vmlinuz boot=live net.ifnames=0 biosdevname=0 persistence nomodeset
    initrd (\$root)/live/initrd
}
END
)"
        ${SUDO} tee "${ISO_PATH}/staging/boot/grub/grub.cfg" <<EOF
${grubcfg_str}
EOF

        ${SUDO} tee "${ISO_PATH}/tmp/grub-standalone.cfg" <<EOF
search --set=root --file /${iso_name}
set prefix=(\$root)/boot/grub/
configfile /boot/grub/grub.cfg
EOF
        ${SUDO} cp -r /usr/lib/grub/x86_64-efi/* "${ISO_PATH}/staging/boot/grub/x86_64-efi/"

        ${SUDO} grub-mkstandalone \
                --format=x86_64-efi \
                --output="${ISO_PATH}"/tmp/bootx64.efi \
                --locales="" \
                --fonts="" \
                "boot/grub/grub.cfg=${ISO_PATH}/tmp/grub-standalone.cfg"

  # prepare uefi secureboot files
  #   bootx64 is the filename is looking to boot, and we force it to be the shimx64 file for uefi secureboot
  #   shimx64 redirects to grubx64 -> src https://askubuntu.com/questions/874584/how-does-secure-boot-actually-work
  #   grubx64 looks for a file in /EFI/debian/grub.cfg -> src src https://unix.stackexchange.com/questions/648089/uefi-grub-not-finding-config-file
        ${SUDO} cp /usr/lib/shim/shimx64.efi.signed /tmp/bootx64.efi
        ${SUDO} cp /usr/lib/grub/x86_64-efi-signed/grubx64.efi.signed /tmp/grubx64.efi
        ${SUDO} cp "${ISO_PATH}/tmp/grub-standalone.cfg" "${ISO_PATH}/staging/EFI/debian/grub.cfg"

        (
                cd "${ISO_PATH}/staging/EFI/boot"
                ${SUDO} dd if=/dev/zero of=efiboot.img bs=1M count=20
                ${SUDO} mkfs.vfat efiboot.img
                ${SUDO} mmd -i efiboot.img efi efi/boot
                ${SUDO} mcopy -vi efiboot.img \
                        /tmp/bootx64.efi \
                        /tmp/grubx64.efi \
                        ::efi/boot/
        )
}

create_boot_system() {
        # both boots disable predicted names -> src https://michlstechblog.info/blog/linux-disable-assignment-of-new-names-for-network-interfaces/
        isolinux_boot
        grub_boot
}

compress_chroot_dir() {
        # Faster squashfs when debugging -> src https://forums.fedoraforum.org/showthread.php?284366-squashfs-wo-compression-speed-up
        if [ "${DEBUG:-}" ]; then
                DEBUG_SQUASHFS_ARGS='-noI -noD -noF -noX'
        fi

        # why squashfs -> https://unix.stackexchange.com/questions/163190/why-do-liveusbs-use-squashfs-and-similar-file-systems
        # noappend option needed to avoid this situation -> https://unix.stackexchange.com/questions/80447/merging-preexisting-source-folders-in-mksquashfs
        ${SUDO} mksquashfs \
                "${ISO_PATH}/chroot" \
                "${ISO_PATH}/staging/live/filesystem.squashfs" \
                ${DEBUG_SQUASHFS_ARGS:-} \
                -noappend -e boot
}

create_persistence_partition() {
        # persistent partition
        rw_img_name="workbench_vfat.img"
        rw_img_path="${ISO_PATH}/staging/${rw_img_name}"
        if [ ! -f "${rw_img_path}" ] || [ "${DEBUG:-}" ] || [ "${FORCE:-}" ]; then
                persistent_volume_size_MB=100
                ${SUDO} dd if=/dev/zero of="${rw_img_path}" bs=1M count=${persistent_volume_size_MB}
                ${SUDO} mkfs.vfat "${rw_img_path}"

                # generate structure on persistent partition
                tmp_rw_mount="/tmp/${rw_img_name}"
                ${SUDO} umount -f -l "${tmp_rw_mount}" >/dev/null 2>&1 || true
                mkdir -p "${tmp_rw_mount}"
                ${SUDO} mount "$(pwd)/${rw_img_path}" "${tmp_rw_mount}"
                ${SUDO} mkdir -p "${tmp_rw_mount}/settings"
                if [ -f "settings.ini" ]; then
                        ${SUDO} cp -v settings.ini "${tmp_rw_mount}/settings/settings.ini"
                else
                        echo "ERROR: settings.ini does not exist yet, cannot read config from there. You can take inspiration with file settings.ini.example"
                        exit 1
                fi
                ${SUDO} umount "${tmp_rw_mount}"

                uuid="$(blkid "${rw_img_path}" | awk '{ print $3; }')"
                # no fail on boot -> src https://askubuntu.com/questions/14365/mount-an-external-drive-at-boot-time-only-if-it-is-plugged-in/99628#99628
                # use tee instead of cat -> src https://stackoverflow.com/questions/2953081/how-can-i-write-a-heredoc-to-a-file-in-bash-script/17093489#17093489
                ${SUDO} tee "${ISO_PATH}/chroot/etc/fstab" <<END
# next three lines originally appeared on fstab, we preserve them
# UNCONFIGURED FSTAB FOR BASE SYSTEM
overlay / overlay rw 0 0
tmpfs /tmp tmpfs nosuid,nodev 0 0
${uuid} /mnt vfat defaults,nofail 0 0
END
  fi
        # src https://manpages.debian.org/testing/open-infrastructure-system-boot/persistence.conf.5.en.html
        echo "/ union" | ${SUDO} tee "${ISO_PATH}/chroot/persistence.conf"
}


chroot_netdns_conf_str="$(cat<<END
###################
# configure network
mkdir -p /etc/network/
cat > /etc/network/interfaces <<END2
auto lo
iface lo inet loopback

auto eth0
iface eth0 inet dhcp
END2

###################
# configure dns
cat > /etc/resolv.conf <<END2
nameserver 8.8.8.8
nameserver 1.1.1.1
END2

###################
# configure hosts
cat > /etc/hosts <<END2
127.0.0.1    localhost workbench
::1          localhost ip6-localhost ip6-loopback
ff02::1      ip6-allnodes
ff02::2      ip6-allrouters
END2
END
)"

prepare_app() {
        # prepare app during prepare_chroot_env
        # Install hardware_metadata module
        workbench_dir="${ISO_PATH}/chroot/opt/workbench"
        ${SUDO} mkdir -p "${workbench_dir}"
        ${SUDO} cp workbench-script.py "${workbench_dir}/"
        ${SUDO} cp requirements.txt "${workbench_dir}/"

        # startup script execution
        cat > "${ISO_PATH}/chroot/root/.profile" <<END
if [ -f /tmp/workbench_lock ]; then
        return
else
        touch /tmp/workbench_lock
fi

set -x
set -e

stty -echo # Do not show what we type in terminal so it does not meddle with our nice output
dmesg -n 1 # Do not report *useless* system messages to the terminal
# detect pxe env
if [ -d /run/live/medium ]; then
        mount --bind /run/live/medium /mnt
        # debian live nfs path is readonly, do a trick
        #   to make snapshots subdir readwrite
        mount ${server_ip}:/snapshots /run/live/medium/snapshots
else
fi
# clearly specify the right working directory, used in the python script as os.getcwd()
cd /mnt
pipenv run python /opt/workbench/workbench-script.py --config /mnt/settings.ini
stty echo

set +x
set +e
END
        #TODO add some useful commands
        cat > "${ISO_PATH}/chroot/root/.bash_history" <<END
poweroff
END

        # sequence of commands to install app in function run_chroot
        install_app_str="$(cat<<END
echo 'Install requirements'

# Install debian requirements
apt-get install -y --no-install-recommends \
  sudo \
  python3 python3-dev python3-pip pipenv \
  dmidecode smartmontools hwinfo pciutils lshw < /dev/null
  # Install python requirements using apt instead of pip
  #python3-dateutils python3-decouple python3-colorlog

# Install lshw B02.19 utility using backports (DEPRECATED in Debian 12)
#apt install -y -t ${VERSION_CODENAME}-backports lshw  < /dev/null

echo 'Install sanitize requirements'

# Install sanitize debian requirements
apt-get install -y --no-install-recommends \
  hdparm nvme-cli < /dev/null

pipenv run pip install -r /opt/workbench/requirements.txt
END
)"
}

run_chroot() {
        # non interactive chroot -> src https://stackoverflow.com/questions/51305706/shell-script-that-does-chroot-and-execute-commands-in-chroot
        # stop apt-get from greedily reading the stdin -> src https://askubuntu.com/questions/638686/apt-get-exits-bash-script-after-installing-packages/638754#638754
        ${SUDO} chroot ${ISO_PATH}/chroot <<CHROOT
set -x
set -e

echo workbench > /etc/hostname

# check what linux images are available on the system
# Figure out which Linux Kernel you want in the live environment.
#   apt-cache search linux-image

backports_path="/etc/apt/sources.list.d/backports.list"
if [ ! -f "\${backports_path}" ]; then
  backports_repo='deb http://deb.debian.org/debian ${VERSION_CODENAME}-backports main contrib'
  printf "\${backports_repo}" > "\${backports_path}"
fi

# this env var confuses sudo detection
unset SUDO_USER
${detect_user_str}
detect_user

# Installing packages
${decide_if_update_str}
decide_if_update

apt-get install -y --no-install-recommends \
  linux-image-amd64 \
  live-boot \
  systemd-sysv

# Install app
${install_app_str}

# Autologin root user
# src https://wiki.archlinux.org/title/getty#Automatic_login_to_virtual_console
mkdir -p /etc/systemd/system/getty@tty1.service.d/
cat > /etc/systemd/system/getty@tty1.service.d/override.conf <<END2
[Service]
ExecStart=
ExecStart=-/sbin/agetty --autologin root --noclear %I \$TERM
END2

systemctl enable getty@tty1.service

# other debian utilities
apt-get install -y --no-install-recommends \
  iproute2 iputils-ping ifupdown isc-dhcp-client \
  fdisk parted \
  curl openssh-client \
  less \
  jq \
  nano vim-tiny \
  < /dev/null

${chroot_netdns_conf_str}

# Set up root user
#   this is the root password
#   Method3: Use echo
#     src https://www.systutorials.com/changing-linux-users-password-in-one-command-line/
printf '${root_passwd}\n${root_passwd}' | passwd root

# general cleanup if production image
if [ -z "${DEBUG:-}" ]; then
  apt-get clean < /dev/null
fi

# cleanup bash history
history -c

CHROOT
}

prepare_chroot_env() {
        # version of debian the bootstrap is going to build
        #   if no VERSION_CODENAME is specified we assume that the bootstrap is going to
        #   be build with the same version of debian being executed because some files
        #   are copied from our root system
        if [ -z "${VERSION_CODENAME:-}" ]; then
                . /etc/os-release
                echo "TAKING OS-RELEASE FILE"
                if [ ! "${ID}" = "debian" ]; then
                        echo "ERROR: ubuntu detected, then you are enforced to specify debian variant"
                        echo "  use for example \`VERSION_CODENAME='bookworm'\` or similar"
                        exit 1
                fi
        fi

        chroot_path="${ISO_PATH}/chroot"
        if [ ! -d "${chroot_path}" ]; then
                ${SUDO} debootstrap --arch=amd64 --variant=minbase ${VERSION_CODENAME} ${ISO_PATH}/chroot http://deb.debian.org/debian/
                ${SUDO} chown -R "${USER}:" ${ISO_PATH}/chroot
        fi

        prepare_app
}

# thanks https://willhaley.com/blog/custom-debian-live-environment/
install_requirements() {
        # Install requirements
        eval "${decide_if_update_str}" && decide_if_update
        image_deps='debootstrap
                    squashfs-tools
                    xorriso
                    mtools
                    dosfstools'
        # secureboot:
        #   -> extra src https://wiki.debian.org/SecureBoot/
        #   -> extra src https://wiki.debian.org/SecureBoot/VirtualMachine
        #   -> extra src https://wiki.debian.org/GrubEFIReinstall
        bootloader_deps='isolinux
                         syslinux-efi
                         grub-pc-bin
                         grub-efi-amd64-bin
                         ovmf
                         grub-efi-amd64-signed'
        ${SUDO} apt-get install -y \
                ${image_deps} \
                ${bootloader_deps}
}

# thanks https://willhaley.com/blog/custom-debian-live-environment/
create_base_dirs() {
        mkdir -p "${ISO_PATH}"
        mkdir -p "${ISO_PATH}/staging/EFI/boot"
        mkdir -p "${ISO_PATH}/staging/boot/grub/x86_64-efi"
        mkdir -p "${ISO_PATH}/staging/isolinux"
        mkdir -p "${ISO_PATH}/staging/live"
        mkdir -p "${ISO_PATH}/tmp"
        # usb name
        ${SUDO} touch "${ISO_PATH}/staging/${iso_name}"

        # for uefi secure boot grub config file
        mkdir -p "${ISO_PATH}/staging/EFI/debian"
}

# this function is used both in shell and chroot
detect_user_str="$(cat <<END
detect_user() {
        userid="\$(id -u)"
        # detect non root user without sudo
        if [ ! "\${userid}" = 0 ] && id \${USER} | grep -qv sudo; then
                echo "ERROR: this script needs root or sudo permissions (current user is not part of sudo group)"
                exit 1
                # detect user with sudo or already on sudo src https://serverfault.com/questions/568627/can-a-program-tell-it-is-being-run-under-sudo/568628#568628
        elif [ ! "\${userid}" = 0 ] || [ -n "\${SUDO_USER}" ]; then
                SUDO='sudo'
                # jump to current dir where the script is so relative links work
                cd "\$(dirname "\${0}")"
                # working directory to build the iso
                ISO_PATH="iso"
                # detect pure root
        elif [ "\${userid}" = 0 ]; then
                SUDO=''
                ISO_PATH="/opt/workbench"
        fi
}
END
)"

main() {

        if [ "${DEBUG:-}" ]; then
                VERSION_ISO='debug'
        else
                VERSION_ISO='production'
        fi
        iso_name="workbench_${VERSION_ISO}"
        hostname='workbench'
        root_passwd='workbench'

        eval "${detect_user_str}" && detect_user

        create_base_dirs

        install_requirements

        prepare_chroot_env

        run_chroot

        create_persistence_partition

        compress_chroot_dir

        create_boot_system

        create_iso
}

main "${@}"