Anytype – everything everywhere all at once!

Alles begann mit einem c’t 3003 Video!

Ich bin großer Fan von den c’t 3003 Videos, es gibt immer wieder Anregungen, aber Anytype ist ein sog. Game-Changer!

Ich hatte von Notion schon gehört, aber wie in dem Video beschrieben, landen alle Daten in einer US Cloud. Das schreckt dann doch ab. Dann kam Anytype. Holy motherf**king shit.

Zunächst erstmal: Ja, es läuft alles auf einer (!) VM. Zwar mit der komplexesten Docker-compose Anweisung, die ich je gesehen habe, aber es läuft in einer VM. Wenn man sich allerdings anschaut, was da dann passiert, sobald man Anytype startet, versteht man auch nur zu gut, warum darauf hingewiesen wird, dass die docker-compose Version nur für einen einzelnen Nutzer ist. Sobald da mehrere hinzukommen, sollte man auf ein Kubernetes-Cluster ausweichen und dieses mit Ansible bauen lassen. Aber ich schweife ab…

Anytype ist das Tool, was a) komplexe Tools vereint und b) auf wirklich jedem Endgerät eine App hat. Und ja, die selbst gehostete Version ist mit ein wenig Netzwerkmagic von überall auf der Welt erreichbar.

Ich stampfe mein Wiki (eigene VM) und mein KanBan (eigene VM) ein, weil ich das ganze jetzt in Anytype machen kann.

Ich kann die Daten lokal (auf meinem Rechner) sichern, ohne große Schwierigkeiten.

Das Ding kann alles beherbergen: von Büchersammlungen inkl. -Rezensionen, über Terminkalender bis hin zu Projektmanagementmappen. Ich habe die Mindmap für die gesamte Userstory, die ich am 16.04.2025 veröffentlicht habe, unter Anytype gemacht.

Und es läuft auf dem Smartphone, egal ob auf Android oder iOS.

Kurzer Eintrag für einen Termin, der nicht vergessen werden sollte? Kein Problem.

Etwas vergessen, auf die Einkaufsliste zu setzen? Kein Problem.

Eine Gedankengang, den man nachher fortführen möchte? Kurz notiert.

Eine zündende Idee, für die man jetzt keine Zeit hat? Kurz notiert und mit Anregungen versehen.

Das Tool ist für Leute mit vollem Terminkalender, mehreren Projekten und einem prall gefüllten Sozialleben die Entlastung, die man immer gesucht hat.

Also, sollte dies hier irgendjemand lesen, dem das Leben gerade über den Kopf zu wachsen scheint: Installiert es, nutzt es, werdet glücklich. Großartiges Tool!

Alte und neue VMs

Nachdem also ein Backup erstellt wurde, das neue OS ausgesucht wurde, ging es nun an die Installation.

In der alten Konfiguration hatte ich im BIOS ein Hardware RAID10 erstellt, den ich zunächst aufgelöst habe.

Anschließend erfolgte die Installation des Betriebssystems über einen USB-Stick. Nicht der Rede wert. Dann wurde es interessant. Die erste Amtshandlung war die Installation von Netplan und der Virtualisierungssoftware.

Warum? Weil ich OPNSense auf einer virtuellen Maschine laufen hatte, und OPNSense als mein Netzwerkhub agiert. Sprich: sämtliche Netzwerkeinstellungen funktionieren nur, sofern diese VM läuft. Und dies VM erreicht nur alles, sofern das Netzwerk auf dem Server ordentlich konfiguriert ist. Also die Netzwerkeinstellungen kopiert und die VM importiert. Somit hatte ich in Minuten wieder mein Netzwerk mit allen zuvor geliebten Einstellungen wieder, was bei insgesamt 8 verschiedenen Netzwerken auch Sinn ergibt.

Anschließend wurden die VMs auch zurückgespielt. Aber da einige davon auch noch auf Ubuntu-Servern liefen, wurden diese halt auch auf Debian Server umgezogen.

Interessante neue Erkenntnisse wurden diesbezüglich gewonnen:

  • MariaDB
    • Ich hatte ursprünglich damit gerechnet, dass es den Export der einzelnen Datenbanken bedurfte, um diese dann DB für DB wieder im neuen Server herzustellen. MariaDB hat dafür mittlerweile ein Backup-Tool, welches allerdings penibel genau mit der Versionierung ist. Aber, das Sichern und Zurückspielen waren jeweils eine CLI Eingabe, und somit mal: Kudos an die MariaDB Betreiber.
  • Bei den restlichen Servern gab es keine Probleme, einige behalten bis auf Weiteres auch ihren Unterbau, doch dazu später mehr.

Der erste interessantere Fall war das NAS.

Kurz zur Ausgangssituation: Ich hatte, wie bereits erwähnt, einen Hardware-RAID10 im BIOS eingestellt, die Kapazität von 16TB (4*8TB/2) dann aber auf das NAS und auf die Cloud aufgeteilt, war damit aber nicht wirklich glücklich. Zum einen bin ich seit geraumer Zeit der Einzige, der meine Cloud nutzt. Zum anderen kann ich die Daten aber dann entweder unter Cloud abrufen, oder aber auf dem NAS, habe keine direkte Verbindung zum NAS.

Dazu lief das NAS noch auf einer sehr umständlichen QNAP NAS Virtualisierung, die mich an nervte. Einer der ersten Beschlüsse beim Umzug war: Sobald das neue System steht, wird das NAS ersetzt.

Im Zuge der Betriebssystemauswahl wurde ich dann auf TrueNAS aufmerksam. Das gefiel mir zwar nicht als Basis-OS, aber in einer virtuellen Maschine würde es dann doch noch laufen. Also Fix installiert, und jede einzelne der vier Platten an die virtuelle Maschine durchgereicht. Diese dann unter TrueNAS zu einem Raidverbund gekoppelt, und schon hatte ich meine gesamten 16 TB als NAS Speicher.

Das brachte mich dann zur Cloud. Ich hätte gern, dass die NAS Freigaben auch von meiner Cloud zugreifbar sind.

Der Neubau der Cloud war eh der Plan. Also die Cloud neu aufgesetzt, und dann die Anbindung an das TrueNAS ausprobiert. Da dies aber nicht im grafischen Menü funktioniert, muss man da pre CLI Hand anlegen.

sudo -u www-data php occ files_external:create <NameInNextcloud> 'smb' password::password -c host=<IP-Adresse> -c share=/<share/auf/TrueNAS>/ -c root=/<Verzeichnis/im/Share> -c domain=<DomainName> -c user=<smbuser> -c password=<smbPasswort>

Der Befehl ist relativ umfangreich, wenn er allerdings gelingt, erscheint dann folgende Zeile auf dem Bildschirm.

Storage created with id 1

Das hat mich natürlich nicht davon abgehalten, dies direkt in der Nextcloud zu überprüfen, und siehe da, der SMB-Share ist angelegt. Allerdings war es noch für jeden Nutzer zugänglich, was eigentlich nicht mein Plan war.

Also musste ich noch diesen Befehl hinterherschicken:

sudo -u www-data php occ files_external:applicable --add-user=<NextcloudNutzername> <Storage ID>

Damit war es dann aber auch getan, und siehe, mein Nutzer hatte vollen Zugriff über die Cloud auf sein NAS Verzeichnis. Mission Accomplished!

Nach TrueNas kam Semaphore. Semaphore ist ein grafisches Frontend gekoppelt an einen Webserver, welches Ansible und Terraform unterstützt, und demnächst noch Powershell bekommen soll (Ansible unterstützt schon einiges auf MS Seite). Terraform kommt von der Hashicorp und ist iaC (infrastructure as Code), in Kurzform: Ich deklariere, was ich an Infrastruktur bauen möchte, und Terraform führt es aus.

Bislang habe ich alles manuell angelegt, aber beide Tools haben mich fasziniert. Also wurde ein Semaphore Server aufgesetzt, ein Projekt begonnen, und ein Testserver aufgesetzt, auf dem ich die Playbooks testen kann. Das, was Semaphore gern hätte, wäre eine Anbindung an Github. Allerdings habe ich kein Interesse, meine Daten auf GitHub zu speichern.

Also habe ich mir mit Gitea kurzerhand ein lokales Github aufgebaut, und das daran angebunden. Was auch funktioniert. Nun habe ich ein Github und ein Semaphore.

Allerdings liegt meine derzeitige Anzahl an ständig laufenden VMs mittlerweile bei 20, und der Server liegt im Moment bei 80 % Auslastung.

Die Auswahl des neuen Betriebssystems für den Server

Bei der Abwahl von Ubuntu und der Wahl eines neuen Betriebssystems für den Server habe ich mir Zeit gelassen, um mir die verschiedenen Optionen intensiv anzuschauen.

Der Server beherbergt einen Xeon E-2278G mit 128 GB ECC RAM, eine 4 GB NVME SSD und vier 8TB Serverplatten, was diesen für reine Virtualisierungsplattform prädestiniert.

Aber Virtualisierungsoptionen gibt es mittlerweile viele am Markt.

Ausrangiert wurde anfänglich alles, was mit unkontrollierbaren Kosten verbunden war.

Dies eliminierte VM-Ware.

Dann kam noch die potenziell notwendige Cloud-Anbindung hinzu.

Das eliminiert alles, was Microsoft veranstaltet. Obwohl zum Zeitpunkt der Auswahl die mittlerweile scheinbare Pflicht zum MS-Konto noch nicht vorhanden war, ist bei MS mittlerweile der eindeutige Trend zur Cloud mehr als erkennbar. Hinzu kamen die Kosten. MS-Server mit angeschlossener SQL Datenbank war einfach nicht im Budget.

Ubuntu fiel aufgrund von Snap aus dem Rahmen.

Ernsthaft in Erwägung gezogen habe ich RHEL, es fiel aber dann doch aufgrund der Kosten aus dem Rahmen.

Meine vier übrigen Optionen waren Debian, Proxmox, TrueNASScale und Unraid.

TrueNas und Unraid sind eigentlich primär NAS-Software-Distributionen, aber beide bieten mittlerweile ein umfangreiches Paket an, welches auch Virtualisierungsoptionen beinhaltet.

Alle vier sind FreeBSD- und/oder Linux-basierende Distributionen, und soweit bekannt, nutzen alle KVM-basierende Virtualisierung. (TruNAS experimentiert mit Incus ab der Version 25.04, Version 24.10 basiert noch auf KVM).

Heruntergebrochen heißt das: Die Virtualisierung erfolgt bei allen gleich, das interessante wird nun das Frontend. Allerdings benötigte ich auch noch ein NAS.

Und so kommt man schnell bei einer eigentlich simpel erscheinenden Software-Auswahl vom Hölzchen aufs Stöckchen.

Erster Schritt war nun, den Funktionsumfang zu testen. Das heißt primär:

Oberfläche + mögliche andere Oberflächen und Tools. TrueNAS und Unraid mögen zwar einfach zu bedienen sein, aber was passiert, wenn ich komplexere Themen angehen möchte, wie z.B. das Editieren von config-, yaml-, json oder gar py-Dateien?

Inwiefern bringen diese Distributionen Firewalls mit und wie leicht sind sie zu schreiben?

Wie editiere ich Netzwerkeinstellungen? Wie schnell werden die Änderungen wirksam? Was passiert mit den laufenden VMs, wenn das OS sich verabschiedet?

Also habe ich alle verglichen. Auf Ubuntu-Server habe ich zu dem Zeitpunkt gearbeitet, Debian war da nicht weit von entfernt, also konnte ich mir den Test sparen. Proxmox hatte ich im Cluster schon getestet.

