把群晖(Synology)硬盘整块搬进飞牛 fnOS:保留数据 + 原生存储空间 + 迁移用户(全命令实操,含一键脚本)
目标:把一块从群晖拆下来的硬盘直接接到飞牛 fnOS 上 让它成为飞牛「原生存储空间」,并把群晖上的用户和家目录也一并迁过来。
适用:群晖 Basic(单盘)卷,即「分区 → mdadm → LVM → btrfs」这种典型布局。
本文所有命令均以 root 在飞牛 SSH 中执行,并给出预期输出;每个高危步骤都有明确警告。仅在btrfs文件系统上做过测试,其他文件系统可参照。
0. 风险总览
【总警告】 下面的操作会改动分区元数据、LVM 卷名、飞牛数据库和系统账号文件。
一步错就可能让飞牛认不出盘、甚至丢数据。请务必:
- 先备份群晖盘上真正重要的数据(接入本身不格式化,但任何底层操作都有风险)。
- 全程先只读挂载确认数据在,再做写操作。
- 改数据库 / 改账号前,先备份对应文件(本文每步都给了备份命令)。
- 看不懂某一步在干嘛,就先停下,别
Enter。
整个流程分十步:
| 步骤 |
做什么 |
危险级别 |
| 1 |
识别硬盘结构 |
只读,安全 |
| 2 |
组装 mdadm + 激活 LVM |
低 |
| 3 |
加载 crc32c(否则 btrfs 挂不上) |
低 |
| 4 |
只读试挂,确认数据在 |
安全 |
| 5 |
改 LVM 名 + 设默认子卷,符合飞牛约定 |
高 |
| 6 |
写飞牛 postgres,注册为存储空间 |
高 |
| 7 |
挂载群晖系统分区(只读) |
低 |
| 8 |
迁移群晖用户 + 密码 |
高 |
| 9 |
家目录 bind 挂载 |
中 |
| 10 |
开机持久化(模块 + systemd) |
中 |
1. 背景知识:两边的存储结构长什么样
群晖 Basic 卷(本文实例盘 /dev/sdd)通常是这样一层套一层的(这是系统安装在这块硬盘上的示例):
/dev/sdd
**─ sdd1 → mdadm 阵列(md127) → ext4 群晖"系统分区"(装 DSM、放 /etc/passwd 等)
**─ sdd2 → mdadm 阵列 → swap 交换分区
**─ sdd3 → mdadm 阵列(md125) → LVM(vg1/volume_1) → btrfs 你的"数据"都在这
也就是说,真正的数据在 btrfs 里,而 btrfs 又装在 LVM 逻辑卷上,LVM 又建在 mdadm 阵列上。要读到数据,必须把这三层一层层"点亮"。
飞牛 fnOS 的存储空间约定则是:
- LVM 卷组名叫
trim_<uuid>,逻辑卷名叫 0,即设备路径 /dev/mapper/trim_<uuid>-0;
- 每个存储空间在飞牛的 PostgreSQL(
trim 库)mount 表里有一行记录,飞牛靠它在 UI 显示、开机自动挂到 /volN。
所以"接入"的本质 = 把群晖那套 LVM 改名成飞牛的命名,再往飞牛数据库里补一行。
2. 准备工作
- 关机,把群晖硬盘装到飞牛主机上,开机。
- SSH 进飞牛,切 root:
sudo -i
- 建一个统一的备份目录,后面每步都会用:
mkdir -p /root/_stor_adopt_backup
3. 第一步:识别硬盘结构(只读,安全)
先看清楚盘接上没、结构对不对。这一步全是只读命令,随便跑。
lsblk -o NAME,SIZE,FSTYPE,LABEL,MOUNTPOINT,MODEL
blkid
cat /proc/mdstat
pvs; vgs; lvs
预期输出(节选,你的盘符/UUID 会不同):
NAME SIZE FSTYPE LABEL MOUNTPOINT
sdd 4T
**─sdd1 8G linux_raid_member
** **─md127 8G ext4
**─sdd2 2G linux_raid_member
** **─md126 2G swap
**─sdd3 3.99T linux_raid_member
**─md125 3.99T LVM2_member
**─vg1-volume_1 3.99T btrfs
# /proc/mdstat 里能看到 md125 / md126 / md127 三个阵列
# lvs 里能看到: volume_1 vg1 -wi-a----- 3.99t
看到 vg1/volume_1 是 btrfs、sdd1 对应的 md 是 ext4,就说明这是标准群晖 Basic 盘。记下:
- 数据卷:
vg1/volume_1(btrfs)
- 系统分区:
sdd1 对应的 md(本例 md127,ext4)
注意 如果 pvs/vgs/lvs 什么都没显示,多半是阵列/LVM 还没激活,继续第二步。
4. 第二步:组装 mdadm + 激活 LVM(低风险)
把三层结构点亮:
mdadm --assemble --scan
vgchange -ay
预期输出:
# mdadm 会打印类似:mdadm: /dev/md125 has been started with 1 drive.
# vgchange 打印: 1 logical volume(s) in volume group "vg1" now active
如果某个 md 显示成 auto-read-only,先让它可写(否则后面写不了):
mdadm --readwrite /dev/md125
此时 /dev/mapper/vg1-volume_1 应该已经存在:
ls -l /dev/mapper/vg1-volume_1
5. 第三步:加载 crc32c,否则 btrfs 挂不上(低风险,但很关键)
飞牛内核默认可能没加载 btrfs 校验要用的 crc32c。这一步不做,下一步挂载几乎必然失败,而且报错很误导人。
先直接尝试挂载(下一步会做),如果看到这种报错:
mount: /vol3: mount(2) system call failed: No such file or directory.
# 或 dmesg 里: BTRFS error: ... crc32c ...
别以为是盘坏了,其实是缺 crc32c 模块。加载它:
modprobe crc32c
grep -i crc32c /proc/crypto | head
预期输出:
name : crc32c
driver : crc32c-intel # 或 crc32c-generic
说明 有的系统 modprobe crc32c_generic 会报 Module ... not found,直接用 modprobe crc32c 即可,内核会自己挑一个实现(intel/generic/cryptoapi)。
开机持久化留到第十步统一做。
6. 第四步:只读试挂,先确认数据真的在(安全,强烈建议)
在做任何改名/写库之前,先只读挂一下,亲眼看到数据,心里才有底。
mkdir -p /mnt/_check
mount -o ro /dev/mapper/vg1-volume_1 /mnt/_check
ls -la /mnt/_check
btrfs subvolume list /mnt/_check
预期输出:
# 顶层能看到群晖的子卷目录,例如:
drwxrwxrwx @syno
# subvolume list 里能看到:
ID 256 gen 12345 top level 5 path @syno
群晖 Basic 卷的实际数据在 @syno 这个 btrfs 子卷里(共享文件夹 photo/homes/... 都在它下面)。记下这个 ID 256,第五步要用。
看完卸载:
umount /mnt/_check
注意 只读挂载不会改动任何东西。如果这里就看不到数据 / 挂不上,停下来排查,不要继续往下写操作。
7. 第五步:改成飞牛约定 —— LVM 改名 + 默认子卷(高危)
【危险】 这一步会 永久改掉 LVM 卷组名和逻辑卷名,并修改 btrfs 默认子卷。
改名后群晖那套命名就没了(反正盘已经不在群晖上,无所谓),但打错名字可能让盘挂不上。
执行前先备份 LVM 元数据:
vgcfgbackup -f /root/_stor_adopt_backup/vg1.bak vg1
7.1 生成飞牛风格的新名字并改名
# 先卸载(如果之前挂着)
umount /vol3 2>/dev/null
# 生成 trim_<uuid> 形式的卷组名
NEWUUID=$(uuidgen | tr 'A-Z-' 'a-z_')
VG="trim_${NEWUUID}"
echo "$VG" | tee /root/_stor_adopt_backup/newvg.txt
# 卷组改名 vg1 -> trim_xxx,逻辑卷改名 volume_1 -> 0
vgrename vg1 "$VG"
lvrename "$VG" volume_1 0
vgchange -ay "$VG"
DM="/dev/mapper/${VG}-0"
ls -l "$DM"
预期输出:
trim_b36db00e_a55c_48ca_95d6_5a127b1ae5b2
Volume group "vg1" successfully renamed to "trim_b36db00e_..."
Renamed "volume_1" to "0" in volume group "trim_b36db00e_..."
1 logical volume(s) ... now active
lrwxrwxrwx 1 root root 7 ... /dev/mapper/trim_b36db00e_...-0 -> ../dm-5
7.2 把默认子卷设成 @syno(让 /volN 直接是共享根)
不做这一步的话,/vol3 打开会多一层 @syno 目录。把默认子卷指向它,数据就直接落在 /vol3 根下。
mkdir -p /mnt/_adopt
mount "$DM" /mnt/_adopt
SYNOID=$(btrfs subvolume list /mnt/_adopt | awk '/path @syno$/{print $2}')
echo "@syno subvol id = $SYNOID" # 预期 256
btrfs subvolume set-default "$SYNOID" /mnt/_adopt
btrfs subvolume get-default /mnt/_adopt # 确认已指向 @syno
umount /mnt/_adopt
7.3 用飞牛风格选项挂到 /vol3
先确定一个空闲的 vol 号(飞牛已有 vol1/vol2 就用 vol3,以此类推):
mkdir -p /vol3
mount -o rw,noatime,trimacl,space_cache=v2 "$DM" /vol3 \
|| mount -o rw,noatime,space_cache=v2 "$DM" /vol3 # 老内核不支持 trimacl 时退化
mount | grep ' /vol3 '
ls -la /vol3 | head # 预期直接看到 photo/ homes/ 等共享
touch /vol3/.wtest && echo "写OK" && rm -f /vol3/.wtest
预期输出:
/dev/mapper/trim_..._0 on /vol3 type btrfs (rw,noatime,...)
drwxrwxrwx ... homes
drwxrwxrwx ... photo
写OK
看到 写OK 就说明读写正常了。
8. 第六步:注册为飞牛「原生存储空间」(高危,改数据库)
到这里 /vol3 已经能用,但只是临时挂载——重启就没了,飞牛 UI 里也看不到。要让它成为原生存储空间,得往飞牛的 PostgreSQL trim 库的 mount 表补一行。
【危险】 直接改飞牛数据库。写错表可能导致飞牛存储模块异常。
务必先备份 mount 表:
( cd /tmp && sudo -u postgres pg_dump -d trim -t mount --data-only --column-inserts ) \
> /root/_stor_adopt_backup/mount_before.sql
先看一下现有的存储空间,确认你要用的 id(本例用 3)没被占:
( cd /tmp && sudo -u postgres psql -d trim -P pager=off -c \
"SELECT id,uuid,pool_type FROM mount ORDER BY id;" )
小坑说明 用 sudo -u postgres 时,如果当前目录是 /root,会刷一堆
could not change directory to "/root": Permission denied——无害,是 postgres 用户进不了 /root 目录而已。
上面用 ( cd /tmp && ... ) 就能避免这个噪音。
插入新记录(id=3 对应 /vol3;uuid 就是 trim_<uuid>-0;pool_type=0 表示 btrfs 单盘):
VG=$(cat /root/_stor_adopt_backup/newvg.txt)
MUUID="${VG}-0"
( cd /tmp && sudo -u postgres psql -d trim -P pager=off -c \
"INSERT INTO mount (id,uuid,comment,sync_speed_min,sync_speed_max,stripe_cache_size,warn_frsize,grow,state,pool_type)
VALUES (3,'${MUUID}','',-1,-1,4096,10,'',0,0) ON CONFLICT (uuid) DO NOTHING;" )
# 确认写进去了
( cd /tmp && sudo -u postgres psql -d trim -P pager=off -c \
"SELECT id,uuid,pool_type FROM mount ORDER BY id;" )
预期输出:
INSERT 0 1
id | uuid | pool_type
----+---------------------------------------------+-----------
1 | trim_....-0 | 0
2 | trim_....-0 | 0
3 | trim_b36db00e_..._0 | 0
看到 id=3 那行,说明飞牛已经认它是原生存储空间了(刷新 UI 即可见)。
9. 第七步:挂载群晖系统分区 sdd1(只读,低风险)
群晖的 /etc/passwd、/etc/shadow 在系统分区(sdd1,ext4)里。要迁用户就得先把它挂出来。只读挂,绝对安全。
mkdir -p /vol3/system_mount
# 用 ext4 文件系统 UUID 挂,不依赖会变的 md 编号
SDD1UUID=$(blkid -o value -s UUID /dev/md127) # 换成你第一步记下的系统分区 md
echo "sdd1 ext4 UUID = $SDD1UUID"
mount -o ro UUID="$SDD1UUID" /vol3/system_mount
ls /vol3/system_mount | head
预期输出:
sdd1 ext4 UUID = 0105bea5-970e-459f-a48d-eb82819a33b5
# system_mount 下能看到群晖系统目录:
bin etc lib root usr var ...
10. 第八步:迁移群晖用户 + 密码(高危,改账号文件)
群晖普通用户的主组 GID 通常是 100(users)。我们把这些用户(排除 admin/guest)迁成飞牛系统用户,并把密码一起带过来。
【危险】 这一步会修改 /etc/passwd、/etc/shadow、/etc/group。
先备份:
ts=$(date +%Y%m%d_%H%M%S)
cp -a /etc/passwd /root/_stor_adopt_backup/passwd.$ts
cp -a /etc/shadow /root/_stor_adopt_backup/shadow.$ts
cp -a /etc/group /root/_stor_adopt_backup/group.$ts
先看一眼要迁哪些人(只读,不动):
awk -F: '$4==100 && $1!="admin" && $1!="guest"{printf " %-16s uid=%s\n",$1,$3}' \
/vol3/system_mount/etc/passwd
预期输出:
feidan uid=1026
liuxiaojue uid=1028
确认无误后执行迁移。下面这段会逐个处理,并且:
- UID 已被占用 → 跳过并记录(绝不覆盖已有账号);
- 用户名重名 → 跳过并记录;
- 否则创建系统用户(
home=/home/<用户>、shell=/usr/sbin/nologin),再从群晖 shadow 迁密码。
注意 一定用 /usr/sbin/nologin(不是 /sbin/nologin)——飞牛/Debian 系的路径是前者,写错会导致 shell 无效。
SM=/vol3/system_mount
while IFS=: read -r u p uid gid gecos home shell; do
[ "$gid" = 100 ] || continue
[ "$u" = admin ] || [ "$u" = guest ] && continue
# UID 冲突?跳过
if awk -F: -v id="$uid" '$3==id{f=1}END{exit !f}' /etc/passwd; then
echo "[跳过] $u uid=$uid 与已有用户冲突"; continue; fi
# 重名?跳过
if awk -F: -v n="$u" '$1==n{f=1}END{exit !f}' /etc/passwd; then
echo "[跳过] $u 用户名已存在"; continue; fi
# 建用户
useradd -u "$uid" -g 100 -M -d "/home/$u" -s /usr/sbin/nologin "$u" \
&& echo "[新增] $u uid=$uid"
mkdir -p "/home/$u"; chown "$uid":100 "/home/$u"; chmod 755 "/home/$u"
# 迁密码(直接搬 shadow 里的哈希)
hash=$(awk -F: -v n="$u" '$1==n{print $2}' "$SM/etc/shadow")
case "$hash" in
\$*) printf '%s:%s\n' "$u" "$hash" | chpasswd -e && echo " 密码已迁移" ;;
*) echo " [注意] $u 源无有效密码哈希,跳过密码" ;;
esac
done < "$SM/etc/passwd"
预期输出:
[新增] feidan uid=1026
密码已迁移
[新增] liuxiaojue uid=1028
密码已迁移
校验:
getent passwd feidan liuxiaojue
说明:飞牛用户 vs 系统用户
上面建的是 Linux 系统用户(能用来做 SMB/权限)。如果还想让它出现在飞牛 UI 的用户列表里,需要把主组设成飞牛的 Users 组:
FN_GID=$(getent group Users | cut -d: -f3) # 通常 1001
usermod -g "$FN_GID" feidan; usermod -aG 100 feidan # 主组 Users,附加组 users(100)
11. 第九步:家目录 bind 挂载到 /volN/(中风险)
飞牛按 /<存储空间>/<用户uid> 找用户的个人空间,而群晖的家目录在 /vol3/homes/<用户名>。用 bind 挂载把两者对上(别用软链接——飞牛 UI 不认软链接,当初踩过这个坑)。
# feidan uid=1026, liuxiaojue uid=1028(换成你的)
for p in 1026:feidan 1028:liuxiaojue; do
id=${p%%:*}; u=${p##*:}
[ -L /vol3/$id ] && rm -f /vol3/$id # 清掉可能存在的旧软链
mkdir -p /vol3/$id
[ -d /vol3/homes/$u ] || { echo "[注意] 源不存在 /vol3/homes/$u,跳过"; continue; }
mountpoint -q /vol3/$id || mount --bind /vol3/homes/$u /vol3/$id
done
mount | grep -E '/vol3/(1026|1028)'
ls -la /vol3/1026 | head # 应能看到 feidan 的家目录内容
预期输出:
/dev/mapper/trim_..._0 on /vol3/1026 type btrfs (...)
/dev/mapper/trim_..._0 on /vol3/1028 type btrfs (...)
# /vol3/1026 里是 feidan 的实际文件
为什么不用软链接:飞牛的文件管理/权限模块只把 bind 挂载当成"真实目录",软链接会显示成一个坏掉的链接文件,属主也不对。
收尾:收紧家目录权限(子项设为 drwx------)
绑定完成后,把每个用户家目录内部收紧为仅本人可访问:用户主文件夹本身保持不变(飞牛需要按它现有的属主/权限来识别),但其下所有子文件夹和子项目统一设为 drwx------(即 700,其他用户完全看不到)。
# 对每个用户的家目录:主目录不动,子项(含所有子文件夹/文件)递归设为 700
for u in feidan liuxiaojue; do
src=/vol3/homes/$u
[ -d "$src" ] || { echo "[注意] $src 不存在,跳过"; continue; }
find "$src" -mindepth 1 -exec chmod 700 {} + # -mindepth 1 => 排除主目录本身
echo "[ok] $u 家目录子项已设为 drwx------(主目录 $src 保持不变)"
done
校验(主目录权限不变,里面的子文件夹变成 drwx------):
ls -ld /vol3/homes/feidan # 主目录:维持原样
ls -la /vol3/homes/feidan | head # 子项:drwx------
预期输出:
drwxrwxrwx 1 feidan Users ... /vol3/homes/feidan # 主目录未变
drwx------ 1 feidan Users ... Photo
drwx------ 1 feidan Users ... Drive
...
说明 -mindepth 1 保证 find 只处理主目录里面的东西,不碰主目录本身。
drwx------ 是目录的 700 权限;子项目里若有文件,同样会被设成 700(rwx------),即仅属主可读写执行,其他用户无任何权限。
12. 第十步:开机持久化(中风险)
前面的挂载和绑定重启后都会消失(存储空间本身由飞牛 mount 表管,会自动挂;但 crc32c、system_mount、用户 bind 需要我们自己保证)。
12.1 crc32c 开机自动加载
echo crc32c > /etc/modules-load.d/crc32c.conf
cat /etc/modules-load.d/crc32c.conf
12.2 系统分区(system_mount)开机自动挂
cat > /etc/systemd/system/vol3-system_mount.service <<'EOF'
[Unit]
Description=Mount Synology system partition at /vol3/system_mount
After=multi-user.target
ConditionPathExists=/dev/disk/by-uuid/0105bea5-970e-459f-a48d-eb82819a33b5
[Service]
Type=oneshot
RemainAfterExit=yes
# 等飞牛把 /vol3 挂好(最多 ~120s)
ExecStartPre=/bin/sh -c 'for i in $(seq 1 60); do mountpoint -q /vol3 && exit 0; sleep 2; done; exit 1'
ExecStartPre=/bin/mkdir -p /vol3/system_mount
ExecStart=/bin/sh -c 'mountpoint -q /vol3/system_mount || mount -o ro UUID=0105bea5-970e-459f-a48d-eb82819a33b5 /vol3/system_mount'
ExecStop=/bin/umount /vol3/system_mount
[Install]
WantedBy=multi-user.target
EOF
systemctl daemon-reload
systemctl enable vol3-system_mount.service
12.3 用户家目录 bind 开机自动恢复
cat > /etc/systemd/system/vol3-user-binds.service <<'EOF'
[Unit]
Description=Bind-mount migrated Synology user homes to /vol3/{uid}
After=multi-user.target vol3-system_mount.service
ConditionPathExists=/vol3/homes
[Service]
Type=oneshot
RemainAfterExit=yes
ExecStartPre=/bin/sh -c 'for i in $(seq 1 60); do mountpoint -q /vol3 && exit 0; sleep 2; done; exit 1'
ExecStart=/bin/sh -c 'for p in 1026:feidan 1028:liuxiaojue; do id=${p%%:*}; u=${p##*:}; mkdir -p /vol3/$id; mountpoint -q /vol3/$id || mount --bind /vol3/homes/$u /vol3/$id; done'
ExecStop=/bin/sh -c 'umount /vol3/1026 2>/dev/null; umount /vol3/1028 2>/dev/null; true'
[Install]
WantedBy=multi-user.target
EOF
systemctl daemon-reload
systemctl enable vol3-user-binds.service
预期输出:两个 systemctl enable 都会打印
Created symlink /etc/systemd/system/multi-user.target.wants/....service → ....
13. 验证
SSH 进来检查:
mount | grep -E ' /vol3' # /vol3 + system_mount + 两个 bind 都在
lsmod | grep crc32c # crc32c 已加载
systemctl status vol3-system_mount vol3-user-binds --no-pager
ls /vol3 # 共享文件夹都在
全部正常,飞牛 UI 里也能看到这块存储空间和迁移过来的用户,就大功告成了。
【重要】 强烈建议重启一次,验证开机后一切自动到位。这是检验持久化配置是否正确的唯一办法。
reboot
14. 回滚方案(万一出问题)
| 想撤销 |
怎么做 |
| 数据库那行 |
sudo -u postgres psql -d trim -c "DELETE FROM mount WHERE id=3;" 或恢复 mount_before.sql 注意id=3可能不一样! |
| 账号文件 |
用 /root/_stor_adopt_backup/passwd.* shadow.* group.* 覆盖回去 |
| LVM 卷名 |
vgrename trim_xxx vg1; lvrename vg1 0 volume_1(反向改回) 都根据自己的数字来写 |
| systemd 服务 |
systemctl disable --now vol3-system_mount vol3-user-binds 后删对应 .service |
注意 数据本身(btrfs 里的文件)在整个过程中没被改写,回滚只影响"接入配置",不影响数据。
结语:整件事的关键点
- 数据在最里层的 btrfs,外面套着 LVM 和 mdadm,得一层层点亮才能读到。
- crc32c 是最容易踩的坑——不加载,btrfs 挂载会报一个跟真相毫无关系的错。
- 接入 = 改 LVM 命名 + 补一行数据库,飞牛并没有魔法,它只是照着
mount 表和 trim_<uuid> 约定去挂盘。
- 先只读、后写入;每步先备份——这是整个流程里唯一不能省的纪律。
- 用户目录用 bind 挂载别用软链接。
按这个顺序走,一块群晖老盘就能在飞牛上"原地复活",数据、账号、家目录一个不少。