#!/usr/bin/env bash

#===============================================================================
#  名称:        lvm_extend_with_disk.sh
#  描述:        使用新增裸盘扩展指定挂载目录对应的 LVM LV（ext4 / xfs）
#  作者:        WDD
#  版本:        1.0.0
#  许可证:      MIT
#
#  依赖(命令):  bash(>=5.0), coreutils, util-linux, lvm2, gdisk 或 parted,
#              findmnt, lsblk, blkid, wipefs, partprobe, udevadm,
#              resize2fs(用于ext4), xfs_growfs(用于xfs)
#
#  安全说明:
#    - 本脚本会对“新增裸盘”进行 GPT 分区、清空签名(wipefs)、创建 PV 等破坏性操作。
#    - 默认会做严格安全检查：若检测到磁盘疑似在使用/含签名/已有分区，将拒绝执行（可用 --force 覆盖部分检查）。
#===============================================================================

set -euo pipefail
IFS=$'\n\t'

#===============================================================================
# 全局常量定义区
#===============================================================================
readonly SCRIPT_NAME="$(basename "$0")"
readonly SCRIPT_VERSION="1.0.0"

# 日志级别：DEBUG/INFO/WARN/ERROR
readonly LOG_LEVEL_DEFAULT="INFO"

#===============================================================================
# 全局变量（由参数控制）
#===============================================================================
log_level="${LOG_LEVEL_DEFAULT}"
dry_run="0"
force="0"
raw_disk=""         # 例如 /dev/sdb
target_mount=""     # 例如 /data

# 运行时探测结果
fs_type=""          # ext4 | xfs
mount_source=""     # /dev/mapper/vg-lv 或 /dev/vg/lv
lv_path=""          # 规范化后的 LV 真实路径
vg_name=""          # VG 名称
new_part=""         # 新建分区路径：/dev/sdb1 或 /dev/nvme0n1p1

# 回滚动作栈（先进后出）
rollback_stack=()

#===============================================================================
# 函数调用关系图（ASCII）
#===============================================================================
# main
#  ├─ parse_args
#  ├─ init_traps
#  ├─ require_root
#  ├─ check_dependencies
#  ├─ detect_target_lvm_by_mount
#  │    ├─ get_mount_info
#  │    ├─ normalize_lv_and_vg
#  ├─ validate_raw_disk_safe
#  ├─ prepare_gpt_partition_for_lvm
#  │    ├─ make_partition_path
#  │    ├─ create_gpt_partition (sgdisk|parted)
#  ├─ create_and_attach_pv_to_vg
#  │    ├─ pvcreate
#  │    ├─ vgextend
#  ├─ extend_lv_and_grow_fs
#  │    ├─ lvextend -r
#  ├─ verify_result
#  └─ success_exit
#
# on_error (trap ERR)
#  └─ rollback_all
#===============================================================================

#===============================================================================
# 日志模块
#===============================================================================

### 输出日志（统一入口）
# @param level string 日志级别(DEBUG/INFO/WARN/ERROR)
# @param message string 日志内容
# @return 0 成功
# @require date, printf
log() {
  local level="$1"; shift
  local message="$*"
  local ts
  ts="$(date '+%Y-%m-%d %H:%M:%S')"

  # > 级别过滤：DEBUG < INFO < WARN < ERROR
  local order_current order_target
  order_current="$(log_level_to_int "$log_level")"
  order_target="$(log_level_to_int "$level")"
  if (( order_target < order_current )); then
    return 0
  fi

  printf '[%s] [%s] %s\n' "$ts" "$level" "$message" >&2
}

### 将日志级别映射为数字
# @param level string 日志级别
# @return 0 成功（echo输出数字）
# @require printf
log_level_to_int() {
  local level="$1"
  case "$level" in
    DEBUG) printf '10' ;;
    INFO)  printf '20' ;;
    WARN)  printf '30' ;;
    ERROR) printf '40' ;;
    *)     printf '20' ;; # 默认 INFO
  esac
}

log_debug(){ log "DEBUG" "$*"; }
log_info(){  log "INFO"  "$*"; }
log_warn(){  log "WARN"  "$*"; }
log_error(){ log "ERROR" "$*"; }

