#!/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=() 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 < 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" 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. 检查实际的扩展是否成功,如果失败,恢复新格式化的裸盘清除的功能,恢复至原本状态