TruNAS und Unraid wurden schnell mal in virtuellen Maschinen installiert, um sich die Oberflächen mal genauer anzuschauen.

Und da stößt man dann ganz schnell auf die Probleme. So nett die Oberflächen auch sind, die geben nicht alles her, was ich von einem System gewohnt bin und was ich erwarte. Und da die Software-Repositories der beiden Distributionen doch erheblich kleiner waren als die von Debian, bin ich auch ganz schnell von diesen ab.

(für die Windowsjünger: man kann unter Linux mal nicht eben alles installieren, was eine .exe oder .msi Endung hat. Software muss für das OS zugeschnitten sein, deswegen haben diese Systeme auch eine eigene Auswahl an Software, welche durch Befehle noch um weitere Softwaredatenbanken erweitert werden kann)

Also lief es nachher auf einen Zweikampf von Debian und Proxmox hinaus. (TrueNAS kommt in Zukunft nochmal ran, aber ich greife vor).

Interessanterweise habe ich eine Debian Installation schon zu einer Proxmox Installation umgewandelt, diese beiden waren also auch technisch gesehen nah beieinander.

Es gab zwei Kriterien zum Abschluss zwei Kriterien: Bedienbarkeit und Sicherung.

Die Sicherung der einzelnen VMs unter Proxmox ist hervorragend. Anbindung des externen Speichers, drei Klicks, und die Sicherung ist erstellt. Viel leichter geht es nicht. Aber ich hatte unter Ubuntu das Backup-Script geschrieben, also war dieser Vorteil von nicht ganz so großer Bedeutung.

Blieb noch die Bedienbarkeit: ClI (Command-Line-Interface, also der gute alte Prompt) hatten beide, Proxmox ist ein Web-Server, Debian kann mehrere Oberflächen unterstützen. Dazu noch Software, welche einem die Arbeit mit dem Gerät erleichtert.

Ausschlaggebend war letztendlich ein kleines Detail an Proxmox: Jedes Mal, wenn ich mich in die Community Edition von Proxmox eingeloggt habe, erschien das Fenster mit dem Hinweis, dass ich kein gültiges Abonnement bei Proxmox hatte. Am Anfang ist es egal, dann wird es nervig, beim 52ten Mal regt es zum Nachdenken an. Das alte System hielt 5 Jahre, bevor ich es neu aufgesetzt habe. Das Neue sollte ebenso lange halten. Und wenn ich spekulieren muss, ob Debian oder Proxmox für nächsten halbe Dekade kostenlos bleibt, bin ich eher bei Debian. Also wurde Debian die nächste Grundlage für meinen Server!

Umzug des Servers von Ubuntu auf Debian

Der Neuaufbau eines Servers erfordert immer Planung. Gibt es Änderungen in der bereitgestellten Hardware, gibt es neue und vor allem bessere Software zum Erreichen der Ziele?

Aber zunächst gilt es die Daten, welche anschließend noch benötigt werden, zu sichern. Das heißt, das Backup sollte vollumfänglich sein. In meinem Fall war dies zunächst die Sicherung der virtuellen Maschinen, und da sich unter den VM auch ein NAS und eine Cloud war, auch eine zusätzliche Sicherung der Daten.

Die vollzieht man vor der Sicherung der VMs. Allerdings gestaltet sich die Sicherung der VMs bei dem Volumen an VMs, welches ich laufen hatte, als schwierig. Jede einzelne VM zu sichern, würde immense Zeit in Anspruch nehmen. Also wäre ein Script die Wahl.

Das Ganze wird natürlich auf einem Backup-Server gesichert. Als Grundlage habe ich mir einen etwas in die Jahre gekommenen Athlon 5350 genommen, ihn mit einer kleinen SSD und einer großen Festplatte bestückt. Die CPU ist schwachbrüstig, aber das macht ehrlich gesagt nichts, denn mehr als das OS hosten und die Daten für die Backup-Platte entgegennehmen muss sie nicht.

Dennoch wäre es schön, wenn der Backup-Server nur dann laufen muss, wenn er wirklich benötigt wird. Und wozu gibt es WakeOnLAN, wenn nicht für solche Situationen?

Also kurzerhand die notwendigen Einstellungen im Bios gesetzt, und dann versucht, ihn mit Netplan zum Annehmen von Paketen zu bewegen.

Den zunächst testet man erstmal, ob das System WakeOnLan Pakete entgegennimmt.

sudo ethtool eth0 | grep Wake-on
        Supports Wake-on: pumbg
	Wake-on: g

Das wäre die optimale Einstellung, allerdings war dies bei mir nicht der Fall. Bei mir sagte der Server, dass er keine WakeOnLAN Pakete entgegennahm.

Es gibt im Netplan die Option:

wakeonlan: true

Aber die brachte mich auch nicht weiter.

Was mir dann doch geholfen hat, war ein Service zu kreieren, der mir mithilfe von ethtool den Service startet:

[Unit]
Description=Wake-on-LAN for eth0
Requires=network.target
After=network.target

[Service]
ExecStart=/sbin/ethtool -s eth0 wol g
Type=oneshot

[Install]
WantedBy=multi-user.target

Dieses Codesnippet in /etc/systemd/system als wol@eth0.service gespeichert und dann gestartet, und siehe da, der PC nimmt jetzt WakeOnLan Pakete an.

Zur weiteren Vorbereitung galt es dann, sich passwortlos auf dem Backup-Server anmelden zu können, am besten via ssh.

ssh-keygen -t ed25519 -f /Pfad/zum/privatenSchlüssel -C "Kommentar zum Schlüssel"

Dieser Befehl erstellt ein Paar Schlüssel. Einen privaten, und einen öffentlichen, welcher die Endung .pub (public) erhält. Bei der Erstellung wird eine doppelte Passwortabfrage zum privaten Schlüssel kommen, diese sollte mit einem ausreichenden Passwort beantwortet werden, kann aber auch leer gelassen werden. Ascnhließend transferiert man den öffentlichen Schlüssel auf die gweünschte Maschine.

ssh-copy-id -i /Pfad/zum/öffentlichenSchlüssel.pub benutzername@<IP Adresses oder Hostname>

Somit kann man jetzt: Anschalten, einloggen, Scheiße bauen, aber es fehlt noch: Ausschalten.

Dies geht zwar auch via SSH, aber spätestens bei dem Befehl:

sudo shutdown now

verlangt das System dann das Passwort.

Aber auch da gibt es Abhilfe:

Entweder man kreiert eine Gruppe, fügt den Nutzer hinzu, der passwortlos den Shutdown einleiten kann, oder man lässt es den User direkt machen.

Sollte man die Gruppe anlegen wollen, muss man /etc/group editieren.

Auf jeden Fall muss man anschließend /etc/sudoers editieren:

%Groupname ALL=NOPASSWD: /sbin/shutdown

Username ALL=NOPASSWD: /sbin/shutdown

Der erste Befehl erlaubt der Gruppe den passwortlosen shutdown, der zweite dem User. Es sollte selbstverständlich sein, dass der Nutzer, mit dem der ssh Zugriff erfolgt, der Gruppe angehören sollte, oder aber der singuläre Nutzer ist, der das System passwortlos herunterfahren darf.

Nachdem jetzt die Vorbereitungen getroffen sind, das Script:

#!/bin/bash
###################################################################################################
#                                                                                                 #
#                                                                                                 #
#                          Backup of Virtual Machines Script                                      #
#                                                                                                 #
#                                                                                                 #
#                                                                                                 #
#                                  by Florian Klaus                                               #
#                                                                                                 #
#                                                                                                 #
#                                                                                                 #
###################################################################################################

# We need xmlllint and wakeonlan, let's scheck if the correspondings software pakets are installed
# xmllint is part of libxml2-utils, etherwake will install wakeonlan automaticly and is also needed
paketEtherwake=$(dpkg -s etherwake | grep Status)
paketLibxmlutils=$(dpkg -s libxml2-utils | grep Status)

#echo $paketEtherwake
#echo $paketLibxmlutils

if [[ $paketEtherwake = "Status: install ok installed" ]] && [[ $paketLibxmlutils = "Status: install ok installed" ]]
then
	echo "etherwake and libxml2-utils are installed"
elif [[ $paketEtherwake = "Status: install ok installed" ]] && [[ $paketLibxmlutils != "Status: install ok installed" ]]
then
	echo "libxml2-utils musst be installed, doing so now"
	apt update
	apt install -y libxml2-utils
elif [[ $paketEtherwake != "Status: install ok installed" ]] && [[ $paketLibxmlutils = "Status: install ok installed" ]]
then
	echo "etherwake musst be installed. doing so now"
	apt update
	apt install -y etherwake
elif [[ $paketEtherwake != "Status: install ok installed" ]] && [[ $paketLibxmlutils != "Status: install ok installed" ]]
then
	echo "neither Paket is installed, installing both now"
	apt update
	apt install -y etherwake libxml2-utils
fi

# Highest Number of source files in a single (!!!) VM installation. Source files may include the installation disk, 
# that is still in use, for one reason or another (mostly .iso-files), but they may also include different hard disks 
# associated with on VM (qcow2-files). 
# Check your installations and take the highest number. if it exceeds 5, then, first of all,
# may god have mercy on your soul. But you will have to set this number higher. Default is 3.

sourceFileHigh=3


# Excluded VMs - Virtual Machines you might want to exclude (for reasons) from the backup
# We need the name of the VM, as defined in the corresponding XML File
# Since this file is going to be parsed one line at a time, one VM per line please

excludedVMsFile="excludedVMs.txt"

nrOfExcludedVMs=$(wc -l < $excludedVMsFile)


# the stat command-output is localized, so please check a file e.g. stat filename.txt and look at the last 4 lines, 
# and remember, they are case-sensitive!!!:

# lastModifiedFile="Modify"
# lastChangedFile="Change"
# birthOfFile="Birth"
lastModifiedFile="Modifiziert"
lastChangedFile="Geändert"
birthOfFile="Geburt"


# XML Folder, where the XMLs for the VirtualMachines are stored, usually '/etc/libvirt/qemu'

xmlDir="/etc/libvirt/qemu/"


# temporal directory to save backup files in. /tmp is not chosen because it only lasts until reboot

localBackupDir=/var/tmp/backup-vms

#local sshfs mount directory, needs to be created

localMntDir=/mnt/backup-server

if [[ -d $localMntDir ]]
then
	echo "Local mount directory exists, nothing to do here ..."
else
	echo "Local mount directory doesn't exist, creating ..."
	mkdir $localMntDir
fi

# external Data

externalIP="192.168.168.192"

macAddress="FF:FF:FF:FF:FF:FF"

externalUserName="Username"

externalBackupDir="/home/$externalUserName/vm-backup-dir"

# Establish connection to backup-server

ping -c 1 $externalIP  &> /dev/null
pingStatus1=$( echo $? )
if [[ $pingStatus1 == 0 ]]
then
	echo "Backup-Server online, establishing connection"
	mountPoint="$externalUserName@$externalIP:$externalBackupDir $localMntDir"
	sshfs $mountPoint
	sleep 5s
	#exit
else
	echo "Backup-Server offline, trying to start it now ..."
	wakeonlan $macAddress
	sleep 120s
	ping -c 1 $externalIP &> /dev/null
	pingStatus2=$( echo $? )
	if [[ $pingStatus2 == 0 ]]
	then
		echo "Backup-Server woken up, establishing connection"
		mountPoint="$externalUserName@$externalIP:$externalBackupDir $localMntDir"
		sshfs $mountPoint
		sleep 5s
		#exit
	else
		echo "No Connection to the back-server, exiting ..."
		exit
	fi
fi

# Let's check if the local backup directory excists, and, if not, create it

