Files
PVE-Tools-9/src/modules/05-vm-container/garbage-cleanup.sh
Maple d8b3d9fbdf feat: release v11.0.0 Liino — menu framework, risk guards, CI gates
Unify all menus on lib/menu.sh (run_menu), harden GPU/GRUB writes with
two-tier confirms and markers, fix remote/self-update to Releases assets,
and close real CI quality gates with SHA256SUMS. Bump to 11.0.0 / Liino.
2026-07-28 14:43:58 +08:00

486 lines
20 KiB
Bash
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/bin/bash
# SPDX-License-Identifier: GPL-3.0-only
# Copyright (C) 2026 Ciriu Networks
garbage_cleanup_sum_sizes() {
awk -F'|' '{sum += $2} END {print sum + 0}'
}
garbage_cleanup_count_records() {
awk 'NF {count++} END {print count + 0}'
}
garbage_cleanup_temp_file_candidates() {
local age_days="${1:-3}"
[[ "$age_days" =~ ^[0-9]+$ ]] || age_days=3
if [[ -d /tmp ]]; then
find /tmp -maxdepth 1 -type f \( \
-name 'pve-tools-*' -o \
-name 'pve-third-party-*' -o \
-name 'fastpve-install.*.sh' -o \
-name 'pve8to9_check.log' -o \
-name 'pve-qemu-kvm.deb' \
\) -mtime +"$age_days" -printf '%p|%s|%TY-%Tm-%Td %TH:%TM|临时文件\n' 2>/dev/null || true
fi
}
garbage_cleanup_pve_tools_old_file_candidates() {
local age_days="${1:-90}"
local root
[[ "$age_days" =~ ^[0-9]+$ ]] || age_days=90
for root in "/var/backups/pve-tools" "$VM_CONFIG_EXPORT_DIR"; do
if [[ -d "$root" ]]; then
find "$root" -type f -mtime +"$age_days" -printf '%p|%s|%TY-%Tm-%Td %TH:%TM|PVE-Tools 旧文件\n' 2>/dev/null || true
fi
done
}
garbage_cleanup_print_file_records() {
local records="$1"
local idx=1 path size mtime reason
printf "%-5s %-10s %-16s %-18s %s\n" "序号" "大小" "时间" "类型" "文件"
echo "$UI_DIVIDER"
while IFS='|' read -r path size mtime reason; do
[[ -n "$path" ]] || continue
printf "%-5s %-10s %-16s %-18s %s\n" "$idx" "$(pve_tools_human_bytes "$size")" "$mtime" "$reason" "$path"
idx=$((idx + 1))
done <<< "$records"
}
garbage_cleanup_delete_file_records() {
local records="$1"
local success=0 failed=0 skipped=0 path size mtime reason mode answer
if [[ -z "$records" ]]; then
log_warn "没有可删除的候选文件。"
return 0
fi
read -p "删除模式:输入 all 批量删除,输入 item 逐项确认 [item]: " mode
mode="${mode:-item}"
[[ "$mode" == "all" || "$mode" == "item" ]] || mode="item"
while IFS='|' read -r path size mtime reason; do
[[ -n "$path" && -f "$path" ]] || {
skipped=$((skipped + 1))
continue
}
if [[ "$mode" == "item" ]]; then
read -p "删除 $path ? 输入 yes 确认: " answer
if [[ "$answer" != "yes" && "$answer" != "YES" ]]; then
skipped=$((skipped + 1))
continue
fi
fi
if rm -f -- "$path"; then
log_success "已删除: $path"
success=$((success + 1))
else
log_error "删除失败: $path"
failed=$((failed + 1))
fi
done <<< "$records"
log_info "删除完成:成功 $success,失败 $failed,跳过 $skipped"
}
garbage_cleanup_basic() {
block_non_pve9_destructive "垃圾清理(缓存/日志/临时文件)" || return 1
local temp_age journal_days pve_age auto_remove records total_size
read -p "清理 /tmp 中超过多少天的 PVE-Tools 临时文件 [3]: " temp_age
temp_age="${temp_age:-3}"
[[ "$temp_age" =~ ^[0-9]+$ ]] || temp_age=3
read -p "systemd-journal 保留天数 [14]: " journal_days
journal_days="${journal_days:-14}"
[[ "$journal_days" =~ ^[0-9]+$ ]] || journal_days=14
read -p "PVE-Tools 备份/导出文件保留天数 [90]: " pve_age
pve_age="${pve_age:-90}"
[[ "$pve_age" =~ ^[0-9]+$ ]] || pve_age=90
read -p "是否执行 apt autoremove 清理孤立依赖?输入 yes 启用 [no]: " auto_remove
auto_remove="${auto_remove:-no}"
records="$(
garbage_cleanup_temp_file_candidates "$temp_age"
garbage_cleanup_pve_tools_old_file_candidates "$pve_age"
)"
total_size="$(echo "$records" | garbage_cleanup_sum_sizes)"
clear
show_menu_header "垃圾清理预览"
echo -e "${CYAN}将执行:${NC}"
echo " - apt-get autoclean 清理过期软件包缓存"
echo " - journalctl --vacuum-time=${journal_days}d 压缩系统日志保留窗口"
echo " - 删除超过 ${temp_age} 天的 PVE-Tools 临时文件"
echo " - 删除超过 ${pve_age} 天的 PVE-Tools 备份/导出文件"
[[ "$auto_remove" == "yes" || "$auto_remove" == "YES" ]] && echo " - apt-get autoremove -y 清理孤立依赖"
echo "$UI_DIVIDER"
if [[ -n "$records" ]]; then
garbage_cleanup_print_file_records "$records"
echo "$UI_DIVIDER"
echo -e "${YELLOW}候选文件合计:${NC} $(pve_tools_human_bytes "$total_size")"
else
echo " 未发现符合年龄条件的 PVE-Tools 临时/旧文件。"
fi
if ! confirm_high_risk_action "执行垃圾清理" "会删除上方列出的本工具临时/旧文件,并清理 apt 缓存与 journal 日志窗口。" "文件删除不可逆apt autoremove 如果启用,可能移除系统认为不再需要的依赖包。" "请确认候选列表只包含可丢弃文件,重要备份已另存。" "CLEAN"; then
return 0
fi
log_step "清理 apt 过期缓存..."
apt-get autoclean -y || log_warn "apt-get autoclean 执行失败,请检查 apt 状态。"
if [[ "$auto_remove" == "yes" || "$auto_remove" == "YES" ]]; then
log_step "清理孤立依赖..."
apt-get autoremove -y || log_warn "apt-get autoremove 执行失败,请检查 apt 状态。"
fi
if command -v journalctl >/dev/null 2>&1; then
log_step "压缩 systemd journal 日志..."
journalctl --vacuum-time="${journal_days}d" || log_warn "journalctl 日志清理失败。"
fi
garbage_cleanup_delete_file_records "$records"
}
garbage_cleanup_backup_candidates() {
local mode="${1:-both}"
local age_days="${2:-180}"
local now cutoff path size mtime type vmid epoch old orphan reason
[[ "$age_days" =~ ^[0-9]+$ ]] || age_days=180
now="$(date +%s)"
cutoff=$((now - age_days * 86400))
while IFS='|' read -r path size mtime; do
[[ -n "$path" && -f "$path" ]] || continue
type="$(vm_backup_archive_guest_type "$path")"
vmid="$(vm_backup_archive_vmid "$path")"
epoch="$(stat -c '%Y' "$path" 2>/dev/null || echo 0)"
old=0
orphan=0
reason=""
if [[ "$epoch" =~ ^[0-9]+$ ]] && (( epoch > 0 && epoch < cutoff )); then
old=1
reason="超过 ${age_days}"
fi
if [[ -n "$vmid" && "$type" != "未知" ]] && ! pve_guest_exists "$type" "$vmid"; then
orphan=1
[[ -n "$reason" ]] && reason+=","
reason+="无对应 ${type} ${vmid}"
fi
case "$mode" in
old) (( old == 1 )) || continue ;;
orphan) (( orphan == 1 )) || continue ;;
both|*) (( old == 1 || orphan == 1 )) || continue ;;
esac
printf '%s|%s|%s|%s|%s|%s\n' "$path" "$size" "$mtime" "$type" "${vmid:-?}" "$reason"
done < <(vm_discover_all_backup_archives)
}
garbage_cleanup_print_backup_records() {
local records="$1"
local idx=1 path size mtime type vmid reason
printf "%-5s %-5s %-7s %-10s %-16s %-24s %s\n" "序号" "类型" "VMID" "大小" "时间" "原因" "文件"
echo "$UI_DIVIDER"
while IFS='|' read -r path size mtime type vmid reason; do
[[ -n "$path" ]] || continue
printf "%-5s %-5s %-7s %-10s %-16s %-24s %s\n" "$idx" "$type" "$vmid" "$(pve_tools_human_bytes "$size")" "$mtime" "$reason" "$path"
idx=$((idx + 1))
done <<< "$records"
}
garbage_cleanup_delete_backup_records() {
local records="$1"
local success=0 failed=0 skipped=0 path size mtime type vmid reason mode answer
if [[ -z "$records" ]]; then
log_warn "没有可删除的备份候选。"
return 0
fi
read -p "删除模式:输入 all 批量删除,输入 item 逐项确认 [item]: " mode
mode="${mode:-item}"
[[ "$mode" == "all" || "$mode" == "item" ]] || mode="item"
while IFS='|' read -r path size mtime type vmid reason; do
[[ -n "$path" && -f "$path" ]] || {
skipped=$((skipped + 1))
continue
}
if [[ "$mode" == "item" ]]; then
echo "备份: ${type:-?} ${vmid:-?} | $(pve_tools_human_bytes "$size") | ${reason:-未标注原因}"
read -p "删除 $path ? 输入 yes 确认: " answer
if [[ "$answer" != "yes" && "$answer" != "YES" ]]; then
skipped=$((skipped + 1))
continue
fi
fi
if rm -f -- "$path"; then
log_success "已删除备份: $path"
success=$((success + 1))
else
log_error "备份删除失败: $path"
failed=$((failed + 1))
fi
done <<< "$records"
log_info "备份清理完成:成功 $success,失败 $failed,跳过 $skipped"
}
garbage_cleanup_prune_backups() {
block_non_pve9_destructive "清理过期/无主备份文件" || return 1
vm_require_commands pvesm || return 1
local scope age_days records total_size
clear
show_menu_header "备份文件清理"
show_menu_option "1" "只列出无对应 VM/CT 的备份"
show_menu_option "2" "只列出超过指定天数的备份"
show_menu_option "3" "同时列出无主备份和过期备份"
echo "$UI_DIVIDER"
read -p "请选择筛选范围 [3]: " scope
scope="${scope:-3}"
case "$scope" in
1) scope="orphan" ;;
2) scope="old" ;;
*) scope="both" ;;
esac
read -p "过期备份阈值天数 [180]: " age_days
age_days="${age_days:-180}"
[[ "$age_days" =~ ^[0-9]+$ ]] || age_days=180
records="$(garbage_cleanup_backup_candidates "$scope" "$age_days")"
clear
show_menu_header "备份文件清理预览"
if [[ -z "$records" ]]; then
log_warn "未发现符合条件的备份文件。"
return 0
fi
garbage_cleanup_print_backup_records "$records"
total_size="$(echo "$records" | garbage_cleanup_sum_sizes)"
echo "$UI_DIVIDER"
echo -e "${YELLOW}候选备份合计:${NC} $(pve_tools_human_bytes "$total_size")"
echo -e "${YELLOW}提醒:${NC} NFS/CIFS 共享备份可能仍被其他节点使用,删除前请确认跨机恢复需求。"
if ! confirm_high_risk_action "删除上方列出的 vzdump 备份文件" "会永久删除备份文件,删除后无法通过这些备份恢复 VM/CT。" "如果误删最后一个可用备份,后续故障将缺少恢复点。" "请确认已有其他可用备份或确定这些备份不再需要。" "DELETE-BACKUP"; then
return 0
fi
garbage_cleanup_delete_backup_records "$records"
}
garbage_cleanup_snapshot_candidates() {
local age_days="${1:-90}"
local cutoff conf vmid guest_type
[[ "$age_days" =~ ^[0-9]+$ ]] || age_days=90
cutoff=$(($(date +%s) - age_days * 86400))
for conf in /etc/pve/qemu-server/*.conf /etc/pve/lxc/*.conf; do
[[ -f "$conf" ]] || continue
vmid="$(basename "$conf" .conf)"
guest_type="VM"
[[ "$conf" == /etc/pve/lxc/* ]] && guest_type="CT"
awk -v guest_type="$guest_type" -v vmid="$vmid" -v cutoff="$cutoff" '
/^\[[^]]+\]$/ {
snap = $0
gsub(/^\[/, "", snap)
gsub(/\]$/, "", snap)
next
}
snap != "" && /^snaptime:[[:space:]]*[0-9]+/ {
ts = $2
if (ts < cutoff) {
print guest_type "|" vmid "|" snap "|" ts
}
snap = ""
}
' "$conf"
done
}
garbage_cleanup_prune_snapshots() {
block_non_pve9_destructive "清理旧快照" || return 1
local age_days records count idx guest_type vmid snapshot_name epoch time_text mode answer success=0 failed=0 skipped=0
read -p "列出超过多少天的快照 [90]: " age_days
age_days="${age_days:-90}"
[[ "$age_days" =~ ^[0-9]+$ ]] || age_days=90
records="$(garbage_cleanup_snapshot_candidates "$age_days")"
clear
show_menu_header "旧快照清理预览"
if [[ -z "$records" ]]; then
log_warn "未发现超过 ${age_days} 天且带 snaptime 的 VM/CT 快照。"
return 0
fi
printf "%-5s %-5s %-7s %-24s %-20s\n" "序号" "类型" "VMID" "快照名" "创建时间"
echo "$UI_DIVIDER"
idx=1
while IFS='|' read -r guest_type vmid snapshot_name epoch; do
[[ -n "$snapshot_name" ]] || continue
time_text="$(date -d "@$epoch" '+%F %T' 2>/dev/null || echo "$epoch")"
printf "%-5s %-5s %-7s %-24s %-20s\n" "$idx" "$guest_type" "$vmid" "$snapshot_name" "$time_text"
idx=$((idx + 1))
done <<< "$records"
count="$(echo "$records" | garbage_cleanup_count_records)"
echo "$UI_DIVIDER"
echo -e "${YELLOW}候选快照数量:${NC} $count"
if ! confirm_high_risk_action "删除上方列出的旧快照" "删除快照后将失去对应时间点的快速回滚能力。" "如果快照是业务回退基线,误删后只能依赖外部备份恢复。" "请确认这些快照已经过期,且已有必要的外部备份。" "DELETE-SNAP"; then
return 0
fi
read -p "删除模式:输入 all 批量删除,输入 item 逐项确认 [item]: " mode
mode="${mode:-item}"
[[ "$mode" == "all" || "$mode" == "item" ]] || mode="item"
while IFS='|' read -r guest_type vmid snapshot_name epoch; do
[[ -n "$snapshot_name" ]] || continue
if [[ "$mode" == "item" ]]; then
read -p "删除 ${guest_type} ${vmid} 快照 ${snapshot_name} ? 输入 yes 确认: " answer
if [[ "$answer" != "yes" && "$answer" != "YES" ]]; then
skipped=$((skipped + 1))
continue
fi
fi
if [[ "$guest_type" == "VM" ]]; then
if qm delsnapshot "$vmid" "$snapshot_name" >/dev/null 2>&1; then
log_success "已删除 VM $vmid 快照: $snapshot_name"
success=$((success + 1))
else
log_error "删除 VM $vmid 快照失败: $snapshot_name"
failed=$((failed + 1))
fi
else
if pct delsnapshot "$vmid" "$snapshot_name" >/dev/null 2>&1; then
log_success "已删除 CT $vmid 快照: $snapshot_name"
success=$((success + 1))
else
log_error "删除 CT $vmid 快照失败: $snapshot_name"
failed=$((failed + 1))
fi
fi
done <<< "$records"
log_info "快照清理完成:成功 $success,失败 $failed,跳过 $skipped"
}
garbage_cleanup_collect_referenced_volumes() {
{
grep -hoE '[A-Za-z0-9_.-]+:(vm|base|subvol)-[0-9]+-[^,[:space:]]+' /etc/pve/qemu-server/*.conf 2>/dev/null || true
grep -hoE '[A-Za-z0-9_.-]+:(vm|base|subvol)-[0-9]+-[^,[:space:]]+' /etc/pve/lxc/*.conf 2>/dev/null || true
} | sort -u
}
garbage_cleanup_orphan_disk_report() {
vm_require_commands pvesm || return 1
local refs store type status total used avail percent content line volid size vmid owner_id records=""
refs="$(garbage_cleanup_collect_referenced_volumes)"
while IFS='|' read -r store type status total used avail percent; do
[[ -n "$store" ]] || continue
for content in images rootdir; do
vm_storage_supports_content "$store" "$content" || continue
while read -r line; do
[[ -n "$line" ]] || continue
volid="$(echo "$line" | awk '{print $1}')"
size="$(echo "$line" | awk '{print $4}')"
vmid="$(echo "$line" | awk '{print $5}')"
[[ "$volid" =~ :((vm|base|subvol)-[0-9]+-) ]] || continue
if echo "$refs" | grep -Fxq "$volid"; then
continue
fi
owner_id="$(echo "$volid" | sed -nE 's/.*:(vm|base|subvol)-([0-9]+)-.*/\2/p')"
if [[ -n "$owner_id" ]] && pve_guest_exists any "$owner_id"; then
continue
fi
records+="${volid}|${size:-0}|${vmid:-${owner_id:-?}}|${content}|${store}"$'\n'
done < <(pvesm list "$store" --content "$content" 2>/dev/null | awk 'NR>1')
done
done < <(pve_storage_status_records)
clear
show_menu_header "疑似孤立磁盘扫描(只读)"
echo -e "${YELLOW}说明:${NC} 该功能只扫描,不删除。底层卷误删风险很高,请先在 Web UI / VM 配置中二次核对。"
echo "$UI_DIVIDER"
if [[ -z "$records" ]]; then
log_success "未发现明显的孤立磁盘候选。"
return 0
fi
printf "%-5s %-18s %-8s %-10s %-8s %s\n" "序号" "存储" "VMID" "内容" "大小" "卷"
echo "$UI_DIVIDER"
local idx=1
while IFS='|' read -r volid size vmid content store; do
[[ -n "$volid" ]] || continue
printf "%-5s %-18s %-8s %-10s %-8s %s\n" "$idx" "$store" "$vmid" "$content" "$(pve_tools_human_bytes "$size")" "$volid"
echo " 人工核对后可用命令: pvesm free \"$volid\""
idx=$((idx + 1))
done <<< "$records"
echo "$UI_DIVIDER"
echo -e "${RED}未在脚本中提供自动删除孤立磁盘入口。${NC}"
}
garbage_cleanup_scan_report() {
local temp_records old_records backup_records snap_records
local temp_count old_count backup_count snap_count apt_cache_size pve_backup_size pve_export_size
temp_records="$(garbage_cleanup_temp_file_candidates 3)"
old_records="$(garbage_cleanup_pve_tools_old_file_candidates 90)"
backup_records="$(garbage_cleanup_backup_candidates both 180)"
snap_records="$(garbage_cleanup_snapshot_candidates 90)"
temp_count="$(echo "$temp_records" | garbage_cleanup_count_records)"
old_count="$(echo "$old_records" | garbage_cleanup_count_records)"
backup_count="$(echo "$backup_records" | garbage_cleanup_count_records)"
snap_count="$(echo "$snap_records" | garbage_cleanup_count_records)"
apt_cache_size="$(du -sh /var/cache/apt/archives 2>/dev/null | awk '{print $1}')"
pve_backup_size="$(du -sh /var/backups/pve-tools 2>/dev/null | awk '{print $1}')"
pve_export_size="$(du -sh "$VM_CONFIG_EXPORT_DIR" 2>/dev/null | awk '{print $1}')"
clear
show_menu_header "垃圾清理扫描报告"
echo -e "${CYAN}缓存与日志:${NC}"
echo " apt 缓存目录: ${apt_cache_size:-未发现}"
if command -v journalctl >/dev/null 2>&1; then
echo -n " systemd journal: "
journalctl --disk-usage 2>/dev/null || echo "无法读取"
fi
echo "$UI_DIVIDER"
echo -e "${CYAN}本工具旧文件:${NC}"
echo " /tmp 中超过 3 天的 PVE-Tools 临时文件: $temp_count"
echo " /var/backups/pve-tools 总大小: ${pve_backup_size:-未发现}"
echo " $VM_CONFIG_EXPORT_DIR 总大小: ${pve_export_size:-未发现}"
echo " 超过 90 天的 PVE-Tools 备份/导出文件: $old_count"
echo "$UI_DIVIDER"
echo -e "${CYAN}PVE 资源候选:${NC}"
echo " 无主或超过 180 天的 vzdump 备份: $backup_count"
echo " 超过 90 天且带 snaptime 的 VM/CT 快照: $snap_count"
echo " 疑似孤立磁盘请使用菜单中的只读扫描单独查看。"
}
garbage_cleanup_menu() {
run_menu "垃圾清理" garbage_cleanup_menu_render garbage_cleanup_menu_dispatch "0-5"
}
garbage_cleanup_menu_render() {
show_menu_option "1" "一键扫描报告 ${CYAN}(只读)${NC}"
show_menu_option "2" "清理缓存、日志与本工具旧文件"
show_menu_option "3" "清理过期/无主 vzdump 备份"
show_menu_option "4" "清理旧 VM/CT 快照"
show_menu_option "5" "扫描疑似孤立磁盘 ${YELLOW}(只读,不删除)${NC}"
echo "$UI_DIVIDER"
echo -e "${YELLOW}说明:${NC} 清理前会列出候选项;备份和快照删除均需要高风险确认。"
}
garbage_cleanup_menu_dispatch() {
case "$1" in
1) garbage_cleanup_scan_report ;;
2) garbage_cleanup_basic ;;
3) garbage_cleanup_prune_backups ;;
4) garbage_cleanup_prune_snapshots ;;
5) garbage_cleanup_orphan_disk_report ;;
*) return 1 ;;
esac
return 0
}