systemd 服务关闭并进行磁盘访问

systemd 服务关闭并进行磁盘访问

我想运行一个小备份脚本(btrfs 快照 + 传输到 USB)在系统关闭时。

以下服务在关闭时执行得非常好,但在重新启动时却无法按预期执行:

[Unit]
Description=Backup on poweroff to external encrypted USB disk.
DefaultDependencies=no
Before=shutdown.target

[Service]
Type=oneshot
ExecStart=/usr/bin/sleep 30
TimeoutSec=infinity
RemainAfterExit=yes

[Install]
WantedBy=poweroff.target

一旦该服务按预期工作,其中/usr/bin/sleep 30将被实际的备份脚本替换。


但为了使备份脚本正常工作,我需要在启动之前使用 cryptsetup 解密外部 USB 磁盘。这一切都是通过/etc/crypttab( noauto) 和 systemd (通过)的组合在后台处理的systemd-cryptsetup-generator,它会自动生成一个 systemd 单元文件systemd-cryptsetup@<name>.service,我可以在上面的单元中引用该文件。

但是一旦我将以下要求添加到 [unit] 部分:

[email protected]
[email protected]

在系统关闭/断电期间,该服务不再启动。

通过手动启动服务systemctl start poweroff-backup.service没有问题,因此服务文件本身没有问题...:-/


思考问题是poweroff.targetviasystemd-poweroff.service需要umount.target反过来卸载所有设备(包括 cryptsetup '坐骑') 通过组合冲突者=之后=systemd mount 和 cryptsetup 单元的语句。但是添加另一个要求umount.target不会改变任何东西,例如:

Before=umount.target

没有任何额外的要求,我可以清楚地看到该服务总是在umount.target达到后立即启动。

答案1

好吧,我发现了这个问题,从中我能够构建一个正确工作的解决方案,该解决方案仍然遵循 systemd 工作流程(大部分情况下)。

问题

以下服务虽然乍一看是正确的,但永远不会被执行:

# 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

该服务永远不会启动的原因是,之间存在依赖循环问题/冲突,其中 shutdown.target 与 cryptsetup.target 冲突,以确保在关闭(断电/重新启动)之前分离所有加密磁盘和 poweroff-backup.service需要通过 poweroff.target 激活它。poweroff.target -[require]-> shutdown.target -[conflicts]-> cryptsetup.target <-[require]- [email protected] <-[require]- poweroff-backup.service <-[wants]- poweroff.target

解决方案

知道了这一点,明显的问题在于我错误的假设,即 systemd 可以通过知道 poweroff-backup.service 是一个“一击“一旦 poweroff-backup.service 被激活并转换为非活动/完成后,系统可以首先解决该服务,同时继续正常关闭转换。

因此,这里有一个使用 ExecStop= 而不是 ExecStart= 的服务,它允许我们避免任何依赖循环,但有一些小警告:

# 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

这是有效的,因为:

  • 防止shutdown.target、cryptsetup.target和poweroff-backup.service引起的依赖循环问题
  • 正确的工作顺序仍然保留,因为 systemd 确保关闭顺序与启动顺序相反(如 After= 和 Before= 给出)

我对这个解决方案只有两个小抱怨:

  • 由于该服务在登录时启动,因此它也会在登录时解密外部 USB 磁盘。我希望这种情况仅在执行实际备份脚本时发生,同时仍使用正常的 systemd After= 和 Require= 顺序和依赖机制。这可以通过添加以下内容来处理,但这意味着我没有使用 cryptsetup 服务来解密/分离 USB 磁盘:

    ExecStop=/usr/lib/systemd/systemd-cryptsetup attach ext
    [...]
    ExecStop=/usr/lib/systemd/systemd-cryptsetup detach ext
    
  • 我没有任何方法可以通过引用 shutdown.target 有条件地执行 ExecStop= 仅在关闭时执行,并且必须依赖于使用的一些辅助命令systemctl list-jobs

问题:有人知道如何改进这些点吗?

完整的服务文件

这是我当前的服务文件,只有在安装 USB 磁盘后,通过将 (WantedBy=) 连接到 USB 设备(通过其 UUID)而不是 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

答案2

我的用例略有不同,我想确保所有其他服务都停止并在重新启动期间使用 Rsync 执行备份。以下内容在 Ubuntu 22.04 上对我有用。我将这个答案留在这里给那些希望将来在关机时运行脚本的人。

来源:https://documentation.suse.com/smart/systems-management/html/reference-managing-systemd-targets-systemctl/index.html

服务文件如下所示:

[Unit]
Description=Run my custom task at shutdown
DefaultDependencies=no
Before=systemd-reboot.service
After=final.target

[Service]
Type=oneshot
ExecStart=/usr/local/scripts/custom_script.sh
TimeoutStartSec=0

[Install]
WantedBy=systemd-reboot.service

我的custom_script.sh样子如何:

#!/bin/sh
ps aux > /usr/local/scripts/processes.txt
df -Th > /usr/local/scripts/df.txt
mount > /usr/local/scripts/mount.txt
sleep 180

由于这只是为了测试,我运行是sudo chmod 777 -R /usr/local/scripts/为了防止出现权限问题。

以下是我测试的步骤:

  1. 为了确保脚本在关机和重新启动时运行,我sleep 180custom_script.sh.接下来,我关闭操作系统。仅 3 分钟后操作系统就完全关闭。当脚本运行(等待)时,会显示 Ubuntu 加载徽标。我启动了系统并重新启动了它。显示相同的 Ubuntu 加载徽标,3 分钟后系统重新启动。这证明该脚本正在关机/或重新启动时运行。
  2. 确保所有其他服务均已停止;我记录了custom_script.sh运行时正在运行的所有进程ps aux > processes.txt。的输出processes.txt不包含其他服务。这证明所有其他服务都已停止。
  3. 为了确保文件系统仍然已安装,我使用df -Th > df.txt和记录了安装情况mount > mount.txt。文件的输出显示我的所有分区均已安装并处于读写模式。

相关内容