diff --git a/README.md b/README.md index d4c9d2f..7fec97e 100644 --- a/README.md +++ b/README.md @@ -26,8 +26,9 @@ Begründung siehe `docs/distro-auswahl.md`. linux-workstations/ ├── README.md ├── docs/ -│ └── distro-auswahl.md # ADR Distribution -├── install/ # Netinstall-Preseed, Partitionierungsnotizen (folgt) +│ ├── distro-auswahl.md # ADR Distribution +│ ├── installation.md # Schritt-für-Schritt Netinstall + Dualboot +│ └── postinstall-ansible.md # Bootstrap & Ablauf └── ansible/ ├── ansible.cfg ├── inventory.yml @@ -35,20 +36,30 @@ linux-workstations/ ├── group_vars/all.yml ├── host_vars/{notebook,pc,werkstatt}.yml └── roles/ - ├── base/ # Grundsystem, APT-Quellen, Firmware - ├── desktop_kde/ # KDE Plasma + Anwendungen + ├── base/ # APT-Quellen, Locale, Firmware, Grundpakete + ├── desktop_kde/ # KDE Plasma 6 + SDDM + Apps ├── hardening/ # SSH, UFW, unattended-upgrades - ├── dev_tools/ # Editor, Git, Sprachen - └── workstation_apps/ # Browser, Office, Mediencodecs + ├── dev_tools/ # Git, Node.js (NodeSource), Python, Perl + ├── workstation_apps/ # Browser, Office, Multimedia + └── claude_workspace/ # Claude Code, ccusage, Workspace-Clone, ~/.claude/settings.json ``` -## Workflow +## Workflow (Kurz) + +Siehe `docs/installation.md` und `docs/postinstall-ansible.md` für Details. ```bash -# Auf einem Zielrechner nach Erstinstallation: -ssh-copy-id tom@notebook +# Nach Debian-Erstinstallation auf Zielrechner: +ssh-copy-id tom@notebook.egonlebt.lan ansible -i ansible/inventory.yml notebook -m ping + +ansible-playbook -i ansible/inventory.yml ansible/site.yml --limit notebook --check --diff ansible-playbook -i ansible/inventory.yml ansible/site.yml --limit notebook + +# Anschließend manuell pro Maschine (~1 min): +# 1. Public-Key aus ~/.ssh/id_ed25519_gitea.pub auf Gitea hochladen +# 2. ansible-playbook ... --tags claude_workspace (holt Workspace nach) +# 3. Auf dem Zielrechner: `claude` → OAuth-Login ``` ## Repo diff --git a/ansible/roles/base/tasks/main.yml b/ansible/roles/base/tasks/main.yml index 1cc22d1..838fb64 100644 --- a/ansible/roles/base/tasks/main.yml +++ b/ansible/roles/base/tasks/main.yml @@ -1,3 +1,84 @@ --- -# Grundsystem: APT-Quellen, Lokalisierung, Firmware, Hilfspakete -# TODO: implementieren — Skeleton-Datei +- name: APT-Quellen mit contrib + non-free-firmware + (Backports) + ansible.builtin.copy: + dest: /etc/apt/sources.list + mode: '0644' + content: | + deb http://deb.debian.org/debian/ trixie main contrib non-free-firmware + deb http://security.debian.org/debian-security trixie-security main contrib non-free-firmware + deb http://deb.debian.org/debian/ trixie-updates main contrib non-free-firmware + {% if enable_backports | default(true) %} + deb http://deb.debian.org/debian/ trixie-backports main contrib non-free-firmware + {% endif %} + register: apt_sources + +- name: APT cache aktualisieren + ansible.builtin.apt: + update_cache: true + when: apt_sources.changed + +- name: Locale-Paket + ansible.builtin.apt: + name: locales + state: present + cache_valid_time: 3600 + +- name: Locale aktivieren + ansible.builtin.lineinfile: + path: /etc/locale.gen + regexp: "^# ?{{ locale }} " + line: "{{ locale }} UTF-8" + register: locale_line + +- name: locale-gen ausführen + ansible.builtin.command: locale-gen + when: locale_line.changed + +- name: Standard-Locale setzen + ansible.builtin.copy: + dest: /etc/default/locale + mode: '0644' + content: "LANG={{ locale }}\n" + +- name: Tastaturlayout + ansible.builtin.copy: + dest: /etc/default/keyboard + mode: '0644' + content: | + XKBMODEL="pc105" + XKBLAYOUT="{{ keyboard_layout }}" + XKBVARIANT="" + XKBOPTIONS="" + BACKSPACE="guess" + +- name: Zeitzone + ansible.builtin.command: "timedatectl set-timezone {{ timezone }}" + changed_when: false + +- name: Grundpakete + ansible.builtin.apt: + name: + - sudo + - curl + - wget + - gnupg + - ca-certificates + - apt-transport-https + - vim + - htop + - tmux + - rsync + - net-tools + - dnsutils + - firmware-linux + - firmware-linux-nonfree + - lsb-release + - bash-completion + - man-db + state: present + +- name: Extra-Pakete je Host + ansible.builtin.apt: + name: "{{ extra_packages }}" + state: present + when: extra_packages | default([]) | length > 0 diff --git a/ansible/roles/claude_workspace/defaults/main.yml b/ansible/roles/claude_workspace/defaults/main.yml new file mode 100644 index 0000000..31dacf3 --- /dev/null +++ b/ansible/roles/claude_workspace/defaults/main.yml @@ -0,0 +1,11 @@ +--- +claude_workspace_repo: "ssh://git@docker.egonlebt.lan:2222/egon/claude-workspace.git" +claude_workspace_dest: "/home/{{ primary_user }}/Claude" +gitea_ssh_host: docker.egonlebt.lan +gitea_ssh_port: 2222 +claude_settings: + model: opus + advisorModel: opus + statusLine: + type: command + command: "node ~/Claude/.claude/statusline.js" diff --git a/ansible/roles/claude_workspace/tasks/main.yml b/ansible/roles/claude_workspace/tasks/main.yml new file mode 100644 index 0000000..5467516 --- /dev/null +++ b/ansible/roles/claude_workspace/tasks/main.yml @@ -0,0 +1,106 @@ +--- +# Setzt voraus: Node.js + git (aus dev_tools), npm verfügbar + +- name: Claude Code (npm global) + ansible.builtin.command: npm install -g @anthropic-ai/claude-code + args: + creates: /usr/lib/node_modules/@anthropic-ai/claude-code/package.json + +- name: ccusage (npm global, für Statusline-Tokenverbrauch) + ansible.builtin.command: npm install -g ccusage + args: + creates: /usr/lib/node_modules/ccusage/package.json + +- name: ~/.ssh existiert + become_user: "{{ primary_user }}" + ansible.builtin.file: + path: "/home/{{ primary_user }}/.ssh" + state: directory + mode: '0700' + +- name: SSH-Key für Gitea (ed25519, ohne Passphrase) + become_user: "{{ primary_user }}" + ansible.builtin.command: > + ssh-keygen -t ed25519 + -f /home/{{ primary_user }}/.ssh/id_ed25519_gitea + -N "" -C "{{ primary_user }}@{{ inventory_hostname }} -> gitea" + args: + creates: "/home/{{ primary_user }}/.ssh/id_ed25519_gitea" + +- name: known_hosts für Gitea vorpopulieren + become_user: "{{ primary_user }}" + ansible.builtin.shell: | + ssh-keyscan -p {{ gitea_ssh_port }} -H {{ gitea_ssh_host }} 2>/dev/null \ + | grep -v '^#' >> /home/{{ primary_user }}/.ssh/known_hosts + sort -u /home/{{ primary_user }}/.ssh/known_hosts \ + -o /home/{{ primary_user }}/.ssh/known_hosts + args: + creates: "/home/{{ primary_user }}/.ssh/known_hosts" + +- name: SSH-Config für Gitea + become_user: "{{ primary_user }}" + ansible.builtin.blockinfile: + path: "/home/{{ primary_user }}/.ssh/config" + create: true + mode: '0600' + marker: "# {mark} ANSIBLE MANAGED — gitea" + block: | + Host {{ gitea_ssh_host }} + Port {{ gitea_ssh_port }} + IdentityFile ~/.ssh/id_ed25519_gitea + IdentitiesOnly yes + User git + +- name: Check ob Workspace schon geklont + become_user: "{{ primary_user }}" + ansible.builtin.stat: + path: "{{ claude_workspace_dest }}/.git" + register: ws_git + +- name: claude-workspace klonen (mit Submodules) + become_user: "{{ primary_user }}" + ansible.builtin.git: + repo: "{{ claude_workspace_repo }}" + dest: "{{ claude_workspace_dest }}" + recursive: true + update: false + accept_hostkey: true + key_file: "/home/{{ primary_user }}/.ssh/id_ed25519_gitea" + when: not ws_git.stat.exists + ignore_errors: true # scheitert bevor Pubkey in Gitea liegt — wird erneut versucht + register: clone_result + +- name: ~/.claude existiert + become_user: "{{ primary_user }}" + ansible.builtin.file: + path: "/home/{{ primary_user }}/.claude" + state: directory + mode: '0700' + +- name: Globale Claude-Settings (~/.claude/settings.json) + become_user: "{{ primary_user }}" + ansible.builtin.copy: + dest: "/home/{{ primary_user }}/.claude/settings.json" + mode: '0644' + content: "{{ claude_settings | to_nice_json }}\n" + +- name: Public-Key für Gitea-Upload anzeigen + become_user: "{{ primary_user }}" + ansible.builtin.command: "cat /home/{{ primary_user }}/.ssh/id_ed25519_gitea.pub" + register: pubkey + changed_when: false + +- name: HINWEIS — Public-Key auf Gitea hochladen + ansible.builtin.debug: + msg: + - "===========================================================" + - "Public-Key dieser Maschine ({{ inventory_hostname }}):" + - "" + - "{{ pubkey.stdout }}" + - "" + - "→ http://{{ gitea_ssh_host }}:3000/user/settings/keys" + - " → 'Schlüssel hinzufügen', oben einfügen, speichern." + - "" + - "Danach ggf. Workspace nachholen:" + - " ansible-playbook ... --tags claude_workspace --limit {{ inventory_hostname }}" + - "===========================================================" diff --git a/ansible/roles/desktop_kde/tasks/main.yml b/ansible/roles/desktop_kde/tasks/main.yml index 52e3ac7..4a413a7 100644 --- a/ansible/roles/desktop_kde/tasks/main.yml +++ b/ansible/roles/desktop_kde/tasks/main.yml @@ -1,3 +1,39 @@ --- -# KDE Plasma 6, SDDM, KDE-Anwendungen -# TODO: implementieren — Skeleton-Datei +- name: KDE Plasma + SDDM (Standardumfang ohne riesige Discover-Hänger) + ansible.builtin.apt: + name: + - kde-plasma-desktop + - sddm + - plasma-nm + - plasma-pa + - kde-config-sddm + - konsole + - dolphin + - kate + - okular + - gwenview + - spectacle + - ark + - kcalc + - kfind + - partitionmanager + - xdg-utils + - fonts-noto + - fonts-noto-color-emoji + state: present + install_recommends: false + +- name: SDDM aktivieren als Default-Display-Manager + ansible.builtin.copy: + dest: /etc/X11/default-display-manager + mode: '0644' + content: "/usr/bin/sddm\n" + +- name: Graphical Target als Default + ansible.builtin.command: systemctl set-default graphical.target + changed_when: false + +- name: SDDM aktivieren + ansible.builtin.systemd: + name: sddm + enabled: true diff --git a/ansible/roles/dev_tools/tasks/main.yml b/ansible/roles/dev_tools/tasks/main.yml index 3ec1b20..7e920a7 100644 --- a/ansible/roles/dev_tools/tasks/main.yml +++ b/ansible/roles/dev_tools/tasks/main.yml @@ -1,3 +1,50 @@ --- -# Git, Editor, Sprachen (Python/Node/Perl für FHEM) -# TODO: implementieren — Skeleton-Datei +- name: Dev-Pakete (Sprachen, Build-Tools, Editor) + ansible.builtin.apt: + name: + - git + - build-essential + - python3 + - python3-venv + - python3-pip + - pipx + - perl + - jq + - direnv + - shellcheck + - meld + state: present + +- name: NodeSource Keyring (für Node.js LTS) + ansible.builtin.get_url: + url: https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key + dest: /etc/apt/keyrings/nodesource.asc + mode: '0644' + register: ns_key + +- name: NodeSource Repo + ansible.builtin.copy: + dest: /etc/apt/sources.list.d/nodesource.list + mode: '0644' + content: "deb [signed-by=/etc/apt/keyrings/nodesource.asc] https://deb.nodesource.com/node_20.x nodistro main\n" + register: ns_repo + +- name: APT update nach NodeSource + ansible.builtin.apt: + update_cache: true + when: ns_key.changed or ns_repo.changed + +- name: Node.js (LTS) installieren + ansible.builtin.apt: + name: nodejs + state: present + +- name: Git globale Defaults für {{ primary_user }} + become_user: "{{ primary_user }}" + ansible.builtin.command: "git config --global {{ item.k }} {{ item.v }}" + loop: + - { k: 'user.name', v: 'egon' } + - { k: 'user.email', v: 'egon@egonlebt.de' } + - { k: 'pull.rebase', v: 'true' } + - { k: 'init.defaultBranch', v: 'main' } + changed_when: false diff --git a/ansible/roles/hardening/handlers/main.yml b/ansible/roles/hardening/handlers/main.yml new file mode 100644 index 0000000..57d0861 --- /dev/null +++ b/ansible/roles/hardening/handlers/main.yml @@ -0,0 +1,5 @@ +--- +- name: restart sshd + ansible.builtin.systemd: + name: ssh + state: restarted diff --git a/ansible/roles/hardening/tasks/main.yml b/ansible/roles/hardening/tasks/main.yml index 5c7f4d4..565379f 100644 --- a/ansible/roles/hardening/tasks/main.yml +++ b/ansible/roles/hardening/tasks/main.yml @@ -1,3 +1,48 @@ --- -# SSH-Hardening, UFW, unattended-upgrades, fail2ban -# TODO: implementieren — Skeleton-Datei +- name: Security-Pakete + ansible.builtin.apt: + name: + - ufw + - unattended-upgrades + - apt-listchanges + state: present + +- name: SSH — Passwort-Login deaktivieren + ansible.builtin.lineinfile: + path: /etc/ssh/sshd_config + regexp: '^#?\s*PasswordAuthentication\s' + line: 'PasswordAuthentication no' + validate: 'sshd -t -f %s' + notify: restart sshd + +- name: SSH — Root-Login deaktivieren + ansible.builtin.lineinfile: + path: /etc/ssh/sshd_config + regexp: '^#?\s*PermitRootLogin\s' + line: 'PermitRootLogin no' + validate: 'sshd -t -f %s' + notify: restart sshd + +- name: UFW Default Policy (incoming deny) + ansible.builtin.command: ufw default deny incoming + register: ufw_default + changed_when: "'Default incoming policy changed' in ufw_default.stdout" + +- name: UFW — SSH erlauben + ansible.builtin.command: ufw allow OpenSSH + register: ufw_allow + changed_when: "'Rule added' in ufw_allow.stdout or 'Rules updated' in ufw_allow.stdout" + +- name: UFW aktivieren + ansible.builtin.command: ufw --force enable + register: ufw_enable + changed_when: "'Firewall is active' in ufw_enable.stdout" + +- name: unattended-upgrades konfigurieren + ansible.builtin.copy: + dest: /etc/apt/apt.conf.d/20auto-upgrades + mode: '0644' + content: | + APT::Periodic::Update-Package-Lists "1"; + APT::Periodic::Unattended-Upgrade "1"; + APT::Periodic::AutocleanInterval "7"; diff --git a/ansible/roles/workstation_apps/tasks/main.yml b/ansible/roles/workstation_apps/tasks/main.yml index 9799787..0048c77 100644 --- a/ansible/roles/workstation_apps/tasks/main.yml +++ b/ansible/roles/workstation_apps/tasks/main.yml @@ -1,3 +1,16 @@ --- -# Browser, Office, Multimedia-Codecs -# TODO: implementieren — Skeleton-Datei +- name: Anwendungen (Browser, Office, Multimedia, Tools) + ansible.builtin.apt: + name: + - firefox-esr + - libreoffice + - libreoffice-l10n-de + - thunderbird + - thunderbird-l10n-de + - keepassxc + - vlc + - ffmpeg + - libavcodec-extra + - gimp + - inkscape + state: present diff --git a/ansible/site.yml b/ansible/site.yml index e779de3..ba8ef92 100644 --- a/ansible/site.yml +++ b/ansible/site.yml @@ -3,8 +3,15 @@ hosts: workstations gather_facts: true roles: - - base - - desktop_kde - - hardening - - dev_tools - - workstation_apps + - role: base + tags: [base] + - role: hardening + tags: [hardening] + - role: dev_tools + tags: [dev_tools] + - role: desktop_kde + tags: [desktop, kde] + - role: workstation_apps + tags: [apps] + - role: claude_workspace + tags: [claude_workspace, claude] diff --git a/docs/installation.md b/docs/installation.md new file mode 100644 index 0000000..62a39a9 --- /dev/null +++ b/docs/installation.md @@ -0,0 +1,105 @@ +# Installation Debian 13 (Trixie) — Dualboot + +Diese Anleitung gilt für alle drei Workstations (notebook, pc, werkstatt). Unterschiede sind unten markiert. + +## 1. Vorbereitung in Windows + +1. **Updates fertigstellen**, dann `Datenträgerverwaltung` (`diskmgmt.msc`) öffnen +2. **BitLocker** deaktivieren auf der Partition, die verkleinert wird, sonst startet Windows nach dem GRUB-Eintrag nicht mehr sauber +3. Windows-Partition verkleinern → freien Bereich erzeugen: + - **Notebook:** mindestens 80 GB frei + - **PC:** mindestens 150 GB frei + - **Werkstatt:** mindestens 60 GB frei +4. **Schnellstart** in Windows deaktivieren (`Energieoptionen → Auswählen, was beim Drücken …`), sonst bleibt das NTFS in einem halben Zustand und Grub kann es nicht sicher zeigen +5. **BIOS/UEFI**: + - Boot-Mode: UEFI (nicht Legacy) + - **Secure Boot:** anlassen, Debian 13 unterstützt es out-of-the-box + - Fast Boot: aus (sonst kein USB-Boot) + +## 2. Boot-Stick erstellen + +ISO: `firmware-13.x.x-amd64-netinst.iso` von +> Die **firmware**-Variante ist Pflicht — sonst fehlen WLAN-/Grafik-Treiber. + +Stick (mind. 2 GB) schreiben: +- Windows: [Rufus](https://rufus.ie) → "DD-Image" Modus +- Linux: `sudo dd if=firmware-13.x.x-amd64-netinst.iso of=/dev/sdX bs=4M status=progress oflag=sync` + +## 3. Debian-Installation + +Vom Stick booten (BIOS-Boot-Menu, meist F12/F10/Esc). + +### Wichtige Antworten im Installer + +| Schritt | Antwort | +|---|---| +| Sprache | Deutsch | +| Tastatur | Deutsch | +| Hostname | `notebook` / `pc` / `werkstatt` | +| Domain | `egonlebt.lan` | +| Root-Passwort | **leer lassen** → Tom bekommt automatisch `sudo` | +| Benutzer | Tom Egon (`tom`) | +| Zeitzone | Europe/Berlin | + +### Partitionierung + +**Notebook (mit LUKS):** +- Methode: **Geführt — gesamten Laufwerk verwenden, mit verschlüsseltem LVM** +- Wenn der Installer die Methode wegen Windows nicht anbietet → **Manuell**: + - ESP existiert bereits (Windows) — wiederverwenden, mountpoint `/boot/efi` + - 1 GB unverschlüsselte `/boot` (ext4) + - Rest: LUKS-Container → LVM: + - `lv-root` 30 GB ext4 → `/` + - `lv-home` Rest ext4 → `/home` + - `lv-swap` 8 GB Swap (= RAM-Größe für Hibernate) + +**PC + Werkstatt (ohne LUKS):** +- Methode: **Manuell** + - ESP wiederverwenden, mountpoint `/boot/efi` + - 30 GB ext4 → `/` + - Rest ext4 → `/home` + - Swap-File statt -Partition (per `systemd` nachgereicht, einfacher) + +### Paketauswahl (Software-Auswahl) + +**Nur ankreuzen:** +- ✅ SSH-Server +- ✅ Standard-Systemwerkzeuge + +**Abwählen:** +- ❌ Debian-Desktop, GNOME, KDE — kommt alles über Ansible + +So bleibt die Basis schlank (~1.2 GB). + +### GRUB + +- "GRUB auf primärem Laufwerk installieren" → **Ja** +- Nach Reboot: GRUB zeigt Debian + "Windows Boot Manager" + +## 4. Erstboot (vor Ansible) + +Auf der Zielmaschine einloggen (Konsole), dann: + +```bash +# IP merken +ip -4 addr show | grep inet +# SSH läuft schon (Paketauswahl). Test vom Admin-Rechner: +# ssh tom@.egonlebt.lan +``` + +Auf dem **Admin-Rechner** (Windows mit OpenSSH, oder schon installierter Linux): + +```bash +# Public Key kopieren — danach kein Passwort mehr nötig +ssh-copy-id tom@notebook.egonlebt.lan +``` + +Damit ist die Basis bereit für den Ansible-Run → `docs/postinstall-ansible.md`. + +## 5. Sanity Checks + +```bash +ssh tom@notebook.egonlebt.lan 'cat /etc/debian_version' # → 13.x +ssh tom@notebook.egonlebt.lan 'sudo -n true && echo sudo-ok' +ssh tom@notebook.egonlebt.lan 'efibootmgr -v | grep -E "debian|Windows"' +``` diff --git a/docs/postinstall-ansible.md b/docs/postinstall-ansible.md new file mode 100644 index 0000000..1e3172e --- /dev/null +++ b/docs/postinstall-ansible.md @@ -0,0 +1,88 @@ +# Postinstall via Ansible + +Voraussetzung: Debian-Grundinstallation läuft, SSH-Login per Key funktioniert (siehe `installation.md`). + +## 1. Ansible auf dem Admin-Rechner + +Ansible kann von **jedem Rechner** ausgeführt werden, der die Zielmaschinen per SSH erreicht und Python 3 hat. Empfohlen: die erste fertige Linux-Workstation wird zum Admin-Rechner für die nächsten. + +Initial vom Windows-Rechner (WSL/Git-Bash) reicht auch: + +```bash +# Linux/WSL +sudo apt install -y ansible +# oder via pipx +pipx install ansible-core +``` + +## 2. Ablauf + +```bash +cd ~/Claude/linux-workstations + +# Verbindungstest +ansible -i ansible/inventory.yml notebook -m ping + +# Trockenlauf +ansible-playbook -i ansible/inventory.yml ansible/site.yml --limit notebook --check --diff + +# Echter Run +ansible-playbook -i ansible/inventory.yml ansible/site.yml --limit notebook +``` + +Die Playbook-Ausführung dauert beim ersten Mal ~20 min (KDE-Plasma-Pakete sind groß). + +## 3. Was nach `site.yml` fertig ist + +- Debian-Basis mit allen Updates, deutsche Locale, Zeitzone Berlin +- KDE Plasma 6 + SDDM (Login-Manager) — Reboot fällig +- SSH gehärtet (Key-only), UFW aktiv (nur SSH offen), `unattended-upgrades` an +- Git, Node.js, Python, Editor, Browser, LibreOffice, Codecs +- **Claude Code + ccusage installiert, `~/.claude/settings.json` mit Statusline gesetzt** +- **`~/Claude` geklont (`--recurse-submodules`)**, Git-User auf `egon` konfiguriert +- SSH-Key für Gitea generiert (`~/.ssh/id_ed25519_gitea`), `known_hosts` für `docker.egonlebt.lan:2222` vorpopuliert + +## 4. Restmanuelle Schritte + +Diese zwei Dinge muss man pro Maschine selbst tun — beides je 1 Minute. + +### 4.1 SSH-Public-Key auf Gitea hochladen + +Am Ende des Ansible-Runs zeigt die Rolle `claude_workspace` den Public-Key an. Wenn der Run schon vorbei ist: + +```bash +ssh tom@notebook 'cat ~/.ssh/id_ed25519_gitea.pub' +``` + +→ in als neuen SSH-Key einfügen, Name z.B. `notebook-tom`. + +Danach auf dem Zielrechner einmal: + +```bash +ssh -i ~/.ssh/id_ed25519_gitea -T git@docker.egonlebt.lan -p 2222 +# Antwort: "Hi there, egon! You've successfully authenticated…" +``` + +### 4.2 Claude Code anmelden + +Direkt am Zielrechner (oder per `ssh -t`): + +```bash +claude +# → folgt dem Browser-Login-Flow einmalig +``` + +Danach ist die Maschine voll einsatzbereit. Statusline, Hooks (Auto-Pull beim Start, Auto-Commit/Push beim Stop), MCP-Server (paperless, imap, ssh-infra) — alles aktiv, weil Konfiguration im Repo liegt. + +## 5. Wartung + +```bash +# Alles aktualisieren (nur APT-Pakete + Konfig-Drift heilen): +ansible-playbook -i ansible/inventory.yml ansible/site.yml + +# Eine einzelne Rolle: +ansible-playbook -i ansible/inventory.yml ansible/site.yml --tags claude_workspace + +# Eine einzelne Maschine: +ansible-playbook -i ansible/inventory.yml ansible/site.yml --limit werkstatt +```