Raspberry Migration ohne seperate Hardware

In diesem Artikel zeige ich wie man einen Raspberry Pi migrieren kann, wenn man zum Einen Software migrieren muss, bei der es keine langen down times geben kann und zum Anderen keine separate Hardware zur Verfügung hat.
Hierfür muss man etwas Erfahrung mit dem Raspberry Pi und Linux Systemen haben. Ferner benötigt man Erfahrung im Umgang mit SystemD und darunter betriebenen Diensten. Geht es um Netzwerk Dienste (u.a Webserver, Samba Shares, etc.) wird notwendig einen Plan zum switch der Dienste zu haben und jeweils den alten vorher abschalten zu müssen. Nicht ganz einfach ist zwischendurch die Unterscheidung der neuen und der alten Welt auf dem System.

Warum diese Mirgation gemacht wurde

Der Hintergrund zu der Raspberry Pi Migration ist, dass ich eine alte Installation des Raspberry Pi Systems hatte. Dieses war über Jahre von Debian 7 upgraded worden bis zuletzt zu Debian 12. Bei diesem stellte ich außer dem das es langsam geworden ist auch fest, dass die Versionen verschiedener Softwarekomponenten nicht mehr passten zu denen die bei gleichem Debian 12 Original per Image ausgelieferten Versionen. Und installierte Pakete, wie Kernel, Bootloader und Firmware bekamen überhaupt keine Updates mehr. Inzwischen stand auch das Update auf Debian 13 an. Doch im upgrade habe ich aus den benannten Gründen keinen Sinn gesehen.
So wollte ich diesmal das System mit allen Software Komponenten neu aufsetzen. Dabei sinnvollerweise auch gleich mit Debian 13. Doch auf dem alten System liefen Dinge, die ich nicht länger abschalten konnte. Alles war auch nativ ins System installiert und das Backup lief unter anderem über den Pi. Das musste weiter funktionieren können, egal wie lange ich für die Migration aller Dienste benötigen würde. Einen zweiten Pi hatte ich zum parallelen Aufbau des neuen Systems nicht zur Verfügung. Und es sollte alles auf dem Pi bleiben, u.a wegen dem Projekt Backup Festplatte. Somit suchte ich nach einem Weg, das alte System weiter betreiben zu können für eine Zeit lang und dabei das neue System aufbauen zu können und das ohne separate Hardware.

Ein virtueller Pi

Eine gern genutzte Variante um das alte System kurze Zeit parallel zum neuen betreiben zu können ist Virtualisierung. Doch eine vollständige Virtuelle Maschine wäre auf dem Raspberry Pi nicht nutzbar gewesen. Einen virtuellen Raspberry Pi auf einem anderen System erfinden konnte man so einfach auch nicht. Außerdem hätte ich dann keine Festplatte für das Backup Projekt schalten können. Alles umziehen auf andere Systeme wollte ich nicht und auch da wäre das Projekt Backup Festplatte nicht anpassbar. Das System als Container zu betreiben wäre noch eine Lösung gewesen. Docker war nicht mein Fachgebiet und ferner würde ich das alte OS mit Kernel nicht einfrieren wollen. In einen LXC für Proxmox hätte ich es wegen dem Architektur unterschied (Arm vs. x86) nicht machen können. Eine Technologie von Linux die quasi die Grundlage zu jeglichen Container Systemen ist, nennt sich Chroot (Change Root für Linux Prozesse). Davon wusste ich, dass man ganz unterschiedliche Linux Systeme z.B Almalinux in einem Debian betreiben kann. Nur ist die Technik nicht unbedingt für das Betreiben eines Systems gedacht. Doch mit etwas Aufwand hätte ich das alte Debian 12 in dem neuen Debian 13 starten können. Nur die Unterstützung für Hardware Zugriff wie GPIO für die Festplatte wäre schwierig geworden. Doch bei der Recherche zu Chroot und Raspberry Pi kam ich auf eine vierte und besser geeignete Lösung.

Systemd-nspawn

