Add smoke test script and expand manufacturing workflow

This commit is contained in:
Aaron
2026-01-02 23:38:22 -05:00
parent bc97e0374f
commit b01c2ba737
2 changed files with 225 additions and 2 deletions

View File

@@ -10,6 +10,7 @@ This documents the *current* workflow and the *target* workflow once profiles +
3) Update the system if needed. 3) Update the system if needed.
4) Run the prep scrub + verify: 4) Run the prep scrub + verify:
- `sudo ./pikit-prep.sh` - `sudo ./pikit-prep.sh`
- `./pikit-smoke-test.sh`
- (optional) `sudo ./pikit-prep.sh --check-only` - (optional) `sudo ./pikit-prep.sh --check-only`
5) Image the SD card with DietPi Imager. 5) Image the SD card with DietPi Imager.
6) Store it as the golden base (e.g., `images/base/pikit-base-YYYYMMDD.img.xz`). 6) Store it as the golden base (e.g., `images/base/pikit-base-YYYYMMDD.img.xz`).
@@ -26,6 +27,7 @@ This documents the *current* workflow and the *target* workflow once profiles +
- `sudo ufw allow from <LAN subnet> to any port <port>` - `sudo ufw allow from <LAN subnet> to any port <port>`
6) Run the prep scrub + verify: 6) Run the prep scrub + verify:
- `sudo ./pikit-prep.sh` - `sudo ./pikit-prep.sh`
- `./pikit-smoke-test.sh`
- (optional) `sudo ./pikit-prep.sh --check-only` - (optional) `sudo ./pikit-prep.sh --check-only`
7) Image the SD card via the QEMU DietPi VM: 7) Image the SD card via the QEMU DietPi VM:
- Insert the SD card into your desktop. - Insert the SD card into your desktop.
@@ -54,6 +56,7 @@ This documents the *current* workflow and the *target* workflow once profiles +
- Confirms services + ports match the profile + base. - Confirms services + ports match the profile + base.
6) Run the prep scrub + verify: 6) Run the prep scrub + verify:
- `sudo ./pikit-prep.sh` - `sudo ./pikit-prep.sh`
- `./pikit-smoke-test.sh`
- (optional) `sudo ./pikit-prep.sh --check-only` - (optional) `sudo ./pikit-prep.sh --check-only`
7) Image the SD card with DietPi Imager. 7) Image the SD card with DietPi Imager.
@@ -74,7 +77,7 @@ Use the helper:
- `sudo ./flash_sd.sh <image.img.xz> /dev/sdX` - `sudo ./flash_sd.sh <image.img.xz> /dev/sdX`
## 4) Manufacturing / imaging checklist (production) ## 4) Manufacturing / imaging checklist (production)
1) Start from the golden base image. 1) Start from the golden base image (stored in `images/base/`).
2) Flash it to a knowngood SD card. 2) Flash it to a knowngood SD card.
3) Boot and verify: 3) Boot and verify:
- `http://pikit.local` and `https://pikit.local` - `http://pikit.local` and `https://pikit.local`
@@ -83,9 +86,13 @@ Use the helper:
4) Apply any required profile/services. 4) Apply any required profile/services.
5) Run prep + verify: 5) Run prep + verify:
- `sudo ./pikit-prep.sh` - `sudo ./pikit-prep.sh`
- `./pikit-smoke-test.sh`
6) Power down cleanly. 6) Power down cleanly.
7) Image the SD card (DietPi Imager via QEMU or ondevice). 7) Image the SD card (DietPi Imager via QEMU or ondevice).
8) Label and archive the image (include date + profile name). 8) Name and archive the image:
- Base: `images/base/pikit-base-YYYYMMDD-dietpi9.20.1.img.xz`
- Profile: `images/profiles/pikit-<profile>-YYYYMMDD.img.xz`
- Testing/staging: `images/staging/pikit-<profile>-YYYYMMDD-rcN.img.xz`
9) Smoke test the flashed image on a second SD card: 9) Smoke test the flashed image on a second SD card:
- boot → firstboot → dashboard → services - boot → firstboot → dashboard → services

216
pikit-smoke-test.sh Executable file
View File

