auto_mount.sh — 跨发行版数据盘自动挂载脚本

一、简介

auto_mount.sh 用于在 Linux 服务器上自动完成数据盘的分区、格式化、挂载、写入开机自动挂载全流程。它能自动识别新加的空闲数据盘并跳过系统盘,也支持手动指定单块盘,适合云主机扩容、批量初始化、新机器交付等场景。

兼容系统

发行版 版本 包管理器 默认文件系统
CentOS 7 yum xfs
CentOS 8 dnf xfs
openEuler 22.03 / 24.04 dnf xfs
Ubuntu 22.04 / 24.04 apt ext4*

*Ubuntu 系统盘多用 ext4,但脚本默认仍以 xfs 格式化数据盘,可用 -t ext4 切换。两种文件系统在上述所有系统都受支持。

核心特性

  • 自动发现空闲盘:识别未挂载、无分区、不在 fstab 中的整块盘,自动排除系统盘。
  • 跨发行版依赖自愈:缺 parted/xfsprogs/e2fsprogs 时按系统自动安装。
  • 设备命名兼容:同时正确处理 /dev/sdb1/dev/nvme0n1p1
  • 按 UUID 写 fstab:避免设备名漂移导致开机挂载失败;带 nofail,盘异常也不会卡住启动。
  • 安全优先:默认交互确认、操作前占用检查、改 fstab 前自动备份、mount -a 校验、系统盘保护。
  • dry-run 预演-n 只打印将要执行的动作,不动磁盘。
  • 全程日志:写入 /var/log/auto_mount_disk.log

二、使用说明

前提

  • root(或 sudo)运行。
  • 目标盘上没有需要保留的数据——分区+格式化会清空整块盘。

命令行选项

选项 说明
-d <dev> 指定磁盘设备,如 /dev/sdb。省略则自动发现所有空闲数据盘。
-m <path> 挂载点,如 /data。配合 -d 使用。
-p <prefix> 自动发现模式下的挂载点前缀,自动编号。默认 /data/data1/data2
-t <fstype> 文件系统类型:xfs(默认)或 ext4
-r remount 模式,把已挂载的盘改挂到新目录、保留数据(不格式化)。需配合 -d-m
-f 强制模式,跳过交互确认。危险,确保盘内无数据。
-n dry-run,只打印将要执行的操作,不做任何实际更改。
-h 显示帮助。

常用示例

# 1. 预演:看自动发现会选中哪些盘、打算如何处理(不动磁盘)
sudo ./scripts/auto_mount.sh -n

# 2. 自动发现并挂载所有空闲盘 -> /data1 /data2 ...(交互确认)
sudo ./scripts/auto_mount.sh

# 3. 指定单盘 + 挂载点
sudo ./scripts/auto_mount.sh -d /dev/sdb -m /data

# 4. NVMe 盘 + ext4 + 免确认
sudo ./scripts/auto_mount.sh -d /dev/nvme0n1 -m /data -t ext4 -f

# 5. 自动发现,挂载点前缀改为 /mnt/disk -> /mnt/disk1 /mnt/disk2
sudo ./scripts/auto_mount.sh -p /mnt/disk

# 6. remount: 把已挂载在 /A 的 /dev/sdb 改挂到 /B, 保留数据(先 -n 预演)
sudo ./scripts/auto_mount.sh -r -d /dev/sdb -m /B -n
sudo ./scripts/auto_mount.sh -r -d /dev/sdb -m /B

切换挂载点(remount)

如果一块盘已经挂载并有数据,想把它从 A 目录改挂到 B 目录而不丢数据,用 -r 模式:

sudo ./scripts/auto_mount.sh -r -d /dev/sdb -m /newpath

它做的是非破坏性搬迁,不会 wipefs/格式化:

  1. 定位该盘当前承载数据的分区及其旧挂载点(自动处理 /dev/sdb 下的 sdb1,或整盘直挂的情况)。
  2. umount 旧挂载点(若被进程占用会报错并中止,提示用 lsof/fuser 排查)。
  3. 创建新挂载点,按 UUID 更新 /etc/fstab(保留盘原有的文件系统类型),改前自动备份。
  4. mount -a 重新挂载并校验,旧目录卸载后保留为空目录。

支持 -n 预演。若新旧挂载点相同则直接跳过。