SystemD besitzt sehr viele Komponenten, die das Linux System, seine Dienste, User und mehr verwalten sollen. Nach dem es bereits Docker und LXC Container gab entwickelten die Entwickler von SystemD auch ein Container System. Als Teil von Systemd-container gehört der systemd-nspawn Befehl und kann vergleichbar mit einem Chroot ein System wie einen Container starten aus einem Verzeichnisbaum heraus. Bedingung dafür ist, dass das Gastsystem mit SystemD als System Manager laufen muss. Was man bei einem Chroot nervig zusammen binden muss, damit darin Prozesse ausgeführt werden können (lose) macht Systemd-nspawn für den User. Dienste die im Gast installiert waren, werden hierbei auch gestartet als reguläre systemd-services. Der Container startet per Default ohne eigenes IP Netzwerk, was anders ist als zum Vergleich LXC oder Docker. Jedoch sind per Default die Prozesse auf gleicher Netzwerk Ebene wie Host Prozesse erreichbar, ähnlich zu einem Prozess im Chroot. Das Bedeutet ein Webserver benutzt die IP des Hostes und versucht bei Standard Port 80 diesen auch an die Host IP zu binden! Es sei Erwähnt, dass man das Netzwerk eines solchen Containers auch in ein privates virtuelles Netzwerk ändern kann. Natürlich wird wie bei Containern üblich nicht ein eigener Kernel gestartet. Alle Prozesse darin nutzen den (neuen) Kernel des Hosts mit.

Wechsel der Architektur

Ein weiterer Punkt bei der Migration ist der wechsel der Architektur. Das alte System war noch ein 32 Bit Arm Linux. Das neue System sollte auch ein 64 Bit Arm Linux sein. Dieser Wechsel kann unter Umständen ein Problem sein. Zum einen kann die Software so nicht 1:1 migriert werden auf ein System wie Proxmox, denn das ist ein x86_64 System in der Regel. Das ist nicht kompartibel. Man kann in dem Fall nur die Konfiguration nachbauen und auf dem Zielsystem die x86 Äquivalente Software installieren. Bei Chroot Prozessen kann auch ein kompatibler Modus wie in x86_64 nicht genutzt werden und es lassen sich so Umgebungen nicht starten. Doch das Problem ist auch bei Arm nicht mehr relevant. Denn Arm64 erlaubt ohne Probleme alte 32-Bit Programme auszuführen.

Vorbedingungen

Verwenden von systemd-nspawn

Um das alte System nun als Container im neuen zu starten, benötigt man es auf zwei Disks. In meinem Fall zwei SD Karten. Die Karte mit dem neuen System steckt dabei bereits im Pi und bootet. Sie wird als Gerätedatei /dev/mmcblk0 und den Partitionen darunter angegeben. Für das alte System habe ich einen externes Kartenlesegerät angeschlossen und die Karte des alten Systems dort eingesteckt. Diese wird im System dann z.B als Gerätedatei /dev/sda angeben, auch hier um Partitionen erweitert. Die Partitionen sind oft Nr. 1 Bootpartition und Nr. 2 Rootpartition. Für den Container muss nicht ein Kernel gebootet werden und also auch kein Bootloader durchlaufen werden. Daher benötigt man vom alten System die Bootpartition nicht. So hängt man die Rootpartition ein. Aber Achtung, soll /mnt für weitere Mountpunkte Host, wie auch Gast genutzt werden, solle man darunter ein separates neues Verzeichnis anlegen. Ich mounte also /dev/sda1 nach /mnt/oldpi. Andere Mountpunkt Verzeichnisse, welche im alten System unter /mnt lagen, lege ich entsprechend an, sofern ich das wieder wie vorher verwenden möchte. Nun ist fast alles für den Container fertig.

Bind Mounts

Bei Chroot müsste man nun aufwendig Special Filesysteme wie /dev, /run, /proc und /sys mounten. Das Entfällt bei der Verwendung von systemd-nspawn für die Containerlaufzeit komplett. Allerdings hat der Container so private virtuelle Filesysteme an deren Stelle. Möchte man aber dem Gast zugriff auf Geräte des Hosts geben, muss man einen Bind Mount für /dev anlegen. Das kann aber per Parameter an systemd-nspawn erreicht werden. Zur Syntax dazu siehe hier drunter.
Ich habe unter /mnt mehrere Mountpunkte verwendet. Unter anderem den wichtigen Mountpunkt für die Backup Festplatte oder den Cloud-Speicher für die externe Sicherung. Daher wie ober gesagt ist die Rootpartition im neuen System nur unter /mnt/oldpi gemountet und die Verzeichnisse der anders genutzten Mountpunkte angelegt. Damit der Gast diese nun selbst benutzen kann (Geräte dorthin mounten kann), musste ich zusätzlich /mnt noch per Argumente als Bind Mount durchreichen.

