Zammad-Fehlersuche: Wenn MySQL-Altlasten die PostgreSQL-Datenbank crashen (und wie man es repariert)
Wer ein Helpdesk-System wie Zammad über Jahre hinweg pflegt, kennt das: Das System wächst, bekommt zahllose Updates und zieht vielleicht sogar von einem Datenbank-System zum nächsten um. In unserem Fall haben wir eine Zammad-Instanz, die ursprünglich unter MySQL lief, vor einiger Zeit nach PostgreSQL migriert und immer fleißig aktualisiert – aktuell auf Version 7.1.1.
Eigentlich lief alles wunderbar, bis unsere Telefonanlagen-Kopplung (CTI) plötzlich streikte. Ein Blick ins Zammad-Log offenbarte diesen unschönen Fehler:
ERROR -- : Error ID Yy6FsVxF: PG::NotNullViolation: FEHLER: NULL-Wert in Spalte »start_at« von Relation »cti_logs« verletzt Not-Null-Constraint
Die Diagnose: Ein Geist aus der MySQL-Vergangenheit
siehe Zammad MySQL Postgres-MigrationUm den Fehler einzugrenzen, habe ich eine komplett frische Zammad 7.1.1 Instanz aufgesetzt. Dort funktionierte die CTI-Integration tadellos! Doch sobald ich den SQL-Dump unseres produktiven Systems in die frische Installation importierte, war auch dieses System kaputt.
Der Verdacht: Das Datenbank-Schema der alten Installation unterscheidet sich von dem einer frischen Installation. Bei der historischen Migration von MySQL zu Postgres und den unzähligen Zammad-Updates wurden offenbar alte, zu strikte Datenbank-Regeln (NOT NULL Constraints) mitgeschleppt, die Zammad heute gar nicht mehr verwendet.
Der Quick-Fix für die CTI-Integration
Um die CTI-Kopplung sofort wieder zum Laufen zu bekommen, reichte es, den falschen Constraint von den betroffenen Spalten zu entfernen. Über die Datenbank-Konsole (sudo -u postgres psql zammad_production) war das schnell erledigt:
ALTER TABLE cti_logs ALTER COLUMN start_at DROP NOT NULL; ALTER TABLE cti_logs ALTER COLUMN end_at DROP NOT NULL;
Sofort nach dem Ausführen funktionierte die Telefon-Integration wieder. Aber das eigentliche Problem war damit nicht gelöst: Wie viele solcher Altlasten schlummerten noch in der Datenbank und warteten darauf, beim nächsten Zammad-Feature abzustürzen?
Der Profi-Weg: Schemata vergleichen mit "migra"
Ich wollte unsere alte Datenbank zu 100 % an das Schema einer frischen Installation angleichen – ohne dabei unsere wertvollen Ticket-Daten zu verlieren.
Das Tool der Wahl dafür heißt migra, ein Python-Programm, das zwei PostgreSQL-Datenbanken vergleicht und automatisch die passenden ALTER TABLE-Befehle ausspuckt, um die Unterschiede auszubügeln.
Da moderne Linux-Distributionen (z. B. Debian) die systemweite Installation von Python-Paketen blockieren, musste eine virtuelle Umgebung (venv) her.
Stolperstein: Python 3.13 und "pkg_resources"
Beim Einrichten der Umgebung stieß ich auf einen modernen Python-Fallstrick: migra stürzte beim Start mit ModuleNotFoundError: No module named 'pkg_resources' ab. Der Grund? Neuere Python-Versionen liefern in den setuptools dieses veraltete Modul nicht mehr mit.
Die Lösung war, eine ältere Version der Setuptools in der virtuellen Umgebung zu erzwingen:
# Virtuelle Umgebung erstellen und aktivieren python3 -m venv ~/migra_env source ~/migra_env/bin/activate # Kompatible Setuptools und Migra installieren pip install "setuptools<70.0.0" migra psycopg2-binary
Den chirurgischen Eingriff vorbereiten
Ich hatte nun beide Datenbanken auf demselben Server:
- zammad (Die frische, saubere Installation)
- zammad_old (Unser Backup mit den Altlasten)
Der Vergleichsbefehl (ausgeführt als User postgres, um Berechtigungsprobleme zu vermeiden) lautete:
sudo -u postgres ~/migra_env/bin/migra --unsafe postgresql:///zammad_old postgresql:///zammad > korrektur.sql
(Das Flag --unsafe ist nötig, weil Migra sonst abbricht, wenn es alte Tabellen löschen will, die in der neuen Version nicht mehr existieren).
Die generierte Datei korrektur.sql war riesig. Migra wollte auch Spalten löschen (DROP COLUMN), die vielleicht noch historische Plugin-Daten enthielten. Da unser Hauptproblem aber die strikten NOT NULL-Bedingungen waren, nutzte ich einen einfachen Linux-Trick, um nur die sicheren Korrekturen herauszufiltern:
grep -i "drop not null" korrektur.sql > sichere_korrektur.sql
Diese gefilterte, nun sehr übersichtliche Datei konnte ich gefahrlos auf unsere kaputte Datenbank anwenden:
sudo -u postgres psql -d zammad_old -f sichere_korrektur.sql
Der Feinschliff: Berechtigungen und Sequenzen
Nach dem Import verglich ich die Schemata nochmals mit pg_dump --schema-only. Fast perfekt! Zwei kleine Unschönheiten fielen noch auf, die ich manuell glattzog:
1. Falscher Tabellen-Besitzer (Owner)
Weil ich als User postgres agiert hatte, gehörten die geänderten Tabellen plötzlich nicht mehr dem User zammad. Ein Update hätte das System später zerschossen. Die Lösung ist ein genialer Postgres-Einzeiler:
sudo -u postgres psql -d zammad_old -c "REASSIGN OWNED BY postgres TO zammad; ALTER SCHEMA public OWNER TO zammad;"
2. Fehlende Datentypen bei Zählern (Sequences)
Bei einigen Sequenzen fehlte der Zusatz AS integer, der bei ganz frischen Postgres-Installationen Standard ist. Um nicht alle manuell tippen zu müssen, ließ ich mir von Postgres die Fixes generieren:
SELECT 'ALTER SEQUENCE ' || sequence_schema || '.' || sequence_name || ' AS integer;' FROM information_schema.sequences WHERE sequence_schema = 'public' AND data_type = 'bigint';
Die ausgegebenen Befehle (z. B. ALTER SEQUENCE public.users_id_seq AS integer;) warf ich wieder in die Konsole – fertig!
Fazit
Nach einem abschließenden Leeren des Zammad-Caches (zammad run rails r "Cache.clear") und einem Service-Neustart lief das System perfekt. Ein Re-Index der Elassic-Search wurde ebenfalls gestartet
sudo -u zammad zammad run rake zammad:searchindex:rebuild[8]
Eine Datenbank über Jahre und Plattformen hinweg zu aktualisieren, hinterlässt fast immer Spuren. Aber mit Werkzeugen wie migra, ein wenig Bash-Magie (grep) und PostgreSQL-Bordmitteln lässt sich selbst das chaotischste gewachsene Schema wieder in einen Zustand versetzen, der von einer "Out-of-the-Box"-Neuinstallation nicht zu unterscheiden ist.
Tipp: Bevor Sie solche weitreichenden Schema-Änderungen durchführen, machen Sie immer einen vollständigen SQL-Dump oder VM-Snapshot!