Das Ansible Trixie Migration Playbook

Ich hatte schon erwähnt, dass eine Migration der Raspberry Pi OS Version auf dem Raspberry Pi von bookworm auf trixie aufgrund des neuen Paketmanagements nicht so einfach möglich ist, wie von bullseye auf bookworm. Daher habe ich mir Gedanken gemacht, wie ich das zumindest durch ein Ansible Playbook unterstützen kann.

Ansible Logo (Quelle: Von Ansible/Red Hat – Wikipedia)

Motivation

Bei der Migration von bullseye auf bookworm habe ich einfach die bestehenden Paketrepositories auf die neuste Version geändert. Da sich bei trixie aber gesamte Struktur unter /etc/apt geändert hat, nämlich, ist es besser, Raspberry Pi OS komplett neu zu installieren und den Raspi dann wieder aufzusetzen. Auf diese Weise erhalten wir ein sauberes System.

Grundüberlegung

Es ist völlig illusorisch, dass ein Ansible Playbook auf die MicroSD Karte im Raspi eine neue Raspberry Pi OS Version flashen kann, da müssen wir zwangsläufig manuell tätig werden. Dadurch zerfällt unser Playbook schon mal in zwei Phasen.

  1. Sicherung der Daten der bookworm Installation auf das NAS
  2. Zurückspielen der Backupdaten und Einrichtung des Raspis auf der neuen Installation

Dazwischen beschreiben wir die Speicherkarte mit dem trixie System.

Phase 1

Um die Phasen zu unterscheiden, kann ich die OS Version in ansible_facts.lsb.codename abfragen. Dort steht entweder bookworm oder trixie drin. Bei einigen älteren Pis könnte steht dort sogar noch bullseye stehen. Daher werden alle Tasks der Phase 1 mit der Bedingung when: ansible_facts.lsb.codename in ["bookworm", "bullseye"] versehen. Dies stellt sicher, dass sie nur in Phase 1 ausgeführt werden.

Überlegen wir als nächstes mal, was in Phase 1 passieren muss:

  • Die /home Verzeichnisse müssen gesichert werden.
  • /etc wird gesichert, allerdings nur, um später darin suchen zu können.
  • Die docker Volumes unter /opt müssen gesichert werden.
  • Die Liste der installierten Pakete muss ausgelesen und für später gesichert werden.

Damit sind bei mir eigentlich in meinen Standardkonfigurationen alle relevanten Daten abgedeckt. Die Raspis mit dedizierten Aufgaben wie Home Assistant und Nextcloud behandle ich später separat, wenn überhaupt. bookworm wird noch bis 2028 gepflegt, daher besteht kein akuter Handlungsbedarf.

Als Backupverzeichnis sehe ich /tank/trixie vor, mein NAS ist auf allen Raspis unter /tank gemounted.

Phase 1 wird auf den aktuell laufenden Raspberry Pi angewendet. Dabei ist die Modellversion unerheblich (außer beim Flashen von trixie auf die Speicherkarte).

Phase 2

Die Tasks der zweiten Phase laufen erst nachdem die neue Raspberry Pi OS Version auf die Speicherkarte geflashed wurde. Alle Tasks tragen daher die Bedingung ansible_facts.lsb.codename == "trixie".

Diese Phase muss folgende Aufgaben übernehmen:

  • Konfiguration des DNS Servers auf meinen Pi-Hole Raspi
  • Aktualisierung der trixie Installation
  • Anlegen des /tank/ Mountpoints und mounten des NAS dorthin
  • Installieren aller Softwarepakete aus der in Phase 1 erzeugten Liste
  • Zurückspielen der Backupdaten von /home und /opt

Die Backupdaten von /etc werden deshalb nicht automatisch zurückkopiert, weil damit u.U. Konfigurationswerte überschrieben werden, die das OS später nicht mehr sauber starten lassen.

Variablen

