first commit

This commit is contained in:
2026-09-06 12:58:04 +08:00
commit 2477352d6f
10 changed files with 1393 additions and 0 deletions

570
src/main.sh Normal file
View File

@@ -0,0 +1,570 @@
#!/usr/bin/env bash
#===============================================================================
# Linux 根目录自动扩容脚本
# 适配: Debian / Ubuntu / CentOS / RHEL / Fedora / AlmaLinux / Rocky 等
# 功能: 自动识别 LVM / 非 LVM 架构,将磁盘未分配空间全部分配给根目录
# 用法: sudo bash main.sh
#===============================================================================
set -euo pipefail
#---------------------------------------
# 全局变量
#---------------------------------------
LOG_FILE="/var/log/disk_auto_expand.log"
ROOT_MOUNT="/"
LVM_DETECTED=false
DRY_RUN=false
#---------------------------------------
# 日志与输出
#---------------------------------------
log() {
local msg="[$(date '+%Y-%m-%d %H:%M:%S')] $*"
echo "$msg" | tee -a "$LOG_FILE"
}
info() { log "[INFO] $*"; }
warn() { log "[WARN] $*"; }
error() { log "[ERROR] $*"; }
die() { error "$@"; exit 1; }
#---------------------------------------
# 权限检查
#---------------------------------------
check_root() {
if [[ $EUID -ne 0 ]]; then
die "此脚本需要 root 权限运行,请使用 sudo bash $0"
fi
}
#---------------------------------------
# 参数解析
#---------------------------------------
parse_args() {
while [[ $# -gt 0 ]]; do
case "$1" in
--dry-run)
DRY_RUN=true
info "当前为模拟运行模式,不会实际执行扩容操作"
shift
;;
-h|--help)
echo "用法: sudo bash $0 [--dry-run]"
echo " --dry-run 模拟运行,不实际执行扩容"
exit 0
;;
*)
die "未知参数: $1,使用 -h 查看帮助"
;;
esac
done
}
#---------------------------------------
# 依赖检查与安装
#---------------------------------------
detect_pkg_manager() {
if command -v apt-get &>/dev/null; then
PKG_MANAGER="apt"
elif command -v yum &>/dev/null; then
PKG_MANAGER="yum"
elif command -v dnf &>/dev/null; then
PKG_MANAGER="dnf"
elif command -v zypper &>/dev/null; then
PKG_MANAGER="zypper"
else
PKG_MANAGER="unknown"
fi
}
install_package() {
local pkg="$1"
if [[ $DRY_RUN == true ]]; then
info "[模拟] 将安装包: $pkg"
return
fi
info "正在安装依赖包: $pkg"
case "$PKG_MANAGER" in
apt)
apt-get update -qq && apt-get install -y -qq "$pkg" >/dev/null 2>&1
;;
yum)
yum install -y -q "$pkg" >/dev/null 2>&1
;;
dnf)
dnf install -y -q "$pkg" >/dev/null 2>&1
;;
zypper)
zypper install -y "$pkg" >/dev/null 2>&1
;;
*)
die "无法识别包管理器,请手动安装: $pkg"
;;
esac
}
ensure_dependencies() {
detect_pkg_manager
info "检测到包管理器: $PKG_MANAGER"
# 检查 growpart用于非 LVM 分区扩展)
if ! command -v growpart &>/dev/null; then
info "growpart 未安装,正在安装 cloud-guest-utils / cloud-utils-growpart..."
# 不同发行版包名不同,依次尝试
if [[ "$PKG_MANAGER" == "apt" ]]; then
install_package "cloud-guest-utils"
else
# CentOS/RHEL 系
if ! install_package "cloud-utils-growpart" 2>/dev/null; then
install_package "cloud-init" || warn "无法安装 growpart非 LVM 模式可能不可用"
fi
fi
fi
# 检查 xfs_growfsXFS 文件系统需要)
if ! command -v xfs_growfs &>/dev/null; then
if [[ "$PKG_MANAGER" == "apt" ]]; then
install_package "xfsprogs"
else
install_package "xfsprogs"
fi
fi
# 检查 lsblk
if ! command -v lsblk &>/dev/null; then
install_package "util-linux"
fi
}
#---------------------------------------
# 获取根分区信息
#---------------------------------------
get_root_info() {
# 获取根目录所在的设备(如 /dev/sda1, /dev/mapper/xxx
ROOT_DEVICE=$(findmnt -n -o SOURCE "$ROOT_MOUNT")
if [[ -z "$ROOT_DEVICE" ]]; then
die "无法获取根目录所在设备"
fi
info "根目录所在设备: $ROOT_DEVICE"
# 获取文件系统类型
ROOT_FS=$(findmnt -n -o FSTYPE "$ROOT_MOUNT")
info "根目录文件系统: $ROOT_FS"
if [[ "$ROOT_FS" != "ext4" && "$ROOT_FS" != "ext3" && "$ROOT_FS" != "xfs" && "$ROOT_FS" != "btrfs" ]]; then
warn "文件系统类型 $ROOT_FS 可能不支持在线扩容,脚本将继续尝试"
fi
}
#---------------------------------------
# 检测 LVM
#---------------------------------------
detect_lvm() {
# 判断根设备是否为 LVM 逻辑卷
if [[ "$ROOT_DEVICE" == /dev/mapper/* ]] || [[ "$ROOT_DEVICE" == /dev/dm-* ]]; then
# 进一步确认是 LVM 而非其他映射设备
if lvs "$ROOT_DEVICE" &>/dev/null; then
LVM_DETECTED=true
info "检测到 LVM 架构"
fi
fi
# 二次检测:通过 lsblk 查看 TYPE
if [[ "$LVM_DETECTED" == false ]]; then
local root_type
root_type=$(lsblk -no TYPE "$ROOT_DEVICE" 2>/dev/null | head -1)
if [[ "$root_type" == "lvm" ]]; then
LVM_DETECTED=true
info "检测到 LVM 架构(通过 lsblk"
fi
fi
if [[ "$LVM_DETECTED" == false ]]; then
info "未检测到 LVM将使用标准分区扩容方式"
fi
}
#---------------------------------------
# 获取磁盘和分区信息
#---------------------------------------
get_disk_for_partition() {
local part="$1"
# 通过 lsblk 获取父级磁盘设备
local disk
disk=$(lsblk -no PKNAME "$part" 2>/dev/null | head -1)
if [[ -z "$disk" ]]; then
die "无法获取 $part 的父级磁盘设备"
fi
echo "/dev/$disk"
}
#---------------------------------------
# 获取分区号
#---------------------------------------
get_partition_number() {
local part="$1"
# 从设备名提取分区号,如 /dev/sda1 -> 1, /dev/vda2 -> 2
local num
num=$(echo "$part" | grep -oP '\d+$')
if [[ -z "$num" ]]; then
# 尝试 p 分隔格式,如 /dev/mmcblk0p1 -> 1
num=$(echo "$part" | grep -oP 'p\K\d+$')
fi
if [[ -z "$num" ]]; then
die "无法从 $part 提取分区号"
fi
echo "$num"
}
#---------------------------------------
# 打印当前磁盘使用情况
#---------------------------------------
print_disk_status() {
info "========== 当前磁盘状态 =========="
echo ""
info "--- 磁盘分区表 ---"
lsblk -o NAME,SIZE,TYPE,FSTYPE,MOUNTPOINT 2>/dev/null | tee -a "$LOG_FILE"
echo ""
info "--- 根目录文件系统 ---"
df -hT "$ROOT_MOUNT" 2>/dev/null | tee -a "$LOG_FILE"
echo ""
if [[ "$LVM_DETECTED" == true ]]; then
info "--- LVM 信息 ---"
info "物理卷 (PV):"
pvs 2>/dev/null | tee -a "$LOG_FILE"
info "卷组 (VG):"
vgs 2>/dev/null | tee -a "$LOG_FILE"
info "逻辑卷 (LV):"
lvs 2>/dev/null | tee -a "$LOG_FILE"
echo ""
fi
info "===================================="
}
#---------------------------------------
# 检查是否有可扩容的空间
#---------------------------------------
check_expandable_space() {
if [[ "$LVM_DETECTED" == true ]]; then
check_lvm_space
else
check_partition_space
fi
}
#---------------------------------------
# LVM 模式:检查 VG 剩余空间
#---------------------------------------
check_lvm_space() {
# 获取根设备所在的 VG
ROOT_VG=$(lvs -no vg_name "$ROOT_DEVICE" 2>/dev/null | xargs)
if [[ -z "$ROOT_VG" ]]; then
die "无法获取根逻辑卷所在的卷组"
fi
info "根目录所在卷组 (VG): $ROOT_VG"
# 获取 VG 的可用空间(单位:字节)
VG_FREE_BYTES=$(vgs -no vg_free "$ROOT_VG" --units b 2>/dev/null | xargs)
# 转换为人类可读格式
VG_FREE_HUMAN=$(vgs -no vg_free "$ROOT_VG" --units g 2>/dev/null | xargs)
info "卷组 $ROOT_VG 可用空间: ${VG_FREE_HUMAN}G"
# 检查 VG 是否有剩余空间
VG_FREE_NUM=$(vgs -no vg_free "$ROOT_VG" --units m 2>/dev/null | xargs | grep -oP '^\d+')
if [[ -z "$VG_FREE_NUM" ]] || [[ "$VG_FREE_NUM" -lt 100 ]]; then
# VG 没有空间,检查是否是因为 PV 没有占满磁盘
info "卷组可用空间不足,将检查是否需要扩展物理卷 (PV)..."
expand_pv_if_needed
# 重新获取可用空间
VG_FREE_NUM=$(vgs -no vg_free "$ROOT_VG" --units m 2>/dev/null | xargs | grep -oP '^\d+')
VG_FREE_HUMAN=$(vgs -no vg_free "$ROOT_VG" --units g 2>/dev/null | xargs)
if [[ -z "$VG_FREE_NUM" ]] || [[ "$VG_FREE_NUM" -lt 100 ]]; then
die "卷组 $ROOT_VG 没有足够的可用空间进行扩容(需要至少 100MB"
fi
fi
info "可用于扩容的空间: ${VG_FREE_HUMAN}G"
}
#---------------------------------------
# 扩展 PV 使其占满磁盘
#---------------------------------------
expand_pv_if_needed() {
# 获取 VG 对应的 PV
local pv_dev
pv_dev=$(pvs -no pv_name "$ROOT_VG" 2>/dev/null | xargs)
if [[ -z "$pv_dev" ]]; then
die "无法获取卷组 $ROOT_VG 的物理卷"
fi
info "当前物理卷 (PV): $pv_dev"
# 判断 PV 是直接磁盘还是分区
local pv_is_partition=false
local parent_disk
# 如果 PV 是分区(如 /dev/sda2需要扩展分区
if [[ "$pv_dev" =~ [0-9]+$ ]] || [[ "$pv_dev" =~ p[0-9]+$ ]]; then
pv_is_partition=true
parent_disk=$(get_disk_for_partition "$pv_dev")
info "PV 是分区 $pv_dev,父级磁盘: $parent_disk"
else
parent_disk="$pv_dev"
info "PV 直接使用整块磁盘: $pv_dev"
fi
# 检查磁盘是否有未分配空间
local disk_size sector_size total_sectors part_end_sector
disk_size=$(blockdev --getsize64 "$parent_disk" 2>/dev/null)
sector_size=$(blockdev --getss "$parent_disk" 2>/dev/null)
total_sectors=$((disk_size / sector_size))
if [[ "$pv_is_partition" == true ]]; then
local part_num
part_num=$(get_partition_number "$pv_dev")
# 获取分区结束扇区
part_end_sector=$(parted -s "$parent_disk" unit s print 2>/dev/null | \
awk -v num="$part_num" '$1 == num { gsub(/s/,"",$3); print $3 }')
if [[ -z "$part_end_sector" ]]; then
warn "无法获取分区结束扇区,尝试直接使用 pvresize..."
if [[ "$DRY_RUN" == false ]]; then
pvresize "$pv_dev" 2>/dev/null || true
fi
return
fi
# 检查分区后面是否有空间(至少 2048 扇区 = 1MB
local remaining_sectors=$((total_sectors - part_end_sector - 1))
info "磁盘总扇区: $total_sectors, 分区结束扇区: $part_end_sector, 剩余扇区: $remaining_sectors"
if [[ $remaining_sectors -gt 2048 ]]; then
info "检测到磁盘有未分配空间,正在扩展分区 $pv_dev..."
if [[ "$DRY_RUN" == false ]]; then
# 使用 growpart 扩展分区
local disk_base part_no
disk_base=$(basename "$parent_disk")
part_no="$part_num"
growpart "$parent_disk" "$part_no" || warn "growpart 执行失败,可能分区已是最大"
# 扩展 PV
pvresize "$pv_dev" || warn "pvresize 执行失败"
else
info "[模拟] 将扩展分区 $pv_dev 并执行 pvresize"
fi
else
info "分区已占满磁盘,无需扩展分区"
fi
else
# PV 直接使用整块磁盘,直接 pvresize
info "PV 使用整块磁盘,执行 pvresize..."
if [[ "$DRY_RUN" == false ]]; then
pvresize "$pv_dev" || warn "pvresize 执行失败"
fi
fi
}
#---------------------------------------
# 非 LVM 模式:检查分区后是否有空间
#---------------------------------------
check_partition_space() {
ROOT_DISK=$(get_disk_for_partition "$ROOT_DEVICE")
info "根目录所在磁盘: $ROOT_DISK"
local part_num
part_num=$(get_partition_number "$ROOT_DEVICE")
info "根分区: $ROOT_DEVICE (分区号: $part_num)"
# 获取磁盘和分区的扇区信息
local disk_size sector_size total_sectors
disk_size=$(blockdev --getsize64 "$ROOT_DISK" 2>/dev/null)
sector_size=$(blockdev --getss "$ROOT_DISK" 2>/dev/null)
total_sectors=$((disk_size / sector_size))
# 获取分区结束扇区
local part_end_sector
part_end_sector=$(parted -s "$ROOT_DISK" unit s print 2>/dev/null | \
awk -v num="$part_num" '$1 == num { gsub(/s/,"",$3); print $3 }')
if [[ -z "$part_end_sector" ]]; then
die "无法获取分区 $ROOT_DEVICE 的结束扇区"
fi
local remaining_sectors=$((total_sectors - part_end_sector - 1))
local remaining_mb=$((remaining_sectors * sector_size / 1024 / 1024))
info "磁盘总大小: $((disk_size / 1024 / 1024 / 1024))GB"
info "分区后剩余扇区: $remaining_sectors (约 ${remaining_mb}MB)"
if [[ $remaining_sectors -le 2048 ]]; then
die "磁盘没有足够的未分配空间进行扩容"
fi
info "检测到约 ${remaining_mb}MB 未分配空间,可以进行扩容"
}
#---------------------------------------
# 执行扩容
#---------------------------------------
do_expand() {
if [[ "$LVM_DETECTED" == true ]]; then
do_expand_lvm
else
do_expand_partition
fi
}
#---------------------------------------
# LVM 模式扩容
#---------------------------------------
do_expand_lvm() {
info "========== 开始 LVM 模式扩容 =========="
# 获取 LV 路径
local lv_path="$ROOT_DEVICE"
info "目标逻辑卷: $lv_path"
if [[ "$DRY_RUN" == false ]]; then
# 扩展 LV 使用 VG 中所有剩余空间
info "正在扩展逻辑卷 $lv_path 使用全部可用空间..."
lvextend -l +100%FREE "$lv_path" 2>&1 | tee -a "$LOG_FILE"
# 扩展文件系统
resize_filesystem "$ROOT_DEVICE" "$ROOT_FS"
info "LVM 模式扩容完成!"
else
info "[模拟] 将执行: lvextend -l +100%FREE $lv_path"
info "[模拟] 将执行文件系统扩容: $ROOT_FS on $ROOT_DEVICE"
fi
}
#---------------------------------------
# 非 LVM 模式扩容
#---------------------------------------
do_expand_partition() {
info "========== 开始标准分区模式扩容 =========="
local part_num
part_num=$(get_partition_number "$ROOT_DEVICE")
if [[ "$DRY_RUN" == false ]]; then
# 第一步:扩展分区
info "正在扩展分区 $ROOT_DEVICE..."
growpart "$ROOT_DISK" "$part_num" 2>&1 | tee -a "$LOG_FILE"
# 第二步:扩展文件系统
resize_filesystem "$ROOT_DEVICE" "$ROOT_FS"
info "标准分区模式扩容完成!"
else
info "[模拟] 将执行: growpart $ROOT_DISK $part_num"
info "[模拟] 将执行文件系统扩容: $ROOT_FS on $ROOT_DEVICE"
fi
}
#---------------------------------------
# 扩展文件系统
#---------------------------------------
resize_filesystem() {
local device="$1"
local fs_type="$2"
info "正在扩展文件系统 (类型: $fs_type, 设备: $device)..."
case "$fs_type" in
ext2|ext3|ext4)
if [[ "$DRY_RUN" == false ]]; then
resize2fs "$device" 2>&1 | tee -a "$LOG_FILE"
else
info "[模拟] 将执行: resize2fs $device"
fi
;;
xfs)
if [[ "$DRY_RUN" == false ]]; then
xfs_growfs "$ROOT_MOUNT" 2>&1 | tee -a "$LOG_FILE"
else
info "[模拟] 将执行: xfs_growfs $ROOT_MOUNT"
fi
;;
btrfs)
if [[ "$DRY_RUN" == false ]]; then
btrfs filesystem resize max "$ROOT_MOUNT" 2>&1 | tee -a "$LOG_FILE"
else
info "[模拟] 将执行: btrfs filesystem resize max $ROOT_MOUNT"
fi
;;
*)
warn "不支持的文件系统类型: $fs_type,请手动扩展"
return 1
;;
esac
}
#---------------------------------------
# 验证扩容结果
#---------------------------------------
verify_result() {
info "========== 扩容后磁盘状态 =========="
echo ""
info "--- 根目录使用情况 ---"
df -hT "$ROOT_MOUNT" 2>/dev/null | tee -a "$LOG_FILE"
echo ""
info "--- 磁盘分区表 ---"
lsblk -o NAME,SIZE,TYPE,FSTYPE,MOUNTPOINT 2>/dev/null | tee -a "$LOG_FILE"
echo ""
if [[ "$LVM_DETECTED" == true ]]; then
info "--- LVM 信息 ---"
pvs 2>/dev/null | tee -a "$LOG_FILE"
vgs 2>/dev/null | tee -a "$LOG_FILE"
lvs 2>/dev/null | tee -a "$LOG_FILE"
echo ""
fi
info "===================================="
}
#---------------------------------------
# 主函数
#---------------------------------------
main() {
parse_args "$@"
echo ""
echo "========================================================"
echo " Linux 根目录自动扩容脚本"
echo " 适配 Debian / Ubuntu / CentOS 等"
echo "========================================================"
echo ""
# 1. 权限检查
check_root
# 2. 安装依赖
info "检查并安装必要依赖..."
ensure_dependencies
# 3. 获取根分区信息
info "获取根分区信息..."
get_root_info
# 4. 检测 LVM
detect_lvm
# 5. 打印当前状态
print_disk_status
# 6. 检查可扩容空间
info "检查可扩容空间..."
check_expandable_space
# 7. 执行扩容
do_expand
# 8. 验证结果
verify_result
info "根目录自动扩容完成!"
info "日志文件: $LOG_FILE"
echo ""
}
main "$@"