diff --git a/README.md b/README.md index 416cd62..f952039 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,118 @@ -# ZFSBootMenu-Root-Install +# ZFSBootMenu Root Install -Set of scripts for installing Linux with ZFS as the root filesystem, and ZFSBootMenu as the boot loader \ No newline at end of file +## Intro + +Well howdy! + +Welcome to the ZFSBootMenu Root Install script! + +This here script is designed to install (currently) either Debian or Fedora +with OpenZFS as the root filesystem, and utilize ZFSBootMenu for actually +getting the system up and running (as opposed to i.e. GRUB2 or systemd-bootd) + +Some of the benefits of this setup include: +- Having a reliable copy-on-write (CoW) filesystem +- Being able to easily create and rollback to snapshots from either the OS or ZFSBootMenu +- Easy backups by sending/receiving snapshots to local or remote locations, including being able to do a "raw send", where the data being sent is the raw encrypted data without the decryption key, useful for backing up to third-party backup services +- Having multiple version of an OS installed on the system via different root datasets (i.e. stable and testing versions, old and new versions, etc.) + + +## Getting Started + +! THESE SCRIPTS ARE TO BE RUN ON THE LIVE MEDIUM OF THE OS YOU INTEND ON +INSTALLING AND AS THE ROOT USER ! + +To use this script, you run the `menu.sh` script, and use its, well, menu to +go step by step through installing the OS you wish to have on your PC + +The steps are as follows, and will be expanded on throughout this document: +- List +- Configure +- Partition +- Install +- Post Install +- Finalize +- WiFi Setup + + +## List + +This lists all currently connected storage devices and network interfaces of the system + +This is handy to know what devices and interfaces were picked up by the OS and the script, and to make sure that everything is "hunky-dory" + +Storage devices are listed seperately for both /dev/ (i.e. sda, sdb), and for /dev/disk/by-id/ (i.e. ata-WD_Blue) + +The latter method (/dev/disk/by-id/) is STRONGLY recommended when selecting the installation disk during configuration, and the former method (/dev/) should only be used in an environment where the latter method is unavailable (i.e. in a virtual machine) + + +## Configure + +When run, this will guide you through creating a configuration file (system.conf) that is used throughout the script for various tasks + +This includes: +- Disk type (SSD or HDD) +- Disk to install to +- Hostname of the machine +- Username of your new user account +- Whether to configure SWAP or not (SWAP will be equal to RAM x2) +- Whether to use encryption or not (ZFS native encryption is used) + + +## Partition + +The disk selected during configuration will be completely wiped and +partitioned, and formatted with the ZFS filesystem + +Debian or Fedora will be installed via their respective means for this setup (debootstrap for Debian, and an rsync for Fedora) + +If encryption was configured on, you will enter a password for your new zpool during this step + +The partition layout is as such: +- A 512MiB partition for EFI +- A RAMx2 partition for SWAP (if configured) +- A partition for ZFS (remaining space) + +Multiple ZFS datatets are setup from this script, including (but not limited to): +- A dataset for the root filesystem (such as to allow multiple OSes) +- A dataset for your home directory +- A datatset for your config directory +- Seperate datasets for /srv, /tmp, /usr, /var, etc. + +After this script has mostly concluded, you will be chrooted into the new installation to continue + +Once the chroot has been exited, everything will be unmounted and the zpool exported + +Afterwards, you will be notified to reboot the machine + + +## Install + +This is where you finish installing the system + +After being chrooted by the partition script, you'll run the menu script again from `/ZFSBootMenu Root Install/menu.sh` and begin this step + +This is where the needed packages are installed, the system gets configured, and ZFSBootMenu is downloaded and EFI entries made + +Exactly what needs doing depends on what OS is being installed (Debian or Fedora) + +After completion, the post install script should be automatically ran + + +## Post Install + +This is where the datasets for the individual user are created, as well as the user themselves + +When installing Debian, this part will also have you select the desktop environment you wish to install (if any), and setup an APT hook to a script that takes a snapshot before any packages are installed, removed, or upgraded, to allow for easy rollback in the event of a botched package operation + + +## Finalize + +This part ensures that timedatectl is set properly, as well as locks the root account, and sets up Flathub in user mode for installation of software packaged as Flatpaks + + +## WiFi Setup + +This part is mostly obsolete + +This script allows for the configuration of a WiFi network from the terminal, in the event that a wired connection is unavailable diff --git a/configure.sh b/configure.sh new file mode 100755 index 0000000..8cb51c8 --- /dev/null +++ b/configure.sh @@ -0,0 +1,268 @@ +#!/bin/bash +set -euo pipefail + + +BASEDIR="$(dirname "${0}" | sed "s|^\.|${PWD}|")" + + +cat << EOF +####################################### +## ## +## $(cat "${BASEDIR}/title") Script ## +## ## +## Jean ## +## ## +####################################### + + +################# +## ## +## Configure ## +## ## +################# + +EOF + + +printf \ + 'ZOL_FEDORA_VER="2-6"\n' | \ + tee \ + "${BASEDIR}/system.conf" \ + &> \ + /dev/null + +printf \ + '(NVMe) SSD or HDD?\n' +select OPTION in 'SSD' 'HDD' +do + case "${OPTION}" in + 'SSD'|'HDD') + break + ;; + *) + printf \ + 'Invalid option\n' + ;; + esac +done + +printf \ + "DISK_TYPE=\"${OPTION}\"\n" | \ + tee \ + --append \ + "${BASEDIR}/system.conf" \ + &> \ + /dev/null + +printf \ + '\033[2J\033[H' + +printf \ + '\nGet disk from:\n' +select OPTION in '/dev/disk/by-id/' '/dev/' +do + case "${OPTION}" in + '/dev/disk/by-id/'|'/dev/') + break + ;; + *) + printf \ + 'Invalid option\n' + ;; + esac +done + +printf \ + '\033[2J\033[H' + +if [[ "${OPTION}" == '/dev/disk/by-id/' ]]; then + DRIVES="$(ls -Ago /dev/disk/by-id/ | grep -v 'sr' | grep -v 'dm-' | grep -v 'nvme-eui.' | grep -v '\-part' | grep -v 'wwn-' | grep -v '_[1-9] -> ' | grep -v 'total' | sed 's|^.*\:[0-5][0-9] ||g; s| -> .*$||g' | tr -d '[:blank:]')" + + printf \ + '\nSelect the disk you want to use:\n' + + DRIVES=( ${DRIVES} ) + shopt -s extglob + MENU="@(${DRIVES[0]}" + + for ((i=1;i<${#DRIVES[@]};i++)); do + MENU+="|${DRIVES[$i]}" + done + + MENU+=")" + + select DRIVE in "${DRIVES[@]}" + do + case ${DRIVE} in + ${MENU}) + break + ;; + *) + printf \ + 'Invalid option\n' + ;; + esac + done + + printf \ + "DISK=\"/dev/disk/by-id/${DRIVE}\"\n" | \ + tee \ + --append \ + "${BASEDIR}/system.conf" \ + &> \ + /dev/null +elif [[ "${OPTION}" == '/dev/' ]]; then + DRIVES="$(lsblk -do name | grep -v 'loop' | grep -v 'sr' | grep -v 'zram' | grep -v 'NAME' | tr -d '[:blank:]')" + + printf \ + '\nSelect the disk you want to use:\n' + + DRIVES=( ${DRIVES} ) + shopt -s extglob + MENU="@(${DRIVES[0]}" + + for ((i=1;i<${#DRIVES[@]};i++)); do + MENU+="|${DRIVES[$i]}" + done + + MENU+=")" + + select DRIVE in "${DRIVES[@]}" + do + case ${DRIVE} in + ${MENU}) + break + ;; + *) + printf \ + 'Invalid option\n' + ;; + esac + done + + printf \ + "DISK=\"/dev/${DRIVE}\"\n" | \ + tee \ + --append \ + "${BASEDIR}/system.conf" \ + &> \ + /dev/null +fi + +printf \ + '\033[2J\033[H' + +HOSTNAME='-' + +while [[ "${HOSTNAME}" == '-' ]] || [[ -z "${HOSTNAME}" ]] || [[ "${HOSTNAME}" = *' '* ]] || [[ "${HOSTNAME}" = *_* ]]; do + printf \ + '\nEnter a hostname for this machine (no spaces or underscores):\n' + read \ + -r \ + HOSTNAME + + if [[ "${HOSTNAME}" = *' '* ]] || [[ "${HOSTNAME}" = *_* ]]; then + printf \ + 'ERROR:\tNo spaces or underscores in the hostname!\n' + fi +done + +printf \ + "HOSTNAME=\"${HOSTNAME}\"\n" | \ + tee \ + --append \ + "${BASEDIR}/system.conf" \ + &> \ + /dev/null + +printf \ + '\033[2J\033[H' + +USERNAME='-' + +while [[ "${USERNAME}" == '-' ]] || [[ -z "${USERNAME}" ]] || [[ "${USERNAME}" = *' '* ]] || [[ "${USERNAME}" = *[A-Z]* ]]; do + printf \ + '\nEnter a name for the new user account (lowercase, no spaces):\n' + read \ + -r \ + USERNAME + + if [[ "${USERNAME}" = *' '* ]] || [[ "${USERNAME}" = *[A-Z]* ]]; then + printf \ + 'ERROR:\tNo spaces or uppercase letters in the username!\n' + fi +done + +printf \ + "USERNAME=\"${USERNAME}\"\n" | \ + tee \ + --append \ + "${BASEDIR}/system.conf" \ + &> \ + /dev/null + +printf \ + '\033[2J\033[H' + +printf \ + '\nEnable SWAP?\n' +select OPTION in 'yes' 'no' +do + case "${OPTION}" in + 'yes'|'no') + break + ;; + *) + printf \ + 'Invalid option\n' + ;; + esac +done + +printf \ + "ENABLE_SWAP=\"${OPTION}\"\n" | \ + tee \ + --append \ + "${BASEDIR}/system.conf" \ + &> \ + /dev/null + +printf \ + '\033[2J\033[H' + +printf \ + '\nEnable encryption?\n' +select OPTION in 'yes' 'no' +do + case "${OPTION}" in + 'yes'|'no') + break + ;; + *) + printf \ + 'Invalid option\n' + ;; + esac +done + +printf \ + "ENCRYPTION=\"${OPTION}\"\n" | \ + tee \ + --append \ + "${BASEDIR}/system.conf" \ + &> \ + /dev/null + +printf \ + '\033[2J\033[H' + +cat << EOF + +Configuration stored in '${BASEDIR}/system.conf' + +Press any key to return to the main menu +EOF + +read \ + -srn \ + 1 diff --git a/finalize.sh b/finalize.sh new file mode 100755 index 0000000..ec34326 --- /dev/null +++ b/finalize.sh @@ -0,0 +1,37 @@ +#!/bin/bash +set -euo pipefail + + +BASEDIR="$(dirname "${0}" | sed "s|^\.|${PWD}|")" + + +printf '\033[?47l\012' + +cat << EOF +####################################### +## ## +## $(cat "${BASEDIR}/title") Script ## +## ## +## Jean ## +## ## +####################################### + + +################ +## ## +## Finalize ## +## ## +################ + +EOF + + +sudo timedatectl + +sudo usermod -p '*' root + +flatpak --user\ + remote-add\ + --if-not-exists\ + flathub\ + https://flathub.org/repo/flathub.flatpakrepo diff --git a/install.sh b/install.sh new file mode 100755 index 0000000..170286c --- /dev/null +++ b/install.sh @@ -0,0 +1,338 @@ +#!/bin/bash +set -euo pipefail + +BASEDIR="$(dirname "${0}" | sed "s|^\.|${PWD}|")" + +printf \ + '\033[?47l\012' + +cat << EOF +####################################### +## ## +## $(cat "${BASEDIR}/title") Script ## +## ## +## Jean ## +## ## +####################################### + + +############### +## ## +## Install ## +## ## +############### + +EOF + +source \ + /etc/os-release +source \ + "${BASEDIR}/system.conf" + +if [[ ! "${DISK}" == **/dev/disk/by-id/** ]]; then + if [[ "${DISK}" == **/dev/nvme** ]]; then + PART1='p1' + PART2='p2' + else + PART1='1' + PART2='2' + fi +else + PART1='-part1' + PART2='-part2' +fi + +if [[ ! -f "/etc/mtab" ]]; then + ln \ + -s \ + /proc/self/mounts \ + /etc/mtab +fi + +if [[ "${ID}" == 'debian' ]] || [[ "${ID}" == 'elementary' ]]; then + apt \ + update +fi + +if [[ "${ID}" == 'fedora' ]]; then + if [[ "${ENCRYPTION}" == 'yes' ]]; then + cat << EOF | tee /etc/dracut.conf.d/zol.conf &> /dev/null +nofsck="yes" +add_dracutmodules+=" zfs " +omit_dracutmodules+=" btrfs " +install_items+=" /etc/zfs/keys/${HOSTNAME,,}.key " +EOF + else + cat </etc/dracut.conf.d/zol.conf +nofsck="yes" +add_dracutmodules+=" zfs " +omit_dracutmodules+=" btrfs " +EOF + fi +fi + +if [[ "${ID}" == 'elementary' ]]; then + apt \ + install \ + --yes \ + --no-install-recommends \ + console-setup \ + cryptsetup \ + curl \ + dpkg-dev \ + efibootmgr \ + ethtool \ + flatpak \ + keyboard-configuration \ + linux-generic \ + locales \ + nano \ + network-manager \ + openssh-{client,server} \ + popularity-contest +elif [[ "${ID}" == 'debian' ]]; then + apt \ + install \ + --yes \ + console-setup \ + cryptsetup \ + curl \ + dpkg-dev \ + efibootmgr \ + ethtool \ + firmware-{ast,atheros,bnx{2,2x},brcm80211,ipw2x00,iwlwifi,libertas,linux,realtek,zd1211} \ + flatpak \ + keyboard-configuration \ + linux-{headers,image}-amd64 \ + locales \ + nano \ + network-manager \ + openssh-{client,server} \ + popularity-contest \ + printer-driver-all \ + tasksel +elif [[ "${ID}" == 'fedora' ]]; then + if [[ "${VERSION_ID}" -lt '41' ]]; then + dnf \ + config-manager \ + --disable \ + updates + else + dnf \ + config-manager \ + setopt \ + updates.enabled=0 + fi + + dnf \ + install \ + -y \ + https://dl.fedoraproject.org/pub/fedora/linux/releases/${VERSION_ID}/Everything/x86_64/os/Packages/k/kernel-devel-$(uname -r).rpm + + dnf \ + --releasever=${VERSION_ID} \ + install \ + -y \ + https://zfsonlinux.org/fedora/zfs-release-${ZOL_FEDORA_VER}$(rpm --eval "%{dist}").noarch.rpm + + dnf \ + install \ + -y \ + zfs \ + zfs-dracut + + if [[ "${VERSION_ID}" -lt '41' ]]; then + dnf \ + config-manager \ + --enable \ + updates + else + dnf \ + config-manager \ + setopt \ + updates.enabled=1 + fi +fi + +if [[ "${ID}" == 'debian' ]] || [[ "${ID}" == 'elementary' ]]; then + cat << EOF + +Regardless of the language(s) you choose, be sure to enable 'en_US.UTF-8'! +Press any key to continue... +EOF + + read -srn 1 + + dpkg-reconfigure \ + locales \ + tzdata \ + keyboard-configuration \ + console-setup + + apt install \ + --yes \ + dosfstools \ + systemd-timesyncd \ + zfs-initramfs +fi + +if [[ "${ID}" == 'debian' ]]; then + printf \ + 'REMAKE_INITRD=yes\n' | \ + tee /etc/dkms/zfs.conf &> /dev/null +fi + +if [[ ! "${*}" = *--no-part* ]]; then + mkdosfs \ + -F 32 \ + -s 1 \ + -n EFI \ + ${DISK}${PART1} +fi + +mkdir \ + -p \ + /boot/efi + +printf \ + "/dev/disk/by-uuid/$(blkid -s UUID -o value ${DISK}${PART1}) /boot/efi vfat defaults 0 0\n" | \ + tee --append /etc/fstab &> /dev/null + +sleep 5 + +mount \ + /boot/efi + +if [[ "${ID}" == 'debian' ]] || [[ "${ID}" == 'elementary' ]]; then + apt \ + purge \ + --yes \ + os-prober +fi + +printf \ + '\033[?47h\033[2J\033[H' + +cat << EOF + +Enter a password for the root account: +EOF + +passwd + +printf \ + '\033[?47l' + +if [[ "${ENABLE_SWAP}" == "yes" ]]; then + printf \ + "swap\t${DISK}${PART2}\t/dev/random\tswap,cipher=aes-xts-plain64,size=512\n" | \ + tee /etc/crypttab &> /dev/null + + printf \ + 'dev/mapper/swap\tnone\tswap\tsw\t0\t0\n' | \ + tee --append /etc/fstab &> /dev/null +fi + +if [[ "${ID}" == 'debian' ]]; then + cp \ + /etc/NetworkManager/NetworkManager.conf \ + /etc/NetworkManager/NetworkManager.conf.orig + + cat \ + /etc/NetworkManager/NetworkManager.conf.orig | \ + sed 's|managed=false|managed=true|' | \ + tee /etc/NetworkManager/NetworkManager.conf &> /dev/null +fi + +if [[ "${ID}" == 'debian' ]] || [[ "${ID}" == 'elementary' ]]; then + printf \ + "UMASK=0077\n" | \ + tee /etc/initramfs-tools/conf.d/umask.conf &> /dev/null + + update-initramfs \ + -c \ + -k all +elif [[ "${ID}" == 'fedora' ]]; then + dracut \ + --force \ + --regenerate-all +fi + +zfs \ + set \ + org.zfsbootmenu:commandline='quiet splash rhgb noresume' \ + ${HOSTNAME,,}/ROOT/${ID} + +if [[ ! "${*}" = *--no-part* ]]; then + if [[ ! -z "$(zfs list | grep 'keystore')" ]]; then + zfs \ + set \ + org.zfsbootmenu:keysource=${HOSTNAME,,}/keystore \ + ${HOSTNAME,,} + fi +fi + +if [[ ! "${*}" = *--no-part* ]]; then + mkdir \ + -p \ + /boot/efi/EFI/ZBM + + mkdir \ + -p \ + /boot/efi/EFI/BOOT + + curl \ + --progress-bar \ + --show-error \ + --output \ + /boot/efi/EFI/ZBM/VMLINUZ.EFI \ + --location \ + https://get.zfsbootmenu.org/efi + + cp \ + /boot/efi/EFI/ZBM/VMLINUZ.EFI \ + /boot/efi/EFI/ZBM/VMLINUZ-BACKUP.EFI + + cp \ + /boot/efi/EFI/ZBM/VMLINUZ.EFI \ + /boot/efi/EFI/BOOT/BOOTX64.EFI + + cp \ + /boot/efi/EFI/ZBM/VMLINUZ.EFI \ + /boot/efi/EFI/BOOT/shellx64.efi + + efibootmgr \ + -c \ + -d "${DISK}" \ + -p '1' \ + -L 'ZFSBootMenu (Backup)' \ + -l '\EFI\ZBM\VMLINUZ-BACKUP.EFI' + + efibootmgr \ + -c \ + -d "${DISK}" \ + -p '1' \ + -L 'ZFSBootMenu' \ + -l '\EFI\ZBM\VMLINUZ.EFI' +fi + +if [[ "${ID}" == 'debian' ]] || [[ "${ID}" == 'elementary' ]]; then + systemctl \ + enable \ + zfs.target + systemctl \ + enable \ + zfs-import-cache + systemctl \ + enable \ + zfs-mount + systemctl \ + enable \ + zfs-import.target +fi + +zfs \ + snapshot \ + ${HOSTNAME,,}/ROOT/${ID}@install + +"${BASEDIR}/post-inst.sh" diff --git a/list.sh b/list.sh new file mode 100755 index 0000000..25ad8b9 --- /dev/null +++ b/list.sh @@ -0,0 +1,49 @@ +#!/bin/bash +set -euo pipefail + + +BASEDIR="$(dirname "${0}" | sed "s|^\.|${PWD}|")" + + +DISKS_BY_ID="$(ls -Ago /dev/disk/by-id/ | grep -v 'sr' | grep -v 'dm-' | grep -v 'nvme-eui.' | grep -v '\-part' | grep -v 'wwn-' | grep -v '_[1-9] -> ' | grep -v 'total' | sed -e 's|^.*\:[0-5][0-9] ||g')" +DISKS="$(lsblk -do name | grep -v 'loop' | grep -v 'sr' | grep -v 'zram' | grep -v 'NAME')" +NETWORK_INTERFACES="$(ip -br addr show | sed -e 's| .*$||g' | grep -v '^lo' | grep -v 'tailscale' | grep -v '^wg')" + + +cat << EOF +####################################### +## ## +## $(cat "${BASEDIR}/title") Script ## +## ## +## Jean ## +## ## +####################################### + + +########################### +## ## +## Disks ## +## & ## +## Network Devices ## +## ## +########################### + +EOF + + +cat << EOF + +Available disks (/dev/disk/by-id/): +${DISKS_BY_ID} + +Available disks (/dev/): +${DISKS} + +Available network interfaces: +${NETWORK_INTERFACES} + + +Press any key to return to the main menu +EOF + +read -srn 1 diff --git a/menu.sh b/menu.sh new file mode 100755 index 0000000..69f98bd --- /dev/null +++ b/menu.sh @@ -0,0 +1,62 @@ +#!/bin/bash +set -euo pipefail + + +BASEDIR="$(dirname "${0}" | sed "s|^\.|${PWD}|")" +OPTION='start' + + +while [[ ! "${OPTION}" == 'Exit' ]]; do + printf '\033[?47h\033[2J\033[H' + + cat << EOF +####################################### +## ## +## $(cat "${BASEDIR}/title") Script ## +## ## +## Jean ## +## ## +####################################### + + +################# +## ## +## Main Menu ## +## ## +################# + +EOF + select OPTION in 'List' 'Configure' 'Partition' 'Install' 'WiFi Setup' 'Post Install' 'Finalize' 'Exit' + do + case "${OPTION}" in + 'List'|'Configure'|'Partition'|'Install'|'WiFi Setup'|'Post Install'|'Finalize'|'Exit') + break + ;; + *) + printf 'Invalid option\n' + ;; + esac + done + + printf '\033[2J\033[H' + + if [[ "${OPTION}" == 'List' ]]; then + "${BASEDIR}/list.sh" + elif [[ "${OPTION}" == 'Configure' ]]; then + "${BASEDIR}/configure.sh" + elif [[ "${OPTION}" == 'Partition' ]]; then + "${BASEDIR}/partition.sh" + elif [[ "${OPTION}" == 'Install' ]]; then + "${BASEDIR}/install.sh" + elif [[ "${OPTION}" == 'WiFi Setup' ]]; then + "${BASEDIR}/wifi.sh" + elif [[ "${OPTION}" == 'Post Install' ]]; then + "${BASEDIR}/post-inst.sh" + elif [[ "${OPTION}" == 'Finalize' ]]; then + "${BASEDIR}/finalize.sh" + fi + + printf '\033[?47h\033[2J\033[H' +done + +printf '\033[?47l' diff --git a/partition.sh b/partition.sh new file mode 100755 index 0000000..730d014 --- /dev/null +++ b/partition.sh @@ -0,0 +1,597 @@ +#!/bin/bash +set -euo pipefail + +BASEDIR="$(dirname "${0}" | sed "s|^\.|${PWD}|")" + +# PRIOR_CODENAMES=(buzz rex bo hamm slink potato woody sarge etch lenny squeeze wheezy jessie stretch buster bullseye bookworm) + +printf \ + '\033[?47l\012' + +cat << EOF +####################################### +## ## +## $(cat "${BASEDIR}/title") Script ## +## ## +## Jean ## +## ## +####################################### + + +################# +## ## +## Partition ## +## ## +################# + +EOF + +source \ + /etc/os-release +source \ + "${BASEDIR}/system.conf" + +if [[ ! "${DISK}" == **/dev/disk/by-id/** ]]; then + if [[ "${DISK}" == **/dev/nvme** ]]; then + PART3='p3' + else + PART3='3' + fi +else + PART3='-part3' +fi + +if [[ "${ID}" == 'debian' ]]; then + if [[ ! "${HOST}" == "debian-live" ]]; then + cat << EOF | tee /mnt/etc/apt/sources.list.d/contrib.sources &> /dev/null +Enabled: yes +Types: deb +URIs: http://deb.debian.org/debian/ +Suites: ${VERSION_CODENAME} +Components: contrib +Signed-By: /usr/share/keyrings/debian-archive-keyring.gpg +EOF + fi +fi + +if [[ -f '/usr/bin/gsettings' ]]; then + gsettings \ + set \ + org.gnome.desktop.media-handling \ + automount \ + false +fi + +if [[ "${ID}" == 'debian' ]]; then + apt \ + update && \ + apt \ + install \ + --yes \ + debootstrap \ + gdisk \ + zfsutils-linux \ + systemd-timesyncd +elif [[ "${ID}" == 'fedora' ]]; then + if [[ "${VERSION_ID}" -lt '41' ]]; then + dnf config-manager \ + --disable \ + updates + else + dnf config-manager \ + setopt \ + updates.enabled=0 + fi + + dnf install \ + -y \ + https://zfsonlinux.org/fedora/zfs-release-${ZOL_FEDORA_VER}$(rpm --eval "%{dist}").noarch.rpm + + dnf install \ + -y \ + https://dl.fedoraproject.org/pub/fedora/linux/releases/${VERSION_ID}/Everything/x86_64/os/Packages/k/kernel-devel-$(uname -r).rpm + + dnf install \ + -y \ + zfs \ + gdisk + + modprobe \ + zfs +fi + +timedatectl + +if [[ ! "${HOST}" == "debian-live" ]]; then + zgenhostid \ + -f \ + 0x00bab10c +fi + +swapoff \ + --all + +if [[ ! "${*}" = *--no-part* ]]; then + wipefs \ + -a \ + ${DISK} + + if [[ ! "${DISK_TYPE}" == 'HDD' ]]; then + blkdiscard \ + -f \ + ${DISK} + fi + + sgdisk \ + --zap-all \ + ${DISK} + + sgdisk \ + -n1:0:+512M \ + -t1:EF00 \ + -c1:EFI \ + ${DISK} + + if [[ "${ENABLE_SWAP}" == "yes" ]]; then + SWAP_SIZE="$(((($(vmstat -sS M | grep 'total memory' | sed 's/ M total memory//') / 1024) + 1) * 2))" + + sgdisk \ + -n2:0:+${SWAP_SIZE}G \ + -t2:BF02 \ + -c2:swap \ + ${DISK} + fi + + sgdisk \ + -n3:0:0 \ + -t3:BF00 \ + -c3:${ID} \ + ${DISK} + + sleep 5 + + if [[ "${ENCRYPTION}" == 'yes' ]]; then + ZPOOL_PASSWORD='A' + ZPOOL_PASSWORD_VERIFY='B' + + printf \ + '\033[?47h\033[2J\033[H' + + while [[ ! "${ZPOOL_PASSWORD}" == "${ZPOOL_PASSWORD_VERIFY}" ]] || [[ -z "${ZPOOL_PASSWORD}" ]] || [[ "${#ZPOOL_PASSWORD}" -lt '8' ]]; do + printf \ + "\nEnter a password to encrypt your root pool (minimum 8 characters):\n" + read \ + -r \ + -s \ + ZPOOL_PASSWORD + + printf \ + "\nVerify the password to encrypt your root pool:\n" + read \ + -r \ + -s \ + ZPOOL_PASSWORD_VERIFY + + if [[ ! "${ZPOOL_PASSWORD}" == "${ZPOOL_PASSWORD_VERIFY}" ]]; then + printf \ + "ERROR:\tPasswords do not match!\n" + elif [[ -z "${ZPOOL_PASSWORD}" ]]; then + printf \ + "ERROR:\tPassword is empty!\n" + elif [[ "${#ZPOOL_PASSWORD}" -lt '8' ]]; then + printf \ + "ERROR:\tPassword is too short!\n" + fi + done + + printf \ + '\033[?47l' + + mkdir \ + -p \ + /etc/zfs/keys/ + + printf \ + "${ZPOOL_PASSWORD}\n" | tee /etc/zfs/keys/${HOSTNAME,,}.key &> /dev/null + + chmod \ + 000 \ + /etc/zfs/keys/${HOSTNAME,,}.key + + zpool create \ + -o ashift=12 \ + -o autotrim=on \ + -o compatibility=openzfs-2.1-linux \ + -O encryption=on \ + -O keylocation=file:///etc/zfs/keys/${HOSTNAME,,}.key \ + -O keyformat=passphrase \ + -O acltype=posixacl \ + -O xattr=sa \ + -O dnodesize=auto \ + -O compression=zstd-3 \ + -O normalization=formD \ + -O relatime=on \ + -O canmount=off \ + -O mountpoint=/ \ + -R /mnt \ + ${HOSTNAME,,} \ + ${DISK}${PART3} + else + zpool create \ + -o ashift=12 \ + -o autotrim=on \ + -o compatibility=openzfs-2.1-linux \ + -O encryption=off \ + -O acltype=posixacl \ + -O xattr=sa \ + -O dnodesize=auto \ + -O compression=zstd-3 \ + -O normalization=formD \ + -O relatime=on \ + -O canmount=off \ + -O mountpoint=/ \ + -R /mnt \ + ${HOSTNAME,,} \ + ${DISK}${PART3} + fi + + zfs create \ + -o canmount=off \ + -o mountpoint=none \ + ${HOSTNAME,,}/ROOT +else + zpool import \ + -N \ + -R \ + /mnt \ + ${HOSTNAME,,} + + zfs load-key \ + -r \ + -L prompt \ + ${HOSTNAME,,} +fi + +zfs create \ + -o canmount=noauto \ + -o mountpoint=/ \ + ${HOSTNAME,,}/ROOT/${ID} + +zfs mount \ + ${HOSTNAME,,}/ROOT/${ID} + +if [[ ! "${*}" = *--no-part* ]]; then + zfs create \ + ${HOSTNAME,,}/home + + zfs create \ + -o mountpoint=/root \ + ${HOSTNAME,,}/home/root + + chmod \ + 700 \ + /mnt/root + + zfs create \ + -o canmount=off \ + -o mountpoint=/var \ + ${HOSTNAME,,}/var + + zfs create \ + -o canmount=off \ + ${HOSTNAME,,}/var/lib + + zfs create \ + ${HOSTNAME,,}/var/log + + zfs create \ + ${HOSTNAME,,}/var/spool + + zfs create \ + -o com.sun:auto-snapshot=false \ + ${HOSTNAME,,}/var/cache + + zfs create \ + -o com.sun:auto-snapshot=false \ + ${HOSTNAME,,}/var/lib/nfs + + zfs create \ + -o com.sun:auto-snapshot=false \ + ${HOSTNAME,,}/var/tmp + + chmod \ + 1777 \ + /mnt/var/tmp + + zfs create \ + -o mountpoint=/srv \ + ${HOSTNAME,,}/srv + + zfs create \ + -o canmount=off \ + -o mountpoint=/usr \ + ${HOSTNAME,,}/usr + + zfs create \ + ${HOSTNAME,,}/usr/local + + zfs create \ + ${HOSTNAME,,}/var/games + + zfs create \ + ${HOSTNAME,,}/var/lib/AccountsService + + zfs create \ + ${HOSTNAME,,}/var/lib/NetworkManager + + zfs create \ + ${HOSTNAME,,}/var/www + + zfs create \ + -o com.sun:auto-snapshot=false \ + -o mountpoint=/tmp \ + ${HOSTNAME,,}/tmp + + if [[ "${ENCRYPTION}" == 'yes' ]]; then + zfs create \ + -o com.sun:auto-snapshot=false \ + -o mountpoint=/etc/zfs/keys \ + ${HOSTNAME,,}/keystore + fi + + zpool set \ + bootfs=${HOSTNAME,,}/ROOT/${ID} \ + ${HOSTNAME,,} +else + zfs mount \ + ${HOSTNAME,,}/home + + zfs mount \ + ${HOSTNAME,,}/home/root + + zfs mount \ + ${HOSTNAME,,}/var/log + + zfs mount \ + ${HOSTNAME,,}/var/spool + + zfs mount \ + ${HOSTNAME,,}/var/cache + + zfs mount \ + ${HOSTNAME,,}/var/lib/nfs + + zfs mount \ + ${HOSTNAME,,}/var/tmp + + zfs mount \ + ${HOSTNAME,,}/srv + + zfs mount \ + ${HOSTNAME,,}/usr/local + + zfs mount \ + ${HOSTNAME,,}/var/games + + zfs mount \ + ${HOSTNAME,,}/var/lib/AccountsService + + zfs mount \ + ${HOSTNAME,,}/var/lib/NetworkManager + + zfs mount \ + ${HOSTNAME,,}/var/www + + zfs mount \ + ${HOSTNAME,,}/keystore +fi + +if [[ "${ID}" == 'fedora' ]]; then + mkdir -p /run/install + + if [[ "${VERSION_ID}" -lt '41' ]]; then + mount /dev/mapper/live-base /run/install + else + mount /dev/live-base /run/install + fi + + rsync -pogAXtlHrDx \ + --stats \ + --exclude=/boot/efi/* \ + --exclude=/etc/machine-id \ + --info=progress2 \ + /run/install/ /mnt +fi + +if [[ ! "${*}" = *--no-part* ]]; then + zfs create \ + ${HOSTNAME,,}/var/mail + + chmod \ + 1777 \ + /mnt/tmp +else + zfs mount \ + ${HOSTNAME,,}/var/mail +fi + +mkdir \ + -p \ + /mnt/run + +mount \ + -t \ + tmpfs \ + tmpfs \ + /mnt/run + +mkdir \ + -p \ + /mnt/run/lock + +if [[ "${ID}" == 'elementary' ]]; then + debootstrap \ + "${UBUNTU_VERSION_CODENAME}" \ + /mnt +elif [[ "${ID}" == 'debian' ]]; then + debootstrap \ + "${VERSION_CODENAME}" \ + /mnt +fi + +printf \ + "${HOSTNAME}\n" | tee /mnt/etc/hostname &> /dev/null + +printf \ + "127.0.1.1\t${HOSTNAME}\n" | tee --append /mnt/etc/hosts &> /dev/null + +if [[ "${ID}" == 'debian' ]]; then + NETWORK_INTERFACE=($(ip -br addr show | sed 's| .*$||g' | grep -v '^lo' | grep -v 'tailscale' | grep -v '^wg')) + shopt -s extglob + + for ((i = 0; i < ${#NETWORK_INTERFACE[@]}; i++)); do + cat << EOF | tee /mnt/etc/network/interfaces.d/${NETWORK_INTERFACE[$i]} &> /dev/null +allow-hotplug ${NETWORK_INTERFACE[$i]} +iface ${NETWORK_INTERFACE[$i]} inet dhcp +EOF + done +fi + +if [[ "${ID}" == 'elementary' ]]; then + cat </mnt/etc/apt/sources.list +deb http://archive.ubuntu.com/ubuntu/ ${UBUNTU_VERSION_CODENAME} main restricted universe multiverse +deb-src http://archive.ubuntu.com/ubuntu/ ${UBUNTU_VERSION_CODENAME} main restricted universe multiverse + +deb http://security.ubuntu.com/ubuntu/ ${UBUNTU_VERSION_CODENAME}-security main restricted universe multiverse +deb-src http://security.ubuntu.com/ubuntu/ ${UBUNTU_VERSION_CODENAME}-security main restricted universe multiverse + +deb http://archive.ubuntu.com/ubuntu/ ${UBUNTU_VERSION_CODENAME}-updates main restricted universe multiverse +deb-src http://archive.ubuntu.com/ubuntu/ ${UBUNTU_VERSION_CODENAME}-updates main restricted universe multiverse +EOF + + cat </mnt/etc/apt/sources.list.d/elementary.list +deb https://ppa.launchpadcontent.net/elementary-os/stable/ubuntu ${UBUNTU_VERSION_CODENAME} main +deb-src https://ppa.launchpadcontent.net/elementary-os/stable/ubuntu ${UBUNTU_VERSION_CODENAME} main +EOF + + cat </mnt/etc/apt/sources.list.d/patches.list +deb https://ppa.launchpadcontent.net/elementary-os/os-patches/ubuntu ${UBUNTU_VERSION_CODENAME} main +deb-src https://ppa.launchpadcontent.net/elementary-os/os-patches/ubuntu ${UBUNTU_VERSION_CODENAME} main +EOF +elif [[ "${ID}" == 'debian' ]]; then + cat << EOF | tee /mnt/etc/apt/sources.list.d/${VERSION_CODENAME}.sources &> /dev/null +# ${VERSION_CODENAME^} +Enabled: yes +Types: deb deb-src +URIs: http://deb.debian.org/debian/ +Suites: ${VERSION_CODENAME} +Components: main non-free-firmware contrib +Signed-By: /usr/share/keyrings/debian-archive-keyring.gpg + +# ${VERSION_CODENAME^} Security +Enabled: yes +Types: deb deb-src +URIs: http://deb.debian.org/debian-security/ +Suites: ${VERSION_CODENAME}-security +Components: main non-free-firmware contrib +Signed-By: /usr/share/keyrings/debian-archive-keyring.gpg + +# ${VERSION_CODENAME^} Updates +Enabled: yes +Types: deb deb-src +URIs: http://deb.debian.org/debian/ +Suites: ${VERSION_CODENAME}-updates +Components: main non-free-firmware contrib +Signed-By: /usr/share/keyrings/debian-archive-keyring.gpg +EOF +fi + +mount \ + --rbind \ + /dev \ + /mnt/dev + +mount \ + --rbind \ + /proc \ + /mnt/proc + +mount \ + --rbind \ + /sys \ + /mnt/sys + +cp \ + /etc/hostid \ + /mnt/etc/ + +if [[ "${ID}" == 'fedora' ]]; then + mv /mnt/etc/resolv.conf \ + /mnt/etc/resolv.conf.orig + + cp -L \ + /etc/resolv.conf \ + /mnt/etc +fi + +rsync -pogAXtlHrDx \ + "${BASEDIR}" \ + /mnt + +if [[ "${ID}" == 'elementary' ]]; then + rsync -pogAXtlHrDx \ + /etc/skel \ + /mnt/etc +fi + +if [[ ! "${*}" = *--no-part* ]]; then + if [[ -f "/etc/zfs/keys/${HOSTNAME,,}.key" ]]; then + cp \ + /etc/zfs/keys/${HOSTNAME,,}.key \ + /mnt/etc/zfs/keys/ + fi +fi + +if [[ "${ID}" == 'elementary' ]]; then + cp \ + /etc/os-release \ + /mnt/etc + + cp \ + /etc/apt/trusted.gpg.d/{elementary,patches}.key.asc \ + /mnt/etc/apt/trusted.gpg.d/ +fi + +printf \ + "\nNow chrooting into /mnt...\n\n" + +chroot \ + /mnt \ + bash \ + --login + +mount | grep -v zfs | tac | awk '/\/mnt/ {print $3}' | xargs -I {} umount -Rlf {} + +if [[ "${ID}" == 'fedora' ]]; then + umount \ + -nR \ + /mnt +fi + +zpool \ + export \ + -a + +printf \ + '\033[?47h\033[2J\033[H' + +cat < ## +## ## +####################################### + + +#################### +## ## +## Post ## +## Installation ## +## ## +#################### + +EOF + + +source \ + /etc/os-release + +source \ + "${BASEDIR}/system.conf" + + +if [[ ! "${*}" = *--no-part* ]]; then + zfs create\ + ${HOSTNAME,,}/home/${USERNAME} + + zfs create\ + ${HOSTNAME,,}/home/${USERNAME}/.config + + zfs create\ + -o canmount=off\ + ${HOSTNAME,,}/home/${USERNAME}/.var + + zfs create\ + ${HOSTNAME,,}/home/${USERNAME}/.var/app + + zfs create\ + -o canmount=off\ + ${HOSTNAME,,}/home/${USERNAME}/.local + + zfs create\ + -o canmount=off\ + ${HOSTNAME,,}/home/${USERNAME}/.local/share + + zfs create\ + ${HOSTNAME,,}/home/${USERNAME}/.local/share/flatpak + + printf \ + 'Adding user account...\n' + + adduser ${USERNAME} +else + adduser --no-create-home ${USERNAME} +fi + +if [[ "${ID}" == 'fedora' ]]; then + printf \ + '\033[?47h\033[2J\033[H' + + cat << EOF + +Enter a password for the new user account: +EOF + passwd \ + ${USERNAME} + + printf \ + '\033[?47l' + + usermod \ + -a \ + -G \ + audio,cdrom,dip,floppy,wheel,video,dialout \ + ${USERNAME} +else + usermod \ + -a \ + -G \ + audio,cdrom,dip,floppy,netdev,plugdev,sudo,video,dialout,lpadmin \ + ${USERNAME} +fi + +if [[ "${ID}" == 'elementary' ]]; then + cat << EOF > /etc/apt/sources.list.d/backports.list +deb http://archive.ubuntu.com/ubuntu/ ${UBUNTU_VERSION_CODENAME}-backports main restricted universe multiverse +deb-src http://archive.ubuntu.com/ubuntu/ ${UBUNTU_VERSION_CODENAME}-backports main restricted universe multiverse +EOF + + cat << EOF > /etc/apt/preferences.d/backports.pref +Package: * +Pin: release n=${UBUNTU_VERSION_CODENAME}* +Pin-Priority: 990 + +Package: linux* /wayland/ +Pin: release n=${UBUNTU_VERSION_CODENAME}-backports +Pin-Priority: -1 +EOF +elif [[ "${ID}" == 'debian' ]]; then + cat << EOF | tee /etc/apt/sources.list.d/backports.sources &> /dev/null +Enabled: yes +Types: deb deb-src +URIs: http://deb.debian.org/debian/ +Suites: ${VERSION_CODENAME}-backports +Components: main non-free-firmware contrib +Signed-By: /usr/share/keyrings/debian-archive-keyring.gpg +EOF + + cat << EOF | tee /etc/apt/preferences.d/backports.pref &> /dev/null +Package: * +Pin: release n=${VERSION_CODENAME}* +Pin-Priority: 990 +EOF + + if [[ "${VERSION_CODENAME}" == 'bookworm' ]]; then + cat << EOF | tee --append /etc/apt/preferences.d/backports.pref &> /dev/null + +Package: /wayland/ +Pin: release n=${VERSION_CODENAME}-backports +Pin-Priority: -1 +EOF + fi +fi + +if [[ "${ID}" == 'debian' ]] || [[ "${ID}" == 'elementary' ]]; then + apt \ + dist-upgrade \ + --yes +fi + +if [[ "${ID}" == 'elementary' ]]; then + apt \ + install \ + --yes \ + --autoremove \ + elementary-desktop +elif [[ "${ID}" = 'debian' ]]; then + if [[ ! -f /usr/bin/tasksel ]]; then + apt \ + install \ + --yes \ + tasksel + fi + + tasksel \ + --new-install +fi + +if [[ "${ID}" == 'debian' ]] || [[ "${ID}" == 'elementary' ]]; then + for file in /etc/logrotate.d/* ; do + if grep -Eq "(^|[^#y])compress" "$file" ; then + sed -i -r "s/(^|[^#y])(compress)/\1#\2/" "$file" + fi + done + + cat << EOF | tee /usr/bin/zfs-system-snapshot-apt &> /dev/null +#!/bin/bash +set -euo pipefail + + +TIMESTAMP="\$(date +%Y_%m_%d-%H_%M_%S)" + + +if [[ -z "\$(zfs list -t snapshot | grep 'apt-\${TIMESTAMP}')" ]]; then + zfs snapshot \$(zfs mount | grep 'ROOT' | sed 's| .*||')@apt-\${TIMESTAMP} && printf "\n- Snapshot taken\n\n" +fi +EOF + + chmod \ + +x \ + /usr/bin/zfs-system-snapshot-apt + + printf \ + 'DPkg::Pre-Install-Pkgs {"/usr/bin/zfs-system-snapshot-apt";};\n' | \ + tee /etc/apt/apt.conf.d/90-zfs_system-snapshot &> /dev/null + + printf \ + 'DPkg::Post-Invoke {"apt moo";};\n' | \ + tee /etc/apt/apt.conf.d/99-apt_moo &> /dev/null +fi + +if [[ ! "${*}" = *--no-part* ]]; then + cat << EOF | tee /usr/bin/home-fix.sh &> /dev/null +#!/bin/bash +set -euo pipefail + +chown -R ${USERNAME}:${USERNAME} /home/${USERNAME} + +sudo -u ${USERNAME} cp -a /etc/skel/. /home/${USERNAME} + +if [[ ! -z "$(find -P /home/${USERNAME}/ | grep '\.face')" ]]; then + find -P /home/${USERNAME}/ | grep '\.face' | xargs -d '\n' -I {} rm {} +fi + +if [[ ! -z "\$(find -P /var/spool/cron | grep 'root')" ]]; then + rm \$(find -P /var/spool/cron | grep 'root') +fi + +printf "\$(date +%Y-%m-%d\ %H:%M:%S) I did the thing\n" | tee /var/log/home-fix.log &> /dev/null + +if [[ -f '/usr/bin/home-fix.sh' ]]; then + rm /usr/bin/home-fix.sh +fi + +zfs snapshot ${HOSTNAME,,}/ROOT/${ID}@home-fix + +zfs snapshot -r ${HOSTNAME,,}/home/${USERNAME}@home-fix +EOF + + chmod \ + +x \ + /usr/bin/home-fix.sh + + if [[ "${ID}" == 'fedora' ]]; then + printf \ + '@reboot /usr/bin/home-fix.sh\n' | \ + tee /var/spool/cron/root &> /dev/null + elif [[ "${ID}" == 'debian' ]]; then + printf \ + '@reboot /usr/bin/home-fix.sh\n' | \ + tee /var/spool/cron/crontabs/root &> /dev/null + + chown \ + :crontab \ + /var/spool/cron/crontabs/root + + chmod \ + 0600 \ + /var/spool/cron/crontabs/root + fi +fi + +zfs \ + snapshot \ + ${HOSTNAME,,}/ROOT/${ID}@post-install + +printf \ + '\033[?47h\033[2J\033[H' + +cat << EOF + +Script has finished running + +Please reboot your computer + +Press any key to return to the main menu +EOF + +read -srn 1 diff --git a/title b/title new file mode 100644 index 0000000..5a9e49a --- /dev/null +++ b/title @@ -0,0 +1 @@ +ZFSBootMenu Root Install diff --git a/wifi.sh b/wifi.sh new file mode 100755 index 0000000..a0a1144 --- /dev/null +++ b/wifi.sh @@ -0,0 +1,49 @@ +#!/bin/bash +set -euo pipefail + + +# This could help with Fedora's DNS resolution: +# +# resolvectl status | grep 'Link' | sed -e 's|^.*(||g; s|).*||g' | while read -r INTERFACE; do resolvectl dns ${INTERFACE} 9.9.9.9; done && sleep 3 && nmcli device status | grep -v 'DEVICE' | grep -v 'lo' | sed -e 's| .*||g' | while read -r INTERFACE; do nmcli device disconnect ${INTERFACE}; sleep 1; nmcli device connect ${INTERFACE}; done + + +BASEDIR="$(dirname "${0}" | sed "s|^\.|${PWD}|")" + + +cat << EOF +####################################### +## ## +## $(cat "${BASEDIR}/title") Script ## +## ## +## Jean ## +## ## +####################################### + + +############# +## ## +## Wi-Fi ## +## Setup ## +## ## +############# + +EOF + + +nmcli radio wifi on + +nmcli device wifi list + +printf "Enter the SSID of the WiFi network you wish to connect to:\n" +read -r WIFI_NAME + +nmcli --ask device wifi connect ${WIFI_NAME} + +cat << EOF + +Network configured + +Press any key to return to the main menu +EOF + +read -srn 1