推荐操作流程

  1. -n 预演,确认选盘和挂载点正确。
  2. 去掉 -n,交互模式执行(需输入大写 YES 确认)。
  3. df -hTlsblk 验证挂载结果。
  4. 可选:reboot 后确认开机自动挂载生效。

退出码

  • 0:成功,或无空闲盘可处理,或用户取消。
  • 1:出错(非 root、不支持的文件系统、设备无效、占用冲突、挂载失败等)。

三、代码说明

执行流程图

脚本有两条主分支:格式化挂载(破坏性)和 remount(非破坏性)。

1. 格式化挂载模式(默认,-r 未设置)

开始

解析参数 -d -m -p -t -f -n

是 root?

die: 请用 root 运行

文件系统
合法?

die: 只支持 xfs/ext4

安装依赖
parted/xfsprogs/e2fsprogs

指定了 -d?

系统盘检查
get_system_disks 回溯

是系统盘?

die: 拒绝操作系统盘

disks = TARGET_DEV

自动发现
discover_data_disks

发现空闲盘?

显示当前布局

退出 0

展示待处理盘和容量

dry-run?

逐盘循环

用户输入
YES?

die: 操作已取消

check_disk_free
LVM/swap/挂载占用检查

有占用?

die: 已被占用

wipefs -a 清签名

parted 建 GPT 分区表
单一主分区 0%-100%

partprobe + udevadm settle

循环探测分区设备
最多 10 秒

分区生成?

die: 分区未生成

mkfs.xfs -f 或
mkfs.ext4 -F

blkid 取 UUID

mkdir -p 挂载点

备份 /etc/fstab

去重并追加
UUID=... nofail

mount -a 校验

挂载成功?

die: mount 失败

还有盘?

显示最终 lsblk 布局

退出 0

2. remount 模式(-r 设置)

开始 -r 模式

-d 和 -m
齐全?

die: 需 -d 和 -m

设备存在?

die: 不是有效块设备

解析承载数据的分区
整盘直挂 或 子分区

找到已挂载
分区?

die: 当前没有已挂载分区

findmnt 取旧挂载点
blkid 取 UUID 和文件系统类型

新旧挂载点
相同?

log: 无需改动

退出 0

dry-run?

打印将执行的动作

退出 0

umount 旧挂载点

卸载成功?

die: 进程占用
用 lsof/fuser 排查

mkdir -p 新挂载点

备份 /etc/fstab

删 UUID 旧行
写新行 保留真实 fstype

mount -a 校验

挂载成功?

die: mount 失败

显示最终 lsblk 布局

退出 0


模块详解

脚本为单文件 Bash,开启 set -euo pipefail(遇错即停、未定义变量报错、管道错误传播)。主要模块如下:

1. 参数与前置检查

getopts 解析选项;检查 root 权限、文件系统类型合法性;初始化日志文件(不可写时降级到 /dev/null)。

2. 日志与 run 包装器

log/warn/err/die 同时输出到终端(带颜色)和日志文件。run 是 dry-run 的核心——DRY_RUN=1 时只打印命令字符串,否则真正执行。所有破坏性命令都经 run 调用。

3. 依赖自愈 ensure_deps / detect_pkg_mgr

dnf > yum > apt 优先级探测包管理器,对缺失命令映射到对应包名并安装。dry-run 下只提示不安装。

4. 系统盘识别 get_system_disks

通过 findmnt 找到 //boot/boot/efi 的来源设备,再用 lsblk -no pkname 回溯到底层物理盘(正确处理 LVM、分区等层级),得到需要排除的系统盘列表。

5. 空闲盘发现 discover_data_disks

遍历 lsblk 所有 TYPE=disk 的盘,按以下条件逐一排除:是系统盘、整盘已挂载、已有子设备(分区/LVM/RAID)、已出现在 fstab 中。剩余即为可用空闲数据盘。

6. 占用检查 check_disk_free

处理前再次确认目标盘未被挂载、未被 LVM 占用、未用作 swap,任一命中即终止。

7. 单盘处理 process_disk

核心流程:wipefs 清签名 → parted 建 GPT 分区表和单一主分区 → partprobe+udevadm settle 刷新 → 循环探测分区设备(最多 10 次,应对内核异步生成延迟)→ mkfs 格式化 → blkid 取 UUID → 备份并去重写入 /etc/fstabmount -a 校验 → mountpoint 确认挂载成功。part_name 负责 sdb1nvme0n1p1 两种命名。