Zur besseren individuellen Konfiguration lege ich ich unter roles/trixiw_migration/vars die Datei main.yml an, in der ich einige Variablen deklariere. Im Repository ist diese Datei main_template.yml benannt, da du hier deine eigene SSID eintragen und sie dann in main.yml umbenennen musst!

# IP
dns_ip: "192.168.178.60"
nas_ip: "192.168.178.36"

#Netzwerkverbindung

ap_ssid: "<ssid>"
nm_connection_name: "netplan-wlan0-{{ ap_ssid }}"
# Pfade

backup_base: /tank/trixie
backup_host_dir: "{{ backup_base }}/{{ inventory_hostname }}"
restore_home_src: "{{ backup_host_dir }}/home/pi"
restore_etc_src: "{{ backup_host_dir }}/etc"
packages_file: "{{ backup_host_dir }}/packages.yml"



Zum einen konfiguriere ich hier in dns_ip die IP meiner PI-Hole Instanz und den Mountpoint des NAS. Zum anderen werden hier alle Pfade festgelegt, die das Playbook benötigt. In backup_base steht der Hauptpfad auf meinen NAS. Davon ausgehend wird über den Hostname ein Raspi spezifisches Verzeichnis in backup_host_dir definiert und später automatisch angelegt. In packages_file deklarieren wir dann noch den Namen der Datei, in der die exportierte Paketliste im YAML Format abgelegt wird.

In nm_connection_name habe ich den Namen der Netzwerkverbindung, wie sie netplan verwaltet, konfigurierbar gemacht. Während ich das Playbook gebaut und getestet habe, hat sie sich von „preconfigured“ in „netplan-wlan0-<ssid>“ geändert. Du musst dort natürlich die SSID deines eigen Access Points eintragen.

Gerade die konfigurierten Pfade werde ich in den einzelnen Tasks immer wieder benutzen.

In backup-dirs.yml deklariere ich eine Liste von Verzeichnissen, die auf von dem Raspi aufs NAS gesichert werden sollen.

backupdirs:
  - /home
  - /etc
  - /opt

Die einzelnen Tasks

Phase 1: debug.yml

Dieser Task ist nicht wirklich relevant für die Gesamtaufgabe, er dient nur der Kontrolle, welcher codename in den Ansible Facts steht.

- name: Zeige Systeminformationen
  debug:
    msg: "Host: {{ inventory_hostname }} | Codename: {{ ansible_facts.lsb.codename }}"

Phase 1: backup-dirs.yml

Im nächsten Tasks werden alle in backupdirs konfigurierten Verzeichnisse in das Backupverzeichnis des Raspis auf dem NAS kopiert.

- name: Sichere Verzeichnisse
  become: true
  ansible.builtin.command:
    cmd: >
      tar
      --create
      --gzip
      --file {{ backup_host_dir }}/{{ item }}.tar.gz
      --preserve-permissions
      --same-owner
      --numeric-owner
      -C /
      {{ item }}
  args:
    creates: "{{ backup_host_dir }}/{{ item }}.tar.gz"
  tags: backup
  loop: "{{ backupdirs }}"
  when: ansible_facts.lsb.codename in ["bookworm", "bullseye"]

Der Backup Task läuft über die in backupdirs eingetragenen Verzeichnisse und verpackt deren gesamten Inhalt in einzelne <verzeichnis>.tar.gz tar-Archive, die mit gzip komprimiert sind. Sie liegen später im Backupverzeichnis auf dem NAS.

Ich rufe hier absichtlich das tar Kommando direkt auf dem Raspi auf, um Verwirrungen mit dem Ansible System zu vermeiden.

Ursprünglich wollte ich dies mit einem copy oder rsync Task lösen, hatte dabei aber Schwierigkeiten mit dem Zurückspielen der Daten.

Phase 1: export.yml

Um später den Raspi unter trixie so genau wie möglich wieder aufzubauen, wird in diesem Task die Liste der installierten Pakete erstellt und in die Datei, die in packages_file konfiguriert ist geschrieben.