Syntax des Befehls:

Container Start Script

Die Argumente die an systemd-nspawn übergeben werden müssen zum Starten des Containers sind komplex. Man möchte dies nicht immer eingeben müssen. Darauf verlassen möchte man sich auch nicht, es irgendwo in der History zu haben. Also lag es nahe, den kompletten Befehl kurz in ein Script zu schreiben. Sodass ich dieses, fest mit allen Argumenten, aufrufen und im Terminal nur durch Auto-Complete auf den Scriptnamen konnte. Das ist für den Start des Containers ganz sinnvoll.
Im Script wird nun der Befehl von systemd-nspawn aufgerufen. Als Argumente werden aber noch -b übergeben und -D <Pfad> [im Script zusammengefasst als -bD <Pfad>]. Dabei bedeutet -b dass Systemd im Container "Booten" soll, d.h. systemd wird darin als Init-System gestartet und danach alle anderen Systemkomponenten wie Dienste. Ohne den Parameter würde nur ein Kommando ausgeführt werden (vgl. chroot /mnt whoami). Der Parameter -D gibt systemd-nspawn an, dass es den angegebenen Pfad als Root Filesystem zum starten verwenden soll. Es könnte dafür auch Disks, Imagedateien oder OCI Images (Docker) verwenden. Dann wird ein anderer Parameter benötigt.

#!/bin/bash
systemd-nspawn -bD /mnt/oldpi --bind=/mnt

Verwenden mit interaktiven Terminal

Zum start des Container muss man beachten, dass dieser im Vordergrund passiert. Startet man das systemd-nspawn Kommando befindet sich die Eingabe und der Bildschirm sofort im Container. Man kommt also nicht einfach zurück in eine reguläre shell auf dem Host System. Andererseits muss das auch als Vordergrund Prozess geöffnet bleiben, damit der Container läuft. Ein Hinweis: Ob man das Problem mit einer Screen Session umgehen kann, habe ich ehrlich gesagt nicht geprüft. Ich habe das System Headless betrieben und bin nur per SSH auf den Raspberry. Das Bedeutet, der Start hätte in der einen SSH Session zwar geklappt, nur wäre ich dann in dem Gast System "gefangen" gewesen. Die SSH Session hätte ich nicht schließen können.
Um das Problem zu umgehen (ohne Screen) habe ich einfach das Terminal des Raspberry Pi genutzt, was nach Boot des Systems mit Login-Screen angeboten wird. Ein Bildschirm war angeschlossen und Maus und Tastatur habe ich ebenfalls verbunden. Dies ist der Login-Screen des Host Systems, an dem ich mich erst mal mit User + Passwort einloggen muss. ! Auch muss ich per Sudo zum Root User werden, da systemd-nspawn nur als Rootuser funktioniert. Und dort wurde dann oben genanntes Script aufgerufen und der Container startete. Das Terminal konnte ich dann offen lassen und mich per SSH auf den Host verbinden. Beim Start von systemd-nspawn ist das so, man befindet sich mit Bildschirm und Eingabe zuerst auf dem Host System vor dessen Login-Screen und meldet sich dort an. Dann Startet man aus der Shell dort den Container. Die Shell Eingabe wartet so lange darauf auf den systemd-nspawn Prozess, bis dieser sich beendet hat. Das passiert erst, wenn der Container sich "heruntergefahren" hat. Man sieht dann am Bildschirm den Startprozess des alten Systems, wie es mit der alten SD-Karte auch zusehen war. Danach landet man wieder auf einem Login-Screen, jetzt aber auf dem des alten System. Jetzt könnte man sich dort wieder Einloggen, mit den Anmeldedaten des alten Systems.

Login in den Container in zweiter Session

Wenn man wie ich im Terminal des Raspberry Pi den Container gestartet hat, dann kann man dort ja nicht die Shell des Host Verwenden. Ich verwende den Raspberry Headless, d.h ich logge mich normalerweise nur per SSH ein. Wenn man dann Zugriff auf den Container benötigt, ist man mit dem SSH in einer separaten Session auf dem Host System Unterwegs. Man kann aber ein weiteres Terminal zum Container öffnen, dass dann ganz ähnliche Voraussetzungen bezüglich Bildschirm und Eingabe und Vordergrundprozess hat, wie der eigentliche systemd-nspawn Befehl. Doch kann man dann wiederum eine weitere SSH Session öffnen, um auf Host und Gast Zugriff zu haben. Der Befehl um in den Container mit einer zweiten Session zu gelangen ist sudo machinectl login oldpi . Auch hier muss wieder mit root gearbeitet werden, daher der sudo Befehl. Das Kommando machinectl verwaltet systemd-container und das Argument login öffnet für einen die Login Session im Gast. Der Name oldpi kommt daher, dass mein altes System unter /mnt/oldpi gemountet und von dort als Verzeichnis gestartet wurde. Ist der Login-Screen erschienen (es ist ist nicht äquivalent zu einer SSH Session oder dem Chroot), muss man sich dort mit User & Passwort anmelden.