8. 主流程 main

确定目标盘集合(指定盘走系统盘保护分支,否则自动发现)→ 展示待处理盘和容量 → 交互确认(dry-run 跳过)→ 逐盘处理并按需自动编号挂载点 → 输出最终 lsblk 布局。-r 时则提前进入 remount 分支,要求 -d/-m 齐全。

9. 切换挂载点 remount_disk

非破坏性流程:解析盘当前承载数据的分区和旧挂载点 → 取 UUID 和真实文件系统类型 → umount 旧点 → 建新点 → 备份并按 UUID 更新 fstabmount -a 校验。全程不碰 wipefs/mkfs,数据原样保留;新旧挂载点相同时直接跳过。

关键安全设计

  • UUID + nofail:设备名变化不影响挂载;盘缺失也不阻塞开机。
  • fstab 备份:每次写入前 cp 一份带时间戳的备份,便于回滚。
  • 写入去重:按挂载点和 UUID 清理旧记录,重复运行不会产生重复条目(幂等)。
  • 系统盘保护:指定盘时,将其底层物理盘与 get_system_disks 回溯出的系统盘列表比对,命中(承载 //boot)即拒绝操作,无论是否强制模式。

四、后续提升方向

按价值和实现成本,可分阶段增强:

易做、收益高

  • --label 文件系统卷标:格式化时打标签,便于识别用途。
  • 可配置挂载选项:当前固定 defaults,nofail,可增加 -o 自定义(如 noatimediscard)。
  • 日志轮转:日志文件无上限,可接入 logrotate 或自带按大小截断。

中等成本

  • LVM 模式:可选用 PV/VG/LV 替代裸分区,支持后续在线扩容、跨盘聚合。
  • 多分区 / 容量比例切分:支持把一块盘切成多个分区按比例分配。
  • 配置文件驱动:支持从 YAML/INI 读取"设备→挂载点→文件系统"映射,实现声明式批量初始化。
  • 幂等性增强:检测到盘已按预期格式化挂载时直接跳过(当前对已用盘是排除而非"对账")。

较高成本 / 工程化

  • btrfs / zfs 等更多文件系统支持
  • 软 RAID(mdadm)编排:多盘做 RAID0/1/5 后再格式化挂载。
  • 加密盘(LUKS)支持:格式化前先建加密层。
  • 可观测性:执行结果上报(退出码、挂载清单)到监控系统,或输出 JSON 供自动化流水线消费。
  • 测试覆盖:用 loop device(losetup + 稀疏文件)在 CI 里模拟磁盘,对各发行版做端到端验证。

五、注意事项

  • 分区+格式化不可逆,务必先用 -n 预演确认选盘。
  • 自动发现依赖"无分区表"判断空闲盘;若新盘上已有残留分区表,会被视为"已使用"而跳过,此时需手动 -d 指定。
  • 强制模式(-f)会跳过所有确认,仅建议在确知无数据的自动化场景使用。
  • 改动记录在 /var/log/auto_mount_disk.log/etc/fstab 的备份为 /etc/fstab.bak.<时间戳>

六、完整脚本

scripts/auto_mount.sh 一致。保存后 chmod +x auto_mount.sh 即可运行。

#!/usr/bin/env bash
#
# auto_mount.sh - 自动识别并挂载数据盘 (跨发行版)
#
# 兼容: CentOS 7 / CentOS 8 / openEuler 22.03 / openEuler 24.04 / Ubuntu 22.04 / Ubuntu 24.04
#
# 功能:
#   1. 指定单盘, 或自动发现未挂载/未分区/不在 fstab 中的空闲数据盘 (自动跳过系统盘)
#   2. wipefs 清残留签名 -> 创建 GPT 分区表 -> 单一主分区
#   3. 格式化为指定文件系统 (默认 xfs, 可选 ext4)
#   4. 按 UUID 写入 /etc/fstab (带 nofail), 备份原 fstab, mount -a 校验
#   5. LVM / swap / 已挂载 占用检查, 操作全程写日志
#   6. remount 模式: 把已挂载的盘原样改挂到新目录, 保留数据 (不格式化)
#
# 用法:
#   ./auto_mount.sh                      # 交互, 自动发现所有空闲数据盘
#   ./auto_mount.sh -d /dev/sdb -m /data # 指定单盘和挂载点
#   ./auto_mount.sh -d /dev/nvme0n1 -t ext4 -f   # NVMe + ext4 + 强制(免确认)
#   ./auto_mount.sh -p /mnt/disk         # 自动发现, 多盘自动编号 disk1/disk2..
#   ./auto_mount.sh -r -d /dev/sdb -m /B # 把 /dev/sdb 从原挂载点改挂到 /B (保留数据)
#
# 选项:
#   -d <dev>     指定磁盘设备 (省略则自动发现所有空闲数据盘)
#   -m <path>    挂载点 (配合 -d 使用)
#   -p <prefix>  自动发现模式的挂载点前缀, 自动编号 (默认 /data -> /data1 /data2)
#   -t <fstype>  文件系统类型: xfs(默认) 或 ext4
#   -r           remount 模式, 把已挂载的盘改挂到新目录, 保留数据 (需配合 -d -m)
#   -f           强制模式, 跳过交互确认 (危险, 确保盘内无数据)
#   -n           dry-run, 只打印将要执行的操作, 不做任何实际更改
#   -h           显示帮助

set -euo pipefail

# ---------- 默认参数 ----------
FSTYPE="xfs"
MOUNT_PREFIX="/data"
TARGET_DEV=""
TARGET_MOUNT=""
FORCE_MODE=0
DRY_RUN=0
REMOUNT_MODE=0
LOG_FILE="/var/log/auto_mount_disk.log"

# ---------- 颜色 / 日志 ----------
RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[0;33m'; NC='\033[0m'
log()  { echo -e "${GREEN}[INFO]${NC}  $*"  | tee -a "$LOG_FILE"; }
warn() { echo -e "${YELLOW}[WARN]${NC}  $*" | tee -a "$LOG_FILE"; }
err()  { echo -e "${RED}[ERROR]${NC} $*" | tee -a "$LOG_FILE" >&2; }
die()  { err "$*"; exit 1; }
# dry-run 下打印命令而不执行; 否则执行并记录
run() {
  if [ "$DRY_RUN" -eq 1 ]; then
    echo -e "${YELLOW}[DRY-RUN]${NC} $*" | tee -a "$LOG_FILE"
  else
    "$@"
  fi
}

usage() { sed -n '2,45p' "$0" | sed 's/^#//; s/^ //'; exit 0; }

# ---------- 解析参数 ----------
while getopts "d:m:p:t:rfnh" opt; do
  case "$opt" in
    d) TARGET_DEV="$OPTARG" ;;
    m) TARGET_MOUNT="${OPTARG%/}" ;;
    p) MOUNT_PREFIX="${OPTARG%/}" ;;
    t) FSTYPE="$OPTARG" ;;
    r) REMOUNT_MODE=1 ;;
    f) FORCE_MODE=1 ;;
    n) DRY_RUN=1 ;;
    h) usage ;;
    *) usage ;;
  esac
