Compare commits

...

6 Commits

Author SHA1 Message Date
da0c105ea2 update README 2026-09-15 21:12:26 +08:00
2fa1eb2ba3 update USAGE.md 2026-09-06 18:27:30 +08:00
8bce42bc84 debug 2026-09-06 13:46:23 +08:00
2957ef1f1e debug 2026-09-06 13:28:50 +08:00
487959f30f debug 2026-09-06 13:17:57 +08:00
7c5ad9b0c4 first commit 2026-09-06 13:05:33 +08:00
9 changed files with 164 additions and 81 deletions

2
.gitignore vendored
View File

@@ -22,3 +22,5 @@ dist-ssr
*.njsproj
*.sln
*.sw?
/debug/
./LinuxDiskAutoExpansion.iml

10
.idea/.gitignore generated vendored
View File

@@ -1,10 +0,0 @@
# 默认忽略的文件
/shelf/
/workspace.xml
# 已忽略包含查询文件的默认文件夹
/queries/
# Datasource local storage ignored files
/dataSources/
/dataSources.local.xml
# 基于编辑器的 HTTP 客户端请求
/httpRequests/

View File

@@ -1,5 +0,0 @@
<component name="ProjectCodeStyleConfiguration">
<state>
<option name="PREFERRED_PROJECT_CODE_STYLE" value="Default" />
</state>
</component>

6
.idea/misc.xml generated
View File

@@ -1,6 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectRootManager" version="2">
<output url="file://$PROJECT_DIR$/out" />
</component>
</project>

8
.idea/modules.xml generated
View File

@@ -1,8 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/LinuxDiskAutoExpansion.iml" filepath="$PROJECT_DIR$/LinuxDiskAutoExpansion.iml" />
</modules>
</component>
</project>

6
.idea/vcs.xml generated
View File

@@ -1,6 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$" vcs="Git" />
</component>
</project>

View File

@@ -28,7 +28,7 @@
```bash
# 下载脚本到服务器
git clone https://github.com/your-repo/LinuxDiskAutoExpansion.git
git clone https://git.opensource-studio.info/database-mysql/LinuxDiskAutoExpansion.git
cd LinuxDiskAutoExpansion
# 模拟运行(推荐先执行,确认扩容计划无误)
@@ -88,17 +88,17 @@ sudo bash main.sh
磁盘: /dev/sda 50GB
┌─────────────────────────────────────────────┐
│ /dev/sda3 30GB (PV) │
│ ┌──────────────────────┬──────────────────┐
│ │ LV root 20GB │ VG Free 10GB │
│ │ (根目录 /) │ (未分配) │
│ └──────────────────────┴──────────────────┘
│ ┌──────────────────────┬──────────────────┐│
│ │ LV root 20GB │ VG Free 10GB ││
│ │ (根目录 /) │ (未分配) ││
│ └──────────────────────┴──────────────────┘│
└─────────────────────────────────────────────┘
↓ 扩容后
┌─────────────────────────────────────────────┐
│ /dev/sda3 50GB (PV → 扩展后) │
│ ┌─────────────────────────────────────────┐
│ │ LV root 50GB (根目录 /) │
│ └─────────────────────────────────────────┘
│ /dev/sda3 50GB (PV → 扩展后)
│ ┌─────────────────────────────────────────┐│
│ │ LV root 50GB (根目录 /) ││
│ └─────────────────────────────────────────┘│
└─────────────────────────────────────────────┘
```
@@ -203,8 +203,13 @@ LVM 场景下VG 的可用空间不足。脚本会先尝试扩展 PV 占满磁
```
LinuxDiskAutoExpansion/
├── src/
│ └── main.sh # 主脚本
├── README.md # 项目说明文档
│ └── main.sh # 主脚本
├── debug/
│ └── logs/ # 调试日志
│ ├── ubuntu.log
│ └── centos.log
├── README.md # 项目说明文档
├── USAGE.md # 详细使用文档
└── LinuxDiskAutoExpansion.iml
```

View File

