15 Commits

Author SHA1 Message Date
Aaron
ed162d3c1d Release prep tweaks and version bump 0.1.4 2026-01-03 12:29:55 -05:00
Aaron
d07fab99a6 Add stable release checklist 2026-01-03 11:56:39 -05:00
Aaron
511978666a Add mock API server for Playwright tests 2026-01-03 11:49:41 -05:00
Aaron
453deac601 Update stable manifest for v0.1.3 2026-01-03 00:18:17 -05:00
Aaron
cb2d75387d Bump version to 0.1.3 2026-01-03 00:15:01 -05:00
Aaron
77bc4c1c36 Fix smoke test JSON parsing 2026-01-02 23:58:54 -05:00
Aaron
13e5788fe1 Fix smoke test JSON parsing and set unattended defaults 2026-01-02 23:56:48 -05:00
Aaron
24e89b516f Use local API endpoint in smoke test 2026-01-02 23:49:38 -05:00
Aaron
9c3156df35 Fix smoke test redirects and default updates to stable 2026-01-02 23:45:47 -05:00
Aaron
b01c2ba737 Add smoke test script and expand manufacturing workflow 2026-01-02 23:38:22 -05:00
Aaron
bc97e0374f Add one-time SSH hardening notice after forced password change 2026-01-02 23:29:17 -05:00
Aaron
0a23902eb0 Default to forcing password change after prep 2026-01-02 23:26:03 -05:00
Aaron
4632704092 Allow forcing password change after prep 2026-01-02 23:23:21 -05:00
Aaron
36d30da30a Make firstboot updates configurable 2026-01-02 23:07:36 -05:00
Aaron
c62f1f018f Note browser restart after CA install 2026-01-02 23:03:00 -05:00
11 changed files with 575 additions and 21 deletions

View File

