完成 72绵阳项目 71雄安集团监管平台 大量优化更新

This commit is contained in:
zeaslity
2026-02-03 17:07:28 +08:00
parent d962ace967
commit a8f6bda703
93 changed files with 21632 additions and 185 deletions

View File

@@ -0,0 +1,862 @@
#!/usr/bin/env bash
#===============================================================================
# 名称: lvm_extend_with_disk.sh
# 描述: 使用新增裸盘扩展指定挂载目录对应的 LVM LVext4 / 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=()
committed="0"
#===============================================================================
# 函数调用关系图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)。"
if [[ "${committed}" == "1" ]]; then
log_error "检测到已提交变更LV/FS 已扩容),为避免破坏性操作:将跳过自动回滚。"
exit "$exit_code"
fi
log_error "将进行回滚..."
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
# 分区工具:优先 sgdiskgdisk套件否则 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/VGsource=$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"
run_cmd lvextend -l +100%FREE -r "$lv"
# > 关键步骤:一旦走到这里,磁盘变更已提交,不应再做自动回滚
committed="1"
log_info "已提交变更LV/FS 扩容已完成),后续将禁止自动回滚。"
}
#===============================================================================
# 结果校验模块
#===============================================================================
### 校验扩展结果修复PV->VG 校验误判)
# @param mount_path string 挂载目录
# @param lv string LV 路径
# @param part string 新增 PV 分区
# @param vg string VG 名称
# @return 0 成功
# @require df, lvs, vgs, pvs, awk, readlink
verify_result() {
local mount_path="$1"
local lv="$2"
local part="$3"
local vg="$4"
log_info "开始校验扩展结果..."
# 1) PV 是否在 VG 中(结构化解析 + 归一化路径)
local part_real
part_real="$(readlink -f "$part" 2>/dev/null || true)"
[[ -z "$part_real" ]] && part_real="$part"
local found="0"
while IFS='|' read -r pv_name pv_vg; do
pv_name="$(awk '{$1=$1;print}' <<<"${pv_name:-}")"
pv_vg="$(awk '{$1=$1;print}' <<<"${pv_vg:-}")"
[[ -z "$pv_name" || -z "$pv_vg" ]] && continue
local pv_real
pv_real="$(readlink -f "$pv_name" 2>/dev/null || true)"
[[ -z "$pv_real" ]] && pv_real="$pv_name"
if [[ "$pv_real" == "$part_real" && "$pv_vg" == "$vg" ]]; then
found="1"
break
fi
done < <(pvs --noheadings --separator '|' -o pv_name,vg_name 2>/dev/null || true)
if [[ "$found" != "1" ]]; then
log_error "校验失败:新增 PV 未正确加入 VG: part=$part (real=$part_real) vg=$vg"
log_error "诊断信息pvs -o pv_name,vg_name 输出如下(供排查):"
if [[ "$dry_run" == "1" ]]; then
log_info "[DRY-RUN] 跳过诊断输出"
else
pvs --noheadings --separator '|' -o pv_name,vg_name >&2 || true
fi
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"
if ! verify_result "$target_mount" "$lv_path" "$new_part" "$vg_name"; then
log_warn "校验未通过但扩容步骤已完成。请人工复核pvs/vgs/lvs/df。"
exit 0
fi
# 成功后:清空回滚栈(避免 EXIT 误触发回滚;本脚本回滚仅在 ERR trap
rollback_stack=()
log_info "扩展成功mount=$target_mount lv=$lv_path vg=$vg_name new_pv=$new_part"
}
main "$@"
# pvs -o pv_name,vg_name,pv_size,pv_free
# vgs datavg -o vg_name,vg_size,vg_free
# lvs datavg/lvdata -o lv_path,lv_size,seg_count
# df -hT /var/lib/docker
# 综合目的是为了支持linux环境下新增磁盘逻辑卷的拓展请实现如下的功能
# 1. 参数变量
# 1. 设置需要格式化的裸盘名称
# 2. 设置需要扩展的磁盘目录
# 2. 实际脚本
# 1. 检查脚本需要使用的依赖
# 2. 根据需要扩展的磁盘目录检测到相应的PV LV名称磁盘格式为ext4或者XFS
# 3. 需要将新格式化的裸盘进行GPT格式的修改PV创建然后将PV扩展至需要扩展的磁盘目录的LV逻辑卷中
# 4. 检查实际的扩展是否成功,如果失败,恢复新格式化的裸盘清除的功能,恢复至原本状态