done

# ---------- 前置检查 ----------
[ "$(id -u)" -eq 0 ] || die "请以 root 运行 (sudo $0 ...)"
touch "$LOG_FILE" 2>/dev/null || LOG_FILE="/dev/null"
echo "===== $(date '+%F %T') 开始 =====" >> "$LOG_FILE"

case "$FSTYPE" in
  xfs|ext4) ;;
  *) die "不支持的文件系统类型: $FSTYPE (仅支持 xfs / ext4)" ;;
esac

# ---------- 依赖处理 ----------
# detect_pkg_mgr - 探测当前系统的包管理器
# 输出: "dnf" / "yum" / "apt" / 空字符串(无法识别)
detect_pkg_mgr() {
  if command -v dnf >/dev/null 2>&1; then echo "dnf"       # CentOS 8 / openEuler
  elif command -v yum >/dev/null 2>&1; then echo "yum"     # CentOS 7
  elif command -v apt-get >/dev/null 2>&1; then echo "apt" # Ubuntu
  else echo ""; fi
}

# ensure_deps - 检查并自动安装所需依赖包
# 根据所选文件系统 ($FSTYPE) 决定需要 xfsprogs 或 e2fsprogs
# dry-run 模式下只打印不安装
ensure_deps() {
  local need=()
  # 分区工具
  command -v parted    >/dev/null 2>&1 || need+=("parted")
  command -v partprobe >/dev/null 2>&1 || need+=("parted")
  # 设备管理工具
  command -v wipefs    >/dev/null 2>&1 || need+=("util-linux")
  command -v lsblk     >/dev/null 2>&1 || need+=("util-linux")
  # 文件系统工具(按选定类型)
  if [ "$FSTYPE" = "xfs" ]; then
    command -v mkfs.xfs  >/dev/null 2>&1 || need+=("xfsprogs")
  else
    command -v mkfs.ext4 >/dev/null 2>&1 || need+=("e2fsprogs")
  fi

  # 去重并检查是否有缺失
  local uniq
  uniq=$(printf "%s\n" "${need[@]:-}" | sort -u | grep -v '^$' || true)
  [ -z "$uniq" ] && return 0  # 全部已安装

  local pm; pm=$(detect_pkg_mgr)
  if [ "$DRY_RUN" -eq 1 ]; then
    warn "[DRY-RUN] 缺少依赖, 将通过 $pm 安装: $(echo $uniq | tr '\n' ' ')"
    return 0
  fi
  # 实际安装
  warn "缺少依赖, 尝试安装: $(echo $uniq | tr '\n' ' ')"
  case "$pm" in
    dnf) dnf install -y $uniq ;;
    yum) yum install -y $uniq ;;
    apt) apt-get update -y && apt-get install -y $uniq ;;
    *)   die "无法识别包管理器, 请手动安装: $uniq" ;;
  esac
}

