用 udev 和 systemd 触发外置盘 Borg 备份

这份 Borg 1.4.5 官方指南演示一种 Linux 自动备份方式:接入已登记的外置硬盘后,系统自动挂载它、创建 Borg 归档,并在完成后选择卸载、停盘或休眠。方案使用 udev 和 systemd,适合按需连接本地备份盘的工作流。

原文提供的是需要按机器调整的脚本模板。尤其是设备识别、失败清理和自动停盘,不能只改一个路径就投入使用。本文先完整说明原流程,再逐项标注静态审查发现;没有在本机执行磁盘、备份或电源管理操作。

udev触发systemd服务,脚本核对并挂载备份盘,然后运行Borg归档并显式处理清理;停盘和休眠是额外可选步骤。
原创技术示意图。原脚本没有完整覆盖图中所强调的失败清理和来源核验,这两项是改造时必须补充的工作。

事件触发与真正执行备份的是两个组件

udev 规则监听块设备加入事件。规则通过 systemd 标签和 SYSTEMD_WANTS 请求启动一个 oneshot 服务。真正的备份脚本运行在标准的 systemd 服务环境中,标准输出和标准错误都会进入 journal,便于用 journalctl 查看。

脚本检查接入设备是否在登记清单内,挂载其文件系统,再向 Borg 仓库创建归档。正常结束后,可以选择卸载文件系统并让硬盘停转,之后再物理断开设备。这里的“自动断开”不是任意时刻拔盘都安全:需要确认备份和卸载确实完成。

建立配置目录与设备触发规则

原文把全部配置放在 /etc/backups 中,并要求以 root 创建该目录。开始前先识别备份硬盘的分区表 ID。原例假设硬盘是 /dev/sdz:

lsblk --fs -o +PTUUID /dev/sdz

/dev/sdz 只是示例设备名,必须按实际设备核对。接下来将以下规则写入 /etc/backups/80-backup.rules;原文要求它保持为一行,并用刚查到的 PTUUID 替换括号内的提示。

ACTION=="add", SUBSYSTEM=="block", ENV{ID_PART_TABLE_UUID}=="<the PTUUID you just noted>", TAG+="systemd", ENV{SYSTEMD_WANTS}+="automatic-backup.service"

ID_PART_TABLE_UUID 是分区表标识,而后面脚本登记的是文件系统 UUID,二者不是同一个字段。规则把块设备接入事件交给 automatic-backup.service。服务文件位于 /etc/backups/automatic-backup.service:

[Service]
Type=oneshot
ExecStart=/etc/backups/run.sh

编者补充:ExecStart 指向的脚本需要存在并具有可执行权限,解释器路径也必须可用。配置目录、脚本与登记清单应只允许受信任管理员修改,因为原流程会以服务权限挂载设备并读取系统文件。/mnt/backup 也要预先建立。oneshot 服务不是交互终端,不能依赖桌面会话中的环境变量或等待人工输入密码。

原文脚本:先理解每一步,再按环境改造

下面完整保留原始模板。其中包含挂载、卸载、停盘和系统休眠命令,仅供静态阅读;它不是已经加固或实测通过的替代方案。部署前请结合后文审查处理设备消歧、退出状态和清理路径。

#!/bin/bash -ue

# The udev rule is not terribly accurate and may trigger our service before
# the kernel has finished probing partitions. Sleep for a bit to ensure
# the kernel is done.
#
# This can be avoided by using a more precise udev rule, e.g. matching
# a specific hardware path and partition.
sleep 5

#
# Script configuration
#

# The backup partition is mounted here
MOUNTPOINT=/mnt/backup

# This is the location of the Borg repository
TARGET=$MOUNTPOINT/borg-backups/backup.borg

# Archive name scheme
DATE=$(date --iso-8601)-$(hostname)

# This is the file that will later contain UUIDs of registered backup drives
DISKS=/etc/backups/backup.disks

# Find whether the connected block device is a backup drive
for uuid in $(lsblk --noheadings --list --output uuid)
do
        if grep --quiet --fixed-strings $uuid $DISKS; then
                break
        fi
        uuid=
done

if [ ! $uuid ]; then
        echo "No backup disk found, exiting"
        exit 0
fi

echo "Disk $uuid is a backup disk"
partition_path=/dev/disk/by-uuid/$uuid
# Mount the file system if not already done. This assumes that if something is already
# mounted at $MOUNTPOINT, it is the backup drive. It won't find the drive if
# it was mounted somewhere else.
findmnt $MOUNTPOINT >/dev/null || mount $partition_path $MOUNTPOINT
drive=$(lsblk --inverse --noheadings --list --paths --output name $partition_path | head --lines 1)
echo "Drive path: $drive"

#
# Create backups
#

# Options for borg create
BORG_OPTS="--stats --one-file-system --compression lz4 --checkpoint-interval 86400"

# Set BORG_PASSPHRASE or BORG_PASSCOMMAND somewhere around here, using export,
# if encryption is used.

