Compare commits
6 Commits
1b8f8e1e59
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| da0c105ea2 | |||
| 2fa1eb2ba3 | |||
| 8bce42bc84 | |||
| 2957ef1f1e | |||
| 487959f30f | |||
| 7c5ad9b0c4 |
2
.gitignore
vendored
2
.gitignore
vendored
@@ -22,3 +22,5 @@ dist-ssr
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
/debug/
|
||||
./LinuxDiskAutoExpansion.iml
|
||||
|
||||
10
.idea/.gitignore
generated
vendored
10
.idea/.gitignore
generated
vendored
@@ -1,10 +0,0 @@
|
||||
# 默认忽略的文件
|
||||
/shelf/
|
||||
/workspace.xml
|
||||
# 已忽略包含查询文件的默认文件夹
|
||||
/queries/
|
||||
# Datasource local storage ignored files
|
||||
/dataSources/
|
||||
/dataSources.local.xml
|
||||
# 基于编辑器的 HTTP 客户端请求
|
||||
/httpRequests/
|
||||
5
.idea/codeStyles/codeStyleConfig.xml
generated
5
.idea/codeStyles/codeStyleConfig.xml
generated
@@ -1,5 +0,0 @@
|
||||
<component name="ProjectCodeStyleConfiguration">
|
||||
<state>
|
||||
<option name="PREFERRED_PROJECT_CODE_STYLE" value="Default" />
|
||||
</state>
|
||||
</component>
|
||||
6
.idea/misc.xml
generated
6
.idea/misc.xml
generated
@@ -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
8
.idea/modules.xml
generated
@@ -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
6
.idea/vcs.xml
generated
@@ -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>
|
||||
21
README.md
21
README.md
@@ -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 (根目录 /) │ │
|
||||
│ └─────────────────────────────────────────┘ │
|
||||
│ ┌─────────────────────────────────────────┐│
|
||||
│ │ LV root 50GB (根目录 /) ││
|
||||
│ └─────────────────────────────────────────┘│
|
||||
└─────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
@@ -204,7 +204,12 @@ LVM 场景下,VG 的可用空间不足。脚本会先尝试扩展 PV 占满磁
|
||||
LinuxDiskAutoExpansion/
|
||||
├── src/
|
||||
│ └── main.sh # 主脚本
|
||||
├── debug/
|
||||
│ └── logs/ # 调试日志
|
||||
│ ├── ubuntu.log
|
||||
│ └── centos.log
|
||||
├── README.md # 项目说明文档
|
||||
├── USAGE.md # 详细使用文档
|
||||
└── LinuxDiskAutoExpansion.iml
|
||||
```
|
||||
|
||||
|
||||
4
USAGE.md
4
USAGE.md
@@ -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 设置执行权限(可选)
|
||||
|
||||
169
src/main.sh
169
src/main.sh
@@ -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")
|
||||
|
||||
# 获取分区结束扇区
|
||||
# 获取分区结束扇区(优先使用 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
|
||||
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 "===================================="
|
||||
|
||||
Reference in New Issue
Block a user