commit 7f6b8ebe07c9318bc061b5cc116ba97d45ee4383 Author: Björn Runden Date: Thu Oct 29 21:56:53 2026 +0100 Basis aus 2nd-brain für LPIC-Prüfungsvorbereitung übernommen Regeln, Standards, Kontextstruktur und Werkzeuge übernommen; 2nd-brain- Inhalte (Stacks, Ansible-Inventar, Architektur, Knowledge) entfernt. Co-Authored-By: Claude Opus 5.5 diff --git a/.continue/rules/00_agents.md b/.continue/rules/00_agents.md new file mode 100644 index 0000000..db1d5f5 --- /dev/null +++ b/.continue/rules/00_agents.md @@ -0,0 +1,5 @@ +--- +name: AI-Basis +alwaysApply: true +--- +Maßgeblich ist `AGENTS.md` im Workspace-Root: vor jeder Aufgabe lesen und den Regeln in `ai/rules/` folgen. diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..00f2c60 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,15 @@ +root = true + +[*] +charset = utf-8 +end_of_line = lf +insert_final_newline = true +trim_trailing_whitespace = true +indent_style = space +indent_size = 2 + +[*.md] +trim_trailing_whitespace = false + +[Makefile] +indent_style = tab diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md new file mode 100644 index 0000000..970af91 --- /dev/null +++ b/.github/copilot-instructions.md @@ -0,0 +1,4 @@ +# Copilot-Anweisungen + +Maßgeblich ist **AGENTS.md** im Repository-Root (Sitzungsstart, Arbeitsgedächtnis `context.md`, Regeln in `ai/rules/`, Standards in `ai/common/`). +Amtssprache und Projekt stehen dort unter „Metadaten“. diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..04732c3 --- /dev/null +++ b/.gitignore @@ -0,0 +1,11 @@ +# Geheimnisse & lokale Werte +.env +*.env.local +.ans-secret* +# Laufzeit +*.log +log/ +fact_caching/ +*.retry +# Sicherungen beim Überschreiben (bewusst lokal behalten, nicht versionieren) +*.orig diff --git a/.shellcheckrc b/.shellcheckrc new file mode 100644 index 0000000..620bf28 --- /dev/null +++ b/.shellcheckrc @@ -0,0 +1,3 @@ +# _functions.sh wird per Pfad aus SCRIPT_DIR gesourct +external-sources=true +source-path=SCRIPTDIR diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..f7054ca --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,61 @@ +# AGENTS.md – Einstieg für KI-Agenten + +> **Maßgebliche Datei** für alle Agenten (Claude Code, Codex, Gemini CLI, VSCodium/Continue, Copilot, Chat-Dialoge). +> `CLAUDE.md`, `GEMINI.md`, `.github/copilot-instructions.md` verweisen nur hierher – Änderungen **nur hier**. + +## Metadaten +- **Projekt:** LPIC-Prüfungsvorbereitung (`lpic`) – Arbeitsgedächtnis: [`context.md`](context.md) +- **Betreiber:** `br` (siehe [`ai/context/responsible.md`](ai/context/responsible.md)) +- **Amtssprache:** `de` +- **Basis:** skel-ai, reduziert auf ein einzelnes Projekt (kein Mandant, keine Teilprojekte) +- **Stand:** 29.10.2026 + +## Amtssprache +Alle Antworten, Dokumente, Kommentare, Log-Meldungen, Ansible-Task-Namen und Commit-Texte in der **Amtssprache** (oben). +Fachbegriffe und Code-Bezeichner im Original. Bei `de`: korrekte Umlaute (ä ö ü ß), nie ae/oe/ue. +Details und Ausnahmen: [`ai/rules/language.md`](ai/rules/language.md). + +## Sitzungsstart (Pflicht, in dieser Reihenfolge) +1. **Regeln lesen:** alle `ai/rules/*.md`. Sie haben **Vorrang vor allem anderen** (außer Sicherheits-/Datenschutzgrenzen des Agenten). +2. **Verabredungen lesen:** `ai/common/agents.md`, dann aufgabenbezogen die passende Datei (`bash.md`, `ansible.md`, `docker.md`, `coding.md`, `markdown.md`, `architecture.md`). +3. **Kontext lesen:** `ai/context/*.md`; Wissen aus `ai/context/knowledge/INDEX.md` gezielt nachladen. +4. **Arbeitsgedächtnis lesen:** [`context.md`](context.md) im Root. +5. Kurz bestätigen: gelesene Dateien, nächster Schritt laut `context.md`. + +## Verzeichnisstruktur +| Pfad | Inhalt | Wer pflegt | +|---|---|---| +| `AGENTS.md` | diese Datei – Einstieg, Metadaten, Sitzungsablauf | Bediener + Agent | +| `context.md` | **Arbeitsgedächtnis** des Projekts: Ziel, Stand, Entscheidungen, offene Fragen, nächster Schritt | Agent | +| `ai/rules/` | **verbindliche** Erlaubnisse/Verbote, eine Regel pro Datei | Bediener (Agent nur auf Anweisung) | +| `ai/common/` | Verabredungen & Standards (Bash, Ansible, Docker, Coding, Doku, Architektur) + `templates/` | Bediener + Agent | +| `ai/context/` | Kontext (Verantwortliche, Stil, Umgebung) | Bediener + Agent | +| `ai/context/knowledge/` | Knowledge-DB (Markdown + Frontmatter, Index per Script) | Agent + Bediener | +| `ai/inbox/` | Eingang: vom Bediener abgelegte `.md`-Dateien, Agent ordnet ein und verschiebt nach `done/` | Bediener → Agent | +| `bash/` | Shell-Scripts, Übungsscripts (`ai/common/bash.md`) – bei Bedarf anlegen | Agent + Bediener | +| `ansible/` | Plays, Rollen, Inventar für Lab-VMs (`ai/common/ansible.md`) – bei Bedarf anlegen | Agent + Bediener | +| `stacks/` | Compose-Stacks, je Stack ein Ordner (`ai/common/docker.md`) – bei Bedarf anlegen | Agent + Bediener | +| `bin/` | Werkzeuge der Basis (nach `ai/common/bash.md` gebaut) | Bediener + Agent | + +Jeder Ordner hat eine `README.md`, die Zweck, Inhalt und Pflege beschreibt. + +## Werkzeuge +```bash +bin/kb_index.sh [--check] # Knowledge-INDEX.md erzeugen / prüfen +bin/bashdoc.sh [-o doku.md] # Doku aus '##'-Kommentaren +``` +Alle Scripts: `-h` zeigt die Hilfe, `-n` Trockenlauf, `-d` Debug. + +## Feste Grundsätze (Kurzfassung – Details in `ai/rules/`) +- **Datenschutz zuerst** – keine personenbezogenen/identifizierenden Daten an externe Dienste, auch nicht in Suchanfragen ([privacy.md](ai/rules/privacy.md)). +- **Geheimnisse** nie in Dateien/Git/Chat; Secret-Store zuerst ([secrets.md](ai/rules/secrets.md)). +- **Verantwortliche im Header** jeder Code-Datei, namentlich aus `ai/context/responsible.md` ([headers.md](ai/rules/headers.md)). +- **Kein Code in Cloud-VMs oder gegen Produktion** testen ohne ausdrückliche Freigabe ([testing.md](ai/rules/testing.md)). +- **Vor Löschen/Überschreiben** Ziel ansehen; `*.orig` statt Löschen ([file_safety.md](ai/rules/file_safety.md)). +- **Keine Erfindungen** – Unsicheres kennzeichnen, Quellen mit Datum nennen. + +## Ergebnisse ablegen +- Projektstand → `context.md` (Sitzungsprotokoll, Entscheidungen, offene Fragen, nächster Schritt). +- Dauerhaftes Wissen (Prüfungsthemen, Systeme, Dienste, Entscheidungen, Anleitungen) → Knowledge-DB, danach `bin/kb_index.sh`. +- Verbesserungen an der Basis → siehe „AI-Basis verbessern“ in [`ai/common/agents.md`](ai/common/agents.md). +- Datumsangaben im Text absolut (TT.MM.JJJJ), in Frontmatter/Dateinamen ISO (JJJJ-MM-TT). diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..635c228 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,4 @@ +# Agenten-Einstieg + +Maßgeblich ist **[AGENTS.md](AGENTS.md)** – zu Sitzungsbeginn vollständig lesen und dem dort beschriebenen Sitzungsstart folgen. +Diese Datei enthält bewusst nichts weiter, damit es nur eine Quelle der Wahrheit gibt. diff --git a/GEMINI.md b/GEMINI.md new file mode 100644 index 0000000..635c228 --- /dev/null +++ b/GEMINI.md @@ -0,0 +1,4 @@ +# Agenten-Einstieg + +Maßgeblich ist **[AGENTS.md](AGENTS.md)** – zu Sitzungsbeginn vollständig lesen und dem dort beschriebenen Sitzungsstart folgen. +Diese Datei enthält bewusst nichts weiter, damit es nur eine Quelle der Wahrheit gibt. diff --git a/README.md b/README.md new file mode 100644 index 0000000..e244fdc --- /dev/null +++ b/README.md @@ -0,0 +1,28 @@ +# lpic – LPIC-Prüfungsvorbereitung + +Lernprojekt zur Vorbereitung auf die LPIC-Zertifizierung. +Aufgebaut auf der skel-ai-Basis: Regeln, Code-Standards (Bash, Ansible, Docker) und eine Markdown-Knowledge-DB, +damit KI-Agenten (agentisch oder im Chat) konsistent arbeiten. + +## Für Agenten +Einstieg ist **[AGENTS.md](AGENTS.md)**. `CLAUDE.md`, `GEMINI.md`, `.github/copilot-instructions.md` und `.continue/rules/` zeigen nur dorthin. +Arbeitsstand: [context.md](context.md). + +## Aufbau +``` +lpic/ +├── AGENTS.md # maßgeblicher Einstieg (Metadaten, Sitzungsstart) +├── context.md # Arbeitsgedächtnis des Projekts +├── CLAUDE.md · GEMINI.md # Zeiger auf AGENTS.md +├── ai/ +│ ├── rules/ # verbindliche Regeln (Vorrang) +│ ├── common/ # Standards & Verabredungen + templates/ +│ └── context/ # Verantwortliche, Stil, Umgebung + knowledge/ (Knowledge-DB, Lerninhalte unter topics/) +└── bin/ # Werkzeuge der Basis +``` +Code für Übungen und Lab (`bash/`, `ansible/`, `stacks/`) wird bei Bedarf angelegt. + +## Einrichten +```bash +cp bin/.env.example bin/.env # optional, lokale Einstellungen der Basis-Werkzeuge +``` diff --git a/ai/README.md b/ai/README.md new file mode 100644 index 0000000..a38561d --- /dev/null +++ b/ai/README.md @@ -0,0 +1,25 @@ +# ai/ – Regeln, Standards und Kontext für KI-Agenten + +| Ordner | Frage, die er beantwortet | Verbindlichkeit | Wer ändert | +|---|---|---|---| +| [`rules/`](rules/README.md) | **Was darf/muss ich (nicht)?** | verbindlich, Vorrang | Bediener; Agent nur auf ausdrückliche Anweisung | +| [`common/`](common/README.md) | **Wie machen wir Dinge?** (Code, Doku, Architektur, Arbeitsweise) | Standard; Abweichung nur begründet | Bediener + Agent (mit Änderungsvermerk) | +| [`context/`](context/README.md) | **Für wen, wo, womit?** (Personen, Stil, Umgebung) + Knowledge-DB | Faktenbasis | Bediener + Agent | +| [`inbox/`](inbox/README.md) | **Was ist neu hereingekommen?** (vom Bediener abgelegte `.md`) | temporär | Bediener legt ab, Agent ordnet ein | + +## Lesereihenfolge +`rules/` → `common/agents.md` → aufgabenbezogener Standard aus `common/` → `context/*.md` → `context/knowledge/INDEX.md` (gezielt) → `../context.md`. + +## Abgrenzung (wohin gehört eine neue Information?) +| Information | Ziel | +|---|---| +| Verbot/Erlaubnis, Grenze, Pflicht | `rules/.md` (eine Regel pro Datei) | +| Konvention, Vorgehen, Vorlage | `common/.md`, Vorlage in `common/templates/` | +| Wer/wo/womit (Überblick) | `context/.md` | +| Fakt, Entscheidung, Anleitung, Recherche mit Bestand | `context/knowledge//…` | +| Projektstand, Ziel, offene Fragen | `../context.md` | +| Code (Übungen, Lab) | `../bash/`, `../ansible/`, `../stacks/` – Ordner erst bei Bedarf anlegen | + +## Namenskonventionen der Basis +- Datei- und Ordnernamen: Englisch, Kleinbuchstaben, `_` als Trenner, ASCII → stabil bei Klonen mit anderer Amtssprache. +- Inhalte: Amtssprache (siehe `AGENTS.md`). diff --git a/ai/common/README.md b/ai/common/README.md new file mode 100644 index 0000000..605c198 --- /dev/null +++ b/ai/common/README.md @@ -0,0 +1,24 @@ +# ai/common/ – Standards & Verabredungen + +> **Wie** wir Dinge tun. Verbindlicher Standard; Abweichungen nur begründet (im Ergebnis bzw. Stack-/Projekt-README). +> Verbesserungen sind erwünscht – Ablauf: `agents.md` § 5 „AI-Basis verbessern“. Jede Datei hat unten ein Änderungsprotokoll. + +| Datei | Inhalt | +|---|---| +| [agents.md](agents.md) | Arbeitsweise der Agenten, Qualitätscheckliste, Basis verbessern, Rollen | +| [bash.md](bash.md) | Bash-Standard: `.env` + `_functions.sh`, Optionen, Logging (`becho`…), Trap, Präambel = Hilfe | +| [ansible.md](ansible.md) | Ansible: Layout, `.env.sh`, Präambel, `hostlist`, Rollen, Facts, Secrets, Tags | +| [docker.md](docker.md) | Docker-Compose-Standard (GELF, Traefik, Bind-Mounts, `.env`, README-Pflicht) | +| [coding.md](coding.md) | Allgemeine Code-Regeln, Namen, Konfiguration, Python, Lint/Test | +| [markdown.md](markdown.md) | Markdown-/Doku-Konventionen, READMEs, Projekt-`context.md` | +| [architecture.md](architecture.md) | Architektur-Skizze (arc42-light), ADRs, Lösungsgrundsätze | + +## templates/ +| Pfad | Inhalt | +|---|---| +| `templates/bash/` | `script_template.sh`, `_functions.sh` (Referenz, Version in `B_FUNCTIONS_VERSION`), `.env.example` | +| `templates/ansible/` | `.env.sh`, `ansible.cfg`, `inv/`, `playbooks/{example,mini_check}.yml`, `roles/example/`, `files/example/` | +| `templates/docker/` | `docker-compose.yml`, `.env.example`, Stack-`README.md` | +| `templates/markdown/` | `knowledge_entry.md`, `adr.md` | + +Vorlagen werden **kopiert**, nicht verlinkt. `bin/_functions.sh` ist eine Kopie von `templates/bash/_functions.sh` – beide gleich halten. diff --git a/ai/common/agents.md b/ai/common/agents.md new file mode 100644 index 0000000..a488376 --- /dev/null +++ b/ai/common/agents.md @@ -0,0 +1,62 @@ +# Agenten – Arbeitsweise (projektübergreifend) + +> Gilt für alle KI-Agenten, agentisch (Claude Code, Codex, Gemini CLI, VSCodium/Continue) wie im Chat. +> `ai/rules/*.md` haben Vorrang. + +## 1. Arbeitsweise +- **Sitzungsstart** gemäß `AGENTS.md`. +- **Erst verstehen, dann bauen:** vorhandenen Code, Knowledge-Einträge und Projektstand lesen, bevor etwas Neues entsteht. Bestehendes wiederverwenden. +- **Empfehlung statt Optionsliste.** Alternativen nur, wenn die Entscheidung wirklich beim Bediener liegt – dann knapp mit Empfehlung. +- **Nachfragen**, wenn: Ziel/Projekt unklar, Regel berührt (Produktion, Löschen, extern), Verantwortliche für den Header fehlen, Annahmen das Ergebnis wesentlich ändern. +- **Selbstständig handeln** bei: Formalien, Konventionen aus `common/`, offensichtlichen Defaults – im Ergebnis kurz erwähnen. +- **Keine Erfindungen:** Hostnamen, IPs, Versionen, Optionen nur aus Knowledge/Projekt/Quelle. Unsicheres als „unsicher“ markieren. +- **Ergebnisse ablegen, nicht nur im Chat:** Projektstand → `context.md`; Dauerhaftes → Knowledge-DB. + +## 2. Qualitätsmaßstab („nah am Optimum“) +Vor Abgabe von Code/Doku/Architektur selbst prüfen: +- [ ] Passender Standard aus `common/` eingehalten, Vorlage aus `common/templates/` genutzt +- [ ] Header mit Verantwortlichen (`rules/headers.md`), Funktionen mit Kurz-Doku +- [ ] Keine Geheimnisse, keine internen Daten in Beispielen für Externe +- [ ] Lokal getestet (Syntax, Lint, Trockenlauf) – oder ausdrücklich gesagt, was **nicht** getestet wurde +- [ ] Doku/Knowledge aktualisiert, `bin/kb_index.sh` gelaufen +- [ ] Einfachste Lösung, die das Ziel erfüllt – keine Vorratsfunktionen + +## 3. Formales +- Sprache, Datum: `rules/language.md`. +- Markdown: `common/markdown.md`. +- Commit-Messages (falls Git): Amtssprache, Imperativ, erste Zeile ≤ 72 Zeichen, Bezug zum Bereich (`ai/rules: Regel Prüfungsfragen ergänzt`). + +## 4. Standards (Verweise) +| Thema | Datei | +|---|---| +| Bash-Scripts | [bash.md](bash.md) | +| Ansible | [ansible.md](ansible.md) | +| Docker-Compose | [docker.md](docker.md) | +| Coding allgemein, Python | [coding.md](coding.md) | +| Markdown, Knowledge-Einträge | [markdown.md](markdown.md) | +| Architektur-Doku, Entscheidungen (ADR) | [architecture.md](architecture.md) | + +## 5. AI-Basis verbessern +Agenten **sollen** die Basis besser machen, wenn sie bei der Arbeit Lücken, Widersprüche oder wiederkehrende Muster bemerken. + +| Was | Wie | +|---|---| +| Regel fehlt/ist unklar (`rules/`) | **Nicht selbst ändern.** Vorschlag im Chat + Eintrag „Offene Fragen“ in `context.md` | +| Standard verbessern (`common/`) | Ändern, wenn klar besser und rückwärtskompatibel; Eintrag im Änderungsprotokoll der Datei (unten, `TT.MM.JJJJ – was – warum`). Größeres vorher vorschlagen | +| Vorlage/Script (`common/templates/`, `bin/`) | Ändern + lokal testen + `@version` erhöhen; `_functions.sh` in `bin/` und `templates/bash/` synchron halten | +| Kontext/Knowledge | Jederzeit ergänzen/korrigieren, mit `updated` und Quelle | +| Wiederkehrende Frage des Bedieners | Antwort als Knowledge-Eintrag (`howto/`) ablegen | + +Verbesserungen, die **allgemein** gelten (nicht nur für dieses Projekt), dem Bediener zur Übernahme ins Skeleton (`~/skel-ai`) vorschlagen. + +## 6. Rollen der Agenten +| Agent | Einsatz | Grenzen | +|---|---|---| +| Claude Code | agentisches Coding, Doku | Datenklasse laut `rules/privacy.md` | +| Chat-Dialog | Konzepte, Reviews | kein Dateizugriff → relevante Dateien einfügen lassen | + +**Chat-Dialog ohne Dateizugriff:** Der Bediener fügt `AGENTS.md`, die betroffenen `rules/`, den passenden Standard und `context.md` ein. Der Agent liefert vollständige Dateien bzw. Diffs und am Ende einen Block „Für context.md“ zum Einpflegen. + +## Änderungsprotokoll +- 07.10.2026 – angelegt (aus skel-ai). +- 23.10.2026 – auf Einzelprojekt reduziert (`context.md` im Root, Rollen festgelegt). diff --git a/ai/common/ansible.md b/ai/common/ansible.md new file mode 100644 index 0000000..a908fb6 --- /dev/null +++ b/ai/common/ansible.md @@ -0,0 +1,91 @@ +# Ansible-Standard (verbindlich) + +> Gilt für alle Plays und Rollen in diesem Projekt. Verallgemeinert aus einem erprobten Homelab-Setup. +> Vorlagen: [`templates/ansible/`](templates/ansible/). Geheimnisse: `rules/secrets.md`. + +## 1. Verzeichnislayout +``` +/ansible/ +├── .env.sh # setzt alle Pfade + Secret-Store-Variablen → source (bash/zsh) +├── ansible.cfg # Fact-Cache, become, Log – Pfade aus .env.sh +├── inv/ +│ ├── # Inventar (INI oder YAML), z. B. prod, lab +│ ├── group_vars/all/ # globale Variablen (ansible_user, ans_files, Secret-Store-Pfade) +│ ├── group_vars/.yml +│ └── host_vars/.yml +├── playbooks/ # ein Dienst = ein Play; Präambel = Doku +├── roles// # Logik: tasks/ handlers/ defaults/ meta/ (keine templates/!) +└── files// # alle Templates (*.j2) und Dateien – Rollen holen sie aus $ANS_FILES +``` +`.env.sh` (interaktiv in der Shell gesourct, setzt `ANSIBLE_*`) ist bewusst etwas anderes als die `.env` der Bash-Scripts. + +## 2. Aufruf & Umgebung +- Vor jedem Aufruf: `source ansible/.env.sh`. Pfade **nie** hart codieren: `$ANSIBLE_BASE_PATH`, `$ANS_PLAYDIR`, `$ANS_FILES` (in YAML: `ans_files`). +- Aufrufe immer: `ansible-playbook $ANS_PLAYDIR/.yml [-e hostlist=…] [--check --diff]`. +- `.env.sh` setzt fremde `ANSIBLE_*`-Variablen zurück (`unset`), damit kein Kontext eines anderen Repos durchschlägt. + +| Variable | Zweck | +|---|---| +| `ANSIBLE_BASE_PATH` | Basis für alles | +| `ANSIBLE_CONFIG`, `ANSIBLE_INVENTORY`, `ANSIBLE_ROLES_PATH` | Standard-Ansible-Variablen | +| `ANS_PLAYDIR` | Playbooks | +| `ANS_FILES` | Templates/Dateien (YAML: `ans_files`) | +| `ANS_DEFAULT_HOSTS` | Dokumentation des Standardziels (s. § 3) | +| `VAULT_ADDR`, `ANSIBLE_HASHI_VAULT_*`, `ANS_VAULT_KV_MOUNT/PATH` | Secret-Store | +| `ANSIBLE_VAULT_PASSWORD_FILE` | nur wenn Datei existiert (Fallback Ansible Vault) | + +## 3. Playbooks +- **Präambel max. 12 Zeilen** im Header-Format (`rules/headers.md`): `@file`, `@brief`, `@author`, `@owner`, `@version`, `@usage` mit nötigen/optionalen `-e`-Variablen, Tags und Voraussetzungen. +- Ziel immer `hosts: "{{ hostlist | default('') }}"` – ohne `-e` genau **ein** bekannter, ungefährlicher Host + (Lab/Test). **Nie** `all` oder eine Produktivgruppe als Default. +- Ein Dienst = ein Play (`radius.yml`, `dns.yml`). Logik liegt in `roles//`; das Play bindet nur die Rolle ein. +- Dateinamen: Kleinbuchstaben, `_` statt `-` (`vault_check.yml`). +- Standard-Hilfsplays je Repo: `mini_check.yml` (Erreichbarkeit, Facts), `vault_check.yml` (Secret-Store erreichbar, nur Schlüsselnamen). + +## 4. Rollen +- Rollenname = Dienst. Tasks thematisch teilen: `main.yml` (Ablauf) → `secrets.yml`, `install.yml`, `config.yml`, `verify.yml`. +- `main.yml` beginnt mit einem **Plattform-Check** (`ansible.builtin.assert` auf `ansible_facts`), Tag `always`. +- **Templates liegen nicht in der Rolle**, sondern in `files//`; Zugriff über `{{ ans_files }}//…`. +- Nur Dateien templaten, die wir „besitzen“. Paket-Defaults nicht komplett überschreiben. +- Konfiguration **vor** dem Restart prüfen: Handler-Kette Check → Restart (`listen`/`notify`), `meta: flush_handlers` vor `service: started`. + Bei Fehler bleibt der laufende Dienst unberührt. +- Jede Rolle hat einen **Smoke-Test** (`verify.yml`, Tag `verify`, `check_mode: false`, `changed_when: false`). +- **Idempotent:** zweiter Lauf `changed=0`. `command`/`shell` nur mit `creates`/`changed_when`. +- Module mit FQCN (`ansible.builtin.…`, `community.…`). **Task-Namen in der Amtssprache**, als Satz („Paket installieren“). +- Variablen der Rolle mit Rollenpräfix (`radius_clients`), Defaults in `defaults/main.yml` mit Kommentar je Variable. + +## 5. SSH-Performance +- `pipelining=True` (Standard in `templates/ansible/ansible.cfg`): Module laufen per stdin statt Kopie nach `/tmp` – spart je Task + mehrere SSH-Roundtrips, typisch der größte Einzelgewinn. Voraussetzung: kein `Defaults requiretty` in `sudoers` (aktuelle Distros: Standard). +- Dazu `ControlMaster=auto` / `ControlPersist=60s`: eine SSH-Verbindung je Host wird wiederverwendet. +- Bei Fehlern wie „sudo: sorry, you must have a tty“: `requiretty` auf dem Ziel entfernen, nicht `pipelining` abschalten. + +## 6. Facts +- `gathering=smart` + Fact-Cache (`jsonfile`, 86400 s) aus `ansible.cfg`. Facts nutzen statt Shell-Abfragen + (`ansible_facts['distribution_major_version']`), Zugriff immer über `ansible_facts[...]`, nicht über `ansible_*`-Kurzformen. +- Facts neu sammeln: `ansible-playbook $ANS_PLAYDIR/mini_check.yml -e check_mode=setup`. + +## 7. Geheimnisse +- Secret-Store per `community.hashi_vault` (Lookup in `secrets.yml`), Fallback Ansible Vault (`vault_*`-Variablen). +- Tasks mit Geheimnissen `no_log: true`, Templates mit Geheimnissen `diff: false`. Nicht entfernen. +- Testbetrieb ohne Secret-Store: `-e _secrets_source=vars -e @/pfad/außerhalb/testsecrets.yml`. + +## 8. Tags +`install`, `config`, `verify`; Plattform-Check und Secrets `always`. + +## 9. Kommentare & Stil +- Deaktivierte, absichtlich behaltene Blöcke mit `#-#` markieren. +- YAML: 2 Leerzeichen, `---` am Anfang, `true`/`false` statt `yes`/`no`, Strings mit Jinja in Anführungszeichen. +- Prüfen vor Abgabe: `ansible-lint`, `yamllint`, `ansible-playbook --syntax-check`, `--check --diff`, Lauf gegen lokalen Testcontainer, Idempotenz (`rules/testing.md`). + +## 10. Neue Rolle in 5 Schritten +1. Original-Doku des Dienstes holen (`rules/web_search.md`), Ergebnisse in `knowledge/services/.md`. +2. `cp -r ai/common/templates/ansible/roles/example roles/`, `files/example` → `files/`, Play aus `playbooks/example.yml`. +3. Präfix `example_` → `_`, Header ausfüllen. +4. Lokal testen (Container), Idempotenz prüfen. +5. Knowledge/`context.md` aktualisieren. + +## Änderungsprotokoll +- 07.10.2026 – angelegt, verallgemeinert aus infra-devices/ansible. +- 08.10.2026 – § 5 SSH-Performance (pipelining, ControlPersist) als Vorgabe – Wunsch des Bedieners. +- 23.10.2026 – Mandantenbezug entfernt (Einzelprojekt). diff --git a/ai/common/architecture.md b/ai/common/architecture.md new file mode 100644 index 0000000..1ba355d --- /dev/null +++ b/ai/common/architecture.md @@ -0,0 +1,34 @@ +# Architektur-Dokumentation & Entscheidungen + +> Ziel: Jede nicht triviale Lösung ist nachvollziehbar – was, warum, mit welchen Alternativen, und was sie voraussetzt. + +## 1. Wann welche Form +| Umfang | Form | Ort | +|---|---|---| +| Einzelne Entscheidung mit Tragweite (Produktwahl, Protokoll, Netzsegmentierung) | **ADR** | `context/knowledge/decisions/adr_NNNN_.md` | +| Lösung des Projekts | **Architektur-Skizze** (arc42-light, § 2) | `architecture.md` (Root) | +| Dauerhafter Systembestand (Host, Dienst, Netz) | Knowledge-Eintrag | `context/knowledge/{systems,services,network}/` | + +## 2. Architektur-Skizze (arc42-light) +1. **Ziel & Rahmen** – Anforderungen, Randbedingungen (Budget, Hardware, Datenschutzklasse). +2. **Kontext** – Diagramm: System, Nutzer, Nachbarsysteme, Schnittstellen (Ports/Protokolle). +3. **Bausteine** – Komponenten, wo sie laufen, wer sie pflegt (Ansible-Rolle, Docker-Stack, Script). +4. **Abläufe** – wichtigste 1–3 Szenarien (z. B. Anmeldung, Backup, Ausfall). +5. **Entscheidungen** – Verweise auf ADRs. +6. **Risiken & offene Punkte**. +7. **Betrieb** – Monitoring, Logging (GELF), Backup/Restore, Update-Weg. + +## 3. ADR-Format +Vorlage: [`templates/markdown/adr.md`](templates/markdown/adr.md). Nummern fortlaufend, nie wiederverwenden. +Überholte ADRs nicht löschen: `status: superseded`, `supersedes`/`superseded_by` pflegen. + +## 4. Grundsätze für Lösungen im Netz/IT-Umfeld +- **Infrastructure as Code:** Neuaufbau = Playbook/Stack + Secret-Store, nicht Handarbeit. Handgriffe, die bleiben, als `howto/` dokumentieren. +- **Ein Weg pro Aufgabe:** Ansible für Hostkonfiguration, Docker-Compose für Dienste, Bash für Werkzeuge/Glue. +- **Sicher per Default:** minimale Rechte, Default-Ziel ungefährlich, Trockenlauf vor Änderung, Rückfallweg bei Netzänderungen. +- **Beobachtbar:** jeder Dienst mit Healthcheck und zentralem Logging. +- **Wiederherstellbar:** Backup-Pfad und Restore-Test sind Teil der Lösung, nicht Nacharbeit. + +## Änderungsprotokoll +- 07.10.2026 – angelegt. +- 23.10.2026 – Architektur-Skizze im Root (Einzelprojekt). diff --git a/ai/common/bash.md b/ai/common/bash.md new file mode 100644 index 0000000..3f3dca8 --- /dev/null +++ b/ai/common/bash.md @@ -0,0 +1,161 @@ +# Bash-Standard (verbindlich) + +> Gilt für jedes Bash-Script dieser Basis und ihrer Projekte. Referenzimplementierung: [`templates/bash/`](templates/bash/). +> Die Scripts in `bin/` sind nach diesem Standard gebaut und dienen als Beispiele. + +## 1. Bausteine – drei Dateien, mehr nicht +``` +/ +├── mein_script.sh # Präambel (= Hilfe) + Bootstrap + Funktionen + main +├── _functions.sh # gemeinsame Basis – Kopie aus templates/bash/, NICHT lokal ändern +├── _functions.local.sh # optional: projektspezifische Funktionen (lädt _functions.sh automatisch) +├── .env # optional: Einstellungen (nie im Git) +└── .env.example # Pflicht, wenn .env genutzt wird: gleiche Schlüssel, Platzhalter +``` +Mehrere Scripts in einem Verzeichnis teilen sich `_functions.sh` und `.env`. +Das frühere Modell (`_global_functions.sh`, `_project_functions.sh`, `_project_settings.sh`) entfällt: +Settings → `.env`, Projektfunktionen → `_functions.local.sh`. + +## 2. Aufbau eines Scripts +```bash +#!/usr/bin/env bash +#------------------------------------------------------------------------------- +## @file backup_db.sh +## @brief Sichert die Datenbank nach $DIR_BACKUP +## @author +## @owner +## @assist KI-unterstützt (), geprüft von +## @version 1.0.0 (07.10.2026) +## +## @usage backup_db.sh [Optionen] +## @options +## @OPTIONS@ +## @env .env neben dem Script (Vorlage: .env.example) +## @exit 0 = ok · 1 = Fehler · 2 = Aufruffehler +#------------------------------------------------------------------------------- +set -Eeuo pipefail + +SCRIPT_PATH="$(readlink -f -- "${BASH_SOURCE[0]}")" # Bootstrap – unverändert übernehmen +SCRIPT_DIR="${SCRIPT_PATH%/*}" +SCRIPT_NAME="${SCRIPT_PATH##*/}" +source "${SCRIPT_DIR}/_functions.sh" || { echo "FEHLER: ${SCRIPT_DIR}/_functions.sh fehlt" >&2; exit 1; } + +B_OPTS+=( "k|keep|value:TAGE|14|Sicherungen so lange behalten" ) +b_init "$@" +set -- "${ARGS[@]}" + +## @brief Hauptablauf +## @param $1 Datenbankname +## @return 0 ok, 1 Fehler +main() { … } + +main "$@" +``` + +### Präambel = Hilfe = Doku +- Der Header zwischen den ersten beiden `#-----`-Linien ist **gleichzeitig** die Hilfe (`-h`): `b_usage` gibt alle `##`-Zeilen ohne `## ` aus. +- `## @OPTIONS@` wird durch die Optionsliste aus `B_OPTS` ersetzt → Hilfe und Optionen können nicht auseinanderlaufen. +- `##` ist der Doku-Marker (wie `/**` bei javadoc). `bin/bashdoc.sh script.sh` erzeugt daraus Markdown. Normale Kommentare mit `#`. +- Header-Pflichtfelder: `rules/headers.md`. + +## 3. Pfadsicherheit +| Variable | Inhalt | +|---|---| +| `SCRIPT_PATH` | absoluter, aufgelöster Pfad des Scripts (Symlinks aufgelöst) | +| `SCRIPT_DIR` | Verzeichnis des Scripts – Basis für `.env`, `_functions.sh`, relative Pfade aus `.env` | +| `SCRIPT_NAME` | Dateiname des Scripts | +| `B_CALL_DIR` | `$PWD` beim Aufruf – Basis für relative Pfade **von der Kommandozeile** | +| `B_LIB_DIR` | Verzeichnis von `_functions.sh` | +| `B_TMP_DIR` | Temp-Verzeichnis nach `b_tmpdir`, wird beim Exit gelöscht | + +- Nie `cd` als Voraussetzung, nie relative Pfade ohne Basis. Helfer: `b_abspath [basis]` (Default-Basis `SCRIPT_DIR`). +- Faustregel: Pfade aus `.env`/Defaults → relativ zu `SCRIPT_DIR`; Pfade, die der Benutzer tippt → relativ zu `B_CALL_DIR`. + +## 4. Optionen (`B_OPTS`) +Format je Eintrag: `"kurz|lang|typ[:META]|default|Beschreibung"` – `typ` = `flag` (0/1) oder `value`. + +| Quelle | Wo | Hinweis | +|---|---|---| +| Standard (`-h -d -q -n -l`) | `_functions.sh` (`B_OPTS_STD`) | immer da | +| Script-eigen | im Script vor `b_init`: `B_OPTS+=(…)` | Regelfall | +| Verzeichnisweit | `.env`: `B_OPTS+=(…)` | gilt für alle Scripts des Verzeichnisses | + +- Ergebnis: Variable `OPTION_` (Großbuchstaben, `-` → `_`): `--dry-run` → `OPTION_DRY_RUN`, `--keep 7` → `OPTION_KEEP=7`. +- Unterstützt: `-d`, `-dn` (gebündelt), `-l datei`, `-ldatei`, `--logfile datei`, `--logfile=datei`, `--debug=0`, `--` (Ende der Optionen). Positionsargumente landen in `ARGS` (auch zwischen Optionen). +- Unbekannte Option / fehlender Wert → Meldung, Hinweis auf `--help`, Exit 2. +- Immer `+=` verwenden, nie `B_OPTS=(…)` (würde Einträge aus `.env`/`_functions.local.sh` löschen). Kein `|` in Default/Text. + +### Vorrang (und Gültigkeit von Umgebungsvariablen) +**Default < `.env` < Umgebung < Kommandozeile** +- Exportierte `B_*`/`OPTION_*`-Variablen aus der Umgebung werden vor dem Laden von `.env` gesichert und danach wiederhergestellt + (`B_LOG_LEVEL=debug ./script.sh` gewinnt gegen `.env`). Nicht exportierte Shell-Variablen des Aufrufers sieht das Script ohnehin nicht. +- Für eigene Projektwerte in `.env` dasselbe Verhalten per `VAR="${VAR:-default}"`. +- `.env` wird **nicht** automatisch exportiert. Werte, die Kindprozesse brauchen, explizit `export`ieren (Geheimnisse möglichst nicht). +- Wirkung der Standardoptionen: `-d` → `B_LOG_LEVEL=debug`, `-q` → `B_LOG_CONSOLE=0`, `-l` → `B_LOG_FILE`, `-n` → `brun` führt nichts aus. + +## 5. Ausgabe & Logging – nie `echo` +| Funktion | Level | Ziel Konsole | Zweck | +|---|---|---|---| +| `becho "…"` | info | stdout, ohne Präfix | Ersatz für `echo` | +| `bdebug "…"` | debug | stderr, `[DEBUG]` | Details, nur bei `-d` | +| `bwarn "…"` | warn | stderr, `[WARN]` | Auffälliges, Lauf geht weiter | +| `berror "…"` | error | stderr, `[FEHLER]` | Fehler melden (Funktion entscheidet über `return`) | +| `bdie "…" [rc]` | error | stderr | melden + beenden (rc 2 = Aufruffehler mit Hilfe-Hinweis) | +| `blog "…"` | – | – | Basis aller obigen | + +Steuerung (`.env`, Umgebung oder Option): +| Variable | Default | Bedeutung | +|---|---|---| +| `B_LOG_LEVEL` | `info` | Mindestlevel Konsole (`debug`/`info`/`warn`/`error`) | +| `B_LOG_CONSOLE` | `1` | Konsole an/aus | +| `B_LOG_FILE` | leer | Logdatei; leer = keine | +| `B_LOG_FILE_LEVEL` | = `B_LOG_LEVEL` | eigenes Mindestlevel für die Datei | +| `B_LOG_COLOR` | `auto` | Farben nur auf Terminal | + +Logdatei-Format: `JJJJ-MM-TT hh:mm:ss LEVEL script.sh[PID] Text`. +**Ausnahme vom echo-Verbot:** echte Daten-Ausgabe für Pipes (`printf '%s\n' "$ergebnis"`) – Meldungen gehen nie auf stdout außer über `becho`. + +## 6. Fehler, Return-Codes, Aufräumen +- `set -Eeuo pipefail` ist Pflicht. `b_init` setzt Traps: `EXIT` (Aufräumen), `ERR` (meldet Befehl, Funktion, Zeile), `INT`/`TERM` (Exit 130/143 → EXIT-Trap). +- **Return-Code-Prinzip:** Jede Funktion endet mit `return 0` (ok), `1` (Fehler) oder `2` (falscher Aufruf); dokumentiert unter `@return`. + Fehler meldet die Funktion selbst (`berror`) und gibt den Code zurück – der Aufrufer entscheidet (`if f; then`, `f || return 1`). +- Ergebnisse über stdout (`x="$(f)"`) oder globale Variablen mit Präfix – nie über Exit-Codes > 2. +- Aufräumen: Script definiert optional `cleanup() { … }` – wird vom EXIT-Trap mit dem Exit-Code aufgerufen, auch bei Fehler/Abbruch. + Temporäres immer über `b_tmpdir` (`$B_TMP_DIR`), wird automatisch entfernt. + +## 7. Funktionsdoku („briefly!“) +```bash +## @brief Was die Funktion tut – ein Satz +## @param $1 Bedeutung +## @param $@ restliche Argumente +## @return 0 ok, 1 Fehler; Ausgabe auf stdout (falls zutreffend) +name() { +``` +- Direkt über der Funktion, keine Leerzeile dazwischen. Interne Helfer mit `_` beginnen (`bashdoc.sh` blendet sie ohne `-p` aus). +- `local` für alle Variablen in Funktionen. + +## 8. Weitere Helfer aus `_functions.sh` +| Funktion | Zweck | +|---|---| +| `brun cmd args…` | ausführen bzw. bei `-n` nur anzeigen (kein `eval`, Argumente als Array) | +| `b_require prog…` | prüft, ob Programme vorhanden sind | +| `b_require_vars VAR…` | prüft, ob Variablen gesetzt sind | +| `b_tmpdir` | legt `$B_TMP_DIR` an | +| `b_abspath pfad [basis]` | absoluter Pfad | +| `b_usage` | Hilfe ausgeben | + +## 9. Stil +- Bash ≥ 4.4. Shebang `#!/usr/bin/env bash`. 2 Leerzeichen Einrückung. +- Immer quoten (`"$var"`, `"${arr[@]}"`), `[[ ]]` statt `[ ]`, `$(…)` statt Backticks, `printf` statt `echo -e`. +- Dateinamen: Kleinbuchstaben, `_` als Trenner, Endung `.sh`; Hilfsdateien mit `_`-Präfix. +- Variablen: Konstanten/Globale `GROSS`, lokale `klein`; Präfix `B_`/`_B_`/`OPTION_` ist für `_functions.sh` reserviert. +- Prüfen vor Abgabe: `bash -n`, `shellcheck -x`, `-h`, `-n`, ein echter Lauf (lokal, `rules/testing.md`). + +## 10. Neues Script in 4 Schritten +1. `cp ai/common/templates/bash/{script_template.sh,_functions.sh,.env.example} /` (`_functions.sh` nur, wenn noch nicht vorhanden). +2. Umbenennen, Header ausfüllen (Verantwortliche aus `responsible.md`), `B_OPTS` anpassen. +3. Funktionen mit Kurz-Doku + `main` schreiben. +4. Testen (§ 9), ggf. `bin/bashdoc.sh` für die Projektdoku. + +## Änderungsprotokoll +- 07.10.2026 – angelegt; Stack auf `.env` + `_functions.sh` (+ optional `_functions.local.sh`) reduziert. diff --git a/ai/common/coding.md b/ai/common/coding.md new file mode 100644 index 0000000..76c28ef --- /dev/null +++ b/ai/common/coding.md @@ -0,0 +1,61 @@ +# Coding – allgemeine Verabredungen (alle Sprachen) + Python + +> Sprachspezifisch gehen vor: [bash.md](bash.md), [ansible.md](ansible.md), [docker.md](docker.md). + +## 1. Für jede Code-Datei +- **Header** mit Verantwortlichen (`rules/headers.md`) – ohne Ausnahme, auch für Templates und Configs. +- **Kurz-Doku je Funktion/Klasse:** Zweck, Parameter, Rückgabe – knapp („briefly!“). +- **Keine Geheimnisse und keine Umgebungsdaten im Code:** Hosts, IPs, Pfade, Domains, Ports → `.env`/Inventar/Konfiguration. +- **Fehler nie verschlucken:** melden (Log/stderr) und definierten Code zurückgeben. Exit-Codes: 0 ok · 1 Fehler · 2 Aufruffehler. +- **Logging statt print/echo** für Meldungen; stdout nur für echte Daten. +- **Dry-Run** (`-n`/`--dry-run`) für alles, was Systeme verändert. +- **Idempotent**, wo möglich: zweiter Lauf ändert nichts. +- **Einfach:** kleinste Lösung, die das Ziel erfüllt; keine Vorratsfunktionen, keine unnötigen Abhängigkeiten. + +## 2. Namen +| Was | Konvention | +|---|---| +| Dateien, Verzeichnisse | Englisch, klein, `_` als Trenner (Ausnahme: Konventionen des Werkzeugs, z. B. `docker-compose.yml`, `README.md`) | +| Variablen/Funktionen | Englisch, sprechend; Bash `snake_case`, Python PEP 8 | +| Konstanten/Umgebung | `GROSS_MIT_UNTERSTRICH` | +| Docker/Compose-Namen | `_` statt `-` (`docker.md`) | +| Kommentare, Meldungen | Amtssprache | + +## 3. Konfiguration +- Reihenfolge überall gleich: **Default < Konfigurationsdatei/.env < Umgebung < Kommandozeile**. +- `.env` nie im Git, `.env.example` immer mit allen Schlüsseln. +- Relative Pfade immer zu einer definierten Basis (Script-/Projektverzeichnis), nie zum zufälligen `$PWD`. + +## 4. Python +- Python ≥ 3.11, nur Standardbibliothek, wenn vertretbar. Abhängigkeiten in `requirements.txt` (gepinnt) bzw. `pyproject.toml`. +- Struktur: `main()` + `if __name__ == "__main__": sys.exit(main())`; Optionen mit `argparse` (Standardoptionen analog Bash: `-d/--debug`, `-q/--quiet`, `-n/--dry-run`, `-l/--logfile`). +- Logging mit `logging` (Konsole + optional Datei, Level aus Umgebung/Option); kein `print` für Meldungen. +- Header als Modul-Docstring mit denselben Tags: + ```python + #!/usr/bin/env python3 + """ + @file sync_dns.py + @brief Gleicht DNS-Einträge mit dem Inventar ab + @author + @owner + @version 0.1.0 (TT.MM.JJJJ) + @usage sync_dns.py [-d] [-n] + """ + ``` +- Funktionen mit Typannotationen und Kurz-Docstring (eine Zeile Zweck, `Args:`/`Returns:` nur wenn nicht offensichtlich). +- Pfade mit `pathlib`, Basis `Path(__file__).resolve().parent`. +- Prüfen: `ruff check`, `ruff format --check` (oder `python -m py_compile`), Tests mit `pytest`, lokal. + +## 5. Lint & Test (Übersicht) +| Sprache | Mindestens | +|---|---| +| Bash | `bash -n`, `shellcheck -x`, `-h`, `-n`, echter Lauf | +| Ansible/YAML | `yamllint`, `ansible-lint`, `--syntax-check`, `--check --diff`, Idempotenz | +| Docker | `docker compose config -q`, Healthcheck `healthy` | +| Python | `ruff`, `pytest` | +| Markdown | Links prüfen, `bin/kb_index.sh --check` | +Was nicht getestet werden konnte, steht ausdrücklich im Ergebnis und in `context.md`. + +## Änderungsprotokoll +- 07.10.2026 – angelegt. +- 23.10.2026 – `$PROJ/context.md` → `context.md` (Einzelprojekt). diff --git a/ai/common/docker.md b/ai/common/docker.md new file mode 100644 index 0000000..6b04e2a --- /dev/null +++ b/ai/common/docker.md @@ -0,0 +1,189 @@ +# Docker-Compose-Standard (verbindlich) + +> Gilt für **jeden** Docker-Stack, der hier entsteht. Nicht zur Diskussion stellen – vollständig „durchdeklinieren“. +> Abweichungen nur, wenn das Image es technisch erzwingt; dann im Stack-README begründen. +> Vorlagen: [`templates/docker/`](templates/docker/). Umgebungswerte (`<…>`) stehen in `ai/context/environment.md`. + +## 1. Pflichtbestandteile je Service +| # | Vorgabe | Warum | +|---|---|---| +| 1 | `restart: unless-stopped` | stabiler Betrieb, manuelles Stoppen bleibt respektiert | +| 2 | `logging: *default-logging` (GELF → Graylog) | zentrales Logging | +| 3 | `container_name: ${COMPOSE_PROJECT_NAME}` (mehrere Services: `${COMPOSE_PROJECT_NAME}_`) | eindeutige Namen, GELF-Tag passt | +| 4 | `env_file: .env` statt aufgeblähtem `environment:` | Stackfile bleibt transportabel | +| 5 | Persistenz nur per **Bind-Mount** aus `.env` (`${DIR_*}`), keine Named Volumes | Stack-Verzeichnis = komplettes Backup, umziehbar | +| 6 | `healthcheck` | „guter Ton“; Test passend zum Image (s. 4.) | +| 7 | Traefik-Labels (s. 3.) für jeden Service mit Web-Oberfläche/API | Exposition über Reverse Proxy | +| 8 | Image-Tag aus `.env` (`${_TAG}`), **nie** `latest` | reproduzierbar, Updates bewusst | +| 9 | **Alle** host-/umgebungsspezifischen Werte in `.env` (Hosts, IPs, Ports, Pfade, Domains, `extra_hosts`) | Stackfile enthält keine Umgebungsdaten | +| 10 | **Namen ohne Bindestrich – Unterstrich statt `-`:** `COMPOSE_PROJECT_NAME`, Stack-Verzeichnis, Service-, Container- und Router-Namen (z. B. `hashi_vault`, nicht `hashi-vault`) | GELF-Tag = Projektname; `_` läuft in der Elasticsearch-Suche sauber (`-` wird als Worttrenner zerlegt) | +| 11 | **Header** mit Verantwortlichen im `docker-compose.yml` (`rules/headers.md`) | Zuständigkeit sofort sichtbar | + +## 2. Verzeichnislayout je Stack +``` +// # = COMPOSE_PROJECT_NAME +├── docker-compose.yml +├── .env # echte Werte inkl. Secrets – nie in Git/extern +├── .env.example # gleiche Schlüssel, Platzhalter statt Secrets +├── README.md # Stack-Doku (s. 5.) +└── data/ config/ … # Bind-Mount-Ziele (über DIR_* referenziert) +``` + +## 3. Vorlagen + +### 3.1 `docker-compose.yml` +```yaml +x-logging: &default-logging + driver: gelf + options: + gelf-address: "udp://${GELF_HOST}:${GELF_PORT}" + tag: "${COMPOSE_PROJECT_NAME}" + labels: "com.docker.compose.project,com.docker.compose.service" + +services: + app: + image: /:${APP_TAG} + container_name: ${COMPOSE_PROJECT_NAME} + restart: unless-stopped + env_file: .env + # ports: nur wenn Zugriff ohne Traefik nötig ist + # - "${APP_PORT}:" + extra_hosts: # nur wenn benötigt; ein Eintrag je Variable + - "${EXTRA_HOST_1}" + volumes: + - ${DIR_DATA}:/ + - ${DIR_CONFIG}:/:ro + networks: + - proxy + labels: # Traefik bleibt aus, bis TRAEFIK_ENABLE=true (s. 3.2) + - "traefik.enable=${TRAEFIK_ENABLE:-false}" + - "traefik.docker.network=${TRAEFIK_NETWORK:-traefik-net}" + - "traefik.http.routers.${COMPOSE_PROJECT_NAME}.rule=Host(`${APP_FQDN:-${COMPOSE_PROJECT_NAME}.invalid}`)" + - "traefik.http.routers.${COMPOSE_PROJECT_NAME}.tls.certresolver=${TRAEFIK_CERTRESOLVER:-le}" + - "traefik.http.services.${COMPOSE_PROJECT_NAME}.loadbalancer.server.port=${APP_INTERNAL_PORT:-80}" + # bei Bedarf manuell ergänzen: entrypoints, middlewares … + logging: *default-logging + healthcheck: + test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://127.0.0.1:/"] + interval: 30s + timeout: 5s + retries: 3 + start_period: 30s + +networks: + proxy: + name: ${TRAEFIK_NETWORK:-traefik-net} + external: true +``` + +### 3.2 Traefik-Labels: Dummy zuerst, Freischaltung per Schalter +- Labels stehen **immer** direkt im Stackfile und werden je Stack manuell angepasst (kein `extends`, keine Auslagerung). +- `traefik.enable=${TRAEFIK_ENABLE:-false}` → solange in `.env` nicht `TRAEFIK_ENABLE=true` steht, ignoriert Traefik den Container: **kein Router, keine Let's-Encrypt-Anfrage.** +- Ablauf: Stack ohne Traefik hochfahren → Healthcheck `healthy`, Funktion geprüft, FQDN/DNS stimmt → erst dann `TRAEFIK_ENABLE=true` + `docker compose up -d`. +- Dummy-FQDN `.invalid` (reservierte TLD) verhindert, dass ein vergessener Wert je eine echte Domain anfragt. +- Mehrere Web-Services: Router-/Service-Namen eindeutig halten (`${COMPOSE_PROJECT_NAME}_`). + +### 3.3 `.env` / `.env.example` +```dotenv +# --- Stack --- +COMPOSE_PROJECT_NAME= # nur a-z, 0-9, _ – kein Bindestrich (GELF-Tag) +APP_TAG= + +# --- Logging (Graylog) --- +GELF_HOST= +GELF_PORT=12201 + +# --- Traefik (Defaults: traefik-net / le – nur bei Abweichung setzen) --- +TRAEFIK_ENABLE=false # erst auf true, wenn Stack sauber läuft (LE-Limits!) +APP_FQDN= +APP_INTERNAL_PORT= +#TRAEFIK_NETWORK=traefik-net +#TRAEFIK_CERTRESOLVER=le + +# --- Hosts --- +EXTRA_HOST_1=: + +# --- Pfade (Bind-Mounts, relativ zum Stack) --- +DIR_DATA=./data +DIR_CONFIG=./config + +# --- Image-spezifische Variablen (laut Orig-Doku, s. README) --- +``` + +### 3.4 Geprüfte, aber verworfene Wege (Compose v5.0.2, 06.10.2026) +- **`label_file:`** – keine Variablen im Schlüssel möglich (Router-Name!) und Werte werden nicht interpoliert. Unbrauchbar für Traefik. +- **`extends` aus gemeinsamer `traefik.yml`** – technisch ok, vom Bediener abgelehnt: Labels gehören sichtbar und manuell anpassbar ins Stackfile. +- **Separate `.traefik.env` per `COMPOSE_ENV_FILES` in `.env`** – wirkt nicht; nur als Shell-Variable oder per `--env-file` bei jedem Aufruf → fehleranfällig. + +## 4. Hinweise zur Umsetzung +- **`extra_hosts` in `.env`:** ja, möglich – Compose interpoliert Listenwerte. Ein Eintrag je Variable (`EXTRA_HOST_1=host01.example.org:192.0.2.10`). Schlüssel in Map-Syntax werden *nicht* interpoliert → Listen-Syntax verwenden. Leere Variable ergibt einen ungültigen Eintrag → Zeile nur aufnehmen, wenn genutzt. +- **Healthcheck:** vorher prüfen, ob `wget`/`curl` im Image existieren (`docker run --rm --entrypoint sh -c 'command -v wget curl'`). Endpunkt wählen, der im Normalbetrieb nicht fälschlich failt (z. B. Vault: `/ui/` statt `/v1/sys/health`, weil versiegelt = 503). +- **`env_file: .env`** reicht auch Stack-Variablen (GELF_*, DIR_*, TRAEFIK_*) in den Container durch – ist so gewollt und in der Regel harmlos. +- Kommentare im Stackfile nur für Nicht-Offensichtliches (wie im Vault-Healthcheck). + +## 5. Stack-README (Pflicht) +Jeder Stack bekommt ein `README.md` mit: +1. **Zweck** und Zugriff (FQDN, ggf. Port). +2. **Orig-Doku:** Links zu Image-Seite (Docker Hub / GHCR) und Hersteller-Doku, Abrufdatum. +3. **ENV-Variablen laut Orig-Doku:** Tabelle `Variable | Bedeutung | Default | gesetzt in .env?` – vollständig aus der Herstellerdoku übernommen, nicht geraten. +4. **Volumes/Pfade** laut Orig-Doku (welcher Container-Pfad persistiert was). +5. **Ports** intern/extern, Healthcheck-Endpunkt und Begründung. +6. **Besonderheiten** (Capabilities wie `IPC_LOCK`, Init-/Unseal-Schritte, Update-Hinweise, Abweichungen vom Standard). + +Vor dem Schreiben eines Stacks die Orig-Doku per Web-Recherche holen (erlaubt laut `rules/web_search.md`). + +## 6. Abnahme-Checkliste +- [ ] Punkte 1–11 aus Abschnitt 1 erfüllt +- [ ] Kein `-` und kein `__` in Projekt-, Service-, Container- und Router-Namen +- [ ] sultan-Host: Port veröffentlicht, kein externes Traefik-Netz, `entrypoints`-Label (7.1) +- [ ] Keine Hosts/IPs/Domains/Pfade/Secrets im Stackfile +- [ ] `.env.example` vollständig, ohne Secrets +- [ ] README mit Orig-Doku und ENV-Tabelle +- [ ] `docker compose config` läuft fehlerfrei +- [ ] Healthcheck wird nach Start `healthy` – **erst danach** `TRAEFIK_ENABLE=true` + +## 7. Ergänzungen (25.10.2026) + +### 7.1 sultan-Variante (Traefik auf anderer VM) +Gilt für Hosts, deren Traefik woanders läuft und per [sultan](../context/knowledge/services/sultan.md) angebunden ist. +- **Kein** externes Netz `proxy`/`traefik-net`, **kein** Label `traefik.docker.network`. +- Web-Service **veröffentlicht** seinen Port: `ports: ["${APP_PORT}:"]` (Bindung `0.0.0.0`; `127.0.0.1` verwirft der sultan-Agent). +- Label `traefik.http.routers..entrypoints=${TRAEFIK_ENTRYPOINT:-websecure}` gehört dazu; sultan prüft Entrypoint-/Resolver-Namen nicht. +- `loadbalancer.server.port` = Port **im** Container (der Agent rechnet auf den Host-Port um). +- Namen: kein doppelter Unterstrich `__` (Trenner bei sultan). +- Wartung: `docker compose stop` → Wartungsseite; `down` → 404. Für Updates `up -d` (ersetzt Container) ist unkritisch. + +### 7.2 Stacks mit mehreren Services / Datenbank +- Datenbanken als **eigener Stack** (`postgres`), nie veröffentlicht. Der Stack legt ein benanntes Netz an (`postgres_net`); + Anwendungen hängen es als `external: true` an. Je Anwendung eigene Datenbank + eigener User. +- Innerhalb eines Stacks: `depends_on` mit `condition: service_healthy`. +- Anwendung braucht Internet/Port → zusätzlich Netz `default`. + +### 7.3 `.env`: Erzeugung und Geheimnisse +- `.env.example` ist die **einzige Quelle** der Schlüssel. Platzhalter: + `@@SECRET@@` → beim ersten Deployment zufällig erzeugt (je Schlüssel eigener Wert); `<…>` → vom Betreiber zu setzen. +- Die echte `.env` erzeugt Ansible **nur, wenn sie fehlt**; danach gehört sie dem Betreiber (kein Überschreiben). + Später ersetzen Vault-Einträge die Werte (`rules/secrets.md`). +- Werte, die mehrere Stacks brauchen (DB-Passwort), stehen in der `.env` der **Anwendung**; Ansible liest sie dort und legt damit DB/User an (über stdin, nie als Argument). + +### 7.4 Deployment per Ansible +- Rolle `compose_stack` (`ansible/roles/compose_stack`): Stack-Verzeichnis `//`, Dateien aus `stacks//`, + Bind-Mount-Ziele mit Eigentümer/Rechten, `.env` (s. 7.3), `community.docker.docker_compose_v2` mit `wait`. +- Datenbank je Anwendung: Rolle `pg_app_db`. + +### 7.5 Rechte, Dateisystem +- Bind-Mount-Ziele mit dem Eigentümer des Containers anlegen (z. B. `1000:1000`); im Stack-README dokumentieren. +- Auf btrfs: Datenbank-Verzeichnisse vor dem ersten Start `chattr +C` (kein Copy-on-Write, weniger Fragmentierung; Prüfsummen entfallen dort). + +### 7.6 Backup & Update +- Datenbanken per Dump sichern (`pg_dumpall`/`pg_dump` vor dem Borgmatic-Lauf) – das reine Kopieren des Datenverzeichnisses im Betrieb ist kein konsistentes Backup. +- Update: neuen Tag in `.env` → Backup → `docker compose pull && docker compose up -d`. Rückweg: alter Tag + Restore. + **Kein automatisches `docker system prune -a`** – das alte Image bleibt für den schnellen Rückweg. + +## Festgelegte Werte +``, `TRAEFIK_NETWORK`, `TRAEFIK_CERTRESOLVER`, `GELF_HOST`/`GELF_PORT` stehen in [`ai/context/environment.md`](../context/environment.md). + +## Änderungsprotokoll +- 07.10.2026 – übernommen und verallgemeinert (Mandantenwerte → Platzhalter, Header-Pflicht ergänzt). +- 23.10.2026 – Mandantenbezug entfernt, Werte nur noch in `environment.md`; Verweis auf `web_search.md` korrigiert. +- 25.10.2026 – § 7: sultan-Variante, Mehrfach-Services/DB, `.env`-Erzeugung (`@@SECRET@@`), Deployment per Ansible, Rechte/btrfs, Backup/Update ohne Prune – Entscheidungen des Betreibers vom 23.–25.10.2026. diff --git a/ai/common/markdown.md b/ai/common/markdown.md new file mode 100644 index 0000000..77629d6 --- /dev/null +++ b/ai/common/markdown.md @@ -0,0 +1,24 @@ +# Markdown & Dokumentation + +## 1. Allgemein +- Eine `#`-Überschrift pro Datei (= Titel), darunter Gliederung mit `##`/`###`. Optional direkt unter dem Titel ein Zitatblock `> Stand: TT.MM.JJJJ · …`. +- Kurz, konkret, prüfbar: Tabellen für Fakten, Checklisten (`- [ ]`) für Abläufe/Offenes, Code-Blöcke mit Sprache (` ```bash `). +- Datumsangaben im Text absolut `TT.MM.JJJJ`; in Frontmatter/Dateinamen ISO `JJJJ-MM-TT`. +- Relative Links innerhalb der Basis (`[bash.md](../common/bash.md)`), keine absoluten Pfade. +- Quellen als Link + Abrufdatum. Unbelegtes als „Einschätzung“ kennzeichnen. +- Platzhalter: `` (auszufüllen), `` (offener Punkt); Vorlagen-Variablen für Scripts: `@@NAME@@`. +- Diagramme: Mermaid oder ASCII im Code-Block (lesbar ohne Renderer); keine Binärbilder ohne Not. + +## 2. READMEs +Jeder Ordner der Basis hat eine `README.md`: Zweck, Inhalt (Tabelle Datei → Inhalt), Pflege (wer, wann, wie). + +## 3. Knowledge-Einträge +Format und Ablage: [`../context/knowledge/README.md`](../context/knowledge/README.md); Vorlage: [`templates/markdown/knowledge_entry.md`](templates/markdown/knowledge_entry.md). + +## 4. `context.md` (Arbeitsgedächtnis im Root) +Abschnitte nicht umbenennen (Agenten suchen danach): +Metadaten · Ziel · Stand · Entscheidungen · Offene Fragen · Nächster Schritt · Sitzungsprotokoll. + +## Änderungsprotokoll +- 07.10.2026 – angelegt. +- 23.10.2026 – `context.md` liegt im Root (Einzelprojekt). diff --git a/ai/common/templates/ansible/.env.sh b/ai/common/templates/ansible/.env.sh new file mode 100644 index 0000000..1bf56ae --- /dev/null +++ b/ai/common/templates/ansible/.env.sh @@ -0,0 +1,35 @@ +#!/usr/bin/env bash +#------------------------------------------------------------------------------- +## @file .env.sh +## @brief Ansible-Kontext für dieses Repo setzen (Pfade + Secret-Store) +## @author +## @owner +## @version 1.0.0 (07.10.2026) +## @usage source ansible/.env.sh (bash und zsh, aus beliebigem Verzeichnis) +#------------------------------------------------------------------------------- +# Pfad dieser Datei – bash: BASH_SOURCE, zsh: %x +_ANS_SRC="${BASH_SOURCE[0]:-${(%):-%x}}" +_ANS_DIR="$(cd "$(dirname "$_ANS_SRC")" && pwd -P)" + +# --- Ansible-Pfade (fremden Kontext zurücksetzen) --- +unset ANSIBLE_CONFIG ANSIBLE_INVENTORY ANSIBLE_ROLES_PATH ANSIBLE_VAULT_PASSWORD_FILE +export ANSIBLE_BASE_PATH="$_ANS_DIR" +export ANSIBLE_CONFIG="$ANSIBLE_BASE_PATH/ansible.cfg" +export ANSIBLE_INVENTORY="$ANSIBLE_BASE_PATH/inv" +export ANSIBLE_ROLES_PATH="$ANSIBLE_BASE_PATH/roles" +export ANS_PLAYDIR="$ANSIBLE_BASE_PATH/playbooks" +export ANS_FILES="$ANSIBLE_BASE_PATH/files" +export ANS_DEFAULT_HOSTS="" # nur Doku – Default steht im Play + +# --- Secret-Store (HashiCorp Vault, bevorzugt) – Token aus ~/.vault-token --- +export VAULT_ADDR="" +export ANSIBLE_HASHI_VAULT_ADDR="$VAULT_ADDR" +export ANSIBLE_HASHI_VAULT_AUTH_METHOD="token" +export ANS_VAULT_KV_MOUNT="secret" +export ANS_VAULT_KV_PATH="" + +# --- Ansible Vault (nur Fallback, Passwortdatei außerhalb des Repos) --- +[ -f "$HOME/.ans-secret-" ] && export ANSIBLE_VAULT_PASSWORD_FILE="$HOME/.ans-secret-" + +unset _ANS_DIR _ANS_SRC +echo "Ansible-Kontext aktiv: ANSIBLE_BASE_PATH=$ANSIBLE_BASE_PATH" diff --git a/ai/common/templates/ansible/ansible.cfg b/ai/common/templates/ansible/ansible.cfg new file mode 100644 index 0000000..063af13 --- /dev/null +++ b/ai/common/templates/ansible/ansible.cfg @@ -0,0 +1,24 @@ +## @file ansible.cfg – Pfade kommen aus .env.sh (source ansible/.env.sh) +[defaults] +inventory=$ANSIBLE_BASE_PATH/inv +roles_path=$ANSIBLE_BASE_PATH/roles +log_path=$ANSIBLE_BASE_PATH/ansible.log +fact_caching=jsonfile +fact_caching_connection=$ANSIBLE_BASE_PATH/fact_caching +fact_caching_prefix=facts_ +fact_caching_timeout=86400 +gathering=smart +interpreter_python=auto_silent +retry_files_enabled=False +stdout_callback=default +callback_result_format=yaml + +[privilege_escalation] +become=True +become_method=sudo +become_user=root +become_ask_pass=False + +[ssh_connection] +ssh_args=-o ControlMaster=auto -o ControlPersist=60s +pipelining=True diff --git a/ai/common/templates/ansible/files/example/example.conf.j2 b/ai/common/templates/ansible/files/example/example.conf.j2 new file mode 100644 index 0000000..8b5fa94 --- /dev/null +++ b/ai/common/templates/ansible/files/example/example.conf.j2 @@ -0,0 +1,10 @@ +{#- +## @file example.conf.j2 +## @brief Konfiguration für – verwaltet von Ansible (Rolle example) +## @author +## @owner +## @version 0.1.0 (TT.MM.JJJJ) +-#} +# {{ ansible_managed }} +# Nicht von Hand ändern – Änderungen über Ansible. +password = {{ example_secrets.password | default('') }} diff --git a/ai/common/templates/ansible/inv/group_vars/all/vars.yml b/ai/common/templates/ansible/inv/group_vars/all/vars.yml new file mode 100644 index 0000000..2738880 --- /dev/null +++ b/ai/common/templates/ansible/inv/group_vars/all/vars.yml @@ -0,0 +1,10 @@ +--- +## @brief Globale Variablen für alle Hosts +ansible_user: "" + +# Ablage für Templates/Dateien (aus .env.sh, Fallback relativ zum Repo) +ans_files: "{{ lookup('env', 'ANS_FILES') | default(playbook_dir ~ '/../files', true) }}" + +# Secret-Store (KV v2) – Werte aus .env.sh +ans_vault_kv_mount: "{{ lookup('env', 'ANS_VAULT_KV_MOUNT') | default('secret', true) }}" +ans_vault_kv_path: "{{ lookup('env', 'ANS_VAULT_KV_PATH') | default('', true) }}" diff --git a/ai/common/templates/ansible/inv/lab b/ai/common/templates/ansible/inv/lab new file mode 100644 index 0000000..606a46c --- /dev/null +++ b/ai/common/templates/ansible/inv/lab @@ -0,0 +1,3 @@ +## @file inv/lab – Inventar Testumgebung (Hosts aus ai/context/knowledge/systems/) +[example] + diff --git a/ai/common/templates/ansible/playbooks/example.yml b/ai/common/templates/ansible/playbooks/example.yml new file mode 100644 index 0000000..541c797 --- /dev/null +++ b/ai/common/templates/ansible/playbooks/example.yml @@ -0,0 +1,16 @@ +#------------------------------------------------------------------------------- +## @file example.yml +## @brief installieren + konfigurieren (Rolle example, Templates aus $ANS_FILES/example) +## @author +## @owner +## @version 0.1.0 (TT.MM.JJJJ) +## @usage ansible-playbook $ANS_PLAYDIR/example.yml [-e hostlist=host1,gruppe] +## Tags: install | config | verify Trockenlauf: --check --diff +## ohne Secret-Store: -e example_secrets_source=vars -e @ +## @requires source ansible/.env.sh und vault login +#------------------------------------------------------------------------------- +--- +- name: Example + hosts: "{{ hostlist | default('') }}" + roles: + - example diff --git a/ai/common/templates/ansible/playbooks/mini_check.yml b/ai/common/templates/ansible/playbooks/mini_check.yml new file mode 100644 index 0000000..a78e9e2 --- /dev/null +++ b/ai/common/templates/ansible/playbooks/mini_check.yml @@ -0,0 +1,28 @@ +#------------------------------------------------------------------------------- +## @file mini_check.yml +## @brief Erreichbarkeit prüfen und Kernfakten anzeigen; -e check_mode=setup sammelt Facts neu +## @author +## @owner +## @version 1.0.0 (07.10.2026) +## @usage ansible-playbook $ANS_PLAYDIR/mini_check.yml [-e hostlist=…] [-e check_mode=setup] +#------------------------------------------------------------------------------- +--- +- name: Mini-Check + hosts: "{{ hostlist | default('') }}" + gather_facts: false + tasks: + - name: Erreichbarkeit prüfen + ansible.builtin.ping: + + - name: Facts neu sammeln + ansible.builtin.setup: + when: (check_mode | default('')) == 'setup' + + - name: Facts aus Cache/Host laden + ansible.builtin.setup: + gather_subset: [min] + when: ansible_facts['distribution'] is not defined + + - name: Kernfakten anzeigen + ansible.builtin.debug: + msg: "{{ inventory_hostname }}: {{ ansible_facts['distribution'] }} {{ ansible_facts['distribution_version'] }}, Kernel {{ ansible_facts['kernel'] }}" diff --git a/ai/common/templates/ansible/roles/example/defaults/main.yml b/ai/common/templates/ansible/roles/example/defaults/main.yml new file mode 100644 index 0000000..3f1f55f --- /dev/null +++ b/ai/common/templates/ansible/roles/example/defaults/main.yml @@ -0,0 +1,10 @@ +--- +## @brief Defaults der Rolle example – alle Variablen mit Präfix example_ + +example_packages: [example] # zu installierende Pakete +example_service: example # systemd-Unit +example_config_file: /etc/example/example.conf # Ziel der eigenen Konfiguration +example_config_check_cmd: "example --check-config" # Prüfbefehl vor jedem Restart +example_smoke_test: true # verify.yml ausführen +example_secrets_source: vault # vault | vars (Test) +example_test_secrets: {} # nur mit secrets_source=vars, Datei außerhalb des Repos diff --git a/ai/common/templates/ansible/roles/example/handlers/main.yml b/ai/common/templates/ansible/roles/example/handlers/main.yml new file mode 100644 index 0000000..e767d43 --- /dev/null +++ b/ai/common/templates/ansible/roles/example/handlers/main.yml @@ -0,0 +1,13 @@ +--- +## @brief Handler-Kette: erst Konfiguration prüfen, dann neu starten + +- name: Konfiguration prüfen + ansible.builtin.command: "{{ example_config_check_cmd }}" + changed_when: false + listen: example config changed + +- name: Dienst neu starten + ansible.builtin.service: + name: "{{ example_service }}" + state: restarted + listen: example config changed diff --git a/ai/common/templates/ansible/roles/example/meta/main.yml b/ai/common/templates/ansible/roles/example/meta/main.yml new file mode 100644 index 0000000..acd342b --- /dev/null +++ b/ai/common/templates/ansible/roles/example/meta/main.yml @@ -0,0 +1,11 @@ +--- +galaxy_info: + role_name: example + author: "" + description: " installieren und konfigurieren" + license: proprietary + min_ansible_version: "2.15" + platforms: + - name: Debian + versions: [trixie] +dependencies: [] diff --git a/ai/common/templates/ansible/roles/example/tasks/config.yml b/ai/common/templates/ansible/roles/example/tasks/config.yml new file mode 100644 index 0000000..3ea4546 --- /dev/null +++ b/ai/common/templates/ansible/roles/example/tasks/config.yml @@ -0,0 +1,13 @@ +--- +## @brief Eigene Konfigurationsdateien schreiben (Templates aus files/example/) + +- name: Konfiguration schreiben + ansible.builtin.template: + src: "{{ ans_files }}/example/example.conf.j2" + dest: "{{ example_config_file }}" + owner: root + group: root + mode: "0640" + backup: true + diff: false # Template enthält Geheimnisse – nicht entfernen + notify: example config changed diff --git a/ai/common/templates/ansible/roles/example/tasks/install.yml b/ai/common/templates/ansible/roles/example/tasks/install.yml new file mode 100644 index 0000000..413aaf9 --- /dev/null +++ b/ai/common/templates/ansible/roles/example/tasks/install.yml @@ -0,0 +1,9 @@ +--- +## @brief Pakete installieren + +- name: Pakete installieren + ansible.builtin.apt: + name: "{{ example_packages }}" + state: present + update_cache: true + cache_valid_time: 3600 diff --git a/ai/common/templates/ansible/roles/example/tasks/main.yml b/ai/common/templates/ansible/roles/example/tasks/main.yml new file mode 100644 index 0000000..ddae127 --- /dev/null +++ b/ai/common/templates/ansible/roles/example/tasks/main.yml @@ -0,0 +1,39 @@ +--- +## @brief Rolle example – Ablauf: prüfen → Geheimnisse → installieren → konfigurieren → testen + +- name: Plattform prüfen (Debian >= 13 erwartet) + ansible.builtin.assert: + that: + - ansible_facts['os_family'] == 'Debian' + - ansible_facts['distribution_major_version'] | int >= 13 + fail_msg: "{{ inventory_hostname }}: {{ ansible_facts['distribution'] }} {{ ansible_facts['distribution_version'] }} – Rolle ist für Debian >= 13 gebaut." + quiet: true + tags: [always] + +- name: Geheimnisse laden + ansible.builtin.import_tasks: secrets.yml + tags: [always] + +- name: Installieren + ansible.builtin.import_tasks: install.yml + tags: [install] + +- name: Konfigurieren + ansible.builtin.import_tasks: config.yml + tags: [config] + +- name: Handler jetzt ausführen (Config-Check vor Restart) + ansible.builtin.meta: flush_handlers + tags: [always] + +- name: Dienst aktiv und gestartet + ansible.builtin.service: + name: "{{ example_service }}" + state: started + enabled: true + tags: [install, config] + +- name: Smoke-Test + ansible.builtin.import_tasks: verify.yml + when: example_smoke_test | bool + tags: [verify] diff --git a/ai/common/templates/ansible/roles/example/tasks/secrets.yml b/ai/common/templates/ansible/roles/example/tasks/secrets.yml new file mode 100644 index 0000000..5685ecc --- /dev/null +++ b/ai/common/templates/ansible/roles/example/tasks/secrets.yml @@ -0,0 +1,14 @@ +--- +## @brief Geheimnisse aus dem Secret-Store (Default) oder aus Testvariablen laden + +- name: Geheimnisse aus dem Secret-Store lesen + ansible.builtin.set_fact: + example_secrets: "{{ lookup('community.hashi_vault.vault_kv2_get', ans_vault_kv_path ~ '/example', engine_mount_point=ans_vault_kv_mount).secret }}" + when: example_secrets_source == 'vault' + no_log: true + +- name: Geheimnisse aus Testvariablen übernehmen + ansible.builtin.set_fact: + example_secrets: "{{ example_test_secrets }}" + when: example_secrets_source == 'vars' + no_log: true diff --git a/ai/common/templates/ansible/roles/example/tasks/verify.yml b/ai/common/templates/ansible/roles/example/tasks/verify.yml new file mode 100644 index 0000000..d11bdd7 --- /dev/null +++ b/ai/common/templates/ansible/roles/example/tasks/verify.yml @@ -0,0 +1,9 @@ +--- +## @brief Smoke-Test: Dienst läuft und antwortet + +- name: Dienststatus abfragen + ansible.builtin.command: systemctl is-active {{ example_service }} + register: example_active + check_mode: false + changed_when: false + failed_when: example_active.stdout != 'active' diff --git a/ai/common/templates/bash/.env.example b/ai/common/templates/bash/.env.example new file mode 100644 index 0000000..5f7c353 --- /dev/null +++ b/ai/common/templates/bash/.env.example @@ -0,0 +1,21 @@ +# .env – Einstellungen für die Scripts in diesem Verzeichnis +# Wird von b_init gesourct (Bash-Syntax). Echte .env nie ins Git – nur .env.example. +# Vorrang: Default < .env < Umgebung (export / VAR=… ./script.sh) < Kommandozeile +# Relative Pfade gelten relativ zum Script-Verzeichnis, nicht zum aufrufenden $PWD. + +## Logging --------------------------------------------------------------------- +B_LOG_LEVEL=info # debug | info | warn | error (-d setzt debug) +B_LOG_CONSOLE=1 # 0 = still (-q) +B_LOG_FILE= # z. B. "log/${SCRIPT_NAME%.sh}.log"; leer = keine Datei (-l DATEI) +B_LOG_FILE_LEVEL= # leer = wie B_LOG_LEVEL; z. B. debug für ausführliche Logdatei +B_LOG_COLOR=auto # auto | 1 | 0 + +## Optionen vorbelegen (überschreibt den Default aus der Definition) ------------ +#OPTION_DRY_RUN=1 + +## Zusätzliche Optionen für alle Scripts dieses Verzeichnisses – immer mit += --- +# Format: "kurz|lang|typ[:META]|default|Beschreibung" typ = flag | value +#B_OPTS+=( "k|keep|flag|0|Zwischendateien behalten" ) + +## Projektwerte – mit ${VAR:-default}, damit die Umgebung gewinnt --------------- +#DIR_BACKUP="${DIR_BACKUP:-/srv/backup}" diff --git a/ai/common/templates/bash/_functions.sh b/ai/common/templates/bash/_functions.sh new file mode 100644 index 0000000..247ce0a --- /dev/null +++ b/ai/common/templates/bash/_functions.sh @@ -0,0 +1,415 @@ +#!/usr/bin/env bash +#------------------------------------------------------------------------------- +## @file _functions.sh +## @brief Gemeinsame Bash-Basis: .env laden, Optionen, Logging, Usage, Trap +## @author +## @owner +## @version 1.0.0 (07.10.2026) +## +## @usage source "${SCRIPT_DIR}/_functions.sh"; B_OPTS+=(…); b_init "$@"; set -- "${ARGS[@]}" +## @note Nur sourcen, nicht ausführen. Benötigt bash >= 4.4. +## Kopien NICHT lokal ändern – Projektspezifisches gehört in +## _functions.local.sh (wird automatisch geladen, falls vorhanden). +## Standard: ai/common/bash.md +#------------------------------------------------------------------------------- + +if [[ "${BASH_SOURCE[0]}" == "$0" ]]; then + echo "_functions.sh wird gesourct, nicht ausgeführt." >&2 + exit 2 +fi +[[ -n "${_B_LOADED:-}" ]] && return 0 +if (( BASH_VERSINFO[0] < 4 || (BASH_VERSINFO[0] == 4 && BASH_VERSINFO[1] < 4) )); then + echo "FEHLER: bash >= 4.4 nötig (gefunden: ${BASH_VERSION})." >&2 + return 1 +fi +_B_LOADED=1 +B_FUNCTIONS_VERSION="1.0.0" + +# --- Pfade (pfadsicher, unabhängig vom aufrufenden $PWD) ---------------------- +# Das Script setzt SCRIPT_PATH/DIR/NAME selbst; hier nur Fallback. +: "${SCRIPT_PATH:=$(readlink -f -- "${BASH_SOURCE[1]:-$0}")}" +: "${SCRIPT_DIR:=${SCRIPT_PATH%/*}}" +: "${SCRIPT_NAME:=${SCRIPT_PATH##*/}}" +B_LIB_DIR="$(readlink -f -- "${BASH_SOURCE[0]}")" +B_LIB_DIR="${B_LIB_DIR%/*}" +B_CALL_DIR="${PWD}" # Verzeichnis, aus dem aufgerufen wurde +B_TMP_DIR="" # wird von b_tmpdir gesetzt + +# --- Standard-Optionen (gelten für jedes Script) ------------------------------ +# Format: "kurz|lang|typ[:META]|default|Beschreibung" typ = flag | value +# Variable im Script: OPTION_ (Großbuchstaben, '-' → '_'), z. B. OPTION_DRY_RUN +B_OPTS_STD=( + "h|help|flag|0|diese Hilfe anzeigen" + "d|debug|flag|0|ausführliche Ausgabe (Log-Level debug)" + "q|quiet|flag|0|keine Konsolenausgabe (Logdatei bleibt)" + "n|dry-run|flag|0|nur anzeigen, nichts ändern" + "l|logfile|value:DATEI||zusätzlich in DATEI loggen" +) +declare -p B_OPTS &>/dev/null || B_OPTS=() # Script/.env hängen mit B_OPTS+=(…) an +declare -ga ARGS=() # Positionsargumente nach b_init + +declare -grA _B_LEVELS=([debug]=10 [info]=20 [warn]=30 [error]=40) +declare -gA _B_OPT_SHORT=() _B_OPT_TYPE=() _B_OPT_META=() _B_OPT_DEFAULT=() _B_OPT_DESC=() +declare -ga _B_OPT_ORDER=() + +#=============================================================================== +# Ausgabe / Logging +#=============================================================================== + +## @brief Zentrale Ausgabe auf Konsole und/oder Logdatei, gefiltert nach Level +## @param $1 Level: debug | info | warn | error +## @param $@ Text +## @return 0 ok, 2 unbekanntes Level +blog() { + local level="${1:-}"; shift || true + local n="${_B_LEVELS[$level]:-}" msg="$*" c="" r="" + [[ -n "$n" ]] || { printf 'blog: unbekanntes Level "%s"\n' "$level" >&2; return 2; } + + if (( ${B_LOG_CONSOLE:-1} )) && (( n >= ${_B_LEVELS[${B_LOG_LEVEL:-info}]:-20} )); then + if [[ "${B_LOG_COLOR:-auto}" == 1 || ( "${B_LOG_COLOR:-auto}" == auto && -t 2 ) ]]; then + r=$'\e[0m' + case "$level" in debug) c=$'\e[2m' ;; warn) c=$'\e[33m' ;; error) c=$'\e[31m' ;; esac + fi + case "$level" in + info) printf '%s\n' "$msg" ;; + debug) printf '%s[DEBUG] %s%s\n' "$c" "$msg" "$r" >&2 ;; + warn) printf '%s[WARN] %s%s\n' "$c" "$msg" "$r" >&2 ;; + error) printf '%s[FEHLER] %s%s\n' "$c" "$msg" "$r" >&2 ;; + esac + fi + + if [[ -n "${B_LOG_FILE:-}" ]] \ + && (( n >= ${_B_LEVELS[${B_LOG_FILE_LEVEL:-${B_LOG_LEVEL:-info}}]:-20} )); then + printf '%(%Y-%m-%d %H:%M:%S)T %-5s %s[%d] %s\n' -1 "${level^^}" "$SCRIPT_NAME" "$$" "$msg" \ + >> "$B_LOG_FILE" 2>/dev/null || true + fi + return 0 +} + +## @brief Ersatz für echo: Info-Meldung über blog +## @param $@ Text +## @return 0 +becho() { blog info "$@"; } + +## @brief Debug-Meldung (nur bei -d bzw. B_LOG_LEVEL=debug) +## @param $@ Text +## @return 0 +bdebug() { blog debug "$@"; } + +## @brief Warnung (stderr) +## @param $@ Text +## @return 0 +bwarn() { blog warn "$@"; } + +## @brief Fehlermeldung (stderr) +## @param $@ Text +## @return 0 +berror() { blog error "$@"; } + +## @brief Fehlermeldung ausgeben und Script beenden (EXIT-Trap räumt auf) +## @param $1 Text +## @param $2 Exit-Code (Default 1; 2 = Aufruffehler, zeigt Hilfe-Hinweis) +## @return – (beendet das Script) +bdie() { + local rc="${2:-1}" + berror "${1:-Abbruch}" + (( rc == 2 )) && blog info "Hilfe: ${SCRIPT_NAME} --help" + exit "$rc" +} + +#=============================================================================== +# Hilfen für den Script-Alltag +#=============================================================================== + +## @brief Befehl ausführen – oder bei --dry-run nur anzeigen +## @param $@ Befehl mit Argumenten (kein String, kein eval) +## @return Exit-Code des Befehls, 0 im Dry-Run +brun() { + if (( ${OPTION_DRY_RUN:-0} )); then + becho "[dry-run] $(printf '%q ' "$@")" + return 0 + fi + bdebug "run: $(printf '%q ' "$@")" + "$@" +} + +## @brief Prüft, ob Programme vorhanden sind +## @param $@ Programmnamen +## @return 0 alle da, 1 mindestens eins fehlt (wird gemeldet) +b_require() { + local cmd rc=0 + for cmd in "$@"; do + command -v "$cmd" &>/dev/null || { berror "Programm fehlt: $cmd"; rc=1; } + done + return "$rc" +} + +## @brief Prüft, ob Variablen gesetzt und nicht leer sind +## @param $@ Variablennamen +## @return 0 alle gesetzt, 1 mindestens eine fehlt (wird gemeldet) +b_require_vars() { + local v rc=0 + for v in "$@"; do + [[ -n "${!v:-}" ]] || { berror "Variable nicht gesetzt: $v (in .env oder Umgebung)"; rc=1; } + done + return "$rc" +} + +## @brief Legt ein temporäres Verzeichnis an (B_TMP_DIR), wird beim Exit gelöscht +## @return 0 ok, 1 mktemp fehlgeschlagen +b_tmpdir() { + [[ -n "$B_TMP_DIR" ]] && return 0 + B_TMP_DIR="$(mktemp -d "${TMPDIR:-/tmp}/${SCRIPT_NAME%.sh}.XXXXXX")" || return 1 + bdebug "Temp-Verzeichnis: $B_TMP_DIR" +} + +## @brief Macht einen Pfad absolut; relative Pfade gelten relativ zu $2 +## @param $1 Pfad +## @param $2 Basisverzeichnis (Default: SCRIPT_DIR) +## @return 0; Ergebnis auf stdout +b_abspath() { + local p="$1" base="${2:-$SCRIPT_DIR}" + [[ "$p" == /* ]] || p="${base%/}/${p}" + printf '%s\n' "$p" +} + +#=============================================================================== +# Usage aus der Präambel +#=============================================================================== + +## @brief Gibt die Präambel des Scripts als Hilfe aus ('## ' wird entfernt, +## die Zeile '## @OPTIONS@' wird durch die Optionsliste ersetzt) +## @return 0 +b_usage() { + local line part=0 + while IFS= read -r line || [[ -n "$line" ]]; do + if [[ "$line" =~ ^#-{10,} ]]; then + (( ++part >= 2 )) && break + continue + fi + (( part == 1 )) && [[ "$line" == '##'* ]] || continue + line="${line#'##'}"; line="${line# }" + if [[ "$line" == *'@OPTIONS@'* ]]; then _b_opts_help; else printf '%s\n' "$line"; fi + done < "$SCRIPT_PATH" + return 0 +} + +## @brief Optionsliste für die Hilfe (aus B_OPTS_STD + B_OPTS) +## @return 0 +_b_opts_help() { + local long s left d + for long in "${_B_OPT_ORDER[@]}"; do + s="${_B_OPT_SHORT[$long]}" + left="${s:+-$s | }--${long}" + [[ "${_B_OPT_TYPE[$long]}" == value ]] && left+=" ${_B_OPT_META[$long]}" + d="${_B_OPT_DEFAULT[$long]}" + if [[ "${_B_OPT_TYPE[$long]}" == value && -n "$d" ]]; then + printf ' %-28s %s (Default: %s)\n' "$left" "${_B_OPT_DESC[$long]}" "$d" + else + printf ' %-28s %s\n' "$left" "${_B_OPT_DESC[$long]}" + fi + done + return 0 +} + +#=============================================================================== +# Optionen +#=============================================================================== + +## @brief Variablenname zu einer Langoption: dry-run → OPTION_DRY_RUN +## @param $1 Langname +## @return 0; Name auf stdout +_b_optvar() { local n="${1^^}"; printf 'OPTION_%s\n' "${n//-/_}"; } + +## @brief Baut die Optionstabellen aus B_OPTS_STD + B_OPTS und prüft sie +## @return 0 ok, 2 Definitionsfehler +_b_opts_build() { + local def s long type meta deflt desc rest + local -A seen_short=() + _B_OPT_ORDER=() + for def in "${B_OPTS_STD[@]}" "${B_OPTS[@]}"; do + IFS='|' read -r s long type deflt desc rest <<< "$def" + meta="${type#*:}"; type="${type%%:*}"; [[ "$meta" == "$type" ]] && meta="WERT" + if [[ -z "$long" || ! "$long" =~ ^[a-z][a-z0-9-]*$ || ! "$type" =~ ^(flag|value)$ || -n "$rest" ]]; then + berror "Ungültige Optionsdefinition: \"$def\" (kurz|lang|flag|value[:META]|default|Text)" + return 2 + fi + if [[ -n "$s" ]]; then + [[ -n "${seen_short[$s]:-}" && "${seen_short[$s]}" != "$long" ]] && { + berror "Kurzoption -$s doppelt vergeben (${seen_short[$s]}, $long)"; return 2; } + seen_short[$s]="$long" + fi + [[ -n "${_B_OPT_TYPE[$long]:-}" ]] || _B_OPT_ORDER+=("$long") + _B_OPT_SHORT[$long]="$s"; _B_OPT_TYPE[$long]="$type"; _B_OPT_META[$long]="$meta" + _B_OPT_DEFAULT[$long]="$deflt"; _B_OPT_DESC[$long]="$desc" + done + return 0 +} + +## @brief Setzt OPTION_ für eine erkannte Option +## @param $1 Langname +## @param $2 Wert +## @return 0 ok, 2 ungültiger Wert +_b_opt_set() { + local long="$1" val="$2" var + var="$(_b_optvar "$long")" + if [[ "${_B_OPT_TYPE[$long]}" == flag && ! "$val" =~ ^[01]$ ]]; then + berror "Option --$long erwartet 0 oder 1, nicht \"$val\"" + return 2 + fi + printf -v "$var" '%s' "$val" + _B_CLI_SET[$long]=1 + return 0 +} + +## @brief Parst die Kommandozeile → OPTION_* und ARGS (Positionsargumente) +## @param $@ Argumente des Scripts +## @return 0 ok, 2 Aufruffehler +_b_opts_parse() { + local arg name val s i long + ARGS=() + while (( $# )); do + arg="$1"; shift + case "$arg" in + --) ARGS+=("$@"); break ;; + --?*) + name="${arg#--}"; val="" + if [[ "$name" == *=* ]]; then val="${name#*=}"; name="${name%%=*}"; fi + [[ -n "${_B_OPT_TYPE[$name]:-}" ]] || { berror "Unbekannte Option: --$name"; return 2; } + if [[ "${_B_OPT_TYPE[$name]}" == value && "$arg" != *=* ]]; then + (( $# )) || { berror "Option --$name braucht einen Wert"; return 2; } + val="$1"; shift + elif [[ "${_B_OPT_TYPE[$name]}" == flag && "$arg" != *=* ]]; then + val=1 + fi + _b_opt_set "$name" "$val" || return 2 + ;; + -?*) + for (( i = 1; i < ${#arg}; i++ )); do + s="${arg:i:1}"; long="" + for name in "${_B_OPT_ORDER[@]}"; do + [[ "${_B_OPT_SHORT[$name]}" == "$s" ]] && { long="$name"; break; } + done + [[ -n "$long" ]] || { berror "Unbekannte Option: -$s"; return 2; } + if [[ "${_B_OPT_TYPE[$long]}" == value ]]; then + val="${arg:i+1}" # -lDATEI oder -l DATEI + if [[ -z "$val" ]]; then + (( $# )) || { berror "Option -$s braucht einen Wert"; return 2; } + val="$1"; shift + fi + _b_opt_set "$long" "$val" || return 2 + break + fi + _b_opt_set "$long" 1 || return 2 + done + ;; + *) ARGS+=("$arg") ;; + esac + done + return 0 +} + +#=============================================================================== +# Trap / Aufräumen +#=============================================================================== + +## @brief EXIT-Trap: ruft cleanup() des Scripts (falls definiert), löscht B_TMP_DIR +## @return – (beendet mit dem ursprünglichen Exit-Code) +_b_on_exit() { + local rc=$? + trap - EXIT + set +e + if declare -F cleanup &>/dev/null; then + cleanup "$rc" || bwarn "cleanup() meldete Fehler" + fi + [[ -n "$B_TMP_DIR" && -d "$B_TMP_DIR" ]] && rm -rf -- "$B_TMP_DIR" + bdebug "Ende ${SCRIPT_NAME} (rc=${rc})" + exit "$rc" +} + +## @brief ERR-Trap: meldet fehlgeschlagenen Befehl mit Funktion und Zeile +## (bewusstes 'return N' hat die Funktion schon selbst gemeldet → nur debug) +## @param $1 Zeilennummer +## @param $2 Befehl +## @return 0 (Abbruch übernimmt set -e) +_b_on_err() { + local rc=$? lvl=error + [[ "$2" == return* ]] && lvl=debug + blog "$lvl" "rc=${rc} in ${FUNCNAME[1]:-main}() Zeile ${1}: ${2}" + return 0 +} + +#=============================================================================== +# Einstieg +#=============================================================================== + +## @brief Initialisiert das Script: Trap, .env, Defaults, Optionen, Logging, Hilfe +## Vorrang: Default < .env < Umgebung (B_*, OPTION_*) < Kommandozeile +## @param $@ Argumente des Scripts (danach: set -- "${ARGS[@]}") +## @return 0 ok; beendet bei --help (0) oder Aufruffehler (2) +b_init() { + local v long var env_file + local -A env_keep=() + declare -gA _B_CLI_SET=() + + trap _b_on_exit EXIT + trap '_b_on_err "$LINENO" "$BASH_COMMAND"' ERR + trap 'exit 130' INT + trap 'exit 143' TERM + + # 1. exportierte Steuer-Variablen aus der Umgebung merken (gewinnen gegen .env) + for v in $(compgen -e); do + [[ "$v" =~ ^(B_[A-Z0-9_]+|OPTION_[A-Z0-9_]+)$ ]] && env_keep[$v]="${!v}" + done + + # 2. .env laden (Bash-Syntax; relativ zum Script, nicht zum $PWD) + env_file="$(b_abspath "${B_ENV_FILE:-.env}")" + if [[ -f "$env_file" ]]; then + # shellcheck source=/dev/null + source "$env_file" || bdie "Fehler beim Laden von $env_file" + fi + for v in "${!env_keep[@]}"; do printf -v "$v" '%s' "${env_keep[$v]}"; done + + # 3. Defaults für alles, was weder .env noch Umgebung gesetzt hat + : "${B_LOG_LEVEL:=info}" "${B_LOG_CONSOLE:=1}" "${B_LOG_FILE:=}" \ + "${B_LOG_FILE_LEVEL:=}" "${B_LOG_COLOR:=auto}" + [[ -n "${_B_LEVELS[$B_LOG_LEVEL]:-}" ]] || { bwarn "B_LOG_LEVEL=$B_LOG_LEVEL ungültig → info"; B_LOG_LEVEL=info; } + [[ -n "$B_LOG_FILE" ]] && B_LOG_FILE="$(b_abspath "$B_LOG_FILE" "$SCRIPT_DIR")" + + _b_opts_build || exit 2 + for long in "${_B_OPT_ORDER[@]}"; do + var="$(_b_optvar "$long")" + [[ -v "$var" ]] || printf -v "$var" '%s' "${_B_OPT_DEFAULT[$long]}" + done + + # 4. Kommandozeile + _b_opts_parse "$@" || bdie "Aufruffehler" 2 + + # 5. Standard-Optionen anwenden + (( OPTION_DEBUG )) && B_LOG_LEVEL=debug + (( OPTION_QUIET )) && B_LOG_CONSOLE=0 + if [[ -n "${_B_CLI_SET[logfile]:-}" ]]; then + B_LOG_FILE="$(b_abspath "$OPTION_LOGFILE" "$B_CALL_DIR")" # CLI: relativ zum $PWD + elif [[ -n "$OPTION_LOGFILE" ]]; then + B_LOG_FILE="$(b_abspath "$OPTION_LOGFILE" "$SCRIPT_DIR")" + fi + if [[ -n "$B_LOG_FILE" ]]; then + mkdir -p -- "${B_LOG_FILE%/*}" 2>/dev/null && touch -- "$B_LOG_FILE" 2>/dev/null \ + || { B_LOG_FILE=""; bwarn "Logdatei nicht beschreibbar – nur Konsole"; } + fi + + (( OPTION_HELP )) && { b_usage; exit 0; } + + bdebug "Start ${SCRIPT_NAME} (_functions ${B_FUNCTIONS_VERSION}) in ${SCRIPT_DIR}, Aufruf aus ${B_CALL_DIR}" + bdebug ".env: $( [[ -f "$env_file" ]] && echo "$env_file" || echo "keine" ) · Log: ${B_LOG_LEVEL}/${B_LOG_FILE:-ohne Datei}" + for long in "${_B_OPT_ORDER[@]}"; do + var="$(_b_optvar "$long")"; bdebug " ${var}=${!var}" + done + return 0 +} + +# --- projektspezifische Erweiterungen ----------------------------------------- +if [[ -f "${B_LIB_DIR}/_functions.local.sh" ]]; then + # shellcheck source=/dev/null + source "${B_LIB_DIR}/_functions.local.sh" +fi diff --git a/ai/common/templates/bash/script_template.sh b/ai/common/templates/bash/script_template.sh new file mode 100755 index 0000000..35498e5 --- /dev/null +++ b/ai/common/templates/bash/script_template.sh @@ -0,0 +1,58 @@ +#!/usr/bin/env bash +#------------------------------------------------------------------------------- +## @file script_template.sh +## @brief +## @author +## @owner +## @assist +## @version 0.1.0 (TT.MM.JJJJ) +## +## @usage script_template.sh [Optionen] +## @options +## @OPTIONS@ +## @env .env neben dem Script (Vorlage: .env.example) +## @exit 0 = ok · 1 = Fehler · 2 = Aufruffehler +#------------------------------------------------------------------------------- +set -Eeuo pipefail + +# --- Bootstrap (unverändert lassen) ------------------------------------------- +SCRIPT_PATH="$(readlink -f -- "${BASH_SOURCE[0]}")" +SCRIPT_DIR="${SCRIPT_PATH%/*}" +SCRIPT_NAME="${SCRIPT_PATH##*/}" +# shellcheck source=_functions.sh +source "${SCRIPT_DIR}/_functions.sh" || { echo "FEHLER: ${SCRIPT_DIR}/_functions.sh fehlt" >&2; exit 1; } + +# Script-eigene Optionen; -h -d -q -n -l kommen automatisch aus _functions.sh +B_OPTS+=( + "f|force|flag|0|vorhandene Ziele überschreiben" + "o|outdir|value:DIR|out|Zielverzeichnis (relativ = zum Script)" +) +b_init "$@" +set -- "${ARGS[@]}" + +#=============================================================================== +# Funktionen +#=============================================================================== + +## @brief Aufräumen beim Beenden (vom EXIT-Trap aufgerufen, auch bei Fehlern) +## @param $1 Exit-Code des Scripts +## @return 0 +cleanup() { + return 0 +} + +## @brief Hauptablauf +## @param $1 Ziel +## @return 0 ok, 1 Fehler +main() { + (( $# == 1 )) || bdie "Genau ein erwartet" 2 + local target="$1" outdir + outdir="$(b_abspath "$OPTION_OUTDIR")" + + becho "Ziel: ${target} → ${outdir}" + [[ -e "$outdir" && "$OPTION_FORCE" != 1 ]] && { berror "$outdir existiert (--force zum Überschreiben)"; return 1; } + brun mkdir -p -- "$outdir" + return 0 +} + +main "$@" diff --git a/ai/common/templates/docker/.env.example b/ai/common/templates/docker/.env.example new file mode 100644 index 0000000..5de01f8 --- /dev/null +++ b/ai/common/templates/docker/.env.example @@ -0,0 +1,23 @@ +# --- Stack --- +COMPOSE_PROJECT_NAME= # nur a-z, 0-9, _ – kein Bindestrich (GELF-Tag) +APP_TAG= + +# --- Logging (Graylog) --- +GELF_HOST= +GELF_PORT=12201 + +# --- Traefik (Defaults: traefik-net / le – nur bei Abweichung setzen) --- +TRAEFIK_ENABLE=false # erst auf true, wenn Stack sauber läuft (LE-Limits!) +APP_FQDN= +APP_INTERNAL_PORT= +#TRAEFIK_NETWORK=traefik-net +#TRAEFIK_CERTRESOLVER=le + +# --- Hosts --- +EXTRA_HOST_1=: + +# --- Pfade (Bind-Mounts, relativ zum Stack) --- +DIR_DATA=./data +DIR_CONFIG=./config + +# --- Image-spezifische Variablen (laut Orig-Doku, s. README) --- diff --git a/ai/common/templates/docker/README.md b/ai/common/templates/docker/README.md new file mode 100644 index 0000000..185e97f --- /dev/null +++ b/ai/common/templates/docker/README.md @@ -0,0 +1,29 @@ +# + +> @author · @owner · Stand TT.MM.JJJJ + +## 1. Zweck und Zugriff + · FQDN: `` · Port: <…> + +## 2. Original-Doku +| Quelle | URL | Abgerufen | +|---|---|---| +| Image | | TT.MM.JJJJ | +| Hersteller | | TT.MM.JJJJ | + +## 3. ENV-Variablen laut Original-Doku +| Variable | Bedeutung | Default | gesetzt in .env? | +|---|---|---|---| +| | | | | + +## 4. Volumes/Pfade +| Container-Pfad | Zweck | Bind-Mount (`.env`) | +|---|---|---| +| | | `DIR_DATA` | + +## 5. Ports und Healthcheck +- intern: <…>, extern: <… oder nur Traefik> +- Healthcheck: `` – Begründung: <…> + +## 6. Besonderheiten +- diff --git a/ai/common/templates/docker/docker-compose.yml b/ai/common/templates/docker/docker-compose.yml new file mode 100644 index 0000000..e41eccf --- /dev/null +++ b/ai/common/templates/docker/docker-compose.yml @@ -0,0 +1,48 @@ +#------------------------------------------------------------------------------- +## @file docker-compose.yml +## @brief +## @author +## @owner +## @version 0.1.0 (TT.MM.JJJJ) +## @usage docker compose up -d (im Stack-Verzeichnis, .env aus .env.example) +#------------------------------------------------------------------------------- +x-logging: &default-logging + driver: gelf + options: + gelf-address: "udp://${GELF_HOST}:${GELF_PORT}" + tag: "${COMPOSE_PROJECT_NAME}" + labels: "com.docker.compose.project,com.docker.compose.service" + +services: + app: + image: /:${APP_TAG} + container_name: ${COMPOSE_PROJECT_NAME} + restart: unless-stopped + env_file: .env + # ports: nur wenn Zugriff ohne Traefik nötig ist + # - "${APP_PORT}:" + extra_hosts: # nur wenn benötigt; ein Eintrag je Variable + - "${EXTRA_HOST_1}" + volumes: + - ${DIR_DATA}:/ + - ${DIR_CONFIG}:/:ro + networks: + - proxy + labels: # Traefik bleibt aus, bis TRAEFIK_ENABLE=true + - "traefik.enable=${TRAEFIK_ENABLE:-false}" + - "traefik.docker.network=${TRAEFIK_NETWORK:-traefik-net}" + - "traefik.http.routers.${COMPOSE_PROJECT_NAME}.rule=Host(`${APP_FQDN:-${COMPOSE_PROJECT_NAME}.invalid}`)" + - "traefik.http.routers.${COMPOSE_PROJECT_NAME}.tls.certresolver=${TRAEFIK_CERTRESOLVER:-le}" + - "traefik.http.services.${COMPOSE_PROJECT_NAME}.loadbalancer.server.port=${APP_INTERNAL_PORT:-80}" + logging: *default-logging + healthcheck: + test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://127.0.0.1:/"] + interval: 30s + timeout: 5s + retries: 3 + start_period: 30s + +networks: + proxy: + name: ${TRAEFIK_NETWORK:-traefik-net} + external: true diff --git a/ai/common/templates/markdown/adr.md b/ai/common/templates/markdown/adr.md new file mode 100644 index 0000000..0f110dd --- /dev/null +++ b/ai/common/templates/markdown/adr.md @@ -0,0 +1,35 @@ +--- +id: adr__ +title: +type: decision +status: draft # draft|active|superseded +updated: +owner: +tags: [] +related: [] +supersedes: +classification: intern +--- +# ADR : + +## Kontext + + +## Entscheidung + + +## Alternativen +| Option | Pro | Contra | Warum nicht | +|---|---|---|---| +| | | | | + +## Konsequenzen +- positiv: <…> +- negativ/Aufwand: <…> +- Folgearbeiten: <…> + +## Quellen +- (abgerufen TT.MM.JJJJ) + +## Änderungen +- TT.MM.JJJJ – vorgeschlagen () diff --git a/ai/common/templates/markdown/knowledge_entry.md b/ai/common/templates/markdown/knowledge_entry.md new file mode 100644 index 0000000..a8acaed --- /dev/null +++ b/ai/common/templates/markdown/knowledge_entry.md @@ -0,0 +1,34 @@ +--- +id: # = Dateiname ohne .md +title: +type: +status: draft # draft|active|deprecated|superseded +updated: +owner: +tags: [] +sources: [] +related: [] +classification: intern +--- +# + +> Kurzfassung in 1–2 Sätzen: Was ist das, wozu dient es? + +## Fakten +| Merkmal | Wert | +|---|---| +| | | + +## Details + + +## Betrieb +- Bereitstellung: +- Prüfung/Healthcheck: <…> +- Backup/Restore: <…> + +## Quellen +- (abgerufen TT.MM.JJJJ) + +## Änderungen +- TT.MM.JJJJ – angelegt () diff --git a/ai/context/README.md b/ai/context/README.md new file mode 100644 index 0000000..4587b05 --- /dev/null +++ b/ai/context/README.md @@ -0,0 +1,16 @@ +# ai/context/ – Kontext & Knowledge-DB + +> **Für wen, wo, womit.** Datenklassen beachten: hier höchstens „intern“ (`rules/privacy.md`). Geheimnisse nie. + +| Datei | Inhalt | Lesen | +|---|---|---| +| [responsible.md](responsible.md) | Verantwortliche – **Quelle für `@author`/`@owner`** in Headern | immer | +| [style.md](style.md) | Zielgruppe, Ansprache, Ton, Ausführlichkeit | immer | +| [environment.md](environment.md) | Infrastruktur-Überblick + zentrale Werte (Secret-Store, Logging, Domains) | immer | +| [knowledge/](knowledge/README.md) | Knowledge-DB: Systeme, Netz, Dienste, Entscheidungen, Anleitungen, Recherchen | gezielt über `INDEX.md` | + +Ziel und Stand des Projekts: [`../../context.md`](../../context.md). + +## Pflege +- Überblicksdateien (`*.md` hier) kurz halten (≤ 1 Bildschirmseite); Details gehören in die Knowledge-DB und werden verlinkt. +- Änderungen mit Datum (`> Stand: TT.MM.JJJJ`). Widersprüche zwischen Überblick und Knowledge: Knowledge gilt, Überblick korrigieren. diff --git a/ai/context/environment.md b/ai/context/environment.md new file mode 100644 index 0000000..d9cd581 --- /dev/null +++ b/ai/context/environment.md @@ -0,0 +1,33 @@ +# Umgebung – Überblick & zentrale Werte + +> Stand: 29.10.2026 · Klasse: intern · Details je System/Dienst in der Knowledge-DB (`knowledge/`). +> Werte hier sind die **Platzhalter-Auflösung** für die Standards in `ai/common/` (``, `` …). + +## Zentrale Werte +| Platzhalter | Bedeutung | Wert | Knowledge | +|---|---|---|---| +| `` | Secret-Store (HashiCorp Vault o. ä.) | | | +| `` (KV-Pfad) | Pfadpräfix im Secret-Store | | | +| `` / Port | zentrales Logging (Graylog, GELF/UDP) | `192.168.218.122` / `12201` | | +| `` | Basisverzeichnis Docker-Stacks | `/docker` (eigenes LV, btrfs, Label `DOCKER`) | | +| `TRAEFIK_NETWORK` / `TRAEFIK_CERTRESOLVER` | Reverse Proxy | | | +| `` | Default-Ziel für Ansible (Lab-VM) | | | +| `` | SSH-Benutzer für Ansible | `nafvoyr` (sudo) | | +| Domains intern/extern | DNS | | | + +## Topologie (Kurzfassung) +- **Standort A – Keller:** `kellerbeast` – GPU-Compute (Ollama, faster-whisper), TrueNAS ZFS. +- **Standort B – Cloud-RZ:** Docker-VMs (Compose, Ansible). +- **Verbindung:** NetBird-Mesh zwischen Standorten und Clients. + +## Werkzeuge +| Bereich | Werkzeug | Ort | Status | +|---|---|---|---| +| Git-Server | Gitea (self-hosted, **vertrauenswürdig**) – Remote dieses Repos: | eigener Server | in Betrieb | +| Secret-Store | HashiCorp Vault (Auto-Unseal) | `vp.chmln.de` | in Betrieb | +| Logging | Graylog (GELF) | `192.168.218.122:12201/udp` | in Betrieb | +| Reverse Proxy | Traefik | | in Betrieb | +| Monitoring | Checkmk | | | +| Backup | Borgmatic → ZFS / Proxmox Backup Server | `kellerbeast` | geplant | +| VPN | NetBird | Mesh | geplant/in Betrieb? | +| Messenger | Mattermost | | | diff --git a/ai/context/knowledge/INDEX.md b/ai/context/knowledge/INDEX.md new file mode 100644 index 0000000..70a90da --- /dev/null +++ b/ai/context/knowledge/INDEX.md @@ -0,0 +1,10 @@ +# Knowledge-Index + +> Automatisch erzeugt von `bin/kb_index.sh` – nicht von Hand bearbeiten. +> Format der Einträge: [README.md](README.md) + +## services + +| id | Titel | Status | Stand | Tags | +|---|---|---|---|---| +| sultan | [sultan – verteiltes Ingress-Routing für Traefik (Eigenentwicklung)](services/sultan.md) | active | 2026-10-23 | traefik, ingress, docker, netbird, checkmk, eigenentwicklung | diff --git a/ai/context/knowledge/README.md b/ai/context/knowledge/README.md new file mode 100644 index 0000000..3a80279 --- /dev/null +++ b/ai/context/knowledge/README.md @@ -0,0 +1,43 @@ +# Knowledge-DB (Markdown) + +> Dauerhaftes Wissen über Umgebung und Projekt – eine Datei pro Eintrag, Metadaten im YAML-Frontmatter, +> Übersicht in [`INDEX.md`](INDEX.md) (erzeugt von `bin/kb_index.sh`, nicht von Hand bearbeiten). + +## Ablage +| Ordner | `type` | Inhalt | Beispiel | +|---|---|---|---| +| `systems/` | `system` | Hosts, VMs, Geräte (Server, Switch, AP, Firewall) | `systems/host01.md` | +| `network/` | `network` | Netze, VLANs, IP-Pläne, DNS-Zonen, VPN, Routing | `network/ip_plan.md` | +| `services/` | `service` | Dienste/Anwendungen (DNS, RADIUS, Backup, Mail) | `services/dns_resolver.md` | +| `decisions/` | `decision` | ADRs – Entscheidungen mit Begründung | `decisions/adr_0001_secret_store.md` | +| `howto/` | `howto` | Anleitungen, Runbooks, wiederkehrende Handgriffe | `howto/restore_backup.md` | +| `research/` | `research` | Recherchen mit Quellen und Datum | `research/wifi_controller_vergleich.md` | +| `topics/` | `topic` | Lerninhalte je LPIC-Prüfungsthema (Objective-Nummer im `id`) | `topics/lpic1_103_1_command_line.md` | + +Dateiname = `id` + `.md` (Englisch/ASCII, klein, `_`). ADRs: `adr_NNNN_`. + +## Frontmatter (Schlüssel englisch, Werte in Amtssprache) +```yaml +--- +id: host01 # Pflicht, eindeutig, = Dateiname ohne .md +title: Virtualisierungshost 1 # Pflicht +type: system # Pflicht: system|network|service|decision|howto|research|topic +status: active # Pflicht: draft|active|deprecated|superseded +updated: 2026-10-07 # Pflicht, ISO +owner: br # Pflicht: Kürzel aus ../responsible.md +tags: [proxmox, server] # optional +sources: [] # optional: URLs (mit Abrufdatum im Text) +related: [dns_resolver] # optional: ids verwandter Einträge +supersedes: # optional (v. a. ADRs) +classification: intern # optional, Default intern; nie höher (rules/privacy.md) +--- +``` +Vorlage: [`../../common/templates/markdown/knowledge_entry.md`](../../common/templates/markdown/knowledge_entry.md), ADR: [`adr.md`](../../common/templates/markdown/adr.md). + +## Regeln für Agenten +- **Lesen:** zuerst `INDEX.md`, dann gezielt Einträge. Suche: `grep -rl "tags:.*dns" ai/context/knowledge/`. +- **Schreiben:** bestehenden Eintrag aktualisieren statt neuen anlegen; `updated` setzen; Quellen mit Datum. +- **Fakten statt Erzählung:** Tabellen (Adressen, Ports, Versionen), Abschnitt „Stand/Änderungen“ unten mit Datum. +- **Nach jeder Änderung:** `bin/kb_index.sh` (prüft Pflichtfelder, doppelte ids, aktualisiert `INDEX.md`). `--check` für reine Prüfung. +- Veraltetes nicht löschen: `status: deprecated`/`superseded`, Verweis auf Nachfolger. +- Keine Geheimnisse, keine Personennamen außer Kürzeln aus `responsible.md`. diff --git a/ai/context/knowledge/decisions/README.md b/ai/context/knowledge/decisions/README.md new file mode 100644 index 0000000..bb5a6aa --- /dev/null +++ b/ai/context/knowledge/decisions/README.md @@ -0,0 +1,4 @@ +# decisions/ + +Architekturentscheidungen als ADR (`type: decision`), Dateiname `adr_NNNN_.md`, fortlaufend nummeriert. +Vorlage: [../../../common/templates/markdown/adr.md](../../../common/templates/markdown/adr.md). Überholte ADRs bleiben (`status: superseded`). diff --git a/ai/context/knowledge/howto/README.md b/ai/context/knowledge/howto/README.md new file mode 100644 index 0000000..e9c47af --- /dev/null +++ b/ai/context/knowledge/howto/README.md @@ -0,0 +1,4 @@ +# howto/ + +Anleitungen und Runbooks (`type: howto`): Voraussetzungen → Schritte (kopierbare Befehle) → Prüfung → Rückweg. +Wiederkehrende Fragen des Bedieners landen hier. diff --git a/ai/context/knowledge/network/README.md b/ai/context/knowledge/network/README.md new file mode 100644 index 0000000..1d725c7 --- /dev/null +++ b/ai/context/knowledge/network/README.md @@ -0,0 +1,4 @@ +# network/ + +Netze, VLANs, IP-Pläne, DNS-Zonen, VPN, Routing (`type: network`). Format: [../README.md](../README.md). +IP-Pläne als Tabelle (Netz | VLAN | Zweck | Gateway | DHCP-Bereich). Diagramme als Mermaid/ASCII. diff --git a/ai/context/knowledge/research/README.md b/ai/context/knowledge/research/README.md new file mode 100644 index 0000000..e91f7ec --- /dev/null +++ b/ai/context/knowledge/research/README.md @@ -0,0 +1,4 @@ +# research/ + +Recherchen mit Bestand (`type: research`): Fragestellung, Ergebnis/Empfehlung, Quellen (URL + Abrufdatum), Einschätzung vs. Beleg getrennt. +Keine internen Daten in Suchanfragen (`rules/web_search.md`). diff --git a/ai/context/knowledge/services/README.md b/ai/context/knowledge/services/README.md new file mode 100644 index 0000000..b801953 --- /dev/null +++ b/ai/context/knowledge/services/README.md @@ -0,0 +1,4 @@ +# services/ + +Dienste und Anwendungen (`type: service`). Format: [../README.md](../README.md). +Mindestinhalt: Zweck, Host(s), Version, Bereitstellung (Ansible-Rolle / Docker-Stack), Ports, Abhängigkeiten, Secret-Store-Pfad (nur Pfad), Healthcheck, Logging, Backup/Restore, Original-Doku (URL + Datum). diff --git a/ai/context/knowledge/services/sultan.md b/ai/context/knowledge/services/sultan.md new file mode 100644 index 0000000..b2f8a61 --- /dev/null +++ b/ai/context/knowledge/services/sultan.md @@ -0,0 +1,49 @@ +--- +id: sultan +title: sultan – verteiltes Ingress-Routing für Traefik (Eigenentwicklung) +type: service +status: active +updated: 2026-10-23 +owner: br +tags: [traefik, ingress, docker, netbird, checkmk, eigenentwicklung] +sources: [https://git.everdc.eu/chmln-app-dev/sultan] +related: [] +classification: intern +--- +# sultan – verteiltes Ingress-Routing für Traefik + +> Eigenentwicklung des Betreibers (Go), Ersatz für HashiCorp Consul im eigenen Bedarf: Dienste deklarieren ihr Routing +> per Traefik-Labels, ein Agent meldet es an den sultan-Server, Traefik holt die Konfiguration per HTTP-Provider. + +## Fakten +| Merkmal | Wert | +|---|---| +| Repo | `ssh://git@git.everdc.eu:10022/chmln-app-dev/sultan.git`, lokal `~/git/sultan` | +| Image | `zot.chmln.de/sultan:` (distroless, nonroot UID 65532) – feste Version in `.env` | +| Rollen | `sultan server` (neben Traefik), `sultan agent` (je Docker-Host), `sultan accountant` (Inventar) | +| Agent → Server | TCP 8443, TLS, nur ausgehend | +| Traefik → Backend | HTTP auf den **veröffentlichten** Host-Port des Containers | +| Monitoring | Checkmk Local Checks (`/metrics/checkmk`) | + +## Details – was das für unsere Compose-Stacks heißt +| Punkt | Folge | +|---|---| +| Traefik läuft auf anderer VM | kein externes Netz `traefik-net` und kein Label `traefik.docker.network` auf dem Docker-Host | +| Port muss veröffentlicht sein | `ports: "${APP_PORT}:"` Pflicht für Web-Services; Bindung an `127.0.0.1` wird verworfen. Bindung an eine andere IP (z. B. NetBird-IP) wird akzeptiert (Code: `pkg/agent/workloads.go`, `reachablePorts`). Festlegung je Host in dessen Knowledge-Eintrag | +| Port-Label | `loadbalancer.server.port` = Port **im Container**; der Agent rechnet auf den Host-Port um | +| Entrypoints/Certresolver | Labels müssen Namen aus der Traefik-Konfiguration nutzen (`websecure`, `le`); sultan prüft das nicht | +| Domains | nur Domains, die dem Mandanten im sultan-Server erlaubt sind (sonst „Policy-Verstoß“ im Server-Log) | +| Namen | IDs: `a-z0-9`, `-`, `_`; **`__` ist reserviert** (Trenner `____`) – unser `_`-Schema passt, doppelter Unterstrich verboten | +| Backend-Adresse | Agent meldet die Host-IP, über die er den Server erreicht; mit `SULTAN_BACKEND_INTERFACE=wt0` die NetBird-IP | +| Wartung | gestoppter Container → Wartungsseite; **entfernter** Container (`down`) → 404 (siehe `docs/wartung.md` im Repo) | + +## Betrieb +- Bereitstellung Agent: Compose mit `network_mode: host`, Docker-Socket `:ro`, `group_add: DOCKER_GID`, `suldata/` (enthält `node.key`) persistent und im Backup +- Einladung: `sultan token invite --tenant=…` auf dem Server, als `SULTAN_TOKEN` in die `.env` des Agents (nur beim ersten Start) +- Prüfung: Agent-Log „Mit Server verbunden / Lease erhalten“; vom Ingress `curl -sI http://: -H 'Host: …'` + +## Quellen +- `~/git/sultan/README.md`, `docs/community.md`, `deploy/compose/agent/` (gelesen 23.10.2026, Commit `dd93277`) + +## Änderungen +- 23.10.2026 – angelegt (br) diff --git a/ai/context/knowledge/systems/README.md b/ai/context/knowledge/systems/README.md new file mode 100644 index 0000000..dafb1e9 --- /dev/null +++ b/ai/context/knowledge/systems/README.md @@ -0,0 +1,4 @@ +# systems/ + +Hosts, VMs und Geräte – ein Eintrag pro System (`type: system`). Format: [../README.md](../README.md). +Mindestinhalt: Rolle, Standort (grob), Hardware/VM, OS + Version, Adressen, Dienste darauf, Zugriff (ohne Geheimnisse), Verantwortlich, Backup. diff --git a/ai/context/knowledge/topics/README.md b/ai/context/knowledge/topics/README.md new file mode 100644 index 0000000..06ef215 --- /dev/null +++ b/ai/context/knowledge/topics/README.md @@ -0,0 +1,5 @@ +# topics/ + +Lerninhalte je LPIC-Prüfungsthema (`type: topic`), ein Eintrag pro Objective. +`id` enthält Stufe und Objective-Nummer, z. B. `lpic1_103_1_command_line` für 103.1. +Aufbau: Lernziel (Wortlaut/Version der Objectives mit Abrufdatum) → Kernbefehle und Dateien → Übungen → typische Fallen. diff --git a/ai/context/responsible.md b/ai/context/responsible.md new file mode 100644 index 0000000..d3bc876 --- /dev/null +++ b/ai/context/responsible.md @@ -0,0 +1,17 @@ +# Verantwortliche + +> Stand: 23.10.2026 · **Quelle für die Header-Felder** `@author` / `@owner` (`rules/headers.md`). +> Nur hier stehen Personennamen. Weitere Personen nur als Rolle (`rules/privacy.md`). + +| Kürzel | Header-String (`Name `) | Rolle | Header-Feld | Zuständig für | +|---|---|---|---|---| +| br | Björn Runden | Bediener, Autor und Verantwortlicher | `@author` (Standard) | Scripts, Ansible, Doku | +| br | Björn Runden | Bediener, Autor und Verantwortlicher | `@owner` (Standard) | fachliche Freigabe | + +## Regeln für Agenten +- **Standard:** `@author` = Eintrag mit „`@author` (Standard)“, `@owner` = Eintrag mit „`@owner` (Standard)“. +- Weitere `@author` (Team): nur wer an der Datei tatsächlich mitarbeitet; je Person eine `@author`-Zeile, Hauptautor zuerst. +- Header-String exakt übernehmen (`Name `), nichts kürzen oder ändern. +- Abweichende Zuständigkeit für einzelne Bereiche: in `context.md` → Metadaten festlegen; das gilt dann vorrangig. +- Tabelle leer/Platzhalter → Platzhalter ``/`` im Header lassen und den Bediener darauf hinweisen. Keine Namen erfinden. +- KI-Beteiligung: `@assist KI-unterstützt (), geprüft von `. diff --git a/ai/context/style.md b/ai/context/style.md new file mode 100644 index 0000000..ebe355e --- /dev/null +++ b/ai/context/style.md @@ -0,0 +1,18 @@ +# Stil & Ansprache + +> Stand: 23.10.2026 + +## Zielgruppe +Der Betreiber (`br`) selbst: knapp, technisch, Empfehlung statt Optionsliste; Markdown und Code. + +## Standard +- Amtssprache laut `AGENTS.md`; direkt, knapp, sachlich. Empfehlung statt Optionsliste. +- Ansprache des Betreibers: du. +- Quellen nennen, Unsicheres kennzeichnen. + +## Ausführlichkeit +| Stufe | Wann | +|---|---| +| kurz | Rückfragen, Statusmeldungen, Chat | +| normal | Lösungsbeschreibung, Projektdoku | +| ausführlich | Konzepte, Entscheidungsvorlagen, Anleitungen | diff --git a/ai/inbox/README.md b/ai/inbox/README.md new file mode 100644 index 0000000..1b08b3d --- /dev/null +++ b/ai/inbox/README.md @@ -0,0 +1,13 @@ +# ai/inbox/ – Eingang für Markdown-Dateien + +> Temporäre `.md`-Dateien, die noch eingeordnet werden müssen. + +## Ablauf +1. Betreiber legt die Datei hier ab. +2. Agent liest sie, ordnet den Inhalt ein (`context.md`, `architecture.md`, Knowledge-DB, `ai/context/*.md`) und vermerkt das in `context.md` → Sitzungsprotokoll. +3. Agent committet das Original vorher (Historie), dann Verschieben nach `done/.md.orig`. + `*.orig` ist in `.gitignore` → in `done/` nur lokal, das Original bleibt im Git-Verlauf. + +| Datei | Inhalt | +|---|---| +| `done/` | eingeordnete Originale (`*.orig`, lokal) | diff --git a/ai/rules/README.md b/ai/rules/README.md new file mode 100644 index 0000000..8960cf2 --- /dev/null +++ b/ai/rules/README.md @@ -0,0 +1,26 @@ +# ai/rules/ – verbindliche Regeln + +> Regeln haben **Vorrang** vor allem anderen in dieser Basis (außer den Sicherheits-/Datenschutzgrenzen des Agenten selbst). +> Eine Regel pro Datei. Ändern nur durch den Bediener oder auf seine ausdrückliche Anweisung. + +| Datei | Regel in einem Satz | +|---|---| +| [language.md](language.md) | Alles in der Amtssprache aus `AGENTS.md`; Struktur sprachneutral englisch. | +| [privacy.md](privacy.md) | Keine identifizierenden Personen-/Umgebungsdaten an externe Dienste; Daten nach Klasse behandeln. | +| [secrets.md](secrets.md) | Geheimnisse nie in Dateien/Git/Chat; Secret-Store zuerst, Ansible Vault als Fallback. | +| [headers.md](headers.md) | Jede Code-Datei trägt einen Header mit namentlich Verantwortlichen. | +| [testing.md](testing.md) | Lokal testen; Cloud-VMs und Produktion nur mit ausdrücklicher Freigabe. | +| [file_safety.md](file_safety.md) | Vor Löschen/Überschreiben ansehen, `*.orig` statt Löschen. | +| [web_search.md](web_search.md) | Web-Recherche erlaubt, aber ohne interne Daten; Quellen mit Datum. | + +## Aufbau einer Regeldatei +```markdown +# Regel: + +> Stand: TT.MM.JJJJ · gilt für: alle Agenten (bis auf Widerruf) + +## Erlaubt / Pflicht +## Verboten / Einschränkungen +## Ausnahmen (wer darf freigeben, wie wird es dokumentiert) +``` +Ausnahmen, die der Bediener erteilt, in `context.md` mit Datum festhalten – sie gelten nur für genau diesen Fall. diff --git a/ai/rules/file_safety.md b/ai/rules/file_safety.md new file mode 100644 index 0000000..2f71007 --- /dev/null +++ b/ai/rules/file_safety.md @@ -0,0 +1,9 @@ +# Regel: Löschen und Überschreiben + +> Stand: 07.10.2026 · gilt für: alle Agenten + +- Vor dem Löschen/Überschreiben: **Ziel ansehen** (Inhalt, Größe, Änderungsdatum) und im Zweifel nachfragen. +- Statt Löschen: Sicherung `.orig` (bei Kollision `.orig.JJJJMMTT`). `*.orig` steht in `.gitignore`. +- Massenoperationen (`rm -r`, `find -delete`, `rsync --delete`, `git clean`, `git reset --hard`) nur mit Freigabe und vorherigem Trockenlauf. +- Fremde Dateien (nicht in dieser Basis/diesem Projekt entstanden) nicht ohne Auftrag umbauen oder „aufräumen“. +- Ansible: `backup: true` bei Templates auf Zielsystemen, sofern die Datei keine Geheimnisse enthält (sonst Kopien mit Geheimnissen → bewusst entscheiden). diff --git a/ai/rules/headers.md b/ai/rules/headers.md new file mode 100644 index 0000000..13ad2c1 --- /dev/null +++ b/ai/rules/headers.md @@ -0,0 +1,35 @@ +# Regel: Header mit Verantwortlichen + +> Stand: 08.10.2026 · gilt für: jede Code-, Konfigurations- und Template-Datei, die in diesem Repo entsteht + +## Pflicht +Jede Datei beginnt (nach Shebang bzw. `---`) mit einem Header im **bashdoc-Stil** (`## @tag`), eingerahmt von `#-----`-Linien: + +| Tag | Pflicht | Inhalt | +|---|---|---| +| `@file` | ja | Dateiname | +| `@brief` | ja | Zweck in einem Satz | +| `@author` | ja | `Name ` des verantwortlichen Menschen (aus `ai/context/responsible.md`) – nie die KI. Mehrere Autoren: Zeile `@author` wiederholen, Hauptautor zuerst | +| `@owner` | ja | `Name ` oder Rolle des fachlich Verantwortlichen (aus `responsible.md`) | +| `@assist` | wenn KI beteiligt | `KI-unterstützt (), geprüft von ` | +| `@version` | ja | SemVer + Datum `x.y.z (TT.MM.JJJJ)` | +| `@usage` | Scripts/Playbooks | Aufruf; Optionen bei Bash über `## @OPTIONS@` (generiert) | + +- Agenten tragen Namen und Mail exakt wie in `responsible.md` (Spalte „Header-String“) ein. Fehlt dort ein Eintrag: Platzhalter `` stehen lassen und den Bediener darauf hinweisen – **keine Namen erfinden**. +- Bei inhaltlicher Änderung: `@version` erhöhen, Datum aktualisieren; `@author` nur ändern, wenn die Verantwortung wechselt. +- Kommentar-Präfix je Sprache: Bash/YAML/Python `##`, Jinja `{#- … -#}`, SQL `--`. Format der Tags bleibt gleich. + +## Vorlagen +`ai/common/templates/` – Bash (`bash/script_template.sh`), Ansible (`ansible/playbooks/example.yml`), Docker (`docker/docker-compose.yml`). + +## Beispiel (mehrere Autoren) +```bash +## @author Erika Muster +## @author Max Beispiel +## @owner Clara Fach +``` + +## Änderungsprotokoll +- 07.10.2026 – angelegt. +- 08.10.2026 – `Name `-Format, mehrere `@author` erlaubt. +- 23.10.2026 – Mandantenbezug entfernt (Einzelprojekt). diff --git a/ai/rules/language.md b/ai/rules/language.md new file mode 100644 index 0000000..b417e58 --- /dev/null +++ b/ai/rules/language.md @@ -0,0 +1,17 @@ +# Regel: Amtssprache + +> Stand: 07.10.2026 · gilt für: alle Agenten, alle Ausgaben + +## Pflicht +- Die **Amtssprache** steht in `AGENTS.md` → „Metadaten“ (Skeleton: `de`). Sie gilt für: + Antworten, Markdown-Dokumente, Code-Kommentare, `@brief`-Texte, Log-/Konsolenmeldungen, Ansible-Task-Namen, Commit-Messages. +- Fachbegriffe, Produktnamen und Code-Bezeichner bleiben im Original. +- Bei `de`: korrekte Orthografie mit Umlauten und ß – nie `ae`/`oe`/`ue`/`ss` als Ersatz (außer in Bezeichnern, s. u.). + +## Sprachneutral (immer Englisch, ASCII) +- Datei- und Ordnernamen, Variablen, Funktionen, Rollen, Tags, YAML-/Frontmatter-Schlüssel. +- Grund: Übernahmen aus/in skel-ai mit anderer Amtssprache behalten Struktur, Verweise und Scripts. + +## Datum & Zahlen +- Fließtext: TT.MM.JJJJ (bei `de`), immer absolut – nie „gestern“, „nächste Woche“. +- Maschinenlesbar (Frontmatter, Dateinamen, Logs): ISO 8601 `JJJJ-MM-TT`. diff --git a/ai/rules/privacy.md b/ai/rules/privacy.md new file mode 100644 index 0000000..ac30c2c --- /dev/null +++ b/ai/rules/privacy.md @@ -0,0 +1,38 @@ +# Regel: Datenschutz + +> Stand: 23.10.2026 · gilt für: alle Agenten · **Priorität 1** + +## Grundsatz +Nichts, was die Basis verlässt (Web-Suche, externe APIs, Cloud-KI außerhalb der Freigabe, Pastebins, Issue-Tracker Dritter), +darf auf den Betreiber, seine Umgebung oder Dritte (Kontakte, Kunden) rückführbar sein. + +## Datenklassen +| Klasse | Beispiele | In der AI-Basis ablegen? | An externe Dienste? | +|---|---|---|---| +| öffentlich | Herstellerdoku, Paketversionen, RFCs | ja | ja | +| intern | Hostnamen, IP-Pläne, Topologie, Dienste, Rollen | ja (Knowledge-DB, Projekte) | **nein** (nicht in Suchen/URLs) | +| vertraulich | Personennamen über die Verantwortlichen hinaus, Kundendaten, Verträge, Logs mit Personenbezug | nein – nur Verweis auf Ablageort | nein | +| geheim | Passwörter, Schlüssel, Tokens, Zertifikats-Privatschlüssel | **nie** (→ `secrets.md`) | nie | + +## Pflicht +- Namentlich genannt werden **nur** die Verantwortlichen aus `ai/context/responsible.md` (Header-Pflicht, `headers.md`). +- Weitere Personen nur als Rolle („Leitung Buchhaltung“, „Azubi IT“), nie mit Namen/Kontaktdaten. +- Beispiele, Testdaten, Doku-Snippets: pseudonymisiert (`host01`, `192.0.2.0/24`, `example.org`, `user_a`). +- Logs/Dumps aus Kundensystemen vor dem Einfügen in einen Chat bereinigen (Namen, Mailadressen, IPs öffentlicher Netze). + +## Leitsatz: so lokal wie möglich, so auswärts wie nötig +Verarbeitung (KI, Speicherung, Dienste) zuerst auf eigener Infrastruktur. Ein externer Dienst nur, wenn lokal nicht +sinnvoll machbar – und nur bis zur unten freigegebenen Klasse. + +## Freigegebene KI-Dienste +| Dienst | Erlaubt für Klasse bis | Grundlage (z. B. AVV, Self-Hosting) | Stand | +|---|---|---|---| +| Lokale Modelle (Ollama, STT auf `kellerbeast`) | vertraulich (inkl. persönlicher Inhalte) | Self-Hosting | 23.10.2026 | +| Claude Code (Anthropic) | intern (Lerninhalte, Lab-Konfiguration, Code, Doku – **keine** persönlichen Inhalte) | Entscheidung Betreiber | 23.10.2026 | + +## Vertrauenswürdige Ablagen +| Ablage | Erlaubt für Klasse bis | Grundlage | Stand | +|---|---|---|---| +| Gitea (self-hosted, Remote dieses Repos) | vertraulich | Self-Hosting | 23.10.2026 | + +> Ohne Eintrag gilt: nur Klasse „öffentlich“ darf an externe KI-Dienste. Rechtliche Bewertung (DSGVO, AVV) liegt beim Betreiber – der Agent gibt keine Rechtsberatung. diff --git a/ai/rules/secrets.md b/ai/rules/secrets.md new file mode 100644 index 0000000..00752e9 --- /dev/null +++ b/ai/rules/secrets.md @@ -0,0 +1,12 @@ +# Regel: Geheimnisse + +> Stand: 07.10.2026 · gilt für: alle Agenten, alle Projekte + +1. **Nie** Passwörter, Shared Secrets, private Schlüssel oder Tokens in Dateien der Basis, ins Git, in Chats oder Logs – auch nicht in Beispielen mit echten Werten. +2. **Quelle Nr. 1: Secret-Store** (Platzhalter: HashiCorp Vault ``, KV v2, Mount `secret`, Pfad `//…`). Konkrete Werte in `ai/context/environment.md`. +3. **Fallback: Ansible Vault.** Passwortdatei **außerhalb** des Repos (`~/.ans-secret-`), Variablennamen beginnen mit `vault_`. +4. `.env`-Dateien: nie im Git, nur `.env.example` mit gleichen Schlüsseln und Platzhaltern. Secrets in `.env` nur, wenn das Werkzeug es erzwingt (z. B. Docker-Stack). +5. **Ansible:** Tasks mit Geheimnissen `no_log: true`, Templates mit Geheimnissen `diff: false` – sonst landen Werte in Terminal und `ansible.log`. +6. **Bash:** Geheimnisse nie als Kommandozeilenargument (sichtbar in `ps`), nie mit `becho`/`bdebug` ausgeben; per Datei-Deskriptor, stdin oder Umgebungsvariable übergeben. +7. Geheimnisse **erzeugen**, nicht ausdenken: `openssl rand -hex 24` direkt im Schreibbefehl (`vault kv put … key="$(openssl rand -hex 24)"`). Bei KV v2 `patch` statt `put`, wenn nur ein Schlüssel geändert wird. +8. **Agenten** lesen keine `.env`/Vault-Inhalte aus und geben keine Geheimnis-Werte aus – nur Schlüsselnamen. Benötigt ein Test ein Geheimnis, fragt der Agent den Bediener, ihn selbst auszuführen. diff --git a/ai/rules/testing.md b/ai/rules/testing.md new file mode 100644 index 0000000..796fcd8 --- /dev/null +++ b/ai/rules/testing.md @@ -0,0 +1,17 @@ +# Regel: Testen + +> Stand: 07.10.2026 · gilt für: alle Agenten + +## Pflicht +- **Lokal testen:** Syntax-/Lint-Checks (`bash -n`, `shellcheck`, `ansible-lint`, `yamllint`, `docker compose config`) und Läufe in lokalen Containern/VMs (z. B. `debian:` per Docker/Podman). +- **Trockenlauf zuerst:** Bash `-n`, Ansible `--check --diff`, dann echter Lauf. +- **Idempotenz** prüfen: zweiter Lauf ohne Änderungen (`changed=0`). +- Ergebnis des Tests (Datum, was, wogegen, Ergebnis) in `context.md` festhalten. + +## Verboten ohne ausdrückliche Freigabe des Bedieners für genau diesen Fall +- Code in **Cloud-VMs/Sandboxes Dritter** ausführen oder testen. +- Läufe gegen **Produktivsysteme** (auch `--check` nicht, wenn Zugangsdaten oder Last im Spiel sind). +- Änderungen an Netzwerkgeräten, die die Verbindung des Agenten/Bedieners kappen können (Firewall, Routing, SSH) – immer mit Rückfallweg (z. B. zeitgesteuertes Rollback) und Freigabe. + +## Ausnahmen +Freigabe im Chat genügt; der Agent vermerkt sie mit Datum in `context.md` („Freigabe: Lauf gegen host01, 07.10.2026“). diff --git a/ai/rules/web_search.md b/ai/rules/web_search.md new file mode 100644 index 0000000..458c325 --- /dev/null +++ b/ai/rules/web_search.md @@ -0,0 +1,16 @@ +# Regel: Web-Recherche + +> Stand: 07.10.2026 · gilt für: alle Agenten mit Web-Zugriff + +## Erlaubt +- Herstellerdoku, Paketversionen, Kompatibilität, Sicherheitshinweise (CVE), Best Practices nachschlagen. +- Vor jedem neuen Docker-Stack oder jeder neuen Ansible-Rolle: Original-Doku holen (Pflicht laut `ai/common/docker.md` / `ansible.md`). + +## Einschränkungen +- **Keine internen Daten** in Suchanfragen oder URLs: keine IPs, Hostnamen, MACs, Domains, Projekt-/Firmennamen, Personennamen (→ `privacy.md`). +- Keine Inhalte aus lokalen Dateien/Code an Web-Dienste übergeben (keine Pastebins, keine Online-Validatoren mit internem Inhalt). +- Ergebnisse mit **Quelle (URL) und Abrufdatum** ablegen; Unbelegtes als Einschätzung kennzeichnen. +- Dauerhaft wertvolle Recherche → Knowledge-DB (`context/knowledge/research/`). + +## Gesperrte Seiten +- (derzeit keine) diff --git a/bin/.env.example b/bin/.env.example new file mode 100644 index 0000000..5f7c353 --- /dev/null +++ b/bin/.env.example @@ -0,0 +1,21 @@ +# .env – Einstellungen für die Scripts in diesem Verzeichnis +# Wird von b_init gesourct (Bash-Syntax). Echte .env nie ins Git – nur .env.example. +# Vorrang: Default < .env < Umgebung (export / VAR=… ./script.sh) < Kommandozeile +# Relative Pfade gelten relativ zum Script-Verzeichnis, nicht zum aufrufenden $PWD. + +## Logging --------------------------------------------------------------------- +B_LOG_LEVEL=info # debug | info | warn | error (-d setzt debug) +B_LOG_CONSOLE=1 # 0 = still (-q) +B_LOG_FILE= # z. B. "log/${SCRIPT_NAME%.sh}.log"; leer = keine Datei (-l DATEI) +B_LOG_FILE_LEVEL= # leer = wie B_LOG_LEVEL; z. B. debug für ausführliche Logdatei +B_LOG_COLOR=auto # auto | 1 | 0 + +## Optionen vorbelegen (überschreibt den Default aus der Definition) ------------ +#OPTION_DRY_RUN=1 + +## Zusätzliche Optionen für alle Scripts dieses Verzeichnisses – immer mit += --- +# Format: "kurz|lang|typ[:META]|default|Beschreibung" typ = flag | value +#B_OPTS+=( "k|keep|flag|0|Zwischendateien behalten" ) + +## Projektwerte – mit ${VAR:-default}, damit die Umgebung gewinnt --------------- +#DIR_BACKUP="${DIR_BACKUP:-/srv/backup}" diff --git a/bin/README.md b/bin/README.md new file mode 100644 index 0000000..c74dc59 --- /dev/null +++ b/bin/README.md @@ -0,0 +1,11 @@ +# bin/ – Werkzeuge der AI-Basis + +> Selbst nach [`ai/common/bash.md`](../ai/common/bash.md) gebaut – zugleich Referenzbeispiele für den Standard. +> Alle: `-h` Hilfe · `-n` Trockenlauf · `-d` Debug · `-q` still · `-l DATEI` Logdatei. + +| Script | Zweck | +|---|---| +| `kb_index.sh [--check]` | Knowledge-`INDEX.md` erzeugen, Pflichtfelder/doppelte ids prüfen | +| `bashdoc.sh … [-o datei] [-p]` | Markdown-Doku aus `##`-Kommentaren | +| `_functions.sh` | gemeinsame Basis – **Kopie** von `ai/common/templates/bash/_functions.sh`, nicht lokal ändern | +| `.env.example` | Vorlage für lokale Einstellungen (`cp .env.example .env`) | diff --git a/bin/_functions.sh b/bin/_functions.sh new file mode 100644 index 0000000..247ce0a --- /dev/null +++ b/bin/_functions.sh @@ -0,0 +1,415 @@ +#!/usr/bin/env bash +#------------------------------------------------------------------------------- +## @file _functions.sh +## @brief Gemeinsame Bash-Basis: .env laden, Optionen, Logging, Usage, Trap +## @author +## @owner +## @version 1.0.0 (07.10.2026) +## +## @usage source "${SCRIPT_DIR}/_functions.sh"; B_OPTS+=(…); b_init "$@"; set -- "${ARGS[@]}" +## @note Nur sourcen, nicht ausführen. Benötigt bash >= 4.4. +## Kopien NICHT lokal ändern – Projektspezifisches gehört in +## _functions.local.sh (wird automatisch geladen, falls vorhanden). +## Standard: ai/common/bash.md +#------------------------------------------------------------------------------- + +if [[ "${BASH_SOURCE[0]}" == "$0" ]]; then + echo "_functions.sh wird gesourct, nicht ausgeführt." >&2 + exit 2 +fi +[[ -n "${_B_LOADED:-}" ]] && return 0 +if (( BASH_VERSINFO[0] < 4 || (BASH_VERSINFO[0] == 4 && BASH_VERSINFO[1] < 4) )); then + echo "FEHLER: bash >= 4.4 nötig (gefunden: ${BASH_VERSION})." >&2 + return 1 +fi +_B_LOADED=1 +B_FUNCTIONS_VERSION="1.0.0" + +# --- Pfade (pfadsicher, unabhängig vom aufrufenden $PWD) ---------------------- +# Das Script setzt SCRIPT_PATH/DIR/NAME selbst; hier nur Fallback. +: "${SCRIPT_PATH:=$(readlink -f -- "${BASH_SOURCE[1]:-$0}")}" +: "${SCRIPT_DIR:=${SCRIPT_PATH%/*}}" +: "${SCRIPT_NAME:=${SCRIPT_PATH##*/}}" +B_LIB_DIR="$(readlink -f -- "${BASH_SOURCE[0]}")" +B_LIB_DIR="${B_LIB_DIR%/*}" +B_CALL_DIR="${PWD}" # Verzeichnis, aus dem aufgerufen wurde +B_TMP_DIR="" # wird von b_tmpdir gesetzt + +# --- Standard-Optionen (gelten für jedes Script) ------------------------------ +# Format: "kurz|lang|typ[:META]|default|Beschreibung" typ = flag | value +# Variable im Script: OPTION_ (Großbuchstaben, '-' → '_'), z. B. OPTION_DRY_RUN +B_OPTS_STD=( + "h|help|flag|0|diese Hilfe anzeigen" + "d|debug|flag|0|ausführliche Ausgabe (Log-Level debug)" + "q|quiet|flag|0|keine Konsolenausgabe (Logdatei bleibt)" + "n|dry-run|flag|0|nur anzeigen, nichts ändern" + "l|logfile|value:DATEI||zusätzlich in DATEI loggen" +) +declare -p B_OPTS &>/dev/null || B_OPTS=() # Script/.env hängen mit B_OPTS+=(…) an +declare -ga ARGS=() # Positionsargumente nach b_init + +declare -grA _B_LEVELS=([debug]=10 [info]=20 [warn]=30 [error]=40) +declare -gA _B_OPT_SHORT=() _B_OPT_TYPE=() _B_OPT_META=() _B_OPT_DEFAULT=() _B_OPT_DESC=() +declare -ga _B_OPT_ORDER=() + +#=============================================================================== +# Ausgabe / Logging +#=============================================================================== + +## @brief Zentrale Ausgabe auf Konsole und/oder Logdatei, gefiltert nach Level +## @param $1 Level: debug | info | warn | error +## @param $@ Text +## @return 0 ok, 2 unbekanntes Level +blog() { + local level="${1:-}"; shift || true + local n="${_B_LEVELS[$level]:-}" msg="$*" c="" r="" + [[ -n "$n" ]] || { printf 'blog: unbekanntes Level "%s"\n' "$level" >&2; return 2; } + + if (( ${B_LOG_CONSOLE:-1} )) && (( n >= ${_B_LEVELS[${B_LOG_LEVEL:-info}]:-20} )); then + if [[ "${B_LOG_COLOR:-auto}" == 1 || ( "${B_LOG_COLOR:-auto}" == auto && -t 2 ) ]]; then + r=$'\e[0m' + case "$level" in debug) c=$'\e[2m' ;; warn) c=$'\e[33m' ;; error) c=$'\e[31m' ;; esac + fi + case "$level" in + info) printf '%s\n' "$msg" ;; + debug) printf '%s[DEBUG] %s%s\n' "$c" "$msg" "$r" >&2 ;; + warn) printf '%s[WARN] %s%s\n' "$c" "$msg" "$r" >&2 ;; + error) printf '%s[FEHLER] %s%s\n' "$c" "$msg" "$r" >&2 ;; + esac + fi + + if [[ -n "${B_LOG_FILE:-}" ]] \ + && (( n >= ${_B_LEVELS[${B_LOG_FILE_LEVEL:-${B_LOG_LEVEL:-info}}]:-20} )); then + printf '%(%Y-%m-%d %H:%M:%S)T %-5s %s[%d] %s\n' -1 "${level^^}" "$SCRIPT_NAME" "$$" "$msg" \ + >> "$B_LOG_FILE" 2>/dev/null || true + fi + return 0 +} + +## @brief Ersatz für echo: Info-Meldung über blog +## @param $@ Text +## @return 0 +becho() { blog info "$@"; } + +## @brief Debug-Meldung (nur bei -d bzw. B_LOG_LEVEL=debug) +## @param $@ Text +## @return 0 +bdebug() { blog debug "$@"; } + +## @brief Warnung (stderr) +## @param $@ Text +## @return 0 +bwarn() { blog warn "$@"; } + +## @brief Fehlermeldung (stderr) +## @param $@ Text +## @return 0 +berror() { blog error "$@"; } + +## @brief Fehlermeldung ausgeben und Script beenden (EXIT-Trap räumt auf) +## @param $1 Text +## @param $2 Exit-Code (Default 1; 2 = Aufruffehler, zeigt Hilfe-Hinweis) +## @return – (beendet das Script) +bdie() { + local rc="${2:-1}" + berror "${1:-Abbruch}" + (( rc == 2 )) && blog info "Hilfe: ${SCRIPT_NAME} --help" + exit "$rc" +} + +#=============================================================================== +# Hilfen für den Script-Alltag +#=============================================================================== + +## @brief Befehl ausführen – oder bei --dry-run nur anzeigen +## @param $@ Befehl mit Argumenten (kein String, kein eval) +## @return Exit-Code des Befehls, 0 im Dry-Run +brun() { + if (( ${OPTION_DRY_RUN:-0} )); then + becho "[dry-run] $(printf '%q ' "$@")" + return 0 + fi + bdebug "run: $(printf '%q ' "$@")" + "$@" +} + +## @brief Prüft, ob Programme vorhanden sind +## @param $@ Programmnamen +## @return 0 alle da, 1 mindestens eins fehlt (wird gemeldet) +b_require() { + local cmd rc=0 + for cmd in "$@"; do + command -v "$cmd" &>/dev/null || { berror "Programm fehlt: $cmd"; rc=1; } + done + return "$rc" +} + +## @brief Prüft, ob Variablen gesetzt und nicht leer sind +## @param $@ Variablennamen +## @return 0 alle gesetzt, 1 mindestens eine fehlt (wird gemeldet) +b_require_vars() { + local v rc=0 + for v in "$@"; do + [[ -n "${!v:-}" ]] || { berror "Variable nicht gesetzt: $v (in .env oder Umgebung)"; rc=1; } + done + return "$rc" +} + +## @brief Legt ein temporäres Verzeichnis an (B_TMP_DIR), wird beim Exit gelöscht +## @return 0 ok, 1 mktemp fehlgeschlagen +b_tmpdir() { + [[ -n "$B_TMP_DIR" ]] && return 0 + B_TMP_DIR="$(mktemp -d "${TMPDIR:-/tmp}/${SCRIPT_NAME%.sh}.XXXXXX")" || return 1 + bdebug "Temp-Verzeichnis: $B_TMP_DIR" +} + +## @brief Macht einen Pfad absolut; relative Pfade gelten relativ zu $2 +## @param $1 Pfad +## @param $2 Basisverzeichnis (Default: SCRIPT_DIR) +## @return 0; Ergebnis auf stdout +b_abspath() { + local p="$1" base="${2:-$SCRIPT_DIR}" + [[ "$p" == /* ]] || p="${base%/}/${p}" + printf '%s\n' "$p" +} + +#=============================================================================== +# Usage aus der Präambel +#=============================================================================== + +## @brief Gibt die Präambel des Scripts als Hilfe aus ('## ' wird entfernt, +## die Zeile '## @OPTIONS@' wird durch die Optionsliste ersetzt) +## @return 0 +b_usage() { + local line part=0 + while IFS= read -r line || [[ -n "$line" ]]; do + if [[ "$line" =~ ^#-{10,} ]]; then + (( ++part >= 2 )) && break + continue + fi + (( part == 1 )) && [[ "$line" == '##'* ]] || continue + line="${line#'##'}"; line="${line# }" + if [[ "$line" == *'@OPTIONS@'* ]]; then _b_opts_help; else printf '%s\n' "$line"; fi + done < "$SCRIPT_PATH" + return 0 +} + +## @brief Optionsliste für die Hilfe (aus B_OPTS_STD + B_OPTS) +## @return 0 +_b_opts_help() { + local long s left d + for long in "${_B_OPT_ORDER[@]}"; do + s="${_B_OPT_SHORT[$long]}" + left="${s:+-$s | }--${long}" + [[ "${_B_OPT_TYPE[$long]}" == value ]] && left+=" ${_B_OPT_META[$long]}" + d="${_B_OPT_DEFAULT[$long]}" + if [[ "${_B_OPT_TYPE[$long]}" == value && -n "$d" ]]; then + printf ' %-28s %s (Default: %s)\n' "$left" "${_B_OPT_DESC[$long]}" "$d" + else + printf ' %-28s %s\n' "$left" "${_B_OPT_DESC[$long]}" + fi + done + return 0 +} + +#=============================================================================== +# Optionen +#=============================================================================== + +## @brief Variablenname zu einer Langoption: dry-run → OPTION_DRY_RUN +## @param $1 Langname +## @return 0; Name auf stdout +_b_optvar() { local n="${1^^}"; printf 'OPTION_%s\n' "${n//-/_}"; } + +## @brief Baut die Optionstabellen aus B_OPTS_STD + B_OPTS und prüft sie +## @return 0 ok, 2 Definitionsfehler +_b_opts_build() { + local def s long type meta deflt desc rest + local -A seen_short=() + _B_OPT_ORDER=() + for def in "${B_OPTS_STD[@]}" "${B_OPTS[@]}"; do + IFS='|' read -r s long type deflt desc rest <<< "$def" + meta="${type#*:}"; type="${type%%:*}"; [[ "$meta" == "$type" ]] && meta="WERT" + if [[ -z "$long" || ! "$long" =~ ^[a-z][a-z0-9-]*$ || ! "$type" =~ ^(flag|value)$ || -n "$rest" ]]; then + berror "Ungültige Optionsdefinition: \"$def\" (kurz|lang|flag|value[:META]|default|Text)" + return 2 + fi + if [[ -n "$s" ]]; then + [[ -n "${seen_short[$s]:-}" && "${seen_short[$s]}" != "$long" ]] && { + berror "Kurzoption -$s doppelt vergeben (${seen_short[$s]}, $long)"; return 2; } + seen_short[$s]="$long" + fi + [[ -n "${_B_OPT_TYPE[$long]:-}" ]] || _B_OPT_ORDER+=("$long") + _B_OPT_SHORT[$long]="$s"; _B_OPT_TYPE[$long]="$type"; _B_OPT_META[$long]="$meta" + _B_OPT_DEFAULT[$long]="$deflt"; _B_OPT_DESC[$long]="$desc" + done + return 0 +} + +## @brief Setzt OPTION_ für eine erkannte Option +## @param $1 Langname +## @param $2 Wert +## @return 0 ok, 2 ungültiger Wert +_b_opt_set() { + local long="$1" val="$2" var + var="$(_b_optvar "$long")" + if [[ "${_B_OPT_TYPE[$long]}" == flag && ! "$val" =~ ^[01]$ ]]; then + berror "Option --$long erwartet 0 oder 1, nicht \"$val\"" + return 2 + fi + printf -v "$var" '%s' "$val" + _B_CLI_SET[$long]=1 + return 0 +} + +## @brief Parst die Kommandozeile → OPTION_* und ARGS (Positionsargumente) +## @param $@ Argumente des Scripts +## @return 0 ok, 2 Aufruffehler +_b_opts_parse() { + local arg name val s i long + ARGS=() + while (( $# )); do + arg="$1"; shift + case "$arg" in + --) ARGS+=("$@"); break ;; + --?*) + name="${arg#--}"; val="" + if [[ "$name" == *=* ]]; then val="${name#*=}"; name="${name%%=*}"; fi + [[ -n "${_B_OPT_TYPE[$name]:-}" ]] || { berror "Unbekannte Option: --$name"; return 2; } + if [[ "${_B_OPT_TYPE[$name]}" == value && "$arg" != *=* ]]; then + (( $# )) || { berror "Option --$name braucht einen Wert"; return 2; } + val="$1"; shift + elif [[ "${_B_OPT_TYPE[$name]}" == flag && "$arg" != *=* ]]; then + val=1 + fi + _b_opt_set "$name" "$val" || return 2 + ;; + -?*) + for (( i = 1; i < ${#arg}; i++ )); do + s="${arg:i:1}"; long="" + for name in "${_B_OPT_ORDER[@]}"; do + [[ "${_B_OPT_SHORT[$name]}" == "$s" ]] && { long="$name"; break; } + done + [[ -n "$long" ]] || { berror "Unbekannte Option: -$s"; return 2; } + if [[ "${_B_OPT_TYPE[$long]}" == value ]]; then + val="${arg:i+1}" # -lDATEI oder -l DATEI + if [[ -z "$val" ]]; then + (( $# )) || { berror "Option -$s braucht einen Wert"; return 2; } + val="$1"; shift + fi + _b_opt_set "$long" "$val" || return 2 + break + fi + _b_opt_set "$long" 1 || return 2 + done + ;; + *) ARGS+=("$arg") ;; + esac + done + return 0 +} + +#=============================================================================== +# Trap / Aufräumen +#=============================================================================== + +## @brief EXIT-Trap: ruft cleanup() des Scripts (falls definiert), löscht B_TMP_DIR +## @return – (beendet mit dem ursprünglichen Exit-Code) +_b_on_exit() { + local rc=$? + trap - EXIT + set +e + if declare -F cleanup &>/dev/null; then + cleanup "$rc" || bwarn "cleanup() meldete Fehler" + fi + [[ -n "$B_TMP_DIR" && -d "$B_TMP_DIR" ]] && rm -rf -- "$B_TMP_DIR" + bdebug "Ende ${SCRIPT_NAME} (rc=${rc})" + exit "$rc" +} + +## @brief ERR-Trap: meldet fehlgeschlagenen Befehl mit Funktion und Zeile +## (bewusstes 'return N' hat die Funktion schon selbst gemeldet → nur debug) +## @param $1 Zeilennummer +## @param $2 Befehl +## @return 0 (Abbruch übernimmt set -e) +_b_on_err() { + local rc=$? lvl=error + [[ "$2" == return* ]] && lvl=debug + blog "$lvl" "rc=${rc} in ${FUNCNAME[1]:-main}() Zeile ${1}: ${2}" + return 0 +} + +#=============================================================================== +# Einstieg +#=============================================================================== + +## @brief Initialisiert das Script: Trap, .env, Defaults, Optionen, Logging, Hilfe +## Vorrang: Default < .env < Umgebung (B_*, OPTION_*) < Kommandozeile +## @param $@ Argumente des Scripts (danach: set -- "${ARGS[@]}") +## @return 0 ok; beendet bei --help (0) oder Aufruffehler (2) +b_init() { + local v long var env_file + local -A env_keep=() + declare -gA _B_CLI_SET=() + + trap _b_on_exit EXIT + trap '_b_on_err "$LINENO" "$BASH_COMMAND"' ERR + trap 'exit 130' INT + trap 'exit 143' TERM + + # 1. exportierte Steuer-Variablen aus der Umgebung merken (gewinnen gegen .env) + for v in $(compgen -e); do + [[ "$v" =~ ^(B_[A-Z0-9_]+|OPTION_[A-Z0-9_]+)$ ]] && env_keep[$v]="${!v}" + done + + # 2. .env laden (Bash-Syntax; relativ zum Script, nicht zum $PWD) + env_file="$(b_abspath "${B_ENV_FILE:-.env}")" + if [[ -f "$env_file" ]]; then + # shellcheck source=/dev/null + source "$env_file" || bdie "Fehler beim Laden von $env_file" + fi + for v in "${!env_keep[@]}"; do printf -v "$v" '%s' "${env_keep[$v]}"; done + + # 3. Defaults für alles, was weder .env noch Umgebung gesetzt hat + : "${B_LOG_LEVEL:=info}" "${B_LOG_CONSOLE:=1}" "${B_LOG_FILE:=}" \ + "${B_LOG_FILE_LEVEL:=}" "${B_LOG_COLOR:=auto}" + [[ -n "${_B_LEVELS[$B_LOG_LEVEL]:-}" ]] || { bwarn "B_LOG_LEVEL=$B_LOG_LEVEL ungültig → info"; B_LOG_LEVEL=info; } + [[ -n "$B_LOG_FILE" ]] && B_LOG_FILE="$(b_abspath "$B_LOG_FILE" "$SCRIPT_DIR")" + + _b_opts_build || exit 2 + for long in "${_B_OPT_ORDER[@]}"; do + var="$(_b_optvar "$long")" + [[ -v "$var" ]] || printf -v "$var" '%s' "${_B_OPT_DEFAULT[$long]}" + done + + # 4. Kommandozeile + _b_opts_parse "$@" || bdie "Aufruffehler" 2 + + # 5. Standard-Optionen anwenden + (( OPTION_DEBUG )) && B_LOG_LEVEL=debug + (( OPTION_QUIET )) && B_LOG_CONSOLE=0 + if [[ -n "${_B_CLI_SET[logfile]:-}" ]]; then + B_LOG_FILE="$(b_abspath "$OPTION_LOGFILE" "$B_CALL_DIR")" # CLI: relativ zum $PWD + elif [[ -n "$OPTION_LOGFILE" ]]; then + B_LOG_FILE="$(b_abspath "$OPTION_LOGFILE" "$SCRIPT_DIR")" + fi + if [[ -n "$B_LOG_FILE" ]]; then + mkdir -p -- "${B_LOG_FILE%/*}" 2>/dev/null && touch -- "$B_LOG_FILE" 2>/dev/null \ + || { B_LOG_FILE=""; bwarn "Logdatei nicht beschreibbar – nur Konsole"; } + fi + + (( OPTION_HELP )) && { b_usage; exit 0; } + + bdebug "Start ${SCRIPT_NAME} (_functions ${B_FUNCTIONS_VERSION}) in ${SCRIPT_DIR}, Aufruf aus ${B_CALL_DIR}" + bdebug ".env: $( [[ -f "$env_file" ]] && echo "$env_file" || echo "keine" ) · Log: ${B_LOG_LEVEL}/${B_LOG_FILE:-ohne Datei}" + for long in "${_B_OPT_ORDER[@]}"; do + var="$(_b_optvar "$long")"; bdebug " ${var}=${!var}" + done + return 0 +} + +# --- projektspezifische Erweiterungen ----------------------------------------- +if [[ -f "${B_LIB_DIR}/_functions.local.sh" ]]; then + # shellcheck source=/dev/null + source "${B_LIB_DIR}/_functions.local.sh" +fi diff --git a/bin/bashdoc.sh b/bin/bashdoc.sh new file mode 100755 index 0000000..0ed8bf5 --- /dev/null +++ b/bin/bashdoc.sh @@ -0,0 +1,104 @@ +#!/usr/bin/env bash +#------------------------------------------------------------------------------- +## @file bashdoc.sh +## @brief Erzeugt Markdown-Doku aus den '##'-Kommentaren von Bash-Scripts +## (Präambel + Funktionsköpfe, angelehnt an javadoc) +## @author +## @owner +## @assist KI-unterstützt (Claude), geprüft von +## @version 1.0.0 (07.10.2026) +## +## @usage bashdoc.sh [Optionen] [...] +## Ausgabe auf stdout oder mit -o in eine Datei +## @options +## @OPTIONS@ +## @exit 0 = ok · 1 = Fehler · 2 = Aufruffehler +#------------------------------------------------------------------------------- +set -Eeuo pipefail + +SCRIPT_PATH="$(readlink -f -- "${BASH_SOURCE[0]}")" +SCRIPT_DIR="${SCRIPT_PATH%/*}" +SCRIPT_NAME="${SCRIPT_PATH##*/}" +# shellcheck source=_functions.sh +source "${SCRIPT_DIR}/_functions.sh" || { echo "FEHLER: ${SCRIPT_DIR}/_functions.sh fehlt" >&2; exit 1; } + +B_OPTS+=( + "o|output|value:DATEI||Markdown in DATEI schreiben (relativ = zum Aufrufverzeichnis)" + "p|private|flag|0|auch interne Funktionen (_name) dokumentieren" +) +b_init "$@" +set -- "${ARGS[@]}" + +## @brief Wandelt ein Script in Markdown um +## @param $1 Script-Datei +## @return 0 ok, 1 Datei nicht lesbar; Markdown auf stdout +doc_file() { + local file="$1" + [[ -r "$file" ]] || { berror "Nicht lesbar: $file"; return 1; } + awk -v fname="${file##*/}" -v private="$OPTION_PRIVATE" ' + function flush_block(name, i, l, tag) { + if (!private && name ~ /^_/) { n = 0; return } + printf "\n### `%s()`\n\n", name + for (i = 1; i <= n; i++) { + l = blk[i] + if (l ~ /^@brief/) { sub(/^@brief[ \t]*/, "", l); print l; last = "brief" } + else if (l ~ /^@param/) { sub(/^@param[ \t]*/, "", l); print "- **Param** " l; last = "param" } + else if (l ~ /^@return/) { sub(/^@return[ \t]*/, "", l); print "- **Return** " l; last = "return" } + else if (l ~ /^@/) { tag = l; sub(/[ \t].*/, "", tag); sub(/^@[a-z]+[ \t]*/, "", l); print "- **" substr(tag, 2) "** " l } + else if (l != "") { print (last == "brief" ? l : " " l) } + } + n = 0 + } + function print_opts( i, f, left) { + print " -h -d -q -n -l Standardoptionen (Hilfe, Debug, still, Trockenlauf, Logdatei)" + for (i = 1; i <= nopt; i++) { + split(opt[i], f, "|"); sub(/:.*/, "", f[3]) + left = (f[1] != "" ? "-" f[1] " | " : "") "--" f[2] + if (opt_meta[i] != "") left = left " " opt_meta[i] + printf " %-26s %s%s\n", left, f[5], (f[3] == "value" && f[4] != "" ? " (Default: " f[4] ")" : "") + } + } + # 1. Durchlauf: Script-Optionen aus B_OPTS+=( "…" ) sammeln + FNR == NR { + if ($0 ~ /^[ \t]*B_OPTS\+=\(/) inopts = 1 + if (inopts) { + l = $0 + while (match(l, /"[^"]*"/)) { + o = substr(l, RSTART + 1, RLENGTH - 2); l = substr(l, RSTART + RLENGTH) + opt[++nopt] = o; split(o, f, "|"); opt_meta[nopt] = (f[3] ~ /:/ ? substr(f[3], index(f[3], ":") + 1) : (f[3] == "value" ? "WERT" : "")) + } + if ($0 ~ /\)[ \t]*$/) inopts = 0 + } + next + } + # 2. Durchlauf: Präambel + Funktionsköpfe + /^#-{10,}/ { part++; if (part == 1) printf "## %s\n\n```text\n", fname; if (part == 2) print "```"; next } + part == 1 && /^##/ { l = $0; sub(/^## ?/, "", l); if (l !~ /@OPTIONS@/) print l; else print_opts(); next } + part >= 2 && /^[ \t]*##/ { l = $0; sub(/^[ \t]*## ?/, "", l); blk[++n] = l; next } + part >= 2 && n > 0 && /^[ \t]*(function[ \t]+)?[A-Za-z_][A-Za-z0-9_]*[ \t]*\(\)/ { + name = $0; sub(/^[ \t]*(function[ \t]+)?/, "", name); sub(/[ \t]*\(.*/, "", name) + flush_block(name); next + } + part >= 2 && n > 0 && !/^[ \t]*#/ { n = 0 } + ' "$file" "$file" +} + +## @brief Hauptablauf +## @param $@ Script-Dateien +## @return 0 ok, 1 Fehler +main() { + (( $# )) || bdie "Mindestens ein Script angeben" 2 + local f out="" rc=0 + [[ -n "$OPTION_OUTPUT" ]] && out="$(b_abspath "$OPTION_OUTPUT" "$B_CALL_DIR")" + { + printf '# Script-Doku\n\n> Erzeugt mit `bin/bashdoc.sh` am %(%d.%m.%Y)T – Quelle sind die `##`-Kommentare.\n\n' -1 + for f in "$@"; do + doc_file "$(b_abspath "$f" "$B_CALL_DIR")" || rc=1 + printf '\n' + done + } > "${out:-/dev/stdout}" + [[ -n "$out" ]] && becho "Doku geschrieben: $out" + return "$rc" +} + +main "$@" diff --git a/bin/kb_index.sh b/bin/kb_index.sh new file mode 100755 index 0000000..65c9123 --- /dev/null +++ b/bin/kb_index.sh @@ -0,0 +1,107 @@ +#!/usr/bin/env bash +#------------------------------------------------------------------------------- +## @file kb_index.sh +## @brief Erzeugt ai/context/knowledge/INDEX.md aus dem Frontmatter aller Einträge +## und prüft die Pflichtfelder (id, title, type, status, updated, owner) +## @author +## @owner +## @assist KI-unterstützt (Claude), geprüft von +## @version 1.0.0 (07.10.2026) +## +## @usage kb_index.sh [Optionen] +## @options +## @OPTIONS@ +## @exit 0 = ok · 1 = Fehler / Index veraltet (--check) · 2 = Aufruffehler +#------------------------------------------------------------------------------- +set -Eeuo pipefail + +SCRIPT_PATH="$(readlink -f -- "${BASH_SOURCE[0]}")" +SCRIPT_DIR="${SCRIPT_PATH%/*}" +SCRIPT_NAME="${SCRIPT_PATH##*/}" +# shellcheck source=_functions.sh +source "${SCRIPT_DIR}/_functions.sh" || { echo "FEHLER: ${SCRIPT_DIR}/_functions.sh fehlt" >&2; exit 1; } + +B_OPTS+=( + "c|check|flag|0|nur prüfen: Pflichtfelder ok und INDEX.md aktuell?" + "k|kbdir|value:DIR|../ai/context/knowledge|Wurzel der Knowledge-DB (relativ = zum Script)" +) +b_init "$@" +set -- "${ARGS[@]}" + +REQUIRED=(id title type status updated owner) + +## @brief Liest einen Frontmatter-Wert (einzeilig) aus einer Markdown-Datei +## @param $1 Datei +## @param $2 Schlüssel +## @return 0; Wert auf stdout (leer, wenn nicht vorhanden) +fm_get() { + awk -v key="$2" ' + NR == 1 && $0 != "---" { exit } + NR > 1 && $0 == "---" { exit } + NR > 1 { + k = $0; sub(/:.*/, "", k) + if (k == key) { v = $0; sub(/^[^:]*:[ \t]*/, "", v); sub(/[ \t]+#.*$/, "", v); gsub(/^"|"$/, "", v); print v; exit } + }' "$1" +} + +## @brief Baut den Index-Inhalt und meldet fehlende Pflichtfelder +## @param $1 KB-Wurzel +## @return 0 ok, 1 mindestens ein Eintrag unvollständig; Markdown auf stdout +build_index() { + local kb="$1" f rel dir last_dir="" key val rc=0 n=0 + local -A ids=() + printf '# Knowledge-Index\n\n' + printf '> Automatisch erzeugt von `bin/kb_index.sh` – nicht von Hand bearbeiten.\n' + printf '> Format der Einträge: [README.md](README.md)\n' + while IFS= read -r f; do + rel="${f#"$kb"/}"; dir="${rel%%/*}" + [[ "$dir" == "$rel" ]] && dir="." + for key in "${REQUIRED[@]}"; do + [[ -n "$(fm_get "$f" "$key")" ]] || { berror "${rel}: Pflichtfeld '${key}' fehlt"; rc=1; } + done + val="$(fm_get "$f" id)" + if [[ -n "$val" ]]; then + [[ -n "${ids[$val]:-}" ]] && { berror "${rel}: id '${val}' doppelt (auch ${ids[$val]})"; rc=1; } + ids[$val]="$rel" + fi + if [[ "$dir" != "$last_dir" ]]; then + printf '\n## %s\n\n| id | Titel | Status | Stand | Tags |\n|---|---|---|---|---|\n' "$dir" + last_dir="$dir" + fi + printf '| %s | [%s](%s) | %s | %s | %s |\n' "$val" "$(fm_get "$f" title)" "$rel" \ + "$(fm_get "$f" status)" "$(fm_get "$f" updated)" "$(fm_get "$f" tags | tr -d '[]')" + (( ++n )) + done < <(find "$kb" -type f -name '*.md' ! -name 'README.md' ! -name 'INDEX.md' ! -name '_*' | LC_ALL=C sort) + (( n )) || printf '\n_(noch keine Einträge)_\n' + bdebug "${n} Einträge gelesen" + return "$rc" +} + +## @brief Hauptablauf +## @return 0 ok, 1 Fehler / veraltet +main() { + (( $# == 0 )) || bdie "Keine Positionsargumente erwartet" 2 + local kb rc=0 new + kb="$(readlink -f -- "$(b_abspath "$OPTION_KBDIR")")" + [[ -d "$kb" ]] || { berror "KB-Verzeichnis fehlt: $kb"; return 1; } + b_tmpdir + new="${B_TMP_DIR}/INDEX.md" + build_index "$kb" > "$new" || rc=1 + + if (( OPTION_CHECK )); then + if ! cmp -s "$new" "${kb}/INDEX.md"; then + berror "INDEX.md ist veraltet – bin/kb_index.sh ausführen"; rc=1 + fi + (( rc )) || becho "Knowledge-DB ok" + return "$rc" + fi + if cmp -s "$new" "${kb}/INDEX.md"; then + becho "INDEX.md unverändert" + else + brun cp -- "$new" "${kb}/INDEX.md" + becho "INDEX.md aktualisiert" + fi + return "$rc" +} + +main "$@" diff --git a/context.md b/context.md new file mode 100644 index 0000000..093ebc2 --- /dev/null +++ b/context.md @@ -0,0 +1,47 @@ +# Projekt-Kontext: LPIC-Prüfungsvorbereitung + +> Arbeitsgedächtnis für Agenten. Wird zu Sitzungsbeginn gelesen und am Ende jeder Sitzung mit relevantem Ergebnis aktualisiert. +> Abschnitte nicht umbenennen. + +## Metadaten +- **Projekt:** `lpic` – Vorbereitung auf die LPIC-Zertifizierung +- **Status:** Idee +- **Angelegt:** 29.10.2026 · **Letzte Aktualisierung:** 29.10.2026 +- **Verantwortlich:** `@author` br · `@owner` br (siehe `ai/context/responsible.md`) +- **Code:** in diesem Repo – Übungsscripts unter `bash/`, Lab-Aufbau unter `ansible/` bzw. `stacks/` (Ordner bei Bedarf anlegen) · Remote: + +## Ziel + +Messbar am Ende: +1. +2. Jedes Prüfungsthema (Objective) hat einen Eintrag unter `ai/context/knowledge/topics/` mit Kernbefehlen, Dateien und Übungen. + +## Rahmen +- Prüfungsgrundlage: offizielle Lernziele (Objectives) des LPI in der jeweils gültigen Version – Version und Abrufdatum im Knowledge-Eintrag nennen. +- Übungen lokal (Container/VMs, `ai/rules/testing.md`); keine Produktivsysteme. +- Leitsatz: **so lokal wie möglich, so auswärts wie nötig** (`ai/rules/privacy.md`). + +## Fahrplan +- [ ] Ziel festlegen (Stufe, Prüfungen, Termin) +- [ ] Objectives als Gliederung in `ai/context/knowledge/topics/` anlegen +- [ ] Lab-Umgebung festlegen (lokale Container/VMs) + +## Stand +- 29.10.2026: Repo aus der 2nd-brain-Basis abgeleitet (Regeln, Standards, Kontextstruktur übernommen; 2nd-brain-Inhalte entfernt, ohne Git-Historie). + +## Entscheidungen +| Datum | Entscheidung | Warum | ADR | +|---|---|---|---| +| 29.10.2026 | Neues Repo ohne Historie aus 2nd-brain; Regeln, `ai/common/`, `bin/`, Umgebung und KI-Freigaben übernommen | Basis wiederverwenden, keine 2nd-brain-Inhalte in der Historie | – | +| 29.10.2026 | Knowledge-Typ `topic` (`topics/`) für Lerninhalte je Prüfungsthema | LPIC-Inhalte von Umgebungswissen trennen | – | + +## Offene Fragen +1. [ ] **Ziel:** Welche Stufe (LPIC-1/-2/-3) und welche Prüfungen, bis wann? +2. [ ] **Remote:** Gitea-Repo anlegen (z. B. `br/lpic`) und als `origin` eintragen. +3. [ ] **Lab:** Welche Umgebung für Übungen (lokale Container, VMs, Distributionen)? + +## Nächster Schritt +Betreiber legt Ziel und Prüfungen fest; danach Objectives als Gliederung unter `topics/` anlegen. + +## Sitzungsprotokoll +- 29.10.2026 – Repo aus `2ndbrain` abgeleitet: Stacks, Ansible-Inventar, `architecture.md`, 2nd-brain-Knowledge (`dockweb112`, `kellerbeast`, LUKS-Recherche) entfernt; `services/sultan` (Verweis aus `docker.md`) bereinigt behalten; `environment.md` und `privacy.md` auf Projekt angepasst; Knowledge-Typ `topic` ergänzt.