#===============================================================================
# 工具/执行模块
#===============================================================================

### 执行命令（支持 dry-run）
# @param cmd string 要执行的命令（以参数形式传入）
# @return 0 成功；非0 失败
# @require printf, bash
run_cmd() {
  if [[ "$dry_run" == "1" ]]; then
    log_info "[DRY-RUN] $*"
    return 0
  fi
  log_debug "RUN: $*"
  "$@"
}

### 检查命令是否存在
# @param cmd string 命令名
# @return 0 存在；非0 不存在
# @require command
require_cmd() {
  local cmd="$1"
  if ! command -v "$cmd" >/dev/null 2>&1; then
    log_error "缺少依赖命令: $cmd"
    return 1
  fi
}

### 推入回滚动作（字符串命令）
# @param action string 回滚动作（将通过 bash -c 执行）
# @return 0 成功
# @require none
push_rollback() {
  local action="$1"
  rollback_stack+=("$action")
  log_debug "已登记回滚动作: $action"
}

### 执行所有回滚动作（LIFO）
# @return 0 成功（尽最大努力）
# @require bash
rollback_all() {
  local i
  if (( ${#rollback_stack[@]} == 0 )); then
    log_warn "无回滚动作可执行。"
    return 0
  fi

  log_warn "开始回滚（共 ${#rollback_stack[@]} 步）..."
  for (( i=${#rollback_stack[@]}-1; i>=0; i-- )); do
    local action="${rollback_stack[$i]}"
    log_warn "回滚: $action"
    if [[ "$dry_run" == "1" ]]; then
      log_info "[DRY-RUN] 跳过回滚执行"
      continue
    fi
    # > 回滚尽力而为：失败不再中断后续回滚
    bash -c "$action" || log_warn "回滚动作执行失败（已忽略）: $action"
  done
  log_warn "回滚结束。"
}

#===============================================================================
# Trap/错误处理模块
#===============================================================================

### 初始化 trap
# @return 0 成功
# @require trap
init_traps() {
  trap 'on_error $? $LINENO' ERR
  trap 'on_exit $?' EXIT
}

### ERR trap 处理
# @param exit_code int 退出码
# @param line_no int 行号
# @return 0 成功
# @require none
on_error() {
  local exit_code="$1"
  local line_no="$2"
  log_error "脚本执行失败（exit=$exit_code, line=$line_no），将进行回滚..."
  rollback_all
  exit "$exit_code"
}

### EXIT trap 处理（用于输出调试信息，不做回滚）
# @param exit_code int 退出码
# @return 0 成功
# @require none
on_exit() {
  local exit_code="$1"
  if [[ "$exit_code" == "0" ]]; then
    log_info "完成：扩展流程已成功结束。"
  else
    log_error "退出：脚本以非0退出码结束（exit=$exit_code）。"
  fi
}

#===============================================================================
# 参数解析与使用说明
#===============================================================================

### 使用说明
# @return 0 成功
# @require cat
usage() {
  cat <<EOF
用法:
  $SCRIPT_NAME --disk /dev/sdX --mount /path/to/mount [--force] [--dry-run] [--log-level DEBUG|INFO|WARN|ERROR]

参数:
  --disk,  -d   需要格式化并用于扩展的裸盘（例如 /dev/sdb, /dev/nvme1n1）
  --mount, -m   需要扩展的挂载目录（例如 /data）
  --force, -f   强制执行：当检测到磁盘含签名/已有分区等风险时仍继续（仍会拒绝“已挂载/已作为PV”的磁盘）
  --dry-run      演练模式：仅打印将执行的操作，不实际改动
  --log-level    日志级别（默认 INFO）

示例:
  $SCRIPT_NAME -d /dev/sdb -m /data
  $SCRIPT_NAME -d /dev/nvme1n1 -m /data --force
EOF
}

### 解析参数
# @param args string 参数列表
# @return 0 成功
# @require shift
parse_args() {
  while (( $# > 0 )); do
    case "$1" in
      -d|--disk)   raw_disk="${2:-}"; shift 2 ;;
      -m|--mount)  target_mount="${2:-}"; shift 2 ;;
      -f|--force)  force="1"; shift ;;
      --dry-run)   dry_run="1"; shift ;;
      --log-level) log_level="${2:-INFO}"; shift 2 ;;
      -h|--help)   usage; exit 0 ;;
      *)
        log_error "未知参数: $1"
        usage
        exit 2
        ;;
    esac
  done

  if [[ -z "$raw_disk" || -z "$target_mount" ]]; then
    log_error "必须提供 --disk 与 --mount"
    usage
    exit 2
  fi
}

#===============================================================================
# 前置检查模块
#===============================================================================

### 检查是否 root
# @return 0 成功；非0 失败
# @require id
require_root() {
  if [[ "$(id -u)" != "0" ]]; then
    log_error "必须以 root 运行（需要分区/LVM 操作权限）。"
    return 1
  fi
}

### 检查依赖命令
# @return 0 成功；非0 失败
# @require command
check_dependencies() {
  # 基础
  require_cmd findmnt
  require_cmd lsblk
  require_cmd blkid
  require_cmd wipefs
  require_cmd partprobe
  require_cmd udevadm

  # LVM
  require_cmd pvs
  require_cmd vgs
  require_cmd lvs
  require_cmd pvcreate
  require_cmd pvremove
  require_cmd vgextend
  require_cmd vgreduce
  require_cmd lvextend

  # 分区工具：优先 sgdisk（gdisk套件），否则 parted
  if command -v sgdisk >/dev/null 2>&1; then
    :
  else
    require_cmd parted
  fi

  # 文件系统扩展工具（由探测类型决定）
  # > 在探测 fs_type 后再校验（此处先不强制）
}

#===============================================================================
# 目标挂载点探测模块
#===============================================================================

### 获取挂载信息（source 与 fstype）
# @param mount_path string 挂载目录
# @return 0 成功
# @require findmnt
get_mount_info() {
  local mount_path="$1"

  if ! findmnt -T "$mount_path" >/dev/null 2>&1; then
    log_error "挂载目录不存在或未挂载: $mount_path"
    return 1
  fi

  mount_source="$(findmnt -nr -T "$mount_path" -o SOURCE)"
  fs_type="$(findmnt -nr -T "$mount_path" -o FSTYPE)"

  if [[ -z "$mount_source" || -z "$fs_type" ]]; then
    log_error "无法获取挂载信息: mount=$mount_path"
    return 1
  fi

  case "$fs_type" in
    ext4|xfs) ;;
    *)
      log_error "仅支持 ext4 或 xfs，当前检测到: $fs_type"
      return 1
      ;;
  esac
}