# ---------- 系统盘识别 (排除根/boot 所在物理盘) ----------
# get_system_disks - 回溯 /、/boot、/boot/efi 所在的底层物理盘
# 输出: 物理盘名列表(每行一个, 如 sda、nvme0n1), 用于自动发现时排除
# 正确处理 LVM、分区等多层级结构
get_system_disks() {
  local sysdisks="" src pk base mp
  # 遍历根和启动分区
  while read -r mp; do
    src=$(findmnt -no SOURCE --target "$mp" 2>/dev/null || true)  # 设备源
    [ -z "$src" ] && continue
    # lsblk pkname 回溯到父级物理盘
    pk=$(lsblk -no pkname "$src" 2>/dev/null | head -n1 || true)
    if [ -n "$pk" ]; then
      sysdisks="$sysdisks $pk"  # 有父级盘, 记录父级
    else
      base=$(basename "$src"); sysdisks="$sysdisks $base"  # 已是顶层, 直接记录
    fi
  done < <(findmnt -rno TARGET | grep -E '^(/|/boot|/boot/efi)$' || true)
  echo "$sysdisks" | tr ' ' '\n' | sort -u | grep -v '^$' || true
}

# ---------- 自动发现空闲数据盘 ----------
# discover_data_disks - 列出所有可用于初始化的空闲数据盘
# 输出: 设备路径列表(每行一个, 如 /dev/sdb), 已排除:
#   - 系统盘(承载 / /boot /boot/efi)
#   - 整盘已挂载
#   - 已有分区/LVM/RAID 子设备
#   - 已出现在 /etc/fstab 中
discover_data_disks() {
  local sysdisks; sysdisks=$(get_system_disks)  # 先取系统盘清单
  local found=() name type mountpoint children
  while read -r name type mountpoint; do
    [ "$type" = "disk" ] || continue                  # 只看整块盘(TYPE=disk)
    echo "$sysdisks" | grep -qx "$name" && continue   # 排除系统盘
    [ -n "$mountpoint" ] && continue                  # 排除整盘已挂载
    # 统计子设备数(分区/LVM/RAID), 有则说明已使用
    children=$(lsblk -rno NAME "/dev/$name" | tail -n +2 | wc -l)
    [ "$children" -ne 0 ] && continue
    # 检查 fstab 是否提及该设备(粗略匹配设备名+数字/p 或空白)
    grep -qE "/dev/$name([0-9p]|[[:space:]])" /etc/fstab 2>/dev/null && continue
    found+=("/dev/$name")  # 通过所有筛选, 记录为可用空闲盘
  done < <(lsblk -rno NAME,TYPE,MOUNTPOINT)
  printf "%s\n" "${found[@]:-}"
}