# No one can answer if Borg asks these questions, it is better to just fail quickly
# instead of hanging.
export BORG_RELOCATED_REPO_ACCESS_IS_OK=no
export BORG_UNKNOWN_UNENCRYPTED_REPO_ACCESS_IS_OK=no

# Log Borg version
borg --version

echo "Starting backup for $DATE"

# This is just an example, change it however you see fit
borg create $BORG_OPTS \
  --exclude root/.cache \
  --exclude var/lib/docker/devicemapper \
  $TARGET::$DATE-$$-system \
  / /boot

# /home is often a separate partition / file system.
# Even if it isn't (add --exclude /home above), it probably makes sense
# to have /home in a separate archive.
borg create $BORG_OPTS \
  --exclude 'sh:home/*/.cache' \
  $TARGET::$DATE-$$-home \
  /home/

echo "Completed backup for $DATE"

# Just to be completely paranoid
sync

if [ -f /etc/backups/autoeject ]; then
        umount $MOUNTPOINT
        hdparm -Y $drive
fi

if [ -f /etc/backups/backup-suspend ]; then
        systemctl suspend
fi

脚本以 #!/bin/bash -ue 开始。-u 使未定义变量成为错误,-e 会在许多非零退出情形下提前结束。先等待五秒,是为了减轻 udev 触发早于内核分区探测完成的问题;原文也指出,更准确地匹配硬件路径和分区,可以减少对这一等待的依赖。固定睡眠时长并不能证明目标设备已经就绪。

MOUNTPOINT 指定挂载点;TARGET 指向该挂载点下的 Borg 仓库;DATE 由日期和主机名组成。归档名额外拼接 shell 进程号 $$ 以及 system 或 home,区分一次调用中创建的两组归档。

脚本列出当前块设备的文件系统 UUID,在 /etc/backups/backup.disks 中寻找匹配项。找到后通过 /dev/disk/by-uuid/ 定位分区,并在认为挂载点空闲时挂载它。随后尝试推导硬盘路径,为可选的停盘操作准备目标。

Borg 参数与备份范围

原例的公共选项是 --stats --one-file-system --compression lz4 --checkpoint-interval 86400:输出统计信息,只在每个指定根路径的文件系统范围内遍历,使用 LZ4 压缩,并把检查点间隔设为 86400 秒。这个间隔值需要结合备份时长和中断容忍度评估,不是所有机器都应采用的默认推荐。

第一份归档包含 / 和 /boot,排除示例中的 root 缓存和 Docker devicemapper 路径。第二份归档单独包含 /home/,排除用户缓存。--one-file-system 会跳过遍历过程中遇到的其他文件系统,因此把 /boot 和 /home 作为明确的根路径很重要;其他独立挂载点仍需逐一检查是否应该备份。

反过来,如果 /home 与根目录在同一文件系统上,第一份归档可能已经包含它,再建 home 归档会重复。原文建议在这种情况下为系统归档另加相应排除项。示例排除的 Docker 路径也不代表所有容器存储驱动;数据库和持续写入的应用需要一致性导出或其他专门方案,单次文件遍历不能保证事务一致。

如果仓库加密,应通过合适的安全方式提供 BORG_PASSPHRASE 或 BORG_PASSCOMMAND。原例没有写入实际密码。无人值守流程不能回答仓库位置变化或未知未加密仓库的确认问题,所以它将 BORG_RELOCATED_REPO_ACCESS_IS_OK 和 BORG_UNKNOWN_UNENCRYPTED_REPO_ACCESS_IS_OK 设为 no,选择尽快失败,而不是挂起等人回答。

完成后的可选动作

原脚本在两次 borg create 都继续执行到末尾后调用 sync。如果存在 /etc/backups/autoeject,便卸载挂载点并执行 hdparm -Y;如果暂时希望保留挂载以检查仓库,可按原文建议把标记文件改名,例如改为 autoeject-no。

如果存在 /etc/backups/backup-suspend,脚本最后调用 systemctl suspend。原文特别提醒:恢复机器之前先拔下备份盘,否则可能再次触发备份并形成循环。停盘和休眠都会影响机器状态,应该由明确的运维策略决定,不能把它们当成成功备份的必要条件。

启用规则与登记备份盘

先建立空的 /etc/backups/backup.disks 登记文件。原文通过符号链接把规则和服务放进系统查找目录,再重新加载:

ln -s /etc/backups/80-backup.rules /etc/udev/rules.d/80-backup.rules
ln -s /etc/backups/automatic-backup.service /etc/systemd/system/automatic-backup.service
systemctl daemon-reload
udevadm control --reload

这些命令会修改系统配置,应只在确认内容、路径和权限之后执行;本稿没有实际执行。重新加载配置不等于已经验证设备匹配正确。

接入待登记硬盘,确认文件系统可用。原文提到必要时格式化,但格式化会破坏已有数据;这里不提供或运行格式化命令。查询应保存备份的文件系统 UUID:

