1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
|
#!/bin/bash
# sanity-test.sh - Automated sanity test for archzfs ISO
#
# Boots the ISO in a headless QEMU VM, waits for SSH, runs verification
# commands, and reports pass/fail. Fully automated - no human input required.
#
# Usage:
# ./scripts/sanity-test.sh # Run sanity test
# ./scripts/sanity-test.sh --verbose # Show detailed output
#
# Exit codes:
# 0 - All tests passed
# 1 - One or more tests failed
# 2 - Setup/infrastructure error (QEMU, SSH, etc.)
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PROJECT_DIR="$(dirname "$SCRIPT_DIR")"
# VM Configuration
VM_DIR="$PROJECT_DIR/vm"
VM_DISK="$VM_DIR/sanity-test.qcow2"
VM_DISK_SIZE="10G"
VM_RAM="2048"
VM_CPUS="2"
VM_NAME="archzfs-sanity"
# UEFI firmware
OVMF_CODE="/usr/share/edk2/x64/OVMF_CODE.4m.fd"
OVMF_VARS_ORIG="/usr/share/edk2/x64/OVMF_VARS.4m.fd"
OVMF_VARS="$VM_DIR/sanity-test-OVMF_VARS.fd"
# SSH settings
SSH_PORT=2223 # Different port to avoid conflicts with test-vm.sh
SSH_USER="root"
SSH_PASS="archzfs"
SSH_TIMEOUT=180 # Max seconds to wait for SSH
SSH_OPTS="-o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o LogLevel=ERROR -o ConnectTimeout=5"
# Colors
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
CYAN='\033[0;36m'
NC='\033[0m'
# State
QEMU_PID=""
VERBOSE=false
TESTS_PASSED=0
TESTS_FAILED=0
info() { echo -e "${GREEN}[INFO]${NC} $1"; }
warn() { echo -e "${YELLOW}[WARN]${NC} $1"; }
error() { echo -e "${RED}[ERROR]${NC} $1"; }
pass() { echo -e "${GREEN}[PASS]${NC} $1"; ((TESTS_PASSED++)); }
fail() { echo -e "${RED}[FAIL]${NC} $1"; ((TESTS_FAILED++)); }
# Parse arguments
while [[ $# -gt 0 ]]; do
case $1 in
--verbose|-v) VERBOSE=true; shift ;;
-h|--help)
echo "Usage: $0 [--verbose]"
echo ""
echo "Automated sanity test for archzfs ISO."
echo "Boots ISO in headless QEMU, verifies via SSH, reports results."
exit 0
;;
*) error "Unknown option: $1"; exit 2 ;;
esac
done
# Find the ISO
find_iso() {
ISO_FILE=$(ls -t "$PROJECT_DIR/out/"*.iso 2>/dev/null | head -1)
if [[ -z "$ISO_FILE" ]]; then
error "No ISO found in $PROJECT_DIR/out/"
exit 2
fi
info "Testing ISO: $(basename "$ISO_FILE")"
}
# Setup VM resources
setup_vm() {
mkdir -p "$VM_DIR"
# Create a fresh disk for sanity testing
if [[ -f "$VM_DISK" ]]; then
rm -f "$VM_DISK"
fi
qemu-img create -f qcow2 "$VM_DISK" "$VM_DISK_SIZE" >/dev/null 2>&1
# Copy OVMF vars
cp "$OVMF_VARS_ORIG" "$OVMF_VARS"
}
# Cleanup on exit
cleanup() {
if [[ -n "$QEMU_PID" ]] && kill -0 "$QEMU_PID" 2>/dev/null; then
info "Shutting down VM..."
kill "$QEMU_PID" 2>/dev/null || true
wait "$QEMU_PID" 2>/dev/null || true
fi
# Clean up sanity test disk (leave main test disk alone)
rm -f "$VM_DISK" "$OVMF_VARS" 2>/dev/null || true
}
trap cleanup EXIT
# Start QEMU in headless mode
start_vm() {
info "Starting headless VM..."
qemu-system-x86_64 \
-name "$VM_NAME" \
-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=$VM_DISK,format=qcow2,if=virtio" \
-cdrom "$ISO_FILE" \
-boot d \
-netdev user,id=net0,hostfwd=tcp::${SSH_PORT}-:22 \
-device virtio-net-pci,netdev=net0 \
-display none \
-serial null \
-daemonize \
-pidfile "$VM_DIR/sanity-test.pid"
sleep 1
if [[ -f "$VM_DIR/sanity-test.pid" ]]; then
QEMU_PID=$(cat "$VM_DIR/sanity-test.pid")
if kill -0 "$QEMU_PID" 2>/dev/null; then
info "VM started (PID: $QEMU_PID)"
else
error "VM failed to start"
exit 2
fi
else
error "VM failed to start - no PID file"
exit 2
fi
}
# Wait for SSH to become available
wait_for_ssh() {
info "Waiting for SSH (timeout: ${SSH_TIMEOUT}s)..."
local elapsed=0
local interval=5
while [[ $elapsed -lt $SSH_TIMEOUT ]]; do
if sshpass -p "$SSH_PASS" ssh $SSH_OPTS -p "$SSH_PORT" "$SSH_USER@localhost" "true" 2>/dev/null; then
info "SSH available after ${elapsed}s"
return 0
fi
sleep $interval
((elapsed += interval))
if $VERBOSE; then
echo -n "."
fi
done
error "SSH timeout after ${SSH_TIMEOUT}s"
return 1
}
# Run a test command via SSH
run_test() {
local name="$1"
local cmd="$2"
local expect_output="$3" # Optional: string that should be in output
if $VERBOSE; then
echo -e "${CYAN}Testing:${NC} $name"
echo -e "${CYAN}Command:${NC} $cmd"
fi
local output
output=$(sshpass -p "$SSH_PASS" ssh $SSH_OPTS -p "$SSH_PORT" "$SSH_USER@localhost" "$cmd" 2>&1) || {
fail "$name (command failed)"
if $VERBOSE; then
echo " Output: $output"
fi
return 1
}
if [[ -n "$expect_output" ]]; then
if echo "$output" | grep -q "$expect_output"; then
pass "$name"
if $VERBOSE; then
echo " Output: $output"
fi
return 0
else
fail "$name (expected '$expect_output' not found)"
if $VERBOSE; then
echo " Output: $output"
fi
return 1
fi
else
pass "$name"
if $VERBOSE; then
echo " Output: $output"
fi
return 0
fi
}
# Run all sanity tests
run_sanity_tests() {
echo ""
echo -e "${CYAN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
echo -e "${CYAN} SANITY TESTS${NC}"
echo -e "${CYAN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
echo ""
# Test 1: ZFS kernel module loaded
run_test "ZFS kernel module loaded" \
"lsmod | grep -q '^zfs' && echo 'zfs module loaded'" \
"zfs module loaded"
# Test 2: ZFS commands work
run_test "ZFS version command works" \
"zfs version | head -1" \
"zfs-"
# Test 3: zpool command works
run_test "zpool command works" \
"zpool version | head -1" \
"zfs-"
# Test 4: Custom scripts present
run_test "install-archzfs script present" \
"test -x /usr/local/bin/install-archzfs && echo 'exists'" \
"exists"
run_test "zfsrollback script present" \
"test -x /usr/local/bin/zfsrollback && echo 'exists'" \
"exists"
run_test "zfssnapshot script present" \
"test -x /usr/local/bin/zfssnapshot && echo 'exists'" \
"exists"
run_test "grub-zfs-snap script present" \
"test -x /usr/local/bin/grub-zfs-snap && echo 'exists'" \
"exists"
run_test "zfs-snap-prune script present" \
"test -x /usr/local/bin/zfs-snap-prune && echo 'exists'" \
"exists"
# Test 5: fzf installed (required by zfsrollback)
run_test "fzf installed" \
"command -v fzf && echo 'found'" \
"found"
# Test 6: SSH is working (implicit - we're connected)
pass "SSH connectivity"
# Test 6b: Root password is set (not empty in shadow file)
run_test "Root password is set" \
"grep '^root:' /etc/shadow | cut -d: -f2 | grep -q '.' && echo 'password set'" \
"password set"
# Test 7: Network manager available
run_test "NetworkManager available" \
"systemctl is-enabled NetworkManager 2>/dev/null || echo 'available'" \
""
# Test 8: Avahi mDNS for network discovery
run_test "Avahi package installed" \
"command -v avahi-daemon && echo 'found'" \
"found"
run_test "Avahi daemon enabled" \
"systemctl is-enabled avahi-daemon" \
"enabled"
run_test "Avahi daemon running" \
"systemctl is-active avahi-daemon" \
"active"
run_test "nss-mdns configured" \
"grep -q 'mdns' /etc/nsswitch.conf && echo 'configured'" \
"configured"
# Test 9: Hostname set to archzfs
run_test "Hostname is archzfs" \
"cat /etc/hostname" \
"archzfs"
# Test 10: Kernel version (LTS)
run_test "Running LTS kernel" \
"uname -r" \
"lts"
# Test 10: archsetup directory present
run_test "archsetup directory present" \
"test -d /code/archsetup && echo 'exists'" \
"exists"
echo ""
}
# Print summary
print_summary() {
echo -e "${CYAN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
echo -e "${CYAN} SUMMARY${NC}"
echo -e "${CYAN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
echo ""
echo -e " Tests passed: ${GREEN}$TESTS_PASSED${NC}"
echo -e " Tests failed: ${RED}$TESTS_FAILED${NC}"
echo ""
if [[ $TESTS_FAILED -eq 0 ]]; then
echo -e "${GREEN}All sanity tests passed!${NC}"
return 0
else
echo -e "${RED}Some tests failed.${NC}"
return 1
fi
}
# Main
main() {
echo ""
echo -e "${CYAN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
echo -e "${CYAN} ARCHZFS ISO SANITY TEST${NC}"
echo -e "${CYAN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
echo ""
# Check dependencies
command -v qemu-system-x86_64 >/dev/null || { error "qemu-system-x86_64 not found"; exit 2; }
command -v sshpass >/dev/null || { error "sshpass not found"; exit 2; }
[[ -f "$OVMF_CODE" ]] || { error "OVMF firmware not found at $OVMF_CODE"; exit 2; }
find_iso
setup_vm
start_vm
if ! wait_for_ssh; then
error "Could not connect to VM via SSH"
exit 2
fi
run_sanity_tests
if print_summary; then
exit 0
else
exit 1
fi
}
main "$@"
|