if [[ -d "$localBackupDir" ]]
then
	echo "local backup directory exists, nothing to do here ..."
else
	echo "local backup directory doesn't exist, creating ..."
	mkdir "$localBackupDir"
fi

# Let's check if the Exclusion File is found

if [ -f $excludedVMsFile ]
then
	echo "found exclusion file"
fi

# Let's name our VM textfile
vmTextFile="listOfXMLs.txt"

# Remove any list of XMLs, if there is any

if [ -f $vmTextFile ]
then
	echo "$vmTextFile exists, removing ..."
	rm $vmTextFile
fi


# Find all XMLs in the given XMLDirectory, and make a List of them
find $xmlDir*.xml | rev | cut -d '/' -f1 | rev > $vmTextFile

# count the Number of XMLs, e.g. the Number of virtual machines

nrOfXMLs=$(wc -l < $vmTextFile)

# Let's create a file with the current running VMs

runningVMs="runningVMs.txt"

# remove any list of running VMs, if there is any

if [ -f $runningVMs ]
then
	echo "$runningVMs exists, removing ..."
	rm $runningVMs
fi  

ps -ef | grep qemu-system-x86_64 | cut -d',' -f1 | grep name | cut -d'=' -f2 > "$runningVMs"

# Let's see how many running VMs we have

nrOfRunningVMs=$(ps -ef | grep qemu-system-x86_64 | cut -d',' -f1 | grep name | cut -d'=' -f2 | wc -l)

# Now we iterate through the list of XMLs (e.g. VMs) and start doing shit with them 

for (( i=1 ; i<=$nrOfXMLs ; i++ ))
do 
	# the current VM
	currentXML=$(sed "${i}q;d" $vmTextFile)
	
	# We grab the Name of the current VM, and cut loose all the shit that thing doesn't need.
	nameOfVM=$(xmllint "$xmlDir$currentXML" | grep '<name>' | cut -d '>' -f2 | rev | cut -d '<' -f2 | rev)

	# Check if the VM to backup isn't exscluded.
	if [ -f $excludedVMsFile ]
	then
		for (( n = 1; n <= $nrOfExcludedVMs; n++ )) 
		do
			excludedVM=$(sed "${n}q;d" $excludedVMsFile)
			if [[ $nameOfVM = $excludedVM ]]
			then
				echo "Nothing to do with $nameOfVM"
				break
			fi
		done
	else
		echo "$nameOfVM is not excluded, continuing ..."
	fi
	if [[ $nameOfVM = $excludedVM ]]
	then
		echo "$nameOfVM has been excluded"
		continue
	elif [[ $nameOfVM != $excludedVM ]]
	then
		if [[ $nrOfRunningVMs -ge 1 ]]
		then 
			for (( x = 1; x <= $nrOfRunningVMs; x++ ))
			do 
				currentRunningVM=$(sed "${x}q;d" $runningVMs)
				if [[ $nameOfVM = $currentRunningVM ]]
				then
					echo "$nameOfVM is currently running, suspending ..."
					virsh suspend $nameOfVM
					break
				fi
			done
		fi

		# bash in itself doesn't let one operate with objects, so we are circumventing this 
		# right now by creating txt-based backupfiles
	
		# We create a current backupfile
		currentBackupFile="$nameOfVM"-Backup.txt""

		currentFileSourceFile="$nameOfVM"-SourceFile.txt""
	
		# The current Backupfile is obsolete, so we are deleting it
		if [[ -f $currentBackupFile ]]
		then 
			rm "$currentBackupFile"
		fi
	
		# The name of the VM is the first line in the newly created backup file
		echo "$nameOfVM" > "$currentBackupFile"
	
		# The XML file (including the directory path) is the second line in the backup file
		echo ""XML File="$xmlDir$currentXML" >> "$currentBackupFile"
	
		# Determining how many source files are there, e.g. we count the number of sorce files. Source files may include the 
		# installation disk (mostly .iso-files),
		# but they may also include different hard disks (qcow2-files). That's why we set the number earlier
		
		currentNrOfSourceFiles=$(xmllint "$xmlDir$currentXML" | grep 'source file' | wc -l)
		
		currentSourceFiles=$(xmllint "$xmlDir$currentXML" | grep 'source file')

		if [[ -f $currentFileSourceFile ]]
		then
			rm "$currentFileSourceFile"
		fi

		# declare k and l, which will incrememnted by 2 every time in the loop, to cut out the path for the different source files
		k=1
		l=2	

		# Now comes the other for loop, in which we will itereate through the different source files. See above

		for (( j=1 ; j<=$sourceFileHigh ; j++  ))
		do			

			cutSourceFile=$(echo $currentSourceFiles | grep 'source file' | cut -d " " -f${k}-${l}) 
			modSourceFile=$(echo $cutSourceFile | cut -d "=" -f2 | cut -d '"' -f2) #| cut -d ">" -f1 | rev | cut -d "/" -f2 | rev | cut -d '"' -f1)
			
			# if the source file is empty, just put a blank line in there
			if [[ -z $modSourceFile ]]
			then
				echo "$modSourceFile" >> "$currentBackupFile"
			# else put the following info in there
			else
				echo ""Source Path="$modSourceFile" >> "$currentBackupFile"

				echo "$modSourceFile" >> "$currentFileSourceFile"

				# now, let's dive into the statistics for the source file 

				# now, let's grep the info and store it in a file
				modifySourceFile=$(stat $modSourceFile |  grep $lastModifiedFile | cut -d ":" -f2-4 | cut -d " " -f2-4)
				echo ""Last Modified="$modifySourceFile" >> "$currentBackupFile"
				changeSourceFile=$(stat $modSourceFile |  grep $lastChangedFile | cut -d ":" -f2-4 | cut -d " " -f2-4)
				echo ""Last Changed="$changeSourceFile" >> "$currentBackupFile"			
				birthOfSourceFile=$(stat $modSourceFile |  grep $birthOfFile | cut -d ":" -f2-4 | cut -d " " -f2-4)
				echo ""Birth Of File="$birthOfSourceFile" >> "$currentBackupFile"
			
				

			fi
	

			# k needs to be incremented by 2
			k=$((k+2))

			# l needs to be incremented by 2
			l=$((l+2))
		
		done

		# Last line in file
		echo "End of File" >> "$currentBackupFile"


		# Check if the local backup directory exists, and, if it doesn't, create it
		if [[ -d "$localBackupDir/$nameOfVM" ]]
		then
			echo "local backup directory for $nameOfVM exists, nothing to do here ..."
		else
			echo "local backup directory for $nameOfVM doesn't exist, creating ..."
			mkdir "$localBackupDir/$nameOfVM"
		fi

		# Let's check if there is a backup file already and compare it to the current file.
		# Since the we just accumulated all the information about the VM, we check if the VM has been started in any way, 
		# and if it hasn't, we won't need to do anything.
		# In the aforementioned case, the loop will stop here and continue to the next VM
		
		if [[ -f "$localBackupDir/$nameOfVM/$currentBackupFile" ]]		
		then
			if [[ 'cmp "$localBackupDir/$nameOfVM/$currentBackupFile" $currentBackupFile' ]]
			then
				echo "The VM hasn't changed since the last backup, no need to do anything ..."
				rm "$currentBackupFile"
				rm "$currentFileSourceFile"
				continue
			else
				echo "The VM has changed, removing old backup file ..."
				rm "$localBackupDir/$nameOfVM/$currentBackupFile"
				echo "Writing new local backup file"
				rsync --progress "$currentBackupFile" "$localBackupDir/$nameOfVM/$currentBackupFile"
			fi
		else
			echo "The backup file doesn't exist, creating ..."
			rsync --progress "$currentBackupFile" "$localBackupDir/$nameOfVM/$currentBackupFile"
		fi

		if [[ -d "$localMntDir/$nameOfVM" ]]
			then
				echo "Directory for $nameOfVM exists, nothing to do here ..."
			else
				echo "Directory for $nameOfVM doesn't exist, creating ..."
				mkdir "$localMntDir/$nameOfVM"
			fi

		date="$(date '+%Y-%m-%d')"

		# This checks if the backup was done today, and if it was, it assumes that something went wrong 
		# (usually there are not two backups in a single day,)
		# If this is the second backup in one day, it removes everything.
			
		if [[ -d "$localMntDir/$nameOfVM/$date" ]]
		then
			echo "Todays date directory exists, cleaning it up ..."
			if [[ -f "$localMntDir/$nameOfVM/$date/$currentXML" ]]
			then
				echo "$currentXML exists, removing ..."
				rm -v "$localMntDir/$nameOfVM/$date/$currentXML"
			fi
			if [[ -f "$localMntDir/$nameOfVM/$date/$currentBackupFile" ]]
			then
				echo "$currentBackupFile exists, removing ..."
				rm -v "$localMntDir/$nameOfVM/$date/$currentBackupFile"
			fi
		else
			echo "Todays date directory doesn't exist, creating ..."
			mkdir "$localMntDir/$nameOfVM/$date"
		fi

		# Last but not least, the copying process of the needed files

			if [[ -d "$localMntDir/$nameOfVM/$date" ]]
			then
				echo " Copying text files to backup destination ..."
				rsync --progress "$xmlDir$currentXML" "$localMntDir/$nameOfVM/$date/"
				rsync --progress "$currentBackupFile" "$localMntDir/$nameOfVM/$date/"
			fi

		sleep 10s

		nrOfSourceFiles=$(wc -l < $currentFileSourceFile)

		for (( z = 1; z <= $nrOfSourceFiles; z++ ))
		do
			currentQcow2=$(sed -n "${z}p" "$currentFileSourceFile")
			currentQcow2File=$(sed -n "${z}p" "$currentFileSourceFile" | rev | cut -d '/' -f1 | rev)

			if [[ -f "$localMntDir/$nameOfVM/$date/$currentQcow2File" ]]
			then
				echo "$currentQcow2 exists, removing ..."
				rm -v "$localMntDir/$nameOfVM/$date/$currentQcow2File"	
			fi

			if [[ -d "$localMntDir/$nameOfVM/$date" ]]
			then
				echo " Copying $currentQcow2 files to backup destination ..."
				rsync --progress "$currentQcow2" "$localMntDir/$nameOfVM/$date/"
			fi
			
		done
		
		# Cleanup ...

		echo "removing created files ..."

		rm "$currentBackupFile"
		rm "$currentFileSourceFile"

		# We need to resume the VM, if it was running in the first place ...

		if [[ $nameOfVM = $currentRunningVM ]]
		then
			echo "$nameOfVM needs to be resumed!"
			virsh resume $nameOfVM
		fi
	fi
done

# Cleanup ...

rm "$vmTextFile"
rm "$runningVMs"

# Unmount the sshfs directory

fusermount -u $localMntDir

# shutdown the remote backup-server

ssh "$externalUserName@$externalIP" "sudo shutdown now"

Knapp 450 geschmeidige Zeilen. Aber warum so viel?

Das Unterfangen wäre mit python wahrscheinlich einfacher gewesen, aber ich wollte es komplett auf bash-ebene durchziehen. Dennoch werden zwei Pakete benötigt, und die ersten Zeilen überpfrüfen ob diese installiert sind und tuen dies, wenn sie es nicht sind.

Anschließend werden ein paar Abfragen gestellt:

# Highest Number of source files in a single (!!!) VM installation. Source files may include the installation disk, 
# that is still in use, for one reason or another (mostly .iso-files), but they may also include different hard disks 
# associated with on VM (qcow2-files). 
# Check your installations and take the highest number. if it exceeds 5, then, first of all,
# may god have mercy on your soul. But you will have to set this number higher. Default is 3.

sourceFileHigh=3


