From a99cba2f6e2bda2a14b0527066d2efca165699b1 Mon Sep 17 00:00:00 2001 From: Daniel Langbein Date: Sat, 16 Jul 2022 17:18:03 +0200 Subject: [PATCH] import from privacy1st/arch --- .gitignore | 1 + PKGBUILD | 29 +++ README.md | 66 +++++ de-p1st-installer.sh | 481 ++++++++++++++++++++++++++++++++++++ example-headless-docker.cfg | 28 +++ example-vbox.cfg | 64 +++++ installer.cfg | 4 + lib/block-device.sh | 191 ++++++++++++++ lib/user-input.sh | 101 ++++++++ lib/util.sh | 83 +++++++ name-reference-test.sh | 39 +++ 11 files changed, 1087 insertions(+) create mode 100644 .gitignore create mode 100644 PKGBUILD create mode 100644 README.md create mode 100755 de-p1st-installer.sh create mode 100644 example-headless-docker.cfg create mode 100644 example-vbox.cfg create mode 100644 installer.cfg create mode 100644 lib/block-device.sh create mode 100644 lib/user-input.sh create mode 100644 lib/util.sh create mode 100644 name-reference-test.sh diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..57f1cb2 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +/.idea/ \ No newline at end of file diff --git a/PKGBUILD b/PKGBUILD new file mode 100644 index 0000000..cfadfe6 --- /dev/null +++ b/PKGBUILD @@ -0,0 +1,29 @@ +# Maintainer: Daniel Langbein +_pkgname=installer +_reponame=arch +pkgname="de-p1st-$_pkgname" +pkgver=0.2.2 +pkgrel=1 +pkgdesc="Installer for Arch Linux written in Bash" +arch=('any') +url="https://codeberg.org/privacy1st/${_reponame}" +license=('MIT') +depends=('dialog') +makedepends=('git') +backup=(etc/"${pkgname}"/installer.cfg) # Use relative paths without leading slash +source=("git+${url}.git") +sha256sums=('SKIP') + +package() { + cd "${_reponame}/pkg/${pkgname}" + + install -Dm0544 de-p1st-installer.sh "$pkgdir"/usr/bin/"${pkgname}" + + install -Dm0644 lib/block-device.sh "$pkgdir"/usr/lib/"${pkgname}"/block-device.sh + install -Dm0644 lib/user-input.sh "$pkgdir"/usr/lib/"${pkgname}"/user-input.sh + install -Dm0644 lib/util.sh "$pkgdir"/usr/lib/"${pkgname}"/util.sh + + install -Dm0644 -o0 installer.cfg "$pkgdir"/etc/"${pkgname}"/installer.cfg + install -Dm0644 -o0 example-vbox.cfg "$pkgdir"/etc/"${pkgname}"/example-vbox.cfg + install -Dm0644 -o0 example-headless-docker.cfg "$pkgdir"/etc/"${pkgname}"/example-headless-docker.cfg +} diff --git a/README.md b/README.md new file mode 100644 index 0000000..029a84e --- /dev/null +++ b/README.md @@ -0,0 +1,66 @@ +# Arch Linux Installer + +## Running the installer + +### Via ssh and custom livemedium + +1) Build custom [archiso](https://gitlab.archlinux.org/archlinux/archiso) live medium using docker in privileged mode: + +```shell +# dependencies: docker, docker-compose + +cd build-iso +# sudo systemctl start docker +sudo docker-compose run archiso +ls out/out_dir/*.iso +``` + +**On the target computer:** + +2) Boot into the live medium +3) Connect to the Internet + * [wifi instructions](https://wiki.archlinux.org/title/Iwd#iwctl) + +**On some other computer:** + +4) Connect to target computer via `ssh` + * See [build-iso/out/out_dir/ssh-host-fingerprints](../../build-iso/out/out_dir/ssh-host-fingerprints) + for verification of the ssh fingerprint of the target computer +5) (Optional) Copy and adjust one of the example configurations (`/etc/de-p1st-installer/*.cfg`) to `/etc/de-p1st-installer/installer.cfg` + * If this step is skipped, then (most) of the configuration can be entered interactively + * TODO: if `ADDITIONAL_PKGS` is empty, interactively ask for it. +6) Run the installer: + +```shell +de-p1st-installer +``` + +### Via official livemedium + +1) Boot into [official live medium](https://archlinux.org/download/) +2) Add to `/etc/pacman.conf`: + +```shell +[de-p1st] +SigLevel = Optional TrustAll +Server = https://arch.p1st.de +``` + +3) (Optional) Install screen with a `screenrc` configuration file for long scrollback history + +```shell +pacman -Sy +pacman -S de-p1st-screen + +# And then start it with: +screen +``` + +4) Install `de-p1st-installer` to get the install script + +```shell +pacman -Sy +pacman -S de-p1st-installer +``` + +5) Continue with step (5) of "Via custom livemedium" diff --git a/de-p1st-installer.sh b/de-p1st-installer.sh new file mode 100755 index 0000000..8db9e50 --- /dev/null +++ b/de-p1st-installer.sh @@ -0,0 +1,481 @@ +#!/bin/bash + +# load config +source /etc/de-p1st-installer/installer.cfg || { exit 1; } + +# load functions +source /usr/lib/de-p1st-installer/util.sh || { exit 1; } +source /usr/lib/de-p1st-installer/user-input.sh || { exit 1; } +source /usr/lib/de-p1st-installer/block-device.sh || { exit 1; } + +function main() { + # @pre + # bash libraries imported + # @post + # installation finished + + check_network || return $? + system_time || return $? + # in: BOOT_FIRMWARE, FS, HOSTNAME, USERNAME, USER_PWD, FDE, LUKS_PWD; (all variables are optional) + # out: BOOT_FIRMWARE, FS, HOSTNAME, USERNAME, USER_PWD, FDE, LUKS_PWD (if FDE='true') + get_user_input || return $? + # in: CPU_VENDOR (optional) + # out: CPU_VENDOR + get_cpu_vendor || return $? + + # in: FS + # out: FS_DEFAULT_MOUNT_OPTIONS + get_default_mount_options || return $? + # in: FS + # out: FS_ADDITIONAL_MOUNT_OPTIONS + get_additional_mount_options || return $? + + # in: TARGET_BLOCK_DEVICE, BOOT_FIRMWARE + # out: BOOT_PART, LUKS_PART + partition || return $? + # in: BOOT_FIRMWARE, BOOT_PART, LUKS_PART, FDE, LUKS_PWD, FS + # out: LUKS_PART_UUID (if FDE='true'), DATA_PART + format || return $? + + # Combine default and additional mount options + # out: FS_MOUNT_OPTIONS + { + TMP1=("${FS_DEFAULT_MOUNT_OPTIONS[@]}" "${FS_ADDITIONAL_MOUNT_OPTIONS[@]}") || return $? + # Join array elements by "," + join_by ',' TMP1 FS_MOUNT_OPTIONS || return $? + } + + mount_partitions || return $? + + # in: BOOT_FIRMWARE, PACSTRAP_INTERACTIVE (optional) + run_pacstrap || return $? + # in: FS + run_genfstab || return $? + + # in: HOSTNAME, FQDN (optional), STATIC_IP (optional), IPV6_CAPABLE (optional) + config_hostname_and_hosts || return $? + # in: USERNAME, USER_PWD, ROOT_PWD (optional) + user_and_pwd || return $? + + sudo arch-chroot /mnt mkinitcpio -P || return $? + # in: TARGET_BLOCK_DEVICE, FDE, LUKS_PART_UUID + bootloader || return $? + + if [ "${LEAVE_MOUNTED}" = 'true' ]; then + echo 'Leaving partitions below /mnt mounted and '"${DATA_PART}"' opened.' + else + sudo umount -R /mnt || return $? + if [ "${FDE}" = 'true' ] ; then + sudo cryptsetup luksClose "$(basename "${DATA_PART}")" || return $? + fi + fi + echo 'Finished installation without errors!' +} + +function check_network() { + echo 'Sending ping to wikipedia.de ...' + ping -c 1 wikipedia.de >/dev/null || { + echo 'Pleas set up network access.' + return 1 + } +} + +function system_time() { + # Use timedatectl(1) to ensure the system clock is accurate + timedatectl set-ntp true +} + +function increase_cow_space() { + # May be useful when running 'pacman -Syu' on the live medium. + # Usually not necessary! + + # make sure that we are on a live medium: + findmnt /run/archiso/cowspace >/dev/null || { + echo 'Not on live medium, did not increase cowspace!' + return 1 + } + + echo 'Increasing cowspace partition of live medium ...' + sudo mount -o remount,size=2G /run/archiso/cowspace || return $? +} + +function get_user_input() { + # @post + # BOOT_FIRMWARE: 'uefi' | 'bios' + # FS: 'BTRFS' | 'EXT4' | 'F2FS' + # FS_BTRFS_SUBVOL_LAYOUT: 'root_only' | '@root@home' + # HOSTNAME + # USERNAME, USER_PWD + # FDE: 'true' | 'false' + # LUKS_PWD: only set if FDE='true' + + get_block_devices_with_size || return $? + single_choice_if_empty TARGET_BLOCK_DEVICE 'Select target device for installation' BLOCK_DEVICE_SIZES || return $? + + if [ "${BOOT_FIRMWARE}" = 'autodetect' ]; then + # Detect boot firmware type: https://askubuntu.com/a/162573 + + # Check exit code; if 0 EFI, else BIOS. + # "-q": tell grep to output nothing + if dmesg | grep -q 'EFI v'; then + echo 'Detected EFI boot.' + BOOT_FIRMWARE='uefi' + else + echo 'Detected BIOS boot' + BOOT_FIRMWARE='bios' + fi + + else # If $BOOT_FIRMWARE is empty: Let user select BIOS type + TMP1=('uefi' 'Newer mainboards' \ + 'bios' 'Legacy BIOS on older mainboards') + single_choice_if_empty BOOT_FIRMWARE 'Select your bios type' TMP1 || return $? + fi + + TMP1=('BTRFS' 'Allows snapshots and dynamic extension of the FS' \ + 'EXT4' 'Default FS of many distributions' \ + 'F2FS' 'Flash-Friendly-FS for SSD or NVMe') + single_choice_if_empty FS 'Select filesystem to use' TMP1 || return $? + + if [ "${FS}" = 'BTRFS' ]; then + TMP1=('root_only' 'Just one subvolume for "/".' \ + '@root@home' 'Two subvolumes @ and @home. This configuration allows usage of "Timeshift".') + single_choice_if_empty FS_BTRFS_SUBVOL_LAYOUT 'Select your preferred subvolume layout' TMP1 || return $? + fi + + ask_user_if_empty HOSTNAME 'Enter hostname:' || return $? + ask_user_if_empty USERNAME 'Enter username:' || return $? + + if [ -z "${USER_PWD}" ]; then + ask_user_if_empty USER_PWD 'Enter a user password:' || return $? + ask_user_if_empty USER_PWD2 'Please enter the password again:' || return $? + # shellcheck disable=SC2153 + [[ "${USER_PWD}" == "${USER_PWD2}" ]] || { + echo 'Passwords did not match'; + exit 1; + } + fi + + TMP1=('true' 'Yes' 'false' 'No') + single_choice_if_empty FDE 'Shall Full-Disk-Encryption be enabled?' TMP1 || return $? + + if [ "${FDE}" = 'true' ] && [ -z "${LUKS_PWD}" ]; then + ask_user_if_empty LUKS_PWD 'Enter a disk encryption password:' || return $? + ask_user_if_empty LUKS_PWD2 'Please enter the password again:' || return $? + # shellcheck disable=SC2153 + [[ "${LUKS_PWD}" == "${LUKS_PWD2}" ]] || { + echo 'Passwords did not match'; + exit 1; + } + fi +} + +function get_default_mount_options() { + # @pre + # FS + # @post + # FS_DEFAULT_MOUNT_OPTIONS (array) + + FS_DEFAULT_MOUNT_OPTIONS=() + + case "${FS}" in + BTRFS) + # "compress=lzo": archwiki -> Btrfs#Compression + # "compress=zstd:1": + # -> https://btrfs.wiki.kernel.org/index.php/Compression#What_are_the_differences_between_compression_methods.3F + # -> https://fedoraproject.org/wiki/Changes/BtrfsTransparentCompression#Q:_Why_use_zstd:1_specifically.3F + # + # "Enable compression (better performance, longer flash lifespan)" + FS_DEFAULT_MOUNT_OPTIONS+=('compress=zstd:1') + ;; + EXT4) + # https://wiki.archlinux.org/index.php/Ext4#Enabling_metadata_checksums + # If the CPU supports SSE 4.2, make sure the crc32c_intel kernel module is loaded + FS_DEFAULT_MOUNT_OPTIONS+=('metadata_csum') + ;; + F2FS) + # When mounting the filesystem, specify compress_algorithm=(lzo|lz4|zstd|lzo-rle). + # Using compress_extension=txt will cause all txt files to be compressed by default. + FS_DEFAULT_MOUNT_OPTIONS+=('compress_algorithm=lz4') + ;; + *) + echo 'Filesystem '"${FS}"' not yet supported!' + return 1 + ;; + esac +} + +function get_additional_mount_options() { + # @pre + # FS + # @post + # FS_ADDITIONAL_MOUNT_OPTIONS (array) + + case "${FS}" in + BTRFS) + # noatime, nodiratime: + # - The atime options do impact drive performance; + # - noatime implies nodiratime, one does not need to specify both; + # - The noatime option fully disables writing file access times to the drive every time you read a file. + # This works well for almost all applications, except for those that need to know if a file has been + # read since the last time it was modified. + TMP1=('noatime' 'Don'\''t write file/folder access times' 'on' 'ssd' 'Enable if using SSD/NVMe' 'off') + ;; + EXT4) + TMP1=('noatime' 'Don'\''t write file/folder access times' 'on') + ;; + F2FS) + # shellcheck disable=SC2034 + TMP1=('noatime' 'Don'\''t write file/folder access times' 'on') + ;; + *) + echo 'Filesystem '"${FS}"' not yet supported!' + return 1 + ;; + esac + + multi_choice_if_empty FS_ADDITIONAL_MOUNT_OPTIONS 'Select mount options' TMP1 || return $? +} + +function mount_partitions() { + # @pre + # FS, FS_BTRFS_SUBVOL_LAYOUT, FS_MOUNT_OPTIONS, DATA_PART, BOOT_PART + + if [ "${FS}" = 'BTRFS' ]; then + case "${FS_BTRFS_SUBVOL_LAYOUT}" in + 'root_only') + # Nothing special; same steps as for a regular FS + echo 'Mounting data partition with options: '"${FS_MOUNT_OPTIONS}" + sudo mount -o "${FS_MOUNT_OPTIONS}" "${DATA_PART}" /mnt || return $? + ;; + '@root@home') + # Timeshift BTRFS subvol layout: + # https://github.com/teejee2008/timeshift#supported-system-configurations + + # Mount top level subvolume + sudo mount -o subvolid=5 "${DATA_PART}" /mnt || return $? + # Create subvolumes @ and @home + sudo btrfs subvolume create /mnt/@ || return $? + sudo btrfs subvolume create /mnt/@home || return $? + # List the created subvolumes + sudo btrfs subvolume list /mnt || return $? + # Umount the top level subvolume + sudo umount -R /mnt || return $? + + echo 'Mounting @ and @home subvolumes with options: '"${FS_MOUNT_OPTIONS}" + sudo mount -o 'subvol=@,'"${FS_MOUNT_OPTIONS}" "${DATA_PART}" /mnt || return $? + sudo mkdir /mnt/home || return $? + sudo mount -o 'subvol=@home,'"${FS_MOUNT_OPTIONS}" "${DATA_PART}" /mnt/home || return $? + ;; + *) + echo 'BTRFS subvolume layout '"${FS_BTRFS_SUBVOL_LAYOUT}"' not supported!' + return 1 + ;; + esac + else + echo 'Mounting data partition with options: '"${FS_MOUNT_OPTIONS}" + sudo mount -o "${FS_MOUNT_OPTIONS}" "${DATA_PART}" /mnt || return $? + fi + + echo 'Mounting boot partition ...' + sudo mkdir /mnt/boot || return $? + sudo mount "${BOOT_PART}" /mnt/boot || return $? +} + +function run_pacstrap() { + # @pre + # BOOT_FIRMWARE + # PACSTRAP_INTERACTIVE: optional, 'true' + + echo 'Running pacstrap ...' + PKGS=("${ADDITIONAL_PKGS[@]}") + + case "${BOOT_FIRMWARE}" in + uefi) + PKGS+=('de-p1st-base') + ;; + bios) + echo 'Not yet implemented' + return 1 + ;; + *) + echo 'Not yet implemented!' + return 1 + ;; + esac + + # If CPU_VENDOR is not empty, then + if [ -n "${CPU_VENDOR}" ]; then + case "${CPU_VENDOR}" in + amd) + PKGS+=('de-p1st-ucode-amd') + ;; + intel) + PKGS+=('de-p1st-ucode-intel') + ;; + none) + PKGS+=('de-p1st-ucode-placeholder') + ;; + *) + echo 'Invalid option: '"${CPU_VENDOR}" + return 1 + ;; + esac + fi + + local args=() + if [ "${PACSTRAP_INTERACTIVE}" = 'true' ]; then + args+=('-i') # run interactively + fi + args+=('/mnt') + + sudo pacstrap "${args[@]}" "${PKGS[@]}" || return $? +} + +function run_genfstab() { + # @pre + # FS + + echo 'Generating fstab ...' + local fstab + fstab="$(genfstab -U /mnt)" + + case "${FS}" in + BTRFS) + # Remove "subvolid=..." mount option but leave "subvol=..." mount option + fstab=$(printf '%s' "${fstab}" | sed 's/,subvolid=[^,\s]\+//') || return $? + # Check if fstab does still contain subvolid mount option + if printf '%s' "${fstab}" | grep -q 'subvolid='; then + echo 'This should not happen!' + return 1 + fi + ;; + EXT4) + true + ;; + F2FS) + true + ;; + *) + echo 'Filesystem '"${FS}"' not yet supported!' + return 1 + ;; + esac + + printf '%s' "${fstab}" | sudo tee /mnt/etc/fstab >/dev/null || return $? +} + +function config_hostname_and_hosts() { + # @pre + # HOSTNAME + # FQDN: optional, e.g. sub.domain.de + # STATIC_IP: optional, e.g. 93.133.433.133 + # IPV6_CAPABLE: optional, 'true' + + echo 'Set hostname ...' + echo "${HOSTNAME}" | sudo tee /mnt/etc/hostname >/dev/null || return $? + + echo 'Create hosts file ...' + # If the system has a permanent IP address, it should be used instead of 127.0.1.1. + # * https://wiki.archlinux.org/index.php/Installation_guide#Network_configuration + + # Desirable entries IPv4/IPv6: + # * https://man.archlinux.org/man/hosts.5#EXAMPLES + + # If FQDN not given, use $HOSTNAME.localdomain instead + FQDN="${FQDN:="${HOSTNAME}.localdomain"}" + # If STATIC_IP not given, use 127.0.1.1 instead + STATIC_IP="${STATIC_IP:=127.0.1.1}" + + echo '# The following lines are desirable for IPv4 capable hosts +127.0.0.1 localhost +# 127.0.1.1 is often used for the FQDN of the machine +'"${STATIC_IP} ${FQDN} ${HOSTNAME}" | sudo tee /mnt/etc/hosts >/dev/null || return $? + + if [ "${IPV6_CAPABLE}" = 'true' ]; then + echo ' +# The following lines are desirable for IPv6 capable hosts +::1 localhost ip6-localhost ip6-loopback +ff02::1 ip6-allnodes +ff02::2 ip6-allrouters' | sudo tee -a /mnt/etc/hosts >/dev/null || return $? + fi +} + +function user_and_pwd() { + # @pre + # USERNAME + # USER_PWD + # ROOT_PWD (optional) + + echo 'Adding user and changing shell to /bin/zsh ...' + # -m: create home + # -U: Create a group with the same name as the user, and add the user to this group. + sudo arch-chroot /mnt useradd -m -s /usr/bin/zsh -g wheel "${USERNAME}" || return $? + sudo arch-chroot /mnt chsh -s /usr/bin/zsh || return $? + + # If ROOT_PWD is not given, the use USER_PWD for root user + ROOT_PWD="${ROOT_PWD:="${USER_PWD}"}" + + printf '%s:%s' "${USERNAME}" "${USER_PWD}" | sudo chpasswd --root /mnt || return $? + printf '%s:%s' "root" "${ROOT_PWD}" | sudo chpasswd --root /mnt || return $? +} + +function bootloader() { + # @pre + # TARGET_BLOCK_DEVICE + # FDE: 'true' | 'false' + # LUKS_PART_UUID: required if FDE='true' + + echo 'Installing grub ...' + case "${BOOT_FIRMWARE}" in + uefi) + # portable fallback efi name for grub: + # * https://www.rodsbooks.com/efi-bootloaders/installation.html#alternative-naming + # * arch-chroot /mnt cp /boot/EFI/GRUB/grubx64.efi /boot/EFI/BOOT/bootx64.efi + sudo arch-chroot /mnt grub-install --target=x86_64-efi --bootloader-id=GRUB --efi-directory=/boot --removable || return $? + ;; + bios) + sudo arch-chroot /mnt grub-install --target=i386-pc "${TARGET_BLOCK_DEVICE}" || return $? + ;; + *) + echo 'Not yet implemented!' + return 1 + ;; + esac + + echo 'Generating /boot/grub/grub.cfg ...' + { + case "${FDE}" in + true) + # /etc/default/grub is managed by Holo. Therefore we should not manually modify it. + # Instead, we create a holosript which writes $LUKS_PART_UUID into GRUB_CMDLINE_LINUX of /etc/default/grub + { + # Assert + grep --quiet '^GRUB_CMDLINE_LINUX=""$' < /mnt/etc/default/grub || return + + # Use filename .../20- for the holoscript so that it gets executed after the one from de-p1st-grub + local holoScriptDir=/mnt/usr/share/holo/files/20-de-p1st-installer/etc/default/ + # The holoscript shall contain one 'sed "..."' command + sudo mkdir -p "${holoScriptDir}" || return $? + sudo echo '#!/bin/sh +sed "s|^GRUB_CMDLINE_LINUX=\"\"\$|GRUB_CMDLINE_LINUX=\"cryptdevice=/dev/disk/by-uuid/'"${LUKS_PART_UUID}"':crypt\"|"' \ + | sudo tee "${holoScriptDir}"/grub.holoscript || return $? + sudo chmod 0544 "${holoScriptDir}"/grub.holoscript || return $? + } + # Then we apply the holoscript + sudo arch-chroot /mnt holo apply --force file:/etc/default/grub || return $? + ;; + false) + true + ;; + *) + echo 'Invalid option: '"${FDE}" + return 1 + ;; + esac + + # And finally run grub-mkconfig + sudo arch-chroot /mnt grub-mkconfig -o /boot/grub/grub.cfg || return $? + } +} + +main "$@" diff --git a/example-headless-docker.cfg b/example-headless-docker.cfg new file mode 100644 index 0000000..87d37ad --- /dev/null +++ b/example-headless-docker.cfg @@ -0,0 +1,28 @@ +# Example config for a headless root server running docker + +FQDN=v2202012136854137045.happysrv.de +STATIC_IP=45.83.105.88 +IPV6_CAPABLE=true +HOSTNAME=p1st-arch-1 +USERNAME=yoda + +TARGET_BLOCK_DEVICE=/dev/sda +BOOT_PART_SIZE=500 # MiB +FS=BTRFS +FS_ADDITIONAL_MOUNT_OPTIONS=('noatime') + +CPU_VENDOR=none +BOOT_FIRMWARE=uefi + +LEAVE_MOUNTED=false +PACSTRAP_INTERACTIVE=true + +ADDITIONAL_PKGS=() +# to skip pacman selections +ADDITIONAL_PKGS+=('mkinitcpio' 'de-p1st-kernel-lts' 'de-p1st-ucode-placeholder' 'qemu-guest-agent') +# remote ssh access +ADDITIONAL_PKGS+=('de-p1st-ssh' 'de-p1st-ssh-key-yoda') +# docker and docker-compose +ADDITIONAL_PKGS+=('de-p1st-docker') +# cronie +ADDITIONAL_PKGS+=('de-p1st-cronie') diff --git a/example-vbox.cfg b/example-vbox.cfg new file mode 100644 index 0000000..7390dc7 --- /dev/null +++ b/example-vbox.cfg @@ -0,0 +1,64 @@ +# Example config for testing in VirtualBox + +# FQDN=domain.name.of.this.host.de +# STATIC_IP=123.123.123.123 +# IPV6_CAPABLE=true +HOSTNAME=yodaTest +USERNAME=yoda + +# One should rather enter these interactively than saving in this cfg. +USER_PWD='test' +FDE=false # enable FULL-DISK-ENCRYPTION +LUKS_PWD='test' + +# If unset, then USER_PWD will be used for ROOT_PWD +# ROOT_PWD=test + +TARGET_BLOCK_DEVICE=/dev/sda # /dev/vda for Virtual Machine Manager +BOOT_PART_SIZE=500 # MiB +FS=BTRFS +FS_BTRFS_SUBVOL_LAYOUT='@root@home' # Subvolume layout that is supported by Timeshift +FS_ADDITIONAL_MOUNT_OPTIONS=('noatime') + +# If not booted into the target system, these values should be set: +# CPU_VENDOR: 'autodetect' | 'amd' | 'intel' | 'none' +CPU_VENDOR=autodetect +# BOOT_FIRMWARE: 'autodetect' | 'uefi' | 'bios' +BOOT_FIRMWARE=uefi + +# If set to 'true', then the data, boot and luks partitions +# will be left mounted/opened for manual inspection +# after the installation +LEAVE_MOUNTED=true +PACSTRAP_INTERACTIVE=true + +############## ADDITIONAL_PKGS from here on ############## +ADDITIONAL_PKGS=() + +# Specify the preferred providers for initramfs, de-p1st-kernel, de-p1st-ucode, de-p1st-sddm-theme +ADDITIONAL_PKGS+=('mkinitcpio' 'de-p1st-kernel-lts' 'de-p1st-ucode-placeholder' 'de-p1st-sddm-theme-nordic') +# VBox guest additions +ADDITIONAL_PKGS+=('virtualbox-guest-utils') +# XFCE4 desktop with HiDPI +ADDITIONAL_PKGS+=('de-p1st-gpu-generic' 'de-p1st-xfce4-hidpi' 'de-p1st-sddm-tablet') +# To be able to install ungoogled-chromium +ADDITIONAL_PKGS+=('de-p1st-pacman-repo-chromium') + +# If your network is unsafe, e.g. if you live in Germany, then include a VPN client +ADDITIONAL_PKGS+=('riseup-vpn') +# Smartcard (e.g. Nitrokey or Yubikey) +ADDITIONAL_PKGS+=('de-p1st-smartcard') + + +# Other programs +ADDITIONAL_PKGS+=('firefox' 'signal-desktop' 'nextcloud-client' 'keepassxc' 'xournalpp' 'zotero') +# Development +ADDITIONAL_PKGS+=('intellij-idea-ultimate-edition' 'intellij-idea-ultimate-edition-jre') +# Document viewer +ADDITIONAL_PKGS+=('evince') +# Gallery / image viewer +ADDITIONAL_PKGS+=('nomacs' 'qt5-imageformats') +# LibreOffice with spell checking (de, us) and hyphenation rules (de, us) +ADDITIONAL_PKGS+=('libreoffice-fresh') +ADDITIONAL_PKGS+=('hunspell-de' 'hunspell-en_US') +ADDITIONAL_PKGS+=('hyphen' 'hyphen-de' 'hyphen-en') diff --git a/installer.cfg b/installer.cfg new file mode 100644 index 0000000..ba2e573 --- /dev/null +++ b/installer.cfg @@ -0,0 +1,4 @@ +# You can find some examples in the same directory +# that can be used as starting point: +# ./example-*.cfg +# Just copy one of those over this file. diff --git a/lib/block-device.sh b/lib/block-device.sh new file mode 100644 index 0000000..31e5b91 --- /dev/null +++ b/lib/block-device.sh @@ -0,0 +1,191 @@ +# +# lsblk +# +# -d, --nodeps Do not print holder devices or slaves. +# -p, --paths Print full device paths. +# -l, --list Produce output in the form of a list. +# -n, --noheadings +# -x, --sort column + +function get_uuid() { + # arg $1: partition + # arg $2: variable name to store UUID + + local -n ptr=$2 || return $? + ptr="$(blkid -o value -s UUID "${1}")" || return $? +} + +function get_partitions(){ + # arg $1: block-device + # @post + # PARTITIONS (array with one partition per entry) + + # Remove first line of output (which is just the block device $1 itself) + # with sed: sed 1d + PARTITIONS="$(lsblk -pln -o name "${1}" | sed 1d)" || return $? + + newline_separated_to_array PARTITIONS PARTITIONS || return $? +} + +function get_block_devices() { + # @post + # BLOCK_DEVICES (array with one entry for each block device) + + # Get list of devices, one per line + BLOCK_DEVICES="$(lsblk -dplnx size -o name | grep -Ev 'boot|rpmb|loop' | tac)" || return $? + + newline_separated_to_array BLOCK_DEVICES BLOCK_DEVICES || return $? +} + +function get_block_devices_with_size() { + # @post + # BLOCK_DEVICE_SIZES (array with two entries for each block device: device path and device size) + + # Get list of devices and their sizes, one pair per line + # Use sed to remove multiple white spaces: sed 's|\s\s*| |' + BLOCK_DEVICE_SIZES="$(lsblk -dplnx size -o name,size | grep -Ev 'boot|rpmb|loop' | sed 's|\s\s*| |' | tac)" || return $? + + newline_to_space BLOCK_DEVICE_SIZES || return $? + space_separated_to_array BLOCK_DEVICE_SIZES BLOCK_DEVICE_SIZES || return $? +} + + +function partition() { + # Creates two partitions: + # - BOOT_PART by default 261mb + # - LUKS_PART rest of the device for (encrypted) data + # + # @pre + # TARGET_BLOCK_DEVICE + # BOOT_FIRMWARE + # @post + # BOOT_PART + # LUKS_PART + + # if BOOT_PART_SIZE not given, set to default value + BOOT_PART_SIZE="${BOOT_PART_SIZE:='261'}" + # if too small, print warning and exit + if [ "${BOOT_PART_SIZE}" -lt '261' ]; then + echo 'BOOT_PART_SIZE should be larger than 260!'; + return 1; + fi + + # As our data partition is encrypted, + # we need a separate boot partition! + case "${BOOT_FIRMWARE}" in + uefi) + # EFI boot partition + # + # Create a partition with fat32 as the file system type and set the + # esp flag on it. + sudo parted --script "${TARGET_BLOCK_DEVICE}" -- mklabel gpt \ + mkpart ESP fat32 2Mib "${BOOT_PART_SIZE}MiB" \ + set 1 esp on \ + mkpart primary "${BOOT_PART_SIZE}MiB" 100% || return $? + + get_partitions "${TARGET_BLOCK_DEVICE}" || return $? + BOOT_PART="${PARTITIONS[0]}" + LUKS_PART="${PARTITIONS[1]}" + ;; + bios) + # > On a BIOS/GPT configuration, a BIOS boot partition is required. GRUB embeds its `core.img` + # > into this partition. + # > For parted set/activate the flag bios_grub on the partition. + # + # archwiki -> GRUB#GUID_Partition_Table_(GPT)_specific_instructions + # https://www.gnu.org/software/grub/manual/grub/html_node/BIOS-installation.html#BIOS-installation + sudo parted --script "${TARGET_BLOCK_DEVICE}" -- mklabel gpt \ + mkpart primary 1MiB 2MiB \ + set 1 bios_grub on \ + mkpart primary 2MiB "${BOOT_PART_SIZE}MiB" \ + mkpart primary "${BOOT_PART_SIZE}MiB" 100% || return $? + + get_partitions "${TARGET_BLOCK_DEVICE}" || return $? + BOOT_PART="${PARTITIONS[2]}" + LUKS_PART="${PARTITIONS[3]}" + ;; + *) + echo 'Expected uefi or bios but got '"${BOOT_FIRMWARE}"' instead!' + return 1 + ;; + esac + + echo 'boot partition: '"${BOOT_PART}" + echo 'luks partition: '"${LUKS_PART}" +} + +function format() { + # Formats BOOT_PART and LUKS_PART. + # If encryption is enabled (FDE='true'), then an encrypted partition will be created at LUKS_PART + # which can be accessed under DATA_PART. + # If encryption is disabled, then DATA_PART is identical to LUKS_PART. + # + # @pre + # BOOT_FIRMWARE + # BOOT_PART + # LUKS_PART + # LUKS_PWD + # FDE: 'true' | 'false' + # FS + # @post + # LUKS_PART_UUID: only set if FDE='true' + # DATA_PART: if FDE='true', then set to /dev/mapper/$(basename "${LUKS_PART}") + # boot partition formatted + # luks partition formatted and accessible under DATA_PART + + echo 'Wiping old signatures from partitions ...' + sudo wipefs "${BOOT_PART}" || return $? + sudo wipefs "${LUKS_PART}" || return $? + + echo 'Formatting boot partition '"${BOOT_PART}"' ...' + sudo mkfs.fat -F32 "${BOOT_PART}" || return $? + + case "${FDE}" in + true) + # Note: + # FDE: GRUB does support LUKS2 since this commit: https://git.savannah.gnu.org/cgit/grub.git/commit/?id=365e0cc3e7e44151c14dd29514c2f870b49f9755 + # -> Using "--type luks1" is no longer required. + echo 'Creating encrypted luks partition '"${LUKS_PART}"' ...' + printf '%s' "${LUKS_PWD}" | sudo cryptsetup luksFormat \ + --cipher aes-xts-plain64 --key-size 512 --hash sha512 \ + --iter-time 10000 --use-random "${LUKS_PART}" || return $? + + get_uuid "${LUKS_PART}" LUKS_PART_UUID || return $? + + local luks_name + luks_name=$(basename "${LUKS_PART}") + DATA_PART="/dev/mapper/${luks_name}" + + # open luks partition + printf '%s' "${LUKS_PWD}" | sudo cryptsetup luksOpen "${LUKS_PART}" "${luks_name}" || return $? + ;; + false) + DATA_PART="${LUKS_PART}" + ;; + *) + echo 'Invalid option: '"${FDE}" + return 1 + ;; + esac + + echo 'Formatting the data partition '"${DATA_PART}"' ...' + case "${FS}" in + BTRFS) + sudo mkfs.btrfs "${DATA_PART}" || return $? + ;; + EXT4) + # archwiki -> Ext4#Enabling_metadata_checksums + sudo mkfs.ext4 -O metadata_csum "${DATA_PART}" || return $? + ;; + F2FS) + # archwiki -> F2FS#Creating_a_F2FS_file_system + # - requires f2fs-tools + # - compression: "-O compression" and when mounting the filesystem, specify compress_algorithm=(lzo|lz4|zstd|lzo-rle) + sudo mkfs.f2fs -O extra_attr,inode_checksum,sb_checksum,compression "${DATA_PART}" || return $? + ;; + *) + echo 'Filesystem '"${FS}"' is not yet supported!' + return 1 + ;; + esac +} diff --git a/lib/user-input.sh b/lib/user-input.sh new file mode 100644 index 0000000..85b5dfb --- /dev/null +++ b/lib/user-input.sh @@ -0,0 +1,101 @@ +function ask_user_if_empty { + # If variable with name $1 is empty, then ask for user input. + # + # Only one line of user input is allowed. + # The input must not be empty. + # + # arg $1: name of variable to store user input + # arg $2: text to display (e.g. "Enter hostname:") + if [ "$#" -ne 2 ]; then + echo 'ask_user_if_empty requires two args!'; + return 1 + fi + for i in "$@"; do + if [ -z "${i}" ]; then + echo 'ask_user_if_empty: all given args must not be empty'; + return 1; + fi + done + + local -n ptr=$1 || return $? + if [ -z "${ptr}" ]; then + # if ptr has no value yet, ask user for input! + echo "${2}" + read -r ptr || return $? + fi + + # check string length to be greater than zero! + if [ "${#ptr}" -lt 1 ]; then + echo 'The input must not be empty!'; + return 1; + fi +} + +function single_choice_if_empty { + # If variable with name $1 is empty, then let user select one of the given options. + # + # arg $1: name of variable to store the selected option + # arg $2: text to display + # arg $3: name of variable with array of options to display (for each option there must be two entries in the array: Item and description) + if [ "$#" -ne 3 ]; then + echo 'single_choice_if_empty requires three args!'; + return 1 + fi + for i in "$@"; do + if [ -z "${i}" ]; then + echo 'single_choice_if_empty: all given args must not be empty'; + return 1; + fi + done + + + local -n ptr=$1 || return $? + if [ -z "${ptr}" ]; then + # if ptr has no value yet, ask user for input! + + local -n MENU_OPTIONS=$3 || return $? + ptr=$(dialog --stdout --menu "${2}" 0 0 0 "${MENU_OPTIONS[@]}") || { + echo 'Error during menu selection!' + exit 1 + } + clear + fi +} + +function multi_choice_if_empty { + # If variable with name $1 is empty, then let user select one or more of the given options. + # + # arg $1: name of variable to store array of selected options + # arg $2: text to display + # arg $3: name of variable with array of options to display (for each option there must be three entries in the array: Item, description, on/off) + if [ "$#" -ne 3 ]; then + echo 'multi_choice_if_empty requires three args!'; + return 1 + fi + for i in "$@"; do + if [ -z "${i}" ]; then + echo 'multi_choice_if_empty: all given args must not be empty'; + return 1; + fi + done + + + local -n ptr=$1 || return $? + if [ -z "${ptr}" ]; then + # if ptr has no value yet, ask user for input! + + local -n MENU_OPTIONS=$3 || return $? + TMP1=$(dialog --stdout --checklist "${2}" 0 0 0 "${MENU_OPTIONS[@]}") || { + echo 'Error during menu selection!' + exit 1 + } + clear + + # Result of dialog is space separated list + # Store this as an array + # Without newline at last array element: https://unix.stackexchange.com/a/519917/315162 + # readarray -d " " -t ptr < <(printf '%s' "$TMP1") + # + space_separated_to_array TMP1 "$1" + fi +} diff --git a/lib/util.sh b/lib/util.sh new file mode 100644 index 0000000..a66eb81 --- /dev/null +++ b/lib/util.sh @@ -0,0 +1,83 @@ +function join_by() { + # Join array elements with character $1 + # + # arg $1: delimiter + # arg $2: name of source array + # arg $3: variable name to store result + + local -n ptr=$2 || return $? + local -n ptr2=$3 || return $? + ptr2=$(printf ',%s' "${ptr[@]}") + ptr2=${ptr2:1} +} + +function newline_to_space() { + # Replaces all newlines with spaces + # + # arg $1: name of variable + + local -n ptr=$1 || return $? + # Replace newlines with spaces + # See bash string substitution: https://gist.github.com/JPvRiel/b279524d3e56229a896477cb8082a72b + + # echo "replacing newlines in str: -->$ptr<--" + ptr="${ptr//$'\n'/' '}" + # echo "new str is: -->$ptr<--" +} + +function newline_separated_to_array() { + # Watch out for tailing newlines as these will get an empty array entry at the end. + # $1 and $2 can be equal (if the result shall be written to the input variable) + # + # arg $1: name of variable with newline separated list + # arg $2: name of array to store values into + + local -n ptr=$1 || return $? + local -n ptr2=$2 || return $? + # ptr newline separated list. + # Store this as array ptr2. + readarray -t ptr2 <<<"${ptr}" +} + +function space_separated_to_array() { + # arg $1: name of variable with space separated list + # arg $2: name of array to store values into + + local -n ptr=$1 || return $? + # shellcheck disable=SC2178 + local -n ptr2=$2 || return $? + # ptr space separated list. + # Store this as array ptr2. + # Without newline at last array element: https://unix.stackexchange.com/a/519917/315162 + readarray -d ' ' -t ptr2 < <(printf '%s' "${ptr}") +} + +function get_cpu_vendor() { + # @pre + # CPU_VENDOR ("", "autodetect") + # @post + # CPU_VENDOR ("amd", "intel", "none", "") + + if [[ -z "${CPU_VENDOR}" ]] || [[ "${CPU_VENDOR}" == 'autodetect' ]]; then + + # If run virtual environment (e.g. VirtualBox) then no CPU microcode is required + if cat /proc/cpuinfo | grep '^flags.*hypervisor' >/dev/null; then + echo 'Detected virtual environment.' + CPU_VENDOR='none' + + else + local vendor_id + vendor_id=$(cat /proc/cpuinfo | grep vendor_id | head -1 | sed 's|vendor_id\s*:\s*||') + + if [ "${vendor_id}" = 'AuthenticAMD' ]; then + CPU_VENDOR='amd' + elif [ "${vendor_id}" = 'GenuineIntel' ]; then + CPU_VENDOR='intel' + else + echo 'Unknown CPU vendor' + return 1 + fi + fi + + fi +} diff --git a/name-reference-test.sh b/name-reference-test.sh new file mode 100644 index 0000000..86d2f6b --- /dev/null +++ b/name-reference-test.sh @@ -0,0 +1,39 @@ +#!/bin/bash +# source: https://stackoverflow.com/a/52678279/6334421 + +# A variable can be assigned the nameref attribute using the -n option +# to the declare or local builtin commands (see Bash Builtins) to create +# a nameref, or a reference to another variable. This allows variables to +# be manipulated indirectly. + +function text_input { + # If variable of name $1 is zero, then ask user for input. + # Only one line user input is allowed. + # User input must not be empty. + # + # $1: variable name to store input to + # $2: text to display (e.g. "Enter hostname:") + if [ -z "${1}" ] || [ -z "${2}" ]; then + echo 'text_input requires two args!'; + return 1 + fi + + local -n ptr=$1 || return $? + if [ -z "${ptr}" ]; then + echo "${2}" + read -r ptr || return $? + fi + + # check string length to be greater than zero! + if [ "${#ptr}" -lt 1 ]; then + echo 'text_input must not be empty!'; + return 1; + fi +} + +text_input foo 'Enter something funny:' +echo "Input: ${foo}" + +bar='' +text_input bar 'Enter something even funnier:' +echo "Input: ${bar}"