rein damit

This commit is contained in:
Björn Runden committed 2026-10-30 10:11:43 +01:00
1 parent 7f6b8ebe07
commit 07dbc01c92
38 files changed
+5632 -5

No files matched your search

+19
View File
@@ -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).
+125
View File
@@ -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.
+200
View File
@@ -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; }
}
+73
View File
@@ -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 }
]
}
]
}
+173
View File
@@ -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"]
}
]
}
+317
View File
@@ -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/"]
}
]
}
+582
View File
@@ -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"]
}
]
}
+327
View File
@@ -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"]
}
]
}
+81
View File
@@ -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 }
]
}
]
}
+195
View File
@@ -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"]
}
]
}
+127
View File
@@ -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"]
}
]
}
+285
View File
@@ -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"]
}
]
}
+269
View File
@@ -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"]
}
]
}
+327
View File
@@ -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"]
}
]
}
+237
View File
@@ -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"] } }
]
}
}
}
+34
View File
@@ -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)[^<]*'
```
+28
View File
@@ -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.
+38
View File
@@ -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)
+35
View File
@@ -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.
+45
View File
@@ -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>
+883
View File
@@ -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) => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' }[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/&lt;prüfung&gt;/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);
})();
+13
View File
@@ -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
};
+138
View File
@@ -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>') === '&lt;b&gt;x&lt;/b&gt;');
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>
+134
View File
@@ -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>
+352
View File
@@ -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())
+19
View File
@@ -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.
+3
View File
@@ -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)
+30
View File
@@ -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;
}
+8
View File
@@ -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
+9
View File
@@ -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
View File
@@ -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);
}