# Excluded VMs - Virtual Machines you might want to exclude (for reasons) from the backup
# We need the name of the VM, as defined in the corresponding XML File
# Since this file is going to be parsed one line at a time, one VM per line please

excludedVMsFile="excludedVMs.txt"

nrOfExcludedVMs=$(wc -l < $excludedVMsFile)


# the stat command-output is localized, so please check a file e.g. stat filename.txt and look at the last 4 lines, 
# and remember, they are case-sensitive!!!:

# lastModifiedFile="Modify"
# lastChangedFile="Change"
# birthOfFile="Birth"
lastModifiedFile="Modifiziert"
lastChangedFile="Geändert"
birthOfFile="Geburt"


# XML Folder, where the XMLs for the VirtualMachines are stored, usually '/etc/libvirt/qemu'

xmlDir="/etc/libvirt/qemu/"


# temporal directory to save backup files in. /tmp is not chosen because it only lasts until reboot

localBackupDir=/var/tmp/backup-vms

#local sshfs mount directory, needs to be created

localMntDir=/mnt/backup-server

if [[ -d $localMntDir ]]
then
	echo "Local mount directory exists, nothing to do here ..."
else
	echo "Local mount directory doesn't exist, creating ..."
	mkdir $localMntDir
fi

# external Data

externalIP="192.168.168.192"

macAddress="FF:FF:FF:FF:FF:FF"

externalUserName="Username"

externalBackupDir="/home/$externalUserName/vm-backup-dir"

Zunächst einmal wird die maximale Anzahl an Source Files abgefragt. Dies sollten Platten sein, die der virtuellen Maschine zugeordnet sind, können aber auch Installationsmedien sein.

sourceFileHigh=3

Danach kann man eine Datei angeben, in die VMs geschrieben werden, die vom Backup ausgeschlossen sein sollen. Ausgehend von dieser Liste wird die Anzahl berechnet.

excludedVMsFile="excludedVMs.txt"

Anschließend wird darauf eingegangen, dass die Daten, die man zieht lokalisiert ausgegeben werden. Diese sollte man der entsprechenden Lokalisation anpassen.

lastModifiedFile="Modifiziert"
lastChangedFile="Geändert"
birthOfFile="Geburt"

Nun kommen noch die Abfragen zu einigen Verzeichnissen, und die Daten für den externen Backup-PC.

Und damit hat das Skript auch schon alles, was es benötigt.

# Establish connection to backup-server

ping -c 1 $externalIP  &> /dev/null
pingStatus1=$( echo $? )
if [[ $pingStatus1 == 0 ]]
then
	echo "Backup-Server online, establishing connection"
	mountPoint="$externalUserName@$externalIP:$externalBackupDir $localMntDir"
	sshfs $mountPoint
	sleep 5s
	#exit
else
	echo "Backup-Server offline, trying to start it now ..."
	wakeonlan $macAddress
	sleep 120s
	ping -c 1 $externalIP &> /dev/null
	pingStatus2=$( echo $? )
	if [[ $pingStatus2 == 0 ]]
	then
		echo "Backup-Server woken up, establishing connection"
		mountPoint="$externalUserName@$externalIP:$externalBackupDir $localMntDir"
		sshfs $mountPoint
		sleep 5s
		#exit
	else
		echo "No Connection to the back-server, exiting ..."
		exit
	fi
fi

Als Erstes stellt das Skript dann eine Verbindung zum Backup-Server her. Sollte diese nicht gegeben sein, wird es auch nichts mit dem Backup. Der Server wird auf Verfügbarkeit geprüft, und wenn dies nicht der Fall ist (wovon wir eigentlich ausgehen, aber man weiß ja nie), wird dieser via WakeOnLan gestartet.

Danach wird 2 Minuten gewartet, um dann eine erneute online Prüfung zu starten. Spätestens zu diesem Zeitpunkt sollte er da sein, und das externe Verzeichnis wird lokal gemountet.

Sollte zu diesem Zeitpunkt immer noch keine Verbindung bestehen, dann bricht das Skript an dieser Stelle ab.

# Let's check if the local backup directory excists, and, if not, create it

if [[ -d "$localBackupDir" ]]
then
	echo "local backup directory exists, nothing to do here ..."
else
	echo "local backup directory doesn't exist, creating ..."
	mkdir "$localBackupDir"
fi

Als Nächstes wird das lokale Backup Verzeichnis erstellt. Moment mal: Lokales Backup-Verzeichnis? Keine Panik, da wir eh mit Text-basierten Dateien arbeiten, ist dies ein Feature, welches uns zu einem späteren Zeitpunkt noch gelegen kommen wird.

# Let's check if the Exclusion File is found

if [ -f $excludedVMsFile ]
then
	echo "found exclusion file"
fi

# Let's name our VM textfile
vmTextFile="listOfXMLs.txt"

# Remove any list of XMLs, if there is any

if [ -f $vmTextFile ]
then
	echo "$vmTextFile exists, removing ..."
	rm $vmTextFile
fi


# Find all XMLs in the given XMLDirectory, and make a List of them
find $xmlDir*.xml | rev | cut -d '/' -f1 | rev > $vmTextFile

# count the Number of XMLs, e.g. the Number of virtual machines

nrOfXMLs=$(wc -l < $vmTextFile)

# Let's create a file with the current running VMs

runningVMs="runningVMs.txt"

# remove any list of running VMs, if there is any

if [ -f $runningVMs ]
then
	echo "$runningVMs exists, removing ..."
	rm $runningVMs
fi  

ps -ef | grep qemu-system-x86_64 | cut -d',' -f1 | grep name | cut -d'=' -f2 > "$runningVMs"

# Let's see how many running VMs we have

nrOfRunningVMs=$(ps -ef | grep qemu-system-x86_64 | cut -d',' -f1 | grep name | cut -d'=' -f2 | wc -l)

Nun geht es um die VMs. Als erstes wird überprüft, ob es eine Ausschlussliste gibt.

Dann wird die VM-Textdatei deklariert, geprüft ob sie bereits existiert (was sie nicht sollte, deswegen werden Vorgänger – Versionen auch gelöscht), und dann wurden alle XML-Dateien im XML Vereichnis gesucht und als Liste in die Datei gepackt. Anschließend wird die Anzahl ermittelt.

Und bevor wir uns jetzt in die Schleife stürzen, wird auch noch eine Liste aller laufenden VMs erstellt. Denn, sollten diese laufen, müssen sie nachher zumindest pausiert werden, damit es keine Inkonsistenten bei den Daten gibt. Auch hier wird die Anzah erneut ermittelt.

Dann beginnt die for Schleife ….

for (( i=1 ; i<=$nrOfXMLs ; i++ ))
do 
	# the current VM
	currentXML=$(sed "${i}q;d" $vmTextFile)
	
	# We grab the Name of the current VM, and cut loose all the shit that thing doesn't need.
	nameOfVM=$(xmllint "$xmlDir$currentXML" | grep '<name>' | cut -d '>' -f2 | rev | cut -d '<' -f2 | rev)

	# Check if the VM to backup isn't exscluded.
	if [ -f $excludedVMsFile ]
	then
		for (( n = 1; n <= $nrOfExcludedVMs; n++ )) 
		do
			excludedVM=$(sed "${n}q;d" $excludedVMsFile)
			if [[ $nameOfVM = $excludedVM ]]
			then
				echo "Nothing to do with $nameOfVM"
				break
			fi
		done
	else
		echo "$nameOfVM is not excluded, continuing ..."
	fi
	if [[ $nameOfVM = $excludedVM ]]
	then
		echo "$nameOfVM has been excluded"
		continue
	elif [[ $nameOfVM != $excludedVM ]]
	then
		if [[ $nrOfRunningVMs -ge 1 ]]
		then 
			for (( x = 1; x <= $nrOfRunningVMs; x++ ))
			do 
				currentRunningVM=$(sed "${x}q;d" $runningVMs)
				if [[ $nameOfVM = $currentRunningVM ]]
				then
					echo "$nameOfVM is currently running, suspending ..."
					virsh suspend $nameOfVM
					break
				fi
			done
		fi

Die Hauptiteration erfolgt durch die Liste an XMLs, die vorher erstellt wurde. Damit sollten alle VMs aufgefangen werden.

Zunächst wird die zueghörige XML Datei herausgeuchst, und in ihr nach dem Namen der VM gesucht, und dieser dann extrahiert.

Nach einer proforma-Abfrage ob die Ausschlussdatei existiert, wird diese dann mit dem Namen der jetzigen VM verglichen. Sollte es eine Übereinstimmung geben, wird aus der schleife ausgebrochen.

Der Vergleich wird nochmal herangezogen, aber diesmal mit dem Unterschied, dass die Hauptiteration nicht verlassen wird, sondern einfach einen Zähler heraufgesetzt wird, und somit die restlichen Anweisungen in der Schleife für diese VMeinfach nicht zum tragen kommen. Sie war ja ausgeschlossen.

Anschließend gibt es die proforma-Abfrage umgekehrt, und dann wird überprüft, ob es sich um eine laufende VM handelt. Diese wird dann angehalten, um die Datenkonsistenz zu garantieren. Nach dem Anhalten wird aus der dazugehörigen Schleife ausgebrochen.

Alles was jetzt kommt, wird mit jeder VM gemacht, es sei denn, sie ist von vorneherein ausgeschlossen.

# bash in itself doesn't let one operate with objects, so we are circumventing this 
		# right now by creating txt-based backupfiles
	
		# We create a current backupfile
		currentBackupFile="$nameOfVM"-Backup.txt""

		currentFileSourceFile="$nameOfVM"-SourceFile.txt""
	
		# The current Backupfile is obsolete, so we are deleting it
		if [[ -f $currentBackupFile ]]
		then 
			rm "$currentBackupFile"
		fi
	
		# The name of the VM is the first line in the newly created backup file
		echo "$nameOfVM" > "$currentBackupFile"
	
		# The XML file (including the directory path) is the second line in the backup file
		echo ""XML File="$xmlDir$currentXML" >> "$currentBackupFile"
	
		# Determining how many source files are there, e.g. we count the number of sorce files. Source files may include the 
		# installation disk (mostly .iso-files),
		# but they may also include different hard disks (qcow2-files). That's why we set the number earlier
		
		currentNrOfSourceFiles=$(xmllint "$xmlDir$currentXML" | grep 'source file' | wc -l)
		
		currentSourceFiles=$(xmllint "$xmlDir$currentXML" | grep 'source file')

		if [[ -f $currentFileSourceFile ]]
		then
			rm "$currentFileSourceFile"
		fi

		# declare k and l, which will incrememnted by 2 every time in the loop, to cut out the path for the different source files
		k=1
		l=2	

		# Now comes the other for loop, in which we will itereate through the different source files. See above

		for (( j=1 ; j<=$sourceFileHigh ; j++  ))
		do			

			cutSourceFile=$(echo $currentSourceFiles | grep 'source file' | cut -d " " -f${k}-${l}) 
			modSourceFile=$(echo $cutSourceFile | cut -d "=" -f2 | cut -d '"' -f2) #| cut -d ">" -f1 | rev | cut -d "/" -f2 | rev | cut -d '"' -f1)
			
			# if the source file is empty, just put a blank line in there
			if [[ -z $modSourceFile ]]
			then
				echo "$modSourceFile" >> "$currentBackupFile"
			# else put the following info in there
			else
				echo ""Source Path="$modSourceFile" >> "$currentBackupFile"

				echo "$modSourceFile" >> "$currentFileSourceFile"
				  
				# now let's grep the info and store it in a file
				modifySourceFile=$(stat $modSourceFile |  grep $lastModifiedFile | cut -d ":" -f2-4 | cut -d " " -f2-4)
				echo ""Last Modified="$modifySourceFile" >> "$currentBackupFile"
				changeSourceFile=$(stat $modSourceFile |  grep $lastChangedFile | cut -d ":" -f2-4 | cut -d " " -f2-4)
				echo ""Last Changed="$changeSourceFile" >> "$currentBackupFile"			
				birthOfSourceFile=$(stat $modSourceFile |  grep $birthOfFile | cut -d ":" -f2-4 | cut -d " " -f2-4)
				echo ""Birth Of File="$birthOfSourceFile" >> "$currentBackupFile"

			fi
	

			# k needs to be incremented by 2
			k=$((k+2))

			# l needs to be incremented by 2
			l=$((l+2))
		
		done

		# Last line in file
		echo "End of File" >> "$currentBackupFile"

