Files
felhom.eu/documentation/audits/sizing-2026-10-13/tools/actions.sh
T
admin c885b5acf4 Audit: what a Felhom box needs — floor, 64 apps' memory and CPU, power (N100 bench); R-993..R-995; R-231 due check read green
Measured on demo-felhom (N100, 16 GB): 61 apps deployed and removed through the
product, sampled every 5 s; heavy apps with one real action (Immich 200 photos,
Jellyfin transcode, Paperless OCR, Nextcloud 500 MB, ...). sizing.json for the
estimator (tools/build_sizing.py), SIZING.md with the proposed minimum machine,
METHOD.md. Power is RAPL CPU-package only (no meter). R-993 asks the operator to
approve the minimum machine; R-994 lists catalog limits/requests that are too low;
R-995 records leftover unnamed volumes after removal. R-231's dated check measured:
dooplex-backup.service success, journal green.
2026-10-13 13:23:51 +02:00

116 lines
7.4 KiB
Bash

#!/bin/bash
# actions.sh APP — one real action per heavy app, run INSIDE the guest, through the app's own front
# door (its container address on the Docker network; the setup gate sits in traefik, in front of it).
# Content is in /root/sizing/content (made by make_content.py: generated or CC0, no personal data).
# PW (env) is a password generated for this run; it is never printed and never stored.
# Every line it prints is evidence; it prints no secret.
set -u
APP=${1:?app}; C=/root/sizing/content; PW=${PW:?pw}
ip() { docker inspect -f '{{range .NetworkSettings.Networks}}{{.IPAddress}} {{end}}' "$1" | awk '{print $1}'; }
src() { docker inspect -f "{{range .Mounts}}{{if eq .Destination \"$2\"}}{{.Source}}{{end}}{{end}}" "$1"; }
waitup() { for i in $(seq 1 120); do c=$(curl -s -o /dev/null -w '%{http_code}' "$1"); case $c in 200|302|401) echo "up after $((i*5))s ($c)"; return 0;; esac; sleep 5; done; echo "never up ($c)"; return 1; }
ts() { echo "@$(date +%s) $*"; }
case $APP in
immich)
U=http://$(ip immich-server):2283; waitup $U/api/server/ping
curl -s -o /dev/null -w 'admin-sign-up %{http_code}\n' -X POST $U/api/auth/admin-sign-up -H 'Content-Type: application/json' \
-d "{\"email\":\"bench@example.invalid\",\"password\":\"$PW\",\"name\":\"Bench\"}"
T=$(curl -s -X POST $U/api/auth/login -H 'Content-Type: application/json' -d "{\"email\":\"bench@example.invalid\",\"password\":\"$PW\"}" | grep -o '"accessToken":"[^"]*"' | cut -d'"' -f4)
echo "token length ${#T}"
ts action_start upload
ok=0; bad=0
for f in $C/photos/*.jpg; do
c=$(curl -s -o /tmp/immich-up.json -w '%{http_code}' -H "Authorization: Bearer $T" -F "deviceAssetId=$(basename $f)" -F deviceId=bench \
-F fileCreatedAt=2026-01-01T00:00:00.000Z -F fileModifiedAt=2026-01-01T00:00:00.000Z -F "assetData=@$f" $U/api/assets)
case $c in 200|201) ok=$((ok+1));; *) bad=$((bad+1)); [ $bad -le 3 ] && echo "upload $(basename $f) -> $c $(head -c 200 /tmp/immich-up.json)";; esac
done
ts upload_done ok=$ok bad=$bad
# wait until every job queue is empty (thumbnails, metadata, smart search, faces)
for i in $(seq 1 720); do
J=$(curl -s -H "Authorization: Bearer $T" $U/api/jobs); [ -z "$J" ] || echo "$J" | grep -q '"statusCode":404' && J=$(curl -s -H "Authorization: Bearer $T" $U/api/queues)
n=$(echo "$J" | grep -oE '"(active|waiting|delayed)":[0-9]+' | awk -F: '{s+=$2} END{print s+0}')
[ $((i % 12)) -eq 1 ] && echo " t=$((i*5))s jobs pending=$n"
if [ "$n" = 0 ] && [ $i -gt 6 ]; then break; fi
sleep 5
done
ts action_end jobs-idle
curl -s -H "Authorization: Bearer $T" $U/api/server/statistics | head -c 300; echo
curl -s -H "Authorization: Bearer $T" "$U/api/people" | grep -oE '"total":[0-9]+' | head -1
;;
jellyfin)
U=http://$(ip jellyfin):8096; waitup $U/health
M=$(src jellyfin /media); echo "media source: $M"; mkdir -p "$M/movies"; cp -r "$C/video/Bench Films/." "$M/movies/"; ls "$M/movies"
H='Content-Type: application/json'
curl -s -o /dev/null -w 'startup-config %{http_code}\n' -X POST $U/Startup/Configuration -H "$H" -d '{"UICulture":"en-US","MetadataCountryCode":"US","PreferredMetadataLanguage":"en"}'
curl -s -o /dev/null $U/Startup/User
curl -s -o /dev/null -w 'startup-user %{http_code}\n' -X POST $U/Startup/User -H "$H" -d "{\"Name\":\"bench\",\"Password\":\"$PW\"}"
curl -s -o /dev/null -w 'startup-complete %{http_code}\n' -X POST $U/Startup/Complete
A='Authorization: MediaBrowser Client="bench", Device="bench", DeviceId="sizing-bench", Version="1.0"'
T=$(curl -s -X POST $U/Users/AuthenticateByName -H "$H" -H "$A" -d "{\"Username\":\"bench\",\"Pw\":\"$PW\"}" | grep -o '"AccessToken":"[^"]*"' | cut -d'"' -f4)
echo "token length ${#T}"
ts action_start scan
curl -s -o /dev/null -w 'add-library %{http_code}\n' -X POST "$U/Library/VirtualFolders?name=Films&collectionType=movies&refreshLibrary=true" \
-H "$H" -H "X-Emby-Token: $T" -d '{"LibraryOptions":{"PathInfos":[{"Path":"/media/movies"}]}}'
for i in $(seq 1 120); do
n=$(curl -s -H "X-Emby-Token: $T" "$U/Items?Recursive=true&IncludeItemTypes=Movie" | grep -oE '"TotalRecordCount":[0-9]+' | cut -d: -f2)
run=$(curl -s -H "X-Emby-Token: $T" "$U/ScheduledTasks?IsHidden=false" | grep -o '"State":"Running"' | wc -l)
[ "${n:-0}" -ge 3 ] && [ "$run" = 0 ] && break; sleep 5
done
ts scan_done movies=${n:-0}
ID=$(curl -s -H "X-Emby-Token: $T" "$U/Items?Recursive=true&IncludeItemTypes=Movie&Limit=1" | grep -oE '"Id":"[0-9a-f]{32}"' | head -1 | cut -d'"' -f4)
ts transcode_start
curl -s -o /tmp/jf.mp4 -w 'transcode %{http_code} %{size_download} bytes %{time_total}s\n' \
"$U/Videos/$ID/stream.mp4?static=false&mediaSourceId=$ID&videoCodec=h264&audioCodec=aac&maxWidth=1920&videoBitRate=3000000&audioBitRate=128000&api_key=$T"
rm -f /tmp/jf.mp4
ts action_end transcode_done
;;
paperless-ngx)
U=http://$(ip paperless-webserver):8000; waitup $U/api/
for i in $(seq 1 60); do c=$(curl -s -o /dev/null -w '%{http_code}' -u "admin:$PW" $U/api/documents/); [ "$c" = 200 ] && break; sleep 5; done; echo "api auth $c"
ts action_start ocr
for f in $C/pdfs/*.pdf; do curl -s -o /dev/null -w "%{http_code} " -u "admin:$PW" -F "document=@$f" $U/api/documents/post_document/; done; echo
for i in $(seq 1 360); do
n=$(curl -s -u "admin:$PW" "$U/api/documents/?page_size=1" | grep -oE '"count":[0-9]+' | cut -d: -f2)
p=$(curl -s -u "admin:$PW" "$U/api/tasks/" | grep -oE '"status":"(PENDING|STARTED)"' | wc -l)
[ $((i % 12)) -eq 1 ] && echo " t=$((i*5))s documents=${n:-0} pending=$p"
[ "${n:-0}" -ge 20 ] && [ "$p" = 0 ] && break; sleep 5
done
ts action_end documents=${n:-0}
;;
nextcloud)
NIP=$(ip nextcloud); TD=$(docker inspect -f '{{range .Config.Env}}{{println .}}{{end}}' nextcloud | sed -n 's/^NEXTCLOUD_TRUSTED_DOMAINS=//p' | awk '{print $1}')
echo "trusted domain: $TD"
for i in $(seq 1 120); do s=$(curl -s -H "Host: $TD" http://$NIP/status.php); echo "$s" | grep -q '"installed":true' && break; sleep 5; done
echo "status: $(echo $s | head -c 160)"
D="http://$NIP/remote.php/dav/files/admin"
ts action_start upload
curl -s -o /dev/null -w 'mkcol %{http_code}\n' -X MKCOL -H "Host: $TD" -u "admin:$PW" $D/bench
ok=0; for f in $C/upload/*.bin; do c=$(curl -s -o /dev/null -w '%{http_code}' -H "Host: $TD" -u "admin:$PW" -T "$f" "$D/bench/$(basename $f)"); case $c in 201|204) ok=$((ok+1));; *) echo "put $(basename $f) -> $c";; esac; done
ts action_end uploaded=$ok
;;
home-assistant)
U=http://$(ip home-assistant):8123; waitup $U/
ts action_start onboarding
curl -s -o /dev/null -w 'onboarding-user %{http_code}\n' -X POST $U/api/onboarding/users -H 'Content-Type: application/json' \
-d "{\"client_id\":\"$U/\",\"name\":\"Bench\",\"username\":\"bench\",\"password\":\"$PW\",\"language\":\"en\"}"
ts action_end onboarding
;;
calibre-web)
I=$(src calibre-web /cwa-book-ingest); L=$(src calibre-web /calibre-library); echo "ingest: $I library: $L"
waitup http://$(ip calibre-web):8083/
ts action_start ingest
cp $C/books/*.epub "$I/"
for i in $(seq 1 240); do n=$(ls "$I" 2>/dev/null | grep -c epub); [ "$n" = 0 ] && break; sleep 5; done
sleep 20
ts action_end ingest-folder-left=$n library-epubs=$(find "$L" -name '*.epub' 2>/dev/null | wc -l)
;;
romm)
R=$(src romm /romm/library); echo "library: $R"; waitup http://$(ip romm):8080/
ts action_start copy-library
mkdir -p "$R/roms"; cp -r $C/roms/. "$R/roms/"; find "$R" -type f | wc -l
ts action_end copied-only-scan-not-triggered
;;
*) echo "no action for $APP";;
esac