diff options
| author | Craig Jennings <c@cjennings.net> | 2026-01-17 17:55:20 -0600 |
|---|---|---|
| committer | Craig Jennings <c@cjennings.net> | 2026-01-17 17:55:20 -0600 |
| commit | e13933a47ed9814927f46d24ef58c969f2e4d0ac (patch) | |
| tree | b85ebbad730a5c2cf978cc73a64df46e58d0f280 /archsetup | |
| parent | 91c8e32100b1062529451cc5466aca669f31fd6c (diff) | |
feat(archsetup): ZFS/sanoid support, gvfs-smb, bug fixes
- Add ZFS detection with sanoid/syncoid for snapshot management
- Add gvfs-smb for Thunar SMB network browsing
- Fix shell quoting throughout script
- Fix stale $action variables in error handlers
- Fix display() return values (was returning 1)
- Fix mkinitcpio.conf sed pattern to be flexible
- Fix vconsole.conf duplicate entries on re-run
- Fix systemd unit overrides using drop-in files
- Fix ufw port typo (55353 -> 5353)
- Fix GRUB_RECORDFAIL_TIMEOUT undefined variable
- Enable NetworkManager service
- Move thunar, libvips, isync to pacman (now in official repos)
- Clean up reflector config with heredoc
- Remove unnecessary sudo when already root
- Convert shebang from sh to bash
Diffstat (limited to 'archsetup')
| -rwxr-xr-x | archsetup | 350 |
1 files changed, 280 insertions, 70 deletions
@@ -1,4 +1,4 @@ -#!/bin/sh +#!/bin/bash # ArchSetup - Craig Jennings <craigmartinjennings@gmail.com> # License: GNU GPLv3 @@ -250,14 +250,15 @@ display () { case "$1" in "title") printf "\n##### %s\n" "$2" | tee -a "$logfile" - return 1; + return 0; ;; "subtitle") printf "\n%s\n" "$2" | tee -a "$logfile" + return 0; ;; "task") printf "...%s @ %s\n" "$2" "$(date +'%T')" | tee -a "$logfile" - return 1; + return 0; ;; *) printf "CRASH: display () called with incorrect arguments.\n" @@ -322,6 +323,12 @@ pip_install() { error "error" "$action" "$?" } +# Filesystem Detection +is_zfs_root() { + # Returns 0 (true) if root filesystem is ZFS, 1 (false) otherwise + [ "$(findmnt -n -o FSTYPE /)" = "zfs" ] +} + ### Prerequisites prerequisites() { # why these software packages are 'required' @@ -383,18 +390,17 @@ prerequisites() { pacman_install reflector action="configuring reflector" && display "task" "$action" - (printf ' - --connection-timeout 3 \ - --download-timeout 3 \ - --protocol https \ - --age 12 \ - --latest 20 \ - --score 10 \ - --fastest 5 \ - --sort score \ - --save /etc/pacman.d/mirrorlist - ' > /etc/xdg/reflector/reflector.conf >> "$logfile" 2>&1) || \ - error "error" "$action" "$?" + cat << 'EOF' > /etc/xdg/reflector/reflector.conf +--connection-timeout 3 +--download-timeout 3 +--protocol https +--age 12 +--latest 20 +--score 10 +--fastest 5 +--sort score +--save /etc/pacman.d/mirrorlist +EOF # Skip running reflector during installation - it can cause SSH disconnections during testing # The reflector.timer will update mirrors automatically later @@ -417,7 +423,7 @@ prerequisites() { [ -f /etc/sudoers.pacnew ] && cp /etc/sudoers.pacnew /etc/sudoers >> "$logfile" 2>&1 action="creating a directory to build/install software from git/AUR." - (mkdir -p $source_dir) || error "crash" "creating the directory $source_dir" + (mkdir -p "$source_dir") || error "crash" "creating the directory $source_dir" } @@ -453,7 +459,7 @@ create_user () { || error "error" "$action" "$?" # mount as ramdisk to speed aur/git build/installs - (sudo mount -t tmpfs -o size=4G archsetup $source_dir >> "$logfile" 2>&1) || \ + (mount -t tmpfs -o size=4G archsetup "$source_dir" >> "$logfile" 2>&1) || \ error "crash" "mounting the RAM disk for archsetup" "$?" (chown -R "$username":wheel "$(dirname "$source_dir")" >> "$logfile" 2>&1) || \ @@ -478,7 +484,7 @@ user_customizations() { dotfiles_dir="$user_archsetup_dir/dotfiles" action="linking dotfiles into place" && display "task" "$action" - (cd $dotfiles_dir && stow --target=/home/$username --no-folding --adopt system \ + (cd "$dotfiles_dir" && stow --target="/home/$username" --no-folding --adopt system \ >> "$logfile" 2>&1 ) || error "error" "$action" "$?" action="restoring dotfile versions" && display "task" "$action" @@ -538,18 +544,23 @@ essential_services() { display "subtitle" "Randomness" pacman_install rng-tools + action="enabling rngd service" && display "task" "$action" systemctl enable rngd >> "$logfile" 2>&1 || error "error" "$action" "$?" + action="starting rngd service" && display "task" "$action" systemctl start rngd >> "$logfile" 2>&1 || error "error" "$action" "$?" # Networking display "subtitle" "Networking" pacman_install networkmanager + action="enabling NetworkManager" && display "task" "$action" + systemctl enable NetworkManager.service >> "$logfile" 2>&1 || error "error" "$action" "$?" # Power display "subtitle" "Power" pacman_install upower + action="enabling upower service" && display "task" "$action" systemctl enable upower >> "$logfile" 2>&1 || error "error" "$action" "$?" # Secure Shell @@ -586,7 +597,7 @@ essential_services() { "80,443,8080/tcp" \ "9040,9050,9051,9053,9119/tcp" \ "IMAP" "IMAPS" \ - "55353/udp" \ + "5353/udp" \ "ssh" \ "22000/tcp" "22000/udp" "21027/udp" \ "42001/tcp" "42001/udp" \ @@ -595,13 +606,13 @@ essential_services() { "transmission" \ ; do action="adding ufw rule to allow $protocol" && display "task" "$action" - (ufw allow $protocol >> "$logfile" 2>&1) || error "error" "$action" "$?" + (ufw allow "$protocol" >> "$logfile" 2>&1) || error "error" "$action" "$?" done action="adding limits to protect from brute force attacks" && display "task" "$action" (ufw limit 22/tcp >> "$logfile" 2>&1 && \ ufw limit 443/tcp >> "$logfile" 2>&1) || \ - error "error" "action" + error "error" "$action" "$?" action="enabling firewall service to launch on boot" && display "task" "$action" systemctl enable ufw.service >> "$logfile" 2>&1 || error "error" "$action" "$?" @@ -640,25 +651,213 @@ essential_services() { action="enabling the package cache cleanup timer" && display "task" "$action" systemctl enable --now paccache.timer >> "$logfile" 2>&1 || error "error" "$action" "$?" - # Timeshift-Autosnap + # Snapshot Service - filesystem-aware display "subtitle" "Snapshot Service" - aur_install timeshift-autosnap - pacman_install grub-btrfs - action="enabling snapshot boot menu updates" && display "task" "$action" - systemctl enable grub-btrfsd >> "$logfile" 2>&1 || error "error" "$action" "$?" - - action="starting snapshot boot menu updates" && display "task" "$action" - # starting and stopping service to generate the grub-btrfs config - systemctl start grub-btrfsd >> "$logfile" 2>&1 || error "error" "$action" "$?" - systemctl stop grub-btrfsd >> "$logfile" 2>&1 || error "error" "$action" "$?" - # edit grub-btrfs config for timeshift auto snapshot support - sed -i \ - 's|ExecStart=/usr/bin/grub-btrfsd --syslog /.snapshots|ExecStart=/usr/bin/grub-btrfsd --syslog --timeshift-auto|' \ - /etc/systemd/system/grub-btrfsd.service - - - action="regenerating boot menu" && display "task" "$action" - grub-mkconfig -o /boot/grub/grub.cfg >> "$logfile" 2>&1 || error "error" "$action" "$?" + + if is_zfs_root; then + # ZFS: Install sanoid for snapshot management + display "task" "ZFS detected - installing sanoid" + aur_install sanoid + + action="creating sanoid configuration" && display "task" "$action" + mkdir -p /etc/sanoid + cat << 'EOF' > /etc/sanoid/sanoid.conf +# Sanoid configuration for ZFS snapshots +# Less aggressive - TrueNAS handles long-term backups + +############################# +# Templates +############################# + +[template_production] + # Local rollback capability + hourly = 6 + daily = 7 + weekly = 2 + monthly = 1 + autosnap = yes + autoprune = yes + +[template_backup] + # Less frequent for large/static data + hourly = 0 + daily = 3 + weekly = 2 + monthly = 1 + autosnap = yes + autoprune = yes + +[template_none] + autosnap = no + autoprune = yes + +############################# +# Datasets +############################# + +[zroot/ROOT/default] + use_template = production + +[zroot/home] + use_template = production + recursive = yes + +[zroot/media] + use_template = backup + +[zroot/vms] + use_template = backup + +[zroot/var/log] + use_template = production + +[zroot/var/lib/pacman] + use_template = production + +[zroot/var/cache] + use_template = none + +[zroot/var/tmp] + use_template = none + +[zroot/tmp] + use_template = none +EOF + + action="creating zfs-replicate script" && display "task" "$action" + cat << 'EOF' > /usr/local/bin/zfs-replicate +#!/bin/bash +# zfs-replicate - Replicate ZFS datasets to TrueNAS +# +# Usage: +# zfs-replicate # Replicate all configured datasets +# zfs-replicate [dataset] # Replicate specific dataset + +set -e + +# TrueNAS Configuration +# Try local network first, fall back to tailscale +TRUENAS_LOCAL="truenas.local" +TRUENAS_TAILSCALE="truenas" +TRUENAS_USER="root" +TRUENAS_POOL="vault" +BACKUP_PATH="backups" # TODO: Configure actual path + +# Datasets to replicate +DATASETS="zroot/ROOT/default zroot/home zroot/media zroot/vms" + +# Colors +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +RED='\033[0;31m' +NC='\033[0m' + +info() { echo -e "${GREEN}[INFO]${NC} $1"; } +warn() { echo -e "${YELLOW}[WARN]${NC} $1"; } +error() { echo -e "${RED}[ERROR]${NC} $1"; exit 1; } + +command -v syncoid >/dev/null 2>&1 || error "syncoid not found. Install sanoid package." + +# Determine which host to use +determine_host() { + if ping -c 1 -W 2 "$TRUENAS_LOCAL" &>/dev/null; then + echo "$TRUENAS_LOCAL" + elif ping -c 1 -W 2 "$TRUENAS_TAILSCALE" &>/dev/null; then + echo "$TRUENAS_TAILSCALE" + else + error "Cannot reach TrueNAS at $TRUENAS_LOCAL or $TRUENAS_TAILSCALE" + fi +} + +TRUENAS_HOST=$(determine_host) +info "Using TrueNAS host: $TRUENAS_HOST" + +# Single dataset mode +if [[ -n "$1" ]]; then + dataset="$1" + dest="$TRUENAS_USER@$TRUENAS_HOST:$TRUENAS_POOL/$BACKUP_PATH/${dataset#zroot/}" + info "Replicating $dataset -> $dest" + syncoid --recursive "$dataset" "$dest" + exit 0 +fi + +# Full replication +info "Starting ZFS replication to $TRUENAS_HOST" +echo "" + +for dataset in $DATASETS; do + dest="$TRUENAS_USER@$TRUENAS_HOST:$TRUENAS_POOL/$BACKUP_PATH/${dataset#zroot/}" + info "Replicating $dataset -> $dest" + + if syncoid --recursive "$dataset" "$dest"; then + info " Success" + else + warn " Failed (will retry next run)" + fi + echo "" +done + +info "Replication complete." +EOF + chmod +x /usr/local/bin/zfs-replicate + + action="creating zfs-replicate systemd service" && display "task" "$action" + cat << 'EOF' > /etc/systemd/system/zfs-replicate.service +[Unit] +Description=ZFS Replication to TrueNAS +After=network-online.target +Wants=network-online.target + +[Service] +Type=oneshot +ExecStart=/usr/local/bin/zfs-replicate +User=root + +[Install] +WantedBy=multi-user.target +EOF + + action="creating zfs-replicate systemd timer" && display "task" "$action" + cat << 'EOF' > /etc/systemd/system/zfs-replicate.timer +[Unit] +Description=Run ZFS replication nightly + +[Timer] +OnCalendar=*-*-* 02:00:00 +RandomizedDelaySec=1800 +Persistent=true + +[Install] +WantedBy=timers.target +EOF + + action="enabling sanoid timer" && display "task" "$action" + systemctl enable sanoid.timer >> "$logfile" 2>&1 || error "error" "$action" "$?" + + # Note: zfs-replicate.timer is NOT enabled automatically + # User must set up SSH key auth to TrueNAS first, then run: + # systemctl enable --now zfs-replicate.timer + display "task" "zfs-replicate timer created (enable after SSH key setup to TrueNAS)" + + else + # Non-ZFS (btrfs): Install timeshift-autosnap + aur_install timeshift-autosnap + pacman_install grub-btrfs + action="enabling snapshot boot menu updates" && display "task" "$action" + systemctl enable grub-btrfsd >> "$logfile" 2>&1 || error "error" "$action" "$?" + + action="starting snapshot boot menu updates" && display "task" "$action" + # starting and stopping service to generate the grub-btrfs config + systemctl start grub-btrfsd >> "$logfile" 2>&1 || error "error" "$action" "$?" + systemctl stop grub-btrfsd >> "$logfile" 2>&1 || error "error" "$action" "$?" + # edit grub-btrfs config for timeshift auto snapshot support + sed -i \ + 's|ExecStart=/usr/bin/grub-btrfsd --syslog /.snapshots|ExecStart=/usr/bin/grub-btrfsd --syslog --timeshift-auto|' \ + /etc/systemd/system/grub-btrfsd.service + + action="regenerating boot menu" && display "task" "$action" + grub-mkconfig -o /boot/grub/grub.cfg >> "$logfile" 2>&1 || error "error" "$action" "$?" + fi } ### Xorg Display Manager @@ -673,7 +872,7 @@ xorg() { xorg-xdpyinfo xorg-xprop \ xorg-xwininfo xorg-xhost \ xorg-xinput xorg-xkill ; do - pacman_install $software + pacman_install "$software" done # disallow vt switching or zapping the xorg server to bypass screen lock @@ -692,15 +891,15 @@ dwm() { action="DWM Window Manager Dependencies" && display "subtitle" "$action" for software in coreutils fontconfig freetype2 glibc libx11 libxft libxinerama; do - pacman_install $software + pacman_install "$software" done; action="DWM Window Manager" && display "subtitle" "$action" - git_install $dwm_repo - git_install $dmenu_repo - git_install $st_repo - git_install $slock_repo + git_install "$dwm_repo" + git_install "$dmenu_repo" + git_install "$st_repo" + git_install "$slock_repo" aur_install pinentry-dmenu # password entry that leverages dmenu } @@ -776,11 +975,11 @@ desktop_environment() { action="Audio System" && display "subtitle" "$action" for software in alsa-utils pipewire wireplumber pipewire-pulse \ pipewire-docs pamixer pulsemixer ffmpeg; do - pacman_install $software + pacman_install "$software" done; # disable the pc speaker beep rmmod pcspkr >> "$logfile" 2>&1 - echo "blacklist pcspkr" >/etc/modprobe.d/nobeep.conf >> "$logfile" 2>&1 + echo "blacklist pcspkr" > /etc/modprobe.d/nobeep.conf # Keyboard Shortcut Manager @@ -802,7 +1001,7 @@ desktop_environment() { action="Bluetooth System" && display "subtitle" "$action" for software in bluez bluez-utils blueman; do - pacman_install $software + pacman_install "$software" done action="enabling bluetooth to launch at boot" && display "task" "$action" systemctl enable bluetooth.service >> "$logfile" 2>&1 || error "error" "$action" "$?" @@ -832,7 +1031,7 @@ desktop_environment() { action="Sync Services" && display "subtitle" "$action" pacman_install syncthing - systemctl enable syncthing@$username.service >> "$logfile" 2>&1 || error "error" "$action" "$?" + systemctl enable "syncthing@$username.service" >> "$logfile" 2>&1 || error "error" "$action" "$?" # Desktop Environment Utilities @@ -840,7 +1039,7 @@ desktop_environment() { for software in brightnessctl network-manager-applet xclip rofi \ conky qalculate-gtk feh; do - pacman_install $software + pacman_install "$software" done; aur_install caffeine-ng @@ -854,12 +1053,12 @@ desktop_environment() { action="UI Theme" && display "subtitle" "$action" for software in picom lxappearance gnome-themes-extra; do - pacman_install $software + pacman_install "$software" done; for software in vimix-cursors \ papirus-icon-theme qt6ct qt5ct; do - aur_install $software + aur_install "$software" done; pacman_install libappindicator-gtk3 # required by some applets @@ -953,8 +1152,8 @@ developer_workstation () { # supporting utilities used by my emacs configuration aur_install exercism-bin # command line tool for exercism.io - aur_install libvips # image previewer library - aur_install isync # email sync + pacman_install libvips # image previewer library + pacman_install isync # email sync aur_install epub-thumbnailer-git # epub previews in dirvish aur_install mu # email indexer and utilities aur_install multimarkdown # markdown conversion @@ -1004,7 +1203,7 @@ developer_workstation () { pacman_install docker pacman_install docker-compose action="adding user to docker group" && display "task" "$action" - (gpasswd -a $username docker >> "$logfile" 2>&1) || error "error" "$action" "$?" + (gpasswd -a "$username" docker >> "$logfile" 2>&1) || error "error" "$action" "$?" action="enabling docker service to launch on boot" && display "task" "$action" systemctl enable docker.service >> "$logfile" 2>&1 || error "error" "$action" "$?" } @@ -1091,7 +1290,8 @@ supplemental_software() { aur_install nsxiv # image viewer aur_install snore-git # sleep with feedback # aur_install tageditor # metadata editor for mkv, webm and related video files (DISABLED: hangs indefinitely building qt5-webengine dependency) - aur_install thunar # file manager + pacman_install thunar # file manager + pacman_install gvfs-smb # SMB network share browsing in Thunar if ! $skip_slow_packages; then aur_install topgrade # upgrade everything utility (SLOW: ~12 min, Rust compilation) fi @@ -1118,20 +1318,30 @@ boot_ux() { error "error" "$action" "$?" action="configuring console font" && display "task" "$action" - (echo "FONT=ter-132n" >>/etc/vconsole.conf) || error "error" "$action" "$?" + if grep -q "^FONT=" /etc/vconsole.conf 2>/dev/null; then + sed -i 's/^FONT=.*/FONT=ter-132n/' /etc/vconsole.conf + else + echo "FONT=ter-132n" >> /etc/vconsole.conf + fi action="delegating fsck messages from udev to systemd" && display "task" "$action" - sed -i "s/.*HOOKS=(base udev autodetect keyboard keymap modconf block filesystems fsck).*/HOOKS=(base systemd autodetect keyboard keymap modconf block filesystems fsck)/" /etc/mkinitcpio.conf || error "error" "running sed on mkinitcpio.conf to hide fsck messages" "$?" + sed -i '/^HOOKS=/ s/\budev\b/systemd/' /etc/mkinitcpio.conf || error "error" "$action" "$?" mkinitcpio -P >> "$logfile" 2>&1 || error "error" "running mkinitcpio -P to silence fsck messages" "$?" - action="instructing systemd to check filesystems" && display "task" "$action" - servicefile=/usr/lib/systemd/system/systemd-fsck-root.service - [ -f $servicefile ] && echo "StandardOutput=null" >>$servicefile && \ - echo "StandardError=journal+console" >>$servicefile + action="configuring quiet fsck output" && display "task" "$action" + mkdir -p /etc/systemd/system/systemd-fsck-root.service.d + cat << 'EOF' > /etc/systemd/system/systemd-fsck-root.service.d/quiet.conf +[Service] +StandardOutput=null +StandardError=journal+console +EOF - servicefile=/usr/lib/systemd/system/systemd-fsck@.service - [ -f $servicefile ] && echo "StandardOutput=null" >>$servicefile && \ - echo "StandardError=journal+console" >>$servicefile + mkdir -p /etc/systemd/system/systemd-fsck@.service.d + cat << 'EOF' > /etc/systemd/system/systemd-fsck@.service.d/quiet.conf +[Service] +StandardOutput=null +StandardError=journal+console +EOF # action="removing hostname from login prompt" && display "task" "$action" # sed -i "s/--noclear/--nohostname --noclear/g" /usr/lib/systemd/system/getty@.service \ @@ -1152,9 +1362,9 @@ boot_ux() { sed -i "s/.*GRUB_DEFAULT=.*/GRUB_DEFAULT=0/g" /etc/default/grub sed -i 's/.*GRUB_TERMINAL_OUTPUT=console/GRUB_TERMINAL_OUTPUT=gfxterm/' /etc/default/grub sed -i 's/.*GRUB_GFXMODE=auto/GRUB_GFXMODE=1024x768/' /etc/default/grub - sed -i "s/.*GRUB_RECORDFAIL_TIMEOUT=.*/GRUB_RECORDFAIL_TIMEOUT=$GRUB_TIMEOUT/g" /etc/default/grub + sed -i "s/.*GRUB_RECORDFAIL_TIMEOUT=.*/GRUB_RECORDFAIL_TIMEOUT=2/g" /etc/default/grub sed -i "s/.*GRUB_CMDLINE_LINUX_DEFAULT=.*/GRUB_CMDLINE_LINUX_DEFAULT=\"rw loglevel=2 rd.systemd.show_status=auto rd.udev.log_level=2 nvme.noacpi=1 mem_sleep_default=deep nowatchdog quiet splash\"/g" /etc/default/grub - grub-mkconfig -o /boot/grub/grub.cfg >> "$logfile" 2>&1 || error "error" "" "$?" + grub-mkconfig -o /boot/grub/grub.cfg >> "$logfile" 2>&1 || error "error" "generating grub config" "$?" fi } @@ -1178,12 +1388,12 @@ outro() { mins=$(($totalsecs / 60)) secs=$(($totalsecs % 60)) - new_packages=$(cat $archsetup_packages | wc -l) + new_packages=$(wc -l < "$archsetup_packages") printf "\n" printf "Completion time : %s\n" "$(date +'%D %T')" | tee -a "$logfile" - printf "Elapsed time : %s minutes, %s seconds\n" $mins $secs | tee -a "$logfile" - printf "Errors encountered : %s\n" $errors_encountered | tee -a "$logfile" + printf "Elapsed time : %s minutes, %s seconds\n" "$mins" "$secs" | tee -a "$logfile" + printf "Errors encountered : %s\n" "$errors_encountered" | tee -a "$logfile" printf "Log file location : %s\n" "$logfile" printf "Packages installed : %s\n" "$new_packages" printf "\n" |