Es werden zwei Dateien kreiert, zum einen die Backup-Datei, zum anderen die Quellen-Datei.

Erstere spielt längerfristig eine Rolle, die zweite nur temporär.

Es erfolgt die Standardmäßige überprüfung, ob sie existiert.

Dann wird angefangen die Backup-Datei zu schreiben.

Daten in ihr:

Name
XML Datei mit Pfad
Quell-Datei-1 mit Pfad
Datum der letzen Modifizierung von Quell-Datei-1
Datum der letzte Änderung von Quell-Datei-1
Datum der Erschaffung von Quell-Datei-1
# Bei mehreren Quell-Dateien
Quell-Datei-2 mit Pfad
Datum der letzen Modifizierung von Quell-Datei-2
Datum der letzte Änderung von Quell-Datei-2
Datum der Erschaffung von Quell-Datei-2

Ende der Datei

Ursprünglich war Zugriff mit in der Datei, aber da dieser sich bei jedem Kopiervorgang ändert, wurde dieser entfernt.

# Check if the local backup directory exists, and, if it doesn't, create it
		if [[ -d "$localBackupDir/$nameOfVM" ]]
		then
			echo "local backup directory for $nameOfVM exists, nothing to do here ..."
		else
			echo "local backup directory for $nameOfVM doesn't exist, creating ..."
			mkdir "$localBackupDir/$nameOfVM"
		fi

Ein Verzeichnis mit Namen der aktuellen VM wird lokal erstellt, sollte es nicht existieren, um darin die Backup-Datei zu speichern.

# Let's check if there is a backup file already and compare it to the current file
    # Since the we just accumulated all the information about the VM, we check if the VM has been        started in any way, 
    # and if it hasn't, we won't need to do anything.
    # In the aforementioned case, the loop will stop here and continue to the next VM

    if [[ -f "$localBackupDir/$nameOfVM/$currentBackupFile" ]]       
    then
        if [[ 'cmp "$localBackupDir/$nameOfVM/$currentBackupFile" $currentBackupFile' ]]
        then
            echo "The VM hasn't changed since the last backup, no need to do anything ..."
            rm "$currentBackupFile"
            rm "$currentFileSourceFile"
            continue
        else
            echo "The VM has changed, removing old backup file ..."
            rm "$localBackupDir/$nameOfVM/$currentBackupFile"
            echo "Writing new local backup file"
            rsync --progress "$currentBackupFile" "$localBackupDir/$nameOfVM/$currentBackupFile"
        fi
    else
        echo "The backup file doesn't exist, creating ..."
        rsync --progress "$currentBackupFile" "$localBackupDir/$nameOfVM/$currentBackupFile"
    fi

Es erfolgt der Vergleich (sofern schon eine Backup-Datei existiert) zwischen der gerade erstellten und der gespeicherten Backup-Datei. Sollten diese identisch sein, wird auf das Backup verzichtet, und mit der nächsten VM weitergemacht. Es lohnt sich nicht, die VM, die bereits auf der backup-Platte existiert, erneut zu backupsichernen. Für diesen Vergleich existiert die Backup-Datei.

if [[ -d "$localMntDir/$nameOfVM" ]]
			then
				echo "Directory for $nameOfVM exists, nothing to do here ..."
			else
				echo "Directory for $nameOfVM doesn't exist, creating ..."
				mkdir "$localMntDir/$nameOfVM"
			fi

		date="$(date '+%Y-%m-%d')"

		# This checks if the backup was done today, and if it was, it assumes that something went wrong 
		# (usually there are not two backups in a single day,)
		# If this is the second backup in one day, it removes everything.
			
		if [[ -d "$localMntDir/$nameOfVM/$date" ]]
		then
			echo "Todays date directory exists, cleaning it up ..."
			if [[ -f "$localMntDir/$nameOfVM/$date/$currentXML" ]]
			then
				echo "$currentXML exists, removing ..."
				rm -v "$localMntDir/$nameOfVM/$date/$currentXML"
			fi
			if [[ -f "$localMntDir/$nameOfVM/$date/$currentBackupFile" ]]
			then
				echo "$currentBackupFile exists, removing ..."
				rm -v "$localMntDir/$nameOfVM/$date/$currentBackupFile"
			fi
		else
			echo "Todays date directory doesn't exist, creating ..."
			mkdir "$localMntDir/$nameOfVM/$date"
		fi

		# Last but not least, the copying process of the needed files

			if [[ -d "$localMntDir/$nameOfVM/$date" ]]
			then
				echo " Copying text files to backup destination ..."
				rsync --progress "$xmlDir$currentXML" "$localMntDir/$nameOfVM/$date/"
				rsync --progress "$currentBackupFile" "$localMntDir/$nameOfVM/$date/"
			fi

		sleep 10s

Jetzt wird Verzeichnis auf dem gemounteten Datenträger kreiert, auch wieder mit Namen der VM. Hinzu kommt ein Verzeichnis im Datumsformat. Sollte es (aus Gründen) zwei Backups am selben Tag geben, ist davon auszugehen, dass etwas schief gelaufen ist. In dem fall werden die vorhandenen daten gelöscht.

nrOfSourceFiles=$(wc -l < $currentFileSourceFile)

		for (( z = 1; z <= $nrOfSourceFiles; z++ ))
		do
			currentQcow2=$(sed -n "${z}p" "$currentFileSourceFile")
			currentQcow2File=$(sed -n "${z}p" "$currentFileSourceFile" | rev | cut -d '/' -f1 | rev)

			if [[ -f "$localMntDir/$nameOfVM/$date/$currentQcow2File" ]]
			then
				echo "$currentQcow2 exists, removing ..."
				rm -v "$localMntDir/$nameOfVM/$date/$currentQcow2File"	
			fi

			if [[ -d "$localMntDir/$nameOfVM/$date" ]]
			then
				echo " Copying $currentQcow2 files to backup destination ..."
				rsync --progress "$currentQcow2" "$localMntDir/$nameOfVM/$date/"
			fi
			
		done

Nun wird die Quellen-Datei untersucht, und alle in ihr befindlichen Dateien in das neue Verzeichnis kopiert. Auch hier: Sollte dies am selben Tag passieren, werden die Daten zuvor gelöscht.

# Cleanup ...

		echo "removing created files ..."

		rm "$currentBackupFile"
		rm "$currentFileSourceFile"

		# We need to resume the VM, if it was running in the first place ...

		if [[ $nameOfVM = $currentRunningVM ]]
		then
			echo "$nameOfVM needs to be resumed!"
			virsh resume $nameOfVM
		fi
	fi
done

# Cleanup ...

rm "$vmTextFile"
rm "$runningVMs"

# Unmount the sshfs directory

fusermount -u $localMntDir

# shutdown the remote backup-server

ssh "$externalUserName@$externalIP" "sudo shutdown now"

Abschließend wird aufgeräumt. Sämtliche erstellten Dateien werden gelöscht, und sobald die Hauptschleife verlassen wurde, wird das lokal gemountete Verzeichnis wieder freigegeben. Somit bleibt nur noch eines: den Backup-Server herunterzufahren.

Das Script war so im Einsatz und hat zwei so gewollte Sicherungen generiert. Aber es ist definitiv noch nicht perfekt. Der Existenz-Abgleich der Dateien könnte noch besser werden, und es gibt noch kein „Packen“ der VMs, d.h. sie werden in ihrer ursprünglichen Größe transferiert.

Dies hat aber folgende Gründe: Ich hatte den Platz auf der dem Backup-Server, und ich wollte die Sicherung in einem angemessenen Zeitrahmen vonstatten bringen. Die Sicherung aller VMs hat mich so schon ca. 36 Stunden gekostet, mit „Packen“ hätte das länger gedauert.

Für die Zukunft ziehe ich das aber in Betracht.

Und das Script, mit ein paar Modifikationen, wird dabei helfen.

Je nach Wochentag kann ich eine bestimmte Liste an VMs sichern, indem ich den Rest einfach auf die „nicht sichern Liste“ setze, und diese kann ich dann auch packen. Dann habe nehme ich mir das Zeitfenster von zwei bis sechs Uhr morgens, und sichere diese dann zu diesem Zeitpunkt weg.

Fünf Tage Linux – Tag 5: Scripting

Vorweg einmal kurz in einen Blick in eine typische Netzwerk-Konfigurationsdatei unter Linux:

network:
    version: 2
    renderer: NetworkManager
    ethernets:
        eno1:
          dhcp4: false
          addresses:
              - 192.168.111.112/27
          nameservers:
              addresses:
                  - 192.168.111.1
          routes:
              - to: default
                via: 192.168.111.1

Und mal dieselbe Netzwerk Konfiguration mal anders:

auto eno1
iface eno1 inet static
  address 192.168.111.112/27
  nameserver 192.168.111.1
  gateway 192.168.111.1

Diese beiden Konfiguration tun im Endeffekt dasselbe. Das Problem: Die erste ist für netplan, die zweite für Ifupdown2. Allein aus Gründen der Effizienz kommt da die Frage auf: Warum nicht einfach ifupdown2 verwenden? Tja, wenn es so einfach wäre….

Die beiden haben andere Anwendungsgebiete. Während ifupdown2 bei Bare-Metal-Systemen allmählich zum Standard wird, ist Netplan bei Cloud Projekten und vor allem bei Kubernetes-Clustern im Einsatz. Da ein Docker-Container eh immer mit einer .yaml-Datei in ein K8s-Cluster eingebunden wird, bot sich da an, auch die Netzwerk-Konfiguration gleich mit in den .yaml-Datei zu schreiben. So entstand Netplan.

Jetzt möchte ich aber eine Proxmox Umgebung (meist auf Bare-Metal) in ein bestehendes Cluster einbetten, das Cluster verfügt aber über mehrere Netze und diese sind auch in einer Netplan-Datei verankert. Da dieses Netzwerk sich aber über mehrere Netzwerke erstreckt, ist die Datei dann nicht mehr ganz so simpel wie das oben genannte Beispiel, sondern sieht dann eher so aus:

