Improve prep and smoke test tooling
This commit is contained in:
@@ -197,6 +197,40 @@ clean_home_dir() {
|
||||
fi
|
||||
}
|
||||
|
||||
reset_iface_to_dhcp() {
|
||||
local iface="$1"
|
||||
local file="/etc/network/interfaces"
|
||||
if [ ! -f "$file" ]; then
|
||||
status SKIP "network config missing: $file"
|
||||
return
|
||||
fi
|
||||
if ! grep -Eq "^[[:space:]]*iface[[:space:]]+$iface[[:space:]]+inet" "$file"; then
|
||||
status SKIP "no iface config for $iface"
|
||||
return
|
||||
fi
|
||||
local tmp
|
||||
tmp="$(mktemp)"
|
||||
awk -v target="$iface" '
|
||||
BEGIN{in=0}
|
||||
/^[[:space:]]*iface[[:space:]]+/ {
|
||||
split($0, parts, /[[:space:]]+/);
|
||||
if (parts[2]==target) { in=1; print "iface " target " inet dhcp"; next; }
|
||||
else { in=0; }
|
||||
}
|
||||
{
|
||||
if (in==1) {
|
||||
if ($1=="address"||$1=="netmask"||$1=="gateway"||$1=="dns-nameservers") next;
|
||||
}
|
||||
print;
|
||||
}' "$file" > "$tmp"
|
||||
if mv "$tmp" "$file"; then
|
||||
status CLEANED "forced DHCP for $iface in $file"
|
||||
else
|
||||
rm -f "$tmp" || true
|
||||
status FAIL "update $file for $iface"
|
||||
fi
|
||||
}
|
||||
|
||||
prep_image() {
|
||||
section "Prep"
|
||||
|
||||
@@ -418,6 +452,10 @@ prep_image() {
|
||||
# --- DHCP leases ---
|
||||
clean_file /var/lib/dhcp/dhclient.eth0.leases
|
||||
|
||||
# --- Network config ---
|
||||
reset_iface_to_dhcp eth0
|
||||
reset_iface_to_dhcp wlan0
|
||||
|
||||
# --- Nginx caches ---
|
||||
if [ -d /var/lib/nginx ]; then
|
||||
find /var/lib/nginx -mindepth 1 -maxdepth 1 -type d -exec rm -rf {} + 2>/dev/null
|
||||
@@ -486,6 +524,20 @@ check_file_empty_or_missing() {
|
||||
fi
|
||||
}
|
||||
|
||||
check_iface_dhcp() {
|
||||
local iface="$1"
|
||||
local file="/etc/network/interfaces"
|
||||
if [ ! -f "$file" ]; then
|
||||
status WARN "network config missing: $file"
|
||||
return
|
||||
fi
|
||||
if grep -Eq "^[[:space:]]*iface[[:space:]]+$iface[[:space:]]+inet[[:space:]]+static" "$file"; then
|
||||
status WARN "$iface set to static in $file"
|
||||
else
|
||||
status OK "$iface not static in $file"
|
||||
fi
|
||||
}
|
||||
|
||||
check_image() {
|
||||
section "Check"
|
||||
|
||||
@@ -595,6 +647,10 @@ check_image() {
|
||||
section "DHCP lease"
|
||||
check_file_missing /var/lib/dhcp/dhclient.eth0.leases
|
||||
|
||||
section "Network config"
|
||||
check_iface_dhcp eth0
|
||||
check_iface_dhcp wlan0
|
||||
|
||||
section "Nginx cache dirs"
|
||||
if [ -d /var/lib/nginx ]; then
|
||||
local nginx_cache
|
||||
|
||||
@@ -25,6 +25,7 @@ Runs a quick post-prep smoke test:
|
||||
- API reachable and returns JSON
|
||||
- firstboot state done
|
||||
- core services active (nginx, pikit-api, dietpi-dashboard-frontend)
|
||||
- profile-specific checks (if /etc/pikit/profile.json exists)
|
||||
|
||||
Options:
|
||||
--local Run locally on the Pi (skip SSH)
|
||||
@@ -77,6 +78,10 @@ remote_cmd() {
|
||||
fi
|
||||
}
|
||||
|
||||
remote_sudo_cmd() {
|
||||
remote_cmd "sudo bash -c \"$1\""
|
||||
}
|
||||
|
||||
extract_json_line() {
|
||||
awk 'BEGIN{found=0} /^[[:space:]]*[{[]/ {print; found=1; exit} END{if(!found) exit 0}'
|
||||
}
|
||||
@@ -213,6 +218,188 @@ check_ports() {
|
||||
fi
|
||||
}
|
||||
|
||||
load_profile() {
|
||||
local body
|
||||
body="$(remote_cmd "python3 -c 'import json, pathlib, sys; p=pathlib.Path(\"/etc/pikit/profile.json\"); print(json.dumps(json.loads(p.read_text()))) if p.exists() else sys.exit(0)'" 2>/dev/null || true)"
|
||||
body="$(printf "%s" "$body" | extract_json_line)"
|
||||
if [ -n "$body" ]; then
|
||||
printf "%s" "$body"
|
||||
return 0
|
||||
fi
|
||||
return 1
|
||||
}
|
||||
|
||||
json_get_list() {
|
||||
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:
|
||||
sys.exit(1)
|
||||
val=data.get(key, [])
|
||||
if not isinstance(val, list):
|
||||
sys.exit(0)
|
||||
for item in val:
|
||||
print(json.dumps(item))
|
||||
' "$key"
|
||||
elif command -v jq >/dev/null 2>&1; then
|
||||
jq -c --arg key "$key" '.[$key] // [] | .[]'
|
||||
fi
|
||||
}
|
||||
|
||||
check_profile_firewall() {
|
||||
local firewall_enable="$1"
|
||||
local ports_csv="$2"
|
||||
local ufw_out
|
||||
if [ "$firewall_enable" != "true" ]; then
|
||||
status OK "profile firewall not required"
|
||||
return
|
||||
fi
|
||||
if ! remote_cmd "test -x /usr/sbin/ufw || test -x /usr/bin/ufw"; then
|
||||
status FAIL "ufw missing (profile expects firewall enabled)"
|
||||
return
|
||||
fi
|
||||
ufw_out="$(remote_sudo_cmd "timeout 6 /usr/sbin/ufw status 2>/dev/null || /usr/bin/ufw status 2>/dev/null || ufw status 2>/dev/null" 2>/dev/null || true)"
|
||||
if ! printf "%s" "$ufw_out" | grep -qi "Status: active"; then
|
||||
status FAIL "ufw not active"
|
||||
return
|
||||
fi
|
||||
status OK "ufw active"
|
||||
if [ -n "$ports_csv" ]; then
|
||||
IFS=',' read -r -a ports <<<"$ports_csv"
|
||||
for port in "${ports[@]}"; do
|
||||
if printf "%s" "$ufw_out" | grep -Eq "(^|[[:space:]])${port}(/|[[:space:]])"; then
|
||||
status OK "ufw allows port $port"
|
||||
else
|
||||
status WARN "ufw rule missing for port $port"
|
||||
fi
|
||||
done
|
||||
fi
|
||||
}
|
||||
|
||||
check_profile_services() {
|
||||
local services_json="$1"
|
||||
local profile_services="$2"
|
||||
if [ -z "$profile_services" ]; then
|
||||
status OK "profile services: none"
|
||||
return
|
||||
fi
|
||||
if [ -z "$services_json" ]; then
|
||||
status FAIL "services.json missing (profile services expected)"
|
||||
return
|
||||
fi
|
||||
if command -v python3 >/dev/null 2>&1; then
|
||||
local result
|
||||
result="$(SERVICES_JSON="$services_json" PROFILE_SERVICES="$profile_services" python3 - <<'PY'
|
||||
import json, os, re
|
||||
services_text = os.environ.get("SERVICES_JSON", "")
|
||||
profile_text = os.environ.get("PROFILE_SERVICES", "")
|
||||
try:
|
||||
services_data = json.loads(services_text or "[]")
|
||||
except Exception:
|
||||
services_data = []
|
||||
profile_lines = [line for line in profile_text.splitlines() if line.strip()]
|
||||
services = [s for s in services_data if isinstance(s, dict)]
|
||||
def norm(x):
|
||||
return re.sub(r"\s+", " ", str(x or "")).strip().lower()
|
||||
missing = []
|
||||
for line in profile_lines:
|
||||
try:
|
||||
psvc = json.loads(line)
|
||||
except Exception:
|
||||
continue
|
||||
name = norm(psvc.get("name"))
|
||||
port = str(psvc.get("port") or "")
|
||||
path = str(psvc.get("path") or "")
|
||||
found = False
|
||||
for svc in services:
|
||||
sname = norm(svc.get("name"))
|
||||
sport = str(svc.get("port") or "")
|
||||
spath = str(svc.get("path") or "")
|
||||
if name and sname == name:
|
||||
found = True
|
||||
if port and sport != port:
|
||||
print(f"WARN: service {name} port mismatch ({sport} != {port})")
|
||||
if path and spath != path:
|
||||
print(f"WARN: service {name} path mismatch ({spath} != {path})")
|
||||
break
|
||||
if port and sport == port and path and spath == path:
|
||||
found = True
|
||||
break
|
||||
if not found:
|
||||
missing.append(name or f"port {port}")
|
||||
if missing:
|
||||
print("MISSING:" + ",".join(missing))
|
||||
PY
|
||||
)"
|
||||
if [ -z "$result" ]; then
|
||||
status OK "profile services registered"
|
||||
return
|
||||
fi
|
||||
if echo "$result" | grep -q "^MISSING:"; then
|
||||
status FAIL "profile services missing: ${result#MISSING:}"
|
||||
fi
|
||||
echo "$result" | grep "^WARN:" | while read -r line; do
|
||||
status WARN "${line#WARN: }"
|
||||
done
|
||||
else
|
||||
status WARN "python3 missing; profile service checks skipped"
|
||||
fi
|
||||
}
|
||||
|
||||
check_dns_stack_profile() {
|
||||
section "Profile: dns-stack"
|
||||
if remote_cmd "systemctl is-active --quiet pihole-FTL"; then
|
||||
status OK "pihole-FTL active"
|
||||
else
|
||||
status FAIL "pihole-FTL not active"
|
||||
fi
|
||||
if remote_cmd "systemctl is-active --quiet unbound"; then
|
||||
status OK "unbound active"
|
||||
else
|
||||
status FAIL "unbound not active"
|
||||
fi
|
||||
if remote_cmd "ss -lnt | grep -Eq ':53[[:space:]]'"; then
|
||||
status OK "DNS port 53 listening"
|
||||
else
|
||||
status FAIL "DNS port 53 not listening"
|
||||
fi
|
||||
if remote_cmd "ss -lnt | grep -q '127.0.0.1:5335'"; then
|
||||
status OK "Unbound 5335 listening on loopback"
|
||||
else
|
||||
status WARN "Unbound 5335 not bound to loopback"
|
||||
fi
|
||||
if remote_cmd "sudo grep -q '127.0.0.1#5335' /etc/pihole/pihole.toml"; then
|
||||
status OK "Pi-hole upstream points to Unbound"
|
||||
else
|
||||
status FAIL "Pi-hole upstream not set to 127.0.0.1#5335"
|
||||
fi
|
||||
if remote_cmd "sudo grep -q 'interface: 127.0.0.1' /etc/unbound/unbound.conf.d/dietpi.conf"; then
|
||||
status OK "Unbound listens on 127.0.0.1"
|
||||
else
|
||||
status WARN "Unbound interface not 127.0.0.1"
|
||||
fi
|
||||
if remote_cmd "sudo grep -q 'port: 5335' /etc/unbound/unbound.conf.d/dietpi.conf"; then
|
||||
status OK "Unbound port 5335 configured"
|
||||
else
|
||||
status WARN "Unbound port 5335 not configured"
|
||||
fi
|
||||
if remote_sudo_cmd "test -f /etc/pihole/tls.pem && test -f /etc/pikit/certs/pikit.local.crt"; then
|
||||
local fp_pikit fp_pihole
|
||||
fp_pikit="$(remote_sudo_cmd "openssl x509 -in /etc/pikit/certs/pikit.local.crt -noout -fingerprint -sha256 | cut -d= -f2" 2>/dev/null || true)"
|
||||
fp_pihole="$(remote_sudo_cmd "openssl x509 -in /etc/pihole/tls.pem -noout -fingerprint -sha256 | cut -d= -f2" 2>/dev/null || true)"
|
||||
if [ -n "$fp_pikit" ] && [ "$fp_pikit" = "$fp_pihole" ]; then
|
||||
status OK "Pi-hole TLS matches Pi-Kit cert"
|
||||
else
|
||||
status WARN "Pi-hole TLS does not match Pi-Kit cert"
|
||||
fi
|
||||
else
|
||||
status WARN "Pi-hole TLS bundle missing"
|
||||
fi
|
||||
}
|
||||
|
||||
finalize() {
|
||||
section "Summary"
|
||||
status OK "warnings: $WARNINGS"
|
||||
@@ -246,6 +433,41 @@ main() {
|
||||
section "Ports"
|
||||
check_ports
|
||||
|
||||
section "Profile"
|
||||
local profile_json
|
||||
profile_json="$(load_profile || true)"
|
||||
if [ -z "$profile_json" ]; then
|
||||
status OK "profile not present; skipping profile checks"
|
||||
finalize
|
||||
exit 0
|
||||
fi
|
||||
local profile_id firewall_enable ports_csv
|
||||
profile_id="$(printf "%s" "$profile_json" | json_get "id" || true)"
|
||||
firewall_enable="$(printf "%s" "$profile_json" | json_get "firewall_enable" || true)"
|
||||
ports_csv="$(printf "%s" "$profile_json" | python3 - <<'PY' 2>/dev/null || true
|
||||
import json, sys
|
||||
try:
|
||||
data=json.load(sys.stdin)
|
||||
except Exception:
|
||||
sys.exit(0)
|
||||
ports=data.get("firewall_ports") or []
|
||||
print(",".join(str(p) for p in ports))
|
||||
PY
|
||||
)"
|
||||
status OK "profile detected: ${profile_id:-unknown}"
|
||||
check_profile_firewall "$firewall_enable" "$ports_csv"
|
||||
|
||||
local services_json profile_services_output
|
||||
services_json="$(remote_sudo_cmd "cat /etc/pikit/services.json" 2>/dev/null || true)"
|
||||
profile_services_output="$(printf "%s" "$profile_json" | json_get_list "services" || true)"
|
||||
check_profile_services "$services_json" "$profile_services_output"
|
||||
|
||||
if [ "$profile_id" = "dns-stack" ]; then
|
||||
check_dns_stack_profile
|
||||
else
|
||||
status OK "profile-specific checks skipped"
|
||||
fi
|
||||
|
||||
finalize
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user