@@ -8,9 +8,12 @@ This documents the *current* workflow and the *target* workflow once profiles +
- Nginx + PiKit dashboard - Nginx + PiKit dashboard
- DietPi dashboard - DietPi dashboard
3) Update the system if needed. 3) Update the system if needed.
4) Run the prep scrub: 4) Run the prep scrub + verify (prep now prompts to shut down):
- `sudo ./pikit-prep.sh` - `sudo ./pikit-prep.sh`
- (optional) `sudo ./check-pikit-clean.sh` - (optional) `sudo ./pikit-prep.sh --no-shutdown`
- (optional) `sudo ./pikit-prep.sh --shutdown-now`
- `./pikit-smoke-test.sh`
- (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`).
@@ -24,9 +27,12 @@ This documents the *current* workflow and the *target* workflow once profiles +
4) Add dashboard services using the UI (Add Service modal). 4) Add dashboard services using the UI (Add Service modal).
5) Open any needed ports in ufw (done as part of testing/config): 5) Open any needed ports in ufw (done as part of testing/config):
- `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: 6) Run the prep scrub + verify (prep now prompts to shut down):
- `sudo ./pikit-prep.sh` - `sudo ./pikit-prep.sh`
- (optional) `sudo ./check-pikit-clean.sh` - (optional) `sudo ./pikit-prep.sh --no-shutdown`
- (optional) `sudo ./pikit-prep.sh --shutdown-now`
- `./pikit-smoke-test.sh`
- (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.
- Identify it with `lsblk`. - Identify it with `lsblk`.
@@ -52,8 +58,12 @@ This documents the *current* workflow and the *target* workflow once profiles +
- Merges services into `/etc/pikit/services.json` (idempotent). - Merges services into `/etc/pikit/services.json` (idempotent).
5) Run the drift check (planned script): 5) Run the drift check (planned script):
- Confirms services + ports match the profile + base. - Confirms services + ports match the profile + base.
6) Run the prep scrub: 6) Run the prep scrub + verify (prep now prompts to shut down):
- `sudo ./pikit-prep.sh` - `sudo ./pikit-prep.sh`
- (optional) `sudo ./pikit-prep.sh --no-shutdown`
- (optional) `sudo ./pikit-prep.sh --shutdown-now`
- `./pikit-smoke-test.sh`
- (optional) `sudo ./pikit-prep.sh --check-only`
7) Image the SD card with DietPi Imager. 7) Image the SD card with DietPi Imager.
First boot on the enduser device will: First boot on the enduser device will:
@@ -61,10 +71,84 @@ First boot on the enduser device will:
- Ensure the profiles firewall ports are open (LANonly). - Ensure the profiles firewall ports are open (LANonly).
- Show a progress overlay until complete. - Show a progress overlay until complete.
Optional: to skip the firstboot update step for faster startup, create
`/etc/pikit/firstboot.conf` with:
```
PIKIT_FIRSTBOOT_UPDATES=0
```
## 3) Flashing an image to SD ## 3) Flashing an image to SD
Use the helper: 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)
1) Start from the golden base image (stored in `images/base/`).
2) Flash it to a knowngood SD card.
3) Boot and verify:
- `http://pikit.local` and `https://pikit.local`
- dashboard loads
- firstboot completes
4) Apply any required profile/services.
5) Run prep + verify (prep now prompts to shut down):
- `sudo ./pikit-prep.sh`
- (optional) `sudo ./pikit-prep.sh --no-shutdown`
- (optional) `sudo ./pikit-prep.sh --shutdown-now`
- `./pikit-smoke-test.sh`
6) Power down cleanly.
7) Image the SD card (DietPi Imager via QEMU or ondevice).
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:
- boot → firstboot → dashboard → services
## 5) Release checklist (stable)
1) Ensure `main` is clean and all changes are pushed.
2) Update `pikit-web/data/version.json` to the new version.
3) Build the web assets:
- `npm --prefix pikit-web run build`
4) Run tests:
- `npm --prefix pikit-web test`
- `./pikit-smoke-test.sh`
5) Commit version bump and push.
6) Tag the release:
- `git tag vX.Y.Z && git push origin vX.Y.Z`
7) Build release bundle + manifest:
- `./tools/release/make-release.sh X.Y.Z https://git.44r0n.cc/44r0n7/pi-kit/releases/download/vX.Y.Z`
8) Generate changelog from git:
- `git log --pretty=format:'- %s (%h)' vPREV..HEAD > out/releases/CHANGELOG-X.Y.Z.txt`
9) Create the Gitea release and upload assets:
- `pikit-X.Y.Z.tar.gz`
- `manifest.json`
- `CHANGELOG-X.Y.Z.txt`
10) Update `manifests/manifest-stable.json` with new version + sha256 and push.
## 5) Release checklist (stable)
1) Ensure `main` is clean and all changes are pushed.
2) Update `pikit-web/data/version.json` to the new version.
3) Build the web assets:
- `npm --prefix pikit-web run build`
4) Run tests:
- `npm --prefix pikit-web test`
- `./pikit-smoke-test.sh`
5) Commit version bump and push.
6) Tag the release:
- `git tag vX.Y.Z && git push origin vX.Y.Z`
7) Build release bundle + manifest:
- `./tools/release/make-release.sh X.Y.Z https://git.44r0n.cc/44r0n7/pi-kit/releases/download/vX.Y.Z`
8) Generate changelog from git:
- `git log --pretty=format:'- %s (%h)' vPREV..HEAD > out/releases/CHANGELOG-X.Y.Z.txt`
9) Create the Gitea release and upload assets:
- `pikit-X.Y.Z.tar.gz`
- `manifest.json`
- `CHANGELOG-X.Y.Z.txt`
10) Update `manifests/manifest-stable.json` with new version + sha256 and push.
## Notes ## Notes
- Profiles are additive to the base image defaults; do not include PiKit or DietPi dashboard entries in profiles. - Profiles are additive to the base image defaults; do not include PiKit or DietPi dashboard entries in profiles.
- Keep `RESCUE.md` in `/root` and `/home/dietpi` only (not in `/var/www`). - Keep `RESCUE.md` in `/root` and `/home/dietpi` only (not in `/var/www`).
- Prep enforces a password change for `dietpi` on first login; set `PIKIT_FORCE_PASSWORD_CHANGE=0` to skip.
- After the password change, a onetime SSH hardening tip is shown on login.
- End-user defaults: OS security unattended upgrades on; Pi-Kit updater auto-check on stable channel, auto-apply off (user can change in dashboard).