@@ -88,7 +88,7 @@ sda 8:0 0 50G 0 disk
**方式一Git 克隆(推荐)**
```bash
git clone https://github.com/your-repo/LinuxDiskAutoExpansion.git
git clone https://git.opensource-studio.info/database-mysql/LinuxDiskAutoExpansion.git
cd LinuxDiskAutoExpansion
```
@@ -104,7 +104,7 @@ scp src/main.sh user@server:/tmp/
**方式三:直接下载**
```bash
wget -O /tmp/main.sh https://raw.githubusercontent.com/your-repo/LinuxDiskAutoExpansion/main/src/main.sh
wget -O /tmp/main.sh https://git.opensource-studio.info/database-mysql/LinuxDiskAutoExpansion/src/commit/8bce42bc846f6404052c64a6511a29e1b1bc7d95/src/main.sh
```
### 3.2 设置执行权限(可选)

View File

@@ -131,6 +131,12 @@ ensure_dependencies() {
fi
fi
# 检查 parted 或 sfdisk用于获取分区信息
if ! command -v parted &>/dev/null && ! command -v sfdisk &>/dev/null; then
info "parted/sfdisk 未安装,正在安装 parted..."
install_package "parted"
fi
# 检查 lsblk
if ! command -v lsblk &>/dev/null; then
install_package "util-linux"
@@ -173,7 +179,7 @@ detect_lvm() {
# 二次检测:通过 lsblk 查看 TYPE
if [[ "$LVM_DETECTED" == false ]]; then
local root_type
root_type=$(lsblk -no TYPE "$ROOT_DEVICE" 2>/dev/null | head -1)
root_type=$(lsblk -no TYPE "$ROOT_DEVICE" 2>/dev/null | head -1) || true
if [[ "$root_type" == "lvm" ]]; then
LVM_DETECTED=true
info "检测到 LVM 架构(通过 lsblk"
@@ -192,7 +198,7 @@ get_disk_for_partition() {
local part="$1"
# 通过 lsblk 获取父级磁盘设备
local disk
disk=$(lsblk -no PKNAME "$part" 2>/dev/null | head -1)
disk=$(lsblk -no PKNAME "$part" 2>/dev/null | head -1) || true
if [[ -z "$disk" ]]; then
die "无法获取 $part 的父级磁盘设备"
fi
@@ -206,10 +212,10 @@ get_partition_number() {
local part="$1"
# 从设备名提取分区号,如 /dev/sda1 -> 1, /dev/vda2 -> 2
local num
num=$(echo "$part" | grep -oP '\d+$')
num=$(echo "$part" | grep -oP '\d+$') || true
if [[ -z "$num" ]]; then
# 尝试 p 分隔格式,如 /dev/mmcblk0p1 -> 1
num=$(echo "$part" | grep -oP 'p\K\d+$')
num=$(echo "$part" | grep -oP 'p\K\d+$') || true
fi
if [[ -z "$num" ]]; then
die "无法从 $part 提取分区号"
@@ -232,11 +238,11 @@ print_disk_status() {
if [[ "$LVM_DETECTED" == true ]]; then
info "--- LVM 信息 ---"
info "物理卷 (PV):"
pvs 2>/dev/null | tee -a "$LOG_FILE"
{ pvs 2>/dev/null || true; } | tee -a "$LOG_FILE"
info "卷组 (VG):"
vgs 2>/dev/null | tee -a "$LOG_FILE"
{ vgs 2>/dev/null || true; } | tee -a "$LOG_FILE"
info "逻辑卷 (LV):"
lvs 2>/dev/null | tee -a "$LOG_FILE"
{ lvs 2>/dev/null || true; } | tee -a "$LOG_FILE"
echo ""
fi
info "===================================="
@@ -253,32 +259,127 @@ check_expandable_space() {
fi
}
#---------------------------------------
# 安全提取数值(去除小数/前缀/后缀,兼容 set -e
#---------------------------------------
parse_int() {
# 从字符串中提取整数部分
# 输入: "8217.60" / "<19.00" / "8.22g" / " 100 " / "0" 等
# 输出: 纯整数字符串,如 "8217" / "19" / "8" / "100" / "0"
local val="$1"
# 去除首尾空格、< > 前缀、非数字字符(保留小数点)
val=$(echo "$val" | tr -d '<>' | sed 's/[^0-9.]//g')
if [[ -z "$val" ]] || [[ "$val" == "." ]]; then
echo "0"
return
fi
# 用 awk 取整数部分
echo "$val" | awk '{printf "%d", $1+0}'
}
#---------------------------------------
# 从设备路径解析 LVM 的 VG 和 LV 名称
#---------------------------------------
resolve_lvm_names() {
local device="$1"
# 尝试多种方法从设备路径获取 VG 名和 LV 路径
# 输入: /dev/mapper/cl_centos85-root 或 /dev/dm-0 等
# 输出: 设置 ROOT_VG 和 ROOT_LV_PATH 全局变量
# 方法1: 直接用 lvs 查询(某些系统支持 /dev/mapper/ 路径)
ROOT_VG=$(lvs --noheadings -o vg_name "$device" 2>/dev/null | xargs) || true
if [[ -n "$ROOT_VG" ]]; then
ROOT_LV_PATH="$device"
return 0
fi
# 方法2: 使用 dmsetup splitname最可靠
if command -v dmsetup &>/dev/null && [[ "$device" == /dev/mapper/* ]]; then
local dm_name
dm_name=$(basename "$device")
ROOT_VG=$(dmsetup splitname "$dm_name" -o vg_name --noheadings 2>/dev/null | xargs) || true
local lv_name
lv_name=$(dmsetup splitname "$dm_name" -o lv_name --noheadings 2>/dev/null | xargs) || true
if [[ -n "$ROOT_VG" && -n "$lv_name" ]]; then
ROOT_LV_PATH="${ROOT_VG}/${lv_name}"
return 0
fi
fi
# 方法3: 从 /dev/mapper/ 路径手动解析
# 规则: -- 转义为 -,然后第一个 - 分隔 VG 和 LV
if [[ "$device" == /dev/mapper/* ]]; then
local mapper_name
mapper_name=$(basename "$device")
# 先将 -- 替换为占位符,再将 - 替换为 /,最后将占位符还原为 -
local vg_lv
vg_lv=$(echo "$mapper_name" | sed 's/--/\x00/g' | tr '-' '/' | tr '\0' '-')
ROOT_VG=$(echo "$vg_lv" | cut -d'/' -f1)
local lv_name
lv_name=$(echo "$vg_lv" | cut -d'/' -f2)
if [[ -n "$ROOT_VG" && -n "$lv_name" ]]; then
ROOT_LV_PATH="${ROOT_VG}/${lv_name}"
return 0
fi
fi
# 方法4: 遍历所有 LV查找匹配当前挂载设备的
local all_lvs
all_lvs=$(lvs --noheadings -o lv_path,vg_name,lv_name 2>/dev/null) || true
while IFS= read -r line; do
local lv_p lv_vg lv_name
lv_p=$(echo "$line" | awk '{print $1}')
lv_vg=$(echo "$line" | awk '{print $2}')
lv_name=$(echo "$line" | awk '{print $3}')
# 检查此 LV 是否是根设备
if [[ -L "$device" ]]; then
local real_dev
real_dev=$(readlink -f "$device")
local lv_real
lv_real=$(readlink -f "$lv_p" 2>/dev/null) || true
if [[ "$real_dev" == "$lv_real" ]]; then
ROOT_VG="$lv_vg"
ROOT_LV_PATH="${lv_vg}/${lv_name}"
return 0
fi
fi
done <<< "$all_lvs"
return 1
}
#---------------------------------------
# LVM 模式:检查 VG 剩余空间
#---------------------------------------
check_lvm_space() {
# 获取根设备所在的 VG
ROOT_VG=$(lvs -no vg_name "$ROOT_DEVICE" 2>/dev/null | xargs)
# 解析 VG 和 LV 路径
if ! resolve_lvm_names "$ROOT_DEVICE"; then
die "无法获取根逻辑卷所在的卷组"
fi
if [[ -z "$ROOT_VG" ]]; then
die "无法获取根逻辑卷所在的卷组"
fi
info "根目录所在卷组 (VG): $ROOT_VG"
info "逻辑卷路径: $ROOT_LV_PATH"
# 获取 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)
# 获取 VG 的可用空间(人类可读格式,用于显示
VG_FREE_HUMAN=$(vgs --noheadings -o vg_name,vg_free --units g --nosuffix 2>/dev/null | awk -v vg="$ROOT_VG" '$1==vg{print $2}' | tr -d '<> ') || true
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+')
# 检查 VG 是否有剩余空间(以 MB 为单位判断)
local vg_free_raw
vg_free_raw=$(vgs --noheadings -o vg_name,vg_free --units m --nosuffix 2>/dev/null | awk -v vg="$ROOT_VG" '$1==vg{print $2}') || true
VG_FREE_NUM=$(parse_int "$vg_free_raw")
info "卷组可用空间: ${VG_FREE_NUM}MB"
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)
vg_free_raw=$(vgs --noheadings -o vg_name,vg_free --units m --nosuffix 2>/dev/null | awk -v vg="$ROOT_VG" '$1==vg{print $2}') || true
VG_FREE_NUM=$(parse_int "$vg_free_raw")
VG_FREE_HUMAN=$(vgs --noheadings -o vg_name,vg_free --units g --nosuffix 2>/dev/null | awk -v vg="$ROOT_VG" '$1==vg{print $2}' | tr -d '<> ') || true
if [[ -z "$VG_FREE_NUM" ]] || [[ "$VG_FREE_NUM" -lt 100 ]]; then
die "卷组 $ROOT_VG 没有足够的可用空间进行扩容(需要至少 100MB"
fi
@@ -293,7 +394,7 @@ check_lvm_space() {
expand_pv_if_needed() {
# 获取 VG 对应的 PV
local pv_dev
pv_dev=$(pvs -no pv_name "$ROOT_VG" 2>/dev/null | xargs)
pv_dev=$(pvs --noheadings -o pv_name,vg_name 2>/dev/null | awk -v vg="$ROOT_VG" '$2==vg{print $1}' | head -1 | xargs) || true
if [[ -z "$pv_dev" ]]; then
die "无法获取卷组 $ROOT_VG 的物理卷"
fi
@@ -314,17 +415,22 @@ expand_pv_if_needed() {
# 检查磁盘是否有未分配空间
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)
disk_size=$(blockdev --getsize64 "$parent_disk" 2>/dev/null) || disk_size=0
sector_size=$(blockdev --getss "$parent_disk" 2>/dev/null) || sector_size=512
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 }')
# 获取分区结束扇区(优先使用 parted备选 sfdisk
if command -v parted &>/dev/null; then
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 }')
elif command -v sfdisk &>/dev/null; then
part_end_sector=$(sfdisk -d "$parent_disk" 2>/dev/null | \
awk -F'[=,]' -v num="$part_num" '{for(i=1;i<=NF;i++) if($i ~ /start=/) {split($i,a,"="); if(a[2]+0 > 0) start=a[2]} if($i ~ /size=/) {split($i,b,"="); size=b[2]}} NR==num {print start+size-1}')
fi
if [[ -z "$part_end_sector" ]]; then
warn "无法获取分区结束扇区,尝试直接使用 pvresize..."
@@ -376,14 +482,19 @@ check_partition_space() {
# 获取磁盘和分区的扇区信息
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)
disk_size=$(blockdev --getsize64 "$ROOT_DISK" 2>/dev/null) || disk_size=0
sector_size=$(blockdev --getss "$ROOT_DISK" 2>/dev/null) || sector_size=512
total_sectors=$((disk_size / sector_size))
# 获取分区结束扇区
# 获取分区结束扇区(优先使用 parted备选 sfdisk
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 command -v parted &>/dev/null; then
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 }')
elif command -v sfdisk &>/dev/null; then
part_end_sector=$(sfdisk -d "$ROOT_DISK" 2>/dev/null | \
awk -F'[=,]' -v num="$part_num" '{for(i=1;i<=NF;i++) if($i ~ /start=/) {split($i,a,"="); if(a[2]+0 > 0) start=a[2]} if($i ~ /size=/) {split($i,b,"="); size=b[2]}} NR==num {print start+size-1}')
fi
if [[ -z "$part_end_sector" ]]; then
die "无法获取分区 $ROOT_DEVICE 的结束扇区"
@@ -419,8 +530,8 @@ do_expand() {
do_expand_lvm() {
info "========== 开始 LVM 模式扩容 =========="
# 获取 LV 路径
local lv_path="$ROOT_DEVICE"
# 获取 LV 路径(使用解析后的路径)
local lv_path="${ROOT_LV_PATH:-$ROOT_DEVICE}"
info "目标逻辑卷: $lv_path"
if [[ "$DRY_RUN" == false ]]; then
@@ -514,9 +625,9 @@ verify_result() {
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"
{ pvs 2>/dev/null || true; } | tee -a "$LOG_FILE"
{ vgs 2>/dev/null || true; } | tee -a "$LOG_FILE"
{ lvs 2>/dev/null || true; } | tee -a "$LOG_FILE"
echo ""
fi
info "===================================="