scripts/onboard — Node Onboarding Tool
Idempotentny, deklaratywny onboarding nodów przez bash — bez Ansible.
Każdy node opisany jest manifestem hosts/<node>/node.yaml; skrypt
onboard.sh czyta manifest i woła numerowane kroki w kolejności.
Użycie
scripts/onboard/onboard.sh --node <name> [--step <name>] [--from <step>] [--dry-run]
| Flaga |
Opis |
--node <name> |
Nazwa node'a (wymagana); pasuje do hosts/<name>/node.yaml |
--step <name> |
Uruchom tylko ten jeden krok (np. 00-access) |
--from <step> |
Zacznij od tego kroku i kontynuuj do końca |
--dry-run |
Ustawia DRY_RUN=1; mutacje symulowane przez run(), sondy wykonywane naprawdę |
# Pełny onboarding
scripts/onboard/onboard.sh --node lustro
# Tylko jeden krok
scripts/onboard/onboard.sh --node lustro --step 00-access
# Od kroku wzwyż
scripts/onboard/onboard.sh --node lustro --from 10-bootstrap-runtime
# Podgląd bez zmian (sondy stanu wykonują się naprawdę — plan jest realistyczny)
scripts/onboard/onboard.sh --node lustro --dry-run
hosts/<node>/node.yaml — schemat
name: LUSTRO # nazwa node'a (ALL CAPS)
role: edge # edge | compute | infra
location: KEN # identyfikator lokalizacji
ssh_user: pi # user SSH; może różnić się od "oskar" na edge nodach
# (kolizja uid=1000 — użyj istniejącego usera)
first_contact: pi@192.168.31.19 # cel SSH przed Tailscale; KONIECZNIE IP, nie .local
# (mDNS .local zawodny w automatyzacji)
tailscale:
hostname: lustro # nazwa w mesh; cel po tailscale up
ip: # wypełniane po join (opcjonalne)
deploy_autonomy: true # true = onboard.sh może wykonywać mutacje autonomicznie
# false = wydrukuj instrukcje manualne i zatrzymaj
git_control: false # true = node pulluje z Forgejo
# false = push-based z SATURN (edge nodes)
hardware:
arch: arm64 # aarch64 | x86_64 | armv7l; wypełnia 00-preflight
ram_mb: 4096 # RAM w MB; wypełnia 00-preflight
swap:
kind: zram # zram | file | none; zram zalecany (SD wear)
docker_present: true # docker już zainstalowany?; wypełnia 00-preflight
mm_runtime: systemd:magicmirror.service
# runtime MagicMirror: systemd:<unit> | pm2 | process | none
# wypełnia 00-preflight
services:
node-agent:
runtime:
engine: docker # docker | docker-compose
mem_limit: 256m # obowiązkowy (RPi4 RAM profil jak VPS — OOM ryzyko)
Uwagi do pól
ssh_user — na edge nodach z istniejącym uid=1000 (np. pi na RPi OS) użyj
tego usera zamiast tworzyć oskar; docker group membership i mem_limit node-agenta
są zaprojektowane pod 1000:1000.
first_contact — zawsze IP, nie hostname .local. mDNS okazał się zawodny
w automatyzacji (transient resolve fail). Po tailscale up używaj tailscale.hostname.
deploy_autonomy — gdy false, kroki 10+ wypisują instrukcje manualne i kończą
pracę bez mutacji. Przydatne dla nodów zarządzanych przez inną osobę.
git_control — gdy false, kroki z git/repo/clone w nazwie są pomijane.
Status kroków
| Krok |
Plik |
Status |
Opis |
00-access |
steps/00-access.sh |
DONE |
SSH key → first_contact, install Tailscale, tailscale up (interaktywny URL), verify pi@<ts_hostname> arch=aarch64 |
00-preflight |
steps/00-preflight.sh |
SCAFFOLD |
Read-only: zbiera fakty (arch, RAM, docker, swap, MM runtime), wypisuje raport + YAML snippet do wklejenia w node.yaml |
10-bootstrap-runtime |
steps/10-bootstrap-runtime.sh |
TODO |
Tworzy /opt/homelab/ layout, chown <ssh_user> |
20-install-docker |
steps/20-install-docker.sh |
TODO |
Instaluje Docker Engine jeśli docker_present=false; skip gdy już zainstalowany |
30-install-tailscale |
steps/30-install-tailscale.sh |
TODO |
Superseded przez 00-access dla nowych nodów; może służyć do re-join |
40-deploy-node-agent |
steps/40-deploy-node-agent.sh |
TODO |
Deploy node-agent docker; user 1000:1000; mem_limit z node.yaml |
50-verify |
steps/50-verify.sh |
TODO |
End-to-end smoke: event dotarł do control plane, widać w UI, alert path Telegram |
Architektura lib/
lib/common.sh — log/warn/die/step/dryrun, run(), yaml_get, ensure_line, git() wrapper
lib/remote.sh — rrun/rcopy/rsync_dir/rcheck (SSH wrappers, ONBOARD_SSH_USER/HOST)
run() i dry-run
DRY_RUN=1 jest eksportowane do wszystkich step-skryptów przez orchestrator.
# Mutacje owijamy w run() — w dry-run drukuje intent, nie wykonuje
run ssh-copy-id -i ~/.ssh/id_ed25519.pub pi@192.168.31.19
# Sondy stanu (ssh BatchMode test, command -v, status query) wykonują się ZAWSZE
# — dry-run musi pokazywać realistyczny plan oparty na aktualnym stanie
if ssh -o BatchMode=yes pi@192.168.31.19 true 2>/dev/null; then
log "key already present — skip"
fi
yaml_get — fallback bez yq
Gdy yq nie jest dostępne, używany jest grep+sed fallback. Pułapki:
- Inline komentarze YAML (
key: value # komentarz) są strippowane przez
s/[[:space:]]\+#.*$// — wymaga co najmniej jednej spacji przed #, więc
url#fragment pozostaje nienaruszone.
- Parser jest non-greedy na
: — s/^[[:space:]]*[^:]*:[[:space:]]*//' —
wartości z dwukropkiem (np. systemd:magicmirror.service) są czytane poprawnie.
- Dot-path (
tailscale.hostname) działa tylko z yq; fallback pasuje po ostatnim
segmencie (hostname). Nazwy pól w node.yaml muszą być unikalne.
Gotchas / Learnings
| Problem |
Rozwiązanie |
mDNS .local zawodny |
Użyj IP w first_contact; .local OK interaktywnie, nie w automatyzacji |
| Istniejący uid=1000 na edge node |
Użyj tego usera; nie twórz oskar (kolizja uid, zepsuje własność MM) |
| swap plik na SD |
Migruj na zram — wear reduction; dodaj krok do 10-bootstrap-runtime |
| dry-run zatrzymuje się na orchestratorze |
run() wrapper + export DRY_RUN=1; sondy muszą działać też w dry-run |
| SSH known-hosts warning w parsowanym output |
-o LogLevel=ERROR na SSH do nowego hosta w mesh |
yaml_get gubi prefix po : w wartości |
Non-greedy ^[[:space:]]*[^:]*: zamiast .*: |
| yaml_get nie usuwa inline komentarzy |
s/[[:space:]]\+#.*$// po ekstrakcji wartości |
| RPi4 4 GB RAM — OOM ryzyko |
mem_limit w node-agent override obowiązkowy (profil jak VPS) |