network:
  version: 2
  renderer: networkd
  ethernets:
    eno1:
      dhcp4: no
      dhcp6: no
  bridges:
    br1:
      dhcp4: no
      dhcp6: no
      interfaces: [ eno1 ]
      addresses: [ 10.17.132.22/25 ]
      routes:
        - to: default
          via: 10.17.132.1
      nameservers:
        addresses:
        - "10.17.132.1"
    br1001:
      dhcp4: no
      dhcp6: no
      interfaces: [ vlan1001 ]
    br1002:
      dhcp4: no
      dhcp6: no
      interfaces: [ vlan1002 ]
    br1003:
      dhcp4: no
      dhcp6: no
      interfaces: [ vlan1003 ]
    br1004:
      dhcp4: no
      dhcp6: no
      interfaces: [ vlan1004 ]
    br1101:
      dhcp4: no
      dhcp6: no
      interfaces: [ vlan1101 ]
    br1102:
      dhcp4: no
      dhcp6: no
      interfaces: [ vlan102 ]
      link-local: [ ipv6 ]
      accept-ra: false
    br1201:
      dhcp4: no
      dhcp6: no
      interfaces: [ vlan81 ]
  vlans:
    vlan1001:
      id: 1001
      link: eno1
      dhcp4: no
      dhcp6: no
    vlan1002:
      id: 1002
      link: eno1
      dhcp4: no
      dhcp6: no
    vlan1003:
      id: 1003
      link: eno1
      dhcp4: no
      dhcp6: no
    vlan1004:
      id: 1004
      link: eno1
      dhcp4: no
      dhcp6: no
    vlan1101:
      id: 1101
      link: eno1
      dhcp4: no
      dhcp6: no
    vlan102:
      id: 102
      link: eno1
      dhcp4: no
      dhcp6: no
      link-local: [ ipv6, ipv4 ]
      accept-ra: true
    vlan1201:
      id: 81
      link: eno1
      dhcp4: no
      dhcp6: no

Das wird mal ein wenig komplexer. Und bevor ich jetzt alles aus dieser Datei in eine ifupdown2 übertrage, mache ich das ganze doch besser via Script, weil vertippen tue ich mich zwischenzeitlich vermutlich eh.

Nun kommt die erste Hürde: bash (die Kommando Zeile von Linux) kann eine .yaml-Datei nicht einfach so parsen, um dies zu tun, braucht man eine python3-Erweiterung namens ’niet‘. Aber nachdem dies geklärt ist, könnte ich mich jetzt hinsetzen und das Script selber runterschreiben, oder ich könnte ein KI-Modul zum Skripten in mein LocalAi laden und das Modul das Script für mich schreiben lassen. Nach dem Testen mehrerer Module ist dies das Beste, was ich bekommen habe:

To create an ifupdown2 configuration file using the niet Python library, you first need to install the niet library. If you haven’t already, you can install it using pip:

pip install niet

Next, you need to write a script that uses niet to generate the ifupdown2 configuration file. ifupdown2 uses a different configuration format, so you’ll need to convert your netplan configuration to ifupdown2 syntax.

Here’s a bash script that performs this conversion:

#!/bin/bash

# Function to convert netplan to ifupdown2
convert_netplan_to_ifupdown2() {
    local netplan_file="$1"
    local ifupdown2_file="$2"

    # Read netplan file
    local netplan
    netplan=$(cat "$netplan_file")

    # Extract interfaces and their configurations
    local interfaces=$(echo "$netplan" | jq -r '.network.ethernets + .network.bridges + .network.vlans | .[] | .id')

    # Create ifupdown2 configuration
    local ifupdown2_content=""
    for interface in $interfaces; do
        local interface_config=$(echo "$netplan" | jq -r --arg interface "$interface" '.network | select (.ethernets[$interface] or .bridges[$interface] or .vlans[$interface]) | .ethernets[$interface] or .bridges[$interface] or .vlans[$interface]')
        local address=$(echo "$interface_config" | jq -r '.addresses[0]')
        local gateway=$(echo "$interface_config" | jq -r '.routes[] | select(.to == "default") | .via')
        local nameservers=$(echo "$interface_config" | jq -r '.nameservers.addresses[0]')

        # Convert to ifupdown2 syntax
        local ifupdown2_entry="auto $interface"
        ifupdown2_entry+="\niface $interface inet static"
        ifupdown2_entry+="\n    address $address"
        ifupdown2_entry+="\n    gateway $gateway"
        ifupdown2_entry+="\n    dns-nameservers $nameservers"

        # Add to ifupdown2 content
        ifupdown2_content+="$ifupdown2_entry\n"
    done

    # Write ifupdown2 configuration to file
    echo "$ifupdown2_content" > "$ifupdown2_file"
}

# Main script execution
if [ -n "$1" ] && [ -n "$2" ]; then
    convert_netplan_to_ifupdown2 "$1" "$2"
    echo "ifupdown2 configuration created at $2"
else
    echo "Usage: $0 <netplan_file> <ifupdown2_file>"
    exit 1
fi

Explanation:

Function convert_netplan_to_ifupdown2:
    Reads the netplan configuration file.
    Extracts the list of interfaces.
    Converts each interface's configuration to ifupdown2 syntax.
    Writes the ifupdown2 configuration to the specified output file.

Main Script Execution:
    Checks if the script is called with the correct number of arguments.
    Calls the function with the provided input and output file paths.

Usage:

Save the script to a file, for example, convert_netplan.sh, give it execution permissions, and run it:

chmod +x convert_netplan.sh
./convert_netplan.sh input_netplan.yaml output_ifupdown2.conf

This will generate an ifupdown2 configuration file from your netplan configuration. Adjust the script as needed to handle more specific configurations or to convert additional types of network configurations.

Das sieht auch alles ganz lustig aus, bis man sich die Befehle mal näher ansieht: Das Script nutzt nicht „niet“, obwohl es den Installationsvorgang noch beschreibt und einen dazu auffordert, die python Bibliothek zu installieren. Es nutzt „jq“, was, wenn man es sich genau anschaut, dafür gebaut ist, eine .json-Datei zu parsen. Nun, eine .yaml und und ein .json Datei sind schon ähnlich, aber leider nicht dasselbe. Sprich, das funktioniert nicht.

Den nächsten Fehler, den das Script begeht, ist die gesamte .yaml-Datei erstmal mit cat zu parsen und in der der Variable netplan zu speichern, um mit dieser weiter zu arbeiten. Was so auch nicht funktioniert, da Formatierungen aufgehoben werden, welche in einer .yaml-Datei allerdings essentiell sind. Also, ja, ich kann die Datei zu einer Variable innerhalb des Scriptes machen. Aber die Datei an sich sollte nicht mit bash-Boardmitteln geparst werden, wenn dies ginge, bräuchte ich auch die python3-Bibliothek nicht!!

Vorweg könnte man mit echo noch die Frage nach der Datei ausgeben. Entweder in demselben Verzeichnis oder mit absolutem Pfad, dann müssen sich Script und Datei nicht in demselben Ordner befinden.

Das, was danach gemacht werden sollte, ist die Bezeichnung des oder der Standard-Ethernetports herauszufinden:

PS3='Bitte nennen Sie die Bezeichnung für ihre Netzwerkkarte: '
options=("eth" "enp0s1 (o.ä)" "eno" "Quit")
select opt in "${options[@]}"
do
    case $opt in
        "eth")
            eName="eth"
            echo "Die Bezeichnung für ihre Netzwerkkarte beginnt mit eth"
            break
            ;;
        "enp0s1 (o.ä)")
            eName="enp"
            echo "Die Bezeichnung für ihre Netzwerkkarte beginnt mit enp"
            break
            ;;
        "eno")
            eName="eno"
            echo "Die Bezeichnung für ihre Netzwerkkarte beginnt mit eno"
            break
            ;;
        "Quit")
            exit 1
            break
            ;;
        *) echo "ungültige Option, bitte nochmal";;
    esac
done

eth, eno und enp werden genommen. swp ist noch nicht integriert, kann man aber als Option schnell intergrieren.

Anschließend werden die Interfaces, Bridges und VLANs gesammelt:
(Hier das Beispiel für Bridges:)

nrOfBridges=`niet ".network.bridges" $netplan | grep br* | wc -l`
declare -a bridges=()
for (( c = 1; c <= $nrOfBridges; c++ ))
do
  bridges+=(`niet ".network.bridges" $netplan | grep br* | sed -n "$c"p`)
done

Man sollte hier dabei daran denken, dass das die erste Variable in einem Array mit [0] adressiert wird, d.h. das die erste Bridge also bridges[0] ist.

Als nächstes zieht man sich die relevanten Informationen aus den Unterpunkten der .yaml-Datei
(Wieder anhand der Bridges:)

for (( i = 1; i <= 1; i++ ))
do 
   j=$i-1
   bridgeWrong=${bridges[$j]}
   bridgeNow=(`echo "$bridgeWrong" | tr -d :`)
   interface=(`niet ".network.bridges."$bridgeNow".interfaces" $netplan`)
   dhcpV4=(`niet ".network.bridges."$bridgeNow".dhcp4" $netplan`)
   dhcpV6=(`niet ".network.bridges."$bridgeNow".dhcp6" $netplan`)
   ipV4=(`niet ".network.bridges."$bridgeNow".addresses" $netplan | cut -d'/' -f1`)
   ipV4Netmask=(`niet ".network.bridges."$bridgeNow".addresses" $netplan | cut -d'/' -f2-`)
   defaultRoute=("`niet ".network.bridges."$bridgeNow".routes" $netplan`")
   dRoute1=(`echo "$defaultRoute" | tr -d ' ' | tr -d "'" | cut -d',' -f1 | cut -d':' -f2-`)
   dRoute2=(`echo "$defaultRoute" | tr -d ' ' | tr -d "'" | cut -d',' -f2- | cut -d':' -f2- | cut -d'}' -f1`)
   nameserver=(`niet ".network.bridges."$bridgeNow".nameservers.addresses" $netplan`)
   linkLocal=(`niet ".network.bridges."$bridgeNow"" $netplan | niet "link-local"`)
   acceptRA=(`niet ".network.bridges."$bridgeNow"" $netplan | niet "accept-ra"`)
done

Was man nun mit den relevanten Informationen macht, muss man schauen. Muss das in ifupdown2 eigentlich noch deklariert werden, oder kann das ignoriert werden.

Sollten die Infos allerdings benötigt werden, direkt schreiben. Da hilft das Ki- generierte Script oben dann wieder.

Aber als Fazit muss ich sagen:

Habe ich dieses Problem häufig (also mindestens im zweistelligen Bereich) würde sich ein Script mit Sicherheit lohnen. Ansonsten würde ich das nach wie vor per Hand machen. Sollten die KI Modelle allerdings irgendwann besser werden, könnte man auch darüber nachdenken, das übernehmen zu lassen.

Fünf Tage Linux – Tag 4: HomeAssistant und LocalAI

Wer sich mit Hausautomatisierung auskennt, sollte den HomeAssistant kennen.

Für die Nicht-Initiierten: Es ist eine Software, welche mehrere Open Source Schnittstellen nutzt, um das Heim weitestgehend zu automatisieren. Das können Jalousien, die Heizung, Licht, Steckdosen, Reinigungsroboter etc. sein.

Dazu gibt es eine App, welche es ermöglicht, das Smartphone bzw. Tablet als Steuerung zu nutzen.

Nachdem beschlossen wurde, welchen Plattform genutzt wird, kann das entsprechende Image heruntergeladen werden. Ich habe mich für die VM-Variante entschieden. Die Installation läuft ohne größere Schwierigkeiten, und anschließend kann auf der fest IP der HomeAssistant geöffnet werden. Das Netz, in dem sich der HA befindet, wird gescannt, und alle erkannten Objekte direkt eingebunden (Allerdings jeweils mit Benutzerabfrage). Bei mir waren es u.a. MQTT Server, NAS und Drucker.

In der App auf dem Smartphone wird jetzt noch die zugehörige IP-Adresse angegeben, und man hat alle Daten und Möglichkeiten sowohl auf einem Webinterface auf dem HomeAssistant Server sowie in der App auf dem Handy.

In der Übersicht: Wetter, Drucker, NAS.

Oben rechts, die Sprechblase führt zum Assistenten, der beim Klick dann aufspringt.