lsblk -o+uuid,label

把该 UUID 登记到 backup.disks,建议一行一个,并准确核对它对应的是预期分区。将目标文件系统挂载到 /mnt/backup,然后在 TARGET 指定的位置初始化仓库。原文命令如下,其中 ... 是需选择的加密参数,不是可以原样执行的有效配置:

borg init --encryption ... /mnt/backup/borg-backups/backup.borg

初始化涉及仓库和密钥管理,应按 Borg 1.4 的加密选项文档选择模式,并保管必要的恢复材料。完成后卸载并重新连接设备,或按原文手动启动服务:

systemctl start --no-block automatic-backup

查看服务日志的原始用法如下。方括号表示可选参数的文档记法,不应连同占位文本原样输入:

journalctl -fu automatic-backup [-n number-of-lines]

例如只指定 journalctl -fu automatic-backup 可跟随查看该服务的日志;是否完成备份还必须结合退出状态、归档列表和后续恢复检查,不能只看到了“服务启动”。

静态审查:原脚本最需要改造的地方

UUID 是识别手段,不是认证。原文安全章节已明确说明,任何文件系统只要使用登记的 UUID,都可能被脚本挂载。这样做主要是避免每插入普通 U 盘都触发错误,不是防止恶意设备。挂载文件系统和读取仓库都会接触内核文件系统驱动、可能的用户态服务以及 Borg 本身的解析面。

原例 grep --quiet --fixed-strings $uuid $DISKS 只做固定字符串查找,没有要求整行相等;变量也未引用。最低限度应初始化变量,过滤空值,并对登记项进行精确匹配。下面只是替换匹配语句的示意,不是完整脚本:

if [ -n "$uuid" ] && grep -Fxq -- "$uuid" "$DISKS"; then
    # 找到一个精确登记项;仍需处理多盘、挂载来源与权限。
    :
fi

如果同时接入多个登记盘,“列表中第一个匹配项”不一定是刚触发事件的盘;udev 还可能对磁盘和分区产生多个事件。需要明确事件与设备的对应关系,并考虑锁和重复触发。仅有 sleep 5 不能解决这些问题。

已有挂载必须核对来源。原脚本只要 findmnt 找到挂载点就不再挂载,并假设那里已经是备份盘。这可能把归档写进错误的文件系统。需要检查该挂载点的实际来源、UUID、预期设备关系及挂载方式;也要处理目标盘已挂在别处的情况。

告警也是非零状态。在 Borg 1.x 中,告警退出码不能简单等同于“全部无误”,但也未必是致命失败。原脚本的 -e 可能在第一次归档产生告警时就退出,第二份归档、sync 与卸载均被跳过。应明确收集每个命令的状态,并在 trap 中设计可验证的清理。退出码分流可以采用下面的思路:

# 编者片段:在条件语句内接收非零状态,避免直接被 set -e 中断。
if borg create ...; then
    rc=0
else
    rc=$?
fi

case "$rc" in
    0) echo "本次归档无告警完成" ;;
    1) echo "本次归档完成但有告警,检查日志" ;;
    *) echo "本次归档失败:$rc" >&2 ;;
esac
# 后续仍要汇总状态、按实际归属清理挂载,并把结果返回给 systemd。

这里的 ... 明确表示省略备份参数;此片段只是控制流示意,没有执行,也不能直接替代完整脚本。清理逻辑应只卸载本流程负责的挂载,保留错误证据,并避免因清理失败掩盖原始故障。

最后,不要从 lsblk 输出的第一行就假定已经确认了可安全停转的整块硬盘。应核对真实设备拓扑,并在不明确时禁用 hdparm 操作。本文没有运行停盘、休眠或任何磁盘修改命令,也没有验证还原结果;静态审查未发现的部分不能据此视为没有漏洞。

来源、作者与许可

原文为 Automated backups to a local hard drive。原作者/维护者:BorgBackup 文档贡献者/The Borg Collective。核对日期:2026-10-05。技术审读与示意图:未完纪编辑整理。原文未标个人作者时,不补造个人署名。

原文及示例保留 BorgBackup 归属,项目许可见 Authors and License。下面附带其版权声明、三项再分发条件及免责条款。编者修改建议和原创图不代表上游验证。

随文保留的原始许可证全文
Copyright (C) 2015-2026 The Borg Collective (see AUTHORS file)
Copyright (C) 2010-2014 Jonas Borgström <jonas@borgstrom.se>
All rights reserved.

Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions
are met:

 1. Redistributions of source code must retain the above copyright
    notice, this list of conditions and the following disclaimer.
 2. Redistributions in binary form must reproduce the above copyright
    notice, this list of conditions and the following disclaimer in
    the documentation and/or other materials provided with the
    distribution.
 3. The name of the author may not be used to endorse or promote
    products derived from this software without specific prior
    written permission.

THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享
评论 抢沙发

请登录后发表评论

    暂无评论内容