运维人的深夜,往往毁于一条告警:磁盘使用率超过 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 挂载点,按使用率排序,输个编号就行。


五、使用前必须说的三句话

  1. 生产环境先 --list,再小流量验证。 任何动分区的操作,都建议先在测试机跑一遍。
  2. LVM 能扩不能缩。 lvextend 很安全,lvreduce 请当作不存在。
  3. 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 著作权归作者所有。请勿转载和采集!

免费AI点我,无需注册和登录