- name: Exportiere installierte Pakete als YAML
  shell: |
    echo "packages:" > "{{ packages_file }}"
    dpkg-query -W -f='  - ${Package}\n' | sort >> "{{ packages_file }}"
  args:
    executable: /usr/bin/bash
  when: ansible_facts.lsb.codename in ["bookworm", "bullseye"]

Damit sind die Tasks für Phase 1 auch schon komplett. Wenn diese alle erfolgreich durchgelaufen sind, kann Raspberry Pi OS trixie auf die Speicherkarte geschrieben werden. Dabei ist wichtig, dass der Imager die Wi-Fi Verbindung initial konfiguriert und der Hostname exakt so ist, wie er beim bestehenden System war. Nach dem erfolgreichen Start und einer ssh Anmeldung kann dann Phase 2 starten.

Diese Backupdaten liegen nach Phase 1 im Verzeichnis auf dem NAS.
Diese Backupdaten liegen nach Phase 1 im Verzeichnis auf dem NAS.

Phase 2: config-dns.yml

Im ersten Task von Phase 2 konfiguriere ich die Default Wi-Fi Verbindung in nm_connection_name so um, dass mein Pi-Hole als DNS benutzt wird. Dies geschieht durch direkte Shellaufrufe des nmcli Kommandos des NetworkManager. Falls du Pi-Hole nicht benutzt, kannst du diesen Task auch weglassen.

- name: Setze DNS-Server auf Pi-hole für NetworkManager-Connection "preconfigured"
  become: true
  ansible.builtin.command:
    cmd: >
      nmcli connection modify "{{ nm_connection_name }}"
      ipv4.dns "{{ dns_ip }}"
      ipv4.ignore-auto-dns yes
  tags: dns
  when: ansible_facts.lsb.codename == "trixie"

- name: Aktiviere geänderte DNS-Einstellungen
  become: true
  ansible.builtin.command: nmcli connection up "{{ nm_connection_name }}"
  tags: dns
  when: ansible_facts.lsb.codename == "trixie"

Wichtig ist, dass {{ nm_connection_name }} in Anführungszeichen steht, damit ein ggf. vorhandenes Leerzeichen in der SSID korrekt übergeben wird.

Phase 2: upgrade.yml

Wie nach jeder neuen OS Installation, spielt dieser Task jetzt alle aktualisierten Paketversionen auf.

- name: Update & dist-upgrade
  apt:
    update_cache: yes
    upgrade: dist
  become: true
  when: ansible_facts.lsb.codename == "trixie"

Phase 2: mounttank.yml

Alle weiteren Schritte benötigen jetzt Zugriff auf die Backupdaten auf dem NAS. Deshalb legt dieser Task den Mountpoint an und hängt das NAS über NFS ein. Der Eintrag in /etc/fstab wird dabei auch aufgenommen. Eingeschoben ist ein Task zum Installieren des nfs-common Paket, das vor dem Einspielen der gesicherten Packageliste zur Verfügung stehen muss.

- name: create tank dir
  become: true
  file:
    path: "/tank"
    state: directory
  tags: mount
  when: ansible_facts.lsb.codename == "trixie"

- name: Installiere nfs-common
  apt:
    name:
     -  nfs-common
    state: present
  become: true
  tags: mount
  when: ansible_facts.lsb.codename == "trixie"


- name: Ensure /tank is mounted
  become: true
  ansible.posix.mount:
    path: /tank
    src: "{{nas_ip}}:/mnt/md0/public"
    fstype: "nfs"

Falls du kein NAS besitzt, tut es auch ein ausreichend dimensionierter USB-Stick. Dann musst du die Mountoptionen entsprechend anpassen.

Phase 2: install.yml

