# scripts/onboard — Node Onboarding Tool Idempotentny, deklaratywny onboarding nodów przez bash — bez Ansible. Każdy node opisany jest manifestem `hosts//node.yaml`; skrypt `onboard.sh` czyta manifest i woła numerowane kroki w kolejności. ## Użycie ```bash scripts/onboard/onboard.sh --node [--step ] [--from ] [--dry-run] ``` | Flaga | Opis | |-------|------| | `--node ` | Nazwa node'a (wymagana); pasuje do `hosts//node.yaml` | | `--step ` | Uruchom tylko ten jeden krok (np. `00-access`) | | `--from ` | Zacznij od tego kroku i kontynuuj do końca | | `--dry-run` | Ustawia `DRY_RUN=1`; mutacje symulowane przez `run()`, sondy wykonywane naprawdę | ```bash # 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.yaml — schemat ```yaml 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: | 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@` 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 ` | | `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. ```bash # 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) |