### 获取块设备 MAJ:MIN（主:次设备号）
# @param dev string 块设备路径
# @return 0 成功（echo 输出 253:0）；非0 失败
# @require lsblk, printf
get_maj_min() {
  local dev="$1"
  local mm
  mm="$(lsblk -dn -o MAJ:MIN "$dev" 2>/dev/null | awk '{$1=$1;print}')"
  if [[ -z "$mm" ]]; then
    log_error "无法获取 MAJ:MIN: dev=$dev"
    return 1
  fi
  printf '%s' "$mm"
}

### 通过扫描 lvs 输出匹配 LV/VG（优先 dm_path，其次 MAJ:MIN）
# @param src_dev string 解析后的挂载设备（如 /dev/mapper/vg-lv 或 /dev/dm-0）
# @param real_dev string 真实设备（readlink -f 后，如 /dev/dm-0）
# @return 0 成功（echo 输出 "lv_path|vg_name"）
# @require lvs, awk, readlink, lsblk
lookup_lv_vg_from_lvs() {
  local src_dev="$1"
  local real_dev="$2"
  local mm
  mm="$(get_maj_min "$real_dev")"

  # A) 尝试：lvs 输出包含 lv_dm_path 时，用 dm_path 直接匹配（最稳）
  # > 不用 --select，避免 selection 兼容性问题
  if out="$(lvs --noheadings --separator '|' -o lv_path,vg_name,lv_dm_path 2>/dev/null)"; then
    local hit
    hit="$(
      echo "$out" | awk -F'|' -v s="$src_dev" -v r="$real_dev" '
        function trim(x){gsub(/^[ \t]+|[ \t]+$/,"",x); return x}
        {
          lv=trim($1); vg=trim($2); dm=trim($3)
          if(lv=="" || vg=="") next
          # 直接匹配 dm_path / src_dev / real_dev
          if(dm==s || dm==r) {print lv "|" vg; exit}
        }
      '
    )"
    if [[ -n "$hit" ]]; then
      printf '%s' "$hit"
      return 0
    fi

    # 再放宽：readlink -f(dm_path) 与 real_dev 比较（处理 dm_path 是 /dev/mapper/* 的情况）
    hit="$(
      echo "$out" | while IFS= read -r line; do
        [[ -z "$line" ]] && continue
        local lv vg dm dm_real
        lv="$(echo "$line" | awk -F'|' '{gsub(/^[ \t]+|[ \t]+$/,"",$1);print $1}')"
        vg="$(echo "$line" | awk -F'|' '{gsub(/^[ \t]+|[ \t]+$/,"",$2);print $2}')"
        dm="$(echo "$line" | awk -F'|' '{gsub(/^[ \t]+|[ \t]+$/,"",$3);print $3}')"
        [[ -z "$lv" || -z "$vg" || -z "$dm" ]] && continue
        dm_real="$(readlink -f "$dm" 2>/dev/null || true)"
        if [[ "$dm_real" == "$real_dev" ]]; then
          echo "${lv}|${vg}"
          break
        fi
      done
    )"
    if [[ -n "$hit" ]]; then
      printf '%s' "$hit"
      return 0
    fi
  fi

  # B) 回退：用 lv_kernel_major/minor 匹配 MAJ:MIN（跨版本最通用）
  # > 注意：部分环境列名可能不同，但大多数 lvm2 支持这两个字段
  if out2="$(lvs --noheadings --separator '|' -o lv_path,vg_name,lv_kernel_major,lv_kernel_minor 2>/dev/null)"; then
    local major minor
    major="${mm%%:*}"
    minor="${mm##*:}"
    local hit2
    hit2="$(
      echo "$out2" | awk -F'|' -v mj="$major" -v mn="$minor" '
        function trim(x){gsub(/^[ \t]+|[ \t]+$/,"",x); return x}
        {
          lv=trim($1); vg=trim($2); kmj=trim($3); kmn=trim($4)
          if(lv=="" || vg=="") next
          if(kmj==mj && kmn==mn) {print lv "|" vg; exit}
        }
      '
    )"
    if [[ -n "$hit2" ]]; then
      printf '%s' "$hit2"
      return 0
    fi
  fi

  # C) 兜底：lsblk 直接读 VG/LV 字段（某些 util-linux 会提供）
  if vg="$(lsblk -dn -o VG "$real_dev" 2>/dev/null | awk '{$1=$1;print}')" \
     && lvname="$(lsblk -dn -o LV "$real_dev" 2>/dev/null | awk '{$1=$1;print}')"; then
    if [[ -n "$vg" && -n "$lvname" ]]; then
      printf '/dev/%s/%s|%s' "$vg" "$lvname" "$vg"
      return 0
    fi
  fi

  return 1
}