Jetzt kommt richtig Musik auf den Raspi, die oben erstellte Paketliste wird eingelesen und die darin enthaltenen Pakete werden auch unter trixie installiert. Um das ganze etwas zu beschleunigen, kopiere ich die packages.yml ins /tmp Verzeichnis, da dort der Zugriff schneller geschieht als direkt auf dem NAS.

- name: Kopiere packages.yml auf Trixie
  ansible.builtin.copy:
    src: "{{ backup_host_dir }}/packages.yml"
    dest: "/tmp/packages.yml"
    remote_src: no  # Quelle liegt auf Control-Host

- name: Prüfe, ob packages.yml auf Trixie existiert
  become: true
  stat:
    path: /tmp/packages.yml
  register: pkgfile

- name: Lade Paketliste auf Trixie ein
  become: true
  slurp:
    src: /tmp/packages.yml
  register: packages_yml
  when: pkgfile.stat.exists

- name: Konvertiere Paketliste in Variablen
  set_fact:
    packages_data: "{{ packages_yml.content | b64decode | from_yaml }}"
  when: pkgfile.stat.exists

- name: Lese Paketliste ein und ersetze exa durch eza
  set_fact:
    packages_data_fixed: "{{ packages_data.packages | map('regex_replace', '^exa$', 'eza') | list }}"
  when: ansible_facts.lsb.codename == "trixie"

- name: Lese Paketliste ein und ersetze neofetch durch fastfetch
  set_fact:
    packages_data_fixed: "{{ packages_data_fixed| map('regex_replace', '^neofetch$', 'fastfetch') | list }}"
  when: ansible_facts.lsb.codename == "trixie"

- name: Installiere gesicherte Pakete
  apt:
    name: "{{ item }}"
    state: present
  become: true
  loop: "{{ packages_data_fixed }}"
  ignore_errors: yes
  when: ansible_facts.lsb.codename == "trixie"

Da ich weiß, dass die zwei häufig von mir benutzten Programme neofetch und exa jetzt in fastfetch und eza umbenannt wurden, ersetze ich diese Namen in der Liste.

Phase 2: restore-dirs.yml

Nun können wir unsere gesicherten Verzeichnisse wieder zurück auf die Speicherkarte kopieren.

- name: Verzeichnisse wiederherstellen
  become: true
  ansible.builtin.command:
    cmd: >
      tar
      --extract
      --gzip
      --file {{ backup_host_dir }}/{{ item }}.tar.gz
      --preserve-permissions
      --same-owner
      --numeric-owner
      -C /
  loop: 
    - /home
    - /opt
  when: ansible_facts.lsb.codename == "trixie"

- name: Eigentümer für /home/pi korrigieren
  file:
      path: /home/pi
      owner: pi
      group: pi
      recurse: yes
  tags: restore
  when: ansible_facts.lsb.codename == "trixie"

Das tar Kommando arbeitet im Prinzip jetzt umgekehrt zum Schritt backup-dirs.yml. Es extrahiert den Inhalt der Verzeichnis Archive zurück auf die MicroSD Karte. Allerdings wird /etc hier ausgeklammert, damit keine notwendigen Konfigurationen des trixie Systems überschrieben werden.Zur Not kann ich später noch manuell im Archiv nachsehen, wenn ich etwas nach justieren muss.

Um Probleme beim Login zu vermeiden, setze ich sicherheitshalber die Eigentümerrechte im gesamten /home/pi Verzeichnis auf pi:pi.

Phase 2: reboot.yml

Zum Abschluss wird der Raspberry Pi durchgestartet. Das garantiert, das alle neuen Versionen von Kernel und Systemdiensten aktiv sind.

--- 
- name: Neustart des neuen Systems.
  ansible.builtin.reboot:
    reboot_timeout: 300
    msg: " Neustart des Trixie Systems wird durchgeführt."
  when: ansible_facts.lsb.codename == "trixie"

main.yml

Bringen wir das jetzt alles in der main.yml in die korrekte Reihenfolge.

- include_vars: "roles/trixie_migration/vars/backup-dirs.yml"

