rein damit
This commit is contained in:
1 parent
7f6b8ebe07
commit
07dbc01c92
38 files changed
+5632
-5
No files matched your search
@@ -9,3 +9,8 @@ fact_caching/
|
||||
*.retry
|
||||
# Sicherungen beim Überschreiben (bewusst lokal behalten, nicht versionieren)
|
||||
*.orig
|
||||
# Simulator: PSK, Export, Python-Cache
|
||||
simulator/tools/.psk
|
||||
simulator/dist/
|
||||
__pycache__/
|
||||
simulator/release/
|
||||
@@ -32,6 +32,7 @@ Details und Ausnahmen: [`ai/rules/language.md`](ai/rules/language.md).
|
||||
| `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 |
|
||||
| `simulator/` | Prüfungssimulator (statisch, reveal.js); Fragen unter `simulator/data/`, Build mit `simulator/tools/build_bank.py` ([README](simulator/README.md)) | Agent + Bediener |
|
||||
| `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 |
|
||||
|
||||
@@ -18,8 +18,13 @@ lpic/
|
||||
│ ├── rules/ # verbindliche Regeln (Vorrang)
|
||||
│ ├── common/ # Standards & Verabredungen + templates/
|
||||
│ └── context/ # Verantwortliche, Stil, Umgebung + knowledge/ (Knowledge-DB, Lerninhalte unter topics/)
|
||||
├── simulator/ # Prüfungssimulator (statisch, reveal.js) – index.html öffnen
|
||||
└── bin/ # Werkzeuge der Basis
|
||||
```
|
||||
|
||||
## Prüfungssimulator
|
||||
`simulator/index.html` im Browser öffnen (kein Server nötig): LPIC-1 101/102 mit je 90 Fragen, Lern- und Prüfungsmodus,
|
||||
Speichern/Export/Import der Sitzung. Details und Fragenformat: [simulator/README.md](simulator/README.md).
|
||||
Code für Übungen und Lab (`bash/`, `ansible/`, `stacks/`) wird bei Bedarf angelegt.
|
||||
|
||||
## Einrichten
|
||||
|
||||
@@ -3,6 +3,12 @@
|
||||
> Automatisch erzeugt von `bin/kb_index.sh` – nicht von Hand bearbeiten.
|
||||
> Format der Einträge: [README.md](README.md)
|
||||
|
||||
## research
|
||||
|
||||
| id | Titel | Status | Stand | Tags |
|
||||
|---|---|---|---|---|
|
||||
| lpi_exam_format | [LPIC-1 – Prüfungsformat, Objectives 5.0 und Regeln zur Fragenerstellung](research/lpi_exam_format.md) | active | 2026-10-29 | lpic, lpic-1, pruefung, objectives, simulator |
|
||||
|
||||
## services
|
||||
|
||||
| id | Titel | Status | Stand | Tags |
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
---
|
||||
id: lpi_exam_format
|
||||
title: LPIC-1 – Prüfungsformat, Objectives 5.0 und Regeln zur Fragenerstellung
|
||||
type: research
|
||||
status: active
|
||||
updated: 2026-10-29
|
||||
owner: br
|
||||
tags: [lpic, lpic-1, pruefung, objectives, simulator]
|
||||
sources:
|
||||
- https://www.lpi.org/de/exam-101-102-objectives/
|
||||
- https://www.lpi.org/value-of-certification/our-certification-development-standards/
|
||||
- https://www.lpi.org/our-certifications/lpic-1-overview/
|
||||
- https://www.lpi.org/faq/
|
||||
related: []
|
||||
classification: öffentlich
|
||||
---
|
||||
# LPIC-1 – Prüfungsformat, Objectives 5.0 und Regeln zur Fragenerstellung
|
||||
|
||||
> Inhalt liegt seit 29.10.2026 im Simulator, damit er mit dem Prüferpaket weitergegeben werden kann (eine Quelle):
|
||||
> **[simulator/docs/exam_format.md](../../../../simulator/docs/exam_format.md)** – Fakten, LPI-Regeln, Objectives mit Gewicht, Quellen (abgerufen 29.10.2026).
|
||||
|
||||
Kurzfassung: 101-500/102-500, Objectives 5.0, je 60 Fragen in 90 min (Multiple Choice + Lückentext), Skala 200–800, bestanden ab 500; Gewicht = Fragenzahl je Objective; LPI nennt keine Prozentgrenze und keine feste Optionenzahl.
|
||||
|
||||
## Änderungen
|
||||
- 29.10.2026 – angelegt (br, KI-unterstützt)
|
||||
- 29.10.2026 – Inhalt nach `simulator/docs/exam_format.md` verschoben, hier nur Verweis
|
||||
+21
-5
@@ -5,13 +5,13 @@
|
||||
|
||||
## Metadaten
|
||||
- **Projekt:** `lpic` – Vorbereitung auf die LPIC-Zertifizierung
|
||||
- **Status:** Idee <!-- Idee | Planung | Umsetzung | Betrieb | abgeschlossen -->
|
||||
- **Status:** Umsetzung <!-- Idee | Planung | Umsetzung | Betrieb | abgeschlossen -->
|
||||
- **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: <!-- TODO: Gitea-Repo anlegen -->
|
||||
- **Code:** in diesem Repo – Prüfungssimulator unter `simulator/`, Übungsscripts unter `bash/`, Lab-Aufbau unter `ansible/` bzw. `stacks/` (Ordner bei Bedarf anlegen) · Remote: <!-- TODO: Gitea-Repo anlegen -->
|
||||
|
||||
## Ziel
|
||||
<!-- TODO: Stufe und Prüfungen festlegen, z. B. LPIC-1 (101 + 102), Zieltermin -->
|
||||
LPIC-1 (101-500 + 102-500), danach LPIC-2. <!-- TODO: Zieltermin -->
|
||||
Messbar am Ende:
|
||||
1. <!-- TODO: Prüfung(en) bestanden bis TT.MM.JJJJ -->
|
||||
2. Jedes Prüfungsthema (Objective) hat einen Eintrag unter `ai/context/knowledge/topics/` mit Kernbefehlen, Dateien und Übungen.
|
||||
@@ -25,23 +25,39 @@ Messbar am Ende:
|
||||
- [ ] Ziel festlegen (Stufe, Prüfungen, Termin)
|
||||
- [ ] Objectives als Gliederung in `ai/context/knowledge/topics/` anlegen
|
||||
- [ ] Lab-Umgebung festlegen (lokale Container/VMs)
|
||||
- [x] Prüfungssimulator LPIC-1 mit 180 Fragen (29.10.2026)
|
||||
- [ ] Fragen fachlich durchsehen (br) und laufend ergänzen
|
||||
- [ ] LPIC-2 (201/202) als weitere Prüfungen im Simulator
|
||||
|
||||
## Stand
|
||||
- 29.10.2026: Repo aus der 2nd-brain-Basis abgeleitet (Regeln, Standards, Kontextstruktur übernommen; 2nd-brain-Inhalte entfernt, ohne Git-Historie).
|
||||
- 29.10.2026: Prüfungssimulator `simulator/` fertig: LPIC-1 101/102 je 90 Fragen (109 single, 39 multi, 32 fill), alle Objectives 5.0 mindestens mit ihrem Gewicht abgedeckt; Lern-/Prüfungsmodus, localStorage + JSON-Export/Import, Fragenpakete nachladbar. Fragen KI-erstellt, **noch nicht fachlich geprüft**.
|
||||
|
||||
## Entscheidungen
|
||||
| Datum | Entscheidung | Warum | ADR |
|
||||
|---|---|---|---|
|
||||
| 29.10.2026 | Simulator in reinem JavaScript (klassische Scripts, kein TypeScript, kein Build für den Code) | kein node/tsc auf dem Rechner; läuft so auch unter `file://` | – |
|
||||
| 29.10.2026 | Fragen als JSON (`simulator/data/`), Bundle `js/bank.js` per `tools/build_bank.py`, zusätzlich Import per Datei-Dialog | `fetch` von JSON ist unter `file://` blockiert | – |
|
||||
| 29.10.2026 | Fragenregeln: genau 5 Optionen bei single/multi, Erläuterung je Option, Bewertung alles-oder-nichts; Prüfungsmodus 60 Fragen/90 min, Auswahl nach Gewicht; Richtwert Bestehen 65 % | Vorgabe Bediener + LPI-Standards; LPI nennt keine Prozentgrenze (200–800, bestanden ab 500) | – |
|
||||
| 29.10.2026 | Ausgelieferte Bank `js/bank.js` ChaCha20-verschlüsselt, Schlüssel aus PSK (`LPIC_SIM_PSK` bzw. `simulator/tools/.psk`, nicht versioniert); Quelltext `data/*.json` bleibt Klartext für Prüfer; Prüfvermerk `review` (pending/approved/rejected); Export ohne Quellen nach `simulator/dist/` | kein Klartext auf der Platte; Schlüssel wird mit ausgeliefert → Verschleierung, kein Schutz gegen gezielte Analyse | – |
|
||||
| 29.10.2026 | Weitergabe an Prüfer als tar.gz (kein Git); Festlegungen und LPI-Kontext liegen dafür in `simulator/docs/` (eine Quelle), Mailadresse in Headern bleibt, Lizenz klärt jeder Empfänger | Prüfer brauchen den vollständigen Kontext, nicht die AI-Basis | – |
|
||||
| 29.10.2026 | reveal.js 5.1.0 unverändert unter `simulator/vendor/reveal/` (eingebautes Dracula-Theme) | offline lauffähig, kein CDN | – |
|
||||
| 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?
|
||||
1. [ ] **Ziel:** LPIC-1 (101/102), danach LPIC-2 – Zieltermin?
|
||||
2. [ ] **Remote:** Gitea-Repo anlegen (z. B. `br/lpic`) und als `origin` eintragen.
|
||||
3. [ ] **Lab:** Welche Umgebung für Übungen (lokale Container, VMs, Distributionen)?
|
||||
4. [ ] **Richtwert Bestehen:** 65 % ist eine Annahme (`pass_percent` in `exam.json`) – anpassen?
|
||||
5. [ ] **Teilpunkte** bei Mehrfachauswahl: LPI dokumentiert nichts; Simulator wertet alles-oder-nichts.
|
||||
6. [ ] **Klartext außerhalb der Bank:** Sitzungs-Export und `localStorage` enthalten die Fragen einer Sitzung im Klartext – ebenfalls verschlüsseln?
|
||||
|
||||
## Nächster Schritt
|
||||
Betreiber legt Ziel und Prüfungen fest; danach Objectives als Gliederung unter `topics/` anlegen.
|
||||
Betreiber testet den Simulator und sieht die Fragen fachlich durch (Rückmeldungen als Korrektur in `simulator/data/`); 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.
|
||||
- 29.10.2026 – Prüfungssimulator gebaut (`simulator/`), Objectives 5.0 und LPI-Regeln recherchiert (Knowledge `research/lpi_exam_format`). Tests lokal: `build_bank.py -c` ok (180 Fragen); `tests/selftest.html` 40/40, `tests/ui_smoke.html` 27/27 in Chromium headless unter `file://` und über lokalen `http.server`; Start und Darstellung zusätzlich in Firefox headless geprüft. Nicht getestet: Mobilgeräte (nur 400-px-Fenster), Safari.
|
||||
- 29.10.2026 – Bank verschlüsselt (ChaCha20, gegen Python-`cryptography` und RFC-8439-Vektor geprüft), Prüfvermerk `review`, Export `build_bank.py -e` → `simulator/dist/`; Lückentext-Feld war links verschoben (input inline-block, `margin:auto` griff nicht) → `display:block`. Tests: selftest 44/44, ui_smoke 27/27; Export über `python3 -m http.server 8080` (alle Schnittstellen) ausgeliefert und geprüft.
|
||||
- 29.10.2026 – Prüferpaket: Doku nach `simulator/docs/` (exam_format, question_rules, decisions, REVIEW) und `simulator/AGENTS.md`; `build_bank.py -b` → `simulator/release/lpic-sim_<datum>.tar.gz` (ohne PSK, `dist/`, `release/`, Caches; ohne `ai/` und Umgebungsdaten). Entpackt in leerem Verzeichnis geprüft: Build, selftest 44/44, ui_smoke 27/27, alle Doku-Links innerhalb des Pakets. Lizenz klärt jeder Prüfer selbst (Betreiber).
|
||||
@@ -0,0 +1,19 @@
|
||||
# AGENTS.md – Hinweise für KI-Agenten
|
||||
|
||||
> Stand: 29.10.2026 · für KI-Assistenten, die Prüfer beim Prüfen oder Ergänzen von Fragen unterstützen.
|
||||
|
||||
## Lesen vor der Arbeit
|
||||
1. [README.md](README.md) – Aufbau, Format einer Frage, Build.
|
||||
2. [docs/question_rules.md](docs/question_rules.md) – verbindliche Regeln für Fragen.
|
||||
3. [docs/exam_format.md](docs/exam_format.md) – Prüfungsformat und Objectives 5.0 (mit Quellen).
|
||||
4. [docs/decisions.md](docs/decisions.md) – Festlegungen; nicht ohne Rücksprache ändern.
|
||||
|
||||
## Regeln
|
||||
- Sprache Deutsch mit Umlauten; Datei-/Schlüsselnamen englisch (ASCII).
|
||||
- Keine Erfindungen: Befehle, Optionen, Dateipfade nur, wenn belegbar (Manpage, offizielle Doku); Unsicheres als unsicher kennzeichnen, nicht als Frage aufnehmen.
|
||||
- Distraktoren eindeutig falsch; bei Zweifel lieber Option ersetzen.
|
||||
- Nach jeder Änderung `python3 tools/build_bank.py -c`; Fehler beheben, bevor gebaut wird.
|
||||
- `js/bank.js` und `dist/` sind erzeugt – nie von Hand ändern. `tools/.psk` nie ausgeben oder weitergeben.
|
||||
- Code bleibt ohne Build und ohne externe Abhängigkeiten lauffähig unter `file://` (kein `fetch`, keine ES-Module, kein CDN).
|
||||
- Datei-Header (`@file`, `@brief`, `@author`, `@owner`, `@assist`, `@version`) erhalten und bei Änderung `@version` erhöhen; `@author` ist immer ein Mensch, nie die KI.
|
||||
- Tests: siehe [docs/REVIEW.md](docs/REVIEW.md#tests).
|
||||
@@ -0,0 +1,125 @@
|
||||
# LPIC-Prüfungssimulator
|
||||
|
||||
> Stand: 29.10.2026 · Statische Web-Anwendung (reveal.js 5.1.0, Dracula-Theme), läuft direkt von `file://` und von jedem statischen Webserver.
|
||||
> Inoffizielles Lernwerkzeug, nicht mit dem Linux Professional Institute (LPI) verbunden. „LPIC“ ist eine Marke des LPI.
|
||||
> Die Nutzungsrechte an Fragen und Code klärt jeder Empfänger selbst; reveal.js steht unter MIT-Lizenz (`vendor/reveal/LICENSE`).
|
||||
|
||||
## Dokumente
|
||||
| Datei | Inhalt |
|
||||
|---|---|
|
||||
| [docs/exam_format.md](docs/exam_format.md) | LPIC-1: Prüfungsformat, Objectives 5.0 mit Gewicht, LPI-Regeln, Quellen |
|
||||
| [docs/question_rules.md](docs/question_rules.md) | Regeln für Fragen (technisch und inhaltlich) |
|
||||
| [docs/decisions.md](docs/decisions.md) | Festlegungen mit Begründung, offene Punkte |
|
||||
| [docs/REVIEW.md](docs/REVIEW.md) | Anleitung für Prüfer |
|
||||
| [AGENTS.md](AGENTS.md) | Hinweise für KI-Assistenten der Prüfer |
|
||||
|
||||
## Benutzung
|
||||
| Weg | Aufruf |
|
||||
|---|---|
|
||||
| Lokal | `index.html` im Browser öffnen (Doppelklick genügt, kein Server nötig) |
|
||||
| Export + Webserver | `python3 tools/build_bank.py -e` schreibt nur die Laufzeitdateien nach `dist/` (ohne `data/`, `tools/`, `tests/`), dann `python3 -m http.server 8080 -d dist` |
|
||||
|
||||
**Startseite:** Prüfung wählen (LPIC-1 101 oder 102), Modus wählen, im Lernmodus optional Topics, Anzahl und Reihenfolge.
|
||||
|
||||
| Modus | Ablauf |
|
||||
|---|---|
|
||||
| Lernmodus | Antwort wählen → **Weiter** (→, Leertaste, „Auflösen ▸“) zeigt die Auflösung: richtig/falsch, Erläuterung je Option, Gesamterläuterung, Manpage-Verweis. Nochmal Weiter → nächste Frage. Aufgelöste Fragen sind gesperrt. |
|
||||
| Prüfungsmodus | 60 Fragen, 90 Minuten Countdown, Auswahl gewichtet nach Objectives (Gewicht = Fragenzahl wie in der echten Prüfung). Fragen markieren, frei vor/zurück, Abgabe auf der letzten Folie (automatisch bei Zeitablauf). Danach Auswertung je Topic und Auflösung aller Fragen. |
|
||||
|
||||
Tasten: → / Leertaste weiter · ← zurück · `1`–`5` Antwort wählen · Enter im Textfeld = weiter.
|
||||
|
||||
## Speichern, Pause, Fortsetzen
|
||||
- Jede Eingabe wird sofort im Browser gespeichert (`localStorage`, Schlüssel `lpic_sim.session`). Beim nächsten Öffnen bietet die Startseite **Fortsetzen** an.
|
||||
- **Export** (Werkzeugleiste) lädt die Sitzung als JSON herunter (`lpic_sim_<prüfung>_<modus>_<zeit>.json`). Die Datei enthält die Fragen selbst; sie bleibt damit auch nach Änderungen an der Fragenbank und auf anderen Rechnern nutzbar.
|
||||
- **Import** erkennt das Format selbst: Eine Sitzung wird an der gespeicherten Stelle fortgesetzt, ein Fragenpaket in die Bank übernommen (und im Browser gemerkt).
|
||||
- `localStorage` gilt je Browser und Herkunft: `file://` und `http://…` sehen getrennte Daten – dafür Export/Import nutzen.
|
||||
|
||||
## Fragen ergänzen
|
||||
1. Datei `data/<prüfung>/questions_<topic>.json` bearbeiten oder neu anlegen (Schema: [`data/schema/questions.schema.json`](data/schema/questions.schema.json) – VSCode/VSCodium validiert über den `$schema`-Eintrag).
|
||||
2. `python3 tools/build_bank.py -c` prüft alle Fragen und zeigt Prüfstatus und Abdeckung je Objective.
|
||||
3. `python3 tools/build_bank.py` erzeugt `js/bank.js` (diese Datei nie von Hand ändern), mit `-e` zusätzlich den Export `dist/`.
|
||||
|
||||
Ohne Build-Schritt: dieselbe JSON-Datei im Simulator über **Import** laden.
|
||||
|
||||
### Quelltext vs. ausgelieferte Bank
|
||||
| | Quelltext `data/*.json` | `js/bank.js` / Export |
|
||||
|---|---|---|
|
||||
| Form | Klartext-JSON, für Prüfer lesbar und kommentierbar | ChaCha20-verschlüsselt (RFC 8439), Base64 |
|
||||
| Zweck | Durchsicht, Freigabe/Ablehnung, Pflege | Auslieferung, kein Klartext auf der Platte |
|
||||
|
||||
- **PSK:** Umgebung `LPIC_SIM_PSK`, sonst `tools/.psk` (beim ersten Build zufällig erzeugt, Rechte 600, nicht weitergeben – im Prüferpaket nicht enthalten). Der Schlüssel ist `SHA-256("lpic-sim/bank/v1:" + PSK)` und steht als `build`-Kennung in `bank.js`, die Nonce als `rev` – der PSK selbst nirgends.
|
||||
- **Grenze:** Der Browser muss ohne Eingabe entschlüsseln können, also wird der Schlüssel mit ausgeliefert. Das verhindert Klartext auf der Platte, Suchen mit `grep` und zufälliges Mitlesen, schützt aber **nicht** vor jemandem, der gezielt `app.js` und `bank.js` analysiert.
|
||||
- **Weiterhin im Klartext:** exportierte Sitzungen (JSON-Datei), die gespeicherte Sitzung und importierte Pakete im `localStorage` – sie enthalten die Fragen der jeweiligen Sitzung.
|
||||
|
||||
### Prüfvermerk (Review)
|
||||
Prüfer tragen je Frage einen Vermerk ein; ohne Vermerk gilt `pending`:
|
||||
```json
|
||||
"review": { "status": "rejected", "by": "Prüfer A", "date": "2026-10-30", "note": "Option C ebenfalls korrekt" }
|
||||
```
|
||||
| Status | Wirkung beim Build |
|
||||
|---|---|
|
||||
| `pending` | übernommen (Standard), mit `--approved-only` nicht |
|
||||
| `approved` | übernommen |
|
||||
| `rejected` | nie übernommen; bleibt im Quelltext zur Nachbearbeitung |
|
||||
|
||||
Der Vermerk selbst wird nicht in die Bank übernommen. Freigabe-Build: `python3 tools/build_bank.py --approved-only -e`.
|
||||
|
||||
### Format einer Frage
|
||||
```json
|
||||
{
|
||||
"id": "103.4-007",
|
||||
"objective": "103.4",
|
||||
"type": "single",
|
||||
"difficulty": 2,
|
||||
"question": "Text mit `Code` und **Betonung**",
|
||||
"code": "optionaler\nmehrzeiliger Block",
|
||||
"options": [
|
||||
{ "id": "a", "text": "…", "correct": true, "explanation": "warum richtig" },
|
||||
{ "id": "b", "text": "…", "correct": false, "explanation": "warum falsch" }
|
||||
],
|
||||
"explanation": "Gesamterläuterung",
|
||||
"references": ["man 1 bash"]
|
||||
}
|
||||
```
|
||||
| `type` | Regel (geprüft von `build_bank.py` und beim Import) |
|
||||
|---|---|
|
||||
| `single` | genau 5 Optionen `a`–`e`, genau 1 richtig |
|
||||
| `multi` | genau 5 Optionen, 2–4 richtig; der Simulator zeigt „Wählen Sie ZWEI/DREI“ und begrenzt die Auswahl |
|
||||
| `fill` | keine Optionen, `answers` = akzeptierte Antworten (erste = Musterlösung), Leerraum wird normalisiert, `case_sensitive` (Standard `true`) |
|
||||
|
||||
### Regeln für Fragen
|
||||
Verbindlich in [docs/question_rules.md](docs/question_rules.md); Prüfablauf in [docs/REVIEW.md](docs/REVIEW.md).
|
||||
|
||||
## Weitere Prüfungen (z. B. LPIC-2)
|
||||
Ordner `data/lpic2_201/` mit `exam.json` (Aufbau wie [`data/lpic1_101/exam.json`](data/lpic1_101/exam.json): `level`, `code`, `questions`, `duration_min`, Topics/Objectives mit Gewicht) und `questions_*.json` anlegen, dann `build_bank.py`. Die Startseite gruppiert automatisch nach `level`.
|
||||
Alternativ zur Laufzeit: ein Fragenpaket mit zusätzlichem Schlüssel `"definition"` (Inhalt einer `exam.json`) importieren.
|
||||
|
||||
## Inhalt
|
||||
| Pfad | Inhalt |
|
||||
|---|---|
|
||||
| `index.html` | Einstieg |
|
||||
| `js/app.js` | Logik (Sitzung, Bewertung, Speichern, Import, Darstellung); reine Funktionen unter `window.LPICSim.core` |
|
||||
| `js/bank.js` | **erzeugt** – verschlüsselte Fragenbank als `window.LPIC_BANK` (Script statt JSON, damit es unter `file://` lädt) |
|
||||
| `dist/` | **erzeugt** mit `build_bank.py -e` – Export zum Ausliefern |
|
||||
| `release/` | **erzeugt** mit `build_bank.py -b` – Prüferpaket `lpic-sim_<datum>.tar.gz` |
|
||||
| `docs/`, `AGENTS.md` | Dokumente (siehe oben) |
|
||||
| `css/simulator.css` | Ergänzungen zum Dracula-Theme |
|
||||
| `data/` | Quelle der Fragen: je Prüfung `exam.json` + `questions_*.json`, Schema unter `schema/` |
|
||||
| `tools/build_bank.py` | Prüfung und Build der Fragenbank (nur Python-Standardbibliothek) |
|
||||
| `tests/selftest.html` | Tests der Kernlogik inkl. ChaCha20-Testvektor RFC 8439 (44 Prüfungen) |
|
||||
| `tests/ui_smoke.html` | UI-Rauchtest im iframe: Lernmodus, Prüfungsmodus, Abgabe, Import (27 Prüfungen) |
|
||||
| `vendor/reveal/` | reveal.js 5.1.0 unverändert (MIT), Herkunft und Prüfsumme in `VERSION.txt` |
|
||||
|
||||
## Tests
|
||||
```bash
|
||||
cd simulator
|
||||
python3 tools/build_bank.py -c
|
||||
chromium --headless --allow-file-access-from-files --virtual-time-budget=5000 --dump-dom tests/selftest.html | grep -o 'RESULT[^<]*'
|
||||
chromium --headless --allow-file-access-from-files --virtual-time-budget=40000 --dump-dom tests/ui_smoke.html | grep -oE '(FAIL|RESULT)[^<]*'
|
||||
```
|
||||
`--allow-file-access-from-files` braucht nur der iframe-Test, nicht der Simulator selbst.
|
||||
|
||||
## Pflege
|
||||
- Fragen: Bediener und Agent; nach jeder Änderung `build_bank.py -c` und Build, `bank.js` mit einchecken.
|
||||
- Neue Objectives-Version: `exam.json` anpassen (Version, Abrufdatum), betroffene Fragen prüfen.
|
||||
- reveal.js-Update: neue `dist/`-Dateien nach `vendor/reveal/`, `VERSION.txt` aktualisieren, Tests laufen lassen.
|
||||
@@ -0,0 +1,200 @@
|
||||
/**
|
||||
* @file simulator.css
|
||||
* @brief Ergänzungen zum reveal.js-Dracula-Theme: scrollbare Fragen-Folien, Optionen, Auflösung, Werkzeugleiste
|
||||
* @author Björn Runden <br@runden.de>
|
||||
* @owner Björn Runden <br@runden.de>
|
||||
* @assist KI-unterstützt (Claude Opus 5.5), geprüft von Björn Runden <br@runden.de>
|
||||
* @version 0.1.1 (29.10.2026)
|
||||
*/
|
||||
|
||||
:root {
|
||||
--bg: #282a36;
|
||||
--line: #44475a;
|
||||
--fg: #f8f8f2;
|
||||
--comment: #6272a4;
|
||||
--cyan: #8be9fd;
|
||||
--green: #50fa7b;
|
||||
--orange: #ffb86c;
|
||||
--pink: #ff79c6;
|
||||
--purple: #bd93f9;
|
||||
--red: #ff5555;
|
||||
--yellow: #f1fa8c;
|
||||
--tb-h: 3rem;
|
||||
--gutter: max(16px, 4vw);
|
||||
/* reveal.js skaliert hier nicht (disableLayout) – Schrift relativ zum Fenster */
|
||||
--r-main-font-size: clamp(15px, 0.6vw + 11px, 21px);
|
||||
--r-heading-font: var(--r-main-font);
|
||||
}
|
||||
|
||||
html, body { background: var(--bg); }
|
||||
|
||||
/* ---------------------------------------------------------------- Werkzeugleiste */
|
||||
.toolbar {
|
||||
position: fixed;
|
||||
inset: 0 0 auto 0;
|
||||
z-index: 50;
|
||||
min-height: var(--tb-h);
|
||||
padding: calc(env(safe-area-inset-top, 0px) + 0.4rem) var(--gutter) 0.4rem;
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
gap: 0.4rem 1rem;
|
||||
background: #21222c;
|
||||
border-bottom: 1px solid var(--line);
|
||||
color: var(--fg);
|
||||
font: 15px/1.3 -apple-system, BlinkMacSystemFont, "Segoe UI", Ubuntu, Cantarell, Roboto, sans-serif;
|
||||
}
|
||||
.tb-title { color: var(--purple); font-weight: 600; }
|
||||
.tb-info { color: var(--cyan); font-variant-numeric: tabular-nums; }
|
||||
#tb-timer.low { color: var(--red); font-weight: 700; }
|
||||
.tb-buttons { margin-left: auto; display: flex; flex-wrap: wrap; gap: 0.3rem; }
|
||||
|
||||
/* ---------------------------------------------------------------- Buttons */
|
||||
.btn {
|
||||
font: inherit;
|
||||
font-size: 0.9em;
|
||||
color: var(--fg);
|
||||
background: var(--line);
|
||||
border: 1px solid var(--comment);
|
||||
border-radius: 6px;
|
||||
padding: 0.45em 1em;
|
||||
cursor: pointer;
|
||||
}
|
||||
.btn:hover:not(:disabled) { border-color: var(--purple); color: var(--purple); }
|
||||
.btn:disabled { opacity: 0.4; cursor: default; }
|
||||
.btn.primary { background: var(--purple); border-color: var(--purple); color: var(--bg); font-weight: 600; }
|
||||
.btn.primary:hover { background: var(--pink); border-color: var(--pink); color: var(--bg); }
|
||||
.btn.small { padding: 0.25em 0.7em; font-size: 0.85em; }
|
||||
|
||||
/* ---------------------------------------------------------------- Folien */
|
||||
.reveal .slides { text-align: left; }
|
||||
.reveal .slides > section {
|
||||
top: 0;
|
||||
height: 100%;
|
||||
overflow-y: auto;
|
||||
box-sizing: border-box;
|
||||
/* --tb-h wird per JS auf die tatsächliche (ggf. umbrochene) Höhe der Werkzeugleiste gesetzt */
|
||||
padding: calc(var(--tb-h) + 1.5rem) var(--gutter) 4.5rem;
|
||||
line-height: 1.45;
|
||||
}
|
||||
.reveal .slides > section > * { max-width: 62rem; margin-left: auto; margin-right: auto; }
|
||||
.reveal h1 { font-size: 2em; color: var(--purple); }
|
||||
.reveal h2 { font-size: 1.6em; color: var(--purple); }
|
||||
.reveal h3 { font-size: 1.15em; color: var(--pink); margin: 0 0 0.6em; }
|
||||
.reveal code { font-family: var(--r-code-font); color: var(--green); font-size: 0.92em; }
|
||||
.reveal small { color: var(--comment); font-size: 0.85em; }
|
||||
.reveal p { margin: 0 auto 0.8em; }
|
||||
|
||||
/* ---------------------------------------------------------------- Startseite */
|
||||
.card {
|
||||
background: rgba(68, 71, 90, 0.35);
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 10px;
|
||||
padding: 1.2em 1.4em;
|
||||
margin-bottom: 1.2em;
|
||||
}
|
||||
.card.resume { border-color: var(--orange); }
|
||||
.setup fieldset { border: 0; padding: 0; margin: 0 0 1em; }
|
||||
.setup legend { color: var(--cyan); font-weight: 600; margin-bottom: 0.3em; }
|
||||
.exam-group { margin-bottom: 0.4em; }
|
||||
.exam-level { display: block; color: var(--orange); font-size: 0.9em; }
|
||||
.choice { display: flex; gap: 0.6em; align-items: baseline; padding: 0.2em 0; cursor: pointer; }
|
||||
.choice small { display: inline-block; margin-left: 0.4em; }
|
||||
.inline { display: inline-flex; gap: 0.5em; align-items: center; margin-right: 1.5em; }
|
||||
.setup input[type=number], .setup select {
|
||||
font: inherit; color: var(--fg); background: var(--bg); border: 1px solid var(--comment);
|
||||
border-radius: 5px; padding: 0.2em 0.4em;
|
||||
}
|
||||
.setup input[type=number] { width: 5em; }
|
||||
input[type=radio], input[type=checkbox] { accent-color: var(--purple); width: 1.05em; height: 1.05em; }
|
||||
.footnote { color: var(--comment); font-size: 0.8em; }
|
||||
.warn-text { color: var(--orange); }
|
||||
|
||||
/* ---------------------------------------------------------------- Frage */
|
||||
.q-head {
|
||||
display: flex; flex-wrap: wrap; gap: 0.3em 1.2em; align-items: baseline;
|
||||
border-bottom: 1px solid var(--line); padding-bottom: 0.5em; margin-bottom: 1em; font-size: 0.85em;
|
||||
}
|
||||
.q-no { color: var(--purple); font-weight: 700; }
|
||||
.q-obj { color: var(--comment); }
|
||||
.flag { margin-left: auto; color: var(--orange); cursor: pointer; display: inline-flex; gap: 0.4em; align-items: center; }
|
||||
.q-text { font-size: 1.1em; margin-bottom: 0.8em; }
|
||||
.q-code {
|
||||
background: #21222c; border: 1px solid var(--line); border-radius: 6px; padding: 0.7em 1em;
|
||||
overflow-x: auto; font-size: 0.85em; width: auto; box-shadow: none; margin-bottom: 1em;
|
||||
}
|
||||
.reveal pre.q-code code { color: var(--fg); max-height: none; padding: 0; white-space: pre; }
|
||||
.q-hint { color: var(--yellow); font-style: italic; }
|
||||
|
||||
/* reveal-Theme setzt Listen auf inline-block mit Einzug – für Optionen/Erläuterungen aufheben */
|
||||
.reveal ol.options, .reveal ul.explain { display: block; list-style: none; padding: 0; margin: 0 auto 1em; }
|
||||
.reveal ol.options > li, .reveal ul.explain > li { margin-left: 0; }
|
||||
.option label {
|
||||
display: flex; gap: 0.8em; align-items: baseline; padding: 0.55em 0.9em; margin-bottom: 0.45em;
|
||||
border: 1px solid var(--line); border-radius: 8px; cursor: pointer; background: rgba(68, 71, 90, 0.25);
|
||||
}
|
||||
.option label:hover { border-color: var(--purple); }
|
||||
.option:has(input:checked) label { border-color: var(--purple); background: rgba(189, 147, 249, 0.12); }
|
||||
.letter { color: var(--cyan); font-weight: 700; min-width: 1.2em; }
|
||||
.locked .option label { cursor: default; }
|
||||
.locked .option.is-correct label { border-color: var(--green); background: rgba(80, 250, 123, 0.1); }
|
||||
.locked .option.is-wrong label { border-color: var(--red); background: rgba(255, 85, 85, 0.12); }
|
||||
.locked .option.is-missed label { border-style: dashed; }
|
||||
|
||||
input.fill {
|
||||
/* block statt inline-block: nur so greift margin:auto der Inhaltsspalte (sonst klebt das Feld links) */
|
||||
display: block;
|
||||
font-family: var(--r-code-font); font-size: 1em; width: 100%; box-sizing: border-box;
|
||||
color: var(--green); background: #21222c; border: 1px solid var(--comment); border-radius: 6px;
|
||||
padding: 0.5em 0.8em; margin-bottom: 1em;
|
||||
}
|
||||
input.fill:focus { outline: none; border-color: var(--purple); }
|
||||
input.fill.is-correct { border-color: var(--green); }
|
||||
input.fill.is-wrong { border-color: var(--red); color: var(--red); }
|
||||
|
||||
/* ---------------------------------------------------------------- Auflösung */
|
||||
.solution {
|
||||
border-left: 4px solid var(--pink); background: rgba(68, 71, 90, 0.3);
|
||||
border-radius: 0 8px 8px 0; padding: 1em 1.2em; margin-top: 0.5em;
|
||||
}
|
||||
.verdict { font-weight: 700; font-size: 1.15em; }
|
||||
.verdict.right { color: var(--green); }
|
||||
.verdict.wrong { color: var(--red); }
|
||||
.verdict.skipped { color: var(--orange); }
|
||||
.explain li { padding: 0.25em 0; }
|
||||
.explain .mark { font-weight: 600; margin-right: 0.3em; }
|
||||
.explain li.ok .mark { color: var(--green); }
|
||||
.explain li.no .mark { color: var(--red); }
|
||||
.explanation { color: var(--fg); border-top: 1px solid var(--line); padding-top: 0.6em; }
|
||||
.refs { color: var(--comment); font-size: 0.85em; margin-top: 0.6em; }
|
||||
|
||||
/* ---------------------------------------------------------------- Ergebnis */
|
||||
.big { font-size: 1.5em; color: var(--cyan); }
|
||||
.table-wrap { overflow-x: auto; }
|
||||
.reveal table { border-collapse: collapse; margin: 0 0 1em; font-size: 0.9em; }
|
||||
.reveal table th, .reveal table td { border-bottom: 1px solid var(--line); padding: 0.35em 0.8em; text-align: left; }
|
||||
.bar { display: inline-block; width: 6em; height: 0.6em; background: var(--line); border-radius: 3px; vertical-align: middle; }
|
||||
.bar > span { display: block; height: 100%; background: var(--green); border-radius: 3px; }
|
||||
.actions .btn { margin: 0 0.4em 0.4em 0; }
|
||||
|
||||
/* ---------------------------------------------------------------- Meldungen */
|
||||
.toast-box {
|
||||
position: fixed; z-index: 60; right: var(--gutter); bottom: calc(env(safe-area-inset-bottom, 0px) + 3.5rem);
|
||||
display: flex; flex-direction: column; gap: 0.4rem; max-width: min(28rem, calc(100vw - 32px));
|
||||
}
|
||||
.toast {
|
||||
background: var(--line); color: var(--fg); border-left: 4px solid var(--cyan); border-radius: 6px;
|
||||
padding: 0.6em 0.9em; font: 14px/1.35 -apple-system, BlinkMacSystemFont, "Segoe UI", Ubuntu, Roboto, sans-serif;
|
||||
box-shadow: 0 4px 14px rgba(0, 0, 0, 0.4);
|
||||
}
|
||||
.toast.warn { border-left-color: var(--orange); }
|
||||
.toast.error { border-left-color: var(--red); }
|
||||
|
||||
/* reveal-Steuerung nicht über den Inhalt legen */
|
||||
.reveal .controls { bottom: calc(env(safe-area-inset-bottom, 0px) + 12px); right: 12px; }
|
||||
|
||||
@media (max-width: 600px) {
|
||||
.tb-buttons { margin-left: 0; }
|
||||
.flag { margin-left: 0; }
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
{
|
||||
"meta": {
|
||||
"file": "exam.json",
|
||||
"brief": "Prüfungsdefinition LPIC-1 101-500: Eckdaten, Topics, Objectives mit Gewichtung",
|
||||
"author": "Björn Runden <br@runden.de>",
|
||||
"owner": "Björn Runden <br@runden.de>",
|
||||
"assist": "KI-unterstützt (Claude Opus 5.5), geprüft von Björn Runden <br@runden.de>",
|
||||
"version": "0.1.0 (29.10.2026)"
|
||||
},
|
||||
"exam": {
|
||||
"id": "lpic1-101",
|
||||
"level": "LPIC-1",
|
||||
"title": "LPIC-1 Prüfung 101",
|
||||
"code": "101-500",
|
||||
"lang": "de",
|
||||
"order": 101,
|
||||
"objectives_version": "5.0",
|
||||
"objectives_source": "https://www.lpi.org/de/exam-101-102-objectives/",
|
||||
"objectives_retrieved": "2026-10-29",
|
||||
"questions": 60,
|
||||
"duration_min": 90,
|
||||
"pass_percent": 65
|
||||
},
|
||||
"topics": [
|
||||
{
|
||||
"id": "101",
|
||||
"title": "Systemarchitektur",
|
||||
"objectives": [
|
||||
{ "id": "101.1", "title": "Hardwareeinstellungen ermitteln und konfigurieren", "weight": 2 },
|
||||
{ "id": "101.2", "title": "Das System starten", "weight": 3 },
|
||||
{ "id": "101.3", "title": "Runlevel wechseln und das System anhalten oder neu starten", "weight": 3 }
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "102",
|
||||
"title": "Linux-Installation und -Paketverwaltung",
|
||||
"objectives": [
|
||||
{ "id": "102.1", "title": "Festplattenaufteilung planen", "weight": 2 },
|
||||
{ "id": "102.2", "title": "Einen Bootmanager installieren", "weight": 2 },
|
||||
{ "id": "102.3", "title": "Shared Libraries verwalten", "weight": 1 },
|
||||
{ "id": "102.4", "title": "Debian-Paketverwaltung verwenden", "weight": 3 },
|
||||
{ "id": "102.5", "title": "RPM und YUM-Paketverwaltung verwenden", "weight": 3 },
|
||||
{ "id": "102.6", "title": "Linux als Virtualisierungs-Gast", "weight": 1 }
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "103",
|
||||
"title": "GNU- und Unix-Befehle",
|
||||
"objectives": [
|
||||
{ "id": "103.1", "title": "Auf der Befehlszeile arbeiten", "weight": 4 },
|
||||
{ "id": "103.2", "title": "Textströme mit Filtern verarbeiten", "weight": 2 },
|
||||
{ "id": "103.3", "title": "Grundlegende Dateiverwaltung", "weight": 4 },
|
||||
{ "id": "103.4", "title": "Ströme, Pipes und Umleitungen verwenden", "weight": 4 },
|
||||
{ "id": "103.5", "title": "Prozesse erzeugen, überwachen und beenden", "weight": 4 },
|
||||
{ "id": "103.6", "title": "Prozess-Ausführungsprioritäten ändern", "weight": 2 },
|
||||
{ "id": "103.7", "title": "Textdateien mit regulären Ausdrücken durchsuchen", "weight": 3 },
|
||||
{ "id": "103.8", "title": "Grundlegendes Editieren von Dateien", "weight": 3 }
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "104",
|
||||
"title": "Geräte, Linux-Dateisysteme, Filesystem Hierarchy Standard",
|
||||
"objectives": [
|
||||
{ "id": "104.1", "title": "Partitionen und Dateisysteme anlegen", "weight": 2 },
|
||||
{ "id": "104.2", "title": "Die Integrität von Dateisystemen sichern", "weight": 2 },
|
||||
{ "id": "104.3", "title": "Das Mounten und Unmounten von Dateisystemen steuern", "weight": 3 },
|
||||
{ "id": "104.5", "title": "Dateizugriffsrechte und -eigentümerschaft verwalten", "weight": 3 },
|
||||
{ "id": "104.6", "title": "Symbolische und Hardlinks anlegen und ändern", "weight": 2 },
|
||||
{ "id": "104.7", "title": "Systemdateien finden und Dateien am richtigen Ort platzieren", "weight": 2 }
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,173 @@
|
||||
{
|
||||
"$schema": "../schema/questions.schema.json",
|
||||
"format": "lpic-sim-questions",
|
||||
"meta": {
|
||||
"file": "questions_101.json",
|
||||
"brief": "Fragen zu Topic 101 Systemarchitektur (LPIC-1 101-500, Objectives 5.0)",
|
||||
"author": "Björn Runden <br@runden.de>",
|
||||
"owner": "Björn Runden <br@runden.de>",
|
||||
"assist": "KI-unterstützt (Claude Opus 5.5), geprüft von Björn Runden <br@runden.de>",
|
||||
"version": "0.1.0 (29.10.2026)"
|
||||
},
|
||||
"exam": "lpic1-101",
|
||||
"questions": [
|
||||
{
|
||||
"id": "101.1-001",
|
||||
"objective": "101.1",
|
||||
"type": "single",
|
||||
"difficulty": 1,
|
||||
"question": "Welcher Befehl listet alle aktuell geladenen Kernelmodule auf?",
|
||||
"options": [
|
||||
{ "id": "a", "text": "`lsmod`", "correct": true, "explanation": "`lsmod` formatiert den Inhalt von `/proc/modules`: Modulname, Größe und Nutzer." },
|
||||
{ "id": "b", "text": "`modinfo`", "correct": false, "explanation": "`modinfo` zeigt Informationen zu einem bestimmten Modul (Autor, Parameter, Datei), listet aber nicht die geladenen Module." },
|
||||
{ "id": "c", "text": "`lspci -k`", "correct": false, "explanation": "`lspci -k` zeigt nur zu PCI-Geräten den verwendeten Treiber, nicht alle geladenen Module." },
|
||||
{ "id": "d", "text": "`depmod -a`", "correct": false, "explanation": "`depmod` erzeugt die Abhängigkeitsdatei `modules.dep`, es listet nichts auf." },
|
||||
{ "id": "e", "text": "`insmod -l`", "correct": false, "explanation": "`insmod` lädt eine einzelne Moduldatei ohne Abhängigkeiten; eine Listen-Option gibt es nicht." }
|
||||
],
|
||||
"explanation": "Geladene Module stehen in `/proc/modules`; `lsmod` ist die lesbare Darstellung davon.",
|
||||
"references": ["man 8 lsmod", "/proc/modules"]
|
||||
},
|
||||
{
|
||||
"id": "101.1-002",
|
||||
"objective": "101.1",
|
||||
"type": "multi",
|
||||
"difficulty": 1,
|
||||
"question": "Welche Verzeichnisse sind Einhängepunkte virtueller Dateisysteme, über die der Kernel zur Laufzeit Informationen über Prozesse, Hardware und Treiber bereitstellt?",
|
||||
"options": [
|
||||
{ "id": "a", "text": "`/proc`", "correct": true, "explanation": "procfs: Prozessinformationen und Kernel-Daten wie `/proc/cpuinfo`, `/proc/interrupts`." },
|
||||
{ "id": "b", "text": "`/sys`", "correct": true, "explanation": "sysfs: Geräte, Treiber und Busse als Verzeichnisbaum; Grundlage für udev." },
|
||||
{ "id": "c", "text": "`/boot`", "correct": false, "explanation": "`/boot` ist ein normales Dateisystem mit Kernel, initramfs und Bootloader-Dateien." },
|
||||
{ "id": "d", "text": "`/etc`", "correct": false, "explanation": "`/etc` enthält statische Konfigurationsdateien auf der Festplatte." },
|
||||
{ "id": "e", "text": "`/var/lib`", "correct": false, "explanation": "`/var/lib` enthält dauerhafte Zustandsdaten von Programmen, keine Kernel-Schnittstelle." }
|
||||
],
|
||||
"explanation": "procfs (`/proc`) und sysfs (`/sys`) existieren nur im Speicher und werden vom Kernel beim Lesen erzeugt. Werkzeuge wie `lspci`, `lsusb` und udev lesen dort.",
|
||||
"references": ["man 5 proc", "man 5 sysfs"]
|
||||
},
|
||||
{
|
||||
"id": "101.1-003",
|
||||
"objective": "101.1",
|
||||
"type": "fill",
|
||||
"difficulty": 1,
|
||||
"question": "Mit welchem Befehl (nur der Befehlsname) lädt man ein Kernelmodul **mitsamt seinen Abhängigkeiten**?",
|
||||
"answers": ["modprobe"],
|
||||
"explanation": "`modprobe` löst Abhängigkeiten über `modules.dep` auf und lädt sie mit; `modprobe -r` entlädt. `insmod` lädt dagegen nur genau die angegebene Datei.",
|
||||
"references": ["man 8 modprobe"]
|
||||
},
|
||||
{
|
||||
"id": "101.2-001",
|
||||
"objective": "101.2",
|
||||
"type": "single",
|
||||
"difficulty": 1,
|
||||
"question": "Ein Administrator möchte die Meldungen des Kernel-Ring-Buffers (z. B. Hardware-Erkennung beim Start) ansehen. Welcher Befehl gibt diesen Puffer aus?",
|
||||
"options": [
|
||||
{ "id": "a", "text": "`dmesg`", "correct": true, "explanation": "`dmesg` liest den Kernel-Ring-Buffer und gibt ihn aus." },
|
||||
{ "id": "b", "text": "`uname -a`", "correct": false, "explanation": "`uname -a` zeigt Kernelname, Version, Architektur – keine Meldungen." },
|
||||
{ "id": "c", "text": "`lsmod`", "correct": false, "explanation": "`lsmod` listet geladene Module, nicht die Kernel-Meldungen." },
|
||||
{ "id": "d", "text": "`cat /proc/cmdline`", "correct": false, "explanation": "`/proc/cmdline` enthält nur die Kernel-Parameter des letzten Starts." },
|
||||
{ "id": "e", "text": "`last -x`", "correct": false, "explanation": "`last -x` zeigt Anmeldungen sowie Shutdown-/Runlevel-Einträge aus `wtmp`, nicht den Ring-Buffer." }
|
||||
],
|
||||
"explanation": "Der Ring-Buffer hat eine feste Größe; ältere Meldungen werden überschrieben. Bei systemd sind Kernel-Meldungen zusätzlich mit `journalctl -k` im Journal abrufbar.",
|
||||
"references": ["man 1 dmesg"]
|
||||
},
|
||||
{
|
||||
"id": "101.2-002",
|
||||
"objective": "101.2",
|
||||
"type": "fill",
|
||||
"difficulty": 2,
|
||||
"question": "In welcher Datei (vollständiger Pfad) kann man nachlesen, mit welchen Parametern der laufende Kernel gestartet wurde?",
|
||||
"answers": ["/proc/cmdline"],
|
||||
"explanation": "`/proc/cmdline` enthält die vom Bootloader übergebene Kernel-Befehlszeile, z. B. `root=…`, `ro`, `quiet`.",
|
||||
"references": ["man 5 proc"]
|
||||
},
|
||||
{
|
||||
"id": "101.2-003",
|
||||
"objective": "101.2",
|
||||
"type": "single",
|
||||
"difficulty": 2,
|
||||
"question": "In welcher Reihenfolge laufen die Phasen beim Start eines BIOS-Systems mit GRUB 2 und systemd ab?",
|
||||
"options": [
|
||||
{ "id": "a", "text": "Firmware (POST) → Bootloader → Kernel mit initramfs → systemd", "correct": true, "explanation": "Die Firmware prüft die Hardware und startet den Bootloader, der Kernel und initramfs lädt; der Kernel startet zuletzt den ersten Prozess." },
|
||||
{ "id": "b", "text": "Bootloader → Firmware (POST) → Kernel mit initramfs → systemd", "correct": false, "explanation": "Der Bootloader wird erst von der Firmware gestartet, kann also nicht vor ihr laufen." },
|
||||
{ "id": "c", "text": "Firmware (POST) → Kernel mit initramfs → Bootloader → systemd", "correct": false, "explanation": "Der Bootloader lädt den Kernel – er läuft daher vor dem Kernel." },
|
||||
{ "id": "d", "text": "Firmware (POST) → Bootloader → systemd → Kernel mit initramfs", "correct": false, "explanation": "systemd ist ein Userspace-Prozess und wird vom Kernel gestartet, nicht umgekehrt." },
|
||||
{ "id": "e", "text": "Firmware (POST) → initramfs → Bootloader → Kernel → systemd", "correct": false, "explanation": "Die initramfs wird vom Bootloader gemeinsam mit dem Kernel geladen und erst vom Kernel benutzt." }
|
||||
],
|
||||
"explanation": "Ablauf: Firmware (BIOS/UEFI) → Bootloader (GRUB 2) → Kernel + initramfs (Treiber, Root-Dateisystem einhängen) → init-Prozess mit PID 1 (systemd oder SysV-init).",
|
||||
"references": ["man 7 bootup"]
|
||||
},
|
||||
{
|
||||
"id": "101.2-004",
|
||||
"objective": "101.2",
|
||||
"type": "multi",
|
||||
"difficulty": 2,
|
||||
"question": "Welche Aussagen zur initramfs treffen zu?",
|
||||
"options": [
|
||||
{ "id": "a", "text": "Sie enthält Treiber und Werkzeuge, die nötig sind, um das eigentliche Root-Dateisystem einzuhängen.", "correct": true, "explanation": "Typisch sind Module für Storage-Controller, LVM, RAID oder Verschlüsselung." },
|
||||
{ "id": "b", "text": "Sie wird vom Bootloader zusammen mit dem Kernel in den Arbeitsspeicher geladen.", "correct": true, "explanation": "GRUB lädt Kernel und initramfs (Eintrag `initrd`) gemeinsam." },
|
||||
{ "id": "c", "text": "Sie ersetzt den Kernel, wenn dieser beschädigt ist.", "correct": false, "explanation": "Die initramfs ist ein Dateisystem-Abbild, kein Ersatzkernel." },
|
||||
{ "id": "d", "text": "Sie wird nur bei UEFI-Systemen verwendet.", "correct": false, "explanation": "Die initramfs ist unabhängig von der Firmware; sie wird bei BIOS und UEFI genutzt." },
|
||||
{ "id": "e", "text": "Sie ist unter `/sys/initramfs` dauerhaft eingehängt.", "correct": false, "explanation": "Nach dem Wechsel auf das echte Root-Dateisystem wird die initramfs verworfen; ihr Abbild liegt meist in `/boot`." }
|
||||
],
|
||||
"explanation": "Die initramfs ist ein (meist komprimiertes cpio-)Abbild eines kleinen Root-Dateisystems. Sie überbrückt die Zeit, bis der Kernel das echte Root-Dateisystem einhängen kann.",
|
||||
"references": ["man 8 dracut", "man 8 update-initramfs"]
|
||||
},
|
||||
{
|
||||
"id": "101.3-001",
|
||||
"objective": "101.3",
|
||||
"type": "single",
|
||||
"difficulty": 1,
|
||||
"question": "Welches systemd-Target entspricht dem SysV-Runlevel 3 (Mehrbenutzerbetrieb mit Netzwerk, ohne grafische Oberfläche)?",
|
||||
"options": [
|
||||
{ "id": "a", "text": "`multi-user.target`", "correct": true, "explanation": "`multi-user.target` entspricht den Runlevels 2–4." },
|
||||
{ "id": "b", "text": "`graphical.target`", "correct": false, "explanation": "`graphical.target` entspricht Runlevel 5 (mit grafischer Anmeldung)." },
|
||||
{ "id": "c", "text": "`rescue.target`", "correct": false, "explanation": "`rescue.target` entspricht Runlevel 1 (Einzelbenutzer)." },
|
||||
{ "id": "d", "text": "`emergency.target`", "correct": false, "explanation": "`emergency.target` startet nur eine Notfall-Shell, Root meist nur lesend eingehängt." },
|
||||
{ "id": "e", "text": "`network.target`", "correct": false, "explanation": "`network.target` ist ein Synchronisationspunkt für Netzwerkdienste, kein Systemzustand wie ein Runlevel." }
|
||||
],
|
||||
"explanation": "Zuordnung: 0 → `poweroff.target`, 1 → `rescue.target`, 2–4 → `multi-user.target`, 5 → `graphical.target`, 6 → `reboot.target`.",
|
||||
"references": ["man 7 systemd.special", "man 1 systemctl"]
|
||||
},
|
||||
{
|
||||
"id": "101.3-002",
|
||||
"objective": "101.3",
|
||||
"type": "fill",
|
||||
"difficulty": 2,
|
||||
"question": "Geben Sie den vollständigen Befehl ein, mit dem bei systemd das Standard-Target dauerhaft auf `graphical.target` gesetzt wird.",
|
||||
"answers": ["systemctl set-default graphical.target", "systemctl set-default graphical"],
|
||||
"explanation": "`systemctl set-default` setzt den Symlink `default.target`; wirksam beim nächsten Start. `systemctl get-default` zeigt das aktuelle Standard-Target.",
|
||||
"references": ["man 1 systemctl"]
|
||||
},
|
||||
{
|
||||
"id": "101.3-003",
|
||||
"objective": "101.3",
|
||||
"type": "single",
|
||||
"difficulty": 1,
|
||||
"question": "Das System soll in 10 Minuten heruntergefahren werden; angemeldete Benutzer sollen die Nachricht „Wartung“ erhalten. Welcher Befehl leistet das?",
|
||||
"options": [
|
||||
{ "id": "a", "text": "`shutdown -h +10 \"Wartung\"`", "correct": true, "explanation": "`-h` hält/schaltet aus, `+10` bedeutet in 10 Minuten, der Text geht per wall an alle Terminals." },
|
||||
{ "id": "b", "text": "`shutdown -c +10 \"Wartung\"`", "correct": false, "explanation": "`-c` bricht einen geplanten Shutdown ab." },
|
||||
{ "id": "c", "text": "`shutdown -r now +10 \"Wartung\"`", "correct": false, "explanation": "`now` und `+10` widersprechen sich; `-r` würde zudem neu starten." },
|
||||
{ "id": "d", "text": "`halt -t 10 \"Wartung\"`", "correct": false, "explanation": "`halt` kennt keine Zeitplanung mit Nachricht; es hält das System sofort an." },
|
||||
{ "id": "e", "text": "`wall -h +10 \"Wartung\"`", "correct": false, "explanation": "`wall` sendet nur eine Nachricht, fährt aber nichts herunter." }
|
||||
],
|
||||
"explanation": "Zeitangabe bei `shutdown`: `now`, `+m` (Minuten) oder `hh:mm`. `-h`/`-P` ausschalten, `-r` Neustart, `-c` abbrechen.",
|
||||
"references": ["man 8 shutdown"]
|
||||
},
|
||||
{
|
||||
"id": "101.3-004",
|
||||
"objective": "101.3",
|
||||
"type": "multi",
|
||||
"difficulty": 2,
|
||||
"question": "Mit welchen Befehlen wechselt ein laufendes System **sofort** in den Einzelbenutzer-/Rettungsmodus?",
|
||||
"options": [
|
||||
{ "id": "a", "text": "`systemctl isolate rescue.target`", "correct": true, "explanation": "`isolate` startet das Target und beendet alle Units, die nicht dazugehören." },
|
||||
{ "id": "b", "text": "`telinit 1`", "correct": true, "explanation": "`telinit 1` (bzw. `init 1`) wechselt in Runlevel 1; unter systemd wird das auf `rescue.target` abgebildet." },
|
||||
{ "id": "c", "text": "`systemctl set-default rescue.target`", "correct": false, "explanation": "Ändert nur das Standard-Target für den nächsten Start." },
|
||||
{ "id": "d", "text": "`runlevel 1`", "correct": false, "explanation": "`runlevel` zeigt nur den vorherigen und aktuellen Runlevel an; es nimmt keinen Wechsel vor." },
|
||||
{ "id": "e", "text": "`systemctl reload rescue.target`", "correct": false, "explanation": "`reload` lässt laufende Dienste ihre Konfiguration neu lesen; ein Targetwechsel ist das nicht." }
|
||||
],
|
||||
"explanation": "Sofortiger Wechsel: `systemctl isolate <target>` bzw. klassisch `telinit <n>`/`init <n>`. Dauerhaft für den Start: `systemctl set-default`.",
|
||||
"references": ["man 1 systemctl", "man 8 telinit"]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,317 @@
|
||||
{
|
||||
"$schema": "../schema/questions.schema.json",
|
||||
"format": "lpic-sim-questions",
|
||||
"meta": {
|
||||
"file": "questions_102.json",
|
||||
"brief": "Fragen zu Topic 102 Linux-Installation und Paketverwaltung (LPIC-1 101-500, Objectives 5.0)",
|
||||
"author": "Björn Runden <br@runden.de>",
|
||||
"owner": "Björn Runden <br@runden.de>",
|
||||
"assist": "KI-unterstützt (Claude Opus 5.5), geprüft von Björn Runden <br@runden.de>",
|
||||
"version": "0.1.0 (29.10.2026)"
|
||||
},
|
||||
"exam": "lpic1-101",
|
||||
"questions": [
|
||||
{
|
||||
"id": "102.1-001",
|
||||
"objective": "102.1",
|
||||
"type": "single",
|
||||
"difficulty": 1,
|
||||
"question": "Welches Verzeichnis legt man typischerweise auf eine eigene Partition, damit wachsende Protokoll- und Spool-Dateien das Root-Dateisystem nicht füllen?",
|
||||
"options": [
|
||||
{ "id": "a", "text": "`/var`", "correct": true, "explanation": "`/var` enthält variable Daten wie Logs, Spool und Caches, die unkontrolliert wachsen können." },
|
||||
{ "id": "b", "text": "`/etc`", "correct": false, "explanation": "`/etc` wird schon früh beim Start gebraucht und muss auf dem Root-Dateisystem liegen; es wächst kaum." },
|
||||
{ "id": "c", "text": "`/bin`", "correct": false, "explanation": "`/bin` enthält grundlegende Programme, die beim Start verfügbar sein müssen." },
|
||||
{ "id": "d", "text": "`/sbin`", "correct": false, "explanation": "`/sbin` enthält Systemprogramme für Start und Reparatur und gehört auf das Root-Dateisystem." },
|
||||
{ "id": "e", "text": "`/lib`", "correct": false, "explanation": "`/lib` enthält Bibliotheken und Module, die für den Start nötig sind." }
|
||||
],
|
||||
"explanation": "Kandidaten für eigene Partitionen: `/var`, `/home`, `/tmp`, ggf. `/boot` und `/opt`. Was zum Start nötig ist (`/etc`, `/bin`, `/sbin`, `/lib`, `/dev`), bleibt auf dem Root-Dateisystem.",
|
||||
"references": ["man 7 hier"]
|
||||
},
|
||||
{
|
||||
"id": "102.1-002",
|
||||
"objective": "102.1",
|
||||
"type": "multi",
|
||||
"difficulty": 2,
|
||||
"question": "Welche Aussagen zum Logical Volume Manager (LVM) treffen zu?",
|
||||
"options": [
|
||||
{ "id": "a", "text": "Logical Volumes werden aus dem Speicher einer Volume Group zugeteilt.", "correct": true, "explanation": "Hierarchie: Physical Volume → Volume Group → Logical Volume." },
|
||||
{ "id": "b", "text": "Eine Volume Group kann mehrere Physical Volumes umfassen.", "correct": true, "explanation": "Mehrere Platten/Partitionen lassen sich zu einer Volume Group zusammenfassen und mit `vgextend` erweitern." },
|
||||
{ "id": "c", "text": "Ein Physical Volume kann immer nur genau ein Logical Volume aufnehmen.", "correct": false, "explanation": "Die Extents eines Physical Volumes können auf beliebig viele Logical Volumes verteilt sein." },
|
||||
{ "id": "d", "text": "LVM setzt zwingend eine GPT-Partitionstabelle voraus.", "correct": false, "explanation": "LVM funktioniert mit MBR, GPT oder auch ganzen Platten ohne Partitionstabelle." },
|
||||
{ "id": "e", "text": "Logical Volumes können nur vergrößert werden, wenn das System neu gestartet wird.", "correct": false, "explanation": "`lvextend` vergrößert im laufenden Betrieb; viele Dateisysteme lassen sich anschließend online wachsen." }
|
||||
],
|
||||
"explanation": "LVM entkoppelt Dateisysteme von physischen Partitionen: `pvcreate` → `vgcreate` → `lvcreate`. Vorteil: flexible Größenänderung und Snapshots.",
|
||||
"references": ["man 8 lvm"]
|
||||
},
|
||||
{
|
||||
"id": "102.1-003",
|
||||
"objective": "102.1",
|
||||
"type": "single",
|
||||
"difficulty": 1,
|
||||
"question": "Mit welchem Dateisystem muss die EFI-Systempartition (ESP) eines UEFI-Systems formatiert sein?",
|
||||
"options": [
|
||||
{ "id": "a", "text": "FAT (üblich FAT32)", "correct": true, "explanation": "Die UEFI-Spezifikation verlangt ein FAT-Dateisystem, damit die Firmware die Bootloader-Dateien lesen kann." },
|
||||
{ "id": "b", "text": "ext4", "correct": false, "explanation": "ext4 kann die Firmware in der Regel nicht lesen; es eignet sich für `/` oder `/boot`." },
|
||||
{ "id": "c", "text": "XFS", "correct": false, "explanation": "XFS wird von UEFI-Firmware nicht unterstützt." },
|
||||
{ "id": "d", "text": "swap", "correct": false, "explanation": "swap ist Auslagerungsspeicher, kein Dateisystem für Dateien." },
|
||||
{ "id": "e", "text": "Btrfs", "correct": false, "explanation": "Btrfs kann die Firmware nicht lesen." }
|
||||
],
|
||||
"explanation": "Die ESP (meist eingehängt unter `/boot/efi`) enthält die `.efi`-Bootloader und ist FAT-formatiert.",
|
||||
"references": ["UEFI-Spezifikation, Abschnitt EFI System Partition"]
|
||||
},
|
||||
{
|
||||
"id": "102.2-001",
|
||||
"objective": "102.2",
|
||||
"type": "single",
|
||||
"difficulty": 1,
|
||||
"question": "Auf einem Debian-System wurde `/etc/default/grub` geändert (z. B. `GRUB_TIMEOUT`). Welcher Befehl übernimmt die Änderung in die GRUB-2-Konfiguration?",
|
||||
"options": [
|
||||
{ "id": "a", "text": "`update-grub`", "correct": true, "explanation": "`update-grub` ist unter Debian ein Wrapper für `grub-mkconfig -o /boot/grub/grub.cfg`." },
|
||||
{ "id": "b", "text": "`grub-mkconfig`", "correct": false, "explanation": "Ohne `-o <datei>` gibt `grub-mkconfig` die Konfiguration nur auf stdout aus und schreibt nichts." },
|
||||
{ "id": "c", "text": "`grub-install /etc/default/grub`", "correct": false, "explanation": "`grub-install` installiert den Bootloader auf ein Gerät; es erwartet ein Gerät, keine Konfigurationsdatei." },
|
||||
{ "id": "d", "text": "`systemctl restart grub`", "correct": false, "explanation": "GRUB ist kein Dienst; es läuft nur beim Start, vor dem Kernel." },
|
||||
{ "id": "e", "text": "`lilo`", "correct": false, "explanation": "`lilo` gehört zum veralteten Bootloader LILO und wertet `/etc/lilo.conf` aus." }
|
||||
],
|
||||
"explanation": "GRUB 2: Einstellungen in `/etc/default/grub` und `/etc/grub.d/`, daraus erzeugt `grub-mkconfig` die Datei `grub.cfg`, die man nicht von Hand bearbeitet.",
|
||||
"references": ["man 8 grub-mkconfig", "info grub"]
|
||||
},
|
||||
{
|
||||
"id": "102.2-002",
|
||||
"objective": "102.2",
|
||||
"type": "fill",
|
||||
"difficulty": 2,
|
||||
"question": "Geben Sie den vollständigen Befehl ein, der die GRUB-2-Konfiguration neu erzeugt und direkt nach `/boot/grub/grub.cfg` schreibt (ohne den Debian-Wrapper `update-grub`).",
|
||||
"answers": ["grub-mkconfig -o /boot/grub/grub.cfg", "grub-mkconfig --output=/boot/grub/grub.cfg", "grub2-mkconfig -o /boot/grub2/grub.cfg"],
|
||||
"explanation": "`grub-mkconfig -o` schreibt die erzeugte Konfiguration in die Datei. Distributionen der Red-Hat-Familie heißen das Werkzeug `grub2-mkconfig` und nutzen `/boot/grub2/grub.cfg`.",
|
||||
"references": ["man 8 grub-mkconfig"]
|
||||
},
|
||||
{
|
||||
"id": "102.2-003",
|
||||
"objective": "102.2",
|
||||
"type": "single",
|
||||
"difficulty": 2,
|
||||
"question": "Wie heißt die Menü-Konfigurationsdatei von GRUB Legacy (Version 0.9x) üblicherweise?",
|
||||
"options": [
|
||||
{ "id": "a", "text": "`/boot/grub/menu.lst`", "correct": true, "explanation": "GRUB Legacy liest `menu.lst` (bei Red Hat auch `grub.conf`) direkt beim Start." },
|
||||
{ "id": "b", "text": "`/boot/grub/grub.cfg`", "correct": false, "explanation": "`grub.cfg` ist die generierte Konfiguration von GRUB 2." },
|
||||
{ "id": "c", "text": "`/etc/default/grub`", "correct": false, "explanation": "`/etc/default/grub` enthält die Einstellungen, aus denen GRUB 2 seine `grub.cfg` erzeugt." },
|
||||
{ "id": "d", "text": "`/etc/lilo.conf`", "correct": false, "explanation": "Das ist die Konfiguration des Bootloaders LILO." },
|
||||
{ "id": "e", "text": "`/boot/efi/grub.ini`", "correct": false, "explanation": "Eine solche Datei gibt es bei GRUB nicht." }
|
||||
],
|
||||
"explanation": "Unterschied: GRUB Legacy wird direkt über `menu.lst` konfiguriert (Partitionen ab `(hd0,0)` gezählt), GRUB 2 über generierte `grub.cfg` (Partitionen ab 1, z. B. `(hd0,1)`).",
|
||||
"references": ["info grub"]
|
||||
},
|
||||
{
|
||||
"id": "102.3-001",
|
||||
"objective": "102.3",
|
||||
"type": "single",
|
||||
"difficulty": 1,
|
||||
"question": "Welcher Befehl zeigt an, welche Shared Libraries ein Programm wie `/usr/bin/ssh` benötigt?",
|
||||
"options": [
|
||||
{ "id": "a", "text": "`ldd /usr/bin/ssh`", "correct": true, "explanation": "`ldd` listet die benötigten Bibliotheken und den Pfad, über den sie aufgelöst werden." },
|
||||
{ "id": "b", "text": "`ldconfig /usr/bin/ssh`", "correct": false, "explanation": "`ldconfig` aktualisiert Links und den Cache `/etc/ld.so.cache`; es analysiert keine Programme." },
|
||||
{ "id": "c", "text": "`ldconfig -p`", "correct": false, "explanation": "`ldconfig -p` zeigt alle Bibliotheken im Cache, nicht die eines bestimmten Programms." },
|
||||
{ "id": "d", "text": "`ld /usr/bin/ssh`", "correct": false, "explanation": "`ld` ist der Linker zum Bauen von Programmen." },
|
||||
{ "id": "e", "text": "`lsmod /usr/bin/ssh`", "correct": false, "explanation": "`lsmod` listet Kernelmodule, keine Bibliotheken." }
|
||||
],
|
||||
"explanation": "Der dynamische Linker (`ld-linux.so`) lädt beim Start die Bibliotheken. Er sucht über `LD_LIBRARY_PATH`, den Cache `/etc/ld.so.cache` und die Standardpfade.",
|
||||
"references": ["man 1 ldd", "man 8 ld.so"]
|
||||
},
|
||||
{
|
||||
"id": "102.3-002",
|
||||
"objective": "102.3",
|
||||
"type": "multi",
|
||||
"difficulty": 2,
|
||||
"question": "Eine Bibliothek wurde nach `/opt/app/lib` installiert und soll **dauerhaft für alle Programme** gefunden werden. Welche Schritte sind nötig?",
|
||||
"options": [
|
||||
{ "id": "a", "text": "Den Pfad in eine Datei unter `/etc/ld.so.conf.d/` eintragen", "correct": true, "explanation": "`/etc/ld.so.conf` bindet die Dateien aus `ld.so.conf.d/` ein; dort stehen zusätzliche Suchpfade." },
|
||||
{ "id": "b", "text": "`ldconfig` ausführen", "correct": true, "explanation": "`ldconfig` liest die Pfade ein und erzeugt den Cache `/etc/ld.so.cache` neu." },
|
||||
{ "id": "c", "text": "`ldd /opt/app/lib` ausführen", "correct": false, "explanation": "`ldd` zeigt nur Abhängigkeiten eines Programms an und ändert nichts." },
|
||||
{ "id": "d", "text": "`LD_LIBRARY_PATH` in `/etc/ld.so.conf` setzen", "correct": false, "explanation": "In `ld.so.conf` stehen Pfade, keine Variablenzuweisungen. `LD_LIBRARY_PATH` ist eine Umgebungsvariable der Shell." },
|
||||
{ "id": "e", "text": "Die Bibliothek nach `/proc/lib` kopieren", "correct": false, "explanation": "`/proc` ist ein virtuelles Dateisystem; dorthin kann man nichts dauerhaft kopieren." }
|
||||
],
|
||||
"explanation": "Dauerhaft: Pfad in `/etc/ld.so.conf(.d)` und `ldconfig`. Nur vorübergehend, für eine Sitzung oder einen Aufruf: `export LD_LIBRARY_PATH=/opt/app/lib`.",
|
||||
"references": ["man 8 ldconfig"]
|
||||
},
|
||||
{
|
||||
"id": "102.4-001",
|
||||
"objective": "102.4",
|
||||
"type": "single",
|
||||
"difficulty": 1,
|
||||
"question": "Welcher Befehl listet alle Dateien auf, die das **installierte** Debian-Paket `openssh-server` auf dem System abgelegt hat?",
|
||||
"options": [
|
||||
{ "id": "a", "text": "`dpkg -L openssh-server`", "correct": true, "explanation": "`-L` (`--listfiles`) listet die Dateien eines installierten Pakets." },
|
||||
{ "id": "b", "text": "`dpkg -l openssh-server`", "correct": false, "explanation": "`-l` zeigt nur Status, Version und Beschreibung in Kurzform." },
|
||||
{ "id": "c", "text": "`dpkg -S openssh-server`", "correct": false, "explanation": "`-S` sucht, zu welchem Paket ein Dateiname gehört." },
|
||||
{ "id": "d", "text": "`dpkg -c openssh-server`", "correct": false, "explanation": "`-c` listet den Inhalt einer `.deb`-Datei, nicht eines installierten Pakets." },
|
||||
{ "id": "e", "text": "`dpkg -s openssh-server`", "correct": false, "explanation": "`-s` zeigt Status und Metadaten, aber keine Dateiliste." }
|
||||
],
|
||||
"explanation": "Merkhilfe: `-L` Liste der Dateien, `-l` Liste der Pakete, `-S` Suche nach Datei, `-s` Status, `-c` Inhalt einer .deb, `-I` Info zu einer .deb.",
|
||||
"references": ["man 1 dpkg"]
|
||||
},
|
||||
{
|
||||
"id": "102.4-002",
|
||||
"objective": "102.4",
|
||||
"type": "fill",
|
||||
"difficulty": 2,
|
||||
"question": "Mit welchem dpkg-Aufruf (Befehl und Option, **ohne** Dateinamen) findet man heraus, zu welchem installierten Paket die Datei `/usr/bin/ls` gehört?",
|
||||
"answers": ["dpkg -S", "dpkg --search"],
|
||||
"explanation": "`dpkg -S /usr/bin/ls` liefert z. B. `coreutils: /usr/bin/ls`. Für nicht installierte Pakete hilft `apt-file search`.",
|
||||
"references": ["man 1 dpkg"]
|
||||
},
|
||||
{
|
||||
"id": "102.4-003",
|
||||
"objective": "102.4",
|
||||
"type": "single",
|
||||
"difficulty": 1,
|
||||
"question": "Welcher Befehl lädt nur die aktuellen Paketlisten aus den konfigurierten Quellen, ohne Pakete zu installieren oder zu aktualisieren?",
|
||||
"options": [
|
||||
{ "id": "a", "text": "`apt-get update`", "correct": true, "explanation": "`update` aktualisiert den Paketindex aus `/etc/apt/sources.list(.d)`." },
|
||||
{ "id": "b", "text": "`apt-get upgrade`", "correct": false, "explanation": "`upgrade` installiert neuere Versionen bereits installierter Pakete." },
|
||||
{ "id": "c", "text": "`apt-get dist-upgrade`", "correct": false, "explanation": "`dist-upgrade` aktualisiert Pakete und darf dabei auch Pakete hinzufügen oder entfernen." },
|
||||
{ "id": "d", "text": "`apt-cache update`", "correct": false, "explanation": "`apt-cache` fragt nur den Cache ab; einen Unterbefehl `update` gibt es nicht." },
|
||||
{ "id": "e", "text": "`apt-get check`", "correct": false, "explanation": "`check` prüft auf defekte Abhängigkeiten und lädt nichts herunter." }
|
||||
],
|
||||
"explanation": "Typische Reihenfolge: `apt-get update` (Listen), dann `apt-get upgrade` (Pakete). Paketquellen stehen in `/etc/apt/sources.list` und `/etc/apt/sources.list.d/`.",
|
||||
"references": ["man 8 apt-get"]
|
||||
},
|
||||
{
|
||||
"id": "102.4-004",
|
||||
"objective": "102.4",
|
||||
"type": "multi",
|
||||
"difficulty": 2,
|
||||
"question": "Mit welchen Befehlen wird ein Paket **einschließlich seiner Konfigurationsdateien** entfernt?",
|
||||
"options": [
|
||||
{ "id": "a", "text": "`apt-get purge paket`", "correct": true, "explanation": "`purge` entfernt das Paket und seine Konfigurationsdateien." },
|
||||
{ "id": "b", "text": "`dpkg -P paket`", "correct": true, "explanation": "`-P` (`--purge`) entfernt alles einschließlich Konfiguration." },
|
||||
{ "id": "c", "text": "`dpkg -r paket`", "correct": false, "explanation": "`-r` (`--remove`) lässt die Konfigurationsdateien stehen (Status `rc`)." },
|
||||
{ "id": "d", "text": "`apt-get remove paket`", "correct": false, "explanation": "`remove` entspricht `dpkg -r`: Konfigurationsdateien bleiben erhalten." },
|
||||
{ "id": "e", "text": "`apt-get autoremove paket`", "correct": false, "explanation": "`autoremove` entfernt automatisch installierte, nicht mehr benötigte Abhängigkeiten; Konfigurationen bleiben." }
|
||||
],
|
||||
"explanation": "Pakete im Zustand `rc` (removed, config-files) zeigt `dpkg -l`. Sie lassen sich nachträglich mit `dpkg -P` vollständig entfernen.",
|
||||
"references": ["man 1 dpkg", "man 8 apt-get"]
|
||||
},
|
||||
{
|
||||
"id": "102.4-005",
|
||||
"objective": "102.4",
|
||||
"type": "single",
|
||||
"difficulty": 2,
|
||||
"question": "Bei der Installation eines Pakets wurden debconf-Fragen beantwortet. Wie lassen sich diese Fragen **erneut** stellen und die Konfiguration neu erzeugen?",
|
||||
"options": [
|
||||
{ "id": "a", "text": "`dpkg-reconfigure paket`", "correct": true, "explanation": "`dpkg-reconfigure` stellt die debconf-Fragen erneut und konfiguriert das Paket neu." },
|
||||
{ "id": "b", "text": "`dpkg --configure paket`", "correct": false, "explanation": "`--configure` schließt die Konfiguration entpackter Pakete ab; bereits beantwortete Fragen werden nicht erneut gestellt." },
|
||||
{ "id": "c", "text": "`apt-get install --reinstall paket`", "correct": false, "explanation": "Die Antworten liegen in der debconf-Datenbank und werden bei der Neuinstallation wiederverwendet." },
|
||||
{ "id": "d", "text": "`apt-cache policy paket`", "correct": false, "explanation": "`policy` zeigt installierte und verfügbare Versionen sowie die Prioritäten der Quellen." },
|
||||
{ "id": "e", "text": "`debconf-show paket`", "correct": false, "explanation": "`debconf-show` zeigt die gespeicherten Antworten nur an." }
|
||||
],
|
||||
"explanation": "debconf speichert Antworten dauerhaft. Typisches Beispiel: `dpkg-reconfigure tzdata` oder `dpkg-reconfigure locales`.",
|
||||
"references": ["man 8 dpkg-reconfigure"]
|
||||
},
|
||||
{
|
||||
"id": "102.5-001",
|
||||
"objective": "102.5",
|
||||
"type": "single",
|
||||
"difficulty": 1,
|
||||
"question": "Welcher rpm-Befehl ermittelt, zu welchem installierten Paket die Datei `/etc/ssh/sshd_config` gehört?",
|
||||
"options": [
|
||||
{ "id": "a", "text": "`rpm -qf /etc/ssh/sshd_config`", "correct": true, "explanation": "`-q` Abfrage, `-f` (file): Paket, dem die Datei gehört." },
|
||||
{ "id": "b", "text": "`rpm -ql /etc/ssh/sshd_config`", "correct": false, "explanation": "`-ql` listet die Dateien eines Pakets; es erwartet einen Paketnamen." },
|
||||
{ "id": "c", "text": "`rpm -qi /etc/ssh/sshd_config`", "correct": false, "explanation": "`-qi` zeigt Informationen zu einem Paket; es erwartet einen Paketnamen." },
|
||||
{ "id": "d", "text": "`rpm -V /etc/ssh/sshd_config`", "correct": false, "explanation": "`-V` prüft installierte Pakete auf Änderungen gegenüber der RPM-Datenbank." },
|
||||
{ "id": "e", "text": "`rpm -qa /etc/ssh/sshd_config`", "correct": false, "explanation": "`-qa` listet alle installierten Pakete (das Argument wird als Muster gelesen, nicht als Datei)." }
|
||||
],
|
||||
"explanation": "Abfragen beginnen mit `-q`: `-qa` alle, `-qi` Info, `-ql` Dateien, `-qf` Besitzer einer Datei, `-qc` Konfigurationsdateien, `-qR` Abhängigkeiten, `-qp` für eine .rpm-Datei.",
|
||||
"references": ["man 8 rpm"]
|
||||
},
|
||||
{
|
||||
"id": "102.5-002",
|
||||
"objective": "102.5",
|
||||
"type": "fill",
|
||||
"difficulty": 1,
|
||||
"question": "Geben Sie den rpm-Befehl ein, der alle Dateien des installierten Pakets `bash` auflistet.",
|
||||
"answers": ["rpm -ql bash", "rpm -q -l bash", "rpm -q --list bash", "rpm --query --list bash"],
|
||||
"explanation": "`-q` (query) mit `-l` (list). Für eine noch nicht installierte Datei: `rpm -qlp paket.rpm`.",
|
||||
"references": ["man 8 rpm"]
|
||||
},
|
||||
{
|
||||
"id": "102.5-003",
|
||||
"objective": "102.5",
|
||||
"type": "single",
|
||||
"difficulty": 2,
|
||||
"question": "Welcher rpm-Aufruf aktualisiert ein Paket aus einer .rpm-Datei – und installiert es, falls es noch nicht vorhanden ist?",
|
||||
"options": [
|
||||
{ "id": "a", "text": "`rpm -Uvh paket.rpm`", "correct": true, "explanation": "`-U` (upgrade) aktualisiert oder installiert; `-v` ausführlich, `-h` Fortschrittsbalken." },
|
||||
{ "id": "b", "text": "`rpm -Fvh paket.rpm`", "correct": false, "explanation": "`-F` (freshen) aktualisiert nur, wenn das Paket bereits installiert ist." },
|
||||
{ "id": "c", "text": "`rpm -ivh paket.rpm`", "correct": false, "explanation": "`-i` installiert neu; bei installierter älterer Version entsteht ein Konflikt (Ausnahme: z. B. Kernel parallel)." },
|
||||
{ "id": "d", "text": "`rpm -e paket.rpm`", "correct": false, "explanation": "`-e` (erase) entfernt ein Paket." },
|
||||
{ "id": "e", "text": "`rpm -Vp paket.rpm`", "correct": false, "explanation": "`-V` prüft, es installiert nichts." }
|
||||
],
|
||||
"explanation": "rpm löst keine Abhängigkeiten aus Repositories auf – dafür gibt es `dnf`/`yum` bzw. `zypper`.",
|
||||
"references": ["man 8 rpm"]
|
||||
},
|
||||
{
|
||||
"id": "102.5-004",
|
||||
"objective": "102.5",
|
||||
"type": "multi",
|
||||
"difficulty": 1,
|
||||
"question": "Welche Werkzeuge installieren RPM-Pakete aus Repositories und lösen dabei Abhängigkeiten automatisch auf?",
|
||||
"options": [
|
||||
{ "id": "a", "text": "`dnf`", "correct": true, "explanation": "Nachfolger von yum, Standard bei Fedora und RHEL ab Version 8." },
|
||||
{ "id": "b", "text": "`yum`", "correct": true, "explanation": "Klassisches Frontend der Red-Hat-Familie; Repos in `/etc/yum.repos.d/`." },
|
||||
{ "id": "c", "text": "`zypper`", "correct": true, "explanation": "Paketverwaltung von openSUSE/SLES, ebenfalls auf RPM-Basis." },
|
||||
{ "id": "d", "text": "`rpm2cpio`", "correct": false, "explanation": "`rpm2cpio` wandelt ein RPM in ein cpio-Archiv um, installiert aber nichts." },
|
||||
{ "id": "e", "text": "`dpkg`", "correct": false, "explanation": "`dpkg` verwaltet Debian-Pakete und löst keine Abhängigkeiten aus Repositories auf." }
|
||||
],
|
||||
"explanation": "rpm ist die Basisschicht, `dnf`/`yum`/`zypper` sind die Frontends mit Repository- und Abhängigkeitsverwaltung (vergleichbar mit dpkg und apt).",
|
||||
"references": ["man 8 dnf", "man 8 zypper"]
|
||||
},
|
||||
{
|
||||
"id": "102.5-005",
|
||||
"objective": "102.5",
|
||||
"type": "single",
|
||||
"difficulty": 2,
|
||||
"question": "Wie entpackt man die Dateien aus `paket.rpm` in das aktuelle Verzeichnis, **ohne** das Paket zu installieren?",
|
||||
"options": [
|
||||
{ "id": "a", "text": "`rpm2cpio paket.rpm | cpio -idmv`", "correct": true, "explanation": "`rpm2cpio` gibt den Inhalt als cpio-Strom aus, `cpio -i` entpackt (`-d` Verzeichnisse anlegen, `-m` Zeiten erhalten)." },
|
||||
{ "id": "b", "text": "`rpm -x paket.rpm`", "correct": false, "explanation": "rpm hat keine Option `-x` zum Entpacken." },
|
||||
{ "id": "c", "text": "`cpio -i < paket.rpm`", "correct": false, "explanation": "Eine RPM-Datei hat einen eigenen Header vor dem Archiv; cpio kann sie nicht direkt lesen." },
|
||||
{ "id": "d", "text": "`tar xf paket.rpm`", "correct": false, "explanation": "RPM ist kein tar-Archiv." },
|
||||
{ "id": "e", "text": "`rpm -qlp paket.rpm`", "correct": false, "explanation": "Das listet nur die enthaltenen Dateien auf, entpackt aber nichts." }
|
||||
],
|
||||
"explanation": "Nützlich, um einzelne Dateien (z. B. eine Original-Konfiguration) aus einem Paket zu holen.",
|
||||
"references": ["man 8 rpm2cpio", "man 1 cpio"]
|
||||
},
|
||||
{
|
||||
"id": "102.6-001",
|
||||
"objective": "102.6",
|
||||
"type": "multi",
|
||||
"difficulty": 2,
|
||||
"question": "Eine Linux-VM wird aus einer Vorlage geklont. Was sollte auf jedem Klon **neu erzeugt** werden, weil es eindeutig sein muss?",
|
||||
"options": [
|
||||
{ "id": "a", "text": "Die D-Bus-/systemd-Maschinen-ID in `/etc/machine-id`", "correct": true, "explanation": "Die Machine-ID identifiziert das System (u. a. Journal, DHCP-Client-ID) und darf nicht doppelt vorkommen." },
|
||||
{ "id": "b", "text": "Die SSH-Host-Schlüssel unter `/etc/ssh/ssh_host_*`", "correct": true, "explanation": "Gleiche Host-Schlüssel auf mehreren Systemen untergraben die Server-Authentifizierung." },
|
||||
{ "id": "c", "text": "Den Kernel unter `/boot`", "correct": false, "explanation": "Der Kernel ist nicht systemspezifisch und kann identisch bleiben." },
|
||||
{ "id": "d", "text": "Die Datei `/etc/services`", "correct": false, "explanation": "`/etc/services` ordnet Portnummern Dienstnamen zu und ist überall gleich." },
|
||||
{ "id": "e", "text": "Die Shared Libraries unter `/usr/lib`", "correct": false, "explanation": "Bibliotheken sind Programmdateien und nicht systemspezifisch." }
|
||||
],
|
||||
"explanation": "Vorlagen werden vorher \"generalisiert\": Machine-ID leeren, SSH-Host-Schlüssel löschen, ggf. Hostname und MAC-gebundene Netzkonfiguration zurücksetzen. cloud-init oder ein Erststart-Skript erzeugt sie neu.",
|
||||
"references": ["man 5 machine-id", "man 8 ssh-keygen"]
|
||||
},
|
||||
{
|
||||
"id": "102.6-002",
|
||||
"objective": "102.6",
|
||||
"type": "single",
|
||||
"difficulty": 1,
|
||||
"question": "Welches Werkzeug richtet Cloud-Instanzen beim ersten Start anhand von Metadaten des Anbieters ein (Hostname, Benutzer, SSH-Schlüssel, Pakete)?",
|
||||
"options": [
|
||||
{ "id": "a", "text": "cloud-init", "correct": true, "explanation": "cloud-init liest Meta- und User-Data (z. B. `#cloud-config` in YAML) und konfiguriert das System beim ersten Start." },
|
||||
{ "id": "b", "text": "Kickstart", "correct": false, "explanation": "Kickstart ist eine Antwortdatei für die automatisierte Installation mit Anaconda, nicht für den Start einer fertigen Instanz." },
|
||||
{ "id": "c", "text": "`virt-install`", "correct": false, "explanation": "`virt-install` legt auf dem Host neue libvirt-VMs an; im Gast konfiguriert es nichts." },
|
||||
{ "id": "d", "text": "`dmidecode`", "correct": false, "explanation": "`dmidecode` liest Hardware-Informationen (DMI/SMBIOS) aus." },
|
||||
{ "id": "e", "text": "`qemu-img`", "correct": false, "explanation": "`qemu-img` erstellt und konvertiert Abbilder virtueller Festplatten." }
|
||||
],
|
||||
"explanation": "Begriffe aus 102.6: Virtuelle Maschine, Linux-Container, Anwendungs-Container, Gasttreiber, SSH-Host-Schlüssel, D-Bus-Machine-ID, cloud-init.",
|
||||
"references": ["https://cloudinit.readthedocs.io/"]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,582 @@
|
||||
{
|
||||
"$schema": "../schema/questions.schema.json",
|
||||
"format": "lpic-sim-questions",
|
||||
"meta": {
|
||||
"file": "questions_103.json",
|
||||
"brief": "Fragen zu Topic 103 GNU- und Unix-Befehle (LPIC-1 101-500, Objectives 5.0)",
|
||||
"author": "Björn Runden <br@runden.de>",
|
||||
"owner": "Björn Runden <br@runden.de>",
|
||||
"assist": "KI-unterstützt (Claude Opus 5.5), geprüft von Björn Runden <br@runden.de>",
|
||||
"version": "0.1.0 (29.10.2026)"
|
||||
},
|
||||
"exam": "lpic1-101",
|
||||
"questions": [
|
||||
{
|
||||
"id": "103.1-001",
|
||||
"objective": "103.1",
|
||||
"type": "single",
|
||||
"difficulty": 1,
|
||||
"question": "Eine Shell-Variable `PROJEKT` soll auch in Programmen sichtbar sein, die aus der aktuellen Shell gestartet werden. Welcher Befehl leistet das?",
|
||||
"options": [
|
||||
{ "id": "a", "text": "`export PROJEKT`", "correct": true, "explanation": "`export` übernimmt die Variable in die Umgebung, die an Kindprozesse vererbt wird." },
|
||||
{ "id": "b", "text": "`set PROJEKT`", "correct": false, "explanation": "`set` setzt Shell-Optionen bzw. Positionsparameter; es exportiert keine Variable." },
|
||||
{ "id": "c", "text": "`env PROJEKT`", "correct": false, "explanation": "`env` würde versuchen, ein Programm namens `PROJEKT` mit der aktuellen Umgebung zu starten." },
|
||||
{ "id": "d", "text": "`local PROJEKT`", "correct": false, "explanation": "`local` erzeugt eine lokale Variable innerhalb einer Shell-Funktion." },
|
||||
{ "id": "e", "text": "`declare -r PROJEKT`", "correct": false, "explanation": "`declare -r` macht die Variable schreibgeschützt, exportiert sie aber nicht (das wäre `declare -x`)." }
|
||||
],
|
||||
"explanation": "Shell-Variablen gelten nur in der aktuellen Shell, Umgebungsvariablen werden vererbt. Anzeige: `set` (alle Shell-Variablen und Funktionen), `env`/`printenv` (nur Umgebung).",
|
||||
"references": ["man 1 bash (Abschnitt ENVIRONMENT)"]
|
||||
},
|
||||
{
|
||||
"id": "103.1-002",
|
||||
"objective": "103.1",
|
||||
"type": "single",
|
||||
"difficulty": 1,
|
||||
"question": "Welcher Befehl gibt den Text `$HOME` **wörtlich** aus, also ohne ihn durch den Pfad des Home-Verzeichnisses zu ersetzen?",
|
||||
"options": [
|
||||
{ "id": "a", "text": "`echo '$HOME'`", "correct": true, "explanation": "In einfachen Anführungszeichen findet keinerlei Ersetzung statt." },
|
||||
{ "id": "b", "text": "`echo \"$HOME\"`", "correct": false, "explanation": "In doppelten Anführungszeichen werden Variablen ersetzt; nur die Worttrennung unterbleibt." },
|
||||
{ "id": "c", "text": "`echo $HOME`", "correct": false, "explanation": "Ohne Quoting wird die Variable ersetzt." },
|
||||
{ "id": "d", "text": "`echo ${HOME}`", "correct": false, "explanation": "`${…}` ist die ausführliche Schreibweise der Variablenersetzung." },
|
||||
{ "id": "e", "text": "`echo $(HOME)`", "correct": false, "explanation": "`$(…)` ist Befehlsersetzung: Die Shell versucht, einen Befehl `HOME` auszuführen." }
|
||||
],
|
||||
"explanation": "Quoting: `'…'` alles wörtlich, `\"…\"` Variablen und Befehlsersetzung aktiv, `\\` maskiert ein einzelnes Zeichen (z. B. `echo \\$HOME`).",
|
||||
"references": ["man 1 bash (Abschnitt QUOTING)"]
|
||||
},
|
||||
{
|
||||
"id": "103.1-003",
|
||||
"objective": "103.1",
|
||||
"type": "fill",
|
||||
"difficulty": 2,
|
||||
"question": "Welches Shell-Builtin zeigt an, ob `ls` ein Alias, eine Funktion, ein Builtin oder ein Programm im `PATH` ist? (nur der Befehlsname)",
|
||||
"answers": ["type"],
|
||||
"explanation": "`type ls` liefert z. B. `ls is aliased to 'ls --color=auto'`. `which` sucht dagegen nur Programme im `PATH` und kennt keine Aliase oder Builtins.",
|
||||
"references": ["help type"]
|
||||
},
|
||||
{
|
||||
"id": "103.1-004",
|
||||
"objective": "103.1",
|
||||
"type": "multi",
|
||||
"difficulty": 2,
|
||||
"question": "Welche Aussagen zur Bash-History treffen zu?",
|
||||
"options": [
|
||||
{ "id": "a", "text": "`!!` führt den zuletzt eingegebenen Befehl erneut aus.", "correct": true, "explanation": "History-Expansion: `!!` steht für den vorherigen Befehl, z. B. `sudo !!`." },
|
||||
{ "id": "b", "text": "Die History wird standardmäßig in `~/.bash_history` gespeichert.", "correct": true, "explanation": "Die Datei legt `HISTFILE` fest; Standard ist `~/.bash_history`." },
|
||||
{ "id": "c", "text": "`history -c` löscht die Datei `~/.bash_history` sofort.", "correct": false, "explanation": "`history -c` leert nur die History im Speicher der laufenden Shell." },
|
||||
{ "id": "d", "text": "`HISTSIZE` legt die maximale Zeilenzahl der History-Datei fest.", "correct": false, "explanation": "`HISTSIZE` gilt für die History im Speicher, `HISTFILESIZE` für die Datei." },
|
||||
{ "id": "e", "text": "`!42` zeigt Befehl Nummer 42 an, ohne ihn auszuführen.", "correct": false, "explanation": "`!42` führt den Befehl aus. Nur anzeigen: `!42:p`." }
|
||||
],
|
||||
"explanation": "Weitere Expansionen: `!$` letztes Argument, `!str` letzter Befehl, der mit `str` beginnt. Strg+R sucht rückwärts in der History.",
|
||||
"references": ["man 1 bash (Abschnitt HISTORY EXPANSION)"]
|
||||
},
|
||||
{
|
||||
"id": "103.1-005",
|
||||
"objective": "103.1",
|
||||
"type": "single",
|
||||
"difficulty": 1,
|
||||
"question": "Welcher Befehl gibt **nur** die Release-Version des laufenden Kernels aus (z. B. `6.1.0-18-amd64`)?",
|
||||
"options": [
|
||||
{ "id": "a", "text": "`uname -r`", "correct": true, "explanation": "`-r` (kernel release) gibt genau die Release-Bezeichnung aus." },
|
||||
{ "id": "b", "text": "`uname -a`", "correct": false, "explanation": "`-a` gibt alle Informationen aus: Name, Host, Release, Version, Architektur …" },
|
||||
{ "id": "c", "text": "`uname -m`", "correct": false, "explanation": "`-m` zeigt die Hardware-Architektur, z. B. `x86_64`." },
|
||||
{ "id": "d", "text": "`uname -n`", "correct": false, "explanation": "`-n` zeigt den Hostnamen (nodename)." },
|
||||
{ "id": "e", "text": "`uname -s`", "correct": false, "explanation": "`-s` zeigt den Kernelnamen, also `Linux`." }
|
||||
],
|
||||
"explanation": "`uname -r` wird oft in Pfaden genutzt, z. B. `/lib/modules/$(uname -r)/`.",
|
||||
"references": ["man 1 uname"]
|
||||
},
|
||||
{
|
||||
"id": "103.1-006",
|
||||
"objective": "103.1",
|
||||
"type": "single",
|
||||
"difficulty": 2,
|
||||
"question": "Wie entfernt man die Variable `TMPDIR` **vollständig** aus der aktuellen Shell?",
|
||||
"options": [
|
||||
{ "id": "a", "text": "`unset TMPDIR`", "correct": true, "explanation": "`unset` löscht die Variable; danach ist sie weder gesetzt noch exportiert." },
|
||||
{ "id": "b", "text": "`TMPDIR=\"\"`", "correct": false, "explanation": "Die Variable bleibt gesetzt, nur ihr Wert ist leer." },
|
||||
{ "id": "c", "text": "`export -n TMPDIR`", "correct": false, "explanation": "`export -n` nimmt nur das Export-Attribut weg; die Shell-Variable bleibt bestehen." },
|
||||
{ "id": "d", "text": "`env -u TMPDIR`", "correct": false, "explanation": "`env -u` entfernt die Variable nur aus der Umgebung des damit gestarteten Befehls." },
|
||||
{ "id": "e", "text": "`set -u TMPDIR`", "correct": false, "explanation": "`set -u` lässt die Shell bei Zugriff auf ungesetzte Variablen einen Fehler melden; `TMPDIR` wird dabei zum Positionsparameter." }
|
||||
],
|
||||
"explanation": "Unterschied gesetzt/leer: `${VAR-x}` liefert `x` nur, wenn `VAR` ungesetzt ist, `${VAR:-x}` auch bei leerem Wert.",
|
||||
"references": ["help unset"]
|
||||
},
|
||||
{
|
||||
"id": "103.2-001",
|
||||
"objective": "103.2",
|
||||
"type": "single",
|
||||
"difficulty": 1,
|
||||
"question": "Welche Befehlskette gibt genau die Zeilen 11 bis 20 einer Datei aus?",
|
||||
"options": [
|
||||
{ "id": "a", "text": "`head -n 20 datei | tail -n 10`", "correct": true, "explanation": "Erst die ersten 20 Zeilen, davon die letzten 10 – also 11 bis 20." },
|
||||
{ "id": "b", "text": "`tail -n 20 datei | head -n 10`", "correct": false, "explanation": "Das liefert die ersten 10 der letzten 20 Zeilen der Datei." },
|
||||
{ "id": "c", "text": "`sed -n '10,20p' datei`", "correct": false, "explanation": "Gibt Zeilen 10 bis 20 aus, also 11 Zeilen." },
|
||||
{ "id": "d", "text": "`head -n 11-20 datei`", "correct": false, "explanation": "`head` kennt keine Bereichsangabe." },
|
||||
{ "id": "e", "text": "`cut -c 11-20 datei`", "correct": false, "explanation": "`cut -c` wählt Zeichen 11 bis 20 jeder Zeile, keine Zeilen." }
|
||||
],
|
||||
"explanation": "Alternativ `sed -n '11,20p' datei`. `tail -n +11` gibt alles ab Zeile 11 aus.",
|
||||
"references": ["man 1 head", "man 1 tail"]
|
||||
},
|
||||
{
|
||||
"id": "103.2-002",
|
||||
"objective": "103.2",
|
||||
"type": "single",
|
||||
"difficulty": 1,
|
||||
"question": "Welcher Befehl gibt nur die Benutzernamen (das erste Feld) aus `/etc/passwd` aus?",
|
||||
"options": [
|
||||
{ "id": "a", "text": "`cut -d: -f1 /etc/passwd`", "correct": true, "explanation": "`-d:` setzt den Doppelpunkt als Trenner, `-f1` wählt das erste Feld." },
|
||||
{ "id": "b", "text": "`cut -f1 /etc/passwd`", "correct": false, "explanation": "Ohne `-d` trennt `cut` an Tabulatoren; die Zeilen werden ganz ausgegeben." },
|
||||
{ "id": "c", "text": "`sort -t: -k1 /etc/passwd`", "correct": false, "explanation": "Das sortiert nach dem ersten Feld, gibt aber vollständige Zeilen aus." },
|
||||
{ "id": "d", "text": "`tr ':' ' ' < /etc/passwd`", "correct": false, "explanation": "`tr` ersetzt nur die Doppelpunkte durch Leerzeichen." },
|
||||
{ "id": "e", "text": "`paste -d: /etc/passwd`", "correct": false, "explanation": "`paste` fügt Zeilen mehrerer Dateien nebeneinander; mit einer Datei gibt es sie unverändert aus." }
|
||||
],
|
||||
"explanation": "Gleichwertig: `awk -F: '{print $1}' /etc/passwd`.",
|
||||
"references": ["man 1 cut"]
|
||||
},
|
||||
{
|
||||
"id": "103.2-003",
|
||||
"objective": "103.2",
|
||||
"type": "single",
|
||||
"difficulty": 2,
|
||||
"question": "In einer unsortierten Datei sollen alle Zeilen mit der Anzahl ihres Vorkommens ausgegeben werden. Welche Befehlskette ist korrekt?",
|
||||
"options": [
|
||||
{ "id": "a", "text": "`sort datei | uniq -c`", "correct": true, "explanation": "`uniq` fasst nur benachbarte gleiche Zeilen zusammen; daher vorher sortieren." },
|
||||
{ "id": "b", "text": "`uniq -c datei`", "correct": false, "explanation": "Ohne Sortierung werden gleiche, aber nicht benachbarte Zeilen getrennt gezählt." },
|
||||
{ "id": "c", "text": "`sort -u datei | wc -l`", "correct": false, "explanation": "Das zählt nur die Anzahl verschiedener Zeilen." },
|
||||
{ "id": "d", "text": "`uniq -d datei | sort`", "correct": false, "explanation": "`uniq -d` zeigt nur benachbarte doppelte Zeilen, ohne Zählung." },
|
||||
{ "id": "e", "text": "`cat -n datei | uniq`", "correct": false, "explanation": "Durch die Zeilennummern sind alle Zeilen verschieden; nichts wird zusammengefasst." }
|
||||
],
|
||||
"explanation": "Häufigste Zeilen zuerst: `sort datei | uniq -c | sort -nr`.",
|
||||
"references": ["man 1 uniq", "man 1 sort"]
|
||||
},
|
||||
{
|
||||
"id": "103.3-001",
|
||||
"objective": "103.3",
|
||||
"type": "single",
|
||||
"difficulty": 1,
|
||||
"question": "Ein Verzeichnis soll rekursiv kopiert werden; Rechte, Eigentümer, Zeitstempel und symbolische Links sollen erhalten bleiben. Welcher Befehl ist geeignet?",
|
||||
"options": [
|
||||
{ "id": "a", "text": "`cp -a quelle ziel`", "correct": true, "explanation": "`-a` (archive) entspricht `-dR --preserve=all`: rekursiv, Links als Links, alle Attribute." },
|
||||
{ "id": "b", "text": "`cp -r quelle ziel`", "correct": false, "explanation": "`-r` kopiert rekursiv, übernimmt aber Eigentümer und Zeitstempel nicht." },
|
||||
{ "id": "c", "text": "`cp -p quelle ziel`", "correct": false, "explanation": "`-p` erhält Attribute, kopiert aber Verzeichnisse nicht rekursiv." },
|
||||
{ "id": "d", "text": "`cp -l quelle ziel`", "correct": false, "explanation": "`-l` legt Hardlinks statt Kopien an." },
|
||||
{ "id": "e", "text": "`mv -a quelle ziel`", "correct": false, "explanation": "`mv` verschiebt und kennt keine Option `-a`." }
|
||||
],
|
||||
"explanation": "Eigentümer kann nur root übernehmen; als normaler Benutzer gehören Kopien dem Kopierenden.",
|
||||
"references": ["man 1 cp"]
|
||||
},
|
||||
{
|
||||
"id": "103.3-002",
|
||||
"objective": "103.3",
|
||||
"type": "fill",
|
||||
"difficulty": 2,
|
||||
"question": "Geben Sie den `find`-Befehl ein, der unterhalb von `/var` alle Dateien findet, die **größer als 100 MiB** sind.",
|
||||
"answers": ["find /var -size +100M", "find /var -type f -size +100M", "find /var -size +100M -type f"],
|
||||
"explanation": "`-size +100M`: größer als 100 MiB (`k`, `M`, `G` als Einheiten). `-size -100M` wäre kleiner. `-type f` beschränkt auf reguläre Dateien.",
|
||||
"references": ["man 1 find"]
|
||||
},
|
||||
{
|
||||
"id": "103.3-003",
|
||||
"objective": "103.3",
|
||||
"type": "single",
|
||||
"difficulty": 1,
|
||||
"question": "Welcher Befehl erstellt ein gzip-komprimiertes tar-Archiv `backup.tar.gz` von `/etc`?",
|
||||
"options": [
|
||||
{ "id": "a", "text": "`tar -czf backup.tar.gz /etc`", "correct": true, "explanation": "`c` create, `z` gzip, `f` Archivdatei." },
|
||||
{ "id": "b", "text": "`tar -xzf backup.tar.gz /etc`", "correct": false, "explanation": "`x` entpackt – hier würde nur `/etc` aus dem Archiv extrahiert." },
|
||||
{ "id": "c", "text": "`tar -cjf backup.tar.gz /etc`", "correct": false, "explanation": "`j` komprimiert mit bzip2; der Name `.gz` wäre irreführend." },
|
||||
{ "id": "d", "text": "`tar -tzf backup.tar.gz /etc`", "correct": false, "explanation": "`t` listet den Inhalt eines vorhandenen Archivs." },
|
||||
{ "id": "e", "text": "`gzip -r /etc > backup.tar.gz`", "correct": false, "explanation": "`gzip -r` komprimiert jede Datei in `/etc` einzeln an Ort und Stelle; es entsteht kein Archiv." }
|
||||
],
|
||||
"explanation": "Kompression bei tar: `z` gzip, `j` bzip2, `J` xz. Modi: `c` erstellen, `x` entpacken, `t` auflisten.",
|
||||
"references": ["man 1 tar"]
|
||||
},
|
||||
{
|
||||
"id": "103.3-004",
|
||||
"objective": "103.3",
|
||||
"type": "multi",
|
||||
"difficulty": 2,
|
||||
"question": "Welche Dateinamen passen auf das Shell-Muster `d?t[0-9]*`?",
|
||||
"options": [
|
||||
{ "id": "a", "text": "`dat1`", "correct": true, "explanation": "`d`, beliebiges Zeichen `a`, `t`, Ziffer `1`, danach nichts (`*` darf leer sein)." },
|
||||
{ "id": "b", "text": "`dot42.txt`", "correct": true, "explanation": "`d`, `o`, `t`, Ziffer `4`, Rest `2.txt` passt auf `*`." },
|
||||
{ "id": "c", "text": "`dt1`", "correct": false, "explanation": "`?` verlangt genau ein Zeichen zwischen `d` und `t`." },
|
||||
{ "id": "d", "text": "`data1`", "correct": false, "explanation": "Das vierte Zeichen `a` ist keine Ziffer." },
|
||||
{ "id": "e", "text": "`Dat1`", "correct": false, "explanation": "Globbing unterscheidet Groß- und Kleinschreibung; `D` ist nicht `d`." }
|
||||
],
|
||||
"explanation": "Globbing: `*` beliebig viele Zeichen (auch keine), `?` genau ein Zeichen, `[…]` ein Zeichen aus der Menge, `[!…]` ein Zeichen nicht aus der Menge.",
|
||||
"references": ["man 7 glob"]
|
||||
},
|
||||
{
|
||||
"id": "103.3-005",
|
||||
"objective": "103.3",
|
||||
"type": "single",
|
||||
"difficulty": 1,
|
||||
"question": "Welcher Befehl ermittelt den Typ einer Datei anhand ihres Inhalts (z. B. „ELF 64-bit executable“ oder „gzip compressed data“)?",
|
||||
"options": [
|
||||
{ "id": "a", "text": "`file`", "correct": true, "explanation": "`file` vergleicht den Inhalt mit Signaturen (Magic Numbers)." },
|
||||
{ "id": "b", "text": "`stat`", "correct": false, "explanation": "`stat` zeigt Metadaten aus dem Inode (Größe, Rechte, Zeiten), nicht den Inhaltstyp." },
|
||||
{ "id": "c", "text": "`type`", "correct": false, "explanation": "`type` bestimmt, wie die Shell einen Befehlsnamen auflöst." },
|
||||
{ "id": "d", "text": "`ls -l`", "correct": false, "explanation": "`ls -l` zeigt nur den Dateityp im Sinne von Datei/Verzeichnis/Link usw." },
|
||||
{ "id": "e", "text": "`which`", "correct": false, "explanation": "`which` sucht ein Programm im `PATH`." }
|
||||
],
|
||||
"explanation": "Unter Linux bestimmt nicht die Endung den Typ, sondern der Inhalt; `file` wertet dazu die magic-Datenbank aus.",
|
||||
"references": ["man 1 file"]
|
||||
},
|
||||
{
|
||||
"id": "103.3-006",
|
||||
"objective": "103.3",
|
||||
"type": "single",
|
||||
"difficulty": 2,
|
||||
"question": "Der Master Boot Record (erster Sektor, 512 Byte) von `/dev/sda` soll in die Datei `mbr.img` gesichert werden. Welcher Befehl ist korrekt?",
|
||||
"options": [
|
||||
{ "id": "a", "text": "`dd if=/dev/sda of=mbr.img bs=512 count=1`", "correct": true, "explanation": "`if` Quelle, `of` Ziel, ein Block zu 512 Byte." },
|
||||
{ "id": "b", "text": "`dd if=mbr.img of=/dev/sda bs=512 count=1`", "correct": false, "explanation": "Quelle und Ziel sind vertauscht – das würde den MBR mit der Datei überschreiben." },
|
||||
{ "id": "c", "text": "`dd if=/dev/sda of=mbr.img bs=512 skip=1 count=1`", "correct": false, "explanation": "`skip=1` überspringt den ersten Block der Quelle – kopiert würde der zweite Sektor, nicht der MBR." },
|
||||
{ "id": "d", "text": "`dd if=/dev/sda1 of=mbr.img bs=512 count=1`", "correct": false, "explanation": "`/dev/sda1` ist die erste Partition; der MBR liegt am Anfang der ganzen Platte." },
|
||||
{ "id": "e", "text": "`cp /dev/sda mbr.img`", "correct": false, "explanation": "Das würde die gesamte Platte kopieren, nicht nur den ersten Sektor." }
|
||||
],
|
||||
"explanation": "Der MBR enthält Bootloader-Code (446 Byte), Partitionstabelle (64 Byte) und Signatur (2 Byte).",
|
||||
"references": ["man 1 dd"]
|
||||
},
|
||||
{
|
||||
"id": "103.4-001",
|
||||
"objective": "103.4",
|
||||
"type": "single",
|
||||
"difficulty": 2,
|
||||
"question": "Standardausgabe **und** Fehlerausgabe eines Befehls sollen in dieselbe Datei geschrieben werden. Welche Schreibweise ist korrekt?",
|
||||
"options": [
|
||||
{ "id": "a", "text": "`befehl > log.txt 2>&1`", "correct": true, "explanation": "Erst stdout in die Datei, dann stderr auf das (bereits umgelenkte) stdout." },
|
||||
{ "id": "b", "text": "`befehl 2>&1 > log.txt`", "correct": false, "explanation": "Reihenfolge: stderr wird auf das ursprüngliche stdout (Terminal) gelegt, erst danach stdout in die Datei." },
|
||||
{ "id": "c", "text": "`befehl 2> log.txt`", "correct": false, "explanation": "Nur die Fehlerausgabe landet in der Datei." },
|
||||
{ "id": "d", "text": "`befehl | log.txt`", "correct": false, "explanation": "Die Pipe erwartet einen Befehl; `log.txt` würde als Programm gesucht." },
|
||||
{ "id": "e", "text": "`befehl >> log.txt 1>&2`", "correct": false, "explanation": "`1>&2` lenkt stdout anschließend wieder auf stderr (Terminal) um; die Datei bleibt leer." }
|
||||
],
|
||||
"explanation": "Umleitungen werden von links nach rechts ausgewertet. Bash-Kurzform: `befehl &> log.txt`.",
|
||||
"references": ["man 1 bash (Abschnitt REDIRECTION)"]
|
||||
},
|
||||
{
|
||||
"id": "103.4-002",
|
||||
"objective": "103.4",
|
||||
"type": "fill",
|
||||
"difficulty": 1,
|
||||
"question": "Welche Umleitung (nur die Umleitung, ohne Befehl) verwirft die **Fehlerausgabe** eines Befehls?",
|
||||
"answers": ["2>/dev/null", "2> /dev/null"],
|
||||
"explanation": "Dateideskriptor 2 ist stderr; `/dev/null` verwirft alles, was hineingeschrieben wird.",
|
||||
"references": ["man 4 null"]
|
||||
},
|
||||
{
|
||||
"id": "103.4-003",
|
||||
"objective": "103.4",
|
||||
"type": "single",
|
||||
"difficulty": 1,
|
||||
"question": "Die Ausgabe eines Befehls soll auf dem Bildschirm erscheinen **und** gleichzeitig in `ausgabe.txt` gespeichert werden. Wie geht das?",
|
||||
"options": [
|
||||
{ "id": "a", "text": "`befehl | tee ausgabe.txt`", "correct": true, "explanation": "`tee` kopiert stdin nach stdout und zusätzlich in die Datei(en); `-a` hängt an." },
|
||||
{ "id": "b", "text": "`befehl > ausgabe.txt | cat`", "correct": false, "explanation": "stdout geht vollständig in die Datei; an `cat` kommt nichts an." },
|
||||
{ "id": "c", "text": "`befehl >> ausgabe.txt`", "correct": false, "explanation": "Hängt nur an die Datei an; auf dem Bildschirm erscheint nichts." },
|
||||
{ "id": "d", "text": "`befehl | cat > ausgabe.txt`", "correct": false, "explanation": "Die Ausgabe landet nur in der Datei." },
|
||||
{ "id": "e", "text": "`befehl 2>&1 ausgabe.txt`", "correct": false, "explanation": "`ausgabe.txt` würde als Argument an den Befehl übergeben." }
|
||||
],
|
||||
"explanation": "`tee` ist wie ein T-Stück in der Pipe. Mit `sudo tee datei` schreibt man z. B. als root in geschützte Dateien.",
|
||||
"references": ["man 1 tee"]
|
||||
},
|
||||
{
|
||||
"id": "103.4-004",
|
||||
"objective": "103.4",
|
||||
"type": "single",
|
||||
"difficulty": 2,
|
||||
"question": "Alle von `find` gefundenen `*.tmp`-Dateien sollen gelöscht werden, indem ihre Namen als **Argumente** an `rm` übergeben werden. Welche Befehlszeile tut das?",
|
||||
"options": [
|
||||
{ "id": "a", "text": "`find . -name '*.tmp' | xargs rm`", "correct": true, "explanation": "`xargs` liest Namen von stdin und hängt sie als Argumente an `rm`." },
|
||||
{ "id": "b", "text": "`find . -name '*.tmp' | rm`", "correct": false, "explanation": "`rm` liest keine Dateinamen von stdin; es meldet fehlende Operanden." },
|
||||
{ "id": "c", "text": "`find . -name '*.tmp' > rm`", "correct": false, "explanation": "Das schreibt die Liste in eine Datei namens `rm`." },
|
||||
{ "id": "d", "text": "`find . -name '*.tmp' | tee rm`", "correct": false, "explanation": "`tee` schreibt die Liste in die Datei `rm` und auf den Bildschirm." },
|
||||
{ "id": "e", "text": "`rm < find . -name '*.tmp'`", "correct": false, "explanation": "`<` erwartet eine Datei als Eingabe, keinen Befehl." }
|
||||
],
|
||||
"explanation": "Bei Namen mit Leerzeichen: `find … -print0 | xargs -0 rm`. Alternativ ohne Pipe: `find . -name '*.tmp' -delete` oder `-exec rm {} +`.",
|
||||
"references": ["man 1 xargs"]
|
||||
},
|
||||
{
|
||||
"id": "103.4-005",
|
||||
"objective": "103.4",
|
||||
"type": "multi",
|
||||
"difficulty": 1,
|
||||
"question": "Welche Aussagen zu Umleitungen und Pipes treffen zu?",
|
||||
"options": [
|
||||
{ "id": "a", "text": "`>>` hängt die Standardausgabe an eine Datei an.", "correct": true, "explanation": "Existiert die Datei nicht, wird sie angelegt." },
|
||||
{ "id": "b", "text": "`<` liest die Standardeingabe aus einer Datei.", "correct": true, "explanation": "Beispiel: `sort < liste.txt`." },
|
||||
{ "id": "c", "text": "`2>&1` leitet die Standardausgabe auf die Fehlerausgabe um.", "correct": false, "explanation": "Umgekehrt: stderr (2) wird dorthin geleitet, wohin stdout (1) gerade zeigt." },
|
||||
{ "id": "d", "text": "`|` verbindet die Fehlerausgabe mit der Eingabe des nächsten Befehls.", "correct": false, "explanation": "Die Pipe verbindet stdout mit stdin; stderr nur mit `|&` bzw. `2>&1 |`." },
|
||||
{ "id": "e", "text": "`>` hängt an, wenn die Datei bereits existiert.", "correct": false, "explanation": "`>` überschreibt (kürzt die Datei auf 0); nur `>>` hängt an." }
|
||||
],
|
||||
"explanation": "Dateideskriptoren: 0 stdin, 1 stdout, 2 stderr. `set -o noclobber` verhindert versehentliches Überschreiben mit `>`.",
|
||||
"references": ["man 1 bash (Abschnitt REDIRECTION)"]
|
||||
},
|
||||
{
|
||||
"id": "103.4-006",
|
||||
"objective": "103.4",
|
||||
"type": "single",
|
||||
"difficulty": 1,
|
||||
"question": "Mit welcher Schreibweise setzt die Bash die **Ausgabe** eines Befehls in die Kommandozeile ein, z. B. in `echo \"Heute ist …\"`?",
|
||||
"options": [
|
||||
{ "id": "a", "text": "`$(date +%A)`", "correct": true, "explanation": "Befehlsersetzung: Die Ausgabe von `date +%A` ersetzt den Ausdruck." },
|
||||
{ "id": "b", "text": "`${date +%A}`", "correct": false, "explanation": "`${…}` ist Parameter-/Variablenersetzung und hier ein Syntaxfehler." },
|
||||
{ "id": "c", "text": "`$((date +%A))`", "correct": false, "explanation": "`$((…))` ist arithmetische Expansion." },
|
||||
{ "id": "d", "text": "`'date +%A'`", "correct": false, "explanation": "Einfache Anführungszeichen ergeben den Text wörtlich." },
|
||||
{ "id": "e", "text": "`<(date +%A)`", "correct": false, "explanation": "Prozessersetzung liefert einen Dateinamen (z. B. `/dev/fd/63`), nicht den Text." }
|
||||
],
|
||||
"explanation": "Ältere, gleichwertige Schreibweise mit Backticks (Gravis-Zeichen). `$(…)` ist besser lesbar und verschachtelbar.",
|
||||
"references": ["man 1 bash (Abschnitt Command Substitution)"]
|
||||
},
|
||||
{
|
||||
"id": "103.5-001",
|
||||
"objective": "103.5",
|
||||
"type": "single",
|
||||
"difficulty": 1,
|
||||
"question": "Welches Signal kann ein Prozess weder abfangen noch ignorieren?",
|
||||
"options": [
|
||||
{ "id": "a", "text": "SIGKILL (9)", "correct": true, "explanation": "SIGKILL beendet den Prozess unmittelbar durch den Kernel; es gibt keinen Handler." },
|
||||
{ "id": "b", "text": "SIGTERM (15)", "correct": false, "explanation": "SIGTERM ist die höfliche Aufforderung zum Beenden und kann abgefangen werden." },
|
||||
{ "id": "c", "text": "SIGHUP (1)", "correct": false, "explanation": "SIGHUP wird oft abgefangen, z. B. um Konfiguration neu zu laden." },
|
||||
{ "id": "d", "text": "SIGINT (2)", "correct": false, "explanation": "SIGINT (Strg+C) kann abgefangen werden." },
|
||||
{ "id": "e", "text": "SIGUSR1 (10)", "correct": false, "explanation": "Benutzerdefiniertes Signal, das Programme frei abfangen." }
|
||||
],
|
||||
"explanation": "Neben SIGKILL kann auch SIGSTOP nicht abgefangen werden. Liste aller Signale: `kill -l`.",
|
||||
"references": ["man 7 signal"]
|
||||
},
|
||||
{
|
||||
"id": "103.5-002",
|
||||
"objective": "103.5",
|
||||
"type": "fill",
|
||||
"difficulty": 1,
|
||||
"question": "Welches Signal sendet `kill <PID>`, wenn kein Signal angegeben wird? (Name oder Nummer)",
|
||||
"answers": ["SIGTERM", "TERM", "15"],
|
||||
"case_sensitive": false,
|
||||
"explanation": "Standard ist SIGTERM (15). Erst wenn ein Prozess darauf nicht reagiert, nimmt man SIGKILL (`kill -9`).",
|
||||
"references": ["man 1 kill"]
|
||||
},
|
||||
{
|
||||
"id": "103.5-003",
|
||||
"objective": "103.5",
|
||||
"type": "single",
|
||||
"difficulty": 1,
|
||||
"question": "Ein Skript soll weiterlaufen, nachdem sich der Benutzer abgemeldet hat. Welche Befehlszeile ist dafür gedacht?",
|
||||
"options": [
|
||||
{ "id": "a", "text": "`nohup ./skript.sh &`", "correct": true, "explanation": "`nohup` ignoriert SIGHUP und leitet die Ausgabe nach `nohup.out` um; `&` startet im Hintergrund." },
|
||||
{ "id": "b", "text": "`./skript.sh &`", "correct": false, "explanation": "Hintergrundjobs können beim Abmelden SIGHUP erhalten und beendet werden." },
|
||||
{ "id": "c", "text": "`bg ./skript.sh`", "correct": false, "explanation": "`bg` setzt einen gestoppten Job fort und erwartet eine Jobnummer, keinen Befehl." },
|
||||
{ "id": "d", "text": "`nice ./skript.sh`", "correct": false, "explanation": "`nice` ändert nur die Priorität." },
|
||||
{ "id": "e", "text": "`jobs ./skript.sh`", "correct": false, "explanation": "`jobs` listet Jobs der aktuellen Shell." }
|
||||
],
|
||||
"explanation": "Alternativen: `disown` für laufende Jobs, oder Terminal-Multiplexer wie `screen`/`tmux`, von denen man sich trennen und später wieder verbinden kann.",
|
||||
"references": ["man 1 nohup"]
|
||||
},
|
||||
{
|
||||
"id": "103.5-004",
|
||||
"objective": "103.5",
|
||||
"type": "single",
|
||||
"difficulty": 1,
|
||||
"question": "`jobs` zeigt drei Jobs. Wie holt man Job Nummer 2 in den Vordergrund?",
|
||||
"options": [
|
||||
{ "id": "a", "text": "`fg %2`", "correct": true, "explanation": "`fg` holt den angegebenen Job in den Vordergrund; `%2` ist die Jobspezifikation." },
|
||||
{ "id": "b", "text": "`bg %2`", "correct": false, "explanation": "`bg` setzt einen gestoppten Job im Hintergrund fort." },
|
||||
{ "id": "c", "text": "`jobs %2`", "correct": false, "explanation": "Zeigt nur den Status von Job 2." },
|
||||
{ "id": "d", "text": "`kill -CONT %2`", "correct": false, "explanation": "Setzt den Job fort, er bleibt aber im Hintergrund." },
|
||||
{ "id": "e", "text": "`wait %2`", "correct": false, "explanation": "`wait` wartet auf das Ende des Jobs, ohne ihn in den Vordergrund zu holen." }
|
||||
],
|
||||
"explanation": "Strg+Z stoppt den Vordergrundjob, `bg` lässt ihn im Hintergrund weiterlaufen, `fg` holt ihn zurück.",
|
||||
"references": ["help fg", "help jobs"]
|
||||
},
|
||||
{
|
||||
"id": "103.5-005",
|
||||
"objective": "103.5",
|
||||
"type": "multi",
|
||||
"difficulty": 2,
|
||||
"question": "Mit welchen Befehlen ermittelt man die Prozess-ID(s) laufender `sshd`-Prozesse anhand des Namens?",
|
||||
"options": [
|
||||
{ "id": "a", "text": "`pgrep sshd`", "correct": true, "explanation": "`pgrep` gibt die PIDs aller Prozesse aus, deren Name auf das Muster passt." },
|
||||
{ "id": "b", "text": "`pidof sshd`", "correct": true, "explanation": "`pidof` gibt die PIDs eines Programms mit genau diesem Namen aus." },
|
||||
{ "id": "c", "text": "`pkill sshd`", "correct": false, "explanation": "`pkill` sendet ein Signal (Standard SIGTERM) an die Prozesse – Vorsicht!" },
|
||||
{ "id": "d", "text": "`ps -p sshd`", "correct": false, "explanation": "`-p` erwartet eine PID-Liste, keinen Namen (richtig wäre `ps -C sshd`)." },
|
||||
{ "id": "e", "text": "`top -n sshd`", "correct": false, "explanation": "`-n` legt bei `top` die Anzahl der Durchläufe fest." }
|
||||
],
|
||||
"explanation": "Auch üblich: `ps aux | grep sshd`. `pgrep -a` zeigt zusätzlich die Befehlszeile, `pgrep -u` filtert nach Benutzer.",
|
||||
"references": ["man 1 pgrep", "man 8 pidof"]
|
||||
},
|
||||
{
|
||||
"id": "103.5-006",
|
||||
"objective": "103.5",
|
||||
"type": "single",
|
||||
"difficulty": 1,
|
||||
"question": "Welcher Befehl zeigt kompakt die Laufzeit des Systems, die Anzahl angemeldeter Benutzer und die Load Average der letzten 1, 5 und 15 Minuten?",
|
||||
"options": [
|
||||
{ "id": "a", "text": "`uptime`", "correct": true, "explanation": "`uptime` gibt genau diese Zeile aus (Werte aus `/proc/uptime` und `/proc/loadavg`)." },
|
||||
{ "id": "b", "text": "`free`", "correct": false, "explanation": "`free` zeigt Arbeitsspeicher und Swap." },
|
||||
{ "id": "c", "text": "`vmstat`", "correct": false, "explanation": "`vmstat` zeigt Prozess-, Speicher-, I/O- und CPU-Statistiken, aber keine Load Average." },
|
||||
{ "id": "d", "text": "`ps aux`", "correct": false, "explanation": "`ps aux` listet Prozesse." },
|
||||
{ "id": "e", "text": "`watch`", "correct": false, "explanation": "`watch` führt einen Befehl wiederholt aus und zeigt seine Ausgabe." }
|
||||
],
|
||||
"explanation": "Die Load Average ist die mittlere Zahl laufender bzw. auf CPU oder I/O wartender Prozesse. Sie steht auch in der ersten Zeile von `top` und `w`.",
|
||||
"references": ["man 1 uptime"]
|
||||
},
|
||||
{
|
||||
"id": "103.6-001",
|
||||
"objective": "103.6",
|
||||
"type": "single",
|
||||
"difficulty": 1,
|
||||
"question": "In welchem Bereich liegen Nice-Werte unter Linux, und welcher Wert bedeutet die **höchste** Priorität?",
|
||||
"options": [
|
||||
{ "id": "a", "text": "−20 bis 19; −20 ist die höchste Priorität", "correct": true, "explanation": "Je niedriger der Nice-Wert, desto „unfreundlicher“ ist der Prozess, also desto mehr CPU-Zeit bekommt er." },
|
||||
{ "id": "b", "text": "−20 bis 19; 19 ist die höchste Priorität", "correct": false, "explanation": "19 ist die niedrigste Priorität." },
|
||||
{ "id": "c", "text": "0 bis 39; 39 ist die höchste Priorität", "correct": false, "explanation": "Nice-Werte sind vorzeichenbehaftet und reichen von −20 bis 19." },
|
||||
{ "id": "d", "text": "1 bis 99; 99 ist die höchste Priorität", "correct": false, "explanation": "1–99 ist der Bereich der Echtzeitprioritäten, keine Nice-Werte." },
|
||||
{ "id": "e", "text": "−19 bis 20; 0 ist die höchste Priorität", "correct": false, "explanation": "Falscher Bereich; 0 ist der Standardwert." }
|
||||
],
|
||||
"explanation": "Standard ist 0. `nice` ohne `-n` startet mit +10. In `ps -l` erscheint der Wert in Spalte NI, in `top` als NI.",
|
||||
"references": ["man 1 nice"]
|
||||
},
|
||||
{
|
||||
"id": "103.6-002",
|
||||
"objective": "103.6",
|
||||
"type": "fill",
|
||||
"difficulty": 2,
|
||||
"question": "Geben Sie den Befehl ein, der den Nice-Wert des laufenden Prozesses mit der PID 1234 auf 10 setzt.",
|
||||
"answers": ["renice -n 10 -p 1234", "renice 10 1234", "renice -n 10 1234", "renice 10 -p 1234", "renice +10 1234", "renice +10 -p 1234", "renice -n +10 -p 1234"],
|
||||
"explanation": "`renice` ändert laufende Prozesse (`-p` PID, `-u` Benutzer, `-g` Prozessgruppe); `nice` gilt nur beim Starten.",
|
||||
"references": ["man 1 renice"]
|
||||
},
|
||||
{
|
||||
"id": "103.6-003",
|
||||
"objective": "103.6",
|
||||
"type": "multi",
|
||||
"difficulty": 2,
|
||||
"question": "Was darf ein **normaler Benutzer** (ohne besondere Limits) standardmäßig?",
|
||||
"options": [
|
||||
{ "id": "a", "text": "Ein Programm mit Nice-Wert 10 starten", "correct": true, "explanation": "Positive Nice-Werte (niedrigere Priorität) darf jeder setzen: `nice -n 10 programm`." },
|
||||
{ "id": "b", "text": "Den Nice-Wert eines eigenen laufenden Prozesses erhöhen", "correct": true, "explanation": "Die eigene Priorität senken ist erlaubt: `renice -n 15 -p <PID>`." },
|
||||
{ "id": "c", "text": "Ein Programm mit Nice-Wert −5 starten", "correct": false, "explanation": "Negative Nice-Werte erfordern root (bzw. CAP_SYS_NICE)." },
|
||||
{ "id": "d", "text": "Den Nice-Wert eines Prozesses eines anderen Benutzers ändern", "correct": false, "explanation": "Nur eigene Prozesse; fremde nur als root." },
|
||||
{ "id": "e", "text": "Den Nice-Wert eines eigenen Prozesses wieder unter den bisherigen Wert senken", "correct": false, "explanation": "Die Priorität wieder anheben darf standardmäßig nur root (Ausnahme: über `RLIMIT_NICE` erlaubte Bereiche)." }
|
||||
],
|
||||
"explanation": "Grundregel: Jeder darf seine Prozesse „netter“ machen, nur root darf Priorität erhöhen.",
|
||||
"references": ["man 1 renice", "man 2 setpriority"]
|
||||
},
|
||||
{
|
||||
"id": "103.7-001",
|
||||
"objective": "103.7",
|
||||
"type": "single",
|
||||
"difficulty": 1,
|
||||
"question": "Welche grep-Option gibt alle Zeilen aus, die **nicht** auf das Muster passen?",
|
||||
"options": [
|
||||
{ "id": "a", "text": "`-v`", "correct": true, "explanation": "`-v` (`--invert-match`) kehrt die Auswahl um." },
|
||||
{ "id": "b", "text": "`-i`", "correct": false, "explanation": "`-i` ignoriert Groß-/Kleinschreibung." },
|
||||
{ "id": "c", "text": "`-c`", "correct": false, "explanation": "`-c` gibt nur die Anzahl passender Zeilen aus." },
|
||||
{ "id": "d", "text": "`-l`", "correct": false, "explanation": "`-l` gibt nur die Namen der Dateien mit Treffern aus." },
|
||||
{ "id": "e", "text": "`-n`", "correct": false, "explanation": "`-n` stellt die Zeilennummer voran." }
|
||||
],
|
||||
"explanation": "Wichtige Optionen: `-E` erweiterte RE, `-F` feste Zeichenkette, `-r` rekursiv, `-w` ganzes Wort, `-o` nur den Treffer.",
|
||||
"references": ["man 1 grep"]
|
||||
},
|
||||
{
|
||||
"id": "103.7-002",
|
||||
"objective": "103.7",
|
||||
"type": "single",
|
||||
"difficulty": 2,
|
||||
"question": "Welcher Befehl gibt eine Konfigurationsdatei **ohne** Kommentarzeilen (beginnen mit `#`) und **ohne** Leerzeilen aus?",
|
||||
"options": [
|
||||
{ "id": "a", "text": "`grep -Ev '^(#|$)' datei`", "correct": true, "explanation": "Das Muster passt auf Zeilen, die mit `#` beginnen oder leer sind; `-v` kehrt um." },
|
||||
{ "id": "b", "text": "`grep -v '#' datei`", "correct": false, "explanation": "Entfernt auch Zeilen mit `#` in der Mitte und lässt Leerzeilen stehen." },
|
||||
{ "id": "c", "text": "`grep -E '^#|^$' datei`", "correct": false, "explanation": "Gibt nur Kommentar- und Leerzeilen aus." },
|
||||
{ "id": "d", "text": "`grep -v '^#$' datei`", "correct": false, "explanation": "Entfernt nur Zeilen, die ausschließlich aus `#` bestehen." },
|
||||
{ "id": "e", "text": "`grep -cv '^#' datei`", "correct": false, "explanation": "`-c` gibt nur eine Anzahl aus." }
|
||||
],
|
||||
"explanation": "Anker: `^` Zeilenanfang, `$` Zeilenende; `^$` ist eine leere Zeile. `|` (Alternative) und `( )` erfordern ERE (`-E`) oder in BRE `\\|` und `\\( \\)`.",
|
||||
"references": ["man 7 regex", "man 1 grep"]
|
||||
},
|
||||
{
|
||||
"id": "103.7-003",
|
||||
"objective": "103.7",
|
||||
"type": "fill",
|
||||
"difficulty": 2,
|
||||
"question": "Geben Sie den sed-Befehl ein, der in der Datei `datei` **jedes** Vorkommen von `foo` durch `bar` ersetzt und das Ergebnis auf stdout ausgibt.",
|
||||
"answers": ["sed 's/foo/bar/g' datei", "sed \"s/foo/bar/g\" datei", "sed s/foo/bar/g datei", "sed -e 's/foo/bar/g' datei", "sed -e \"s/foo/bar/g\" datei"],
|
||||
"explanation": "`s/alt/neu/` ersetzt das erste Vorkommen je Zeile, das Flag `g` alle. Mit `-i` wird die Datei selbst geändert.",
|
||||
"references": ["man 1 sed"]
|
||||
},
|
||||
{
|
||||
"id": "103.7-004",
|
||||
"objective": "103.7",
|
||||
"type": "multi",
|
||||
"difficulty": 2,
|
||||
"question": "Welche Aussagen zu regulären Ausdrücken (grep) treffen zu?",
|
||||
"options": [
|
||||
{ "id": "a", "text": "`.` passt auf genau ein beliebiges Zeichen.", "correct": true, "explanation": "Für einen wörtlichen Punkt: `\\.`." },
|
||||
{ "id": "b", "text": "`*` bedeutet: das vorherige Element null- oder mehrmals.", "correct": true, "explanation": "`a*` passt auch auf die leere Zeichenkette; anders als beim Shell-Globbing." },
|
||||
{ "id": "c", "text": "In Basic RE (grep ohne `-E`) bedeutet `+` „ein- oder mehrmals“.", "correct": false, "explanation": "In BRE ist `+` ein normales Zeichen; GNU grep versteht dort `\\+`. Mit `-E` ist `+` ein Quantor." },
|
||||
{ "id": "d", "text": "`$` verankert am Zeilenanfang.", "correct": false, "explanation": "`$` ist das Zeilenende, `^` der Zeilenanfang." },
|
||||
{ "id": "e", "text": "`[^a-z]` passt auf einen Kleinbuchstaben.", "correct": false, "explanation": "`^` am Anfang einer Klammer negiert: ein Zeichen, das **kein** Kleinbuchstabe ist." }
|
||||
],
|
||||
"explanation": "ERE-Zusätze: `+`, `?`, `{n,m}`, `|`, `( )`. Zeichenklassen: `[[:digit:]]`, `[[:space:]]`, `[[:alpha:]]`.",
|
||||
"references": ["man 7 regex"]
|
||||
},
|
||||
{
|
||||
"id": "103.8-001",
|
||||
"objective": "103.8",
|
||||
"type": "single",
|
||||
"difficulty": 1,
|
||||
"question": "Mit welcher Eingabe im Befehlsmodus von vi speichert man die Datei und beendet den Editor?",
|
||||
"options": [
|
||||
{ "id": "a", "text": "`:wq`", "correct": true, "explanation": "`:w` schreibt, `:q` beendet. Gleichwertig: `:x` und `ZZ`." },
|
||||
{ "id": "b", "text": "`:q!`", "correct": false, "explanation": "Beendet ohne zu speichern und verwirft Änderungen." },
|
||||
{ "id": "c", "text": "`:w`", "correct": false, "explanation": "Speichert nur, vi bleibt offen." },
|
||||
{ "id": "d", "text": "`ZQ`", "correct": false, "explanation": "`ZQ` entspricht `:q!`, also beenden ohne Speichern." },
|
||||
{ "id": "e", "text": "`:e!`", "correct": false, "explanation": "Lädt die Datei neu und verwirft ungespeicherte Änderungen." }
|
||||
],
|
||||
"explanation": "Mit Esc kommt man aus dem Einfügemodus in den Befehlsmodus. Doppelpunkt-Befehle sind Ex-Befehle.",
|
||||
"references": ["man 1 vim", ":help ZZ"]
|
||||
},
|
||||
{
|
||||
"id": "103.8-002",
|
||||
"objective": "103.8",
|
||||
"type": "fill",
|
||||
"difficulty": 1,
|
||||
"question": "Welche Tastenfolge löscht in vi im Befehlsmodus die **aktuelle Zeile** (und legt sie in den Puffer)?",
|
||||
"answers": ["dd"],
|
||||
"explanation": "`dd` löscht die Zeile, `5dd` fünf Zeilen, `yy` kopiert, `p` fügt ein, `u` macht rückgängig.",
|
||||
"references": [":help dd"]
|
||||
},
|
||||
{
|
||||
"id": "103.8-003",
|
||||
"objective": "103.8",
|
||||
"type": "single",
|
||||
"difficulty": 1,
|
||||
"question": "Wie sucht man in vi im Befehlsmodus **vorwärts** nach dem Wort `Port`?",
|
||||
"options": [
|
||||
{ "id": "a", "text": "`/Port`", "correct": true, "explanation": "`/` sucht vorwärts; `n` springt zum nächsten, `N` zum vorherigen Treffer." },
|
||||
{ "id": "b", "text": "`?Port`", "correct": false, "explanation": "`?` sucht rückwärts." },
|
||||
{ "id": "c", "text": "`:s/Port`", "correct": false, "explanation": "`:s` ist das Ersetzen in der aktuellen Zeile." },
|
||||
{ "id": "d", "text": "`fPort`", "correct": false, "explanation": "`f` springt zum nächsten Vorkommen eines einzelnen Zeichens in der aktuellen Zeile (hier `P`)." },
|
||||
{ "id": "e", "text": "`nPort`", "correct": false, "explanation": "`n` wiederholt die letzte Suche; die folgenden Zeichen würden als weitere Befehle gelesen." }
|
||||
],
|
||||
"explanation": "Suchen und ersetzen in der ganzen Datei: `:%s/alt/neu/g`.",
|
||||
"references": [":help /", ":help n"]
|
||||
},
|
||||
{
|
||||
"id": "103.8-004",
|
||||
"objective": "103.8",
|
||||
"type": "multi",
|
||||
"difficulty": 1,
|
||||
"question": "Welche Tasten wechseln in vi aus dem Befehlsmodus in den **Einfügemodus**?",
|
||||
"options": [
|
||||
{ "id": "a", "text": "`i`", "correct": true, "explanation": "Einfügen vor dem Cursor." },
|
||||
{ "id": "b", "text": "`o`", "correct": true, "explanation": "Öffnet eine neue Zeile unterhalb und wechselt in den Einfügemodus." },
|
||||
{ "id": "c", "text": "`x`", "correct": false, "explanation": "Löscht das Zeichen unter dem Cursor; man bleibt im Befehlsmodus." },
|
||||
{ "id": "d", "text": "`p`", "correct": false, "explanation": "Fügt den Pufferinhalt ein (paste); man bleibt im Befehlsmodus." },
|
||||
{ "id": "e", "text": "`u`", "correct": false, "explanation": "Macht die letzte Änderung rückgängig." }
|
||||
],
|
||||
"explanation": "Einfügemodus auch mit `a`/`A` (nach Cursor/Zeilenende), `I` (Zeilenanfang), `O` (neue Zeile oberhalb). Der Standard-Editor wird über `EDITOR`/`VISUAL` festgelegt.",
|
||||
"references": [":help insert"]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,327 @@
|
||||
{
|
||||
"$schema": "../schema/questions.schema.json",
|
||||
"format": "lpic-sim-questions",
|
||||
"meta": {
|
||||
"file": "questions_104.json",
|
||||
"brief": "Fragen zu Topic 104 Geräte, Linux-Dateisysteme, FHS (LPIC-1 101-500, Objectives 5.0)",
|
||||
"author": "Björn Runden <br@runden.de>",
|
||||
"owner": "Björn Runden <br@runden.de>",
|
||||
"assist": "KI-unterstützt (Claude Opus 5.5), geprüft von Björn Runden <br@runden.de>",
|
||||
"version": "0.1.0 (29.10.2026)"
|
||||
},
|
||||
"exam": "lpic1-101",
|
||||
"questions": [
|
||||
{
|
||||
"id": "104.1-001",
|
||||
"objective": "104.1",
|
||||
"type": "single",
|
||||
"difficulty": 1,
|
||||
"question": "Auf der ersten Partition der zweiten Festplatte (`/dev/sdb1`) soll ein ext4-Dateisystem angelegt werden. Welcher Befehl ist korrekt?",
|
||||
"options": [
|
||||
{ "id": "a", "text": "`mkfs.ext4 /dev/sdb1`", "correct": true, "explanation": "`mkfs.ext4` (gleichwertig `mkfs -t ext4` oder `mke2fs -t ext4`) legt das Dateisystem an." },
|
||||
{ "id": "b", "text": "`mkfs.ext4 /dev/sdb`", "correct": false, "explanation": "Das würde die ganze Platte ohne Partitionstabelle formatieren, nicht die erste Partition." },
|
||||
{ "id": "c", "text": "`mount -t ext4 /dev/sdb1`", "correct": false, "explanation": "`mount` hängt ein vorhandenes Dateisystem ein, es legt keines an." },
|
||||
{ "id": "d", "text": "`fdisk -t ext4 /dev/sdb1`", "correct": false, "explanation": "`fdisk` bearbeitet Partitionstabellen, legt aber keine Dateisysteme an." },
|
||||
{ "id": "e", "text": "`tune2fs -j /dev/sdb1`", "correct": false, "explanation": "`tune2fs -j` fügt einem bestehenden ext2 ein Journal hinzu." }
|
||||
],
|
||||
"explanation": "Reihenfolge: partitionieren (`fdisk`, `gdisk`, `parted`), Dateisystem anlegen (`mkfs.*`), einhängen (`mount`), dauerhaft in `/etc/fstab`.",
|
||||
"references": ["man 8 mkfs", "man 8 mke2fs"]
|
||||
},
|
||||
{
|
||||
"id": "104.1-002",
|
||||
"objective": "104.1",
|
||||
"type": "fill",
|
||||
"difficulty": 1,
|
||||
"question": "Die Partition `/dev/sdc2` soll als Swap-Bereich vorbereitet (initialisiert) werden. Geben Sie den vollständigen Befehl ein.",
|
||||
"answers": ["mkswap /dev/sdc2"],
|
||||
"explanation": "`mkswap` schreibt die Swap-Signatur; aktiviert wird mit `swapon /dev/sdc2`, dauerhaft über einen fstab-Eintrag vom Typ `swap`.",
|
||||
"references": ["man 8 mkswap", "man 8 swapon"]
|
||||
},
|
||||
{
|
||||
"id": "104.1-003",
|
||||
"objective": "104.1",
|
||||
"type": "multi",
|
||||
"difficulty": 2,
|
||||
"question": "Welche Werkzeuge können GPT-Partitionstabellen anlegen und bearbeiten?",
|
||||
"options": [
|
||||
{ "id": "a", "text": "`gdisk`", "correct": true, "explanation": "`gdisk` ist ein fdisk-ähnliches Werkzeug speziell für GPT." },
|
||||
{ "id": "b", "text": "`parted`", "correct": true, "explanation": "`parted` unterstützt MBR (msdos) und GPT und kann auch skriptgesteuert arbeiten." },
|
||||
{ "id": "c", "text": "`fdisk` (aktuelles util-linux)", "correct": true, "explanation": "Aktuelle `fdisk`-Versionen aus util-linux unterstützen GPT (Befehl `g` legt eine leere GPT an)." },
|
||||
{ "id": "d", "text": "`blkid`", "correct": false, "explanation": "`blkid` zeigt nur Attribute wie UUID, LABEL und TYPE an." },
|
||||
{ "id": "e", "text": "`mkswap`", "correct": false, "explanation": "`mkswap` initialisiert einen Swap-Bereich, partitioniert aber nicht." }
|
||||
],
|
||||
"explanation": "MBR: höchstens 4 primäre Partitionen (bzw. 3 primäre und eine erweiterte), max. 2 TiB. GPT: standardmäßig 128 Partitionen, sehr große Platten, Sicherungskopie der Tabelle am Plattenende.",
|
||||
"references": ["man 8 gdisk", "man 8 parted", "man 8 fdisk"]
|
||||
},
|
||||
{
|
||||
"id": "104.2-001",
|
||||
"objective": "104.2",
|
||||
"type": "single",
|
||||
"difficulty": 1,
|
||||
"question": "Was ist zu beachten, bevor man ein ext4-Dateisystem mit `fsck` prüft und repariert?",
|
||||
"options": [
|
||||
{ "id": "a", "text": "Das Dateisystem darf nicht (oder höchstens nur lesend) eingehängt sein.", "correct": true, "explanation": "Eine Reparatur an einem schreibend eingehängten Dateisystem kann es zerstören." },
|
||||
{ "id": "b", "text": "Das Dateisystem muss schreibend eingehängt sein.", "correct": false, "explanation": "Genau das sollte man vermeiden." },
|
||||
{ "id": "c", "text": "`fsck` funktioniert nur mit XFS.", "correct": false, "explanation": "`fsck` ist ein Frontend und ruft für ext4 `e2fsck` auf; für XFS ist es gerade nicht zuständig." },
|
||||
{ "id": "d", "text": "Vorher muss `mkfs` ausgeführt werden.", "correct": false, "explanation": "`mkfs` würde ein neues, leeres Dateisystem anlegen und alle Daten verlieren." },
|
||||
{ "id": "e", "text": "`fsck` darf nur auf Swap-Partitionen angewendet werden.", "correct": false, "explanation": "Swap enthält kein Dateisystem und wird nicht mit `fsck` geprüft." }
|
||||
],
|
||||
"explanation": "Die Root-Partition prüft man daher beim Start (sechstes Feld in `/etc/fstab`) oder aus einem Rettungssystem. `tune2fs` stellt die automatische Prüfung nach Anzahl Mounts/Zeit ein.",
|
||||
"references": ["man 8 fsck", "man 8 e2fsck"]
|
||||
},
|
||||
{
|
||||
"id": "104.2-002",
|
||||
"objective": "104.2",
|
||||
"type": "single",
|
||||
"difficulty": 2,
|
||||
"question": "Welches Werkzeug repariert ein beschädigtes **XFS**-Dateisystem?",
|
||||
"options": [
|
||||
{ "id": "a", "text": "`xfs_repair`", "correct": true, "explanation": "`xfs_repair` prüft und repariert XFS; mit `-n` nur prüfen, ohne zu ändern." },
|
||||
{ "id": "b", "text": "`xfs_fsr`", "correct": false, "explanation": "`xfs_fsr` defragmentiert ein eingehängtes XFS." },
|
||||
{ "id": "c", "text": "`fsck.xfs`", "correct": false, "explanation": "`fsck.xfs` tut absichtlich nichts und verweist auf `xfs_repair`; XFS prüft sich beim Mounten über das Journal." },
|
||||
{ "id": "d", "text": "`e2fsck`", "correct": false, "explanation": "`e2fsck` ist für ext2/3/4." },
|
||||
{ "id": "e", "text": "`xfs_info`", "correct": false, "explanation": "`xfs_info` zeigt nur die Geometrie eines XFS an." }
|
||||
],
|
||||
"explanation": "Weitere XFS-Werkzeuge: `xfs_db` (Debugger), `xfs_growfs` (vergrößern). ext-Werkzeuge: `e2fsck`, `tune2fs`, `dumpe2fs`, `debugfs`.",
|
||||
"references": ["man 8 xfs_repair"]
|
||||
},
|
||||
{
|
||||
"id": "104.2-003",
|
||||
"objective": "104.2",
|
||||
"type": "multi",
|
||||
"difficulty": 1,
|
||||
"question": "Welche Befehle helfen, den **Speicherplatz** auf Dateisystemen zu überwachen?",
|
||||
"options": [
|
||||
{ "id": "a", "text": "`df -h`", "correct": true, "explanation": "Belegter und freier Platz je eingehängtem Dateisystem; `df -i` zeigt die Inodes." },
|
||||
{ "id": "b", "text": "`du -sh /var/log`", "correct": true, "explanation": "Summe des belegten Platzes eines Verzeichnisbaums." },
|
||||
{ "id": "c", "text": "`free -h`", "correct": false, "explanation": "`free` zeigt Arbeitsspeicher und Swap, keinen Plattenplatz." },
|
||||
{ "id": "d", "text": "`blkid`", "correct": false, "explanation": "`blkid` zeigt UUID, Label und Typ, keine Belegung." },
|
||||
{ "id": "e", "text": "`mount`", "correct": false, "explanation": "`mount` ohne Argumente listet eingehängte Dateisysteme mit Optionen, ohne Belegung." }
|
||||
],
|
||||
"explanation": "Ein Dateisystem kann auch „voll“ sein, wenn noch Platz frei ist, aber keine Inodes mehr – dann hilft `df -i`.",
|
||||
"references": ["man 1 df", "man 1 du"]
|
||||
},
|
||||
{
|
||||
"id": "104.3-001",
|
||||
"objective": "104.3",
|
||||
"type": "single",
|
||||
"difficulty": 1,
|
||||
"question": "In welcher Reihenfolge stehen die sechs Felder eines Eintrags in `/etc/fstab`?",
|
||||
"options": [
|
||||
{ "id": "a", "text": "Gerät · Einhängepunkt · Dateisystemtyp · Optionen · dump · fsck-Reihenfolge", "correct": true, "explanation": "Beispiel: `UUID=… /home ext4 defaults 0 2`." },
|
||||
{ "id": "b", "text": "Einhängepunkt · Gerät · Dateisystemtyp · Optionen · dump · fsck-Reihenfolge", "correct": false, "explanation": "Das Gerät steht zuerst, danach der Einhängepunkt." },
|
||||
{ "id": "c", "text": "Gerät · Dateisystemtyp · Einhängepunkt · Optionen · dump · fsck-Reihenfolge", "correct": false, "explanation": "Der Einhängepunkt kommt vor dem Typ." },
|
||||
{ "id": "d", "text": "Gerät · Einhängepunkt · Optionen · Dateisystemtyp · fsck-Reihenfolge · dump", "correct": false, "explanation": "Typ vor Optionen; dump (5.) vor fsck-Reihenfolge (6.)." },
|
||||
{ "id": "e", "text": "Gerät · Einhängepunkt · Dateisystemtyp · Benutzer · Optionen · fsck-Reihenfolge", "correct": false, "explanation": "Ein Benutzerfeld gibt es nicht; `user`/`users` sind Optionen im vierten Feld." }
|
||||
],
|
||||
"explanation": "Sechstes Feld: 0 = nicht prüfen, 1 = Root-Dateisystem, 2 = andere Dateisysteme. systemd erzeugt aus fstab automatisch `.mount`-Units.",
|
||||
"references": ["man 5 fstab"]
|
||||
},
|
||||
{
|
||||
"id": "104.3-002",
|
||||
"objective": "104.3",
|
||||
"type": "fill",
|
||||
"difficulty": 1,
|
||||
"question": "Mit welchem Befehl (mit Option) hängt man alle in `/etc/fstab` eingetragenen Dateisysteme ein, die noch nicht eingehängt sind (außer denen mit `noauto`)?",
|
||||
"answers": ["mount -a", "mount --all"],
|
||||
"explanation": "`mount -a` eignet sich auch zum Testen eines neuen fstab-Eintrags vor dem nächsten Neustart.",
|
||||
"references": ["man 8 mount"]
|
||||
},
|
||||
{
|
||||
"id": "104.3-003",
|
||||
"objective": "104.3",
|
||||
"type": "single",
|
||||
"difficulty": 2,
|
||||
"question": "Welche Zeile ist ein gültiger `/etc/fstab`-Eintrag, der ein Dateisystem anhand seiner UUID nach `/data` einhängt?",
|
||||
"options": [
|
||||
{ "id": "a", "text": "`UUID=3f1c9a2e-77b0-4d6e-9c55-2b8e1d0a4f10 /data ext4 defaults 0 2`", "correct": true, "explanation": "Alle sechs Felder in der richtigen Reihenfolge; `UUID=` ist eine gültige Gerätekennung." },
|
||||
{ "id": "b", "text": "`/data UUID=3f1c9a2e-77b0-4d6e-9c55-2b8e1d0a4f10 ext4 defaults 0 2`", "correct": false, "explanation": "Gerät und Einhängepunkt sind vertauscht." },
|
||||
{ "id": "c", "text": "`UUID:3f1c9a2e-77b0-4d6e-9c55-2b8e1d0a4f10 /data ext4 defaults 0 2`", "correct": false, "explanation": "Die Schreibweise ist `UUID=` mit Gleichheitszeichen." },
|
||||
{ "id": "d", "text": "`UUID=3f1c9a2e-77b0-4d6e-9c55-2b8e1d0a4f10 /data defaults ext4 0 2`", "correct": false, "explanation": "Typ und Optionen sind vertauscht." },
|
||||
{ "id": "e", "text": "`/dev/disk/by-label/3f1c9a2e-77b0-4d6e-9c55-2b8e1d0a4f10 /data ext4 defaults 0 2`", "correct": false, "explanation": "`by-label` enthält Labels, keine UUIDs; der Pfad wäre `/dev/disk/by-uuid/…`." }
|
||||
],
|
||||
"explanation": "UUIDs und Labels sind stabil, Gerätenamen wie `/dev/sdb1` können sich beim Hinzufügen von Platten ändern. Auch `LABEL=…` ist erlaubt.",
|
||||
"references": ["man 5 fstab"]
|
||||
},
|
||||
{
|
||||
"id": "104.3-004",
|
||||
"objective": "104.3",
|
||||
"type": "multi",
|
||||
"difficulty": 1,
|
||||
"question": "Mit welchen Befehlen kann man die UUID des Dateisystems auf `/dev/sdb1` anzeigen?",
|
||||
"options": [
|
||||
{ "id": "a", "text": "`blkid /dev/sdb1`", "correct": true, "explanation": "`blkid` zeigt UUID, TYPE und ggf. LABEL." },
|
||||
{ "id": "b", "text": "`lsblk -f /dev/sdb1`", "correct": true, "explanation": "`-f` ergänzt die Spalten FSTYPE, LABEL und UUID." },
|
||||
{ "id": "c", "text": "`mount -U /dev/sdb1`", "correct": false, "explanation": "`mount -U <uuid>` hängt anhand einer UUID ein, zeigt aber keine an." },
|
||||
{ "id": "d", "text": "`df -u /dev/sdb1`", "correct": false, "explanation": "`df` hat keine Option zur UUID-Anzeige." },
|
||||
{ "id": "e", "text": "`swapon -s`", "correct": false, "explanation": "`swapon -s` listet aktive Swap-Bereiche." }
|
||||
],
|
||||
"explanation": "Die UUIDs stehen auch als Symlinks unter `/dev/disk/by-uuid/`.",
|
||||
"references": ["man 8 blkid", "man 8 lsblk"]
|
||||
},
|
||||
{
|
||||
"id": "104.5-001",
|
||||
"objective": "104.5",
|
||||
"type": "single",
|
||||
"difficulty": 1,
|
||||
"question": "Welche Rechte hat eine Datei nach `chmod 750 datei`?",
|
||||
"options": [
|
||||
{ "id": "a", "text": "`rwxr-x---`", "correct": true, "explanation": "7 = rwx (Eigentümer), 5 = r-x (Gruppe), 0 = --- (andere)." },
|
||||
{ "id": "b", "text": "`rwxrw----`", "correct": false, "explanation": "Das wäre 760." },
|
||||
{ "id": "c", "text": "`rw-r-x---`", "correct": false, "explanation": "Das wäre 650." },
|
||||
{ "id": "d", "text": "`rwxr-xr-x`", "correct": false, "explanation": "Das wäre 755." },
|
||||
{ "id": "e", "text": "`r-xrwx---`", "correct": false, "explanation": "Das wäre 570." }
|
||||
],
|
||||
"explanation": "Wertigkeit: r = 4, w = 2, x = 1, je Stelle für Eigentümer, Gruppe, andere addiert.",
|
||||
"references": ["man 1 chmod"]
|
||||
},
|
||||
{
|
||||
"id": "104.5-002",
|
||||
"objective": "104.5",
|
||||
"type": "fill",
|
||||
"difficulty": 1,
|
||||
"question": "Welcher oktale Modus (drei Ziffern) entspricht den Rechten `rw-r--r--`?",
|
||||
"answers": ["644", "0644"],
|
||||
"explanation": "rw- = 4+2 = 6, r-- = 4, r-- = 4. Typisch für normale Dateien; Verzeichnisse haben meist 755.",
|
||||
"references": ["man 1 chmod"]
|
||||
},
|
||||
{
|
||||
"id": "104.5-003",
|
||||
"objective": "104.5",
|
||||
"type": "single",
|
||||
"difficulty": 2,
|
||||
"question": "Welche Wirkung hat das SUID-Bit auf eine ausführbare Datei (z. B. `/usr/bin/passwd`)?",
|
||||
"options": [
|
||||
{ "id": "a", "text": "Das Programm läuft mit den Rechten des Datei-Eigentümers statt mit denen des Aufrufers.", "correct": true, "explanation": "Deshalb kann `passwd` (Eigentümer root) `/etc/shadow` ändern." },
|
||||
{ "id": "b", "text": "Neue Dateien im Verzeichnis erben die Gruppe des Verzeichnisses.", "correct": false, "explanation": "Das ist die Wirkung des SGID-Bits auf ein Verzeichnis." },
|
||||
{ "id": "c", "text": "Nur der Eigentümer darf Dateien im Verzeichnis löschen.", "correct": false, "explanation": "Das ist das Sticky-Bit auf Verzeichnissen, z. B. `/tmp`." },
|
||||
{ "id": "d", "text": "Die Datei kann nicht mehr geändert oder gelöscht werden.", "correct": false, "explanation": "Das beschreibt das immutable-Attribut (`chattr +i`)." },
|
||||
{ "id": "e", "text": "Das Programm darf nur von root ausgeführt werden.", "correct": false, "explanation": "Ausführen dürfen alle mit x-Recht; SUID ändert die effektive Benutzer-ID." }
|
||||
],
|
||||
"explanation": "Setzen: `chmod u+s datei` oder oktal 4xxx (SGID 2xxx, Sticky 1xxx). In `ls -l` erscheint `s` statt `x` beim Eigentümer.",
|
||||
"references": ["man 1 chmod", "man 7 credentials"]
|
||||
},
|
||||
{
|
||||
"id": "104.5-004",
|
||||
"objective": "104.5",
|
||||
"type": "single",
|
||||
"difficulty": 2,
|
||||
"question": "Die umask ist `027`. Welche Rechte erhält eine neu angelegte **reguläre Datei** (z. B. mit `touch`)?",
|
||||
"options": [
|
||||
{ "id": "a", "text": "`640` (`rw-r-----`)", "correct": true, "explanation": "Dateien starten mit 666; davon werden 027 maskiert → 640." },
|
||||
{ "id": "b", "text": "`750` (`rwxr-x---`)", "correct": false, "explanation": "Das erhält ein neues **Verzeichnis** (777 − 027)." },
|
||||
{ "id": "c", "text": "`027` (`----w-rwx`)", "correct": false, "explanation": "Die umask gibt an, was entfernt wird, nicht die Rechte selbst." },
|
||||
{ "id": "d", "text": "`751` (`rwxr-x--x`)", "correct": false, "explanation": "Das passt weder zu Dateien noch zu Verzeichnissen mit dieser umask." },
|
||||
{ "id": "e", "text": "`600` (`rw-------`)", "correct": false, "explanation": "Das ergäbe die umask 077." }
|
||||
],
|
||||
"explanation": "Programme erzeugen Dateien üblicherweise mit 666 und Verzeichnisse mit 777; die umask entfernt Bits. Anzeigen/Setzen mit `umask`, symbolisch mit `umask -S`.",
|
||||
"references": ["help umask"]
|
||||
},
|
||||
{
|
||||
"id": "104.5-005",
|
||||
"objective": "104.5",
|
||||
"type": "multi",
|
||||
"difficulty": 1,
|
||||
"question": "Mit welchen Befehlen ändert man die **Gruppe** der Datei `bericht.txt` auf `team`?",
|
||||
"options": [
|
||||
{ "id": "a", "text": "`chgrp team bericht.txt`", "correct": true, "explanation": "`chgrp` ändert nur die Gruppe." },
|
||||
{ "id": "b", "text": "`chown :team bericht.txt`", "correct": true, "explanation": "Bei `chown` ändert die Form `:gruppe` nur die Gruppe (auch `.gruppe` wird noch akzeptiert)." },
|
||||
{ "id": "c", "text": "`chmod g=team bericht.txt`", "correct": false, "explanation": "`chmod g=` setzt Rechte der Gruppe; `team` ist keine gültige Rechteangabe." },
|
||||
{ "id": "d", "text": "`newgrp team bericht.txt`", "correct": false, "explanation": "`newgrp` startet eine Shell mit anderer primärer Gruppe; es ändert keine Datei." },
|
||||
{ "id": "e", "text": "`usermod -g team bericht.txt`", "correct": false, "explanation": "`usermod -g` ändert die primäre Gruppe eines Benutzers." }
|
||||
],
|
||||
"explanation": "`chown benutzer:gruppe datei` ändert beides; `-R` arbeitet rekursiv. Normale Benutzer dürfen nur auf Gruppen wechseln, in denen sie Mitglied sind.",
|
||||
"references": ["man 1 chgrp", "man 1 chown"]
|
||||
},
|
||||
{
|
||||
"id": "104.6-001",
|
||||
"objective": "104.6",
|
||||
"type": "single",
|
||||
"difficulty": 1,
|
||||
"question": "Es soll ein symbolischer Link `aktuell` angelegt werden, der auf `release-2.4` zeigt. Welcher Befehl ist korrekt?",
|
||||
"options": [
|
||||
{ "id": "a", "text": "`ln -s release-2.4 aktuell`", "correct": true, "explanation": "Reihenfolge wie bei `cp`: erst Ziel (Original), dann der Name des Links." },
|
||||
{ "id": "b", "text": "`ln -s aktuell release-2.4`", "correct": false, "explanation": "Vertauscht: Das legt `release-2.4` als Link auf `aktuell` an (bzw. scheitert, wenn es existiert)." },
|
||||
{ "id": "c", "text": "`ln release-2.4 aktuell`", "correct": false, "explanation": "Ohne `-s` entsteht ein Hardlink; für Verzeichnisse ist das nicht erlaubt." },
|
||||
{ "id": "d", "text": "`link -s release-2.4 aktuell`", "correct": false, "explanation": "`link` legt nur Hardlinks an und kennt kein `-s`." },
|
||||
{ "id": "e", "text": "`mklink aktuell release-2.4`", "correct": false, "explanation": "`mklink` ist ein Windows-Befehl." }
|
||||
],
|
||||
"explanation": "Ein bestehenden Link umsetzen: `ln -sfn neues-ziel aktuell`.",
|
||||
"references": ["man 1 ln"]
|
||||
},
|
||||
{
|
||||
"id": "104.6-002",
|
||||
"objective": "104.6",
|
||||
"type": "multi",
|
||||
"difficulty": 2,
|
||||
"question": "Welche Aussagen über **Hardlinks** treffen zu?",
|
||||
"options": [
|
||||
{ "id": "a", "text": "Alle Hardlinks einer Datei verweisen auf denselben Inode.", "correct": true, "explanation": "Ein Hardlink ist nur ein weiterer Verzeichniseintrag für denselben Inode." },
|
||||
{ "id": "b", "text": "Hardlinks können nicht über Dateisystemgrenzen hinweg angelegt werden.", "correct": true, "explanation": "Inode-Nummern sind nur innerhalb eines Dateisystems eindeutig." },
|
||||
{ "id": "c", "text": "Normale Benutzer können Hardlinks auf Verzeichnisse anlegen.", "correct": false, "explanation": "Hardlinks auf Verzeichnisse sind nicht erlaubt (Schleifen im Baum)." },
|
||||
{ "id": "d", "text": "Wird die ursprüngliche Datei gelöscht, ist der Hardlink defekt.", "correct": false, "explanation": "Die Daten bleiben, bis der Linkzähler 0 erreicht; defekt werden nur Symlinks." },
|
||||
{ "id": "e", "text": "Ein Hardlink hat eigene, vom Original unabhängige Zugriffsrechte.", "correct": false, "explanation": "Rechte stehen im Inode und gelten für alle Namen gemeinsam." }
|
||||
],
|
||||
"explanation": "Der Linkzähler steht in `ls -l` in der zweiten Spalte. Symlinks haben einen eigenen Inode und enthalten den Pfad zum Ziel.",
|
||||
"references": ["man 1 ln", "man 7 inode"]
|
||||
},
|
||||
{
|
||||
"id": "104.6-003",
|
||||
"objective": "104.6",
|
||||
"type": "fill",
|
||||
"difficulty": 1,
|
||||
"question": "Welcher Befehl mit Option zeigt beim Auflisten eines Verzeichnisses die **Inode-Nummern** der Dateien an?",
|
||||
"answers": ["ls -i", "ls --inode", "ls -li", "ls -il"],
|
||||
"explanation": "Gleiche Inode-Nummer = Hardlinks derselben Datei. Alternativ zeigt `stat datei` den Inode einer einzelnen Datei.",
|
||||
"references": ["man 1 ls"]
|
||||
},
|
||||
{
|
||||
"id": "104.7-001",
|
||||
"objective": "104.7",
|
||||
"type": "single",
|
||||
"difficulty": 1,
|
||||
"question": "Wo gehören laut Filesystem Hierarchy Standard (FHS) die Protokolldateien des Systems hin?",
|
||||
"options": [
|
||||
{ "id": "a", "text": "`/var/log`", "correct": true, "explanation": "`/var` enthält variable Daten; `/var/log` die Protokolle." },
|
||||
{ "id": "b", "text": "`/usr/log`", "correct": false, "explanation": "`/usr` ist für weitgehend statische, teilbare Daten gedacht und kann nur lesend eingehängt sein." },
|
||||
{ "id": "c", "text": "`/etc/log`", "correct": false, "explanation": "`/etc` ist für hostspezifische Konfiguration gedacht." },
|
||||
{ "id": "d", "text": "`/opt/log`", "correct": false, "explanation": "`/opt` ist für zusätzliche Softwarepakete gedacht; variable Daten solcher Pakete gehören nach `/var/opt`." },
|
||||
{ "id": "e", "text": "`/srv/log`", "correct": false, "explanation": "`/srv` enthält Daten, die das System als Dienst ausliefert (z. B. Web, FTP)." }
|
||||
],
|
||||
"explanation": "Weitere FHS-Orte: `/var/spool` (Warteschlangen), `/var/cache`, `/var/lib` (Zustandsdaten), `/tmp` (temporär), `/usr/local` (lokal installierte Software).",
|
||||
"references": ["man 7 hier", "https://refspecs.linuxfoundation.org/fhs.shtml"]
|
||||
},
|
||||
{
|
||||
"id": "104.7-002",
|
||||
"objective": "104.7",
|
||||
"type": "single",
|
||||
"difficulty": 1,
|
||||
"question": "`locate` findet eine soeben angelegte Datei nicht. Was ist zu tun?",
|
||||
"options": [
|
||||
{ "id": "a", "text": "Die Datenbank mit `updatedb` aktualisieren", "correct": true, "explanation": "`locate` durchsucht eine Datenbank, die meist täglich per Cron/Timer mit `updatedb` erneuert wird." },
|
||||
{ "id": "b", "text": "`hash -r` ausführen", "correct": false, "explanation": "`hash -r` leert die Tabelle der Programmpfade der Shell." },
|
||||
{ "id": "c", "text": "`ldconfig` ausführen", "correct": false, "explanation": "`ldconfig` aktualisiert den Cache der Shared Libraries." },
|
||||
{ "id": "d", "text": "`mandb` ausführen", "correct": false, "explanation": "`mandb` aktualisiert den Index der Manpages." },
|
||||
{ "id": "e", "text": "`whereis -u` ausführen", "correct": false, "explanation": "`whereis -u` sucht nach „ungewöhnlichen“ Einträgen; mit der locate-Datenbank hat es nichts zu tun." }
|
||||
],
|
||||
"explanation": "Welche Pfade `updatedb` auslässt, steht in `/etc/updatedb.conf` (z. B. `PRUNEPATHS`, `PRUNEFS`). `find` durchsucht dagegen live das Dateisystem.",
|
||||
"references": ["man 8 updatedb", "man 5 updatedb.conf"]
|
||||
},
|
||||
{
|
||||
"id": "104.7-003",
|
||||
"objective": "104.7",
|
||||
"type": "multi",
|
||||
"difficulty": 1,
|
||||
"question": "Mit welchen Befehlen findet man heraus, wo das **Programm** `ls` im Dateisystem liegt?",
|
||||
"options": [
|
||||
{ "id": "a", "text": "`which ls`", "correct": true, "explanation": "`which` durchsucht den `PATH` und gibt den ersten Treffer aus." },
|
||||
{ "id": "b", "text": "`whereis ls`", "correct": true, "explanation": "`whereis` zeigt Programm, Quellen und Manpages zu einem Befehl." },
|
||||
{ "id": "c", "text": "`whatis ls`", "correct": false, "explanation": "`whatis` zeigt die Kurzbeschreibung aus der Manpage." },
|
||||
{ "id": "d", "text": "`file ls`", "correct": false, "explanation": "`file` bestimmt den Typ einer Datei; `ls` würde im aktuellen Verzeichnis gesucht." },
|
||||
{ "id": "e", "text": "`stat ls`", "correct": false, "explanation": "`stat` zeigt Inode-Daten einer Datei `ls` im aktuellen Verzeichnis." }
|
||||
],
|
||||
"explanation": "Für Shell-Aliase und Builtins hilft `type`; für beliebige Dateien `find` (live) oder `locate` (Datenbank).",
|
||||
"references": ["man 1 which", "man 1 whereis"]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
{
|
||||
"meta": {
|
||||
"file": "exam.json",
|
||||
"brief": "Prüfungsdefinition LPIC-1 102-500: Eckdaten, Topics, Objectives mit Gewichtung",
|
||||
"author": "Björn Runden <br@runden.de>",
|
||||
"owner": "Björn Runden <br@runden.de>",
|
||||
"assist": "KI-unterstützt (Claude Opus 5.5), geprüft von Björn Runden <br@runden.de>",
|
||||
"version": "0.1.0 (29.10.2026)"
|
||||
},
|
||||
"exam": {
|
||||
"id": "lpic1-102",
|
||||
"level": "LPIC-1",
|
||||
"title": "LPIC-1 Prüfung 102",
|
||||
"code": "102-500",
|
||||
"lang": "de",
|
||||
"order": 102,
|
||||
"objectives_version": "5.0",
|
||||
"objectives_source": "https://www.lpi.org/de/exam-101-102-objectives/",
|
||||
"objectives_retrieved": "2026-10-29",
|
||||
"questions": 60,
|
||||
"duration_min": 90,
|
||||
"pass_percent": 65
|
||||
},
|
||||
"topics": [
|
||||
{
|
||||
"id": "105",
|
||||
"title": "Shells und Shell-Skripte",
|
||||
"objectives": [
|
||||
{ "id": "105.1", "title": "Die Shell-Umgebung anpassen und verwenden", "weight": 4 },
|
||||
{ "id": "105.2", "title": "Einfache Skripte anpassen oder schreiben", "weight": 4 }
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "106",
|
||||
"title": "Benutzerschnittstellen und Desktops",
|
||||
"objectives": [
|
||||
{ "id": "106.1", "title": "X11 installieren und konfigurieren", "weight": 2 },
|
||||
{ "id": "106.2", "title": "Grafische Desktops", "weight": 1 },
|
||||
{ "id": "106.3", "title": "Barrierefreiheit", "weight": 1 }
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "107",
|
||||
"title": "Administrative Aufgaben",
|
||||
"objectives": [
|
||||
{ "id": "107.1", "title": "Benutzer- und Gruppenkonten und dazugehörige Systemdateien verwalten", "weight": 5 },
|
||||
{ "id": "107.2", "title": "Systemadministrationsaufgaben durch Einplanen von Jobs automatisieren", "weight": 4 },
|
||||
{ "id": "107.3", "title": "Lokalisierung und Internationalisierung", "weight": 3 }
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "108",
|
||||
"title": "Grundlegende Systemdienste",
|
||||
"objectives": [
|
||||
{ "id": "108.1", "title": "Die Systemzeit verwalten", "weight": 3 },
|
||||
{ "id": "108.2", "title": "Systemprotokollierung", "weight": 4 },
|
||||
{ "id": "108.3", "title": "Grundlagen von Mail Transfer Agents (MTA)", "weight": 3 },
|
||||
{ "id": "108.4", "title": "Drucker und Druckvorgänge verwalten", "weight": 2 }
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "109",
|
||||
"title": "Netzwerkgrundlagen",
|
||||
"objectives": [
|
||||
{ "id": "109.1", "title": "Grundlagen von Internetprotokollen", "weight": 4 },
|
||||
{ "id": "109.2", "title": "Persistente Netzwerkkonfiguration", "weight": 4 },
|
||||
{ "id": "109.3", "title": "Grundlegende Netzwerkfehlerbehebung", "weight": 4 },
|
||||
{ "id": "109.4", "title": "Clientseitiges DNS konfigurieren", "weight": 2 }
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "110",
|
||||
"title": "Sicherheit",
|
||||
"objectives": [
|
||||
{ "id": "110.1", "title": "Administrationsaufgaben für Sicherheit durchführen", "weight": 3 },
|
||||
{ "id": "110.2", "title": "Einen Rechner absichern", "weight": 3 },
|
||||
{ "id": "110.3", "title": "Daten durch Verschlüsselung schützen", "weight": 4 }
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,195 @@
|
||||
{
|
||||
"$schema": "../schema/questions.schema.json",
|
||||
"format": "lpic-sim-questions",
|
||||
"meta": {
|
||||
"file": "questions_105.json",
|
||||
"brief": "Fragen zu Topic 105 Shells und Shell-Skripte (LPIC-1 102-500, Objectives 5.0)",
|
||||
"author": "Björn Runden <br@runden.de>",
|
||||
"owner": "Björn Runden <br@runden.de>",
|
||||
"assist": "KI-unterstützt (Claude Opus 5.5), geprüft von Björn Runden <br@runden.de>",
|
||||
"version": "0.1.0 (29.10.2026)"
|
||||
},
|
||||
"exam": "lpic1-102",
|
||||
"questions": [
|
||||
{
|
||||
"id": "105.1-001",
|
||||
"objective": "105.1",
|
||||
"type": "single",
|
||||
"difficulty": 2,
|
||||
"question": "Welche Datei liest eine interaktive **Login**-Bash als erste ein?",
|
||||
"options": [
|
||||
{ "id": "a", "text": "`/etc/profile`", "correct": true, "explanation": "Login-Shells lesen zuerst die systemweite `/etc/profile`, danach die erste vorhandene aus `~/.bash_profile`, `~/.bash_login`, `~/.profile`." },
|
||||
{ "id": "b", "text": "`~/.bashrc`", "correct": false, "explanation": "`~/.bashrc` lesen interaktive Nicht-Login-Shells; Login-Shells nur, wenn `~/.bash_profile` sie einbindet." },
|
||||
{ "id": "c", "text": "`~/.bash_profile`", "correct": false, "explanation": "Wird gelesen, aber erst nach `/etc/profile`." },
|
||||
{ "id": "d", "text": "`/etc/bash.bashrc`", "correct": false, "explanation": "Systemweite Datei für interaktive Nicht-Login-Shells (Debian); wird oft über `/etc/profile` eingebunden." },
|
||||
{ "id": "e", "text": "`~/.bash_logout`", "correct": false, "explanation": "Wird beim Beenden einer Login-Shell ausgeführt." }
|
||||
],
|
||||
"explanation": "Login-Shell: `/etc/profile` → `~/.bash_profile` | `~/.bash_login` | `~/.profile`. Nicht-Login-Shell: `/etc/bash.bashrc` (distributionsabhängig) → `~/.bashrc`.",
|
||||
"references": ["man 1 bash (Abschnitt INVOCATION)"]
|
||||
},
|
||||
{
|
||||
"id": "105.1-002",
|
||||
"objective": "105.1",
|
||||
"type": "multi",
|
||||
"difficulty": 1,
|
||||
"question": "Welche Schreibweisen definieren in der Bash korrekt eine Funktion `gruss`?",
|
||||
"options": [
|
||||
{ "id": "a", "text": "`gruss() { echo Hallo; }`", "correct": true, "explanation": "POSIX-Schreibweise; vor der schließenden Klammer ist `;` oder ein Zeilenumbruch nötig." },
|
||||
{ "id": "b", "text": "`function gruss { echo Hallo; }`", "correct": true, "explanation": "Bash-spezifische Schreibweise mit dem Schlüsselwort `function`." },
|
||||
{ "id": "c", "text": "`def gruss(): echo Hallo`", "correct": false, "explanation": "Das ist Python-ähnliche Syntax." },
|
||||
{ "id": "d", "text": "`gruss = { echo Hallo }`", "correct": false, "explanation": "Leerzeichen um `=` und diese Klammerung sind keine gültige Funktionssyntax." },
|
||||
{ "id": "e", "text": "`alias gruss() echo Hallo`", "correct": false, "explanation": "Aliase werden mit `alias name='befehl'` gesetzt und haben keine Klammern." }
|
||||
],
|
||||
"explanation": "Funktionen sehen Argumente als `$1`, `$2`, …; `declare -f` listet definierte Funktionen. Dauerhaft z. B. in `~/.bashrc`.",
|
||||
"references": ["man 1 bash (Abschnitt FUNCTIONS)"]
|
||||
},
|
||||
{
|
||||
"id": "105.1-003",
|
||||
"objective": "105.1",
|
||||
"type": "fill",
|
||||
"difficulty": 2,
|
||||
"question": "Welches Bash-Builtin führt die Befehle einer Datei in der **aktuellen** Shell aus, sodass dort gesetzte Variablen danach erhalten bleiben? (Name des Builtins)",
|
||||
"answers": ["source", "."],
|
||||
"explanation": "`source ~/.bashrc` bzw. `. ~/.bashrc`. Ein Aufruf als `./datei` oder `bash datei` läuft dagegen in einer Subshell; deren Variablen gehen verloren.",
|
||||
"references": ["help source"]
|
||||
},
|
||||
{
|
||||
"id": "105.1-004",
|
||||
"objective": "105.1",
|
||||
"type": "single",
|
||||
"difficulty": 1,
|
||||
"question": "Der Alias `ll='ls -l'` soll für einen Benutzer dauerhaft in jeder interaktiven Bash verfügbar sein. Wo trägt man ihn ein?",
|
||||
"options": [
|
||||
{ "id": "a", "text": "`~/.bashrc`", "correct": true, "explanation": "`~/.bashrc` wird von jeder interaktiven Nicht-Login-Bash gelesen und meist auch aus `~/.bash_profile` eingebunden." },
|
||||
{ "id": "b", "text": "`/etc/aliases`", "correct": false, "explanation": "`/etc/aliases` enthält E-Mail-Aliase für den MTA, keine Shell-Aliase." },
|
||||
{ "id": "c", "text": "`~/.bash_logout`", "correct": false, "explanation": "Wird nur beim Abmelden ausgeführt." },
|
||||
{ "id": "d", "text": "`/etc/shells`", "correct": false, "explanation": "`/etc/shells` listet die zulässigen Login-Shells." },
|
||||
{ "id": "e", "text": "`~/.bash_history`", "correct": false, "explanation": "Enthält nur die gespeicherte Befehls-History." }
|
||||
],
|
||||
"explanation": "Ein Alias in der Shell gilt nur bis zum Ende der Sitzung; dauerhaft wird er durch einen Eintrag in einer Startdatei.",
|
||||
"references": ["help alias"]
|
||||
},
|
||||
{
|
||||
"id": "105.1-005",
|
||||
"objective": "105.1",
|
||||
"type": "single",
|
||||
"difficulty": 1,
|
||||
"question": "Das Verzeichnis `/opt/tools/bin` soll **zusätzlich** zum bisherigen Suchpfad verwendet werden. Welcher Befehl ist korrekt?",
|
||||
"options": [
|
||||
{ "id": "a", "text": "`export PATH=$PATH:/opt/tools/bin`", "correct": true, "explanation": "Der alte Inhalt bleibt erhalten, das neue Verzeichnis wird mit `:` angehängt." },
|
||||
{ "id": "b", "text": "`export PATH=/opt/tools/bin`", "correct": false, "explanation": "Ersetzt den Suchpfad; Standardbefehle wie `ls` werden nicht mehr gefunden." },
|
||||
{ "id": "c", "text": "`export PATH=$PATH;/opt/tools/bin`", "correct": false, "explanation": "`;` trennt Befehle; die Shell würde `/opt/tools/bin` als Befehl ausführen." },
|
||||
{ "id": "d", "text": "`export $PATH=$PATH:/opt/tools/bin`", "correct": false, "explanation": "Links vom `=` steht der Variablenname ohne `$`." },
|
||||
{ "id": "e", "text": "`set PATH /opt/tools/bin`", "correct": false, "explanation": "Das ist csh-Syntax; in der Bash setzt `set` Positionsparameter." }
|
||||
],
|
||||
"explanation": "Verzeichnisse im `PATH` werden von links nach rechts durchsucht; `$PATH` voranzustellen oder anzuhängen bestimmt die Priorität.",
|
||||
"references": ["man 1 bash (Abschnitt Shell Variables)"]
|
||||
},
|
||||
{
|
||||
"id": "105.1-006",
|
||||
"objective": "105.1",
|
||||
"type": "single",
|
||||
"difficulty": 1,
|
||||
"question": "Welches Verzeichnis enthält die Vorlagen (z. B. `.bashrc`, `.profile`), die beim Anlegen eines Benutzers mit `useradd -m` in dessen Home-Verzeichnis kopiert werden?",
|
||||
"options": [
|
||||
{ "id": "a", "text": "`/etc/skel`", "correct": true, "explanation": "Der Inhalt von `/etc/skel` wird in neue Home-Verzeichnisse kopiert." },
|
||||
{ "id": "b", "text": "`/etc/default/useradd`", "correct": false, "explanation": "Datei mit Standardwerten für `useradd` (u. a. `SKEL=`), kein Vorlagenverzeichnis." },
|
||||
{ "id": "c", "text": "`/etc/profile.d`", "correct": false, "explanation": "Enthält Skripte, die `/etc/profile` beim Login einbindet." },
|
||||
{ "id": "d", "text": "`/etc/login.defs`", "correct": false, "explanation": "Datei mit Vorgaben wie UID-Bereichen und Passwort-Alterung." },
|
||||
{ "id": "e", "text": "`/home/template`", "correct": false, "explanation": "Kein Standardpfad." }
|
||||
],
|
||||
"explanation": "Änderungen an `/etc/skel` wirken nur für künftig angelegte Benutzer.",
|
||||
"references": ["man 8 useradd"]
|
||||
},
|
||||
{
|
||||
"id": "105.2-001",
|
||||
"objective": "105.2",
|
||||
"type": "single",
|
||||
"difficulty": 1,
|
||||
"question": "Welche Variable enthält in einem Shell-Skript die **Anzahl** der übergebenen Argumente?",
|
||||
"options": [
|
||||
{ "id": "a", "text": "`$#`", "correct": true, "explanation": "`$#` ist die Anzahl der Positionsparameter." },
|
||||
{ "id": "b", "text": "`$0`", "correct": false, "explanation": "`$0` ist der Name des Skripts." },
|
||||
{ "id": "c", "text": "`$@`", "correct": false, "explanation": "`$@` sind alle Argumente als einzelne Wörter." },
|
||||
{ "id": "d", "text": "`$?`", "correct": false, "explanation": "`$?` ist der Exit-Status des letzten Befehls." },
|
||||
{ "id": "e", "text": "`$$`", "correct": false, "explanation": "`$$` ist die PID der Shell." }
|
||||
],
|
||||
"explanation": "Weitere: `$1`…`$9` Argumente, `${10}` ab dem zehnten, `$*` alle als ein Wort (in Anführungszeichen), `shift` verschiebt die Argumente.",
|
||||
"references": ["man 1 bash (Abschnitt Special Parameters)"]
|
||||
},
|
||||
{
|
||||
"id": "105.2-002",
|
||||
"objective": "105.2",
|
||||
"type": "single",
|
||||
"difficulty": 1,
|
||||
"question": "Mit welcher Bedingung prüft ein Skript, ob `/etc/app.conf` existiert **und eine reguläre Datei** ist?",
|
||||
"options": [
|
||||
{ "id": "a", "text": "`[ -f /etc/app.conf ]`", "correct": true, "explanation": "`-f` ist wahr für existierende reguläre Dateien." },
|
||||
{ "id": "b", "text": "`[ -d /etc/app.conf ]`", "correct": false, "explanation": "`-d` prüft auf ein Verzeichnis." },
|
||||
{ "id": "c", "text": "`[ -e /etc/app.conf ]`", "correct": false, "explanation": "`-e` prüft nur auf Existenz, gleich welcher Typ (auch Verzeichnis)." },
|
||||
{ "id": "d", "text": "`[ -x /etc/app.conf ]`", "correct": false, "explanation": "`-x` prüft auf Ausführbarkeit." },
|
||||
{ "id": "e", "text": "`[ -L /etc/app.conf ]`", "correct": false, "explanation": "`-L` prüft auf einen symbolischen Link." }
|
||||
],
|
||||
"explanation": "`[ … ]` ist der Befehl `test`; Leerzeichen innerhalb der Klammern sind Pflicht. Weitere Tests: `-s` nicht leer, `-r`/`-w` lesbar/schreibbar, `-z`/`-n` leerer/nicht leerer String.",
|
||||
"references": ["man 1 test"]
|
||||
},
|
||||
{
|
||||
"id": "105.2-003",
|
||||
"objective": "105.2",
|
||||
"type": "fill",
|
||||
"difficulty": 1,
|
||||
"question": "Welche spezielle Variable enthält den Exit-Status (Rückgabewert) des zuletzt ausgeführten Befehls?",
|
||||
"answers": ["$?"],
|
||||
"explanation": "0 bedeutet Erfolg, alles andere einen Fehler. Darauf bauen `&&`, `||` und `if` auf.",
|
||||
"references": ["man 1 bash (Abschnitt Special Parameters)"]
|
||||
},
|
||||
{
|
||||
"id": "105.2-004",
|
||||
"objective": "105.2",
|
||||
"type": "single",
|
||||
"difficulty": 1,
|
||||
"question": "Welche Schleife ist in der Bash syntaktisch korrekt?",
|
||||
"options": [
|
||||
{ "id": "a", "text": "`for i in 1 2 3; do echo $i; done`", "correct": true, "explanation": "Aufbau: `for <var> in <liste>; do … done`." },
|
||||
{ "id": "b", "text": "`for i in 1 2 3; then echo $i; fi`", "correct": false, "explanation": "`then`/`fi` gehören zu `if`." },
|
||||
{ "id": "c", "text": "`for (i in 1 2 3) { echo $i }`", "correct": false, "explanation": "C/JavaScript-ähnliche Syntax; die Bash kennt das nicht." },
|
||||
{ "id": "d", "text": "`foreach i (1 2 3) echo $i end`", "correct": false, "explanation": "`foreach … end` ist csh-Syntax." },
|
||||
{ "id": "e", "text": "`for i = 1 to 3; do echo $i; done`", "correct": false, "explanation": "BASIC-ähnlich; nicht gültig. Zahlenreihen: `$(seq 1 3)` oder `{1..3}`." }
|
||||
],
|
||||
"explanation": "Weitere Schleifen: `while bedingung; do … done`, `until …`. C-Stil in der Bash: `for ((i=1; i<=3; i++)); do … done`.",
|
||||
"references": ["man 1 bash (Abschnitt Compound Commands)"]
|
||||
},
|
||||
{
|
||||
"id": "105.2-005",
|
||||
"objective": "105.2",
|
||||
"type": "multi",
|
||||
"difficulty": 2,
|
||||
"question": "Welche Aussagen zum Ausführen von Shell-Skripten treffen zu?",
|
||||
"options": [
|
||||
{ "id": "a", "text": "Die erste Zeile `#!/bin/bash` legt den Interpreter fest, wenn das Skript direkt ausgeführt wird.", "correct": true, "explanation": "Der Kernel wertet die Shebang-Zeile aus und startet den angegebenen Interpreter." },
|
||||
{ "id": "b", "text": "Für den Aufruf `./skript.sh` braucht die Datei das Ausführungsrecht.", "correct": true, "explanation": "Ohne `x`-Recht meldet die Shell „Permission denied“." },
|
||||
{ "id": "c", "text": "Auch `bash skript.sh` funktioniert nur mit Ausführungsrecht.", "correct": false, "explanation": "Hier ist `bash` das ausgeführte Programm; es braucht nur Leserecht auf das Skript." },
|
||||
{ "id": "d", "text": "Die Shebang-Zeile darf an beliebiger Stelle im Skript stehen.", "correct": false, "explanation": "Sie wirkt nur als allererste Zeile und muss mit `#!` beginnen." },
|
||||
{ "id": "e", "text": "Das aktuelle Verzeichnis ist standardmäßig im `PATH`; `skript.sh` ohne `./` genügt.", "correct": false, "explanation": "Aus Sicherheitsgründen steht `.` normalerweise nicht im `PATH`." }
|
||||
],
|
||||
"explanation": "SUID/SGID-Bits werden bei Skripten unter Linux ignoriert. Mit `exec` ersetzt ein Skript sich selbst durch ein anderes Programm.",
|
||||
"references": ["man 2 execve"]
|
||||
},
|
||||
{
|
||||
"id": "105.2-006",
|
||||
"objective": "105.2",
|
||||
"type": "single",
|
||||
"difficulty": 1,
|
||||
"question": "Welcher Befehl liest in einem Skript eine Zeile von der Tastatur in die Variable `NAME` ein?",
|
||||
"options": [
|
||||
{ "id": "a", "text": "`read NAME`", "correct": true, "explanation": "`read` liest eine Zeile von stdin; `-p \"Text\"` zeigt vorher eine Eingabeaufforderung an." },
|
||||
{ "id": "b", "text": "`input NAME`", "correct": false, "explanation": "Einen Befehl `input` gibt es in der Bash nicht." },
|
||||
{ "id": "c", "text": "`echo $NAME`", "correct": false, "explanation": "Gibt den Inhalt der Variable aus, liest nichts ein." },
|
||||
{ "id": "d", "text": "`NAME < /dev/stdin`", "correct": false, "explanation": "Die Shell versucht, einen Befehl namens `NAME` auszuführen; eine Variable wird nicht gesetzt." },
|
||||
{ "id": "e", "text": "`get NAME`", "correct": false, "explanation": "Einen Befehl `get` gibt es in der Bash nicht." }
|
||||
],
|
||||
"explanation": "Mehrere Variablen: `read VORNAME NACHNAME` verteilt die Wörter nach `IFS`. Ohne Variablenname landet die Eingabe in `REPLY`.",
|
||||
"references": ["help read"]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,127 @@
|
||||
{
|
||||
"$schema": "../schema/questions.schema.json",
|
||||
"format": "lpic-sim-questions",
|
||||
"meta": {
|
||||
"file": "questions_106.json",
|
||||
"brief": "Fragen zu Topic 106 Benutzerschnittstellen und Desktops (LPIC-1 102-500, Objectives 5.0)",
|
||||
"author": "Björn Runden <br@runden.de>",
|
||||
"owner": "Björn Runden <br@runden.de>",
|
||||
"assist": "KI-unterstützt (Claude Opus 5.5), geprüft von Björn Runden <br@runden.de>",
|
||||
"version": "0.1.0 (29.10.2026)"
|
||||
},
|
||||
"exam": "lpic1-102",
|
||||
"questions": [
|
||||
{
|
||||
"id": "106.1-001",
|
||||
"objective": "106.1",
|
||||
"type": "single",
|
||||
"difficulty": 1,
|
||||
"question": "Welche Umgebungsvariable legt fest, auf welchem X-Server ein X11-Programm seine Fenster darstellt?",
|
||||
"options": [
|
||||
{ "id": "a", "text": "`DISPLAY`", "correct": true, "explanation": "Format `host:display.screen`, lokal meist `:0`." },
|
||||
{ "id": "b", "text": "`XAUTHORITY`", "correct": false, "explanation": "Verweist auf die Datei mit den Zugriffs-Cookies (Standard `~/.Xauthority`)." },
|
||||
{ "id": "c", "text": "`TERM`", "correct": false, "explanation": "Beschreibt den Typ des Text-Terminals." },
|
||||
{ "id": "d", "text": "`XDG_SESSION_TYPE`", "correct": false, "explanation": "Zeigt nur an, ob die Sitzung `x11` oder `wayland` ist." },
|
||||
{ "id": "e", "text": "`WAYLAND_DISPLAY`", "correct": false, "explanation": "Gibt den Socket des Wayland-Compositors an, nicht den X-Server." }
|
||||
],
|
||||
"explanation": "Zugriff auf einen X-Server steuern `xhost` (host-basiert) und `xauth` (Cookies). X-Weiterleitung über SSH: `ssh -X`.",
|
||||
"references": ["man 7 X"]
|
||||
},
|
||||
{
|
||||
"id": "106.1-002",
|
||||
"objective": "106.1",
|
||||
"type": "single",
|
||||
"difficulty": 1,
|
||||
"question": "Wo liegt die Hauptkonfiguration des Xorg-Servers bzw. wo legt man ergänzende Konfigurationsschnipsel ab?",
|
||||
"options": [
|
||||
{ "id": "a", "text": "`/etc/X11/xorg.conf` bzw. `/etc/X11/xorg.conf.d/`", "correct": true, "explanation": "Moderne Xorg-Server erkennen vieles automatisch; Anpassungen kommen als Dateien in `xorg.conf.d`." },
|
||||
{ "id": "b", "text": "`/etc/xorg/server.conf`", "correct": false, "explanation": "Diesen Pfad gibt es nicht." },
|
||||
{ "id": "c", "text": "`~/.xinitrc`", "correct": false, "explanation": "`~/.xinitrc` legt fest, was `startx` startet, nicht die Server-Hardwarekonfiguration." },
|
||||
{ "id": "d", "text": "`/etc/X11/Xresources`", "correct": false, "explanation": "X-Ressourcen konfigurieren das Aussehen von Clients, nicht den Server." },
|
||||
{ "id": "e", "text": "`/var/log/Xorg.0.log`", "correct": false, "explanation": "Das ist das Protokoll des Servers, hilfreich bei Fehlern, aber keine Konfiguration." }
|
||||
],
|
||||
"explanation": "Abschnitte in `xorg.conf`: `Section \"Device\"`, `\"Monitor\"`, `\"Screen\"`, `\"InputDevice\"`, `\"ServerLayout\"` … Fehlersuche über `/var/log/Xorg.0.log`.",
|
||||
"references": ["man 5 xorg.conf"]
|
||||
},
|
||||
{
|
||||
"id": "106.1-003",
|
||||
"objective": "106.1",
|
||||
"type": "multi",
|
||||
"difficulty": 2,
|
||||
"question": "Welche Aussagen zu Wayland treffen zu?",
|
||||
"options": [
|
||||
{ "id": "a", "text": "Unter Wayland übernimmt der Compositor zugleich die Aufgaben des Display-Servers.", "correct": true, "explanation": "Es gibt keinen getrennten Server wie Xorg; der Compositor (z. B. Mutter, KWin) zeichnet und verteilt Eingaben." },
|
||||
{ "id": "b", "text": "XWayland ermöglicht es, X11-Programme in einer Wayland-Sitzung auszuführen.", "correct": true, "explanation": "XWayland ist ein X-Server, der als Wayland-Client läuft." },
|
||||
{ "id": "c", "text": "Wayland wird über `/etc/X11/xorg.conf` konfiguriert.", "correct": false, "explanation": "Die Konfiguration erfolgt im jeweiligen Compositor bzw. der Desktopumgebung." },
|
||||
{ "id": "d", "text": "Wayland ist wie X11 netzwerktransparent über `DISPLAY=host:0`.", "correct": false, "explanation": "Das Wayland-Protokoll selbst ist nicht netzwerktransparent; Fernzugriff läuft über Zusatzlösungen." },
|
||||
{ "id": "e", "text": "Wayland setzt den Display-Manager `xdm` voraus.", "correct": false, "explanation": "Wayland-Sitzungen werden z. B. von GDM oder SDDM gestartet; `xdm` kann das nicht." }
|
||||
],
|
||||
"explanation": "Ob eine Sitzung X11 oder Wayland nutzt, zeigt `echo $XDG_SESSION_TYPE`.",
|
||||
"references": ["https://wayland.freedesktop.org/"]
|
||||
},
|
||||
{
|
||||
"id": "106.2-001",
|
||||
"objective": "106.2",
|
||||
"type": "single",
|
||||
"difficulty": 1,
|
||||
"question": "Welches Fernzugriffsverfahren für grafische Desktops basiert auf dem Remote-Framebuffer-Protokoll (RFB)?",
|
||||
"options": [
|
||||
{ "id": "a", "text": "VNC", "correct": true, "explanation": "VNC (Virtual Network Computing) überträgt Bildschirminhalte per RFB, Standardport 5900 + Display-Nummer." },
|
||||
{ "id": "b", "text": "RDP", "correct": false, "explanation": "RDP ist das Remote Desktop Protocol von Microsoft (Port 3389), unter Linux z. B. mit xrdp." },
|
||||
{ "id": "c", "text": "SPICE", "correct": false, "explanation": "SPICE ist ein eigenes Protokoll für die Anzeige virtueller Maschinen (QEMU/KVM)." },
|
||||
{ "id": "d", "text": "XDMCP", "correct": false, "explanation": "XDMCP vermittelt Anmeldungen an einen entfernten X-Display-Manager, unverschlüsselt." },
|
||||
{ "id": "e", "text": "SMTP", "correct": false, "explanation": "SMTP überträgt E-Mails." }
|
||||
],
|
||||
"explanation": "106.2 verlangt Grundwissen zu Desktopumgebungen und Protokollen für den entfernten Desktopzugriff (X11, XDMCP, VNC, SPICE, RDP).",
|
||||
"references": ["RFC 6143"]
|
||||
},
|
||||
{
|
||||
"id": "106.2-002",
|
||||
"objective": "106.2",
|
||||
"type": "multi",
|
||||
"difficulty": 1,
|
||||
"question": "Welche der folgenden sind **Desktopumgebungen**?",
|
||||
"options": [
|
||||
{ "id": "a", "text": "GNOME", "correct": true, "explanation": "Verbreitete Desktopumgebung (GTK-basiert)." },
|
||||
{ "id": "b", "text": "KDE Plasma", "correct": true, "explanation": "Desktopumgebung auf Qt-Basis." },
|
||||
{ "id": "c", "text": "Xfce", "correct": true, "explanation": "Ressourcenschonende Desktopumgebung (GTK-basiert)." },
|
||||
{ "id": "d", "text": "Xorg", "correct": false, "explanation": "Xorg ist der X-Display-Server, auf dem Desktopumgebungen laufen können." },
|
||||
{ "id": "e", "text": "systemd", "correct": false, "explanation": "systemd ist Init-System und Dienstverwaltung." }
|
||||
],
|
||||
"explanation": "Weitere Desktops: Cinnamon, MATE, LXDE/LXQt. Eine Desktopumgebung bündelt Fenstermanager, Panel, Dateimanager und Einstellungen.",
|
||||
"references": ["LPI 102-500 Objective 106.2"]
|
||||
},
|
||||
{
|
||||
"id": "106.3-001",
|
||||
"objective": "106.3",
|
||||
"type": "single",
|
||||
"difficulty": 1,
|
||||
"question": "Welches Programm ist ein verbreiteter **Bildschirmleser** (Screenreader) für grafische Linux-Desktops wie GNOME?",
|
||||
"options": [
|
||||
{ "id": "a", "text": "Orca", "correct": true, "explanation": "Orca gibt Bildschirminhalte per Sprachausgabe oder Braillezeile aus." },
|
||||
{ "id": "b", "text": "Onboard", "correct": false, "explanation": "Onboard ist eine Bildschirmtastatur." },
|
||||
{ "id": "c", "text": "xmag", "correct": false, "explanation": "`xmag` vergrößert einen Bildschirmausschnitt." },
|
||||
{ "id": "d", "text": "Sticky Keys", "correct": false, "explanation": "Eine Tastatureinstellung, kein Bildschirmleser." },
|
||||
{ "id": "e", "text": "`xrandr`", "correct": false, "explanation": "`xrandr` stellt Auflösung und Anordnung von Monitoren ein." }
|
||||
],
|
||||
"explanation": "Hilfstechnologien: Bildschirmleser, Braillezeilen, Bildschirmlupe, Bildschirmtastatur, Hochkontrast-Themes, große Schriften, Sprachsynthese.",
|
||||
"references": ["https://help.gnome.org/users/orca/stable/"]
|
||||
},
|
||||
{
|
||||
"id": "106.3-002",
|
||||
"objective": "106.3",
|
||||
"type": "single",
|
||||
"difficulty": 1,
|
||||
"question": "Was bewirken **einrastende Tasten** (Sticky Keys)?",
|
||||
"options": [
|
||||
{ "id": "a", "text": "Umschalt-, Strg- oder Alt-Taste bleiben nach dem Loslassen aktiv, sodass Tastenkombinationen nacheinander gedrückt werden können.", "correct": true, "explanation": "Hilft Menschen, die nicht mehrere Tasten gleichzeitig drücken können." },
|
||||
{ "id": "b", "text": "Eine Taste muss eine Weile gehalten werden, bevor der Anschlag zählt.", "correct": false, "explanation": "Das sind die Langsamen Tasten (Slow Keys)." },
|
||||
{ "id": "c", "text": "Schnell wiederholte Anschläge derselben Taste werden ignoriert.", "correct": false, "explanation": "Das sind die Springenden Tasten (Bounce Keys)." },
|
||||
{ "id": "d", "text": "Der Mauszeiger wird mit dem Ziffernblock gesteuert.", "correct": false, "explanation": "Das sind die Maustasten (Mouse Keys)." },
|
||||
{ "id": "e", "text": "Der Bildschirm wird in hohem Kontrast dargestellt.", "correct": false, "explanation": "Das ist ein Hochkontrast-Theme." }
|
||||
],
|
||||
"explanation": "AccessX-Funktionen der Tastatur: Sticky, Slow, Bounce, Toggle Keys sowie Mouse Keys.",
|
||||
"references": ["LPI 102-500 Objective 106.3"]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,285 @@
|
||||
{
|
||||
"$schema": "../schema/questions.schema.json",
|
||||
"format": "lpic-sim-questions",
|
||||
"meta": {
|
||||
"file": "questions_107.json",
|
||||
"brief": "Fragen zu Topic 107 Administrative Aufgaben (LPIC-1 102-500, Objectives 5.0)",
|
||||
"author": "Björn Runden <br@runden.de>",
|
||||
"owner": "Björn Runden <br@runden.de>",
|
||||
"assist": "KI-unterstützt (Claude Opus 5.5), geprüft von Björn Runden <br@runden.de>",
|
||||
"version": "0.1.0 (29.10.2026)"
|
||||
},
|
||||
"exam": "lpic1-102",
|
||||
"questions": [
|
||||
{
|
||||
"id": "107.1-001",
|
||||
"objective": "107.1",
|
||||
"type": "single",
|
||||
"difficulty": 1,
|
||||
"question": "In welcher Datei stehen auf aktuellen Linux-Systemen die Passwort-Hashes der Benutzerkonten?",
|
||||
"options": [
|
||||
{ "id": "a", "text": "`/etc/shadow`", "correct": true, "explanation": "Nur für root lesbar; enthält Hash und Daten zur Passwort-Alterung." },
|
||||
{ "id": "b", "text": "`/etc/passwd`", "correct": false, "explanation": "Für alle lesbar; im Passwortfeld steht nur `x` als Verweis auf `/etc/shadow`." },
|
||||
{ "id": "c", "text": "`/etc/group`", "correct": false, "explanation": "Enthält Gruppen und ihre Mitglieder." },
|
||||
{ "id": "d", "text": "`/etc/gshadow`", "correct": false, "explanation": "Enthält Gruppenpasswörter und Gruppenadministratoren." },
|
||||
{ "id": "e", "text": "`/etc/login.defs`", "correct": false, "explanation": "Enthält Vorgaben, z. B. UID-Bereiche und Standardwerte der Passwort-Alterung." }
|
||||
],
|
||||
"explanation": "Felder von `/etc/shadow`: Name, Hash, letzte Änderung, min, max, Warnung, Inaktivität, Ablaufdatum, reserviert.",
|
||||
"references": ["man 5 shadow"]
|
||||
},
|
||||
{
|
||||
"id": "107.1-002",
|
||||
"objective": "107.1",
|
||||
"type": "single",
|
||||
"difficulty": 1,
|
||||
"question": "Was steht im **siebten** (letzten) Feld eines Eintrags in `/etc/passwd`?",
|
||||
"options": [
|
||||
{ "id": "a", "text": "Die Login-Shell", "correct": true, "explanation": "z. B. `/bin/bash` oder `/usr/sbin/nologin` für Konten ohne Anmeldung." },
|
||||
{ "id": "b", "text": "Das Home-Verzeichnis", "correct": false, "explanation": "Das ist das sechste Feld." },
|
||||
{ "id": "c", "text": "Das GECOS-Feld (Kommentar, voller Name)", "correct": false, "explanation": "Das ist das fünfte Feld." },
|
||||
{ "id": "d", "text": "Die primäre Gruppen-ID", "correct": false, "explanation": "Das ist das vierte Feld." },
|
||||
{ "id": "e", "text": "Die Benutzer-ID", "correct": false, "explanation": "Das ist das dritte Feld." }
|
||||
],
|
||||
"explanation": "Aufbau: `name:x:UID:GID:GECOS:home:shell`, z. B. `anna:x:1001:1001:Anna:/home/anna:/bin/bash`.",
|
||||
"references": ["man 5 passwd"]
|
||||
},
|
||||
{
|
||||
"id": "107.1-003",
|
||||
"objective": "107.1",
|
||||
"type": "fill",
|
||||
"difficulty": 2,
|
||||
"question": "Der Benutzer `anna` soll zusätzlich in die Gruppe `docker` aufgenommen werden, **ohne** seine übrigen Zusatzgruppen zu verlieren. Geben Sie den Befehl ein.",
|
||||
"answers": ["usermod -aG docker anna", "usermod -a -G docker anna", "usermod -G docker -a anna", "usermod --append --groups docker anna", "gpasswd -a anna docker"],
|
||||
"explanation": "`-G` allein ersetzt die Liste der Zusatzgruppen; erst `-a` (append) hängt an. Die Änderung wirkt erst nach erneuter Anmeldung (oder mit `newgrp`).",
|
||||
"references": ["man 8 usermod", "man 1 gpasswd"]
|
||||
},
|
||||
{
|
||||
"id": "107.1-004",
|
||||
"objective": "107.1",
|
||||
"type": "single",
|
||||
"difficulty": 1,
|
||||
"question": "Mit welchem Befehl legt man den Benutzer `bob` an und erzeugt dabei gleich sein Home-Verzeichnis (mit Inhalt aus `/etc/skel`)?",
|
||||
"options": [
|
||||
{ "id": "a", "text": "`useradd -m bob`", "correct": true, "explanation": "`-m` (`--create-home`) legt das Home-Verzeichnis an und kopiert `/etc/skel`." },
|
||||
{ "id": "b", "text": "`useradd -d /home/bob bob`", "correct": false, "explanation": "`-d` legt nur den Pfad fest, erzeugt das Verzeichnis aber nicht." },
|
||||
{ "id": "c", "text": "`useradd -s /bin/bash bob`", "correct": false, "explanation": "`-s` setzt nur die Login-Shell." },
|
||||
{ "id": "d", "text": "`useradd -g users bob`", "correct": false, "explanation": "`-g` setzt die primäre Gruppe." },
|
||||
{ "id": "e", "text": "`useradd -k /etc/skel bob`", "correct": false, "explanation": "`-k` wählt das Vorlagenverzeichnis und wirkt nur zusammen mit `-m`." }
|
||||
],
|
||||
"explanation": "Ob `useradd` ohne `-m` ein Home anlegt, hängt von `CREATE_HOME` in `/etc/login.defs` ab. Debian bietet zusätzlich das interaktive `adduser`.",
|
||||
"references": ["man 8 useradd"]
|
||||
},
|
||||
{
|
||||
"id": "107.1-005",
|
||||
"objective": "107.1",
|
||||
"type": "multi",
|
||||
"difficulty": 2,
|
||||
"question": "Mit welchen Befehlen sperrt man das Passwort des Kontos `carl`, sodass keine Anmeldung per Passwort mehr möglich ist?",
|
||||
"options": [
|
||||
{ "id": "a", "text": "`usermod -L carl`", "correct": true, "explanation": "`-L` setzt ein `!` vor den Hash in `/etc/shadow`." },
|
||||
{ "id": "b", "text": "`passwd -l carl`", "correct": true, "explanation": "`passwd -l` sperrt ebenfalls durch Voranstellen von `!`; `-u` entsperrt." },
|
||||
{ "id": "c", "text": "`passwd -d carl`", "correct": false, "explanation": "`-d` löscht das Passwort – je nach Konfiguration ist dann sogar eine Anmeldung ohne Passwort möglich." },
|
||||
{ "id": "d", "text": "`chage -l carl`", "correct": false, "explanation": "`chage -l` zeigt nur die Alterungsinformationen an." },
|
||||
{ "id": "e", "text": "`userdel -l carl`", "correct": false, "explanation": "`userdel` löscht Konten; eine Option `-l` zum Sperren gibt es nicht." }
|
||||
],
|
||||
"explanation": "Eine Passwortsperre verhindert keine Anmeldung per SSH-Schlüssel. Komplett sperren: zusätzlich Ablaufdatum setzen (`usermod -e 1` bzw. `chage -E 0`) oder Shell auf `nologin`.",
|
||||
"references": ["man 8 usermod", "man 1 passwd"]
|
||||
},
|
||||
{
|
||||
"id": "107.1-006",
|
||||
"objective": "107.1",
|
||||
"type": "single",
|
||||
"difficulty": 2,
|
||||
"question": "Der Benutzer `dora` soll sein Passwort spätestens alle 90 Tage ändern müssen. Welcher Befehl setzt das?",
|
||||
"options": [
|
||||
{ "id": "a", "text": "`chage -M 90 dora`", "correct": true, "explanation": "`-M` setzt die maximale Gültigkeit des Passworts in Tagen." },
|
||||
{ "id": "b", "text": "`chage -m 90 dora`", "correct": false, "explanation": "`-m` ist die Mindestdauer zwischen zwei Änderungen." },
|
||||
{ "id": "c", "text": "`chage -W 90 dora`", "correct": false, "explanation": "`-W` legt fest, wie viele Tage vorher gewarnt wird." },
|
||||
{ "id": "d", "text": "`chage -E 90 dora`", "correct": false, "explanation": "`-E` setzt das Ablaufdatum des Kontos (Datum oder Tage seit 1.1.1970)." },
|
||||
{ "id": "e", "text": "`chage -I 90 dora`", "correct": false, "explanation": "`-I` legt die Inaktivitätsfrist nach Ablauf des Passworts fest." }
|
||||
],
|
||||
"explanation": "`chage -l dora` zeigt die aktuellen Werte. Dieselben Felder lassen sich teils auch mit `passwd -n/-x/-w/-i` setzen.",
|
||||
"references": ["man 1 chage"]
|
||||
},
|
||||
{
|
||||
"id": "107.1-007",
|
||||
"objective": "107.1",
|
||||
"type": "single",
|
||||
"difficulty": 2,
|
||||
"question": "Benutzer kommen teils aus `/etc/passwd`, teils aus LDAP. Welcher Befehl zeigt den Kontoeintrag von `anna` **unabhängig von der Quelle** an?",
|
||||
"options": [
|
||||
{ "id": "a", "text": "`getent passwd anna`", "correct": true, "explanation": "`getent` fragt die Datenbanken über NSS ab, also in der Reihenfolge aus `/etc/nsswitch.conf`." },
|
||||
{ "id": "b", "text": "`grep anna /etc/passwd`", "correct": false, "explanation": "Findet nur lokale Einträge." },
|
||||
{ "id": "c", "text": "`who anna`", "correct": false, "explanation": "`who` zeigt angemeldete Benutzer; ein Argument wird als Datei interpretiert." },
|
||||
{ "id": "d", "text": "`last anna`", "correct": false, "explanation": "`last` zeigt die Anmeldehistorie aus `wtmp`." },
|
||||
{ "id": "e", "text": "`cat /etc/nsswitch.conf`", "correct": false, "explanation": "Zeigt nur die Konfiguration der Quellen, keine Konten." }
|
||||
],
|
||||
"explanation": "Analog: `getent group`, `getent hosts`, `getent shadow` (als root).",
|
||||
"references": ["man 1 getent", "man 5 nsswitch.conf"]
|
||||
},
|
||||
{
|
||||
"id": "107.1-008",
|
||||
"objective": "107.1",
|
||||
"type": "multi",
|
||||
"difficulty": 2,
|
||||
"question": "Welche Aussagen zur Benutzer- und Gruppenverwaltung treffen zu?",
|
||||
"options": [
|
||||
{ "id": "a", "text": "`userdel -r anna` löscht auch das Home-Verzeichnis und das Mail-Spool von `anna`.", "correct": true, "explanation": "Ohne `-r` bleiben Home-Verzeichnis und Mail-Spool erhalten." },
|
||||
{ "id": "b", "text": "Systemkonten haben üblicherweise UIDs unterhalb von `UID_MIN` aus `/etc/login.defs`.", "correct": true, "explanation": "Typisch: Systemkonten unter 1000, normale Benutzer ab 1000; `useradd -r` legt Systemkonten an." },
|
||||
{ "id": "c", "text": "`groupdel` löscht eine Gruppe auch dann, wenn sie die primäre Gruppe eines Benutzers ist.", "correct": false, "explanation": "`groupdel` verweigert das; zuerst muss die primäre Gruppe des Benutzers geändert werden." },
|
||||
{ "id": "d", "text": "Die UID 0 kann nur ein Konto mit dem Namen `root` haben.", "correct": false, "explanation": "Entscheidend ist die UID: Jedes Konto mit UID 0 hat Root-Rechte – ein Sicherheitsrisiko." },
|
||||
{ "id": "e", "text": "`passwd` ohne Argument ändert immer das Passwort von root.", "correct": false, "explanation": "Ohne Argument ändert `passwd` das Passwort des aufrufenden Benutzers." }
|
||||
],
|
||||
"explanation": "Gruppen: `groupadd`, `groupmod -n neu alt` (umbenennen), `groupdel`. Mitgliedschaften prüfen: `id anna`, `groups anna`.",
|
||||
"references": ["man 8 userdel", "man 5 login.defs"]
|
||||
},
|
||||
{
|
||||
"id": "107.2-001",
|
||||
"objective": "107.2",
|
||||
"type": "single",
|
||||
"difficulty": 1,
|
||||
"question": "Wann läuft der Benutzer-crontab-Eintrag `30 2 * * 1 /usr/local/bin/backup.sh`?",
|
||||
"options": [
|
||||
{ "id": "a", "text": "Jeden Montag um 02:30 Uhr", "correct": true, "explanation": "Minute 30, Stunde 2, jeder Tag, jeder Monat, Wochentag 1 = Montag." },
|
||||
{ "id": "b", "text": "Jeden Tag um 02:30 Uhr", "correct": false, "explanation": "Das wäre `30 2 * * *`." },
|
||||
{ "id": "c", "text": "Am 1. jedes Monats um 02:30 Uhr", "correct": false, "explanation": "Das wäre `30 2 1 * *` (drittes Feld = Tag des Monats)." },
|
||||
{ "id": "d", "text": "Im Januar täglich um 02:30 Uhr", "correct": false, "explanation": "Das wäre `30 2 * 1 *` (viertes Feld = Monat)." },
|
||||
{ "id": "e", "text": "Montags alle 30 Minuten ab 02:00 Uhr", "correct": false, "explanation": "Das wäre z. B. `*/30 2-23 * * 1`." }
|
||||
],
|
||||
"explanation": "Felder: Minute · Stunde · Tag des Monats · Monat · Wochentag (0 und 7 = Sonntag) · Befehl.",
|
||||
"references": ["man 5 crontab"]
|
||||
},
|
||||
{
|
||||
"id": "107.2-002",
|
||||
"objective": "107.2",
|
||||
"type": "fill",
|
||||
"difficulty": 1,
|
||||
"question": "Mit welchem Befehl (mit Option) bearbeitet ein Benutzer seine eigene crontab?",
|
||||
"answers": ["crontab -e"],
|
||||
"explanation": "`crontab -e` öffnet den Editor und prüft beim Speichern die Syntax; `-l` listet, `-r` löscht. Die Dateien liegen unter `/var/spool/cron/`.",
|
||||
"references": ["man 1 crontab"]
|
||||
},
|
||||
{
|
||||
"id": "107.2-003",
|
||||
"objective": "107.2",
|
||||
"type": "single",
|
||||
"difficulty": 2,
|
||||
"question": "Worin unterscheidet sich ein Eintrag in `/etc/crontab` (bzw. `/etc/cron.d/*`) von einem Eintrag in einer Benutzer-crontab?",
|
||||
"options": [
|
||||
{ "id": "a", "text": "Vor dem Befehl steht zusätzlich ein Feld mit dem Benutzernamen, unter dem der Befehl läuft.", "correct": true, "explanation": "Aufbau systemweit: fünf Zeitfelder, Benutzer, Befehl – z. B. `17 * * * * root run-parts /etc/cron.hourly`." },
|
||||
{ "id": "b", "text": "Es gibt kein Feld für den Wochentag.", "correct": false, "explanation": "Die fünf Zeitfelder sind identisch." },
|
||||
{ "id": "c", "text": "Die Zeitangaben stehen in Sekunden statt in Minuten.", "correct": false, "explanation": "cron arbeitet mit Minuten als kleinster Einheit." },
|
||||
{ "id": "d", "text": "Die Datei wird mit `crontab -e` bearbeitet.", "correct": false, "explanation": "`/etc/crontab` wird direkt mit einem Editor bearbeitet." },
|
||||
{ "id": "e", "text": "Befehle laufen dort immer als root, ein Benutzerfeld gibt es nicht.", "correct": false, "explanation": "Gerade das Benutzerfeld ist der Unterschied." }
|
||||
],
|
||||
"explanation": "Daneben gibt es `/etc/cron.hourly`, `.daily`, `.weekly`, `.monthly` für Skripte ohne Zeitangabe.",
|
||||
"references": ["man 5 crontab", "man 8 cron"]
|
||||
},
|
||||
{
|
||||
"id": "107.2-004",
|
||||
"objective": "107.2",
|
||||
"type": "single",
|
||||
"difficulty": 1,
|
||||
"question": "Ein Befehl soll **einmalig** heute um 23:00 Uhr ausgeführt werden. Welches Werkzeug ist dafür gedacht?",
|
||||
"options": [
|
||||
{ "id": "a", "text": "`at 23:00`", "correct": true, "explanation": "`at` liest die Befehle von stdin und führt sie einmalig zur angegebenen Zeit aus." },
|
||||
{ "id": "b", "text": "`batch 23:00`", "correct": false, "explanation": "`batch` führt Aufträge aus, sobald die Systemlast es zulässt; eine Uhrzeit nimmt es nicht an." },
|
||||
{ "id": "c", "text": "`crontab -t 23:00`", "correct": false, "explanation": "`crontab` hat keine Option `-t`; cron ist für wiederkehrende Aufträge." },
|
||||
{ "id": "d", "text": "`sleep 23:00`", "correct": false, "explanation": "`sleep` erwartet eine Dauer, keine Uhrzeit." },
|
||||
{ "id": "e", "text": "`cron 23:00`", "correct": false, "explanation": "`cron` ist der Daemon und nimmt keine Aufträge auf der Befehlszeile an." }
|
||||
],
|
||||
"explanation": "`atq` listet anstehende at-Aufträge, `atrm` löscht sie. Zugriff steuern `/etc/at.allow` und `/etc/at.deny`. Mit systemd: `systemd-run --on-calendar=…`.",
|
||||
"references": ["man 1 at"]
|
||||
},
|
||||
{
|
||||
"id": "107.2-005",
|
||||
"objective": "107.2",
|
||||
"type": "multi",
|
||||
"difficulty": 2,
|
||||
"question": "Welche Dateien steuern, welche Benutzer `crontab` verwenden dürfen?",
|
||||
"options": [
|
||||
{ "id": "a", "text": "`/etc/cron.allow`", "correct": true, "explanation": "Existiert sie, dürfen nur die dort genannten Benutzer crontab nutzen." },
|
||||
{ "id": "b", "text": "`/etc/cron.deny`", "correct": true, "explanation": "Existiert keine `cron.allow`, sind die hier genannten Benutzer ausgeschlossen." },
|
||||
{ "id": "c", "text": "`/etc/at.deny`", "correct": false, "explanation": "Steuert nur den Zugriff auf `at`." },
|
||||
{ "id": "d", "text": "`/etc/crontab`", "correct": false, "explanation": "Die systemweite crontab enthält Aufträge, keine Zugriffsregeln." },
|
||||
{ "id": "e", "text": "`/etc/security/access.conf`", "correct": false, "explanation": "Regelt per PAM, wer sich wo anmelden darf." }
|
||||
],
|
||||
"explanation": "Fehlen beide Dateien, hängt das Verhalten von der Distribution ab (oft dürfen alle, teils nur root).",
|
||||
"references": ["man 1 crontab"]
|
||||
},
|
||||
{
|
||||
"id": "107.2-006",
|
||||
"objective": "107.2",
|
||||
"type": "single",
|
||||
"difficulty": 1,
|
||||
"question": "Welcher Befehl listet alle systemd-Timer mit ihrem nächsten und letzten Ausführungszeitpunkt auf?",
|
||||
"options": [
|
||||
{ "id": "a", "text": "`systemctl list-timers`", "correct": true, "explanation": "Mit `--all` auch inaktive Timer." },
|
||||
{ "id": "b", "text": "`systemctl status cron`", "correct": false, "explanation": "Zeigt den Zustand des cron-Dienstes, nicht die systemd-Timer." },
|
||||
{ "id": "c", "text": "`atq`", "correct": false, "explanation": "Listet at-Aufträge." },
|
||||
{ "id": "d", "text": "`crontab -l`", "correct": false, "explanation": "Listet die crontab des aktuellen Benutzers." },
|
||||
{ "id": "e", "text": "`journalctl --timers`", "correct": false, "explanation": "Eine solche Option gibt es nicht." }
|
||||
],
|
||||
"explanation": "Ein Timer (`name.timer`) startet die gleichnamige `name.service`-Unit; Zeitangaben z. B. `OnCalendar=daily` oder `OnBootSec=10min`.",
|
||||
"references": ["man 5 systemd.timer"]
|
||||
},
|
||||
{
|
||||
"id": "107.3-001",
|
||||
"objective": "107.3",
|
||||
"type": "single",
|
||||
"difficulty": 1,
|
||||
"question": "Mit welchem Befehl stellt man auf einem systemd-System die Zeitzone **dauerhaft** auf Europe/Berlin?",
|
||||
"options": [
|
||||
{ "id": "a", "text": "`timedatectl set-timezone Europe/Berlin`", "correct": true, "explanation": "Setzt den Link `/etc/localtime` auf `/usr/share/zoneinfo/Europe/Berlin`." },
|
||||
{ "id": "b", "text": "`localectl set-timezone Europe/Berlin`", "correct": false, "explanation": "`localectl` verwaltet Locale und Tastaturbelegung, keine Zeitzone." },
|
||||
{ "id": "c", "text": "`export TZ=Europe/Berlin`", "correct": false, "explanation": "Gilt nur für die aktuelle Shell und ihre Kindprozesse." },
|
||||
{ "id": "d", "text": "`hwclock --timezone Europe/Berlin`", "correct": false, "explanation": "`hwclock` liest und stellt die Hardware-Uhr, eine Zeitzone setzt es nicht." },
|
||||
{ "id": "e", "text": "`date --zone Europe/Berlin`", "correct": false, "explanation": "`date` hat keine solche Option; man kann nur per `TZ=… date` einmalig anzeigen." }
|
||||
],
|
||||
"explanation": "Ohne systemd: `/etc/localtime` von Hand verlinken bzw. unter Debian `/etc/timezone` und `dpkg-reconfigure tzdata`. Interaktiv hilft `tzselect`.",
|
||||
"references": ["man 1 timedatectl", "man 8 tzselect"]
|
||||
},
|
||||
{
|
||||
"id": "107.3-002",
|
||||
"objective": "107.3",
|
||||
"type": "single",
|
||||
"difficulty": 2,
|
||||
"question": "Welche Variable überschreibt **alle** anderen Locale-Einstellungen (`LANG` und alle `LC_*`)?",
|
||||
"options": [
|
||||
{ "id": "a", "text": "`LC_ALL`", "correct": true, "explanation": "`LC_ALL` hat Vorrang vor allen einzelnen `LC_*` und vor `LANG`." },
|
||||
{ "id": "b", "text": "`LANG`", "correct": false, "explanation": "`LANG` ist der Standardwert mit der niedrigsten Priorität." },
|
||||
{ "id": "c", "text": "`LC_MESSAGES`", "correct": false, "explanation": "Gilt nur für die Sprache von Programmmeldungen." },
|
||||
{ "id": "d", "text": "`LC_CTYPE`", "correct": false, "explanation": "Gilt nur für Zeichenklassifizierung und Zeichensatz." },
|
||||
{ "id": "e", "text": "`TZ`", "correct": false, "explanation": "`TZ` legt die Zeitzone fest, keine Locale." }
|
||||
],
|
||||
"explanation": "`locale` zeigt die wirksamen Werte, `locale -a` alle verfügbaren Locales. `LANG=C` bzw. `C.UTF-8` erzwingt neutrale Ausgaben, z. B. in Skripten.",
|
||||
"references": ["man 7 locale"]
|
||||
},
|
||||
{
|
||||
"id": "107.3-003",
|
||||
"objective": "107.3",
|
||||
"type": "fill",
|
||||
"difficulty": 1,
|
||||
"question": "Welches Programm konvertiert Textdateien zwischen Zeichenkodierungen, z. B. von ISO-8859-1 nach UTF-8? (nur der Befehlsname)",
|
||||
"answers": ["iconv"],
|
||||
"explanation": "Beispiel: `iconv -f ISO-8859-1 -t UTF-8 alt.txt > neu.txt`; `iconv -l` listet die bekannten Kodierungen.",
|
||||
"references": ["man 1 iconv"]
|
||||
},
|
||||
{
|
||||
"id": "107.3-004",
|
||||
"objective": "107.3",
|
||||
"type": "multi",
|
||||
"difficulty": 2,
|
||||
"question": "Welche Aussagen zu Zeichenkodierungen treffen zu?",
|
||||
"options": [
|
||||
{ "id": "a", "text": "UTF-8 kodiert Unicode-Zeichen mit variabler Länge (1 bis 4 Byte).", "correct": true, "explanation": "Häufige Zeichen sind kurz, seltene länger." },
|
||||
{ "id": "b", "text": "ASCII-Zeichen haben in UTF-8 dieselben Byte-Werte wie in ASCII.", "correct": true, "explanation": "UTF-8 ist abwärtskompatibel zu 7-Bit-ASCII." },
|
||||
{ "id": "c", "text": "ISO-8859-1 enthält das Euro-Zeichen.", "correct": false, "explanation": "Das Euro-Zeichen kam erst mit ISO-8859-15 (Latin-9)." },
|
||||
{ "id": "d", "text": "UTF-8 benötigt für jedes Zeichen genau 2 Byte.", "correct": false, "explanation": "Feste 2 Byte entsprächen UCS-2; UTF-8 ist variabel." },
|
||||
{ "id": "e", "text": "`LANG=C` aktiviert UTF-8 für alle Programme.", "correct": false, "explanation": "Die C/POSIX-Locale verwendet ASCII; für UTF-8 z. B. `C.UTF-8` oder `de_DE.UTF-8`." }
|
||||
],
|
||||
"explanation": "Im Objective genannt: UTF-8, ISO-8859, ASCII, Unicode sowie `locale`, `iconv`, `/etc/timezone`, `/etc/localtime`, `/usr/share/zoneinfo/`.",
|
||||
"references": ["man 7 utf-8", "man 7 charsets"]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,269 @@
|
||||
{
|
||||
"$schema": "../schema/questions.schema.json",
|
||||
"format": "lpic-sim-questions",
|
||||
"meta": {
|
||||
"file": "questions_108.json",
|
||||
"brief": "Fragen zu Topic 108 Grundlegende Systemdienste (LPIC-1 102-500, Objectives 5.0)",
|
||||
"author": "Björn Runden <br@runden.de>",
|
||||
"owner": "Björn Runden <br@runden.de>",
|
||||
"assist": "KI-unterstützt (Claude Opus 5.5), geprüft von Björn Runden <br@runden.de>",
|
||||
"version": "0.1.0 (29.10.2026)"
|
||||
},
|
||||
"exam": "lpic1-102",
|
||||
"questions": [
|
||||
{
|
||||
"id": "108.1-001",
|
||||
"objective": "108.1",
|
||||
"type": "single",
|
||||
"difficulty": 1,
|
||||
"question": "Die Systemzeit ist korrekt. Mit welchem Befehl überträgt man sie in die Hardware-Uhr (RTC)?",
|
||||
"options": [
|
||||
{ "id": "a", "text": "`hwclock --systohc`", "correct": true, "explanation": "system to hardware clock: Systemzeit → RTC." },
|
||||
{ "id": "b", "text": "`hwclock --hctosys`", "correct": false, "explanation": "Umgekehrte Richtung: RTC → Systemzeit (passiert beim Start)." },
|
||||
{ "id": "c", "text": "`hwclock --show`", "correct": false, "explanation": "Zeigt die Zeit der Hardware-Uhr nur an." },
|
||||
{ "id": "d", "text": "`date -s now`", "correct": false, "explanation": "`date -s` stellt die Systemzeit, nicht die Hardware-Uhr." },
|
||||
{ "id": "e", "text": "`timedatectl set-ntp true`", "correct": false, "explanation": "Aktiviert die NTP-Synchronisation der Systemzeit." }
|
||||
],
|
||||
"explanation": "Linux führt eine Systemuhr (Kernel) und die batteriegepufferte Hardware-Uhr. Ob die RTC in UTC oder Ortszeit läuft, zeigt `timedatectl` (RTC in local TZ).",
|
||||
"references": ["man 8 hwclock"]
|
||||
},
|
||||
{
|
||||
"id": "108.1-002",
|
||||
"objective": "108.1",
|
||||
"type": "single",
|
||||
"difficulty": 2,
|
||||
"question": "Auf einem System läuft `chronyd`. Welcher Befehl zeigt die konfigurierten Zeitquellen und deren Zustand?",
|
||||
"options": [
|
||||
{ "id": "a", "text": "`chronyc sources`", "correct": true, "explanation": "Listet die Quellen; `*` markiert die aktuell gewählte. `chronyc tracking` zeigt die Abweichung." },
|
||||
{ "id": "b", "text": "`ntpq -p`", "correct": false, "explanation": "`ntpq -p` gehört zum klassischen `ntpd` und fragt dessen Peers ab." },
|
||||
{ "id": "c", "text": "`chronyd -q`", "correct": false, "explanation": "Stellt die Uhr einmalig und beendet sich (ähnlich `ntpdate`)." },
|
||||
{ "id": "d", "text": "`timedatectl timesync-status`", "correct": false, "explanation": "Zeigt den Status von `systemd-timesyncd`, nicht von chrony." },
|
||||
{ "id": "e", "text": "`hwclock -r`", "correct": false, "explanation": "Liest nur die Hardware-Uhr." }
|
||||
],
|
||||
"explanation": "Konfiguration: `/etc/chrony.conf` bzw. `/etc/chrony/chrony.conf` (Debian), beim klassischen ntpd `/etc/ntp.conf`.",
|
||||
"references": ["man 1 chronyc"]
|
||||
},
|
||||
{
|
||||
"id": "108.1-003",
|
||||
"objective": "108.1",
|
||||
"type": "fill",
|
||||
"difficulty": 1,
|
||||
"question": "Geben Sie den `timedatectl`-Befehl ein, der die automatische Zeitsynchronisation per NTP einschaltet.",
|
||||
"answers": ["timedatectl set-ntp true", "timedatectl set-ntp yes", "timedatectl set-ntp on", "timedatectl set-ntp 1"],
|
||||
"explanation": "`timedatectl` ohne Argumente zeigt u. a. `NTP service: active` und `System clock synchronized`.",
|
||||
"references": ["man 1 timedatectl"]
|
||||
},
|
||||
{
|
||||
"id": "108.1-004",
|
||||
"objective": "108.1",
|
||||
"type": "multi",
|
||||
"difficulty": 1,
|
||||
"question": "Welche Aussagen zum Network Time Protocol (NTP) treffen zu?",
|
||||
"options": [
|
||||
{ "id": "a", "text": "NTP verwendet UDP-Port 123.", "correct": true, "explanation": "Sowohl Client als auch Server nutzen Port 123/udp." },
|
||||
{ "id": "b", "text": "`pool.ntp.org` ist ein öffentlicher Verbund von NTP-Servern.", "correct": true, "explanation": "Über DNS werden wechselnde Server aus dem Pool vergeben." },
|
||||
{ "id": "c", "text": "NTP verwendet TCP-Port 123.", "correct": false, "explanation": "NTP läuft über UDP." },
|
||||
{ "id": "d", "text": "`ntpdate` gleicht die Uhr dauerhaft und kontinuierlich ab.", "correct": false, "explanation": "`ntpdate` stellt die Uhr einmalig und ist veraltet." },
|
||||
{ "id": "e", "text": "`date` ohne Optionen stellt die Uhr nach einem Zeitserver.", "correct": false, "explanation": "`date` ohne Optionen zeigt nur Datum und Uhrzeit an." }
|
||||
],
|
||||
"explanation": "Begriffe: Stratum (Abstand zur Referenzuhr), Offset, Drift. NTP-Implementierungen: ntpd, chrony, systemd-timesyncd (nur Client).",
|
||||
"references": ["RFC 5905"]
|
||||
},
|
||||
{
|
||||
"id": "108.2-001",
|
||||
"objective": "108.2",
|
||||
"type": "single",
|
||||
"difficulty": 1,
|
||||
"question": "Wie zeigt man alle Journal-Meldungen der systemd-Unit `ssh.service` an?",
|
||||
"options": [
|
||||
{ "id": "a", "text": "`journalctl -u ssh.service`", "correct": true, "explanation": "`-u` filtert nach Unit." },
|
||||
{ "id": "b", "text": "`journalctl -k ssh.service`", "correct": false, "explanation": "`-k` zeigt nur Kernel-Meldungen." },
|
||||
{ "id": "c", "text": "`journalctl -b ssh.service`", "correct": false, "explanation": "`-b` filtert nach Boot; der Unit-Name würde als Feldfilter missverstanden." },
|
||||
{ "id": "d", "text": "`systemctl log ssh.service`", "correct": false, "explanation": "`systemctl` hat keinen Unterbefehl `log`; `systemctl status` zeigt nur die letzten Zeilen." },
|
||||
{ "id": "e", "text": "`journalctl -p ssh.service`", "correct": false, "explanation": "`-p` filtert nach Priorität, z. B. `-p err`." }
|
||||
],
|
||||
"explanation": "Kombinierbar: `journalctl -u ssh -b -p warning --since \"1 hour ago\"`.",
|
||||
"references": ["man 1 journalctl"]
|
||||
},
|
||||
{
|
||||
"id": "108.2-002",
|
||||
"objective": "108.2",
|
||||
"type": "fill",
|
||||
"difficulty": 1,
|
||||
"question": "Welche `journalctl`-Option zeigt neu eintreffende Meldungen fortlaufend an (wie `tail -f`)? (nur die Option)",
|
||||
"answers": ["-f", "--follow"],
|
||||
"explanation": "`journalctl -f` folgt dem Journal; `-n 50` zeigt zuvor die letzten 50 Zeilen.",
|
||||
"references": ["man 1 journalctl"]
|
||||
},
|
||||
{
|
||||
"id": "108.2-003",
|
||||
"objective": "108.2",
|
||||
"type": "single",
|
||||
"difficulty": 2,
|
||||
"question": "Was bewirkt die rsyslog-Regel `mail.err /var/log/mail.err`?",
|
||||
"options": [
|
||||
{ "id": "a", "text": "Meldungen der Facility `mail` mit Priorität `err` **und höher** landen in der Datei.", "correct": true, "explanation": "Eine Priorität ohne Zusatz bedeutet: diese und alle schwerwiegenderen." },
|
||||
{ "id": "b", "text": "Nur Meldungen der Facility `mail` mit genau der Priorität `err` landen in der Datei.", "correct": false, "explanation": "Genau eine Priorität: `mail.=err`." },
|
||||
{ "id": "c", "text": "Alle Meldungen der Facility `mail` landen in der Datei.", "correct": false, "explanation": "Das wäre `mail.*`." },
|
||||
{ "id": "d", "text": "Fehlermeldungen aller Facilities landen in der Datei.", "correct": false, "explanation": "Das wäre `*.err`." },
|
||||
{ "id": "e", "text": "Mail-Fehler werden per E-Mail an root geschickt.", "correct": false, "explanation": "Ziel ist eine Datei; Weiterleitung an Hosts erfolgt mit `@host` bzw. `@@host`." }
|
||||
],
|
||||
"explanation": "Prioritäten aufsteigend: debug, info, notice, warning, err, crit, alert, emerg. `none` schließt aus, z. B. `*.info;mail.none`.",
|
||||
"references": ["man 5 rsyslog.conf"]
|
||||
},
|
||||
{
|
||||
"id": "108.2-004",
|
||||
"objective": "108.2",
|
||||
"type": "single",
|
||||
"difficulty": 1,
|
||||
"question": "Mit welchem Befehl schreibt ein Shell-Skript eine eigene Meldung ins Systemprotokoll?",
|
||||
"options": [
|
||||
{ "id": "a", "text": "`logger \"Backup beendet\"`", "correct": true, "explanation": "`logger` übergibt die Meldung an syslog/journald; `-t` setzt einen Tag, `-p` die Priorität." },
|
||||
{ "id": "b", "text": "`wall \"Backup beendet\"`", "correct": false, "explanation": "`wall` sendet die Nachricht an alle Terminals." },
|
||||
{ "id": "c", "text": "`journalctl --write \"Backup beendet\"`", "correct": false, "explanation": "`journalctl` liest das Journal; eine Option `--write` gibt es nicht." },
|
||||
{ "id": "d", "text": "`dmesg -w \"Backup beendet\"`", "correct": false, "explanation": "`dmesg -w` wartet auf neue Kernel-Meldungen (follow)." },
|
||||
{ "id": "e", "text": "`syslog \"Backup beendet\"`", "correct": false, "explanation": "Einen Befehl `syslog` gibt es nicht." }
|
||||
],
|
||||
"explanation": "Beispiel mit Facility und Priorität: `logger -p local0.notice -t backup \"fertig\"`.",
|
||||
"references": ["man 1 logger"]
|
||||
},
|
||||
{
|
||||
"id": "108.2-005",
|
||||
"objective": "108.2",
|
||||
"type": "multi",
|
||||
"difficulty": 2,
|
||||
"question": "Wie erreicht man, dass das systemd-Journal Neustarts **überdauert** (persistent gespeichert wird)?",
|
||||
"options": [
|
||||
{ "id": "a", "text": "In `/etc/systemd/journald.conf` `Storage=persistent` setzen", "correct": true, "explanation": "journald schreibt dann nach `/var/log/journal`." },
|
||||
{ "id": "b", "text": "Das Verzeichnis `/var/log/journal` anlegen (bei `Storage=auto`)", "correct": true, "explanation": "Bei der Standardeinstellung `auto` wird persistent gespeichert, sobald das Verzeichnis existiert." },
|
||||
{ "id": "c", "text": "`Storage=volatile` setzen", "correct": false, "explanation": "`volatile` speichert nur im RAM unter `/run/log/journal`." },
|
||||
{ "id": "d", "text": "`journalctl --vacuum-size=1G` ausführen", "correct": false, "explanation": "Löscht alte Journal-Dateien, bis die Größe unterschritten ist." },
|
||||
{ "id": "e", "text": "`logrotate` für das Journal einrichten", "correct": false, "explanation": "journald rotiert selbst; logrotate ist für Textlogs." }
|
||||
],
|
||||
"explanation": "Größe begrenzen: `SystemMaxUse=` in `journald.conf`. Journal einer anderen Installation lesen: `journalctl -D <verzeichnis>`.",
|
||||
"references": ["man 5 journald.conf"]
|
||||
},
|
||||
{
|
||||
"id": "108.2-006",
|
||||
"objective": "108.2",
|
||||
"type": "single",
|
||||
"difficulty": 1,
|
||||
"question": "Wo legt man üblicherweise die Rotationsregeln für die Logdateien **eines einzelnen Dienstes** ab?",
|
||||
"options": [
|
||||
{ "id": "a", "text": "In einer Datei unter `/etc/logrotate.d/`", "correct": true, "explanation": "`/etc/logrotate.conf` bindet dieses Verzeichnis per `include` ein." },
|
||||
{ "id": "b", "text": "In einer Datei unter `/etc/rsyslog.d/`", "correct": false, "explanation": "Dort stehen Regeln, wohin rsyslog Meldungen schreibt, nicht wie rotiert wird." },
|
||||
{ "id": "c", "text": "In `/etc/systemd/journald.conf`", "correct": false, "explanation": "Gilt nur für das Journal, nicht für Textlogs eines Dienstes." },
|
||||
{ "id": "d", "text": "In `/var/log/rotate/`", "correct": false, "explanation": "Kein Standardpfad." },
|
||||
{ "id": "e", "text": "In `/etc/cron.daily/`", "correct": false, "explanation": "Dort liegt das Skript, das logrotate täglich startet, nicht die Regeln." }
|
||||
],
|
||||
"explanation": "Typische Direktiven: `daily`/`weekly`, `rotate 7`, `compress`, `missingok`, `notifempty`, `postrotate … endscript`.",
|
||||
"references": ["man 8 logrotate"]
|
||||
},
|
||||
{
|
||||
"id": "108.3-001",
|
||||
"objective": "108.3",
|
||||
"type": "single",
|
||||
"difficulty": 1,
|
||||
"question": "Welches der folgenden Programme ist ein **Mail Transfer Agent (MTA)**?",
|
||||
"options": [
|
||||
{ "id": "a", "text": "Postfix", "correct": true, "explanation": "Postfix nimmt Mails per SMTP an und leitet sie weiter bzw. stellt sie zu." },
|
||||
{ "id": "b", "text": "Dovecot", "correct": false, "explanation": "Dovecot ist ein IMAP/POP3-Server für den Abruf von Mails." },
|
||||
{ "id": "c", "text": "Thunderbird", "correct": false, "explanation": "Thunderbird ist ein Mail User Agent (MUA)." },
|
||||
{ "id": "d", "text": "mutt", "correct": false, "explanation": "mutt ist ein textbasierter MUA." },
|
||||
{ "id": "e", "text": "procmail", "correct": false, "explanation": "procmail ist ein Mail Delivery Agent (MDA), der Mails lokal sortiert und zustellt." }
|
||||
],
|
||||
"explanation": "Weitere MTAs: Sendmail, Exim. Alle bieten aus Kompatibilitätsgründen einen `sendmail`-Befehl.",
|
||||
"references": ["LPI 102-500 Objective 108.3"]
|
||||
},
|
||||
{
|
||||
"id": "108.3-002",
|
||||
"objective": "108.3",
|
||||
"type": "single",
|
||||
"difficulty": 1,
|
||||
"question": "In `/etc/aliases` wurde `root: admin@example.org` eingetragen. Welcher Befehl muss danach ausgeführt werden?",
|
||||
"options": [
|
||||
{ "id": "a", "text": "`newaliases`", "correct": true, "explanation": "Erzeugt die Alias-Datenbank (z. B. `/etc/aliases.db`) neu, die der MTA liest." },
|
||||
{ "id": "b", "text": "`mailq`", "correct": false, "explanation": "`mailq` zeigt die Warteschlange." },
|
||||
{ "id": "c", "text": "`sendmail -q`", "correct": false, "explanation": "Arbeitet die Warteschlange ab." },
|
||||
{ "id": "d", "text": "`postfix check`", "correct": false, "explanation": "Prüft die Postfix-Konfiguration, baut die Aliase aber nicht neu." },
|
||||
{ "id": "e", "text": "`systemctl restart aliases`", "correct": false, "explanation": "Einen Dienst `aliases` gibt es nicht." }
|
||||
],
|
||||
"explanation": "`newaliases` entspricht `sendmail -bi`.",
|
||||
"references": ["man 5 aliases", "man 1 newaliases"]
|
||||
},
|
||||
{
|
||||
"id": "108.3-003",
|
||||
"objective": "108.3",
|
||||
"type": "fill",
|
||||
"difficulty": 1,
|
||||
"question": "Welche Datei legt ein Benutzer in seinem Home-Verzeichnis an, um seine E-Mails an eine andere Adresse weiterzuleiten? (Dateiname)",
|
||||
"answers": [".forward", "~/.forward"],
|
||||
"explanation": "`~/.forward` enthält die Zieladresse(n). Systemweite Weiterleitungen gehören in `/etc/aliases`.",
|
||||
"references": ["man 5 forward"]
|
||||
},
|
||||
{
|
||||
"id": "108.3-004",
|
||||
"objective": "108.3",
|
||||
"type": "single",
|
||||
"difficulty": 1,
|
||||
"question": "Welcher Befehl zeigt die Mail-Warteschlange des lokalen MTA an?",
|
||||
"options": [
|
||||
{ "id": "a", "text": "`mailq`", "correct": true, "explanation": "`mailq` entspricht `sendmail -bp` und funktioniert bei Sendmail, Postfix und Exim." },
|
||||
{ "id": "b", "text": "`newaliases`", "correct": false, "explanation": "Baut die Alias-Datenbank neu." },
|
||||
{ "id": "c", "text": "`mail`", "correct": false, "explanation": "`mail` ist ein einfacher MUA zum Lesen und Senden." },
|
||||
{ "id": "d", "text": "`postqueue -f`", "correct": false, "explanation": "`postqueue -f` versucht, die Postfix-Warteschlange sofort zuzustellen (flush)." },
|
||||
{ "id": "e", "text": "`smtpd`", "correct": false, "explanation": "`smtpd` ist ein Serverprozess von Postfix." }
|
||||
],
|
||||
"explanation": "Lokale Mails von Diensten (z. B. cron) landen ohne Weiterleitung im Spool unter `/var/spool/mail/` bzw. `/var/mail/`.",
|
||||
"references": ["man 1 mailq"]
|
||||
},
|
||||
{
|
||||
"id": "108.4-001",
|
||||
"objective": "108.4",
|
||||
"type": "single",
|
||||
"difficulty": 1,
|
||||
"question": "Auf welchem TCP-Port ist die Weboberfläche von CUPS standardmäßig erreichbar?",
|
||||
"options": [
|
||||
{ "id": "a", "text": "631", "correct": true, "explanation": "CUPS spricht IPP auf Port 631; dort liegt auch die Weboberfläche (http://localhost:631)." },
|
||||
{ "id": "b", "text": "515", "correct": false, "explanation": "515 ist der Port des alten LPD-Protokolls." },
|
||||
{ "id": "c", "text": "9100", "correct": false, "explanation": "9100 ist der Port für Raw-/JetDirect-Druck direkt auf Netzwerkdrucker." },
|
||||
{ "id": "d", "text": "8080", "correct": false, "explanation": "Üblicher alternativer HTTP-Port, nicht CUPS." },
|
||||
{ "id": "e", "text": "443", "correct": false, "explanation": "443 ist HTTPS." }
|
||||
],
|
||||
"explanation": "CUPS-Konfiguration: `/etc/cups/cupsd.conf` (Dienst), `/etc/cups/printers.conf` (Drucker, nicht von Hand bei laufendem Dienst bearbeiten).",
|
||||
"references": ["man 8 cupsd"]
|
||||
},
|
||||
{
|
||||
"id": "108.4-002",
|
||||
"objective": "108.4",
|
||||
"type": "single",
|
||||
"difficulty": 1,
|
||||
"question": "Welcher Befehl entfernt einen Druckauftrag aus der Warteschlange?",
|
||||
"options": [
|
||||
{ "id": "a", "text": "`lprm`", "correct": true, "explanation": "`lprm <jobnr>` löscht den Auftrag (System-V-Variante: `cancel`)." },
|
||||
{ "id": "b", "text": "`lpq`", "correct": false, "explanation": "`lpq` zeigt die Warteschlange an." },
|
||||
{ "id": "c", "text": "`lpr`", "correct": false, "explanation": "`lpr` druckt eine Datei." },
|
||||
{ "id": "d", "text": "`lpstat`", "correct": false, "explanation": "`lpstat` zeigt Status von Druckern und Aufträgen." },
|
||||
{ "id": "e", "text": "`lpadmin`", "correct": false, "explanation": "`lpadmin` richtet Drucker ein und konfiguriert sie." }
|
||||
],
|
||||
"explanation": "Berkeley-Befehle: `lpr`, `lpq`, `lprm`. System-V-Befehle: `lp`, `lpstat`, `cancel`. CUPS unterstützt beide.",
|
||||
"references": ["man 1 lprm"]
|
||||
},
|
||||
{
|
||||
"id": "108.4-003",
|
||||
"objective": "108.4",
|
||||
"type": "multi",
|
||||
"difficulty": 2,
|
||||
"question": "Welche Aussagen zum Drucken unter Linux treffen zu?",
|
||||
"options": [
|
||||
{ "id": "a", "text": "`lpr -P buero bericht.pdf` druckt auf dem Drucker `buero`.", "correct": true, "explanation": "`-P` wählt den Drucker; ohne Angabe gilt der Standarddrucker." },
|
||||
{ "id": "b", "text": "`lpq` zeigt die Aufträge in der Warteschlange.", "correct": true, "explanation": "Mit `-P` für einen bestimmten Drucker, `-a` für alle." },
|
||||
{ "id": "c", "text": "`lpq` druckt eine Datei aus.", "correct": false, "explanation": "Drucken: `lpr` bzw. `lp`." },
|
||||
{ "id": "d", "text": "Die CUPS-Dienstkonfiguration liegt in `/etc/printcap.conf`.", "correct": false, "explanation": "Sie liegt in `/etc/cups/cupsd.conf`; `/etc/printcap` ist ein Überbleibsel von LPD." },
|
||||
{ "id": "e", "text": "`cupsenable buero` löscht den Drucker `buero`.", "correct": false, "explanation": "`cupsenable` aktiviert einen Drucker; löschen: `lpadmin -x buero`." }
|
||||
],
|
||||
"explanation": "Annahme von Aufträgen steuern `cupsaccept`/`cupsreject`, das Drucken `cupsenable`/`cupsdisable`.",
|
||||
"references": ["man 1 lpr", "man 1 lpq"]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,327 @@
|
||||
{
|
||||
"$schema": "../schema/questions.schema.json",
|
||||
"format": "lpic-sim-questions",
|
||||
"meta": {
|
||||
"file": "questions_109.json",
|
||||
"brief": "Fragen zu Topic 109 Netzwerkgrundlagen (LPIC-1 102-500, Objectives 5.0)",
|
||||
"author": "Björn Runden <br@runden.de>",
|
||||
"owner": "Björn Runden <br@runden.de>",
|
||||
"assist": "KI-unterstützt (Claude Opus 5.5), geprüft von Björn Runden <br@runden.de>",
|
||||
"version": "0.1.0 (29.10.2026)"
|
||||
},
|
||||
"exam": "lpic1-102",
|
||||
"questions": [
|
||||
{
|
||||
"id": "109.1-001",
|
||||
"objective": "109.1",
|
||||
"type": "single",
|
||||
"difficulty": 1,
|
||||
"question": "Welche Subnetzmaske entspricht dem Präfix `/26`?",
|
||||
"options": [
|
||||
{ "id": "a", "text": "`255.255.255.192`", "correct": true, "explanation": "26 Bits gesetzt: 24 + 2 → letztes Oktett 11000000 = 192." },
|
||||
{ "id": "b", "text": "`255.255.255.128`", "correct": false, "explanation": "Das ist `/25`." },
|
||||
{ "id": "c", "text": "`255.255.255.224`", "correct": false, "explanation": "Das ist `/27`." },
|
||||
{ "id": "d", "text": "`255.255.255.240`", "correct": false, "explanation": "Das ist `/28`." },
|
||||
{ "id": "e", "text": "`255.255.255.252`", "correct": false, "explanation": "Das ist `/30`." }
|
||||
],
|
||||
"explanation": "Werte im letzten Oktett: /25 = 128, /26 = 192, /27 = 224, /28 = 240, /29 = 248, /30 = 252.",
|
||||
"references": ["RFC 4632"]
|
||||
},
|
||||
{
|
||||
"id": "109.1-002",
|
||||
"objective": "109.1",
|
||||
"type": "single",
|
||||
"difficulty": 2,
|
||||
"question": "Wie viele Adressen stehen in einem IPv4-Subnetz `/27` für Hosts zur Verfügung?",
|
||||
"options": [
|
||||
{ "id": "a", "text": "30", "correct": true, "explanation": "2^(32−27) = 32 Adressen, abzüglich Netz- und Broadcast-Adresse = 30." },
|
||||
{ "id": "b", "text": "32", "correct": false, "explanation": "Das ist die Gesamtzahl der Adressen inklusive Netz- und Broadcast-Adresse." },
|
||||
{ "id": "c", "text": "62", "correct": false, "explanation": "Das gilt für `/26`." },
|
||||
{ "id": "d", "text": "14", "correct": false, "explanation": "Das gilt für `/28`." },
|
||||
{ "id": "e", "text": "27", "correct": false, "explanation": "Das Präfix ist die Zahl der Netzbits, nicht der Hosts." }
|
||||
],
|
||||
"explanation": "Hosts = 2^(32 − Präfix) − 2. Beispiel `192.0.2.32/27`: Netz .32, Hosts .33–.62, Broadcast .63.",
|
||||
"references": ["RFC 950"]
|
||||
},
|
||||
{
|
||||
"id": "109.1-003",
|
||||
"objective": "109.1",
|
||||
"type": "fill",
|
||||
"difficulty": 1,
|
||||
"question": "Welchen TCP-Port verwendet SSH standardmäßig? (nur die Zahl)",
|
||||
"answers": ["22"],
|
||||
"explanation": "Zuordnungen von Ports zu Diensten stehen in `/etc/services`.",
|
||||
"references": ["man 5 services"]
|
||||
},
|
||||
{
|
||||
"id": "109.1-004",
|
||||
"objective": "109.1",
|
||||
"type": "single",
|
||||
"difficulty": 2,
|
||||
"question": "Welche Adresse gehört zu einem **privaten** Adressbereich nach RFC 1918?",
|
||||
"options": [
|
||||
{ "id": "a", "text": "`172.20.1.5`", "correct": true, "explanation": "Liegt in `172.16.0.0/12` (172.16.0.0 – 172.31.255.255)." },
|
||||
{ "id": "b", "text": "`172.32.0.1`", "correct": false, "explanation": "Liegt knapp außerhalb von `172.16.0.0/12`." },
|
||||
{ "id": "c", "text": "`192.169.1.1`", "correct": false, "explanation": "Privat ist nur `192.168.0.0/16`." },
|
||||
{ "id": "d", "text": "`100.64.0.1`", "correct": false, "explanation": "`100.64.0.0/10` ist Shared Address Space für Carrier-Grade-NAT (RFC 6598), nicht RFC 1918." },
|
||||
{ "id": "e", "text": "`169.254.1.1`", "correct": false, "explanation": "`169.254.0.0/16` ist Link-Local (automatische Adressvergabe ohne DHCP)." }
|
||||
],
|
||||
"explanation": "RFC 1918: `10.0.0.0/8`, `172.16.0.0/12`, `192.168.0.0/16`.",
|
||||
"references": ["RFC 1918"]
|
||||
},
|
||||
{
|
||||
"id": "109.1-005",
|
||||
"objective": "109.1",
|
||||
"type": "multi",
|
||||
"difficulty": 2,
|
||||
"question": "Welche Aussagen zu IPv6 treffen zu?",
|
||||
"options": [
|
||||
{ "id": "a", "text": "Eine IPv6-Adresse ist 128 Bit lang.", "correct": true, "explanation": "Geschrieben als 8 Gruppen à 16 Bit in Hexadezimal." },
|
||||
{ "id": "b", "text": "`::1` ist die Loopback-Adresse.", "correct": true, "explanation": "Entspricht `127.0.0.1` bei IPv4." },
|
||||
{ "id": "c", "text": "Adressen aus `fe80::/10` sind global routbar.", "correct": false, "explanation": "`fe80::/10` ist Link-Local und wird nicht geroutet." },
|
||||
{ "id": "d", "text": "IPv6 verwendet Broadcast-Adressen wie IPv4.", "correct": false, "explanation": "IPv6 kennt keinen Broadcast; stattdessen Multicast (z. B. `ff02::1`)." },
|
||||
{ "id": "e", "text": "Mehrere Nullgruppen dürfen mehrfach in einer Adresse mit `::` abgekürzt werden.", "correct": false, "explanation": "`::` darf nur einmal pro Adresse vorkommen, sonst wäre sie mehrdeutig." }
|
||||
],
|
||||
"explanation": "Weitere Unterschiede: SLAAC zur automatischen Konfiguration, Neighbor Discovery statt ARP, Präfix meist `/64` im LAN.",
|
||||
"references": ["RFC 4291"]
|
||||
},
|
||||
{
|
||||
"id": "109.1-006",
|
||||
"objective": "109.1",
|
||||
"type": "single",
|
||||
"difficulty": 1,
|
||||
"question": "Welche Zuordnung von Standardport zu Dienst ist korrekt?",
|
||||
"options": [
|
||||
{ "id": "a", "text": "443/tcp → HTTPS", "correct": true, "explanation": "HTTP über TLS verwendet Port 443." },
|
||||
{ "id": "b", "text": "25/tcp → POP3", "correct": false, "explanation": "25 ist SMTP; POP3 nutzt 110 (POP3S 995)." },
|
||||
{ "id": "c", "text": "53 → NTP", "correct": false, "explanation": "53 ist DNS; NTP nutzt 123/udp." },
|
||||
{ "id": "d", "text": "143/tcp → SNMP", "correct": false, "explanation": "143 ist IMAP (IMAPS 993); SNMP nutzt 161/udp." },
|
||||
{ "id": "e", "text": "21/tcp → SSH", "correct": false, "explanation": "21 ist FTP (Steuerkanal); SSH nutzt 22." }
|
||||
],
|
||||
"explanation": "Prüfungsrelevant: 20/21 FTP, 22 SSH, 23 Telnet, 25 SMTP, 53 DNS, 80 HTTP, 110 POP3, 123 NTP, 139 NetBIOS, 143 IMAP, 161/162 SNMP, 389 LDAP, 443 HTTPS, 465 SMTPS, 514 syslog, 636 LDAPS, 993 IMAPS, 995 POP3S.",
|
||||
"references": ["man 5 services"]
|
||||
},
|
||||
{
|
||||
"id": "109.2-001",
|
||||
"objective": "109.2",
|
||||
"type": "single",
|
||||
"difficulty": 1,
|
||||
"question": "Welches Kommandozeilenwerkzeug verwaltet Verbindungen des NetworkManager?",
|
||||
"options": [
|
||||
{ "id": "a", "text": "`nmcli`", "correct": true, "explanation": "z. B. `nmcli connection show`, `nmcli device status`, `nmcli con up <name>`." },
|
||||
{ "id": "b", "text": "`ifconfig`", "correct": false, "explanation": "Altes Werkzeug (net-tools), das Schnittstellen direkt konfiguriert, ohne NetworkManager." },
|
||||
{ "id": "c", "text": "`iwconfig`", "correct": false, "explanation": "Altes Werkzeug für WLAN-Parameter." },
|
||||
{ "id": "d", "text": "`systemd-networkd`", "correct": false, "explanation": "Ein eigener Netzwerkdienst, kein Frontend für NetworkManager." },
|
||||
{ "id": "e", "text": "`netstat`", "correct": false, "explanation": "Zeigt Verbindungen und Routen, konfiguriert nichts." }
|
||||
],
|
||||
"explanation": "Textoberfläche für NetworkManager: `nmtui`. systemd-networkd wird über `.network`-Dateien und `networkctl` verwaltet.",
|
||||
"references": ["man 1 nmcli"]
|
||||
},
|
||||
{
|
||||
"id": "109.2-002",
|
||||
"objective": "109.2",
|
||||
"type": "single",
|
||||
"difficulty": 1,
|
||||
"question": "Wie setzt man auf einem systemd-System den Hostnamen dauerhaft auf `web01`?",
|
||||
"options": [
|
||||
{ "id": "a", "text": "`hostnamectl set-hostname web01`", "correct": true, "explanation": "Setzt den Hostnamen sofort und schreibt ihn nach `/etc/hostname`." },
|
||||
{ "id": "b", "text": "`hostname web01`", "correct": false, "explanation": "Gilt nur bis zum nächsten Neustart." },
|
||||
{ "id": "c", "text": "`sysctl kernel.hostname=web01`", "correct": false, "explanation": "Ändert nur den Laufzeitwert im Kernel." },
|
||||
{ "id": "d", "text": "`domainname web01`", "correct": false, "explanation": "Setzt den NIS-Domainnamen." },
|
||||
{ "id": "e", "text": "Nur einen Eintrag in `/etc/hosts` ergänzen", "correct": false, "explanation": "`/etc/hosts` dient der Namensauflösung, nicht dem Setzen des Hostnamens." }
|
||||
],
|
||||
"explanation": "Den eigenen Namen trotzdem in `/etc/hosts` eintragen, damit er lokal auflösbar ist.",
|
||||
"references": ["man 1 hostnamectl"]
|
||||
},
|
||||
{
|
||||
"id": "109.2-003",
|
||||
"objective": "109.2",
|
||||
"type": "fill",
|
||||
"difficulty": 1,
|
||||
"question": "In welcher Datei (vollständiger Pfad) steht der statische Hostname des Systems?",
|
||||
"answers": ["/etc/hostname"],
|
||||
"explanation": "Die Datei enthält nur den Namen in einer Zeile.",
|
||||
"references": ["man 5 hostname"]
|
||||
},
|
||||
{
|
||||
"id": "109.2-004",
|
||||
"objective": "109.2",
|
||||
"type": "single",
|
||||
"difficulty": 1,
|
||||
"question": "Welche Zeile ist in `/etc/hosts` korrekt aufgebaut?",
|
||||
"options": [
|
||||
{ "id": "a", "text": "`192.0.2.10 web01.example.org web01`", "correct": true, "explanation": "IP-Adresse, kanonischer Name, optional Aliase – durch Leerraum getrennt." },
|
||||
{ "id": "b", "text": "`web01.example.org 192.0.2.10`", "correct": false, "explanation": "Die IP-Adresse muss am Anfang stehen." },
|
||||
{ "id": "c", "text": "`192.0.2.10:web01.example.org`", "correct": false, "explanation": "Die Felder werden durch Leerraum getrennt, nicht durch Doppelpunkt." },
|
||||
{ "id": "d", "text": "`host web01 = 192.0.2.10`", "correct": false, "explanation": "Keine gültige Syntax." },
|
||||
{ "id": "e", "text": "`nameserver 192.0.2.10`", "correct": false, "explanation": "Das ist Syntax von `/etc/resolv.conf`." }
|
||||
],
|
||||
"explanation": "Ob `/etc/hosts` vor DNS gefragt wird, legt die Zeile `hosts:` in `/etc/nsswitch.conf` fest.",
|
||||
"references": ["man 5 hosts"]
|
||||
},
|
||||
{
|
||||
"id": "109.2-005",
|
||||
"objective": "109.2",
|
||||
"type": "multi",
|
||||
"difficulty": 2,
|
||||
"question": "Welche Befehle setzen (nicht dauerhaft) korrekt eine IPv4-Adresse bzw. das Standard-Gateway?",
|
||||
"options": [
|
||||
{ "id": "a", "text": "`ip addr add 192.0.2.5/24 dev eth0`", "correct": true, "explanation": "Fügt der Schnittstelle eine Adresse mit Präfix hinzu." },
|
||||
{ "id": "b", "text": "`ip route add default via 192.0.2.1`", "correct": true, "explanation": "Setzt die Standardroute über das Gateway." },
|
||||
{ "id": "c", "text": "`ip addr set eth0 192.0.2.5`", "correct": false, "explanation": "`ip addr` kennt kein `set`; Adressen werden mit `add` hinzugefügt." },
|
||||
{ "id": "d", "text": "`ip gateway add 192.0.2.1`", "correct": false, "explanation": "Ein Objekt `gateway` gibt es bei `ip` nicht." },
|
||||
{ "id": "e", "text": "`ip route default 192.0.2.1`", "correct": false, "explanation": "Es fehlen Aktion (`add`) und Schlüsselwort `via`." }
|
||||
],
|
||||
"explanation": "Mit `ip` gesetzte Werte gehen beim Neustart verloren. Dauerhaft je nach System: NetworkManager, systemd-networkd, netplan oder `/etc/network/interfaces`.",
|
||||
"references": ["man 8 ip-address", "man 8 ip-route"]
|
||||
},
|
||||
{
|
||||
"id": "109.2-006",
|
||||
"objective": "109.2",
|
||||
"type": "single",
|
||||
"difficulty": 2,
|
||||
"question": "Wo werden auf einem Debian-System mit **ifupdown** Netzwerkschnittstellen dauerhaft konfiguriert?",
|
||||
"options": [
|
||||
{ "id": "a", "text": "`/etc/network/interfaces`", "correct": true, "explanation": "Dort stehen z. B. `auto eth0` und `iface eth0 inet static` mit `address`, `gateway`; aktivieren mit `ifup`." },
|
||||
{ "id": "b", "text": "`/etc/sysconfig/network-scripts/ifcfg-eth0`", "correct": false, "explanation": "Klassischer Ort der Red-Hat-Familie, nicht Debian." },
|
||||
{ "id": "c", "text": "`/etc/netplan/01-netcfg.yaml`", "correct": false, "explanation": "netplan wird bei Ubuntu verwendet und erzeugt Konfiguration für networkd oder NetworkManager." },
|
||||
{ "id": "d", "text": "`/etc/resolv.conf`", "correct": false, "explanation": "Enthält nur DNS-Einstellungen." },
|
||||
{ "id": "e", "text": "`/etc/hosts`", "correct": false, "explanation": "Statische Namensauflösung, keine Schnittstellenkonfiguration." }
|
||||
],
|
||||
"explanation": "Hinweis: LPI hat angekündigt, `ifup`/`ifdown` und net-tools künftig aus den Objectives zu entfernen; in Version 5.0 sind sie noch enthalten (Stand 29.10.2026).",
|
||||
"references": ["man 5 interfaces"]
|
||||
},
|
||||
{
|
||||
"id": "109.3-001",
|
||||
"objective": "109.3",
|
||||
"type": "single",
|
||||
"difficulty": 1,
|
||||
"question": "Auf welchem Protokoll und welchen Nachrichten beruht `ping`?",
|
||||
"options": [
|
||||
{ "id": "a", "text": "ICMP Echo Request / Echo Reply", "correct": true, "explanation": "`ping` sendet ICMP Echo Requests und wertet die Antworten aus (IPv6: ICMPv6, `ping -6`)." },
|
||||
{ "id": "b", "text": "UDP-Pakete an Port 7", "correct": false, "explanation": "Port 7 ist der echo-Dienst; `ping` nutzt ihn nicht." },
|
||||
{ "id": "c", "text": "TCP-SYN-Pakete", "correct": false, "explanation": "TCP wird von `ping` nicht verwendet." },
|
||||
{ "id": "d", "text": "ARP Requests", "correct": false, "explanation": "ARP löst im LAN IP- in MAC-Adressen auf (`arping` nutzt ARP)." },
|
||||
{ "id": "e", "text": "DNS-Anfragen", "correct": false, "explanation": "DNS wird höchstens zur Auflösung des Zielnamens genutzt." }
|
||||
],
|
||||
"explanation": "Keine Antwort auf ping heißt nicht zwingend, dass der Host aus ist – ICMP kann gefiltert sein.",
|
||||
"references": ["man 8 ping"]
|
||||
},
|
||||
{
|
||||
"id": "109.3-002",
|
||||
"objective": "109.3",
|
||||
"type": "single",
|
||||
"difficulty": 2,
|
||||
"question": "Welcher Befehl listet alle lauschenden TCP- und UDP-Ports numerisch mit den zugehörigen Prozessen?",
|
||||
"options": [
|
||||
{ "id": "a", "text": "`ss -tulpn`", "correct": true, "explanation": "`-t` TCP, `-u` UDP, `-l` lauschend, `-p` Prozess, `-n` numerisch." },
|
||||
{ "id": "b", "text": "`netstat -r`", "correct": false, "explanation": "`-r` zeigt die Routingtabelle." },
|
||||
{ "id": "c", "text": "`ip link show`", "correct": false, "explanation": "Zeigt Schnittstellen und deren Status." },
|
||||
{ "id": "d", "text": "`ss -s`", "correct": false, "explanation": "Zeigt nur eine Zusammenfassung der Sockets." },
|
||||
{ "id": "e", "text": "`nc -l 22`", "correct": false, "explanation": "Startet selbst einen lauschenden Socket." }
|
||||
],
|
||||
"explanation": "Die alte Entsprechung ist `netstat -tulpn` (net-tools). Prozessangaben fremder Benutzer sieht nur root.",
|
||||
"references": ["man 8 ss"]
|
||||
},
|
||||
{
|
||||
"id": "109.3-003",
|
||||
"objective": "109.3",
|
||||
"type": "fill",
|
||||
"difficulty": 1,
|
||||
"question": "Mit welchem Befehl (nur der Name) ermittelt man die Router (Hops) auf dem Weg zu einem Zielhost?",
|
||||
"answers": ["traceroute", "tracepath", "traceroute6", "tracepath6", "mtr"],
|
||||
"explanation": "`traceroute` erhöht schrittweise die TTL und wertet die ICMP-Antworten „Time Exceeded“ aus. `tracepath` braucht keine Root-Rechte und ermittelt zusätzlich die Path-MTU.",
|
||||
"references": ["man 8 traceroute", "man 8 tracepath"]
|
||||
},
|
||||
{
|
||||
"id": "109.3-004",
|
||||
"objective": "109.3",
|
||||
"type": "single",
|
||||
"difficulty": 1,
|
||||
"question": "Welcher Befehl zeigt die aktuelle Routingtabelle an?",
|
||||
"options": [
|
||||
{ "id": "a", "text": "`ip route`", "correct": true, "explanation": "Kurz `ip r`; zeigt u. a. die `default`-Route." },
|
||||
{ "id": "b", "text": "`ip addr`", "correct": false, "explanation": "Zeigt die Adressen der Schnittstellen." },
|
||||
{ "id": "c", "text": "`ip neigh`", "correct": false, "explanation": "Zeigt den Neighbor-/ARP-Cache." },
|
||||
{ "id": "d", "text": "`ss -r`", "correct": false, "explanation": "`-r` löst bei `ss` Namen auf; es zeigt keine Routen." },
|
||||
{ "id": "e", "text": "`host -r`", "correct": false, "explanation": "`host` ist ein DNS-Abfragewerkzeug." }
|
||||
],
|
||||
"explanation": "Alte Werkzeuge: `route -n`, `netstat -rn`. Welche Route für ein Ziel gilt: `ip route get 198.51.100.7`.",
|
||||
"references": ["man 8 ip-route"]
|
||||
},
|
||||
{
|
||||
"id": "109.3-005",
|
||||
"objective": "109.3",
|
||||
"type": "multi",
|
||||
"difficulty": 2,
|
||||
"question": "Mit welchen Befehlen kann man prüfen, ob ein Server Verbindungen auf **TCP-Port 443** annimmt?",
|
||||
"options": [
|
||||
{ "id": "a", "text": "`nc -zv server.example.org 443`", "correct": true, "explanation": "`-z` baut nur die Verbindung auf, ohne Daten zu senden; `-v` meldet das Ergebnis." },
|
||||
{ "id": "b", "text": "`telnet server.example.org 443`", "correct": true, "explanation": "Kommt eine Verbindung zustande, nimmt der Port Verbindungen an." },
|
||||
{ "id": "c", "text": "`ping -p 443 server.example.org`", "correct": false, "explanation": "`-p` legt bei ping ein Füllmuster fest; ICMP kennt keine Ports." },
|
||||
{ "id": "d", "text": "`dig server.example.org 443`", "correct": false, "explanation": "`dig` stellt nur DNS-Anfragen." },
|
||||
{ "id": "e", "text": "`ip neigh server.example.org 443`", "correct": false, "explanation": "Zeigt den Neighbor-Cache und prüft keine Ports." }
|
||||
],
|
||||
"explanation": "Vorgehen bei Netzproblemen von unten nach oben: Link (`ip link`) → Adresse (`ip addr`) → Gateway (`ping`) → Route (`ip route`, `traceroute`) → DNS (`dig`) → Port (`nc`).",
|
||||
"references": ["man 1 nc"]
|
||||
},
|
||||
{
|
||||
"id": "109.3-006",
|
||||
"objective": "109.3",
|
||||
"type": "single",
|
||||
"difficulty": 2,
|
||||
"question": "`ping 198.51.100.7` funktioniert, `ping www.example.org` meldet jedoch „Name or service not known“. Was sollte man als Erstes prüfen?",
|
||||
"options": [
|
||||
{ "id": "a", "text": "Die Namensauflösung, z. B. `/etc/resolv.conf` bzw. `resolvectl status`", "correct": true, "explanation": "IP-Verbindung und Routing funktionieren; nur der Name lässt sich nicht auflösen." },
|
||||
{ "id": "b", "text": "Ob ein Standard-Gateway gesetzt ist", "correct": false, "explanation": "Der Ping an eine entfernte IP funktioniert – Routing ist also in Ordnung." },
|
||||
{ "id": "c", "text": "Ob die Netzwerkkarte einen Link hat", "correct": false, "explanation": "Ohne Link wäre auch der Ping an die IP gescheitert." },
|
||||
{ "id": "d", "text": "Ob eine Firewall ICMP blockiert", "correct": false, "explanation": "ICMP kommt durch, wie der erste Ping zeigt." },
|
||||
{ "id": "e", "text": "Ob die MTU zu groß ist", "correct": false, "explanation": "MTU-Probleme zeigen sich nicht als Fehler bei der Namensauflösung." }
|
||||
],
|
||||
"explanation": "DNS gezielt testen: `dig www.example.org`, `host www.example.org`, `getent hosts www.example.org`.",
|
||||
"references": ["man 5 resolv.conf"]
|
||||
},
|
||||
{
|
||||
"id": "109.4-001",
|
||||
"objective": "109.4",
|
||||
"type": "single",
|
||||
"difficulty": 2,
|
||||
"question": "Welche Datei legt fest, in welcher Reihenfolge Quellen wie `files` (also `/etc/hosts`) und `dns` zur Auflösung von Hostnamen befragt werden?",
|
||||
"options": [
|
||||
{ "id": "a", "text": "`/etc/nsswitch.conf`", "correct": true, "explanation": "Zeile z. B. `hosts: files dns`." },
|
||||
{ "id": "b", "text": "`/etc/resolv.conf`", "correct": false, "explanation": "Enthält die DNS-Server und Suchdomänen, nicht die Reihenfolge der Quellen." },
|
||||
{ "id": "c", "text": "`/etc/hosts`", "correct": false, "explanation": "Ist selbst eine Quelle (`files`)." },
|
||||
{ "id": "d", "text": "`/etc/hostname`", "correct": false, "explanation": "Enthält den eigenen Hostnamen." },
|
||||
{ "id": "e", "text": "`/etc/systemd/resolved.conf`", "correct": false, "explanation": "Konfiguriert den Dienst systemd-resolved, nicht die NSS-Reihenfolge." }
|
||||
],
|
||||
"explanation": "NSS (Name Service Switch) regelt auch `passwd`, `group` usw.; `getent` fragt genau diese Kette ab.",
|
||||
"references": ["man 5 nsswitch.conf"]
|
||||
},
|
||||
{
|
||||
"id": "109.4-002",
|
||||
"objective": "109.4",
|
||||
"type": "fill",
|
||||
"difficulty": 1,
|
||||
"question": "Mit welchem Schlüsselwort trägt man in `/etc/resolv.conf` einen DNS-Server ein?",
|
||||
"answers": ["nameserver"],
|
||||
"explanation": "Beispiel: `nameserver 192.0.2.53`. Weitere Schlüsselwörter: `search`, `domain`, `options`. Bei systemd-resolved verweist die Datei oft auf den Stub `127.0.0.53`.",
|
||||
"references": ["man 5 resolv.conf"]
|
||||
},
|
||||
{
|
||||
"id": "109.4-003",
|
||||
"objective": "109.4",
|
||||
"type": "multi",
|
||||
"difficulty": 2,
|
||||
"question": "Welche Programme stellen **gezielt DNS-Anfragen** an einen Nameserver (ohne `/etc/hosts` zu berücksichtigen)?",
|
||||
"options": [
|
||||
{ "id": "a", "text": "`dig`", "correct": true, "explanation": "Ausführliche Abfragen, z. B. `dig @192.0.2.53 example.org MX`." },
|
||||
{ "id": "b", "text": "`host`", "correct": true, "explanation": "Kompakte Ausgabe, z. B. `host example.org`." },
|
||||
{ "id": "c", "text": "`nslookup`", "correct": true, "explanation": "Älteres, interaktiv nutzbares DNS-Werkzeug." },
|
||||
{ "id": "d", "text": "`getent hosts`", "correct": false, "explanation": "Fragt über NSS ab – also auch `/etc/hosts` gemäß `nsswitch.conf`." },
|
||||
{ "id": "e", "text": "`ip neigh`", "correct": false, "explanation": "Zeigt den ARP-/Neighbor-Cache." }
|
||||
],
|
||||
"explanation": "Ein Name ist per `dig` auflösbar, Programme finden ihn trotzdem nicht? Dann `nsswitch.conf` und `/etc/hosts` prüfen – und umgekehrt.",
|
||||
"references": ["man 1 dig", "man 1 host"]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,237 @@
|
||||
{
|
||||
"$schema": "../schema/questions.schema.json",
|
||||
"format": "lpic-sim-questions",
|
||||
"meta": {
|
||||
"file": "questions_110.json",
|
||||
"brief": "Fragen zu Topic 110 Sicherheit (LPIC-1 102-500, Objectives 5.0)",
|
||||
"author": "Björn Runden <br@runden.de>",
|
||||
"owner": "Björn Runden <br@runden.de>",
|
||||
"assist": "KI-unterstützt (Claude Opus 5.5), geprüft von Björn Runden <br@runden.de>",
|
||||
"version": "0.1.0 (29.10.2026)"
|
||||
},
|
||||
"exam": "lpic1-102",
|
||||
"questions": [
|
||||
{
|
||||
"id": "110.1-001",
|
||||
"objective": "110.1",
|
||||
"type": "single",
|
||||
"difficulty": 2,
|
||||
"question": "Welcher Befehl findet alle Dateien, bei denen das **SUID-Bit** gesetzt ist?",
|
||||
"options": [
|
||||
{ "id": "a", "text": "`find / -perm -4000 -type f`", "correct": true, "explanation": "`-perm -4000`: mindestens das SUID-Bit gesetzt, andere Bits beliebig." },
|
||||
{ "id": "b", "text": "`find / -perm 4000 -type f`", "correct": false, "explanation": "Ohne `-` müssen die Rechte **exakt** 4000 sein – praktisch keine Datei." },
|
||||
{ "id": "c", "text": "`find / -perm -2000 -type f`", "correct": false, "explanation": "2000 ist das SGID-Bit." },
|
||||
{ "id": "d", "text": "`find / -type s`", "correct": false, "explanation": "`-type s` findet Sockets." },
|
||||
{ "id": "e", "text": "`find / -user root -type f`", "correct": false, "explanation": "Findet alle Dateien von root, unabhängig von SUID." }
|
||||
],
|
||||
"explanation": "`-perm /6000` findet Dateien mit SUID **oder** SGID. Unerwartete SUID-Programme sind ein Sicherheitsrisiko.",
|
||||
"references": ["man 1 find"]
|
||||
},
|
||||
{
|
||||
"id": "110.1-002",
|
||||
"objective": "110.1",
|
||||
"type": "single",
|
||||
"difficulty": 1,
|
||||
"question": "Wie bearbeitet man die sudo-Konfiguration `/etc/sudoers` sicher?",
|
||||
"options": [
|
||||
{ "id": "a", "text": "Mit `visudo`", "correct": true, "explanation": "`visudo` sperrt die Datei und prüft die Syntax vor dem Speichern; ein Fehler könnte sonst sudo unbenutzbar machen." },
|
||||
{ "id": "b", "text": "Direkt mit `vi /etc/sudoers`", "correct": false, "explanation": "Keine Syntaxprüfung – ein Tippfehler kann den Zugriff auf sudo sperren." },
|
||||
{ "id": "c", "text": "Mit `sudo -l`", "correct": false, "explanation": "`sudo -l` zeigt nur die erlaubten Befehle des Benutzers." },
|
||||
{ "id": "d", "text": "Mit `chmod 777 /etc/sudoers` und einem Editor", "correct": false, "explanation": "sudo verweigert den Dienst bei falschen Rechten; zudem wäre das ein Sicherheitsloch." },
|
||||
{ "id": "e", "text": "Mit `su -c sudoers`", "correct": false, "explanation": "Führt einen Befehl `sudoers` als root aus – den es nicht gibt." }
|
||||
],
|
||||
"explanation": "Ergänzungen besser als eigene Dateien unter `/etc/sudoers.d/` (`visudo -f /etc/sudoers.d/team`). Beispielzeile: `%admin ALL=(ALL) ALL`.",
|
||||
"references": ["man 8 visudo", "man 5 sudoers"]
|
||||
},
|
||||
{
|
||||
"id": "110.1-003",
|
||||
"objective": "110.1",
|
||||
"type": "fill",
|
||||
"difficulty": 1,
|
||||
"question": "Welcher Befehl (nur der Name) zeigt die Liste der letzten Anmeldungen und Neustarts aus `/var/log/wtmp`?",
|
||||
"answers": ["last"],
|
||||
"explanation": "`last` liest `wtmp`, `lastb` die fehlgeschlagenen Anmeldungen aus `btmp`. Aktuell angemeldete Benutzer zeigen `who` und `w`.",
|
||||
"references": ["man 1 last"]
|
||||
},
|
||||
{
|
||||
"id": "110.1-004",
|
||||
"objective": "110.1",
|
||||
"type": "multi",
|
||||
"difficulty": 2,
|
||||
"question": "Welche Aussagen treffen zu?",
|
||||
"options": [
|
||||
{ "id": "a", "text": "`ulimit -n` zeigt die maximale Anzahl offener Dateien für die aktuelle Shell.", "correct": true, "explanation": "`ulimit -a` zeigt alle Limits; dauerhaft per `/etc/security/limits.conf`." },
|
||||
{ "id": "b", "text": "`who` zeigt die aktuell angemeldeten Benutzer.", "correct": true, "explanation": "Liest `utmp`; `w` zeigt zusätzlich deren Aktivität." },
|
||||
{ "id": "c", "text": "`su -` übernimmt die Umgebung des aufrufenden Benutzers unverändert.", "correct": false, "explanation": "`su -` startet eine Login-Shell mit der Umgebung des Zielbenutzers." },
|
||||
{ "id": "d", "text": "`sudo -l` zeigt das Passwort von root an.", "correct": false, "explanation": "Es listet die per sudo erlaubten Befehle." },
|
||||
{ "id": "e", "text": "`chage -l anna` sperrt das Konto von `anna`.", "correct": false, "explanation": "`chage -l` zeigt nur die Passwort-Alterung an." }
|
||||
],
|
||||
"explanation": "Sicherheitsrelevante Befehle aus 110.1: `find`, `passwd`, `fuser`, `lsof`, `nmap`, `chage`, `netstat`, `sudo`, `su`, `usermod`, `ulimit`, `who`, `w`, `last`.",
|
||||
"references": ["help ulimit", "man 1 who"]
|
||||
},
|
||||
{
|
||||
"id": "110.2-001",
|
||||
"objective": "110.2",
|
||||
"type": "single",
|
||||
"difficulty": 1,
|
||||
"question": "Ein nicht benötigter Dienst soll sofort gestoppt werden und beim nächsten Start nicht mehr starten. Welcher Befehl leistet beides?",
|
||||
"options": [
|
||||
{ "id": "a", "text": "`systemctl disable --now dienst`", "correct": true, "explanation": "`disable` entfernt die Autostart-Verknüpfungen, `--now` stoppt zusätzlich sofort." },
|
||||
{ "id": "b", "text": "`systemctl stop dienst`", "correct": false, "explanation": "Stoppt nur; beim nächsten Start läuft der Dienst wieder." },
|
||||
{ "id": "c", "text": "`systemctl reload dienst`", "correct": false, "explanation": "Lässt den Dienst seine Konfiguration neu lesen." },
|
||||
{ "id": "d", "text": "`systemctl status dienst`", "correct": false, "explanation": "Zeigt nur den Zustand." },
|
||||
{ "id": "e", "text": "`systemctl isolate dienst`", "correct": false, "explanation": "`isolate` ist für Targets gedacht und beendet alle nicht zugehörigen Units." }
|
||||
],
|
||||
"explanation": "Noch strenger: `systemctl mask` verhindert auch den manuellen Start oder den Start als Abhängigkeit. Grundsatz: Was nicht läuft, kann nicht angegriffen werden.",
|
||||
"references": ["man 1 systemctl"]
|
||||
},
|
||||
{
|
||||
"id": "110.2-002",
|
||||
"objective": "110.2",
|
||||
"type": "single",
|
||||
"difficulty": 2,
|
||||
"question": "Was bedeutet ein `!` am Anfang des Passwortfelds eines Eintrags in `/etc/shadow`?",
|
||||
"options": [
|
||||
{ "id": "a", "text": "Das Passwort ist gesperrt; eine Anmeldung per Passwort ist nicht möglich.", "correct": true, "explanation": "Gesetzt z. B. durch `passwd -l` oder `usermod -L`; der ursprüngliche Hash bleibt dahinter erhalten." },
|
||||
{ "id": "b", "text": "Das Konto hat kein Passwort; Anmeldung ohne Passwort ist möglich.", "correct": false, "explanation": "Ein **leeres** Feld bedeutet kein Passwort." },
|
||||
{ "id": "c", "text": "Das Passwort muss bei der nächsten Anmeldung geändert werden.", "correct": false, "explanation": "Das wird über das Feld „letzte Änderung“ = 0 erreicht (`chage -d 0`)." },
|
||||
{ "id": "d", "text": "Der Hash wurde mit SHA-512 erzeugt.", "correct": false, "explanation": "Das Verfahren erkennt man am Präfix, z. B. `$6$` für SHA-512 oder `$y$` für yescrypt." },
|
||||
{ "id": "e", "text": "Der Benutzer ist Mitglied der Gruppe `wheel`.", "correct": false, "explanation": "Gruppenmitgliedschaften stehen in `/etc/group`." }
|
||||
],
|
||||
"explanation": "`*` oder `!` allein werden für Systemkonten verwendet, die sich nie mit Passwort anmelden sollen.",
|
||||
"references": ["man 5 shadow"]
|
||||
},
|
||||
{
|
||||
"id": "110.2-003",
|
||||
"objective": "110.2",
|
||||
"type": "single",
|
||||
"difficulty": 2,
|
||||
"question": "`/etc/hosts.allow` ist leer, `/etc/hosts.deny` enthält `ALL: ALL`. Was bewirkt das bei Diensten, die TCP-Wrapper (libwrap) verwenden?",
|
||||
"options": [
|
||||
{ "id": "a", "text": "Alle Zugriffe auf diese Dienste werden von allen Hosts abgewiesen.", "correct": true, "explanation": "Zuerst wird `hosts.allow` geprüft (kein Treffer), dann `hosts.deny`: Alle Dienste, alle Clients → abgelehnt." },
|
||||
{ "id": "b", "text": "Die Kernel-Firewall blockiert sämtlichen Netzwerkverkehr.", "correct": false, "explanation": "TCP-Wrapper wirken nur bei Programmen, die libwrap nutzen, nicht im Kernel." },
|
||||
{ "id": "c", "text": "Alle lokalen Anmeldungen an der Konsole werden verhindert.", "correct": false, "explanation": "Konsolenanmeldungen werden von TCP-Wrappern nicht erfasst." },
|
||||
{ "id": "d", "text": "Alle Zugriffe werden erlaubt, weil `hosts.deny` zuerst gelesen wird.", "correct": false, "explanation": "Die Reihenfolge ist `hosts.allow`, dann `hosts.deny`." },
|
||||
{ "id": "e", "text": "Nichts, weil die Syntax ungültig ist.", "correct": false, "explanation": "`dienst: client` mit `ALL` als Platzhalter ist gültig." }
|
||||
],
|
||||
"explanation": "Typisches Muster: `hosts.deny` = `ALL: ALL`, Ausnahmen in `hosts.allow`, z. B. `sshd: 192.0.2.`. Laut LPI sollen TCP-Wrapper künftig aus den Objectives entfallen; in 5.0 sind sie noch enthalten (Stand 29.10.2026).",
|
||||
"references": ["man 5 hosts_access"]
|
||||
},
|
||||
{
|
||||
"id": "110.2-004",
|
||||
"objective": "110.2",
|
||||
"type": "multi",
|
||||
"difficulty": 1,
|
||||
"question": "Mit welchen Befehlen stellt man fest, welche Netzwerkdienste auf einem Rechner erreichbar sind?",
|
||||
"options": [
|
||||
{ "id": "a", "text": "`ss -tuln`", "correct": true, "explanation": "Lokale Sicht: alle lauschenden TCP-/UDP-Sockets." },
|
||||
{ "id": "b", "text": "`nmap localhost`", "correct": true, "explanation": "Portscan von außen bzw. lokal; nur gegen eigene Systeme oder mit Erlaubnis." },
|
||||
{ "id": "c", "text": "`systemctl list-timers`", "correct": false, "explanation": "Listet systemd-Timer." },
|
||||
{ "id": "d", "text": "`ip addr`", "correct": false, "explanation": "Zeigt nur Adressen der Schnittstellen." },
|
||||
{ "id": "e", "text": "`lsmod`", "correct": false, "explanation": "Listet Kernelmodule." }
|
||||
],
|
||||
"explanation": "Weitere: `netstat -tuln`, `lsof -i`, `fuser`. Ziel: unnötige Dienste finden und abschalten.",
|
||||
"references": ["man 8 ss", "man 1 nmap"]
|
||||
},
|
||||
{
|
||||
"id": "110.2-005",
|
||||
"objective": "110.2",
|
||||
"type": "fill",
|
||||
"difficulty": 1,
|
||||
"question": "Welche Datei (vollständiger Pfad) verhindert, solange sie existiert, die Anmeldung aller Benutzer außer root? Ihr Inhalt wird abgewiesenen Benutzern angezeigt.",
|
||||
"answers": ["/etc/nologin"],
|
||||
"explanation": "Nützlich bei Wartungsarbeiten; `shutdown` legt sie kurz vor dem Herunterfahren automatisch an (bei systemd unter `/run/nologin`).",
|
||||
"references": ["man 5 nologin"]
|
||||
},
|
||||
{
|
||||
"id": "110.3-001",
|
||||
"objective": "110.3",
|
||||
"type": "single",
|
||||
"difficulty": 1,
|
||||
"question": "Mit welchem Befehl erzeugt ein Benutzer ein neues SSH-Schlüsselpaar vom Typ Ed25519?",
|
||||
"options": [
|
||||
{ "id": "a", "text": "`ssh-keygen -t ed25519`", "correct": true, "explanation": "Erzeugt `~/.ssh/id_ed25519` (privat) und `~/.ssh/id_ed25519.pub` (öffentlich)." },
|
||||
{ "id": "b", "text": "`ssh-add -t ed25519`", "correct": false, "explanation": "`ssh-add` lädt Schlüssel in den ssh-agent (`-t` = Lebensdauer)." },
|
||||
{ "id": "c", "text": "`ssh-copy-id -t ed25519`", "correct": false, "explanation": "`ssh-copy-id` überträgt einen öffentlichen Schlüssel auf einen Server." },
|
||||
{ "id": "d", "text": "`ssh-agent ed25519`", "correct": false, "explanation": "`ssh-agent` hält entschlüsselte Schlüssel im Speicher, erzeugt aber keine." },
|
||||
{ "id": "e", "text": "`gpg --gen-key --ssh`", "correct": false, "explanation": "`gpg` erzeugt OpenPGP-Schlüssel; die Option `--ssh` gibt es so nicht." }
|
||||
],
|
||||
"explanation": "Weitere Typen: `rsa` (mit `-b 4096`), `ecdsa`. Eine Passphrase schützt den privaten Schlüssel.",
|
||||
"references": ["man 1 ssh-keygen"]
|
||||
},
|
||||
{
|
||||
"id": "110.3-002",
|
||||
"objective": "110.3",
|
||||
"type": "fill",
|
||||
"difficulty": 1,
|
||||
"question": "In welche Datei im Home-Verzeichnis des Zielbenutzers auf dem **Server** gehört der öffentliche Schlüssel, damit eine Anmeldung per Schlüssel möglich ist? (Pfad relativ zum Home oder mit `~`)",
|
||||
"answers": ["~/.ssh/authorized_keys", ".ssh/authorized_keys", "authorized_keys"],
|
||||
"explanation": "Ein Schlüssel pro Zeile; `ssh-copy-id benutzer@server` trägt ihn ein. Zu offene Rechte auf `~/.ssh` (700) oder die Datei (600) lassen sshd den Schlüssel ignorieren.",
|
||||
"references": ["man 8 sshd (Abschnitt AUTHORIZED_KEYS FILE FORMAT)"]
|
||||
},
|
||||
{
|
||||
"id": "110.3-003",
|
||||
"objective": "110.3",
|
||||
"type": "single",
|
||||
"difficulty": 1,
|
||||
"question": "Wozu dient die Datei `~/.ssh/known_hosts` auf dem Client?",
|
||||
"options": [
|
||||
{ "id": "a", "text": "Sie speichert die Host-Schlüssel bereits besuchter Server, um sie bei späteren Verbindungen zu prüfen.", "correct": true, "explanation": "Ändert sich der Host-Schlüssel, warnt ssh vor einem möglichen Man-in-the-Middle-Angriff." },
|
||||
{ "id": "b", "text": "Sie enthält die öffentlichen Schlüssel von Benutzern, die sich anmelden dürfen.", "correct": false, "explanation": "Das ist `~/.ssh/authorized_keys` auf dem Server." },
|
||||
{ "id": "c", "text": "Sie enthält den privaten Schlüssel des Benutzers.", "correct": false, "explanation": "Private Schlüssel liegen z. B. in `~/.ssh/id_ed25519`." },
|
||||
{ "id": "d", "text": "Sie listet die Hosts, die per SSH gesperrt sind.", "correct": false, "explanation": "Eine Sperrliste ist das nicht." },
|
||||
{ "id": "e", "text": "Sie enthält die Client-Einstellungen wie Benutzername und Port je Host.", "correct": false, "explanation": "Das ist `~/.ssh/config`." }
|
||||
],
|
||||
"explanation": "Einen veralteten Eintrag entfernt man mit `ssh-keygen -R host`. Die Host-Schlüssel des Servers liegen in `/etc/ssh/ssh_host_*`.",
|
||||
"references": ["man 1 ssh (Abschnitt VERIFYING HOST KEYS)"]
|
||||
},
|
||||
{
|
||||
"id": "110.3-004",
|
||||
"objective": "110.3",
|
||||
"type": "single",
|
||||
"difficulty": 2,
|
||||
"question": "Die Datei `vertrag.pdf` soll mit GnuPG so verschlüsselt werden, dass nur `anna@example.org` sie entschlüsseln kann. Welcher Befehl ist korrekt?",
|
||||
"options": [
|
||||
{ "id": "a", "text": "`gpg --encrypt --recipient anna@example.org vertrag.pdf`", "correct": true, "explanation": "Verschlüsselt mit Annas öffentlichem Schlüssel; Ergebnis `vertrag.pdf.gpg`." },
|
||||
{ "id": "b", "text": "`gpg --sign --recipient anna@example.org vertrag.pdf`", "correct": false, "explanation": "`--sign` signiert mit dem eigenen privaten Schlüssel; verschlüsselt wird nicht." },
|
||||
{ "id": "c", "text": "`gpg --export anna@example.org vertrag.pdf`", "correct": false, "explanation": "`--export` gibt öffentliche Schlüssel aus." },
|
||||
{ "id": "d", "text": "`gpg --decrypt --recipient anna@example.org vertrag.pdf`", "correct": false, "explanation": "`--decrypt` entschlüsselt." },
|
||||
{ "id": "e", "text": "`gpg --gen-key anna@example.org vertrag.pdf`", "correct": false, "explanation": "`--gen-key` erzeugt ein neues eigenes Schlüsselpaar." }
|
||||
],
|
||||
"explanation": "Dazu muss Annas öffentlicher Schlüssel importiert sein (`gpg --import`). Signatur prüfen: `gpg --verify`. Schlüssel widerrufen: Widerrufszertifikat (`--gen-revoke`).",
|
||||
"references": ["man 1 gpg"]
|
||||
},
|
||||
{
|
||||
"id": "110.3-005",
|
||||
"objective": "110.3",
|
||||
"type": "multi",
|
||||
"difficulty": 2,
|
||||
"question": "Welche Aussagen zu OpenSSH treffen zu?",
|
||||
"options": [
|
||||
{ "id": "a", "text": "Mit `ssh -L 8080:localhost:80 benutzer@server` wird der lokale Port 8080 über SSH an Port 80 des Servers weitergeleitet.", "correct": true, "explanation": "Lokales Port-Forwarding; „localhost“ ist aus Sicht des Servers gemeint." },
|
||||
{ "id": "b", "text": "`ssh-agent` hält entschlüsselte private Schlüssel im Speicher, sodass die Passphrase nur einmal eingegeben werden muss.", "correct": true, "explanation": "Schlüssel werden mit `ssh-add` in den Agent geladen." },
|
||||
{ "id": "c", "text": "Ein privater Schlüssel sollte die Rechte 644 haben.", "correct": false, "explanation": "Private Schlüssel nur für den Eigentümer lesbar (600); sonst verweigert ssh sie." },
|
||||
{ "id": "d", "text": "`ssh -R` startet auf dem Server eine grafische X11-Sitzung.", "correct": false, "explanation": "`-R` richtet Remote-Port-Forwarding ein; X11-Weiterleitung ist `-X`/`-Y`." },
|
||||
{ "id": "e", "text": "Die Server-Konfiguration steht in `~/.ssh/config`.", "correct": false, "explanation": "Server: `/etc/ssh/sshd_config`; `~/.ssh/config` ist Client-Konfiguration." }
|
||||
],
|
||||
"explanation": "Härtung in `sshd_config`: z. B. `PermitRootLogin no`, `PasswordAuthentication no`.",
|
||||
"references": ["man 1 ssh", "man 5 sshd_config"]
|
||||
},
|
||||
{
|
||||
"id": "110.3-006",
|
||||
"objective": "110.3",
|
||||
"type": "single",
|
||||
"difficulty": 1,
|
||||
"question": "Nach `ssh-keygen -t ed25519` mit Standardnamen: Welche Datei ist der **private** Schlüssel?",
|
||||
"options": [
|
||||
{ "id": "a", "text": "`~/.ssh/id_ed25519`", "correct": true, "explanation": "Die Datei ohne Endung ist der private Schlüssel; sie darf das System nie verlassen." },
|
||||
{ "id": "b", "text": "`~/.ssh/id_ed25519.pub`", "correct": false, "explanation": "`.pub` ist der öffentliche Schlüssel, der auf Server verteilt wird." },
|
||||
{ "id": "c", "text": "`~/.ssh/authorized_keys`", "correct": false, "explanation": "Liste erlaubter öffentlicher Schlüssel (serverseitig)." },
|
||||
{ "id": "d", "text": "`~/.ssh/known_hosts`", "correct": false, "explanation": "Host-Schlüssel bekannter Server." },
|
||||
{ "id": "e", "text": "`/etc/ssh/ssh_host_ed25519_key`", "correct": false, "explanation": "Privater **Host**-Schlüssel des SSH-Servers, nicht des Benutzers." }
|
||||
],
|
||||
"explanation": "Öffentliche und private Schlüssel gibt es auch beim Server (Host-Schlüssel). Benutzer- und Host-Schlüssel nicht verwechseln.",
|
||||
"references": ["man 1 ssh-keygen"]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"$id": "questions.schema.json",
|
||||
"title": "LPIC-Simulator: Fragendatei",
|
||||
"description": "Fragenpaket für eine Prüfung. Datei-Header in 'meta' (Regel headers.md). Fachliche Regeln (genau 5 Optionen, Erläuterung je Option) prüft zusätzlich tools/build_bank.py.",
|
||||
"type": "object",
|
||||
"required": ["format", "exam", "questions"],
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"$schema": { "type": "string" },
|
||||
"format": { "const": "lpic-sim-questions" },
|
||||
"meta": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"file": { "type": "string" },
|
||||
"brief": { "type": "string" },
|
||||
"author": { "type": "string" },
|
||||
"owner": { "type": "string" },
|
||||
"assist": { "type": "string" },
|
||||
"version": { "type": "string" }
|
||||
}
|
||||
},
|
||||
"exam": { "type": "string", "description": "id der Prüfung aus exam.json, z. B. lpic1-101", "pattern": "^[a-z0-9-]+$" },
|
||||
"questions": { "type": "array", "items": { "$ref": "#/$defs/question" } }
|
||||
},
|
||||
"$defs": {
|
||||
"question": {
|
||||
"type": "object",
|
||||
"required": ["id", "objective", "type", "question", "explanation"],
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"id": { "type": "string", "pattern": "^[0-9]{3}\\.[0-9]+-[0-9]{3}$", "description": "<objective>-<laufende Nr.>, z. B. 103.1-004" },
|
||||
"objective": { "type": "string", "pattern": "^[0-9]{3}\\.[0-9]+$" },
|
||||
"type": { "enum": ["single", "multi", "fill"], "description": "single: genau 1 richtig · multi: 2–4 richtig (Anzahl wird angezeigt) · fill: Lückentext" },
|
||||
"lang": { "type": "string", "default": "de" },
|
||||
"difficulty": { "type": "integer", "minimum": 1, "maximum": 3 },
|
||||
"question": { "type": "string", "description": "Fragetext; `Inline-Code` in Backticks, **fett** möglich" },
|
||||
"code": { "type": "string", "description": "optionaler mehrzeiliger Block (Ausgabe, Datei, Befehl)" },
|
||||
"options": {
|
||||
"type": "array",
|
||||
"minItems": 5,
|
||||
"maxItems": 5,
|
||||
"items": {
|
||||
"type": "object",
|
||||
"required": ["id", "text", "correct", "explanation"],
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"id": { "enum": ["a", "b", "c", "d", "e"] },
|
||||
"text": { "type": "string" },
|
||||
"correct": { "type": "boolean" },
|
||||
"explanation": { "type": "string", "description": "Warum diese Option richtig bzw. falsch ist" }
|
||||
}
|
||||
}
|
||||
},
|
||||
"answers": { "type": "array", "minItems": 1, "items": { "type": "string" }, "description": "fill: akzeptierte Antworten (erste = Musterlösung)" },
|
||||
"case_sensitive": { "type": "boolean", "default": true },
|
||||
"explanation": { "type": "string", "description": "Gesamterläuterung zur Frage" },
|
||||
"references": { "type": "array", "items": { "type": "string" }, "description": "z. B. man 8 modprobe" },
|
||||
"review": {
|
||||
"type": "object",
|
||||
"description": "Prüfvermerk. Fehlt er, gilt pending. rejected wird nie in die Bank übernommen, mit --approved-only nur approved.",
|
||||
"required": ["status"],
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"status": { "enum": ["pending", "approved", "rejected"] },
|
||||
"by": { "type": "string", "description": "Prüfer (Name oder Rolle)" },
|
||||
"date": { "type": "string", "pattern": "^\\d{4}-\\d{2}-\\d{2}$" },
|
||||
"note": { "type": "string", "description": "Begründung, v. a. bei rejected" }
|
||||
}
|
||||
}
|
||||
},
|
||||
"allOf": [
|
||||
{ "if": { "properties": { "type": { "const": "fill" } } }, "then": { "required": ["answers"] }, "else": { "required": ["options"] } }
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
# Anleitung für Prüfer
|
||||
|
||||
> Stand: 29.10.2026 · Ziel: jede Frage fachlich prüfen und freigeben oder ablehnen.
|
||||
|
||||
## Voraussetzungen
|
||||
- Python ≥ 3.11 (nur Standardbibliothek), ein aktueller Browser (Firefox, Chromium).
|
||||
- Editor mit JSON-Schema-Unterstützung (z. B. VSCode/VSCodium) – validiert beim Tippen über `$schema`.
|
||||
|
||||
## Ablauf
|
||||
1. Paket entpacken, `index.html` öffnen und die Fragen im **Lernmodus** durchgehen (Topic-Filter auf der Startseite, Reihenfolge „nach Objective“, Optionen nicht mischen).
|
||||
2. Fragen im Quelltext `data/<prüfung>/questions_<topic>.json` prüfen – Regeln: [question_rules.md](question_rules.md), Prüfungsrahmen: [exam_format.md](exam_format.md).
|
||||
3. Ergebnis je Frage als Vermerk eintragen:
|
||||
```json
|
||||
"review": { "status": "approved", "by": "<Name oder Rolle>", "date": "2026-11-02" }
|
||||
"review": { "status": "rejected", "by": "<Name oder Rolle>", "date": "2026-11-02", "note": "Option ‚…‘ ist ebenfalls korrekt" }
|
||||
```
|
||||
Korrekturen direkt in der Frage vornehmen und `meta.version` der Datei erhöhen.
|
||||
4. Prüfen und bauen:
|
||||
```bash
|
||||
python3 tools/build_bank.py -c # Regeln, Prüfstatus, Abdeckung je Objective
|
||||
python3 tools/build_bank.py # js/bank.js neu (verschlüsselt, eigener PSK in tools/.psk)
|
||||
python3 tools/build_bank.py --approved-only -e # nur Freigegebenes, Export nach dist/
|
||||
python3 -m http.server 8080 -d dist # Export ausliefern
|
||||
```
|
||||
5. Geänderte `questions_*.json` zurückgeben (nicht `js/bank.js`, nicht `tools/.psk`).
|
||||
|
||||
## Neue Fragen
|
||||
Nächste freie Nummer im Objective verwenden (`<objective>-<NNN>`), Format siehe [`../README.md`](../README.md#format-einer-frage). Ohne Build testen: Datei im Simulator über **Import** laden.
|
||||
|
||||
## Tests
|
||||
```bash
|
||||
chromium --headless --allow-file-access-from-files --virtual-time-budget=5000 --dump-dom tests/selftest.html | grep -o 'RESULT[^<]*'
|
||||
chromium --headless --allow-file-access-from-files --virtual-time-budget=40000 --dump-dom tests/ui_smoke.html | grep -oE '(FAIL|RESULT)[^<]*'
|
||||
```
|
||||
@@ -0,0 +1,28 @@
|
||||
# Festlegungen
|
||||
|
||||
> Stand: 29.10.2026 · getroffene Entscheidungen mit Begründung und offene Punkte.
|
||||
|
||||
## Entscheidungen
|
||||
| Datum | Entscheidung | Warum |
|
||||
|---|---|---|
|
||||
| 29.10.2026 | Zunächst LPIC-1 (101-500, 102-500), Objectives 5.0; weitere Prüfungen (LPIC-2) als zusätzliche Ordner unter `data/` | Prüfungen auswählbar, erweiterbar ohne Codeänderung |
|
||||
| 29.10.2026 | Statische Web-Anwendung, läuft von `file://` **und** von jedem statischen Webserver; kein Backend | lokal ohne Installation nutzbar |
|
||||
| 29.10.2026 | reveal.js 5.1.0 unverändert unter `vendor/reveal/`, eingebautes **Dracula**-Theme; kein CDN | offline lauffähig, reproduzierbar |
|
||||
| 29.10.2026 | Reines JavaScript (klassische Scripts, ES2022), kein TypeScript, kein Build für den Code | ES-Module und `fetch` sind unter `file://` blockiert; kein node nötig |
|
||||
| 29.10.2026 | Fragen-Quelltext als JSON unter `data/` (Klartext), Bank `js/bank.js` per `tools/build_bank.py` (nur Python-Standardbibliothek) | JSON-Schema-Prüfung im Editor; Script-Datei lädt auch unter `file://` |
|
||||
| 29.10.2026 | Fragen: `single`/`multi` mit **genau 5 Optionen**, Erläuterung je Option, Gesamterläuterung; `fill` für Lückentext | Vorgabe des Betreibers; LPI-Fragetypen |
|
||||
| 29.10.2026 | Lernmodus: „Weiter“ zeigt erst die Auflösung mit Erläuterungen, dann die nächste Frage | Lernen mit sofortiger Begründung |
|
||||
| 29.10.2026 | Prüfungsmodus: 60 Fragen, 90 min, Auswahl gewichtet nach Objectives, Auflösung nach Abgabe | entspricht dem LPI-Format ([exam_format.md](exam_format.md)) |
|
||||
| 29.10.2026 | Bewertung alles-oder-nichts; Richtwert Bestehen 65 % (`pass_percent` in `exam.json`) | LPI veröffentlicht weder Teilpunkte noch Prozentgrenze (Skala 200–800, bestanden ab 500) – **Annahme** |
|
||||
| 29.10.2026 | Fortschritt automatisch im `localStorage`; Export/Import der Sitzung als JSON (enthält die Fragen der Sitzung); Fragenpakete per Import nachladbar | Pause und Fortsetzen, auch auf anderem Rechner |
|
||||
| 29.10.2026 | Ausgelieferte Bank ChaCha20-verschlüsselt (RFC 8439); Schlüssel = SHA-256 über einen PSK (`LPIC_SIM_PSK` bzw. `tools/.psk`, nicht weitergeben), steht als `build`-Kennung in `bank.js` | kein Klartext auf der Platte; **Verschleierung**, kein Schutz gegen gezielte Analyse, da der Browser den Schlüssel braucht |
|
||||
| 29.10.2026 | Eigene ChaCha20-Implementierung statt WebCrypto | WebCrypto fehlt bei `http://<IP>` (kein Secure Context) und kennt kein ChaCha20 |
|
||||
| 29.10.2026 | Prüfvermerk `review` je Frage; `rejected` nie in der Bank, `--approved-only` für Freigabe-Builds | Durchsicht und Freigabe durch Prüfer |
|
||||
| 29.10.2026 | Export `build_bank.py -e` nur mit Laufzeitdateien (ohne `data/`, `tools/`, `tests/`) | Auslieferung an Lernende ohne Quelltext |
|
||||
|
||||
## Offene Punkte
|
||||
1. Richtwert 65 % für „bestanden“ – anpassen?
|
||||
2. Teilpunkte bei Mehrfachauswahl: von LPI nicht dokumentiert; derzeit alles-oder-nichts.
|
||||
3. Sitzungs-Export und `localStorage` enthalten die Fragen einer Sitzung im Klartext.
|
||||
4. Alle 180 Fragen sind KI-erstellt (Claude Opus 5.5) und **noch nicht fachlich geprüft** – Aufgabe der Prüfer ([REVIEW.md](REVIEW.md)).
|
||||
5. Nicht getestet: Safari, echte Mobilgeräte.
|
||||
@@ -0,0 +1,38 @@
|
||||
# LPIC-1 – Prüfungsformat und Objectives 5.0
|
||||
|
||||
> Stand: 29.10.2026 · Alle Angaben von lpi.org, abgerufen am 29.10.2026. Grundlage für Prüfungsmodus und Fragenbank.
|
||||
> Inoffizielles Lernwerkzeug, nicht mit dem Linux Professional Institute (LPI) verbunden. „LPIC“ ist eine Marke des LPI.
|
||||
|
||||
## Fakten
|
||||
| Merkmal | Wert | Quelle |
|
||||
|---|---|---|
|
||||
| Prüfungen | 101-500 und 102-500 | Objectives-Seite |
|
||||
| Objectives-Version | 5.0 | Objectives-Seite |
|
||||
| Umfang je Prüfung | 60 Fragen, 90 Minuten | LPIC-1-Übersicht („60 multiple-choice and fill-in-the-blank questions“) |
|
||||
| Fragetypen | Multiple Choice (Einfach-/Mehrfachauswahl) und Lückentext | LPIC-1-Übersicht |
|
||||
| Bewertung | Skala 200–800, bestanden ab 500 | FAQ („ranked on a scale from 200 to 800, with a passing score of 500“) |
|
||||
| Prozentgrenze | nicht veröffentlicht; hängt von der Fragenkombination ab (psychometrische Studien, modifizierte Angoff-Studie) | FAQ, Development Standards |
|
||||
| Gewichtung | Gewicht eines Objectives = Anzahl Fragen dazu; Summe je Prüfung = 60 | FAQ, Objectives-Seite |
|
||||
| Teilpunkte bei Mehrfachauswahl | nicht dokumentiert | – |
|
||||
| Angekündigte Änderungen | `ifup`/`ifdown`, net-tools und TCP-Wrapper sollen entfallen (ohne Termin) | Objectives-Seite |
|
||||
|
||||
## Regeln zur Fragenerstellung laut LPI (Development Standards)
|
||||
- Multiple Choice bevorzugt, da „more information per unit time“.
|
||||
- Distraktoren müssen **falsch, aber plausibel** sein.
|
||||
- Formulierung klar, auch für Nicht-Muttersprachler verständlich; verwirrende Items werden abgelehnt oder umformuliert.
|
||||
- Items, die vorhandenen im Wesentlichen gleichen, werden abgelehnt.
|
||||
- Fachleute prüfen Korrektheit, Relevanz und erwartete Schwierigkeit.
|
||||
- Eine feste Anzahl Antwortoptionen nennt LPI **nicht**; die 5 Optionen sind eine Projektvorgabe ([decisions.md](decisions.md)).
|
||||
|
||||
## Objectives 5.0 mit Gewicht
|
||||
**101:** 101.1 (2) · 101.2 (3) · 101.3 (3) · 102.1 (2) · 102.2 (2) · 102.3 (1) · 102.4 (3) · 102.5 (3) · 102.6 (1) · 103.1 (4) · 103.2 (2) · 103.3 (4) · 103.4 (4) · 103.5 (4) · 103.6 (2) · 103.7 (3) · 103.8 (3) · 104.1 (2) · 104.2 (2) · 104.3 (3) · 104.5 (3) · 104.6 (2) · 104.7 (2) – 104.4 entfallen.
|
||||
|
||||
**102:** 105.1 (4) · 105.2 (4) · 106.1 (2) · 106.2 (1) · 106.3 (1) · 107.1 (5) · 107.2 (4) · 107.3 (3) · 108.1 (3) · 108.2 (4) · 108.3 (3) · 108.4 (2) · 109.1 (4) · 109.2 (4) · 109.3 (4) · 109.4 (2) · 110.1 (3) · 110.2 (3) · 110.3 (4).
|
||||
|
||||
Titel und Topics maschinenlesbar: [`../data/lpic1_101/exam.json`](../data/lpic1_101/exam.json), [`../data/lpic1_102/exam.json`](../data/lpic1_102/exam.json).
|
||||
|
||||
## Quellen
|
||||
- https://www.lpi.org/de/exam-101-102-objectives/ (abgerufen 29.10.2026)
|
||||
- https://www.lpi.org/value-of-certification/our-certification-development-standards/ (abgerufen 29.10.2026)
|
||||
- https://www.lpi.org/our-certifications/lpic-1-overview/ (abgerufen 29.10.2026)
|
||||
- https://www.lpi.org/faq/ (abgerufen 29.10.2026)
|
||||
@@ -0,0 +1,35 @@
|
||||
# Regeln für Fragen
|
||||
|
||||
> Stand: 29.10.2026 · gilt für alle Dateien `data/<prüfung>/questions_*.json`.
|
||||
> Technische Regeln prüft `tools/build_bank.py -c` (und der Import im Simulator); inhaltliche Regeln prüfen Menschen.
|
||||
|
||||
## Technisch (automatisch geprüft)
|
||||
| Regel | Details |
|
||||
|---|---|
|
||||
| Datei | `format: "lpic-sim-questions"`, `exam` = id aus `exam.json`, Schema `data/schema/questions.schema.json` |
|
||||
| id | `<objective>-<NNN>`, z. B. `103.4-007`, eindeutig je Prüfung; Objective muss in `exam.json` existieren |
|
||||
| `single` | genau 5 Optionen `a`–`e` in dieser Reihenfolge, genau 1 richtig |
|
||||
| `multi` | genau 5 Optionen, 2–4 richtig; der Simulator zeigt „Wählen Sie ZWEI/DREI/VIER“ und begrenzt die Auswahl |
|
||||
| `fill` | keine Optionen; `answers` = akzeptierte Antworten, erste = Musterlösung; Leerraum wird normalisiert; `case_sensitive` Standard `true` |
|
||||
| Erläuterungen | jede Option hat `explanation`, jede Frage eine Gesamt-`explanation` |
|
||||
| keine doppelten Optionstexte | |
|
||||
| `review.status` | `pending` (Standard), `approved`, `rejected` |
|
||||
|
||||
## Inhaltlich (Prüfer)
|
||||
- **Genau eine fachlich richtige Lösung** (bzw. genau die markierten bei `multi`). Distraktoren eindeutig falsch, aber plausibel – keine „auch richtig, aber nicht gemeint“-Optionen.
|
||||
- Klar und kurz, auch für Nicht-Muttersprachler verständlich; keine Fangfragen über Formulierungstricks, keine doppelten Verneinungen.
|
||||
- Genau einem Objective zugeordnet; Inhalt passt zu dessen Lernzielen (Objectives 5.0, [exam_format.md](exam_format.md)).
|
||||
- Keine inhaltlichen Dubletten.
|
||||
- Erläuterungen begründen **warum** – nicht nur „richtig/falsch“; möglichst mit `references` (Manpage).
|
||||
- Erläuterungen nie auf Buchstaben beziehen („Option A“) – die Optionen werden im Simulator gemischt.
|
||||
- `fill`: alle gleichwertigen Schreibweisen in `answers` aufnehmen (Lang-/Kurzoptionen, Reihenfolge der Optionen).
|
||||
- Bewertung im Simulator wie vermutlich bei LPI: alles oder nichts.
|
||||
|
||||
## Schreibweise
|
||||
- Sprache Deutsch mit korrekten Umlauten (ä ö ü ß); Befehle, Optionen, Pfade im Original.
|
||||
- Inline-Code zwischen zwei Backticks; einzelne Backticks vermeiden (Befehlsersetzung als `$(…)` schreiben). `**fett**` für Betonung.
|
||||
- Mehrzeilige Ausgaben/Dateien ins Feld `code`.
|
||||
- Beispieladressen und -namen aus Dokumentationsbereichen: `192.0.2.0/24`, `198.51.100.0/24`, `example.org`, `host01`, `anna`.
|
||||
|
||||
## Datei-Header
|
||||
Jede Datei trägt im Objekt `meta` (bei Code als Kommentar) `file`, `brief`, `author`, `owner`, `version` (`x.y.z (TT.MM.JJJJ)`), bei KI-Beteiligung `assist`. Bei inhaltlicher Änderung `version` erhöhen.
|
||||
@@ -0,0 +1,45 @@
|
||||
<!doctype html>
|
||||
<!--
|
||||
@file index.html
|
||||
@brief Einstieg des LPIC-Prüfungssimulators (reveal.js, Dracula); läuft von file:// und vom Webserver
|
||||
@author Björn Runden <br@runden.de>
|
||||
@owner Björn Runden <br@runden.de>
|
||||
@assist KI-unterstützt (Claude Opus 5.5), geprüft von Björn Runden <br@runden.de>
|
||||
@version 0.1.0 (29.10.2026)
|
||||
-->
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>LPIC-Prüfungssimulator</title>
|
||||
<link rel="stylesheet" href="vendor/reveal/reset.css">
|
||||
<link rel="stylesheet" href="vendor/reveal/reveal.css">
|
||||
<link rel="stylesheet" href="vendor/reveal/theme/dracula.css">
|
||||
<link rel="stylesheet" href="css/simulator.css">
|
||||
</head>
|
||||
<body>
|
||||
<header class="toolbar">
|
||||
<span id="tb-title" class="tb-title">LPIC-Prüfungssimulator</span>
|
||||
<span id="tb-progress" class="tb-info"></span>
|
||||
<span id="tb-timer" class="tb-info"></span>
|
||||
<span class="tb-buttons">
|
||||
<button class="btn small" data-act="prev" title="Zurück (←)">◀</button>
|
||||
<button class="btn small" data-act="next" title="Weiter (→ / Leertaste)">▶</button>
|
||||
<button class="btn small" id="tb-export" title="Sitzung als JSON speichern">Export</button>
|
||||
<button class="btn small" data-act="import" title="Sitzung fortsetzen oder Fragenpaket laden (JSON)">Import</button>
|
||||
<button class="btn small" data-act="menu" title="Zur Startseite (Sitzung bleibt gespeichert)">Menü</button>
|
||||
</span>
|
||||
<input type="file" id="import-file" accept=".json,application/json" hidden>
|
||||
</header>
|
||||
|
||||
<div class="reveal">
|
||||
<div class="slides"></div>
|
||||
</div>
|
||||
|
||||
<div id="toast" class="toast-box" aria-live="polite"></div>
|
||||
|
||||
<script src="vendor/reveal/reveal.js"></script>
|
||||
<script src="js/bank.js"></script>
|
||||
<script src="js/app.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,883 @@
|
||||
/**
|
||||
* @file app.js
|
||||
* @brief LPIC-Prüfungssimulator: Sitzungen, Bewertung, Speichern/Import und Darstellung mit reveal.js
|
||||
* @author Björn Runden <br@runden.de>
|
||||
* @owner Björn Runden <br@runden.de>
|
||||
* @assist KI-unterstützt (Claude Opus 5.5), geprüft von Björn Runden <br@runden.de>
|
||||
* @version 0.2.0 (29.10.2026)
|
||||
*
|
||||
* Bewusst klassisches Script (kein ES-Modul, kein fetch): läuft so auch direkt von file://.
|
||||
* Die Fragenbank kommt verschlüsselt aus js/bank.js (window.LPIC_BANK, ChaCha20), weitere Pakete per Import.
|
||||
* Reine Logik liegt in window.LPICSim.core und wird von tests/selftest.html geprüft.
|
||||
*/
|
||||
(function () {
|
||||
'use strict';
|
||||
|
||||
const STORE_SESSION = 'lpic_sim.session';
|
||||
const STORE_PACKS = 'lpic_sim.packs';
|
||||
const SESSION_FORMAT = 'lpic-sim-session';
|
||||
const PACK_FORMAT = 'lpic-sim-questions';
|
||||
const OPTION_IDS = ['a', 'b', 'c', 'd', 'e'];
|
||||
const LETTERS = 'ABCDE';
|
||||
const COUNT_WORDS = { 2: 'ZWEI', 3: 'DREI', 4: 'VIER' };
|
||||
const SAVE_EVERY_S = 15;
|
||||
|
||||
/* ------------------------------------------------------------------ Speicher */
|
||||
|
||||
/** localStorage mit Fehlertoleranz (privater Modus, gesperrter Speicher). */
|
||||
const store = {
|
||||
get(key) {
|
||||
try { const v = localStorage.getItem(key); return v ? JSON.parse(v) : null; }
|
||||
catch (e) { console.warn('localStorage nicht lesbar:', e); return null; }
|
||||
},
|
||||
set(key, value) {
|
||||
try { localStorage.setItem(key, JSON.stringify(value)); return true; }
|
||||
catch (e) { console.warn('localStorage nicht schreibbar:', e); return false; }
|
||||
},
|
||||
del(key) {
|
||||
try { localStorage.removeItem(key); } catch (e) { console.warn('localStorage:', e); }
|
||||
},
|
||||
};
|
||||
|
||||
/* ------------------------------------------------------------------ Hilfen */
|
||||
|
||||
/** Mischt eine Kopie des Arrays (Fisher-Yates). */
|
||||
function shuffled(arr) {
|
||||
const a = arr.slice();
|
||||
for (let i = a.length - 1; i > 0; i--) {
|
||||
const j = Math.floor(Math.random() * (i + 1));
|
||||
[a[i], a[j]] = [a[j], a[i]];
|
||||
}
|
||||
return a;
|
||||
}
|
||||
|
||||
/** Vergleicht Objective-IDs numerisch („103.10“ nach „103.9“). */
|
||||
function cmpObjective(a, b) {
|
||||
const [a1, a2] = a.split('.').map(Number);
|
||||
const [b1, b2] = b.split('.').map(Number);
|
||||
return a1 - b1 || a2 - b2;
|
||||
}
|
||||
|
||||
function esc(s) {
|
||||
return String(s).replace(/[&<>"']/g, (c) => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c]));
|
||||
}
|
||||
|
||||
/** Mini-Markup für Fragetexte: `Code`, **fett**, Zeilenumbruch. Returns: sicheres HTML. */
|
||||
function fmt(text) {
|
||||
return String(text ?? '').split(/(`[^`]+`)/).map((part) =>
|
||||
/^`[^`]+`$/.test(part)
|
||||
? `<code>${esc(part.slice(1, -1))}</code>`
|
||||
: esc(part).replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>').replace(/\n/g, '<br>')
|
||||
).join('');
|
||||
}
|
||||
|
||||
function fmtTime(sec) {
|
||||
const s = Math.max(0, Math.floor(sec));
|
||||
const h = Math.floor(s / 3600), m = Math.floor((s % 3600) / 60), r = s % 60;
|
||||
const mm = String(m).padStart(2, '0'), ss = String(r).padStart(2, '0');
|
||||
return h ? `${h}:${mm}:${ss}` : `${mm}:${ss}`;
|
||||
}
|
||||
|
||||
/** Normalisiert Freitext: Leerraum an den Rändern weg, innen auf ein Leerzeichen. */
|
||||
function normalize(s, caseSensitive) {
|
||||
const n = String(s ?? '').trim().replace(/\s+/g, ' ');
|
||||
return caseSensitive === false ? n.toLowerCase() : n;
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ Fragenbank entschlüsseln */
|
||||
|
||||
/**
|
||||
* ChaCha20 nach RFC 8439 (Gegenstück zu tools/build_bank.py). Eigene Implementierung, weil
|
||||
* WebCrypto kein ChaCha20 kennt und über http://<LAN-IP> (kein Secure Context) ganz fehlt.
|
||||
* @param {Uint8Array} key 32 Byte · @param {Uint8Array} nonce 12 Byte · @param {Uint8Array} data
|
||||
* @returns {Uint8Array} ver-/entschlüsselte Daten
|
||||
*/
|
||||
function chacha20(key, nonce, data, counter = 1) {
|
||||
const kv = new DataView(key.buffer, key.byteOffset, 32);
|
||||
const nv = new DataView(nonce.buffer, nonce.byteOffset, 12);
|
||||
const st = new Uint32Array(16);
|
||||
st.set([0x61707865, 0x3320646e, 0x79622d32, 0x6b206574]);
|
||||
for (let i = 0; i < 8; i++) st[4 + i] = kv.getUint32(i * 4, true);
|
||||
for (let i = 0; i < 3; i++) st[13 + i] = nv.getUint32(i * 4, true);
|
||||
const x = new Uint32Array(16);
|
||||
const block = new Uint8Array(64);
|
||||
const bv = new DataView(block.buffer);
|
||||
const out = new Uint8Array(data.length);
|
||||
const qr = (a, b, c, d) => {
|
||||
x[a] += x[b]; x[d] ^= x[a]; x[d] = (x[d] << 16) | (x[d] >>> 16);
|
||||
x[c] += x[d]; x[b] ^= x[c]; x[b] = (x[b] << 12) | (x[b] >>> 20);
|
||||
x[a] += x[b]; x[d] ^= x[a]; x[d] = (x[d] << 8) | (x[d] >>> 24);
|
||||
x[c] += x[d]; x[b] ^= x[c]; x[b] = (x[b] << 7) | (x[b] >>> 25);
|
||||
};
|
||||
for (let off = 0; off < data.length; off += 64) {
|
||||
st[12] = counter + off / 64;
|
||||
x.set(st);
|
||||
for (let r = 0; r < 10; r++) {
|
||||
qr(0, 4, 8, 12); qr(1, 5, 9, 13); qr(2, 6, 10, 14); qr(3, 7, 11, 15);
|
||||
qr(0, 5, 10, 15); qr(1, 6, 11, 12); qr(2, 7, 8, 13); qr(3, 4, 9, 14);
|
||||
}
|
||||
for (let i = 0; i < 16; i++) bv.setUint32(i * 4, (x[i] + st[i]) >>> 0, true);
|
||||
const n = Math.min(64, data.length - off);
|
||||
for (let i = 0; i < n; i++) out[off + i] = data[off + i] ^ block[i];
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
const hexBytes = (h) => new Uint8Array(String(h).match(/../g).map((b) => parseInt(b, 16)));
|
||||
|
||||
/** Entschlüsselt window.LPIC_BANK (Felder build, rev, payload). Returns: {exams} oder wirft Error. */
|
||||
function decodeBank(raw) {
|
||||
if (!raw) return { exams: [] };
|
||||
if (!raw.payload) return raw; // unverschlüsselte Bank (ältere Builds)
|
||||
const bin = atob(raw.payload);
|
||||
const data = new Uint8Array(bin.length);
|
||||
for (let i = 0; i < bin.length; i++) data[i] = bin.charCodeAt(i);
|
||||
const plain = chacha20(hexBytes(raw.build), hexBytes(raw.rev), data);
|
||||
try { return JSON.parse(new TextDecoder().decode(plain)); }
|
||||
catch (e) { throw new Error('Fragenbank nicht lesbar (bank.js beschädigt oder unvollständig)'); }
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ Kernlogik */
|
||||
|
||||
/** Prüft eine Frage gegen die Erstellungsregeln. Returns: Fehlertext oder null. */
|
||||
function validateQuestion(q, objectives) {
|
||||
if (!q || typeof q !== 'object') return 'keine Frage';
|
||||
if (!/^\d{3}\.\d+-\d{3}$/.test(q.id || '')) return `ungültige id ${q.id}`;
|
||||
if (!objectives.has(q.objective)) return `${q.id}: unbekanntes Objective ${q.objective}`;
|
||||
if (!String(q.question || '').trim() || !String(q.explanation || '').trim()) return `${q.id}: question/explanation fehlt`;
|
||||
if (q.type === 'fill') {
|
||||
return Array.isArray(q.answers) && q.answers.length ? null : `${q.id}: answers fehlt`;
|
||||
}
|
||||
if (q.type !== 'single' && q.type !== 'multi') return `${q.id}: unbekannter type ${q.type}`;
|
||||
const opts = q.options || [];
|
||||
if (opts.map((o) => o.id).join() !== OPTION_IDS.join()) return `${q.id}: genau 5 Optionen a–e nötig`;
|
||||
if (opts.some((o) => !String(o.text || '').trim() || !String(o.explanation || '').trim())) return `${q.id}: Option ohne Text/Erläuterung`;
|
||||
const nOk = opts.filter((o) => o.correct === true).length;
|
||||
if (q.type === 'single' && nOk !== 1) return `${q.id}: single braucht genau 1 richtige Option`;
|
||||
if (q.type === 'multi' && (nOk < 2 || nOk > 4)) return `${q.id}: multi braucht 2–4 richtige Optionen`;
|
||||
return null;
|
||||
}
|
||||
|
||||
/** Alle Objective-IDs einer Prüfung. */
|
||||
function objectiveIds(exam) {
|
||||
return new Set(exam.topics.flatMap((t) => t.objectives.map((o) => o.id)));
|
||||
}
|
||||
|
||||
/** Objective-Metadaten (Titel, Gewicht, Topic) zu einer ID. */
|
||||
function objectiveInfo(exam, oid) {
|
||||
for (const t of exam.topics) {
|
||||
const o = t.objectives.find((x) => x.id === oid);
|
||||
if (o) return { ...o, topic: t.id, topicTitle: t.title };
|
||||
}
|
||||
return { id: oid, title: '', weight: 0, topic: oid.split('.')[0], topicTitle: '' };
|
||||
}
|
||||
|
||||
/**
|
||||
* Fügt ein Fragenpaket in die Bank ein (gleiche id ersetzt).
|
||||
* Ein Paket mit "definition" (Inhalt einer exam.json) legt eine neue Prüfung an.
|
||||
* Returns: {added, replaced, errors}
|
||||
*/
|
||||
function mergePack(bankExams, pack) {
|
||||
const res = { added: 0, replaced: 0, errors: [] };
|
||||
if (!pack || pack.format !== PACK_FORMAT) { res.errors.push(`format muss '${PACK_FORMAT}' sein`); return res; }
|
||||
let exam = bankExams.find((e) => e.id === pack.exam);
|
||||
if (!exam && pack.definition && pack.definition.exam && pack.definition.exam.id === pack.exam) {
|
||||
exam = { ...pack.definition.exam, topics: pack.definition.topics || [], bank: [] };
|
||||
bankExams.push(exam);
|
||||
}
|
||||
if (!exam) { res.errors.push(`Prüfung '${pack.exam}' unbekannt (Paket ohne "definition")`); return res; }
|
||||
const objectives = objectiveIds(exam);
|
||||
for (const q of pack.questions || []) {
|
||||
if (q && q.review && q.review.status === 'rejected') continue; // von Prüfern abgelehnt
|
||||
const err = validateQuestion(q, objectives);
|
||||
if (err) { res.errors.push(err); continue; }
|
||||
const idx = exam.bank.findIndex((x) => x.id === q.id);
|
||||
if (idx >= 0) { exam.bank[idx] = q; res.replaced++; } else { exam.bank.push(q); res.added++; }
|
||||
}
|
||||
exam.bank.sort((a, b) => cmpObjective(a.objective, b.objective) || a.id.localeCompare(b.id));
|
||||
return res;
|
||||
}
|
||||
|
||||
/**
|
||||
* Wählt n Fragen gewichtet nach Objective (wie die echte Prüfung: Gewicht = Fragenzahl).
|
||||
* Fehlende Fragen eines Objectives werden zufällig aus dem Rest aufgefüllt.
|
||||
*/
|
||||
function pickWeighted(exam, pool, n) {
|
||||
const byObj = new Map();
|
||||
for (const q of shuffled(pool)) {
|
||||
if (!byObj.has(q.objective)) byObj.set(q.objective, []);
|
||||
byObj.get(q.objective).push(q);
|
||||
}
|
||||
const objs = [...byObj.keys()].map((id) => ({ id, w: objectiveInfo(exam, id).weight || 1 }));
|
||||
const total = objs.reduce((s, o) => s + o.w, 0) || 1;
|
||||
const target = Math.min(n, pool.length);
|
||||
for (const o of objs) { o.exact = (target * o.w) / total; o.quota = Math.min(Math.floor(o.exact), byObj.get(o.id).length); }
|
||||
let left = target - objs.reduce((s, o) => s + o.quota, 0);
|
||||
for (const o of objs.slice().sort((a, b) => (b.exact - Math.floor(b.exact)) - (a.exact - Math.floor(a.exact)))) {
|
||||
if (left <= 0) break;
|
||||
if (o.quota < byObj.get(o.id).length) { o.quota++; left--; }
|
||||
}
|
||||
const picked = objs.flatMap((o) => byObj.get(o.id).slice(0, o.quota));
|
||||
if (picked.length < target) {
|
||||
const ids = new Set(picked.map((q) => q.id));
|
||||
picked.push(...shuffled(pool.filter((q) => !ids.has(q.id))).slice(0, target - picked.length));
|
||||
}
|
||||
return picked;
|
||||
}
|
||||
|
||||
/**
|
||||
* Legt eine neue Sitzung an. Die Fragen werden als Kopie in die Sitzung übernommen,
|
||||
* damit Export/Import unabhängig vom späteren Stand der Bank funktioniert.
|
||||
* @param {object} exam Prüfung aus der Bank
|
||||
* @param {object} o {mode:'learn'|'exam', topics?:string[], count?:number, order?:'random'|'objective', shuffleOptions?:bool, questionIds?:string[]}
|
||||
*/
|
||||
function createSession(exam, o) {
|
||||
const mode = o.mode === 'exam' ? 'exam' : 'learn';
|
||||
let pool = exam.bank;
|
||||
if (o.questionIds) pool = pool.filter((q) => o.questionIds.includes(q.id));
|
||||
else if (mode === 'learn' && o.topics && o.topics.length) pool = pool.filter((q) => o.topics.includes(q.objective.split('.')[0]));
|
||||
let list;
|
||||
if (mode === 'exam') {
|
||||
list = shuffled(pickWeighted(exam, pool, exam.questions || 60));
|
||||
} else {
|
||||
list = o.order === 'objective'
|
||||
? pool.slice().sort((a, b) => cmpObjective(a.objective, b.objective) || a.id.localeCompare(b.id))
|
||||
: shuffled(pool);
|
||||
if (o.count > 0) list = list.slice(0, o.count);
|
||||
}
|
||||
const questions = list.map((q) => {
|
||||
const c = JSON.parse(JSON.stringify(q));
|
||||
if (c.options && o.shuffleOptions !== false) c.options = shuffled(c.options);
|
||||
return c;
|
||||
});
|
||||
const now = new Date().toISOString();
|
||||
return {
|
||||
format: SESSION_FORMAT,
|
||||
version: 1,
|
||||
id: Date.now().toString(36),
|
||||
exam_id: exam.id,
|
||||
exam_title: exam.title,
|
||||
exam_code: exam.code,
|
||||
topics: exam.topics,
|
||||
mode,
|
||||
created: now,
|
||||
updated: now,
|
||||
elapsed_s: 0,
|
||||
time_limit_s: mode === 'exam' ? (exam.duration_min || 90) * 60 : 0,
|
||||
pass_percent: exam.pass_percent || 65,
|
||||
current: 0,
|
||||
submitted: false,
|
||||
questions,
|
||||
answers: {},
|
||||
};
|
||||
}
|
||||
|
||||
/** Anzahl richtiger Optionen einer Choice-Frage. */
|
||||
function correctCount(q) {
|
||||
return (q.options || []).filter((o) => o.correct).length;
|
||||
}
|
||||
|
||||
/** Bewertet eine Antwort (alles oder nichts, wie LPI). Returns: true/false. */
|
||||
function isCorrect(q, value) {
|
||||
if (q.type === 'fill') {
|
||||
const v = normalize(value, q.case_sensitive);
|
||||
return v !== '' && q.answers.some((a) => normalize(a, q.case_sensitive) === v);
|
||||
}
|
||||
const sel = new Set(Array.isArray(value) ? value : value ? [value] : []);
|
||||
const ok = q.options.filter((o) => o.correct).map((o) => o.id);
|
||||
return sel.size === ok.length && ok.every((id) => sel.has(id));
|
||||
}
|
||||
|
||||
/** Hat die Frage eine (nicht leere) Antwort? */
|
||||
function isAnswered(a) {
|
||||
if (!a) return false;
|
||||
return Array.isArray(a.value) ? a.value.length > 0 : normalize(a.value) !== '';
|
||||
}
|
||||
|
||||
/** Prüft eine importierte Sitzung grob. Returns: Fehlertext oder null. */
|
||||
function validateSession(s) {
|
||||
if (!s || s.format !== SESSION_FORMAT) return `format muss '${SESSION_FORMAT}' sein`;
|
||||
if (s.version !== 1) return `Sitzungsversion ${s.version} wird nicht unterstützt`;
|
||||
if (!Array.isArray(s.questions) || !s.questions.length) return 'Sitzung ohne Fragen';
|
||||
if (typeof s.answers !== 'object' || s.answers === null) return 'Sitzung ohne answers';
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Auswertung einer Sitzung, gesamt und je Topic.
|
||||
* @param {boolean} onlyRevealed im Lernmodus nur bereits aufgelöste Fragen zählen
|
||||
*/
|
||||
function score(session, onlyRevealed) {
|
||||
const res = { total: 0, correct: 0, answered: 0, wrong: [], topics: {} };
|
||||
session.questions.forEach((q, i) => {
|
||||
const a = session.answers[q.id];
|
||||
if (onlyRevealed && !(a && a.revealed)) return;
|
||||
const t = q.objective.split('.')[0];
|
||||
const ts = res.topics[t] || (res.topics[t] = { total: 0, correct: 0 });
|
||||
const ok = !!a && isCorrect(q, a.value);
|
||||
res.total++; ts.total++;
|
||||
if (isAnswered(a)) res.answered++;
|
||||
if (ok) { res.correct++; ts.correct++; } else res.wrong.push(i);
|
||||
});
|
||||
res.percent = res.total ? Math.round((res.correct / res.total) * 100) : 0;
|
||||
res.passed = res.percent >= session.pass_percent;
|
||||
return res;
|
||||
}
|
||||
|
||||
const core = { chacha20, decodeBank, validateQuestion, mergePack, pickWeighted, createSession, isCorrect, isAnswered, validateSession, score, fmt, normalize, shuffled };
|
||||
|
||||
/* ------------------------------------------------------------------ Oberfläche */
|
||||
|
||||
const ui = {
|
||||
exams: [], // Bank inkl. importierter Pakete
|
||||
session: null, // laufende Sitzung oder null (Startseite)
|
||||
tickCount: 0,
|
||||
};
|
||||
|
||||
const $ = (sel, root = document) => root.querySelector(sel);
|
||||
const $$ = (sel, root = document) => Array.from(root.querySelectorAll(sel));
|
||||
|
||||
/** Lädt die Bank aus bank.js und gespeicherte Pakete aus localStorage. */
|
||||
function loadBank() {
|
||||
try { ui.exams = decodeBank(window.LPIC_BANK).exams || []; }
|
||||
catch (e) { ui.exams = []; console.error(e); setTimeout(() => toast(e.message, 'error'), 0); }
|
||||
for (const pack of store.get(STORE_PACKS) || []) mergePack(ui.exams, pack);
|
||||
ui.exams.sort((a, b) => String(a.level).localeCompare(String(b.level)) || (a.order || 0) - (b.order || 0));
|
||||
}
|
||||
|
||||
function save() {
|
||||
if (!ui.session) return;
|
||||
ui.session.updated = new Date().toISOString();
|
||||
if (!store.set(STORE_SESSION, ui.session)) toast('Speichern im Browser nicht möglich – bitte regelmäßig exportieren.', 'warn');
|
||||
}
|
||||
|
||||
function toast(msg, kind = 'info') {
|
||||
const box = $('#toast');
|
||||
const el = document.createElement('div');
|
||||
el.className = `toast ${kind}`;
|
||||
el.textContent = msg;
|
||||
box.appendChild(el);
|
||||
setTimeout(() => el.remove(), kind === 'error' ? 9000 : 4500);
|
||||
}
|
||||
|
||||
/** Bietet Text als Datei zum Speichern an. */
|
||||
function download(name, text) {
|
||||
const url = URL.createObjectURL(new Blob([text], { type: 'application/json' }));
|
||||
const a = document.createElement('a');
|
||||
a.href = url; a.download = name;
|
||||
document.body.appendChild(a); a.click(); a.remove();
|
||||
setTimeout(() => URL.revokeObjectURL(url), 1000);
|
||||
}
|
||||
|
||||
/** Setzt die Folien neu und synchronisiert reveal.js. */
|
||||
function setSlides(html, index) {
|
||||
$('.reveal .slides').innerHTML = html;
|
||||
Reveal.sync();
|
||||
Reveal.slide(index || 0, 0);
|
||||
updateToolbar();
|
||||
}
|
||||
|
||||
/* ----------------------------- Startseite */
|
||||
|
||||
function renderStart() {
|
||||
ui.session = null;
|
||||
const saved = store.get(STORE_SESSION);
|
||||
const levels = [...new Set(ui.exams.map((e) => e.level))];
|
||||
const examChoices = levels.map((lvl) => `
|
||||
<div class="exam-group"><span class="exam-level">${esc(lvl)}</span>
|
||||
${ui.exams.filter((e) => e.level === lvl).map((e, i) => `
|
||||
<label class="choice"><input type="radio" name="exam" value="${esc(e.id)}"${ui.exams.indexOf(e) === 0 && i === 0 ? ' checked' : ''}>
|
||||
<span>${esc(e.title)} <small>${esc(e.code || '')} · ${e.bank.length} Fragen</small></span></label>`).join('')}
|
||||
</div>`).join('');
|
||||
let resume = '';
|
||||
if (saved && !validateSession(saved)) {
|
||||
const done = saved.questions.filter((q) => isAnswered(saved.answers[q.id])).length;
|
||||
resume = `
|
||||
<div class="card resume">
|
||||
<h3>Gespeicherte Sitzung</h3>
|
||||
<p>${esc(saved.exam_title)} · ${saved.mode === 'exam' ? 'Prüfungsmodus' : 'Lernmodus'}${saved.submitted ? ' · abgegeben' : ''}
|
||||
· ${done}/${saved.questions.length} beantwortet · Stand ${esc(new Date(saved.updated).toLocaleString('de-DE'))}</p>
|
||||
<button class="btn primary" data-act="resume">Fortsetzen</button>
|
||||
<button class="btn" data-act="discard">Verwerfen</button>
|
||||
</div>`;
|
||||
}
|
||||
const html = `
|
||||
<section class="start">
|
||||
<h1>LPIC-Prüfungssimulator</h1>
|
||||
${ui.exams.length ? '' : '<p class="warn-text">Keine Fragenbank gefunden – <code>tools/build_bank.py</code> ausführen oder ein Fragenpaket importieren.</p>'}
|
||||
${resume}
|
||||
<form class="card setup" id="setup">
|
||||
<h3>Neue Sitzung</h3>
|
||||
<fieldset><legend>Prüfung</legend>${examChoices}</fieldset>
|
||||
<fieldset><legend>Modus</legend>
|
||||
<label class="choice"><input type="radio" name="mode" value="learn" checked>
|
||||
<span>Lernmodus <small>Weiter (→ / Leertaste) zeigt die Auflösung mit Erläuterung</small></span></label>
|
||||
<label class="choice"><input type="radio" name="mode" value="exam">
|
||||
<span>Prüfungsmodus <small id="exam-info"></small></span></label>
|
||||
</fieldset>
|
||||
<fieldset class="learn-only"><legend>Themen</legend><div id="topic-list"></div></fieldset>
|
||||
<fieldset class="learn-only"><legend>Ablauf</legend>
|
||||
<label class="inline">Anzahl Fragen <input type="number" name="count" min="0" value="0" title="0 = alle"> <small>(0 = alle)</small></label>
|
||||
<label class="inline"><select name="order"><option value="random">zufällige Reihenfolge</option>
|
||||
<option value="objective">nach Objective sortiert</option></select></label>
|
||||
</fieldset>
|
||||
<label class="choice"><input type="checkbox" name="shuffle" checked><span>Antwortoptionen mischen</span></label>
|
||||
<p><button class="btn primary" type="submit">Starten</button></p>
|
||||
</form>
|
||||
<p class="footnote">Objectives laut LPI, Quelle und Version je Prüfung in <code>data/<prüfung>/exam.json</code>.
|
||||
Stand der Fragenbank: ${esc(window.LPIC_BANK ? window.LPIC_BANK.generated : '–')}.
|
||||
Tasten: → / Leertaste weiter · ← zurück · 1–5 Antwort wählen · Enter im Textfeld = weiter.</p>
|
||||
</section>`;
|
||||
setSlides(html, 0);
|
||||
updateStartForm();
|
||||
}
|
||||
|
||||
/** Aktualisiert Themenliste und Hinweise je gewählter Prüfung/Modus. */
|
||||
function updateStartForm() {
|
||||
const form = $('#setup');
|
||||
if (!form) return;
|
||||
const exam = ui.exams.find((e) => e.id === (form.exam && form.exam.value));
|
||||
const learn = form.mode.value === 'learn';
|
||||
$$('.learn-only', form).forEach((el) => { el.hidden = !learn; });
|
||||
if (!exam) return;
|
||||
$('#exam-info').textContent = `${exam.questions} Fragen, ${exam.duration_min} Minuten, gewichtet nach Objectives, Auflösung erst nach Abgabe`;
|
||||
const counts = {};
|
||||
exam.bank.forEach((q) => { const t = q.objective.split('.')[0]; counts[t] = (counts[t] || 0) + 1; });
|
||||
const list = $('#topic-list');
|
||||
if (list.dataset.exam !== exam.id) {
|
||||
list.dataset.exam = exam.id;
|
||||
list.innerHTML = exam.topics.map((t) => `
|
||||
<label class="choice"><input type="checkbox" name="topic" value="${esc(t.id)}" checked>
|
||||
<span>${esc(t.id)} ${esc(t.title)} <small>${counts[t.id] || 0} Fragen</small></span></label>`).join('');
|
||||
}
|
||||
}
|
||||
|
||||
function startFromForm(form) {
|
||||
const exam = ui.exams.find((e) => e.id === form.exam.value);
|
||||
if (!exam) { toast('Bitte eine Prüfung wählen.', 'warn'); return; }
|
||||
const topics = $$('input[name=topic]:checked', form).map((i) => i.value);
|
||||
const s = createSession(exam, {
|
||||
mode: form.mode.value,
|
||||
topics,
|
||||
count: parseInt(form.count.value, 10) || 0,
|
||||
order: form.order.value,
|
||||
shuffleOptions: form.shuffle.checked,
|
||||
});
|
||||
if (!s.questions.length) { toast('Keine Fragen für diese Auswahl.', 'warn'); return; }
|
||||
startSession(s, 0);
|
||||
}
|
||||
|
||||
function startSession(session, index) {
|
||||
ui.session = session;
|
||||
save();
|
||||
renderSession(index);
|
||||
}
|
||||
|
||||
/* ----------------------------- Fragen-Folien */
|
||||
|
||||
function renderSession(index) {
|
||||
const s = ui.session;
|
||||
const slides = s.questions.map((q, i) => questionSlide(q, i)).join('');
|
||||
const last = s.mode === 'exam' && !s.submitted ? submitSlide() : resultSlide();
|
||||
setSlides(slides + last, Math.min(index ?? s.current ?? 0, s.questions.length));
|
||||
}
|
||||
|
||||
/** Ist die Frage gesperrt (aufgelöst bzw. Prüfung abgegeben)? */
|
||||
function isLocked(q) {
|
||||
const a = ui.session.answers[q.id];
|
||||
return ui.session.submitted || !!(a && a.revealed);
|
||||
}
|
||||
|
||||
function questionSlide(q, i) {
|
||||
const s = ui.session;
|
||||
const a = s.answers[q.id] || {};
|
||||
const locked = isLocked(q);
|
||||
const info = objectiveInfo({ topics: s.topics }, q.objective);
|
||||
const showSolution = s.mode === 'learn' || s.submitted;
|
||||
const nOk = correctCount(q);
|
||||
let hint = '';
|
||||
let body;
|
||||
if (q.type === 'fill') {
|
||||
hint = `Antwort eintippen${q.case_sensitive === false ? '' : ' – Groß-/Kleinschreibung beachten'}.`;
|
||||
body = `<input class="fill" type="text" autocomplete="off" autocapitalize="off" spellcheck="false"
|
||||
value="${esc(a.value || '')}"${locked ? ' disabled' : ''} aria-label="Antwort">`;
|
||||
} else {
|
||||
const multi = q.type === 'multi';
|
||||
if (multi) hint = `Wählen Sie ${COUNT_WORDS[nOk] || nOk}.`;
|
||||
const sel = new Set(Array.isArray(a.value) ? a.value : []);
|
||||
body = `<ol class="options">${q.options.map((o, k) => `
|
||||
<li class="option" data-oid="${o.id}"><label>
|
||||
<input type="${multi ? 'checkbox' : 'radio'}" name="q_${esc(q.id)}" value="${o.id}"${sel.has(o.id) ? ' checked' : ''}${locked ? ' disabled' : ''}>
|
||||
<span class="letter">${LETTERS[k]}</span><span class="text">${fmt(o.text)}</span></label></li>`).join('')}
|
||||
</ol>`;
|
||||
}
|
||||
const flag = s.mode === 'exam' && !s.submitted
|
||||
? `<label class="flag"><input type="checkbox" data-flag${a.flagged ? ' checked' : ''}> zur Überprüfung markieren</label>` : '';
|
||||
const solution = showSolution ? solutionBlock(q, locked) : '';
|
||||
const learnBtn = s.mode === 'learn' && !locked ? '<p class="actions"><button class="btn" data-act="next">Auflösen ▸</button></p>' : '';
|
||||
return `
|
||||
<section class="question${locked ? ' locked' : ''}" data-qid="${esc(q.id)}" data-index="${i}">
|
||||
<div class="q-head"><span class="q-no">Frage ${i + 1} von ${s.questions.length}</span>
|
||||
<span class="q-obj">${esc(q.objective)} ${esc(info.title)}${info.weight ? ` · Gewicht ${info.weight}` : ''}</span>${flag}</div>
|
||||
<div class="q-text">${fmt(q.question)}</div>
|
||||
${q.code ? `<pre class="q-code"><code>${esc(q.code)}</code></pre>` : ''}
|
||||
${hint ? `<p class="q-hint">${hint}</p>` : ''}
|
||||
${body}
|
||||
${learnBtn}
|
||||
${solution}
|
||||
</section>`;
|
||||
}
|
||||
|
||||
/** Auflösung: Ergebnis, Erläuterung je Option, Gesamterläuterung. Ungesperrt als reveal-Fragment. */
|
||||
function solutionBlock(q, locked) {
|
||||
const a = ui.session.answers[q.id];
|
||||
let res = '';
|
||||
if (locked) {
|
||||
const ok = !!a && isCorrect(q, a.value);
|
||||
res = !isAnswered(a) ? '<p class="verdict skipped">Nicht beantwortet</p>'
|
||||
: ok ? '<p class="verdict right">Richtig</p>' : '<p class="verdict wrong">Falsch</p>';
|
||||
}
|
||||
let details;
|
||||
if (q.type === 'fill') {
|
||||
const alt = q.answers.slice(1);
|
||||
details = `<p><strong>Musterlösung:</strong> <code>${esc(q.answers[0])}</code>${alt.length
|
||||
? ` · ebenfalls akzeptiert: ${alt.map((x) => `<code>${esc(x)}</code>`).join(', ')}` : ''}</p>`
|
||||
+ (a && isAnswered(a) ? `<p>Ihre Antwort: <code>${esc(a.value)}</code></p>` : '');
|
||||
} else {
|
||||
details = `<ul class="explain">${q.options.map((o, k) => `
|
||||
<li class="${o.correct ? 'ok' : 'no'}"><span class="letter">${LETTERS[k]}</span>
|
||||
<span class="mark">${o.correct ? '✔ richtig' : '✘ falsch'}</span> ${fmt(o.explanation)}</li>`).join('')}</ul>`;
|
||||
}
|
||||
const refs = q.references && q.references.length
|
||||
? `<p class="refs">Nachschlagen: ${q.references.map((r) => `<code>${esc(r)}</code>`).join(' · ')}</p>` : '';
|
||||
return `
|
||||
<div class="solution${locked ? '' : ' fragment'}">
|
||||
<h3>Auflösung</h3>${res}${details}
|
||||
<div class="explanation">${fmt(q.explanation)}</div>${refs}
|
||||
</div>`;
|
||||
}
|
||||
|
||||
/** Markiert Optionen nach der Auflösung (richtig/falsch gewählt). */
|
||||
function markOptions(section, q) {
|
||||
const a = ui.session.answers[q.id];
|
||||
const sel = new Set(a && Array.isArray(a.value) ? a.value : []);
|
||||
$$('.option', section).forEach((li) => {
|
||||
const o = q.options.find((x) => x.id === li.dataset.oid);
|
||||
li.classList.toggle('is-correct', o.correct);
|
||||
li.classList.toggle('is-wrong', !o.correct && sel.has(o.id));
|
||||
li.classList.toggle('is-missed', o.correct && !sel.has(o.id));
|
||||
});
|
||||
const fill = $('.fill', section);
|
||||
if (fill) fill.classList.add(a && isCorrect(q, a.value) ? 'is-correct' : 'is-wrong');
|
||||
}
|
||||
|
||||
/** Lernmodus: Frage auflösen, sperren und Auflösung dauerhaft sichtbar machen. */
|
||||
function revealQuestion(section) {
|
||||
const s = ui.session;
|
||||
const q = s.questions[Number(section.dataset.index)];
|
||||
const a = s.answers[q.id] || (s.answers[q.id] = { value: q.type === 'fill' ? '' : [] });
|
||||
if (a.revealed) return;
|
||||
a.revealed = true;
|
||||
save();
|
||||
const fresh = document.createElement('div');
|
||||
fresh.innerHTML = solutionBlock(q, true);
|
||||
$('.solution', section).replaceWith(fresh.firstElementChild);
|
||||
$$('input', section).forEach((i) => { i.disabled = true; });
|
||||
const btn = $('.actions', section);
|
||||
if (btn) btn.remove();
|
||||
section.classList.add('locked');
|
||||
markOptions(section, q);
|
||||
Reveal.syncFragments();
|
||||
$('.solution', section).scrollIntoView({ behavior: 'smooth', block: 'nearest' });
|
||||
updateToolbar();
|
||||
}
|
||||
|
||||
/* ----------------------------- Abgabe & Ergebnis */
|
||||
|
||||
function submitSlide() {
|
||||
const s = ui.session;
|
||||
const open = [], flagged = [];
|
||||
s.questions.forEach((q, i) => {
|
||||
const a = s.answers[q.id];
|
||||
if (!isAnswered(a)) open.push(i);
|
||||
if (a && a.flagged) flagged.push(i);
|
||||
});
|
||||
const jump = (list) => list.length
|
||||
? list.map((i) => `<button class="btn small" data-goto="${i}">${i + 1}</button>`).join(' ') : '–';
|
||||
return `
|
||||
<section class="final submit">
|
||||
<h2>Prüfung abgeben</h2>
|
||||
<p>Beantwortet: ${s.questions.length - open.length} von ${s.questions.length}</p>
|
||||
<p>Offen: ${jump(open)}</p>
|
||||
<p>Markiert: ${jump(flagged)}</p>
|
||||
<p><button class="btn primary" data-act="submit">Prüfung abgeben und auswerten</button></p>
|
||||
</section>`;
|
||||
}
|
||||
|
||||
function resultSlide() {
|
||||
const s = ui.session;
|
||||
const learn = s.mode === 'learn';
|
||||
const r = score(s, learn);
|
||||
const topicTitle = (id) => (s.topics.find((t) => t.id === id) || {}).title || '';
|
||||
const rows = Object.keys(r.topics).sort().map((t) => {
|
||||
const ts = r.topics[t];
|
||||
const p = Math.round((ts.correct / ts.total) * 100);
|
||||
return `<tr><td>${esc(t)}</td><td>${esc(topicTitle(t))}</td><td>${ts.correct}/${ts.total}</td>
|
||||
<td><span class="bar"><span style="width:${p}%"></span></span> ${p} %</td></tr>`;
|
||||
}).join('');
|
||||
const wrong = r.wrong.length
|
||||
? r.wrong.map((i) => `<button class="btn small" data-goto="${i}">${i + 1}</button>`).join(' ') : '–';
|
||||
const verdict = r.total === 0 ? ''
|
||||
: `<p class="verdict ${r.passed ? 'right' : 'wrong'}">${r.passed ? 'Bestanden' : 'Nicht bestanden'} (Richtwert ${s.pass_percent} %)</p>`;
|
||||
return `
|
||||
<section class="final result">
|
||||
<h2>Auswertung${learn ? ' (bisher aufgelöste Fragen)' : ''}</h2>
|
||||
<p class="big">${r.correct} von ${r.total} richtig · ${r.percent} %</p>
|
||||
${verdict}
|
||||
<p class="footnote">LPI bewertet auf einer Skala von 200–800 (bestanden ab 500) und veröffentlicht keine Prozentgrenze.
|
||||
Der Richtwert ist eine Annahme und in <code>exam.json</code> (<code>pass_percent</code>) einstellbar.</p>
|
||||
<div class="table-wrap"><table><thead><tr><th>Topic</th><th></th><th>richtig</th><th>Quote</th></tr></thead>
|
||||
<tbody>${rows}</tbody></table></div>
|
||||
<p>Falsch/offen (anklicken zum Ansehen): ${wrong}</p>
|
||||
<p class="actions">
|
||||
${r.wrong.length ? '<button class="btn primary" data-act="retry">Falsche im Lernmodus wiederholen</button>' : ''}
|
||||
<button class="btn" data-act="export">Sitzung exportieren</button>
|
||||
<button class="btn" data-act="menu">Neue Sitzung</button>
|
||||
</p>
|
||||
</section>`;
|
||||
}
|
||||
|
||||
function submitExam(auto) {
|
||||
const s = ui.session;
|
||||
s.submitted = true;
|
||||
save();
|
||||
renderSession(s.questions.length);
|
||||
toast(auto ? 'Zeit abgelaufen – Prüfung automatisch abgegeben.' : 'Prüfung abgegeben.', auto ? 'warn' : 'info');
|
||||
}
|
||||
|
||||
/** Neue Lernsitzung mit den falsch beantworteten Fragen der aktuellen Sitzung. */
|
||||
function retryWrong() {
|
||||
const s = ui.session;
|
||||
const r = score(s, s.mode === 'learn');
|
||||
const qs = r.wrong.map((i) => s.questions[i]);
|
||||
const exam = { ...(ui.exams.find((e) => e.id === s.exam_id) || {}), id: s.exam_id, title: s.exam_title,
|
||||
code: s.exam_code, topics: s.topics, pass_percent: s.pass_percent, bank: qs };
|
||||
startSession(createSession(exam, { mode: 'learn', order: 'random', shuffleOptions: true }), 0);
|
||||
}
|
||||
|
||||
/* ----------------------------- Werkzeugleiste, Timer */
|
||||
|
||||
/** Überträgt die tatsächliche Höhe der (ggf. umbrochenen) Werkzeugleiste nach CSS --tb-h. */
|
||||
function syncToolbarHeight() {
|
||||
document.documentElement.style.setProperty('--tb-h', `${$('.toolbar').offsetHeight}px`);
|
||||
}
|
||||
|
||||
function updateToolbar() {
|
||||
const s = ui.session;
|
||||
$('#tb-title').textContent = s ? `${s.exam_title} · ${s.mode === 'exam' ? 'Prüfung' : 'Lernen'}` : 'LPIC-Prüfungssimulator';
|
||||
$('#tb-export').disabled = !s;
|
||||
if (!s) { $('#tb-progress').textContent = ''; $('#tb-timer').textContent = ''; return; }
|
||||
const done = s.questions.filter((q) => isAnswered(s.answers[q.id])).length;
|
||||
$('#tb-progress').textContent = `${done}/${s.questions.length} beantwortet`;
|
||||
const timer = $('#tb-timer');
|
||||
if (s.mode === 'exam' && !s.submitted) {
|
||||
const rest = s.time_limit_s - s.elapsed_s;
|
||||
timer.textContent = `⏱ ${fmtTime(rest)}`;
|
||||
timer.classList.toggle('low', rest < 600);
|
||||
} else {
|
||||
timer.textContent = `⏱ ${fmtTime(s.elapsed_s)}`;
|
||||
timer.classList.remove('low');
|
||||
}
|
||||
}
|
||||
|
||||
function tick() {
|
||||
const s = ui.session;
|
||||
if (!s || s.submitted) return;
|
||||
s.elapsed_s++;
|
||||
if (++ui.tickCount % SAVE_EVERY_S === 0) save();
|
||||
if (s.mode === 'exam' && s.elapsed_s >= s.time_limit_s) { submitExam(true); return; }
|
||||
updateToolbar();
|
||||
}
|
||||
|
||||
/* ----------------------------- Export / Import */
|
||||
|
||||
function exportSession() {
|
||||
const s = ui.session;
|
||||
if (!s) return;
|
||||
save();
|
||||
const d = new Date();
|
||||
const pad = (n) => String(n).padStart(2, '0');
|
||||
const stamp = `${d.getFullYear()}${pad(d.getMonth() + 1)}${pad(d.getDate())}_${pad(d.getHours())}${pad(d.getMinutes())}`;
|
||||
download(`lpic_sim_${s.exam_id}_${s.mode}_${stamp}.json`, JSON.stringify(s, null, 1));
|
||||
}
|
||||
|
||||
/** Import: erkennt Sitzung (fortsetzen) oder Fragenpaket (in Bank übernehmen). */
|
||||
function importFile(file) {
|
||||
const reader = new FileReader();
|
||||
reader.onload = () => {
|
||||
let doc;
|
||||
try { doc = JSON.parse(reader.result); }
|
||||
catch (e) { toast(`${file.name}: kein gültiges JSON (${e.message})`, 'error'); return; }
|
||||
if (doc.format === SESSION_FORMAT) {
|
||||
const err = validateSession(doc);
|
||||
if (err) { toast(`${file.name}: ${err}`, 'error'); return; }
|
||||
startSession(doc, doc.current || 0);
|
||||
toast(`Sitzung „${doc.exam_title}“ geladen – weiter bei Frage ${Math.min((doc.current || 0) + 1, doc.questions.length)}.`);
|
||||
} else if (doc.format === PACK_FORMAT) {
|
||||
const res = mergePack(ui.exams, doc);
|
||||
if (res.added + res.replaced > 0) {
|
||||
const packs = (store.get(STORE_PACKS) || []).filter((p) => p.name !== file.name);
|
||||
packs.push({ ...doc, name: file.name });
|
||||
store.set(STORE_PACKS, packs);
|
||||
}
|
||||
res.errors.slice(0, 5).forEach((e) => toast(e, 'error'));
|
||||
toast(`${file.name}: ${res.added} neu, ${res.replaced} ersetzt${res.errors.length ? `, ${res.errors.length} abgelehnt` : ''}.`,
|
||||
res.errors.length ? 'warn' : 'info');
|
||||
if (!ui.session) renderStart();
|
||||
} else {
|
||||
toast(`${file.name}: unbekanntes Format (erwartet ${SESSION_FORMAT} oder ${PACK_FORMAT})`, 'error');
|
||||
}
|
||||
};
|
||||
reader.readAsText(file);
|
||||
}
|
||||
|
||||
/* ----------------------------- Ereignisse */
|
||||
|
||||
function onChange(ev) {
|
||||
const t = ev.target;
|
||||
if (t.closest('#setup')) { updateStartForm(); return; }
|
||||
const section = t.closest('section.question');
|
||||
if (!section || !ui.session) return;
|
||||
const s = ui.session;
|
||||
const q = s.questions[Number(section.dataset.index)];
|
||||
const a = s.answers[q.id] || (s.answers[q.id] = { value: q.type === 'fill' ? '' : [] });
|
||||
if (t.hasAttribute('data-flag')) { a.flagged = t.checked; save(); return; }
|
||||
if (isLocked(q)) return;
|
||||
if (q.type === 'single') a.value = [t.value];
|
||||
else if (q.type === 'multi') {
|
||||
let sel = $$('.options input:checked', section).map((i) => i.value);
|
||||
const max = correctCount(q);
|
||||
if (sel.length > max) {
|
||||
t.checked = false;
|
||||
sel = sel.filter((v) => v !== t.value);
|
||||
toast(`Höchstens ${max} Antworten wählbar.`, 'warn');
|
||||
}
|
||||
a.value = sel;
|
||||
} else a.value = t.value;
|
||||
save();
|
||||
updateToolbar();
|
||||
}
|
||||
|
||||
function onClick(ev) {
|
||||
const btn = ev.target.closest('[data-act],[data-goto]');
|
||||
if (!btn) return;
|
||||
if (btn.dataset.goto !== undefined) { Reveal.slide(Number(btn.dataset.goto)); return; }
|
||||
switch (btn.dataset.act) {
|
||||
case 'next': Reveal.next(); break;
|
||||
case 'prev': Reveal.prev(); break;
|
||||
case 'resume': { const s = store.get(STORE_SESSION); if (s) { ui.session = s; renderSession(s.current); } break; }
|
||||
case 'discard':
|
||||
if (window.confirm('Gespeicherte Sitzung verwerfen? (Vorher exportieren, falls sie noch gebraucht wird.)')) {
|
||||
store.del(STORE_SESSION); renderStart();
|
||||
}
|
||||
break;
|
||||
case 'submit': {
|
||||
const open = ui.session.questions.filter((q) => !isAnswered(ui.session.answers[q.id])).length;
|
||||
if (!open || window.confirm(`${open} Fragen sind offen. Trotzdem abgeben?`)) submitExam(false);
|
||||
break;
|
||||
}
|
||||
case 'retry': retryWrong(); break;
|
||||
case 'export': exportSession(); break;
|
||||
case 'import': $('#import-file').click(); break;
|
||||
case 'menu': save(); renderStart(); break;
|
||||
default: break;
|
||||
}
|
||||
}
|
||||
|
||||
function onKey(ev) {
|
||||
const t = ev.target;
|
||||
if (t.matches && t.matches('input.fill')) {
|
||||
if (ev.key === 'Enter') { ev.preventDefault(); t.blur(); Reveal.next(); }
|
||||
return;
|
||||
}
|
||||
if (t.matches && t.matches('input[type=text],input[type=number],select,textarea')) return;
|
||||
if (ev.ctrlKey || ev.altKey || ev.metaKey) return;
|
||||
const n = Number(ev.key);
|
||||
if (n >= 1 && n <= 5) {
|
||||
const input = $$('.present .options input')[n - 1];
|
||||
if (input && !input.disabled) { input.click(); ev.preventDefault(); }
|
||||
}
|
||||
}
|
||||
|
||||
function onSlideChanged(ev) {
|
||||
const s = ui.session;
|
||||
if (!s) return;
|
||||
s.current = ev.indexh;
|
||||
const sec = ev.currentSlide;
|
||||
if (sec.classList.contains('result')) {
|
||||
// Lernmodus: Auswertung bei jedem Aufruf neu berechnen
|
||||
const fresh = document.createElement('div');
|
||||
fresh.innerHTML = resultSlide();
|
||||
sec.innerHTML = fresh.firstElementChild.innerHTML;
|
||||
} else if (sec.classList.contains('submit')) {
|
||||
const fresh = document.createElement('div');
|
||||
fresh.innerHTML = submitSlide();
|
||||
sec.innerHTML = fresh.firstElementChild.innerHTML;
|
||||
} else if (sec.classList.contains('question')) {
|
||||
const q = s.questions[Number(sec.dataset.index)];
|
||||
if (isLocked(q) && q.options) markOptions(sec, q);
|
||||
sec.scrollTop = 0;
|
||||
}
|
||||
save();
|
||||
updateToolbar();
|
||||
}
|
||||
|
||||
function onFragmentShown(ev) {
|
||||
const frag = ev.fragment;
|
||||
if (frag && frag.classList.contains('solution')) revealQuestion(frag.closest('section.question'));
|
||||
}
|
||||
|
||||
function init() {
|
||||
loadBank();
|
||||
// Erst eine Platzhalter-Folie, dann reveal.js starten, dann die echte Startseite setzen
|
||||
$('.reveal .slides').innerHTML = '<section></section>';
|
||||
Reveal.initialize({
|
||||
hash: false,
|
||||
history: false,
|
||||
controls: true,
|
||||
controlsTutorial: false,
|
||||
progress: true,
|
||||
center: false,
|
||||
disableLayout: true,
|
||||
scrollActivationWidth: null,
|
||||
overview: false,
|
||||
touch: false,
|
||||
pause: false,
|
||||
transition: 'slide',
|
||||
transitionSpeed: 'fast',
|
||||
keyboard: { 66: null, 190: null, 79: null }, // B, Punkt, O: Pause/Übersicht aus
|
||||
}).then(() => {
|
||||
Reveal.on('slidechanged', onSlideChanged);
|
||||
Reveal.on('fragmentshown', onFragmentShown);
|
||||
const slides = $('.reveal .slides');
|
||||
slides.addEventListener('change', onChange);
|
||||
slides.addEventListener('input', (ev) => { if (ev.target.matches('input.fill')) onChange(ev); });
|
||||
slides.addEventListener('submit', (ev) => { ev.preventDefault(); startFromForm(ev.target); });
|
||||
document.addEventListener('click', onClick);
|
||||
document.addEventListener('keydown', onKey);
|
||||
$('#import-file').addEventListener('change', (ev) => {
|
||||
const f = ev.target.files && ev.target.files[0];
|
||||
if (f) importFile(f);
|
||||
ev.target.value = '';
|
||||
});
|
||||
$('#tb-export').addEventListener('click', exportSession);
|
||||
window.addEventListener('beforeunload', save);
|
||||
new ResizeObserver(syncToolbarHeight).observe($('.toolbar'));
|
||||
setInterval(tick, 1000);
|
||||
renderStart();
|
||||
});
|
||||
}
|
||||
|
||||
window.LPICSim = { core, ui };
|
||||
if (window.Reveal && !window.LPICSIM_NO_UI) document.addEventListener('DOMContentLoaded', init);
|
||||
})();
|
||||
@@ -0,0 +1,13 @@
|
||||
/**
|
||||
* @file bank.js
|
||||
* @brief Fragenbank (verschlüsselt) – ERZEUGT von tools/build_bank.py, nicht von Hand bearbeiten
|
||||
* @author Björn Runden <br@runden.de>
|
||||
* @owner Björn Runden <br@runden.de>
|
||||
* @version 2026-10-29T22:40:55Z
|
||||
*/
|
||||
window.LPIC_BANK = {
|
||||
"generated": "2026-10-29T22:40:55Z",
|
||||
"build": "179de2341e47860b1d8ad91f2d6dad734ec7915808c0fd02d276c13a6b2ed86a",
|
||||
"rev": "1ae157f0c162c0674a00e01c",
|
||||
"payload": "teKT1gUgtOV6NRIL1VW9Jr+HID9gbX2p1wMdJFLJIwDPdzP2sKUZB7RQjQDr17hweZf8jAGfie8P0yyoXihOLYh3VplFV/xeCdO+4na9D7Qy8RQX5bcY2eBptZJMAnEW/Bytj3EQ73JOR5lC+XIXhuJTBP9C8QEW8GR9YkmNVQVXV4fEu1Zpd3cs02SqjyKG7yKx29nB+Cs2Md1TwoG7QZROEtQdzoGLWJ9/apgjJVs2ak9HcpAmo2IYbjQ2XcbYeU+k8teRObPkjFHy7kjDn09pR7xDz4HhHBCMdd9kKVaNd748gNodxCcB1JMoz/2cXcWdf3WINCR831WDl+RqOMsrRFfsN2Hg3YVVq8YA0L77EgEkyaN1d23vI8AjZFso0IkyweW/bdW12So+mwjAzMhZXvTahZVNaqhg4uuvb+Px7lDv3bDp13vH3LejKf0/+VMHKtVEoCDIu/fzgf+FfCP5tJEYw/gG/+g2hBcXegu8yTbU/0nxaEnP7sDulXzAiSrnGdRzK47/AV+vqDrCzA26gN2luHVxw61a4v4JIPpR+WiEQA9jBW+gLXw717GbVgnRupxlLAc8sn2z01VfbYHONqn3UCeHXhoBn9ANuBIRiXTHfZwPNeH0Isvx797ESOkwSvYb23/BIXJiIjGUqeEwtAihb06ZNpszHJV1SLhuqFlYPo8lpf/cmghGvD9k3EK+dFmakXTYqtl7gQjkR8RGBrBcZqBKcyrJxXY6Dr/Q3zxR43OJYIMoe/Mckl7c3uIxfYfbvYUyUIj3szhKFbmW6O4DeAWjVgvqFYc01/hr9U9xjRXJcsE4agTJK1iYMASXjYOY6RZ6aBBwoIevb4f+eP4RSifO514PxcyiKz/UfGAMsczmVEaHbiHOPNdSht4SneY9ZiDmhcJ8oyvwz6gXX3Q+x5WuLzWn1nhC8xxOG/hLKZz/7WEA179fU3Y3CNHu0okimhKbMTFe6XWoHNPBcNDdMVYn83lTJGDGvZ7iP6cSnKOKGBaMyG77JNwdrw2VKV9pzdEgGhNE4t6bi3uIssssa8dX86j7yIVpukLjqBTM2StxJaIDugQSv75UEge/REgCFMfEhZNbLvZLEPrpND+6eKEy3+AfEjmZhMDuMy18oxeC4CjbK5FSS6gkKvM9Xqu3me0L5IPHvFr2+SL42ozW/QvJAz8qeFUKgyX3XCv+62LsUNlASyL3EPVuQqNl2vVhn2Fe9WA0v+4Yq9MpAxWzspUGy4u3xiOzDnp5DwlR0PHMwicHNgVoZLNXmFQ2rfWkIt/cacFsY4o/hoZdFBelOLKgrq4T0fjv57FYVwXQBRI2Z3rvLVGU5Bslx0K39HJJC6beyreFDiLAK9fKMtdZtrLkRoRFmNCfnk3UMec77ya6ne2sQmaBL9UaUarwzY8GQsdQ4BT+UKBn9JVulMSpDsOdEFml3SXp9daX3avgZk6JlvsLNxJL2At+oGch3XP6ou3RMivjxsndJRkkPhHyJJVNSSZsvomteZz8YuwmPSnoV6GG6iB4ekocJ3qXNLiJ/qgpXnEQbBxtXZBj5yxBUJ/N8I+T9ftpMjjikESC2+pU9WytVvwJZHDk0MizuCejNt+rNnU32a1vd3H/jPnyOZZBkj+RKBvfZf6S+lWUY/2a9BbFv712xgkGsIGSmVn+znUE17MMJclHC2UCiHmpX22dgauWrC2wagVj4EpgX0st96YIW4tLRbvSz0t+sZ0qB9ZuDekYyKeZKulrFcwNM8aw/NvZYFGpJJQQUUv78Es8JQHL/xMEykmNvzcjbegpsY62YvAw9lJwkT1gaAxTPrcLmLa1lRrV/qFRRzx2o3MKb2/GDKZJ7EZPzs9TalnRHJtjg66iMDzUOk7/t/dA9CHnXh64/NxRd6EJINXr+/PN685ZDotGm3hhgIDC5VfBsj9PtBuZar3rUytRHOjtnvJDXg+HbysKzW4etj3gMbx/3wOiN8DeVhhl8GG4JKJLCMv65/wa96UxpJp+D4ScEZ9QJN9mFefWJ1Gm6sext0F/TKzvx3YEdFD6W8G9GvuRn/fIeq8aUSX2wsrTtX5YQGz549cl8mCBhrUs4tS2u+cx6ij6rM1H3qpMnw8SlesAXbMi2jAcwbT4zr5d+d/wm9eDgzkQJ2qyTPC/6JZlZbiybFLi/pM8afZE9tzEQoctpgsNSVUElVEohqofN5PZNAsp7EqGUGsVTja9TENoqte+tAu/muplAIU9zBrznsawOi+DzqgcOLBi58urxajVFubC0aSCLOP8ZE+bAA0ArsZwnKcOUE2wo6wjHOYNYAFTIXO0vFnlKq9XRz8CgTjwZUdJkKiMp65YegOouU9WOApyCBbem9aR7bVsT5EZcsf21yybPp3zUIyxcY9srUH7mBru5GjOysuiBjUqhpS6TZJAis3x4IU+eB5ssicu8RsdbBopY8uvfaUCS+9a5KG0Ikrb+rWiXk7IE7DYg125UOCOyZi9Q//bWxJX8DA4/ILfs2EihiFIVnvmN2fR90cq0+mOilzddftflZRzNWf9gGjYFiXTIxdhKMHms431FrWNWpaUCAT9vQrmsOvKiqxljQesHL5VnB02SufvTRpJQrShsEr/4FVIUMS0q9MMY1arPsF6AGJavdxBGw1TbHjkuYnDc9qUdL3Erudyqc3iA/G1aw/EiXmD32dCThzDlb9jp2x8t9rBRt0+LjA2ZX/w/ljwS0El8KOK/9tAGeXBSXES9xS8nryFQT0VyZmBQWxzmOZE1X+FcIXXDkdT4g1iOyZbJyw4T2HGpKrKLbEzAN3YfGlRf7MHvWnHinVqwHq38X7cH77PIHazuJ9EYies2MU+i1fHn3RsTpdEhwh9VXTijc8EjELOgmppkuApPVNXy9uck6ypdnSWAslmhwabBEkfftmTR0g0uWBcAApj74sG1gDpAsD1mFarUX759lgBrICbKzwn6TIXs0MVPwBW6DLz0ZfoySfHqCwG3Ma4LwrRqoFMX4g/GSrm/KxupwO11ZGxliousumUEY3zAiUjzw+Cz6mZxzVHfWLtX/tetlDGApLJDNjqTxUSB4bKRoMA+DCinFhqL2R3MUdSyPY3hBtP2yviQQXOUhukIj84ZoRRrrmc6XjRxVuDxDkVB0NgIV4qaXKtHTOhzhy1MdOug6/5Ltmr7QOIqJfiKzGUZiwdUn1+ByHnOJPkYCHUQ72LZO6pxebp66aUvhlZ76GAzauZNkjPKk1Cn4RVcU1cWHsYcCOvGZ3UEm/7RE3Ggs2OHKba61hdLBnsJ3zMF00oA0OtCgf0Vr1uT0sSw8WcBfvmmQ3sbtKLj+1MjmKL06VJIbS0vfrlgwPYmpCM7kD9KNAVO0ETa205oS0GPJey50uLHC3lYwf+g1RqtM5K80Rgy3aakbJicBqzelR2qQnWmLO1vBPZyFe1alCaKEsuRIBn35lwAQt1/p6YP9JZb3ChGipw8r5s1BeHZdqsqLI5sW5fnpgU9NU7tmfdQdcu6h+gfDBCv+zOiN38NAqssCDZswuOopd/3H2RcUlvD4eQAkrRDytbm6MOo/1UbLEf58DFOyKDnDzGMK4ReByCN4TVkT+2XiXB65TSE2IxEGhPkFi3JY7meAnS5E7VXyqjAH7gHchQ3TVWIHXLIk3oJbsnIG47W5phX5asBG9aEVddt1mUpEmzt5+lZ7Y+Nm/uCjqkknuNkiHaapR5krWi73+undXqgXmpWnj2+z22qONanzwdbkd/9Vl//z9vKJjXjrYIZHA2JhbYMUaMlonULnSUCfGYNnKGFnT+0wGrRypASaYTscUPeWmpegjAXlpzQQsLV+bqviFiChONU6JAzSBH1aLVwOedfgTHtI4TjDT7NhEkupyEhqRZQ38QO1Qj1kJ68+gX0QGurhxRLHxlFblx8lX3BIuAwJSgGKv5DdfXE3+dLCDZ5VzqbviN/i9/h1PfotqLLTnnD1P9/GTcabCXidAkfYikhfi0u8U0vGZLFqRoL0OK7p8GPDoarsSckgL0pgzETYC9E7WA7AcqrQiKyk6BQ2zEvh8Sk+hgrofJFEfwjLfPbbSUrGeESn6XlYHGTgjeFcslsOP9nm9NcXKtn5BiNPjrAsMk1cYtuEUh4d9GBAJJsu6D4SM3ZMfe7V64MPBmnh3qkCWTYeWGBkTcY5VfFjnu1/4bmLykIQvt8PSDjKnqlBwpsj9k3zOhU5RgEPTNiqSR3aHkKTxmSMS5nOZBP1zKRCYlePS3qypMUs7o+L2bMDKiuXh6F/IzATaval5NK0kHx/xDHba+VqXKMfCHIBe2LDCPlQkYyr92lFE7ffRxMoCd9AIGSKafGGtC5pbeQcLuhIXnfVMlIOSn++uermYGyHnEgwQntmDlplmzAif5lld6Yk6Q5aMkBJagvtQFCou/qg4eDWkX9beR0TevA3kuzg4OiYJ0yGLeMck0iD/W4AxEqosIbQA4SeaBHsK8FA15vqWwTI6KhcJyYx7Ug+QUcQDzxD6seyus5ntsumNS9TWGPYCQv8r6jZYn4jL+PIymTTL9wIS6pVyqN2xZlZYbOop3tKXbObJw07Vm3RJsZDsXabIccfX7rLo5ZZgETAg4DCR5wx7+X5TPJ6OsKldpeSdhlsiDKJ93VsBRslm5WOd4652Vq+Jxkjp8jPvb/qcnv/9EEIKuywa7m97GTFzNzO47Jsd9x5A5jgj0/QVl4m9BtPjlwqm8QrD8AKpVtv7467VZSXY08J596tNqVwPuiGVU7JLFsfVRaJI8PGMK4jXGOIDAU/y3QV6fuYc1DIzwHwdNEVgIwzbu4e3AMBlzLz6dEHCoYt7PrRigH5QOo9Ihf+4rLScZmXZRU8Vz1w15qleaatJ05cXgTWT9mZCofP2qM7hh7be0W10IcbbgDA+c8MK7N2bqDQM1vmUP79A38GGq0zDQBuo2v4qJ6cfigpngwwbXnO7RW4sWNJWY/DTrxJXKLD3qayy0chpVPJLine truncated
|
||||
};
|
||||
@@ -0,0 +1,138 @@
|
||||
<!doctype html>
|
||||
<!--
|
||||
@file selftest.html
|
||||
@brief Selbsttest der Simulator-Kernlogik (Bewertung, Auswahl, Import, Validierung) im Browser
|
||||
@author Björn Runden <br@runden.de>
|
||||
@owner Björn Runden <br@runden.de>
|
||||
@assist KI-unterstützt (Claude Opus 5.5), geprüft von Björn Runden <br@runden.de>
|
||||
@version 0.1.0 (29.10.2026)
|
||||
@usage im Browser öffnen oder: chromium --headless --dump-dom tests/selftest.html | grep RESULT
|
||||
-->
|
||||
<html lang="de">
|
||||
<head><meta charset="utf-8"><title>Selbsttest LPIC-Simulator</title>
|
||||
<style>body{background:#282a36;color:#f8f8f2;font:14px monospace;padding:16px}.fail{color:#ff5555}.ok{color:#50fa7b}</style>
|
||||
</head>
|
||||
<body>
|
||||
<pre id="out"></pre>
|
||||
<script>window.LPICSIM_NO_UI = true;</script>
|
||||
<script src="../js/bank.js"></script>
|
||||
<script src="../js/app.js"></script>
|
||||
<script>
|
||||
(function () {
|
||||
'use strict';
|
||||
const out = document.getElementById('out');
|
||||
const c = window.LPICSim.core;
|
||||
let pass = 0, fail = 0;
|
||||
function t(name, cond) {
|
||||
const line = document.createElement('div');
|
||||
line.className = cond ? 'ok' : 'fail';
|
||||
line.textContent = `${cond ? 'ok ' : 'FAIL'} ${name}`;
|
||||
out.appendChild(line);
|
||||
cond ? pass++ : fail++;
|
||||
}
|
||||
|
||||
const single = { id: '101.1-900', objective: '101.1', type: 'single', question: 'q', explanation: 'e',
|
||||
options: ['a', 'b', 'c', 'd', 'e'].map((id) => ({ id, text: id, correct: id === 'c', explanation: 'x' })) };
|
||||
const multi = { ...single, id: '101.1-901', type: 'multi',
|
||||
options: single.options.map((o) => ({ ...o, correct: o.id === 'a' || o.id === 'd' })) };
|
||||
const fill = { id: '101.1-902', objective: '101.1', type: 'fill', question: 'q', explanation: 'e', answers: ['modprobe', 'modprobe -a'] };
|
||||
const fillCi = { ...fill, id: '101.1-903', case_sensitive: false };
|
||||
|
||||
t('single richtig', c.isCorrect(single, ['c']));
|
||||
t('single falsch', !c.isCorrect(single, ['a']));
|
||||
t('single leer falsch', !c.isCorrect(single, []));
|
||||
t('multi vollständig richtig', c.isCorrect(multi, ['d', 'a']));
|
||||
t('multi teilweise = falsch', !c.isCorrect(multi, ['a']));
|
||||
t('multi zu viel = falsch', !c.isCorrect(multi, ['a', 'd', 'e']));
|
||||
t('fill exakt', c.isCorrect(fill, 'modprobe'));
|
||||
t('fill Leerraum normalisiert', c.isCorrect(fill, ' modprobe -a '));
|
||||
t('fill Alternative', c.isCorrect(fill, 'modprobe -a'));
|
||||
t('fill Groß/klein beachtet', !c.isCorrect(fill, 'Modprobe'));
|
||||
t('fill case_sensitive=false', c.isCorrect(fillCi, 'MODPROBE'));
|
||||
t('fill leer falsch', !c.isCorrect(fill, ''));
|
||||
|
||||
const objs = new Set(['101.1']);
|
||||
t('validate ok single', c.validateQuestion(single, objs) === null);
|
||||
t('validate ok multi', c.validateQuestion(multi, objs) === null);
|
||||
t('validate ok fill', c.validateQuestion(fill, objs) === null);
|
||||
t('validate: 4 Optionen abgelehnt', c.validateQuestion({ ...single, options: single.options.slice(0, 4) }, objs) !== null);
|
||||
t('validate: single mit 2 richtigen abgelehnt', c.validateQuestion({ ...multi, type: 'single' }, objs) !== null);
|
||||
t('validate: Objective unbekannt', c.validateQuestion({ ...single, objective: '999.9', id: '999.9-001' }, objs) !== null);
|
||||
t('validate: Option ohne Erläuterung', c.validateQuestion({ ...single,
|
||||
options: single.options.map((o, i) => (i ? o : { ...o, explanation: '' })) }, objs) !== null);
|
||||
|
||||
// ChaCha20: RFC 8439, Abschnitt 2.4.2 (Ergebnis mit Python-cryptography gegengeprüft)
|
||||
const hex = (h) => new Uint8Array(h.match(/../g).map((b) => parseInt(b, 16)));
|
||||
const toHex = (u) => Array.from(u, (b) => b.toString(16).padStart(2, '0')).join('');
|
||||
const rfcKey = new Uint8Array(32).map((_, i) => i);
|
||||
const rfcNonce = hex('000000000000004a00000000');
|
||||
const rfcPlain = new TextEncoder().encode("Ladies and Gentlemen of the class of '99: If I could offer you only one tip for the future, sunscreen would be it.");
|
||||
const rfcCipher = '6e2e359a2568f98041ba0728dd0d6981e97e7aec1d4360c20a27afccfd9fae0bf91b65c5524733ab8f593dabcd62b3571639d624e65152ab8f530c359f0861d807ca0dbf500d6a6156a38e088a22b65e52bc514d16ccf806818ce91ab77937365af90bbf74a35be6b40b8eedf2785e42874d';
|
||||
t('ChaCha20 RFC 8439 Testvektor', toHex(c.chacha20(rfcKey, rfcNonce, rfcPlain)) === rfcCipher);
|
||||
t('ChaCha20 Rückweg', new TextDecoder().decode(c.chacha20(rfcKey, rfcNonce, hex(rfcCipher))) === new TextDecoder().decode(rfcPlain));
|
||||
|
||||
// Bank: verschlüsselt ausgeliefert, kein Klartext in bank.js
|
||||
const raw = window.LPIC_BANK;
|
||||
t('bank.js enthält nur Chiffrat', !!raw.payload && !JSON.stringify(raw).includes('lsmod') && !JSON.stringify(raw).includes('explanation'));
|
||||
t('kaputte Bank meldet Fehler', (() => { try { c.decodeBank({ ...raw, rev: '00'.repeat(12) }); return false; } catch (e) { return /nicht lesbar/.test(e.message); } })());
|
||||
|
||||
// Bank und Paket-Import
|
||||
const exams = c.decodeBank(raw).exams;
|
||||
const ex = exams.find((e) => e.id === 'lpic1-101');
|
||||
t('Bank geladen', !!ex && ex.bank.length > 0);
|
||||
const n0 = ex.bank.length;
|
||||
let r = c.mergePack(exams, { format: 'lpic-sim-questions', exam: 'lpic1-101', questions: [single, { ...single, id: 'kaputt' }] });
|
||||
t('Paket: 1 neu, 1 abgelehnt', r.added === 1 && r.errors.length === 1 && ex.bank.length === n0 + 1);
|
||||
r = c.mergePack(exams, { format: 'lpic-sim-questions', exam: 'lpic1-101', questions: [single] });
|
||||
t('Paket: gleiche id ersetzt', r.replaced === 1 && ex.bank.length === n0 + 1);
|
||||
r = c.mergePack(exams, { format: 'lpic-sim-questions', exam: 'lpic2-201', questions: [] });
|
||||
t('Paket: unbekannte Prüfung abgelehnt', r.errors.length === 1);
|
||||
r = c.mergePack(exams, { format: 'lpic-sim-questions', exam: 'lpic2-201',
|
||||
definition: { exam: { id: 'lpic2-201', level: 'LPIC-2', title: 'LPIC-2 201', questions: 60, duration_min: 90 },
|
||||
topics: [{ id: '200', title: 'Kapazität', objectives: [{ id: '200.1', title: 'x', weight: 2 }] }] },
|
||||
questions: [{ ...single, id: '200.1-001', objective: '200.1' }] });
|
||||
t('Paket mit definition legt Prüfung an', r.added === 1 && exams.some((e) => e.id === 'lpic2-201'));
|
||||
|
||||
// Gewichtete Auswahl
|
||||
const fake = { questions: 10, duration_min: 90, id: 'x', title: 'x', topics: [{ id: '1', title: 't', objectives: [
|
||||
{ id: '1.1', weight: 4 }, { id: '1.2', weight: 1 }] }], bank: [] };
|
||||
for (let i = 0; i < 20; i++) fake.bank.push({ ...single, id: `1.1-${100 + i}`, objective: '1.1' });
|
||||
for (let i = 0; i < 20; i++) fake.bank.push({ ...single, id: `1.2-${100 + i}`, objective: '1.2' });
|
||||
const pick = c.pickWeighted(fake, fake.bank, 10);
|
||||
t('Auswahl: Anzahl 10', pick.length === 10);
|
||||
t('Auswahl: Gewichtung 8:2', pick.filter((q) => q.objective === '1.1').length === 8);
|
||||
t('Auswahl: keine Dubletten', new Set(pick.map((q) => q.id)).size === 10);
|
||||
const few = { ...fake, bank: fake.bank.filter((q) => q.objective === '1.2' || q.id === '1.1-100') };
|
||||
t('Auswahl: füllt bei Mangel auf', c.pickWeighted(few, few.bank, 10).length === 10);
|
||||
|
||||
// Sitzung
|
||||
const s = c.createSession(ex, { mode: 'exam' });
|
||||
t('Prüfungssitzung: Zeitlimit 90 min', s.time_limit_s === 5400);
|
||||
t('Prüfungssitzung: Fragen = min(60, Bank)', s.questions.length === Math.min(60, ex.bank.length));
|
||||
t('Sitzung gültig', c.validateSession(s) === null);
|
||||
t('Sitzung ist Kopie', s.questions[0] !== ex.bank.find((q) => q.id === s.questions[0].id));
|
||||
const sl = c.createSession(ex, { mode: 'learn', topics: ['101'], count: 3, order: 'objective', shuffleOptions: false });
|
||||
t('Lernsitzung: count und Filter', sl.questions.length === 3 && sl.questions.every((q) => q.objective.startsWith('101.')));
|
||||
t('Lernsitzung: sortiert, Optionen ungemischt', sl.questions[0].options === undefined || sl.questions[0].options[0].id === 'a');
|
||||
t('JSON-Rundreise', c.validateSession(JSON.parse(JSON.stringify(s))) === null);
|
||||
|
||||
// Auswertung
|
||||
const q0 = sl.questions[0];
|
||||
sl.answers[q0.id] = { value: q0.type === 'fill' ? q0.answers[0] : q0.options.filter((o) => o.correct).map((o) => o.id), revealed: true };
|
||||
let sc = c.score(sl, true);
|
||||
t('Score Lernmodus zählt nur Aufgelöste', sc.total === 1 && sc.correct === 1 && sc.percent === 100);
|
||||
sc = c.score(sl, false);
|
||||
t('Score gesamt', sc.total === 3 && sc.correct === 1 && sc.wrong.length === 2);
|
||||
|
||||
t('fmt escaped HTML', c.fmt('<b>x</b>') === '<b>x</b>');
|
||||
t('fmt Code und fett', c.fmt('a `ls -l` **b**') === 'a <code>ls -l</code> <strong>b</strong>');
|
||||
t('fmt kein Markup in Code', c.fmt('`**x**`') === '<code>**x**</code>');
|
||||
|
||||
const res = document.createElement('div');
|
||||
res.id = 'result';
|
||||
res.textContent = `RESULT pass=${pass} fail=${fail}`;
|
||||
out.appendChild(res);
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,134 @@
|
||||
<!doctype html>
|
||||
<!--
|
||||
@file ui_smoke.html
|
||||
@brief UI-Rauchtest: bedient index.html im iframe (Start, Antwort, Auflösung, Speichern, Prüfungsabgabe)
|
||||
@author Björn Runden <br@runden.de>
|
||||
@owner Björn Runden <br@runden.de>
|
||||
@assist KI-unterstützt (Claude Opus 5.5), geprüft von Björn Runden <br@runden.de>
|
||||
@version 0.1.0 (29.10.2026)
|
||||
@usage chromium --headless --allow-file-access-from-files --virtual-time-budget=20000 --dump-dom tests/ui_smoke.html
|
||||
?stop=learn|exam|result hält an dieser Stelle an (für Screenshots)
|
||||
-->
|
||||
<html lang="de">
|
||||
<head><meta charset="utf-8"><title>UI-Rauchtest</title>
|
||||
<style>html,body{margin:0;height:100%;background:#282a36}iframe{border:0;width:100%;height:100%}#out{position:fixed;left:0;bottom:0;color:#fff;font:11px monospace;max-height:30%;overflow:auto;background:#000a;display:none}</style>
|
||||
</head>
|
||||
<body>
|
||||
<iframe id="app" src="../index.html"></iframe>
|
||||
<pre id="out"></pre>
|
||||
<script>
|
||||
(async function () {
|
||||
'use strict';
|
||||
const stop = new URLSearchParams(location.search).get('stop');
|
||||
const out = document.getElementById('out');
|
||||
let pass = 0, fail = 0;
|
||||
const t = (name, cond) => { out.textContent += `${cond ? 'ok ' : 'FAIL'} ${name}\n`; cond ? pass++ : fail++; };
|
||||
const wait = (ms) => new Promise((r) => setTimeout(r, ms));
|
||||
const done = () => { out.textContent += `RESULT pass=${pass} fail=${fail}\n`; if (!stop) out.style.display = 'block'; };
|
||||
|
||||
try {
|
||||
const frame = document.getElementById('app');
|
||||
await new Promise((r) => frame.addEventListener('load', r));
|
||||
const w = frame.contentWindow, d = w.document;
|
||||
w.localStorage.clear();
|
||||
await wait(800);
|
||||
const $ = (s) => d.querySelector(s);
|
||||
|
||||
t('Startseite sichtbar', !!$('#setup'));
|
||||
// Lernmodus, nach Objective sortiert, ungemischt
|
||||
$('#setup [name=order]').value = 'objective';
|
||||
$('#setup [name=shuffle]').checked = false;
|
||||
$('#setup [name=count]').value = '3';
|
||||
$('#setup [type=submit]').click();
|
||||
await wait(500);
|
||||
const sec = () => $('section.present');
|
||||
t('Frage 1 angezeigt', sec() && sec().classList.contains('question'));
|
||||
const q = w.LPICSim.ui.session.questions[0];
|
||||
const correct = q.options.find((o) => o.correct).id;
|
||||
sec().querySelector(`input[value="${correct}"]`).click();
|
||||
await wait(100);
|
||||
t('Antwort gespeichert (localStorage)', JSON.parse(w.localStorage.getItem('lpic_sim.session')).answers[q.id].value[0] === correct);
|
||||
t('Lösung noch verborgen', !sec().querySelector('.solution').classList.contains('visible'));
|
||||
if (stop === 'learn') return done();
|
||||
w.Reveal.next();
|
||||
await wait(400);
|
||||
t('noch auf Frage 1 (Auflösung statt Weiter)', Number(sec().dataset.index) === 0);
|
||||
t('Auflösung sichtbar und kein Fragment mehr', !sec().querySelector('.solution').classList.contains('fragment'));
|
||||
t('Urteil „Richtig“', sec().querySelector('.verdict.right') !== null);
|
||||
t('Eingaben gesperrt', [...sec().querySelectorAll('input')].every((i) => i.disabled));
|
||||
t('Option grün markiert', sec().querySelector('.option.is-correct') !== null);
|
||||
if (stop === 'solution') return done();
|
||||
w.Reveal.next();
|
||||
await wait(400);
|
||||
t('Frage 2 nach zweitem Weiter', Number(sec().dataset.index) === 1);
|
||||
w.Reveal.next(); await wait(300); // Frage 2 unbeantwortet auflösen
|
||||
t('unbeantwortet = „Nicht beantwortet“', sec().querySelector('.verdict.skipped') !== null);
|
||||
w.Reveal.prev(); await wait(300);
|
||||
t('Zurück auf Frage 1, Auflösung bleibt', Number(sec().dataset.index) === 0 && sec().querySelector('.verdict.right') !== null);
|
||||
|
||||
// Menü → gespeicherte Sitzung → fortsetzen
|
||||
d.querySelector('[data-act=menu]').click(); await wait(400);
|
||||
t('Startseite bietet Fortsetzen', !!$('.resume'));
|
||||
$('.resume [data-act=resume]').click(); await wait(400);
|
||||
t('Fortgesetzt an gespeicherter Stelle', Number(sec().dataset.index) === 0);
|
||||
|
||||
// Prüfungsmodus
|
||||
d.querySelector('[data-act=menu]').click(); await wait(400);
|
||||
$('#setup [name=mode][value=exam]').click(); await wait(100);
|
||||
$('#setup [type=submit]').click(); await wait(500);
|
||||
const s = w.LPICSim.ui.session;
|
||||
t('Prüfungsmodus aktiv', s.mode === 'exam' && s.time_limit_s === 5400);
|
||||
t('keine Auflösung im Prüfungsmodus', !sec().querySelector('.solution'));
|
||||
t('Timer läuft rückwärts', /⏱ (1:29|1:30):/.test(d.getElementById('tb-timer').textContent));
|
||||
const fill = sec().querySelector('input.fill');
|
||||
if (fill) { fill.value = 'test'; fill.dispatchEvent(new w.Event('input', { bubbles: true })); }
|
||||
else sec().querySelector('.options input').click();
|
||||
sec().querySelector('[data-flag]').click();
|
||||
await wait(100);
|
||||
t('Markierung gespeichert', s.answers[s.questions[0].id].flagged === true);
|
||||
if (stop === 'exam') return done();
|
||||
w.Reveal.slide(s.questions.length); await wait(400);
|
||||
t('Abgabe-Folie', sec().classList.contains('submit'));
|
||||
w.confirm = () => true;
|
||||
sec().querySelector('[data-act=submit]').click(); await wait(600);
|
||||
t('abgegeben, Ergebnis-Folie', s.submitted && sec().classList.contains('result'));
|
||||
t('Ergebnis zeigt Quote', /von \d+ richtig/.test(sec().textContent));
|
||||
w.Reveal.slide(0); await wait(400);
|
||||
t('nach Abgabe Auflösung sichtbar', !!sec().querySelector('.solution .verdict'));
|
||||
if (stop === 'result') { w.Reveal.slide(s.questions.length); await wait(400); return done(); }
|
||||
|
||||
// Import über das Datei-Feld: erst Fragenpaket, dann exportierte Sitzung
|
||||
const importJson = async (name, obj) => {
|
||||
const dt = new w.DataTransfer();
|
||||
dt.items.add(new w.File([JSON.stringify(obj)], name, { type: 'application/json' }));
|
||||
const input = d.getElementById('import-file');
|
||||
input.files = dt.files;
|
||||
const n = d.querySelectorAll('.toast').length;
|
||||
input.dispatchEvent(new w.Event('change', { bubbles: true }));
|
||||
// FileReader arbeitet asynchron: auf die Rückmeldung (Toast) warten, höchstens 5 s
|
||||
for (let i = 0; i < 50 && d.querySelectorAll('.toast').length === n; i++) await wait(100);
|
||||
await wait(300);
|
||||
};
|
||||
d.querySelector('[data-act=menu]').click(); await wait(400);
|
||||
const exam101 = w.LPICSim.ui.exams.find((e) => e.id === 'lpic1-101');
|
||||
const before = exam101.bank.length;
|
||||
const extra = { id: '101.1-990', objective: '101.1', type: 'fill', question: 'Testfrage', explanation: 'x', answers: ['ok'] };
|
||||
await importJson('paket.json', { format: 'lpic-sim-questions', exam: 'lpic1-101', questions: [extra, { ...extra, id: 'kaputt' }] });
|
||||
t('Paket-Import: 1 Frage ergänzt, 1 abgelehnt', exam101.bank.length === before + 1);
|
||||
t('Paket in localStorage gemerkt', (JSON.parse(w.localStorage.getItem('lpic_sim.packs')) || []).length === 1);
|
||||
t('Startseite zählt neue Frage', $('#setup').textContent.includes(`${before + 1} Fragen`));
|
||||
const saved = JSON.parse(JSON.stringify(s));
|
||||
saved.submitted = false; saved.current = 2; saved.answers = {};
|
||||
await importJson('sitzung.json', saved);
|
||||
t('Sitzungs-Import setzt an Frage 3 fort', Number(sec().dataset.index) === 2 && w.LPICSim.ui.session.mode === 'exam');
|
||||
await importJson('kaputt.json', { format: 'unbekannt' });
|
||||
t('Unbekanntes Format: Fehlermeldung', d.querySelector('.toast.error') !== null);
|
||||
w.localStorage.removeItem('lpic_sim.packs');
|
||||
} catch (e) {
|
||||
t(`Ausnahme: ${e && e.stack || e}`, false);
|
||||
}
|
||||
done();
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,352 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
@file build_bank.py
|
||||
@brief Prüft alle Fragendateien unter data/, erzeugt die verschlüsselte js/bank.js und optional ein Export-Verzeichnis
|
||||
@author Björn Runden <br@runden.de>
|
||||
@owner Björn Runden <br@runden.de>
|
||||
@assist KI-unterstützt (Claude Opus 5.5), geprüft von Björn Runden <br@runden.de>
|
||||
@version 0.3.0 (29.10.2026)
|
||||
@usage build_bank.py [-d] [-q] [-n] [-c] [--approved-only] [-o js/bank.js] [-e [DIR]] [-b [FILE]]
|
||||
|
||||
Aufbau von data/: je Prüfung ein Ordner mit exam.json und beliebig vielen questions_*.json (Quelltext, Klartext).
|
||||
js/bank.js enthält die Fragen nur ChaCha20-verschlüsselt (RFC 8439). Der Schlüssel wird aus einem PSK abgeleitet
|
||||
(Umgebung LPIC_SIM_PSK, sonst tools/.psk, sonst zufällig erzeugt) und steht als „build“-Kennung in bank.js –
|
||||
das verhindert Klartext auf der Platte, ist aber kein Schutz gegen gezielte Analyse (der Browser braucht den Schlüssel).
|
||||
Mit -n/--dry-run bzw. -c/--check wird nur geprüft und die Abdeckung je Objective ausgegeben.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import base64
|
||||
import hashlib
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import re
|
||||
import secrets
|
||||
import shutil
|
||||
import struct
|
||||
import sys
|
||||
from datetime import datetime, timezone
|
||||
from pathlib import Path
|
||||
|
||||
BASE = Path(__file__).resolve().parent.parent
|
||||
DATA = BASE / "data"
|
||||
OUT_DEFAULT = BASE / "js" / "bank.js"
|
||||
PSK_FILE = BASE / "tools" / ".psk"
|
||||
EXPORT_DEFAULT = BASE / "dist"
|
||||
EXPORT_MARKER = ".lpic-sim-export"
|
||||
EXPORT_ITEMS = ["index.html", "css", "js", "vendor"]
|
||||
|
||||
OPTION_IDS = ["a", "b", "c", "d", "e"]
|
||||
QID_RE = re.compile(r"^(\d{3}\.\d+)-\d{3}$")
|
||||
QUESTION_KEYS = {"id", "objective", "type", "lang", "difficulty", "question", "code", "options",
|
||||
"answers", "case_sensitive", "explanation", "references", "review"}
|
||||
REVIEW_STATES = {"pending", "approved", "rejected"}
|
||||
|
||||
log = logging.getLogger("build_bank")
|
||||
|
||||
|
||||
# ------------------------------------------------------------------ ChaCha20 (RFC 8439), nur Standardbibliothek
|
||||
|
||||
def _chacha20_block(key_words: tuple, counter: int, nonce_words: tuple) -> bytes:
|
||||
"""Ein 64-Byte-Keystream-Block nach RFC 8439, Abschnitt 2.3."""
|
||||
state = [0x61707865, 0x3320646E, 0x79622D32, 0x6B206574, *key_words, counter & 0xFFFFFFFF, *nonce_words]
|
||||
x = state[:]
|
||||
|
||||
def qr(a: int, b: int, c: int, d: int) -> None:
|
||||
x[a] = (x[a] + x[b]) & 0xFFFFFFFF; x[d] ^= x[a]; x[d] = ((x[d] << 16) | (x[d] >> 16)) & 0xFFFFFFFF
|
||||
x[c] = (x[c] + x[d]) & 0xFFFFFFFF; x[b] ^= x[c]; x[b] = ((x[b] << 12) | (x[b] >> 20)) & 0xFFFFFFFF
|
||||
x[a] = (x[a] + x[b]) & 0xFFFFFFFF; x[d] ^= x[a]; x[d] = ((x[d] << 8) | (x[d] >> 24)) & 0xFFFFFFFF
|
||||
x[c] = (x[c] + x[d]) & 0xFFFFFFFF; x[b] ^= x[c]; x[b] = ((x[b] << 7) | (x[b] >> 25)) & 0xFFFFFFFF
|
||||
|
||||
for _ in range(10):
|
||||
qr(0, 4, 8, 12); qr(1, 5, 9, 13); qr(2, 6, 10, 14); qr(3, 7, 11, 15)
|
||||
qr(0, 5, 10, 15); qr(1, 6, 11, 12); qr(2, 7, 8, 13); qr(3, 4, 9, 14)
|
||||
return struct.pack("<16L", *((x[i] + state[i]) & 0xFFFFFFFF for i in range(16)))
|
||||
|
||||
|
||||
def chacha20(key: bytes, nonce: bytes, data: bytes, counter: int = 1) -> bytes:
|
||||
"""Ver-/Entschlüsselt data mit ChaCha20 (32-Byte-Schlüssel, 12-Byte-Nonce). Returns: Ergebnis gleicher Länge."""
|
||||
kw, nw = struct.unpack("<8L", key), struct.unpack("<3L", nonce)
|
||||
out = bytearray(len(data))
|
||||
for off in range(0, len(data), 64):
|
||||
block = _chacha20_block(kw, counter + off // 64, nw)
|
||||
chunk = data[off:off + 64]
|
||||
out[off:off + len(chunk)] = bytes(a ^ b for a, b in zip(chunk, block))
|
||||
return bytes(out)
|
||||
|
||||
|
||||
def load_psk() -> str:
|
||||
"""PSK aus LPIC_SIM_PSK oder tools/.psk; fehlt beides, wird einer erzeugt und in tools/.psk abgelegt."""
|
||||
env = os.environ.get("LPIC_SIM_PSK", "").strip()
|
||||
if env:
|
||||
return env
|
||||
if PSK_FILE.is_file():
|
||||
psk = PSK_FILE.read_text(encoding="utf-8").strip()
|
||||
if psk:
|
||||
return psk
|
||||
psk = secrets.token_urlsafe(24)
|
||||
PSK_FILE.write_text(psk + "\n", encoding="utf-8")
|
||||
PSK_FILE.chmod(0o600)
|
||||
log.info("neuer PSK erzeugt: %s (nicht weitergeben)", PSK_FILE.relative_to(BASE))
|
||||
return psk
|
||||
|
||||
|
||||
def derive_key(psk: str) -> bytes:
|
||||
"""Schlüssel = SHA-256 über den PSK mit festem Kontext; so taucht der PSK selbst nirgends auf."""
|
||||
return hashlib.sha256(b"lpic-sim/bank/v1:" + psk.encode("utf-8")).digest()
|
||||
|
||||
|
||||
# ------------------------------------------------------------------ Prüfung der Fragen
|
||||
|
||||
def load_json(path: Path) -> dict:
|
||||
"""Liest eine JSON-Datei; Fehler werden mit Dateiname gemeldet."""
|
||||
try:
|
||||
return json.loads(path.read_text(encoding="utf-8"))
|
||||
except json.JSONDecodeError as exc:
|
||||
raise ValueError(f"{path.relative_to(BASE)}: ungültiges JSON ({exc})") from exc
|
||||
|
||||
|
||||
def review_state(q: dict) -> str:
|
||||
"""Prüfstatus einer Frage; ohne Eintrag 'pending'."""
|
||||
return (q.get("review") or {}).get("status", "pending")
|
||||
|
||||
|
||||
def check_question(q: dict, objectives: set[str], where: str) -> list[str]:
|
||||
"""Prüft eine Frage gegen die Erstellungsregeln. Returns: Liste der Fehlermeldungen."""
|
||||
err: list[str] = []
|
||||
qid = q.get("id", "?")
|
||||
p = f"{where} [{qid}]"
|
||||
unknown = set(q) - QUESTION_KEYS
|
||||
if unknown:
|
||||
err.append(f"{p}: unbekannte Schlüssel {sorted(unknown)}")
|
||||
m = QID_RE.match(qid)
|
||||
if not m:
|
||||
err.append(f"{p}: id muss <objective>-<NNN> sein")
|
||||
elif m.group(1) != q.get("objective"):
|
||||
err.append(f"{p}: id passt nicht zu objective {q.get('objective')}")
|
||||
if q.get("objective") not in objectives:
|
||||
err.append(f"{p}: objective {q.get('objective')} gibt es in der Prüfung nicht")
|
||||
for key in ("question", "explanation"):
|
||||
if not str(q.get(key, "")).strip():
|
||||
err.append(f"{p}: '{key}' fehlt oder ist leer")
|
||||
if review_state(q) not in REVIEW_STATES:
|
||||
err.append(f"{p}: review.status muss pending, approved oder rejected sein")
|
||||
qtype = q.get("type")
|
||||
if qtype in ("single", "multi"):
|
||||
opts = q.get("options") or []
|
||||
if [o.get("id") for o in opts] != OPTION_IDS:
|
||||
err.append(f"{p}: genau 5 Optionen mit ids a–e in dieser Reihenfolge erforderlich")
|
||||
texts = [o.get("text", "").strip() for o in opts]
|
||||
if len(set(texts)) != len(texts):
|
||||
err.append(f"{p}: doppelte Optionstexte")
|
||||
for o in opts:
|
||||
if not o.get("text", "").strip() or not o.get("explanation", "").strip():
|
||||
err.append(f"{p}: Option {o.get('id')} ohne Text oder Erläuterung")
|
||||
if not isinstance(o.get("correct"), bool):
|
||||
err.append(f"{p}: Option {o.get('id')}: 'correct' muss true/false sein")
|
||||
n_ok = sum(1 for o in opts if o.get("correct") is True)
|
||||
if qtype == "single" and n_ok != 1:
|
||||
err.append(f"{p}: single braucht genau 1 richtige Option (hat {n_ok})")
|
||||
if qtype == "multi" and not 2 <= n_ok <= 4:
|
||||
err.append(f"{p}: multi braucht 2–4 richtige Optionen (hat {n_ok})")
|
||||
if "answers" in q:
|
||||
err.append(f"{p}: 'answers' nur bei type fill")
|
||||
elif qtype == "fill":
|
||||
if not q.get("answers") or not all(str(a).strip() for a in q["answers"]):
|
||||
err.append(f"{p}: fill braucht nicht leere 'answers'")
|
||||
if "options" in q:
|
||||
err.append(f"{p}: 'options' nicht bei type fill")
|
||||
else:
|
||||
err.append(f"{p}: type muss single, multi oder fill sein")
|
||||
return err
|
||||
|
||||
|
||||
def load_exam(exam_dir: Path, approved_only: bool) -> tuple[dict, list[str]]:
|
||||
"""Lädt exam.json und alle questions_*.json eines Prüfungsordners. Returns: (Prüfung, Fehler)."""
|
||||
exam_doc = load_json(exam_dir / "exam.json")
|
||||
exam = dict(exam_doc["exam"])
|
||||
exam["topics"] = exam_doc["topics"]
|
||||
objectives = {o["id"] for t in exam["topics"] for o in t["objectives"]}
|
||||
errors: list[str] = []
|
||||
questions: list[dict] = []
|
||||
seen: set[str] = set()
|
||||
states = {s: 0 for s in sorted(REVIEW_STATES)}
|
||||
for qfile in sorted(exam_dir.glob("questions_*.json")):
|
||||
doc = load_json(qfile)
|
||||
where = str(qfile.relative_to(BASE))
|
||||
if doc.get("format") != "lpic-sim-questions":
|
||||
errors.append(f"{where}: format muss 'lpic-sim-questions' sein")
|
||||
if doc.get("exam") != exam["id"]:
|
||||
errors.append(f"{where}: exam '{doc.get('exam')}' passt nicht zu '{exam['id']}'")
|
||||
for q in doc.get("questions", []):
|
||||
errors += check_question(q, objectives, where)
|
||||
if q.get("id") in seen:
|
||||
errors.append(f"{where}: doppelte id {q.get('id')}")
|
||||
seen.add(q.get("id"))
|
||||
state = review_state(q)
|
||||
states[state] = states.get(state, 0) + 1
|
||||
if state == "rejected" or (approved_only and state != "approved"):
|
||||
continue
|
||||
q = {k: v for k, v in q.items() if k != "review"} # Prüfvermerke gehören nicht in die Bank
|
||||
q.setdefault("lang", exam.get("lang", "de"))
|
||||
questions.append(q)
|
||||
questions.sort(key=lambda q: (tuple(int(x) for x in q["objective"].split(".")), q["id"]))
|
||||
exam["questions_bank"] = questions
|
||||
exam["review_states"] = states
|
||||
return exam, errors
|
||||
|
||||
|
||||
def coverage(exam: dict) -> None:
|
||||
"""Gibt Prüfstatus und die Anzahl der Fragen je Objective im Verhältnis zur Gewichtung aus."""
|
||||
counts: dict[str, int] = {}
|
||||
for q in exam["questions_bank"]:
|
||||
counts[q["objective"]] = counts.get(q["objective"], 0) + 1
|
||||
st = exam["review_states"]
|
||||
log.info("%s: %d Fragen in der Bank (Prüfstatus: %d approved, %d pending, %d rejected)", exam["id"],
|
||||
len(exam["questions_bank"]), st.get("approved", 0), st.get("pending", 0), st.get("rejected", 0))
|
||||
for t in exam["topics"]:
|
||||
for o in t["objectives"]:
|
||||
n = counts.get(o["id"], 0)
|
||||
flag = "" if n >= o["weight"] else " <- weniger Fragen als Gewicht (Prüfungsmodus füllt auf)"
|
||||
log.info(" %-6s Gewicht %d Fragen %3d%s", o["id"], o["weight"], n, flag)
|
||||
|
||||
|
||||
# ------------------------------------------------------------------ Ausgabe
|
||||
|
||||
def write_bank(exams: list[dict], output: Path) -> None:
|
||||
"""Schreibt bank.js: Metadaten im Klartext, Prüfungen und Fragen ChaCha20-verschlüsselt (Base64)."""
|
||||
generated = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
|
||||
# exam["questions"] bleibt die Fragenzahl der Prüfung, die Fragen selbst stehen unter "bank"
|
||||
plain = json.dumps({"exams": [{**{k: v for k, v in e.items() if k not in ("questions_bank", "review_states")},
|
||||
"bank": e["questions_bank"]} for e in exams]},
|
||||
ensure_ascii=False, separators=(",", ":")).encode("utf-8")
|
||||
key, nonce = derive_key(load_psk()), secrets.token_bytes(12)
|
||||
bank = {
|
||||
"generated": generated,
|
||||
"build": key.hex(), # Schlüssel, getarnt als Build-Kennung
|
||||
"rev": nonce.hex(), # Nonce
|
||||
"payload": base64.b64encode(chacha20(key, nonce, plain)).decode("ascii"),
|
||||
}
|
||||
header = ("/**\n * @file bank.js\n * @brief Fragenbank (verschlüsselt) – ERZEUGT von tools/build_bank.py, nicht von Hand bearbeiten\n"
|
||||
" * @author Björn Runden <br@runden.de>\n * @owner Björn Runden <br@runden.de>\n"
|
||||
f" * @version {generated}\n */\n")
|
||||
output.write_text(header + "window.LPIC_BANK = " + json.dumps(bank, indent=1) + ";\n", encoding="utf-8")
|
||||
|
||||
|
||||
def export_site(target: Path, dry_run: bool) -> None:
|
||||
"""Kopiert nur die Laufzeitdateien (ohne data/, tools/, tests/) nach target.
|
||||
Ein vorhandenes Ziel wird nur ersetzt, wenn es von hier stammt (Markierungsdatei)."""
|
||||
target = target.resolve()
|
||||
if target == BASE or BASE in target.parents and target.name in EXPORT_ITEMS + ["data", "tools", "tests"]:
|
||||
raise ValueError(f"Export-Ziel {target} liegt im Quellbaum")
|
||||
if target.exists() and any(target.iterdir()) and not (target / EXPORT_MARKER).exists():
|
||||
raise ValueError(f"Export-Ziel {target} ist nicht leer und kein früherer Export – bitte prüfen")
|
||||
if dry_run:
|
||||
log.info("Trockenlauf: würde %s nach %s exportieren", ", ".join(EXPORT_ITEMS), target)
|
||||
return
|
||||
if target.exists():
|
||||
shutil.rmtree(target)
|
||||
target.mkdir(parents=True)
|
||||
for item in EXPORT_ITEMS:
|
||||
src = BASE / item
|
||||
(shutil.copytree if src.is_dir() else shutil.copy2)(src, target / item)
|
||||
(target / EXPORT_MARKER).write_text("Export von simulator/tools/build_bank.py – Verzeichnis wird beim nächsten Export ersetzt\n",
|
||||
encoding="utf-8")
|
||||
log.info("Export nach %s geschrieben (Start: python3 -m http.server 8080 -d %s)", target, target)
|
||||
|
||||
|
||||
def bundle_excluded(rel: Path) -> bool:
|
||||
"""Nicht ins Prüferpaket: PSK, Erzeugtes (Export, Pakete), Caches, Sicherungen."""
|
||||
parts = rel.parts
|
||||
return (rel.as_posix() == "tools/.psk" or parts[0] in ("dist", "release")
|
||||
or "__pycache__" in parts or rel.name.endswith(".orig"))
|
||||
|
||||
|
||||
def write_bundle(target: Path, dry_run: bool) -> None:
|
||||
"""Packt den kompletten Quellstand (Fragen, Doku, Compiler, Tests, Laufzeit) als tar.gz mit Wurzel lpic-sim/."""
|
||||
import tarfile
|
||||
files = sorted(p for p in BASE.rglob("*") if p.is_file() and not bundle_excluded(p.relative_to(BASE)))
|
||||
if dry_run:
|
||||
log.info("Trockenlauf: würde %d Dateien nach %s packen", len(files), target)
|
||||
return
|
||||
target.parent.mkdir(parents=True, exist_ok=True)
|
||||
with tarfile.open(target, "w:gz") as tar:
|
||||
for p in files:
|
||||
tar.add(p, arcname=f"lpic-sim/{p.relative_to(BASE).as_posix()}", recursive=False)
|
||||
log.info("Prüferpaket %s geschrieben (%d Dateien, %d KiB)", target, len(files), target.stat().st_size // 1024)
|
||||
|
||||
|
||||
def main() -> int:
|
||||
ap = argparse.ArgumentParser(description="Fragenbank prüfen, verschlüsselte js/bank.js erzeugen, optional exportieren")
|
||||
ap.add_argument("-d", "--debug", action="store_true", help="Debug-Ausgaben")
|
||||
ap.add_argument("-q", "--quiet", action="store_true", help="nur Fehler ausgeben")
|
||||
ap.add_argument("-n", "--dry-run", action="store_true", help="nur prüfen, nichts schreiben")
|
||||
ap.add_argument("-c", "--check", action="store_true", help="wie --dry-run, mit Abdeckungsbericht")
|
||||
ap.add_argument("--approved-only", action="store_true", help="nur Fragen mit review.status approved übernehmen")
|
||||
ap.add_argument("-o", "--output", type=Path, default=OUT_DEFAULT, help="Zieldatei (Default js/bank.js)")
|
||||
ap.add_argument("-e", "--export", type=Path, nargs="?", const=EXPORT_DEFAULT, metavar="DIR",
|
||||
help=f"Laufzeitdateien nach DIR exportieren (Default {EXPORT_DEFAULT.relative_to(BASE)})")
|
||||
ap.add_argument("-b", "--bundle", type=Path, nargs="?", metavar="FILE",
|
||||
const=BASE / "release" / f"lpic-sim_{datetime.now():%Y-%m-%d}.tar.gz",
|
||||
help="Prüferpaket (tar.gz, ohne PSK und Erzeugtes) schreiben; Default release/lpic-sim_<datum>.tar.gz")
|
||||
args = ap.parse_args()
|
||||
level = logging.DEBUG if args.debug else logging.ERROR if args.quiet else logging.INFO
|
||||
logging.basicConfig(level=level, format="%(levelname)s %(message)s")
|
||||
|
||||
exams: list[dict] = []
|
||||
errors: list[str] = []
|
||||
try:
|
||||
for exam_file in sorted(DATA.glob("*/exam.json")):
|
||||
exam, errs = load_exam(exam_file.parent, args.approved_only)
|
||||
exams.append(exam)
|
||||
errors += errs
|
||||
except (ValueError, KeyError) as exc:
|
||||
log.error("%s", exc)
|
||||
return 1
|
||||
for e in errors:
|
||||
log.error("%s", e)
|
||||
if errors:
|
||||
log.error("%d Fehler – bank.js nicht geschrieben", len(errors))
|
||||
return 1
|
||||
exams.sort(key=lambda e: (e.get("level", ""), e.get("order", 0)))
|
||||
total = sum(len(e["questions_bank"]) for e in exams)
|
||||
if args.check or args.debug:
|
||||
for exam in exams:
|
||||
coverage(exam)
|
||||
if args.dry_run or args.check:
|
||||
log.info("Prüfung ok: %d Prüfungen, %d Fragen", len(exams), total)
|
||||
if args.export:
|
||||
try:
|
||||
export_site(args.export, True)
|
||||
except ValueError as exc:
|
||||
log.error("%s", exc)
|
||||
return 1
|
||||
if args.bundle:
|
||||
write_bundle(args.bundle, True)
|
||||
return 0
|
||||
if total == 0:
|
||||
log.error("keine Fragen für die Bank (bei --approved-only: noch nichts freigegeben?)")
|
||||
return 1
|
||||
|
||||
write_bank(exams, args.output)
|
||||
log.info("%s geschrieben (verschlüsselt): %d Prüfungen, %d Fragen", args.output, len(exams), total)
|
||||
if args.export:
|
||||
try:
|
||||
export_site(args.export, False)
|
||||
except (ValueError, OSError) as exc:
|
||||
log.error("Export fehlgeschlagen: %s", exc)
|
||||
return 1
|
||||
if args.bundle:
|
||||
try:
|
||||
write_bundle(args.bundle, False)
|
||||
except OSError as exc:
|
||||
log.error("Prüferpaket fehlgeschlagen: %s", exc)
|
||||
return 1
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
Vendored
+19
@@ -0,0 +1,19 @@
|
||||
Copyright (C) 2011-2024 Hakim El Hattab, http://hakim.se, and reveal.js contributors
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
Vendored
+3
@@ -0,0 +1,3 @@
|
||||
reveal.js 5.1.0 (dist), Quelle: https://github.com/hakimel/reveal.js/archive/refs/tags/5.1.0.tar.gz
|
||||
Abgerufen: 2026-10-29, sha256 (tar.gz): ddc83539ec50583eac9a972e88f892971b37c44e70dd0c08be069e2688684b71
|
||||
Unverändert übernommen: reveal.js, reveal.css, reset.css, theme/dracula.css, LICENSE (MIT)
|
||||
Vendored
+30
@@ -0,0 +1,30 @@
|
||||
/* http://meyerweb.com/eric/tools/css/reset/
|
||||
v4.0 | 20180602
|
||||
License: none (public domain)
|
||||
*/
|
||||
|
||||
html, body, div, span, applet, object, iframe,
|
||||
h1, h2, h3, h4, h5, h6, p, blockquote, pre,
|
||||
a, abbr, acronym, address, big, cite, code,
|
||||
del, dfn, em, img, ins, kbd, q, s, samp,
|
||||
small, strike, strong, sub, sup, tt, var,
|
||||
b, u, i, center,
|
||||
dl, dt, dd, ol, ul, li,
|
||||
fieldset, form, label, legend,
|
||||
table, caption, tbody, tfoot, thead, tr, th, td,
|
||||
article, aside, canvas, details, embed,
|
||||
figure, figcaption, footer, header, hgroup,
|
||||
main, menu, nav, output, ruby, section, summary,
|
||||
time, mark, audio, video {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
border: 0;
|
||||
font-size: 100%;
|
||||
font: inherit;
|
||||
vertical-align: baseline;
|
||||
}
|
||||
/* HTML5 display-role reset for older browsers */
|
||||
article, aside, details, figcaption, figure,
|
||||
footer, header, hgroup, main, menu, nav, section {
|
||||
display: block;
|
||||
}
|
||||
Vendored
+8
@@ -0,0 +1,8 @@
|
||||
/*!
|
||||
* reveal.js 5.1.0
|
||||
* https://revealjs.com
|
||||
* MIT licensed
|
||||
*
|
||||
* Copyright (C) 2011-2024 Hakim El Hattab, https://hakim.se
|
||||
*/
|
||||
.reveal .r-stretch,.reveal .stretch{max-width:none;max-height:none}.reveal pre.r-stretch code,.reveal pre.stretch code{height:100%;max-height:100%;box-sizing:border-box}.reveal .r-fit-text{display:inline-block;white-space:nowrap}.reveal .r-stack{display:grid;grid-template-rows:100%}.reveal .r-stack>*{grid-area:1/1;margin:auto}.reveal .r-hstack,.reveal .r-vstack{display:flex}.reveal .r-hstack img,.reveal .r-hstack video,.reveal .r-vstack img,.reveal .r-vstack video{min-width:0;min-height:0;object-fit:contain}.reveal .r-vstack{flex-direction:column;align-items:center;justify-content:center}.reveal .r-hstack{flex-direction:row;align-items:center;justify-content:center}.reveal .items-stretch{align-items:stretch}.reveal .items-start{align-items:flex-start}.reveal .items-center{align-items:center}.reveal .items-end{align-items:flex-end}.reveal .justify-between{justify-content:space-between}.reveal .justify-around{justify-content:space-around}.reveal .justify-start{justify-content:flex-start}.reveal .justify-center{justify-content:center}.reveal .justify-end{justify-content:flex-end}html.reveal-full-page{width:100%;height:100%;height:100vh;height:calc(var(--vh,1vh) * 100);height:100svh;overflow:hidden}.reveal-viewport{height:100%;overflow:hidden;position:relative;line-height:1;margin:0;background-color:#fff;color:#000;--r-controls-spacing:12px}.reveal-viewport:fullscreen{top:0!important;left:0!important;width:100%!important;height:100%!important;transform:none!important}.reveal .fragment{transition:all .2s ease}.reveal .fragment:not(.custom){opacity:0;visibility:hidden;will-change:opacity}.reveal .fragment.visible{opacity:1;visibility:inherit}.reveal .fragment.disabled{transition:none}.reveal .fragment.grow{opacity:1;visibility:inherit}.reveal .fragment.grow.visible{transform:scale(1.3)}.reveal .fragment.shrink{opacity:1;visibility:inherit}.reveal .fragment.shrink.visible{transform:scale(.7)}.reveal .fragment.zoom-in{transform:scale(.1)}.reveal .fragment.zoom-in.visible{transform:none}.reveal .fragment.fade-out{opacity:1;visibility:inherit}.reveal .fragment.fade-out.visible{opacity:0;visibility:hidden}.reveal .fragment.semi-fade-out{opacity:1;visibility:inherit}.reveal .fragment.semi-fade-out.visible{opacity:.5;visibility:inherit}.reveal .fragment.strike{opacity:1;visibility:inherit}.reveal .fragment.strike.visible{text-decoration:line-through}.reveal .fragment.fade-up{transform:translate(0,40px)}.reveal .fragment.fade-up.visible{transform:translate(0,0)}.reveal .fragment.fade-down{transform:translate(0,-40px)}.reveal .fragment.fade-down.visible{transform:translate(0,0)}.reveal .fragment.fade-right{transform:translate(-40px,0)}.reveal .fragment.fade-right.visible{transform:translate(0,0)}.reveal .fragment.fade-left{transform:translate(40px,0)}.reveal .fragment.fade-left.visible{transform:translate(0,0)}.reveal .fragment.current-visible,.reveal .fragment.fade-in-then-out{opacity:0;visibility:hidden}.reveal .fragment.current-visible.current-fragment,.reveal .fragment.fade-in-then-out.current-fragment{opacity:1;visibility:inherit}.reveal .fragment.fade-in-then-semi-out{opacity:0;visibility:hidden}.reveal .fragment.fade-in-then-semi-out.visible{opacity:.5;visibility:inherit}.reveal .fragment.fade-in-then-semi-out.current-fragment{opacity:1;visibility:inherit}.reveal .fragment.highlight-blue,.reveal .fragment.highlight-current-blue,.reveal .fragment.highlight-current-green,.reveal .fragment.highlight-current-red,.reveal .fragment.highlight-green,.reveal .fragment.highlight-red{opacity:1;visibility:inherit}.reveal .fragment.highlight-red.visible{color:#ff2c2d}.reveal .fragment.highlight-green.visible{color:#17ff2e}.reveal .fragment.highlight-blue.visible{color:#1b91ff}.reveal .fragment.highlight-current-red.current-fragment{color:#ff2c2d}.reveal .fragment.highlight-current-green.current-fragment{color:#17ff2e}.reveal .fragment.highlight-current-blue.current-fragment{color:#1b91ff}.reveal:after{content:"";font-style:italic}.reveal iframe{z-index:1}.reveal a{position:relative}@keyframes bounce-right{0%,10%,25%,40%,50%{transform:translateX(0)}20%{transform:translateX(10px)}30%{transform:translateX(-5px)}}@keyframes bounce-left{0%,10%,25%,40%,50%{transform:translateX(0)}20%{transform:translateX(-10px)}30%{transform:translateX(5px)}}@keyframes bounce-down{0%,10%,25%,40%,50%{transform:translateY(0)}20%{transform:translateY(10px)}30%{transform:translateY(-5px)}}.reveal .controls{display:none;position:absolute;top:auto;bottom:var(--r-controls-spacing);right:var(--r-controls-spacing);left:auto;z-index:11;color:#000;pointer-events:none;font-size:10px}.reveal .controls button{position:absolute;padding:0;background-color:transparent;border:0;outline:0;cursor:pointer;color:currentColor;transform:scale(.9999);transition:color .2s ease,opacity .2s ease,transform .2s ease;z-index:2;pointer-events:auto;font-size:inherit;visibility:hidden;opacity:0;-webkit-appearance:none;-webkit-tap-highlight-color:transparent}.reveal .controls .controLine truncated
|
||||
Vendored
+9
@@ -0,0 +1,9 @@
|
||||
/*!
|
||||
* reveal.js 5.1.0
|
||||
* https://revealjs.com
|
||||
* MIT licensed
|
||||
*
|
||||
* Copyright (C) 2011-2024 Hakim El Hattab, https://hakim.se
|
||||
*/
|
||||
!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?module.exports=t():"function"==typeof define&&define.amd?define(t):(e="undefined"!=typeof globalThis?globalThis:e||self).Reveal=t()}(this,(function(){"use strict";const e=(e,t)=>{for(let i in t)e[i]=t[i];return e},t=(e,t)=>Array.from(e.querySelectorAll(t)),i=(e,t,i)=>{i?e.classList.add(t):e.classList.remove(t)},s=e=>{if("string"==typeof e){if("null"===e)return null;if("true"===e)return!0;if("false"===e)return!1;if(e.match(/^-?[\d\.]+$/))return parseFloat(e)}return e},a=(e,t)=>{e.style.transform=t},n=(e,t)=>{let i=e.matches||e.matchesSelector||e.msMatchesSelector;return!(!i||!i.call(e,t))},r=(e,t)=>{if("function"==typeof e.closest)return e.closest(t);for(;e;){if(n(e,t))return e;e=e.parentNode}return null},o=e=>{let t=(e=e||document.documentElement).requestFullscreen||e.webkitRequestFullscreen||e.webkitRequestFullScreen||e.mozRequestFullScreen||e.msRequestFullscreen;t&&t.apply(e)},l=e=>{let t=document.createElement("style");return t.type="text/css",e&&e.length>0&&(t.styleSheet?t.styleSheet.cssText=e:t.appendChild(document.createTextNode(e))),document.head.appendChild(t),t},d=()=>{let e={};location.search.replace(/[A-Z0-9]+?=([\w\.%-]*)/gi,(t=>{e[t.split("=").shift()]=t.split("=").pop()}));for(let t in e){let i=e[t];e[t]=s(unescape(i))}return void 0!==e.dependencies&&delete e.dependencies,e},c={mp4:"video/mp4",m4a:"video/mp4",ogv:"video/ogg",mpeg:"video/mpeg",webm:"video/webm"},h=navigator.userAgent,u=/(iphone|ipod|ipad|android)/gi.test(h)||"MacIntel"===navigator.platform&&navigator.maxTouchPoints>1,g=/android/gi.test(h);var p=function(e){if(e){var t=function(e){return[].slice.call(e)},i=3,s=[],a=null,n="requestAnimationFrame"in e?function(){e.cancelAnimationFrame(a),a=e.requestAnimationFrame((function(){return o(s.filter((function(e){return e.dirty&&e.active})))}))}:function(){},r=function(e){return function(){s.forEach((function(t){return t.dirty=e})),n()}},o=function(e){e.filter((function(e){return!e.styleComputed})).forEach((function(e){e.styleComputed=h(e)})),e.filter(u).forEach(g);var t=e.filter(c);t.forEach(d),t.forEach((function(e){g(e),l(e)})),t.forEach(p)},l=function(e){return e.dirty=0},d=function(e){e.availableWidth=e.element.parentNode.clientWidth,e.currentWidth=e.element.scrollWidth,e.previousFontSize=e.currentFontSize,e.currentFontSize=Math.min(Math.max(e.minSize,e.availableWidth/e.currentWidth*e.previousFontSize),e.maxSize),e.whiteSpace=e.multiLine&&e.currentFontSize===e.minSize?"normal":"nowrap"},c=function(e){return 2!==e.dirty||2===e.dirty&&e.element.parentNode.clientWidth!==e.availableWidth},h=function(t){var i=e.getComputedStyle(t.element,null);return t.currentFontSize=parseFloat(i.getPropertyValue("font-size")),t.display=i.getPropertyValue("display"),t.whiteSpace=i.getPropertyValue("white-space"),!0},u=function(e){var t=!1;return!e.preStyleTestCompleted&&(/inline-/.test(e.display)||(t=!0,e.display="inline-block"),"nowrap"!==e.whiteSpace&&(t=!0,e.whiteSpace="nowrap"),e.preStyleTestCompleted=!0,t)},g=function(e){e.element.style.whiteSpace=e.whiteSpace,e.element.style.display=e.display,e.element.style.fontSize=e.currentFontSize+"px"},p=function(e){e.element.dispatchEvent(new CustomEvent("fit",{detail:{oldValue:e.previousFontSize,newValue:e.currentFontSize,scaleFactor:e.currentFontSize/e.previousFontSize}}))},v=function(e,t){return function(){e.dirty=t,e.active&&n()}},m=function(e){return function(){s=s.filter((function(t){return t.element!==e.element})),e.observeMutations&&e.observer.disconnect(),e.element.style.whiteSpace=e.originalStyle.whiteSpace,e.element.style.display=e.originalStyle.display,e.element.style.fontSize=e.originalStyle.fontSize}},f=function(e){return function(){e.active||(e.active=!0,n())}},y=function(e){return function(){return e.active=!1}},b=function(e){e.observeMutations&&(e.observer=new MutationObserver(v(e,1)),e.observer.observe(e.element,e.observeMutations))},w={minSize:16,maxSize:512,multiLine:!0,observeMutations:"MutationObserver"in e&&{subtree:!0,childList:!0,characterData:!0}},E=null,S=function(){e.clearTimeout(E),E=e.setTimeout(r(2),k.observeWindowDelay)},A=["resize","orientationchange"];return Object.defineProperty(k,"observeWindow",{set:function(t){var i="".concat(t?"add":"remove","EventListener");A.forEach((function(t){e[i](t,S)}))}}),k.observeWindow=!0,k.observeWindowDelay=100,k.fitAll=r(i),k}function R(e,t){var a=Object.assign({},w,t),r=e.map((function(e){var t=Object.assign({},a,{element:e,active:!0});return function(e){e.originalStyle={whiteSpace:e.element.style.whiteSpace,display:e.element.style.display,fontSize:e.element.style.fontSize},b(e),e.newbie=!0,e.dirty=!0,s.push(e)}(t),{element:e,fit:v(t,i),unfreeze:f(t),freeze:y(t),unsubscribe:m(t)}}));return n(),r}function k(e){var i=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{};return"string"==typeof e?R(t(document.querySelectorAll(e)),i):R([e],i)[0]}}("undefined"==typeof window?null:window);class v{constructor(e){this.Reveal=e,Line truncated
|
||||
//# sourceMappingURL=reveal.js.map
|
||||
+385
@@ -0,0 +1,385 @@
|
||||
/**
|
||||
* Dracula Dark theme for reveal.js.
|
||||
* Based on https://draculatheme.com
|
||||
*/
|
||||
/**
|
||||
* Dracula colors by Zeno Rocha
|
||||
* https://draculatheme.com/contribute
|
||||
*/
|
||||
html * {
|
||||
color-profile: sRGB;
|
||||
rendering-intent: auto;
|
||||
}
|
||||
|
||||
section.has-light-background, section.has-light-background h1, section.has-light-background h2, section.has-light-background h3, section.has-light-background h4, section.has-light-background h5, section.has-light-background h6 {
|
||||
color: #282A36;
|
||||
}
|
||||
|
||||
/*********************************************
|
||||
* GLOBAL STYLES
|
||||
*********************************************/
|
||||
:root {
|
||||
--r-background-color: #282A36;
|
||||
--r-main-font: -apple-system, BlinkMacSystemFont, avenir next, avenir, segoe ui, helvetica neue, helvetica, Cantarell, Ubuntu, roboto, noto, arial, sans-serif;
|
||||
--r-main-font-size: 40px;
|
||||
--r-main-color: #F8F8F2;
|
||||
--r-block-margin: 20px;
|
||||
--r-heading-margin: 0 0 20px 0;
|
||||
--r-heading-font: League Gothic, Impact, sans-serif;
|
||||
--r-heading-color: #BD93F9;
|
||||
--r-heading-line-height: 1.2;
|
||||
--r-heading-letter-spacing: normal;
|
||||
--r-heading-text-transform: none;
|
||||
--r-heading-text-shadow: none;
|
||||
--r-heading-font-weight: normal;
|
||||
--r-heading1-text-shadow: none;
|
||||
--r-heading1-size: 3.77em;
|
||||
--r-heading2-size: 2.11em;
|
||||
--r-heading3-size: 1.55em;
|
||||
--r-heading4-size: 1em;
|
||||
--r-code-font: Fira Code, Menlo, Consolas, Monaco, Liberation Mono, Lucida Console, monospace;
|
||||
--r-link-color: #FF79C6;
|
||||
--r-link-color-dark: #ff2da5;
|
||||
--r-link-color-hover: #8BE9FD;
|
||||
--r-selection-background-color: #44475A;
|
||||
--r-selection-color: #fff;
|
||||
--r-overlay-element-bg-color: 240, 240, 240;
|
||||
--r-overlay-element-fg-color: 0, 0, 0;
|
||||
}
|
||||
|
||||
.reveal-viewport {
|
||||
background: #282A36;
|
||||
background-color: var(--r-background-color);
|
||||
}
|
||||
|
||||
.reveal {
|
||||
font-family: var(--r-main-font);
|
||||
font-size: var(--r-main-font-size);
|
||||
font-weight: normal;
|
||||
color: var(--r-main-color);
|
||||
}
|
||||
|
||||
.reveal ::selection {
|
||||
color: var(--r-selection-color);
|
||||
background: var(--r-selection-background-color);
|
||||
text-shadow: none;
|
||||
}
|
||||
|
||||
.reveal ::-moz-selection {
|
||||
color: var(--r-selection-color);
|
||||
background: var(--r-selection-background-color);
|
||||
text-shadow: none;
|
||||
}
|
||||
|
||||
.reveal .slides section,
|
||||
.reveal .slides section > section {
|
||||
line-height: 1.3;
|
||||
font-weight: inherit;
|
||||
}
|
||||
|
||||
/*********************************************
|
||||
* HEADERS
|
||||
*********************************************/
|
||||
.reveal h1,
|
||||
.reveal h2,
|
||||
.reveal h3,
|
||||
.reveal h4,
|
||||
.reveal h5,
|
||||
.reveal h6 {
|
||||
margin: var(--r-heading-margin);
|
||||
color: var(--r-heading-color);
|
||||
font-family: var(--r-heading-font);
|
||||
font-weight: var(--r-heading-font-weight);
|
||||
line-height: var(--r-heading-line-height);
|
||||
letter-spacing: var(--r-heading-letter-spacing);
|
||||
text-transform: var(--r-heading-text-transform);
|
||||
text-shadow: var(--r-heading-text-shadow);
|
||||
word-wrap: break-word;
|
||||
}
|
||||
|
||||
.reveal h1 {
|
||||
font-size: var(--r-heading1-size);
|
||||
}
|
||||
|
||||
.reveal h2 {
|
||||
font-size: var(--r-heading2-size);
|
||||
}
|
||||
|
||||
.reveal h3 {
|
||||
font-size: var(--r-heading3-size);
|
||||
}
|
||||
|
||||
.reveal h4 {
|
||||
font-size: var(--r-heading4-size);
|
||||
}
|
||||
|
||||
.reveal h1 {
|
||||
text-shadow: var(--r-heading1-text-shadow);
|
||||
}
|
||||
|
||||
/*********************************************
|
||||
* OTHER
|
||||
*********************************************/
|
||||
.reveal p {
|
||||
margin: var(--r-block-margin) 0;
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
/* Remove trailing margins after titles */
|
||||
.reveal h1:last-child,
|
||||
.reveal h2:last-child,
|
||||
.reveal h3:last-child,
|
||||
.reveal h4:last-child,
|
||||
.reveal h5:last-child,
|
||||
.reveal h6:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
/* Ensure certain elements are never larger than the slide itself */
|
||||
.reveal img,
|
||||
.reveal video,
|
||||
.reveal iframe {
|
||||
max-width: 95%;
|
||||
max-height: 95%;
|
||||
}
|
||||
|
||||
.reveal strong,
|
||||
.reveal b {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.reveal em {
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.reveal ol,
|
||||
.reveal dl,
|
||||
.reveal ul {
|
||||
display: inline-block;
|
||||
text-align: left;
|
||||
margin: 0 0 0 1em;
|
||||
}
|
||||
|
||||
.reveal ol {
|
||||
list-style-type: decimal;
|
||||
}
|
||||
|
||||
.reveal ul {
|
||||
list-style-type: disc;
|
||||
}
|
||||
|
||||
.reveal ul ul {
|
||||
list-style-type: square;
|
||||
}
|
||||
|
||||
.reveal ul ul ul {
|
||||
list-style-type: circle;
|
||||
}
|
||||
|
||||
.reveal ul ul,
|
||||
.reveal ul ol,
|
||||
.reveal ol ol,
|
||||
.reveal ol ul {
|
||||
display: block;
|
||||
margin-left: 40px;
|
||||
}
|
||||
|
||||
.reveal dt {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.reveal dd {
|
||||
margin-left: 40px;
|
||||
}
|
||||
|
||||
.reveal blockquote {
|
||||
display: block;
|
||||
position: relative;
|
||||
width: 70%;
|
||||
margin: var(--r-block-margin) auto;
|
||||
padding: 5px;
|
||||
font-style: italic;
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
box-shadow: 0px 0px 2px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.reveal blockquote p:first-child,
|
||||
.reveal blockquote p:last-child {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.reveal q {
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.reveal pre {
|
||||
display: block;
|
||||
position: relative;
|
||||
width: 90%;
|
||||
margin: var(--r-block-margin) auto;
|
||||
text-align: left;
|
||||
font-size: 0.55em;
|
||||
font-family: var(--r-code-font);
|
||||
line-height: 1.2em;
|
||||
word-wrap: break-word;
|
||||
box-shadow: 0px 5px 15px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
.reveal code {
|
||||
font-family: var(--r-code-font);
|
||||
text-transform: none;
|
||||
tab-size: 2;
|
||||
}
|
||||
|
||||
.reveal pre code {
|
||||
display: block;
|
||||
padding: 5px;
|
||||
overflow: auto;
|
||||
max-height: 400px;
|
||||
word-wrap: normal;
|
||||
}
|
||||
|
||||
.reveal .code-wrapper {
|
||||
white-space: normal;
|
||||
}
|
||||
|
||||
.reveal .code-wrapper code {
|
||||
white-space: pre;
|
||||
}
|
||||
|
||||
.reveal table {
|
||||
margin: auto;
|
||||
border-collapse: collapse;
|
||||
border-spacing: 0;
|
||||
}
|
||||
|
||||
.reveal table th {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.reveal table th,
|
||||
.reveal table td {
|
||||
text-align: left;
|
||||
padding: 0.2em 0.5em 0.2em 0.5em;
|
||||
border-bottom: 1px solid;
|
||||
}
|
||||
|
||||
.reveal table th[align=center],
|
||||
.reveal table td[align=center] {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.reveal table th[align=right],
|
||||
.reveal table td[align=right] {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.reveal table tbody tr:last-child th,
|
||||
.reveal table tbody tr:last-child td {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.reveal sup {
|
||||
vertical-align: super;
|
||||
font-size: smaller;
|
||||
}
|
||||
|
||||
.reveal sub {
|
||||
vertical-align: sub;
|
||||
font-size: smaller;
|
||||
}
|
||||
|
||||
.reveal small {
|
||||
display: inline-block;
|
||||
font-size: 0.6em;
|
||||
line-height: 1.2em;
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
.reveal small * {
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
.reveal img {
|
||||
margin: var(--r-block-margin) 0;
|
||||
}
|
||||
|
||||
/*********************************************
|
||||
* LINKS
|
||||
*********************************************/
|
||||
.reveal a {
|
||||
color: var(--r-link-color);
|
||||
text-decoration: none;
|
||||
transition: color 0.15s ease;
|
||||
}
|
||||
|
||||
.reveal a:hover {
|
||||
color: var(--r-link-color-hover);
|
||||
text-shadow: none;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.reveal .roll span:after {
|
||||
color: #fff;
|
||||
background: var(--r-link-color-dark);
|
||||
}
|
||||
|
||||
/*********************************************
|
||||
* Frame helper
|
||||
*********************************************/
|
||||
.reveal .r-frame {
|
||||
border: 4px solid var(--r-main-color);
|
||||
box-shadow: 0 0 10px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
.reveal a .r-frame {
|
||||
transition: all 0.15s linear;
|
||||
}
|
||||
|
||||
.reveal a:hover .r-frame {
|
||||
border-color: var(--r-link-color);
|
||||
box-shadow: 0 0 20px rgba(0, 0, 0, 0.55);
|
||||
}
|
||||
|
||||
/*********************************************
|
||||
* NAVIGATION CONTROLS
|
||||
*********************************************/
|
||||
.reveal .controls {
|
||||
color: var(--r-link-color);
|
||||
}
|
||||
|
||||
/*********************************************
|
||||
* PROGRESS BAR
|
||||
*********************************************/
|
||||
.reveal .progress {
|
||||
background: rgba(0, 0, 0, 0.2);
|
||||
color: var(--r-link-color);
|
||||
}
|
||||
|
||||
/*********************************************
|
||||
* PRINT BACKGROUND
|
||||
*********************************************/
|
||||
@media print {
|
||||
.backgrounds {
|
||||
background-color: var(--r-background-color);
|
||||
}
|
||||
}
|
||||
:root {
|
||||
--r-bold-color: #FFB86C;
|
||||
--r-italic-color: #F1FA8C;
|
||||
--r-inline-code-color: #50FA7B;
|
||||
--r-list-bullet-color: #8BE9FD;
|
||||
}
|
||||
|
||||
.reveal strong, .reveal b {
|
||||
color: var(--r-bold-color);
|
||||
}
|
||||
.reveal em, .reveal i, .reveal blockquote {
|
||||
color: var(--r-italic-color);
|
||||
}
|
||||
.reveal code {
|
||||
color: var(--r-inline-code-color);
|
||||
}
|
||||
.reveal ul li::marker, .reveal ol li::marker {
|
||||
color: var(--r-list-bullet-color);
|
||||
}
|
||||
Reference in new issue
Block a user