@@ -0,0 +1,216 @@
#!/bin/bash
# Pi-Kit post-prep smoke test (HTTP/HTTPS/API/firstboot/services)
set -euo pipefail
PIKIT_HOST="${PIKIT_HOST:-pikit.local}"
PIKIT_USER="${PIKIT_USER:-dietpi}"
PIKIT_SSH_KEY="${PIKIT_SSH_KEY:-$HOME/.ssh/pikit}"
PIKIT_SSH_OPTS="${PIKIT_SSH_OPTS:-}"
PIKIT_HTTP_URL="${PIKIT_HTTP_URL:-http://$PIKIT_HOST}"
PIKIT_HTTPS_URL="${PIKIT_HTTPS_URL:-https://$PIKIT_HOST}"
LOCAL_ONLY=0
ERRORS=0
WARNINGS=0
usage() {
cat <<'USAGE'
Usage: pikit-smoke-test.sh [--local]
Runs a quick post-prep smoke test:
- HTTP/HTTPS reachable
- API reachable and returns JSON
- firstboot state done
- core services active (nginx, pikit-api, dietpi-dashboard-frontend)
Options:
--local Run locally on the Pi (skip SSH)
--help Show this help
Env:
PIKIT_HOST, PIKIT_USER, PIKIT_SSH_KEY, PIKIT_SSH_OPTS
PIKIT_HTTP_URL, PIKIT_HTTPS_URL
USAGE
}
status() {
local level="$1"
shift
printf '[%s] %s\n' "$level" "$*"
case "$level" in
FAIL) ERRORS=$((ERRORS + 1)) ;;
WARN) WARNINGS=$((WARNINGS + 1)) ;;
esac
}
section() {
printf '\n== %s ==\n' "$1"
}
is_dietpi() {
grep -qi "dietpi" /etc/os-release 2>/dev/null
}
parse_args() {
for arg in "$@"; do
case "$arg" in
--local) LOCAL_ONLY=1 ;;
--help|-h) usage; exit 0 ;;
*)
echo "[FAIL] Unknown argument: $arg" >&2
usage
exit 1
;;
esac
done
}
remote_cmd() {
local cmd="$1"
if [ "$LOCAL_ONLY" -eq 1 ] || is_dietpi; then
bash -c "$cmd"
else
ssh -i "$PIKIT_SSH_KEY" $PIKIT_SSH_OPTS -o ConnectTimeout=10 "${PIKIT_USER}@${PIKIT_HOST}" "$cmd"
fi
}
json_get() {
local key="$1"
if command -v python3 >/dev/null 2>&1; then
python3 - "$key" <<'PY'
import json
import sys
key = sys.argv[1]
try:
data = json.load(sys.stdin)
except Exception:
print("")
sys.exit(1)
val = data.get(key, "")
if isinstance(val, bool):
print("true" if val else "false")
else:
print(val)
PY
elif command -v jq >/dev/null 2>&1; then
jq -r --arg key "$key" '.[$key] // empty'
else
cat >/dev/null
echo ""
return 1
fi
}
check_http() {
local url="$1"
local label="$2"
if curl -fsS --max-time 5 "$url" >/dev/null; then
status OK "$label reachable"
else
status FAIL "$label not reachable"
fi
}
check_https() {
local url="$1"
local label="$2"
if curl -kfsS --max-time 5 "$url" >/dev/null; then
status OK "$label reachable"
else
status FAIL "$label not reachable"
fi
}
check_api() {
local url="$1"
local body
if ! body="$(curl -fsS --max-time 5 "$url")"; then
status FAIL "API not reachable: $url"
return
fi
local ok
ok="$(printf "%s" "$body" | json_get "services" || true)"
if [ -n "$ok" ]; then
status OK "API responds with JSON"
else
status WARN "API response did not include expected fields"
fi
}
check_firstboot() {
local url="$1"
local body state error_present
if ! body="$(curl -fsS --max-time 5 "$url")"; then
status FAIL "firstboot API not reachable"
return
fi
state="$(printf "%s" "$body" | json_get "state" || true)"
error_present="$(printf "%s" "$body" | json_get "error_present" || true)"
if [ "$state" = "done" ] && [ "$error_present" != "true" ]; then
status OK "firstboot completed"
else
status FAIL "firstboot not complete (state=$state error=$error_present)"
fi
}
check_services() {
local services=("nginx" "pikit-api" "dietpi-dashboard-frontend")
for svc in "${services[@]}"; do
if remote_cmd "systemctl is-active --quiet $svc"; then
status OK "$svc active"
else
status FAIL "$svc not active"
fi
done
}
check_ports() {
local cmd="ss -lnt | awk '{print \$4}' | grep -E ':(80|443|5252|5253)\$' | sort -u"
local out
if out="$(remote_cmd "$cmd" 2>/dev/null)"; then
if echo "$out" | grep -q ":80" && echo "$out" | grep -q ":443"; then
status OK "ports 80/443 listening"
else
status WARN "ports 80/443 not both listening"
fi
else
status WARN "unable to check ports"
fi
}
finalize() {
section "Summary"
status OK "warnings: $WARNINGS"
status OK "errors: $ERRORS"
if [ "$ERRORS" -gt 0 ]; then
echo "[FAIL] Smoke test failed."
exit 1
fi
echo "[OK] Smoke test passed."
}
main() {
parse_args "$@"
section "HTTP/HTTPS"
check_http "$PIKIT_HTTP_URL" "HTTP"
check_https "$PIKIT_HTTPS_URL" "HTTPS"
section "API"
check_api "$PIKIT_HTTP_URL/api/status"
section "Firstboot"
check_firstboot "$PIKIT_HTTP_URL/api/firstboot"
section "Services"
check_services
section "Ports"
check_ports
finalize
}
main "$@"