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 <noreply@anthropic.com>
This commit is contained in:
Björn RundenandClaude Opus 5.5 committed 2026-10-29 21:56:53 +01:00
commit 7f6b8ebe07
70 files changed
+2816

No files matched your search

+35
View File
@@ -0,0 +1,35 @@
#!/usr/bin/env bash
#-------------------------------------------------------------------------------
## @file .env.sh
## @brief Ansible-Kontext für dieses Repo setzen (Pfade + Secret-Store)
## @author <AUTOR – aus ai/context/responsible.md>
## @owner <VERANTWORTLICH – aus ai/context/responsible.md>
## @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="<STANDARDHOST>" # nur Doku – Default steht im Play
# --- Secret-Store (HashiCorp Vault, bevorzugt) – Token aus ~/.vault-token ---
export VAULT_ADDR="<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="<projekt>"
# --- Ansible Vault (nur Fallback, Passwortdatei außerhalb des Repos) ---
[ -f "$HOME/.ans-secret-<projekt>" ] && export ANSIBLE_VAULT_PASSWORD_FILE="$HOME/.ans-secret-<projekt>"
unset _ANS_DIR _ANS_SRC
echo "Ansible-Kontext aktiv: ANSIBLE_BASE_PATH=$ANSIBLE_BASE_PATH"
+24
View File
@@ -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
@@ -0,0 +1,10 @@
{#-
## @file example.conf.j2
## @brief Konfiguration für <Dienst> – verwaltet von Ansible (Rolle example)
## @author <AUTOR – aus ai/context/responsible.md>
## @owner <VERANTWORTLICH – aus ai/context/responsible.md>
## @version 0.1.0 (TT.MM.JJJJ)
-#}
# {{ ansible_managed }}
# Nicht von Hand ändern – Änderungen über Ansible.
password = {{ example_secrets.password | default('') }}
@@ -0,0 +1,10 @@
---
## @brief Globale Variablen für alle Hosts
ansible_user: "<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('<projekt>', true) }}"
+3
View File
@@ -0,0 +1,3 @@
## @file inv/lab – Inventar Testumgebung (Hosts aus ai/context/knowledge/systems/)
[example]
<STANDARDHOST>
@@ -0,0 +1,16 @@
#-------------------------------------------------------------------------------
## @file example.yml
## @brief <Dienst> installieren + konfigurieren (Rolle example, Templates aus $ANS_FILES/example)
## @author <AUTOR – aus ai/context/responsible.md>
## @owner <VERANTWORTLICH – aus ai/context/responsible.md>
## @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 @<testsecrets.yml>
## @requires source ansible/.env.sh und vault login
#-------------------------------------------------------------------------------
---
- name: Example
hosts: "{{ hostlist | default('<STANDARDHOST>') }}"
roles:
- example
@@ -0,0 +1,28 @@
#-------------------------------------------------------------------------------
## @file mini_check.yml
## @brief Erreichbarkeit prüfen und Kernfakten anzeigen; -e check_mode=setup sammelt Facts neu
## @author <AUTOR – aus ai/context/responsible.md>
## @owner <VERANTWORTLICH – aus ai/context/responsible.md>
## @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('<STANDARDHOST>') }}"
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'] }}"
@@ -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
@@ -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
@@ -0,0 +1,11 @@
---
galaxy_info:
role_name: example
author: "<AUTOR – aus ai/context/responsible.md>"
description: "<Dienst> installieren und konfigurieren"
license: proprietary
min_ansible_version: "2.15"
platforms:
- name: Debian
versions: [trixie]
dependencies: []
@@ -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
@@ -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
@@ -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]
@@ -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
@@ -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'
+21
View File
@@ -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}"
+415
View File
@@ -0,0 +1,415 @@
#!/usr/bin/env bash
#-------------------------------------------------------------------------------
## @file _functions.sh
## @brief Gemeinsame Bash-Basis: .env laden, Optionen, Logging, Usage, Trap
## @author <AUTOR – siehe ai/context/responsible.md>
## @owner <VERANTWORTLICH – siehe ai/context/responsible.md>
## @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_<LANG> (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_<LANG> 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
+58
View File
@@ -0,0 +1,58 @@
#!/usr/bin/env bash
#-------------------------------------------------------------------------------
## @file script_template.sh
## @brief <Was das Script tut – ein Satz>
## @author <AUTOR – aus ai/context/responsible.md>
## @owner <VERANTWORTLICH – aus ai/context/responsible.md>
## @assist <optional: KI-unterstützt (Modell), geprüft von AUTOR>
## @version 0.1.0 (TT.MM.JJJJ)
##
## @usage script_template.sh [Optionen] <ziel>
## @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 <ziel> 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 "$@"
+23
View File
@@ -0,0 +1,23 @@
# --- Stack ---
COMPOSE_PROJECT_NAME=<stack> # nur a-z, 0-9, _ – kein Bindestrich (GELF-Tag)
APP_TAG=<version>
# --- Logging (Graylog) ---
GELF_HOST=<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.domain.tld>
APP_INTERNAL_PORT=<port>
#TRAEFIK_NETWORK=traefik-net
#TRAEFIK_CERTRESOLVER=le
# --- Hosts ---
EXTRA_HOST_1=<fqdn>:<ip>
# --- Pfade (Bind-Mounts, relativ zum Stack) ---
DIR_DATA=./data
DIR_CONFIG=./config
# --- Image-spezifische Variablen (laut Orig-Doku, s. README) ---
+29
View File
@@ -0,0 +1,29 @@
# <stack>
> @author <AUTOR> · @owner <VERANTWORTLICH> · Stand TT.MM.JJJJ
## 1. Zweck und Zugriff
<Was der Stack tut> · FQDN: `<app.domain.tld>` · Port: <…>
## 2. Original-Doku
| Quelle | URL | Abgerufen |
|---|---|---|
| Image | <Docker Hub / GHCR> | TT.MM.JJJJ |
| Hersteller | <Doku-URL> | 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: `<endpunkt>` – Begründung: <…>
## 6. Besonderheiten
- <Capabilities, Init-/Unseal-Schritte, Update-Hinweise, Abweichungen vom Standard>
@@ -0,0 +1,48 @@
#-------------------------------------------------------------------------------
## @file docker-compose.yml
## @brief <Stack-Zweck in einem Satz>
## @author <AUTOR – aus ai/context/responsible.md>
## @owner <VERANTWORTLICH – aus ai/context/responsible.md>
## @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: <hersteller>/<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}:<intern>"
extra_hosts: # nur wenn benötigt; ein Eintrag je Variable
- "${EXTRA_HOST_1}"
volumes:
- ${DIR_DATA}:/<pfad-im-container>
- ${DIR_CONFIG}:/<config-pfad>: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:<intern>/<pfad>"]
interval: 30s
timeout: 5s
retries: 3
start_period: 30s
networks:
proxy:
name: ${TRAEFIK_NETWORK:-traefik-net}
external: true
+35
View File
@@ -0,0 +1,35 @@
---
id: adr_<NNNN>_<slug>
title: <Entscheidung als Aussage, z. B. „HashiCorp Vault als Secret-Store“>
type: decision
status: draft # draft|active|superseded
updated: <JJJJ-MM-TT>
owner: <Kürzel aus responsible.md>
tags: []
related: []
supersedes:
classification: intern
---
# ADR <NNNN>: <Titel>
## Kontext
<Welches Problem, welche Randbedingungen, welche Kräfte wirken?>
## Entscheidung
<Was wir tun – ein klarer Satz, dann Details.>
## Alternativen
| Option | Pro | Contra | Warum nicht |
|---|---|---|---|
| | | | |
## Konsequenzen
- positiv: <…>
- negativ/Aufwand: <…>
- Folgearbeiten: <…>
## Quellen
- <URL> (abgerufen TT.MM.JJJJ)
## Änderungen
- TT.MM.JJJJ – vorgeschlagen (<Kürzel>)
@@ -0,0 +1,34 @@
---
id: <id> # = Dateiname ohne .md
title: <Titel>
type: <system|network|service|decision|howto|research>
status: draft # draft|active|deprecated|superseded
updated: <JJJJ-MM-TT>
owner: <Kürzel aus responsible.md>
tags: []
sources: []
related: []
classification: intern
---
# <Titel>
> Kurzfassung in 1–2 Sätzen: Was ist das, wozu dient es?
## Fakten
| Merkmal | Wert |
|---|---|
| | |
## Details
<Aufbau, Konfiguration, Abhängigkeiten – sachlich, mit Tabellen und Code-Blöcken>
## Betrieb
- Bereitstellung: <Ansible-Rolle / Docker-Stack / manuell (→ howto)>
- Prüfung/Healthcheck: <…>
- Backup/Restore: <…>
## Quellen
- <URL> (abgerufen TT.MM.JJJJ)
## Änderungen
- TT.MM.JJJJ – angelegt (<Kürzel>)