### 将 findmnt 的 SOURCE 解析为可用的块设备路径（兼容 UUID/LABEL）
# @param source string 挂载源（/dev/xxx, UUID=..., LABEL=...）
# @return 0 成功（echo 输出 /dev/xxx）；非0 失败
# @require blkid, printf
resolve_mount_source_device() {
  local source="$1"
  local dev=""

  case "$source" in
    /dev/*)
      dev="$source"
      ;;
    UUID=*|LABEL=*)
      # > 关键步骤：把 UUID=xxxx / LABEL=xxx 解析成 /dev/xxx
      dev="$(blkid -o device -t "$source" 2>/dev/null | head -n1 || true)"
      ;;
    *)
      # 少数情况下 SOURCE 可能是 mapper 名称等，尝试补全
      if [[ -e "/dev/$source" ]]; then
        dev="/dev/$source"
      fi
      ;;
  esac

  if [[ -z "$dev" || ! -e "$dev" ]]; then
    log_error "无法将挂载源解析为设备路径: source=$source"
    return 1
  fi

  if [[ ! -b "$dev" ]]; then
    log_error "解析后的路径不是块设备: dev=$dev (from source=$source)"
    return 1
  fi

  printf '%s' "$dev"
}

### 规范化 LV 路径并获取 VG 名称（强健版：避免 lvs --select 兼容性坑）
# @param source string 挂载源（findmnt SOURCE）
# @return 0 成功
# @require readlink, lvs, awk, lsblk, blkid
normalize_lv_and_vg() {
  local source="$1"
  local src_dev real_dev out

  src_dev="$(resolve_mount_source_device "$source")"
  real_dev="$(readlink -f "$src_dev")"

  if [[ -z "$real_dev" || ! -b "$real_dev" ]]; then
    log_error "无法解析真实设备路径: src_dev=$src_dev real_dev=$real_dev"
    return 1
  fi

  log_debug "mount source resolve: source=$source src_dev=$src_dev real_dev=$real_dev"

  if ! out="$(lookup_lv_vg_from_lvs "$src_dev" "$real_dev")"; then
    log_error "无法从挂载源反查 LVM LV/VG：source=$source src_dev=$src_dev real_dev=$real_dev"
    log_error "诊断建议：执行 `lvs -a -o lv_path,vg_name,lv_dm_path,lv_kernel_major,lv_kernel_minor` 查看映射。"
    return 1
  fi

  lv_path="${out%%|*}"
  vg_name="${out##*|}"

  if [[ -z "$lv_path" || -z "$vg_name" ]]; then
    log_error "解析 LV/VG 失败: out=$out"
    return 1
  fi

  # 额外校验：lv_path 设备存在（有的环境返回的是 /dev/vg/lv，应该存在）
  if [[ ! -e "$lv_path" ]]; then
    log_warn "lvs 返回的 lv_path 不存在，尝试 readlink/替代路径: lv_path=$lv_path"
    # > 这里不强制失败，后续 lvextend 会更明确报错
  fi

  log_info "已解析目标: vg=$vg_name lv=$lv_path (from source=$source)"
}


### 综合探测目标 LVM
# @return 0 成功
# @require none
detect_target_lvm_by_mount() {
  get_mount_info "$target_mount"
  normalize_lv_and_vg "$mount_source"

  # fs 工具依赖补充检查
  if [[ "$fs_type" == "ext4" ]]; then
    require_cmd resize2fs
  else
    require_cmd xfs_growfs
  fi

  log_info "目标挂载点: $target_mount"
  log_info "挂载源设备: $mount_source"
  log_info "文件系统类型: $fs_type"
  log_info "LV 路径: $lv_path"
  log_info "VG 名称: $vg_name"
}

#===============================================================================
# 新增裸盘安全检查模块
#===============================================================================

### 校验裸盘是否安全可用
# @param disk string 裸盘路径（/dev/sdX 或 /dev/nvmeXnY）
# @return 0 成功
# @require lsblk, pvs, wipefs
validate_raw_disk_safe() {
  local disk="$1"

  if [[ ! -b "$disk" ]]; then
    log_error "指定磁盘不是块设备: $disk"
    return 1
  fi

  # 必须是 disk 类型
  local dtype
  dtype="$(lsblk -dn -o TYPE "$disk" 2>/dev/null || true)"
  if [[ "$dtype" != "disk" ]]; then
    log_error "指定设备不是磁盘(disk)类型: $disk (type=$dtype)"
    return 1
  fi

  # 不能已作为 PV
  if pvs --noheadings -o pv_name 2>/dev/null | awk '{$1=$1;print}' | grep -Fxq "$disk"; then
    log_error "磁盘已是 LVM PV，拒绝操作: $disk"
    return 1
  fi

  # 不能存在已挂载分区
  local mps
  mps="$(lsblk -nr -o MOUNTPOINT "$disk" 2>/dev/null | awk 'NF{print}')"
  if [[ -n "$mps" ]]; then
    log_error "检测到磁盘或其分区已挂载，拒绝操作: $disk"
    log_error "挂载点: $mps"
    return 1
  fi

  # 若存在子分区/签名，默认拒绝（--force 可放宽）
  local has_children has_sig
  has_children="$(lsblk -nr "$disk" -o NAME | awk 'NR>1{print}' | wc -l | awk '{$1=$1;print}')"
  has_sig="0"
  if wipefs -n "$disk" | awk 'NR>1{exit 0} END{exit 1}'; then
    has_sig="1"
  fi

  if [[ "$force" != "1" ]]; then
    if [[ "$has_children" != "0" ]]; then
      log_error "磁盘存在分区/子设备（默认拒绝）。可加 --force 继续: $disk"
      return 1
    fi
    if [[ "$has_sig" == "1" ]]; then
      log_error "磁盘存在文件系统/签名（默认拒绝）。可加 --force 继续: $disk"
      return 1
    fi
  else
    # --force 仍然禁止“已作为PV的分区”
    if pvs --noheadings -o pv_name 2>/dev/null | awk '{$1=$1;print}' | grep -q "^${disk}"; then
      log_error "检测到磁盘相关分区已是 PV（即使 --force 也拒绝）: $disk"
      return 1
    fi
  fi

  log_info "新增裸盘安全检查通过: $disk"
}

#===============================================================================
# 分区与 PV/VG/LV 扩展模块
#===============================================================================

### 生成分区路径（兼容 nvme 设备命名）
# @param disk string 磁盘路径
# @return 0 成功（echo 输出分区路径）
# @require printf
make_partition_path() {
  local disk="$1"
  # > nvme/mmc 通常以数字结尾，需要 p1
  if [[ "$disk" =~ [0-9]$ ]]; then
    printf '%sp1' "$disk"
  else
    printf '%s1' "$disk"
  fi
}

### 创建 GPT 分区并设置为 LVM 类型（1号分区使用全盘）
# @param disk string 裸盘
# @return 0 成功
# @require sgdisk 或 parted, partprobe, udevadm, wipefs
prepare_gpt_partition_for_lvm() {
  local disk="$1"

  new_part="$(make_partition_path "$disk")"

  # 回滚：尽量恢复到“无分区表/无签名”的状态
  # > 注意：这是破坏性回滚，仅针对“新增裸盘”
  push_rollback "wipefs -a '$disk' || true"
  if command -v sgdisk >/dev/null 2>&1; then
    push_rollback "sgdisk --zap-all '$disk' || true"
  else
    # parted 无直接 zap-all；用 dd 清前 10MiB（尽力而为）
    push_rollback "dd if=/dev/zero of='$disk' bs=1M count=10 conv=fsync || true"
  fi

  log_info "开始为裸盘创建 GPT + LVM 分区: disk=$disk part=$new_part"

  # > 关键步骤：先清签名，再分区
  run_cmd wipefs -a "$disk"

  if command -v sgdisk >/dev/null 2>&1; then
    # > 关键步骤：清旧分区表并创建新 GPT
    run_cmd sgdisk --zap-all "$disk"
    run_cmd sgdisk -og "$disk"
    # > 关键步骤：创建 1号分区：全盘，类型 8e00(Linux LVM)
    run_cmd sgdisk -n 1:0:0 -t 1:8e00 -c 1:"lvm-pv" "$disk"
  else
    # parted 方式
    # > 关键步骤：创建 GPT + 单分区 + LVM 标记
    run_cmd parted -s "$disk" mklabel gpt
    run_cmd parted -s "$disk" mkpart primary 1MiB 100%
    run_cmd parted -s "$disk" set 1 lvm on
  fi

  run_cmd partprobe "$disk"
  run_cmd udevadm settle

  if [[ ! -b "$new_part" ]]; then
    log_error "分区设备未出现（可能需要稍等或内核未识别）: $new_part"
    return 1
  fi

  log_info "分区创建完成: $new_part"
}

### 创建 PV 并扩展到目标 VG
# @param part string 新分区
# @param vg string 目标 VG 名称
# @return 0 成功
# @require pvcreate, vgextend, vgreduce, pvremove
create_and_attach_pv_to_vg() {
  local part="$1"
  local vg="$2"

  # 防止误用：新分区不应已是 PV
  if pvs --noheadings -o pv_name 2>/dev/null | awk '{$1=$1;print}' | grep -Fxq "$part"; then
    log_error "分区已是 PV，拒绝重复操作: $part"
    return 1
  fi

  log_info "创建 PV: $part"
  run_cmd pvcreate -y "$part"
  push_rollback "pvremove -ff -y '$part' || true"

  log_info "扩展 VG: vgextend $vg $part"
  run_cmd vgextend "$vg" "$part"
  push_rollback "vgreduce '$vg' '$part' || true"
}

### 扩展 LV 并增长文件系统（使用 -r 自动处理）
# @param lv string LV 路径
# @return 0 成功
# @require lvextend
extend_lv_and_grow_fs() {
  local lv="$1"

  log_info "扩展 LV 并自动扩容文件系统: lvextend -l +100%FREE -r $lv"
  # > 关键步骤：-r 自动增长 ext4/xfs（底层调用 resize2fs / xfs_growfs）
  run_cmd lvextend -l +100%FREE -r "$lv"
}

#===============================================================================
# 结果校验模块
#===============================================================================

### 校验扩展结果
# @param mount_path string 挂载目录
# @param lv string LV 路径
# @param part string 新增 PV 分区
# @param vg string VG 名称
# @return 0 成功
# @require df, lvs, vgs, pvs, awk
verify_result() {
  local mount_path="$1"
  local lv="$2"
  local part="$3"
  local vg="$4"

  log_info "开始校验扩展结果..."

  # 1) PV 是否在 VG 中
  if ! pvs --noheadings -o pv_name,vg_name 2>/dev/null | awk '{$1=$1;print}' | grep -E "^\Q${part}\E[[:space:]]+\Q${vg}\E$" >/dev/null 2>&1; then
    log_error "校验失败：新增 PV 未正确加入 VG: part=$part vg=$vg"
    return 1
  fi

  # 2) LV 是否可查询
  if ! lvs --noheadings -o lv_path 2>/dev/null | awk '{$1=$1;print}' | grep -Fxq "$lv"; then
    log_error "校验失败：无法查询到 LV: $lv"
    return 1
  fi

  # 3) df 输出（人类可读）
  log_info "df -h（$mount_path）："
  if [[ "$dry_run" == "1" ]]; then
    log_info "[DRY-RUN] 跳过 df -h"
  else
    df -h "$mount_path" >&2
  fi

  log_info "校验通过。"
}

#===============================================================================
# 主流程
#===============================================================================

### 主函数
# @return 0 成功
# @require none
main() {
  parse_args "$@"
  init_traps
  require_root
  check_dependencies

  detect_target_lvm_by_mount
  validate_raw_disk_safe "$raw_disk"

  prepare_gpt_partition_for_lvm "$raw_disk"
  create_and_attach_pv_to_vg "$new_part" "$vg_name"
  extend_lv_and_grow_fs "$lv_path"

  verify_result "$target_mount" "$lv_path" "$new_part" "$vg_name"

  # 成功后：清空回滚栈（避免 EXIT 误触发回滚；本脚本回滚仅在 ERR trap）
  rollback_stack=()
  log_info "扩展成功：mount=$target_mount lv=$lv_path vg=$vg_name new_pv=$new_part"
}

main "$@"



# 综合目的是为了，支持linux环境下新增磁盘逻辑卷的拓展，请实现如下的功能
# 1. 参数变量
#     1. 设置需要格式化的裸盘名称
#     2. 设置需要扩展的磁盘目录
# 2. 实际脚本
#     1. 检查脚本需要使用的依赖
#     2. 根据需要扩展的磁盘目录，检测到相应的PV LV名称，磁盘格式为ext4或者XFS
#     3. 需要将新格式化的裸盘，进行GPT格式的修改，PV创建，然后将PV扩展至需要扩展的磁盘目录的LV逻辑卷中
#     4. 检查实际的扩展是否成功，如果失败，恢复新格式化的裸盘清除的功能，恢复至原本状态
