Compare commits
2 commits
471ba09c4a
...
d2fb2b3d41
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d2fb2b3d41 | ||
|
|
e59eb12da3 |
18
CLAUDE.md
18
CLAUDE.md
|
|
@ -30,10 +30,22 @@ scripts/deploy/deploy.sh --service mosquitto # specific service only
|
|||
./scripts/deploy/deploy-node.sh chelsty-infra # CHELSTY nodes (individually)
|
||||
./scripts/bootstrap/prepare-node.sh # general node bootstrap
|
||||
./scripts/bootstrap/chelsty-runtime.sh # CHELSTY-specific bootstrap
|
||||
scripts/onboard/onboard.sh --node <name> # onboard a new node (idempotent, bash)
|
||||
scripts/onboard/onboard.sh --node <name> --step 00-access # single step
|
||||
scripts/onboard/onboard.sh --node <name> --dry-run # simulate
|
||||
```
|
||||
|
||||
Pipeline stages: **prepare → validate → deploy → verify → diagnose (on failure) → complete**. Stage state persisted in `/opt/homelab/state/deploy/`.
|
||||
|
||||
## Node Onboarding
|
||||
|
||||
New nodes are onboarded via `scripts/onboard/` — an idempotent bash tool driven by
|
||||
`hosts/<node>/node.yaml` manifests (no Ansible). See `scripts/onboard/README.md` for
|
||||
the full schema, step status table, and gotchas.
|
||||
|
||||
Key fields in `node.yaml`: `ssh_user`, `first_contact` (LAN IP — not `.local`),
|
||||
`tailscale.hostname`, `deploy_autonomy`, `git_control`, `hardware.*`.
|
||||
|
||||
## Service Structure
|
||||
|
||||
Every service must follow this layout:
|
||||
|
|
@ -186,6 +198,12 @@ Before any new or changed service is considered ready:
|
|||
`~/homelab-codex-ws` (main checkout) is **deploy-only** and belongs to the human operator.
|
||||
Parallel agent tasks run in isolated git worktrees created by `scripts/dev/agent.sh new <name>`.
|
||||
|
||||
**DISCIPLINE RULE — enforced after 2026-06-08 session violation:**
|
||||
All feature/implementation work MUST happen in a task worktree, never directly in the main
|
||||
checkout. The main checkout is for reading context and running deploys only. If you are
|
||||
about to create a new branch or make implementation commits while `pwd` is
|
||||
`~/homelab-codex-ws`, stop and ask the operator to run `agent.sh new <name>` first.
|
||||
|
||||
If `.agent-task` exists in your current working directory, you are in a task worktree.
|
||||
**You must immediately read `.agent-task` and load `.claude/skills/worktree-aware/SKILL.md`
|
||||
before taking any action.** That skill defines all branch-hygiene rules for task worktrees.
|
||||
|
|
|
|||
90
docs/sessions/2026-06-08-lustro-onboarding.md
Normal file
90
docs/sessions/2026-06-08-lustro-onboarding.md
Normal file
|
|
@ -0,0 +1,90 @@
|
|||
# Sesja 2026-06-08 — onboarding LUSTRO (RPi4 / Magic Mirror / KEN)
|
||||
|
||||
## Cel
|
||||
|
||||
Budowa reużywalnego narzędzia onboardingu nodów `scripts/onboard/` (bash idempotentny,
|
||||
NIE Ansible — świadoma decyzja), napędzanego deklaratywnym manifestem
|
||||
`hosts/<node>/node.yaml`. Pierwszy realny node: LUSTRO.
|
||||
|
||||
## Node LUSTRO (fakty z preflight)
|
||||
|
||||
- RPi4, aarch64, Debian bookworm, hostname pimirror2, sieć KEN 192.168.31.x
|
||||
- RAM 4 GB (MM zjada ~1.7 Gi — ten sam profil co VPS z OOM 2026-06-01 → `mem_limit` obowiązkowy)
|
||||
- dysk 58 G / 48% (luz)
|
||||
- docker 29.5.3 już zainstalowany (krok `20-install-docker` zbędny dla tego node'a)
|
||||
- user `pi`: uid=1000, passwordless sudo (potwierdzone `sudo -n true`=0), grupy docker+ollama
|
||||
- Magic Mirror = systemd unit `magicmirror.service` (Electron jako pi) — **NIETKNIĘTY** przez całą sesję
|
||||
- swap = 200 M plik `/var/swap` na SD → do migracji na zram (wear karty)
|
||||
- Tailscale: zainstalowany w tej sesji, Running, IP 100.99.85.73
|
||||
|
||||
## Decyzje
|
||||
|
||||
- **user = istniejący `pi`** (NIE tworzymy `oskar` — `pi` już zajmuje uid 1000, jest
|
||||
właścicielem MM, ma docker+sudo; node-agent docker `1000:1000` pasuje out-of-box).
|
||||
Świadome odstępstwo od konwencji "oskar wszędzie".
|
||||
- runtime node-agent = docker
|
||||
- `first_contact` = LAN IP `pi@192.168.31.19` (mDNS `.local` okazał się zawodny —
|
||||
transient resolve fail); po `tailscale up` kontakt przejmuje mesh (`pi@lustro`)
|
||||
- Tailscale auth = login interaktywny (URL), bez authkey
|
||||
- swap target = zram
|
||||
|
||||
## Stan: 00-access ZAMKNIĘTY
|
||||
|
||||
Idempotentny, przeszedł na ostro + re-run czysty. Lustro w mesh, kanał SATURN→lustro
|
||||
przez Tailscale działa bezhasłowo. Verify czysty (arch=aarch64).
|
||||
|
||||
## Bugi narzędzia naprawione w tej sesji
|
||||
|
||||
1. **dry-run był płytki** (tylko orchestrator) → `run()` helper + propagacja `DRY_RUN=1`
|
||||
do steps (`lib/common.sh`, `onboard.sh`, `remote.sh`, `00-access.sh`)
|
||||
2. **`yaml_get` fallback** (bez `yq`):
|
||||
- inline-comment stripping — `[[:space:]]+#.*$` po wartości
|
||||
- PRE-EXISTING greedy-colon bug — `.*:` ucinał ostatni dwukropek, gubił prefix
|
||||
w `systemd:magicmirror.service`; fix: `^[[:space:]]*[^:]*:[[:space:]]*`
|
||||
3. **`00-access` verify** — ssh known-hosts warning wpadał do parsowanego `arch`
|
||||
(`WARN "Unexpected arch 'Warning:Permanently…'"`); fix: `-o LogLevel=ERROR`
|
||||
+ czysty stdout (bez `2>&1`)
|
||||
|
||||
## Branch / commity
|
||||
|
||||
`feat/node-onboarding` (6 commitów):
|
||||
|
||||
| Hash | Opis |
|
||||
|------|------|
|
||||
| `adb8407` | scaffold — onboard.sh, lib/, steps/00-preflight, hosts/lustro/node.yaml draft |
|
||||
| `9012a36` | 00-access.sh + node.yaml ssh_user/first_contact/hardware |
|
||||
| `931fd46` | dry-run propagacja — run() helper, DRY_RUN=0/1 |
|
||||
| `eed0ad0` | yaml_get fix — inline-comment + greedy-colon |
|
||||
| `1bed855` | first_contact: IP zamiast mDNS .local |
|
||||
| `471ba09` | verify fix — LogLevel=ERROR, czysty stdout |
|
||||
|
||||
## OTWARTE — do następnej sesji (kolejność)
|
||||
|
||||
1. **WORKTREE HYGIENE** (pierwsza rzecz): cała sesja jechała w MAIN checkout wbrew
|
||||
zasadzie "main = deploy-only". Decyzja nierozstrzygnięta:
|
||||
- (A) rename `feat/` → `task/node-onboarding` + worktree + main→master
|
||||
(pełna zgodność z `agent.sh`; merge=FF)
|
||||
- (B) zostać `feat/` + ręczny `git merge --ff-only`
|
||||
|
||||
`agent.sh new` tworzy `task/<name>` od `master` i NIE bierze istniejącego brancha.
|
||||
`git worktree list` jeszcze nieodczytany (potrzebny wzorzec ścieżki).
|
||||
|
||||
2. **base step**: migracja swap 200 M-plik → zram; `/opt/homelab` + `chown pi`
|
||||
(uid 1000 już pasuje); event dir `/opt/homelab/events/lustro/`
|
||||
3. **node-agent step**: docker override, user 1000:1000 (pi=1000), `mem_limit: 256m`
|
||||
4. **register step**: observer/supervisor inventory + redis sub + UI panel agents.okit.pl
|
||||
5. **verify step (50)**: smoke end-to-end (event dotarł do control plane, widać w UI,
|
||||
realny alert path Telegram)
|
||||
6. **mm-watch**: health check `systemctl is-active magicmirror.service`
|
||||
7. **drobiazgi**: baner URL w 00-access ma defekt wyrównania; `locale pl_PL`
|
||||
niewygenerowane na lustrze (niegroźne)
|
||||
|
||||
## Learnings
|
||||
|
||||
(odzwierciedlone też w `scripts/onboard/README.md`)
|
||||
|
||||
- mDNS `.local` zawodny do automatyzacji → `first_contact` przez IP lub tailscale, nie `.local`
|
||||
- istniejący node z userem uid=1000: użyj go zamiast tworzyć `oskar` (kolizja uid)
|
||||
- swap na SD = wear → zram
|
||||
- dry-run MUSI propagować do step-skryptów (`run()` wrapper), inaczej bezużyteczny
|
||||
- yaml fallback bez `yq` musi strippować inline komentarze i nie być greedy na `:`
|
||||
139
scripts/onboard/README.md
Normal file
139
scripts/onboard/README.md
Normal file
|
|
@ -0,0 +1,139 @@
|
|||
# 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
|
||||
|
||||
```bash
|
||||
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ę |
|
||||
|
||||
```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\>/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:<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.
|
||||
|
||||
```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) |
|
||||
Loading…
Reference in a new issue