ISO/airootfs/root/install.sh
LinuxSquare 810c4ff1be Major Release 2412-1
* Replaced salt w. ansible
* Switched os-base to Alpine 3.21
* Reworked Installer
* * Added Installation Targets gaming,proxy,build
* * End-screen now presents options to reboot, poweroff or alpine-shell
2024-12-28 14:21:03 +01:00

529 lines
18 KiB
Bash

#!/usr/bin/env bash
###
##
## Novos 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_ANSIBLE_ROOT="srv/ansible"
readonly INSTALLATION_ANSIBLE_GIT="https://git.noveria.org/Novos/ansible-playbooks.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
###
##
# Run commands in chroot
# - $1: command
##
function runInChroot() {
chroot "${INSTALLATION_MOUNTPOINT}" /bin/ash -c "$1"
}
##
# 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/urandom | head -c "${1:-11}"
echo
}
##
# Hard exit with console error message
##
function errorHardExit() {
echo -e "${RED}$1${NC} \nAborting \n"
exit 1
}
###
## Functions
###
##
# Pre checks
##
function preChecks() {
# Check if another installation is in progress
if [ -f $INSTALLATION_LOCK_FILE ]; then
dialog --stdout --clear --no-collapse --yes-label "Exit" --no-label "Start anyway" --yesno " Another installation is in progress" 6 50
case $? in
0)
clear
exit
;;
1) rm "$INSTALLATION_LOCK_FILE" ;;
255)
clear
exit
;;
esac
fi
# Check if booted in UEFI
if [ ! -d /sys/firmware/efi ]; then
dialog --stdout --clear --no-collapse --yes-label "Shutdown" --nolabel "Alpine shell" --yesno "The ISO hasn't been booted in UEFI mode. Please boot the ISO in UEFI to proceed installation" 6 50
case $? in
0)
clear
COUNTER=5
echo "Novos is shutting down in: "
while [ 1 ]; do
if [ ${COUNTER} -eq 0 ]; then
break
fi
echo -n "${COUNTER}"
COUNTER=$( echo "${COUNTER}-1" | bc )
sleep 0.2
for (( i=0; i<4; i++ )); do
echo -n "."
sleep 0.2
done
done
poweroff
;;
1)
clear
exit 0
;;
255)
clear
exit
;;
esac
fi
introDialogue
}
##
# Introduction dialog
##
function introDialogue() {
local introtext="\n\n\n
Velkommen til
_ _\n
| \ | | _____ _____ ___\n
| \| |/ _ \ \ / / _ \/ __|\n
| |\ | (_) \ V / (_) \__ \ \n
|_| \_|\___/ \_/ \___/|___/\n\n
OS: Novos
Version: $(date "+%Y%m")
IP: $(ifconfig eth0 | grep "inet addr" | awk '{$1=$1};1' | awk '{print $2}' | cut -d: -f2)
How do you want to continue?
"
dialog --stdout --clear --cr-wrap --no-collapse --yes-label "Graphical Guide" --no-label "Alpine shell" --yesno "$introtext" 31 93
case $? in
1)
clear
exit
;;
255)
clear
exit
;;
esac
checkInternetConnection
}
##
# Internet connection
##
function checkInternetConnection() {
dialog --no-collapse --infobox "\n Check internet connection" 5 35
# wait 5 seconds to give some more time for network initialization
sleep 5
while ! ping -c 1 "$CHECK_INTERNET_URL" >/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
# hostname
INSTALLATION_HOSTNAME=$(dialog --clear --title "What's the hostname of this device?" --inputbox "Enter hostname" 10 70 3>&1 1>&2 2>&3 3>&-)
# domain
INSTALLATION_DOMAIN=$(dialog --clear --title "What's the domain of this device?" --inputbox "Enter domain (leave empty for localhost)" 10 70 3>&1 1>&2 2>&3 3>&-)
[[ -z "$INSTALLATION_DOMAIN" ]] && INSTALLATION_DOMAIN="localhost"
# installation type
INSTALLATION_TYPE=$(dialog --clear --title "Choose the main installation type of this host" --radiolist "Select one" 10 70 3 gaming Game-Server false proxy Proxy-Server false build Build-Server false 3>&1 1>&2 2>&3 3>&-)
# show summary
summary
}
##
# Summary to confirm
##
function summary() {
dialog --stdout --clear --title "Summary" --yes-label "Confirm" --no-label "Abort" --yesno "\n
Hostname: ${INSTALLATION_HOSTNAME}.${INSTALLATION_DOMAIN}\n
Type: ${INSTALLATION_TYPE}\n
Disk: ${INSTALLATION_DISK}
" 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"
runInChroot "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 -t efivarfs efivarfs "${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"
runInChroot "apk add alpine-base tzdata eudev udev-init-scripts --no-cache" || installationFailed
installationSubtaskTitle "Overwrite default repositories"
cp /etc/apk/repositories "${INSTALLATION_MOUNTPOINT}/etc/apk/repositories" || installationFailed
installationSubtaskTitle "Install Ansible"
runInChroot "apk add ansible envsubst" || installationFailed
installationSubtaskTitle "Setup keymap"
runInChroot "setup-keymap ch ch" || installationFailed
installationSubtaskTitle "Setting localtime to Europe/Zurich"
runInChroot "ln -s /usr/share/zoneinfo/Europe/Zurich /etc/localtime" || installationFailed
installationSubtaskTitle "Time sync"
runInChroot "hwclock --systohc" || installationFailed
installationSubtaskTitle "Setup hostname"
runInChroot "echo '${INSTALLATION_HOSTNAME}.${INSTALLATION_DOMAIN}' > /etc/hostname" || installationFailed
runInChroot "hostname -F /etc/hostname" || installationFailed
runInChroot "rc-update add hostname" || installationFailed
installationSubtaskTitle "Setup hosts"
cp /etc/hosts "${INSTALLATION_MOUNTPOINT}/etc/hosts" || installationFailed
installationSubtaskTitle "Set root password"
runInChroot "echo -e \"${INSTALLATION_ROOT_PW}\n${INSTALLATION_ROOT_PW}\" | passwd" || installationFailed
installationSubtaskTitle "Enable btrfs module"
runInChroot "echo 'btrfs' >> /etc/modules"
runInChroot "echo 'efivarfs' >> /etc/modules"
installationSubtaskTitle "Enable udev services"
runInChroot "rc-update add udev sysinit"
runInChroot "rc-update add udev-trigger sysinit"
runInChroot "rc-update add udev-settle sysinit"
runInChroot "rc-update add udev-postmount default"
echo ""
echo "┌──────────────────────────────────────────┐"
echo "│ Configure Ansible and playbook-run │"
echo "└──────────────────────────────────────────┘"
installationSubtaskTitle "Clone Playbook-repo"
mkdir -p ${INSTALLATION_MOUNTPOINT}/${INSTALLATION_ANSIBLE_ROOT}/playbooks
git clone ${INSTALLATION_ANSIBLE_GIT} ${INSTALLATION_MOUNTPOINT}/${INSTALLATION_ANSIBLE_ROOT}/playbooks
mkdir -p ${INSTALLATION_MOUNTPOINT}/etc/ansible
cat >"${INSTALLATION_MOUNTPOINT}/etc/ansible/ansible.cfg" <<EOT || installationFailed
[defaults]
inventory=/${INSTALLATION_ANSIBLE_ROOT}/inventory.yml
EOT
cat >"${INSTALLATION_MOUNTPOINT}/${INSTALLATION_ANSIBLE_ROOT}/inventory.yml" <<EOT
ungrouped:
hosts:
localhost
vars:
ansible_connection: local
start_timestamp: ${START_TIMESTAMP}
tmpfs_size: 4G
installation_type: ${INSTALLATION_TYPE}
mysql_root_password: $(randomPasswordGen 32)
EOT
installationSubtaskTitle "Execute Ansible playbooks"
runInChroot "ansible-playbook /${INSTALLATION_ANSIBLE_ROOT}/playbooks/top.ansible.yml" || installationFailed
echo ""
echo "┌──────────────────────────────────────────┐"
echo "│ Boot │"
echo "└──────────────────────────────────────────┘"
installationSubtaskTitle "Make EFI boot image with mkinitfs"
latest_kernel="$(chroot $INSTALLATION_MOUNTPOINT /bin/ash -c 'echo $(apk search linux-lts | head -n1 | cut -d- -f3- | sed "s|r||")-lts')"
runInChroot "mkinitfs $latest_kernel" || installationFailed
installationSubtaskTitle "Installing grub to /efi"
runInChroot "grub-install --target=x86_64-efi --efi-directory=/efi --bootloader-id=alpine" || installationFailed
installationSubtaskTitle "Generating Bootmenu entries"
chroot "${INSTALLATION_MOUNTPOINT}" /bin/bash -c "/usr/local/noveria/bin/noveriablcgen --noconfirm" || installationFailed
installationSubtaskTitle "Generating motd"
chroot "${INSTALLATION_MOUNTPOINT}" /bin/bash -c "/usr/local/noveria/bin/generate_motd" || 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
selected_option=$(dialog --output-fd 1 --menu "What would you like to do?" 10 70 5 reboot "Reboot into your newly installed system" poweroff "Shut down the current live system" alpine-shell "Switch to an interactive shell")
case "$selected_option" in
"reboot")
reboot
;;
"poweroff")
poweroff
;;
"alpine-shell")
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
###