Compare commits
15 Commits
32a9f42361
...
v0.1.4
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ed162d3c1d | ||
|
|
d07fab99a6 | ||
|
|
511978666a | ||
|
|
453deac601 | ||
|
|
cb2d75387d | ||
|
|
77bc4c1c36 | ||
|
|
13e5788fe1 | ||
|
|
24e89b516f | ||
|
|
9c3156df35 | ||
|
|
b01c2ba737 | ||
|
|
bc97e0374f | ||
|
|
0a23902eb0 | ||
|
|
4632704092 | ||
|
|
36d30da30a | ||
|
|
c62f1f018f |
@@ -8,9 +8,12 @@ This documents the *current* workflow and the *target* workflow once profiles +
|
|||||||
- Nginx + Pi‑Kit dashboard
|
- Nginx + Pi‑Kit 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 end‑user device will:
|
First boot on the end‑user device will:
|
||||||
@@ -61,10 +71,84 @@ First boot on the end‑user device will:
|
|||||||
- Ensure the profile’s firewall ports are open (LAN‑only).
|
- Ensure the profile’s firewall ports are open (LAN‑only).
|
||||||
- Show a progress overlay until complete.
|
- Show a progress overlay until complete.
|
||||||
|
|
||||||
|
Optional: to skip the first‑boot 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 known‑good SD card.
|
||||||
|
3) Boot and verify:
|
||||||
|
- `http://pikit.local` and `https://pikit.local`
|
||||||
|
- dashboard loads
|
||||||
|
- first‑boot 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 on‑device).
|
||||||
|
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 → first‑boot → 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 Pi‑Kit or DietPi dashboard entries in profiles.
|
- Profiles are additive to the base image defaults; do not include Pi‑Kit 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 one‑time 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).
|
||||||
|
|||||||
@@ -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" }
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
252
pikit-smoke-test.sh
Executable 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 "$@"
|
||||||
@@ -1,3 +1,3 @@
|
|||||||
{
|
{
|
||||||
"version": "0.1.3-dev6"
|
"version": "0.1.4"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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',
|
||||||
|
|||||||
85
pikit-web/tests/mock-api.js
Normal file
85
pikit-web/tests/mock-api.js
Normal 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);
|
||||||
@@ -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,
|
||||||
|
|||||||
26
systemd/pikit-first-login.sh
Normal file
26
systemd/pikit-first-login.sh
Normal 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
|
||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user