Sowohl in der App, als auch im Webbrowser kann man nun etwas tippen. Es gibt sogar die Möglichkeit, mit entsprechenden Plugins (Speech-to-Text, Text-to-Speech) sprachliche Eingaben zu machen. Der Standard Assistant, der mitgeliefert wurde, kann allerdings erweitert werden. Zum einen besteht die Möglichkeit diesen direkt mit OpenAI zu verbinden, zum zweiten kann man, mit LocalAI auch eine lokale künstliche Intelligenz als Kommunikationspartner wählen.

Die rudimentäre Anleitung dafür findet man auf Youtube.

Essenziell bietet LocalAI die Schnittstelle an, die auch OpenAI bereitstellt. Somit kann LocalAI als lokale Instanz von OpenAI aufgebaut werden. Also Linux OS aufgesetzt, LocalAI installiert, und dann mit HomeAssistant verknüpft. Innerhalb HomeAssistant muss jedoch zuvor HACS, der Home Assistant Community Store installiert und ein Plugin aus diesem heruntergeladen werden. Sofern man alles hat, kann man allerdings direkt mit seinem System sprechen. Abschließend müssen noch die Geräte für AI freigegeben werden, und nun kann man Sie auch via Remote steuern. Zusätzlich kommt dann noch sowas bei raus:

Es macht Spaß. Als Nächstes muss ich mal lernen, wie man die AI’s trainiert.

Tag 5: Skripting

Fünf Tage Linux – Tag 3: Proxmox

Wird eine in den allermeisten Fällen notwendige
IT-Infrastruktur betrieben, so stellt sich mittlerweile die Frage: Wie mache
ich das denn jetzt? Das Angebot ist vielfältig (um nicht zu sagen
unübersichtlich), es muss auf moderne Arbeitsweisen eingegangen werden (Home
Office, mehr als eine Person pro Arbeitsplatz etc.) und es sollte jetzt nicht
die eigene Firma in die roten Zahlen stürzen. Diesem Umstand sehen sich aber mittlerweile mehrere
mittelständische Unternehmen ausgesetzt, die zu Beginn der Covid-19 Pandemie
freudig auf den Microsoft Zug aufgesprungen sind. Oder aber auch Benutzer:innen
von VMWare, die jahrelang gut mit der Software gearbeitet haben, sich jetzt
aber bei den gestiegenen Preisen verwundert die Augen reiben. Und wer kennt es nicht: Man möchte, entweder lokal oder im Rechenzentrum, eine Armada virtueller Maschinen aufsetzen, überlegt aber
aufgrund von Kosten oder Kontrollierbarkeit (AWS, Azure, GCP) auf eine eigens
aufgebaute Infrastruktur zurückzugreifen. Dann hat man die Wahl…. Bis vor kurzem lief Proxmox noch unter Virtualisierung für Nerds, langsam, aber sicher wird es zu einer echten Alternative zu VMware, nicht
zuletzt aufgrund der neuen Preispolitik des Inhabers der Virtualisierungssoftware. Mehrere Rechenzentren berichten zudem, dass aufgrund von Broadcoms Abonnementsystem mittlerweile schon wieder die Installation
direkt auf Blech Einzug hält. Dies muss nicht sein. Und da ich mich mit KVM /
Qemu und der Virtualisierung unter Debian basierten Linuxsystemen eh schon
auskannte, war der Sprung zu Proxmox eh schon angedacht, aber die Einbindung
von Ceph zur Handhabung größerer Datenmengen war dann doch ausschlaggebend sich
mal näher mit der Virtualisierungsumgebung auseinanderzusetzen.

Es bestehen zwei Möglichkeiten, Proxmox zu installieren: Die
erste beinhaltet die geradlinige Installation der ISO, d.h. ISO herunterladen,
ggf. noch einen USB-Boot-Stick draus basteln, und dann durch den Installationsprozess.
Es gibt ehrlich gesagt nicht viel zu tun. Platte auswählen (Es gibt noch die
Möglichkeit diese zu Partitionieren), root-Passwort (Admin-Passwort, für alle
nicht initiierten) setzen, dem Rechner eine feste IP geben (braucht er),
Hostnamen festlegen, und dann läuft die Installation schon durch. Beim Neustart
10-15 Minuten später landet man dann auf dem Login-Prompt, über dem die IP mit
dem Port 8006 angezeigt wird, auf dem die Weboberfläche zur Steuerung dann auch
erscheinen wird.

Nach der Eingabe der IP-Adresse mit dem dazugehörigen Port
im Browser kommt man dann auch zum Login. So weit, so, einfach. Die andere, lustigere Methode beinhaltet die Installation eines Debiansystems, dem sich ein Systemumbau mit Kerneltausch anschließt. Und
welcher Kernel wird installiert? Der von Proxmox bereitgestellte natürlich. Die Frage, die sich da aufdrängt: wenn ich erst Debian aufsetze, um es dann in Proxmox umzuwandeln, kann ich dann nicht gleich Proxmox
nehmen? Eigentlich ja, es sei denn ich möchte die extra Features eines
Debian-Installers nutzen. Diese umfassen im Expert-Modus das Erstellen eines
Software-Raids, das Zusammenfassen der Datenträger mittels des Logical Volume
Managers (LVM) und nicht zuletzt die Option, die Datenträger auch noch zu
verschlüsseln. Letzteres ergibt besonders viel Sinn, wenn ich die Datenträger
in einem externen Datencenter liegen habe. Auch auf dieses Szenario bin ich mal angegangen. Der
Installationsprozess ist in dem Fall nicht zu verachten, und er zieht sich.
Aber er lohnt sich. Also habe ich folgendes Szenario mal konzipiert:

1 x Proxmox Controller (direkte Installation, 2 Kerne, 4 GB
RAM, 50-GB-Festplatte)

2x Proxmox Nodes (direkte Installation, 2 Kerne, 16 GB RAM,
200-GB-Festplatte)

1x Proxmox on Debian (Debian Installation, 4 Kerne, 16 GB
RAM, 2x 200-GB-Festplatte im Raidverbund, dazu LVM und verschlüsselt)

1x Linuxmint, um ein Gerät mit Browser zum Navgieren zu
haben (ich benötige eigentlich nur den Browser, und wollte es nach dem C‘T 3003
Video mal testen).

Diese vier Maschinen sollen sich nachher in einem Cluster
befinden, in dem ich dann probeweise VMs installiere und Ceph nutze. Proxmox on
Debian läuft deswegen, weil ich wissen möchte, ob es auf dieser Maschine trotz
besseren Hardwarevoraussetzungen zu längeren Arbeits- und Ladezeiten aufgrund
der Verschlüsselung kommt. Nach einigen Stunden Arbeit kommt dann folgendes bei herum:

Diese Nachricht erscheint jedes Mal beim Einloggen, egal,
auf welchem Server. Wer mag, darf Scripte und Hooks ergooglen, um diese
Nachricht loszuwerden. Proxmox ist aber zu 100% kostenlos, sofern man keinen
Support benötigt. Anschließend geht es dann daran, die einzelnen Rechner zu
einem Cluster zusammenzufügen. Dazu loggt man sich in die Oberfläche des
Cluster Controllers ein, klickt links in der Leiste auf Cluster, und dann auf
Create Cluster. Das Cluster wird dann erstellt. Anscheinend gibt es einen
Button mit Clusterinformation, welcher auch den Zugangskey enthält.  Diesen kopiert man am besten gen Cloud oder
auf einen USB-Stick. Anschließend geht es an die anderen Maschinen im Cluster, wo
man ebenfalls auf Cluster klickt, allerdings dann auf ‚Join Cluster‘ klickt, um
dann den Key einzufügen, was die Maschinen dazu veranlasst, sich dem Cluster
anschließen zu wollen (die Route zu dem Cluster Controller sollte zu diesem
Zeitpunkt bereits stehen). Zum Abschluss wird man noch aufgefordert, das
‚root‘-Passwort des ClusterControllers einzugeben, um dann die zusätzlichen
Maschinen ins Cluster einzufügen. Hat man alle Maschinen zum Cluster
hinzugefügt, dann ergibt sich folgendes Bild:

Alle Nodes werden im Cluster angezeigt, und auf jedem können
nun Storage oder VMs laufen. Anmerkung: Den Maschinen reicht zur Installation eine
einzelne Netzwerkkarte, es wird nicht überprüft, ob mehr als eine Karte
eingebaut ist, VLANs oder Bridges existieren. Das ist besonders relevant, wenn
man Debian als Grundinstallationsoption wählt: während bei den aktuellen
Proxmox Installationen ifupdown2 bereits integriert ist, muss man es bei der
Debian-basierten Installation via apt hinterher installieren. Dies wird zum
einen bei den erstellen der Bridges relevant (die VMs brauchen ja auch
Netzwerkarten und -anschlüsse), da Proxmox ein hook hat um diese direkt
einzubinden, und dieses Hook ifupdown2 voraussetzt. Das Schöne an ifupdown2:
Man kann die Netzwerkkonfiguration ändern, ohne die Maschinen neu starten zu
müssen. Relevant bei 24/7 erreichbaren Maschinen. Man kann die bridges und vlans dann entweder via CLI auf
den einzelnen Maschinen eintragen, oder aber das über das Interface
reglementieren. Allerdings müssen diese Einstellungen für die einzelnen Nodes
jeweils vorgenommen werden, es gibt keine Netzwerkkonfiguration für alle.

Fazit: Man sieht die Möglichkeiten, die sich mit Proxmox bieten. Selbst paravirtualisiert macht die Oberfläche noch zügig, das was Sie soll. Nun können neue VMs installiert und Ceph getestet werden!

Tag 4: HomeAssistant und LocalAI

Fünf Tage Linux – Tag 2: OPNSense

Beim Konfigurieren meines OpenWRT Routers gehörte jedes Mal auch ein abschließender Geschwindigkeitstest mit zum Prozedere. Und dabei fiel auf, dass die Geschwindigkeit nicht das erreichte, was der ISP versprach. Ein Test direkt am Router des ISPs offenbarte jedoch, dass mir die Geschwindigkeit sehr wohl zur Verfügung stand, OpenWRT aber rund 60% davon zu kappen schien. Und das auf kabelgebundener Ebene, WLAN wurde aufgrund der möglichen Fehlerquellen (weitere Netze, ungünstige Position des Routers, weitere Geräte innerhalb des Netzes) gar nicht erst zum Test herangezogen.

Nach all den Schwierigkeiten mit dem R8000, kam ich dann kurzerhand zu dem Entschluss mal eine Firewall / Router Distribution auf virtueller Ebene zu testen. Also begann die Recherche. Ich bin im professionellen Einsatz mit PFSense mal in Berührung gekommen, also war dies meine erste Anlaufstelle. Allerdings legt einem Netgate, die Firma hinter PFSense, ordentlich Steine in den Weg. So müsste die Person, selbst wenn sie nur die Community Edition (CE) herunterladen wollen würde, zunächst einen Account erstellen und dann das Produkt für 0 Euro ‚kaufen‘. Eine direkte Downloadoption ist nicht mehr existent. Allerdings gibt es von PFSense mittlerweile einen sog. Fork, nämlich OPNSense. Die Software ist ohne Probleme downloadbar und wird in manchen Foren sogar als das bessere Produkt gesehen. Also OPNSense Dokumentation durchgelesen, virtuelle Hardware dementsprechend angepasst, den Installer heruntergeladen, und dann installiert.

Zwei Netzwerkkarten sind Minimum, OPNSense kann aber mit wesentlich mehr umgehen. Da ich eh schon ein vollkonfiguriertes Netzwerk auf dem Server laufen habe, war es kein großes Problem Karten in der VM anzulegen und anschließend in OPNSense zu konfigurieren. Also WAN auf der einen und die Netze dahinter auf der anderen Seite konfiguriert, und voilà, ein lauffähiges Netzwerk wie unter OpenWRT:

Das Dashboard einer voll eingerichteten OPNSense-Distribution.

Es kamen, wie bei OpenWRT übrigens auch, noch die Firewall- und Port-Forward-Regeln hinzu, allerdings war dies auch schnell erledigt. Und dann kam der obligatorische Speedtest. Und siehe da: endlich kann ich mit der vollen Bandbreite arbeiten. Anschließend habe ich den OpenWRT Router dann in einen einfachen Access Point umkonfiguriert, sodass dieser jetzt nur noch für das WLAN (wo die Geschwindigkeiten eh nie die volle Bandbreite erreichen werden) zuständig ist.

Tag 3: Proxmox

Fünf Tage Linux – Tag 1: Wechsel von Ubuntu zu Debian.

Mein Server läuft im Moment noch auf einer Ubuntu LTS Version, aber nach den zumindest mir immer absurder erscheinenden Entwicklungen im Hause Canonical spiele ich mit dem Gedanken zu Debian zu wechseln.

Debian als Desktop System scheidet für mich aus, da ich doch etwas zügiger Zugang zu neuen Entwicklungen haben möchte. Aber als Serversystem rückt es aus Gründen tatsächlich mittlerweile an die Spitze meiner Liste.

Da Ubuntu ein Derivat von Debian ist, nahm ich an, dass mit ein paar Konfigurationsänderungen und einem Kernel-Tausch ein Wechsel möglich wäre. Zudem ist auf dem Ubuntu Server ein Hardware Raid eingerichtet, knapp 3 TB sind mit laufenden virtuellen Maschinen belegt, und wenn zusätzlich noch das Netzwerk mit mehr als zehn Netzen sowie dazugehörigen Brücken und Firewalls umzuziehen ist, überlegt man sich doch eine Neu-Installation zu umgehen.

Aber das am laufenden System zu versuchen, kam nicht infrage. Kurzerhand eine VM mit dem Ubuntu LTS aufgesetzt. Zuvor allerdings noch eine Liste aller installierten Pakete erstellt, diese in die VM transferiert und die VM somit dem laufenden System angeglichen.

Anschließend bin ich dieser Anleitung gefolgt:

https://github.com/alexmyczko/autoexec.bat/blob/master/config.sys/ubuntu-deluxe

Das Beste daran war der Verweis auf die De-Installation von Snap, was auch einwandfrei funktionierte.

Bis zum „dIst-upgrade“ Schritt lief auch noch alles normal. Aber anschließend wurde es ein permanenter Mix und Wechsel aus Debian-Paket-Recherche im Netz (Wie heißt das Paket jetzt hier doch gleich wieder??), Reparatur installierter Pakete, Zwangsdeinstallation von verbliebenen Ubuntu Paketen und Re-Installation der Debian Paketen. Nach ca. zweieinhalb Stunden, und immer noch ca. 35 % der Pakete mit dem unliebsamen ‚ubuntu‘ im Paketnamen, hab ich das Projekt dann aufgegeben.

Es wird jetzt doch eine neue NVME SSD angeschafft!!

Tag 2: OPNSense

Das Broadcom Dilemma – oder: Wie ich lernte, meinen Router zu verfluchen.

„You see, there are still faint glimmers of civilization left in this barbaric slaughterhouse that was once known as humanity. Indeed that’s what we provide in our own modest, humble, insignificant … oh, fuck it!“

M.Gustave – The Grand Budapest Hotel

Alles begann, wie so oft, mit einem Versuch.

Ich hatte auf dem VM-Server begonnen, Proxmox VMs aufzusetzen.

Zum einen, weil ich es eh schon lange mal vorhatte, zum zweiten, weil sich ein Ex-Kollege mit einer How-To bei mir gemeldet hatte, und ich so die Möglichkeit hatte, das endlich mal zu probieren und ich mich gleichzeitig ein wenig gefällig erweisen konnte.

Nachdem ich die vierte Maschine aufgesetzt hatte, schoss mir ein Gedanke durch den Kopf:

Du reizt deine Hardware gerade ganz schön aus.

Zugegeben, Paravirtualisierung von einer Virtualisierungsumgebung ist schon was Schickes, aber wenn auf der zugrunde liegenden Hardware deine Produktivumgebung läuft, überlegst du zweimal, ob du das Ding nun Performance-technisch in die Knie zwingen möchtest! 🙂

Da ich eine nahezu identische Hardware-Konfiguration in meinem Arbeitsrechner verbaut war, und die Proxmox VMs sowieso nur zu Testzeiten liefen, beschloss ich, einige der VMs umzuziehen. Nichts leichter als das. Virtuelle Maschine herunterfahren, Dot-qcow2 – Datei über das NAS auf die Linux-SSD meines Arbeitsrechners transferieren, und anschließend wieder hochfahren.

So weit, so gut. Doch jetzt kommunizierten die Maschinen auf einmal nicht mehr untereinander. Nun gut, alle bekamen im Zuge der Installation eine feste IP; vielleicht war der Bridge-Zugang in meiner VM-Umgebung auf dem Arbeitsrechner einfach noch nicht vernünftig konfiguriert, und die Maschinen befanden sich in unterschiedlichen VLANs.

Auf allen Maschinen mal das Gateway pingen …. Was auch funktionierte … was war denn jetzt los? Zwei Desktop-Clients mit DHCP-basierter IP aufgesetzt; auch diese konnten zwar das Gateway, allerdings sich nicht gegenseitig anpingen. Irgendwas lief da schief …

Hatte ich bei der Einrichtung der VLANs etwas verkehrt gemacht? War dies ein typischer Layer 8 Fehler?

Also hieß es: Tutorials gucken.

OpenWrt war die erste Adresse:

https://openwrt.org/docs/guide-user/network/vlan/switch_configuration

Hier wurde das ganze via CLI beschrieben, was ich aber zugegebenermaßen so nicht gemacht hatte.

Ich hatte die VLANs über Luci, das Web-Interface, angelegt. Zum Glück gab es diesbezüglich Tutorials:

Hier wird erklärt, wie man VLANs anlegt …. Aber Moment, das unterscheidet sich doch von meiner Methodik ….

Ich hatte die VLANs via

Network→Interfaces→Devices→„Add device configuration“

angelegt, in dem Video wurde aber die Methode

Network→Interfaces→Devices→named-bridge→configure→“Bridge VLAN filtering“

angelegt …

Der Unterschied sieht nicht spektakulär aus, die letztgenannte Methode hat aber einen entscheidenden Vorteil:

Während ich mit der erstgenannten Methode zwar die VLANs angelegt hatte, hatte ich die Verarbeitung zwischen den Ethernet Ports noch nicht berücksichtigt, d.h. zwar kamen alle Ports in den Genuss der VLANs, die Ethernet Ports untereinander kommunizierten aber nicht via VLAN (nur über VLAN 1 war das möglich).

Kurzum: Ich habe die VLANs angelegt, aber kein Tagging über die Ethernet Ports hinweg angesetzt. Die zweite Methode tut dies sofort, weswegen das Anlegen der entsprechenden VLANs dann auch automatisch geschieht.

Den Schritt nachgezogen, und einen Abend lang war ich glücklich. Als ich am nächsten Morgen aufwachte, wunderte ich mich, warum ich denn kein WLAN auf meinem Handy hatte.

Ein kurzer Blick in die Konfiguration des Routers offenbarte mir, dass mein WLAN mal lief und mal nicht, und selbst wenn es lief, konnte sich mein Smartphone nicht verbinden.

Also forderte ich Google heraus, mir Antworten zu liefern ….

Und das tat es auch, allerdings keine, die ich unbedingt lesen wollte ….

https://github.com/openwrt/openwrt/issues/15529

https://github.com/openwrt/openwrt/issues/10219

Kurzum: VLAN-Tagging über Ethernet-Ports hinweg würde funktionieren, sofern man auf Passwörter bei seinem WLAN verzichtet.

Dies war aber keine Option.

Ich habe es dennoch versucht (es gibt keine Hardware, die nicht von einem verrückten Layer 8 Problem zerstört werden kann).

Nach mehrmaligen Resets verabschiedete sich dann das Wireless-Interface komplett, sodass ich gezwungen war, den Netgear Nighthawk wieder mit seiner Originalfirmware zum Laufen zu bringen. (Gruselig, aber danach lief das Wireless-Interface wieder). Zwischenzeitlich fand dann die DD-WRT Firmware Einzug auf dem Router, wurde aber ganz schnell wieder verworfen, da nur ein DDNS Dienst gestartet werden konnte.

Also stand ich vor dem Dilemma: Entweder du verzichtest aufs WLAN (keine Option, neben dem Gastnetz gab es noch ein IoT Netz), oder aber ich verabschiede mich vom VLAN Tagging über die Ethernet Ports hinweg … Ich kam zur Einsicht, dass dies die bessere Option war.

Ein Gedanke ließ mich allerdings nicht los: Die VLANs kommen ja an allen Ports an, nur über die Ports hinweg wird nicht kommuniziert …

Was passiert, wenn ich da hinter dann einen Switch hänge?

Dann geschaut, ob OpenWrt neben Routern auch Switches unterstützt. Tut es, wenn auch in einer nahezu überschaubaren Menge …

https://openwrt.org/toh/views/switches

Anschließend stürzte ich mich auf Ebay.

Die Anschaffung eines Switches war eh schon länger in Planung, da das Netzwerk z.T. aus kaskadierenden zu Switches umfunktionierten Routern besteht, und ich auf die Konfiguration der Netzwerke über mehrere Knotenpunkte hinweg eigentlich eh keinen Bock mehr hatte ….

Mein erstes Mal bieten (ich wollte jetzt nicht Unmengen an Geld ausgeben, es bestand nach wie vor noch die Option, dass mein Unterfangen scheitern würde) lief noch ins Leere, das zweite Objekt der Begierde wurde dann aber vergleichsweise günstig ergattert.

Ausgepackt, angeschlossen, mit OpenWrt gefüttert, VLANs eingerichtet, zuletzt dann ins Netzwerk integriert und voilà … das Tagging auf der Maschine funktioniert, die VLANs werden durchgereicht, und die beiden Maschinen kommunizieren jetzt auch wunderbar über das VLAN (Sie hängen ja am Router auch noch an einem Port).

Und WLAN habe ich auch noch!

Der Router ist ersten Erkenntnissen nach sogar schneller geworden, scheint so, als wäre das Gerät nicht dafür ausgelegt, dass alle Ethernet Ports belegt sind.

Eigentlich alles gut, aber ….

Auf OpenWrt wird KEINE direkte Kaufempfehlung gegeben.

Ich habe mich damals an den Kapazitäten der Router (CPU, RAM, (Flash- bzw. ROM-))Speicher orientiert, und eine der potenteren Maschinen gekauft. Im direkten Link zu dem Gerät (https://openwrt.org/toh/netgear/r8000) wird zwar über die WLAN-Schwierigkeiten gesprochen, aber auch gleich eine Lösung präsentiert. Die Schwierigkeiten mit dem VLAN werden nicht erwähnt. Und auch wenn es etwas harsch erscheint (es handelt sich immerhin um ein kostenloses Produkt, welches, das Know-how vorausgesetzt, mehr Potenzial aus den Routern holt), ich hätte die Warnung gerne vor dem Neukauf bekommen, dann hätte ich mich gegebenenfalls noch umorientiert.

Der Router davor (D-LINK Dir-825) kommt allmählich in die Jahre und es wird aktiv davor gewarnt, diesen aufgrund der geringen Kapazitäten noch zu nutzen. Und ja, auch einen Weg zurück zum alten Router hatte ich in Erwägung gezogen …

Mein Fazit: Broadcom kommt mir nicht mehr ins Haus!