Dienste, IP Adressen und Ports

In jedem Raspberry Pi System gibt es Dienste. Einige sind Dabei auch Server und binden sich zum Einen an eine IP Adresse und zum Anderen an einen Port. Man möchte ggf. auch Dienste, wie z.B. Webserver vom alten System auf das neue System migrieren. Dabei gibt es einiges zu beachten.
Der Container startet, so wie hier gezeigt, mit der identischen Netzwerkkonfiguration wie der Host. So teilen sich Host- und Gast- Prozesse die IP-Adressen und somit auch die Verfügbaren Ports. Auf dem neuen System ist z.B ein SSH Server installiert. Dieser startet zuerst - mit dem Host System start. Danach wird der Gast gestartet, in dem auch ein SSH Server installiert ist. Dieser wird bei Container start auch versucht zu starten. Doch dass scheitert: "Address already in use". Um das zu lösen kann man das Netzwerk ändern (s.o.) oder den SSH Server im Gast auf einen anderen Port ändern. Aber das passt nicht zu dem kurzen Gastspiel während der Migration. Der Fehler im Gast ist so einfach normal. Das Bedeutet, dass man beim Versuch eine SSH Verbindung aufzubauen aber immer im Host landet.
Ferner wollte ich Dienste wie Webserver migrieren. Diese waren noch nicht im Host System installiert und starteten im Gast. So konnte ich den dort existierenden alten Webserver weiter benutzen. Ich bereitete also die neue Installation zunächst für das neue System vor, während der Server im neuen System noch nicht gestartet war. Dann loggte ich mich in den Gast ein, stoppte den Dienst dort und startete auf dem Host den Dienst dort. So konnte ich die Dienste nacheinander umziehen lassen.

Die Migration danach

Der Start des alten Systems war nur die Vorbereitung zur eigentlichen Migration. Es gig darum das alte System weiter laufen lassen zu können, während man mit der Migration versucht Schrittweise voran zu kommen. Ich habe dann noch einen weiteren Schritt benötigt. Das alte Backup Projekt wollte die Festplatte einschalten können. Das benutze noch einen Befehl, der sehr lange nicht mehr integriert wurde und dessen Projekt tot war. Auch trotz Hardware Support des Containers konnte ich das alte gpio Kommando nicht mehr verwenden, da es durch den Architektur unterschied im proc- FS nicht eine Info angebe fand, um den Raspberry Pi Typ zu erkennen. Vorzeitig musste ich das Script zum GPIO steuern migrieren auf den Host und auf das neue äquivalente Kommando pinctrl. Das ging sehr schnell. Doch das Backup System verwendete zum Zugriff von anderen Servern aus eine Web Schnittstelle. Diese funktionierte dank Container zwar bis zum Webserver. Doch diese versuchte per SSH zum User pi sich zu verbinden und dann erst das Shell Script zu starten. Somit landete aber der Web- Request dann im Host System. Aber arbeiten wollte das Backup System vorläufig im Gast. Auch da musste ich mit einem extra Bind-Mount das Shell-Script vom Gast entfernbar "Überschreiben". So blieb theoretisch das alte System unverändert, im Container jedoch nutzte es andere Anweisungen.
Die Web Schnittstelle des Backup Systems habe ich dann auch noch umgestellt. Mehr dazu hier.
Nach und nach habe ich dann die Dienste auf dem neuen System installiert und die Dienste auf dem alten System deaktiviert. Als die letzten Dienste migriert waren, habe ich in das Terminal mich ein letztes mal innerhalb des Containers eingeloggt. Dort habe ich nur noch das Gastsystem mit einem poweroff heruntergefahren. So befand ich mich wieder auf der Shell im Host System vom Terminal dort. Die shell habe ich dann nur verlassen und das alte System unmountet. So war die Migration abgeschlossen.