# ---------- 占用检查 ----------
# check_disk_free - 检查目标盘是否被挂载/LVM/swap 占用
# 参数: $1 设备路径(如 /dev/sdb)
# 有占用则 die 终止, 无占用则静默返回
check_disk_free() {
  local dev="$1"
  local mp
  # 检查盘或其子设备是否已挂载
  mp=$(lsblk -no MOUNTPOINT "$dev" 2>/dev/null | grep -v '^$' | head -1 || true)
  [ -n "$mp" ] && die "$dev 已有分区挂载在 $mp, 请先卸载"
  # 检查 LVM 占用(PV)
  if command -v lvs >/dev/null 2>&1 && lvs 2>/dev/null | grep -q "$(basename "$dev")"; then
    die "$dev 被 LVM 使用, 请先处理"
  fi
  # 检查 swap 占用
  if swapon --show 2>/dev/null | grep -q "$dev"; then
    die "$dev 被用作 swap, 请先 swapoff"
  fi
}

# ---------- 分区设备名 (/dev/sdb -> sdb1, /dev/nvme0n1 -> nvme0n1p1) ----------
# part_name - 根据磁盘名推算第一分区的设备名
# 参数: $1 磁盘设备路径
# 输出: 分区设备路径(NVMe 类设备后缀 p1, 其他设备后缀 1)
part_name() {
  local dev="$1"
  if [[ "$dev" =~ [0-9]$ ]]; then echo "${dev}p1"; else echo "${dev}1"; fi
}

# ---------- 处理单盘(格式化并挂载) ----------
# process_disk - 对空闲数据盘执行: wipefs -> 分区 -> 格式化 -> 挂载 -> 写 fstab
# 参数: $1 磁盘设备, $2 挂载点
# 破坏性操作: 清空整块盘数据
# dry-run 模式下只打印不执行
process_disk() {
  local dev="$1" mount="$2"
  [ -b "$dev" ] || die "$dev 不是有效的块设备"
  check_disk_free "$dev"  # 占用检查

  log "处理磁盘 $dev -> 挂载点 $mount (文件系统 $FSTYPE)"

  # === 1. 分区 ===
  run wipefs -a "$dev"                            # 清残留签名(旧文件系统/分区表)
  run parted -s "$dev" mklabel gpt                # 创建 GPT 分区表
  run parted -s "$dev" mkpart primary 0% 100%     # 单一主分区占满整盘
  run partprobe "$dev"                            # 通知内核重新读取分区表
  [ "$DRY_RUN" -eq 0 ] && udevadm settle 2>/dev/null || true  # 等待 udev 规则完成

  # === 2. 循环探测分区设备(应对内核异步生成延迟) ===
  local part; part=$(part_name "$dev")
  if [ "$DRY_RUN" -eq 1 ]; then
    log "(dry-run) 预期分区设备: $part"
    log "(dry-run) 将格式化 $part$FSTYPE, 写入 /etc/fstab(UUID), 挂载到 $mount"
    return 0
  fi
  local retry=0
  while [ ! -b "$part" ] && [ "$retry" -lt 10 ]; do
    sleep 1; retry=$((retry+1)); partprobe "$dev" 2>/dev/null || true
  done
  [ -b "$part" ] || die "分区 $part 未生成, 请检查磁盘"
  log "分区创建完成: $part"

  # === 3. 格式化 ===
  log "格式化 $part$FSTYPE ..."
  if [ "$FSTYPE" = "xfs" ]; then mkfs.xfs -f "$part"; else mkfs.ext4 -F "$part"; fi

  # === 4. 获取 UUID(用于 fstab 写入, 避免设备名漂移) ===
  local uuid; uuid=$(blkid -s UUID -o value "$part")
  [ -n "$uuid" ] || die "无法获取 $part 的 UUID"

  # === 5. 创建挂载点(非空则告警, 挂载后原内容被遮蔽) ===
  if [ -d "$mount" ] && [ -n "$(ls -A "$mount" 2>/dev/null)" ]; then
    warn "挂载点 $mount 非空, 挂载后原内容将被遮蔽"
  fi
  mkdir -p "$mount"

  # === 6. 写入 fstab(备份 + 按 UUID 和挂载点去重 + 追加新行, 带 nofail 防开机卡死) ===
  cp -a /etc/fstab "/etc/fstab.bak.$(date +%Y%m%d%H%M%S)"
  sed -i "\#[[:space:]]${mount}[[:space:]]#d" /etc/fstab  # 删同挂载点旧行
  sed -i "\#^UUID=${uuid}#d" /etc/fstab                   # 删同 UUID 旧行
  echo "UUID=${uuid}  ${mount}  ${FSTYPE}  defaults,nofail  0  0" >> /etc/fstab
  log "已写入 /etc/fstab: UUID=${uuid} -> ${mount}"

  # === 7. 挂载并校验 ===
  if ! mount -a 2>&1 | tee -a "$LOG_FILE"; then
    die "mount -a 执行失败, 请检查 /etc/fstab 语法"
  fi
  if mountpoint -q "$mount"; then
    log "挂载成功:"
    df -hT "$mount" | sed 's/^/    /' | tee -a "$LOG_FILE"
  else
    die "$mount 挂载失败, 请手动排查"
  fi
}

