Systemd: start und stop dependencies

Das Problem

Auf einem Debian-Server (jessie, systemd 215) läuft eine Web-Applikation, die einen Samba-Share (cifs) via openvpn (IPv6) benötigt. Mit der naiven Konfiguration funktioniert manuelles Starten und Stoppen problemlos, beim Reboot geht’s aber nicht richtig.

Naive Konfiguration:

  • Debian Packages: apache2 cifs-utils openvpn
  • cifs mount in /etc/fstab konfiguriert
  • openvpn Konfiguration in /etc/openvpn/client.conf

Wenn man alles einzeln startet, funktioniert es reibungslos:

  • systemctl start openvpn@client.service
  • mount /var/www/data
  • systemctl start apache2.service

Auch das Stoppen geht manuell problemlos:

  • systemctl stop apache2.service
  • umount /var/www/data
  • systemctl stop openvpn@client.service

Aber bei einem reboot klappt weder Hochfahren…

  • Beim Start wird der mount versucht, bevor das VPN verfügbar ist und schlägt natürlich fehl.
  • Apache läuft zwar, aber die Web-Applikation hat ein Problem, sobald sie auf Files im cifs mount zugreifen will.

… noch Runterfahren:

  • Das VPN ist unterbrochen, bevor der umount abgeschlossen ist.
  • Das System hängt 2 Minuten bis der Timeout für den umount abgelaufen ist. Sehr lästig.

Ansatz #1

Die Lösung sollte mit systemd units nicht allzu schwierig sein.

Da sich in /etc/fstab keine Abhängigkeiten explizit formulieren lassen, wird für den mount eine eigene Unit-Datei erstellt. Wir starten mit der automatisch erzeugten Unit-Datei:

systemctl cat var-www-data.mount > /etc/systemd/system/var-www-data.mount

Und löschen die Einträge SourcePath und Documentation, so dass nur noch das Minimum da steht:

#/etc/systemd/system/var-www-data.mount
[Unit]
Before=remote-fs.target

[Mount]
What=//v6.smb.example.com/WwwDataShare
Where=/var/www/data
Type=cifs
Options=ro,guest,iocharset=utf8

Dann wird der Eintrag aus /etc/fstab gelöscht und die Unit aktiviert mit:

systemctl enable var-www-data.mount; systemctl daemon-reload

/var/www/data müsste jetzt sauber ein- und wieder ausgehängt werden können.

Jetzt können wir die Abhängigkeit zu openvpn formulieren:

#/etc/systemd/system/var-www-data.mount
[Unit]
Before=remote-fs.target
After=openvpn@client.service
Requires=openvpn@client.service

systemctl daemon-reload

Funktioniert leider immer noch nicht. Nach diversen Analysen mit journalctl -f war klar, dass

  • die openvpn Unit beim Start behauptet, sie sei „fertig“, obwohl die IPv6 Routen noch nicht korrekt gesetzt sind.
  • der umount von /var/www/data beim Stoppen behauptet, er sei fertig, obwohl es noch ausstehende Netzwerk-Kommunikation hat.

Lösung

Zuerst braucht es ein Script, welches die Vorbedingungen beim Starten (ping) und Stoppen (umount) prüft:

/etc/openvpn/checks_updown

#!/bin/bash
LOGGER_RED="systemd-cat -t $0 -p err"
LOGGER_BOLD="systemd-cat -t $0 -p notice"
LOGGER_NORM="systemd-cat -t $0 -p info"

case "$1" in
  ping) 
    while true
    do
      if ping6 -q -c 3 v6.smb.example.com > /dev/null 2>&1 
      then
        echo ping6 ok | $LOGGER_BOLD
        exit 0
      else
        echo "." | $LOGGER_NORM
        sleep 4
      fi
    done
    ;;
  umount) 
    while true
    do
      if mount | grep "//v6.smb.example.com/" >/dev/null 2>&1
      then
        echo ";" | $LOGGER_NORM
        sleep 4
      else
        echo umount ok | $LOGGER_BOLD
        exit 0
      fi
    done
    ;;
  *) 
    echo "invalid call: $1" | tee | $LOGGER_RED
    exit 1
    ;;
esac

Für dieses Script bauen wir eine eigene Unit:

# /etc/systemd/system/vpnbarrier.service
[Unit]
Requires=openvpn@client.service
After=openvpn@client.service

[Service]
Type=oneshot
RemainAfterExit=yes
ExecStart=/etc/openvpn/checks_updown ping
ExecStop=/etc/openvpn/checks_updown umount

[Install]
RequiredBy=var-www-data.mount

Das RemainAfterExit=yes bewirkt (zusammen mit oneshot), dass ExecStop erst beim Stop dieser Unit ausgeführt wird. Ohne diese Einstellung würde ExecStop unmittelbar nach ExecStart, also noch während des Hochfahrens, ausgeführt.

Abhängigkeiten in der mount-Unit erweitern:

# /etc/systemd/system/var-www-data.mount
[Unit]
Before=apache2.service
Requires=vpnbarrier.service
After=vpnbarrier.service

[Mount]
...

[Install]
RequiredBy=apache2.service

systemctl enable vpnbarrier.service; systemctl daemon-reload

Erst jetzt funktioniert alles wie es muss:

  • systemctl stop openvpn@client.service: Zuerst wird apache2 gestoppt, dann /var/www/data abgehängt und erst dann das VPN gestoppt.
  • systemctl start apache2.service: Zuerst wird das VPN gestartet, dann der cifs-mount und schliesslich apache2.
  • Ein reboot dauert knapp 20 Sekunden.

Bemerkungen

  • Die Dokumentation von systemd ist recht gut, zum Beispiel bei digitalocean. Der Schwerpunkt ist aber meistens auf dem Hochfahren und man findet kaum etwas zum Runterfahren.
  • Eine etwas andere Diskussion hat gezeigt, dass es auch andere gibt, denen etwas fehlt.
  • Die wichtigste Einsicht: Immer sowohl Requires als auch After benutzen, nur so kriegt man auch das Runterfahren in den Griff.
  • Root Cause: Ist das Problem ein generisches systemd-Problem oder ein Bug im openvpn-Paket? Und beim cifs umount?
  • Könnte man das Problem noch etwas eleganter Lösen, z.B. indem man auf eine separate Unit vpnbarrier.service verzichtet und den „ping“- bzw „umount“-Check durch Einträge in *.conf.d-Verzeichnisse geeignet einbaut?
  • Ab und zu war es nicht ausreichend, systemd daemon-reload zu machen, erst nach einem Reboot hat alles wie erwartet funktioniert.