#!/bin/bash # das-server-install - console installer for Das OS Server. # # Usage: das-server-install [--config FILE] # # Asks its questions with dialog and installs the live image on a disk: # GPT with an EFI or BIOS boot partition and Btrfs with the same subvolumes # as the desktop installer, so snapshots and rollback work the same way. # With --config it asks nothing and takes the answers from FILE (see # /usr/share/examples/das-server-installer/install.conf). set -Eeuo pipefail log=/tmp/das-server-install.log target=/mnt/das-target source_root=/run/das-installer/root libexec=/usr/libexec/das/installer # Answers; a config file sets the same variables DISK="" HOST_NAME=dasos-server USER_NAME="" USER_PASSWORD="" SSH_KEY="" SSH_PASSWORDS=yes TIMEZONE=Europe/Kyiv KEYMAP=us LOCALE=en_US.UTF-8 NETWORK=dhcpcd ERASE_DISK=no lang=en [[ ${LANG:-} == uk* ]] && lang=uk # t KEY: the message in the installer's language t() { local key=$1 if [[ $lang == uk ]]; then case $key in title) echo "Встановлення Das OS Server" ;; lang) echo "Мова встановлення:" ;; disk) echo "Диск для встановлення. Усі дані на ньому буде знищено." ;; nodisk) echo "Не знайдено дисків для встановлення." ;; host) echo "Ім'я сервера (латинські літери, цифри, дефіс):" ;; badhost) echo "Неправильне ім'я сервера." ;; user) echo "Ім'я користувача-адміністратора (вхід через sudo, root вимкнено):" ;; baduser) echo "Неправильне ім'я: малі латинські літери, цифри, _ та -, не root." ;; pass) echo "Пароль користувача:" ;; pass2) echo "Ще раз той самий пароль:" ;; badpass) echo "Паролі порожні або не збігаються." ;; key) echo "Публічний ключ SSH для входу (вставте рядок ssh-ed25519 ... або залиште порожнім):" ;; badkey) echo "Це не схоже на публічний ключ SSH." ;; nopass) echo "Заборонити вхід по SSH за паролем (лише за ключем)?" ;; tz) echo "Часовий пояс (наприклад, Europe/Kyiv, UTC):" ;; badtz) echo "Немає такого часового поясу." ;; keymap) echo "Розкладка клавіатури в консолі:" ;; locale) echo "Мова системи:" ;; net) echo "Мережа:" ;; net_dhcpcd) echo "dhcpcd: DHCP на всіх інтерфейсах (просто)" ;; net_nm) echo "NetworkManager: налаштування через nmtui (Wi-Fi, статичні адреси)" ;; summary) echo "Перевірте налаштування:" ;; erase) echo "УСІ ДАНІ НА ДИСКУ %s БУДЕ ЗНИЩЕНО. Продовжити?" ;; cancel) echo "Встановлення скасовано." ;; s_source) echo "Підготовка образу системи..." ;; s_disk) echo "Розмітка диска %s..." ;; s_copy) echo "Копіювання системи (кілька хвилин)..." ;; s_config) echo "Налаштування системи..." ;; s_post) echo "Локалі, знімки, initramfs (кілька хвилин)..." ;; s_boot) echo "Встановлення завантажувача..." ;; finished) echo "Das OS Server встановлено. Вийміть інсталяційний носій і перезавантажте сервер.\n\nВхід: %s, далі sudo. Служби: dsv. Користувачі: duser. Порти: dfw. Налаштування: dsys. Оновлення: das-update. Профіль розробника: dprofile install dev. Запрошення терміналу: dprompt. Банер при вході: dbanner." ;; reboot) echo "Перезавантажити зараз?" ;; failed) echo "Помилка встановлення. Журнал: %s" ;; root) echo "Інсталятор треба запускати від root (sudo das-server-install)." ;; yes) echo "так" ;; no) echo "ні" ;; esac else case $key in title) echo "Das OS Server installation" ;; lang) echo "Installation language:" ;; disk) echo "Disk to install to. All data on it will be erased." ;; nodisk) echo "No disks to install to were found." ;; host) echo "Server name (Latin letters, digits, hyphen):" ;; badhost) echo "Invalid server name." ;; user) echo "Administrator user name (uses sudo, root login is disabled):" ;; baduser) echo "Invalid name: lowercase Latin letters, digits, _ and -, not root." ;; pass) echo "User password:" ;; pass2) echo "The same password again:" ;; badpass) echo "The passwords are empty or do not match." ;; key) echo "SSH public key to log in with (paste the ssh-ed25519 ... line, or leave empty):" ;; badkey) echo "This does not look like an SSH public key." ;; nopass) echo "Disable SSH password login (keys only)?" ;; tz) echo "Time zone (for example Europe/Kyiv, UTC):" ;; badtz) echo "No such time zone." ;; keymap) echo "Console keyboard layout:" ;; locale) echo "System language:" ;; net) echo "Network:" ;; net_dhcpcd) echo "dhcpcd: DHCP on all interfaces (simple)" ;; net_nm) echo "NetworkManager: set up with nmtui (Wi-Fi, static addresses)" ;; summary) echo "Check the settings:" ;; erase) echo "ALL DATA ON %s WILL BE ERASED. Continue?" ;; cancel) echo "Installation cancelled." ;; s_source) echo "Preparing the system image..." ;; s_disk) echo "Partitioning %s..." ;; s_copy) echo "Copying the system (a few minutes)..." ;; s_config) echo "Configuring the system..." ;; s_post) echo "Locales, snapshots, initramfs (a few minutes)..." ;; s_boot) echo "Installing the boot loader..." ;; finished) echo "Das OS Server is installed. Remove the installation medium and reboot.\n\nLog in as %s, then use sudo. Services: dsv. Users: duser. Ports: dfw. Settings: dsys. Updates: das-update. Developer profile: dprofile install dev. Terminal prompt: dprompt. Login banner: dbanner." ;; reboot) echo "Reboot now?" ;; failed) echo "Installation failed. Log: %s" ;; root) echo "The installer must run as root (sudo das-server-install)." ;; yes) echo "yes" ;; no) echo "no" ;; esac fi } interactive=1 dialog_ui() { local labels=() if [[ $lang == uk ]]; then labels=(--ok-label "Гаразд" --cancel-label "Скасувати" --yes-label "Так" --no-label "Ні") fi dialog --backtitle "Das OS Server" --title "$(t title)" "${labels[@]}" "$@" 3>&1 1>&2 2>&3 } # Shows a step in the dialog, or prints it when installing from a config file step() { if [[ -n $interactive ]]; then dialog --backtitle "Das OS Server" --title "$(t title)" --infobox "$1" 5 70 echo "==> $1" >>"$log" else echo "==> $1" | tee -a "$log" fi } fail() { trap - ERR local msg # shellcheck disable=SC2059 msg=$(printf "$(t failed)" "$log") if [[ -n $interactive ]]; then dialog --backtitle "Das OS Server" --title "$(t title)" --msgbox "$msg\n\n$(tail -n 12 "$log" | sed 's/\\/\\\\/g')" 22 76 || true clear fi echo "das-server-install: $msg" >&2 exit 1 } cancelled() { [[ -n $interactive ]] && clear t cancel exit 1 } # --- Questions ------------------------------------------------------------- # Disks that can be installed to: not read-only, not the live medium list_disks() { local live="" dev if dev=$(blkid -L VOID_LIVE 2>/dev/null); then live=$(lsblk -no PKNAME "$dev" 2>/dev/null | head -n 1) [[ -n $live ]] && live=/dev/$live || live=$dev fi lsblk -dpno NAME,TYPE,RO,SIZE,MODEL | while read -r name type ro size model; do [[ $type == disk && $ro == 0 && $name != "$live" ]] || continue [[ $name == /dev/zram* || $name == /dev/loop* ]] && continue printf '%s\t%s %s\n' "$name" "$size" "${model:-}" done } ask_language() { local choice choice=$(dialog --backtitle "Das OS Server" --title "Das OS Server" \ --default-item "$lang" --menu "Language / Мова:" 10 50 2 \ en "English" uk "Українська" 3>&1 1>&2 2>&3) || cancelled lang=$choice if [[ $lang == uk ]]; then LOCALE=uk_UA.UTF-8 # The default console font has no Cyrillic (a font needs a Unicode table, # .psfu); there is no font to set on serial or ssh case $(tty) in /dev/tty[0-9]*) setfont cyr-sun16 2>/dev/null || true ;; esac fi } ask_disk() { local items=() name desc while IFS=$'\t' read -r name desc; do items+=("$name" "$desc") done < <(list_disks) if [[ ${#items[@]} -eq 0 ]]; then dialog_ui --msgbox "$(t nodisk)" 7 60 || true cancelled fi DISK=$(dialog_ui --menu "$(t disk)" 15 70 6 "${items[@]}") || cancelled } valid_host() { [[ $1 =~ ^[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?$ ]] } valid_user() { [[ $1 =~ ^[a-z_][a-z0-9_-]{0,31}$ ]] || return 1 case $1 in root | live | nobody | daemon | bin | sys) return 1 ;; esac } valid_key() { [[ -z $1 || $1 =~ ^(ssh-(ed25519|rsa|dss)|ecdsa-sha2-[a-z0-9]+|sk-[a-z0-9@.-]+)\ [A-Za-z0-9+/=]+ ]] } ask_host() { while :; do HOST_NAME=$(dialog_ui --inputbox "$(t host)" 8 60 "$HOST_NAME") || cancelled valid_host "$HOST_NAME" && return dialog_ui --msgbox "$(t badhost)" 6 50 || true done } ask_user() { local p1 p2 while :; do USER_NAME=$(dialog_ui --inputbox "$(t user)" 9 60 "$USER_NAME") || cancelled valid_user "$USER_NAME" && break dialog_ui --msgbox "$(t baduser)" 7 60 || true done while :; do p1=$(dialog_ui --insecure --passwordbox "$(t pass)" 8 60) || cancelled p2=$(dialog_ui --insecure --passwordbox "$(t pass2)" 8 60) || cancelled if [[ -n $p1 && $p1 == "$p2" ]]; then USER_PASSWORD=$p1 return fi dialog_ui --msgbox "$(t badpass)" 6 50 || true done } ask_ssh() { while :; do SSH_KEY=$(dialog_ui --inputbox "$(t key)" 10 76 "$SSH_KEY") || cancelled valid_key "$SSH_KEY" && break dialog_ui --msgbox "$(t badkey)" 6 50 || true done SSH_PASSWORDS=yes if [[ -n $SSH_KEY ]] && dialog_ui --yesno "$(t nopass)" 7 60; then SSH_PASSWORDS=no fi } ask_system() { while :; do TIMEZONE=$(dialog_ui --inputbox "$(t tz)" 8 60 "$TIMEZONE") || cancelled [[ -f /usr/share/zoneinfo/$TIMEZONE ]] && break dialog_ui --msgbox "$(t badtz)" 6 50 || true done KEYMAP=$(dialog_ui --default-item "$KEYMAP" --menu "$(t keymap)" 15 50 7 \ us "English (US)" ua-utf "Українська" uk "English (UK)" de-latin1 "Deutsch" \ fr-latin1 "Français" pl2 "Polski") || cancelled LOCALE=$(dialog_ui --default-item "$LOCALE" --menu "$(t locale)" 10 50 2 \ en_US.UTF-8 "English" uk_UA.UTF-8 "Українська") || cancelled NETWORK=$(dialog_ui --default-item "$NETWORK" --menu "$(t net)" 10 76 2 \ dhcpcd "$(t net_dhcpcd)" NetworkManager "$(t net_nm)") || cancelled } confirm() { local key pw msg key=$(t no) pw=$(t yes) [[ -n $SSH_KEY ]] && key=$(t yes) [[ $SSH_PASSWORDS == no ]] && pw=$(t no) dialog_ui --no-collapse --msgbox "$(t summary) Disk: $DISK Host: $HOST_NAME User: $USER_NAME SSH key: $key SSH passwords: $pw Time zone: $TIMEZONE Keyboard: $KEYMAP Language: $LOCALE Network: $NETWORK" 18 60 || cancelled # shellcheck disable=SC2059 msg=$(printf "$(t erase)" "$DISK") dialog_ui --defaultno --yesno "$msg" 7 60 || cancelled ERASE_DISK=yes } # --- Installation ---------------------------------------------------------- # part N: the device of partition N on $DISK part() { lsblk -lnpo NAME,TYPE "$DISK" | awk '$2 == "part" { print $1 }' | sed -n "${1}p" } in_target() { chroot "$target" "$@" } # Undo mounts from an earlier attempt or a failure unmount_target() { if mountpoint -q "$target"; then umount -R "$target" || umount -Rl "$target" fi } uefi() { [[ -d /sys/firmware/efi ]] } serial_console() { grep -q 'console=ttyS0' /proc/cmdline } partition() { local efi root unmount_target wipefs -a "$DISK" sgdisk --zap-all "$DISK" if uefi; then sgdisk -n 1:0:+512M -t 1:ef00 -c 1:EFI -n 2:0:0 -t 2:8300 -c 2:dasos "$DISK" else # GRUB on BIOS keeps its core image in a small partition on GPT sgdisk -n 1:0:+1M -t 1:ef02 -c 1:bios -n 2:0:0 -t 2:8300 -c 2:dasos "$DISK" fi partprobe "$DISK" 2>/dev/null || true udevadm settle efi=$(part 1) root=$(part 2) [[ -b $root ]] || return 1 uefi && mkfs.vfat -F 32 -n EFI "$efi" mkfs.btrfs -f -L dasos "$root" # The same subvolumes as the Calamares configuration (mount.conf) mkdir -p "$target" mount "$root" "$target" for sv in @ @home @snapshots @var_log @var_cache_xbps; do btrfs subvolume create "$target/$sv" done umount "$target" local opts=noatime,compress=zstd:1 mount -o "$opts,subvol=@" "$root" "$target" mkdir -p "$target"/{home,.snapshots,var/log,var/cache/xbps} mount -o "$opts,subvol=@home" "$root" "$target/home" mount -o "$opts,subvol=@snapshots" "$root" "$target/.snapshots" mount -o "$opts,subvol=@var_log" "$root" "$target/var/log" mount -o "$opts,subvol=@var_cache_xbps" "$root" "$target/var/cache/xbps" if uefi; then mkdir -p "$target/boot/efi" mount -o umask=0077 "$efi" "$target/boot/efi" fi } copy_system() { # No --sparse: GRUB can't read kernels with Btrfs holes (see postinstall) rsync -aHAX --numeric-ids "$source_root/" "$target/" } write_fstab() { local root_uuid efi_uuid opts=noatime,compress=zstd:1 root_uuid=$(blkid -s UUID -o value "$(part 2)") { echo "# " echo "UUID=$root_uuid / btrfs $opts,subvol=@ 0 0" echo "UUID=$root_uuid /home btrfs $opts,subvol=@home 0 0" echo "UUID=$root_uuid /.snapshots btrfs $opts,subvol=@snapshots 0 0" echo "UUID=$root_uuid /var/log btrfs $opts,subvol=@var_log 0 0" echo "UUID=$root_uuid /var/cache/xbps btrfs $opts,subvol=@var_cache_xbps 0 0" if uefi; then efi_uuid=$(blkid -s UUID -o value "$(part 1)") echo "UUID=$efi_uuid /boot/efi vfat defaults,umask=0077 0 2" fi } >"$target/etc/fstab" } mount_chroot() { mount -t proc proc "$target/proc" mount -t sysfs sys "$target/sys" mount --rbind /dev "$target/dev" mount -t tmpfs tmpfs "$target/run" if uefi; then mount -t efivarfs efivarfs "$target/sys/firmware/efi/efivars" 2>/dev/null || true fi } # set_rc VAR VALUE: sets VAR in /etc/rc.conf, uncommenting Void's example line set_rc() { local rc=$target/etc/rc.conf if grep -q "^#\?$1=" "$rc"; then sed -i "s|^#\?$1=.*|$1=\"$2\"|" "$rc" else echo "$1=\"$2\"" >>"$rc" fi } enable_service() { ln -sfn "/etc/sv/$1" "$target/etc/runit/runsvdir/default/$1" } disable_service() { rm -f "$target/etc/runit/runsvdir/default/$1" } configure() { local home # The live image's /etc/issue tells how to log in to the live system printf 'Das OS Server \\r (\\l)\n\n' >"$target/etc/issue" echo "$HOST_NAME" >"$target/etc/hostname" if ! grep -q "[[:space:]]$HOST_NAME\$" "$target/etc/hosts"; then printf '127.0.1.1\t%s\n' "$HOST_NAME" >>"$target/etc/hosts" fi echo "LANG=$LOCALE" >"$target/etc/locale.conf" ln -sfn "/usr/share/zoneinfo/$TIMEZONE" "$target/etc/localtime" set_rc KEYMAP "$KEYMAP" set_rc HARDWARECLOCK UTC [[ $LOCALE == uk* || $KEYMAP == ua* ]] && set_rc FONT cyr-sun16 # The administrator: sudo through wheel, root has no password in_target useradd -m -s /bin/bash -G wheel,docker,socklog "$USER_NAME" # -c: Void's PAM stack for chpasswd only has pam_permit for passwords, so # without it nothing is written (Void's own installer does the same) printf '%s:%s\n' "$USER_NAME" "$USER_PASSWORD" | in_target chpasswd -c SHA512 in_target passwd -l root >/dev/null echo "%wheel ALL=(ALL:ALL) ALL" >"$target/etc/sudoers.d/10-wheel" chmod 440 "$target/etc/sudoers.d/10-wheel" if [[ -n $SSH_KEY ]]; then home=$target/home/$USER_NAME install -d -m 700 "$home/.ssh" echo "$SSH_KEY" >"$home/.ssh/authorized_keys" chmod 600 "$home/.ssh/authorized_keys" in_target chown -R "$USER_NAME:$USER_NAME" "/home/$USER_NAME/.ssh" fi if [[ $SSH_PASSWORDS == no ]]; then printf '%s\n' "# Written by das-server-install: SSH keys only" \ "PasswordAuthentication no" "KbdInteractiveAuthentication no" \ >"$target/etc/ssh/sshd_config.d/40-das-keys-only.conf" fi for sv in sshd chronyd nftables fail2ban docker socklog-unix nanoklogd; do enable_service "$sv" done if [[ $NETWORK == NetworkManager ]]; then disable_service dhcpcd enable_service dbus enable_service NetworkManager else enable_service dhcpcd fi serial_console && enable_service agetty-ttyS0 return 0 } install_bootloader() { local grub=$target/etc/default/grub cmdline="loglevel=4" serial_console && cmdline+=" console=tty0 console=ttyS0,115200" sed -i -e '/^GRUB_DISTRIBUTOR=/d' -e '/^GRUB_CMDLINE_LINUX_DEFAULT=/d' -e '/^GRUB_TIMEOUT=/d' \ -e '/^GRUB_TERMINAL/d' -e '/^GRUB_SERIAL_COMMAND=/d' -e '/^GRUB_DISABLE_OS_PROBER=/d' "$grub" { echo 'GRUB_DISTRIBUTOR="Das OS"' echo "GRUB_CMDLINE_LINUX_DEFAULT=\"$cmdline\"" echo 'GRUB_TIMEOUT=5' echo 'GRUB_DISABLE_OS_PROBER=true' if serial_console; then echo 'GRUB_TERMINAL="console serial"' echo 'GRUB_SERIAL_COMMAND="serial --unit=0 --speed=115200"' fi } >>"$grub" if uefi; then in_target grub-install --target=x86_64-efi --efi-directory=/boot/efi --bootloader-id=dasos # Also at the fallback path, for firmware that forgets boot entries in_target grub-install --target=x86_64-efi --efi-directory=/boot/efi --removable --no-nvram else in_target grub-install --target=i386-pc "$DISK" fi } finish() { # The first snapshot, also shown in the boot menu (like Calamares' shellprocess@snapshot) in_target snapper --no-dbus -c root create -d "Fresh install" -c number --userdata important=yes || true in_target grub-mkconfig -o /boot/grub/grub.cfg # The installer is not needed on the installed system in_target xbps-remove -y das-server-installer das-install-common || true sync } # Any failing command stops the installation with the log (set -E: also # inside functions; "f || fail" would switch set -e off within f). install_system() { : >"$log" trap fail ERR step "$(t s_source)" "$libexec/mount-source" >>"$log" 2>&1 # shellcheck disable=SC2059 step "$(printf "$(t s_disk)" "$DISK")" partition >>"$log" 2>&1 step "$(t s_copy)" copy_system >>"$log" 2>&1 step "$(t s_config)" write_fstab >>"$log" 2>&1 mount_chroot >>"$log" 2>&1 configure >>"$log" 2>&1 step "$(t s_post)" in_target "$libexec/postinstall" >>"$log" 2>&1 step "$(t s_boot)" install_bootloader >>"$log" 2>&1 finish >>"$log" 2>&1 unmount_target >>"$log" 2>&1 trap - ERR } load_config() { local file=$1 [[ -r $file ]] || { echo "das-server-install: cannot read $file" >&2 exit 1 } # shellcheck source=/dev/null . "$file" interactive="" local bad="" [[ -b $DISK ]] || bad+=" DISK" valid_host "$HOST_NAME" || bad+=" HOST_NAME" valid_user "$USER_NAME" || bad+=" USER_NAME" [[ -n $USER_PASSWORD ]] || bad+=" USER_PASSWORD" valid_key "$SSH_KEY" || bad+=" SSH_KEY" [[ $SSH_PASSWORDS == yes || ( $SSH_PASSWORDS == no && -n $SSH_KEY ) ]] || bad+=" SSH_PASSWORDS" [[ -f /usr/share/zoneinfo/$TIMEZONE ]] || bad+=" TIMEZONE" [[ $NETWORK == dhcpcd || $NETWORK == NetworkManager ]] || bad+=" NETWORK" [[ $ERASE_DISK == yes ]] || bad+=" ERASE_DISK" if [[ -n $bad ]]; then echo "das-server-install: invalid or missing in $file:$bad" >&2 exit 1 fi } main() { if [[ $EUID -ne 0 ]]; then exec sudo --preserve-env=LANG "$0" "$@" fi case ${1:-} in --config) load_config "${2:?--config needs a file}" ;; -h | --help) sed -n '2,11s/^# \{0,1\}//p' "$0" exit 0 ;; "") ;; *) echo "das-server-install: unknown option $1" >&2 exit 1 ;; esac if [[ -n $interactive ]]; then ask_language ask_disk ask_host ask_user ask_ssh ask_system confirm fi install_system if [[ -n $interactive ]]; then local msg # shellcheck disable=SC2059 msg=$(printf "$(t finished)" "$USER_NAME") if dialog_ui --yesno "$msg\n\n$(t reboot)" 12 70; then reboot fi clear else echo "==> done, log: $log" fi } main "$@"