confirm_yes() {
  [ "$FORCE_MODE" -eq 1 ] && return 0
  local ans
  read -r -p "$1 请输入 YES 确认: " ans
  [ "$ans" = "YES" ]
}

# ---------- remount: 把已挂载的盘改挂到新目录, 保留数据 (不格式化) ----------
# remount_disk - 改变已挂载盘的挂载点, 数据原样保留
# 参数: $1 磁盘设备(如 /dev/sdb, 内部自动定位其承载数据的分区), $2 新挂载点
# 非破坏性: 不做 wipefs/mkfs, 只卸载/改 fstab/重新挂载
# 失败场景: 盘未挂载、卸载失败(进程占用)、新旧挂载点相同(直接跳过)
remount_disk() {
  local dev="$1" newmount="$2"
  [ -b "$dev" ] || die "$dev 不是有效的块设备"

  # === 1. 找到承载数据的实际分区及其当前挂载点 ===
  local part oldmount uuid
  # 先判断整盘是否直接被挂载(无分区表场景)
  if mountpoint -q "$dev" 2>/dev/null || findmnt -rno TARGET -S "$dev" >/dev/null 2>&1; then
    part="$dev"   # 整盘直挂
  else
    # 取该盘下已挂载的第一个分区(NR>1 跳过父设备行)
    part=$(lsblk -rno NAME,MOUNTPOINT "$dev" | awk 'NR>1 && $2!="" {print $1; exit}')
    [ -n "$part" ] && part="/dev/$part"
  fi
  [ -n "$part" ] || die "$dev 当前没有已挂载的分区, 无需 remount (新盘请用普通挂载模式)"

  oldmount=$(findmnt -nro TARGET "$part" | head -1)
  [ -n "$oldmount" ] || die "无法确定 $part 的当前挂载点"
  uuid=$(blkid -s UUID -o value "$part")
  [ -n "$uuid" ] || die "无法获取 $part 的 UUID"
  # 保留盘的真实文件系统类型(而非 $FSTYPE 默认值), 写 fstab 时用
  local realfs; realfs=$(blkid -s TYPE -o value "$part")
  [ -n "$realfs" ] || realfs="$FSTYPE"

  # === 2. 新旧挂载点相同则跳过 ===
  if [ "$oldmount" = "$newmount" ]; then
    log "$part 已挂载在 $newmount, 无需改动"
    return 0
  fi

  log "remount: $part (UUID=$uuid) 从 $oldmount 改挂到 $newmount, 数据保留"

  if [ "$DRY_RUN" -eq 1 ]; then
    log "(dry-run) umount $oldmount"
    log "(dry-run) mkdir -p $newmount"
    log "(dry-run) 更新 /etc/fstab: 删除 UUID=$uuid 旧行, 写入 -> $newmount"
    log "(dry-run) mount -a 并校验"
    return 0
  fi

  # === 3. 卸载旧挂载点(失败则终止, 提示用 lsof/fuser 排查占用进程) ===
  umount "$oldmount" || die "卸载 $oldmount 失败 (可能有进程占用, 可用 lsof/fuser 排查)"

  # === 4. 创建新挂载点(非空则告警) ===
  if [ -d "$newmount" ] && [ -n "$(ls -A "$newmount" 2>/dev/null)" ]; then
    warn "新挂载点 $newmount 非空, 挂载后原内容将被遮蔽"
  fi
  mkdir -p "$newmount"

  # === 5. 更新 fstab(备份 + 按 UUID 和挂载点去重 + 写新行, 保留真实文件系统类型) ===
  cp -a /etc/fstab "/etc/fstab.bak.$(date +%Y%m%d%H%M%S)"
  sed -i "\#^UUID=${uuid}#d" /etc/fstab                    # 删旧 UUID 行
  sed -i "\#[[:space:]]${newmount}[[:space:]]#d" /etc/fstab  # 删新挂载点旧行(防重复)
  echo "UUID=${uuid}  ${newmount}  ${realfs}  defaults,nofail  0  0" >> /etc/fstab
  log "已更新 /etc/fstab: UUID=${uuid} (${realfs}) -> ${newmount}"

  # === 6. 挂载并校验 ===
  if ! mount -a 2>&1 | tee -a "$LOG_FILE"; then
    die "mount -a 执行失败, 请检查 /etc/fstab 语法 (备份可回滚)"
  fi
  if mountpoint -q "$newmount"; then
    log "改挂成功:"
    df -hT "$newmount" | sed 's/^/    /' | tee -a "$LOG_FILE"
    log "旧挂载点 $oldmount 已卸载 (空目录保留)"
  else
    die "$newmount 挂载失败, 请手动排查 (/etc/fstab 已备份)"
  fi
}

