#!/usr/bin/env bash ### ## ## NoveriaOS Install script ## ### ### ## Definitions ### # Checks readonly CHECK_INTERNET_URL='www.alpinelinux.org' # Installation readonly INSTALLATION_LOCK_FILE='/root/.installation.lock' readonly INSTALLATION_LOGFILE='/root/installation.log' readonly INSTALLATION_PARTLABEL_ESP="ESP" readonly INSTALLATION_PARTLABEL_ROOT="ROOT" readonly INSTALLATION_ESP_PARTITION_SIZE=4 readonly INSTALLATION_MOUNTPOINT='/mnt' readonly INSTALLATION_NOVERIA_BIN='/usr/local/noveria/bin' readonly INSTALLATION_SECRETS_FILE="/root/installation.secrets" readonly INSTALLATION_SALT_ROOT="srv/salt" readonly INSTALLATION_SALT_GIT="https://git.noveria.org/NoveriaOS/salt-statetree.git" readonly INSTALLATION_PILLAR_ROOT="srv/pillar" readonly INSTALLATION_PILLAR_GIT="https://git.noveria.org/NoveriaOS/salt-pillartree.git" readonly INSTALLATION_ALPINE_VERSION=$(cat /etc/os-release | grep VERSION_ID | cut -d= -f2) # Colors readonly RED='\033[0;31m' readonly NC='\033[0m' # No Color ### ## Errorhandling ### # set -e exit script when a command fails # set -o pipefail check any command in pipeline for error, not just last one # set -eo pipefail # catch ^C and other signals and clean up trap "errorHardExit 'Interrupted with CTRL+C'" SIGINT SIGHUP SIGTERM SIGABRT ### ## Helper Functions ### ## # Installation subtask title output # - $1: subtask title ## function installationSubtaskTitle() { echo -e "\n=> $1" } ## # Random password generator (alphanumeric) # - $1: password length (default 11) # - $2: avoid poorly readable characters: l/I/1, O/0 (default false) ## function randomPasswordGen() { # character set if ${2:-false}; then local character_set='a-km-zA-HJ-NP-Z2-9' else local character_set='a-zA-Z0-9' fi # https://en.wikipedia.org/wiki/randomPasswordGen#Bash LC_ALL=C tr -dc "$character_set" /dev/null 2>&1; do dialog --clear --title "No internet connection" --msgbox " Insert network cable, wait 5 seconds and press 'OK'" 7 65 done prepareInstallation } ## # Installation preparation ## function prepareInstallation() { # disk readonly AVAILABLE_DISKS=($(lsblk | grep -vE "p[0-9]+" | grep -vE "[s,v]d[a-z][0-9]+" | grep -v "luks" | grep -v "rom" | grep -vE "sd[a-z][0-9]+" | grep -v "/" | tail -n +2 | awk '{print $1}')) for available_disk in ${AVAILABLE_DISKS[@]}; do if [[ -z "$diskString" ]]; then diskString="$available_disk /dev/$available_disk off" else diskString="$diskString $available_disk /dev/$available_disk off" fi done INSTALLATION_DISK=$(dialog --clear --radiolist "Select Disk to install the system" 10 70 3 $(echo $diskString) 3>&1 1>&2 2>&3 3>&-) INSTALLATION_DISK_BYID=$(lshw -class disk | grep ${INSTALLATION_DISK} | tr -d "[:space:]" | cut -d: -f2) # root password INSTALLATION_ROOT_PW=$(randomPasswordGen 5) for _ in {0..2}; do INSTALLATION_ROOT_PW="${INSTALLATION_ROOT_PW}-$(randomPasswordGen 5)" done # salt-master or minion SALT_RUNNER=$(dialog --clear --radiolist "What shall this host be?" 10 70 3 "salt-master" "salt-master" "off" "salt-minion" "salt-minion" "on" 3>&1 1>&2 2>&3 3>&-) if [[ "$SALT_RUNNER" == "salt-minion" ]]; then dialog --clear --title "Is there a salt-master in this network?" --yes-label "Yes" --no-label "No" --yesno case $? in 0) dialog --clear --title "What's the IP of the salt-master?" --inputbox "Enter IP's Address" 10 70 3 >$SALT_MASTER_IP ;; 1) SALT_MASTER_IP="127.0.0.1" esac fi # show summary summary } ## # Summary to confirm ## function summary() { dialog --stdout --clear --title "Summary" --yes-label "Confirm" --no-label "Abort" --yesno "\n Disk: $INSTALLATION_DISK \n Salt: $SALT_RUNNER $([[ $SALT_RUNNER == 'salt-minion' ]] && ([[ $SALT_MASTER_IP != '127.0.0.1' ]] && echo \"\n Salt-Master IP: $SALT_MASTER_IP\")) " 9 60 case $? in 0) installation ;; 1) clear errorHardExit "Abort on summary dialog" ;; 255) clear errorHardExit "Abort on summary dialog" ;; esac } ## # Installation ## function installation() { # clear display clear # lock file touch "$INSTALLATION_LOCK_FILE" || installationFailed # log all output to logfile rm -f "$INSTALLATION_LOGFILE" || installationFailed exec &> >(tee -a "$INSTALLATION_LOGFILE") # create boot environment timestamp START_TIMESTAMP=$(date +"%Y-%m-%d_%H-%M-%S") || installationFailed echo "" echo "┌──────────────────────────────────────────┐" echo "│ Swipe and repartition disk │" echo "└──────────────────────────────────────────┘" installationSubtaskTitle "Wipe disk" blkdiscard -f "${INSTALLATION_DISK_BYID}" || installationFailed installationSubtaskTitle "Repartitioning disk" parted -s "${INSTALLATION_DISK_BYID}" mklabel gpt || installationFailed parted -s "${INSTALLATION_DISK_BYID}" mkpart "${INSTALLATION_PARTLABEL_ESP}" fat32 1MiB ${INSTALLATION_ESP_PARTITION_SIZE}GiB || installationFailed parted -s "${INSTALLATION_DISK_BYID}" set 1 esp on || installationFailed parted -s "${INSTALLATION_DISK_BYID}" mkpart "${INSTALLATION_PARTLABEL_ROOT}" btrfs ${INSTALLATION_ESP_PARTITION_SIZE}GiB 100% || installationFailed # Informing the Kernel of the changes. sleep 0.1 partprobe "${INSTALLATION_DISK_BYID}" || installationFailed # loop until lsblk is updated and gives the partition back while sleep 0.1 ESP_PARTITION="/dev/$(lsblk "${INSTALLATION_DISK_BYID}" -o NAME,PARTLABEL | grep "${INSTALLATION_PARTLABEL_ESP}" | cut -d " " -f1 | cut -c7-)" || installationFailed ROOT_PARTITION="/dev/$(lsblk "${INSTALLATION_DISK_BYID}" -o NAME,PARTLABEL | grep "${INSTALLATION_PARTLABEL_ROOT}" | cut -d " " -f1 | cut -c7-)" || installationFailed [[ "${ESP_PARTITION}" == '/dev/' || "${ROOT_PARTITION}" == '/dev/' ]] do :; done installationSubtaskTitle "File system creation" mkfs.vfat -F 32 -n EFI "${ESP_PARTITION}" || installationFailed mkfs.btrfs -f -L ROOT "${ROOT_PARTITION}" || installationFailed installationSubtaskTitle "Create btrfs subvolumes" mount -t btrfs "${ROOT_PARTITION}" "${INSTALLATION_MOUNTPOINT}" || installationFailed btrfs sub create "${INSTALLATION_MOUNTPOINT}/@root_${START_TIMESTAMP}" || installationFailed btrfs sub create "${INSTALLATION_MOUNTPOINT}/@home" || installationFailed btrfs sub create "${INSTALLATION_MOUNTPOINT}/@podman" || installationFailed btrfs sub create "${INSTALLATION_MOUNTPOINT}/@mysql" || installationFailed umount "${INSTALLATION_MOUNTPOINT}" || installationFailed echo "" echo "┌──────────────────────────────────────────┐" echo "│ Mount filesystems │" echo "└──────────────────────────────────────────┘" installationSubtaskTitle "Mount btrfs subvolumes" mount -o noatime,nodiratime,discard=async,space_cache=v2,subvol=@root_"${START_TIMESTAMP}" "${ROOT_PARTITION}" "${INSTALLATION_MOUNTPOINT}" || installationFailed mkdir -p ${INSTALLATION_MOUNTPOINT}/{efi,home,btrfs,var/lib/mysql,opt/podman,sys/firmware/efi/efivars} || installationFailed mount -o noatime,nodiratime,discard=async,space_cache=v2,subvol=@home "${ROOT_PARTITION}" "${INSTALLATION_MOUNTPOINT}/home" || installationFailed mount -o noatime,nodiratime,discard=async,space_cache=v2,subvol=@podman "${ROOT_PARTITION}" "${INSTALLATION_MOUNTPOINT}/opt/podman" || installationFailed mount -o noatime,nodiratime,discard=async,space_cache=v2,subvol=@mysql "${ROOT_PARTITION}" "${INSTALLATION_MOUNTPOINT}/var/lib/mysql" || installationFailed mount -o noatime,nodiratime,discard=async,space_cache=v2,subvol=/ "${ROOT_PARTITION}" "${INSTALLATION_MOUNTPOINT}/btrfs" || installationFailed installationSubtaskTitle "Mount ESP" mount -o nodev,nosuid,noexec "${ESP_PARTITION}" "${INSTALLATION_MOUNTPOINT}/efi" || installationFailed echo "" echo "┌──────────────────────────────────────────┐" echo "│ Install and configure OS │" echo "└──────────────────────────────────────────┘" installationSubtaskTitle "Install base packages" wget https://raw.githubusercontent.com/alpinelinux/alpine-make-rootfs/v0.7.0/alpine-make-rootfs chmod u+x alpine-make-rootfs ./alpine-make-rootfs --no-cleanup --branch 'v'$(echo ${INSTALLATION_ALPINE_VERSION} | rev | cut -d. -f2- | rev) --packages "apk-tools alpine-base linux-lts linux-firmware-none zsh vim btrfs-progs dialog wget git mkinitfs lsblk parted lshw shadow" ${INSTALLATION_MOUNTPOINT} installationSubtaskTitle "Setup resolv.conf" if [[ -f "${INSTALLATION_MOUNTPOINT}/etc/resolv.conf" ]]; then rm -f "${INSTALLATION_MOUNTPOINT}/etc/resolv.conf" fi cp /etc/resolv.conf "${INSTALLATION_MOUNTPOINT}/etc/resolv.conf" || installationFailed installationSubtaskTitle "Setup PATH" chroot "${INSTALLATION_MOUNTPOINT}" /bin/ash -c "export PATH='/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin'" || installationFailed installationSubtaskTitle "Mount extra mounts for chroot" mount -t proc /proc "${INSTALLATION_MOUNTPOINT}/proc" || installationFailed mount -t sysfs /sys "${INSTALLATION_MOUNTPOINT}/sys" || installationFailed mount -o bind /sys/firmware/efi/efivars "${INSTALLATION_MOUNTPOINT}/sys/firmware/efi/efivars/" || __installationFailed mount -o bind /dev "${INSTALLATION_MOUNTPOINT}/dev" || installationFailed mount -o bind /run "${INSTALLATION_MOUNTPOINT}/run" || installationFailed installationSubtaskTitle "Install base-packages" chroot "${INSTALLATION_MOUNTPOINT}" /bin/ash -c "apk add alpine-base --no-cache" || installationFailed installationSubtaskTitle "Overwrite default repositories" cp /etc/apk/repositories "${INSTALLATION_MOUNTPOINT}/etc/apk/repositories" || installationFailed installationSubtaskTitle "Install SaltStack" chroot "${INSTALLATION_MOUNTPOINT}" /bin/ash -c "apk add $([[ $SALT_RUNNER == 'salt-master' ]] && echo 'salt-master salt-minion' || echo 'salt-minion' ]]) envsubst" || installationFailed installationSubtaskTitle "Setup keymap" chroot "${INSTALLATION_MOUNTPOINT}" /bin/ash -c "setup-keymap ch ch" || installationFailed installationSubtaskTitle "Setting localtime to Europe/Zurich" chroot "${INSTALLATION_MOUNTPOINT}" /bin/ash -c "setup-timezone Europe/Zurich" || installationFailed installationSubtaskTitle "Time sync" chroot "${INSTALLATION_MOUNTPOINT}" /bin/ash -c "hwclock --systohc" || installationFailed installationSubtaskTitle "Setup hostname" chroot "${INSTALLATION_MOUNTPOINT}" /bin/ash -c "echo 'nov-alp1.localhost' > /etc/hostname" || installationFailed chroot "${INSTALLATION_MOUNTPOINT}" /bin/ash -c "hostname -F /etc/hostname" || installationFailed chroot "${INSTALLATION_MOUNTPOINT}" /bin/ash -c "rc-update add hostname" || installationFailed installationSubtaskTitle "Setup hosts" cp /etc/hosts "${INSTALLATION_MOUNTPOINT}/etc/hosts" || installationFailed installationSubtaskTitle "Set root password" chroot "${INSTALLATION_MOUNTPOINT}" /bin/ash -c "echo -e \"${INSTALLATION_ROOT_PW}\n${INSTALLATION_ROOT_PW}\" | passwd" || installationFailed installationSubtaskTitle "Enable btrfs module" chroot "${INSTALLATION_MOUNTPOINT}" /bin/ash -c "echo 'btrfs' >> /etc/modules" echo "" echo "┌──────────────────────────────────────────┐" echo "│ Configure SaltStack and highstate │" echo "└──────────────────────────────────────────┘" installationSubtaskTitle "Clone Salt-Repo" mkdir -p ${INSTALLATION_MOUNTPOINT}/${INSTALLATION_SALT_ROOT} git clone ${INSTALLATION_SALT_GIT} ${INSTALLATION_MOUNTPOINT}/${INSTALLATION_SALT_ROOT} if [[ $SALT_RUNNER == "salt-master" ]]; then cat >"${INSTALLATION_MOUNTPOINT}/etc/salt/master" << EOT || installationFailed --- state_verbose: False file_client: local file_roots: base: - /srv/salt pillar_roots: base: - /srv/pillar ... EOT cat >"${INSTALLATION_MOUNTPOINT}/etc/salt/minion" <"${INSTALLATION_MOUNTPOINT}/etc/salt/minion" << EOT || installationFailed --- master: $SALT_MASTER_IP state_verbose: False file_client: remote ... EOT else cat >"${INSTALLATION_MOUNTPOINT}/etc/salt/minion" <"${INSTALLATION_MOUNTPOINT}/etc/salt/grains" <"${INSTALLATION_MOUNTPOINT}/${INSTALLATION_PILLAR_ROOT}/system/init.sls" << EOT timestamp: ${START_TIMESTAMP} EOT installationSubtaskTitle "Salt highstate" chroot "${INSTALLATION_MOUNTPOINT}" /bin/ash -c "salt-call state.highstate" || installationFailed chroot "${INSTALLATION_MOUNTPOINT}" /bin/ash -c "salt-call state.sls system.candy" || installationFailed echo "" echo "┌──────────────────────────────────────────┐" echo "│ Boot │" echo "└──────────────────────────────────────────┘" installationSubtaskTitle "Make EFI boot image with mkinitfs" chroot "${INSTALLATION_MOUNTPOINT}" /bin/ash -c "mkinitfs $(uname -r)" || installationFailed installationSubtaskTitle "Installing grub to /efi" chroot "${INSTALLATION_MOUNTPOINT}" /bin/ash -c "grub-install --target=x86_64-efi --efi-directory=/efi --bootloader-id=noveriaos" || installationFailed installationSubtaskTitle "Generating Bootmenu entries" chroot "${INSTALLATION_MOUNTPOINT}" /bin/bash -c "/usr/local/noveria/bin/noveriablcgen --noconfirm" || installationFailed echo "" echo "┌──────────────────────────────────────────┐" echo "│ Finishing │" echo "└──────────────────────────────────────────┘" #installationSubtaskTitle "Unmount" #umount -l ${INSTALLATION_MOUNTPOINT} || installationFailed installationSubtaskTitle "End of installation" # end log all output to logfile exec &>"$(tty)" # write secrets file writeInstallationSecretsToFile # remove installation lock file rm -f "$INSTALLATION_LOCK_FILE" # remove shell histories rm -f /root/.zsh_history dialog --stdout --clear --cr-wrap --no-collapse --yes-label "Reboot" --no-label "Alpine shell" --yesno "\n Installation finished" 7 50 case $? in 0) reboot ;; 1) clear exit ;; 255) clear exit ;; esac } ## # Write installation secrets to file ## writeInstallationSecretsToFile() { rm -f "$INSTALLATION_SECRETS_FILE" { echo "# Installation secrets from $START_TIMESTAMP" echo "" echo "FQDN: $(cat /mnt/etc/hostname)" echo "" echo "root_pw: ${INSTALLATION_ROOT_PW}" echo "" } >>"$INSTALLATION_SECRETS_FILE" } ## # Installation failed # - $1: comment ## installationFailed() { # log error echo -e "\n=> ERROR" if [ -n "$1" ]; then echo -e "=> Comment: $1" fi # end log all output to logfile exec &>"$(tty)" # Remove lock file rm -f $INSTALLATION_LOCK_FILE dialog --no-collapse --ok-label "Exit" --msgbox "\n Installation failed\n\nLog: ${INSTALLATION_LOGFILE} " 9 32 clear exit 1 } ### ## Script Start ### preChecks ### ## Script End ###