workbench-script/deploy-workbench.sh

526 lines
18 KiB
Bash
Executable file

#!/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}"
if [ -f "settings.ini" ]; then
${SUDO} cp -v settings.ini "${tmp_rw_mount}/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
workbench_dir="${ISO_PATH}/chroot/opt/workbench"
${SUDO} mkdir -p "${workbench_dir}"
${SUDO} cp workbench-script.py "${workbench_dir}/"
${SUDO} cp -arp locale "${workbench_dir}/"
# TODO uncomment when we have dependencies again
#${SUDO} cp requirements.txt "${workbench_dir}/"
# startup script execution
cat > "${ISO_PATH}/chroot/root/.profile" <<END
if [ -f /tmp/workbench_lock ]; then
return 0
else
touch /tmp/workbench_lock
fi
set -x
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
nfs_host="\$(df -hT | grep nfs | cut -f1 -d: | head -n1)"
if [ "\${nfs_host}" ]; then
mount --bind /run/live/medium /mnt
# debian live nfs path is readonly, do a trick
# to make snapshots subdir readwrite
mount \${nfs_host}:/snapshots /run/live/medium/snapshots
# reload mounts on systemd
systemctl daemon-reload
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
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 locales \
python3 python3-dev python3-pip pipenv \
dmidecode smartmontools hwinfo pciutils lshw nfs-common < /dev/null
# 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
# TODO uncomment when we have dependencies again
# 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}
# thanks src https://serverfault.com/questions/362903/how-do-you-set-a-locale-non-interactively-on-debian-ubuntu
export LANG=${LANG}
export LC_ALL=${LANG}
# this is a high level command that does locale-gen and update-locale altogether
dpkg-reconfigure --frontend=noninteractive locales
locale -a
# 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() {
LANG="${CUSTOM_LANG:-es_ES.UTF-8}"
# 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 "${@}"