凌晨三点,磁盘又双叒满了:一个脚本,把 LVM 扩容的坑全填了
运维人的深夜,往往毁于一条告警:
磁盘使用率超过 90%。
如果你也经历过这些画面,那这篇文章就是写给你的:
df -h一看,/只剩 3%,业务日志还在疯涨;- 云控制台点了"磁盘扩容 500G",登进系统一看,
lsblk容量变了,df纹丝不动; - 手忙脚乱敲完
lvextend,忘了resize2fs,空间白扩了; - 硬盘明明 4TiB,分区死活扩不过 2TiB,查半天才知道是 MBR 分区表;
- 新买一块盘要挂
/data,pvcreate、vgextend、lvcreate、mkfs、mount、改fstab……七八条命令,少一条就出问题; - 半夜被叫醒,手一抖,
lvextend打成了lvremove。
LVM 扩容不难,难的是:步骤多、场景杂、不能错、还要快。
今天分享一个通用脚本 lvm_auto_extend.sh v2.1,把上面这些痛点一次性打包解决。
一、先说说运维到底痛在哪
痛点 1:扩容是一条"五步流水线",漏一步就白干
一次标准的 LVM 扩容,至少要经过:
pvresize(刷物理卷)
→ vgextend(加盘到卷组)
→ lvextend(扩逻辑卷)
→ resize2fs / xfs_growfs(扩文件系统)
→ 检查 df 确认生效
问题在于:每一步都可能"看起来成功了",但空间就是不生效。
最典型的就是只做了 lvextend,忘了扩文件系统——lvs 显示 LV 变大了,df 还是老样子。
痛点 2:云盘在线扩容后,系统里根本"看不见"
你在控制台把 100G 的云盘扩到了 500G,进系统 lsblk 确实显示 500G。
但 df -h 还是 100G。为什么?
因为磁盘变大了 ≠ 分区变大了 ≠ PV 变大了 ≠ LV 变大了 ≠ 文件系统变大了。
这五层,得一层层往上"顶"。少顶一层,那 400G 就是"薛定谔的空间"。
痛点 3:2TiB 魔咒,静默浪费空间
MBR(msdos)分区表最大只能寻址 2TiB。
一块 4TiB 的盘,用 MBR 分区,超过 2TiB 的部分永远用不上,而且很多操作不报错,你只会疑惑:"我明明扩容了,怎么没多出来?"
痛点 4:不同文件系统,命令还不一样
- ext4:
resize2fs /dev/vg/lv - XFS:
xfs_growfs /mountpoint
一个是设备路径,一个是挂载点。半夜困得要死的时候,这种细节就是事故来源。
痛点 5:新盘接入,流程固定但繁琐
新盘挂 /data,标准动作:分区 → pvcreate → vgextend → lvcreate → mkfs → mount → 写 fstab → mount -a 验证。
每一步都是重复劳动,每一步都可能因为一个参数写错而返工。
二、这个脚本干了什么
一句话概括:
你只需要告诉它"扩哪个挂载点、扩多大",剩下的它自己找空间、自己扩、自己扩文件系统。
核心能力:
| 能力 | 说明 |
|---|---|
| 任意挂载点 | /、/home、/data、/opt 都行 |
| 自定义大小 | 100G、500M、2T、10T、50%、all |
| 自动找空间 | VG 剩余 → pvresize → 扩分区 → 加新盘,四级兜底 |
| 大于 2TiB | 自动检测 MBR 限制并明确提示,不让你白折腾 |
| 自动扩文件系统 | 自动识别 xfs / ext2/3/4,用对命令 |
| 自动挂载 | 挂载点没有卷时,自动建卷、格式化、挂载、写 fstab |
| 只读模式 | --list 只看不动,安全第一 |
| 无人值守 | -y 全自动,适合 CI / 自动化运维 |
三、四个真正解决问题的地方
亮点 1:空间来源"四级兜底",不用你猜
脚本会按优先级依次尝试:
a) VG 里还有剩余空间 → 直接 lvextend
b) 云盘已在线扩容 → 对已有 PV 执行 pvresize
c) PV 所在磁盘尾部有未分区空间 → growpart/parted 扩分区 + pvresize
d) 发现新的空闲整盘 → pvcreate + vgextend 后扩容
这意味着:你不用先判断"我的空间从哪来",脚本自己会找。
云盘扩容、加新盘、扩分区,三种场景一套逻辑。
亮点 2:把 2TiB 这个坑,摆到台面上
if [ "$label" = "msdos" ]; then
if [ "${total:-0}" -gt "$MSDOS_MAX_BYTES" ]; then
warn "$disk 使用 MBR(msdos) 分区表且容量超过 2TiB"
warn "2TiB 之后约 ${beyond}GiB 空间受 MBR 寻址上限限制, 无法通过扩分区使用"
warn "如需全部利用, 二选一: 1) 转 GPT 2) 清空后整盘作为 PV"
fi
fi
它不会假装成功,而是明确告诉你浪费了多少、为什么、怎么解。
这比"扩完了但没效果"强一百倍。
亮点 3:大小写法,贴近运维直觉
./linux_lvm.sh -m /data -s all # 用尽所有可用空间
./linux_lvm.sh -m / -s 100G -y # 根目录扩 100G
./linux_lvm.sh -m /home -s 50% -y # 扩 50% 的 VG 剩余空间
all、100G、50% 三种表达,覆盖了 90% 的实际需求。
亮点 4:非交互环境不会"卡死"
confirm() {
[ "$ASSUME_YES" = "1" ] && return 0
if [ ! -t 0 ]; then
warn "非交互环境且未加 -y, 跳过需要确认的操作"
return 1
fi
...
}
在 Jenkins、Ansible、定时任务里跑,没加 -y 时它不会傻等输入,而是明确跳过并告警。
这个小设计,能避免无数次"任务挂起"的排查。
四、几个真实使用姿势
① 先看一眼,不动手(最推荐的第一步)
./linux_lvm.sh --list
输出当前文件系统使用率、VG/LV 状态、可自动发现的空闲磁盘。只读,零风险。
② 根目录扩容(最常见也最紧张)
./linux_lvm.sh -m / -s 100G -y
③ 自动找最紧张的挂载点扩满
./linux_lvm.sh --auto -s all -y
适合放进监控告警的自动处置流程。
④ 指定新盘加入并扩容
./linux_lvm.sh -m /data -s all -d /dev/sdc
⑤ 啥也不带,进交互菜单
./linux_lvm.sh
会列出所有可扩容的 LVM 挂载点,按使用率排序,输个编号就行。
五、使用前必须说的三句话
- 生产环境先
--list,再小流量验证。 任何动分区的操作,都建议先在测试机跑一遍。 - LVM 能扩不能缩。
lvextend很安全,lvreduce请当作不存在。 - MBR 转 GPT 有风险。 脚本只负责"提示你被 2TiB 卡住了",转换本身请老老实实备份后再操作(
gdisk可无损转,但不是零风险)。
六、写在最后
运维这行,真正消耗人的从来不是"会不会",而是重复、琐碎、还要在高压下不出错。
LVM 扩容这件事,技术含量不高,但架不住它:
- 场景多(云盘 / 新盘 / 分区 / 根目录)
- 步骤长(五层往上顶)
- 坑还隐蔽(2TiB、忘扩文件系统)
把它脚本化,不是因为懒,而是因为人会在凌晨三点犯错,脚本不会。
工具不能替代判断,但能把判断之外的所有体力活,全部消灭。
如果你也有类似的"每次都要查文档、每次都要小心翼翼"的运维操作,欢迎在评论区聊聊——下一个被脚本化的,可能就是你提的那个痛点。
脚本名:linux_lvm.sh(v2.1);依赖:lvm2、growpart 或 parted;建议 root 运行,执行前务必备份关键数据。
#!/bin/bash
#===============================================================================
# lvm_auto_extend.sh v2.1 - 通用 LVM 磁盘扩容脚本(多挂载点 / 自定义大小 / 大于2TiB磁盘)
#
# 功能:
# 1. 支持对任意 LVM 挂载点扩容: / 、/home 、/data 、/opt 等
# 2. 扩容大小可自定义: 100G / 500M / 2T / 10T / 50% / all(用尽所有可用空间)
# 3. 大于 2TiB 的磁盘支持:
# - 新盘整盘作为 PV 使用, 无 MBR 2TiB 寻址限制
# - GPT 分区表磁盘: 分区扩展无 2TiB 限制, 正常 growpart
# - MBR(msdos) 磁盘: 自动检测并明确提示 2TiB 寻址上限,
# 只扩展 2TiB 以内的空间, 避免静默浪费, 并给出转 GPT 建议
# 3. 自动识别扩容空间来源, 优先级:
# a) VG 内剩余空间 (vg_free)
# b) 云盘/虚拟机磁盘在线扩容后, 对已有 PV 直接 pvresize
# c) PV 所在磁盘尾部未分区空间 (自动 growpart/parted + pvresize)
# d) 自动发现新的空闲整盘 (自动 pvcreate + vgextend)
# 4. --auto 模式: 自动找出使用率最高的 LVM 挂载点并扩容
#
# 用法:
# ./linux_lvm.sh --list # 仅查看当前磁盘/LVM/挂载点状态
# ./linux_lvm.sh # 交互模式(菜单选择)
# ./linux_lvm.sh -m /data -s all # /data 扩到最大
# ./linux_lvm.sh -m / -s 100G -y # 根目录扩 100G, 免交互确认
# ./linux_lvm.sh -m /home -s 50% -y # /home 扩容 50% 的 VG 剩余空间
# ./linux_lvm.sh --auto -s all -y # 自动找最紧张的挂载点扩满
# ./linux_lvm.sh -m /data -s all -d /dev/sdc # 指定把 /dev/sdc 加入 VG 后扩容
#
# 选项:
# -m MOUNT 目标挂载点 (如 / /home /data), 必填(除 --list/--auto 外)
# -s SIZE 扩容大小: 数字+[KMGT] | N% | all (默认 all)
# -d DISK 额外指定加入 VG 的磁盘, 可多次使用
# -v VGNAME 新建卷时使用的 VG 名 (默认 data_vg)
# -y / --yes 所有确认自动回答 yes (用于自动化/CI)
# --auto 自动选择使用率最高的 LVM 挂载点
# --list 只显示状态, 不做任何变更
# -h / --help 帮助
#
# 依赖: lsblk blkid findmnt vgs/pvs/lvs (lvm2), growpart 或 parted(二选一)
#===============================================================================
set -e
export LC_ALL=C
VERSION="2.1"
# MBR(msdos) 分区表的最大可寻址容量: 2TiB
MSDOS_MAX_BYTES=2199023255552
VG_CREATE_NAME="data_vg" # 新建卷时的默认 VG 名, 可用 -v 覆盖
ASSUME_YES=0
LIST_ONLY=0
AUTO_MODE=0
TARGET_MOUNT=""
TARGET_SIZE="all"
FORCE_DISKS=()
#------------------------------- 基础工具 -------------------------------------
info() { echo "[信息] $*"; }
warn() { echo "[警告] $*" >&2; }
die() { echo "[错误] $*" >&2; exit 1; }
confirm() {
# confirm "提示语" -> 返回 0 表示用户同意
[ "$ASSUME_YES" = "1" ] && return 0
if [ ! -t 0 ]; then
warn "非交互环境且未加 -y, 跳过需要确认的操作: $*"
return 1
fi
local ans
read -r -p "$1 [y/N] " ans
case "$ans" in [yY]|[yY][eE][sS]) return 0 ;; *) return 1 ;; esac
}
usage() { sed -n '2,36p' "$0" | sed 's/^# \{0,1\}//'; }
require_root() {
[ "$(id -u)" -eq 0 ] || die "请使用 root 用户运行 (sudo ./linux_lvm.sh)"
}
check_deps() {
local cmd missing=0
for cmd in lsblk blkid findmnt awk sed vgs pvs lvs df mount mkdir; do
command -v "$cmd" >/dev/null 2>&1 || { warn "缺少命令: $cmd"; missing=1; }
done
[ "$missing" = "0" ] || die "请先安装上述缺失的软件包"
if ! command -v growpart >/dev/null 2>&1 && ! command -v parted >/dev/null 2>&1; then
warn "缺少 growpart 和 parted, 将无法扩展已有分区 (安装: yum install cloud-utils-growpart / parted)"
fi
}
#------------------------------ 磁盘发现 --------------------------------------
# 找出所有"完全空闲"的磁盘: 无分区、无挂载、无文件系统签名、不是 PV、非只读
find_free_disks() {
local name type disk child_count
FREE_DISKS=()
while read -r name type; do
[ "$type" = "disk" ] || continue
disk="/dev/$name"
[ -b "$disk" ] || continue
# 有子分区/子设备的跳过
child_count=$(lsblk -nlo NAME "$disk" 2>/dev/null | wc -l)
[ "$child_count" -gt 1 ] && continue
# 有挂载点跳过
lsblk -nlo MOUNTPOINT "$disk" 2>/dev/null | grep -q . && continue
# 有文件系统签名跳过
blkid "$disk" >/dev/null 2>&1 && continue
# 已是 PV 跳过
pvs "$disk" >/dev/null 2>&1 && continue
# 只读设备跳过
[ "$(lsblk -dno RO "$disk" 2>/dev/null | tr -d ' ')" = "1" ] && continue
FREE_DISKS+=("$disk")
done < <(lsblk -dno NAME,TYPE)
}
#------------------------------ 空间来源 --------------------------------------
# 尝试对 VG 内所有 PV 执行 pvresize (云盘在线扩容场景, 幂等操作)
pvresize_all() {
local vg="$1" pv changed=0
while read -r pv; do
[ -n "$pv" ] || continue
if pvresize "$pv" >/dev/null 2>&1; then
# pvresize 本身不报错; 检测是否真的变大了交给后续 vg_free 判断
:
fi
done < <(pvs --noheadings -o pv_name -S "vg_name=$vg" 2>/dev/null)
return 0
}
# 尝试把 PV 所在分区的尾部空闲空间扩展给该分区, 成功返回 0
# 场景: 系统盘当初只分了一个分区, 后来云盘/虚拟机磁盘扩容了
grow_pv_partition() {
local pv="$1"
local base parent disk partnum free_bytes
base="$(basename "$pv")"
# 不是分区(整盘 PV)则无需 growpart
[ -e "/sys/class/block/$base/partition" ] || return 1
parent="$(lsblk -no PKNAME "$pv" 2>/dev/null | head -n1 | tr -d ' ')"
[ -n "$parent" ] || return 1
disk="/dev/$parent"
# ---- 分区表类型与磁盘容量 (处理大于 2TiB 的磁盘) ----
local label total lim=0
label=$(parted -sm "$disk" print 2>/dev/null | awk -F: 'NR==2{print $6}')
total=$(lsblk -bno SIZE "$disk" 2>/dev/null | tail -n1)
# MBR(msdos) 分区表最大只能寻址 2TiB, 超出部分对分区永远不可见
if [ "$label" = "msdos" ]; then
lim="$MSDOS_MAX_BYTES"
if [ "${total:-0}" -gt "$MSDOS_MAX_BYTES" ]; then
local beyond=$(( (total - MSDOS_MAX_BYTES) / 1024 / 1024 / 1024 ))
warn "$disk 使用 MBR(msdos) 分区表且容量超过 2TiB"
warn "2TiB 之后约 ${beyond}GiB 空间受 MBR 寻址上限限制, 无法通过扩分区使用"
warn "如需全部利用, 二选一:"
warn " 1) 备份数据后将磁盘转换为 GPT (gdisk 可无损转换, 操作有风险)"
warn " 2) 清空磁盘后整盘作为 PV 使用 (无 2TiB 限制)"
fi
fi
# 计算磁盘尾部空闲字节数 (阈值 4MiB, 忽略 GPT 备份/对齐的零头)
# msdos 分区表时截断到 2TiB 以内, 只统计真正可用的部分
free_bytes=$(parted -sm "$disk" unit B print free 2>/dev/null \
| awk -F: -v lim="$lim" '$5=="free;" {s=$2+0; e=$3+0; if (lim>0 && e>lim) e=lim; if (e>s) t+=e-s} END{print t+0}')
[ "${free_bytes:-0}" -gt 4194304 ] || return 1
partnum="$(lsblk -no PARTNUM "$pv" 2>/dev/null | tr -d ' ')"
[ -n "$partnum" ] || return 1
echo "发现 $disk 尾部有约 $((free_bytes / 1024 / 1024)) MiB 未分区空间"
if ! confirm "确认扩展分区 $pv ( ${disk} 的第 ${partnum} 个分区 ) 以利用这些空间?"; then
return 1
fi
if command -v growpart >/dev/null 2>&1; then
growpart "$disk" "$partnum" || return 1
elif command -v parted >/dev/null 2>&1; then
parted -s "$disk" resizepart "$partnum" 100% || return 1
else
warn "缺少 growpart/parted, 无法扩展分区"
return 1
fi
partprobe "$disk" 2>/dev/null || partx -u "$disk" 2>/dev/null || true
pvresize "$pv" || return 1
return 0
}
# 把一块磁盘(整盘)加入 VG
add_disk_to_vg() {
local disk="$1" vg="$2"
local dsize dlabel
dsize=$(lsblk -dno SIZE "$disk" 2>/dev/null)
dlabel=$(lsblk -dno PTTYPE "$disk" 2>/dev/null)
if pvs "$disk" >/dev/null 2>&1; then
warn "$disk 已经是 PV, 尝试直接 pvresize"
pvresize "$disk" || return 1
else
pvcreate -y "$disk" || return 1
fi
if vgs "$vg" >/dev/null 2>&1; then
vgextend "$vg" "$disk" || return 1
else
vgcreate "$vg" "$disk" || return 1
fi
info "$disk (容量 ${dsize:-?}, 分区表: ${dlabel:-无}) 已加入 VG $vg — 整盘作为 PV, 无 MBR 2TiB 寻址限制"
}
# 按大小执行 lvextend, 成功返回 0
apply_lvextend() {
local lv_path="$1" size="$2"
case "$size" in
all|ALL|All)
lvextend -l +100%FREE "$lv_path" ;;
*%)
lvextend -l "+${size%%\%}%FREE" "$lv_path" ;;
*)
lvextend -L "+${size}" "$lv_path" ;;
esac
}
vg_free_pe() {
vgs --noheadings -o vg_free_count "$1" 2>/dev/null | tr -d ' '
}
# 汇集空间并扩展 LV: 依次尝试 VG 剩余 -> pvresize -> 分区扩展 -> 新空闲盘
gather_and_extend() {
local vg="$1" lv_path="$2" size="$3"
local disk need_more=1
# 来源 a: VG 已有剩余空间
if [ "$(vg_free_pe "$vg")" -gt 0 ] 2>/dev/null; then
info "VG $vg 有剩余空间, 直接扩展 LV"
apply_lvextend "$lv_path" "$size" && return 0
fi
# 来源 b: 已有 PV 在线扩容 (pvresize 幂等, 先刷一遍)
pvresize_all "$vg"
if [ "$(vg_free_pe "$vg")" -gt 0 ] 2>/dev/null; then
info "pvresize 后 VG $vg 出现剩余空间, 扩展 LV"
apply_lvextend "$lv_path" "$size" && return 0
fi
# 来源 c: PV 所在磁盘的尾部未分区空间
local pv
while read -r pv; do
[ -n "$pv" ] || continue
if grow_pv_partition "$pv"; then
if apply_lvextend "$lv_path" "$size"; then
return 0
fi
fi
done < <(pvs --noheadings -o pv_name -S "vg_name=$vg" 2>/dev/null)
# 来源 d: 新的空闲整盘
find_free_disks
# 追加用户 -d 指定的磁盘
local fd
for fd in "${FORCE_DISKS[@]}"; do
[ -b "$fd" ] || die "指定的磁盘 $fd 不存在"
FREE_DISKS+=("$fd")
done
if [ "${#FREE_DISKS[@]}" -gt 0 ]; then
echo ""
echo "发现可用的空闲磁盘:"
local fd dsize dlabel
for fd in "${FREE_DISKS[@]}"; do
dsize=$(lsblk -dno SIZE "$fd" 2>/dev/null)
dlabel=$(lsblk -dno PTTYPE "$fd" 2>/dev/null)
printf ' %-14s %-10s 分区表: %s\n' "$fd" "$dsize" "${dlabel:-无(整盘可用)}"
done
for disk in "${FREE_DISKS[@]}"; do
if confirm "确认将 $disk 加入 VG $vg 并扩展 LV?"; then
add_disk_to_vg "$disk" "$vg" || { warn "$disk 加入失败, 跳过"; continue; }
if apply_lvextend "$lv_path" "$size"; then
return 0
fi
fi
done
fi
return 1
}
#------------------------------ 文件系统 --------------------------------------
grow_fs() {
local lv_path="$1" mount="$2" fstype
fstype="$(lsblk -no FSTYPE "$lv_path" 2>/dev/null)"
case "$fstype" in
xfs)
info "扩展 XFS 文件系统 (xfs_growfs $mount)"
xfs_growfs "$mount" ;;
ext2|ext3|ext4)
info "扩展 ${fstype} 文件系统 (resize2fs $lv_path)"
resize2fs "$lv_path" ;;
"")
die "$lv_path 上没有文件系统, 请先格式化" ;;
*)
die "未知文件系统: $fstype, 请手动扩展" ;;
esac
}
#------------------------------ 目标解析 ---------------------------------------
# 解析挂载点对应的 VG/LV, 结果存入全局 LV_VG / LV_LV / LV_PATH
resolve_lv() {
local mount="$1" src vg lv path
LV_VG=""; LV_LV=""; LV_PATH=""
src="$(findmnt -no SOURCE --target "$mount" 2>/dev/null || true)"
if [ -z "$src" ]; then
# 未挂载, 尝试从 fstab 找
src="$(findmnt -no SOURCE --fstab --target "$mount" 2>/dev/null || true)"
fi
[ -n "$src" ] || return 1
case "$src" in
/dev/mapper/*|/dev/dm-*|/dev/*/mapper/*) ;;
*)
# 形如 /dev/vg/lv 也算
case "$src" in
/dev/*/*) ;;
*) return 1 ;; # 普通分区, 非 LVM
esac
;;
esac
while read -r vg lv path; do
[ -n "$vg" ] || continue
if [ "$src" = "/dev/mapper/${vg}-${lv}" ] || [ "$src" = "/dev/${vg}/${lv}" ] \
|| [ "$src" = "$path" ]; then
LV_VG="$vg"; LV_LV="$lv"; LV_PATH="$path"
return 0
fi
done < <(lvs --noheadings --separator ' ' -o vg_name,lv_name,lv_path 2>/dev/null)
return 1
}
# 自动选择使用率最高的 LVM 挂载点, 存入 TARGET_MOUNT
pick_auto_target() {
local best="" best_pct=-1 pct mount
while read -r pct mount; do
[ -n "$mount" ] || continue
if resolve_lv "$mount"; then
if [ "$pct" -gt "$best_pct" ]; then
best_pct="$pct"; best="$mount"
fi
fi
done < <(df -PTl -x tmpfs -x devtmpfs -x squashfs -x iso9660 -x overlay 2>/dev/null \
| awk 'NR>1 {gsub(/%/,"",$6); print $6+0, $7}' | sort -rn)
[ -n "$best" ] || die "未找到任何可扩容的 LVM 挂载点"
TARGET_MOUNT="$best"
}
#------------------------------ 状态展示 ---------------------------------------
show_status() {
echo "================ 当前状态 ================"
echo "--- 文件系统使用情况 ---"
df -PTl -x tmpfs -x devtmpfs -x squashfs -x iso9660 -x overlay 2>/dev/null \
| awk 'NR==1 || $2=="xfs" || $2 ~ /^ext[234]$/'
echo ""
echo "--- 卷组 (VG) ---"
vgs 2>/dev/null || echo "(无 VG)"
echo ""
echo "--- 逻辑卷 (LV) ---"
lvs 2>/dev/null || echo "(无 LV)"
echo ""
find_free_disks
echo "--- 可自动发现的空闲磁盘 ---"
if [ "${#FREE_DISKS[@]}" -gt 0 ]; then
local d dsize dlabel
for d in "${FREE_DISKS[@]}"; do
dsize=$(lsblk -dno SIZE "$d" 2>/dev/null)
dlabel=$(lsblk -dno PTTYPE "$d" 2>/dev/null)
printf ' %-14s %-10s 分区表: %s\n' "$d" "$dsize" "${dlabel:-无(整盘可用)}"
done
else
echo " (无)"
fi
echo "=========================================="
}
#------------------------------ 主扩容流程 -------------------------------------
do_extend() {
local mount="$1" size="$2"
[ -d "$mount" ] || die "挂载点 $mount 不存在"
if resolve_lv "$mount"; then
info "目标: $mount -> LV $LV_PATH (VG: $LV_VG)"
echo ""
if gather_and_extend "$LV_VG" "$LV_PATH" "$size"; then
grow_fs "$LV_PATH" "$mount"
else
die "无法获取更多空间 (VG 无剩余、无可扩分区、无空闲磁盘), 扩容失败"
fi
else
# 挂载点还没有对应卷 -> 走"新建卷"流程
echo "未发现 $mount 对应的 LVM 逻辑卷, 尝试新建卷并挂载"
find_free_disks
if [ "${#FREE_DISKS[@]}" -eq 0 ] && [ "${#FORCE_DISKS[@]}" -eq 0 ]; then
die "没有可用的空闲磁盘, 无法新建卷"
fi
local vg="$VG_CREATE_NAME"
local lv="lv_$(echo "$mount" | sed 's|^/||; s|/|_|g')"
[ -n "$lv" ] || lv="data_lv"
confirm "确认使用空闲磁盘为 $mount 创建 VG=$vg / LV=$lv ?" \
|| die "用户取消"
local disk first=1
for disk in "${FREE_DISKS[@]}"; do
if [ "$first" = "1" ]; then
add_disk_to_vg "$disk" "$vg" || die "初始化 $disk 失败"
first=0
else
add_disk_to_vg "$disk" "$vg" || warn "$disk 加入失败, 跳过"
fi
done
LV_PATH="/dev/$vg/$lv"
info "创建 LV: $LV_PATH (使用全部 VG 空间)"
lvcreate -y -l 100%FREE -n "$lv" "$vg"
info "格式化 $LV_PATH 为 xfs"
mkfs.xfs -f "$LV_PATH"
mkdir -p "$mount"
if ! mountpoint -q "$mount"; then
mount "$LV_PATH" "$mount"
fi
local uuid
uuid="$(blkid -s UUID -o value "$LV_PATH")"
if [ -n "$uuid" ] && ! grep -q "$uuid" /etc/fstab; then
cp /etc/fstab /etc/fstab.bak.$(date +%s) 2>/dev/null || true
echo "UUID=$uuid $mount xfs defaults 0 0" >> /etc/fstab
info "已写入 /etc/fstab (已先备份)"
fi
fi
echo ""
info "==== 扩容完成 ===="
df -h "$mount"
}
#------------------------------ 交互菜单 ---------------------------------------
interactive_mode() {
show_status
echo ""
echo "请选择要扩容的挂载点 (自动识别 LVM 卷):"
local -a mounts=()
local pct mount idx=1
while read -r pct mount; do
[ -n "$mount" ] || continue
if resolve_lv "$mount"; then
printf ' %d) %-16s 使用率 %s%% (LV: %s)\n' "$idx" "$mount" "$pct" "$LV_PATH"
mounts+=("$mount")
idx=$((idx + 1))
fi
done < <(df -PTl -x tmpfs -x devtmpfs -x squashfs -x iso9660 -x overlay 2>/dev/null \
| awk 'NR>1 {gsub(/%/,"",$6); print $6+0, $7}' | sort -rn)
[ "${#mounts[@]}" -gt 0 ] || die "没有找到任何 LVM 挂载点"
local choice
read -r -p "输入编号 [1-${#mounts[@]}]: " choice
case "$choice" in
''|*[!0-9]*) die "无效输入" ;;
esac
[ "$choice" -ge 1 ] && [ "$choice" -le "${#mounts[@]}" ] || die "编号超出范围"
TARGET_MOUNT="${mounts[$((choice - 1))]}"
echo ""
read -r -p "扩容大小 [回车=用尽所有空间, 例: 100G / 50%]: " size
[ -n "$size" ] || size="all"
echo ""
do_extend "$TARGET_MOUNT" "$size"
}
#------------------------------ 参数校验 ---------------------------------------
validate_size() {
case "$1" in
all|ALL|All) return 0 ;;
*[!0-9.%KMGTkmgt]*) die "无效的扩容大小: $1 (示例: 100G / 500M / 2T / 50% / all)" ;;
*%) return 0 ;;
*[KMGTkmgt]|*[0-9]) return 0 ;;
*) die "无效的扩容大小: $1" ;;
esac
}
#------------------------------ 入口 -------------------------------------------
while [ $# -gt 0 ]; do
case "$1" in
-m) TARGET_MOUNT="$2"; shift 2 ;;
-s) TARGET_SIZE="$2"; shift 2 ;;
-d) FORCE_DISKS+=("$2"); shift 2 ;;
-v) VG_CREATE_NAME="$2"; shift 2 ;;
-y|--yes) ASSUME_YES=1; shift ;;
--auto) AUTO_MODE=1; shift ;;
--list) LIST_ONLY=1; shift ;;
-h|--help) usage; exit 0 ;;
--) shift; break ;;
*) die "未知参数: $1 (使用 -h 查看帮助)" ;;
esac
done
require_root
check_deps
if [ "$LIST_ONLY" = "1" ]; then
show_status
exit 0
fi
if [ "$AUTO_MODE" = "1" ]; then
pick_auto_target
info "自动选择使用率最高的挂载点: $TARGET_MOUNT"
do_extend "$TARGET_MOUNT" "$TARGET_SIZE"
exit 0
fi
if [ -z "$TARGET_MOUNT" ]; then
interactive_mode
exit 0
fi
validate_size "$TARGET_SIZE"
do_extend "$TARGET_MOUNT" "$TARGET_SIZE"
原文地址: https://www.cveoy.top/t/topic/qHRQ 著作权归作者所有。请勿转载和采集!