Improve ISO installer flow and VM tooling
This commit is contained in:
8 files changed
+807
-246
No files matched your search
+119
-12
@@ -2,28 +2,135 @@
|
||||
|
||||
set -e
|
||||
|
||||
ISO="$1"
|
||||
REUSE="$2"
|
||||
ISO=""
|
||||
REUSE=""
|
||||
SSH_HOST="${OMARCHY_VM_SSH_HOST:-127.0.0.1}"
|
||||
SSH_PORT="${OMARCHY_VM_SSH_PORT:-2222}"
|
||||
NETWORK="${OMARCHY_VM_NETWORK:-1}"
|
||||
DISK="${OMARCHY_VM_DISK:-/tmp/omarchy-iso-boot.qcow2}"
|
||||
OVMF_VARS="${OMARCHY_VM_OVMF_VARS:-/tmp/OVMF_VARS.4m.fd}"
|
||||
EXTRA_QEMU_ARGS=()
|
||||
|
||||
if [ $# -eq 0 ]; then
|
||||
usage() {
|
||||
cat <<USAGE
|
||||
Usage: omarchy-iso-boot [options] [release/omarchy.iso] [reuse]
|
||||
|
||||
Options:
|
||||
--reuse Reuse the existing VM disk
|
||||
--ssh-port PORT Forward host PORT to guest port 22 (default: $SSH_PORT)
|
||||
--ssh-host HOST Host address for SSH forwarding (default: $SSH_HOST)
|
||||
--no-network Boot without a virtual NIC
|
||||
--disk PATH VM disk path (default: $DISK)
|
||||
--ovmf-vars PATH OVMF vars path (default: $OVMF_VARS)
|
||||
-h, --help Show this help
|
||||
-- Pass the remaining arguments directly to QEMU
|
||||
|
||||
SSH access after starting sshd in the guest:
|
||||
ssh -p $SSH_PORT root@localhost
|
||||
scp -P $SSH_PORT /path/to/system-manifest root@localhost:/tmp/
|
||||
USAGE
|
||||
}
|
||||
|
||||
while (( $# > 0 )); do
|
||||
case "$1" in
|
||||
--reuse|reuse)
|
||||
REUSE="reuse"
|
||||
shift
|
||||
;;
|
||||
--ssh-port)
|
||||
SSH_PORT="${2:-}"
|
||||
shift 2
|
||||
;;
|
||||
--ssh-port=*)
|
||||
SSH_PORT="${1#*=}"
|
||||
shift
|
||||
;;
|
||||
--ssh-host)
|
||||
SSH_HOST="${2:-}"
|
||||
shift 2
|
||||
;;
|
||||
--ssh-host=*)
|
||||
SSH_HOST="${1#*=}"
|
||||
shift
|
||||
;;
|
||||
--no-network|--no-net)
|
||||
NETWORK=0
|
||||
shift
|
||||
;;
|
||||
--disk)
|
||||
DISK="${2:-}"
|
||||
shift 2
|
||||
;;
|
||||
--disk=*)
|
||||
DISK="${1#*=}"
|
||||
shift
|
||||
;;
|
||||
--ovmf-vars)
|
||||
OVMF_VARS="${2:-}"
|
||||
shift 2
|
||||
;;
|
||||
--ovmf-vars=*)
|
||||
OVMF_VARS="${1#*=}"
|
||||
shift
|
||||
;;
|
||||
-h|--help)
|
||||
usage
|
||||
exit 0
|
||||
;;
|
||||
--)
|
||||
shift
|
||||
EXTRA_QEMU_ARGS+=("$@")
|
||||
break
|
||||
;;
|
||||
*)
|
||||
if [[ -z $ISO ]]; then
|
||||
ISO="$1"
|
||||
else
|
||||
echo "Unexpected argument: $1" >&2
|
||||
usage >&2
|
||||
exit 1
|
||||
fi
|
||||
shift
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
if [[ -z $ISO && -t 0 ]]; then
|
||||
ISO=$(ls -t release/*.iso 2>/dev/null | gum choose --header "Select ISO to boot")
|
||||
if gum confirm --default=false "Reuse existing disk?"; then
|
||||
REUSE="reuse"
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ -z "$ISO" ]; then
|
||||
echo "Usage: omarchy-iso-boot [release/omarchy.iso] [reuse]"
|
||||
echo " SSH available at: ssh -p 2222 root@localhost (after installing openssh)"
|
||||
if [[ -z $ISO ]]; then
|
||||
usage
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ -z $DISK || -z $OVMF_VARS ]]; then
|
||||
echo "Disk and OVMF vars paths must not be empty" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
omarchy-pkg-add qemu-full edk2-ovmf
|
||||
|
||||
DISK="/tmp/omarchy-iso-boot.qcow2"
|
||||
if [[ ! -f $DISK || $REUSE != "reuse" ]]; then
|
||||
qemu-img create -f qcow2 "$DISK" 40G
|
||||
cp /usr/share/edk2/x64/OVMF_VARS.4m.fd /tmp/OVMF_VARS.4m.fd
|
||||
cp /usr/share/edk2/x64/OVMF_VARS.4m.fd "$OVMF_VARS"
|
||||
fi
|
||||
|
||||
NET_ARGS=()
|
||||
if [[ $NETWORK != "0" && $NETWORK != "false" && $NETWORK != "no" ]]; then
|
||||
if [[ -n $SSH_PORT && $SSH_PORT != "0" ]]; then
|
||||
NET_ARGS=(-netdev "user,id=net0,hostfwd=tcp:${SSH_HOST}:${SSH_PORT}-:22" -device virtio-net-pci,netdev=net0)
|
||||
echo "SSH forwarding enabled: ssh -p $SSH_PORT root@localhost"
|
||||
echo "SCP example: scp -P $SSH_PORT /path/to/system-manifest root@localhost:/tmp/"
|
||||
else
|
||||
NET_ARGS=(-netdev user,id=net0 -device virtio-net-pci,netdev=net0)
|
||||
echo "Networking enabled without SSH port forwarding."
|
||||
fi
|
||||
else
|
||||
echo "Networking disabled."
|
||||
fi
|
||||
|
||||
# Boot from ISO before we've installed, then boot from drive after
|
||||
@@ -32,7 +139,7 @@ qemu-system-x86_64 \
|
||||
-smp "$(nproc)" \
|
||||
-m 8192 \
|
||||
-drive if=pflash,format=raw,readonly=on,file=/usr/share/edk2/x64/OVMF_CODE.4m.fd \
|
||||
-drive if=pflash,format=raw,file=/tmp/OVMF_VARS.4m.fd \
|
||||
-drive if=pflash,format=raw,file="$OVMF_VARS" \
|
||||
-drive file="$DISK",format=qcow2,if=none,id=drive0 \
|
||||
-device virtio-blk-pci,drive=drive0,bootindex=1 \
|
||||
-drive file="$ISO",media=cdrom,if=none,format=raw,id=cdrom0 \
|
||||
@@ -40,6 +147,6 @@ qemu-system-x86_64 \
|
||||
-device virtio-vga-gl \
|
||||
-display gtk,gl=on \
|
||||
-usb -device usb-tablet \
|
||||
-netdev user,id=net0,hostfwd=tcp::2222-:22 \
|
||||
-device virtio-net-pci,netdev=net0 \
|
||||
-boot menu=on
|
||||
"${NET_ARGS[@]}" \
|
||||
-boot menu=on \
|
||||
"${EXTRA_QEMU_ARGS[@]}"
|
||||
+68
-15
@@ -4,8 +4,8 @@ set -e
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
VM_DIR="$(dirname "$SCRIPT_DIR")/vm-saves"
|
||||
TMP_DISK="/tmp/omarchy-iso-boot.qcow2"
|
||||
TMP_OVMF="/tmp/OVMF_VARS.4m.fd"
|
||||
TMP_DISK="${OMARCHY_VM_DISK:-/tmp/omarchy-iso-boot.qcow2}"
|
||||
TMP_OVMF="${OMARCHY_VM_OVMF_VARS:-/tmp/OVMF_VARS.4m.fd}"
|
||||
|
||||
mkdir -p "$VM_DIR"
|
||||
|
||||
@@ -18,7 +18,7 @@ save_vm() {
|
||||
fi
|
||||
|
||||
if [[ ! -f "$TMP_DISK" || ! -f "$TMP_OVMF" ]]; then
|
||||
echo "Error: No active VM found in /tmp"
|
||||
echo "Error: No active VM found at $TMP_DISK / $TMP_OVMF"
|
||||
echo "Boot a VM first with: omarchy-iso-boot [iso]"
|
||||
exit 1
|
||||
fi
|
||||
@@ -53,7 +53,44 @@ list_vms() {
|
||||
}
|
||||
|
||||
boot_vm() {
|
||||
local name="$1"
|
||||
local name=""
|
||||
local boot_args=()
|
||||
local active_disk="$TMP_DISK"
|
||||
local active_ovmf="$TMP_OVMF"
|
||||
|
||||
if [[ $# -gt 0 && ${1:-} != --* ]]; then
|
||||
name="$1"
|
||||
shift
|
||||
fi
|
||||
|
||||
while (( $# > 0 )); do
|
||||
case "$1" in
|
||||
--disk)
|
||||
active_disk="${2:-}"
|
||||
boot_args+=("$1" "${2:-}")
|
||||
shift 2
|
||||
;;
|
||||
--disk=*)
|
||||
active_disk="${1#*=}"
|
||||
boot_args+=("$1")
|
||||
shift
|
||||
;;
|
||||
--ovmf-vars)
|
||||
active_ovmf="${2:-}"
|
||||
boot_args+=("$1" "${2:-}")
|
||||
shift 2
|
||||
;;
|
||||
--ovmf-vars=*)
|
||||
active_ovmf="${1#*=}"
|
||||
boot_args+=("$1")
|
||||
shift
|
||||
;;
|
||||
*)
|
||||
boot_args+=("$1")
|
||||
shift
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
if [[ -z "$(ls -A "$VM_DIR" 2>/dev/null)" ]]; then
|
||||
echo "No VM snapshots available"
|
||||
@@ -73,25 +110,34 @@ boot_vm() {
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ -f "$TMP_DISK" ]] && ! gum confirm "Override existing /tmp VM?"; then
|
||||
if [[ -z $active_disk || -z $active_ovmf ]]; then
|
||||
echo "Error: active disk and OVMF vars paths must not be empty"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ -f $active_disk ]] && ! gum confirm "Override existing active VM disk at $active_disk?"; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo "Copying VM '$name' to /tmp..."
|
||||
cp "$vm_path/omarchy-iso-boot.qcow2" "$TMP_DISK"
|
||||
cp "$vm_path/OVMF_VARS.4m.fd" "$TMP_OVMF"
|
||||
echo "Copying VM '$name' to $active_disk..."
|
||||
mkdir -p "$(dirname "$active_disk")" "$(dirname "$active_ovmf")"
|
||||
cp "$vm_path/omarchy-iso-boot.qcow2" "$active_disk"
|
||||
cp "$vm_path/OVMF_VARS.4m.fd" "$active_ovmf"
|
||||
|
||||
gum style --foreground 212 "✓ VM ready. Booting..."
|
||||
|
||||
exec omarchy-iso-boot /dev/null reuse
|
||||
exec omarchy-iso-boot "${boot_args[@]}" /dev/null reuse
|
||||
}
|
||||
|
||||
case "${1:-}" in
|
||||
command="${1:-}"
|
||||
[[ $# -gt 0 ]] && shift
|
||||
|
||||
case "$command" in
|
||||
save)
|
||||
save_vm "$2"
|
||||
save_vm "${1:-}"
|
||||
;;
|
||||
boot)
|
||||
boot_vm "$2"
|
||||
boot_vm "$@"
|
||||
;;
|
||||
list)
|
||||
list_vms
|
||||
@@ -101,9 +147,16 @@ case "${1:-}" in
|
||||
"$(gum style --bold 'omarchy-vm') - VM snapshot manager" \
|
||||
"" \
|
||||
"$(gum style --foreground 245 'Commands:')" \
|
||||
" save [name] Save current VM from /tmp" \
|
||||
" boot [name] Boot saved VM (interactive if no name)" \
|
||||
" list List available VMs" \
|
||||
" save [name] Save current active VM" \
|
||||
" boot [name] [boot-options] Boot saved VM (interactive if no name)" \
|
||||
" list List available VMs" \
|
||||
"" \
|
||||
"$(gum style --foreground 245 'Boot options:')" \
|
||||
" --ssh-port PORT Forward host PORT to guest SSH (default: 2222)" \
|
||||
" --ssh-host HOST Host address for SSH forwarding (default: 127.0.0.1)" \
|
||||
" --disk PATH Active disk path, useful for parallel VMs" \
|
||||
" --ovmf-vars PATH Active OVMF vars path, useful for parallel VMs" \
|
||||
" --no-network Boot without a virtual NIC" \
|
||||
"" \
|
||||
"$(gum style --foreground 245 'Current VMs:')"
|
||||
list_vms 2>/dev/null || echo " None"
|
||||
|
||||
@@ -1,171 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
# Local smoke test for the foreground installer UI supervisor.
|
||||
# Runs without rebuilding the ISO.
|
||||
set -euo pipefail
|
||||
|
||||
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
DASHBOARD="$ROOT/configs/airootfs/usr/local/bin/omarchy-install-dashboard"
|
||||
AUTOMATED="$ROOT/configs/airootfs/root/.automated_script.sh"
|
||||
ORCH="$ROOT/configs/airootfs/usr/share/omarchy-iso/orchestrator"
|
||||
OMARCHY_PATH_FOR_TEST="${OMARCHY_PATH_FOR_TEST:-$ROOT/../omarchy-installer}"
|
||||
|
||||
tmp="$(mktemp -d)"
|
||||
trap 'rm -rf "$tmp"' EXIT
|
||||
|
||||
strip_ansi() {
|
||||
python - "$1" <<'PY'
|
||||
from pathlib import Path
|
||||
import re, sys
|
||||
s = Path(sys.argv[1]).read_text(errors="ignore")
|
||||
s = re.sub(r"\x1b\[[0-9;?]*[A-Za-z]", "", s)
|
||||
sys.stdout.write(s)
|
||||
PY
|
||||
}
|
||||
|
||||
assert_contains() {
|
||||
local file="$1" needle="$2" stripped
|
||||
stripped="$tmp/stripped.$(basename "$file")"
|
||||
strip_ansi "$file" >"$stripped"
|
||||
if ! grep -Fq "$needle" "$stripped"; then
|
||||
echo "Expected $file to contain: $needle" >&2
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
assert_not_contains() {
|
||||
local file="$1" needle="$2" stripped
|
||||
stripped="$tmp/stripped.$(basename "$file")"
|
||||
strip_ansi "$file" >"$stripped"
|
||||
if grep -Fq "$needle" "$stripped"; then
|
||||
echo "Expected $file not to contain: $needle" >&2
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
write_state_py='import json, sys, time, pathlib
|
||||
path=pathlib.Path(sys.argv[1]); phase=sys.argv[2]
|
||||
state={"started_at":time.time(),"current_phase":phase,"phase_started_at":time.time(),"phases":[]}
|
||||
tmp=path.with_name(".state.json.tmp")
|
||||
tmp.write_text(json.dumps(state))
|
||||
tmp.replace(path)'
|
||||
|
||||
success_worker="$tmp/success-worker"
|
||||
cat >"$success_worker" <<EOF
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
state="\$1"; log="\$2"
|
||||
mkdir -p "\$(dirname "\$state")" "\$(dirname "\$log")"
|
||||
python - "\$state" "Preparing live environment" <<'PY'
|
||||
$write_state_py
|
||||
PY
|
||||
sleep 0.2
|
||||
python - "\$state" "Installing Arch + Omarchy" <<'PY'
|
||||
$write_state_py
|
||||
PY
|
||||
sleep 0.4
|
||||
python - "\$state" "Validating boot setup" <<'PY'
|
||||
$write_state_py
|
||||
PY
|
||||
sleep 0.2
|
||||
cat >>"\$log" <<'LOG'
|
||||
=== Installation Time Summary ===
|
||||
Omarchy: 1m 40s
|
||||
=================================
|
||||
LOG
|
||||
EOF
|
||||
chmod +x "$success_worker"
|
||||
|
||||
no_duration_worker="$tmp/no-duration-worker"
|
||||
cat >"$no_duration_worker" <<EOF
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
state="\$1"
|
||||
mkdir -p "\$(dirname "\$state")"
|
||||
python - "\$state" "Validating boot setup" <<'PY'
|
||||
$write_state_py
|
||||
PY
|
||||
sleep 0.2
|
||||
EOF
|
||||
chmod +x "$no_duration_worker"
|
||||
|
||||
fail_worker="$tmp/fail-worker"
|
||||
cat >"$fail_worker" <<EOF
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
state="\$1"
|
||||
mkdir -p "\$(dirname "\$state")"
|
||||
python - "\$state" "Finalizing in chroot" <<'PY'
|
||||
$write_state_py
|
||||
PY
|
||||
echo "Syncing the system..."
|
||||
echo "Installation completed without any errors."
|
||||
printf '\033[?25h'
|
||||
echo "You may reboot when ready."
|
||||
sleep 0.2
|
||||
exit 7
|
||||
EOF
|
||||
chmod +x "$fail_worker"
|
||||
|
||||
success_state="$tmp/success-state.json"
|
||||
success_log="$tmp/success.log"
|
||||
success_capture="$tmp/success.capture"
|
||||
OMARCHY_UI_AUTO_REBOOT=no OMARCHY_PATH="$OMARCHY_PATH_FOR_TEST" script -qfec \
|
||||
"$DASHBOARD '$success_log' '$success_state' -- '$success_worker' '$success_state' '$success_log'" \
|
||||
"$success_capture" >/dev/null
|
||||
assert_contains "$success_capture" "Installing Omarchy"
|
||||
assert_contains "$success_capture" "Installed in 1m 40s"
|
||||
|
||||
no_duration_state="$tmp/no-duration-state.json"
|
||||
no_duration_log="$tmp/no-duration.log"
|
||||
no_duration_capture="$tmp/no-duration.capture"
|
||||
OMARCHY_UI_AUTO_REBOOT=no OMARCHY_PATH="$OMARCHY_PATH_FOR_TEST" script -qfec \
|
||||
"$DASHBOARD '$no_duration_log' '$no_duration_state' -- '$no_duration_worker' '$no_duration_state'" \
|
||||
"$no_duration_capture" >/dev/null
|
||||
assert_contains "$no_duration_capture" "Finished installing"
|
||||
|
||||
fail_state="$tmp/fail-state.json"
|
||||
fail_log="$tmp/fail.log"
|
||||
fail_capture="$tmp/fail.capture"
|
||||
set +e
|
||||
OMARCHY_UI_AUTO_REBOOT=no OMARCHY_PATH="$OMARCHY_PATH_FOR_TEST" script -qfec \
|
||||
"$DASHBOARD '$fail_log' '$fail_state' -- '$fail_worker' '$fail_state'" \
|
||||
"$fail_capture" >/dev/null
|
||||
fail_status=$?
|
||||
set -e
|
||||
if [[ $fail_status -ne 7 ]]; then
|
||||
echo "Expected failure worker status 7, got $fail_status" >&2
|
||||
exit 1
|
||||
fi
|
||||
assert_contains "$fail_log" "Installation completed without any errors."
|
||||
assert_contains "$fail_log" "[dashboard] installer child exited with status 7"
|
||||
assert_contains "$fail_log" "[dashboard] last installer phase: Finalizing in chroot"
|
||||
python - "$fail_log" <<'PY'
|
||||
from pathlib import Path
|
||||
import sys
|
||||
if "\x1b[?25h" in Path(sys.argv[1]).read_text(errors="ignore"):
|
||||
raise SystemExit("CSI cursor sequence leaked into log")
|
||||
PY
|
||||
assert_not_contains "$fail_capture" "Installed in"
|
||||
assert_not_contains "$fail_capture" "Finished installing"
|
||||
|
||||
bash -n "$DASHBOARD" "$AUTOMATED"
|
||||
python -m py_compile "$ORCH"/*.py
|
||||
git -C "$ROOT" diff --check
|
||||
|
||||
if rg -n 'dashboard\.pid|dashboard\.stop|OMARCHY_INSTALL_DASHBOARD|_stop_install_dashboard|_dashboard_|killpg|current_phase.*Finishing|confirm\(|Reboot now' \
|
||||
"$AUTOMATED" "$DASHBOARD" "$ORCH"; then
|
||||
echo "Forbidden legacy dashboard lifecycle pattern found" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! grep -Fq '/usr/local/bin/omarchy-install-dashboard \' "$AUTOMATED"; then
|
||||
echo "Expected .automated_script.sh to call dashboard in foreground" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if grep -Eq 'omarchy-install-dashboard .*&' "$AUTOMATED"; then
|
||||
echo "Dashboard must not be launched in background" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "installer UI flow smoke test passed"
|
||||
Reference in new issue
Block a user