summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rwxr-xr-xarchsetup10
-rw-r--r--assets/archangel.conf.example96
-rw-r--r--dotfiles/common/.config/dunst/dunstrc4
-rw-r--r--dotfiles/common/.config/gtk-3.0/settings.ini4
-rw-r--r--dotfiles/common/.config/topgrade.toml2
-rw-r--r--dotfiles/hyprland/.config/gammastep/config.ini21
-rw-r--r--dotfiles/hyprland/.config/hypr/hyprland.conf45
-rw-r--r--dotfiles/hyprland/.config/themes/dupre/Xresources4
-rw-r--r--dotfiles/hyprland/.config/themes/dupre/dunstrc4
-rw-r--r--dotfiles/hyprland/.config/themes/dupre/waybar.css24
-rw-r--r--dotfiles/hyprland/.config/themes/hudson/Xresources4
-rw-r--r--dotfiles/hyprland/.config/themes/hudson/dunstrc4
-rw-r--r--dotfiles/hyprland/.config/themes/hudson/waybar.css24
-rw-r--r--dotfiles/hyprland/.config/waybar/config10
-rw-r--r--dotfiles/hyprland/.config/waybar/style.css24
-rwxr-xr-xdotfiles/hyprland/.local/bin/pinentry-fuzzel2
-rwxr-xr-xdotfiles/hyprland/.local/bin/screenshot23
-rwxr-xr-xdotfiles/hyprland/.local/bin/set-theme18
-rwxr-xr-xdotfiles/hyprland/.local/bin/waybar-disk7
-rwxr-xr-xdotfiles/hyprland/.local/bin/waybar-netspeed2
-rw-r--r--scripts/testing/README.org379
-rw-r--r--scripts/testing/archinstall-config.json117
-rw-r--r--scripts/testing/archsetup-test.conf14
-rw-r--r--scripts/testing/archsetup-vm.conf11
-rwxr-xr-xscripts/testing/cleanup-tests.sh125
-rwxr-xr-xscripts/testing/create-base-vm.sh249
-rwxr-xr-xscripts/testing/debug-vm.sh122
-rwxr-xr-xscripts/testing/finalize-base-vm.sh31
-rw-r--r--scripts/testing/lib/network-diagnostics.sh24
-rw-r--r--scripts/testing/lib/validation.sh11
-rwxr-xr-xscripts/testing/lib/vm-utils.sh471
-rwxr-xr-xscripts/testing/run-test.sh173
-rwxr-xr-xscripts/testing/setup-testing-env.sh104
-rw-r--r--todo.org425
34 files changed, 1219 insertions, 1369 deletions
diff --git a/archsetup b/archsetup
index 790e966..0779d2b 100755
--- a/archsetup
+++ b/archsetup
@@ -1081,7 +1081,13 @@ EOF
action="enabling geoclue geolocation service" && display "task" "$action"
systemctl enable geoclue.service >> "$logfile" 2>&1 || error_warn "$action" "$?"
- # Whitelist gammastep in geoclue (prevents "unable to obtain geoclue client path" error)
+ # Enable BeaconDB as geoclue wifi location provider (default MLS/Ichnaea API is defunct)
+ action="configuring geoclue to use BeaconDB location service" && display "task" "$action"
+ if grep -q '^#url=https://api.beacondb.net/v1/geolocate' /etc/geoclue/geoclue.conf 2>/dev/null; then
+ sed -i 's|^#url=https://api.beacondb.net/v1/geolocate|url=https://api.beacondb.net/v1/geolocate|' /etc/geoclue/geoclue.conf
+ fi
+
+ # Whitelist gammastep in geoclue config (geoclue demo agent is started via hyprland.conf exec-once)
action="whitelisting gammastep in geoclue" && display "task" "$action"
if ! grep -q "^\[gammastep\]" /etc/geoclue/geoclue.conf 2>/dev/null; then
cat >> /etc/geoclue/geoclue.conf << 'EOF'
@@ -1606,7 +1612,7 @@ desktop_environment() {
pacman_install "$software"
done
- for software in vimix-cursors \
+ for software in bibata-cursor-theme-bin \
papirus-icon-theme qt6ct qt5ct; do
aur_install "$software"
done
diff --git a/assets/archangel.conf.example b/assets/archangel.conf.example
new file mode 100644
index 0000000..dacf340
--- /dev/null
+++ b/assets/archangel.conf.example
@@ -0,0 +1,96 @@
+# archangel.conf - Unattended Installation Configuration
+#
+# Copy this file and edit values.
+# Usage: archangel --config-file /path/to/your-config.conf
+#
+# Required fields: HOSTNAME, TIMEZONE, DISKS, ROOT_PASSWORD
+# For ZFS: also need ZFS_PASSPHRASE or NO_ENCRYPT=yes
+# All other fields have sensible defaults.
+
+#############################
+# Filesystem Selection
+#############################
+
+# Filesystem type (optional, default: zfs)
+# Options: zfs, btrfs
+# Note: btrfs support coming soon
+FILESYSTEM=zfs
+
+#############################
+# System Configuration
+#############################
+
+# Hostname for the installed system (required)
+HOSTNAME=archangel
+
+# Timezone (required) - Use format: Region/City
+# Examples: America/Los_Angeles, Europe/London, Asia/Tokyo
+TIMEZONE=America/Los_Angeles
+
+# Locale (optional, default: en_US.UTF-8)
+LOCALE=en_US.UTF-8
+
+# Console keymap (optional, default: us)
+KEYMAP=us
+
+#############################
+# Disk Configuration
+#############################
+
+# Disks to use for installation (required)
+# Single disk: DISKS=/dev/vda
+# Multiple disks: DISKS=/dev/vda,/dev/vdb,/dev/vdc
+DISKS=/dev/vda
+
+# RAID level for multi-disk setups (optional)
+# Options: mirror, stripe, raidz1, raidz2, raidz3
+# Default: mirror (when multiple disks specified)
+# Leave empty for single disk
+RAID_LEVEL=
+
+#############################
+# Security
+#############################
+
+# ZFS encryption passphrase (required unless NO_ENCRYPT=yes)
+# This will be required at every boot to unlock the pool
+ZFS_PASSPHRASE=changeme
+
+# Skip ZFS encryption (optional, default: no)
+# Set to "yes" to create an unencrypted pool
+# Use cases:
+# - VMs or test environments
+# - Systems with hardware encryption (SED drives)
+# - Data that doesn't require encryption
+# WARNING: Without encryption, anyone with physical access can read your data
+#NO_ENCRYPT=no
+
+# Root password (required)
+ROOT_PASSWORD=changeme
+
+#############################
+# Network Configuration
+#############################
+
+# Enable SSH with root login (optional, default: yes)
+# Set to "no" to disable SSH
+ENABLE_SSH=yes
+
+# WiFi configuration (optional)
+# Leave empty for ethernet-only or to skip WiFi setup
+WIFI_SSID=
+WIFI_PASSWORD=
+
+#############################
+# Advanced ZFS Options
+#############################
+
+# Pool name (optional, default: zroot)
+#POOL_NAME=zroot
+
+# Compression algorithm (optional, default: zstd)
+#COMPRESSION=zstd
+
+# Sector size shift (optional, default: 12 for 4K sectors)
+# Use 13 for 8K sector drives
+#ASHIFT=12
diff --git a/dotfiles/common/.config/dunst/dunstrc b/dotfiles/common/.config/dunst/dunstrc
index 2cbf0e6..f27bd33 100644
--- a/dotfiles/common/.config/dunst/dunstrc
+++ b/dotfiles/common/.config/dunst/dunstrc
@@ -13,7 +13,7 @@ notification_limit = 5
gap_size = 12
padding = 12
horizontal_padding = 20
-frame_width = 2
+frame_width = 0
sort = no
corner_radius = 10
@@ -27,7 +27,7 @@ frame_color = "#daa520"
highlight = "#daa520, #f0c674"
# Text (size in points, Pango syntax)
-font = BerkeleyMono Nerd Font 13
+font = BerkeleyMono Nerd Font 10
markup = full
format = "<small>%a</small>\n<b>%s</b>\n%b"
alignment = left
diff --git a/dotfiles/common/.config/gtk-3.0/settings.ini b/dotfiles/common/.config/gtk-3.0/settings.ini
index 2023ae6..5b2ba74 100644
--- a/dotfiles/common/.config/gtk-3.0/settings.ini
+++ b/dotfiles/common/.config/gtk-3.0/settings.ini
@@ -3,8 +3,8 @@ gtk-print-backends=file,cups,pdf
gtk-theme-name=Adwaita-dark
gtk-icon-theme-name=Papirus-Dark
gtk-font-name=Cantarell 11
-gtk-cursor-theme-name=Vimix-white-cursors
-gtk-cursor-theme-size=0
+gtk-cursor-theme-name=Bibata-Modern-Ice
+gtk-cursor-theme-size=24
gtk-toolbar-style=GTK_TOOLBAR_BOTH
gtk-toolbar-icon-size=GTK_ICON_SIZE_LARGE_TOOLBAR
gtk-button-images=1
diff --git a/dotfiles/common/.config/topgrade.toml b/dotfiles/common/.config/topgrade.toml
index b4434f2..f2dece4 100644
--- a/dotfiles/common/.config/topgrade.toml
+++ b/dotfiles/common/.config/topgrade.toml
@@ -16,7 +16,7 @@ sudo_command = "sudo"
# Disable specific steps - same options as the command line flag
# disable = ["system", "emacs"]
-disable = ["emacs", "poetry", "gnome_shell_extensions"]
+disable = ["emacs", "poetry", "gnome_shell_extensions", "lensfun"]
# Ignore failures for these steps
# ignore_failures = ["powershell"]
diff --git a/dotfiles/hyprland/.config/gammastep/config.ini b/dotfiles/hyprland/.config/gammastep/config.ini
index f50a701..bbb15ee 100644
--- a/dotfiles/hyprland/.config/gammastep/config.ini
+++ b/dotfiles/hyprland/.config/gammastep/config.ini
@@ -1,25 +1,8 @@
-[gammastep]
-; Color temperature in Kelvin (same as redshift)
+[general]
temp-day=6500
temp-night=4500
-
-; Brightness (1.0 = full, 0.8 = 20% dimmed)
brightness-day=1.0
brightness-night=1.0
-
-; Gamma correction
gamma=1.0
-
-; Fade between day/night
fade=1
-
-; Wayland adjustment method (not randr)
-adjustment-method=wayland
-
-; Location for sunrise/sunset calculation
-location-provider=manual
-
-[manual]
-; New Orleans, LA
-lat=29.951065
-lon=-90.071533
+location-provider=geoclue2
diff --git a/dotfiles/hyprland/.config/hypr/hyprland.conf b/dotfiles/hyprland/.config/hypr/hyprland.conf
index 709865a..755edad 100644
--- a/dotfiles/hyprland/.config/hypr/hyprland.conf
+++ b/dotfiles/hyprland/.config/hypr/hyprland.conf
@@ -15,7 +15,8 @@ exec-once = waybar
exec-once = swww-daemon && sleep 1 && swww img ~/pictures/wallpaper/trondheim-norway.jpg
exec-once = dunst
exec-once = hypridle
-exec-once = gammastep-indicator
+exec-once = /usr/lib/geoclue-2.0/demos/agent
+exec-once = gammastep
exec-once = blueman-applet
exec-once = /usr/bin/gnome-keyring-daemon --start --components=pkcs11,secrets,ssh
exec-once = dbus-update-activation-environment --systemd WAYLAND_DISPLAY XDG_CURRENT_DESKTOP
@@ -23,12 +24,13 @@ exec-once = mpd
exec-once = signal-desktop --start-in-tray --ozone-platform=wayland
exec-once = protonmail-bridge --no-window
exec-once = insync start
+exec-once = set-theme "$(cat ~/.config/current-theme 2>/dev/null || echo hudson)"
# ============================================================================
# Environment Variables
# ============================================================================
env = XCURSOR_SIZE,24
-env = XCURSOR_THEME,capitaine-cursors-light
+env = XCURSOR_THEME,Bibata-Modern-Ice
env = XDG_CURRENT_DESKTOP,Hyprland
env = XDG_SESSION_TYPE,wayland
env = XDG_SESSION_DESKTOP,Hyprland
@@ -73,7 +75,7 @@ animations {
# Layout (master-stack like DWM tile)
# ============================================================================
master {
- new_status = slave
+ new_status = master
mfact = 0.55
}
@@ -100,6 +102,7 @@ input {
misc {
force_default_wallpaper = 0
disable_hyprland_logo = true
+ focus_on_activate = true
}
# ============================================================================
@@ -110,6 +113,27 @@ xwayland {
}
# ============================================================================
+# Plugins
+# ============================================================================
+plugin {
+ hy3 {
+ tabs {
+ col.active = rgba(444444ff)
+ col.active.text = rgba(bbbbbbff)
+ col.active.border = rgba(daa520ff)
+ col.focused = rgba(444444ff)
+ col.focused.text = rgba(bbbbbbff)
+ col.focused.border = rgba(daa520ff)
+ col.inactive = rgba(333333ff)
+ col.inactive.text = rgba(888888ff)
+ col.inactive.border = rgba(333333ff)
+ col.urgent = rgba(cc6666ff)
+ col.locked = rgba(8abeb7ff)
+ }
+ }
+}
+
+# ============================================================================
# Window Rules (Hyprland 0.53+ syntax: match:CONDITION, RULE)
# ============================================================================
# Floating windows (from DWM rules)
@@ -130,12 +154,12 @@ bind = $mod, T, exec, foot
bind = $mod, E, exec, emacsclient -c -a "" || emacs
bind = $mod, W, exec, $BROWSER
bind = $mod, P, exec, fuzzel
+bind = $mod, F, exec, nautilus
# From sxhkdrc
bind = $mod, SPACE, exec, fuzzel
bind = $mod SHIFT, W, exec, $ALTBROWSER
bind = CTRL ALT, W, exec, tor-browser
-bind = CTRL ALT, F, exec, nautilus
bind = $mod, V, exec, virtualbox
bind = $mod SHIFT, L, exec, calibre
bind = $mod SHIFT, R, exec, shortwave
@@ -229,7 +253,6 @@ workspace = special:term, on-created-empty:foot --app-id foot-term tmux
workspace = special:audio, on-created-empty:foot --app-id foot-audio pulsemixer
workspace = special:monitor, on-created-empty:foot --app-id foot-monitor gotop
workspace = special:music, on-created-empty:foot --app-id foot-music ncmpcpp
-workspace = special:files, on-created-empty:foot --app-id foot-files ranger
workspace = special:htop, on-created-empty:foot --app-id foot-htop htop
workspace = special:network, on-created-empty:foot --app-id foot-network nmtui
@@ -238,7 +261,6 @@ bind = $mod SHIFT, RETURN, togglespecialworkspace, term
bind = $mod, A, togglespecialworkspace, audio
bind = $mod, M, togglespecialworkspace, monitor
bind = $mod, SLASH, togglespecialworkspace, music
-bind = $mod, F, togglespecialworkspace, files
bind = $mod SHIFT, H, togglespecialworkspace, htop
# Window rules (using app-id/class matching, Hyprland 0.53+ syntax)
@@ -262,11 +284,6 @@ windowrule = match:class ^(foot-music)$, float on
windowrule = match:class ^(foot-music)$, size (monitor_w*0.6) (monitor_h*0.6)
windowrule = match:class ^(foot-music)$, center on
-# File manager
-windowrule = match:class ^(foot-files)$, float on
-windowrule = match:class ^(foot-files)$, size (monitor_w*0.6) (monitor_h*0.6)
-windowrule = match:class ^(foot-files)$, center on
-
# Htop
windowrule = match:class ^(foot-htop)$, float on
windowrule = match:class ^(foot-htop)$, size (monitor_w*0.6) (monitor_h*0.6)
@@ -294,9 +311,9 @@ bind = CTRL ALT, SPACE, exec, amixer set Capture toggle && amixer get Capture |
# Bluetooth (from DWM and sxhkdrc)
bind = $mod SHIFT, B, exec, blueman-manager
-# Screenshots (grim + slurp + satty for annotation)
-bind = $mod, S, exec, grim -g "$(slurp)" - | satty --filename - --output-filename ~/pictures/screenshots/$(date +%Y.%m.%d-%H%M%S).png
-bind = , Print, exec, grim - | satty --filename - --output-filename ~/pictures/screenshots/$(date +%Y.%m.%d-%H%M%S).png
+# Screenshots (grim + slurp + fuzzel menu)
+bind = $mod, S, exec, screenshot region
+bind = $mod, Print, exec, screenshot fullscreen
# Lock screen (from sxhkdrc: super+Escape)
bind = $mod, ESCAPE, exec, hyprlock
diff --git a/dotfiles/hyprland/.config/themes/dupre/Xresources b/dotfiles/hyprland/.config/themes/dupre/Xresources
index be8f7f4..1646b87 100644
--- a/dotfiles/hyprland/.config/themes/dupre/Xresources
+++ b/dotfiles/hyprland/.config/themes/dupre/Xresources
@@ -4,8 +4,8 @@
!! X Font Settings
Xft.dpi: 109
-Xcursor.size: 32
-Xcursor.theme: Vimix-white-cursors
+Xcursor.size: 24
+Xcursor.theme: Bibata-Modern-Ice
Xft.autohint: 0
Xft.lcdfilter: lcddefault
Xft.hintstyle: hintfull
diff --git a/dotfiles/hyprland/.config/themes/dupre/dunstrc b/dotfiles/hyprland/.config/themes/dupre/dunstrc
index 12ffac0..0847fdf 100644
--- a/dotfiles/hyprland/.config/themes/dupre/dunstrc
+++ b/dotfiles/hyprland/.config/themes/dupre/dunstrc
@@ -13,7 +13,7 @@ notification_limit = 5
gap_size = 12
padding = 12
horizontal_padding = 20
-frame_width = 2
+frame_width = 0
sort = no
corner_radius = 10
@@ -27,7 +27,7 @@ frame_color = "#d7af5f"
highlight = "#d7af5f, #ccc768"
# Text (size in points, Pango syntax)
-font = BerkeleyMono Nerd Font 13
+font = BerkeleyMono Nerd Font 10
markup = full
format = "<small>%a</small>\n<b>%s</b>\n%b"
alignment = left
diff --git a/dotfiles/hyprland/.config/themes/dupre/waybar.css b/dotfiles/hyprland/.config/themes/dupre/waybar.css
index e83f851..18ba0f1 100644
--- a/dotfiles/hyprland/.config/themes/dupre/waybar.css
+++ b/dotfiles/hyprland/.config/themes/dupre/waybar.css
@@ -49,7 +49,7 @@ window#waybar {
#cpu,
#temperature,
#memory,
-#disk,
+#custom-disk,
#custom-netspeed,
#battery,
#wireplumber,
@@ -73,7 +73,7 @@ window#waybar {
#cpu:hover,
#temperature:hover,
#memory:hover,
-#disk:hover,
+#custom-disk:hover,
#custom-netspeed:hover,
#wireplumber:hover,
#sysmonitor:hover,
@@ -90,8 +90,24 @@ window#waybar {
color: #969385;
}
-#wireplumber.muted {
- color: #58574e;
+#custom-netspeed.disconnected {
+ color: #d47c59;
+}
+
+#temperature.warning {
+ color: #d7af5f;
+}
+
+#temperature.critical {
+ color: #d47c59;
+}
+
+#custom-disk.warning {
+ color: #d7af5f;
+}
+
+#custom-disk.critical {
+ color: #d47c59;
}
#battery.warning {
diff --git a/dotfiles/hyprland/.config/themes/hudson/Xresources b/dotfiles/hyprland/.config/themes/hudson/Xresources
index 96e4803..4b35809 100644
--- a/dotfiles/hyprland/.config/themes/hudson/Xresources
+++ b/dotfiles/hyprland/.config/themes/hudson/Xresources
@@ -10,8 +10,8 @@ Xft.hintstyle: hintfull
Xft.hinting: 1
Xft.antialias: 1
Xft.rgba: rgb
-Xcursor.size: 32
-Xcursor.theme: Vimix-white-cursors
+Xcursor.size: 24
+Xcursor.theme: Bibata-Modern-Ice
!! Emacs
Emacs*toolBar: 0
diff --git a/dotfiles/hyprland/.config/themes/hudson/dunstrc b/dotfiles/hyprland/.config/themes/hudson/dunstrc
index 2cbf0e6..f27bd33 100644
--- a/dotfiles/hyprland/.config/themes/hudson/dunstrc
+++ b/dotfiles/hyprland/.config/themes/hudson/dunstrc
@@ -13,7 +13,7 @@ notification_limit = 5
gap_size = 12
padding = 12
horizontal_padding = 20
-frame_width = 2
+frame_width = 0
sort = no
corner_radius = 10
@@ -27,7 +27,7 @@ frame_color = "#daa520"
highlight = "#daa520, #f0c674"
# Text (size in points, Pango syntax)
-font = BerkeleyMono Nerd Font 13
+font = BerkeleyMono Nerd Font 10
markup = full
format = "<small>%a</small>\n<b>%s</b>\n%b"
alignment = left
diff --git a/dotfiles/hyprland/.config/themes/hudson/waybar.css b/dotfiles/hyprland/.config/themes/hudson/waybar.css
index 9056529..299f1c2 100644
--- a/dotfiles/hyprland/.config/themes/hudson/waybar.css
+++ b/dotfiles/hyprland/.config/themes/hudson/waybar.css
@@ -49,7 +49,7 @@ window#waybar {
#cpu,
#temperature,
#memory,
-#disk,
+#custom-disk,
#custom-netspeed,
#battery,
#wireplumber,
@@ -73,7 +73,7 @@ window#waybar {
#cpu:hover,
#temperature:hover,
#memory:hover,
-#disk:hover,
+#custom-disk:hover,
#custom-netspeed:hover,
#wireplumber:hover,
#sysmonitor:hover,
@@ -90,8 +90,24 @@ window#waybar {
color: #bbbbbb;
}
-#wireplumber.muted {
- color: #666666;
+#custom-netspeed.disconnected {
+ color: #ff5858;
+}
+
+#temperature.warning {
+ color: #daa520;
+}
+
+#temperature.critical {
+ color: #ff5858;
+}
+
+#custom-disk.warning {
+ color: #daa520;
+}
+
+#custom-disk.critical {
+ color: #ff5858;
}
#battery.warning {
diff --git a/dotfiles/hyprland/.config/waybar/config b/dotfiles/hyprland/.config/waybar/config
index 5b9879e..fdff160 100644
--- a/dotfiles/hyprland/.config/waybar/config
+++ b/dotfiles/hyprland/.config/waybar/config
@@ -28,7 +28,7 @@
"cpu",
"temperature",
"memory",
- "disk",
+ "custom/disk",
"battery"
]
},
@@ -62,6 +62,7 @@
"interval": 10,
"hwmon-path-abs": "/sys/devices/pci0000:00/0000:00:18.3/hwmon",
"input-filename": "temp1_input",
+ "warning-threshold": 70,
"critical-threshold": 80,
"format": "<span size='large'>󰔏</span> {temperatureC}°C",
"format-critical": "<span size='large'>󰸁</span> {temperatureC}°C",
@@ -74,10 +75,11 @@
"on-click": "toggle-scratchpad monitor"
},
- "disk": {
+ "custom/disk": {
+ "exec": "waybar-disk",
+ "return-type": "json",
"interval": 30,
- "format": "<span size='large'>󰆼</span> {percentage_used}%",
- "path": "/",
+ "format": "<span size='large'>󰆼</span> {}",
"on-click": "toggle-scratchpad monitor"
},
diff --git a/dotfiles/hyprland/.config/waybar/style.css b/dotfiles/hyprland/.config/waybar/style.css
index 9056529..299f1c2 100644
--- a/dotfiles/hyprland/.config/waybar/style.css
+++ b/dotfiles/hyprland/.config/waybar/style.css
@@ -49,7 +49,7 @@ window#waybar {
#cpu,
#temperature,
#memory,
-#disk,
+#custom-disk,
#custom-netspeed,
#battery,
#wireplumber,
@@ -73,7 +73,7 @@ window#waybar {
#cpu:hover,
#temperature:hover,
#memory:hover,
-#disk:hover,
+#custom-disk:hover,
#custom-netspeed:hover,
#wireplumber:hover,
#sysmonitor:hover,
@@ -90,8 +90,24 @@ window#waybar {
color: #bbbbbb;
}
-#wireplumber.muted {
- color: #666666;
+#custom-netspeed.disconnected {
+ color: #ff5858;
+}
+
+#temperature.warning {
+ color: #daa520;
+}
+
+#temperature.critical {
+ color: #ff5858;
+}
+
+#custom-disk.warning {
+ color: #daa520;
+}
+
+#custom-disk.critical {
+ color: #ff5858;
}
#battery.warning {
diff --git a/dotfiles/hyprland/.local/bin/pinentry-fuzzel b/dotfiles/hyprland/.local/bin/pinentry-fuzzel
index 4cbe6b7..5c64968 100755
--- a/dotfiles/hyprland/.local/bin/pinentry-fuzzel
+++ b/dotfiles/hyprland/.local/bin/pinentry-fuzzel
@@ -83,7 +83,7 @@ while read cmd rest; do
else
LABEL="reenter: "
fi
- PASS=$(fuzzel --prompt "$LABEL" --width 25 --lines 0 --cache /dev/null --password --dmenu)
+ PASS=$(fuzzel --prompt "$LABEL" --width 35 --lines 0 --cache /dev/null --password --dmenu --border-color=d47c59ff)
if [ -z "$PASS" ]; then
# User cancelled - return error to GPG
rm -f "$LASTFILE"
diff --git a/dotfiles/hyprland/.local/bin/screenshot b/dotfiles/hyprland/.local/bin/screenshot
new file mode 100755
index 0000000..45925c6
--- /dev/null
+++ b/dotfiles/hyprland/.local/bin/screenshot
@@ -0,0 +1,23 @@
+#!/bin/sh
+# Screenshot tool with fuzzel menu
+# Usage: screenshot [region|fullscreen]
+
+DIR="$HOME/pictures/screenshots"
+mkdir -p "$DIR"
+FILE="$DIR/$(date +%Y-%m-%d_%H%M%S).png"
+
+# Capture
+case "${1:-region}" in
+ region) grim -g "$(slurp)" "$FILE" || exit 1 ;;
+ fullscreen) grim "$FILE" || exit 1 ;;
+esac
+
+# Menu
+CHOICE=$(printf '󰅍 Copy Path\n󰋩 Copy Image\n󰏫 Annotate' | \
+ fuzzel --dmenu --prompt "Screenshot: " --width 20 --lines 3)
+
+case "$CHOICE" in
+ *"Copy Path"*) echo -n "$FILE" | wl-copy --type text/plain ;;
+ *"Copy Image"*) wl-copy --type image/png < "$FILE" ;;
+ *"Annotate"*) satty --filename "$FILE" --output-filename "$FILE" --copy-command wl-copy ;;
+esac
diff --git a/dotfiles/hyprland/.local/bin/set-theme b/dotfiles/hyprland/.local/bin/set-theme
index e95b27b..9bbcbd4 100755
--- a/dotfiles/hyprland/.local/bin/set-theme
+++ b/dotfiles/hyprland/.local/bin/set-theme
@@ -56,8 +56,13 @@ apply_theme() {
# hy3 tab bar (monocle mode)
hyprctl keyword plugin:hy3:tabs:col.active "rgba(474544ff)"
hyprctl keyword plugin:hy3:tabs:col.active.text "rgba(969385ff)"
- hyprctl keyword plugin:hy3:tabs:col.inactive "rgba(d0cbc0ff)"
- hyprctl keyword plugin:hy3:tabs:col.inactive.text "rgba(d0cbc0ff)"
+ hyprctl keyword plugin:hy3:tabs:col.active.border "rgba(d7af5fff)"
+ hyprctl keyword plugin:hy3:tabs:col.focused "rgba(474544ff)"
+ hyprctl keyword plugin:hy3:tabs:col.focused.text "rgba(969385ff)"
+ hyprctl keyword plugin:hy3:tabs:col.focused.border "rgba(d7af5fff)"
+ hyprctl keyword plugin:hy3:tabs:col.inactive "rgba(2a2725ff)"
+ hyprctl keyword plugin:hy3:tabs:col.inactive.text "rgba(6c6a60ff)"
+ hyprctl keyword plugin:hy3:tabs:col.inactive.border "rgba(2a2725ff)"
hyprctl keyword plugin:hy3:tabs:col.urgent "rgba(d47c59ff)"
hyprctl keyword plugin:hy3:tabs:col.locked "rgba(8a9496ff)"
;;
@@ -68,8 +73,13 @@ apply_theme() {
# hy3 tab bar (monocle mode)
hyprctl keyword plugin:hy3:tabs:col.active "rgba(444444ff)"
hyprctl keyword plugin:hy3:tabs:col.active.text "rgba(bbbbbbff)"
- hyprctl keyword plugin:hy3:tabs:col.inactive "rgba(c5c8c6ff)"
- hyprctl keyword plugin:hy3:tabs:col.inactive.text "rgba(c5c8c6ff)"
+ hyprctl keyword plugin:hy3:tabs:col.active.border "rgba(daa520ff)"
+ hyprctl keyword plugin:hy3:tabs:col.focused "rgba(444444ff)"
+ hyprctl keyword plugin:hy3:tabs:col.focused.text "rgba(bbbbbbff)"
+ hyprctl keyword plugin:hy3:tabs:col.focused.border "rgba(daa520ff)"
+ hyprctl keyword plugin:hy3:tabs:col.inactive "rgba(333333ff)"
+ hyprctl keyword plugin:hy3:tabs:col.inactive.text "rgba(888888ff)"
+ hyprctl keyword plugin:hy3:tabs:col.inactive.border "rgba(333333ff)"
hyprctl keyword plugin:hy3:tabs:col.urgent "rgba(cc6666ff)"
hyprctl keyword plugin:hy3:tabs:col.locked "rgba(8abeb7ff)"
;;
diff --git a/dotfiles/hyprland/.local/bin/waybar-disk b/dotfiles/hyprland/.local/bin/waybar-disk
new file mode 100755
index 0000000..6050653
--- /dev/null
+++ b/dotfiles/hyprland/.local/bin/waybar-disk
@@ -0,0 +1,7 @@
+#!/bin/sh
+# Waybar disk usage module with warning/critical states
+PCT=$(df --output=pcent / | tail -1 | tr -d ' %')
+CLASS=""
+[ "$PCT" -ge 80 ] && CLASS="warning"
+[ "$PCT" -ge 90 ] && CLASS="critical"
+printf '{"text": "%s%%", "class": "%s", "tooltip": "Root: %s%% used"}\n' "$PCT" "$CLASS" "$PCT"
diff --git a/dotfiles/hyprland/.local/bin/waybar-netspeed b/dotfiles/hyprland/.local/bin/waybar-netspeed
index 97e8e5e..a6296b0 100755
--- a/dotfiles/hyprland/.local/bin/waybar-netspeed
+++ b/dotfiles/hyprland/.local/bin/waybar-netspeed
@@ -6,7 +6,7 @@
INTERFACE=$(ip route | awk '/default/ {print $5; exit}')
if [ -z "$INTERFACE" ]; then
- echo '{"text": "󰤭 Disconnected", "tooltip": "No network connection", "class": "disconnected"}'
+ echo '{"text": "󰤭 Offline", "tooltip": "No network connection", "class": "disconnected"}'
exit 0
fi
diff --git a/scripts/testing/README.org b/scripts/testing/README.org
index a52dbea..e58a89e 100644
--- a/scripts/testing/README.org
+++ b/scripts/testing/README.org
@@ -10,20 +10,23 @@ This directory contains the complete testing infrastructure for archsetup, built
*Realism over speed.* We use full VMs (not containers) to test everything archsetup does: user creation, systemd services, X11/DWM, hardware drivers, and boot process.
+** Architecture
+
+Uses direct QEMU with user-mode networking and SSH port forwarding. No libvirt/virsh dependency. Base VM creation is fully automated via the archangel ISO (unattended Arch Linux installer).
+
* Quick Start
** One-Time Setup
#+begin_src bash
-# Install required packages and configure environment
+# Install required packages (qemu-full, sshpass, edk2-ovmf, socat)
./scripts/testing/setup-testing-env.sh
-# Log out and back in (for libvirt group membership)
+# Copy an archangel ISO to vm-images/
+cp /path/to/archzfs-*.iso vm-images/
-# Create the base VM (minimal Arch installation)
+# Create the base VM (fully automated, no manual steps)
./scripts/testing/create-base-vm.sh
-# Follow on-screen instructions to complete installation
-# Run finalize-base-vm.sh when done
#+end_src
** Run a Test
@@ -42,14 +45,11 @@ This directory contains the complete testing infrastructure for archsetup, built
** Debug Interactively
#+begin_src bash
-# Clone base VM for debugging
-./scripts/testing/debug-vm.sh
-
-# Use existing test disk
-./scripts/testing/debug-vm.sh test-results/20251108-143000/test.qcow2
-
-# Use base VM (read-only)
+# Launch base VM with graphical display (copy-on-write overlay, safe)
./scripts/testing/debug-vm.sh --base
+
+# Use existing test disk directly
+./scripts/testing/debug-vm.sh vm-images/some-disk.qcow2
#+end_src
** Clean Up
@@ -72,82 +72,44 @@ This directory contains the complete testing infrastructure for archsetup, built
*Purpose:* One-time setup of testing infrastructure
*What it does:*
-- Installs QEMU/KVM, libvirt, and related tools
-- Configures libvirt networking
-- Adds user to libvirt group
+- Installs QEMU/KVM, sshpass, OVMF firmware, and socat
- Verifies KVM support
- Creates directories for artifacts
*When to run:* Once per development machine
-*Usage:*
-#+begin_src bash
-./scripts/testing/setup-testing-env.sh
-#+end_src
-
** create-base-vm.sh
*Purpose:* Create the "golden image" minimal Arch VM
*What it does:*
-- Downloads latest Arch ISO
-- Creates VM and boots from ISO
-- Opens virt-viewer for you to complete installation manually
+- Boots an archangel ISO in QEMU with SSH port forwarding
+- Waits for SSH to become available on the live ISO
+- Copies archangel config file (=archsetup-test.conf=) into the VM
+- Runs =archangel --config-file= for fully unattended installation
+- Boots the installed system and verifies SSH/services
+- Creates a "clean-install" qemu-img snapshot
*When to run:* Once (or when you want to refresh base image)
-*Usage:*
-#+begin_src bash
-./scripts/testing/create-base-vm.sh
-#+end_src
-
-*Process:*
-1. Script creates VM and boots from Arch ISO
-2. virt-viewer opens automatically showing VM display
-3. You complete installation manually using archinstall:
- - Login as root (no password)
- - Run =archinstall=
- - Configure: hostname=archsetup-test, root password=archsetup
- - Install packages: openssh git vim sudo
- - Enable services: sshd, dhcpcd
- - Configure SSH to allow root login
- - Poweroff when done
-4. Run =./scripts/testing/finalize-base-vm.sh= to complete
-
-*See also:* [[file:../../docs/base-vm-installation-checklist.org][Base VM Installation Checklist]]
-
-*Result:* Base VM image at =vm-images/archsetup-base.qcow2=
+*Result:* Base VM image at =vm-images/archsetup-base.qcow2= with "clean-install" snapshot
** run-test.sh
*Purpose:* Execute a full test run of archsetup
*What it does:*
-- Clones base VM (copy-on-write, fast)
-- Starts test VM
-- Transfers archsetup script and dotfiles
+- Reverts base VM to clean-install snapshot
+- Boots the VM via QEMU
+- Transfers archsetup script and dotfiles via git bundle
- Executes archsetup inside VM
- Captures logs and results
-- Runs validation checks
+- Runs comprehensive validation checks
- Generates test report
-- Cleans up (unless =--keep=)
+- Reverts to clean snapshot (unless =--keep=)
*When to run:* Every time you want to test archsetup
-*Usage:*
-#+begin_src bash
-# Test current archsetup
-./scripts/testing/run-test.sh
-
-# Test specific version
-./scripts/testing/run-test.sh --script /path/to/archsetup
-
-# Keep VM for debugging
-./scripts/testing/run-test.sh --keep
-#+end_src
-
-*Time:* 30-60 minutes (mostly downloading packages)
-
*Results:* Saved to =test-results/TIMESTAMP/=
- =test.log= - Complete log output
- =test-report.txt= - Summary of results
@@ -159,51 +121,34 @@ This directory contains the complete testing infrastructure for archsetup, built
*Purpose:* Launch VM for interactive debugging
*What it does:*
-- Creates VM from base image or existing test disk
-- Configures console and SSH access
-- Provides connection instructions
+- Creates a copy-on-write overlay disk (instant, protects base image)
+- Boots QEMU with GTK graphical display
+- Provides SSH connection info
+- Cleans up overlay when the GTK window is closed
*When to run:* When you need to manually test or debug
-*Usage:*
-#+begin_src bash
-# Clone base VM for debugging
-./scripts/testing/debug-vm.sh
-
-# Use existing test disk
-./scripts/testing/debug-vm.sh vm-images/archsetup-test-20251108-143000.qcow2
-
-# Use base VM (read-only)
-./scripts/testing/debug-vm.sh --base
-#+end_src
-
*Connect via:*
-- Console: =virsh console archsetup-debug-TIMESTAMP=
-- SSH: =ssh root@IP_ADDRESS= (password: archsetup)
-- VNC: =virt-viewer archsetup-debug-TIMESTAMP=
+- GTK window (opens automatically)
+- SSH: =sshpass -p 'archsetup' ssh -p 2222 root@localhost=
** cleanup-tests.sh
*Purpose:* Clean up old test VMs and artifacts
*What it does:*
-- Lists all test VMs and destroys them
-- Removes test disk images
+- Stops running QEMU processes
+- Kills orphaned QEMU processes
+- Removes temporary disk images (overlays, test clones)
- Keeps last N test results, deletes rest
*When to run:* Periodically to free disk space
-*Usage:*
-#+begin_src bash
-# Interactive cleanup
-./scripts/testing/cleanup-tests.sh
+** archsetup-test.conf
-# Keep last 3 test results
-./scripts/testing/cleanup-tests.sh --keep 3
+*Purpose:* Archangel config file for automated base VM creation
-# Force without prompts
-./scripts/testing/cleanup-tests.sh --force
-#+end_src
+Defines the base system: btrfs filesystem, no encryption, SSH enabled, root password "archsetup". Used by =create-base-vm.sh=.
* Directory Structure
@@ -213,27 +158,30 @@ archsetup/
│ └── testing/
│ ├── README.org # This file
│ ├── setup-testing-env.sh # Setup infrastructure
-│ ├── create-base-vm.sh # Create base VM
+│ ├── create-base-vm.sh # Create base VM (automated)
│ ├── run-test.sh # Run tests
+│ ├── run-test-baremetal.sh # Bare-metal testing
│ ├── debug-vm.sh # Interactive debugging
│ ├── cleanup-tests.sh # Clean up
-│ ├── finalize-base-vm.sh # Finalize base (generated)
-│ ├── archinstall-config.json # Archinstall config
+│ ├── archsetup-test.conf # Archangel config for test VMs
│ └── lib/
│ ├── logging.sh # Logging utilities
-│ └── vm-utils.sh # VM management
+│ ├── vm-utils.sh # QEMU VM management
+│ ├── validation.sh # Test validation checks
+│ └── network-diagnostics.sh # Network pre-flight checks
├── vm-images/ # VM disk images (gitignored)
-│ ├── archsetup-base.qcow2 # Golden image
-│ ├── arch-latest.iso # Arch ISO
-│ └── archsetup-test-*.qcow2 # Test VMs
+│ ├── archsetup-base.qcow2 # Golden image with snapshot
+│ ├── archzfs-*.iso # Archangel ISO
+│ ├── OVMF_VARS.fd # UEFI variables (per-VM)
+│ └── debug-overlay-*.qcow2 # Temp debug overlays
├── test-results/ # Test results (gitignored)
-│ ├── TIMESTAMP/
-│ │ ├── test.log
-│ │ ├── test-report.txt
-│ │ └── archsetup-*.log
-│ └── latest -> TIMESTAMP/ # Symlink to latest
+│ └── TIMESTAMP/
+│ ├── test.log
+│ ├── test-report.txt
+│ └── archsetup-*.log
└── docs/
- └── testing-strategy.org # Complete strategy doc
+ ├── testing-strategy.org # Complete strategy doc
+ └── archangel-vm-testing-guide.org # Archangel integration guide
#+end_example
* Configuration
@@ -241,30 +189,27 @@ archsetup/
** VM Specifications
All test VMs use:
-- *CPUs:* 2 vCPUs
-- *RAM:* 4GB (matches archsetup tmpfs build directory)
+- *CPUs:* 4 vCPUs
+- *RAM:* 4GB
- *Disk:* 50GB (thin provisioned qcow2)
-- *Network:* NAT via libvirt default network
-- *Boot:* UEFI (systemd-boot bootloader)
-- *Display:* Serial console + VNC
+- *Network:* User-mode networking with SSH port forwarding (localhost:2222)
+- *Boot:* UEFI (via OVMF firmware)
+- *Display:* Headless (automated) or GTK (debug)
Set environment variables to customize:
#+begin_src bash
-VM_CPUS=4 VM_RAM=8192 ./scripts/testing/run-test.sh
+VM_CPUS=8 VM_RAM=8192 ./scripts/testing/run-test.sh
#+end_src
** Base VM Specifications
-The base VM contains a minimal Arch installation:
-- Base system packages
-- Linux kernel and firmware
-- OpenSSH server (for automation)
-- dhcpcd (for networking)
-- git, vim, sudo (essentials)
+The base VM is created automatically by archangel with:
+- Btrfs filesystem (no encryption)
+- GRUB bootloader
+- OpenSSH server (root login enabled)
+- NetworkManager
- Root password: "archsetup"
-This matches the documented prerequisites for archsetup.
-
* Validation Checks
Each test run performs these validation checks:
@@ -274,62 +219,37 @@ Each test run performs these validation checks:
2. User 'cjennings' was created
3. Dotfiles are stowed (symlinks exist)
4. yay (AUR helper) is installed
-5. DWM is built and installed
+5. DWM or Hyprland is configured
-** Additional (Future)
+** Additional
- All expected packages installed
-- X11 can start
- systemd services enabled
- Firewall configured
+- Developer tools present
* Troubleshooting
** VM fails to start
-Check if libvirtd is running:
-#+begin_src bash
-sudo systemctl status libvirtd
-sudo systemctl start libvirtd
-#+end_src
-
-** Cannot get VM IP address
-
-The VM may not have booted completely or networking failed:
+Check if KVM is available and port 2222 is free:
#+begin_src bash
-# Check VM status
-virsh domstate VM_NAME
-
-# Connect to console to debug
-virsh console VM_NAME
-
-# Check if dhcpcd is running in VM
-systemctl status dhcpcd
+ls -l /dev/kvm
+ss -tln | grep 2222
#+end_src
** SSH connection refused
-Wait longer for VM to boot, or check if sshd is enabled:
+Wait longer for VM to boot, or check serial log:
#+begin_src bash
-virsh console VM_NAME
-# Inside VM:
-systemctl status sshd
-systemctl start sshd
+cat vm-images/qemu-serial.log
#+end_src
** KVM not available
Check if virtualization is enabled in BIOS and KVM modules loaded:
#+begin_src bash
-# Check for /dev/kvm
ls -l /dev/kvm
-
-# Load KVM module (Intel)
-sudo modprobe kvm-intel
-
-# Or for AMD
-sudo modprobe kvm-amd
-
-# Verify
+sudo modprobe kvm-amd # or kvm-intel
lsmod | grep kvm
#+end_src
@@ -338,160 +258,27 @@ lsmod | grep kvm
Clean up old tests:
#+begin_src bash
./scripts/testing/cleanup-tests.sh --force
-#+end_src
-
-Check disk usage:
-#+begin_src bash
du -sh vm-images/ test-results/
#+end_src
-** Base VM Installation Issues
-
-*** Firewall blocking VM network
-
-If the VM cannot reach the internet (100% packet loss when pinging), check the host firewall:
-
-#+begin_src bash
-# Check UFW status on host
-sudo ufw status
-
-# Check libvirt NAT rules on host
-sudo iptables -t nat -L -n -v | grep -i libvirt
-#+end_src
-
-*Solution:* Temporarily disable UFW during base VM creation:
-#+begin_src bash
-sudo ufw disable
-# Create base VM
-sudo ufw enable
-#+end_src
-
-Or add libvirt rules to UFW:
-#+begin_src bash
-sudo ufw allow in on virbr0
-sudo ufw allow out on virbr0
-#+end_src
-
-*** VM network not working after boot
-
-If =dhcpcd= isn't running or network isn't configured:
-
-#+begin_src bash
-# In the VM - restart network from scratch
-killall dhcpcd
-
-# Bring interface down and up (replace enp1s0 with your interface)
-ip link set enp1s0 down
-ip link set enp1s0 up
-
-# Start dhcpcd
-dhcpcd enp1s0
-
-# Wait and verify
-sleep 3
-ip addr show enp1s0
-ip route
-
-# Test connectivity
-ping -c 3 192.168.122.1 # Gateway
-ping -c 3 8.8.8.8 # Google DNS
-#+end_src
-
-*** DNS not working (127.0.0.53 in resolv.conf)
-
-The Live ISO uses systemd-resolved stub resolver which may not work:
-
-#+begin_src bash
-# In the VM - set real DNS servers
-echo "nameserver 8.8.8.8" > /etc/resolv.conf
-echo "nameserver 1.1.1.1" >> /etc/resolv.conf
-
-# Test
-ping -c 3 archlinux.org
-#+end_src
-
-*** Cannot paste into virt-viewer terminal
-
-Clipboard integration doesn't work well with virt-viewer. Use HTTP server instead:
-
-#+begin_src bash
-# On host - serve the installation script
-cd vm-images
-python -m http.server 8000
-
-# In VM - download and run
-curl http://192.168.122.1:8000/auto-install.sh | bash
-
-# Or download first to review
-curl http://192.168.122.1:8000/auto-install.sh -o install.sh
-cat install.sh
-bash install.sh
-#+end_src
-
-*** Partitions "in use" error
-
-If re-running installation after a failed attempt:
+** Port 2222 already in use
+Another QEMU instance may be running:
#+begin_src bash
-# In the VM - unmount and wipe partitions
-mount | grep vda
-umount /mnt/boot 2>/dev/null
-umount /mnt 2>/dev/null
-umount -l /mnt # Lazy unmount if still busy
-
-# Wipe partition table completely
-wipefs -a /dev/vda
-
-# Run install script again
-bash install.sh
-#+end_src
-
-*** Alternative: Use archinstall
-
-Instead of the auto-install.sh script, you can use Arch's built-in installer:
-
-#+begin_src bash
-# In the VM
-archinstall
-#+end_src
-
-*Recommended settings:*
-- Disk: =/dev/vda=
-- Filesystem: =ext4=
-- Bootloader: =systemd-boot=
-- Hostname: =archsetup-test=
-- Root password: =archsetup=
-- Profile: =minimal=
-- Additional packages: =openssh dhcpcd git vim sudo=
-- Network: =NetworkManager= or =systemd-networkd=
-
-*After installation, before rebooting:*
-#+begin_src bash
-# Chroot into new system
-arch-chroot /mnt
-
-# Enable services
-systemctl enable sshd
-systemctl enable dhcpcd # or NetworkManager
-
-# Allow root SSH login
-sed -i 's/#PermitRootLogin prohibit-password/PermitRootLogin yes/' /etc/ssh/sshd_config
-sed -i 's/#PasswordAuthentication yes/PasswordAuthentication yes/' /etc/ssh/sshd_config
+# Find what's using the port
+ss -tlnp | grep 2222
-# Exit and poweroff
-exit
-poweroff
+# Kill orphaned QEMU processes
+pkill -f "qemu-system.*archsetup-test"
#+end_src
* Future Enhancements
** Planned Improvements
-- [ ] Fully automated base VM creation (expect-based console automation)
-- [ ] Parallel test execution (multiple VMs)
-- [ ] Screenshot capture of X11 desktop
+- [ ] Parallel test execution (multiple VMs on different ports)
+- [ ] Screenshot capture of X11/Wayland desktop
- [ ] CI/CD integration (GitHub Actions / GitLab CI)
- [ ] Performance benchmarking over time
-- [ ] Cloud-init based base image (faster provisioning)
** Test Scenarios
- [ ] Idempotency test (run archsetup twice)
@@ -502,7 +289,7 @@ poweroff
* References
- [[file:../../docs/testing-strategy.org][Testing Strategy Document]]
-- [[https://wiki.archlinux.org/title/Libvirt][Arch Wiki: libvirt]]
+- [[file:../../docs/archangel-vm-testing-guide.org][Archangel VM Testing Guide]]
- [[https://wiki.archlinux.org/title/QEMU][Arch Wiki: QEMU]]
- [[file:../../archsetup][Main archsetup script]]
-- [[file:../../TODO.org][Project TODO]]
+- [[file:../../todo.org][Project TODO]]
diff --git a/scripts/testing/archinstall-config.json b/scripts/testing/archinstall-config.json
deleted file mode 100644
index a55e2a1..0000000
--- a/scripts/testing/archinstall-config.json
+++ /dev/null
@@ -1,117 +0,0 @@
-{
- "!users": {
- "0": {
- "!password": "archsetup",
- "username": "root",
- "sudo": false
- }
- },
- "archinstall-language": "English",
- "audio_config": null,
- "bootloader": "systemd-bootctl",
- "config_version": "2.8.0",
- "debug": false,
- "disk_config": {
- "config_type": "default_layout",
- "device_modifications": [
- {
- "device": "/dev/vda",
- "partitions": [
- {
- "btrfs": [],
- "flags": [
- "Boot"
- ],
- "fs_type": "fat32",
- "length": {
- "sector_size": null,
- "total_size": null,
- "unit": "MiB",
- "value": 512
- },
- "mount_options": [],
- "mountpoint": "/boot",
- "obj_id": "boot_partition",
- "start": {
- "sector_size": null,
- "total_size": null,
- "unit": "MiB",
- "value": 1
- },
- "status": "create",
- "type": "primary"
- },
- {
- "btrfs": [],
- "flags": [],
- "fs_type": "ext4",
- "length": {
- "sector_size": null,
- "total_size": null,
- "unit": "MiB",
- "value": 100
- },
- "mount_options": [],
- "mountpoint": "/",
- "obj_id": "root_partition",
- "start": {
- "sector_size": null,
- "total_size": null,
- "unit": "MiB",
- "value": 513
- },
- "status": "create",
- "type": "primary"
- }
- ],
- "wipe": true
- }
- ]
- },
- "disk_encryption": null,
- "hostname": "archsetup-test",
- "kernels": [
- "linux"
- ],
- "locale_config": {
- "kb_layout": "us",
- "sys_enc": "UTF-8",
- "sys_lang": "en_US"
- },
- "mirror_config": {
- "custom_mirrors": [],
- "mirror_regions": {
- "United States": [
- "https://mirror.rackspace.com/archlinux/$repo/os/$arch",
- "https://mirror.leaseweb.com/archlinux/$repo/os/$arch"
- ]
- }
- },
- "network_config": {
- "type": "nm"
- },
- "no_pkg_lookups": false,
- "ntp": true,
- "offline": false,
- "packages": [
- "openssh",
- "dhcpcd",
- "git",
- "vim"
- ],
- "parallel downloads": 5,
- "profile_config": {
- "gfx_driver": "All open-source",
- "greeter": null,
- "profile": {
- "custom_settings": {},
- "details": [],
- "main": "Minimal"
- }
- },
- "script": "guided",
- "silent": false,
- "swap": false,
- "timezone": "America/Chicago",
- "version": "2.8.0"
-}
diff --git a/scripts/testing/archsetup-test.conf b/scripts/testing/archsetup-test.conf
new file mode 100644
index 0000000..d6a8d69
--- /dev/null
+++ b/scripts/testing/archsetup-test.conf
@@ -0,0 +1,14 @@
+# archsetup-test.conf - Archangel config for archsetup test VMs
+# Used by create-base-vm.sh for fully automated base VM creation
+#
+# Usage: archangel --config-file /root/archsetup-test.conf
+
+FILESYSTEM=btrfs
+HOSTNAME=archsetup-test
+TIMEZONE=America/Chicago
+LOCALE=en_US.UTF-8
+KEYMAP=us
+DISKS=/dev/vda
+NO_ENCRYPT=yes
+ROOT_PASSWORD=archsetup
+ENABLE_SSH=yes
diff --git a/scripts/testing/archsetup-vm.conf b/scripts/testing/archsetup-vm.conf
new file mode 100644
index 0000000..4117278
--- /dev/null
+++ b/scripts/testing/archsetup-vm.conf
@@ -0,0 +1,11 @@
+# archsetup-vm.conf - Config for running archsetup in test VMs
+# Used by run-test.sh for unattended archsetup execution
+#
+# Usage: ./archsetup --config-file /tmp/archsetup-test/archsetup-vm.conf
+
+USERNAME=cjennings
+PASSWORD=archsetup
+LOCALE=en_US.UTF-8
+DESKTOP_ENV=hyprland
+NO_GPU_DRIVERS=yes
+AUTOLOGIN=yes
diff --git a/scripts/testing/cleanup-tests.sh b/scripts/testing/cleanup-tests.sh
index e4289a7..fd2f8de 100755
--- a/scripts/testing/cleanup-tests.sh
+++ b/scripts/testing/cleanup-tests.sh
@@ -36,85 +36,86 @@ while [[ $# -gt 0 ]]; do
esac
done
-# Initialize logging
+# Initialize logging and VM paths
LOGFILE="/tmp/cleanup-tests-$(date +'%Y%m%d-%H%M%S').log"
init_logging "$LOGFILE"
+init_vm_paths "$PROJECT_ROOT/vm-images"
section "Cleaning Up Test Artifacts"
-# Find all test VMs
-step "Finding test VMs"
-TEST_VMS=$(virsh --connect qemu:///system list --all | grep "archsetup-test-" | awk '{print $2}' || true)
+# Find and stop running QEMU processes
+step "Checking for running QEMU processes"
-if [ -z "$TEST_VMS" ]; then
- info "No test VMs found"
-else
- VM_COUNT=$(echo "$TEST_VMS" | wc -l)
- info "Found $VM_COUNT test VM(s)"
-
- if ! $FORCE; then
- echo ""
- echo "$TEST_VMS"
- echo ""
- read -p "Destroy these VMs? [y/N] " -n 1 -r
+if vm_is_running; then
+ info "Found running QEMU test VM (PID: $(cat "$PID_FILE"))"
+ if $FORCE; then
+ stop_qemu
+ else
+ read -p "Stop running VM? [y/N] " -n 1 -r
echo ""
- if [[ ! $REPLY =~ ^[Yy]$ ]]; then
- info "Skipping VM cleanup"
+ if [[ $REPLY =~ ^[Yy]$ ]]; then
+ stop_qemu
else
- for vm in $TEST_VMS; do
- step "Destroying VM: $vm"
- if vm_is_running "$vm"; then
- virsh --connect qemu:///system destroy "$vm" >> "$LOGFILE" 2>&1
- fi
- virsh --connect qemu:///system undefine "$vm" --nvram >> "$LOGFILE" 2>&1 || true
- success "VM destroyed: $vm"
- done
+ info "Skipping VM shutdown"
fi
+ fi
+else
+ info "No running VM found"
+fi
+
+# Check for orphaned QEMU processes
+QEMU_PIDS=$(pgrep -f "qemu-system.*archsetup-test" 2>/dev/null || true)
+if [ -n "$QEMU_PIDS" ]; then
+ info "Found orphaned QEMU processes: $QEMU_PIDS"
+ if $FORCE; then
+ echo "$QEMU_PIDS" | xargs kill -9 2>/dev/null || true
+ success "Orphaned processes killed"
else
- for vm in $TEST_VMS; do
- step "Destroying VM: $vm"
- if vm_is_running "$vm"; then
- virsh --connect qemu:///system destroy "$vm" >> "$LOGFILE" 2>&1
- fi
- virsh --connect qemu:///system undefine "$vm" --nvram >> "$LOGFILE" 2>&1 || true
- success "VM destroyed: $vm"
- done
+ read -p "Kill orphaned QEMU processes? [y/N] " -n 1 -r
+ echo ""
+ if [[ $REPLY =~ ^[Yy]$ ]]; then
+ echo "$QEMU_PIDS" | xargs kill -9 2>/dev/null || true
+ success "Orphaned processes killed"
+ fi
fi
fi
-# Clean up test disk images
+# Clean up QEMU runtime files
+rm -f "$PID_FILE" "$MONITOR_SOCK"
+
+# Clean up debug overlay and test disk images
section "Cleaning Up Disk Images"
-step "Finding test disk images"
+step "Finding temporary disk images"
if [ -d "$PROJECT_ROOT/vm-images" ]; then
- TEST_DISKS=$(find "$PROJECT_ROOT/vm-images" -name "archsetup-test-*.qcow2" 2>/dev/null || true)
+ TEMP_DISKS=$(find "$PROJECT_ROOT/vm-images" -name "debug-overlay-*.qcow2" -o -name "archsetup-test-*.qcow2" 2>/dev/null || true)
- if [ -z "$TEST_DISKS" ]; then
- info "No test disk images found"
+ if [ -z "$TEMP_DISKS" ]; then
+ info "No temporary disk images found"
else
- DISK_COUNT=$(echo "$TEST_DISKS" | wc -l)
- DISK_SIZE=$(du -ch $TEST_DISKS | tail -1 | awk '{print $1}')
- info "Found $DISK_COUNT test disk image(s) totaling $DISK_SIZE"
+ DISK_COUNT=$(echo "$TEMP_DISKS" | wc -l)
+ DISK_SIZE=$(du -ch $TEMP_DISKS 2>/dev/null | tail -1 | awk '{print $1}')
+ info "Found $DISK_COUNT temporary disk image(s) totaling $DISK_SIZE"
- if ! $FORCE; then
+ if $FORCE; then
+ echo "$TEMP_DISKS" | while read disk; do
+ rm -f "$disk"
+ done
+ success "Temporary disk images deleted"
+ else
echo ""
- echo "$TEST_DISKS"
+ echo "$TEMP_DISKS"
echo ""
read -p "Delete these disk images? [y/N] " -n 1 -r
echo ""
- if [[ ! $REPLY =~ ^[Yy]$ ]]; then
- info "Skipping disk cleanup"
- else
- echo "$TEST_DISKS" | while read disk; do
+ if [[ $REPLY =~ ^[Yy]$ ]]; then
+ echo "$TEMP_DISKS" | while read disk; do
rm -f "$disk"
done
- success "Test disk images deleted"
+ success "Temporary disk images deleted"
+ else
+ info "Skipping disk cleanup"
fi
- else
- echo "$TEST_DISKS" | while read disk; do
- rm -f "$disk"
- done
- success "Test disk images deleted"
fi
fi
fi
@@ -141,26 +142,26 @@ else
DELETE_COUNT=$(echo "$TO_DELETE" | wc -l)
info "Keeping last $KEEP_LAST, deleting $DELETE_COUNT old result(s)"
- if ! $FORCE; then
+ if $FORCE; then
+ echo "$TO_DELETE" | while read dir; do
+ rm -rf "$dir"
+ done
+ success "Old test results deleted"
+ else
echo ""
echo "Will delete:"
echo "$TO_DELETE"
echo ""
read -p "Delete these test results? [y/N] " -n 1 -r
echo ""
- if [[ ! $REPLY =~ ^[Yy]$ ]]; then
- info "Skipping results cleanup"
- else
+ if [[ $REPLY =~ ^[Yy]$ ]]; then
echo "$TO_DELETE" | while read dir; do
rm -rf "$dir"
done
success "Old test results deleted"
+ else
+ info "Skipping results cleanup"
fi
- else
- echo "$TO_DELETE" | while read dir; do
- rm -rf "$dir"
- done
- success "Old test results deleted"
fi
fi
fi
diff --git a/scripts/testing/create-base-vm.sh b/scripts/testing/create-base-vm.sh
index 03409fe..7979bd2 100755
--- a/scripts/testing/create-base-vm.sh
+++ b/scripts/testing/create-base-vm.sh
@@ -1,10 +1,11 @@
#!/bin/bash
-# Create base VM for archsetup testing - Manual Installation
+# Create base VM for archsetup testing - Automated via Archangel ISO
# Author: Craig Jennings <craigmartinjennings@gmail.com>
# License: GNU GPLv3
#
-# This script creates a VM booted from Arch ISO, then waits for you to
-# manually install Arch using archinstall.
+# This script boots an archangel ISO in QEMU, copies a config file into the
+# live environment via SSH, and runs a fully unattended Arch Linux installation.
+# The result is a base VM disk with a "clean-install" snapshot ready for testing.
set -e
@@ -17,157 +18,139 @@ source "$SCRIPT_DIR/lib/logging.sh"
source "$SCRIPT_DIR/lib/vm-utils.sh"
# Configuration
-VM_NAME="archsetup-base"
-VM_CPUS="${VM_CPUS:-4}"
-VM_RAM="${VM_RAM:-8192}" # MB
-VM_DISK="${VM_DISK:-50}" # GB
VM_IMAGES_DIR="$PROJECT_ROOT/vm-images"
-ISO_URL="https://mirrors.kernel.org/archlinux/iso/latest/archlinux-x86_64.iso"
-ISO_PATH="$VM_IMAGES_DIR/arch-latest.iso"
-DISK_PATH="$VM_IMAGES_DIR/archsetup-base.qcow2"
+CONFIG_FILE="$SCRIPT_DIR/archsetup-test.conf"
+LIVE_ISO_PASSWORD="archzfs"
+SNAPSHOT_NAME="clean-install"
# Initialize logging
+mkdir -p "$PROJECT_ROOT/test-results"
LOGFILE="$PROJECT_ROOT/test-results/create-base-vm-$(date +'%Y%m%d-%H%M%S').log"
init_logging "$LOGFILE"
+init_vm_paths "$VM_IMAGES_DIR"
section "Creating Base VM for ArchSetup Testing"
-# Verify prerequisites
+# ─── Prerequisites ────────────────────────────────────────────────────
+
step "Checking prerequisites"
-check_libvirt || fatal "libvirt not running"
-check_libvirt_group || fatal "User not in libvirt group"
-check_kvm || fatal "KVM not available"
+check_prerequisites || fatal "Missing prerequisites"
success "Prerequisites satisfied"
-# Create vm-images directory
-mkdir -p "$VM_IMAGES_DIR"
+# Verify config file exists
+if [ ! -f "$CONFIG_FILE" ]; then
+ fatal "Config file not found: $CONFIG_FILE"
+fi
+
+# Find archangel ISO in vm-images/
+ISO_PATH=$(find "$VM_IMAGES_DIR" -maxdepth 1 -name "archzfs-*.iso" -type f 2>/dev/null | sort -V | tail -1)
+if [ -z "$ISO_PATH" ]; then
+ fatal "No archangel ISO found in $VM_IMAGES_DIR/"
+ info "Copy an archzfs-*.iso file to: $VM_IMAGES_DIR/"
+fi
+info "Using ISO: $(basename "$ISO_PATH")"
-# Download Arch ISO if needed
-section "Preparing Arch Linux ISO"
+# ─── Prepare Disk ─────────────────────────────────────────────────────
-if [ -f "$ISO_PATH" ]; then
- info "Arch ISO exists: $ISO_PATH"
+section "Preparing VM Disk"
- # Check if ISO is older than 30 days
- if [ $(find "$ISO_PATH" -mtime +30 | wc -l) -gt 0 ]; then
- warn "ISO is older than 30 days"
- info "Downloading latest version..."
- rm -f "$ISO_PATH"
- else
- success "Using existing ISO"
- fi
+# Remove old disk and OVMF vars if they exist
+if [ -f "$DISK_PATH" ]; then
+ warn "Removing existing disk: $DISK_PATH"
+ rm -f "$DISK_PATH"
fi
+rm -f "$OVMF_VARS"
+
+# Create fresh disk
+step "Creating ${VM_DISK_SIZE}G qcow2 disk"
+if qemu-img create -f qcow2 "$DISK_PATH" "${VM_DISK_SIZE}G" >> "$LOGFILE" 2>&1; then
+ success "Disk created: $DISK_PATH"
+else
+ fatal "Failed to create disk image"
+fi
+
+# ─── Phase 1: Install from ISO ───────────────────────────────────────
+
+section "Phase 1: Archangel Installation"
-if [ ! -f "$ISO_PATH" ]; then
- step "Downloading latest Arch ISO"
- info "URL: $ISO_URL"
- info "This may take several minutes..."
+start_timer "install"
- if wget --progress=dot:giga -O "$ISO_PATH" "$ISO_URL" 2>&1 | tee -a "$LOGFILE"; then
- success "ISO downloaded"
- else
- fatal "ISO download failed"
- fi
+# Boot from ISO
+start_qemu "$DISK_PATH" "iso" "$ISO_PATH" "none" || fatal "Failed to start QEMU"
+
+# Wait for live ISO SSH
+wait_for_ssh "$LIVE_ISO_PASSWORD" 120 || fatal "Live ISO SSH not available"
+
+# Copy config file into VM
+copy_to_vm "$CONFIG_FILE" "/root/archsetup-test.conf" "$LIVE_ISO_PASSWORD" || \
+ fatal "Failed to copy config to VM"
+
+# Run archangel installer (synchronous - typically 5-10 minutes)
+step "Running archangel installer (unattended)..."
+info "This will partition, install, and configure the base system"
+
+if vm_exec "$LIVE_ISO_PASSWORD" "archangel --config-file /root/archsetup-test.conf"; then
+ success "Archangel installation completed"
+else
+ error "Archangel installation failed"
+ step "Capturing serial log for debugging"
+ info "Serial log: $SERIAL_LOG"
+ fatal "Base VM installation failed - check logs"
fi
-# Remove existing VM and disk
-if vm_exists "$VM_NAME"; then
- warn "VM $VM_NAME already exists - destroying it"
- if vm_is_running "$VM_NAME"; then
- virsh destroy "$VM_NAME" >> "$LOGFILE" 2>&1
- fi
- virsh undefine "$VM_NAME" --nvram >> "$LOGFILE" 2>&1 || true
+# Power off
+stop_qemu
+stop_timer "install"
+
+# ─── Phase 2: Verify Installation ────────────────────────────────────
+
+section "Phase 2: Verifying Installation"
+
+start_timer "verify"
+
+# Boot from installed disk
+start_qemu "$DISK_PATH" "disk" "" "none" || fatal "Failed to boot installed system"
+
+# Wait for SSH on installed system
+wait_for_ssh "$ROOT_PASSWORD" 120 || fatal "Installed system SSH not available"
+
+# Basic verification
+step "Verifying base system"
+
+KERNEL=$(vm_exec "$ROOT_PASSWORD" "uname -r" 2>/dev/null)
+success "Kernel: $KERNEL"
+
+if vm_exec "$ROOT_PASSWORD" "systemctl is-active sshd" &>/dev/null; then
+ success "SSH service active"
+else
+ warn "SSH service not active"
fi
-[ -f "$DISK_PATH" ] && rm -f "$DISK_PATH"
-
-# Create and start VM
-section "Creating and Starting VM"
-
-info "Creating VM: $VM_NAME"
-info " CPUs: $VM_CPUS | RAM: ${VM_RAM}MB | Disk: ${VM_DISK}GB"
-
-virt-install \
- --connect qemu:///system \
- --name "$VM_NAME" \
- --memory "$VM_RAM" \
- --vcpus "$VM_CPUS" \
- --disk path="$DISK_PATH",size="$VM_DISK",format=qcow2,bus=virtio \
- --cdrom "$ISO_PATH" \
- --os-variant archlinux \
- --network network=default,model=virtio \
- --graphics vnc,listen=127.0.0.1 \
- --console pty,target.type=serial \
- --boot uefi \
- --noreboot \
- --check path_in_use=off \
- --filesystem type=mount,mode=mapped,source="$PROJECT_ROOT/scripts",target=host-scripts \
- >> "$LOGFILE" 2>&1 &
-
-VIRT_INSTALL_PID=$!
-
-progress "Waiting for VM to boot from ISO"
-sleep 30
-
-# Check if VM started
-if ! vm_is_running "$VM_NAME"; then
- wait $VIRT_INSTALL_PID
- EXIT_CODE=$?
- fatal "VM failed to start (exit code: $EXIT_CODE)"
+if vm_exec "$ROOT_PASSWORD" "systemctl is-active NetworkManager" &>/dev/null; then
+ success "NetworkManager active"
+else
+ warn "NetworkManager not active"
fi
-success "VM started successfully"
-
-# Display manual installation instructions
-section "Manual Installation Required"
-
-cat << 'EOF'
-
-[i]
-[i] Base VM is running from Arch ISO
-[i]
-[i] NEXT STEPS - Complete installation manually:
-[i]
-[i] 1. Open virt-viewer (should already be open):
-[i] virt-viewer --connect qemu:///system archsetup-base
-[i]
-[i] 2. Login as 'root' (no password)
-[i]
-[i] 3. Run: archinstall
-[i]
-[i] 4. Configure with these settings:
-[i] - Hostname: archsetup-test
-[i] - Root password: archsetup
-[i] - Profile: minimal
-[i] - Network: dhcpcd (or NetworkManager)
-[i] - Additional packages: openssh git vim sudo iperf3 mtr traceroute bind net-tools sshfs
-[i] - Enable: sshd, dhcpcd (or NetworkManager)
-[i]
-[i] 5. After archinstall completes:
-[i] - Chroot into /mnt: arch-chroot /mnt
-[i] - Edit /etc/ssh/sshd_config:
-[i] sed -i 's/#PermitRootLogin.*/PermitRootLogin yes/' /etc/ssh/sshd_config
-[i] sed -i 's/#PasswordAuthentication.*/PasswordAuthentication yes/' /etc/ssh/sshd_config
-[i] - Set up shared folder mount (9p filesystem):
-[i] mkdir -p /mnt/host-scripts
-[i] echo 'host-scripts /mnt/host-scripts 9p trans=virtio,version=9p2000.L,rw 0 0' >> /etc/fstab
-[i] - Exit chroot: exit
-[i] - Poweroff: poweroff
-[i]
-[i] 6. After VM powers off, run:
-[i] ./scripts/testing/finalize-base-vm.sh
-[i]
-[i] Log file: $LOGFILE
-[i]
-
-EOF
-
-info "Waiting for VM to power off..."
-info "(This script will exit when you manually power off the VM)"
-
-# Wait for virt-install to finish (VM powers off)
-wait $VIRT_INSTALL_PID || true
-
-success "VM has powered off"
+# Power off for snapshot
+stop_qemu
+stop_timer "verify"
+
+# ─── Phase 3: Create Snapshot ────────────────────────────────────────
+
+section "Phase 3: Creating Clean-Install Snapshot"
+
+create_snapshot "$DISK_PATH" "$SNAPSHOT_NAME" || fatal "Failed to create snapshot"
+
+# ─── Done ─────────────────────────────────────────────────────────────
+
+section "Base VM Created Successfully"
+
+info ""
+info " Disk: $DISK_PATH"
+info " Snapshot: $SNAPSHOT_NAME"
+info " Config: $(basename "$CONFIG_FILE")"
+info " Log: $LOGFILE"
+info ""
+info "Next step: Run ./scripts/testing/run-test.sh"
info ""
-info "Next step: Run ./scripts/testing/finalize-base-vm.sh"
diff --git a/scripts/testing/debug-vm.sh b/scripts/testing/debug-vm.sh
index a442850..5b2b197 100755
--- a/scripts/testing/debug-vm.sh
+++ b/scripts/testing/debug-vm.sh
@@ -2,6 +2,9 @@
# Launch VM for interactive debugging
# Author: Craig Jennings <craigmartinjennings@gmail.com>
# License: GNU GPLv3
+#
+# Launches a QEMU VM with a graphical display for interactive debugging.
+# Uses a copy-on-write overlay when using --base to protect the base image.
set -e
@@ -27,21 +30,23 @@ else
echo "Usage: $0 [disk-image.qcow2 | --base]"
echo ""
echo "Options:"
- echo " --base Use base VM (read-only, safe for testing)"
- echo " disk-image.qcow2 Use existing test disk image"
- echo " (no args) Clone base VM for debugging"
+ echo " --base Use base VM via copy-on-write overlay (safe)"
+ echo " disk-image.qcow2 Use existing test disk image directly"
+ echo " (no args) Same as --base"
exit 1
fi
# Configuration
TIMESTAMP=$(date +'%Y%m%d-%H%M%S')
-DEBUG_VM_NAME="archsetup-debug-$TIMESTAMP"
-BASE_DISK="$PROJECT_ROOT/vm-images/archsetup-base.qcow2"
+VM_IMAGES_DIR="$PROJECT_ROOT/vm-images"
+BASE_DISK="$VM_IMAGES_DIR/archsetup-base.qcow2"
ROOT_PASSWORD="archsetup"
+OVERLAY_DISK=""
-# Initialize logging
+# Initialize logging and VM paths
LOGFILE="/tmp/debug-vm-$TIMESTAMP.log"
init_logging "$LOGFILE"
+init_vm_paths "$VM_IMAGES_DIR"
section "Launching Debug VM"
@@ -50,84 +55,55 @@ if $USE_BASE; then
if [ ! -f "$BASE_DISK" ]; then
fatal "Base disk not found: $BASE_DISK"
fi
- VM_DISK="$BASE_DISK"
- info "Using base VM (read-only snapshot mode)"
-else
- if [ -z "$VM_DISK" ]; then
- # Clone base VM
- VM_DISK="$PROJECT_ROOT/vm-images/$DEBUG_VM_NAME.qcow2"
- step "Cloning base VM for debugging"
- clone_disk "$BASE_DISK" "$VM_DISK" || fatal "Failed to clone base VM"
- success "Debug disk created: $VM_DISK"
+
+ # Create a copy-on-write overlay (instant, protects base image)
+ OVERLAY_DISK="$VM_IMAGES_DIR/debug-overlay-$TIMESTAMP.qcow2"
+ step "Creating copy-on-write overlay"
+ if qemu-img create -f qcow2 -b "$BASE_DISK" -F qcow2 "$OVERLAY_DISK" >> "$LOGFILE" 2>&1; then
+ success "Overlay created: $(basename "$OVERLAY_DISK")"
else
- info "Using existing disk: $VM_DISK"
+ fatal "Failed to create overlay disk"
fi
+ VM_DISK="$OVERLAY_DISK"
+else
+ info "Using existing disk: $VM_DISK"
fi
-# Create debug VM
-step "Creating debug VM: $DEBUG_VM_NAME"
-virt-install \
- --connect qemu:///system \
- --name "$DEBUG_VM_NAME" \
- --memory 4096 \
- --vcpus 2 \
- --disk path="$VM_DISK",format=qcow2,bus=virtio \
- --os-variant archlinux \
- --network network=default,model=virtio \
- --graphics vnc,listen=127.0.0.1 \
- --console pty,target.type=serial \
- --boot uefi \
- --import \
- --noautoconsole \
- >> "$LOGFILE" 2>&1
-
-success "Debug VM created"
-
-# Wait for boot
-step "Waiting for VM to boot..."
-sleep 20
-
-# Get VM IP
-VM_IP=$(get_vm_ip "$DEBUG_VM_NAME" 2>/dev/null || true)
+# If snapshot exists, restore it first (only for non-overlay disks)
+if [ -z "$OVERLAY_DISK" ] && snapshot_exists "$VM_DISK" "clean-install"; then
+ step "Restoring clean-install snapshot"
+ restore_snapshot "$VM_DISK" "clean-install"
+fi
+
+# Launch QEMU with graphical display
+step "Starting QEMU with graphical display"
+start_qemu "$VM_DISK" "disk" "" "gtk" || fatal "Failed to start QEMU"
# Display connection information
section "Debug VM Ready"
info ""
-info "VM Name: $DEBUG_VM_NAME"
-if [ -n "$VM_IP" ]; then
- info "IP Address: $VM_IP"
-fi
-info "Disk: $VM_DISK"
+info " Disk: $(basename "$VM_DISK")"
+info " SSH: sshpass -p '$ROOT_PASSWORD' ssh -p $SSH_PORT root@localhost"
+info " Root password: $ROOT_PASSWORD"
info ""
-info "Connect via:"
-info " Console: virsh console $DEBUG_VM_NAME"
-if [ -n "$VM_IP" ]; then
- info " SSH: ssh root@$VM_IP"
-fi
-info " VNC: virt-viewer $DEBUG_VM_NAME"
-info ""
-info "Root password: $ROOT_PASSWORD"
-info ""
-info "When done debugging:"
-info " virsh destroy $DEBUG_VM_NAME"
-info " virsh undefine $DEBUG_VM_NAME"
-if [ ! "$VM_DISK" = "$BASE_DISK" ] && [ -z "$1" ]; then
- info " rm $VM_DISK"
-fi
-info ""
-info "Log file: $LOGFILE"
+info " The GTK window should be open. Close it to stop the VM."
+info " Log file: $LOGFILE"
info ""
-# Offer to connect to console
-read -p "Connect to console now? [Y/n] " -n 1 -r
-echo ""
-if [[ $REPLY =~ ^[Nn]$ ]]; then
- info "VM is running in background"
- info "Connect later with: virsh console $DEBUG_VM_NAME"
-else
- info "Connecting to console..."
- info "Press Ctrl+] to disconnect from console"
+# Wait for QEMU to exit (user closes GTK window)
+step "Waiting for VM to exit..."
+while vm_is_running; do
sleep 2
- virsh console "$DEBUG_VM_NAME"
+done
+success "VM has stopped"
+
+# Clean up overlay disk
+if [ -n "$OVERLAY_DISK" ] && [ -f "$OVERLAY_DISK" ]; then
+ step "Removing overlay disk"
+ rm -f "$OVERLAY_DISK"
+ success "Overlay cleaned up"
fi
+
+_cleanup_qemu_files
+info "Debug session complete"
diff --git a/scripts/testing/finalize-base-vm.sh b/scripts/testing/finalize-base-vm.sh
deleted file mode 100755
index 225ffae..0000000
--- a/scripts/testing/finalize-base-vm.sh
+++ /dev/null
@@ -1,31 +0,0 @@
-#!/bin/bash
-# Finalize base VM after installation
-VM_NAME="archsetup-base"
-SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
-PROJECT_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
-BASE_DISK="$PROJECT_ROOT/vm-images/archsetup-base.qcow2"
-
-echo "[i] Removing ISO from VM..."
-virsh --connect qemu:///system change-media $VM_NAME sda --eject 2>/dev/null || true
-virsh --connect qemu:///system change-media $VM_NAME hda --eject 2>/dev/null || true
-echo "[✓] ISO removed"
-
-echo "[i] Fixing base disk permissions..."
-sudo chown $USER:$USER "$BASE_DISK"
-sudo chmod 644 "$BASE_DISK"
-echo "[✓] Permissions fixed"
-
-echo "[i] Starting VM from installed system..."
-virsh --connect qemu:///system start $VM_NAME
-echo "[i] Waiting for boot..."
-sleep 30
-IP=$(virsh --connect qemu:///system domifaddr $VM_NAME 2>/dev/null | grep -oP '(\d+\.){3}\d+' | head -1)
-echo "[✓] Base VM is ready!"
-echo ""
-echo "Connect via:"
-echo " Console: virsh console $VM_NAME"
-echo " SSH: ssh root@$IP"
-echo " Password: archsetup"
-echo ""
-echo "To create a test clone:"
-echo " ./scripts/testing/run-test.sh"
diff --git a/scripts/testing/lib/network-diagnostics.sh b/scripts/testing/lib/network-diagnostics.sh
index 3f9735b..f7cae11 100644
--- a/scripts/testing/lib/network-diagnostics.sh
+++ b/scripts/testing/lib/network-diagnostics.sh
@@ -3,27 +3,30 @@
# Author: Craig Jennings <craigmartinjennings@gmail.com>
# License: GNU GPLv3
-# Note: logging.sh should already be sourced by the calling script
+# Note: logging.sh and vm-utils.sh should already be sourced by the calling script
+# Uses globals: ROOT_PASSWORD, SSH_PORT, SSH_OPTS, VM_IP (from vm-utils.sh or calling script)
# Run quick network diagnostics
-# Args: $1 = VM IP address or hostname
run_network_diagnostics() {
- local vm_host="$1"
+ local password="${ROOT_PASSWORD:-archsetup}"
+ local port="${SSH_PORT:-22}"
+ local host="${VM_IP:-localhost}"
+ local ssh_base="sshpass -p $password ssh $SSH_OPTS -p $port root@$host"
section "Pre-flight Network Diagnostics"
- # Test 1: Basic connectivity
+ # Test 1: Basic connectivity (use curl instead of ping - SLIRP may not handle ICMP)
step "Testing internet connectivity"
- if sshpass -p 'archsetup' ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null root@$vm_host "ping -c 3 8.8.8.8 >/dev/null 2>&1"; then
+ if $ssh_base "curl -s --connect-timeout 5 -o /dev/null http://archlinux.org" 2>/dev/null; then
success "Internet connectivity OK"
else
error "No internet connectivity"
return 1
fi
- # Test 2: DNS resolution
+ # Test 2: DNS resolution (use getent which is always available, unlike nslookup/dig)
step "Testing DNS resolution"
- if sshpass -p 'archsetup' ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null root@$vm_host "nslookup archlinux.org >/dev/null 2>&1"; then
+ if $ssh_base "getent hosts archlinux.org >/dev/null 2>&1" 2>/dev/null; then
success "DNS resolution OK"
else
error "DNS resolution failed"
@@ -32,7 +35,7 @@ run_network_diagnostics() {
# Test 3: Arch mirror accessibility
step "Testing Arch mirror access"
- if sshpass -p 'archsetup' ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null root@$vm_host "curl -s -I https://mirrors.kernel.org/archlinux/ | head -1 | grep -qE '(200|301)'"; then
+ if $ssh_base "curl -s -I https://mirrors.kernel.org/archlinux/ | head -1 | grep -qE '(200|301)'" 2>/dev/null; then
success "Arch mirrors accessible"
else
error "Cannot reach Arch mirrors"
@@ -41,7 +44,7 @@ run_network_diagnostics() {
# Test 4: AUR accessibility
step "Testing AUR access"
- if sshpass -p 'archsetup' ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null root@$vm_host "curl -s -I https://aur.archlinux.org/ | head -1 | grep -qE '(200|405)'"; then
+ if $ssh_base "curl -s -I https://aur.archlinux.org/ | head -1 | grep -qE '(200|405)'" 2>/dev/null; then
success "AUR accessible"
else
error "Cannot reach AUR"
@@ -50,8 +53,7 @@ run_network_diagnostics() {
# Show network info
info "Network configuration:"
- sshpass -p 'archsetup' ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null root@$vm_host \
- "ip addr show | grep 'inet ' | grep -v '127.0.0.1'" 2>/dev/null | while read line; do
+ $ssh_base "ip addr show | grep 'inet ' | grep -v '127.0.0.1'" 2>/dev/null | while read line; do
info " $line"
done
diff --git a/scripts/testing/lib/validation.sh b/scripts/testing/lib/validation.sh
index 8c4787e..3191c64 100644
--- a/scripts/testing/lib/validation.sh
+++ b/scripts/testing/lib/validation.sh
@@ -20,7 +20,7 @@ declare -a UNKNOWN_ISSUES
# SSH helper (uses globals: VM_IP, ROOT_PASSWORD)
ssh_cmd() {
sshpass -p "$ROOT_PASSWORD" ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null \
- -o ConnectTimeout=10 "root@$VM_IP" "$@" 2>/dev/null
+ -o ConnectTimeout=10 -p "${SSH_PORT:-22}" "root@$VM_IP" "$@" 2>/dev/null
}
# Validation result helpers
@@ -458,17 +458,12 @@ validate_hyprland_tools() {
local missing=""
# Check core Hyprland packages
- for pkg in hyprland hypridle hyprlock waybar wofi swww grim slurp gammastep; do
+ for pkg in hyprland hypridle hyprlock waybar fuzzel swww grim slurp gammastep foot; do
if ! ssh_cmd "pacman -Q $pkg &>/dev/null"; then
missing="$missing $pkg"
fi
done
- # st should still be installed (via XWayland)
- if ! ssh_cmd "test -f /usr/local/bin/st"; then
- missing="$missing st"
- fi
-
if [ -z "$missing" ]; then
validation_pass "All Hyprland tools installed"
else
@@ -483,7 +478,7 @@ validate_hyprland_config() {
for config in ".config/hypr/hyprland.conf" ".config/hypr/hypridle.conf" \
".config/hypr/hyprlock.conf" ".config/waybar/config" \
- ".config/wofi/config" ".config/gammastep/config.ini"; do
+ ".config/fuzzel/fuzzel.ini" ".config/gammastep/config.ini"; do
if ! ssh_cmd "test -f /home/cjennings/$config"; then
missing="$missing $config"
fi
diff --git a/scripts/testing/lib/vm-utils.sh b/scripts/testing/lib/vm-utils.sh
index 81aec33..47bd391 100755
--- a/scripts/testing/lib/vm-utils.sh
+++ b/scripts/testing/lib/vm-utils.sh
@@ -1,39 +1,52 @@
#!/bin/bash
-# VM management utilities for archsetup testing
+# VM management utilities for archsetup testing (direct QEMU)
# Author: Craig Jennings <craigmartinjennings@gmail.com>
# License: GNU GPLv3
+#
+# Manages QEMU VMs directly without libvirt. Uses user-mode networking
+# with port forwarding for SSH access and qemu-img for snapshots.
# Note: logging.sh should already be sourced by the calling script
# VM configuration defaults
VM_CPUS="${VM_CPUS:-4}"
-VM_RAM="${VM_RAM:-8192}" # MB
-VM_DISK="${VM_DISK:-50}" # GB
-VM_NETWORK="${VM_NETWORK:-default}"
-LIBVIRT_URI="qemu:///system" # Use system session, not user session
-
-# Check if libvirt is running
-check_libvirt() {
- if ! systemctl is-active --quiet libvirtd; then
- error "libvirtd service is not running"
- info "Start it with: sudo systemctl start libvirtd"
- return 1
- fi
- return 0
+VM_RAM="${VM_RAM:-4096}" # MB
+VM_DISK_SIZE="${VM_DISK_SIZE:-50}" # GB
+
+# SSH configuration
+SSH_PORT="${SSH_PORT:-2222}"
+SSH_OPTS="-o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o ConnectTimeout=10"
+ROOT_PASSWORD="${ROOT_PASSWORD:-archsetup}"
+
+# OVMF firmware paths
+OVMF_CODE="/usr/share/edk2/x64/OVMF_CODE.4m.fd"
+OVMF_VARS_TEMPLATE="/usr/share/edk2/x64/OVMF_VARS.4m.fd"
+
+# VM runtime paths (set by init_vm_paths)
+VM_IMAGES_DIR=""
+DISK_PATH=""
+OVMF_VARS=""
+PID_FILE=""
+MONITOR_SOCK=""
+SERIAL_LOG=""
+
+# Initialize all VM paths from images directory
+# Must be called before any other vm-utils function
+init_vm_paths() {
+ local images_dir="${1:-$VM_IMAGES_DIR}"
+ [ -z "$images_dir" ] && fatal "VM_IMAGES_DIR not set"
+
+ VM_IMAGES_DIR="$images_dir"
+ DISK_PATH="$VM_IMAGES_DIR/archsetup-base.qcow2"
+ OVMF_VARS="$VM_IMAGES_DIR/OVMF_VARS.fd"
+ PID_FILE="$VM_IMAGES_DIR/qemu.pid"
+ MONITOR_SOCK="$VM_IMAGES_DIR/qemu-monitor.sock"
+ SERIAL_LOG="$VM_IMAGES_DIR/qemu-serial.log"
+ mkdir -p "$VM_IMAGES_DIR"
}
-# Check if user is in libvirt group
-check_libvirt_group() {
- if ! groups | grep -q libvirt; then
- warn "Current user is not in libvirt group"
- info "Add yourself with: sudo usermod -a -G libvirt $USER"
- info "Then log out and back in for changes to take effect"
- return 1
- fi
- return 0
-}
+# ─── Prerequisite Checks ─────────────────────────────────────────────
-# Check if KVM is available
check_kvm() {
if [ ! -e /dev/kvm ]; then
error "KVM is not available"
@@ -44,249 +57,311 @@ check_kvm() {
return 0
}
-# Wait for VM to boot (check for SSH or serial console)
-wait_for_vm() {
- local vm_name="$1"
- local timeout="${2:-300}" # 5 minutes default
- local elapsed=0
+check_qemu() {
+ if ! command -v qemu-system-x86_64 &>/dev/null; then
+ error "qemu-system-x86_64 not found"
+ info "Install with: sudo pacman -S qemu-full"
+ return 1
+ fi
+ return 0
+}
- progress "Waiting for VM $vm_name to boot..."
+check_ovmf() {
+ if [ ! -f "$OVMF_CODE" ]; then
+ error "OVMF firmware not found: $OVMF_CODE"
+ info "Install with: sudo pacman -S edk2-ovmf"
+ return 1
+ fi
+ return 0
+}
- while [ $elapsed -lt $timeout ]; do
- if virsh --connect "$LIBVIRT_URI" domstate "$vm_name" 2>/dev/null | grep -q "running"; then
- sleep 5
- complete "VM $vm_name is running"
- return 0
- fi
- sleep 2
- elapsed=$((elapsed + 2))
- done
+check_sshpass() {
+ if ! command -v sshpass &>/dev/null; then
+ error "sshpass not found"
+ info "Install with: sudo pacman -S sshpass"
+ return 1
+ fi
+ return 0
+}
- error "Timeout waiting for VM $vm_name to boot"
- return 1
+check_socat() {
+ if ! command -v socat &>/dev/null; then
+ error "socat not found"
+ info "Install with: sudo pacman -S socat"
+ return 1
+ fi
+ return 0
}
-# Check if VM exists
-vm_exists() {
- local vm_name="$1"
- virsh --connect "$LIBVIRT_URI" dominfo "$vm_name" &>/dev/null
- return $?
+check_prerequisites() {
+ local failed=0
+ check_kvm || failed=1
+ check_qemu || failed=1
+ check_ovmf || failed=1
+ check_sshpass || failed=1
+ check_socat || failed=1
+ return $failed
}
-# Check if VM is running
+# ─── VM Lifecycle ─────────────────────────────────────────────────────
+
+# Check if a QEMU VM is running via PID file
vm_is_running() {
- local vm_name="$1"
- [ "$(virsh --connect "$LIBVIRT_URI" domstate "$vm_name" 2>/dev/null)" = "running" ]
- return $?
-}
+ [ -f "$PID_FILE" ] || return 1
-# Start VM
-start_vm() {
- local vm_name="$1"
+ local pid
+ pid=$(cat "$PID_FILE" 2>/dev/null) || return 1
- if vm_is_running "$vm_name"; then
- warn "VM $vm_name is already running"
+ if kill -0 "$pid" 2>/dev/null && grep -q "qemu" "/proc/$pid/cmdline" 2>/dev/null; then
return 0
fi
- step "Starting VM: $vm_name"
- if virsh --connect "$LIBVIRT_URI" start "$vm_name" >> "$LOGFILE" 2>&1; then
- success "VM $vm_name started"
- return 0
- else
- error "Failed to start VM $vm_name"
+ # Stale PID file
+ rm -f "$PID_FILE"
+ return 1
+}
+
+# Start a QEMU VM
+# Args: $1 = disk path
+# $2 = boot mode: "iso" or "disk" (default: disk)
+# $3 = ISO path (required if mode=iso)
+# $4 = display: "none" (headless) or "gtk" (graphical, default: none)
+start_qemu() {
+ local disk="$1"
+ local mode="${2:-disk}"
+ local iso_path="${3:-}"
+ local display="${4:-none}"
+
+ # Stop any existing instance
+ stop_qemu 2>/dev/null || true
+
+ # Check port availability
+ if ss -tln 2>/dev/null | grep -q ":${SSH_PORT} "; then
+ error "Port $SSH_PORT is already in use"
+ info "Another QEMU instance or service may be running"
return 1
fi
-}
-# Stop VM gracefully
-stop_vm() {
- local vm_name="$1"
- local timeout="${2:-60}"
+ # Ensure OVMF_VARS exists
+ if [ ! -f "$OVMF_VARS" ]; then
+ cp "$OVMF_VARS_TEMPLATE" "$OVMF_VARS"
+ fi
+
+ # Truncate serial log
+ : > "$SERIAL_LOG"
+
+ # Build QEMU command
+ local qemu_cmd=(
+ qemu-system-x86_64
+ -name "archsetup-test"
+ -machine "q35,accel=kvm"
+ -cpu host
+ -smp "$VM_CPUS"
+ -m "$VM_RAM"
+ -drive "if=pflash,format=raw,readonly=on,file=$OVMF_CODE"
+ -drive "if=pflash,format=raw,file=$OVMF_VARS"
+ -drive "file=$disk,format=qcow2,if=virtio"
+ -netdev "user,id=net0,hostfwd=tcp::${SSH_PORT}-:22"
+ -device "virtio-net-pci,netdev=net0"
+ -monitor "unix:$MONITOR_SOCK,server,nowait"
+ -pidfile "$PID_FILE"
+ -serial "file:$SERIAL_LOG"
+ -usb
+ -device usb-tablet
+ )
+
+ # Boot mode
+ if [ "$mode" = "iso" ]; then
+ [ -z "$iso_path" ] && fatal "ISO path required for iso boot mode"
+ qemu_cmd+=(-cdrom "$iso_path" -boot d)
+ else
+ qemu_cmd+=(-boot c)
+ fi
- if ! vm_is_running "$vm_name"; then
- info "VM $vm_name is not running"
- return 0
+ # Display mode
+ if [ "$display" = "gtk" ]; then
+ qemu_cmd+=(-device virtio-vga-gl -display "gtk,gl=on")
+ else
+ qemu_cmd+=(-display none)
fi
- step "Shutting down VM: $vm_name"
- if virsh --connect "$LIBVIRT_URI" shutdown "$vm_name" >> "$LOGFILE" 2>&1; then
- # Wait for graceful shutdown
- local elapsed=0
- while [ $elapsed -lt $timeout ]; do
- if ! vm_is_running "$vm_name"; then
- success "VM $vm_name stopped gracefully"
- return 0
- fi
- sleep 2
- elapsed=$((elapsed + 2))
- done
-
- warn "VM $vm_name did not stop gracefully, forcing..."
- virsh --connect "$LIBVIRT_URI" destroy "$vm_name" >> "$LOGFILE" 2>&1
+ step "Starting QEMU (mode=$mode, display=$display)"
+
+ # Launch in background
+ "${qemu_cmd[@]}" &>> "$LOGFILE" &
+
+ # Wait for PID file to appear
+ local wait=0
+ while [ ! -f "$PID_FILE" ] && [ $wait -lt 10 ]; do
+ sleep 1
+ wait=$((wait + 1))
+ done
+
+ if ! vm_is_running; then
+ error "QEMU failed to start"
+ return 1
fi
- success "VM $vm_name stopped"
+ success "QEMU started (PID: $(cat "$PID_FILE"))"
return 0
}
-# Destroy VM (force stop)
-destroy_vm() {
- local vm_name="$1"
+# Stop VM gracefully via ACPI powerdown, fallback to kill
+stop_qemu() {
+ local timeout="${1:-60}"
- if ! vm_exists "$vm_name"; then
- info "VM $vm_name does not exist"
+ if ! vm_is_running; then
return 0
fi
- step "Destroying VM: $vm_name"
- if vm_is_running "$vm_name"; then
- virsh --connect "$LIBVIRT_URI" destroy "$vm_name" >> "$LOGFILE" 2>&1
+ step "Sending shutdown signal to VM"
+
+ # Send ACPI powerdown via monitor socket
+ if [ -S "$MONITOR_SOCK" ]; then
+ echo "system_powerdown" | socat - "UNIX-CONNECT:$MONITOR_SOCK" >> "$LOGFILE" 2>&1 || true
fi
- virsh --connect "$LIBVIRT_URI" undefine "$vm_name" --nvram >> "$LOGFILE" 2>&1
- success "VM $vm_name destroyed"
+ # Wait for graceful shutdown
+ local elapsed=0
+ while [ $elapsed -lt $timeout ]; do
+ if ! vm_is_running; then
+ success "VM stopped gracefully"
+ _cleanup_qemu_files
+ return 0
+ fi
+ sleep 2
+ elapsed=$((elapsed + 2))
+ done
+
+ # Force kill
+ warn "VM did not stop gracefully after ${timeout}s, force killing"
+ kill_qemu
return 0
}
-# Create snapshot
+# Force kill VM immediately
+kill_qemu() {
+ if [ -f "$PID_FILE" ]; then
+ local pid
+ pid=$(cat "$PID_FILE" 2>/dev/null)
+ if [ -n "$pid" ]; then
+ kill -9 "$pid" 2>/dev/null || true
+ fi
+ fi
+ _cleanup_qemu_files
+}
+
+# Clean up runtime files
+_cleanup_qemu_files() {
+ rm -f "$PID_FILE" "$MONITOR_SOCK"
+}
+
+# ─── Snapshot Operations (qemu-img) ──────────────────────────────────
+# All snapshot operations require the VM to be stopped.
+
create_snapshot() {
- local vm_name="$1"
- local snapshot_name="$2"
+ local disk="${1:-$DISK_PATH}"
+ local snapshot_name="${2:-clean-install}"
+
+ if vm_is_running; then
+ error "Cannot create snapshot while VM is running"
+ return 1
+ fi
step "Creating snapshot: $snapshot_name"
- if virsh --connect "$LIBVIRT_URI" snapshot-create-as "$vm_name" "$snapshot_name" >> "$LOGFILE" 2>&1; then
- success "Snapshot $snapshot_name created"
+ if qemu-img snapshot -c "$snapshot_name" "$disk" >> "$LOGFILE" 2>&1; then
+ success "Snapshot '$snapshot_name' created"
return 0
else
- error "Failed to create snapshot $snapshot_name"
+ error "Failed to create snapshot '$snapshot_name'"
return 1
fi
}
-# Restore snapshot
restore_snapshot() {
- local vm_name="$1"
- local snapshot_name="$2"
+ local disk="${1:-$DISK_PATH}"
+ local snapshot_name="${2:-clean-install}"
+
+ if vm_is_running; then
+ error "Cannot restore snapshot while VM is running"
+ return 1
+ fi
step "Restoring snapshot: $snapshot_name"
- if virsh --connect "$LIBVIRT_URI" snapshot-revert "$vm_name" "$snapshot_name" >> "$LOGFILE" 2>&1; then
- success "Snapshot $snapshot_name restored"
+ if qemu-img snapshot -a "$snapshot_name" "$disk" >> "$LOGFILE" 2>&1; then
+ success "Snapshot '$snapshot_name' restored"
return 0
else
- error "Failed to restore snapshot $snapshot_name"
+ error "Failed to restore snapshot '$snapshot_name'"
return 1
fi
}
-# Delete snapshot
delete_snapshot() {
- local vm_name="$1"
- local snapshot_name="$2"
+ local disk="${1:-$DISK_PATH}"
+ local snapshot_name="${2:-clean-install}"
step "Deleting snapshot: $snapshot_name"
- if virsh --connect "$LIBVIRT_URI" snapshot-delete "$vm_name" "$snapshot_name" >> "$LOGFILE" 2>&1; then
- success "Snapshot $snapshot_name deleted"
+ if qemu-img snapshot -d "$snapshot_name" "$disk" >> "$LOGFILE" 2>&1; then
+ success "Snapshot '$snapshot_name' deleted"
return 0
else
- error "Failed to delete snapshot $snapshot_name"
+ error "Failed to delete snapshot '$snapshot_name'"
return 1
fi
}
-# Clone disk image (copy-on-write)
-clone_disk() {
- local base_image="$1"
- local new_image="$2"
-
- if [ ! -f "$base_image" ]; then
- error "Base image not found: $base_image"
- return 1
- fi
-
- step "Cloning disk image (full copy)"
- if qemu-img convert -f qcow2 -O qcow2 "$base_image" "$new_image" >> "$LOGFILE" 2>&1; then
- success "Disk cloned: $new_image"
- else
- error "Failed to clone disk"
- return 1
- fi
-
- # Truncate machine-id so systemd generates a new one on boot (avoids DHCP conflicts)
- step "Clearing machine-id for unique network identity"
- if guestfish -a "$new_image" -i truncate /etc/machine-id >> "$LOGFILE" 2>&1; then
- success "Machine-ID cleared (will regenerate on boot)"
- return 0
- else
- warn "Failed to clear machine-ID (guestfish failed)"
- info "Network may conflict with base VM if both run simultaneously"
- return 0 # Don't fail the whole operation
- fi
+list_snapshots() {
+ local disk="${1:-$DISK_PATH}"
+ qemu-img snapshot -l "$disk" 2>/dev/null
}
-# Get VM IP address (requires guest agent or DHCP lease)
-get_vm_ip() {
- local vm_name="$1"
+snapshot_exists() {
+ local disk="${1:-$DISK_PATH}"
+ local snapshot_name="${2:-clean-install}"
+ qemu-img snapshot -l "$disk" 2>/dev/null | grep -q "$snapshot_name"
+}
- # Try guest agent first
- local ip
- ip=$(virsh --connect "$LIBVIRT_URI" domifaddr "$vm_name" 2>/dev/null | grep -oP '(\d+\.){3}\d+' | head -1)
+# ─── SSH Operations ───────────────────────────────────────────────────
- if [ -n "$ip" ]; then
- echo "$ip"
- return 0
- fi
-
- # Fall back to DHCP leases
- local mac
- mac=$(virsh --connect "$LIBVIRT_URI" domiflist "$vm_name" | grep -oP '([0-9a-f]{2}:){5}[0-9a-f]{2}' | head -1)
+# Wait for SSH to become available on localhost:$SSH_PORT
+wait_for_ssh() {
+ local password="${1:-$ROOT_PASSWORD}"
+ local timeout="${2:-120}"
+ local elapsed=0
- if [ -n "$mac" ]; then
- ip=$(grep "$mac" /var/lib/libvirt/dnsmasq/default.leases 2>/dev/null | awk '{print $3}')
- if [ -n "$ip" ]; then
- echo "$ip"
+ progress "Waiting for SSH on localhost:$SSH_PORT..."
+ while [ $elapsed -lt $timeout ]; do
+ if sshpass -p "$password" ssh $SSH_OPTS -p "$SSH_PORT" root@localhost true 2>/dev/null; then
+ success "SSH is available"
return 0
fi
- fi
+ sleep 5
+ elapsed=$((elapsed + 5))
+ done
+ error "SSH timeout after ${timeout}s"
return 1
}
# Execute command in VM via SSH
vm_exec() {
- local vm_name="$1"
+ local password="${1:-$ROOT_PASSWORD}"
shift
- local cmd="$*"
-
- local ip
- ip=$(get_vm_ip "$vm_name")
-
- if [ -z "$ip" ]; then
- error "Could not get IP address for VM $vm_name"
- return 1
- fi
-
- ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null \
- "root@$ip" "$cmd" 2>> "$LOGFILE"
+ sshpass -p "$password" ssh $SSH_OPTS \
+ -o ServerAliveInterval=30 -o ServerAliveCountMax=10 \
+ -p "$SSH_PORT" root@localhost "$@" 2>> "$LOGFILE"
}
# Copy file to VM
copy_to_vm() {
- local vm_name="$1"
- local local_file="$2"
- local remote_path="$3"
-
- local ip
- ip=$(get_vm_ip "$vm_name")
-
- if [ -z "$ip" ]; then
- error "Could not get IP address for VM $vm_name"
- return 1
- fi
+ local local_file="$1"
+ local remote_path="$2"
+ local password="${3:-$ROOT_PASSWORD}"
- step "Copying $local_file to VM"
- if scp -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null \
- "$local_file" "root@$ip:$remote_path" >> "$LOGFILE" 2>&1; then
+ step "Copying $(basename "$local_file") to VM:$remote_path"
+ if sshpass -p "$password" scp $SSH_OPTS -P "$SSH_PORT" \
+ "$local_file" "root@localhost:$remote_path" >> "$LOGFILE" 2>&1; then
success "File copied to VM"
return 0
else
@@ -297,21 +372,13 @@ copy_to_vm() {
# Copy file from VM
copy_from_vm() {
- local vm_name="$1"
- local remote_file="$2"
- local local_path="$3"
-
- local ip
- ip=$(get_vm_ip "$vm_name")
-
- if [ -z "$ip" ]; then
- error "Could not get IP address for VM $vm_name"
- return 1
- fi
+ local remote_file="$1"
+ local local_path="$2"
+ local password="${3:-$ROOT_PASSWORD}"
step "Copying $remote_file from VM"
- if scp -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null \
- "root@$ip:$remote_file" "$local_path" >> "$LOGFILE" 2>&1; then
+ if sshpass -p "$password" scp $SSH_OPTS -P "$SSH_PORT" \
+ "root@localhost:$remote_file" "$local_path" >> "$LOGFILE" 2>&1; then
success "File copied from VM"
return 0
else
diff --git a/scripts/testing/run-test.sh b/scripts/testing/run-test.sh
index 4c41cc3..a600870 100755
--- a/scripts/testing/run-test.sh
+++ b/scripts/testing/run-test.sh
@@ -5,7 +5,7 @@
#
# This script:
# 1. Reverts base VM to clean snapshot
-# 2. Starts the base VM
+# 2. Boots the VM via QEMU
# 3. Transfers archsetup and dotfiles
# 4. Executes archsetup in the VM
# 5. Captures logs and validates results
@@ -55,15 +55,20 @@ done
# Configuration
TIMESTAMP=$(date +'%Y%m%d-%H%M%S')
-VM_NAME="archsetup-base"
+VM_IMAGES_DIR="$PROJECT_ROOT/vm-images"
TEST_RESULTS_DIR="$PROJECT_ROOT/test-results/$TIMESTAMP"
ROOT_PASSWORD="archsetup"
ARCHZFS_INBOX="$HOME/code/archzfs/inbox"
+ARCHSETUP_VM_CONF="$SCRIPT_DIR/archsetup-vm.conf"
-# Initialize logging
+# Set VM_IP for validation.sh backward compatibility
+VM_IP="localhost"
+
+# Initialize logging and VM paths
mkdir -p "$TEST_RESULTS_DIR"
LOGFILE="$TEST_RESULTS_DIR/test.log"
init_logging "$LOGFILE"
+init_vm_paths "$VM_IMAGES_DIR"
section "ArchSetup Test Run: $TIMESTAMP"
@@ -72,9 +77,9 @@ if [ ! -f "$ARCHSETUP_SCRIPT" ]; then
fatal "ArchSetup script not found: $ARCHSETUP_SCRIPT"
fi
-# Check if VM exists
-if ! vm_exists "$VM_NAME"; then
- fatal "Base VM not found: $VM_NAME"
+# Check disk exists
+if [ ! -f "$DISK_PATH" ]; then
+ fatal "Base disk not found: $DISK_PATH"
info "Create it first: ./scripts/testing/create-base-vm.sh"
fi
@@ -82,72 +87,34 @@ fi
section "Preparing Test Environment"
step "Checking for snapshot: $SNAPSHOT_NAME"
-if ! virsh --connect "$LIBVIRT_URI" snapshot-list "$VM_NAME" --name 2>/dev/null | grep -q "^$SNAPSHOT_NAME$"; then
- fatal "Snapshot '$SNAPSHOT_NAME' not found on VM $VM_NAME"
+if ! snapshot_exists "$DISK_PATH" "$SNAPSHOT_NAME"; then
+ fatal "Snapshot '$SNAPSHOT_NAME' not found on $DISK_PATH"
info "Available snapshots:"
- virsh --connect "$LIBVIRT_URI" snapshot-list "$VM_NAME" 2>/dev/null || info " (none)"
+ list_snapshots "$DISK_PATH"
info ""
- info "Create snapshot with:"
- info " virsh snapshot-create-as $VM_NAME $SNAPSHOT_NAME --description 'Clean Arch install'"
+ info "Create base VM with: ./scripts/testing/create-base-vm.sh"
fi
success "Snapshot $SNAPSHOT_NAME exists"
-# Shut down VM if running
-if vm_is_running "$VM_NAME"; then
- warn "VM $VM_NAME is currently running - shutting down for snapshot revert"
- stop_vm "$VM_NAME"
-fi
+# Stop VM if running and restore snapshot
+stop_qemu 2>/dev/null || true
-# Revert to clean snapshot
step "Reverting to snapshot: $SNAPSHOT_NAME"
-if restore_snapshot "$VM_NAME" "$SNAPSHOT_NAME"; then
+if restore_snapshot "$DISK_PATH" "$SNAPSHOT_NAME"; then
success "Reverted to clean state"
else
fatal "Failed to revert snapshot"
fi
-# Start VM
+# Start VM and wait for SSH
start_timer "boot"
step "Starting VM and waiting for SSH..."
-if ! start_vm "$VM_NAME"; then
- fatal "Failed to start VM"
-fi
-
-sleep 10 # Give VM time to boot
-
-# Get VM IP address
-VM_IP=""
-for i in {1..30}; do
- VM_IP=$(get_vm_ip "$VM_NAME" 2>/dev/null || true)
- if [ -n "$VM_IP" ]; then
- break
- fi
- sleep 2
-done
-
-if [ -z "$VM_IP" ]; then
- error "Could not get VM IP address"
- info "VM may not have booted correctly"
- fatal "VM boot failed"
-fi
-
-success "VM is running at $VM_IP"
+start_qemu "$DISK_PATH" "disk" "" "none" || fatal "Failed to start VM"
+wait_for_ssh "$ROOT_PASSWORD" 120 || fatal "VM SSH not available"
stop_timer "boot"
-# Wait for SSH
-step "Waiting for SSH to become available..."
-for i in {1..60}; do
- if sshpass -p "$ROOT_PASSWORD" ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null \
- -o ConnectTimeout=2 "root@$VM_IP" "echo connected" &>/dev/null; then
- break
- fi
- sleep 2
-done
-
-success "SSH is available"
-
# Run network diagnostics
-if ! run_network_diagnostics "$VM_IP"; then
+if ! run_network_diagnostics; then
fatal "Network diagnostics failed - aborting test"
fi
@@ -165,39 +132,59 @@ BUNDLE_FILE=$(mktemp)
git bundle create "$BUNDLE_FILE" HEAD >> "$LOGFILE" 2>&1
# Transfer bundle and extract on VM
-sshpass -p "$ROOT_PASSWORD" ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null \
- "root@$VM_IP" "rm -rf /tmp/archsetup-test && mkdir -p /tmp/archsetup-test" >> "$LOGFILE" 2>&1
+vm_exec "$ROOT_PASSWORD" "rm -rf /tmp/archsetup-test && mkdir -p /tmp/archsetup-test" >> "$LOGFILE" 2>&1
-sshpass -p "$ROOT_PASSWORD" scp -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null \
- "$BUNDLE_FILE" "root@$VM_IP:/tmp/archsetup.bundle" >> "$LOGFILE" 2>&1
+copy_to_vm "$BUNDLE_FILE" "/tmp/archsetup.bundle" "$ROOT_PASSWORD"
# Clone from bundle on VM (simulates git clone)
-sshpass -p "$ROOT_PASSWORD" ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null \
- "root@$VM_IP" "cd /tmp && git clone --depth 1 /tmp/archsetup.bundle archsetup-test && rm /tmp/archsetup.bundle" >> "$LOGFILE" 2>&1
+vm_exec "$ROOT_PASSWORD" \
+ "cd /tmp && git clone --depth 1 /tmp/archsetup.bundle archsetup-test && rm /tmp/archsetup.bundle" \
+ >> "$LOGFILE" 2>&1
rm -f "$BUNDLE_FILE"
success "Repository cloned to VM (simulating git clone --depth 1)"
+# Transfer archsetup VM config file
+step "Copying archsetup VM config"
+copy_to_vm "$ARCHSETUP_VM_CONF" "/tmp/archsetup-test/archsetup-vm.conf" "$ROOT_PASSWORD"
+
+# Pre-archsetup VM setup
+section "Pre-ArchSetup VM Setup"
+
+step "Importing archzfs PGP key"
+vm_exec "$ROOT_PASSWORD" \
+ "curl -sL https://archzfs.com/archzfs.gpg | pacman-key --add - && pacman-key --lsign-key DDF7DB817396A49B2A2723F7403BD972F75D9D76" \
+ >> "$LOGFILE" 2>&1 && success "archzfs PGP key imported" || warn "Failed to import archzfs key (may already be present)"
+
+step "Syncing package databases"
+vm_exec "$ROOT_PASSWORD" "pacman -Sy --noconfirm" >> "$LOGFILE" 2>&1 && \
+ success "Package databases synced" || warn "Package database sync had issues"
+
# Execute archsetup
section "Executing ArchSetup"
start_timer "archsetup"
step "Starting archsetup script in detached session on VM..."
-info "This will take 30-60 minutes depending on network speed"
info "Log file: $LOGFILE"
-# Start archsetup in a detached session on the VM (resilient to SSH disconnections)
+# Start archsetup fully detached.
+# Use ssh -T -n to prevent PTY allocation and stdin forwarding, which allows
+# the SSH session to close immediately after the remote shell exits.
REMOTE_LOG="/tmp/archsetup-test/archsetup-output.log"
-sshpass -p "$ROOT_PASSWORD" ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null \
- "root@$VM_IP" "cd /tmp/archsetup-test && nohup bash archsetup > $REMOTE_LOG 2>&1 & echo \$!" \
+
+sshpass -p "$ROOT_PASSWORD" ssh -T -n $SSH_OPTS \
+ -p "$SSH_PORT" root@localhost \
+ "setsid bash -c 'cd /tmp/archsetup-test && bash archsetup --config-file /tmp/archsetup-test/archsetup-vm.conf > $REMOTE_LOG 2>&1' < /dev/null > /dev/null 2>&1 &" \
>> "$LOGFILE" 2>&1
-if [ $? -ne 0 ]; then
- fatal "Failed to start archsetup on VM"
+# Verify the process started
+sleep 3
+if vm_exec "$ROOT_PASSWORD" "pgrep -f 'bash archsetup'" >> "$LOGFILE" 2>/dev/null; then
+ success "ArchSetup started in background on VM"
+else
+ fatal "ArchSetup process not found after launch"
fi
-success "ArchSetup started in background on VM"
-
# Poll for completion
step "Monitoring archsetup progress (polling every 30 seconds)..."
POLL_COUNT=0
@@ -205,9 +192,7 @@ MAX_POLLS=180 # 90 minutes max (180 * 30 seconds)
while [ $POLL_COUNT -lt $MAX_POLLS ]; do
# Check if archsetup process is still running
- # Use ps to avoid pgrep matching its own SSH command
- if sshpass -p "$ROOT_PASSWORD" ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null \
- "root@$VM_IP" "ps aux | grep '[b]ash archsetup' > /dev/null" 2>/dev/null; then
+ if vm_exec "$ROOT_PASSWORD" "ps aux | grep '[b]ash archsetup' > /dev/null" 2>/dev/null; then
# Still running, wait and continue
sleep 30
POLL_COUNT=$((POLL_COUNT + 1))
@@ -229,8 +214,9 @@ if [ $POLL_COUNT -ge $MAX_POLLS ]; then
else
# Get exit code from the remote log
step "Retrieving archsetup exit status..."
- ARCHSETUP_EXIT_CODE=$(sshpass -p "$ROOT_PASSWORD" ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null \
- "root@$VM_IP" "grep -q 'ARCHSETUP_EXECUTION_COMPLETE' /var/log/archsetup-*.log 2>/dev/null && echo 0 || echo 1" 2>/dev/null)
+ ARCHSETUP_EXIT_CODE=$(vm_exec "$ROOT_PASSWORD" \
+ "grep -q 'ARCHSETUP_EXECUTION_COMPLETE' /var/log/archsetup-*.log 2>/dev/null && echo 0 || echo 1" \
+ 2>/dev/null)
if [ "$ARCHSETUP_EXIT_CODE" = "0" ]; then
success "ArchSetup completed successfully"
@@ -241,8 +227,7 @@ fi
# Copy the remote output log
step "Retrieving archsetup output from VM..."
-sshpass -p "$ROOT_PASSWORD" scp -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null \
- "root@$VM_IP:$REMOTE_LOG" "$TEST_RESULTS_DIR/archsetup-output.log" 2>> "$LOGFILE" || \
+copy_from_vm "$REMOTE_LOG" "$TEST_RESULTS_DIR/archsetup-output.log" "$ROOT_PASSWORD" || \
warn "Could not copy remote output log"
# Append remote output to main test log
@@ -256,15 +241,17 @@ stop_timer "archsetup"
section "Capturing Test Artifacts"
step "Copying archsetup log from VM"
-sshpass -p "$ROOT_PASSWORD" scp -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null \
- "root@$VM_IP:/var/log/archsetup-*.log" "$TEST_RESULTS_DIR/" 2>> "$LOGFILE" || \
+copy_from_vm "/var/log/archsetup-*.log" "$TEST_RESULTS_DIR/" "$ROOT_PASSWORD" || \
warn "Could not copy archsetup log"
step "Copying package lists from VM"
-sshpass -p "$ROOT_PASSWORD" scp -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null \
- "root@$VM_IP:/root/.local/src/archsetup-*.txt" "$TEST_RESULTS_DIR/" 2>> "$LOGFILE" || \
+copy_from_vm "/var/log/archsetup-*-package-list.txt" "$TEST_RESULTS_DIR/" "$ROOT_PASSWORD" || \
warn "Could not copy package lists"
+step "Copying installed packages list"
+copy_from_vm "/var/log/archsetup-installed-packages.txt" "$TEST_RESULTS_DIR/" "$ROOT_PASSWORD" || \
+ warn "Could not copy installed packages list"
+
# Capture post-install state
capture_post_install_state "$TEST_RESULTS_DIR"
@@ -282,9 +269,9 @@ generate_issue_report "$TEST_RESULTS_DIR" "$ARCHZFS_INBOX"
# Set validation result based on failure count
if [ $VALIDATION_FAILED -eq 0 ]; then
- VALIDATION_PASSED=true
+ TEST_PASSED=true
else
- VALIDATION_PASSED=false
+ TEST_PASSED=false
fi
# Generate test report
@@ -298,16 +285,16 @@ ArchSetup Test Report
Test ID: $TIMESTAMP
Date: $(date +'%Y-%m-%d %H:%M:%S')
-Test Method: Snapshot-based
+Test Method: QEMU snapshot-based
VM Configuration:
- Name: $VM_NAME
- IP: $VM_IP
+ Disk: $DISK_PATH
Snapshot: $SNAPSHOT_NAME
+ SSH: localhost:$SSH_PORT
Results:
ArchSetup Exit Code: $ARCHSETUP_EXIT_CODE
- Validation: $(if $VALIDATION_PASSED; then echo "PASSED"; else echo "FAILED"; fi)
+ Validation: $(if $TEST_PASSED; then echo "PASSED"; else echo "FAILED"; fi)
Validation Summary:
Passed: $VALIDATION_PASSED
@@ -338,16 +325,14 @@ section "Cleanup"
if $KEEP_VM; then
info "VM is still running in post-test state (--keep flag was used)"
info "Connect with:"
- info " Console: virsh console $VM_NAME"
- info " SSH: ssh root@$VM_IP"
+ info " SSH: sshpass -p '$ROOT_PASSWORD' ssh -p $SSH_PORT root@localhost"
info ""
- info "To revert to clean state when done:"
- info " virsh shutdown $VM_NAME"
- info " virsh snapshot-revert $VM_NAME $SNAPSHOT_NAME"
+ info "To stop VM: kill \$(cat $PID_FILE)"
+ info "To revert: qemu-img snapshot -a $SNAPSHOT_NAME $DISK_PATH"
else
step "Shutting down VM and reverting to clean snapshot"
- stop_vm "$VM_NAME"
- if restore_snapshot "$VM_NAME" "$SNAPSHOT_NAME"; then
+ stop_qemu
+ if restore_snapshot "$DISK_PATH" "$SNAPSHOT_NAME"; then
success "VM reverted to clean state"
else
warn "Failed to revert snapshot - VM may be in modified state"
@@ -357,7 +342,7 @@ fi
# Final summary
section "Test Complete"
-if [ $ARCHSETUP_EXIT_CODE -eq 0 ] && $VALIDATION_PASSED; then
+if [ "$ARCHSETUP_EXIT_CODE" = "0" ] && $TEST_PASSED; then
success "TEST PASSED"
exit 0
else
diff --git a/scripts/testing/setup-testing-env.sh b/scripts/testing/setup-testing-env.sh
index e682553..f0e63aa 100755
--- a/scripts/testing/setup-testing-env.sh
+++ b/scripts/testing/setup-testing-env.sh
@@ -4,9 +4,7 @@
# License: GNU GPLv3
#
# This script performs one-time setup of the testing infrastructure:
-# - Installs QEMU/KVM and libvirt
-# - Configures libvirt networking
-# - Adds user to libvirt group
+# - Installs QEMU/KVM, sshpass, OVMF firmware, and socat
# - Verifies KVM support
# - Creates directories for test artifacts
@@ -20,6 +18,7 @@ PROJECT_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
source "$SCRIPT_DIR/lib/logging.sh"
# Initialize logging
+mkdir -p "$PROJECT_ROOT/test-results"
LOGFILE="$PROJECT_ROOT/test-results/setup-$(date +'%Y%m%d-%H%M%S').log"
init_logging "$LOGFILE"
@@ -41,13 +40,9 @@ section "Installing Required Packages"
PACKAGES=(
qemu-full
- libvirt
- virt-manager
- dnsmasq
- bridge-utils
- iptables
- virt-install
- libguestfs
+ sshpass
+ edk2-ovmf
+ socat
)
for pkg in "${PACKAGES[@]}"; do
@@ -64,46 +59,6 @@ for pkg in "${PACKAGES[@]}"; do
fi
done
-# Enable and start libvirt service
-section "Configuring libvirt Service"
-
-step "Enabling libvirtd service"
-if sudo systemctl enable libvirtd.service >> "$LOGFILE" 2>&1; then
- success "libvirtd service enabled"
-else
- warn "Failed to enable libvirtd service (may already be enabled)"
-fi
-
-step "Starting libvirtd service"
-if sudo systemctl start libvirtd.service >> "$LOGFILE" 2>&1; then
- success "libvirtd service started"
-else
- if sudo systemctl is-active --quiet libvirtd.service; then
- info "libvirtd service is already running"
- else
- error "Failed to start libvirtd service"
- fatal "Service startup failed"
- fi
-fi
-
-# Add user to libvirt group
-section "Configuring User Permissions"
-
-if groups | grep -q libvirt; then
- success "User $USER is already in libvirt group"
-else
- step "Adding user $USER to libvirt group"
- if sudo usermod -a -G libvirt "$USER" >> "$LOGFILE" 2>&1; then
- success "User added to libvirt group"
- warn "You must log out and back in for group membership to take effect"
- warn "After logging back in, re-run this script to continue"
- exit 0
- else
- error "Failed to add user to libvirt group"
- fatal "User configuration failed"
- fi
-fi
-
# Verify KVM support
section "Verifying KVM Support"
@@ -126,6 +81,18 @@ else
info "Load with: sudo modprobe kvm-intel (or kvm-amd)"
fi
+# Verify OVMF firmware
+section "Verifying OVMF Firmware"
+
+OVMF_CODE="/usr/share/edk2/x64/OVMF_CODE.4m.fd"
+OVMF_VARS="/usr/share/edk2/x64/OVMF_VARS.4m.fd"
+
+if [ -f "$OVMF_CODE" ] && [ -f "$OVMF_VARS" ]; then
+ success "OVMF firmware files present"
+else
+ fatal "OVMF firmware files not found (expected at $OVMF_CODE)"
+fi
+
# Create directory structure
section "Creating Directory Structure"
@@ -147,45 +114,14 @@ for dir in "${DIRS[@]}"; do
fi
done
-# Configure default libvirt network
-section "Configuring libvirt Network"
-
-if virsh net-info default &>/dev/null; then
- info "Default network exists"
-
- if virsh net-info default | grep -q "Active:.*yes"; then
- success "Default network is active"
- else
- step "Starting default network"
- if virsh net-start default >> "$LOGFILE" 2>&1; then
- success "Default network started"
- else
- error "Failed to start default network"
- fi
- fi
-
- if virsh net-info default | grep -q "Autostart:.*yes"; then
- info "Default network autostart is enabled"
- else
- step "Enabling default network autostart"
- if virsh net-autostart default >> "$LOGFILE" 2>&1; then
- success "Default network autostart enabled"
- else
- warn "Failed to enable default network autostart"
- fi
- fi
-else
- error "Default network not found"
- info "This is unusual - libvirt should create it automatically"
-fi
-
# Summary
section "Setup Complete"
success "Testing environment is ready"
info ""
info "Next steps:"
-info " 1. Create base VM: ./scripts/testing/create-base-vm.sh"
-info " 2. Run a test: ./scripts/testing/run-test.sh"
+info " 1. Copy an archangel ISO to: $PROJECT_ROOT/vm-images/"
+info " 2. Create base VM: ./scripts/testing/create-base-vm.sh"
+info " 3. Run a test: ./scripts/testing/run-test.sh"
info ""
info "Log file: $LOGFILE"
diff --git a/todo.org b/todo.org
index f9cf542..c90bb1f 100644
--- a/todo.org
+++ b/todo.org
@@ -120,83 +120,77 @@ The script handles edge cases gracefully, provides detailed error messages with
*Why this is Method 1:* Can't build testing infrastructure or maintain packages if the script doesn't work. This is the foundation—everything else depends on reliable execution.
*** TODO [#A] Make Hyprland Bulletproof and Comfy
-**** DONE [#A] Fix Hyprland Configuration Issues
-CLOSED: [2026-01-25 Sun]
-All critical issues resolved. Remaining sub-items are "consider" tasks for future enhancements.
-
-***** DONE [#B] Consider Hyprland Plugins
-CLOSED: [2026-01-25 Sun]
-Official plugins: https://github.com/hyprwm/hyprland-plugins
-Install via hyprpm (added cpio dependency to archsetup).
-
-Interesting plugins:
-
-****** hyprscrolling (INTERESTED)
-Arranges windows in vertical columns that scroll horizontally - like a paper tape.
-Instead of tiling in a grid, windows line up side-by-side and you scroll through them.
-Note: Still marked "work in progress" - may have rough edges.
-
-Options:
-- column_width: default column width (0.5 = half monitor)
-- explicit_column_widths: widths to cycle through ("0.333, 0.5, 0.667, 1.0")
-- fullscreen_on_one_column: maximize when only one column
-- focus_fit_method: 0=center focused column, 1=just fit on screen
-- follow_focus: auto-scroll when focusing a window
-
-****** xtra-dispatchers (INTERESTED)
-Adds 4 new dispatcher commands:
-
-| Dispatcher | What it does |
-|-------------------------------------+-----------------------------------------------|
-| plugin:xtd:moveorexec WINDOW,CMD | Move window here, or launch if not running |
-| plugin:xtd:throwunfocused WORKSPACE | Send all unfocused windows away (focus mode!) |
-| plugin:xtd:bringallfrom WORKSPACE | Bring all windows from another workspace |
-| plugin:xtd:closeunfocused | Close all unfocused windows |
-
-throwunfocused is useful for focus mode - declutter workspace, then bringallfrom to restore.
-closeunfocused is handy for cleaning up.
-****** Other official plugins
-- hyprbars: window title bars
-- hyprexpo: workspace overview (like macOS Mission Control)
-- hyprfocus: flash effect when changing focus
-- hyprtrails: window trail effects (eye candy)
-- borders-plus-plus: extra border layers (cosmetic)
-- hyprwinwrap: use any app as wallpaper
+**** DONE [#B] Change Keybinding: mod-f should open nautilus
+remove the terminal+ranger keybinding and replace it with opening nautilus
+**** DONE [#B] Monocle layout *(tab group) bar at the top needs to be themed
+the bar appears to be colored straight cyan. cyan should be replaced with dupre blue.
+the other neutral colors should be replaced with dupre neutral colors.
+this will need visual inspection to ensure the right colors are chosen and don't clash in different layouts
+**** DONE [#B] Remove border color from dunst notifications and make the text size match waybar's text size/
+Dunst notification popups have a visible border that looks out of place.
+Remove or disable the border color to match the clean aesthetic.
+
+Also, the text is quite large. For consistency, we should make it the same size as the waybar text.
+
+**** DONE [#B] Change fuzzel pinentry border color for visibility
+Fuzzel menu border blends in too much. Change border color to blue or red
+to make the menu more visible when it appears.
+
+**** TODO [#C] Review theme config architecture for dunst/fuzzel
+The active dunst config is stowed from dotfiles/common/ but theme templates
+live in dotfiles/hyprland/.config/themes/. set-theme copies the templates to
+the stowed locations at runtime, so edits to the common file get overwritten
+on theme switch. This split between stowed configs and theme templates is
+error-prone — changes must be made in both places. Consider:
+- Having set-theme be the single source of truth (remove common dunstrc from stow)
+- Or symlinking the stowed config to a theme-managed location
+- Same situation applies to fuzzel.ini
+The goal is a single place to edit each config, not two.
+
+**** TODO [#D] Consider Customizing Hyprland Animations
+Current: windows pop in, scratchpads slide from bottom.
-***** DONE [#C] Evaluate Hyprland Layout Options
-CLOSED: [2026-01-25 Sun]
-Current layouts: master (left/top/center), dwindle. No deck layout available natively.
+Customizable animations:
+- windows / windowsOut / windowsMove - window open/close/move
+- fade - opacity changes
+- border / borderangle - border color and gradient angle
+- workspaces - workspace switching
+- specialWorkspace - scratchpads (currently slidevert)
+- layers - waybar, notifications, etc.
-Options to consider:
-1. Restrict to commonly used layouts only (remove unused from cycle)
-2. Add hy3 plugin for i3-style manual tiling (horizontal/vertical/tab groups)
+Styles: slide, slidevert, popin X%, fade
+Parameters: animation = NAME, ON/OFF, SPEED, BEZIER, STYLE
+Speed: lower = faster (1-10 typical)
-hy3 plugin:
-- URL: https://github.com/outfoxxed/hy3
-- Provides: horizontal splits, vertical splits, tab groups, autotiling
-- Does NOT provide deck layout
-- Requires different dispatchers (hy3:movefocus, hy3:makegroup, etc.)
+Example tweaks:
+#+begin_src conf
+animation = windows, 1, 2, myBezier, popin 80%
+animation = workspaces, 1, 4, default, slide
+animation = fade, 1, 2, default
+animation = layers, 1, 2, default, fade
+#+end_src
-Other layout plugins (none have deck):
-- hyprNStack: N-stack tiling
-- hyprscroller: PaperWM-style scrolling
-- hyprRiver: River-inspired layouts
+**** VERIFY [#D] Test wlogout menu on laptop
+Test wlogout exit menu on laptop to verify sizing works on different display.
+Current config uses fixed pixel margins - may need adjustment for laptop screen.
-Decision: Keep current setup or adopt hy3 for more flexible manual tiling.
+**** VERIFY [#D] Test hy3 tab bar theming colors
+Test the monocle/tab group mode to verify hy3 tab bar colors look correct with dupre and hudson themes.
+Run =set-theme dupre= and =set-theme hudson= while in tab group mode ($mod SHIFT M).
-***** CANCELLED [#C] Consider Pywal for Dynamic Theming
+**** CANCELLED [#C] Consider Pywal for Dynamic Theming
CLOSED: [2026-01-26 Mon]
Use pywal to generate color schemes from wallpaper. Reference: reference-repos/kastrbl4nik-dots/
Tools: swww (wallpaper), wal (colors), pywalfox (Firefox), pywal-spicetify (Spotify)
-****** How pywal works
+***** How pywal works
1. Run =wal -i /path/to/wallpaper.jpg=
2. Analyzes image and extracts 16-color palette
3. Colors written to =~/.cache/wal/colors.json= (and other formats)
4. Apps read from these files or use templates that pywal fills in
-****** Reverting to original theme
+***** Reverting to original theme
Pywal doesn't overwrite original configs - you either:
- Use templates (pywal fills in colors, copies to config locations)
- Have configs that source from =~/.cache/wal/= files
@@ -205,14 +199,14 @@ Pywal doesn't overwrite original configs - you either:
Save/restore themes: =wal --theme= lists saved themes, =wal -f theme-name= restores.
Could save current goldenrod scheme and switch back anytime.
-****** Emacs integration
+***** Emacs integration
- Pywal works with Emacs but requires extra setup
- =ewal= package reads pywal colors from =~/.cache/wal/colors.json= and creates Emacs theme
- Terminal Emacs (=-nw=) inherits terminal colors automatically
- GUI Emacs (emacsclient -c) needs ewal or similar explicit integration
- Adds complexity; replaces carefully-tuned theme with auto-generated colors
-****** Auto-trigger on wallpaper change
+***** Auto-trigger on wallpaper change
Option 1 - Wrapper script (simplest):
#+begin_src sh
#!/bin/sh
@@ -228,13 +222,13 @@ Waypaper has post-command setting in config - set to run =wal -i= after wallpape
Option 3 - File watcher:
Watch =~/.cache/swww/= for changes and trigger wal automatically. More complex.
-****** Considerations
+***** Considerations
- Best for: unified colors across apps, variety, matching wallpaper
- Less ideal for: specific color scheme you love, consistency, carefully tuned app colors
- Some wallpapers produce great palettes; others produce muddy low-contrast results
- Current goldenrod/dark gray theme works well - pywal would replace it
-***** CANCELLED [#D] Consider improving waybar netspeed click responsiveness
+**** CANCELLED [#D] Consider improving waybar netspeed click responsiveness
CLOSED: [2026-01-26 Mon]
The network module has a 1-second sleep to measure throughput, causing occasional
missed clicks when toggling the scratchpad. Options if this becomes annoying:
@@ -242,30 +236,7 @@ missed clicks when toggling the scratchpad. Options if this becomes annoying:
2. Background daemon approach (separate process writes to file, script just reads it)
Current interval: 2 seconds. Script runtime: ~1 second.
-***** TODO [#C] Consider Customizing Hyprland Animations
-Current: windows pop in, scratchpads slide from bottom.
-
-Customizable animations:
-- windows / windowsOut / windowsMove - window open/close/move
-- fade - opacity changes
-- border / borderangle - border color and gradient angle
-- workspaces - workspace switching
-- specialWorkspace - scratchpads (currently slidevert)
-- layers - waybar, notifications, etc.
-
-Styles: slide, slidevert, popin X%, fade
-Parameters: animation = NAME, ON/OFF, SPEED, BEZIER, STYLE
-Speed: lower = faster (1-10 typical)
-
-Example tweaks:
-#+begin_src conf
-animation = windows, 1, 2, myBezier, popin 80%
-animation = workspaces, 1, 4, default, slide
-animation = fade, 1, 2, default
-animation = layers, 1, 2, default, fade
-#+end_src
-
-***** CANCELLED [#D] Consider "show desktop" toggle via empty special workspace
+**** CANCELLED [#D] Consider "show desktop" toggle via empty special workspace
CLOSED: [2026-01-25 Sun 21:13]
I think what I really want is the throw to other workspace dispatcher.
@@ -276,34 +247,139 @@ Implementation:
bind = $mod, Z, togglespecialworkspace, desktop
#+end_src
-***** DONE Scratchpads not spawning on keybind
+**** DONE [#B] waybar's temperature monitor should change color when hot.
+Consider the temperature at which the CPU is getting "too hot". define two temperature thresholds that are "approaching too hot" (when the user should begin paying attention) and "nearly too hot" (when it's approaching a temperature where the user should take action). have the color change to yellow when it's approaching the first threshold, and red when it approaches the second. we'll be using themed colors from the dupre theme.
+
+**** DONE [#B] waybar's storage monitor should change color when approaching full.
+waybar's storage monitor should change color when it approaches full, to allow the user to take action. note that since this system could be running on zfs, btrfs, and ext4 file systems, this should work no matter which of these filesystems we are running on. When storage is at or over 80% full, the color of the storage icon on the waybar panel should turn yellow. When the storage is at or over 90$ full, it should be red. we'll be using themed colors form the dupre theme.
+
+**** DONE [#A] Change cursor theme (curren theme's cross too small for screenshots)
+CLOSED: [2026-01-27 Tue]
+Switched to Bibata-Modern-Ice across all configs (hyprland, Xresources, GTK, archsetup).
+**** DONE [#B] new master windows do not have focus
+CLOSED: [2026-01-27 Tue]
+Added focus_on_activate = true to misc section in hyprland.conf.
+
+**** DONE [#B] screenshot functionality should leave path of last image in the clipboard ready to paste.
+CLOSED: [2026-01-27 Tue]
+Created screenshot script with fuzzel menu offering Copy Path, Copy Image, and Annotate.
+**** DONE [#B] screenshot functionality should also bind shift printscreen to select region.
+CLOSED: [2026-01-27 Tue]
+Replaced with $mod+S (region) and $mod+Print (fullscreen), both using fuzzel menu.
+**** DONE [#B] waybar's network monitor should change color when there's no network available.
+CLOSED: [2026-01-27 Tue]
+Added #custom-netspeed.disconnected CSS rule with themed red color across
+base, dupre, and hudson waybar CSS. Changed label from "Disconnected" to "Offline".
+
+**** DONE [#C] Verify gammastep and geoclue fix
+CLOSED: [2026-01-27 Tue 07:18]
+Ensure after reboot there is no error.
+Ensure gamma changes at night
+**** DONE [#B] hyprland: new windows should open in master area in tile layout
+CLOSED: [2026-01-27 Tue]
+Changed master layout new_status from slave to master in hyprland.conf.
+**** DONE [#B] waybar's volume module should not change color when muted.
+CLOSED: [2026-01-27 Tue]
+Removed #wireplumber.muted color overrides from all waybar CSS files.
+Icon and "Muted" text provide sufficient visual cue without darkening.
+
+**** DONE [#B] Review scripts and configs in dotfiles
+CLOSED: [2026-01-27 Tue 05:34]
+112 scripts across system and hyprland dotfiles. Many are from DWM/X11 era.
+- Identify scripts still in use vs obsolete
+- Migrate useful dmenu scripts to fuzzel for Wayland
+- Remove or archive unused scripts
+- Remove config files for apps no longer installed
+
+**** DONE [#B] Consider Hyprland Plugins
+CLOSED: [2026-01-25 Sun]
+Official plugins: https://github.com/hyprwm/hyprland-plugins
+Install via hyprpm (added cpio dependency to archsetup).
+
+Interesting plugins:
+
+***** hyprscrolling (INTERESTED)
+Arranges windows in vertical columns that scroll horizontally - like a paper tape.
+Instead of tiling in a grid, windows line up side-by-side and you scroll through them.
+Note: Still marked "work in progress" - may have rough edges.
+
+Options:
+- column_width: default column width (0.5 = half monitor)
+- explicit_column_widths: widths to cycle through ("0.333, 0.5, 0.667, 1.0")
+- fullscreen_on_one_column: maximize when only one column
+- focus_fit_method: 0=center focused column, 1=just fit on screen
+- follow_focus: auto-scroll when focusing a window
+
+***** xtra-dispatchers (INTERESTED)
+Adds 4 new dispatcher commands:
+
+| Dispatcher | What it does |
+|-------------------------------------+-----------------------------------------------|
+| plugin:xtd:moveorexec WINDOW,CMD | Move window here, or launch if not running |
+| plugin:xtd:throwunfocused WORKSPACE | Send all unfocused windows away (focus mode!) |
+| plugin:xtd:bringallfrom WORKSPACE | Bring all windows from another workspace |
+| plugin:xtd:closeunfocused | Close all unfocused windows |
+
+throwunfocused is useful for focus mode - declutter workspace, then bringallfrom to restore.
+closeunfocused is handy for cleaning up.
+
+***** Other official plugins
+- hyprbars: window title bars
+- hyprexpo: workspace overview (like macOS Mission Control)
+- hyprfocus: flash effect when changing focus
+- hyprtrails: window trail effects (eye candy)
+- borders-plus-plus: extra border layers (cosmetic)
+- hyprwinwrap: use any app as wallpaper
+
+**** DONE [#C] Evaluate Hyprland Layout Options
+CLOSED: [2026-01-25 Sun]
+Current layouts: master (left/top/center), dwindle. No deck layout available natively.
+
+Options to consider:
+1. Restrict to commonly used layouts only (remove unused from cycle)
+2. Add hy3 plugin for i3-style manual tiling (horizontal/vertical/tab groups)
+
+hy3 plugin:
+- URL: https://github.com/outfoxxed/hy3
+- Provides: horizontal splits, vertical splits, tab groups, autotiling
+- Does NOT provide deck layout
+- Requires different dispatchers (hy3:movefocus, hy3:makegroup, etc.)
+
+Other layout plugins (none have deck):
+- hyprNStack: N-stack tiling
+- hyprscroller: PaperWM-style scrolling
+- hyprRiver: River-inspired layouts
+
+Decision: Keep current setup or adopt hy3 for more flexible manual tiling.
+
+**** DONE Scratchpads not spawning on keybind
mod+shift+return toggles special workspace (screen dims) but foot terminal doesn't spawn.
- Manual spawn works: =foot -T spterm tmux=
- Fixed pgrep self-match issue (bracket trick)
- Fixed windowrule syntax (match:class first, then rule)
- Still not working - need further investigation
-***** DONE Wofi launcher can't be dismissed
+**** DONE Wofi launcher can't be dismissed
CLOSED: [2026-01-25 Sun 20:50]
mod+space launches wofi but mod+space and mod+shift+c don't dismiss it.
Need to configure proper dismiss keybinding.
-***** DONE No wallpaper displaying
+**** DONE No wallpaper displaying
Desktop has no wallpaper. Need to set up swww or similar.
-***** DONE Verify foot -T flag sets window title correctly
+**** DONE Verify foot -T flag sets window title correctly
Need to confirm foot uses -T for title (vs -t or --title).
-***** DONE Pulsemixer scratchpad sizing different from others
+**** DONE Pulsemixer scratchpad sizing different from others
mod+a pulsemixer scratchpad appears to be sized differently than other scratchpads.
All use same rules: size (monitor_w*0.6) (monitor_h*0.6). Investigate why it differs.
-***** DONE Waybar wireplumber volume control not working
+**** DONE Waybar wireplumber volume control not working
CLOSED: [2026-01-25 Sun 20:50]
Click to mute and scroll to adjust not responding. Config uses wpctl commands.
May need to verify wpctl is working or try different approach.
-***** DONE Waybar clock not displaying
+**** DONE Waybar clock not displaying
Clock module is configured correctly but not showing on bar.
- Config and style.css are stowed correctly
- Font (FiraCode Nerd Font Mono) is installed
@@ -311,120 +387,67 @@ Clock module is configured correctly but not showing on bar.
- Other modules (tray, disk, workspaces) appear to work
- Reference config in reference-repos/hyprland-dotfiles/waybar/
-***** DONE [#B] Add caffeine replacement for Hyprland
+**** DONE [#B] Add caffeine replacement for Hyprland
CLOSED: [2026-01-26 Sun]
Added waybar idle_inhibitor module as caffeine replacement.
Click to toggle; prevents screen lock/sleep when activated.
-***** DONE [#B] Fill out the exit menu for Hyprland
+**** DONE [#B] Fill out the exit menu for Hyprland
CLOSED: [2026-01-26 Sun]
Created wlogout exit menu with dupre theme and nerd font icons.
Keybinding: $mod SHIFT Q. Uses pgrep to prevent multiple instances.
-***** TODO [#C] Test wlogout menu on laptop
-Test wlogout exit menu on laptop to verify sizing works on different display.
-Current config uses fixed pixel margins - may need adjustment for laptop screen.
-
-***** DONE [#B] Fix rseed32 issue in console at boot
+**** DONE [#B] Fix rseed32 issue in console at boot
CLOSED: [2026-01-26 Mon]
Attempted clearcpuid=rdseed kernel parameter but it doesn't suppress the message.
The kernel detection runs separately and still logs the warning. Proper fix requires
a BIOS update from Framework with newer AGESA microcode. Reverted the workaround
since it's cosmetic only - the kernel already mitigates the actual security issue.
-***** DONE [#B] Prune desktop files from application menu
+**** DONE [#B] Prune desktop files from application menu
CLOSED: [2026-01-26 Mon]
Hide unwanted .desktop entries from fuzzel/app launcher. Created ~90 NoDisplay=true overrides
in dotfiles/hyprland/.local/share/applications/.
-***** TODO [#B] Review scripts and configs in dotfiles
-112 scripts across system and hyprland dotfiles. Many are from DWM/X11 era.
-- Identify scripts still in use vs obsolete
-- Migrate useful dmenu scripts to fuzzel for Wayland
-- Remove or archive unused scripts
-- Remove config files for apps no longer installed
-
-***** DONE [#C] Standardize font size and name across theme configs
+**** DONE [#C] Standardize font size and name across theme configs
CLOSED: [2026-01-26 Mon]
- Changed font to BerkeleyMono Nerd Font (patched version) across all configs
- Standardized point-based configs (foot, fuzzel, dunstrc, Xresources) to 13pt
- Added comments explaining unit type (points vs pixels) in each config
- Waybar CSS kept at 14px with explanatory comment
-***** DONE [#B] Color theming the hy3 tab bar in monocle view
+**** DONE [#B] Color theming the hy3 tab bar in monocle view
CLOSED: [2026-01-26 Mon]
Added hy3 plugin tab bar colors to set-theme script for both dupre and hudson themes.
Replaces default bright cyan with theme-appropriate colors.
Dupre: active bg+2/steel, inactive gray+2, urgent red, locked cyan.
Hudson: active #444/#bbb, inactive #c5c8c6, urgent red, locked cyan.
-***** TODO [#C] Test hy3 tab bar theming colors
-Test the monocle/tab group mode to verify hy3 tab bar colors look correct with dupre and hudson themes.
-Run =set-theme dupre= and =set-theme hudson= while in tab group mode ($mod SHIFT M).
-
-***** DONE [#C] Consider breaking out Hardware Workarounds as separate section
+**** DONE [#C] Consider breaking out Hardware Workarounds as separate section
CLOSED: [2026-01-26 Mon]
Removed the Hardware Workarounds section from archsetup entirely. The only workaround
(AMD Zen 5 RDSEED32) didn't actually suppress the warning message - the kernel detection
runs separately. Proper fix requires BIOS updates from vendors with AGESA microcode.
-*** TODO [#A] Fix sleep/suspend on Framework Laptop (velox only)
+*** TODO [#A] Ensure sleep/suspend works on laptops
Critical functionality for laptop use - current battery drain unacceptable
**NOTE:** This applies to Framework Laptop (velox), not Framework Desktop (ratio)
Add kernel parameter: ~rtc_cmos.use_acpi_alarm=1~ (will become systemd default)
Consider: ~acpi_mask_gpe=0x1A~ for battery drain, suspend-then-hibernate config
See Framework community notes on logind.conf and sleep.conf settings
-*** TODO [#B] Review slow and failed packages from 8GB RAM test
-See [[file:docs/slow-failed-packages.org][Slow and Failed Packages Analysis]]
-
-Test run from 2025-11-09 with 8GB RAM, 50GB disk identified:
-- 2 packages that hang indefinitely (anki, tageditor)
-- 4 packages that fail to install (nitrogen, gtk-engine-murrine, adwaita-color-schemes, vagrant)
-- Several slow but successful packages (multimarkdown, ptyxis, thunderbird, etc.)
-
-High priority actions:
-- Remove or make optional: anki (hangs 98 min), tageditor (hangs on qt5-webengine)
-- Investigate repository/build issues for failing packages
-
-*** TODO [#B] Improve error handling: UFW firewall, rmmod pcspkr, mkdir missing quotes
-**** DONE [#B] Fix UFW firewall error handling (archsetup:395,410)
-CLOSED: [2026-01-21 Wed]
-Firewall failures use ~|| error "error"~ which logs but continues - system may be left exposed
-Should use ~|| error "crash"~ or validate rules were applied successfully
-RESOLVED: Added firewall verification after setup (checks "ufw status | grep Status: active").
-If verification fails, displays CRITICAL SECURITY WARNING in outro with manual fix commands.
-**** DONE [#B] Fix rmmod pcspkr error (archsetup:588)
-CLOSED: [2026-01-21 Wed]
-~rmmod pcspkr~ doesn't check if module is loaded, produces error if already unloaded
-Should use ~rmmod pcspkr 2>/dev/null || true~ or check with ~lsmod~
-RESOLVED: Changed to ~rmmod pcspkr 2>/dev/null || true~
-**** DONE [#B] Fix mkdir missing quotes (archsetup:247)
-CLOSED: [2026-01-21 Wed]
-Line 247: ~mkdir -p $source_dir~ should be ~mkdir -p "$source_dir"~ - fails if path contains spaces
-RESOLVED: Current code at line 577 properly quotes: ~(mkdir -p "$source_dir")~
-
-*** TODO [#B] Test complete end-to-end run on fresh VM
-Validates the script actually works in a clean environment (blocks claiming Method 1 complete)
-
-*** TODO [#B] Make all error messages actionable with recovery steps
+*** TODO [#B] All error messages should be actionable with recovery steps
Currently just reports errors without guidance on how to fix them
-*** TODO [#B] Improve progress indicators throughout install
-Enhance existing indicators to show what's happening in real-time
-
-*** TODO [#B] Check that full install logs have timestamps
+*** TODO [#B] Full install logs should contain timestamps
Verify timestamps exist for debugging failures
-*** TODO [#B] Add retry logic to git_install function
-pacman_install and aur_install have retry logic, but git_install doesn't
-
*** TODO [#B] Add input validation for username and paths
Variables like ~$username~, ~$source_dir~, and paths are not validated
Special characters or malicious input could break the script or cause security issues
Should validate inputs match expected patterns (alphanumeric, valid paths, etc.)
-*** TODO [#B] Enable TLP power management for Framework Laptop
+*** TODO [#B] Enable TLP power management for laptops
TLP manages power-saving modes for Wi-Fi, USB, PCIe, Bluetooth, CPU scheduler
Install tlp, enable service, add custom Framework 13 config to /etc/tlp.d/01-custom.conf
Improves battery life and prevents power-related issues during install/post-install
@@ -434,14 +457,20 @@ Some operations log to ~$logfile~, others don't - standardize logging
All package installs should log, all system modifications should log, all errors should log with context
Makes debugging failed installations easier
-*** TODO [#C] Add backup before system file modifications
+*** TODO [#B] Add backup before system file modifications
Safety net for /etc/X11/xorg.conf.d and other system file edits
Files like ~/etc/sudoers~, ~/etc/pacman.conf~, ~/etc/default/grub~ modified without backup
If modifications fail or are incorrect, difficult to recover - should backup files to ~.backup~ before modifying
-*** TODO [#C] Parse and improve AUR error reporting
+*** TODO [#D] Parse and improve AUR error reporting
Parse yay errors and provide specific, actionable fixes instead of generic error messages
+*** TODO [#D] Improve progress indicators throughout install
+Enhance existing indicators to show what's happening in real-time
+
+*** TODO [#D] Add retry logic to git_install function
+pacman_install and aur_install have retry logic, but git_install doesn't
+
*** TODO [#D] Add cpupower installation and enabling to archsetup
cpupower service configures the default CPU scheduler (powersave or performance)
Install cpupower, configure /etc/default/cpupower, enable service: ~systemctl enable --now cpupower.service~
@@ -449,6 +478,19 @@ Install cpupower, configure /etc/default/cpupower, enable service: ~systemctl en
*** VERIFY [#C] FZF works everywhere
Especially the ** expander for all files - may already be fixed, needs verification
+*** CANCELLED [#B] Review slow and failed packages from 8GB RAM test
+CLOSED: [2026-01-27 Tue 06:24]
+See [[file:docs/slow-failed-packages.org][Slow and Failed Packages Analysis]]
+
+Test run from 2025-11-09 with 8GB RAM, 50GB disk identified:
+- 2 packages that hang indefinitely (anki, tageditor)
+- 4 packages that fail to install (nitrogen, gtk-engine-murrine, adwaita-color-schemes, vagrant)
+- Several slow but successful packages (multimarkdown, ptyxis, thunderbird, etc.)
+
+High priority actions:
+- Remove or make optional: anki (hangs 98 min), tageditor (hangs on qt5-webengine)
+- Investigate repository/build issues for failing packages
+
*** DONE [#A] Fix: no dotfiles were set up on last run
CLOSED: [2025-11-13 Wed]
RESOLVED - VM test confirms dotfiles are properly stowed as symlinks; all configs and scripts in place
@@ -500,6 +542,28 @@ CLOSED: [2026-01-20 Mon]
NVMe devices weren't available when ZFS hook tried to import pool at boot.
RESOLVED: Added has_nvme_drives() detection and auto-add nvme to mkinitcpio MODULES.
+*** DONE [#B] Improve error handling: UFW firewall, rmmod pcspkr, mkdir missing quotes
+CLOSED: [2026-01-27 Tue 06:24]
+**** DONE [#B] Fix UFW firewall error handling (archsetup:395,410)
+CLOSED: [2026-01-21 Wed]
+Firewall failures use ~|| error "error"~ which logs but continues - system may be left exposed
+Should use ~|| error "crash"~ or validate rules were applied successfully
+RESOLVED: Added firewall verification after setup (checks "ufw status | grep Status: active").
+If verification fails, displays CRITICAL SECURITY WARNING in outro with manual fix commands.
+**** DONE [#B] Fix rmmod pcspkr error (archsetup:588)
+CLOSED: [2026-01-21 Wed]
+~rmmod pcspkr~ doesn't check if module is loaded, produces error if already unloaded
+Should use ~rmmod pcspkr 2>/dev/null || true~ or check with ~lsmod~
+RESOLVED: Changed to ~rmmod pcspkr 2>/dev/null || true~
+**** DONE [#B] Fix mkdir missing quotes (archsetup:247)
+CLOSED: [2026-01-21 Wed]
+Line 247: ~mkdir -p $source_dir~ should be ~mkdir -p "$source_dir"~ - fails if path contains spaces
+RESOLVED: Current code at line 577 properly quotes: ~(mkdir -p "$source_dir")~
+
+*** DONE [#B] Test complete end-to-end run on fresh VM
+CLOSED: [2026-01-27 Tue 06:24]
+Validates the script actually works in a clean environment (blocks claiming Method 1 complete)
+
*** DONE [#B] Add random.trust_cpu=off to kernel parameters
CLOSED: [2026-01-20 Mon]
AMD RDSEED warnings appearing at boot ("RDSEED32 is broken").
@@ -563,8 +627,9 @@ RESOLVED: Standardized rofi configuration in commit 590aa02:
**** DONE [#B] Match Rofi CSS style to notification CSS and move into proper place
CLOSED: [2025-12-01 Sun]
Rofi theme now matches dunst notifications (colors, border-radius, font)
-**** TODO [#C] Consider rofi-wayland for future Wayland migration
-~rofi~ doesn't support Wayland - evaluate ~rofi-wayland~, ~wofi~, or ~fuzzel~ for future
+**** CANCELLED [#C] Consider rofi-wayland for future Wayland migration
+CLOSED: [2026-01-27 Tue]
+Chose fuzzel for Wayland launcher and wlogout for exit menu. No need for rofi-wayland.
*** DONE [#B] Complete Warpinator setup for file transfers
CLOSED: [2026-01-21 Wed]
@@ -750,34 +815,18 @@ Parse shell history files for ~/.local/bin script names to identify last usage d
*** TODO [#B] Automate dotfile validation
Parse config files for binary/command references and verify those binaries exist - catch orphaned references
-*** TODO [#C] Review and reorganize dotfiles for unused applications
-Review all dotfiles by application and remove unused application configurations.
-
-Options:
-1. Move to new =dotfiles/unused/= directory (next to =dotfiles/common/=)
-2. Design better restowing mechanism (perhaps with Makefile)
- - Selective stowing of only active applications
- - Track which configs are actually in use
- - Make it easy to enable/disable application dotfiles
-
-Benefits:
-- Cleaner dotfiles directory with only actively used configs
-- Faster stow operations (fewer files to process)
-- Clear separation between active and archived configurations
-- Easier to maintain and understand what's actually being used
-
-Current state:
-- Many application configs for apps removed from archsetup (mpd, ncmpcpp, mopidy, obs-studio, obsidian)
-- Unclear which dotfiles correspond to currently installed applications
-- No easy way to selectively stow/unstow configs
-
-Reference: Identified on 2025-11-15 during package cleanup session
+*** DONE [#C] Review and reorganize dotfiles for unused applications
+CLOSED: [2026-01-27 Tue]
+Completed through multiple cleanup sessions: deleted configs for uninstalled apps (ghostty, lf,
+mopidy, nitrogen, pychess, JetBrains, youtube-dl, sublime-merge), reduced scripts from 112 to 48.
+Dotfiles now contain only active configs. Archive directory and selective stow tooling not needed.
*** TODO [#C] Set up alerts for deprecated packages
Proactive monitoring integrated with Method 2 testing
-*** TODO [#C] Cleanup dotfiles repository
-The .dotfiles repo has configuration for applications no longer used - remove stale configs
+*** DONE [#C] Cleanup dotfiles repository
+CLOSED: [2026-01-27 Tue]
+Stale configs removed during January 2026 cleanup sessions.
*** DONE [#B] Replace deprecated ntp with chrony
CLOSED: [2026-01-21 Wed]