# Phase 1: bookworm sichern
- import_tasks: debug.yml
- import_tasks: create-backup-dirs.yml
- import_tasks: export.yml
- import_tasks: backup-dirs.yml

#Phase 2: Trixie aufsetzen
- import_tasks: config-dns.yml
- import_tasks: upgrade.yml
- import_tasks: mounttank.yml
- import_tasks: install.yml
- import_tasks: restore-dirs.yml
- import_tasks: reboot.yml

Test

Jetzt kommt der spannende Augenblick des ersten Testdurchlaufs. Mein Testziel mit dem Hostnamen quimby ist der älteste Raspberry Pi 4B in meinem Bestand. Er hat schon viel mit gemacht. Und ist wie der Bürgermeister von Springfeld immer noch am Start (Sorry, der Nerd in mir wollte, dass das im Post steht 😉 Er wurde schon von buster über bullseye zu bookworm migriert.

Zunächst mache ich, wie üblich ein Komplettbackup der Speicherkarte, das hat quimby schon mehrfach reanimiert.

Sobald quimby wieder online ist, starte ich das Playbook zum ersten Mal

ansible-playbook -e="target=quimby" trixie_migration.yml

Es werden jetzt zunächst nur die Tasks der Phase 1 ausgeführt. Nachdem die erfolgreich durchlaufen wurde, fahre ich den Raspi runter und flashe mittels des Imager Raspberry Pi OS trixie auf die MicroSD. Der Neustart dauert dann etwas, da die Partitionen auf die gesamte Größe der Speicherkarte angepasst werden. Die ersten Anmeldung mit

ssh pi@quimby

muss ich manuell machen, um die Hostkey verification Warnung aufzulösen.

Beim Login warnt ssh vor einer möglichen Man-in-the-Middle Attacke
Beim Login warnt ssh vor einer möglichen Man-in-the-Middle Attacke

Mit dieser Meldung will mich ssh darauf hinweisen, dass sich was an dem Vertrauensverhältnis zwischen meinem Desktoprechner und quimby geändert hat. Das ist kein Wunder, schließlich habe ich den Raspi neu initialisiert.

Deshalb entferne ich mit

ssh-keygen -f "/home/olli/.ssh/known_hosts" -R "quimby"

den alten Eintrag aus ~/.ssh/known_hosts. Beim nächsten Anmeldeversuch muss ich das Vertrauensverhältnis zur trixie Installation neu herstellen.

Nachdem der Login mittels ssh und Publickey sichergestellt ist, starte ich das Playbook wie oben erneut, um Phase 2 auszuführen.Diese benötigt deutlich mehr Zeit, das Upgrade von Raspberry Pi OS wird je nach Auslastung der Repository Server und der Geschwindigkeit deiner Internetverbindung etwas Zeit in Anspruch nehmen. Diese Werte wirken sich auch auf die Neuinstallation der Pakete aus der gesicherten Paketliste aus.Viele der Pakete aus der Liste sind systemnah und wurden bei trixie schon vorinstalliert. Sie werden in grün mit ok: seitens Ansible quittiert. Andere werden jetzt nachinstalliert und in orange changed: angezeigt. Über Pakete die in rot failed: darfst du dich nicht wundern. Dies sind Softwarepakete, die umbenannt oder entfallen sind. Entweder werden diese Pakete jetzt durch neuere Versionen ersetzt, wie bei mir z.B. alte libs des gcc oder sie sind inzwischen völlig unnötig.

Das Installieren der Pakete dauert sehr sehr lang, da für jedes Paket eine neue ssh Verbindung aufgebaut wird. Das ist zwar langsam, scheint mir aber die zuverlässigste Methode zu sein.

Phase 2 ist erfolgreich durchgelaufen.
Phase 2 ist erfolgreich durchgelaufen.

Belastungstest

Nachdem der Raspberry Pi namens quimby erfolgreich umgestellt ist, benutzen ich ihn für einen Test, auf den ich mich schon die ganze Zeit freue: Ich möchte alle 6 Raspis im frink-Stack umstellen.Manuell würde ich geschätzt dafür wahrscheinlich drei Tage benötigen.

Also mache ich von allen sechs Speicherkarten der Reihe nach ein Komplettbackup. Das benötigt jeweils etwa 25 Minuten. Nachdem ich damit komplett fertig bin, starte ich das Playbook auf quimby für die erste Phase auf allen sechs Nodes gleichzeitig. In der Konfiguration kannst du sehen, dass ich sechs simultane ssh Verbindungen zulasse, sodass auf meinem NAS während der Datensicherung richtig was passiert.

Nach Ende von Phase 1, fahre ich jeden Raspi der Reihe nach runter und schreibe eine neue trixie Installation auf die MicroSD. Dabei ist mir die Speicherkarte von frink02 endgültig kaputt gegangen, was der Imager bei der Überprüfung bemerkt hat. Ich hatte damit schon halb gerechnet, da diese Karte schon mehrfach komplett überschrieben wurde.

Das Bespielen der Karten ist für mich mental der anstrengendste Teil, da das Warten auf den Imager viel Geduld benötigt.

Mit der trixie Installation fahre ich alle sechs Nodes wieder hoch und stelle den Login per ssh sicher.

Jetzt starte ich das Playbook für Phase zwei erneut. Die Installation der gesicherten Pakete aus den jeweiligen Paketlisten braucht hierbei die längste Zeit, auch aufgrund der Bandbreite meiner Internetverbindung. Aber den Teil habe ich einfach unbeaufsichtigt laufen lassen. Ich schätze, dass Phase 2 nach etwa drei oder vier Stunden abgeschlossen war.

Abschlussbericht des Ansible Playbook nach Phase 2
Abschlussbericht des Ansible Playbook nach Phase 2

Durch das Playbook war die Umstellung des gesamten 6er Stack schon nach 12-13 Stunden abgeschlossen.

Wärmebildaufnahme des Clusters während des Einspielens der Pakete
Wärmebildaufnahme des Clusters während des Einspielens der Pakete

Was das Playbook kann

Das Playbook sichert alle relevanten Daten aufs NAS. Das Wiederherstellen später hat mir am meisten Kopfzerbrechen bereitet, da sowohl der eingebaute copy Task von Ansible als auch das rsync Modul immer wieder Schwierigkeiten bereitet haben. Daher habe ich jetzt mit dem tarball Archiv eine scheinbar gute Alternative gefunden. Trotzdem gebe ich keine Garantie dafür, dass wirklich alle Daten korrekt übertragen werden. Ein vollständiges Backup deiner wichtigen Daten obliegt der Verantwortung des Nutzers.

Was das Playbook nicht kann

Die Raspberry Pi OS Version trixie muss du manuell z:b: mit dem Imager auf die Speicherkarte schreiben und die Grundkonfiguration von WLAN und ssh Zugang herstellen.

Das Playbook kann ferner keine Dockercontainer erzeugen und starten. Dazu sind die Installationen bei mir zu inhomogen.

Fazit

Mit diesem Playbook kann eine Upgrade auf trixie unterstützt werden, dass gut im Hintergrund laufen kann.

Ich habe dieses Playbook sehr ausführlich getestet. Da es vor allem auf meine Bedürfnisse zugeschnitten ist, gebe ich keine Garantie dafür, dass es bei jedem von euch genauso gut funktioniert.

Den Hinweis aufs Repository habe ich oben schon verlinkt.

Schreibe einen Kommentar

Insert math as
Block
Inline
Additional settings
Formula color
Text color
#333333
Type math using LaTeX
Preview
\({}\)
Nothing to preview
Insert
Creative Commons License
Except where otherwise noted, the content on this site is licensed under a Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International License.
raspithek.de - Olli Graf
WordPress Cookie Hinweis von Real Cookie Banner