# ---------- 主流程 ----------
# main - 脚本入口, 分支到 remount 或 格式化挂载 两种模式
main() {
  # === remount 模式分支: 改挂已有盘到新目录, 保留数据 ===
  if [ "$REMOUNT_MODE" -eq 1 ]; then
    [ -n "$TARGET_DEV" ]   || die "remount 模式需用 -d 指定磁盘设备"
    [ -n "$TARGET_MOUNT" ] || die "remount 模式需用 -m 指定新挂载点"
    remount_disk "$TARGET_DEV" "$TARGET_MOUNT"
    echo
    log "完成。最终磁盘布局:"
    lsblk -o NAME,SIZE,TYPE,FSTYPE,MOUNTPOINT
    return 0
  fi

  # === 格式化挂载模式 ===
  ensure_deps  # 安装依赖

  # --- 确定目标盘集合(指定单盘 或 自动发现) ---
  local -a disks=()
  if [ -n "$TARGET_DEV" ]; then
    disks=("$TARGET_DEV")
    # 系统盘保护: 用真实回溯结果判定, 拒绝操作系统盘
    local devbase sysdisks
    devbase=$(basename "$TARGET_DEV")
    sysdisks=$(get_system_disks)
    if echo "$sysdisks" | grep -qx "$devbase"; then
      die "$TARGET_DEV 是系统盘 (承载 / 或 /boot), 拒绝操作以防系统损坏"
    fi
  else
    # 自动发现空闲数据盘
    log "自动发现空闲数据盘 (跳过系统盘和已使用的盘)..."
    mapfile -t disks < <(discover_data_disks)
    if [ "${#disks[@]}" -eq 0 ] || [ -z "${disks[0]:-}" ]; then
      warn "没有发现可用的空闲数据盘。当前磁盘布局:"
      lsblk -o NAME,SIZE,TYPE,FSTYPE,MOUNTPOINT
      exit 0
    fi
  fi

  # --- 展示待处理盘, 交互确认(非 dry-run 且非强制模式) ---
  echo
  if [ "$DRY_RUN" -eq 1 ]; then
    warn "[DRY-RUN 模式] 以下磁盘将被【分区 + 格式化】(本次不会真正执行):"
  else
    warn "即将对以下磁盘执行【分区 + 格式化】, 盘上原有数据将全部丢失:"
  fi
  for d in "${disks[@]}"; do
    echo "    $d  (容量: $(lsblk -dno SIZE "$d" 2>/dev/null || echo '?'))"
  done
  echo
  if [ "$DRY_RUN" -eq 0 ]; then
    confirm_yes "确认继续?" || die "操作已取消, 未做任何更改。"
  fi

  # --- 逐盘处理: 分区、格式化、挂载、写 fstab ---
  local idx=1
  for d in "${disks[@]}"; do
    local mount
    if [ -n "$TARGET_MOUNT" ]; then
      mount="$TARGET_MOUNT"  # 指定单盘时用指定挂载点
    else
      mount="${MOUNT_PREFIX}${idx}"  # 多盘自动编号(data1 data2 ...)
    fi
    process_disk "$d" "$mount"
    idx=$((idx+1))
  done

  # --- 输出最终磁盘布局 ---
  echo
  log "全部完成。最终磁盘布局:"
  lsblk -o NAME,SIZE,TYPE,FSTYPE,MOUNTPOINT
}

main
Logo

openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构

更多推荐