View File

@@ -1,9 +1,9 @@
{ {
"version": "0.1.2", "version": "0.1.3",
"_release_date": "2025-12-10T00:00:00Z", "_release_date": "2026-01-03T05:16:09Z",
"bundle": "https://git.44r0n.cc/44r0n7/pi-kit/releases/download/v0.1.2/pikit-0.1.2.tar.gz", "bundle": "https://git.44r0n.cc/44r0n7/pi-kit/releases/download/v0.1.3/pikit-0.1.3.tar.gz",
"changelog": "https://git.44r0n.cc/44r0n7/pi-kit/releases/download/v0.1.2/CHANGELOG-0.1.2.txt", "changelog": "https://git.44r0n.cc/44r0n7/pi-kit/releases/download/v0.1.3/CHANGELOG-0.1.3.txt",
"files": [ "files": [
{ "path": "bundle.tar.gz", "sha256": "8d2e0f8b260063cab0d52e862cb42f10472a643123f984af0248592479dd613d" } { "path": "bundle.tar.gz", "sha256": "a31db945f08a8cdb0906a913b3b5507cc50225e9ce6b23bef525951d23335865" }
] ]
} }

View File

@@ -13,9 +13,13 @@ PIKIT_SSH_KEY="${PIKIT_SSH_KEY:-$HOME/.ssh/pikit}"
PIKIT_SSH_OPTS="${PIKIT_SSH_OPTS:-}" PIKIT_SSH_OPTS="${PIKIT_SSH_OPTS:-}"
PIKIT_REMOTE_TMP="${PIKIT_REMOTE_TMP:-/tmp/pikit-prep.sh}" PIKIT_REMOTE_TMP="${PIKIT_REMOTE_TMP:-/tmp/pikit-prep.sh}"
PIKIT_SELF_DELETE="${PIKIT_SELF_DELETE:-0}" PIKIT_SELF_DELETE="${PIKIT_SELF_DELETE:-0}"
PIKIT_FORCE_PASSWORD_CHANGE="${PIKIT_FORCE_PASSWORD_CHANGE:-1}"
PIKIT_SHUTDOWN_AFTER_PREP="${PIKIT_SHUTDOWN_AFTER_PREP:-1}"
PIKIT_SHUTDOWN_PROMPT="${PIKIT_SHUTDOWN_PROMPT:-1}"
MODE="both" MODE="both"
LOCAL_ONLY=0 LOCAL_ONLY=0
DID_PREP=0
ERRORS=0 ERRORS=0
WARNINGS=0 WARNINGS=0
@@ -31,7 +35,14 @@ Options:
--prep-only Run prep only (no check) --prep-only Run prep only (no check)
--check-only Run checks only (no prep) --check-only Run checks only (no prep)
--local Force local execution (no SSH copy) --local Force local execution (no SSH copy)
--shutdown-now Shutdown after prep completes without prompting
--no-shutdown Skip shutdown prompt after prep
--help Show this help --help Show this help
Env:
PIKIT_FORCE_PASSWORD_CHANGE=0 Skip forcing a password change (default is on)
PIKIT_SHUTDOWN_AFTER_PREP=0 Skip shutdown prompt after prep (default on)
PIKIT_SHUTDOWN_PROMPT=0 Skip shutdown prompt (default on)
USAGE USAGE
} }
@@ -66,6 +77,8 @@ parse_args() {
--prep-only) MODE="prep" ;; --prep-only) MODE="prep" ;;
--check-only) MODE="check" ;; --check-only) MODE="check" ;;
--local) LOCAL_ONLY=1 ;; --local) LOCAL_ONLY=1 ;;
--shutdown-now) PIKIT_SHUTDOWN_AFTER_PREP=1; PIKIT_SHUTDOWN_PROMPT=0 ;;
--no-shutdown) PIKIT_SHUTDOWN_AFTER_PREP=0 ;;
--help|-h) usage; exit 0 ;; --help|-h) usage; exit 0 ;;
*) *)
echo "[FAIL] Unknown argument: $arg" >&2 echo "[FAIL] Unknown argument: $arg" >&2
@@ -82,12 +95,16 @@ run_remote() {
[ "$arg" = "--local" ] && continue [ "$arg" = "--local" ] && continue
forward+=("$arg") forward+=("$arg")
done done
local ssh_tty=()
if [ "$PIKIT_SHUTDOWN_AFTER_PREP" -eq 1 ] && [ "$PIKIT_SHUTDOWN_PROMPT" -eq 1 ] && [ -t 0 ]; then
ssh_tty=(-t)
fi
if ! command -v scp >/dev/null 2>&1 || ! command -v ssh >/dev/null 2>&1; then if ! command -v scp >/dev/null 2>&1 || ! command -v ssh >/dev/null 2>&1; then
echo "[FAIL] ssh/scp not available for remote prep" >&2 echo "[FAIL] ssh/scp not available for remote prep" >&2
exit 1 exit 1
fi fi
scp -i "$PIKIT_SSH_KEY" $PIKIT_SSH_OPTS "$SCRIPT_PATH" "${PIKIT_USER}@${PIKIT_HOST}:${PIKIT_REMOTE_TMP}" scp -i "$PIKIT_SSH_KEY" $PIKIT_SSH_OPTS "$SCRIPT_PATH" "${PIKIT_USER}@${PIKIT_HOST}:${PIKIT_REMOTE_TMP}"
ssh -i "$PIKIT_SSH_KEY" $PIKIT_SSH_OPTS "${PIKIT_USER}@${PIKIT_HOST}" \ ssh "${ssh_tty[@]}" -i "$PIKIT_SSH_KEY" $PIKIT_SSH_OPTS "${PIKIT_USER}@${PIKIT_HOST}" \
"sudo PIKIT_SELF_DELETE=1 bash ${PIKIT_REMOTE_TMP} --local ${forward[*]}; rc=\$?; rm -f ${PIKIT_REMOTE_TMP}; exit \$rc" "sudo PIKIT_SELF_DELETE=1 bash ${PIKIT_REMOTE_TMP} --local ${forward[*]}; rc=\$?; rm -f ${PIKIT_REMOTE_TMP}; exit \$rc"
exit $? exit $?
} }
@@ -206,6 +223,16 @@ prep_image() {
# --- Default login --- # --- Default login ---
if id -u dietpi >/dev/null 2>&1; then if id -u dietpi >/dev/null 2>&1; then
echo "dietpi:pikit" | chpasswd && status CLEANED "reset dietpi password" || status FAIL "reset dietpi password" echo "dietpi:pikit" | chpasswd && status CLEANED "reset dietpi password" || status FAIL "reset dietpi password"
mkdir -p /var/lib/pikit
rm -f /var/lib/pikit/first-login.notice
case "${PIKIT_FORCE_PASSWORD_CHANGE,,}" in
1|true|yes|on)
chage -d 0 dietpi && status CLEANED "force dietpi password change on next login" || status FAIL "force dietpi password change"
:> /var/lib/pikit/first-login.notice && chmod 644 /var/lib/pikit/first-login.notice \
&& status CLEANED "first-login notice armed" || status FAIL "first-login notice"
;;
*) ;;
esac
else else
status SKIP "dietpi user missing" status SKIP "dietpi user missing"
fi fi
@@ -236,6 +263,8 @@ prep_image() {
clean_file /etc/pikit/certs/pikit.local.csr clean_file /etc/pikit/certs/pikit.local.csr
clean_file /var/www/pikit-web/assets/pikit-ca.crt clean_file /var/www/pikit-web/assets/pikit-ca.crt
clean_file /var/www/pikit-web/assets/pikit-ca.sha256 clean_file /var/www/pikit-web/assets/pikit-ca.sha256
clean_file /var/lib/pikit-update/state.json
clean_file /var/run/pikit-update.lock
# --- Backup/editor cruft --- # --- Backup/editor cruft ---
clean_backups /var/www/pikit-web clean_backups /var/www/pikit-web
@@ -517,6 +546,8 @@ check_image() {
check_file_missing /etc/pikit/certs/pikit.local.key check_file_missing /etc/pikit/certs/pikit.local.key
check_file_missing /var/www/pikit-web/assets/pikit-ca.crt check_file_missing /var/www/pikit-web/assets/pikit-ca.crt
check_file_missing /var/www/pikit-web/assets/pikit-ca.sha256 check_file_missing /var/www/pikit-web/assets/pikit-ca.sha256
check_file_missing /var/lib/pikit-update/state.json
check_file_missing /var/run/pikit-update.lock
section "Logs" section "Logs"
if [ -d /var/log ]; then if [ -d /var/log ]; then
@@ -589,6 +620,33 @@ finalize() {
echo "[OK] Prep/check completed." echo "[OK] Prep/check completed."
} }
maybe_shutdown() {
if [ "$PIKIT_SHUTDOWN_AFTER_PREP" -ne 1 ] || [ "$DID_PREP" -ne 1 ]; then
return
fi
local do_shutdown=1
if [ "$PIKIT_SHUTDOWN_PROMPT" -eq 1 ]; then
if [ -t 0 ]; then
local reply=""
printf '\nShutdown now? [y/N] '
read -r reply || reply=""
case "${reply,,}" in
y|yes) do_shutdown=1 ;;
*) do_shutdown=0 ;;
esac
else
status WARN "no TTY; skipping shutdown (use --shutdown-now to force)"
do_shutdown=0
fi
fi
if [ "$do_shutdown" -eq 1 ]; then
status OK "Shutting down"
shutdown -f now || status FAIL "shutdown"
else
status OK "Shutdown skipped"
fi
}
maybe_self_delete() { maybe_self_delete() {
if [ "$PIKIT_SELF_DELETE" -eq 1 ] && [[ "$SCRIPT_PATH" == /tmp/* ]]; then if [ "$PIKIT_SELF_DELETE" -eq 1 ] && [[ "$SCRIPT_PATH" == /tmp/* ]]; then
rm -f "$SCRIPT_PATH" || true rm -f "$SCRIPT_PATH" || true
@@ -605,15 +663,17 @@ main() {
require_root require_root
case "$MODE" in case "$MODE" in
prep) prep_image ;; prep) prep_image; DID_PREP=1 ;;
check) check_image ;; check) check_image ;;
both) both)
prep_image prep_image
DID_PREP=1
check_image check_image
;; ;;
esac esac
finalize finalize
maybe_shutdown
maybe_self_delete maybe_self_delete
} }

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

@@ -0,0 +1,252 @@
#!/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}"
PIKIT_API_URL="${PIKIT_API_URL:-http://127.0.0.1:4000}"
LOCAL_ONLY=0
ERRORS=0
WARNINGS=0
REMOTE_MODE=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
}
extract_json_line() {
awk 'BEGIN{found=0} /^[[:space:]]*[{[]/ {print; found=1; exit} END{if(!found) exit 0}'
}
json_get() {
local key="$1"
if command -v python3 >/dev/null 2>&1; then
python3 -c 'import json,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)
' "$key"
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="$(remote_cmd "curl -fsS --max-time 5 $url")"; then
status FAIL "API not reachable: $url"
return
fi
if [ "$REMOTE_MODE" -eq 1 ]; then
body="$(printf "%s\n" "$body" | extract_json_line)"
fi
if [ -z "$body" ]; then
status FAIL "API response empty or not JSON"
return
fi
if command -v python3 >/dev/null 2>&1; then
if printf "%s" "$body" | python3 -c 'import json,sys
try:
data=json.load(sys.stdin)
except Exception:
sys.exit(1)
for key in ("services","hostname","uptime_seconds"):
if key in data:
sys.exit(0)
sys.exit(1)
'
then
status OK "API responds with JSON"
else
status WARN "API response did not include expected fields"
fi
else
status WARN "python3 missing; API JSON check skipped"
fi
}
check_firstboot() {
local url="$1"
local body state error_present
if ! body="$(remote_cmd "curl -fsS --max-time 5 $url")"; then
status FAIL "firstboot API not reachable"
return
fi
if [ "$REMOTE_MODE" -eq 1 ]; then
body="$(printf "%s\n" "$body" | extract_json_line)"
fi
if [ -z "$body" ]; then
status FAIL "firstboot status invalid or missing"
return
fi
state="$(printf "%s" "$body" | json_get "state" || true)"
error_present="$(printf "%s" "$body" | json_get "error_present" || true)"
if [ -z "$state" ]; then
status FAIL "firstboot status invalid or missing"
return
fi
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 "$@"
if [ "$LOCAL_ONLY" -eq 0 ] && ! is_dietpi; then
REMOTE_MODE=1
fi
section "HTTP/HTTPS"
check_http "$PIKIT_HTTP_URL" "HTTP"
check_https "$PIKIT_HTTPS_URL" "HTTPS"
section "API"
check_api "$PIKIT_API_URL/api/status"
section "Firstboot"
check_firstboot "$PIKIT_API_URL/api/firstboot"
section "Services"
check_services
section "Ports"
check_ports
finalize
}
main "$@"

View File

@@ -1,3 +1,3 @@
{ {
"version": "0.1.3-dev6" "version": "0.1.4"
} }

View File

@@ -48,6 +48,7 @@
Download Pi-Kit CA Download Pi-Kit CA
</a> </a>
</div> </div>
<p class="subtle">After installing the CA, close and reopen your browser so it takes effect.</p>
<p class="checksum">SHA256: <code id="caHash" class="inline">Loading...</code></p> <p class="checksum">SHA256: <code id="caHash" class="inline">Loading...</code></p>
<details> <details>
<summary id="win">Windows</summary> <summary id="win">Windows</summary>

View File

@@ -17,7 +17,7 @@ export default defineConfig({
trace: 'retain-on-failure', trace: 'retain-on-failure',
}, },
webServer: { webServer: {
command: 'npm run dev', command: 'node tests/mock-api.js & npm run dev',
url: BASE_URL, url: BASE_URL,
reuseExistingServer: !process.env.CI, reuseExistingServer: !process.env.CI,
stdout: 'pipe', stdout: 'pipe',

View File

@@ -0,0 +1,85 @@
import http from 'http';
const port = Number(process.env.PIKIT_MOCK_API_PORT || 4000);
const updatesConfig = {
enabled: false,
scope: 'security',
cleanup: false,
bandwidth_limit_kbps: null,
auto_reboot: false,
reboot_time: '04:30',
reboot_with_users: false,
update_time: '04:00',
upgrade_time: '04:30',
state: { enabled: false },
};
const routes = {
'/health': { ok: true },
'/api/status': {
hostname: 'pikit-test',
uptime_seconds: 0,
load: [0, 0, 0],
memory_mb: { total: 1024, free: 1024 },
disk_mb: { total: 1024, free: 1024 },
cpu_temp_c: 35.0,
lan_ip: '127.0.0.1',
os_version: 'DietPi',
auto_updates_enabled: false,
auto_updates: { enabled: false },
updates_config: updatesConfig,
reboot_required: false,
ready: true,
services: [],
},
'/api/firstboot': {
state: 'done',
steps: [],
current_step: null,
log_tail: '',
error_present: false,
error_path: '/api/firstboot/error',
ca_hash: null,
ca_url: '/assets/pikit-ca.crt',
},
'/api/firstboot/error': { present: false, text: '' },
'/api/services': { services: [] },
'/api/updates/config': updatesConfig,
'/api/updates/auto': { enabled: false, details: { enabled: false } },
'/api/update/status': {
status: 'up_to_date',
current_version: '0.0.0',
latest_version: '0.0.0',
message: 'Mocked',
channel: 'stable',
in_progress: false,
},
'/api/update/releases': { releases: [] },
'/api/diag/log': { entries: [], state: { enabled: false, level: 'normal' } },
};
const server = http.createServer((req, res) => {
const url = (req.url || '/').split('?')[0];
const body = routes[url] || (url.startsWith('/api/') ? {} : null);
if (body === null) {
res.writeHead(404, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ error: 'not found' }));
return;
}
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify(body));
});
server.listen(port, '127.0.0.1', () => {
console.log(`[mock-api] listening on 127.0.0.1:${port}`);
});
const shutdown = () => {
server.close(() => process.exit(0));
};
process.on('SIGTERM', shutdown);
process.on('SIGINT', shutdown);

View File

@@ -90,10 +90,10 @@ def load_update_state() -> Dict[str, Any]:
"last_check": None, "last_check": None,
"status": "unknown", "status": "unknown",
"message": "", "message": "",
"auto_check": False, "auto_check": True,
"in_progress": False, "in_progress": False,
"progress": None, "progress": None,
"channel": os.environ.get("PIKIT_CHANNEL", "dev"), "channel": os.environ.get("PIKIT_CHANNEL", "stable"),
"changelog_url": None, "changelog_url": None,
"latest_release_date": None, "latest_release_date": None,
"current_release_date": None, "current_release_date": None,

View File

@@ -0,0 +1,26 @@
#!/usr/bin/env sh
# Install as /etc/profile.d/pikit-first-login.sh
# Prints a one-time SSH hardening tip after the forced password change.
FLAG="/var/lib/pikit/first-login.notice"
DONE_FILE=".pikit-first-login.done"
case "$-" in
*i*) interactive=1 ;;
*) interactive=0 ;;
esac
USER_NAME="$(id -un 2>/dev/null || echo "")"
DONE_PATH="${HOME:-}/$DONE_FILE"
if [ "$interactive" -eq 1 ] && [ -f "$FLAG" ] && [ "$USER_NAME" = "dietpi" ]; then
if [ -n "${HOME:-}" ] && [ -d "${HOME:-}" ] && [ ! -f "$DONE_PATH" ]; then
echo ""
echo "Pi-Kit: For better security, set up an SSH key and disable password auth once working."
echo " Run these from your computer (not the Pi):"
echo " ssh-keygen -t ed25519"
echo " ssh-copy-id dietpi@pikit.local"
echo ""
:> "$DONE_PATH" 2>/dev/null || true
fi
fi

View File

@@ -14,6 +14,8 @@ CERT_DIR="/etc/pikit/certs"
WEB_ASSETS="/var/www/pikit-web/assets" WEB_ASSETS="/var/www/pikit-web/assets"
PROFILE_FILE="/etc/pikit/profile.json" PROFILE_FILE="/etc/pikit/profile.json"
MOTD_FILE="/etc/motd" MOTD_FILE="/etc/motd"
FIRSTBOOT_CONF="/etc/pikit/firstboot.conf"
APT_UA_OVERRIDE="/etc/apt/apt.conf.d/51pikit-unattended.conf"
STEPS=( STEPS=(
"Preparing system" "Preparing system"
@@ -26,11 +28,49 @@ STEPS=(
STEP_STATUS=(pending pending pending pending pending pending) STEP_STATUS=(pending pending pending pending pending pending)
CURRENT_STEP="" CURRENT_STEP=""
CURRENT_INDEX=-1 CURRENT_INDEX=-1
PIKIT_FIRSTBOOT_UPDATES="${PIKIT_FIRSTBOOT_UPDATES:-1}"
log() { log() {
printf '[%s] %s\n' "$(date '+%Y-%m-%dT%H:%M:%S%z')" "$*" printf '[%s] %s\n' "$(date '+%Y-%m-%dT%H:%M:%S%z')" "$*"
} }
load_config() {
if [ -f "$FIRSTBOOT_CONF" ]; then
# shellcheck disable=SC1090
. "$FIRSTBOOT_CONF"
fi
PIKIT_FIRSTBOOT_UPDATES="${PIKIT_FIRSTBOOT_UPDATES:-1}"
}
skip_updates() {
case "${PIKIT_FIRSTBOOT_UPDATES,,}" in
0|false|no|off) return 0 ;;
esac
return 1
}
configure_unattended_defaults() {
if [ -f "$APT_UA_OVERRIDE" ]; then
log "Unattended-upgrades config already present; skipping defaults."
return
fi
if ! command -v python3 >/dev/null 2>&1; then
log "python3 missing; skipping unattended-upgrades defaults."
return
fi
PYTHONPATH=/usr/local/bin python3 - <<'PY'
import sys
try:
from pikit_api.auto_updates import set_updates_config
except Exception as e:
print(f"pikit_api unavailable: {e}")
sys.exit(0)
set_updates_config({"enable": True, "scope": "security"})
PY
log "Unattended-upgrades defaults applied (security-only)."
}
write_state() { write_state() {
local state="$1" local state="$1"
local current="$2" local current="$2"
@@ -121,6 +161,7 @@ mkdir -p "$FIRSTBOOT_DIR"
exec >>"$LOG_FILE" 2>&1 exec >>"$LOG_FILE" 2>&1
log "Pi-Kit firstboot starting" log "Pi-Kit firstboot starting"
load_config
if [ -f "$DONE_FILE" ]; then if [ -f "$DONE_FILE" ]; then
log "Firstboot already completed; exiting." log "Firstboot already completed; exiting."
@@ -206,14 +247,19 @@ fi
finish_step 2 finish_step 2
begin_step 3 begin_step 3
if skip_updates; then
log "Skipping software updates (PIKIT_FIRSTBOOT_UPDATES=$PIKIT_FIRSTBOOT_UPDATES)."
else
export DEBIAN_FRONTEND=noninteractive export DEBIAN_FRONTEND=noninteractive
mkdir -p /var/cache/apt/archives/partial /var/lib/apt/lists/partial mkdir -p /var/cache/apt/archives/partial /var/lib/apt/lists/partial
chmod 755 /var/cache/apt/archives /var/cache/apt/archives/partial /var/lib/apt/lists /var/lib/apt/lists/partial chmod 755 /var/cache/apt/archives /var/cache/apt/archives/partial /var/lib/apt/lists /var/lib/apt/lists/partial
apt-get update apt-get update
apt-get -y -o Dpkg::Options::=--force-confold full-upgrade apt-get -y -o Dpkg::Options::=--force-confold full-upgrade
fi
finish_step 3 finish_step 3
begin_step 4 begin_step 4
configure_unattended_defaults
if [ -f "$PROFILE_FILE" ] && command -v ufw >/dev/null 2>&1; then if [ -f "$PROFILE_FILE" ] && command -v ufw >/dev/null 2>&1; then
python3 - <<'PY' > /tmp/pikit-profile-ports.txt python3 - <<'PY' > /tmp/pikit-profile-ports.txt
import json import json