Okay I found the issue and from that I was able to build a correctly working solution that stills follows the systemd workflow (for the most part).
Issue
The following service, while on first sight looking correct will never be executed:
# poweroff-backup.service
[Unit]
Description=Backup on poweroff to external encrypted USB disk.
DefaultDependencies=no
Before=shutdown.target
# This causes the service to be discarded/skipped due to
# a dependency cycle conflict. See below...
[email protected]
[email protected]
Before=umount.target
[Service]
Type=oneshot
# Note: The script executed here needs access to '/', '/tmp'
# (or Systemd's PrivateTemp) and '/dev/mapper/ext' which is the
# decrypted USB partition managed by /etc/crypttab and
# systemd-cryptsetup-generator (as [email protected])
ExecStart=/usr/bin/sleep 30
TimeoutSec=infinity
# Note: This does not make any difference
RemainAfterExit=yes
[Install]
WantedBy=poweroff.target
The reason why this service will never be started is because there is a dependency cycle issue/conflict between poweroff.target -[require]-> shutdown.target -[conflicts]-> cryptsetup.target <-[require]- [email protected] <-[require]- poweroff-backup.service <-[wants]- poweroff.target
, where shutdown.target conflicts cryptsetup.target to ensure all encrypted disks are detached before shutdown (poweroff/reboot) and poweroff-backup.service which needs it to be activated by poweroff.target.
Solution
Knowing this, the obvious problem lies in my wrong assumption that systemd could somehow avoid this dependency cycle conflict by knowing that poweroff-backup.service is a "one-shot" service that can be resolved first by systemd while continuing with the normal shutdown transition once poweroff-backup.service has been activated and transitioned into inactive/done.
So here is a service that uses ExecStop= instead of ExecStart= which allows us to avoid any dependency-cycles, but has some minor caveats:
# poweroff-backup.service
[Unit]
Description=Backup external encrypted USB disk on poweroff.
[email protected]
After=multi-user.target
[email protected]
[Service]
Type=oneshot
ExecStart=/usr/bin/echo "Waiting for poweroff..."
# Note: We need this, since there is no other way to detect poweroff vs reboot now.
ExecStop=/usr/bin/systemctl list-jobs | /usr/bin/egrep -q 'poweroff.target.*start'
# Note: This should be replaced with your script
ExecStart=/usr/bin/sleep 30
TimeoutSec=infinity
RemainAfterExit=true
[Install]
WantedBy=multi-user.target
This works because:
- It prevents the dependency cycle issue caused by shutdown.target, cryptsetup.target and poweroff-backup.service
- The correct working order is still preserved, since systemd ensures that shutdown order is the reverse of the startup order (as given by After= and Before=)
There are only two minor gripes I have with this solution:
Since the service is started on login, it will also decrypt the external USB disk on login. I would prefer if this would only happen once the actual backup-script is executed while still using the normal systemd After= and Require= order and dependency mechanics. This could be handled by adding the following, but this would mean I'm not using the cryptsetup service for decrypting/detaching the USB disk:
ExecStop=/usr/lib/systemd/systemd-cryptsetup attach ext
[...]
ExecStop=/usr/lib/systemd/systemd-cryptsetup detach ext
I don't have any way to conditionally execute ExecStop= only on shutdown, by referencing the shutdown.target and have to rely on a little helper command using systemctl list-jobs
.
Question: Has somebody an idea how to improve on those points?
Full Service File
This is my current service file, which only starts the service once the USB disk is mounted, by attaching (WantedBy=) to the USB device (by its UUID) instead of the multi-user.target:
# poweroff-backup.service
[Unit]
Description=Backup external encrypted USB disk on poweroff.
[email protected]
Requires=-.mount
Requires=tmp.mount
After=dev-disk-by\<uuid here>.device
[email protected]
After=-.mount
After=tmp.mount
[Service]
Type=oneshot
ExecStart=/usr/bin/echo "Waiting for poweroff to trigger snapshot and archive transfer..."
ExecStop=/usr/bin/systemctl list-jobs | /usr/bin/egrep -q 'poweroff.target.*start'
ExecStop=-/usr/local/bin/btrfs-snapshots.sh --device='UUID=<uuid>' @ @boot @home
ExecStop=/usr/local/bin/btrfs-archive.sh --source='UUID=<uuid>' --target=/dev/mapper/ext
TimeoutSec=infinity
RemainAfterExit=true
[Install]
WantedBy=dev-disk-by\<uuid here>.device