mirror of
https://github.com/Mapleawaa/PVE-Tools-9.git
synced 2026-05-07 05:50:41 +08:00
7273 lines
254 KiB
Bash
7273 lines
254 KiB
Bash
#!/bin/bash
|
||
|
||
# SPDX-License-Identifier: GPL-3.0-only
|
||
# Copyright (C) 2026 Ciriu Networks
|
||
# Auther:Maple
|
||
|
||
# 二次修改使用请不要删除此段注释
|
||
# PVE 9.0 配置工具脚本
|
||
# 支持换源、删除订阅弹窗、硬盘管理等功能
|
||
# 适用于 Proxmox VE 9.0 (基于 Debian 13)
|
||
|
||
|
||
# 版本信息
|
||
CURRENT_VERSION="6.7.0"
|
||
VERSION_FILE_URL="https://raw.githubusercontent.com/Mapleawaa/PVE-Tools-9/main/VERSION"
|
||
UPDATE_FILE_URL="https://raw.githubusercontent.com/Mapleawaa/PVE-Tools-9/main/UPDATE"
|
||
PVE_VERSION_DETECTED=""
|
||
PVE_MAJOR_VERSION=""
|
||
RISK_ACK_BYPASS=false
|
||
|
||
# ============ 颜色系统 ============
|
||
|
||
# 终端颜色初始化
|
||
setup_colors() {
|
||
if [[ -t 1 && -z "${NO_COLOR}" ]]; then
|
||
# 使用 printf 确保变量包含真实的转义字符,提高不同 shell 间的兼容性
|
||
RED=$(printf '\033[0;31m')
|
||
GREEN=$(printf '\033[0;32m')
|
||
YELLOW=$(printf '\033[1;33m')
|
||
BLUE=$(printf '\033[0;34m')
|
||
PINK=$(printf '\033[0;35m')
|
||
CYAN=$(printf '\033[0;36m')
|
||
MAGENTA=$(printf '\033[0;35m')
|
||
WHITE=$(printf '\033[1;37m')
|
||
ORANGE=$(printf '\033[0;33m')
|
||
NC=$(printf '\033[0m')
|
||
|
||
|
||
# UI 辅助色映射
|
||
PRIMARY="${CYAN}"
|
||
H1=$(printf '\033[1;36m')
|
||
H2=$(printf '\033[1;37m')
|
||
else
|
||
RED='' GREEN='' YELLOW='' BLUE='' CYAN='' MAGENTA='' WHITE='' ORANGE='' NC=''
|
||
PRIMARY='' H1='' H2=''
|
||
fi
|
||
|
||
# UI 界面一致性常量
|
||
UI_BORDER="${NC}═════════════════════════════════════════════════${NC}"
|
||
UI_DIVIDER="${NC}═════════════════════════════════════════════════${NC}"
|
||
UI_FOOTER="${NC}═════════════════════════════════════════════════${NC}"
|
||
UI_HEADER="${NC}═════════════════════════════════════════════════${NC}"
|
||
}
|
||
|
||
# 初始化颜色
|
||
setup_colors
|
||
|
||
# 镜像源配置
|
||
MIRROR_USTC="https://mirrors.ustc.edu.cn/proxmox/debian/pve"
|
||
MIRROR_TUNA="https://mirrors.tuna.tsinghua.edu.cn/proxmox/debian/pve"
|
||
MIRROR_DEBIAN="https://deb.debian.org/debian"
|
||
SELECTED_MIRROR=""
|
||
|
||
# ceph 模板源配置
|
||
CEPH_MIRROR_USTC="https://mirrors.ustc.edu.cn/proxmox/debian/ceph-squid"
|
||
CEPH_MIRROR_TUNA="https://mirrors.tuna.tsinghua.edu.cn/proxmox/debian/ceph-squid"
|
||
CEPH_MIRROR_OFFICIAL="http://download.proxmox.com/debian/ceph-squid"
|
||
|
||
# CT 模板源配置
|
||
CT_MIRROR_USTC="https://mirrors.ustc.edu.cn/proxmox"
|
||
CT_MIRROR_TUNA="https://mirrors.tuna.tsinghua.edu.cn/proxmox"
|
||
CT_MIRROR_OFFICIAL="http://download.proxmox.com"
|
||
|
||
# 自动更新网络检测配置
|
||
CF_TRACE_URL="https://www.cloudflare.com/cdn-cgi/trace"
|
||
GITHUB_MIRROR_PREFIX="https://ghfast.top/"
|
||
USE_MIRROR_FOR_UPDATE=0
|
||
USER_COUNTRY_CODE=""
|
||
|
||
# 快速虚拟机下载脚本配置
|
||
FASTPVE_INSTALLER_URL="https://raw.githubusercontent.com/kspeeder/fastpve/main/fastpve-install.sh"
|
||
FASTPVE_PROJECT_URL="https://github.com/kspeeder/fastpve"
|
||
|
||
# 日志函数
|
||
log_info() {
|
||
local timestamp=$(date +'%H:%M:%S')
|
||
echo -e "${GREEN}[$timestamp]${NC} ${CYAN}INFO${NC} $1"
|
||
echo "[$timestamp] INFO $1" >> /var/log/pve-tools.log
|
||
}
|
||
|
||
log_warn() {
|
||
local timestamp=$(date +'%H:%M:%S')
|
||
echo -e "${YELLOW}[$timestamp]${NC} ${ORANGE}WARN${NC} $1"
|
||
echo "[$timestamp] WARN $1" >> /var/log/pve-tools.log
|
||
}
|
||
|
||
log_error() {
|
||
local timestamp=$(date +'%H:%M:%S')
|
||
echo -e "${RED}[$timestamp]${NC} ${RED}ERROR${NC} $1" >&2
|
||
echo "[$timestamp] ERROR $1" >> /var/log/pve-tools.log
|
||
}
|
||
|
||
log_step() {
|
||
local timestamp=$(date +'%H:%M:%S')
|
||
echo -e "${BLUE}[$timestamp]${NC} ${MAGENTA}STEP${NC} $1"
|
||
echo "[$timestamp] STEP $1" >> /var/log/pve-tools.log
|
||
}
|
||
|
||
log_success() {
|
||
local timestamp=$(date +'%H:%M:%S')
|
||
echo -e "${GREEN}[$timestamp]${NC} ${GREEN}OK${NC} $1"
|
||
echo "[$timestamp] OK $1" >> /var/log/pve-tools.log
|
||
}
|
||
|
||
log_tips(){
|
||
local timestamp=$(date +'%H:%M:%S')
|
||
echo -e "${CYAN}[$timestamp]${NC} ${MAGENTA}TIPS${NC} $1"
|
||
echo "[$timestamp] TIPS $1" >> /var/log/pve-tools.log
|
||
}
|
||
|
||
# Enhanced error handling function with consistent messaging
|
||
display_error() {
|
||
local error_msg="$1"
|
||
local suggestion="${2:-请检查输入或联系作者寻求帮助。}"
|
||
|
||
log_error "$error_msg"
|
||
echo -e "${YELLOW}提示: $suggestion${NC}"
|
||
pause_function
|
||
}
|
||
|
||
# Enhanced success feedback
|
||
display_success() {
|
||
local success_msg="$1"
|
||
local next_step="${2:-}"
|
||
|
||
log_success "$success_msg"
|
||
if [[ -n "$next_step" ]]; then
|
||
echo -e "${GREEN}下一步: $next_step${NC}"
|
||
fi
|
||
}
|
||
|
||
# Confirmation prompt with consistent UI
|
||
confirm_action() {
|
||
local action_desc="$1"
|
||
local default_choice="${2:-N}"
|
||
|
||
echo -e "${YELLOW}确认操作: $action_desc${NC}"
|
||
read -p "请输入 'yes' 确认继续,其他任意键取消 [$default_choice]: " -r confirm
|
||
if [[ "$confirm" == "yes" || "$confirm" == "YES" ]]; then
|
||
return 0
|
||
else
|
||
log_info "操作已取消"
|
||
return 1
|
||
fi
|
||
}
|
||
|
||
LEGAL_VERSION="1.0"
|
||
LEGAL_EFFECTIVE_DATE="2026-__-__"
|
||
|
||
ensure_legal_acceptance() {
|
||
local dir="/var/lib/pve-tools"
|
||
local marker="${dir}/legal_acceptance_${LEGAL_VERSION}"
|
||
mkdir -p "$dir" >/dev/null 2>&1 || true
|
||
|
||
if [[ -f "$marker" ]]; then
|
||
return 0
|
||
fi
|
||
|
||
clear
|
||
show_menu_header "许可与服务条款"
|
||
echo -e "${CYAN}继续使用本脚本前,请阅读并同意以下条款:${NC}"
|
||
echo -e " - ULA(最终用户许可与使用协议): https://pve.u3u.icu/ula"
|
||
echo -e " - TOS(服务条款): https://pve.u3u.icu/tos"
|
||
echo -e "${RED} 您可以随时撤回同意,只需删除 ${marker} 文件即可。${NC}"
|
||
echo -e "${UI_DIVIDER}"
|
||
echo -n "是否同意并继续?(Y/N): "
|
||
local ans
|
||
read -n 1 -r ans
|
||
echo
|
||
if [[ "$ans" == "Y" || "$ans" == "y" ]]; then
|
||
printf '%s\n' "accepted_version=${LEGAL_VERSION}" "accepted_time=$(date +%F\ %T)" > "$marker" 2>/dev/null || true
|
||
log_success "已记录同意条款,后续将跳过许可检查。"
|
||
return 0
|
||
fi
|
||
|
||
log_info "未同意条款,退出脚本"
|
||
exit 0
|
||
}
|
||
|
||
# ============ 配置文件安全管理函数 ============
|
||
|
||
# 备份文件到 /var/backups/pve-tools/
|
||
backup_file() {
|
||
local file_path="$1"
|
||
local backup_dir="/var/backups/pve-tools"
|
||
|
||
if [[ ! -f "$file_path" ]]; then
|
||
log_warn "文件不存在,跳过备份: $file_path"
|
||
return 1
|
||
fi
|
||
|
||
# 创建备份目录
|
||
mkdir -p "$backup_dir"
|
||
|
||
# 生成带时间戳的备份文件名
|
||
local filename=$(basename "$file_path")
|
||
local timestamp=$(date +%Y%m%d_%H%M%S)
|
||
local backup_path="${backup_dir}/${filename}.${timestamp}.bak"
|
||
|
||
# 执行备份
|
||
if cp -a "$file_path" "$backup_path"; then
|
||
log_success "文件已备份: $backup_path"
|
||
return 0
|
||
else
|
||
log_error "备份失败: $file_path"
|
||
return 1
|
||
fi
|
||
}
|
||
|
||
# 写入配置块(带标记)
|
||
# 用法: apply_block <file> <marker> <content>
|
||
apply_block() {
|
||
local file_path="$1"
|
||
local marker="$2"
|
||
local content="$3"
|
||
|
||
if [[ -z "$file_path" || -z "$marker" ]]; then
|
||
log_error "apply_block: 缺少必需参数"
|
||
return 1
|
||
fi
|
||
|
||
# 先备份文件
|
||
backup_file "$file_path"
|
||
|
||
# 移除旧的配置块(如果存在)
|
||
remove_block "$file_path" "$marker"
|
||
|
||
# 写入新的配置块
|
||
{
|
||
echo "# PVE-TOOLS BEGIN $marker"
|
||
echo "$content"
|
||
echo "# PVE-TOOLS END $marker"
|
||
} >> "$file_path"
|
||
|
||
log_success "配置块已写入: $file_path [$marker]"
|
||
}
|
||
|
||
# 删除配置块(精确匹配标记)
|
||
# 用法: remove_block <file> <marker>
|
||
remove_block() {
|
||
local file_path="$1"
|
||
local marker="$2"
|
||
|
||
if [[ -z "$file_path" || -z "$marker" ]]; then
|
||
log_error "remove_block: 缺少必需参数"
|
||
return 1
|
||
fi
|
||
|
||
if [[ ! -f "$file_path" ]]; then
|
||
log_warn "文件不存在,跳过删除: $file_path"
|
||
return 0
|
||
fi
|
||
|
||
# 使用 sed 删除标记之间的所有内容(包括标记行)
|
||
sed -i "/# PVE-TOOLS BEGIN $marker/,/# PVE-TOOLS END $marker/d" "$file_path"
|
||
|
||
log_info "配置块已删除: $file_path [$marker]"
|
||
}
|
||
|
||
# ============ 配置文件安全管理函数结束 ============
|
||
|
||
# ============ GRUB 参数幂等管理函数 ============
|
||
|
||
# 添加 GRUB 参数(幂等操作,不会重复添加)
|
||
# 用法: grub_add_param "intel_iommu=on"
|
||
grub_add_param() {
|
||
local param="$1"
|
||
|
||
if [[ -z "$param" ]]; then
|
||
log_error "grub_add_param: 缺少参数"
|
||
return 1
|
||
fi
|
||
|
||
# 备份 GRUB 配置
|
||
backup_file "/etc/default/grub"
|
||
|
||
# 读取当前的 GRUB_CMDLINE_LINUX_DEFAULT 值
|
||
local current_line=$(grep '^GRUB_CMDLINE_LINUX_DEFAULT=' /etc/default/grub)
|
||
|
||
if [[ -z "$current_line" ]]; then
|
||
log_error "未找到 GRUB_CMDLINE_LINUX_DEFAULT 配置"
|
||
return 1
|
||
fi
|
||
|
||
# 提取引号内的参数
|
||
local current_params=$(echo "$current_line" | sed 's/^GRUB_CMDLINE_LINUX_DEFAULT="\(.*\)"$/\1/')
|
||
|
||
# 检查参数是否已存在(支持 key=value 和 key 两种格式)
|
||
local param_key=$(echo "$param" | cut -d'=' -f1)
|
||
|
||
if echo "$current_params" | grep -qw "$param_key"; then
|
||
# 参数已存在,先删除旧值
|
||
current_params=$(echo "$current_params" | sed "s/\b${param_key}[^ ]*\b//g")
|
||
fi
|
||
|
||
# 添加新参数(去除多余空格)
|
||
local new_params=$(echo "$current_params $param" | sed 's/ */ /g' | sed 's/^ //;s/ $//')
|
||
|
||
# 写回配置文件
|
||
sed -i "s|^GRUB_CMDLINE_LINUX_DEFAULT=.*|GRUB_CMDLINE_LINUX_DEFAULT=\"$new_params\"|" /etc/default/grub
|
||
|
||
log_success "GRUB 参数已添加: $param"
|
||
}
|
||
|
||
# 删除 GRUB 参数(精确删除,不影响其他参数)
|
||
# 用法: grub_remove_param "intel_iommu=on"
|
||
grub_remove_param() {
|
||
local param="$1"
|
||
|
||
if [[ -z "$param" ]]; then
|
||
log_error "grub_remove_param: 缺少参数"
|
||
return 1
|
||
fi
|
||
|
||
# 备份 GRUB 配置
|
||
backup_file "/etc/default/grub"
|
||
|
||
# 读取当前的 GRUB_CMDLINE_LINUX_DEFAULT 值
|
||
local current_line=$(grep '^GRUB_CMDLINE_LINUX_DEFAULT=' /etc/default/grub)
|
||
|
||
if [[ -z "$current_line" ]]; then
|
||
log_error "未找到 GRUB_CMDLINE_LINUX_DEFAULT 配置"
|
||
return 1
|
||
fi
|
||
|
||
# 提取引号内的参数
|
||
local current_params=$(echo "$current_line" | sed 's/^GRUB_CMDLINE_LINUX_DEFAULT="\(.*\)"$/\1/')
|
||
|
||
# 删除指定参数(支持精确匹配和前缀匹配)
|
||
local param_key=$(echo "$param" | cut -d'=' -f1)
|
||
local new_params=$(echo "$current_params" | sed "s/\b${param_key}[^ ]*\b//g" | sed 's/ */ /g' | sed 's/^ //;s/ $//')
|
||
|
||
# 写回配置文件
|
||
sed -i "s|^GRUB_CMDLINE_LINUX_DEFAULT=.*|GRUB_CMDLINE_LINUX_DEFAULT=\"$new_params\"|" /etc/default/grub
|
||
|
||
log_success "GRUB 参数已删除: $param"
|
||
}
|
||
|
||
# ============ GRUB 参数幂等管理函数结束 ============
|
||
|
||
# 进度指示函数
|
||
show_progress() {
|
||
local message="$1"
|
||
local spinner="|/-\\"
|
||
local i=0
|
||
# Print initial message
|
||
echo -ne "${CYAN}[ ]${NC} $message\033[0K\r"
|
||
|
||
# Update the spinner position in the box
|
||
while true; do
|
||
i=$(( (i + 1) % 4 ))
|
||
echo -ne "\b\b\b\b\b${CYAN}[${spinner:$i:1}]${NC}\033[0K\r"
|
||
sleep 0.1
|
||
done &
|
||
# Store the background job ID to be killed later
|
||
SPINNER_PID=$!
|
||
}
|
||
|
||
update_progress() {
|
||
local message="$1"
|
||
# Kill the spinner if running
|
||
if [[ -n "$SPINNER_PID" ]]; then
|
||
kill $SPINNER_PID 2>/dev/null
|
||
fi
|
||
echo -ne "${GREEN}[ OK ]${NC} $message\033[0K\r"
|
||
echo
|
||
}
|
||
|
||
# Enhanced visual feedback function
|
||
show_status() {
|
||
local status="$1"
|
||
local message="$2"
|
||
local color="$3"
|
||
|
||
case $status in
|
||
"info")
|
||
echo -e "${CYAN}[INFO]${NC} $message"
|
||
;;
|
||
"success")
|
||
echo -e "${GREEN}[ OK ]${NC} $message"
|
||
;;
|
||
"warning")
|
||
echo -e "${YELLOW}[WARN]${NC} $message"
|
||
;;
|
||
"error")
|
||
echo -e "${RED}[FAIL]${NC} $message"
|
||
;;
|
||
"step")
|
||
echo -e "${MAGENTA}[STEP]${NC} $message"
|
||
;;
|
||
*)
|
||
echo -e "${WHITE}[$status]${NC} $message"
|
||
;;
|
||
esac
|
||
}
|
||
|
||
# Progress bar function
|
||
show_progress_bar() {
|
||
local current="$1"
|
||
local total="$2"
|
||
local message="$3"
|
||
local width=40
|
||
local percentage=$(( current * 100 / total ))
|
||
local filled=$(( width * current / total ))
|
||
|
||
printf "${CYAN}[${NC}"
|
||
for ((i=0; i<filled; i++)); do
|
||
printf "█"
|
||
done
|
||
for ((i=filled; i<width; i++)); do
|
||
printf " "
|
||
done
|
||
printf "${CYAN}]${NC} ${percentage}%% $message\r"
|
||
}
|
||
|
||
# 通过 Cloudflare Trace 检测地区,决定是否启用镜像源
|
||
detect_network_region() {
|
||
local timeout=5
|
||
USER_COUNTRY_CODE=""
|
||
USE_MIRROR_FOR_UPDATE=0
|
||
|
||
if ! command -v curl &> /dev/null; then
|
||
return 1
|
||
fi
|
||
|
||
local trace_output
|
||
trace_output=$(curl -s --connect-timeout $timeout --max-time $timeout "$CF_TRACE_URL" 2>/dev/null)
|
||
if [[ -z "$trace_output" ]]; then
|
||
return 1
|
||
fi
|
||
|
||
local loc
|
||
loc=$(echo "$trace_output" | awk -F= '/^loc=/{print $2}' | tr -d '\r')
|
||
if [[ -z "$loc" ]]; then
|
||
return 1
|
||
fi
|
||
|
||
USER_COUNTRY_CODE="$loc"
|
||
if [[ "$USER_COUNTRY_CODE" == "CN" ]]; then
|
||
USE_MIRROR_FOR_UPDATE=1
|
||
fi
|
||
|
||
return 0
|
||
}
|
||
|
||
# 显示横幅
|
||
show_banner() {
|
||
clear
|
||
echo -ne "${NC}"
|
||
cat << 'EOF'
|
||
██████╗ ██╗ ██╗███████╗ ████████╗ ██████╗ ██████╗ ██╗ ███████╗ █████╗
|
||
██╔══██╗██║ ██║██╔════╝ ╚══██╔══╝██╔═══██╗██╔═══██╗██║ ██╔════╝ ██╔══██╗
|
||
██████╔╝██║ ██║█████╗ ██║ ██║ ██║██║ ██║██║ ███████╗ ╚██████║
|
||
██╔═══╝ ╚██╗ ██╔╝██╔══╝ ██║ ██║ ██║██║ ██║██║ ╚════██║ ╚═══██║
|
||
██║ ╚████╔╝ ███████╗ ██║ ╚██████╔╝╚██████╔╝███████╗███████║ █████╔╝
|
||
╚═╝ ╚═══╝ ╚══════╝ ╚═╝ ╚═════╝ ╚═════╝ ╚══════╝╚══════╝ ╚════╝
|
||
EOF
|
||
echo -ne "${NC}"
|
||
echo "$UI_BORDER"
|
||
echo -e " ${H1}PVE-Tools-9 一键脚本${NC}"
|
||
echo " 让每个人都能体验虚拟化技术的的便利。"
|
||
echo -e " 作者: ${PINK}Maple${NC} | 交流群: ${CYAN}1031976463${NC}"
|
||
echo -e " 当前版本: ${GREEN}$CURRENT_VERSION${NC} | 最新版本: ${remote_version:-"未检测"}"
|
||
echo "$UI_BORDER"
|
||
}
|
||
|
||
# 检查是否为 root 用户
|
||
check_root() {
|
||
if [[ $EUID -ne 0 ]]; then
|
||
log_error "哎呀!需要超级管理员权限才能运行哦"
|
||
echo "请使用以下命令重新运行:"
|
||
echo "sudo $0"
|
||
exit 1
|
||
fi
|
||
}
|
||
|
||
# 检查调试模式
|
||
check_debug_mode() {
|
||
for arg in "$@"; do
|
||
if [[ "$arg" == "--i-know-what-i-do" ]]; then
|
||
RISK_ACK_BYPASS=true
|
||
fi
|
||
done
|
||
|
||
for arg in "$@"; do
|
||
if [[ "$arg" == "--debug" ]]; then
|
||
log_warn "警告:您正在使用调试模式!"
|
||
echo "此模式将跳过 PVE 系统版本检测"
|
||
echo "仅在开发和测试环境中使用"
|
||
echo "在非 PVE (Debian 系) 系统上使用可能导致系统损坏"
|
||
echo "您确定要继续吗?输入 'yes' 确认,其他任意键退出: "
|
||
read -r confirm
|
||
if [[ "$confirm" != "yes" ]]; then
|
||
log_info "已取消操作,退出脚本"
|
||
exit 0
|
||
fi
|
||
DEBUG_MODE=true
|
||
log_success "已启用调试模式"
|
||
return
|
||
fi
|
||
done
|
||
DEBUG_MODE=false
|
||
}
|
||
|
||
# 检查是否安装依赖软件包
|
||
check_packages() {
|
||
# 程序依赖的软件包: `sudo` `curl`
|
||
local packages=("sudo" "curl")
|
||
for pkg in "${packages[@]}"; do
|
||
if ! command -v "$pkg" &> /dev/null; then
|
||
log_error "哎呀!需要安装 $pkg 软件包才能运行哦"
|
||
echo "请使用以下命令安装:apt install -y $pkg"
|
||
exit 1
|
||
fi
|
||
done
|
||
}
|
||
|
||
|
||
|
||
|
||
# 检查 PVE 版本
|
||
check_pve_version() {
|
||
# 如果在调试模式下,跳过 PVE 版本检测
|
||
if [[ "$DEBUG_MODE" == "true" ]]; then
|
||
log_warn "调试模式:跳过 PVE 版本检测"
|
||
echo "请注意:您正在非 PVE 系统上运行此脚本,某些功能可能无法正常工作"
|
||
PVE_VERSION_DETECTED="debug"
|
||
PVE_MAJOR_VERSION="debug"
|
||
return
|
||
fi
|
||
|
||
if ! command -v pveversion &> /dev/null; then
|
||
log_error "咦?这里好像不是 PVE 环境呢"
|
||
echo "请在 Proxmox VE 系统上运行此脚本"
|
||
exit 1
|
||
fi
|
||
|
||
local pve_version pkg_ver out
|
||
out="$(pveversion 2>/dev/null || true)"
|
||
if [[ "$out" =~ pve-manager/([0-9]+(\.[0-9]+)*) ]]; then
|
||
pve_version="${BASH_REMATCH[1]}"
|
||
else
|
||
pve_version=""
|
||
fi
|
||
if [[ -z "$pve_version" ]] && command -v dpkg-query >/dev/null 2>&1; then
|
||
pkg_ver="$(dpkg-query -W -f='${Version}' pve-manager 2>/dev/null || true)"
|
||
pve_version="$(echo "$pkg_ver" | grep -oE '^[0-9]+(\.[0-9]+)*' | head -n 1)"
|
||
fi
|
||
if [[ -z "$pve_version" ]]; then
|
||
pve_version="unknown"
|
||
fi
|
||
|
||
PVE_VERSION_DETECTED="$pve_version"
|
||
if [[ "$pve_version" =~ ^[0-9]+(\.[0-9]+)*$ ]]; then
|
||
PVE_MAJOR_VERSION="$(echo "$pve_version" | cut -d'.' -f1)"
|
||
else
|
||
PVE_MAJOR_VERSION="unknown"
|
||
fi
|
||
|
||
log_info "太好了!检测到 PVE 版本: $pve_version"
|
||
|
||
if [[ "$PVE_MAJOR_VERSION" != "9" && "$RISK_ACK_BYPASS" != "true" ]]; then
|
||
clear
|
||
show_menu_header "高风险提示:非 PVE9 环境"
|
||
echo -e "${RED}警告:检测到当前不是 PVE 9.x(当前:${PVE_VERSION_DETECTED})。${NC}"
|
||
echo -e "${RED}本脚本面向 PVE 9.x(Debian 13 / trixie)编写。${NC}"
|
||
echo -e "${RED}在 PVE 7/8 等系统上执行“换源/升级/一键优化”等自动化修改,可能是毁灭性的:${NC}"
|
||
echo -e "${RED}可能导致软件源错配、系统升级路径错误、依赖冲突、宿主机不可用。${NC}"
|
||
echo -e "${UI_DIVIDER}"
|
||
echo -e "${YELLOW}严禁在非 PVE9 上使用的选项(脚本将强制拦截):${NC}"
|
||
echo -e " - 一键优化(换源+删弹窗+更新)"
|
||
echo -e " - 软件源与更新(更换软件源/更新系统软件包/PVE 8 升级到 9)"
|
||
echo -e "${UI_DIVIDER}"
|
||
echo -e "${CYAN}如你仍要继续使用脚本的其它功能,请手动输入以下任意一项以确认风险:${NC}"
|
||
echo -e " - 确认"
|
||
echo -e " - Confirm with Risks"
|
||
echo -e "${UI_DIVIDER}"
|
||
local ack ack_lc
|
||
read -r -p "请输入确认文本以继续(回车退出): " ack
|
||
if [[ -z "$ack" ]]; then
|
||
log_info "未确认风险,退出脚本"
|
||
exit 0
|
||
fi
|
||
ack_lc="$(echo "$ack" | tr 'A-Z' 'a-z' | sed -E 's/[[:space:]]+/ /g' | sed -E 's/^ +| +$//g')"
|
||
if [[ "$ack" != "确认" && "$ack_lc" != "confirm with risks" ]]; then
|
||
log_error "确认文本不匹配,已退出"
|
||
exit 1
|
||
fi
|
||
log_warn "已确认风险:当前为非 PVE9 环境,将拦截毁灭性自动化修改功能"
|
||
fi
|
||
}
|
||
|
||
block_non_pve9_destructive() {
|
||
local feature="$1"
|
||
if [[ "$DEBUG_MODE" == "true" ]]; then
|
||
return 0
|
||
fi
|
||
if [[ "$RISK_ACK_BYPASS" == "true" ]]; then
|
||
return 0
|
||
fi
|
||
if [[ "${PVE_MAJOR_VERSION:-}" != "9" ]]; then
|
||
display_error "已拦截:非 PVE9 环境禁止执行该自动化操作" "功能:${feature}。请在 PVE9 上使用,或手动参考文档/自行处理。如需强制执行,请加启动参数 --i-know-what-i-do"
|
||
return 1
|
||
fi
|
||
return 0
|
||
}
|
||
|
||
pve_mail_send_test() {
|
||
local from_addr="$1"
|
||
local to_addr="$2"
|
||
local subject="$3"
|
||
local body="$4"
|
||
|
||
if ! command -v sendmail >/dev/null 2>&1; then
|
||
display_error "未找到 sendmail" "请确认 postfix 已安装并提供 sendmail。"
|
||
return 1
|
||
fi
|
||
|
||
{
|
||
echo "From: ${from_addr}"
|
||
echo "To: ${to_addr}"
|
||
echo "Subject: ${subject}"
|
||
echo
|
||
echo "${body}"
|
||
} | sendmail -f "${from_addr}" -t >/dev/null 2>&1
|
||
}
|
||
|
||
pve_mail_configure_postfix_smtp() {
|
||
local relay_host="$1"
|
||
local relay_port="$2"
|
||
local tls_mode="$3"
|
||
local sasl_user="$4"
|
||
local sasl_pass="$5"
|
||
|
||
if ! command -v postconf >/dev/null 2>&1; then
|
||
display_error "未找到 postconf" "请先安装 postfix 并确保其命令可用。"
|
||
return 1
|
||
fi
|
||
|
||
local relay
|
||
relay="[${relay_host}]:${relay_port}"
|
||
|
||
backup_file "/etc/postfix/main.cf" >/dev/null 2>&1 || true
|
||
postconf -e "relayhost = ${relay}"
|
||
postconf -e "smtp_use_tls = yes"
|
||
postconf -e "smtp_tls_security_level = encrypt"
|
||
postconf -e "smtp_sasl_auth_enable = yes"
|
||
postconf -e "smtp_sasl_security_options ="
|
||
postconf -e "smtp_sasl_password_maps = hash:/etc/postfix/sasl_passwd"
|
||
postconf -e "smtp_tls_CApath = /etc/ssl/certs"
|
||
postconf -e "smtp_tls_session_cache_database = btree:/var/lib/postfix/smtp_tls_session_cache"
|
||
postconf -e "smtp_tls_session_cache_timeout = 3600s"
|
||
|
||
if [[ "$tls_mode" == "wrapper" ]]; then
|
||
postconf -e "smtp_tls_wrappermode = yes"
|
||
else
|
||
postconf -e "smtp_tls_wrappermode = no"
|
||
fi
|
||
|
||
local sasl_file="/etc/postfix/sasl_passwd"
|
||
backup_file "$sasl_file" >/dev/null 2>&1 || true
|
||
umask 077
|
||
printf '%s %s:%s\n' "${relay}" "${sasl_user}" "${sasl_pass}" > "$sasl_file"
|
||
chmod 600 "$sasl_file" >/dev/null 2>&1 || true
|
||
|
||
if ! command -v postmap >/dev/null 2>&1; then
|
||
display_error "未找到 postmap" "请确认 postfix 已安装完整。"
|
||
return 1
|
||
fi
|
||
postmap "hash:${sasl_file}" >/dev/null 2>&1 || {
|
||
display_error "postmap 执行失败" "请检查 /etc/postfix/sasl_passwd 格式与权限。"
|
||
return 1
|
||
}
|
||
|
||
postfix reload >/dev/null 2>&1 || {
|
||
systemctl reload postfix >/dev/null 2>&1 || systemctl restart postfix >/dev/null 2>&1 || true
|
||
}
|
||
|
||
return 0
|
||
}
|
||
|
||
pve_mail_configure_datacenter_emails() {
|
||
local from_addr="$1"
|
||
local root_addr="$2"
|
||
|
||
if ! command -v pvesh >/dev/null 2>&1; then
|
||
display_error "未找到 pvesh" "请确认当前环境为 PVE 宿主机。"
|
||
return 1
|
||
fi
|
||
|
||
pvesh set /cluster/options --email-from "$from_addr" >/dev/null 2>&1 || {
|
||
display_error "设置“来自…邮件”失败" "请在 WebUI:数据中心 -> 选项 -> 电子邮件(From)中手动设置。"
|
||
return 1
|
||
}
|
||
|
||
pvesh set /access/users/root@pam --email "$root_addr" >/dev/null 2>&1 || {
|
||
display_error "设置 root 邮箱失败" "请在 WebUI:数据中心 -> 权限 -> 用户 -> root@pam 中手动设置邮箱。"
|
||
return 1
|
||
}
|
||
|
||
return 0
|
||
}
|
||
|
||
pve_mail_configure_zed_mail() {
|
||
local from_addr="$1"
|
||
local to_addr="$2"
|
||
|
||
local zed_rc="/etc/zfs/zed.d/zed.rc"
|
||
if [[ ! -f "$zed_rc" ]]; then
|
||
log_warn "未找到 zed.rc(跳过 ZFS ZED 邮件配置)"
|
||
return 0
|
||
fi
|
||
|
||
backup_file "$zed_rc" >/dev/null 2>&1 || true
|
||
|
||
if grep -qE '^ZED_EMAIL_ADDR=' "$zed_rc"; then
|
||
sed -i "s|^ZED_EMAIL_ADDR=.*|ZED_EMAIL_ADDR=\"${to_addr}\"|g" "$zed_rc"
|
||
else
|
||
printf '\nZED_EMAIL_ADDR="%s"\n' "$to_addr" >> "$zed_rc"
|
||
fi
|
||
|
||
if grep -qE '^ZED_EMAIL_OPTS=' "$zed_rc"; then
|
||
sed -i "s|^ZED_EMAIL_OPTS=.*|ZED_EMAIL_OPTS=\"-r ${from_addr}\"|g" "$zed_rc"
|
||
else
|
||
printf 'ZED_EMAIL_OPTS="-r %s"\n' "$from_addr" >> "$zed_rc"
|
||
fi
|
||
|
||
systemctl restart zfs-zed >/dev/null 2>&1 || true
|
||
return 0
|
||
}
|
||
|
||
pve_mail_notification_setup() {
|
||
block_non_pve9_destructive "配置邮件通知(SMTP)" || return 1
|
||
log_step "配置 PVE 邮件通知(商业邮箱 SMTP)"
|
||
|
||
if ! command -v postfix >/dev/null 2>&1 && ! command -v postconf >/dev/null 2>&1; then
|
||
display_error "未检测到 postfix" "请先安装 postfix 后再配置(安装过程可能需要交互)。"
|
||
return 1
|
||
fi
|
||
|
||
local from_addr root_addr
|
||
read -p "请输入“来自…邮件”(发件人邮箱): " from_addr
|
||
if [[ -z "$from_addr" ]]; then
|
||
display_error "发件人邮箱不能为空"
|
||
return 1
|
||
fi
|
||
|
||
read -p "请输入 root 通知邮箱(收件人邮箱): " root_addr
|
||
if [[ -z "$root_addr" ]]; then
|
||
display_error "收件人邮箱不能为空"
|
||
return 1
|
||
fi
|
||
|
||
local preset
|
||
echo -e "${CYAN}请选择 SMTP 预设:${NC}"
|
||
echo " 1) QQ 邮箱(smtp.qq.com:465 SSL)"
|
||
echo " 2) 163 邮箱(smtp.163.com:465 SSL)"
|
||
echo " 3) Gmail(smtp.gmail.com:587 STARTTLS)"
|
||
echo " 4) 自定义(SMTP 兼容)"
|
||
read -p "请选择 [1-4] (默认: 1): " preset
|
||
preset="${preset:-1}"
|
||
|
||
local smtp_host smtp_port tls_mode
|
||
case "$preset" in
|
||
1) smtp_host="smtp.qq.com"; smtp_port="465"; tls_mode="wrapper" ;;
|
||
2) smtp_host="smtp.163.com"; smtp_port="465"; tls_mode="wrapper" ;;
|
||
3) smtp_host="smtp.gmail.com"; smtp_port="587"; tls_mode="starttls" ;;
|
||
4)
|
||
read -p "请输入 SMTP 服务器地址(如 smtp.xxx.com): " smtp_host
|
||
read -p "请输入 SMTP 端口(如 465/587): " smtp_port
|
||
read -p "TLS 模式(wrapper/starttls)[wrapper]: " tls_mode
|
||
tls_mode="${tls_mode:-wrapper}"
|
||
;;
|
||
*) smtp_host="smtp.qq.com"; smtp_port="465"; tls_mode="wrapper" ;;
|
||
esac
|
||
|
||
if [[ -z "$smtp_host" || -z "$smtp_port" ]]; then
|
||
display_error "SMTP 参数不完整"
|
||
return 1
|
||
fi
|
||
if [[ "$tls_mode" != "wrapper" && "$tls_mode" != "starttls" ]]; then
|
||
display_error "TLS 模式无效" "仅支持 wrapper 或 starttls"
|
||
return 1
|
||
fi
|
||
|
||
local smtp_user smtp_pass
|
||
read -p "请输入 SMTP 登录账号(通常为邮箱地址)[${from_addr}]: " smtp_user
|
||
smtp_user="${smtp_user:-$from_addr}"
|
||
if [[ -z "$smtp_user" ]]; then
|
||
display_error "SMTP 账号不能为空"
|
||
return 1
|
||
fi
|
||
|
||
echo -n "请输入 SMTP 密码/授权码(输入不回显): "
|
||
read -r -s smtp_pass
|
||
echo
|
||
if [[ -z "$smtp_pass" ]]; then
|
||
display_error "SMTP 密码/授权码不能为空"
|
||
return 1
|
||
fi
|
||
|
||
clear
|
||
show_menu_header "邮件通知配置确认"
|
||
echo -e "${YELLOW}发件人(From):${NC} $from_addr"
|
||
echo -e "${YELLOW}收件人(root 邮箱):${NC} $root_addr"
|
||
echo -e "${YELLOW}SMTP 服务器:${NC} ${smtp_host}:${smtp_port}"
|
||
echo -e "${YELLOW}TLS 模式:${NC} ${tls_mode}"
|
||
echo -e "${YELLOW}SMTP 账号:${NC} ${smtp_user}"
|
||
echo -e "${UI_DIVIDER}"
|
||
echo -e "${RED}提醒:此功能会修改 postfix 配置并写入 SMTP 凭据文件。${NC}"
|
||
echo -e "${RED}请确保你使用的是邮箱提供商的 SMTP 授权码/应用专用密码,而非登录密码。${NC}"
|
||
echo -e "${UI_DIVIDER}"
|
||
|
||
if ! confirm_action "开始应用配置并重载 postfix?"; then
|
||
return 0
|
||
fi
|
||
|
||
log_step "配置 PVE 数据中心邮件选项"
|
||
pve_mail_configure_datacenter_emails "$from_addr" "$root_addr" || return 1
|
||
|
||
log_step "安装 SASL 模块(libsasl2-modules)"
|
||
apt-get update >/dev/null 2>&1 || true
|
||
if ! apt-get install -y libsasl2-modules >/dev/null 2>&1; then
|
||
display_error "安装 libsasl2-modules 失败" "请检查网络与软件源。"
|
||
return 1
|
||
fi
|
||
|
||
log_step "配置 postfix 通过 SMTP 中继发信"
|
||
pve_mail_configure_postfix_smtp "$smtp_host" "$smtp_port" "$tls_mode" "$smtp_user" "$smtp_pass" || return 1
|
||
|
||
local test_choice="yes"
|
||
read -p "是否发送测试邮件?(yes/no) [yes]: " test_choice
|
||
test_choice="${test_choice:-yes}"
|
||
if [[ "$test_choice" == "yes" || "$test_choice" == "YES" ]]; then
|
||
log_step "发送测试邮件"
|
||
if pve_mail_send_test "$from_addr" "$root_addr" "PVE-Tools 邮件测试" "这是一封测试邮件:如果你收到,说明 SMTP 中继已可用。"; then
|
||
log_success "测试邮件已提交发送队列(请检查收件箱与垃圾箱)"
|
||
else
|
||
log_warn "测试邮件发送失败,请检查 postfix 日志与 SMTP 配置"
|
||
log_tips "可查看:journalctl -u postfix -n 200 或 tail -n 200 /var/log/mail.log"
|
||
fi
|
||
fi
|
||
|
||
local zed_choice="no"
|
||
read -p "是否额外配置 ZFS ZED 邮件(ZFS 阵列事件通知)?(yes/no) [no]: " zed_choice
|
||
zed_choice="${zed_choice:-no}"
|
||
if [[ "$zed_choice" == "yes" || "$zed_choice" == "YES" ]]; then
|
||
log_step "配置 ZFS ZED 邮件参数"
|
||
pve_mail_configure_zed_mail "$from_addr" "$root_addr" || true
|
||
log_success "ZED 配置已处理(建议手动制造一次 ZFS 事件验证)"
|
||
fi
|
||
|
||
display_success "邮件通知配置完成" "建议在 WebUI 里触发一次通知或检查系统事件确认生效。"
|
||
return 0
|
||
}
|
||
|
||
# 获取已安装的 PVE 内核包(兼容 pve-kernel / proxmox-kernel 以及 -signed 后缀)
|
||
get_installed_kernel_packages() {
|
||
local status_regex="${1:-ii|hi}"
|
||
|
||
dpkg -l 2>/dev/null | awk -v sr="$status_regex" '
|
||
$1 ~ ("^(" sr ")$") &&
|
||
$2 ~ /^(pve-kernel|proxmox-kernel)-[0-9].*-pve(-signed)?$/ {
|
||
print $2
|
||
}
|
||
' | sort -Vu
|
||
}
|
||
|
||
# 检测当前内核版本
|
||
check_kernel_version() {
|
||
log_info "检测当前内核信息..."
|
||
local current_kernel=$(uname -r)
|
||
local kernel_arch=$(uname -m)
|
||
local kernel_variant=""
|
||
|
||
# 检测内核变体(普通/企业版/测试版)
|
||
if [[ $current_kernel == *"pve"* ]]; then
|
||
kernel_variant="PVE标准内核"
|
||
elif [[ $current_kernel == *"edge"* ]]; then
|
||
kernel_variant="PVE边缘内核"
|
||
elif [[ $current_kernel == *"test"* ]]; then
|
||
kernel_variant="测试内核"
|
||
else
|
||
kernel_variant="未知类型"
|
||
fi
|
||
|
||
echo -e "${CYAN}当前内核信息:${NC}"
|
||
echo -e " 版本: ${GREEN}$current_kernel${NC}"
|
||
echo -e " 架构: ${GREEN}$kernel_arch${NC}"
|
||
echo -e " 类型: ${GREEN}$kernel_variant${NC}"
|
||
|
||
# 检测可用的内核版本
|
||
local installed_kernels=$(get_installed_kernel_packages)
|
||
if [[ -n "$installed_kernels" ]]; then
|
||
echo -e "${CYAN}已安装的内核版本:${NC}"
|
||
while IFS= read -r kernel; do
|
||
echo -e " ${GREEN}•${NC} $kernel"
|
||
done <<< "$installed_kernels"
|
||
fi
|
||
|
||
return 0
|
||
}
|
||
|
||
# 获取可用内核列表
|
||
get_available_kernels() {
|
||
log_info "获取可用内核列表..."
|
||
|
||
# 检查网络连接
|
||
if ! ping -c 1 mirrors.tuna.tsinghua.edu.cn &> /dev/null; then
|
||
log_error "网络连接失败,无法获取内核列表"
|
||
return 1
|
||
fi
|
||
|
||
# 获取当前 PVE 版本
|
||
local pve_version=$(pveversion | head -n1 | cut -d'/' -f2 | cut -d'-' -f1)
|
||
local major_version=$(echo $pve_version | cut -d'.' -f1)
|
||
|
||
# 构建内核包URL
|
||
local kernel_url="https://mirrors.tuna.tsinghua.edu.cn/proxmox/debian/pve/dists/trixie/pve-no-subscription/binary-amd64/Packages"
|
||
|
||
# 下载并解析可用内核
|
||
local available_kernels=$(curl -s "$kernel_url" | grep -E 'Package: (pve-kernel|linux-pve)' | awk '{print $2}' | sort -V | uniq)
|
||
|
||
if [[ -z "$available_kernels" ]]; then
|
||
log_warn "无法获取可用内核列表,使用备用方法"
|
||
# 备用方法:使用apt-cache搜索
|
||
available_kernels=$(apt-cache search --names-only '^pve-kernel-.*' | awk '{print $1}' | sort -V)
|
||
fi
|
||
|
||
if [[ -n "$available_kernels" ]]; then
|
||
echo -e "${CYAN}可用内核版本:${NC}"
|
||
while IFS= read -r kernel; do
|
||
echo -e " ${BLUE}•${NC} $kernel"
|
||
done <<< "$available_kernels"
|
||
else
|
||
log_error "无法找到可用内核"
|
||
return 1
|
||
fi
|
||
|
||
return 0
|
||
}
|
||
|
||
# 安装指定内核版本
|
||
install_kernel() {
|
||
local kernel_version=$1
|
||
|
||
# 验证内核版本格式
|
||
if [[ -z "$kernel_version" ]]; then
|
||
log_error "请指定要安装的内核版本"
|
||
return 1
|
||
fi
|
||
|
||
# 检查是否已经是完整包名格式 (contains "pve" and ends with "pve")
|
||
if [[ "$kernel_version" =~ ^[a-zA-Z0-9.-]+pve$ ]]; then
|
||
# This looks like a complete package name, use it as is
|
||
log_info "检测到完整包名格式: $kernel_version"
|
||
elif ! [[ "$kernel_version" =~ ^pve-kernel- ]]; then
|
||
# If not in the correct format, prepend "pve-kernel-"
|
||
log_info "检测到版本号格式,自动补全包名为 pve-kernel-$kernel_version"
|
||
kernel_version="pve-kernel-$kernel_version"
|
||
fi
|
||
|
||
log_info "开始安装内核: $kernel_version"
|
||
|
||
# 检查内核是否已安装
|
||
if dpkg -l | grep -q "^ii.*$kernel_version"; then
|
||
log_warn "内核 $kernel_version 已经安装"
|
||
read -p "是否重新安装?(y/N): " reinstall
|
||
if [[ "$reinstall" != "y" && "$reinstall" != "Y" ]]; then
|
||
return 0
|
||
fi
|
||
fi
|
||
|
||
# 更新软件包列表
|
||
log_info "更新软件包列表..."
|
||
if ! apt-get update; then
|
||
log_error "更新软件包列表失败"
|
||
return 1
|
||
fi
|
||
|
||
# 安装内核
|
||
log_info "正在安装内核 $kernel_version ..."
|
||
if ! apt-get install -y "$kernel_version"; then
|
||
log_error "内核安装失败"
|
||
return 1
|
||
fi
|
||
|
||
log_success "内核 $kernel_version 安装成功"
|
||
|
||
# 更新引导配置
|
||
update_grub_config
|
||
|
||
return 0
|
||
}
|
||
|
||
# 更新 GRUB 配置
|
||
update_grub_config() {
|
||
log_info "更新引导配置..."
|
||
|
||
# 检查是否是 UEFI 系统
|
||
local efi_dir="/boot/efi"
|
||
local grub_cfg=""
|
||
|
||
if [[ -d "$efi_dir" ]]; then
|
||
log_info "检测到 UEFI 启动模式"
|
||
grub_cfg="/boot/efi/EFI/proxmox/grub.cfg"
|
||
else
|
||
log_info "检测到 Legacy BIOS 启动模式"
|
||
grub_cfg="/boot/grub/grub.cfg"
|
||
fi
|
||
|
||
# 更新 GRUB
|
||
if command -v update-grub &> /dev/null; then
|
||
if update-grub; then
|
||
log_success "GRUB 配置更新成功"
|
||
else
|
||
log_warn "GRUB 配置更新过程中出现警告,但可能仍然成功"
|
||
fi
|
||
elif command -v grub-mkconfig &> /dev/null; then
|
||
if grub-mkconfig -o "$grub_cfg"; then
|
||
log_success "GRUB 配置更新成功"
|
||
else
|
||
log_warn "GRUB 配置更新过程中出现警告"
|
||
fi
|
||
else
|
||
log_error "找不到 GRUB 更新工具"
|
||
return 1
|
||
fi
|
||
|
||
return 0
|
||
}
|
||
|
||
# 切换默认启动内核
|
||
set_default_kernel() {
|
||
local kernel_version=$1
|
||
|
||
if [[ -z "$kernel_version" ]]; then
|
||
log_error "请指定要设置为默认的内核版本"
|
||
return 1
|
||
fi
|
||
|
||
log_info "设置默认启动内核: ${GREEN}$kernel_version${NC}"
|
||
|
||
# 检查内核是否存在
|
||
if ! [[ -f "/boot/initrd.img-$kernel_version" && -f "/boot/vmlinuz-$kernel_version" ]]; then
|
||
log_error "内核文件不存在,请先安装该内核"
|
||
log_error "缺失文件: /boot/vmlinuz-$kernel_version 或 /boot/initrd.img-$kernel_version"
|
||
return 1
|
||
fi
|
||
|
||
# 使用 grub-set-default 设置默认内核
|
||
if command -v grub-set-default &> /dev/null; then
|
||
# 查找内核在 GRUB 菜单中的位置
|
||
local menu_entry=$(grep -n "$kernel_version" /boot/grub/grub.cfg | head -1 | cut -d: -f1)
|
||
if [[ -n "$menu_entry" ]]; then
|
||
# 计算 GRUB 菜单项索引(从0开始)
|
||
local grub_index=$(( (menu_entry - 1) / 2 ))
|
||
if grub-set-default "$grub_index"; then
|
||
log_success "默认启动内核设置成功"
|
||
return 0
|
||
fi
|
||
fi
|
||
fi
|
||
|
||
# 备用方法:手动编辑 GRUB 配置
|
||
log_warn "使用备用方法设置默认内核"
|
||
|
||
# 备份当前 GRUB 配置
|
||
cp /etc/default/grub /etc/default/grub.backup.$(date +%Y%m%d%H%M%S)
|
||
|
||
# 设置 GRUB_DEFAULT 为内核版本
|
||
if sed -i "s/^GRUB_DEFAULT=.*/GRUB_DEFAULT=\"Advanced options for Proxmox VE GNU\/Linux>Proxmox VE GNU\/Linux, with Linux $kernel_version\"/" /etc/default/grub; then
|
||
log_success "GRUB 配置更新成功"
|
||
update_grub_config
|
||
return 0
|
||
else
|
||
log_error "GRUB 配置更新失败"
|
||
return 1
|
||
fi
|
||
}
|
||
|
||
# 删除旧内核(保留最近2个版本)
|
||
remove_old_kernels() {
|
||
log_info "清理旧内核..."
|
||
|
||
# 获取所有已安装的内核
|
||
local installed_kernels
|
||
installed_kernels="$(get_installed_kernel_packages "ii")"
|
||
local -a kernel_list
|
||
mapfile -t kernel_list < <(printf '%s\n' "$installed_kernels" | sed '/^$/d')
|
||
local kernel_count=${#kernel_list[@]}
|
||
|
||
if [[ $kernel_count -le 2 ]]; then
|
||
log_info "当前只有 $kernel_count 个内核,无需清理"
|
||
return 0
|
||
fi
|
||
|
||
# 计算需要保留的内核数量(保留最新的2个)
|
||
local keep_count=2
|
||
local remove_count=$((kernel_count - keep_count))
|
||
|
||
echo -e "${YELLOW}将删除 $remove_count 个旧内核,保留最新的 $keep_count 个内核${NC}"
|
||
|
||
# 获取要删除的内核列表(最旧的几个)
|
||
local kernels_to_remove=("${kernel_list[@]:0:$remove_count}")
|
||
|
||
read -p "是否继续?(y/N): " confirm
|
||
if [[ "$confirm" != "y" && "$confirm" != "Y" ]]; then
|
||
log_info "取消内核清理"
|
||
return 0
|
||
fi
|
||
|
||
# 删除旧内核
|
||
for kernel in "${kernels_to_remove[@]}"; do
|
||
log_info "正在删除内核: $kernel"
|
||
if apt-get remove -y --purge "$kernel"; then
|
||
log_success "内核 $kernel 删除成功"
|
||
else
|
||
log_error "删除内核 $kernel 失败"
|
||
fi
|
||
done
|
||
|
||
# 更新引导配置
|
||
update_grub_config
|
||
|
||
log_success "旧内核清理完成"
|
||
return 0
|
||
}
|
||
|
||
# 内核管理主菜单
|
||
kernel_management_menu() {
|
||
while true; do
|
||
clear
|
||
show_menu_header "内核管理菜单"
|
||
show_menu_option "1" "显示当前内核信息"
|
||
show_menu_option "2" "查看可用内核列表"
|
||
show_menu_option "3" "安装新内核"
|
||
show_menu_option "4" "设置默认启动内核"
|
||
show_menu_option "5" "${RED}清理旧内核${NC}"
|
||
show_menu_option "6" "${YELLOW}重启系统应用新内核${NC}"
|
||
echo "${UI_DIVIDER}"
|
||
show_menu_option "0" "返回主菜单"
|
||
show_menu_footer
|
||
|
||
read -p "请选择操作 [0-6]: " choice
|
||
|
||
case $choice in
|
||
1)
|
||
check_kernel_version
|
||
;;
|
||
2)
|
||
get_available_kernels
|
||
;;
|
||
3)
|
||
echo "请输入要安装的内核版本:"
|
||
echo " - 完整包名格式 (推荐): 如 proxmox-kernel-6.14.8-2-pve"
|
||
echo " - 简化版本格式: 如 6.8.8-1 (将自动补全为 pve-kernel-6.8.8-1)"
|
||
read -p "请输入内核标识: " kernel_ver
|
||
if [[ -n "$kernel_ver" ]]; then
|
||
install_kernel "$kernel_ver"
|
||
else
|
||
log_error "请输入有效的内核版本"
|
||
fi
|
||
;;
|
||
4)
|
||
read -p "请输入要设置为默认的内核版本 (例如: 6.8.8-1-pve): " kernel_ver
|
||
if [[ -n "$kernel_ver" ]]; then
|
||
set_default_kernel "$kernel_ver"
|
||
else
|
||
log_error "请输入有效的内核版本"
|
||
fi
|
||
;;
|
||
5)
|
||
remove_old_kernels
|
||
;;
|
||
6)
|
||
read -p "确认要重启系统吗?(y/N): " reboot_confirm
|
||
if [[ "$reboot_confirm" == "y" || "$reboot_confirm" == "Y" ]]; then
|
||
log_info "系统将在5秒后重启..."
|
||
echo "按 Ctrl+C 取消重启"
|
||
sleep 5
|
||
reboot
|
||
else
|
||
log_info "取消重启"
|
||
fi
|
||
;;
|
||
0)
|
||
break
|
||
;;
|
||
*)
|
||
log_error "无效的选择,请重新输入"
|
||
;;
|
||
esac
|
||
|
||
echo
|
||
pause_function
|
||
done
|
||
}
|
||
|
||
# 内核同步更新(自动检测并更新到最新稳定版)
|
||
sync_kernel_update() {
|
||
log_info "开始内核同步更新检查..."
|
||
|
||
# 获取当前内核版本
|
||
local current_kernel=$(uname -r)
|
||
log_info "当前内核版本: ${GREEN}$current_kernel${NC}"
|
||
|
||
# 获取最新可用内核
|
||
local latest_kernel=$(get_available_kernels | tail -1 | awk '{print $2}')
|
||
|
||
if [[ -z "$latest_kernel" ]]; then
|
||
log_error "无法获取最新内核信息"
|
||
return 1
|
||
fi
|
||
|
||
log_info "最新可用内核: ${GREEN}$latest_kernel${NC}"
|
||
|
||
# 检查是否需要更新
|
||
if [[ "$current_kernel" == *"$latest_kernel"* ]]; then
|
||
log_success "当前已是最新内核,无需更新"
|
||
return 0
|
||
fi
|
||
|
||
echo -e "${YELLOW}发现新内核版本: $latest_kernel${NC}"
|
||
read -p "是否安装并更新到最新内核?(Y/n): " update_confirm
|
||
|
||
if [[ "$update_confirm" == "n" || "$update_confirm" == "N" ]]; then
|
||
log_info "取消内核更新"
|
||
return 0
|
||
fi
|
||
|
||
# 安装最新内核
|
||
if install_kernel "$latest_kernel"; then
|
||
# 设置新内核为默认启动项
|
||
if set_default_kernel "$latest_kernel"; then
|
||
log_success "内核同步更新完成"
|
||
echo -e "${YELLOW}建议重启系统以应用新内核${NC}"
|
||
return 0
|
||
else
|
||
log_warn "内核安装成功但设置默认启动项失败"
|
||
return 1
|
||
fi
|
||
else
|
||
log_error "内核更新失败"
|
||
return 1
|
||
fi
|
||
}
|
||
|
||
# 备份文件
|
||
backup_file() {
|
||
local file="$1"
|
||
if [[ -f "$file" ]]; then
|
||
# 创建备份目录
|
||
local backup_dir="/etc/pve-tools-9-bak"
|
||
mkdir -p "$backup_dir"
|
||
|
||
# 生成带时间戳的备份文件名
|
||
local filename=$(basename "$file")
|
||
local timestamp=$(date +%Y%m%d_%H%M%S)
|
||
local backup_path="${backup_dir}/${filename}.backup.${timestamp}"
|
||
|
||
cp "$file" "$backup_path"
|
||
|
||
# 仅记录到日志文件,减少控制台干扰
|
||
echo "[$(date +'%H:%M:%S')] [BACKUP] $file -> $backup_path" >> /var/log/pve-tools.log
|
||
fi
|
||
}
|
||
|
||
# 换源功能
|
||
change_sources() {
|
||
block_non_pve9_destructive "更换软件源" || return 1
|
||
log_step "开始为您的 PVE 换上飞速源"
|
||
|
||
# 根据选择的镜像源确定URL
|
||
local debian_mirror=""
|
||
local debian_security_mirror=""
|
||
local pve_mirror=""
|
||
local ct_mirror=""
|
||
|
||
case $SELECTED_MIRROR in
|
||
$MIRROR_USTC)
|
||
debian_mirror="https://mirrors.ustc.edu.cn/debian"
|
||
pve_mirror="$MIRROR_USTC"
|
||
ceph_mirror="$CEPH_MIRROR_USTC"
|
||
ct_mirror="$CT_MIRROR_USTC"
|
||
;;
|
||
$MIRROR_TUNA)
|
||
debian_mirror="https://mirrors.tuna.tsinghua.edu.cn/debian"
|
||
pve_mirror="$MIRROR_TUNA"
|
||
ceph_mirror="$CEPH_MIRROR_TUNA"
|
||
ct_mirror="$CT_MIRROR_TUNA"
|
||
;;
|
||
$MIRROR_DEBIAN)
|
||
debian_mirror="https://deb.debian.org/debian"
|
||
debian_security_mirror="https://security.debian.org/debian-security"
|
||
pve_mirror="https://ftp.debian.org/debian"
|
||
ceph_mirror="$CEPH_MIRROR_OFFICIAL"
|
||
ct_mirror="$CT_MIRROR_OFFICIAL"
|
||
;;
|
||
esac
|
||
|
||
# 询问用户是否要更换安全更新源
|
||
log_info "安全更新源选择"
|
||
echo "═════════════════════════════════════════════════"
|
||
echo " 安全更新源包含重要的系统安全补丁,选择合适的源很重要:"
|
||
echo " 1) 使用官方安全源 (推荐,更新最及时,但可能较慢)"
|
||
echo " 2) 使用镜像站安全源 (速度快,但可能有延迟)"
|
||
echo "═════════════════════════════════════════════════"
|
||
|
||
read -p " 请选择 [1-2] (默认: 1): " security_choice
|
||
security_choice=${security_choice:-1}
|
||
|
||
if [[ "$security_choice" == "2" ]]; then
|
||
# 使用镜像站的安全源
|
||
case $SELECTED_MIRROR in
|
||
$MIRROR_USTC)
|
||
debian_security_mirror="https://mirrors.ustc.edu.cn/debian-security"
|
||
;;
|
||
$MIRROR_TUNA)
|
||
debian_security_mirror="https://mirrors.tuna.tsinghua.edu.cn/debian-security"
|
||
;;
|
||
$MIRROR_DEBIAN)
|
||
debian_security_mirror="https://security.debian.org/debian-security"
|
||
;;
|
||
esac
|
||
log_info "将使用镜像站的安全更新源"
|
||
else
|
||
# 使用官方安全源
|
||
debian_security_mirror="https://security.debian.org/debian-security"
|
||
log_info "将使用官方安全更新源"
|
||
fi
|
||
|
||
# 1. 更换 Debian 软件源 (DEB822 格式)
|
||
log_info "正在配置 Debian 镜像源..."
|
||
backup_file "/etc/apt/sources.list.d/debian.sources"
|
||
|
||
cat > /etc/apt/sources.list.d/debian.sources << EOF
|
||
Types: deb
|
||
URIs: $debian_mirror
|
||
Suites: trixie trixie-updates trixie-backports
|
||
Components: main contrib non-free non-free-firmware
|
||
Signed-By: /usr/share/keyrings/debian-archive-keyring.gpg
|
||
|
||
# 默认注释了源码镜像以提高 apt update 速度,如有需要可自行取消注释
|
||
# Types: deb-src
|
||
# URIs: $debian_mirror
|
||
# Suites: trixie trixie-updates trixie-backports
|
||
# Components: main contrib non-free non-free-firmware
|
||
# Signed-By: /usr/share/keyrings/debian-archive-keyring.gpg
|
||
|
||
# 以下安全更新软件源包含了官方源与镜像站配置,如有需要可自行修改注释切换
|
||
Types: deb
|
||
URIs: $debian_security_mirror
|
||
Suites: trixie-security
|
||
Components: main contrib non-free non-free-firmware
|
||
Signed-By: /usr/share/keyrings/debian-archive-keyring.gpg
|
||
|
||
# Types: deb-src
|
||
# URIs: $debian_security_mirror
|
||
# Suites: trixie-security
|
||
# Components: main contrib non-free non-free-firmware
|
||
# Signed-By: /usr/share/keyrings/debian-archive-keyring.gpg
|
||
EOF
|
||
|
||
# 2. 注释企业源
|
||
log_info "正在关闭企业源(我们用免费版就够啦)..."
|
||
if [[ -f "/etc/apt/sources.list.d/pve-enterprise.sources" ]]; then
|
||
backup_file "/etc/apt/sources.list.d/pve-enterprise.sources"
|
||
sed -i 's/^Types:/#Types:/g' /etc/apt/sources.list.d/pve-enterprise.sources
|
||
sed -i 's/^URIs:/#URIs:/g' /etc/apt/sources.list.d/pve-enterprise.sources
|
||
sed -i 's/^Suites:/#Suites:/g' /etc/apt/sources.list.d/pve-enterprise.sources
|
||
sed -i 's/^Components:/#Components:/g' /etc/apt/sources.list.d/pve-enterprise.sources
|
||
sed -i 's/^Signed-By:/#Signed-By:/g' /etc/apt/sources.list.d/pve-enterprise.sources
|
||
fi
|
||
|
||
# 3. 更换 Ceph 源
|
||
log_info "正在配置 Ceph 镜像源..."
|
||
if [[ -f "/etc/apt/sources.list.d/ceph.sources" ]]; then
|
||
backup_file "/etc/apt/sources.list.d/ceph.sources"
|
||
cat > /etc/apt/sources.list.d/ceph.sources << EOF
|
||
Types: deb
|
||
URIs: $ceph_mirror
|
||
Suites: trixie
|
||
Components: no-subscription
|
||
Signed-By: /usr/share/keyrings/proxmox-archive-keyring.gpg
|
||
EOF
|
||
fi
|
||
|
||
# 4. 添加无订阅源
|
||
log_info "正在添加免费版专用源..."
|
||
cat > /etc/apt/sources.list.d/pve-no-subscription.sources << EOF
|
||
Types: deb
|
||
URIs: $pve_mirror
|
||
Suites: trixie
|
||
Components: pve-no-subscription
|
||
Signed-By: /usr/share/keyrings/proxmox-archive-keyring.gpg
|
||
EOF
|
||
|
||
# 5. 更换 CT 模板源
|
||
log_info "正在加速 CT 模板下载..."
|
||
if [[ -f "/usr/share/perl5/PVE/APLInfo.pm" ]]; then
|
||
backup_file "/usr/share/perl5/PVE/APLInfo.pm"
|
||
# 先恢复为官方源,确保可以二次替换
|
||
sed -i "s|https://mirrors.ustc.edu.cn/proxmox|http://download.proxmox.com|g" /usr/share/perl5/PVE/APLInfo.pm
|
||
sed -i "s|https://mirrors.tuna.tsinghua.edu.cn/proxmox|http://download.proxmox.com|g" /usr/share/perl5/PVE/APLInfo.pm
|
||
# 然后替换为选定的镜像源
|
||
sed -i "s|http://download.proxmox.com|$ct_mirror|g" /usr/share/perl5/PVE/APLInfo.pm
|
||
fi
|
||
|
||
log_success "太棒了!所有源都换成飞速版本啦"
|
||
}
|
||
|
||
# 删除订阅弹窗
|
||
remove_subscription_popup() {
|
||
block_non_pve9_destructive "删除订阅弹窗" || return 1
|
||
log_step "正在消除那个烦人的订阅弹窗"
|
||
|
||
local js_file="/usr/share/javascript/proxmox-widget-toolkit/proxmoxlib.js"
|
||
if [[ -f "$js_file" ]]; then
|
||
backup_file "$js_file"
|
||
|
||
# 修复逻辑:
|
||
# 新版 PVE 的 proxmoxlib.js 在 Ext.Msg.show 调用前有大量换行和空格
|
||
# 原有的 sed 正则 "Ext.Msg.show\(\{\s+title" 可能因为换行符匹配失败
|
||
# 新方案:直接将判断条件中的 !== 'active' 改为 == 'active',从逻辑上短路
|
||
# 匹配模式:res.data.status.toLowerCase() !== 'active'
|
||
# 这种方式比替换 Ext.Msg.show 更稳定,且代码侵入性更小
|
||
|
||
if grep -q "res.data.status.toLowerCase() !== 'active'" "$js_file"; then
|
||
sed -i "s/res.data.status.toLowerCase() !== 'active'/res.data.status.toLowerCase() == 'active'/g" "$js_file"
|
||
log_success "策略A生效:修改了判断逻辑"
|
||
elif grep -q "Ext.Msg.show({" "$js_file"; then
|
||
# 备用方案:如果找不到特定判断逻辑,尝试旧方法的宽泛匹配,但增强兼容性
|
||
# 使用 perl 替代 sed 以更好地支持多行匹配
|
||
perl -i -0777 -pe "s/(Ext\.Msg\.show\(\{\s+title: gettext\('No valid sub)/void\(\{ \/\/\1/g" "$js_file"
|
||
log_success "策略B生效:屏蔽了弹窗函数"
|
||
else
|
||
log_error "未找到匹配的代码片段,可能文件版本已更新"
|
||
return 1
|
||
fi
|
||
|
||
systemctl restart pveproxy.service
|
||
log_success "完美!再也不会有烦人的弹窗啦"
|
||
else
|
||
log_warn "咦?没找到弹窗文件,可能已经被处理过了"
|
||
fi
|
||
}
|
||
|
||
reinstall_pve_webui_packages() {
|
||
log_step "正在重新安装官方 Web UI 相关软件包"
|
||
if apt-get install --reinstall -y pve-manager proxmox-widget-toolkit; then
|
||
systemctl restart pveproxy.service
|
||
log_success "官方 Web UI 文件已恢复"
|
||
return 0
|
||
fi
|
||
|
||
log_error "重新安装失败,请检查软件源或网络后重试:apt-get install --reinstall -y pve-manager proxmox-widget-toolkit"
|
||
return 1
|
||
}
|
||
|
||
# 恢复 proxmoxlib.js 文件
|
||
restore_proxmoxlib() {
|
||
log_step "准备恢复官方 Web UI 文件"
|
||
log_warn "此操作会重新安装 pve-manager 和 proxmox-widget-toolkit,并覆盖当前前端补丁"
|
||
|
||
if ! confirm_action "确定要恢复官方 Web UI 文件吗?"; then
|
||
return
|
||
fi
|
||
|
||
reinstall_pve_webui_packages
|
||
}
|
||
|
||
# 合并 local 与 local-lvm
|
||
merge_local_storage() {
|
||
log_step "准备合并存储空间,让小硬盘发挥最大价值"
|
||
log_warn "重要提醒:此操作会删除 local-lvm,请确保重要数据已备份!"
|
||
|
||
echo -e "${YELLOW}您确定要继续吗?这个操作不可逆哦${NC}"
|
||
read -p "输入 'yes' 确认继续,其他任意键取消: " -r
|
||
if [[ ! $REPLY == "yes" ]]; then
|
||
log_info "明智的选择!操作已取消"
|
||
return
|
||
fi
|
||
|
||
# 检查 local-lvm 是否存在
|
||
if ! lvdisplay /dev/pve/data &> /dev/null; then
|
||
log_warn "没有找到 local-lvm 分区,可能已经合并过了"
|
||
return
|
||
fi
|
||
|
||
log_info "正在删除 local-lvm 分区..."
|
||
lvremove -f /dev/pve/data
|
||
|
||
log_info "正在扩容 local 分区..."
|
||
lvextend -l +100%FREE /dev/pve/root
|
||
|
||
log_info "正在扩展文件系统..."
|
||
resize2fs /dev/pve/root
|
||
|
||
log_success "存储合并完成!现在空间更充裕了"
|
||
log_warn "温馨提示:请在 Web UI 中删除 local-lvm 存储配置,并编辑 local 存储勾选所有内容类型"
|
||
}
|
||
|
||
# 删除 Swap 分配给主分区
|
||
remove_swap() {
|
||
log_step "准备释放 Swap 空间给系统使用"
|
||
log_warn "注意:删除 Swap 后请确保内存充足!"
|
||
|
||
echo -e "${YELLOW}您确定要删除 Swap 分区吗?${NC}"
|
||
read -p "输入 'yes' 确认继续,其他任意键取消: " -r
|
||
if [[ ! $REPLY == "yes" ]]; then
|
||
log_info "好的,操作已取消"
|
||
return
|
||
fi
|
||
|
||
# 检查 swap 是否存在
|
||
if ! lvdisplay /dev/pve/swap &> /dev/null; then
|
||
log_warn "没有找到 swap 分区,可能已经删除过了"
|
||
return
|
||
fi
|
||
|
||
log_info "正在关闭 Swap..."
|
||
swapoff /dev/mapper/pve-swap
|
||
|
||
log_info "正在修改启动配置..."
|
||
backup_file "/etc/fstab"
|
||
sed -i 's|^/dev/pve/swap|# /dev/pve/swap|g' /etc/fstab
|
||
|
||
log_info "正在删除 swap 分区..."
|
||
lvremove -f /dev/pve/swap
|
||
|
||
log_info "正在扩展系统分区..."
|
||
lvextend -l +100%FREE /dev/mapper/pve-root
|
||
|
||
log_info "正在扩展文件系统..."
|
||
resize2fs /dev/mapper/pve-root
|
||
|
||
log_success "Swap 删除完成!系统空间更宽裕了"
|
||
}
|
||
|
||
# 更新系统
|
||
update_system() {
|
||
block_non_pve9_destructive "更新系统软件包" || return 1
|
||
log_step "开始更新系统,让 PVE 保持最新状态 📦"
|
||
|
||
echo -e "${CYAN}正在更新软件包列表...${NC}"
|
||
apt update
|
||
|
||
echo -e "${CYAN}正在升级系统软件包...${NC}"
|
||
apt upgrade -y
|
||
|
||
echo -e "${CYAN}正在清理不需要的软件包...${NC}"
|
||
apt autoremove -y
|
||
|
||
log_success "系统更新完成!您的 PVE 现在是最新版本"
|
||
}
|
||
|
||
# 标准化暂停函数
|
||
pause_function() {
|
||
echo -n "按任意键继续... "
|
||
read -n 1 -s input
|
||
if [[ -n ${input} ]]; then
|
||
echo -e "\b
|
||
"
|
||
fi
|
||
}
|
||
|
||
|
||
|
||
#--------------开启硬件直通----------------
|
||
# 开启硬件直通
|
||
enable_pass() {
|
||
echo
|
||
log_step "开启硬件直通..."
|
||
if [ `dmesg | grep -e DMAR -e IOMMU|wc -l` = 0 ];then
|
||
log_error "您的硬件不支持直通!不如检查一下主板的BIOS设置?"
|
||
pause_function
|
||
return
|
||
fi
|
||
if [ `cat /proc/cpuinfo|grep Intel|wc -l` = 0 ];then
|
||
iommu="amd_iommu=on"
|
||
else
|
||
iommu="intel_iommu=on"
|
||
fi
|
||
if [ `grep $iommu /etc/default/grub|wc -l` = 0 ];then
|
||
backup_file "/etc/default/grub"
|
||
sed -i 's|quiet|quiet '$iommu'|' /etc/default/grub
|
||
update-grub
|
||
if [ `grep "vfio" /etc/modules|wc -l` = 0 ];then
|
||
cat <<-EOF >> /etc/modules
|
||
vfio
|
||
vfio_iommu_type1
|
||
vfio_pci
|
||
vfio_virqfd
|
||
kvmgt
|
||
EOF
|
||
fi
|
||
|
||
# 使用安全的配置块管理
|
||
blacklist_content="blacklist snd_hda_intel
|
||
blacklist snd_hda_codec_hdmi
|
||
blacklist i915"
|
||
apply_block "/etc/modprobe.d/blacklist.conf" "HARDWARE_PASSTHROUGH" "$blacklist_content"
|
||
|
||
# 使用安全的配置块管理
|
||
vfio_content="options vfio-pci ids=8086:3185"
|
||
apply_block "/etc/modprobe.d/vfio.conf" "HARDWARE_PASSTHROUGH" "$vfio_content"
|
||
|
||
log_success "开启设置后需要重启系统,请准备就绪后重启宿主机"
|
||
log_tips "重启后才可以应用对内核引导的修改哦!命令是 reboot"
|
||
else
|
||
log_warn "您已经配置过!"
|
||
fi
|
||
}
|
||
|
||
# 关闭硬件直通
|
||
disable_pass() {
|
||
echo
|
||
log_step "关闭硬件直通..."
|
||
if [ `dmesg | grep -e DMAR -e IOMMU|wc -l` = 0 ];then
|
||
log_error "您的硬件不支持直通!"
|
||
log_tips "不如检查一下主板的BIOS设置?"
|
||
pause_function
|
||
return
|
||
fi
|
||
if [ `cat /proc/cpuinfo|grep Intel|wc -l` = 0 ];then
|
||
iommu="amd_iommu=on"
|
||
else
|
||
iommu="intel_iommu=on"
|
||
fi
|
||
if [ `grep $iommu /etc/default/grub|wc -l` = 0 ];then
|
||
log_warn "您还没有配置过该项"
|
||
else
|
||
backup_file "/etc/default/grub"
|
||
{
|
||
sed -i 's/ '$iommu'//g' /etc/default/grub
|
||
sed -i '/vfio/d' /etc/modules
|
||
# 使用安全的配置块删除,而不是直接删除整个文件
|
||
remove_block "/etc/modprobe.d/blacklist.conf" "HARDWARE_PASSTHROUGH"
|
||
remove_block "/etc/modprobe.d/vfio.conf" "HARDWARE_PASSTHROUGH"
|
||
sleep 1
|
||
}
|
||
log_success "关闭设置后需要重启系统,请准备就绪后重启宿主机。"
|
||
log_tips "重启后才可以应用对内核引导的修改哦!命令是 reboot"
|
||
sleep 1
|
||
update-grub
|
||
fi
|
||
}
|
||
|
||
# 硬件直通菜单
|
||
hw_passth() {
|
||
while :; do
|
||
clear
|
||
show_menu_header "配置硬件直通"
|
||
show_menu_option "1" "开启硬件直通"
|
||
show_menu_option "2" "关闭硬件直通"
|
||
echo "${UI_DIVIDER}"
|
||
show_menu_option "0" "返回"
|
||
show_menu_footer
|
||
read -p "请选择: [ ]" -n 1 hwmenuid
|
||
echo # New line after input
|
||
hwmenuid=${hwmenuid:-0}
|
||
case "${hwmenuid}" in
|
||
1)
|
||
enable_pass
|
||
pause_function
|
||
;;
|
||
2)
|
||
disable_pass
|
||
pause_function
|
||
;;
|
||
0)
|
||
break
|
||
;;
|
||
*)
|
||
log_error "无效选项!"
|
||
pause_function
|
||
;;
|
||
esac
|
||
done
|
||
}
|
||
#--------------磁盘/控制器直通----------------
|
||
|
||
# 磁盘/控制器直通总菜单
|
||
menu_disk_controller_passthrough() {
|
||
while true; do
|
||
clear
|
||
show_menu_header "磁盘/控制器直通"
|
||
show_menu_option "1" "RDM(裸磁盘映射)- 单个磁盘直通"
|
||
show_menu_option "2" "RDM 取消直通(--delete)"
|
||
show_menu_option "3" "磁盘控制器直通(PCIe)"
|
||
show_menu_option "4" "NVMe 直通(含 MSI-X 重定位)"
|
||
show_menu_option "5" "引导配置辅助(UEFI/Legacy)"
|
||
show_menu_option "0" "返回"
|
||
show_menu_footer
|
||
read -p "请选择操作 [0-5]: " choice
|
||
case "$choice" in
|
||
1) rdm_single_disk_attach ;;
|
||
2) rdm_single_disk_detach ;;
|
||
3) storage_controller_passthrough ;;
|
||
4) nvme_passthrough ;;
|
||
5) boot_config_assistant ;;
|
||
0) return ;;
|
||
*) log_error "无效选择" ;;
|
||
esac
|
||
pause_function
|
||
done
|
||
}
|
||
|
||
# ============ RDM(裸磁盘映射)单盘直通 ============
|
||
|
||
# 获取 VM 配置文件路径(不保证一定存在,需调用方自行判断)
|
||
get_qm_conf_path() {
|
||
local vmid="$1"
|
||
echo "/etc/pve/qemu-server/${vmid}.conf"
|
||
}
|
||
|
||
# 校验 VMID 并确保 VM 存在
|
||
validate_qm_vmid() {
|
||
local vmid="$1"
|
||
if [[ -z "$vmid" || ! "$vmid" =~ ^[0-9]+$ ]]; then
|
||
log_error "VMID 必须是数字"
|
||
return 1
|
||
fi
|
||
if ! qm status "$vmid" >/dev/null 2>&1; then
|
||
log_error "VMID 不存在或无法访问: $vmid"
|
||
return 1
|
||
fi
|
||
return 0
|
||
}
|
||
|
||
# 将 /dev/disk/by-id 的链接解析为真实磁盘设备,并过滤不可直通设备
|
||
# 过滤规则:
|
||
# - 排除分区:by-id 名称包含 -partX 或目标设备为分区(lsblk TYPE=part)
|
||
# - 排除 DM/LVM:目标设备为 dm-* 或 /dev/mapper/*
|
||
# - 仅保留 TYPE=disk 的完整磁盘
|
||
rdm_discover_whole_disks() {
|
||
local byid_dir="/dev/disk/by-id"
|
||
if [[ ! -d "$byid_dir" ]]; then
|
||
log_error "未找到目录: $byid_dir"
|
||
return 1
|
||
fi
|
||
|
||
local -A best_id_for_dev=()
|
||
local -A best_pri_for_dev=()
|
||
local -A ata_id_for_dev=()
|
||
|
||
local link
|
||
while IFS= read -r -d '' link; do
|
||
local base_name real_dev dev_name dev_type pri
|
||
base_name="$(basename "$link")"
|
||
|
||
if [[ "$base_name" =~ -part[0-9]+$ ]]; then
|
||
continue
|
||
fi
|
||
|
||
real_dev="$(readlink -f "$link" 2>/dev/null)"
|
||
if [[ -z "$real_dev" ]]; then
|
||
continue
|
||
fi
|
||
|
||
if [[ "$real_dev" == /dev/mapper/* || "$(basename "$real_dev")" == dm-* ]]; then
|
||
continue
|
||
fi
|
||
|
||
if [[ ! -b "$real_dev" ]]; then
|
||
continue
|
||
fi
|
||
|
||
dev_type="$(lsblk -dn -o TYPE "$real_dev" 2>/dev/null | head -n 1)"
|
||
if [[ "$dev_type" != "disk" ]]; then
|
||
continue
|
||
fi
|
||
|
||
pri=50
|
||
if [[ "$base_name" =~ ^wwn- ]]; then pri=10; fi
|
||
if [[ "$base_name" =~ ^nvme-eui ]]; then pri=10; fi
|
||
if [[ "$base_name" =~ ^nvme-uuid ]]; then pri=15; fi
|
||
if [[ "$base_name" =~ ^ata- ]]; then pri=20; fi
|
||
if [[ "$base_name" =~ ^scsi- ]]; then pri=30; fi
|
||
if [[ "$base_name" =~ ^pci- ]]; then pri=40; fi
|
||
|
||
if [[ "$base_name" =~ ^ata- ]] && [[ -z "${ata_id_for_dev[$real_dev]:-}" ]]; then
|
||
ata_id_for_dev["$real_dev"]="$link"
|
||
fi
|
||
|
||
if [[ -z "${best_id_for_dev[$real_dev]:-}" || "$pri" -lt "${best_pri_for_dev[$real_dev]}" ]]; then
|
||
best_id_for_dev["$real_dev"]="$link"
|
||
best_pri_for_dev["$real_dev"]="$pri"
|
||
fi
|
||
done < <(find "$byid_dir" -maxdepth 1 -type l -print0 2>/dev/null)
|
||
|
||
local dev
|
||
for dev in "${!best_id_for_dev[@]}"; do
|
||
local id_path size model ata_path
|
||
id_path="${best_id_for_dev[$dev]}"
|
||
ata_path="${ata_id_for_dev[$dev]:-}"
|
||
size="$(lsblk -dn -o SIZE "$dev" 2>/dev/null | head -n 1)"
|
||
model="$(lsblk -dn -o MODEL "$dev" 2>/dev/null | head -n 1)"
|
||
printf '%s|%s|%s|%s|%s\n' "$id_path" "$dev" "${size:-?}" "${model:-?}" "$ata_path"
|
||
done | sort -t'|' -k2,2
|
||
}
|
||
|
||
# 自动查找总线类型下可用插槽(sata 最多 6 个,ide 最多 4 个)
|
||
rdm_find_free_slot() {
|
||
local vmid="$1"
|
||
local bus="$2"
|
||
|
||
local max_idx=0
|
||
case "$bus" in
|
||
sata) max_idx=5 ;;
|
||
ide) max_idx=3 ;;
|
||
scsi) max_idx=30 ;;
|
||
*) log_error "不支持的总线类型: $bus"; return 1 ;;
|
||
esac
|
||
|
||
local cfg
|
||
cfg="$(qm config "$vmid" 2>/dev/null)"
|
||
if [[ -z "$cfg" ]]; then
|
||
log_error "无法读取 VM 配置: $vmid"
|
||
return 1
|
||
fi
|
||
|
||
local i
|
||
for ((i=0; i<=max_idx; i++)); do
|
||
if ! echo "$cfg" | grep -qE "^${bus}${i}:"; then
|
||
echo "${bus}${i}"
|
||
return 0
|
||
fi
|
||
done
|
||
|
||
log_error "无可用插槽: $bus (0-$max_idx)"
|
||
return 1
|
||
}
|
||
|
||
# RDM 单盘直通(添加)
|
||
rdm_single_disk_attach() {
|
||
log_step "RDM 单盘直通 - 磁盘发现"
|
||
|
||
local disks
|
||
disks="$(rdm_discover_whole_disks)"
|
||
if [[ -z "$disks" ]]; then
|
||
display_error "未发现可直通的完整磁盘" "请检查 /dev/disk/by-id 是否存在可用磁盘,或确认磁盘未被 DM/LVM 接管。"
|
||
return 1
|
||
fi
|
||
|
||
echo -e "${CYAN}可直通磁盘列表(完整磁盘):${NC}"
|
||
echo "$disks" | awk -F'|' '{
|
||
ata=$5;
|
||
if (ata == "") ata="-";
|
||
else {
|
||
n=split(ata,a,"/");
|
||
ata=a[n];
|
||
}
|
||
printf " [%d] %-55s -> %-12s %-8s %-28s ATA:%s\n", NR, $1, $2, $3, $4, ata
|
||
}'
|
||
echo -e "${UI_DIVIDER}"
|
||
|
||
local pick
|
||
read -p "请选择磁盘序号 (返回请输入 0): " pick
|
||
pick="${pick:-0}"
|
||
if [[ "$pick" == "0" ]]; then
|
||
return 0
|
||
fi
|
||
if [[ ! "$pick" =~ ^[0-9]+$ ]]; then
|
||
display_error "磁盘序号必须是数字"
|
||
return 1
|
||
fi
|
||
|
||
local selected
|
||
selected="$(echo "$disks" | awk -F'|' -v n="$pick" 'NR==n{print $0}')"
|
||
if [[ -z "$selected" ]]; then
|
||
display_error "无效的磁盘序号: $pick"
|
||
return 1
|
||
fi
|
||
|
||
local id_path real_dev
|
||
id_path="$(echo "$selected" | awk -F'|' '{print $1}')"
|
||
real_dev="$(echo "$selected" | awk -F'|' '{print $2}')"
|
||
|
||
local vmid
|
||
read -p "请输入目标 VMID: " vmid
|
||
if ! validate_qm_vmid "$vmid"; then
|
||
pause_function
|
||
return 1
|
||
fi
|
||
|
||
local bus
|
||
read -p "请选择总线类型 (scsi/sata/ide) [scsi]: " bus
|
||
bus="${bus:-scsi}"
|
||
if [[ "$bus" != "scsi" && "$bus" != "sata" && "$bus" != "ide" ]]; then
|
||
display_error "不支持的总线类型: $bus" "仅支持 scsi/sata/ide"
|
||
return 1
|
||
fi
|
||
|
||
local cfg
|
||
cfg="$(qm config "$vmid" 2>/dev/null)"
|
||
if echo "$cfg" | grep -Fq "$id_path" || echo "$cfg" | grep -Fq "$real_dev"; then
|
||
display_error "该磁盘已在 VM 配置中存在直通记录" "请先执行取消直通,或选择其他磁盘。"
|
||
return 1
|
||
fi
|
||
|
||
local slot
|
||
slot="$(rdm_find_free_slot "$vmid" "$bus")" || return 1
|
||
|
||
log_info "将直通磁盘: $id_path -> $real_dev"
|
||
log_info "目标 VM: $vmid, 插槽: $slot"
|
||
|
||
local conf_path
|
||
conf_path="$(get_qm_conf_path "$vmid")"
|
||
if [[ -f "$conf_path" ]]; then
|
||
log_tips "修改 VM 配置前建议备份原配置"
|
||
backup_file "$conf_path" >/dev/null 2>&1 || true
|
||
fi
|
||
|
||
if ! confirm_action "为 VM $vmid 添加直通磁盘($slot = $id_path)"; then
|
||
return 0
|
||
fi
|
||
|
||
if qm set "$vmid" "-$slot" "$id_path" >/dev/null 2>&1; then
|
||
display_success "直通配置已写入" "如需引导此磁盘,请在 VM 启动顺序中选择该磁盘。"
|
||
return 0
|
||
else
|
||
display_error "qm set 执行失败" "请检查磁盘是否被占用、VM 是否锁定,或查看 /var/log/pve-tools.log。"
|
||
return 1
|
||
fi
|
||
}
|
||
|
||
# RDM 取消直通(--delete)
|
||
rdm_single_disk_detach() {
|
||
log_step "RDM 取消直通(--delete)"
|
||
|
||
local vmid
|
||
read -p "请输入目标 VMID: " vmid
|
||
if ! validate_qm_vmid "$vmid"; then
|
||
return 1
|
||
fi
|
||
|
||
local cfg
|
||
cfg="$(qm config "$vmid" 2>/dev/null)"
|
||
if [[ -z "$cfg" ]]; then
|
||
display_error "无法读取 VM 配置: $vmid"
|
||
return 1
|
||
fi
|
||
|
||
local disks_lines
|
||
disks_lines="$(echo "$cfg" | grep -E '^(scsi|sata|ide)[0-9]+:')"
|
||
if [[ -z "$disks_lines" ]]; then
|
||
display_error "该 VM 未发现任何磁盘插槽配置" "如果只是没有直通盘,可忽略此提示。"
|
||
return 1
|
||
fi
|
||
|
||
echo -e "${CYAN}当前 VM 磁盘插槽:${NC}"
|
||
echo "$disks_lines" | awk '{printf " [%d] %s\n", NR, $0}'
|
||
echo -e "${UI_DIVIDER}"
|
||
|
||
local pick
|
||
read -p "请选择要删除的插槽序号 (返回请输入 0): " pick
|
||
pick="${pick:-0}"
|
||
if [[ "$pick" == "0" ]]; then
|
||
return 0
|
||
fi
|
||
if [[ ! "$pick" =~ ^[0-9]+$ ]]; then
|
||
display_error "序号必须是数字"
|
||
return 1
|
||
fi
|
||
|
||
local line slot
|
||
line="$(echo "$disks_lines" | awk -v n="$pick" 'NR==n{print $0}')"
|
||
if [[ -z "$line" ]]; then
|
||
display_error "无效的序号: $pick"
|
||
return 1
|
||
fi
|
||
slot="$(echo "$line" | cut -d':' -f1)"
|
||
|
||
local conf_path
|
||
conf_path="$(get_qm_conf_path "$vmid")"
|
||
if [[ -f "$conf_path" ]]; then
|
||
log_tips "修改 VM 配置前建议备份原配置"
|
||
backup_file "$conf_path" >/dev/null 2>&1 || true
|
||
fi
|
||
|
||
if ! confirm_action "从 VM $vmid 删除磁盘插槽(--delete $slot)"; then
|
||
return 0
|
||
fi
|
||
|
||
if qm set "$vmid" --delete "$slot" >/dev/null 2>&1; then
|
||
display_success "插槽已删除: $slot"
|
||
return 0
|
||
else
|
||
display_error "qm set --delete 执行失败" "请检查 VM 是否锁定,或查看 /var/log/pve-tools.log。"
|
||
return 1
|
||
fi
|
||
}
|
||
|
||
# ============ PCIe 控制器 / NVMe 直通 ============
|
||
|
||
# 检查 IOMMU 是否已开启(用于 PCIe 设备直通的前置条件)
|
||
iommu_is_enabled() {
|
||
if [[ -d /sys/kernel/iommu_groups ]]; then
|
||
local group_count
|
||
group_count="$(find /sys/kernel/iommu_groups -maxdepth 1 -type d 2>/dev/null | wc -l)"
|
||
if [[ "${group_count:-0}" -gt 1 ]]; then
|
||
return 0
|
||
fi
|
||
fi
|
||
|
||
if dmesg 2>/dev/null | grep -Eiq 'DMAR: IOMMU enabled|IOMMU enabled|AMD-Vi:.*enabled'; then
|
||
return 0
|
||
fi
|
||
|
||
return 1
|
||
}
|
||
|
||
# 从 udev 路径中解析 PCI BDF(格式:0000:00:00.0)
|
||
parse_pci_bdf_from_udev_path() {
|
||
local udev_path="$1"
|
||
if [[ "$udev_path" =~ ([0-9a-f]{4}:[0-9a-f]{2}:[0-9a-f]{2}\.[0-9a-f]) ]]; then
|
||
echo "${BASH_REMATCH[1]}"
|
||
return 0
|
||
fi
|
||
return 1
|
||
}
|
||
|
||
# 获取指定块设备所在的 PCI BDF(用于系统盘控制器保护、控制器磁盘映射)
|
||
get_blockdev_pci_bdf() {
|
||
local dev_path="$1"
|
||
if [[ -z "$dev_path" || ! -b "$dev_path" ]]; then
|
||
return 1
|
||
fi
|
||
|
||
local udev_path
|
||
udev_path="$(udevadm info --query=path --name="$dev_path" 2>/dev/null)"
|
||
if [[ -n "$udev_path" ]]; then
|
||
parse_pci_bdf_from_udev_path "$udev_path" && return 0
|
||
fi
|
||
|
||
return 1
|
||
}
|
||
|
||
# 获取 PVE 系统盘对应的“整盘设备名”列表(sda / nvme0n1 等)
|
||
get_system_whole_disks() {
|
||
local -A disks=()
|
||
local mount_src
|
||
|
||
for mp in / /boot /boot/efi; do
|
||
mount_src="$(findmnt -n -o SOURCE "$mp" 2>/dev/null || true)"
|
||
if [[ -z "$mount_src" ]]; then
|
||
continue
|
||
fi
|
||
|
||
if [[ "$mount_src" == /dev/mapper/* ]]; then
|
||
if command -v pvs >/dev/null 2>&1; then
|
||
while IFS= read -r pv; do
|
||
pv="$(echo "$pv" | awk '{$1=$1;print}')"
|
||
if [[ -n "$pv" && -b "$pv" ]]; then
|
||
local pk
|
||
pk="$(lsblk -dn -o PKNAME "$pv" 2>/dev/null | head -n 1)"
|
||
if [[ -n "$pk" ]]; then
|
||
disks["$pk"]=1
|
||
else
|
||
disks["$(basename "$pv")"]=1
|
||
fi
|
||
fi
|
||
done < <(pvs --noheadings -o pv_name 2>/dev/null)
|
||
fi
|
||
continue
|
||
fi
|
||
|
||
if [[ -b "$mount_src" ]]; then
|
||
local pk
|
||
pk="$(lsblk -dn -o PKNAME "$mount_src" 2>/dev/null | head -n 1)"
|
||
if [[ -n "$pk" ]]; then
|
||
disks["$pk"]=1
|
||
else
|
||
disks["$(basename "$mount_src")"]=1
|
||
fi
|
||
fi
|
||
done
|
||
|
||
for d in "${!disks[@]}"; do
|
||
echo "$d"
|
||
done | sort
|
||
}
|
||
|
||
# 获取“必须保护”的 PCI BDF(包含系统盘的控制器)
|
||
get_protected_pci_bdfs() {
|
||
local -A bdfs=()
|
||
local disk
|
||
while IFS= read -r disk; do
|
||
local bdf
|
||
bdf="$(get_blockdev_pci_bdf "/dev/$disk" 2>/dev/null || true)"
|
||
if [[ -n "$bdf" ]]; then
|
||
bdfs["$bdf"]=1
|
||
fi
|
||
done < <(get_system_whole_disks)
|
||
|
||
for b in "${!bdfs[@]}"; do
|
||
echo "$b"
|
||
done | sort
|
||
}
|
||
|
||
# 列出系统内的 SATA/SCSI/RAID 控制器(用于整控制器直通)
|
||
list_storage_controllers() {
|
||
lspci -Dnn 2>/dev/null | grep -Eiin 'SATA controller|RAID bus controller|SCSI storage controller|Serial Attached SCSI controller' | sed 's/^[0-9]\+://'
|
||
}
|
||
|
||
# 列出系统内的 NVMe 控制器(用于 NVMe 直通)
|
||
list_nvme_controllers() {
|
||
lspci -Dnn 2>/dev/null | grep -Eiin 'Non-Volatile memory controller' | sed 's/^[0-9]\+://'
|
||
}
|
||
|
||
# 展示指定 PCI BDF 下的所有“整盘”设备(用于磁盘映射展示与保护提示)
|
||
show_disks_under_pci_bdf() {
|
||
local bdf="$1"
|
||
if [[ -z "$bdf" ]]; then
|
||
return 1
|
||
fi
|
||
|
||
local found=0
|
||
while IFS= read -r name; do
|
||
local dev_bdf
|
||
dev_bdf="$(get_blockdev_pci_bdf "/dev/$name" 2>/dev/null || true)"
|
||
if [[ "$dev_bdf" == "$bdf" ]]; then
|
||
local size model
|
||
size="$(lsblk -dn -o SIZE "/dev/$name" 2>/dev/null | head -n 1)"
|
||
model="$(lsblk -dn -o MODEL "/dev/$name" 2>/dev/null | head -n 1)"
|
||
echo " /dev/$name ${size:-?} ${model:-?}"
|
||
found=1
|
||
fi
|
||
done < <(lsblk -dn -o NAME,TYPE 2>/dev/null | awk '$2=="disk"{print $1}')
|
||
|
||
if [[ "$found" -eq 0 ]]; then
|
||
echo " (未能识别到该控制器下的磁盘,可能是映射方式不同或权限受限)"
|
||
fi
|
||
return 0
|
||
}
|
||
|
||
# 获取 VM 是否为 q35(决定 hostpci 是否添加 pcie=1)
|
||
qm_is_q35_machine() {
|
||
local vmid="$1"
|
||
local machine
|
||
machine="$(qm config "$vmid" 2>/dev/null | awk -F': ' '/^machine:/{print $2}' | head -n 1)"
|
||
if echo "$machine" | grep -q 'q35'; then
|
||
return 0
|
||
fi
|
||
return 1
|
||
}
|
||
|
||
# 获取可用的 hostpci 插槽号(0-15)
|
||
qm_find_free_hostpci_index() {
|
||
local vmid="$1"
|
||
local cfg used
|
||
cfg="$(qm config "$vmid" 2>/dev/null)"
|
||
used="$(echo "$cfg" | awk -F'[: ]' '/^hostpci[0-9]+:/{gsub("hostpci","",$1); print $1}' | sort -n | uniq)"
|
||
|
||
local i
|
||
for ((i=0; i<=15; i++)); do
|
||
if ! echo "$used" | grep -qx "$i"; then
|
||
echo "$i"
|
||
return 0
|
||
fi
|
||
done
|
||
return 1
|
||
}
|
||
|
||
# 从 VM 配置中查找某个 BDF 是否已被直通
|
||
qm_has_hostpci_bdf() {
|
||
local vmid="$1"
|
||
local bdf="$2"
|
||
qm config "$vmid" 2>/dev/null | grep -qE "^hostpci[0-9]+:.*\\b${bdf}\\b"
|
||
}
|
||
|
||
# 直通整个 SATA/SCSI/RAID 控制器到 VM(含系统盘控制器保护)
|
||
storage_controller_passthrough() {
|
||
log_step "磁盘控制器直通 - 扫描控制器"
|
||
|
||
if ! iommu_is_enabled; then
|
||
display_error "未检测到 IOMMU 已开启" "请先在 BIOS 开启 VT-d/AMD-Vi,并在 PVE 中启用 IOMMU(可在“硬件直通一键配置(IOMMU)”里开启)。"
|
||
return 1
|
||
fi
|
||
|
||
local controllers
|
||
controllers="$(list_storage_controllers)"
|
||
if [[ -z "$controllers" ]]; then
|
||
display_error "未发现 SATA/SCSI/RAID 控制器" "可尝试手工执行 lspci -Dnn 确认控制器是否存在。"
|
||
return 1
|
||
fi
|
||
|
||
echo -e "${CYAN}可用控制器列表:${NC}"
|
||
echo "$controllers" | awk '{printf " [%d] %s\n", NR, $0}'
|
||
echo -e "${UI_DIVIDER}"
|
||
|
||
local pick
|
||
read -p "请选择控制器序号 (返回请输入 0): " pick
|
||
pick="${pick:-0}"
|
||
if [[ "$pick" == "0" ]]; then
|
||
return 0
|
||
fi
|
||
if [[ ! "$pick" =~ ^[0-9]+$ ]]; then
|
||
display_error "序号必须是数字"
|
||
return 1
|
||
fi
|
||
|
||
local line bdf
|
||
line="$(echo "$controllers" | awk -v n="$pick" 'NR==n{print $0}')"
|
||
if [[ -z "$line" ]]; then
|
||
display_error "无效的序号: $pick"
|
||
return 1
|
||
fi
|
||
bdf="$(echo "$line" | awk '{print $1}')"
|
||
|
||
echo -e "${CYAN}该控制器下识别到的整盘设备:${NC}"
|
||
show_disks_under_pci_bdf "$bdf"
|
||
echo -e "${UI_DIVIDER}"
|
||
|
||
local protected
|
||
protected="$(get_protected_pci_bdfs)"
|
||
if echo "$protected" | grep -qx "$bdf"; then
|
||
display_error "安全拦截:禁止直通系统盘所在控制器 $bdf" "请勿直通包含 PVE 系统盘的控制器,否则会导致宿主机不可用。"
|
||
return 1
|
||
fi
|
||
|
||
local vmid
|
||
read -p "请输入目标 VMID: " vmid
|
||
if ! validate_qm_vmid "$vmid"; then
|
||
return 1
|
||
fi
|
||
|
||
if qm_has_hostpci_bdf "$vmid" "$bdf"; then
|
||
display_error "该控制器已在 VM 配置中存在直通记录" "无需重复直通。"
|
||
return 1
|
||
fi
|
||
|
||
local idx
|
||
idx="$(qm_find_free_hostpci_index "$vmid" 2>/dev/null)" || {
|
||
display_error "未找到可用 hostpci 插槽" "请先释放 VM 的 hostpci0-hostpci15 后再试。"
|
||
return 1
|
||
}
|
||
|
||
local hostpci_value="$bdf"
|
||
if qm_is_q35_machine "$vmid"; then
|
||
hostpci_value="${hostpci_value},pcie=1"
|
||
fi
|
||
|
||
local conf_path
|
||
conf_path="$(get_qm_conf_path "$vmid")"
|
||
if [[ -f "$conf_path" ]]; then
|
||
log_tips "修改 VM 配置前建议备份原配置"
|
||
backup_file "$conf_path" >/dev/null 2>&1 || true
|
||
fi
|
||
|
||
if ! confirm_action "为 VM $vmid 直通控制器(hostpci$idx = $hostpci_value)"; then
|
||
return 0
|
||
fi
|
||
|
||
if qm set "$vmid" "-hostpci${idx}" "$hostpci_value" >/dev/null 2>&1; then
|
||
local status
|
||
status="$(qm status "$vmid" 2>/dev/null | awk '{print $2}' | head -n 1)"
|
||
display_success "控制器直通已写入 VM 配置" "当前 VM 状态: ${status:-unknown}(如在运行中,需重启 VM 后生效)"
|
||
return 0
|
||
else
|
||
display_error "qm set 执行失败" "请检查 IOMMU/IOMMU group、VM 是否锁定,或查看 /var/log/pve-tools.log。"
|
||
return 1
|
||
fi
|
||
}
|
||
|
||
# 判断 NVMe 设备是否建议启用 MSI-X 重定位(启发式:存在 MSI-X 且存在 BAR2/Region 2)
|
||
nvme_should_enable_msix_relocation() {
|
||
local bdf="$1"
|
||
local vv
|
||
vv="$(lspci -vv -s "$bdf" 2>/dev/null || true)"
|
||
if echo "$vv" | grep -q 'MSI-X:' && echo "$vv" | grep -qE 'Region 2: Memory|Region 2:.*Memory'; then
|
||
return 0
|
||
fi
|
||
return 1
|
||
}
|
||
|
||
# 获取当前 VM args(不存在则返回空)
|
||
qm_get_args() {
|
||
local vmid="$1"
|
||
qm config "$vmid" 2>/dev/null | awk -F': ' '/^args:/{sub(/^args: /,""); print $0; exit}'
|
||
}
|
||
|
||
# 幂等追加 VM args 片段(通过 qm set -args 覆盖式写入,但内容基于现有 args 合并)
|
||
qm_append_args() {
|
||
local vmid="$1"
|
||
local token="$2"
|
||
|
||
if [[ -z "$token" ]]; then
|
||
return 1
|
||
fi
|
||
|
||
local current
|
||
current="$(qm_get_args "$vmid")"
|
||
if echo "$current" | grep -Fq "$token"; then
|
||
return 0
|
||
fi
|
||
|
||
local new_args
|
||
if [[ -z "$current" ]]; then
|
||
new_args="$token"
|
||
else
|
||
new_args="${current} ${token}"
|
||
fi
|
||
|
||
qm set "$vmid" -args "$new_args" >/dev/null 2>&1
|
||
}
|
||
|
||
# NVMe 控制器直通到 VM(含系统盘控制器保护与 MSI-X 重定位 args)
|
||
nvme_passthrough() {
|
||
log_step "NVMe 直通 - 扫描 NVMe 控制器"
|
||
|
||
if ! iommu_is_enabled; then
|
||
display_error "未检测到 IOMMU 已开启" "请先在 BIOS 开启 VT-d/AMD-Vi,并在 PVE 中启用 IOMMU(可在“硬件直通一键配置(IOMMU)”里开启)。"
|
||
return 1
|
||
fi
|
||
|
||
local controllers
|
||
controllers="$(list_nvme_controllers)"
|
||
if [[ -z "$controllers" ]]; then
|
||
display_error "未发现 NVMe 控制器" "可尝试手工执行 lspci -Dnn | grep -i NVMe 确认设备是否存在。"
|
||
return 1
|
||
fi
|
||
|
||
echo -e "${CYAN}可用 NVMe 控制器列表:${NC}"
|
||
echo "$controllers" | awk '{printf " [%d] %s\n", NR, $0}'
|
||
echo -e "${UI_DIVIDER}"
|
||
|
||
local pick
|
||
read -p "请选择 NVMe 控制器序号 (返回请输入 0): " pick
|
||
pick="${pick:-0}"
|
||
if [[ "$pick" == "0" ]]; then
|
||
return 0
|
||
fi
|
||
if [[ ! "$pick" =~ ^[0-9]+$ ]]; then
|
||
display_error "序号必须是数字"
|
||
return 1
|
||
fi
|
||
|
||
local line bdf
|
||
line="$(echo "$controllers" | awk -v n="$pick" 'NR==n{print $0}')"
|
||
if [[ -z "$line" ]]; then
|
||
display_error "无效的序号: $pick"
|
||
return 1
|
||
fi
|
||
bdf="$(echo "$line" | awk '{print $1}')"
|
||
|
||
echo -e "${CYAN}该 NVMe 控制器下识别到的整盘设备:${NC}"
|
||
show_disks_under_pci_bdf "$bdf"
|
||
echo -e "${UI_DIVIDER}"
|
||
|
||
local protected
|
||
protected="$(get_protected_pci_bdfs)"
|
||
if echo "$protected" | grep -qx "$bdf"; then
|
||
display_error "安全拦截:禁止直通系统盘所在 NVMe 控制器 $bdf" "请勿直通包含 PVE 系统盘的 NVMe 控制器,否则会导致宿主机不可用。"
|
||
return 1
|
||
fi
|
||
|
||
local vmid
|
||
read -p "请输入目标 VMID: " vmid
|
||
if ! validate_qm_vmid "$vmid"; then
|
||
return 1
|
||
fi
|
||
|
||
if qm_has_hostpci_bdf "$vmid" "$bdf"; then
|
||
display_error "该 NVMe 已在 VM 配置中存在直通记录" "无需重复直通。"
|
||
return 1
|
||
fi
|
||
|
||
local idx
|
||
idx="$(qm_find_free_hostpci_index "$vmid" 2>/dev/null)" || {
|
||
display_error "未找到可用 hostpci 插槽" "请先释放 VM 的 hostpci0-hostpci15 后再试。"
|
||
return 1
|
||
}
|
||
|
||
local hostpci_value="$bdf"
|
||
if qm_is_q35_machine "$vmid"; then
|
||
hostpci_value="${hostpci_value},pcie=1"
|
||
fi
|
||
|
||
local enable_msix="no"
|
||
if nvme_should_enable_msix_relocation "$bdf"; then
|
||
echo -e "${YELLOW}检测到该 NVMe 可能需要 MSI-X 重定位(bar2)以提高兼容性。${NC}"
|
||
local ans
|
||
read -p "是否写入 MSI-X 重定位 args?(yes/no) [yes]: " ans
|
||
ans="${ans:-yes}"
|
||
if [[ "$ans" == "yes" || "$ans" == "YES" ]]; then
|
||
enable_msix="yes"
|
||
fi
|
||
fi
|
||
|
||
local conf_path
|
||
conf_path="$(get_qm_conf_path "$vmid")"
|
||
if [[ -f "$conf_path" ]]; then
|
||
log_tips "修改 VM 配置前建议备份原配置"
|
||
backup_file "$conf_path" >/dev/null 2>&1 || true
|
||
fi
|
||
|
||
if ! confirm_action "为 VM $vmid 直通 NVMe(hostpci$idx = $hostpci_value),并写入 MSI-X 重定位参数(${enable_msix})"; then
|
||
return 0
|
||
fi
|
||
|
||
if ! qm set "$vmid" "-hostpci${idx}" "$hostpci_value" >/dev/null 2>&1; then
|
||
display_error "qm set 执行失败" "请检查 IOMMU/IOMMU group、VM 是否锁定,或查看 /var/log/pve-tools.log。"
|
||
return 1
|
||
fi
|
||
|
||
if [[ "$enable_msix" == "yes" ]]; then
|
||
local token
|
||
token="-set device.hostpci${idx}.x-msix-relocation=bar2"
|
||
if qm_append_args "$vmid" "$token"; then
|
||
log_success "已写入 args: $token"
|
||
else
|
||
log_warn "args 写入失败(已完成 hostpci 直通)"
|
||
fi
|
||
fi
|
||
|
||
local status
|
||
status="$(qm status "$vmid" 2>/dev/null | awk '{print $2}' | head -n 1)"
|
||
display_success "NVMe 直通已写入 VM 配置" "当前 VM 状态: ${status:-unknown}(如在运行中,需重启 VM 后生效)"
|
||
return 0
|
||
}
|
||
|
||
# ============ 引导配置辅助 ============
|
||
|
||
# 解析用户输入的磁盘路径为真实整盘设备(返回 /dev/sdX 或 /dev/nvme0n1)
|
||
resolve_whole_disk() {
|
||
local input="$1"
|
||
if [[ -z "$input" ]]; then
|
||
return 1
|
||
fi
|
||
|
||
local real
|
||
if [[ "$input" == /dev/disk/by-id/* ]]; then
|
||
real="$(readlink -f "$input" 2>/dev/null || true)"
|
||
else
|
||
real="$input"
|
||
fi
|
||
|
||
if [[ ! -b "$real" ]]; then
|
||
return 1
|
||
fi
|
||
|
||
local t
|
||
t="$(lsblk -dn -o TYPE "$real" 2>/dev/null | head -n 1)"
|
||
if [[ "$t" == "disk" ]]; then
|
||
echo "$real"
|
||
return 0
|
||
fi
|
||
|
||
local pk
|
||
pk="$(lsblk -dn -o PKNAME "$real" 2>/dev/null | head -n 1)"
|
||
if [[ -n "$pk" && -b "/dev/$pk" ]]; then
|
||
echo "/dev/$pk"
|
||
return 0
|
||
fi
|
||
|
||
return 1
|
||
}
|
||
|
||
# 识别直通磁盘上的引导类型(UEFI / Legacy / Unknown)
|
||
detect_disk_boot_mode() {
|
||
local disk="$1"
|
||
if [[ -z "$disk" || ! -b "$disk" ]]; then
|
||
echo "Unknown"
|
||
return 1
|
||
fi
|
||
|
||
if command -v lsblk >/dev/null 2>&1; then
|
||
local esp_guid="c12a7328-f81f-11d2-ba4b-00a0c93ec93b"
|
||
local parts
|
||
parts="$(lsblk -rno NAME,PARTTYPE,FSTYPE "$disk" 2>/dev/null | awk 'NF>=2{print}')"
|
||
if echo "$parts" | grep -qi "$esp_guid"; then
|
||
echo "UEFI"
|
||
return 0
|
||
fi
|
||
if echo "$parts" | awk '{print $3}' | grep -qi '^vfat$'; then
|
||
if echo "$parts" | grep -Eqi 'EFI|esp'; then
|
||
echo "UEFI"
|
||
return 0
|
||
fi
|
||
fi
|
||
fi
|
||
|
||
if command -v parted >/dev/null 2>&1; then
|
||
local out
|
||
out="$(parted -s "$disk" print 2>/dev/null || true)"
|
||
if echo "$out" | grep -Eqi 'Partition Table:\s*gpt'; then
|
||
if echo "$out" | grep -Eqi '\besp\b|EFI System|boot, esp'; then
|
||
echo "UEFI"
|
||
return 0
|
||
fi
|
||
echo "Unknown"
|
||
return 0
|
||
fi
|
||
if echo "$out" | grep -Eqi 'Partition Table:\s*msdos'; then
|
||
echo "Legacy"
|
||
return 0
|
||
fi
|
||
fi
|
||
|
||
echo "Unknown"
|
||
return 0
|
||
}
|
||
|
||
# 根据磁盘引导类型与直通方式给出 VM 配置建议(仅提示,不修改配置)
|
||
boot_config_assistant() {
|
||
log_step "引导配置辅助"
|
||
|
||
local disk_input
|
||
read -p "请输入直通磁盘路径(/dev/disk/by-id/... 或 /dev/sdX /dev/nvme0n1)(返回请输入 0): " disk_input
|
||
disk_input="${disk_input:-0}"
|
||
if [[ "$disk_input" == "0" ]]; then
|
||
return 0
|
||
fi
|
||
|
||
local disk
|
||
disk="$(resolve_whole_disk "$disk_input" 2>/dev/null || true)"
|
||
if [[ -z "$disk" ]]; then
|
||
display_error "磁盘路径无效或不可访问: $disk_input" "请确认输入为块设备或 by-id 路径,并在宿主机上存在。"
|
||
return 1
|
||
fi
|
||
|
||
local boot_mode
|
||
boot_mode="$(detect_disk_boot_mode "$disk")"
|
||
|
||
echo -e "${CYAN}检测结果:${NC}"
|
||
echo " 磁盘: $disk"
|
||
echo " 引导类型: $boot_mode"
|
||
echo -e "${UI_DIVIDER}"
|
||
|
||
echo -e "${CYAN}直通方式选择(用于生成更贴近场景的建议):${NC}"
|
||
echo " 1) 单个磁盘直通(RDM)"
|
||
echo " 2) 整控制器直通(SATA/SCSI/RAID)"
|
||
echo " 3) NVMe 控制器直通"
|
||
local mode
|
||
read -p "请选择直通方式 [1-3] [1]: " mode
|
||
mode="${mode:-1}"
|
||
if [[ "$mode" != "1" && "$mode" != "2" && "$mode" != "3" ]]; then
|
||
display_error "无效选择: $mode" "请输入 1/2/3"
|
||
return 1
|
||
fi
|
||
|
||
local slot=""
|
||
if [[ "$mode" == "1" ]]; then
|
||
read -p "如果已知 VM 插槽(如 scsi0/sata1/ide0)可输入用于 boot order(回车跳过): " slot
|
||
if [[ -n "$slot" && ! "$slot" =~ ^(scsi|sata|ide)[0-9]+$ ]]; then
|
||
display_error "插槽格式不合法: $slot" "示例:scsi0 / sata0 / ide0"
|
||
return 1
|
||
fi
|
||
fi
|
||
|
||
echo -e "${UI_DIVIDER}"
|
||
echo -e "${CYAN}配置建议(不自动修改):${NC}"
|
||
|
||
if [[ "$boot_mode" == "UEFI" ]]; then
|
||
echo " 1) 固件建议:OVMF(UEFI)"
|
||
echo " 2) 额外建议:添加 efidisk0 用于 NVRAM(PVE 界面可创建)"
|
||
if [[ "$mode" != "1" ]]; then
|
||
echo " 3) 机器类型建议:q35(PCIe 设备直通更友好)"
|
||
fi
|
||
elif [[ "$boot_mode" == "Legacy" ]]; then
|
||
echo " 1) 固件建议:SeaBIOS(Legacy)"
|
||
else
|
||
echo " 1) 未能可靠判断 UEFI/Legacy:建议检查磁盘分区表与是否存在 ESP"
|
||
echo " 2) 如果是 UEFI 系统:优先使用 OVMF + q35"
|
||
fi
|
||
|
||
if [[ "$mode" == "1" ]]; then
|
||
echo " 4) 总线类型建议:优先 scsi;总线受限时使用 sata/ide"
|
||
if [[ -n "$slot" ]]; then
|
||
echo " 5) 启动顺序建议:boot: order=${slot};ide2;net0(按实际设备调整)"
|
||
else
|
||
echo " 5) 启动顺序建议:确保直通磁盘所在插槽在 boot order 中靠前"
|
||
fi
|
||
else
|
||
echo " 4) 启动建议:控制器/NVMe 直通后,来宾系统会直接看到物理设备;建议使用 UEFI 启动管理器选择启动项"
|
||
fi
|
||
return 0
|
||
}
|
||
|
||
#--------------开启硬件直通----------------
|
||
|
||
#--------------设置CPU电源模式----------------
|
||
# 设置CPU电源模式
|
||
cpupower() {
|
||
governors=`cat /sys/devices/system/cpu/cpu0/cpufreq/scaling_available_governors`
|
||
while :; do
|
||
clear
|
||
show_menu_header "设置CPU电源模式"
|
||
echo " 1. 设置CPU模式 conservative 保守模式 [变身老年机]"
|
||
echo " 2. 设置CPU模式 ondemand 按需模式 [默认]"
|
||
echo " 3. 设置CPU模式 powersave 节能模式 [省电小能手]"
|
||
echo " 4. 设置CPU模式 performance 性能模式 [性能释放]"
|
||
echo " 5. 设置CPU模式 schedutil 负载模式 [交给负载自动配置]"
|
||
echo
|
||
echo " 6. 恢复系统默认电源设置"
|
||
echo "${UI_DIVIDER}"
|
||
show_menu_option "0" "返回"
|
||
show_menu_footer
|
||
echo
|
||
echo "部分CPU仅支持 performance 和 powersave 模式,只能选择这两项,其他模式无效不要选!"
|
||
echo
|
||
echo "你的CPU支持 ${governors} 模式"
|
||
echo
|
||
read -p "请选择: [ ]" -n 1 cpupowerid
|
||
echo # New line after input
|
||
cpupowerid=${cpupowerid:-2}
|
||
case "${cpupowerid}" in
|
||
1)
|
||
GOVERNOR="conservative"
|
||
;;
|
||
2)
|
||
GOVERNOR="ondemand"
|
||
;;
|
||
3)
|
||
GOVERNOR="powersave"
|
||
;;
|
||
4)
|
||
GOVERNOR="performance"
|
||
;;
|
||
5)
|
||
GOVERNOR="schedutil"
|
||
;;
|
||
6)
|
||
cpupower_del
|
||
pause_function
|
||
break
|
||
;;
|
||
0)
|
||
break
|
||
;;
|
||
*)
|
||
log_error "你的输入无效,请重新输入!"
|
||
pause_function
|
||
;;
|
||
esac
|
||
if [[ ${GOVERNOR} != "" ]]; then
|
||
if [[ -n `echo "${governors}" | grep -o "${GOVERNOR}"` ]]; then
|
||
echo "您选择的CPU模式:${GOVERNOR}"
|
||
echo
|
||
cpupower_add
|
||
pause_function
|
||
else
|
||
log_error "您的CPU不支持该模式!"
|
||
log_tips "现在暂时不会对你的系统造成影响,但是下次开机时,CPU模式会恢复为默认模式。"
|
||
pause_function
|
||
fi
|
||
fi
|
||
done
|
||
}
|
||
|
||
# 修改CPU模式
|
||
cpupower_add() {
|
||
echo "${GOVERNOR}" | tee /sys/devices/system/cpu/cpu*/cpufreq/scaling_governor >/dev/null
|
||
echo "查看当前CPU模式"
|
||
cat /sys/devices/system/cpu/cpu0/cpufreq/scaling_governor
|
||
|
||
echo "正在添加开机任务"
|
||
NEW_CRONTAB_COMMAND="sleep 10 && echo "${GOVERNOR}" | tee /sys/devices/system/cpu/cpu*/cpufreq/scaling_governor >/dev/null #CPU Power Mode"
|
||
EXISTING_CRONTAB=$(crontab -l 2>/dev/null)
|
||
if [[ -n "$EXISTING_CRONTAB" ]]; then
|
||
TEMP_CRONTAB_FILE=$(mktemp)
|
||
# 使用 -F 精确匹配标记,避免误删用户的其他任务
|
||
echo "$EXISTING_CRONTAB" | grep -vF "#CPU Power Mode" > "$TEMP_CRONTAB_FILE"
|
||
crontab "$TEMP_CRONTAB_FILE"
|
||
rm "$TEMP_CRONTAB_FILE"
|
||
fi
|
||
log_success "CPU模式已修改完成"
|
||
# 修改完成
|
||
(crontab -l 2>/dev/null; echo "@reboot $NEW_CRONTAB_COMMAND") | crontab -
|
||
echo -e "
|
||
检查计划任务设置 (使用 'crontab -l' 命令来检查)"
|
||
}
|
||
|
||
# 恢复系统默认电源设置
|
||
cpupower_del() {
|
||
# 恢复性模式
|
||
echo "performance" | tee /sys/devices/system/cpu/cpu*/cpufreq/scaling_governor >/dev/null
|
||
# 删除计划任务
|
||
EXISTING_CRONTAB=$(crontab -l 2>/dev/null)
|
||
if [[ -n "$EXISTING_CRONTAB" ]]; then
|
||
TEMP_CRONTAB_FILE=$(mktemp)
|
||
# 使用 -F 精确匹配标记,避免误删用户的其他任务
|
||
echo "$EXISTING_CRONTAB" | grep -vF "#CPU Power Mode" > "$TEMP_CRONTAB_FILE"
|
||
crontab "$TEMP_CRONTAB_FILE"
|
||
rm "$TEMP_CRONTAB_FILE"
|
||
fi
|
||
|
||
log_success "已恢复系统默认电源设置!还是默认的好用吧"
|
||
}
|
||
#--------------设置CPU电源模式----------------
|
||
|
||
#--------------CPU、主板、硬盘温度显示----------------
|
||
# 安装工具
|
||
cpu_add() {
|
||
nodes="/usr/share/perl5/PVE/API2/Nodes.pm"
|
||
pvemanagerlib="/usr/share/pve-manager/js/pvemanagerlib.js"
|
||
proxmoxlib="/usr/share/javascript/proxmox-widget-toolkit/proxmoxlib.js"
|
||
|
||
pvever=$(pveversion | awk -F"/" '{print $2}')
|
||
echo pve版本$pvever
|
||
|
||
# 判断是否已经执行过修改 (使用 modbyshowtempfreq 标记检测)
|
||
if [ $(grep 'modbyshowtempfreq' $nodes $pvemanagerlib $proxmoxlib 2>/dev/null | wc -l) -eq 3 ]; then
|
||
log_warn "已经修改过,请勿重复修改"
|
||
log_tips "如果没有生效,请使用 Shift+F5 刷新浏览器缓存"
|
||
log_tips "如果需要强制重新修改,请先执行还原操作"
|
||
pause_function
|
||
return
|
||
fi
|
||
|
||
# 先刷新下源
|
||
log_step "更新软件包列表..."
|
||
apt-get update
|
||
|
||
log_step "开始安装所需工具..."
|
||
# 输入需要安装的软件包 (添加 hdparm 用于 SATA 硬盘休眠检测, apcupsd for UPS support)
|
||
packages=(lm-sensors nvme-cli sysstat linux-cpupower hdparm smartmontools apcupsd)
|
||
|
||
# 查询软件包,判断是否安装
|
||
for package in "${packages[@]}"; do
|
||
if ! dpkg -s "$package" &> /dev/null; then
|
||
log_info "$package 未安装,开始安装软件包"
|
||
apt-get install "${packages[@]}" -y
|
||
modprobe msr
|
||
install=ok
|
||
break
|
||
fi
|
||
done
|
||
|
||
# 设置执行权限 (修正路径)
|
||
[[ -e /usr/sbin/linux-cpupower ]] && chmod +s /usr/sbin/linux-cpupower
|
||
chmod +s /usr/sbin/nvme
|
||
chmod +s /usr/sbin/smartctl
|
||
chmod +s /usr/sbin/turbostat || log_warn "无法设置 turbostat 权限"
|
||
|
||
# 启用 MSR 模块
|
||
modprobe msr && echo msr > /etc/modules-load.d/turbostat-msr.conf
|
||
|
||
# 软件包安装完成
|
||
if [ "$install" == "ok" ]; then
|
||
log_success "软件包安装完成,检测硬件信息"
|
||
sensors-detect --auto > /tmp/sensors
|
||
drivers=$(sed -n '/Chip drivers/,/\#----cut here/p' /tmp/sensors | sed '/Chip /d' | sed '/cut/d')
|
||
|
||
if [ $(echo $drivers | wc -w) = 0 ]; then
|
||
log_warn "没有找到任何驱动,似乎你的系统不支持或驱动安装失败。"
|
||
pause_function
|
||
else
|
||
for i in $drivers; do
|
||
modprobe $i
|
||
if [ $(grep $i /etc/modules | wc -l) = 0 ]; then
|
||
echo $i >> /etc/modules
|
||
fi
|
||
done
|
||
sensors
|
||
sleep 3
|
||
log_success "驱动信息配置成功。"
|
||
fi
|
||
[[ -e /etc/init.d/kmod ]] && /etc/init.d/kmod start
|
||
rm /tmp/sensors
|
||
fi
|
||
|
||
log_step "备份源文件"
|
||
# 备份当前版本文件
|
||
backup_file "$nodes"
|
||
backup_file "$pvemanagerlib"
|
||
backup_file "$proxmoxlib"
|
||
|
||
log_info "是否启用 UPS 监控?"
|
||
echo -n "(如果没有 UPS 设备或不想显示,请选择 N,默认Y)(y/N): "
|
||
read -n 1 -r
|
||
echo
|
||
if [[ $REPLY =~ ^[Yy]$ ]]; then
|
||
enable_ups=true
|
||
log_success "已选择启用UPS监控"
|
||
else
|
||
enable_ups=false
|
||
log_info "已选择跳过UPS监控"
|
||
fi
|
||
|
||
# 生成系统变量 (参考 PVE 8 脚本的改进实现)
|
||
tmpf=tmpfile.temp
|
||
touch $tmpf
|
||
cat > $tmpf << 'EOF'
|
||
|
||
#modbyshowtempfreq
|
||
|
||
$res->{thermalstate} = `sensors -A`;
|
||
$res->{cpuFreq} = `
|
||
goverf=/sys/devices/system/cpu/cpufreq/policy0/scaling_governor
|
||
maxf=/sys/devices/system/cpu/cpufreq/policy0/cpuinfo_max_freq
|
||
minf=/sys/devices/system/cpu/cpufreq/policy0/cpuinfo_min_freq
|
||
|
||
cat /proc/cpuinfo | grep -i "cpu mhz"
|
||
echo -n 'gov:'
|
||
[ -f \$goverf ] && cat \$goverf || echo none
|
||
echo -n 'min:'
|
||
[ -f \$minf ] && cat \$minf || echo none
|
||
echo -n 'max:'
|
||
[ -f \$maxf ] && cat \$maxf || echo none
|
||
echo -n 'pkgwatt:'
|
||
[ -e /usr/sbin/turbostat ] && turbostat --quiet --cpu package --show "PkgWatt" -S sleep 0.25 2>&1 | tail -n1
|
||
`;
|
||
EOF
|
||
|
||
if [ "$enable_ups" = true ]; then
|
||
cat >> $tmpf << 'EOF'
|
||
$res->{ups_status} = `apcaccess status`;
|
||
EOF
|
||
fi
|
||
|
||
|
||
echo >> $tmpf
|
||
|
||
# NVME 硬盘变量 (动态检测,参考 PVE 8 实现)
|
||
log_info "检测系统中的 NVME 硬盘"
|
||
nvi=0
|
||
for nvme in $(ls /dev/nvme[0-9] 2> /dev/null); do
|
||
chmod +s /usr/sbin/smartctl 2>/dev/null
|
||
|
||
cat >> $tmpf << EOF
|
||
|
||
\$res->{nvme$nvi} = \`smartctl $nvme -a -j\`;
|
||
EOF
|
||
echo "检测到 NVME 硬盘: $nvme (nvme$nvi)"
|
||
let nvi++
|
||
done
|
||
echo "已添加 $nvi 块 NVME 硬盘"
|
||
|
||
# SATA 硬盘变量 (动态检测,参考 PVE 8 实现)
|
||
log_info "检测系统中的 SATA 固态和机械硬盘"
|
||
sdi=0
|
||
for sd in $(ls /dev/sd[a-z] 2> /dev/null); do
|
||
chmod +s /usr/sbin/smartctl 2>/dev/null
|
||
chmod +s /usr/sbin/hdparm 2>/dev/null
|
||
|
||
# 检测是否是真的硬盘
|
||
sdsn=$(awk -F '/' '{print $NF}' <<< $sd)
|
||
sdcr=/sys/block/$sdsn/queue/rotational
|
||
[ -f $sdcr ] || continue
|
||
|
||
if [ "$(cat $sdcr)" = "0" ]; then
|
||
hddisk=false
|
||
sdtype="固态硬盘"
|
||
else
|
||
hddisk=true
|
||
sdtype="机械硬盘"
|
||
fi
|
||
|
||
# 硬盘输出信息逻辑,如果硬盘不存在就输出空 JSON
|
||
cat >> $tmpf << EOF
|
||
|
||
\$res->{sd$sdi} = \`
|
||
if [ -b $sd ]; then
|
||
# 增加 SAS 盘检测,SAS 盘不使用 hdparm 检测休眠,防止误报
|
||
if $hddisk && ! smartctl -i $sd | grep -q "Transport protocol:.*SAS" && hdparm -C $sd 2>/dev/null | grep -iq 'standby'; then
|
||
echo '{"standy": true}'
|
||
else
|
||
smartctl $sd -a -j
|
||
fi
|
||
else
|
||
echo '{}'
|
||
fi
|
||
\`;
|
||
EOF
|
||
echo "检测到 $sdtype: $sd (sd$sdi)"
|
||
let sdi++
|
||
done
|
||
echo "已添加 $sdi 块 SATA 固态和机械硬盘"
|
||
|
||
|
||
################### 修改node.pm ##########################
|
||
log_info "修改node.pm:"
|
||
log_info "找到关键字 PVE::pvecfg::version_text 的行号并跳到下一行"
|
||
|
||
# 显示匹配的行
|
||
ln=$(expr $(sed -n -e '/PVE::pvecfg::version_text/=' $nodes) + 1)
|
||
echo "匹配的行号:" $ln
|
||
|
||
log_info "修改结果:"
|
||
sed -i "${ln}r $tmpf" $nodes
|
||
# 显示修改结果
|
||
sed -n '/PVE::pvecfg::version_text/,+18p' $nodes
|
||
rm $tmpf
|
||
|
||
################### 修改pvemanagerlib.js ##########################
|
||
tmpf=tmpfile.temp
|
||
touch $tmpf
|
||
cat > $tmpf << 'EOF'
|
||
|
||
//modbyshowtempfreq
|
||
{
|
||
itemId: 'cpumhz',
|
||
colspan: 2,
|
||
printBar: false,
|
||
title: gettext('CPU频率(GHz)'),
|
||
textField: 'cpuFreq',
|
||
renderer:function(v){
|
||
console.log(v);
|
||
|
||
// 解析所有核心频率
|
||
let m = v.match(/(?<=^cpu[^\d]+)\d+/img);
|
||
if (!m || m.length === 0) {
|
||
return '无法获取CPU频率信息';
|
||
}
|
||
|
||
let freqs = m.map(e => parseFloat((e / 1000).toFixed(1)));
|
||
|
||
// 计算统计信息
|
||
let avgFreq = (freqs.reduce((a, b) => a + b, 0) / freqs.length).toFixed(1);
|
||
let minFreq = Math.min(...freqs).toFixed(1);
|
||
let maxFreq = Math.max(...freqs).toFixed(1);
|
||
let coreCount = freqs.length;
|
||
|
||
// 获取系统配置的频率范围
|
||
let sysMin = (v.match(/(?<=^min:).+/im)[0]);
|
||
if (sysMin !== 'none') {
|
||
sysMin = (sysMin / 1000000).toFixed(1);
|
||
}
|
||
|
||
let sysMax = (v.match(/(?<=^max:).+/im)[0]);
|
||
if (sysMax !== 'none') {
|
||
sysMax = (sysMax / 1000000).toFixed(1);
|
||
}
|
||
|
||
let gov = v.match(/(?<=^gov:).+/im)[0].toUpperCase();
|
||
|
||
let watt = v.match(/(?<=^pkgwatt:)[\d.]+$/im);
|
||
watt = watt ? " | 功耗: " + (watt[0]/1).toFixed(1) + 'W' : '';
|
||
|
||
// 简洁显示:平均值 + 当前范围 + 系统范围 + 功耗 + 调速器
|
||
return `${coreCount}核心 平均: ${avgFreq} GHz (当前: ${minFreq}~${maxFreq}) | 范围: ${sysMin}~${sysMax} GHz${watt} | 调速器: ${gov}`;
|
||
}
|
||
},
|
||
|
||
{
|
||
itemId: 'thermal',
|
||
colspan: 2,
|
||
printBar: false,
|
||
title: gettext('CPU温度'),
|
||
textField: 'thermalstate',
|
||
renderer:function(value){
|
||
function colorizeTemp(temp) {
|
||
let tempNum = Number(temp);
|
||
if (Number.isNaN(tempNum)) {
|
||
return temp + '°C';
|
||
}
|
||
if (tempNum < 60) {
|
||
return '<span style="color: #27ae60; font-weight: 600;">' + tempNum.toFixed(0) + '°C</span>';
|
||
}
|
||
if (tempNum < 80) {
|
||
return '<span style="color: #f39c12; font-weight: 600;">' + tempNum.toFixed(0) + '°C</span>';
|
||
}
|
||
return '<span style="color: #e74c3c; font-weight: 600;">' + tempNum.toFixed(0) + '°C</span>';
|
||
}
|
||
|
||
console.log(value);
|
||
let b = value.trim().split(/\s+(?=^\w+-)/m).sort();
|
||
let cpuResults = [];
|
||
let otherResults = [];
|
||
|
||
const cpuSensorRegex = /(CORETEMP|K10TEMP|ZENPOWER|ZENPOWER3|K8TEMP|FAM15H|ZENPROBE)/i;
|
||
const amdLabelRegex = /\bT(CTL|DIE|CCD|CCD\d+|Sx|LOOP)\b/i;
|
||
|
||
b.forEach(function(v){
|
||
// 风扇转速数据
|
||
let fandata = v.match(/(?<=:\s+)[1-9]\d*(?=\s+RPM\s+)/ig);
|
||
if (fandata) {
|
||
otherResults.push('风扇: ' + fandata.join(', ') + ' RPM');
|
||
return;
|
||
}
|
||
|
||
let name = v.match(/^[^-]+/);
|
||
if (!name) return;
|
||
name = name[0].toUpperCase();
|
||
|
||
let temps = v.match(/(?<=:\s+)[+-][\d.]+(?=.?°C)/g);
|
||
if (!temps) return;
|
||
|
||
temps = temps.map(t => parseFloat(t));
|
||
|
||
// 只处理 CPU 温度(Intel coretemp 或 AMD 相关传感器)
|
||
const isCpuSensor = cpuSensorRegex.test(name) || amdLabelRegex.test(v);
|
||
|
||
if (isCpuSensor) {
|
||
let packageTemp = temps[0];
|
||
|
||
if (temps.length > 1) {
|
||
let coreTemps = temps.slice(1);
|
||
let avgCore = coreTemps.reduce((a, b) => a + b, 0) / coreTemps.length;
|
||
let maxCore = Math.max(...coreTemps);
|
||
let minCore = Math.min(...coreTemps);
|
||
|
||
cpuResults.push(`封装: ${colorizeTemp(packageTemp)} | 核心: 平均 ${colorizeTemp(avgCore)} (${colorizeTemp(minCore)}~${colorizeTemp(maxCore)})`);
|
||
} else {
|
||
cpuResults.push(`封装: ${colorizeTemp(packageTemp)}`);
|
||
}
|
||
|
||
// 添加临界温度
|
||
let crit = v.match(/(?<=\bcrit\b[^+]+\+)\d+/);
|
||
if (crit) {
|
||
cpuResults[cpuResults.length - 1] += ` | 临界: ${colorizeTemp(crit[0])}`;
|
||
}
|
||
} else {
|
||
// 非 CPU 温度(主板、NVME等)放到其他结果中
|
||
let tempStr = `${name}: ${colorizeTemp(temps[0])}`;
|
||
let crit = v.match(/(?<=\bcrit\b[^+]+\+)\d+/);
|
||
if (crit) {
|
||
tempStr += ` (临界: ${colorizeTemp(crit[0])})`;
|
||
}
|
||
otherResults.push(tempStr);
|
||
}
|
||
});
|
||
|
||
// 只返回 CPU 相关温度,其他传感器信息不显示在这里
|
||
// (NVME温度会在NVME硬盘信息中单独显示)
|
||
if (cpuResults.length === 0) {
|
||
return '未获取到CPU温度信息';
|
||
}
|
||
|
||
// 如果有多个CPU(如双路服务器),分别显示
|
||
if (cpuResults.length > 1) {
|
||
return cpuResults.map((temp, idx) => `CPU${idx}: ${temp}`).join(' | ');
|
||
} else {
|
||
return cpuResults[0];
|
||
}
|
||
}
|
||
},
|
||
EOF
|
||
|
||
# 动态为每个 NVME 硬盘添加 JavaScript 代码
|
||
for i in $(seq 0 $((nvi - 1))); do
|
||
cat >> $tmpf << EOF
|
||
|
||
{
|
||
itemId: 'nvme${i}0',
|
||
colspan: 2,
|
||
printBar: false,
|
||
title: gettext('NVME${i}'),
|
||
textField: 'nvme${i}',
|
||
renderer:function(value){
|
||
function colorizeTemp(temp) {
|
||
let tempNum = Number(temp);
|
||
if (Number.isNaN(tempNum)) {
|
||
return temp + '°C';
|
||
}
|
||
if (tempNum < 50) {
|
||
return '<span style="color: #27ae60; font-weight: 600;">' + tempNum + '°C</span>';
|
||
}
|
||
if (tempNum < 70) {
|
||
return '<span style="color: #f39c12; font-weight: 600;">' + tempNum + '°C</span>';
|
||
}
|
||
return '<span style="color: #e74c3c; font-weight: 600;">' + tempNum + '°C</span>';
|
||
}
|
||
|
||
function colorizeHealth(percent) {
|
||
let healthNum = Number(percent);
|
||
if (Number.isNaN(healthNum)) {
|
||
return percent + '%';
|
||
}
|
||
if (healthNum >= 80) {
|
||
return '<span style="color: #27ae60; font-weight: 600;">' + healthNum + '%</span>';
|
||
}
|
||
if (healthNum >= 50) {
|
||
return '<span style="color: #f39c12; font-weight: 600;">' + healthNum + '%</span>';
|
||
}
|
||
return '<span style="color: #e74c3c; font-weight: 600;">' + healthNum + '%</span>';
|
||
}
|
||
|
||
try{
|
||
let v = JSON.parse(value);
|
||
|
||
// 检查是否为空 JSON(硬盘不存在或已直通)
|
||
if (Object.keys(v).length === 0) {
|
||
return '<span style="color: #888;">未检测到 NVME(可能已直通或移除)</span>';
|
||
}
|
||
|
||
// 检查型号
|
||
let model = v.model_name;
|
||
if (!model) {
|
||
return '<span style="color: #f39c12;">NVME 信息不完整(建议检查连接状态)</span>';
|
||
}
|
||
|
||
// 构建显示内容
|
||
let parts = [model];
|
||
let hasData = false;
|
||
|
||
// 温度
|
||
if (v.temperature?.current !== undefined) {
|
||
parts.push('温度: ' + colorizeTemp(v.temperature.current));
|
||
hasData = true;
|
||
}
|
||
|
||
// 健康度和读写
|
||
let log = v.nvme_smart_health_information_log;
|
||
if (log) {
|
||
// 健康度
|
||
if (log.percentage_used !== undefined) {
|
||
let healthRemain = 100 - log.percentage_used;
|
||
let health = '健康: ' + colorizeHealth(healthRemain);
|
||
if (log.media_errors !== undefined && log.media_errors > 0) {
|
||
health += ' <span style="color: #e74c3c;">(0E: ' + log.media_errors + ')</span>';
|
||
}
|
||
parts.push(health);
|
||
hasData = true;
|
||
}
|
||
|
||
if (log.unsafe_shutdowns !== undefined) {
|
||
let shutdownColor = Number(log.unsafe_shutdowns) > 0 ? '#e74c3c' : '#27ae60';
|
||
parts.push('异常断电: <span style="color: ' + shutdownColor + '; font-weight: 600;">' + log.unsafe_shutdowns + '</span>');
|
||
hasData = true;
|
||
}
|
||
|
||
// 读写
|
||
if (log.data_units_read && log.data_units_written) {
|
||
let read = (log.data_units_read / 1956882).toFixed(1);
|
||
let write = (log.data_units_written / 1956882).toFixed(1);
|
||
parts.push('读写: ' + read + 'T / ' + write + 'T');
|
||
hasData = true;
|
||
}
|
||
}
|
||
|
||
// 通电时间
|
||
if (v.power_on_time?.hours !== undefined) {
|
||
let pot = '通电: ' + v.power_on_time.hours + '时';
|
||
if (v.power_cycle_count) {
|
||
pot += ' (次: ' + v.power_cycle_count + ')';
|
||
}
|
||
parts.push(pot);
|
||
hasData = true;
|
||
}
|
||
|
||
// SMART 状态
|
||
if (v.smart_status?.passed !== undefined) {
|
||
parts.push('SMART: ' + (v.smart_status.passed ? '<span style="color: #27ae60;">正常</span>' : '<span style="color: #e74c3c;">警告!</span>'));
|
||
hasData = true;
|
||
}
|
||
|
||
// 如果只有型号,没有其他数据,说明可能是权限或驱动问题
|
||
if (!hasData) {
|
||
return model + ' <span style="color: #888;">| 无法获取详细信息(检查 smartctl 权限或驱动)</span>';
|
||
}
|
||
|
||
return parts.join(' | ');
|
||
|
||
}catch(e){
|
||
return '<span style="color: #888;">无法解析 NVME 信息(可能使用控制器直通)</span>';
|
||
};
|
||
|
||
}
|
||
},
|
||
EOF
|
||
done
|
||
|
||
# 动态为每个 SATA 硬盘添加 JavaScript 代码
|
||
for i in $(seq 0 $((sdi - 1))); do
|
||
# 获取硬盘类型(固态/机械)
|
||
sd="/dev/sd$(echo {a..z} | cut -d' ' -f$((i+1)))"
|
||
sdsn=$(basename $sd 2>/dev/null)
|
||
sdcr=/sys/block/$sdsn/queue/rotational
|
||
if [ -f $sdcr ] && [ "$(cat $sdcr)" = "0" ]; then
|
||
sdtype="固态硬盘$i"
|
||
else
|
||
sdtype="机械硬盘$i"
|
||
fi
|
||
|
||
cat >> $tmpf << EOF
|
||
|
||
{
|
||
itemId: 'sd${i}0',
|
||
colspan: 2,
|
||
printBar: false,
|
||
title: gettext('${sdtype}'),
|
||
textField: 'sd${i}',
|
||
renderer:function(value){
|
||
function colorizeTemp(temp) {
|
||
let tempNum = Number(temp);
|
||
if (Number.isNaN(tempNum)) {
|
||
return temp + '°C';
|
||
}
|
||
if (tempNum < 40) {
|
||
return '<span style="color: #27ae60; font-weight: 600;">' + tempNum + '°C</span>';
|
||
}
|
||
if (tempNum < 50) {
|
||
return '<span style="color: #f39c12; font-weight: 600;">' + tempNum + '°C</span>';
|
||
}
|
||
return '<span style="color: #e74c3c; font-weight: 600;">' + tempNum + '°C</span>';
|
||
}
|
||
|
||
function findAtaSmartRawValue(table, ids) {
|
||
if (!Array.isArray(table)) {
|
||
return null;
|
||
}
|
||
let found = table.find(item => ids.includes(item?.id));
|
||
if (!found || !found.raw) {
|
||
return null;
|
||
}
|
||
return found.raw.string ?? found.raw.value ?? null;
|
||
}
|
||
|
||
try{
|
||
let v = JSON.parse(value);
|
||
console.log(v)
|
||
|
||
// 场景 1:硬盘休眠(节能模式)
|
||
if (v.standy === true) {
|
||
return '<span style="color: #27ae60;">硬盘休眠中(省电模式)</span>'
|
||
}
|
||
|
||
// 场景 2:空 JSON(硬盘不存在或已直通)
|
||
if (Object.keys(v).length === 0) {
|
||
return '<span style="color: #888;">未检测到硬盘(可能已直通或移除)</span>';
|
||
}
|
||
|
||
// 场景 3:检查型号
|
||
let model = v.model_name;
|
||
if (!model) {
|
||
return '<span style="color: #f39c12;">硬盘信息不完整(建议检查连接状态)</span>';
|
||
}
|
||
|
||
// 场景 4:构建正常显示内容
|
||
let parts = [model];
|
||
|
||
// 温度
|
||
if (v.temperature?.current !== undefined) {
|
||
parts.push('温度: ' + colorizeTemp(v.temperature.current));
|
||
}
|
||
|
||
// 通电时间
|
||
if (v.power_on_time?.hours !== undefined) {
|
||
let pot = '通电: ' + v.power_on_time.hours + '时';
|
||
if (v.power_cycle_count) {
|
||
pot += ',次: ' + v.power_cycle_count;
|
||
}
|
||
parts.push(pot);
|
||
}
|
||
|
||
// SMART 状态
|
||
if (v.smart_status?.passed !== undefined) {
|
||
parts.push('SMART: ' + (v.smart_status.passed ? '<span style="color: #27ae60;">正常</span>' : '<span style="color: #e74c3c;">警告!</span>'));
|
||
}
|
||
|
||
let unsafeShutdowns = findAtaSmartRawValue(v.ata_smart_attributes?.table, [174, 192]);
|
||
if (unsafeShutdowns !== null && unsafeShutdowns !== undefined && unsafeShutdowns !== '') {
|
||
let shutdownCount = String(unsafeShutdowns).trim();
|
||
let shutdownColor = Number(shutdownCount) > 0 ? '#e74c3c' : '#27ae60';
|
||
parts.push('异常断电: <span style="color: ' + shutdownColor + '; font-weight: 600;">' + shutdownCount + '</span>');
|
||
}
|
||
|
||
return parts.join(' | ');
|
||
|
||
}catch(e){
|
||
// JSON 解析失败
|
||
return '<span style="color: #888;">无法获取硬盘信息(可能使用 HBA 直通)</span>';
|
||
};
|
||
}
|
||
},
|
||
EOF
|
||
done
|
||
|
||
if [ "$enable_ups" = true ]; then
|
||
cat >> $tmpf << 'EOF'
|
||
|
||
{
|
||
itemId: 'ups-status',
|
||
colspan: 2,
|
||
printBar: false,
|
||
title: gettext('UPS 信息'),
|
||
textField: 'ups_status',
|
||
cellWrap: true,
|
||
renderer: function(value) {
|
||
if (!value || value.length === 0) {
|
||
return '提示: 未检测到 UPS 或 apcaccess 未运行';
|
||
}
|
||
|
||
try {
|
||
const DATE_MATCH = value.match(/DATE\s*:\s*([^\n]+)/m);
|
||
const STATUS_MATCH = value.match(/STATUS\s*:\s*([A-Z]+)/m);
|
||
const OUTPUTV_MATCH = value.match(/OUTPUTV\s*:\s*([\d\.]+)/m);
|
||
const LINEV_MATCH = value.match(/LINEV\s*:\s*([\d\.]+)/m);
|
||
const LOADPCT_MATCH = value.match(/LOADPCT\s*:\s*([\d\.]+)/m);
|
||
const BCHARGE_MATCH = value.match(/BCHARGE\s*:\s*([\d\.]+)/m);
|
||
const TIMELEFT_MATCH = value.match(/TIMELEFT\s*:\s*([\d\.]+)/m);
|
||
const NOMPOWER_MATCH = value.match(/NOMPOWER\s*:\s*([\d\.]+)/m);
|
||
const MODEL_MATCH = value.match(/MODEL\s*:\s*(.+)/m);
|
||
|
||
const DATE = DATE_MATCH ? DATE_MATCH[1].trim() : '未知时间';
|
||
const STATUS = STATUS_MATCH ? STATUS_MATCH[1] : 'UNKNOWN';
|
||
const VOLTAGE = (OUTPUTV_MATCH || LINEV_MATCH) ? (OUTPUTV_MATCH || LINEV_MATCH)[1] : '-';
|
||
const LOADPCT = LOADPCT_MATCH ? parseFloat(LOADPCT_MATCH[1]) : NaN;
|
||
const LOADPCT_TXT= isNaN(LOADPCT) ? '-' : LOADPCT_MATCH[1];
|
||
const BCHARGE = BCHARGE_MATCH ? BCHARGE_MATCH[1] : '-';
|
||
const TIMELEFT = TIMELEFT_MATCH ? TIMELEFT_MATCH[1] : '-';
|
||
const NOMPOWER = NOMPOWER_MATCH ? parseFloat(NOMPOWER_MATCH[1]) : NaN;
|
||
const MODEL = MODEL_MATCH ? MODEL_MATCH[1].trim() : '未知型号';
|
||
|
||
let powerStatusText = '';
|
||
switch (STATUS) {
|
||
case 'ONLINE':
|
||
powerStatusText = '市电供电正常';
|
||
break;
|
||
case 'ONBATT':
|
||
powerStatusText = '电池供电中(市电中断)';
|
||
break;
|
||
case 'CHRG':
|
||
powerStatusText = '电池充电中';
|
||
break;
|
||
case 'DISCHRG':
|
||
powerStatusText = '电池放电中';
|
||
break;
|
||
default:
|
||
powerStatusText = '状态: ' + STATUS;
|
||
break;
|
||
}
|
||
|
||
let totalPowerText = '-';
|
||
let currentPowerText = '-';
|
||
|
||
if (!isNaN(NOMPOWER) && NOMPOWER > 0) {
|
||
const totalPowerW = NOMPOWER;
|
||
totalPowerText = totalPowerW.toFixed(0) + ' W';
|
||
|
||
if (!isNaN(LOADPCT)) {
|
||
const currentPowerW = totalPowerW * LOADPCT / 100;
|
||
currentPowerText = currentPowerW.toFixed(0) + ' W';
|
||
}
|
||
}
|
||
|
||
return `${MODEL} | ${powerStatusText} | ${DATE}<br>
|
||
电量: ${BCHARGE} % | 剩余供电时间: ${TIMELEFT} 分钟<br>
|
||
电压: ${VOLTAGE} V | 负载: ${LOADPCT_TXT} %<br>
|
||
额定功率: ${totalPowerText} | 估算当前功率: ${currentPowerText}`;
|
||
} catch(e) {
|
||
return 'UPS 信息解析失败: ' + value;
|
||
}
|
||
}
|
||
},
|
||
EOF
|
||
fi
|
||
|
||
log_info "找到关键字pveversion的行号"
|
||
# 显示匹配的行
|
||
ln=$(sed -n '/pveversion/,+10{/},/{=;q}}' $pvemanagerlib)
|
||
echo "匹配的行号pveversion:" $ln
|
||
|
||
log_info "修改结果:"
|
||
sed -i "${ln}r $tmpf" $pvemanagerlib
|
||
# 显示修改结果
|
||
# sed -n '/pveversion/,+30p' $pvemanagerlib
|
||
|
||
log_info "修改页面高度"
|
||
# 统计添加了几条内容(2个基础项 + NVME + SATA + UPS)
|
||
if [ "$has_ups" = true ]; then
|
||
addRs=$((2 + nvi + sdi + 1))
|
||
ups_info="+ 1 个UPS"
|
||
else
|
||
addRs=$((2 + nvi + sdi))
|
||
ups_info=""
|
||
fi
|
||
|
||
echo
|
||
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||
echo "检测到添加了 $addRs 条监控项 (2个基础项 + $nvi 个NVME + $sdi 个SATA $ups_info)"
|
||
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||
echo "请选择高度调整方式:"
|
||
echo " 1. 自动计算 (推荐,参考 PVE 8 算法:28px/项)"
|
||
echo " 2. 手动设置 (自定义每项的高度增量)"
|
||
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||
read -p "请输入选项 [1-2] (直接回车使用自动计算): " height_choice
|
||
|
||
case ${height_choice:-1} in
|
||
1)
|
||
# 自动计算:每项 28px
|
||
addHei=$((28 * addRs))
|
||
log_info "使用自动计算:$addRs 项 × 28px = ${addHei}px"
|
||
;;
|
||
2)
|
||
# 手动设置
|
||
echo
|
||
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||
echo "手动设置说明:"
|
||
echo " - 推荐值范围: 20-40 (默认 28)"
|
||
echo " - 如果 CPU 核心很多或想显示更多信息,可适当增大"
|
||
echo " - 如果界面出现遮挡,可适当减小此值"
|
||
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||
read -p "请输入每项的高度增量 (px) [默认: 28]: " height_per_item
|
||
|
||
# 验证输入是否为数字,如果不是或为空则使用默认值 28
|
||
if [[ -z "$height_per_item" ]] || ! [[ "$height_per_item" =~ ^[0-9]+$ ]]; then
|
||
height_per_item=28
|
||
log_info "使用默认值: 28px/项"
|
||
else
|
||
log_info "使用自定义值: ${height_per_item}px/项"
|
||
fi
|
||
|
||
addHei=$((height_per_item * addRs))
|
||
log_success "计算结果:$addRs 项 × ${height_per_item}px = ${addHei}px"
|
||
;;
|
||
*)
|
||
# 无效选项,使用自动计算
|
||
addHei=$((28 * addRs))
|
||
log_warn "无效选项,使用自动计算:${addHei}px"
|
||
;;
|
||
esac
|
||
|
||
rm $tmpf
|
||
|
||
# 修改左栏高度(原高度 300)
|
||
log_step "修改左栏高度"
|
||
wph=$(sed -n -E "/widget\.pveNodeStatus/,+4{/height:/{s/[^0-9]*([0-9]+).*/\1/p;q}}" $pvemanagerlib)
|
||
if [ -n "$wph" ]; then
|
||
sed -i -E "/widget\.pveNodeStatus/,+4{/height:/{s#[0-9]+#$((wph + addHei))#}}" $pvemanagerlib
|
||
echo "左栏高度: $wph → $((wph + addHei))" >> /var/log/pve-tools.log
|
||
else
|
||
log_warn "找不到左栏高度修改点"
|
||
fi
|
||
|
||
log_info "跳过强制修改右栏 minHeight,避免磁盘较多时图表区域被异常拉高"
|
||
|
||
# 调整显示布局
|
||
ln=$(expr $(sed -n -e '/widget.pveDcGuests/=' $pvemanagerlib) + 10)
|
||
sed -i "${ln}a\ textAlign: 'right'," $pvemanagerlib
|
||
ln=$(expr $(sed -n -e '/widget.pveNodeStatus/=' $pvemanagerlib) + 10)
|
||
sed -i "${ln}a\ textAlign: 'right'," $pvemanagerlib
|
||
|
||
################### 修改proxmoxlib.js ##########################
|
||
|
||
log_info "加强去除订阅弹窗"
|
||
# 调用 remove_subscription_popup 函数,避免重复代码
|
||
remove_subscription_popup
|
||
|
||
# 显示修改结果
|
||
# sed -n '/\/nodes\/localhost\/subscription/,+10p' $proxmoxlib >> /var/log/pve-tools.log
|
||
systemctl restart pveproxy
|
||
|
||
log_success "请刷新浏览器缓存shift+f5"
|
||
}
|
||
|
||
cpu_del() {
|
||
local nodes="/usr/share/perl5/PVE/API2/Nodes.pm"
|
||
local pvemanagerlib="/usr/share/pve-manager/js/pvemanagerlib.js"
|
||
local proxmoxlib="/usr/share/javascript/proxmox-widget-toolkit/proxmoxlib.js"
|
||
local pvever
|
||
|
||
pvever=$(pveversion | awk -F"/" '{print $2}')
|
||
log_step "Restore official node overview files"
|
||
log_warn "This will remove the temperature patch and reinstall official pve-manager / proxmox-widget-toolkit files"
|
||
|
||
if ! confirm_action "Restore official node overview files?"; then
|
||
return
|
||
fi
|
||
|
||
if reinstall_pve_webui_packages; then
|
||
rm -f "$nodes.$pvever.bak" "$pvemanagerlib.$pvever.bak" "$proxmoxlib.$pvever.bak"
|
||
log_success "Official node overview files restored. Use Shift+F5 to refresh browser cache."
|
||
fi
|
||
}
|
||
#--------------CPU、主板、硬盘温度显示----------------
|
||
|
||
#--------------GRUB 配置管理工具----------------
|
||
# 展示当前 GRUB 配置
|
||
show_grub_config() {
|
||
log_info "当前 GRUB 配置信息"
|
||
echo "$UI_DIVIDER"
|
||
|
||
if [ ! -f "/etc/default/grub" ]; then
|
||
log_error "未找到 /etc/default/grub 文件"
|
||
return 1
|
||
fi
|
||
|
||
log_info "文件路径: ${CYAN}/etc/default/grub${NC}"
|
||
log_info "当前内核参数:"
|
||
|
||
# 读取并显示 GRUB_CMDLINE_LINUX_DEFAULT
|
||
current_config=$(grep '^GRUB_CMDLINE_LINUX_DEFAULT=' /etc/default/grub | sed 's/GRUB_CMDLINE_LINUX_DEFAULT=//' | tr -d '"')
|
||
|
||
if [ -z "$current_config" ]; then
|
||
log_warn "未找到 GRUB_CMDLINE_LINUX_DEFAULT 配置"
|
||
else
|
||
log_success "GRUB_CMDLINE_LINUX_DEFAULT 内容:"
|
||
# 逐行显示参数
|
||
echo "$current_config" | tr ' ' '\n' | while read -r param; do
|
||
[ -n "$param" ] && echo -e " ${BLUE}•${NC} $param"
|
||
done
|
||
fi
|
||
|
||
echo "$UI_DIVIDER"
|
||
|
||
# 检测关键参数
|
||
log_info "关键参数检测:"
|
||
|
||
# 检测 IOMMU
|
||
if echo "$current_config" | grep -q "intel_iommu=on\|amd_iommu=on"; then
|
||
echo -e " ${GREEN}[ OK ]${NC} IOMMU: 已启用"
|
||
else
|
||
echo -e " ${YELLOW}[WARN]${NC} IOMMU: 未启用"
|
||
fi
|
||
|
||
# 检测 SR-IOV
|
||
if echo "$current_config" | grep -q "i915.enable_guc=3"; then
|
||
echo -e " ${GREEN}[ OK ]${NC} SR-IOV: 已配置"
|
||
else
|
||
echo -e " ${BLUE}[INFO]${NC} SR-IOV: 未配置"
|
||
fi
|
||
|
||
# 检测 GVT-g
|
||
if echo "$current_config" | grep -q "i915.enable_gvt=1"; then
|
||
echo -e " ${GREEN}[ OK ]${NC} GVT-g: 已配置"
|
||
else
|
||
echo -e " ${BLUE}[INFO]${NC} GVT-g: 未配置"
|
||
fi
|
||
|
||
# 检测硬件直通
|
||
if echo "$current_config" | grep -q "iommu=pt"; then
|
||
echo -e " ${GREEN}[ OK ]${NC} 硬件直通: 已启用"
|
||
else
|
||
echo -e " ${BLUE}[INFO]${NC} 硬件直通: 未启用"
|
||
fi
|
||
|
||
echo "$UI_DIVIDER"
|
||
}
|
||
|
||
# GRUB 配置备份
|
||
backup_grub_with_note() {
|
||
local note="$1"
|
||
local backup_dir="/etc/pvetools9/backup/grub"
|
||
local timestamp=$(date +"%Y%m%d_%H%M%S")
|
||
local backup_file="${backup_dir}/${timestamp}_${note}.bak"
|
||
|
||
log_step "备份 GRUB 配置..."
|
||
|
||
# 创建备份目录
|
||
if [ ! -d "$backup_dir" ]; then
|
||
mkdir -p "$backup_dir" || {
|
||
log_error "无法创建备份目录: $backup_dir"
|
||
return 1
|
||
}
|
||
log_info "创建备份目录: $backup_dir"
|
||
fi
|
||
|
||
# 检查源文件
|
||
if [ ! -f "/etc/default/grub" ]; then
|
||
log_error "源文件不存在: /etc/default/grub"
|
||
return 1
|
||
fi
|
||
|
||
# 执行备份
|
||
cp "/etc/default/grub" "$backup_file" || {
|
||
log_error "备份失败"
|
||
return 1
|
||
}
|
||
|
||
log_success "GRUB 配置已备份"
|
||
log_info "备份文件: $backup_file"
|
||
log_info "备份时间: $(date '+%Y-%m-%d %H:%M:%S')"
|
||
log_info "备份备注: $note"
|
||
|
||
# 统计备份文件数量
|
||
local backup_count=$(ls -1 "$backup_dir"/*.bak 2>/dev/null | wc -l)
|
||
log_info "当前共有 $backup_count 个备份文件"
|
||
|
||
return 0
|
||
}
|
||
|
||
# 列出所有 GRUB 备份
|
||
list_grub_backups() {
|
||
local backup_dir="/etc/pvetools9/backup/grub"
|
||
|
||
log_info "GRUB 配置备份列表"
|
||
log_step "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||
|
||
if [ ! -d "$backup_dir" ]; then
|
||
log_warn "备份目录不存在: $backup_dir"
|
||
log_tips "尚未创建任何备份"
|
||
return 0
|
||
fi
|
||
|
||
local backup_files=$(ls -1t "$backup_dir"/*.bak 2>/dev/null)
|
||
|
||
if [ -z "$backup_files" ]; then
|
||
log_warn "未找到任何备份文件"
|
||
return 0
|
||
fi
|
||
|
||
local count=1
|
||
echo "$backup_files" | while read -r backup_file; do
|
||
local filename=$(basename "$backup_file")
|
||
local filesize=$(du -h "$backup_file" | awk '{print $1}')
|
||
local filetime=$(stat -c '%y' "$backup_file" 2>/dev/null || stat -f '%Sm' "$backup_file")
|
||
|
||
log_info "备份 $count:"
|
||
log_info " 文件名: $filename"
|
||
log_info " 大小: $filesize"
|
||
log_info " 时间: $filetime"
|
||
log_step " ────────────────────────────────────"
|
||
|
||
count=$((count + 1))
|
||
done
|
||
|
||
log_step "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||
}
|
||
|
||
# 恢复 GRUB 备份
|
||
restore_grub_backup() {
|
||
local backup_dir="/etc/pvetools9/backup/grub"
|
||
|
||
list_grub_backups
|
||
|
||
if [ ! -d "$backup_dir" ] || [ -z "$(ls -A "$backup_dir"/*.bak 2>/dev/null)" ]; then
|
||
log_error "没有可恢复的备份文件"
|
||
pause_function
|
||
return 1
|
||
fi
|
||
|
||
echo
|
||
log_warn "请输入要恢复的备份文件名(完整文件名):"
|
||
read -p "> " backup_filename
|
||
|
||
local backup_file="${backup_dir}/${backup_filename}"
|
||
|
||
if [ ! -f "$backup_file" ]; then
|
||
log_error "备份文件不存在: $backup_filename"
|
||
pause_function
|
||
return 1
|
||
fi
|
||
|
||
log_warn "即将恢复 GRUB 配置"
|
||
log_info "源文件: $backup_file"
|
||
log_info "目标文件: /etc/default/grub"
|
||
|
||
if ! confirm_action "确认恢复此备份"; then
|
||
log_info "用户取消恢复操作"
|
||
return 0
|
||
fi
|
||
|
||
# 在恢复前备份当前配置
|
||
backup_grub_with_note "恢复前自动备份"
|
||
|
||
# 执行恢复
|
||
cp "$backup_file" "/etc/default/grub" || {
|
||
log_error "恢复失败"
|
||
pause_function
|
||
return 1
|
||
}
|
||
|
||
log_success "GRUB 配置已恢复"
|
||
|
||
# 更新 GRUB
|
||
if confirm_action "是否立即更新 GRUB"; then
|
||
update-grub && log_success "GRUB 更新完成" || log_error "GRUB 更新失败"
|
||
fi
|
||
|
||
pause_function
|
||
}
|
||
#--------------GRUB 配置管理工具----------------
|
||
|
||
#--------------核显虚拟化管理----------------
|
||
# 核显管理菜单
|
||
# 简化版核显虚拟化菜单(保留用于兼容性)
|
||
igpu_management_menu_simple() {
|
||
while true; do
|
||
clear
|
||
show_menu_header "Intel 核显虚拟化管理"
|
||
show_menu_option "1" "Intel 11-15代 SR-IOV 配置 (DKMS)"
|
||
show_menu_option "2" "Intel 6-10代 GVT-g 配置 (传统模式)"
|
||
show_menu_option "3" "验证核显虚拟化状态"
|
||
show_menu_option "4" "清理核显虚拟化配置 (恢复默认)"
|
||
show_menu_option "0" "返回主菜单"
|
||
show_menu_footer
|
||
|
||
read -p "请选择操作 [0-4]: " choice
|
||
case $choice in
|
||
1) igpu_sriov_setup ;;
|
||
2) igpu_gvtg_setup ;;
|
||
3) igpu_verify ;;
|
||
4) restore_igpu_config ;;
|
||
0) return ;;
|
||
*) log_error "无效选择" ;;
|
||
esac
|
||
pause_function
|
||
done
|
||
}
|
||
|
||
# Intel 11-15代 SR-IOV 核显虚拟化配置
|
||
igpu_sriov_setup() {
|
||
echo -e "${H2}开始配置 Intel 11-15代 SR-IOV 核显虚拟化${NC}"
|
||
echo -e "详细原理与教程: ${CYAN}https://pve.u3u.icu/advanced/gpu-virtualization${NC}"
|
||
echo -e "如果配置失败,请访问文档站下方留言反馈。"
|
||
echo
|
||
|
||
# 检查内核版本
|
||
kernel_version=$(uname -r | awk -F'-' '{print $1}')
|
||
kernel_major=$(echo $kernel_version | cut -d'.' -f1)
|
||
kernel_minor=$(echo $kernel_version | cut -d'.' -f2)
|
||
|
||
if [ "$kernel_major" -lt 6 ] || ([ "$kernel_major" -eq 6 ] && [ "$kernel_minor" -lt 8 ]); then
|
||
echo -e "${RED}SR-IOV 需要内核版本 6.8 或更高${NC}"
|
||
echo -e " ${YELLOW}提示:${NC} 当前内核版本: $(uname -r)"
|
||
echo -e " ${YELLOW}提示:${NC} 请先使用内核管理功能升级到 6.8 内核"
|
||
pause_function
|
||
return 1
|
||
fi
|
||
|
||
echo -e "${GREEN}✓ 内核版本检查通过: $(uname -r)${NC}"
|
||
|
||
# 展示当前 GRUB 配置
|
||
echo
|
||
show_grub_config
|
||
echo
|
||
|
||
# 危险性警告
|
||
echo "$UI_BORDER"
|
||
echo -e " ${RED}【高危操作警告】${NC} SR-IOV 核显虚拟化配置"
|
||
echo "$UI_BORDER"
|
||
echo -e " 此操作属于${RED}【高危险性】${NC}系统配置,配置错误可能导致:"
|
||
echo -e " - ${YELLOW}系统无法正常启动${NC}(GRUB 配置错误)"
|
||
echo -e " - ${YELLOW}核显完全不可用${NC}(参数配置错误)"
|
||
echo -e " - ${YELLOW}虚拟机黑屏或无法启动${NC}(直通配置错误)"
|
||
echo -e " - ${YELLOW}需要通过恢复模式修复系统${NC}"
|
||
echo "$UI_BORDER"
|
||
echo -e " 此功能将修改以下系统配置:"
|
||
echo -e " 1. 修改 ${CYAN}GRUB 引导参数${NC}(启用 IOMMU 和 SR-IOV)"
|
||
echo -e " 2. 加载 ${CYAN}VFIO${NC} 内核模块"
|
||
echo -e " 3. 下载并安装 ${CYAN}i915-sriov-dkms${NC} 驱动(约 10MB)"
|
||
echo -e " 4. 配置虚拟核显数量(VFs)"
|
||
echo
|
||
echo -e " ${GREEN}前置要求(请确认已完成):${NC}"
|
||
echo -e " ${GREEN}✓${NC} BIOS 已开启 ${CYAN}VT-d${NC} 虚拟化"
|
||
echo -e " ${GREEN}✓${NC} BIOS 已开启 ${CYAN}SR-IOV${NC}(如有此选项)"
|
||
echo -e " ${GREEN}✓${NC} BIOS 已开启 ${CYAN}Above 4GB${NC}(如有此选项)"
|
||
echo -e " ${GREEN}✓${NC} BIOS 已关闭 ${CYAN}Secure Boot${NC} 安全启动"
|
||
echo -e " ${GREEN}✓${NC} CPU 为 ${CYAN}Intel 11-15 代${NC} 处理器"
|
||
echo -e " ${RED}重要:${NC}物理核显 (00:02.0) 不能直通,否则所有虚拟核显将消失"
|
||
echo "$UI_BORDER"
|
||
echo
|
||
echo -e "${YELLOW}强烈建议:${NC}"
|
||
echo -e " ${CYAN}提示 1:${NC} 在继续前先备份当前 GRUB 配置"
|
||
echo -e " ${CYAN}提示 2:${NC} 确保了解核显虚拟化的工作原理"
|
||
echo -e " ${CYAN}提示 3:${NC} 准备好通过 SSH 或物理访问恢复系统"
|
||
echo
|
||
|
||
# 询问是否要备份
|
||
if confirm_action "是否先备份当前 GRUB 配置(强烈推荐)"; then
|
||
echo
|
||
echo "请输入备份备注(例如:SR-IOV配置前备份):"
|
||
read -p "> " backup_note
|
||
backup_note=${backup_note:-"SR-IOV配置前备份"}
|
||
backup_grub_with_note "$backup_note"
|
||
echo
|
||
fi
|
||
|
||
if ! confirm_action "确认继续配置 SR-IOV 核显虚拟化"; then
|
||
echo "用户取消操作"
|
||
return 0
|
||
fi
|
||
|
||
# 安装必要的软件包
|
||
echo "安装必要的软件包..."
|
||
apt-get update
|
||
|
||
echo "安装 pve-headers..."
|
||
apt-get install -y "pve-headers-$(uname -r)" || {
|
||
echo -e "${RED}安装 pve-headers 失败${NC}"
|
||
pause_function
|
||
return 1
|
||
}
|
||
|
||
echo "安装构建工具..."
|
||
apt-get install -y build-essential dkms sysfsutils || {
|
||
echo -e "安装构建工具失败"
|
||
pause_function
|
||
return 1
|
||
}
|
||
|
||
echo -e "✓ 软件包安装完成"
|
||
|
||
# 备份并修改 GRUB 配置
|
||
echo "配置 GRUB 引导参数..."
|
||
backup_file "/etc/default/grub"
|
||
|
||
# 使用幂等的 GRUB 参数管理函数
|
||
echo "配置 GRUB 参数..."
|
||
|
||
# 移除旧的 GVT-g 配置(如果有)
|
||
grub_remove_param "i915.enable_gvt"
|
||
grub_remove_param "pcie_acs_override"
|
||
|
||
# 添加 SR-IOV 参数(幂等操作,不会重复添加)
|
||
# 针对 6.8+ 内核,必须屏蔽 xe 驱动以防止冲突
|
||
# 参考: https://github.com/strongtz/i915-sriov-dkms
|
||
grub_add_param "intel_iommu=on"
|
||
grub_add_param "iommu=pt"
|
||
grub_add_param "i915.enable_guc=3"
|
||
grub_add_param "i915.max_vfs=7"
|
||
grub_add_param "module_blacklist=xe"
|
||
|
||
echo -e "✓ GRUB 配置已更新 (已添加 module_blacklist=xe 以兼容 PVE 9.1)"
|
||
|
||
# 更新 GRUB
|
||
echo "更新 GRUB..."
|
||
update-grub || {
|
||
echo -e "更新 GRUB 失败"
|
||
pause_function
|
||
return 1
|
||
}
|
||
|
||
# 配置内核模块
|
||
echo "配置内核模块..."
|
||
backup_file "/etc/modules"
|
||
|
||
# 清理可能存在的 i915 及音视频相关黑名单 (SR-IOV 需要 i915 驱动加载)
|
||
echo "清理可能存在的 i915 及音视频相关黑名单..."
|
||
for f in /etc/modprobe.d/blacklist.conf /etc/modprobe.d/pve-blacklist.conf; do
|
||
if [ -f "$f" ]; then
|
||
sed -i '/blacklist i915/d' "$f"
|
||
sed -i '/blacklist snd_hda_intel/d' "$f"
|
||
sed -i '/blacklist snd_hda_codec_hdmi/d' "$f"
|
||
fi
|
||
done
|
||
|
||
# 添加 VFIO 模块(如果未添加)
|
||
for module in vfio vfio_iommu_type1 vfio_pci vfio_virqfd; do
|
||
if ! grep -q "^$module$" /etc/modules; then
|
||
echo "$module" >> /etc/modules
|
||
echo "已添加模块: $module"
|
||
fi
|
||
done
|
||
|
||
# 移除 kvmgt 模块(如果有 GVT-g 配置)
|
||
sed -i '/^kvmgt$/d' /etc/modules
|
||
|
||
echo -e "✓ 内核模块配置完成"
|
||
|
||
# 更新 initramfs
|
||
echo "更新 initramfs..."
|
||
update-initramfs -u -k all || {
|
||
echo -e "更新 initramfs 失败,但可以继续"
|
||
}
|
||
|
||
# 下载并安装 i915-sriov-dkms 驱动
|
||
echo "下载 i915-sriov-dkms 驱动..."
|
||
echo " 提示: 请在浏览器访问 https://github.com/strongtz/i915-sriov-dkms/releases 选择匹配的版本"
|
||
echo " 一般建议选择最新的 release 版本以兼容最新的内核版本"
|
||
echo " 输入格式:例如:2025.11.10"
|
||
echo " 不输入回车的默认版本为 2025.11.10,可能不兼容老版本内核,故障表现在无法虚拟出 VFs"
|
||
|
||
default_dkms_version="2025.11.10"
|
||
read -p "请输入要安装的 release 版本号 [默认: ${default_dkms_version}]: " dkms_version_input
|
||
dkms_version_input=$(echo "$dkms_version_input" | xargs)
|
||
|
||
if [ -z "$dkms_version_input" ]; then
|
||
dkms_version_input="$default_dkms_version"
|
||
fi
|
||
|
||
# release 标签可能以 v 打头,但 deb 文件名不包含 v
|
||
dkms_asset_version=$(echo "$dkms_version_input" | sed 's/^[vV]//')
|
||
dkms_tag="$dkms_version_input"
|
||
|
||
dkms_url="https://github.com/strongtz/i915-sriov-dkms/releases/download/${dkms_tag}/i915-sriov-dkms_${dkms_asset_version}_amd64.deb"
|
||
dkms_file="/tmp/i915-sriov-dkms_${dkms_asset_version}_amd64.deb"
|
||
|
||
# 检查是否已下载
|
||
if [ -f "$dkms_file" ]; then
|
||
echo "驱动文件已存在,跳过下载"
|
||
else
|
||
echo "从 GitHub 下载驱动..."
|
||
echo " 提示: 如果下载失败,请检查网络或手动下载后放到 /tmp/ 目录"
|
||
|
||
wget -O "$dkms_file" "$dkms_url" || {
|
||
echo -e "下载驱动失败"
|
||
echo " 提示: 请手动下载: $dkms_url"
|
||
echo " 提示: 并上传到 PVE 的 /tmp/ 目录后重试"
|
||
pause_function
|
||
return 1
|
||
}
|
||
fi
|
||
|
||
echo "安装 i915-sriov-dkms 驱动..."
|
||
echo -e "驱动安装可能需要较长时间,请耐心等待..."
|
||
|
||
dpkg -i "$dkms_file" || {
|
||
echo -e "安装驱动失败"
|
||
pause_function
|
||
return 1
|
||
}
|
||
|
||
# 验证驱动安装
|
||
echo "验证驱动安装..."
|
||
if modinfo i915 2>/dev/null | grep -q "max_vfs"; then
|
||
echo -e "✓ i915-sriov 驱动安装成功"
|
||
else
|
||
echo -e "驱动验证失败,请检查安装过程"
|
||
pause_function
|
||
return 1
|
||
fi
|
||
|
||
# 配置 VFs 数量
|
||
echo
|
||
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||
echo "配置虚拟核显(VFs)数量"
|
||
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||
echo
|
||
echo "虚拟核显数量范围: 1-7"
|
||
echo "推荐配置:"
|
||
echo " - 1 个 VF: 性能最强,适合单个高性能虚拟机"
|
||
echo " - 2-3 个 VF: 平衡性能,适合多个虚拟机"
|
||
echo " - 4-7 个 VF: 最多虚拟机数量,性能较弱"
|
||
echo
|
||
read -p "请输入 VFs 数量 [1-7, 默认: 3]: " vfs_num
|
||
|
||
# 验证输入
|
||
if [[ -z "$vfs_num" ]]; then
|
||
vfs_num=3
|
||
elif ! [[ "$vfs_num" =~ ^[1-7]$ ]]; then
|
||
echo -e "无效的 VFs 数量,必须是 1-7"
|
||
pause_function
|
||
return 1
|
||
fi
|
||
|
||
echo "配置 $vfs_num 个虚拟核显"
|
||
|
||
# 写入 sysfs.conf
|
||
echo "devices/pci0000:00/0000:00:02.0/sriov_numvfs = $vfs_num" > /etc/sysfs.conf
|
||
echo -e "✓ VFs 数量配置完成"
|
||
|
||
# 完成提示
|
||
echo
|
||
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||
echo -e "✓ SR-IOV 核显虚拟化配置完成!"
|
||
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||
echo
|
||
echo "配置摘要:"
|
||
echo " • 内核参数: intel_iommu=on iommu=pt i915.enable_guc=3 i915.max_vfs=7"
|
||
echo " • VFIO 模块: 已加载"
|
||
echo " • i915-sriov 驱动: 已安装"
|
||
echo " • 虚拟核显数量: $vfs_num 个"
|
||
echo
|
||
echo -e "下一步操作:"
|
||
echo -e " 1. 重启系统使配置生效"
|
||
echo " 2. 重启后使用 '验证核显虚拟化状态' 检查配置"
|
||
echo " 3. 在虚拟机配置中添加核显 SR-IOV 设备"
|
||
echo
|
||
echo -e "重要提示:"
|
||
echo -e " • 物理核显 (00:02.0) 不能直通给虚拟机"
|
||
echo -e " • 只能直通虚拟核显 (00:02.1 ~ 00:02.$vfs_num)"
|
||
echo -e " • 虚拟机需要勾选 ROM-Bar 和 PCIE 选项"
|
||
echo
|
||
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||
|
||
if confirm_action "是否现在重启系统"; then
|
||
echo "正在重启系统..."
|
||
reboot
|
||
else
|
||
echo -e "请记得手动重启系统以使配置生效"
|
||
fi
|
||
}
|
||
|
||
# Intel 6-10代 GVT-g 核显虚拟化配置
|
||
igpu_gvtg_setup() {
|
||
echo -e "${H2}开始配置 Intel 6-10代 GVT-g 核显虚拟化${NC}"
|
||
echo -e "详细原理与教程: ${CYAN}https://pve.u3u.icu/advanced/gpu-virtualization${NC}"
|
||
echo -e "如果配置失败,请访问文档站下方留言反馈。"
|
||
echo
|
||
|
||
# 展示当前 GRUB 配置
|
||
echo
|
||
show_grub_config
|
||
echo
|
||
|
||
# 危险性警告
|
||
echo "$UI_BORDER"
|
||
echo -e " ${RED}【高危操作警告】${NC} GVT-g 核显虚拟化配置"
|
||
echo "$UI_BORDER"
|
||
echo -e " 此操作属于${RED}【高危险性】${NC}系统配置,配置错误可能导致:"
|
||
echo -e " - ${YELLOW}系统无法正常启动${NC}(GRUB 配置错误)"
|
||
echo -e " - ${YELLOW}核显完全不可用${NC}(参数配置错误)"
|
||
echo -e " - ${YELLOW}虚拟机黑屏或无法启动${NC}(直通配置错误)"
|
||
echo -e " - ${YELLOW}需要通过恢复模式修复系统${NC}"
|
||
echo "$UI_BORDER"
|
||
echo
|
||
echo -e " 此功能将修改以下系统配置:"
|
||
echo -e " 1. 修改 ${CYAN}GRUB 引导参数${NC}(启用 IOMMU 和 GVT-g)"
|
||
echo -e " 2. 加载 ${CYAN}VFIO${NC} 和 ${CYAN}kvmgt${NC} 内核模块"
|
||
echo
|
||
echo -e " ${GREEN}前置要求(请确认已完成):${NC}"
|
||
echo -e " ${GREEN}✓${NC} BIOS 已开启 ${CYAN}VT-d${NC} 虚拟化"
|
||
echo -e " ${GREEN}✓${NC} BIOS 已开启 ${CYAN}SR-IOV${NC}(如有此选项)"
|
||
echo -e " ${GREEN}✓${NC} BIOS 已开启 ${CYAN}Above 4GB${NC}(如有此选项)"
|
||
echo -e " ${GREEN}✓${NC} BIOS 已关闭 ${CYAN}Secure Boot${NC} 安全启动"
|
||
echo -e " ${GREEN}✓${NC} CPU 为 ${CYAN}Intel 6-10 代${NC} 处理器"
|
||
echo
|
||
echo -e " ${PRIMARY}支持的处理器代号:${NC}"
|
||
echo -e " ${BLUE}•${NC} Skylake (6代)"
|
||
echo -e " ${BLUE}•${NC} Kaby Lake (7代)"
|
||
echo -e " ${BLUE}•${NC} Coffee Lake (8代)"
|
||
echo -e " ${BLUE}•${NC} Coffee Lake Refresh (9代)"
|
||
echo -e " ${BLUE}•${NC} Comet Lake (10代)"
|
||
echo
|
||
echo -e " ${MAGENTA}特殊的处理器代号:${NC}"
|
||
echo -e " ${MAGENTA}•${NC} Rocket Lake / Tiger Lake (11代) 因处在当前代与上一代交界"
|
||
echo -e " 部分型号支持,但是不保证兼容性,请谨慎使用"
|
||
echo "$UI_BORDER"
|
||
echo
|
||
echo -e "${YELLOW}强烈建议:${NC}"
|
||
echo -e " ${CYAN}提示 1:${NC} 在继续前先备份当前 GRUB 配置"
|
||
echo -e " ${CYAN}提示 2:${NC} 确保了解核显虚拟化的工作原理"
|
||
echo -e " ${CYAN}提示 3:${NC} 准备好通过 SSH 或物理访问恢复系统"
|
||
echo
|
||
|
||
# 询问是否要备份
|
||
if confirm_action "是否先备份当前 GRUB 配置(强烈推荐)"; then
|
||
echo
|
||
echo "请输入备份备注(例如:GVT-g配置前备份):"
|
||
read -p "> " backup_note
|
||
backup_note=${backup_note:-"GVT-g配置前备份"}
|
||
backup_grub_with_note "$backup_note"
|
||
echo
|
||
fi
|
||
|
||
if ! confirm_action "确认继续配置 GVT-g 核显虚拟化"; then
|
||
echo "用户取消操作"
|
||
return 0
|
||
fi
|
||
|
||
# 备份并修改 GRUB 配置
|
||
echo "配置 GRUB 引导参数..."
|
||
backup_file "/etc/default/grub"
|
||
|
||
# 使用幂等的 GRUB 参数管理函数
|
||
echo "配置 GRUB 参数..."
|
||
|
||
# 移除旧的 SR-IOV 配置(如果有)
|
||
grub_remove_param "i915.enable_guc"
|
||
grub_remove_param "i915.max_vfs"
|
||
grub_remove_param "module_blacklist"
|
||
|
||
# 添加 GVT-g 参数(幂等操作,不会重复添加)
|
||
grub_add_param "intel_iommu=on"
|
||
grub_add_param "iommu=pt"
|
||
grub_add_param "i915.enable_gvt=1"
|
||
grub_add_param "pcie_acs_override=downstream,multifunction"
|
||
|
||
echo -e "✓ GRUB 配置已更新"
|
||
|
||
# 更新 GRUB
|
||
echo "更新 GRUB..."
|
||
update-grub || {
|
||
echo -e "更新 GRUB 失败"
|
||
pause_function
|
||
return 1
|
||
}
|
||
|
||
# 配置内核模块
|
||
echo "配置内核模块..."
|
||
backup_file "/etc/modules"
|
||
|
||
# 清理可能存在的 i915 及音视频相关黑名单 (GVT-g 需要 i915 驱动加载)
|
||
echo "清理可能存在的 i915 及音视频相关黑名单..."
|
||
for f in /etc/modprobe.d/blacklist.conf /etc/modprobe.d/pve-blacklist.conf; do
|
||
if [ -f "$f" ]; then
|
||
sed -i '/blacklist i915/d' "$f"
|
||
sed -i '/blacklist snd_hda_intel/d' "$f"
|
||
sed -i '/blacklist snd_hda_codec_hdmi/d' "$f"
|
||
fi
|
||
done
|
||
|
||
# 添加 VFIO 和 kvmgt 模块
|
||
for module in vfio vfio_iommu_type1 vfio_pci vfio_virqfd kvmgt; do
|
||
if ! grep -q "^$module$" /etc/modules; then
|
||
echo "$module" >> /etc/modules
|
||
echo "已添加模块: $module"
|
||
fi
|
||
done
|
||
|
||
echo -e "✓ 内核模块配置完成"
|
||
|
||
# 更新 initramfs
|
||
echo "更新 initramfs..."
|
||
update-initramfs -u -k all || {
|
||
echo -e "更新 initramfs 失败,但可以继续"
|
||
}
|
||
|
||
# 完成提示
|
||
echo
|
||
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||
echo -e "✓ GVT-g 核显虚拟化配置完成!"
|
||
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||
echo
|
||
echo "配置摘要:"
|
||
echo " • 内核参数: intel_iommu=on iommu=pt i915.enable_gvt=1"
|
||
echo " • VFIO 模块: 已加载"
|
||
echo " • kvmgt 模块: 已加载"
|
||
echo
|
||
echo -e "下一步操作:"
|
||
echo -e " 1. 重启系统使配置生效"
|
||
echo " 2. 重启后使用 '验证核显虚拟化状态' 检查配置"
|
||
echo " 3. 在虚拟机配置中添加核显 GVT-g 设备(Mdev 类型)"
|
||
echo
|
||
echo "常见 Mdev 类型:"
|
||
echo " • i915-GVTg_V5_4: 低性能,可创建更多虚拟机"
|
||
echo " • i915-GVTg_V5_8: 高性能,推荐使用(UHD630 最多 2 个)"
|
||
echo
|
||
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||
|
||
if confirm_action "是否现在重启系统"; then
|
||
echo "正在重启系统..."
|
||
reboot
|
||
else
|
||
echo -e "请记得手动重启系统以使配置生效"
|
||
fi
|
||
}
|
||
|
||
# 清理 GVT-g 和 SR-IOV 配置 (恢复默认)
|
||
restore_igpu_config() {
|
||
log_step "开始清理核显虚拟化配置 (恢复默认)"
|
||
echo -e " 此操作将执行以下步骤:"
|
||
echo -e " 1. 移除 ${CYAN}GRUB${NC} 中的核显相关参数"
|
||
echo -e " 2. 从 ${CYAN}/etc/modules${NC} 移除核显相关模块"
|
||
echo -e " 3. 更新 ${CYAN}GRUB${NC} 和 ${CYAN}initramfs${NC}"
|
||
echo -e " 适用于因配置核显虚拟化导致系统异常或想要重置配置的情况。"
|
||
echo
|
||
|
||
if ! confirm_action "是否继续执行清理操作?"; then
|
||
return
|
||
fi
|
||
|
||
# 1. 恢复 GRUB 配置
|
||
log_info "正在清理 GRUB 参数..."
|
||
if [[ -f "/etc/default/grub" ]]; then
|
||
# 备份 GRUB 配置
|
||
backup_file "/etc/default/grub"
|
||
|
||
# 移除相关参数
|
||
sed -i 's/intel_iommu=on//g' /etc/default/grub
|
||
sed -i 's/iommu=pt//g' /etc/default/grub
|
||
sed -i 's/i915.enable_gvt=1//g' /etc/default/grub
|
||
sed -i 's/i915.enable_guc=[0-9]*//g' /etc/default/grub
|
||
sed -i 's/i915.max_vfs=[0-9]*//g' /etc/default/grub
|
||
|
||
# 清理多余空格
|
||
sed -i 's/GRUB_CMDLINE_LINUX_DEFAULT="[[:space:]]*/GRUB_CMDLINE_LINUX_DEFAULT="/g' /etc/default/grub
|
||
sed -i 's/[[:space:]]*"$/"/g' /etc/default/grub
|
||
sed -i 's/[[:space:]]\{2,\}/ /g' /etc/default/grub
|
||
|
||
log_success "GRUB 参数清理完成"
|
||
else
|
||
log_error "未找到 /etc/default/grub 文件"
|
||
fi
|
||
|
||
# 2. 恢复 /etc/modules
|
||
log_info "正在清理 /etc/modules..."
|
||
if [[ -f "/etc/modules" ]]; then
|
||
backup_file "/etc/modules"
|
||
sed -i '/vfio/d' /etc/modules
|
||
sed -i '/vfio_iommu_type1/d' /etc/modules
|
||
sed -i '/vfio_pci/d' /etc/modules
|
||
sed -i '/vfio_virqfd/d' /etc/modules
|
||
sed -i '/kvmgt/d' /etc/modules
|
||
log_success "/etc/modules 清理完成"
|
||
fi
|
||
|
||
# 3. 更新系统配置
|
||
log_info "正在更新 GRUB..."
|
||
update-grub
|
||
|
||
log_info "正在更新 initramfs..."
|
||
update-initramfs -u -k all
|
||
|
||
log_success "清理完成!核显虚拟化配置已重置。"
|
||
if confirm_action "是否现在重启系统?"; then
|
||
reboot
|
||
fi
|
||
}
|
||
|
||
# 验证核显虚拟化状态
|
||
igpu_verify() {
|
||
echo
|
||
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||
echo " 核显虚拟化状态检查"
|
||
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||
echo
|
||
|
||
# 检查 IOMMU
|
||
echo "1. 检查 IOMMU 状态..."
|
||
if dmesg | grep -qi "DMAR.*IOMMU\|iommu.*enabled"; then
|
||
echo -e " ✓ IOMMU 已启用"
|
||
echo " $(dmesg | grep -i "DMAR.*IOMMU\|iommu.*enabled" | head -3)"
|
||
else
|
||
echo -e " ✗ IOMMU 未启用"
|
||
echo " 提示: 请检查 BIOS 是否开启 VT-d"
|
||
echo " 提示: 请检查 GRUB 配置是否包含 intel_iommu=on"
|
||
fi
|
||
echo
|
||
|
||
# 检查 VFIO 模块
|
||
echo "2. 检查 VFIO 模块加载状态..."
|
||
if lsmod | grep -q vfio; then
|
||
echo -e " ✓ VFIO 模块已加载"
|
||
echo " $(lsmod | grep vfio)"
|
||
else
|
||
echo -e " ✗ VFIO 模块未加载"
|
||
echo " 提示: 请检查 /etc/modules 配置"
|
||
fi
|
||
echo
|
||
|
||
# 检查 SR-IOV
|
||
echo "3. 检查 SR-IOV 虚拟核显..."
|
||
if lspci | grep -i "VGA.*Intel" | wc -l | grep -q "^[2-9]"; then
|
||
vf_count=$(($(lspci | grep -i "VGA.*Intel" | wc -l) - 1))
|
||
echo -e " ✓ 检测到 $vf_count 个虚拟核显 (SR-IOV)"
|
||
echo
|
||
lspci | grep -i "VGA.*Intel"
|
||
echo
|
||
echo " 提示: 物理核显 00:02.0 不能直通"
|
||
echo " 提示: 虚拟核显 00:02.1 ~ 00:02.$vf_count 可直通给虚拟机"
|
||
else
|
||
echo -e " ! 未检测到 SR-IOV 虚拟核显"
|
||
fi
|
||
echo
|
||
|
||
# 检查 GVT-g
|
||
echo "4. 检查 GVT-g mdev 类型..."
|
||
if [ -d "/sys/bus/pci/devices/0000:00:02.0/mdev_supported_types" ]; then
|
||
mdev_types=$(ls /sys/bus/pci/devices/0000:00:02.0/mdev_supported_types 2>/dev/null | wc -l)
|
||
if [ "$mdev_types" -gt 0 ]; then
|
||
echo -e " ✓ GVT-g 已启用,可用 Mdev 类型: $mdev_types 个"
|
||
echo
|
||
ls -1 /sys/bus/pci/devices/0000:00:02.0/mdev_supported_types
|
||
else
|
||
echo -e " ! GVT-g 未正确配置"
|
||
fi
|
||
else
|
||
echo -e " ! 未检测到 GVT-g 支持"
|
||
echo " 提示: 此 CPU 可能不支持 GVT-g 或未配置"
|
||
fi
|
||
echo
|
||
|
||
# 检查 kvmgt 模块(GVT-g 需要)
|
||
echo "5. 检查 kvmgt 模块(GVT-g)..."
|
||
if lsmod | grep -q kvmgt; then
|
||
echo -e " ✓ kvmgt 模块已加载(GVT-g 模式)"
|
||
else
|
||
echo " kvmgt 模块未加载(SR-IOV 模式或未配置 GVT-g)"
|
||
fi
|
||
echo
|
||
|
||
# 检查 i915 驱动参数
|
||
echo "6. 检查 i915 驱动参数..."
|
||
if [ -f "/sys/module/i915/parameters/enable_guc" ]; then
|
||
guc_value=$(cat /sys/module/i915/parameters/enable_guc)
|
||
if [ "$guc_value" = "3" ]; then
|
||
echo -e " ✓ i915.enable_guc = 3 (SR-IOV 模式)"
|
||
else
|
||
echo " i915.enable_guc = $guc_value"
|
||
fi
|
||
fi
|
||
|
||
if [ -f "/sys/module/i915/parameters/enable_gvt" ]; then
|
||
gvt_value=$(cat /sys/module/i915/parameters/enable_gvt)
|
||
if [ "$gvt_value" = "Y" ]; then
|
||
echo -e " ✓ i915.enable_gvt = Y (GVT-g 模式)"
|
||
else
|
||
echo " i915.enable_gvt = $gvt_value"
|
||
fi
|
||
fi
|
||
echo
|
||
|
||
# 总结
|
||
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||
echo " 检查完成"
|
||
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||
|
||
pause_function
|
||
}
|
||
|
||
# 移除核显虚拟化配置
|
||
igpu_remove() {
|
||
echo
|
||
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||
echo -e " 警告 - 移除核显虚拟化配置"
|
||
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||
echo
|
||
echo -e " 此操作将:"
|
||
echo " • 恢复 GRUB 配置为默认值"
|
||
echo " • 清理 /etc/modules 中的 VFIO 和 kvmgt 模块"
|
||
echo " • 删除 /etc/sysfs.conf 中的 VFs 配置"
|
||
echo " • 卸载 i915-sriov-dkms 驱动(如已安装)"
|
||
echo
|
||
echo -e " 注意:此操作不会自动重启系统"
|
||
echo
|
||
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||
|
||
if ! confirm_action "确认移除核显虚拟化配置"; then
|
||
echo "用户取消操作"
|
||
return 0
|
||
fi
|
||
|
||
# 恢复 GRUB 配置
|
||
echo "恢复 GRUB 配置..."
|
||
backup_file "/etc/default/grub"
|
||
|
||
# 移除所有核显虚拟化参数
|
||
sed -i 's/intel_iommu=on//g; s/iommu=pt//g; s/i915.enable_guc=3//g; s/i915.max_vfs=7//g; s/module_blacklist=xe//g; s/i915.enable_gvt=1//g; s/pcie_acs_override=downstream,multifunction//g' /etc/default/grub
|
||
|
||
# 清理多余空格
|
||
sed -i 's/ */ /g' /etc/default/grub
|
||
|
||
update-grub
|
||
echo -e " ✓ GRUB 配置已恢复"
|
||
|
||
# 清理 /etc/modules
|
||
echo "清理内核模块配置..."
|
||
backup_file "/etc/modules"
|
||
|
||
sed -i '/^vfio$/d; /^vfio_iommu_type1$/d; /^vfio_pci$/d; /^vfio_virqfd$/d; /^kvmgt$/d' /etc/modules
|
||
echo -e " ✓ 内核模块配置已清理"
|
||
|
||
# 清理 /etc/sysfs.conf
|
||
if [ -f "/etc/sysfs.conf" ]; then
|
||
echo "清理 sysfs 配置..."
|
||
backup_file "/etc/sysfs.conf"
|
||
sed -i '/sriov_numvfs/d' /etc/sysfs.conf
|
||
echo -e " ✓ sysfs 配置已清理"
|
||
fi
|
||
|
||
# 卸载 i915-sriov-dkms
|
||
echo "检查 i915-sriov-dkms 驱动..."
|
||
if dpkg -l | grep -q i915-sriov-dkms; then
|
||
echo "卸载 i915-sriov-dkms 驱动..."
|
||
dpkg -P i915-sriov-dkms || echo -e "${YELLOW}警告: 卸载驱动失败,可能需要手动处理${NC}"
|
||
echo -e "✓ 驱动已卸载"
|
||
else
|
||
echo "未安装 i915-sriov-dkms 驱动,跳过"
|
||
fi
|
||
|
||
# 更新 initramfs
|
||
echo "更新 initramfs..."
|
||
update-initramfs -u -k all
|
||
|
||
echo
|
||
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||
echo -e "✓ 核显虚拟化配置已移除"
|
||
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||
echo -e "提示: 请重启系统使更改生效"
|
||
|
||
if confirm_action "是否现在重启系统"; then
|
||
echo "正在重启系统..."
|
||
reboot
|
||
else
|
||
echo "请记得手动重启系统"
|
||
fi
|
||
}
|
||
|
||
# 核显高级功能菜单
|
||
igpu_management_menu() {
|
||
while true; do
|
||
clear
|
||
show_menu_header "核显虚拟化高级功能"
|
||
echo -e " ${RED}【危险警告】${NC} 核显虚拟化属于高危操作"
|
||
echo -e " 配置错误可能导致系统无法启动,请务必提前备份 GRUB 配置"
|
||
echo "${UI_DIVIDER}"
|
||
show_menu_option "1" "Intel 11-15代 SR-IOV 核显虚拟化"
|
||
echo -e " ${CYAN}支持:${NC} Rocket Lake, Alder Lake, Raptor Lake"
|
||
echo -e " ${CYAN}特性:${NC} 最多 7 个虚拟核显,性能较好"
|
||
show_menu_option "2" "Intel 6-10代 GVT-g 核显虚拟化"
|
||
echo -e " ${CYAN}支持:${NC} Skylake ~ Comet Lake"
|
||
echo -e " ${CYAN}特性:${NC} 最多 2-8 个虚拟核显(取决于型号)"
|
||
show_menu_option "3" "验证核显虚拟化状态"
|
||
echo -e " ${CYAN}检查:${NC} IOMMU、VFIO、SR-IOV/GVT-g 配置"
|
||
show_menu_option "4" "移除核显虚拟化配置"
|
||
echo -e " ${CYAN}恢复:${NC} 默认配置,移除所有核显虚拟化设置"
|
||
echo "${UI_DIVIDER}"
|
||
show_menu_option "" "GRUB 配置管理(强烈推荐使用)"
|
||
echo "${UI_DIVIDER}"
|
||
show_menu_option "5" "查看当前 GRUB 配置"
|
||
echo -e " ${CYAN}展示:${NC} 当前的 GRUB 引导参数和关键配置"
|
||
show_menu_option "6" "备份 GRUB 配置"
|
||
echo -e " ${CYAN}路径:${NC} /etc/pvetools9/backup/grub/"
|
||
show_menu_option "7" "查看 GRUB 备份列表"
|
||
show_menu_option "8" "恢复 GRUB 配置"
|
||
echo "${UI_DIVIDER}"
|
||
show_menu_option "0" "返回主菜单"
|
||
show_menu_footer
|
||
echo
|
||
read -p "请选择操作 [0-8]: " choice
|
||
|
||
case $choice in
|
||
1)
|
||
igpu_sriov_setup
|
||
;;
|
||
2)
|
||
igpu_gvtg_setup
|
||
;;
|
||
3)
|
||
igpu_verify
|
||
;;
|
||
4)
|
||
igpu_remove
|
||
;;
|
||
5)
|
||
show_grub_config
|
||
pause_function
|
||
;;
|
||
6)
|
||
echo
|
||
echo "请输入备份备注(例如:手动备份_测试):"
|
||
read -p "> " backup_note
|
||
backup_note=${backup_note:-"手动备份"}
|
||
backup_grub_with_note "$backup_note"
|
||
pause_function
|
||
;;
|
||
7)
|
||
list_grub_backups
|
||
pause_function
|
||
;;
|
||
8)
|
||
restore_grub_backup
|
||
;;
|
||
0)
|
||
echo "返回主菜单"
|
||
return 0
|
||
;;
|
||
*)
|
||
echo -e "无效的选择,请输入 0-8"
|
||
pause_function
|
||
;;
|
||
esac
|
||
done
|
||
}
|
||
#--------------核显虚拟化管理----------------
|
||
|
||
#---------PVE8/9添加ceph-squid源-----------
|
||
pve9_ceph() {
|
||
sver=`cat /etc/debian_version |awk -F"." '{print $1}'`
|
||
case "$sver" in
|
||
13 )
|
||
sver="trixie"
|
||
;;
|
||
12 )
|
||
sver="bookworm"
|
||
;;
|
||
* )
|
||
sver=""
|
||
;;
|
||
esac
|
||
if [ ! $sver ];then
|
||
log_error "版本不支持!"
|
||
pause_function
|
||
return
|
||
fi
|
||
|
||
log_info "ceph-squid目前仅支持PVE8和9!"
|
||
[[ ! -d /etc/apt/backup ]] && mkdir -p /etc/apt/backup
|
||
[[ ! -d /etc/apt/sources.list.d ]] && mkdir -p /etc/apt/sources.list.d
|
||
|
||
[[ -e /etc/apt/sources.list.d/ceph.sources ]] && mv /etc/apt/sources.list.d/ceph.sources /etc/apt/backup/ceph.sources.bak
|
||
[[ -e /etc/apt/sources.list.d/ceph.list ]] && mv /etc/apt/sources.list.d/ceph.list /etc/apt/backup/ceph.list.bak
|
||
|
||
[[ -e /usr/share/perl5/PVE/CLI/pveceph.pm ]] && cp -rf /usr/share/perl5/PVE/CLI/pveceph.pm /etc/apt/backup/pveceph.pm.bak
|
||
sed -i 's|http://download.proxmox.com|https://mirrors.tuna.tsinghua.edu.cn/proxmox|g' /usr/share/perl5/PVE/CLI/pveceph.pm
|
||
|
||
cat > /etc/apt/sources.list.d/ceph.list <<-EOF
|
||
deb https://mirrors.tuna.tsinghua.edu.cn/proxmox/debian/ceph-squid ${sver} no-subscription
|
||
EOF
|
||
log_success "添加ceph-squid源完成!"
|
||
}
|
||
#---------PVE8/9添加ceph-squid源-----------
|
||
|
||
#---------PVE7/8添加ceph-quincy源-----------
|
||
pve8_ceph() {
|
||
sver=`cat /etc/debian_version |awk -F"." '{print $1}'`
|
||
case "$sver" in
|
||
12 )
|
||
sver="bookworm"
|
||
;;
|
||
11 )
|
||
sver="bullseye"
|
||
;;
|
||
* )
|
||
sver=""
|
||
;;
|
||
esac
|
||
if [ ! $sver ];then
|
||
log_error "版本不支持!"
|
||
pause_function
|
||
return
|
||
fi
|
||
|
||
log_info "ceph-quincy目前仅支持PVE7和8!"
|
||
[[ ! -d /etc/apt/backup ]] && mkdir -p /etc/apt/backup
|
||
[[ ! -d /etc/apt/sources.list.d ]] && mkdir -p /etc/apt/sources.list.d
|
||
|
||
[[ -e /etc/apt/sources.list.d/ceph.sources ]] && mv /etc/apt/sources.list.d/ceph.sources /etc/apt/backup/ceph.sources.bak
|
||
[[ -e /etc/apt/sources.list.d/ceph.list ]] && mv /etc/apt/sources.list.d/ceph.list /etc/apt/backup/ceph.list.bak
|
||
|
||
[[ -e /usr/share/perl5/PVE/CLI/pveceph.pm ]] && cp -rf /usr/share/perl5/PVE/CLI/pveceph.pm /etc/apt/backup/pveceph.pm.bak
|
||
sed -i 's|http://download.proxmox.com|https://mirrors.tuna.tsinghua.edu.cn/proxmox|g' /usr/share/perl5/PVE/CLI/pveceph.pm
|
||
|
||
cat > /etc/apt/sources.list.d/ceph.list <<-EOF
|
||
deb https://mirrors.tuna.tsinghua.edu.cn/proxmox/debian/ceph-quincy ${sver} main
|
||
EOF
|
||
log_success "添加ceph-quincy源完成!"
|
||
}
|
||
#---------PVE7/8添加ceph-quincy源-----------
|
||
# 待办
|
||
#---------PVE7/8添加ceph-quincy源-----------
|
||
#---------PVE一键卸载ceph-----------
|
||
remove_ceph() {
|
||
log_warn "会卸载ceph,并删除所有ceph相关文件!"
|
||
|
||
systemctl stop ceph-mon.target && systemctl stop ceph-mgr.target && systemctl stop ceph-mds.target && systemctl stop ceph-osd.target
|
||
rm -rf /etc/systemd/system/ceph*
|
||
|
||
killall -9 ceph-mon ceph-mgr ceph-mds ceph-osd
|
||
rm -rf /var/lib/ceph/mon/* && rm -rf /var/lib/ceph/mgr/* && rm -rf /var/lib/ceph/mds/* && rm -rf /var/lib/ceph/osd/*
|
||
|
||
pveceph purge
|
||
|
||
apt purge -y ceph-mon ceph-osd ceph-mgr ceph-mds
|
||
apt purge -y ceph-base ceph-mgr-modules-core
|
||
|
||
rm -rf /etc/ceph && rm -rf /etc/pve/ceph.conf && rm -rf /etc/pve/priv/ceph.* && rm -rf /var/log/ceph && rm -rf /etc/pve/ceph && rm -rf /var/lib/ceph
|
||
|
||
[[ -e /etc/apt/sources.list.d/ceph.sources ]] && mv /etc/apt/sources.list.d/ceph.sources /etc/apt/backup/ceph.sources.bak
|
||
|
||
log_success "已成功卸载ceph."
|
||
}
|
||
#---------PVE一键卸载ceph-----------
|
||
|
||
#---------第三方小工具管理-----------
|
||
# 小工具配置
|
||
# FastPVE - PVE 虚拟机快速下载
|
||
fastpve_quick_download_menu() {
|
||
clear
|
||
show_banner
|
||
show_menu_header "PVE 虚拟机快速下载 (FastPVE)"
|
||
|
||
echo " FastPVE 由社区开发者 @kspeeder 维护,提供热门 PVE 虚拟机模板快速拉取能力。"
|
||
echo " 本功能将直接运行 FastPVE 官方脚本,请在执行前确保信任该来源。"
|
||
echo
|
||
echo " 项目地址: $FASTPVE_PROJECT_URL"
|
||
echo " 安装脚本: $FASTPVE_INSTALLER_URL"
|
||
echo
|
||
echo -e "${RED}⚠️ 重要提示:${NC} 这是第三方脚本,出现任何问题请前往 FastPVE 项目反馈,别找我喔~"
|
||
echo -e "${YELLOW} 我们只负责帮你下载并执行,后续操作和风险请自行承担。${NC}"
|
||
echo "${UI_DIVIDER}"
|
||
echo " 使用说明:"
|
||
echo " • FastPVE 会拉取独立菜单,按提示选择需要的虚拟机模板"
|
||
echo " • 需要互联网访问 GitHub(大陆环境自动优先使用镜像源)"
|
||
echo " • 本脚本仅负责下载并执行 FastPVE,具体操作由 FastPVE 完成"
|
||
echo "${UI_DIVIDER}"
|
||
|
||
read -p "是否立即运行 FastPVE 脚本?(y/N): " confirm
|
||
confirm=${confirm:-N}
|
||
if [[ "$confirm" != "y" && "$confirm" != "Y" ]]; then
|
||
log_info "已取消执行 FastPVE"
|
||
return 0
|
||
fi
|
||
|
||
local fastpve_url="$FASTPVE_INSTALLER_URL"
|
||
local fastpve_mirror_url="${GITHUB_MIRROR_PREFIX}${FASTPVE_INSTALLER_URL}"
|
||
local preferred_url="$fastpve_url"
|
||
local fallback_url="$fastpve_mirror_url"
|
||
local preferred_label="GitHub"
|
||
local fallback_label="加速镜像"
|
||
|
||
if detect_network_region; then
|
||
if [[ $USE_MIRROR_FOR_UPDATE -eq 1 ]]; then
|
||
preferred_url="$fastpve_mirror_url"
|
||
fallback_url="$fastpve_url"
|
||
preferred_label="加速镜像"
|
||
fallback_label="GitHub"
|
||
log_info "检测到中国大陆网络环境,优先使用 FastPVE 加速镜像下载"
|
||
else
|
||
if [[ -n "$USER_COUNTRY_CODE" ]]; then
|
||
log_info "检测到当前地区: $USER_COUNTRY_CODE,将通过 GitHub 下载 FastPVE"
|
||
else
|
||
log_info "网络检测成功,将通过 GitHub 下载 FastPVE"
|
||
fi
|
||
fi
|
||
else
|
||
log_warn "无法检测网络地区,默认使用 GitHub 下载 FastPVE"
|
||
fi
|
||
|
||
local -a download_cmd
|
||
local downloader_name=""
|
||
if command -v curl &> /dev/null; then
|
||
download_cmd=(curl -fsSL --connect-timeout 10 --max-time 60 -o)
|
||
downloader_name="curl"
|
||
elif command -v wget &> /dev/null; then
|
||
download_cmd=(wget -q -O)
|
||
downloader_name="wget"
|
||
else
|
||
log_error "未检测到 curl 或 wget,无法下载 FastPVE 脚本"
|
||
return 1
|
||
fi
|
||
|
||
local tmp_script
|
||
if ! tmp_script=$(mktemp /tmp/fastpve-install.XXXXXX.sh); then
|
||
log_error "无法创建临时文件,FastPVE 启动失败"
|
||
return 1
|
||
fi
|
||
|
||
log_info "使用 $preferred_label 下载 FastPVE 安装脚本 (下载器: $downloader_name)..."
|
||
if ! "${download_cmd[@]}" "$tmp_script" "$preferred_url"; then
|
||
log_warn "$preferred_label 下载失败,尝试改用 $fallback_label..."
|
||
: > "$tmp_script"
|
||
if ! "${download_cmd[@]}" "$tmp_script" "$fallback_url"; then
|
||
log_error "FastPVE 安装脚本下载失败,请检查网络或稍后重试"
|
||
rm -f "$tmp_script"
|
||
return 1
|
||
fi
|
||
fi
|
||
|
||
chmod +x "$tmp_script"
|
||
echo
|
||
log_step "FastPVE 脚本即将运行,请根据 FastPVE 菜单提示选择虚拟机模板"
|
||
echo "${UI_BORDER}"
|
||
sh "$tmp_script"
|
||
local run_status=$?
|
||
echo "${UI_BORDER}"
|
||
|
||
rm -f "$tmp_script"
|
||
|
||
if [[ $run_status -eq 0 ]]; then
|
||
log_success "FastPVE 虚拟机快速下载脚本执行完成"
|
||
else
|
||
log_error "FastPVE 脚本执行失败 (退出码: $run_status)"
|
||
fi
|
||
|
||
return $run_status
|
||
}
|
||
#---------FastPVE 虚拟机快速下载-----------
|
||
|
||
# 社区第三方工具集合提示
|
||
third_party_tools_menu() {
|
||
clear
|
||
show_menu_header "第三方工具集 (Community Scripts)"
|
||
|
||
echo " 这里推荐一个由社区维护的庞大脚本集合,覆盖 Proxmox 安装、容器/虚拟机模版、监控等各种高级玩法。"
|
||
echo
|
||
echo " 项目主页: https://community-scripts.github.io/ProxmoxVE/"
|
||
echo " GitHub 仓库: https://github.com/community-scripts/ProxmoxVE"
|
||
echo
|
||
echo -e "${RED}⚠️ 重要提示:${NC} 该工具集完全由第三方维护,与 PVE-Tools 项目无关。"
|
||
echo -e "${YELLOW} 如果脚本运行出现问题,请直接前往上述项目反馈,不要来找我喔~${NC}"
|
||
echo
|
||
echo " 使用建议:"
|
||
echo " • 全站为英文界面,可配合浏览器或翻译软件使用,中文用户建议提前准备。"
|
||
echo " • 网站中包含大量脚本和功能说明,建议按需阅读说明后再执行。"
|
||
echo " • 执行任何第三方脚本前,请务必备份关键配置并了解潜在风险。"
|
||
echo "${UI_DIVIDER}"
|
||
read -p "按任意键返回主菜单..." -n 1 _
|
||
echo
|
||
}
|
||
#---------社区第三方工具集合-----------
|
||
|
||
# PVE8 to PVE9 升级功能
|
||
pve8_to_pve9_upgrade() {
|
||
block_non_pve9_destructive "PVE 8.x 升级到 PVE 9.x" || return 1
|
||
log_step "开始 PVE 8.x 升级到 PVE 9.x"
|
||
|
||
# 检查当前 PVE 版本
|
||
local current_pve_version=$(pveversion | head -n1 | cut -d'/' -f2 | cut -d'-' -f1)
|
||
local major_version=$(echo $current_pve_version | cut -d'.' -f1)
|
||
|
||
if [[ "$major_version" != "8" ]]; then
|
||
log_error "当前 PVE 版本为 $current_pve_version,不是 PVE 8.x 版本,无法执行此升级"
|
||
log_info "PVE7 请先试用ISO或升级教程升级哦! :https://pve.proxmox.com/wiki/Upgrade_from_7_to_8"
|
||
log_tips "如果你已经是PVE 9.x了,你还来用这个脚本,敲你额头!"
|
||
return 1
|
||
fi
|
||
|
||
log_info "检测到当前 PVE 版本: $current_pve_version"
|
||
log_warn "即将开始 PVE 8.x 到 PVE 9.x 的升级流程"
|
||
log_warn "此过程不可逆,请确保已备份重要数据!"
|
||
log_warn "建议在升级前阅读详细原理与避坑指南:https://pve.u3u.icu/advanced/pve-upgrade"
|
||
log_warn "建议在升级前手动备份 /var/lib/pve-cluster/ 目录"
|
||
echo
|
||
log_warn "升级过程中请勿中断,确保有稳定的网络连接"
|
||
log_warn "升级完成后,系统将自动重启以应用更改"
|
||
log_warn "如果脚本出现升级问题,请及时联系作者或参照官方文档解决。"
|
||
echo
|
||
log_info "推荐使用我的新项目嘿嘿,一个独立的升级AGENT: https://github.com/Mapleawaa/PVE-8-Upgrage-helper"
|
||
|
||
# 确认用户要继续执行升级
|
||
echo "您确定要继续升级吗?本次任务执行以下操作:"
|
||
echo " 1. 安装 pve8to9 检查工具"
|
||
echo " 2. 运行升级前检查"
|
||
echo " 3. 更新软件源到 Debian 13 (Trixie)"
|
||
echo " 4. 执行系统升级"
|
||
echo " 5. 重启系统以应用更改"
|
||
echo
|
||
echo "注意:升级过程中可能会遇到一些警告或错误,请根据提示进行处理!脚本无法处理故障提示!(脚本只能把提示扔给你..) )"
|
||
read -p "输入 'yesido' 确认继续,其他任意键取消: " confirm
|
||
if [[ "$confirm" != "yesido" ]]; then
|
||
log_info "已取消升级操作"
|
||
return 0
|
||
fi
|
||
|
||
# 1. 更新当前系统到最新 PVE 8.x 版本
|
||
log_info "更新当前系统到最新 PVE 8.x 版本..."
|
||
if ! apt update && apt dist-upgrade -y; then
|
||
log_error "更新 PVE 8.x 到最新版本失败了,请检查网络连接或源配置,或者前往作者的GitHub反馈issue.."
|
||
return 1
|
||
fi
|
||
|
||
# 再次检查当前版本
|
||
current_pve_version=$(pveversion | head -n1 | cut -d'/' -f2 | cut -d'-' -f1)
|
||
log_info "更新后 PVE 版本: ${GREEN}$current_pve_version${NC}"
|
||
|
||
# PVE8.4 自带这个包,此处无需检查安装,apt 源无此包会报错。
|
||
# 2. 安装和运行 pve8to9 检查工具
|
||
# log_info "安装 pve8to9 升级检查工具..."
|
||
# if ! apt install -y pve8to9; then
|
||
# log_warn "pve8to9 工具安装失败,尝试手动安装..."
|
||
# # 尝试手动添加 PVE 8 仓库安装 pve8to9
|
||
# if ! apt install -y pve8to9; then
|
||
# log_error "无法安装 pve8to9 检查工具,奇怪!请检查网络连接或源配置,或者前往作者的GitHub反馈issue.."
|
||
# return 1
|
||
# fi
|
||
# fi
|
||
|
||
log_info "运行升级前检查..."
|
||
echo -e "${CYAN}pve8to9 检查结果:${NC}"
|
||
# 运行 pve8to9 检查,但不直接退出,而是捕获输出并分析
|
||
echo -e "检查结果会保存到 /tmp/pve8to9_check.log 文件中,如出现故障建议查看该文件以获取详细信息"
|
||
echo -e "再次提示,脚本只能做到把错误扔给你,无法修复问题,请根据提示自行解决(或前往作者issue反馈问题)..."
|
||
local check_result=$(pve8to9 | tee /tmp/pve8to9_check.log)
|
||
echo "$check_result"
|
||
|
||
# 检查是否有 FAIL 标记(这意味着有严重错误需要修复)
|
||
if echo "$check_result" | grep -E -i "FAIL" > /dev/null; then
|
||
log_error "pve8to9 检查发现严重错误!! 一般是软件包冲突或是其他报错!建议修复后再进行升级!"
|
||
echo -e "${YELLOW}升级检查结果详情:${NC}"
|
||
cat /tmp/pve8to9_check.log
|
||
read -p "您确定要忽略这些错误并继续升级吗?这不是在开玩笑!(y/N): " force_upgrade
|
||
if [[ "$force_upgrade" != "y" && "$force_upgrade" != "Y" ]]; then
|
||
log_info "由于存在严重错误,已取消升级操作...返回主界面"
|
||
return 1
|
||
fi
|
||
else
|
||
log_success "pve8to9 检查通过,没有发现严重错误,太好了!"
|
||
|
||
# 检查是否有 WARNING 标记
|
||
if echo "$check_result" | grep -E -i "WARN" > /dev/null; then
|
||
log_warn "pve8to9 检查发现一些警告信息,请查看以上详情并根据需要处理。(有些可能是软件包没升级上去,不是关键软件包可以无视先升级喔)"
|
||
read -p "是否继续升级?(Y/n): " continue_check
|
||
if [[ "$continue_check" == "n" || "$continue_check" == "N" ]]; then
|
||
log_info "已取消升级操作"
|
||
return 0
|
||
fi
|
||
fi
|
||
fi
|
||
|
||
# 3. 安装 CPU 微码(如果提示需要)
|
||
log_info "检查是否需要安装 CPU 微码..."
|
||
if command -v lscpu &> /dev/null; then
|
||
local cpu_vendor=$(lscpu | grep "Vendor ID" | awk '{print $3}')
|
||
if [[ "$cpu_vendor" == "GenuineIntel" ]]; then
|
||
log_info "检测到 Intel CPU,安装 Intel 微码..."
|
||
apt install -y intel-microcode
|
||
elif [[ "$cpu_vendor" == "AuthenticAMD" ]]; then
|
||
log_info "检测到 AMD CPU,安装 AMD 微码..."
|
||
apt install -y amd64-microcode
|
||
fi
|
||
fi
|
||
|
||
# 4. 检查当前启动方式并更新引导配置
|
||
log_info "检查系统启动方式..."
|
||
local boot_method="unknown"
|
||
if [[ -d "/boot/efi" ]]; then
|
||
boot_method="efi"
|
||
log_info "检测到 EFI 启动模式"
|
||
# 为 EFI 系统配置 GRUB
|
||
echo 'grub-efi-amd64 grub2/force_efi_extra_removable boolean true' | debconf-set-selections -v -u
|
||
else
|
||
boot_method="bios"
|
||
log_info "检测到 BIOS 启动模式"
|
||
log_tips "怎么还在用BIOS启用呀?建议升级到UEFI启动方式,提升系统兼容性和安全性"
|
||
fi
|
||
|
||
# 5. 备份当前源文件
|
||
log_info "备份当前源文件..."
|
||
local backup_dir="/etc/pve-tools-9-bak"
|
||
mkdir -p "$backup_dir"
|
||
local timestamp=$(date +%Y%m%d_%H%M%S)
|
||
|
||
# 备份各种源文件
|
||
if [[ -f "/etc/apt/sources.list" ]]; then
|
||
cp /etc/apt/sources.list "${backup_dir}/sources.list.backup.${timestamp}"
|
||
fi
|
||
|
||
if [[ -f "/etc/apt/sources.list.d/pve-enterprise.list" ]]; then
|
||
cp /etc/apt/sources.list.d/pve-enterprise.list "${backup_dir}/pve-enterprise.list.backup.${timestamp}"
|
||
fi
|
||
|
||
# 备份 PVE 核心数据库
|
||
log_info "备份 PVE 核心数据库..."
|
||
if [[ -d "/var/lib/pve-cluster" ]]; then
|
||
cp -r /var/lib/pve-cluster "${backup_dir}/pve-cluster.backup.${timestamp}"
|
||
log_success "核心数据库已备份至 ${backup_dir}"
|
||
fi
|
||
|
||
# 6. 更新源到 Debian 13 (Trixie) 并添加 PVE 9.x 源
|
||
log_info "更新软件源到 Debian 13 (Trixie)..."
|
||
|
||
# 将所有 bookworm 源替换为 trixie
|
||
log_step "替换 sources.list 和 pve-enterprise.list 中的 bookworm 为 trixie"
|
||
sed -i 's/bookworm/trixie/g' /etc/apt/sources.list 2>/dev/null || true
|
||
sed -i 's/bookworm/trixie/g' /etc/apt/sources.list.d/pve-enterprise.list 2>/dev/null || true
|
||
|
||
# 创建 PVE 9.x 的 sources 配置文件
|
||
log_step "创建 PVE 9.x 的 sources 配置文件..."
|
||
cat > /etc/apt/sources.list.d/proxmox.sources << EOF
|
||
Types: deb
|
||
URIs: http://download.proxmox.com/debian/pve
|
||
Suites: trixie
|
||
Components: pve-no-subscription
|
||
Signed-By: /usr/share/keyrings/proxmox-archive-keyring.gpg
|
||
EOF
|
||
|
||
# 创建 Ceph Squid 源配置文件
|
||
log_step "创建 Ceph Squid 源配置文件..."
|
||
cat > /etc/apt/sources.list.d/ceph.sources << EOF
|
||
Types: deb
|
||
URIs: http://download.proxmox.com/debian/ceph-squid
|
||
Suites: trixie
|
||
Components: no-subscription
|
||
Signed-By: /usr/share/keyrings/proxmox-archive-keyring.gpg
|
||
EOF
|
||
|
||
log_info "软件源已更新到 Debian 13 (Trixie) 和 PVE 9.x 配置"
|
||
|
||
# 7. 再次运行升级前检查确认源更新无误
|
||
log_info "再次运行 pve8to9 检查以确认源配置..."
|
||
local final_check_result=$(pve8to9)
|
||
if echo "$final_check_result" | grep -E -i "FAIL" > /dev/null; then
|
||
log_error "pve8to9 最终检查发现错误,请手动检查源配置后再继续"
|
||
echo "$final_check_result"
|
||
return 1
|
||
else
|
||
log_success "源更新配置检查通过"
|
||
fi
|
||
|
||
# 8. 更新包列表并开始升级
|
||
log_info "更新包列表..."
|
||
if ! apt update; then
|
||
log_error "更新包列表失败,请检查网络连接和源配置"
|
||
return 1
|
||
fi
|
||
|
||
log_info "开始 PVE 9.x 升级过程,这可能需要较长时间..."
|
||
log_warn "如果你正在使用Web UI内置的终端,建议改用SSH连接以防止连接中断"
|
||
echo -e "${YELLOW}升级过程中可能会出现多个提示,通常按回车键或选择默认选项即可${NC}"
|
||
|
||
# 使用非交互模式升级,自动回答问题
|
||
DEBIAN_FRONTEND=noninteractive apt dist-upgrade -y \
|
||
-o Dpkg::Options::="--force-confdef" \
|
||
-o Dpkg::Options::="--force-confold"
|
||
|
||
if [[ $? -ne 0 ]]; then
|
||
log_error "PVE 升级过程失败,请查看日志并手动处理...如果是在看不明白可以试试问AI或者提交issue"
|
||
return 1
|
||
fi
|
||
|
||
# 9. 清理无用包
|
||
log_info "清理无用软件包..."
|
||
apt autoremove -y
|
||
apt autoclean
|
||
|
||
# 10. 检查升级结果
|
||
local new_pve_version=$(pveversion | head -n1 | cut -d'/' -f2 | cut -d'-' -f1)
|
||
local new_major_version=$(echo $new_pve_version | cut -d'.' -f1)
|
||
|
||
if [[ "$new_major_version" == "9" ]]; then
|
||
log_success "(撒花)PVE 升级成功!新的 PVE 版本: ${GREEN}$new_pve_version${NC}"
|
||
|
||
# 运行最终的升级后检查
|
||
log_info "运行升级后检查..."
|
||
pve8to9 2>/dev/null || true
|
||
|
||
log_info "系统将在 30 秒后重启以完成升级..."
|
||
log_success "如果一切顺利,重启后就能体验到PVE9啦!"
|
||
log_warn "如果升级后出现问题,例如卡内核卡Grub,请先使用LiveCD抢修内核,提取日志文件后联系作者寻求帮助"
|
||
echo -e "${YELLOW}按 Ctrl+C 可取消自动重启${NC}"
|
||
sleep 30
|
||
|
||
# 重启系统以完成升级
|
||
log_info "正在重启系统以完成 PVE 9.x 升级..."
|
||
reboot
|
||
else
|
||
log_error "升级完成后检查发现,PVE 版本仍为 $new_pve_version,升级可能未完全成功"
|
||
log_tips "请手动检查系统状态,并确认是否需要重试升级"
|
||
return 1
|
||
fi
|
||
}
|
||
|
||
# 显示系统信息
|
||
show_system_info() {
|
||
log_step "为您展示系统运行状况"
|
||
echo
|
||
echo "${UI_BORDER}"
|
||
echo -e " ${H1}系统信息概览${NC}"
|
||
echo "${UI_DIVIDER}"
|
||
echo -e " ${PRIMARY}PVE 版本:${NC} $(pveversion | head -n1)"
|
||
echo -e " ${PRIMARY}内核版本:${NC} $(uname -r)"
|
||
echo -e " ${PRIMARY}CPU 信息:${NC} $(lscpu | grep 'Model name' | sed 's/Model name:[ \t]*//')"
|
||
echo -e " ${PRIMARY}CPU 核心:${NC} $(nproc) 核心"
|
||
echo -e " ${PRIMARY}系统架构:${NC} $(dpkg --print-architecture)"
|
||
echo -e " ${PRIMARY}系统启动:${NC} $(uptime -p | sed 's/up //')"
|
||
echo -e " ${PRIMARY}引导类型:${NC} $(if [ -d /sys/firmware/efi ]; then echo UEFI; else echo BIOS; fi)"
|
||
echo -e " ${PRIMARY}系统负载:${NC} $(uptime | awk -F'load average:' '{print $2}')"
|
||
echo -e " ${PRIMARY}内存使用:${NC} $(free -h | grep Mem | awk '{print $3"/"$2}')"
|
||
echo -e " ${PRIMARY}磁盘使用:${NC}"
|
||
df -h | grep -E '^/dev/' | awk '{print " "$1" "$3"/"$2" ("$5")"}'
|
||
echo -e " ${PRIMARY}网络接口:${NC}"
|
||
ip -br addr show | awk '{print " "$1" "$3}'
|
||
echo -e " ${PRIMARY}当前时间:${NC} $(date)"
|
||
echo "${UI_FOOTER}"
|
||
}
|
||
|
||
# 主菜单
|
||
show_menu() {
|
||
show_banner
|
||
show_menu_option "" "请选择您需要的功能:"
|
||
show_menu_option "1" "系统优化 ${CYAN}(订阅弹窗/温度监控/电源模式)${NC}"
|
||
show_menu_option "2" "软件源与更新 ${CYAN}(换源/更新/PVE8→9升级)${NC}"
|
||
show_menu_option "3" "启动与内核 ${CYAN}(内核切换/更新/清理)${NC}"
|
||
show_menu_option "4" "直通与显卡 ${CYAN}(核显/NVIDIA/硬件直通)${NC}"
|
||
show_menu_option "5" "虚拟机与容器 ${CYAN}(FastPVE/第三方工具)${NC}"
|
||
show_menu_option "6" "存储与硬盘 ${CYAN}(Local合并/Ceph/休眠)${NC}"
|
||
show_menu_option "7" "工具与关于 ${CYAN}(系统信息/救砖//)${NC}"
|
||
echo "$UI_DIVIDER"
|
||
show_menu_option "0" "${RED}退出脚本${NC}"
|
||
show_menu_footer
|
||
|
||
# 贴吧老梗随机轮播 (卡吧特供版)
|
||
local tips=(
|
||
"装机前记得先吃饭,不然修电脑修到低血糖"
|
||
"一定要在中午刷机,因为早晚会出事"
|
||
"三千预算进卡吧,加钱加到九万八"
|
||
"八核E5洋垃圾,一核有难七核围观"
|
||
"GTX690战术核显卡,一发摧毁一个航母战斗群"
|
||
"遇事不决,重启解决;重启不行,重装系统"
|
||
"勤备份,保平安;删库跑路,牢底坐穿"
|
||
"一入卡吧深似海,从此钱包是路人"
|
||
"RGB能提升200%的性能,不信你试试"
|
||
"只要我不看日志,报错就不存在"
|
||
"高端的服务器,往往只需要最朴素的重启方式"
|
||
"硬盘有价,数据无价,请谨慎操作"
|
||
"千万不要在生产环境测试脚本,除非你想被祭天"
|
||
"刷机有风险,变砖请自重,虽然PVE很难刷砖"
|
||
"配置千万条,安全第一条,操作不规范,亲人两行泪"
|
||
"玄学时刻:刷机前洗手,成功率提升50%"
|
||
"四路泰坦刷贴吧,流畅度提升明显"
|
||
"什么?你问我电源多少瓦?能亮就行!"
|
||
"散热全靠吼,除尘全靠抖"
|
||
"矿卡锻炼身体,新卡锻炼钱包"
|
||
"图吧捡垃圾,五十包邮解君愁"
|
||
"开机卡logo?大力出奇迹,拍一下就好了"
|
||
"超频一时爽,缩缸火葬场"
|
||
"水冷漏液不要慌,先拍照发个朋友圈"
|
||
"魔改U配寨板,翻车是日常,点亮算惊喜"
|
||
"牙膏厂挤牙膏,AMD,YES!"
|
||
"双路E5开网吧,电表倒转笑哈哈"
|
||
"捡垃圾要趁早,晚了都是传家宝"
|
||
"亮机卡才是真传家宝,核显都是异端"
|
||
"跑分没赢过,体验没输过"
|
||
"硅脂不要钱,就往死里涂"
|
||
"装机三大神器:筷子、手电筒、扎带"
|
||
"先点菜吧,不然跑分的时候没东西吃"
|
||
"二手东七天机,垃圾佬的圣诞节"
|
||
"战术核弹已就位,准备烤机!"
|
||
"散热器用原装?你是AMD原教旨主义者吗?"
|
||
"RGB风扇装反了?不,那是故意的光污染"
|
||
"别问,问就是加钱上3090"
|
||
"电费?什么电费?我都是去星巴克蹭电的"
|
||
"理论性能翻一倍,电费账单翻两倍"
|
||
"二手矿龙传三代,人走板卡它还在"
|
||
"玄学调参:BIOS里随便改几个数,万一稳了呢"
|
||
"垃圾佬的浪漫:用最少的钱,跑最多的分"
|
||
"蓝屏?那是微软给你的思考人生的时间"
|
||
"卡巴基佬烧友,图吧垃圾佬,我们都有光明的未来"
|
||
"点亮了没?没有。再等等,电容在充电"
|
||
"这U温度怎么这么高?硅脂还没干呢"
|
||
"不要怂,就是超,缩了就当是降压降温用"
|
||
"开机箱侧板,被动散热大师"
|
||
"论斤买的服务器内存,香是真的香,吵也是真的吵"
|
||
"别问机箱多少钱,鞋盒赛高,通风又好还便宜"
|
||
"显卡啸叫?那是高端显卡在唱歌给你听"
|
||
"多盘位NAS?不,那是捡来的硬盘别墅"
|
||
"电源必须传家宝,矿龙一响,黄金万两"
|
||
"降压降频用矿卡,温度和噪音都沉默了"
|
||
"风冷压i9?只要不开机,它就永远不热"
|
||
"小黄鱼蹲守口诀:早蹲、晚蹲、凌晨三点继续蹲"
|
||
"魔改QLC刷SLC缓存,用寿命换速度的赌徒艺术"
|
||
"开机自检一分钟?那是给你的开机仪式感"
|
||
"‘又不是不能用’,垃圾佬的终极哲学"
|
||
"集显战3A,720P最低画质也是风景"
|
||
"线材理个啥?盖上侧板就是理好了"
|
||
"洋垃圾平台开机先听交响乐:风扇全速起飞"
|
||
"捡垃圾三境界:能用,够用,战未来"
|
||
"‘这价格还要啥自行车’,下单前的自我催眠"
|
||
"双路主板搭单U,另一半座位留给未来的梦想"
|
||
"固态硬盘用清零盘,数据坐过山车,刺激"
|
||
"‘完美下车’——垃圾佬的最高赞誉,通常管三天"
|
||
"导热垫用久了出油?那是散热器在流泪"
|
||
"显卡高温?下个冬天的主机暖气就有了"
|
||
"老至强配RECC内存,电表倒转不是梦"
|
||
"刷鸡血BIOS,让老U回光返照再战三年"
|
||
"开机箱用风扇直吹,物理外挂,最为致命"
|
||
"‘五十包邮解君愁’——垃圾佬的接头暗号"
|
||
"网吧倒闭盘,写入量?不要在意那些细节"
|
||
"‘点不亮就当手办’,垃圾佬的事后安慰剂"
|
||
"用PCIe转接卡上NVMe,老主板焕发第N春"
|
||
"散热器用钉子固定,垃圾佬的硬核改装"
|
||
"“这电容鼓了?敲平了接着用”"
|
||
"二手电源带核弹,宿舍跳闸的罪魁祸首"
|
||
"用牙膏代替硅脂?极限操作,仅供瞻仰"
|
||
"“跑个分看看” —— 垃圾佬的赛博晒娃"
|
||
"机箱里养猫?那是不请自来的蒲公英培育基地"
|
||
"“又不是不能用”的终点是“确实不能用了”"
|
||
"图吧真传:一百预算进图吧,学校门口开网吧"
|
||
)
|
||
local random_index=$((RANDOM % ${#tips[@]}))
|
||
echo -e " ${YELLOW} 小贴士:${tips[$random_index]}${NC}"
|
||
echo
|
||
echo -ne " ${PRIMARY}请输入您的选择 [0-7]: ${NC}"
|
||
}
|
||
|
||
# 应急救砖工具箱菜单
|
||
show_menu_rescue() {
|
||
while true; do
|
||
clear
|
||
show_menu_header "应急救砖工具箱"
|
||
echo -e "${RED}警告:本工具箱用于修复因误操作导致的系统问题,请谨慎使用!${NC}"
|
||
echo
|
||
show_menu_option "1" "恢复官方 Web UI 文件 (重装 pve-manager / proxmox-widget-toolkit)"
|
||
show_menu_option "2" "恢复官方 pve-qemu-kvm (修复修改版 QEMU 问题)"
|
||
show_menu_option "3" "清理驱动黑名单 (i915/snd_hda_intel)"
|
||
show_menu_option "0" "返回主菜单"
|
||
show_menu_footer
|
||
read -p "请选择操作 [0-3]: " choice
|
||
case $choice in
|
||
1) restore_proxmoxlib ;;
|
||
2) restore_qemu_kvm ;;
|
||
3)
|
||
if confirm_action "确定要清理显卡和声卡驱动的黑名单设置吗?"; then
|
||
log_info "正在清理黑名单配置..."
|
||
sed -i '/blacklist i915/d' /etc/modprobe.d/pve-blacklist.conf
|
||
sed -i '/blacklist snd_hda_intel/d' /etc/modprobe.d/pve-blacklist.conf
|
||
sed -i '/blacklist snd_hda_codec_hdmi/d' /etc/modprobe.d/pve-blacklist.conf
|
||
log_info "正在更新 initramfs..."
|
||
update-initramfs -u -k all
|
||
log_success "黑名单清理完成,请重启系统"
|
||
fi
|
||
;;
|
||
0) return ;;
|
||
*) log_error "无效选择" ;;
|
||
esac
|
||
pause_function
|
||
done
|
||
}
|
||
|
||
# 二级菜单:系统优化
|
||
menu_optimization() {
|
||
while true; do
|
||
clear
|
||
show_menu_header "系统优化"
|
||
show_menu_option "1" "删除订阅弹窗"
|
||
show_menu_option "2" "温度监控管理 ${CYAN}(CPU/硬盘监控设置)${NC}"
|
||
show_menu_option "3" "CPU 电源模式配置"
|
||
show_menu_option "4" "${MAGENTA}一键优化 (换源+删弹窗+更新)${NC}"
|
||
show_menu_option "5" "配置邮件通知 ${CYAN}(SMTP/Postfix)${NC}"
|
||
echo "$UI_DIVIDER"
|
||
show_menu_option "0" "返回主菜单"
|
||
show_menu_footer
|
||
read -p "请选择操作 [0-5]: " choice
|
||
case $choice in
|
||
1) remove_subscription_popup ;;
|
||
2) temp_monitoring_menu ;;
|
||
3) cpupower ;;
|
||
4) quick_setup ;;
|
||
5) pve_mail_notification_setup ;;
|
||
0) return ;;
|
||
*) log_error "无效选择" ;;
|
||
esac
|
||
pause_function
|
||
done
|
||
}
|
||
|
||
# 二级菜单:软件源与更新
|
||
menu_sources_updates() {
|
||
while true; do
|
||
clear
|
||
show_menu_header "软件源与更新"
|
||
show_menu_option "1" "更换软件源"
|
||
show_menu_option "2" "更新系统软件包"
|
||
show_menu_option "3" "${YELLOW}PVE 8.x 升级到 PVE 9.x${NC}"
|
||
echo "$UI_DIVIDER"
|
||
show_menu_option "0" "返回主菜单"
|
||
show_menu_footer
|
||
read -p "请选择操作 [0-3]: " choice
|
||
case $choice in
|
||
1) change_sources ;;
|
||
2) update_system ;;
|
||
3) pve8_to_pve9_upgrade ;;
|
||
0) return ;;
|
||
*) log_error "无效选择" ;;
|
||
esac
|
||
pause_function
|
||
done
|
||
}
|
||
|
||
# 二级菜单:启动与内核
|
||
menu_boot_kernel() {
|
||
while true; do
|
||
clear
|
||
show_menu_header "启动与内核"
|
||
show_menu_option "1" "内核管理 ${CYAN}(内核切换/更新/清理)${NC}"
|
||
show_menu_option "2" "查看/备份 GRUB 配置"
|
||
echo "$UI_DIVIDER"
|
||
show_menu_option "0" "返回主菜单"
|
||
show_menu_footer
|
||
read -p "请选择操作 [0-2]: " choice
|
||
case $choice in
|
||
1) kernel_management_menu ;;
|
||
2)
|
||
while true; do
|
||
clear
|
||
show_menu_header "GRUB 配置管理"
|
||
show_menu_option "1" "查看当前 GRUB 配置"
|
||
show_menu_option "2" "备份 GRUB 配置"
|
||
show_menu_option "3" "查看备份列表"
|
||
show_menu_option "4" "恢复 GRUB 备份"
|
||
show_menu_option "0" "返回上级菜单"
|
||
show_menu_footer
|
||
read -p "请选择操作 [0-4]: " grub_choice
|
||
case $grub_choice in
|
||
1) show_grub_config; pause_function ;;
|
||
2)
|
||
echo "请输入备份备注:"
|
||
read -p "> " note
|
||
backup_grub_with_note "${note:-手动备份}"
|
||
pause_function
|
||
;;
|
||
3) list_grub_backups; pause_function ;;
|
||
4) restore_grub_backup ;;
|
||
0) break ;;
|
||
*) log_error "无效选择" ;;
|
||
esac
|
||
done
|
||
;;
|
||
0) return ;;
|
||
*) log_error "无效选择" ;;
|
||
esac
|
||
pause_function
|
||
done
|
||
}
|
||
|
||
# 二级菜单:直通与显卡
|
||
menu_gpu_passthrough() {
|
||
while true; do
|
||
clear
|
||
show_menu_header "直通与显卡"
|
||
show_menu_option "1" "Intel 核显虚拟化管理 (SR-IOV/GVT-g)"
|
||
show_menu_option "2" "Intel 核显直通配置 (修改版 QEMU)"
|
||
show_menu_option "3" "NVIDIA 显卡直通/虚拟化 (开发中)"
|
||
show_menu_option "4" "硬件直通一键配置 (IOMMU)"
|
||
show_menu_option "5" "磁盘/控制器直通 (RDM/PCIe/NVMe)"
|
||
show_menu_option "0" "返回主菜单"
|
||
show_menu_footer
|
||
read -p "请选择操作 [0-5]: " choice
|
||
case $choice in
|
||
1) igpu_management_menu ;;
|
||
2) intel_gpu_passthrough ;;
|
||
3) nvidia_gpu_management_menu ;;
|
||
4) hw_passth ;;
|
||
5) menu_disk_controller_passthrough ;;
|
||
0) return ;;
|
||
*) log_error "无效选择" ;;
|
||
esac
|
||
pause_function
|
||
done
|
||
}
|
||
|
||
# 虚拟机/容器定时开关机管理
|
||
manage_vm_schedule() {
|
||
while true; do
|
||
clear
|
||
show_menu_header "虚拟机/容器定时开关机"
|
||
echo -e "${YELLOW}当前配置的任务:${NC}"
|
||
if [ -f "/etc/cron.d/pve-tools-schedule" ]; then
|
||
grep -E "^[^#]" /etc/cron.d/pve-tools-schedule | sed 's/root \/usr\/sbin\///g'
|
||
else
|
||
echo " 暂无定时任务"
|
||
fi
|
||
echo -e "${UI_DIVIDER}"
|
||
|
||
echo -e "${BLUE}可用虚拟机 (QM):${NC}"
|
||
qm list 2>/dev/null | awk 'NR>1 {printf " ID: %-8s Name: %-20s Status: %s\n", $1, $2, $3}' || echo " 未发现虚拟机"
|
||
echo -e "${BLUE}可用容器 (PCT):${NC}"
|
||
pct list 2>/dev/null | awk 'NR>1 {printf " ID: %-8s Name: %-20s Status: %s\n", $1, $4, $2}' || echo " 未发现容器"
|
||
echo -e "${UI_DIVIDER}"
|
||
|
||
read -p "请输入要操作的 ID (返回请输入 0): " target_id
|
||
target_id=${target_id:-0}
|
||
if [[ "$target_id" == "0" ]]; then
|
||
return
|
||
fi
|
||
|
||
local cmd=""
|
||
if qm status "$target_id" >/dev/null 2>&1; then
|
||
cmd="qm"
|
||
elif pct status "$target_id" >/dev/null 2>&1; then
|
||
cmd="pct"
|
||
else
|
||
log_error "无效的 ID: $target_id"
|
||
pause_function
|
||
continue
|
||
fi
|
||
|
||
echo -e "${CYAN}正在配置 $cmd $target_id${NC}"
|
||
show_menu_option "1" "设置/修改定时任务"
|
||
show_menu_option "2" "删除定时任务"
|
||
show_menu_option "0" "取消"
|
||
read -p "请选择操作 [0-2]: " sub_choice
|
||
|
||
case $sub_choice in
|
||
1)
|
||
read -p "请输入开机时间 (格式 HH:MM, 如 07:00, 直接回车跳过): " start_time
|
||
read -p "请输入关机时间 (格式 HH:MM, 如 00:00, 直接回车跳过): " stop_time
|
||
|
||
local cron_content=""
|
||
if [[ -n "$start_time" ]]; then
|
||
if [[ "$start_time" =~ ^([0-1]?[0-9]|2[0-3]):([0-5][0-9])$ ]]; then
|
||
local hour=${BASH_REMATCH[1]}
|
||
local min=${BASH_REMATCH[2]}
|
||
min=$((10#$min))
|
||
hour=$((10#$hour))
|
||
cron_content+="$min $hour * * * root /usr/sbin/$cmd start $target_id >/dev/null 2>&1\n"
|
||
else
|
||
log_error "开机时间格式错误: $start_time"
|
||
fi
|
||
fi
|
||
|
||
if [[ -n "$stop_time" ]]; then
|
||
if [[ "$stop_time" =~ ^([0-1]?[0-9]|2[0-3]):([0-5][0-9])$ ]]; then
|
||
local hour=${BASH_REMATCH[1]}
|
||
local min=${BASH_REMATCH[2]}
|
||
min=$((10#$min))
|
||
hour=$((10#$hour))
|
||
cron_content+="$min $hour * * * root /usr/sbin/$cmd stop $target_id >/dev/null 2>&1"
|
||
else
|
||
log_error "关机时间格式错误: $stop_time"
|
||
fi
|
||
fi
|
||
|
||
if [[ -n "$cron_content" ]]; then
|
||
apply_block "/etc/cron.d/pve-tools-schedule" "SCHEDULE_$target_id" "$(echo -e "$cron_content")"
|
||
log_success "ID $target_id 的定时任务已更新"
|
||
systemctl restart cron 2>/dev/null || service cron restart 2>/dev/null
|
||
else
|
||
log_warn "未设置任何有效时间,操作取消"
|
||
fi
|
||
;;
|
||
2)
|
||
remove_block "/etc/cron.d/pve-tools-schedule" "SCHEDULE_$target_id"
|
||
log_success "ID $target_id 的定时任务已删除"
|
||
systemctl restart cron 2>/dev/null || service cron restart 2>/dev/null
|
||
;;
|
||
0)
|
||
continue
|
||
;;
|
||
*)
|
||
log_error "无效选择"
|
||
;;
|
||
esac
|
||
pause_function
|
||
done
|
||
}
|
||
|
||
img_bytes_to_human() {
|
||
local bytes="$1"
|
||
if [[ -z "$bytes" || ! "$bytes" =~ ^[0-9]+$ ]]; then
|
||
echo "?"
|
||
return 0
|
||
fi
|
||
awk -v b="$bytes" 'BEGIN{
|
||
split("B KB MB GB TB PB", u, " ");
|
||
i=1; x=b;
|
||
while (x>=1024 && i<6) {x/=1024; i++}
|
||
if (i==1) printf "%d%s", b, u[i];
|
||
else printf "%.1f%s", x, u[i];
|
||
}'
|
||
}
|
||
|
||
img_discover_img_files() {
|
||
local roots=("/root" "/var/lib/vz/template/iso" "/home")
|
||
local root
|
||
for root in "${roots[@]}"; do
|
||
if [[ -d "$root" ]]; then
|
||
find "$root" -xdev -type f \( -iname '*.img' \) -printf '%p|%s|%TY-%Tm-%Td %TH:%TM\n' 2>/dev/null || true
|
||
fi
|
||
done
|
||
}
|
||
|
||
img_select_img_file() {
|
||
local files
|
||
files="$(img_discover_img_files)"
|
||
if [[ -z "$files" ]]; then
|
||
log_error "未发现 .img 文件"
|
||
log_tips "已扫描目录:/root、/var/lib/vz/template/iso、/home"
|
||
return 1
|
||
fi
|
||
|
||
{
|
||
echo -e "${CYAN}已发现 .img 文件:${NC}"
|
||
echo "$files" | awk -F'|' '
|
||
function human(x, u,i){
|
||
split("B KB MB GB TB PB", u, " ");
|
||
i=1;
|
||
while (x>=1024 && i<6){x/=1024;i++}
|
||
if (i==1) return sprintf("%d%s", x, u[i]);
|
||
return sprintf("%.1f%s", x, u[i]);
|
||
}
|
||
{
|
||
printf " [%d] %-9s %-16s %s\n", NR, human($2), $3, $1
|
||
}'
|
||
echo -e "${UI_DIVIDER}"
|
||
} >&2
|
||
|
||
local pick
|
||
read -p "请选择镜像序号 (0 返回): " pick
|
||
pick="${pick:-0}"
|
||
if [[ "$pick" == "0" ]]; then
|
||
return 2
|
||
fi
|
||
if [[ ! "$pick" =~ ^[0-9]+$ ]]; then
|
||
log_error "序号必须是数字"
|
||
return 1
|
||
fi
|
||
|
||
local line path
|
||
line="$(echo "$files" | awk -F'|' -v n="$pick" 'NR==n{print $0}')"
|
||
path="$(echo "$line" | awk -F'|' '{print $1}')"
|
||
if [[ -z "$path" || ! -f "$path" ]]; then
|
||
log_error "无效选择"
|
||
return 1
|
||
fi
|
||
echo "$path"
|
||
return 0
|
||
}
|
||
|
||
img_select_vmid() {
|
||
local vms
|
||
vms="$(qm list 2>/dev/null | awk 'NR>1{print $1 "|" $2 "|" $3}')"
|
||
if [[ -z "$vms" ]]; then
|
||
log_error "未发现虚拟机"
|
||
log_tips "请先创建虚拟机后再操作。"
|
||
return 1
|
||
fi
|
||
|
||
{
|
||
echo -e "${CYAN}可用虚拟机列表:${NC}"
|
||
echo "$vms" | awk -F'|' '{printf " [%d] VMID: %-6s Name: %-22s Status: %s\n", NR, $1, $2, $3}'
|
||
echo -e "${UI_DIVIDER}"
|
||
} >&2
|
||
|
||
local pick
|
||
read -p "请选择虚拟机序号 (0 返回): " pick
|
||
pick="${pick:-0}"
|
||
if [[ "$pick" == "0" ]]; then
|
||
return 2
|
||
fi
|
||
if [[ ! "$pick" =~ ^[0-9]+$ ]]; then
|
||
log_error "序号必须是数字"
|
||
return 1
|
||
fi
|
||
|
||
local line vmid
|
||
line="$(echo "$vms" | awk -F'|' -v n="$pick" 'NR==n{print $0}')"
|
||
vmid="$(echo "$line" | awk -F'|' '{print $1}')"
|
||
if [[ -z "$vmid" ]]; then
|
||
log_error "无效选择"
|
||
return 1
|
||
fi
|
||
if ! validate_qm_vmid "$vmid"; then
|
||
return 1
|
||
fi
|
||
echo "$vmid"
|
||
return 0
|
||
}
|
||
|
||
img_select_storage() {
|
||
local stores
|
||
stores="$(pvesm status 2>/dev/null | awk 'NR>1{print $1 "|" $2}')"
|
||
if [[ -z "$stores" ]]; then
|
||
local manual
|
||
read -p "未能获取存储列表,请手动输入存储名(如 local-lvm): " manual
|
||
if [[ -z "$manual" ]]; then
|
||
log_error "存储名不能为空"
|
||
return 1
|
||
fi
|
||
echo "$manual"
|
||
return 0
|
||
fi
|
||
|
||
{
|
||
echo -e "${CYAN}可用存储列表:${NC}"
|
||
echo "$stores" | awk -F'|' '{printf " [%d] %-18s (%s)\n", NR, $1, $2}'
|
||
echo -e "${UI_DIVIDER}"
|
||
} >&2
|
||
|
||
local pick
|
||
read -p "请选择存储序号 (0 返回): " pick
|
||
pick="${pick:-0}"
|
||
if [[ "$pick" == "0" ]]; then
|
||
return 2
|
||
fi
|
||
if [[ ! "$pick" =~ ^[0-9]+$ ]]; then
|
||
log_error "序号必须是数字"
|
||
return 1
|
||
fi
|
||
|
||
local line store
|
||
line="$(echo "$stores" | awk -F'|' -v n="$pick" 'NR==n{print $0}')"
|
||
store="$(echo "$line" | awk -F'|' '{print $1}')"
|
||
if [[ -z "$store" ]]; then
|
||
log_error "无效选择"
|
||
return 1
|
||
fi
|
||
echo "$store"
|
||
return 0
|
||
}
|
||
|
||
img_convert_and_import_to_vm() {
|
||
log_step "IMG 镜像转换并导入虚拟机"
|
||
|
||
if ! command -v qemu-img >/dev/null 2>&1; then
|
||
display_error "未找到 qemu-img" "请先安装:apt install -y qemu-utils"
|
||
return 1
|
||
fi
|
||
if ! command -v qm >/dev/null 2>&1; then
|
||
display_error "未找到 qm 命令" "请确认当前环境为 PVE 宿主机。"
|
||
return 1
|
||
fi
|
||
|
||
local img_path
|
||
img_path="$(img_select_img_file)"
|
||
local rc=$?
|
||
if [[ "$rc" -eq 2 ]]; then
|
||
return 0
|
||
fi
|
||
if [[ -z "$img_path" ]]; then
|
||
return 1
|
||
fi
|
||
|
||
local vmid
|
||
vmid="$(img_select_vmid)"
|
||
rc=$?
|
||
if [[ "$rc" -eq 2 ]]; then
|
||
return 0
|
||
fi
|
||
if [[ -z "$vmid" ]]; then
|
||
return 1
|
||
fi
|
||
|
||
local store
|
||
store="$(img_select_storage)"
|
||
rc=$?
|
||
if [[ "$rc" -eq 2 ]]; then
|
||
return 0
|
||
fi
|
||
if [[ -z "$store" ]]; then
|
||
return 1
|
||
fi
|
||
|
||
local out_fmt
|
||
read -p "请选择目标格式 (qcow2/raw) [qcow2]: " out_fmt
|
||
out_fmt="${out_fmt:-qcow2}"
|
||
if [[ "$out_fmt" != "qcow2" && "$out_fmt" != "raw" ]]; then
|
||
display_error "不支持的格式: $out_fmt" "仅支持 qcow2/raw"
|
||
return 1
|
||
fi
|
||
|
||
local ts ext out_path out_dir
|
||
ts="$(date +%Y%m%d_%H%M%S)"
|
||
ext="$out_fmt"
|
||
out_dir="$(dirname "$img_path")"
|
||
out_path="${out_dir}/vm-${vmid}-disk-import-${ts}.${ext}"
|
||
if [[ -e "$out_path" ]]; then
|
||
out_path="${out_dir}/vm-${vmid}-disk-import-${ts}-1.${ext}"
|
||
fi
|
||
|
||
clear
|
||
show_menu_header "IMG 镜像转换并导入虚拟机"
|
||
local sz
|
||
sz="$(stat -c '%s' "$img_path" 2>/dev/null || echo "")"
|
||
echo -e "${YELLOW}源镜像:${NC} $img_path"
|
||
if [[ -n "$sz" ]]; then
|
||
echo -e "${YELLOW}大小:${NC} $(img_bytes_to_human "$sz")"
|
||
fi
|
||
echo -e "${YELLOW}目标 VMID:${NC} $vmid"
|
||
echo -e "${YELLOW}目标存储:${NC} $store"
|
||
echo -e "${YELLOW}目标格式:${NC} $out_fmt"
|
||
echo -e "${YELLOW}临时输出:${NC} $out_path"
|
||
echo -e "${UI_DIVIDER}"
|
||
|
||
if ! confirm_action "开始转换并导入磁盘?"; then
|
||
return 0
|
||
fi
|
||
|
||
log_step "开始转换(qemu-img convert)"
|
||
if ! qemu-img convert -p -f raw -O "$out_fmt" "$img_path" "$out_path"; then
|
||
display_error "镜像转换失败" "请检查镜像文件是否为 raw 格式,或查看日志输出。"
|
||
return 1
|
||
fi
|
||
|
||
log_step "开始导入(qm importdisk)"
|
||
local import_out vol
|
||
if ! import_out="$(qm importdisk "$vmid" "$out_path" "$store" 2>&1)"; then
|
||
echo "$import_out" | sed 's/^/ /'
|
||
display_error "导入失败" "请检查存储名称与空间,或查看上方输出。"
|
||
return 1
|
||
fi
|
||
|
||
vol="$(echo "$import_out" | sed -n "s/.*as '\\([^']\\+\\)'.*/\\1/p" | tail -n 1)"
|
||
[[ -z "$vol" ]] && vol="$(echo "$import_out" | grep -oE "${store}:[^ ]+" | tail -n 1)"
|
||
|
||
if [[ -n "$vol" ]]; then
|
||
log_success "导入完成: $vol"
|
||
else
|
||
log_success "导入完成"
|
||
fi
|
||
|
||
local attach_bus attach_slot cfg
|
||
local auto_attach="yes"
|
||
read -p "是否自动挂载到 VM?(yes/no) [yes]: " auto_attach
|
||
auto_attach="${auto_attach:-yes}"
|
||
if [[ "$auto_attach" == "yes" || "$auto_attach" == "YES" ]]; then
|
||
read -p "请选择总线类型 (scsi/sata/ide) [scsi]: " attach_bus
|
||
attach_bus="${attach_bus:-scsi}"
|
||
if [[ "$attach_bus" != "scsi" && "$attach_bus" != "sata" && "$attach_bus" != "ide" ]]; then
|
||
log_warn "不支持的总线类型,跳过自动挂载: $attach_bus"
|
||
else
|
||
cfg="$(qm config "$vmid" 2>/dev/null || true)"
|
||
if [[ -n "$vol" && -n "$cfg" ]] && echo "$cfg" | grep -Fq "$vol"; then
|
||
log_info "检测到该卷已写入 VM 配置(可能为 unusedX 或已挂载),跳过自动挂载。"
|
||
elif [[ -z "$vol" ]]; then
|
||
log_info "未能解析导入卷 ID,跳过自动挂载。"
|
||
else
|
||
attach_slot="$(rdm_find_free_slot "$vmid" "$attach_bus" 2>/dev/null)" || true
|
||
if [[ -z "$attach_slot" ]]; then
|
||
log_warn "未找到可用插槽,跳过自动挂载"
|
||
else
|
||
if confirm_action "将磁盘挂载到 VM $vmid(${attach_slot} = ${vol})"; then
|
||
if qm set "$vmid" "-$attach_slot" "$vol" >/dev/null 2>&1; then
|
||
log_success "已挂载: $attach_slot"
|
||
else
|
||
log_warn "自动挂载失败,请在 PVE WebUI 中手动添加该磁盘"
|
||
fi
|
||
fi
|
||
fi
|
||
fi
|
||
fi
|
||
fi
|
||
|
||
local del_tmp="yes"
|
||
read -p "是否删除临时输出文件 $out_path ?(yes/no) [yes]: " del_tmp
|
||
del_tmp="${del_tmp:-yes}"
|
||
if [[ "$del_tmp" == "yes" || "$del_tmp" == "YES" ]]; then
|
||
rm -f "$out_path" >/dev/null 2>&1 || true
|
||
fi
|
||
|
||
display_success "处理完成" "如需从该磁盘引导,请在 VM 启动顺序中选择对应磁盘。"
|
||
return 0
|
||
}
|
||
|
||
img_convert_import_menu() {
|
||
clear
|
||
show_menu_header "IMG 镜像导入(转换为 QCOW2/RAW)"
|
||
echo -e "${CYAN}功能说明:${NC}"
|
||
echo -e " - 自动扫描:/root、/var/lib/vz/template/iso、/home 下的 .img 文件"
|
||
echo -e " - 使用 qemu-img 转换后,通过 qm importdisk 导入到指定 VM 与存储"
|
||
echo -e "${UI_DIVIDER}"
|
||
img_convert_and_import_to_vm
|
||
}
|
||
|
||
# 二级菜单:虚拟机与容器
|
||
menu_vm_container() {
|
||
while true; do
|
||
clear
|
||
show_menu_header "虚拟机与容器"
|
||
show_menu_option "1" "${CYAN}FastPVE${NC} - 虚拟机快速下载"
|
||
show_menu_option "2" "${CYAN}Community Scripts${NC} - 第三方工具集"
|
||
show_menu_option "3" "虚拟机/容器定时开关机"
|
||
show_menu_option "4" "IMG 镜像导入(转 QCOW2/RAW)"
|
||
echo "$UI_DIVIDER"
|
||
show_menu_option "0" "返回主菜单"
|
||
show_menu_footer
|
||
read -p "请选择操作 [0-4]: " choice
|
||
case $choice in
|
||
1) fastpve_quick_download_menu ;;
|
||
2) third_party_tools_menu ;;
|
||
3) manage_vm_schedule ;;
|
||
4) img_convert_import_menu ;;
|
||
0) return ;;
|
||
*) log_error "无效选择" ;;
|
||
esac
|
||
pause_function
|
||
done
|
||
}
|
||
|
||
# 二级菜单:存储与硬盘
|
||
menu_storage_disk() {
|
||
while true; do
|
||
clear
|
||
show_menu_header "存储与硬盘"
|
||
show_menu_option "1" "合并 ${CYAN}local${NC} 与 ${CYAN}local-lvm${NC}"
|
||
show_menu_option "2" "${CYAN}Ceph${NC} 管理 (安装/卸载/换源)"
|
||
show_menu_option "3" "硬盘休眠配置 ${CYAN}(hdparm)${NC}"
|
||
show_menu_option "4" "${RED}删除 Swap 分区${NC}"
|
||
echo "$UI_DIVIDER"
|
||
show_menu_option "0" "返回主菜单"
|
||
show_menu_footer
|
||
read -p "请选择操作 [0-4]: " choice
|
||
case $choice in
|
||
1) merge_local_storage ;;
|
||
2) ceph_management_menu ;;
|
||
3)
|
||
lsblk -o NAME,MODEL,TYPE,SIZE,MOUNTPOINT | grep disk
|
||
read -p "请输入要配置休眠的硬盘盘符 (如 sdb, 不含/dev/): " disk_name
|
||
if [ -b "/dev/$disk_name" ]; then
|
||
read -p "请输入休眠时间 (1-255, 120=10分钟, 240=20分钟, 0=禁用): " sleep_val
|
||
if [[ "$sleep_val" =~ ^[0-9]+$ ]]; then
|
||
hdparm -S "$sleep_val" "/dev/$disk_name"
|
||
log_success "配置已应用到 /dev/$disk_name"
|
||
else
|
||
log_error "无效的时间值"
|
||
fi
|
||
else
|
||
log_error "未找到磁盘 /dev/$disk_name"
|
||
fi
|
||
;;
|
||
4) remove_swap ;;
|
||
0) return ;;
|
||
*) log_error "无效选择" ;;
|
||
esac
|
||
pause_function
|
||
done
|
||
}
|
||
|
||
# 二级菜单:工具与关于
|
||
menu_tools_about() {
|
||
while true; do
|
||
clear
|
||
show_menu_header "工具与关于"
|
||
show_menu_option "1" "系统信息概览"
|
||
show_menu_option "2" "应急救砖工具箱"
|
||
show_menu_option "3" "给作者点个 Star 吧"
|
||
show_menu_option "0" "返回主菜单"
|
||
show_menu_footer
|
||
read -p "请选择操作 [0-3]: " choice
|
||
case $choice in
|
||
1) show_system_info ;;
|
||
2) show_menu_rescue ;;
|
||
3)
|
||
echo -e "${YELLOW}项目地址:https://github.com/Mapleawaa/PVE-Tools-9${NC}"
|
||
echo -e "${GREEN}您的支持是我更新的最大动力,谢谢喵~${NC}"
|
||
;;
|
||
0) return ;;
|
||
*) log_error "无效选择" ;;
|
||
esac
|
||
pause_function
|
||
done
|
||
}
|
||
|
||
# 一键配置
|
||
quick_setup() {
|
||
block_non_pve9_destructive "一键优化(换源+删弹窗+更新)" || return 1
|
||
log_step "开始一键配置"
|
||
log_step "天涯若比邻,海内存知己,坐和放宽,让我来搞定一切。"
|
||
echo
|
||
change_sources
|
||
echo
|
||
remove_subscription_popup
|
||
echo
|
||
update_system
|
||
echo
|
||
log_success "一键配置全部完成!您的 PVE 已经完美优化"
|
||
echo -e "现在您可以愉快地使用 PVE 了!"
|
||
}
|
||
|
||
# 通用UI函数
|
||
show_menu_header() {
|
||
local title="$1"
|
||
echo -e "${UI_BORDER}"
|
||
echo -e " ${H2}${title}${NC}"
|
||
echo -e "${UI_DIVIDER}"
|
||
}
|
||
|
||
show_menu_footer() {
|
||
echo -e "${UI_FOOTER}"
|
||
}
|
||
|
||
show_menu_option() {
|
||
local num="$1"
|
||
local desc="$2"
|
||
if [[ -z "$desc" ]]; then
|
||
# 仅作为消息或标题显示
|
||
echo -e " ${H2}$num${NC}"
|
||
else
|
||
printf " ${PRIMARY}%-3s${NC}. %s\\n" "$num" "$desc"
|
||
fi
|
||
}
|
||
|
||
# 镜像源选择函数
|
||
select_mirror() {
|
||
while true; do
|
||
clear
|
||
show_menu_header "请选择镜像源"
|
||
show_menu_option "1" "中科大镜像源"
|
||
show_menu_option "2" "清华Tuna镜像源"
|
||
show_menu_option "3" "Debian默认源"
|
||
echo -e "${UI_DIVIDER}"
|
||
echo "注意:选择后将作为后续所有软件源操作的基础"
|
||
echo -e "${UI_DIVIDER}"
|
||
echo
|
||
|
||
read -p "请选择 [1-3]: " mirror_choice
|
||
|
||
case $mirror_choice in
|
||
1)
|
||
SELECTED_MIRROR=$MIRROR_USTC
|
||
log_success "已选择中科大镜像源"
|
||
break
|
||
;;
|
||
2)
|
||
SELECTED_MIRROR=$MIRROR_TUNA
|
||
log_success "已选择清华Tuna镜像源"
|
||
break
|
||
;;
|
||
3)
|
||
SELECTED_MIRROR=$MIRROR_DEBIAN
|
||
log_success "已选择Debian默认源"
|
||
break
|
||
;;
|
||
*)
|
||
log_error "无效选择,请重新输入"
|
||
pause_function
|
||
;;
|
||
esac
|
||
done
|
||
}
|
||
|
||
# 版本检查函数
|
||
check_update() {
|
||
log_info "正在检查更新..."
|
||
|
||
download_file() {
|
||
local url="$1"
|
||
local timeout=10
|
||
|
||
if command -v curl &> /dev/null; then
|
||
curl -s --connect-timeout $timeout --max-time $timeout "$url" 2>/dev/null
|
||
elif command -v wget &> /dev/null; then
|
||
wget -q -T $timeout -O - "$url" 2>/dev/null
|
||
else
|
||
echo ""
|
||
fi
|
||
}
|
||
|
||
# 显示进度提示
|
||
echo -ne "[....] 正在检查更新...\033[0K\r"
|
||
|
||
local prefer_mirror=0
|
||
local preferred_version_url="$VERSION_FILE_URL"
|
||
local preferred_update_url="$UPDATE_FILE_URL"
|
||
local mirror_version_url="${GITHUB_MIRROR_PREFIX}${VERSION_FILE_URL}"
|
||
local mirror_update_url="${GITHUB_MIRROR_PREFIX}${UPDATE_FILE_URL}"
|
||
|
||
if detect_network_region; then
|
||
prefer_mirror=$USE_MIRROR_FOR_UPDATE
|
||
if [[ $prefer_mirror -eq 1 ]]; then
|
||
log_info "当前地区为: $USER_COUNTRY_CODE,使用镜像源检查更新...请等待 3 秒"
|
||
# log_info "检测到中国大陆网络环境,将优先使用镜像源检查更新"
|
||
preferred_version_url="$mirror_version_url"
|
||
preferred_update_url="$mirror_update_url"
|
||
else
|
||
if [[ -n "$USER_COUNTRY_CODE" ]]; then
|
||
log_info "检测到当前地区为: $USER_COUNTRY_CODE,将使用 GitHub 源检查更新"
|
||
fi
|
||
fi
|
||
else
|
||
log_warn "无法获取网络地区信息,默认使用 GitHub 源检查更新"
|
||
fi
|
||
|
||
remote_content=$(download_file "$preferred_version_url")
|
||
|
||
if [ -z "$remote_content" ]; then
|
||
if [[ $prefer_mirror -eq 1 ]]; then
|
||
log_warn "镜像源连接失败,尝试使用 GitHub 源..."
|
||
remote_content=$(download_file "$VERSION_FILE_URL")
|
||
else
|
||
log_warn "GitHub 连接失败,尝试使用镜像源..."
|
||
remote_content=$(download_file "$mirror_version_url")
|
||
fi
|
||
fi
|
||
|
||
# 清除进度显示
|
||
echo -ne "\033[0K\r"
|
||
|
||
# 如果下载失败
|
||
if [ -z "$remote_content" ]; then
|
||
log_warn "网络连接失败,跳过版本检查"
|
||
echo "提示:您可以手动访问以下地址检查更新:"
|
||
echo "https://github.com/Mapleawaa/PVE-Tools-9"
|
||
echo "按回车键继续..."
|
||
read -r
|
||
return
|
||
fi
|
||
|
||
# 提取版本号和更新日志
|
||
remote_version=$(echo "$remote_content" | head -1 | tr -d '[:space:]')
|
||
version_changelog=$(echo "$remote_content" | tail -n +2)
|
||
|
||
if [ -z "$remote_version" ]; then
|
||
log_warn "获取的版本信息格式不正确"
|
||
return
|
||
fi
|
||
|
||
detailed_changelog=$(download_file "$preferred_update_url")
|
||
|
||
if [ -z "$detailed_changelog" ]; then
|
||
if [[ $prefer_mirror -eq 1 ]]; then
|
||
log_warn "镜像源更新日志获取失败,尝试使用 GitHub 源..."
|
||
detailed_changelog=$(download_file "$UPDATE_FILE_URL")
|
||
else
|
||
log_warn "GitHub 更新日志获取失败,尝试使用镜像源..."
|
||
detailed_changelog=$(download_file "$mirror_update_url")
|
||
fi
|
||
fi
|
||
|
||
# 比较版本
|
||
if [ "$(printf '%s\n' "$remote_version" "$CURRENT_VERSION" | sort -V | tail -n1)" != "$CURRENT_VERSION" ]; then
|
||
echo -e "${UI_HEADER}"
|
||
echo -e "${YELLOW}🚀 发现新版本!推荐更新以获取最新功能和修复喵${NC}"
|
||
echo -e "----------------------------------------------"
|
||
echo -e "当前版本: ${WHITE}$CURRENT_VERSION${NC}"
|
||
echo -e "最新版本: ${GREEN}$remote_version${NC}"
|
||
echo -e "${BLUE}更新日志:${NC}"
|
||
|
||
# 如果获取到了详细的更新日志
|
||
if [ -n "$detailed_changelog" ]; then
|
||
# 使用 sed 提取第一行作为标题,其余行缩进显示
|
||
local first_line=$(echo "$detailed_changelog" | head -n 1)
|
||
local rest_lines=$(echo "$detailed_changelog" | tail -n +2)
|
||
|
||
echo -e " ${CYAN}★ $first_line${NC}"
|
||
if [ -n "$rest_lines" ]; then
|
||
echo "$rest_lines" | sed 's/^/ /'
|
||
fi
|
||
else
|
||
# 格式化显示版本文件中的更新内容
|
||
if [ -n "$version_changelog" ] && [ "$version_changelog" != "$remote_version" ]; then
|
||
echo "$version_changelog" | sed 's/^/ /'
|
||
else
|
||
echo -e " ${YELLOW}- 请访问项目页面获取详细更新内容${NC}"
|
||
fi
|
||
fi
|
||
|
||
echo -e "----------------------------------------------"
|
||
echo -e "${CYAN}官方文档与最新脚本:${NC}"
|
||
echo -e "🔗 https://pve.u3u.icu (推荐)"
|
||
echo -e "🔗 https://github.com/Mapleawaa/PVE-Tools-9"
|
||
echo -e "${UI_FOOTER}"
|
||
echo -e "按 ${GREEN}回车键${NC} 进入主菜单..."
|
||
read -r
|
||
else
|
||
log_success "当前已是最新版本 ($CURRENT_VERSION) 放心用吧"
|
||
fi
|
||
}
|
||
|
||
# 温度监控管理菜单
|
||
temp_monitoring_menu() {
|
||
while true; do
|
||
clear
|
||
show_menu_header "温度监控管理"
|
||
show_menu_option "1" "配置温度监控 ${CYAN}(CPU/硬盘温度显示)${NC}"
|
||
show_menu_option "2" "${RED}移除温度监控${NC} (移除温度监控功能)"
|
||
show_menu_option "3" "自定义温度监控选项 ${MAGENTA}(高级)${NC}"
|
||
echo "${UI_DIVIDER}"
|
||
show_menu_option "0" "返回上级菜单"
|
||
show_menu_footer
|
||
echo
|
||
read -p "请选择 [0-3]: " temp_choice
|
||
echo
|
||
|
||
case $temp_choice in
|
||
1)
|
||
cpu_add
|
||
;;
|
||
2)
|
||
cpu_del
|
||
;;
|
||
3)
|
||
custom_temp_monitoring
|
||
;;
|
||
0)
|
||
break
|
||
;;
|
||
*)
|
||
log_error "无效选择,请重新输入"
|
||
;;
|
||
esac
|
||
|
||
echo
|
||
pause_function
|
||
done
|
||
}
|
||
|
||
# 自定义温度监控配置
|
||
custom_temp_monitoring() {
|
||
clear
|
||
|
||
|
||
# Define options
|
||
declare -A options
|
||
# options[0]="CPU 实时主频"
|
||
# options[1]="CPU 最小及最大主频 (必选 0)"
|
||
# options[2]="CPU 线程主频"
|
||
# options[3]="CPU 工作模式 (必选 0)"
|
||
# options[4]="CPU 功率 (必选 0)"
|
||
# options[5]="CPU 温度"
|
||
# options[6]="CPU 核心温度 (不支持 AMD, 必选 5)"
|
||
# options[7]="核显温度 (仅支持 AMD, 必选 5)"
|
||
# options[8]="风扇转速 (可能需要单独安装传感器驱动, 必选 5)"
|
||
# options[9]="UPS 信息 (仅支持 apcupsd - apcaccess 软件包)"
|
||
# options[a]="硬盘基础信息 (容量、寿命 (仅 NVME )、温度)"
|
||
# options[b]="硬盘通电信息 (必选 a)"
|
||
# options[c]="硬盘 IO 信息 (必选 a)"
|
||
# options[l]="概要信息: 居左显示"
|
||
# options[r]="概要信息: 居右显示"
|
||
# options[m]="概要信息: 居中显示"
|
||
# options[j]="概要信息: 平铺显示"
|
||
options[o]="推荐方案一:高大全 (除 UPS 信息以外全部居右显示)"
|
||
options[p]="推荐方案二:精简"
|
||
options[q]="推荐方案三:极简"
|
||
options[x]="一键清空 (还原默认)"
|
||
options[s]="跳过本次修改"
|
||
|
||
echo "请选择要启用的监控项目 (用空格分隔,如: o):"
|
||
echo
|
||
|
||
# Display options with checkboxes
|
||
# for key in 0 1 2 3 4 5 6 7 8 9 a b c l r m j o p q x s; do
|
||
for key in o p q x s; do
|
||
if [[ -n "${options[$key]}" ]]; then
|
||
echo " [ ] $key) ${options[$key]}"
|
||
fi
|
||
done
|
||
|
||
echo
|
||
read -p "请输入选择 (如: 0 5 6 或 o 或 s): " input
|
||
|
||
# Process user selections
|
||
if [[ "$input" == "s" ]]; then
|
||
log_info "跳过自定义配置"
|
||
return
|
||
fi
|
||
|
||
if [[ "$input" == "x" ]]; then
|
||
log_info "正在还原默认设置..."
|
||
cpu_del
|
||
log_success "已还原默认设置"
|
||
return
|
||
fi
|
||
|
||
if [[ "$input" == "o" ]]; then
|
||
log_info "应用推荐方案一:高大全..."
|
||
# Apply comprehensive configuration
|
||
cpu_add
|
||
log_success "推荐方案一已应用"
|
||
return
|
||
fi
|
||
|
||
if [[ "$input" == "p" ]]; then
|
||
log_info "应用推荐方案二:精简..."
|
||
# Apply simplified configuration
|
||
cpu_add
|
||
log_success "推荐方案二已应用"
|
||
return
|
||
fi
|
||
|
||
if [[ "$input" == "q" ]]; then
|
||
log_info "应用推荐方案三:极简..."
|
||
# Apply minimal configuration
|
||
cpu_add
|
||
log_success "推荐方案三已应用"
|
||
return
|
||
fi
|
||
|
||
# Process selected individual options
|
||
echo "您选择了: $input"
|
||
echo "正在配置自定义温度监控..."
|
||
|
||
# Parse and validate dependencies
|
||
selections=($input)
|
||
dependencies_met=true
|
||
|
||
# Check for dependencies
|
||
for selection in "${selections[@]}"; do
|
||
case "$selection" in
|
||
1) if [[ ! " ${selections[@]} " =~ " 0 " ]]; then
|
||
log_error "选项 1 需要选项 0,请重新选择"
|
||
dependencies_met=false
|
||
break
|
||
fi ;;
|
||
3|4) if [[ ! " ${selections[@]} " =~ " 0 " ]]; then
|
||
log_error "选项 3 或 4 需要选项 0,请重新选择"
|
||
dependencies_met=false
|
||
break
|
||
fi ;;
|
||
6|7|8) if [[ ! " ${selections[@]} " =~ " 5 " ]]; then
|
||
log_error "选项 6, 7 或 8 需要选项 5,请重新选择"
|
||
dependencies_met=false
|
||
break
|
||
fi ;;
|
||
b) if [[ ! " ${selections[@]} " =~ " a " ]]; then
|
||
log_error "选项 b 需要选项 a,请重新选择"
|
||
dependencies_met=false
|
||
break
|
||
fi ;;
|
||
c) if [[ ! " ${selections[@]} " =~ " a " ]]; then
|
||
log_error "选项 c 需要选项 a,请重新选择"
|
||
dependencies_met=false
|
||
break
|
||
fi ;;
|
||
esac
|
||
done
|
||
|
||
if [[ "$dependencies_met" == true ]]; then
|
||
log_info "配置所选监控项..."
|
||
# In a real implementation, this would customize the monitoring based on selections
|
||
# For now, we'll use the existing cpu_add function
|
||
cpu_add # Use the existing function to install the basic monitoring
|
||
log_success "自定义温度监控配置完成"
|
||
else
|
||
log_error "配置失败,依赖关系不满足"
|
||
fi
|
||
}
|
||
|
||
# Ceph管理菜单
|
||
ceph_management_menu() {
|
||
while true; do
|
||
clear
|
||
|
||
show_menu_header "Ceph管理"
|
||
show_menu_option "1" "添加 ${CYAN}ceph-squid${NC} 源 (PVE8/9专用)"
|
||
show_menu_option "2" "添加 ${CYAN}ceph-quincy${NC} 源 (PVE7/8专用)"
|
||
show_menu_option "3" "${RED}卸载 Ceph${NC} (完全移除Ceph)"
|
||
echo "${UI_DIVIDER}"
|
||
show_menu_option "0" "返回主菜单"
|
||
show_menu_footer
|
||
echo
|
||
read -p "请选择 [0-3]: " ceph_choice
|
||
echo
|
||
|
||
case $ceph_choice in
|
||
1)
|
||
pve9_ceph
|
||
;;
|
||
2)
|
||
pve8_ceph
|
||
;;
|
||
3)
|
||
remove_ceph
|
||
;;
|
||
0)
|
||
break
|
||
;;
|
||
*)
|
||
log_error "无效选择,请重新输入"
|
||
;;
|
||
esac
|
||
|
||
echo
|
||
pause_function
|
||
done
|
||
}
|
||
|
||
# 救砖:恢复官方 pve-qemu-kvm
|
||
restore_qemu_kvm() {
|
||
log_step "开始恢复官方 pve-qemu-kvm"
|
||
echo "此操作将执行以下步骤:"
|
||
echo "1. 解除 pve-qemu-kvm 的版本锁定 (unhold)"
|
||
echo "2. 强制重新安装官方版本的 pve-qemu-kvm"
|
||
echo "3. 恢复官方的 initramfs 设置"
|
||
echo "适用于因安装修改版 QEMU 导致虚拟机无法启动或系统异常的情况。"
|
||
echo
|
||
|
||
if ! confirm_action "是否继续执行恢复操作?"; then
|
||
return
|
||
fi
|
||
|
||
# 1. 解除锁定
|
||
log_info "正在解除软件包锁定..."
|
||
apt-mark unhold pve-qemu-kvm
|
||
|
||
# 2. 强制重装官方版本
|
||
log_info "正在重新安装官方 pve-qemu-kvm..."
|
||
if apt-get update && apt-get install --reinstall -y pve-qemu-kvm; then
|
||
log_success "官方 pve-qemu-kvm 恢复成功"
|
||
else
|
||
log_error "恢复失败,请检查网络连接或手动尝试: apt-get install --reinstall pve-qemu-kvm"
|
||
return 1
|
||
fi
|
||
|
||
# 3. 清理黑名单 (可选)
|
||
if confirm_action "是否同时清理 Intel 核显相关的驱动黑名单?"; then
|
||
log_info "正在清理黑名单配置..."
|
||
sed -i '/blacklist i915/d' /etc/modprobe.d/pve-blacklist.conf
|
||
sed -i '/blacklist snd_hda_intel/d' /etc/modprobe.d/pve-blacklist.conf
|
||
sed -i '/blacklist snd_hda_codec_hdmi/d' /etc/modprobe.d/pve-blacklist.conf
|
||
|
||
log_info "正在更新 initramfs..."
|
||
update-initramfs -u -k all
|
||
log_success "黑名单清理完成"
|
||
fi
|
||
|
||
log_success "救砖操作完成!建议重启系统。"
|
||
if confirm_action "是否现在重启系统?"; then
|
||
reboot
|
||
fi
|
||
}
|
||
|
||
#英特尔核显直通
|
||
intel_gpu_passthrough() {
|
||
log_step "开始 Intel 核显直通配置"
|
||
echo "注意:此功能基于 lixiaoliu666 的修改版 QEMU 和 ROM"
|
||
echo "详细原理与教程:https://pve.u3u.icu/advanced/gpu-passthrough"
|
||
echo "适用于需要将 Intel 核显直通给 Windows 虚拟机且遇到代码 43 或黑屏的情况"
|
||
echo "支持的 CPU 架构:6代(Skylake) 到 14代(Raptor Lake Refresh)"
|
||
echo "项目地址:https://github.com/lixiaoliu666/intel6-14rom"
|
||
echo
|
||
log_warn "警告"
|
||
log_warn "本功能并非能100%一次成功!"
|
||
echo
|
||
log_warn "由于 Intel 牙膏厂混乱的代号和半代升级策略(如 N5105 Jasper Lake 等)"
|
||
log_warn "通用 ROM 无法保证 100% 适用于所有 CPU 型号!"
|
||
log_warn "直通失败属于正常现象,请尝试更换其他版本的 ROM 或自行寻找专用 ROM"
|
||
log_warn "本功能仅提供自动化配置辅助,作者精力有限,无法提供免费的一对一排错服务"
|
||
log_warn "折腾有风险,入坑需谨慎!"
|
||
echo
|
||
log_tips "如果配置失败,请访问文档站查看详细教程并留言反馈:"
|
||
log_tips "🔗 https://pve.u3u.icu/advanced/gpu-passthrough"
|
||
echo
|
||
log_tips "如需要反馈或者请求更新ROM文件适配你的CPU,请前往lixiaoliu666的GitHub仓库开ISSUE反馈,不是找我。"
|
||
echo
|
||
|
||
echo "请选择操作:"
|
||
echo " 1) 开始配置 (安装修改版 QEMU + 下载 ROM)"
|
||
echo " 2) 救砖模式 (恢复官方 QEMU + 清理配置)"
|
||
echo " 0) 返回上级菜单"
|
||
read -p "请输入选择 [0-2]: " choice
|
||
|
||
case $choice in
|
||
1)
|
||
# 继续执行配置流程
|
||
;;
|
||
2)
|
||
restore_qemu_kvm
|
||
return
|
||
;;
|
||
0)
|
||
return
|
||
;;
|
||
*)
|
||
log_error "无效选择"
|
||
return
|
||
;;
|
||
esac
|
||
|
||
# 1. 配置黑名单
|
||
log_step "配置驱动黑名单 (屏蔽宿主机占用核显)"
|
||
if ! grep -q "blacklist i915" /etc/modprobe.d/pve-blacklist.conf; then
|
||
echo "blacklist i915" >> /etc/modprobe.d/pve-blacklist.conf
|
||
echo "blacklist snd_hda_intel" >> /etc/modprobe.d/pve-blacklist.conf
|
||
echo "blacklist snd_hda_codec_hdmi" >> /etc/modprobe.d/pve-blacklist.conf
|
||
log_success "已添加黑名单配置"
|
||
|
||
log_info "正在更新 initramfs..."
|
||
update-initramfs -u -k all
|
||
else
|
||
log_info "黑名单配置已存在,跳过"
|
||
fi
|
||
|
||
# 2. 安装修改版 QEMU
|
||
log_step "安装修改版 pve-qemu-kvm"
|
||
echo "正在获取最新 release 版本..."
|
||
|
||
# 尝试获取最新下载链接 (这里为了稳定性暂时写死或使用最新已知的逻辑,实际可爬虫获取)
|
||
# 根据用户提供的信息,修改版 QEMU 下载地址: https://github.com/lixiaoliu666/pve-anti-detection/releases
|
||
# 为了简化,我们使用 ghfast.top 加速下载最新的 release
|
||
# 注意:这里需要动态获取最新 deb 包链接,或者让用户手动输入链接
|
||
# 为方便起见,这里演示自动获取逻辑
|
||
|
||
local qemu_releases_url="https://api.github.com/repos/lixiaoliu666/pve-anti-detection/releases/latest"
|
||
local qemu_deb_url=$(curl -s $qemu_releases_url | grep "browser_download_url.*deb" | cut -d '"' -f 4 | head -n 1)
|
||
|
||
if [ -z "$qemu_deb_url" ]; then
|
||
log_warn "无法自动获取修改版 QEMU 下载链接,尝试使用备用链接或手动下载"
|
||
# 备用逻辑:提示用户手动下载
|
||
echo "请访问 https://github.com/lixiaoliu666/pve-anti-detection/releases 下载最新 deb 包"
|
||
echo "然后使用 dpkg -i 安装"
|
||
else
|
||
# 加速下载
|
||
local fast_qemu_url="https://ghfast.top/${qemu_deb_url}"
|
||
log_info "正在下载: $fast_qemu_url"
|
||
wget -O /tmp/pve-qemu-kvm.deb "$fast_qemu_url"
|
||
|
||
if [ -s "/tmp/pve-qemu-kvm.deb" ]; then
|
||
log_info "正在安装修改版 QEMU..."
|
||
dpkg -i /tmp/pve-qemu-kvm.deb
|
||
log_success "安装完成"
|
||
|
||
# 阻止更新
|
||
apt-mark hold pve-qemu-kvm
|
||
log_info "已锁定 pve-qemu-kvm 防止自动更新"
|
||
else
|
||
log_error "下载失败"
|
||
fi
|
||
fi
|
||
|
||
# 3. 下载 ROM 文件
|
||
log_step "下载核显 ROM 文件"
|
||
echo "正在检测 CPU 型号..."
|
||
local cpu_model=$(lscpu | grep "Model name" | awk -F: '{print $2}' | xargs)
|
||
echo "CPU 型号: $cpu_model"
|
||
|
||
# 优先推荐的通用 ROM
|
||
local recommended_rom="6-14-qemu10.rom"
|
||
|
||
# 特殊 CPU 型号映射表 (根据 release 信息整理)
|
||
# 格式: "关键字|ROM文件名"
|
||
local special_cpus=(
|
||
"J6412|11-J6412-q10.rom"
|
||
"N5095|11-n5095-q10.rom"
|
||
"1240P|12-1240p-q10.rom"
|
||
"N100|12-n100-q10.rom"
|
||
"J4125|j4125-q10.rom"
|
||
"N2930|N2930-q10.rom"
|
||
"N3350|N3350-q10.rom"
|
||
"11700H|nb-11-11700h-q10.rom"
|
||
"1185G7|nb-11-1185G7E-q10.rom"
|
||
"12700H|nb-12-12700h-q10.rom"
|
||
"13700H|nb-13-13700h-q10.rom"
|
||
)
|
||
|
||
# 检测是否为特殊 CPU
|
||
for item in "${special_cpus[@]}"; do
|
||
local keyword="${item%%|*}"
|
||
local rom_name="${item##*|}"
|
||
if echo "$cpu_model" | grep -qi "$keyword"; then
|
||
recommended_rom="$rom_name"
|
||
log_success "检测到特殊 CPU ($keyword),推荐使用专用 ROM: $recommended_rom"
|
||
break
|
||
fi
|
||
done
|
||
|
||
# 下载 ROM 文件
|
||
local rom_releases_url="https://api.github.com/repos/lixiaoliu666/intel6-14rom/releases/latest"
|
||
log_info "正在获取 ROM 列表..."
|
||
|
||
# 获取 release 信息
|
||
# 注意:这里我们使用 grep 简单提取下载链接和文件名
|
||
local release_info=$(curl -s $rom_releases_url)
|
||
local assets=$(echo "$release_info" | grep "browser_download_url" | cut -d '"' -f 4)
|
||
|
||
if [ -z "$assets" ]; then
|
||
log_error "无法获取 ROM 下载链接"
|
||
return
|
||
fi
|
||
|
||
# 显示 ROM 列表供用户选择
|
||
echo "------------------------------------------------"
|
||
echo "可用的 ROM 文件列表:"
|
||
local i=1
|
||
local rom_list=()
|
||
local recommended_index=0
|
||
|
||
for url in $assets; do
|
||
local fname=$(basename "$url")
|
||
# 过滤非 .rom 文件 (如 patch)
|
||
if [[ "$fname" != *.rom ]]; then
|
||
continue
|
||
fi
|
||
|
||
rom_list+=("$fname|$url")
|
||
|
||
if [[ "$fname" == "$recommended_rom" ]]; then
|
||
echo -e " $i) ${GREEN}$fname (推荐)${NC}"
|
||
recommended_index=$i
|
||
else
|
||
echo " $i) $fname"
|
||
fi
|
||
((i++))
|
||
done
|
||
echo "------------------------------------------------"
|
||
|
||
# 让用户选择
|
||
local choice
|
||
if [ $recommended_index -gt 0 ]; then
|
||
read -p "请输入序号选择 ROM [默认 $recommended_index]: " choice
|
||
choice=${choice:-$recommended_index}
|
||
else
|
||
read -p "请输入序号选择 ROM: " choice
|
||
fi
|
||
|
||
# 验证选择
|
||
if [[ ! "$choice" =~ ^[0-9]+$ ]] || [ "$choice" -lt 1 ] || [ "$choice" -ge $i ]; then
|
||
log_error "无效选择"
|
||
return
|
||
fi
|
||
|
||
# 获取选中的 ROM 信息
|
||
local selected_item="${rom_list[$((choice-1))]}"
|
||
local selected_fname="${selected_item%%|*}"
|
||
local selected_url="${selected_item##*|}"
|
||
|
||
# 下载选中的 ROM
|
||
local fast_url="https://ghfast.top/${selected_url}"
|
||
log_info "正在下载: $selected_fname"
|
||
wget -O "/usr/share/kvm/$selected_fname" "$fast_url"
|
||
|
||
if [ ! -s "/usr/share/kvm/$selected_fname" ]; then
|
||
log_error "下载失败"
|
||
return
|
||
fi
|
||
log_success "ROM 文件已就绪: $selected_fname"
|
||
local rom_filename="$selected_fname"
|
||
|
||
# 4. 自动配置虚拟机
|
||
log_step "配置虚拟机参数"
|
||
|
||
# 获取 VMID
|
||
echo "请选择要配置直通的虚拟机 ID (VMID):"
|
||
ls /etc/pve/qemu-server/*.conf | awk -F/ '{print $NF}' | sed 's/.conf//' | xargs -n1 echo " -"
|
||
read -p "请输入 VMID: " vmid
|
||
|
||
if [ -z "$vmid" ] || [ ! -f "/etc/pve/qemu-server/$vmid.conf" ]; then
|
||
log_error "无效的 VMID 或配置文件不存在"
|
||
return
|
||
fi
|
||
|
||
# 获取核显 PCI ID
|
||
echo "正在查找 Intel 核显设备..."
|
||
local igpu_pci=$(lspci -D | grep -i "VGA compatible controller" | grep -i "Intel" | head -n1 | awk '{print $1}')
|
||
|
||
if [ -z "$igpu_pci" ]; then
|
||
log_error "未找到 Intel 核显设备"
|
||
return
|
||
fi
|
||
echo "找到核显设备: $igpu_pci"
|
||
|
||
# 获取声卡 PCI ID (通常和核显在一起,但也可能分开)
|
||
local audio_pci=$(lspci -D | grep -i "Audio device" | grep -i "Intel" | head -n1 | awk '{print $1}')
|
||
if [ -n "$audio_pci" ]; then
|
||
echo "找到声卡设备: $audio_pci"
|
||
else
|
||
log_warn "未找到配套声卡设备,将只直通核显"
|
||
fi
|
||
|
||
if ! confirm_action "即将修改虚拟机 $vmid 的配置,是否继续?"; then
|
||
return
|
||
fi
|
||
|
||
# 备份配置文件
|
||
backup_file "/etc/pve/qemu-server/$vmid.conf"
|
||
|
||
# 修改 args
|
||
local args_line="-set device.hostpci0.bus=pcie.0 -set device.hostpci0.addr=0x02.0 -set device.hostpci0.x-igd-gms=0x2 -set device.hostpci0.x-igd-opregion=on -set device.hostpci0.x-igd-lpc=on"
|
||
|
||
# 如果有声卡,添加 hostpci1 的 args 配置
|
||
if [ -n "$audio_pci" ]; then
|
||
args_line="$args_line -set device.hostpci1.bus=pcie.0 -set device.hostpci1.addr=0x03.0"
|
||
fi
|
||
|
||
# 写入 args (先删除旧的 args)
|
||
sed -i '/^args:/d' "/etc/pve/qemu-server/$vmid.conf"
|
||
echo "args: $args_line" >> "/etc/pve/qemu-server/$vmid.conf"
|
||
|
||
# 写入 hostpci0 (核显)
|
||
# 先删除旧的 hostpci0
|
||
sed -i '/^hostpci0:/d' "/etc/pve/qemu-server/$vmid.conf"
|
||
# 格式: hostpci0: 0000:00:02.0,romfile=xxx.rom
|
||
# 注意:这里 PCI ID 使用 lspci 获取到的真实 ID,通常是 0000:00:02.0
|
||
echo "hostpci0: $igpu_pci,romfile=$rom_filename" >> "/etc/pve/qemu-server/$vmid.conf"
|
||
|
||
# 写入 hostpci1 (声卡)
|
||
if [ -n "$audio_pci" ]; then
|
||
sed -i '/^hostpci1:/d' "/etc/pve/qemu-server/$vmid.conf"
|
||
echo "hostpci1: $audio_pci" >> "/etc/pve/qemu-server/$vmid.conf"
|
||
fi
|
||
|
||
log_success "虚拟机 $vmid 配置完成"
|
||
echo "已添加 args 参数和 hostpci 设备"
|
||
echo "请记得在虚拟机中安装驱动: https://downloadmirror.intel.com/854560/gfx_win_101.6793.exe"
|
||
|
||
echo
|
||
echo "注意:需要重启宿主机使黑名单生效"
|
||
if confirm_action "是否现在重启系统?"; then
|
||
reboot
|
||
fi
|
||
}
|
||
|
||
# NVIDIA显卡管理菜单
|
||
nvidia_t() {
|
||
local key="$1"
|
||
case "$key" in
|
||
MENU_TITLE) echo "NVIDIA 显卡管理" ;;
|
||
MENU_DESC) echo "请选择功能模块(高风险操作会强制二次确认)" ;;
|
||
OPT_PT) echo "显卡直通虚拟机" ;;
|
||
OPT_VGPU) echo "vGPU 配置与分配" ;;
|
||
OPT_DRV_INFO) echo "驱动信息与监控" ;;
|
||
OPT_DRV_SWITCH) echo "驱动切换(开源/闭源)" ;;
|
||
OPT_BACK) echo "返回" ;;
|
||
ERR_NO_GPU) echo "未检测到 NVIDIA GPU" ;;
|
||
ERR_IOMMU) echo "未检测到 IOMMU 已开启" ;;
|
||
TIP_ENABLE_IOMMU) echo "请先开启 BIOS 的 VT-d/AMD-Vi,并在脚本中启用 IOMMU(硬件直通一键配置)。" ;;
|
||
INPUT_CHOICE) echo "请选择操作" ;;
|
||
INPUT_PICK) echo "请选择序号" ;;
|
||
WARN_HIGH_RISK) echo "高风险操作:不同驱动性能侧重点不同,误操作可能导致宿主机不可用。" ;;
|
||
OK_DONE) echo "操作完成" ;;
|
||
*) echo "$key" ;;
|
||
esac
|
||
}
|
||
|
||
nvidia_get_cols() {
|
||
tput cols 2>/dev/null || echo 80
|
||
}
|
||
|
||
nvidia_trunc() {
|
||
local s="$1"
|
||
local w="$2"
|
||
if [[ -z "$w" || "$w" -le 0 ]]; then
|
||
echo "$s"
|
||
return 0
|
||
fi
|
||
if [[ "${#s}" -le "$w" ]]; then
|
||
echo "$s"
|
||
return 0
|
||
fi
|
||
echo "${s:0:$((w-3))}..."
|
||
}
|
||
|
||
nvidia_list_vms() {
|
||
qm list 2>/dev/null | awk 'NR>1{print $1 "|" $2 "|" $3}'
|
||
}
|
||
|
||
nvidia_list_nvidia_gpus() {
|
||
lspci -Dnn 2>/dev/null | grep -Ei 'VGA compatible controller|3D controller' | grep -i 'NVIDIA' | awk '{bdf=$1; sub(/^[0-9a-f]{4}:/,"",bdf); print $1 "|" $0}'
|
||
}
|
||
|
||
nvidia_get_pci_ids() {
|
||
local bdf="$1"
|
||
lspci -n -s "$bdf" 2>/dev/null | awk '{for(i=1;i<=NF;i++) if($i ~ /^[0-9a-fA-F]{4}:[0-9a-fA-F]{4}$/){print tolower($i); exit}}'
|
||
}
|
||
|
||
nvidia_pci_has_function() {
|
||
local bdf="$1"
|
||
local func="$2"
|
||
local base
|
||
base="${bdf%.*}"
|
||
lspci -Dnn 2>/dev/null | awk '{print $1}' | grep -qx "${base}.${func}"
|
||
}
|
||
|
||
nvidia_pci_kernel_driver() {
|
||
local bdf="$1"
|
||
lspci -nnk -s "$bdf" 2>/dev/null | awk -F': ' '/Kernel driver in use:/{print $2; exit}'
|
||
}
|
||
|
||
nvidia_select_vmid() {
|
||
local vms
|
||
vms="$(nvidia_list_vms)"
|
||
if [[ -z "$vms" ]]; then
|
||
log_error "未发现虚拟机"
|
||
log_tips "请先创建虚拟机后再操作。"
|
||
return 1
|
||
fi
|
||
|
||
{
|
||
echo -e "${CYAN}可用虚拟机列表:${NC}"
|
||
echo "$vms" | awk -F'|' '{printf " [%d] VMID: %-6s Name: %-22s Status: %s\n", NR, $1, $2, $3}'
|
||
echo -e "${UI_DIVIDER}"
|
||
} >&2
|
||
|
||
local pick
|
||
read -p "$(nvidia_t INPUT_PICK) (0 返回): " pick
|
||
pick="${pick:-0}"
|
||
if [[ "$pick" == "0" ]]; then
|
||
return 2
|
||
fi
|
||
if [[ ! "$pick" =~ ^[0-9]+$ ]]; then
|
||
log_error "序号必须是数字"
|
||
return 1
|
||
fi
|
||
|
||
local line vmid
|
||
line="$(echo "$vms" | awk -v n="$pick" -F'|' 'NR==n{print $0}')"
|
||
vmid="$(echo "$line" | awk -F'|' '{print $1}')"
|
||
if [[ -z "$vmid" ]]; then
|
||
log_error "无效选择"
|
||
return 1
|
||
fi
|
||
if ! validate_qm_vmid "$vmid"; then
|
||
return 1
|
||
fi
|
||
echo "$vmid"
|
||
return 0
|
||
}
|
||
|
||
nvidia_select_gpu_bdf() {
|
||
local gpus
|
||
gpus="$(nvidia_list_nvidia_gpus)"
|
||
if [[ -z "$gpus" ]]; then
|
||
log_error "$(nvidia_t ERR_NO_GPU)"
|
||
log_tips "请先确认已安装 NVIDIA GPU 并执行 lspci 可见。"
|
||
return 1
|
||
fi
|
||
|
||
local cols
|
||
cols="$(nvidia_get_cols)"
|
||
local max_line=$((cols-6))
|
||
if [[ "$max_line" -lt 40 ]]; then
|
||
max_line=40
|
||
fi
|
||
|
||
{
|
||
echo -e "${CYAN}可用 NVIDIA GPU 列表:${NC}"
|
||
echo "$gpus" | awk -F'|' -v w="$max_line" '{
|
||
line=$2;
|
||
if (length(line)>w) line=substr(line,1,w-3)"...";
|
||
printf " [%d] %s\n", NR, line
|
||
}'
|
||
echo -e "${UI_DIVIDER}"
|
||
} >&2
|
||
|
||
local pick
|
||
read -p "$(nvidia_t INPUT_PICK) (0 返回): " pick
|
||
pick="${pick:-0}"
|
||
if [[ "$pick" == "0" ]]; then
|
||
return 2
|
||
fi
|
||
if [[ ! "$pick" =~ ^[0-9]+$ ]]; then
|
||
log_error "序号必须是数字"
|
||
return 1
|
||
fi
|
||
|
||
local line bdf
|
||
line="$(echo "$gpus" | awk -v n="$pick" -F'|' 'NR==n{print $0}')"
|
||
bdf="$(echo "$line" | awk -F'|' '{print $1}')"
|
||
if [[ -z "$bdf" ]]; then
|
||
log_error "无效选择"
|
||
return 1
|
||
fi
|
||
echo "$bdf"
|
||
return 0
|
||
}
|
||
|
||
nvidia_show_passthrough_status() {
|
||
local bdf="$1"
|
||
local drv
|
||
drv="$(nvidia_pci_kernel_driver "$bdf")"
|
||
echo -e "${CYAN}设备: ${NC}$bdf"
|
||
echo -e "${CYAN}Kernel driver in use: ${NC}${drv:-unknown}"
|
||
lspci -nnk -s "$bdf" 2>/dev/null | sed 's/^/ /'
|
||
}
|
||
|
||
nvidia_try_write_vfio_ids_conf() {
|
||
local ids_csv="$1"
|
||
local file="/etc/modprobe.d/pve-tools-nvidia-vfio.conf"
|
||
|
||
local other
|
||
other="$(grep -RhsE '^\s*options\s+vfio-pci\s+ids=' /etc/modprobe.d 2>/dev/null | grep -vF "pve-tools-nvidia-vfio.conf" || true)"
|
||
if [[ -n "$other" ]]; then
|
||
display_error "检测到系统已存在 vfio-pci ids 配置" "为避免冲突,本功能不会自动写入。请手工合并 vfio-pci ids 后再 update-initramfs -u。"
|
||
return 1
|
||
fi
|
||
|
||
if ! confirm_action "写入 VFIO 绑定配置($file)并要求重启宿主机?"; then
|
||
return 0
|
||
fi
|
||
|
||
local content
|
||
content="options vfio-pci ids=${ids_csv}"
|
||
apply_block "$file" "NVIDIA_VFIO_IDS" "$content"
|
||
display_success "VFIO 绑定配置已写入" "请执行 update-initramfs -u 并重启宿主机后再进行直通。"
|
||
return 0
|
||
}
|
||
|
||
nvidia_gpu_passthrough_vm() {
|
||
log_step "$(nvidia_t OPT_PT)"
|
||
|
||
if ! iommu_is_enabled; then
|
||
display_error "$(nvidia_t ERR_IOMMU)" "$(nvidia_t TIP_ENABLE_IOMMU)"
|
||
return 1
|
||
fi
|
||
|
||
local vmid
|
||
vmid="$(nvidia_select_vmid)"
|
||
local rc=$?
|
||
if [[ "$rc" -eq 2 ]]; then
|
||
return 0
|
||
fi
|
||
if [[ -z "$vmid" ]]; then
|
||
return 1
|
||
fi
|
||
|
||
local gpu_bdf
|
||
gpu_bdf="$(nvidia_select_gpu_bdf)"
|
||
rc=$?
|
||
if [[ "$rc" -eq 2 ]]; then
|
||
return 0
|
||
fi
|
||
if [[ -z "$gpu_bdf" ]]; then
|
||
return 1
|
||
fi
|
||
|
||
clear
|
||
show_menu_header "$(nvidia_t OPT_PT)"
|
||
echo -e "${YELLOW}VMID: ${NC}$vmid"
|
||
echo -e "${YELLOW}GPU: ${NC}$gpu_bdf"
|
||
echo -e "${UI_DIVIDER}"
|
||
nvidia_show_passthrough_status "$gpu_bdf"
|
||
|
||
local audio_bdf=""
|
||
if nvidia_pci_has_function "$gpu_bdf" "1"; then
|
||
audio_bdf="${gpu_bdf%.*}.1"
|
||
echo -e "${UI_DIVIDER}"
|
||
nvidia_show_passthrough_status "$audio_bdf"
|
||
fi
|
||
|
||
local gpu_id audio_id ids_csv
|
||
gpu_id="$(nvidia_get_pci_ids "$gpu_bdf")"
|
||
audio_id=""
|
||
if [[ -n "$audio_bdf" ]]; then
|
||
audio_id="$(nvidia_get_pci_ids "$audio_bdf")"
|
||
fi
|
||
ids_csv="$gpu_id"
|
||
if [[ -n "$audio_id" ]]; then
|
||
ids_csv="${ids_csv},${audio_id}"
|
||
fi
|
||
|
||
echo -e "${UI_DIVIDER}"
|
||
if [[ -n "$ids_csv" ]]; then
|
||
echo -e "${CYAN}VFIO ids 建议: ${NC}$ids_csv"
|
||
fi
|
||
echo -e "${YELLOW}提示:如果宿主机正在加载 nvidia/nouveau 驱动,直通可能失败。${NC}"
|
||
echo -e "${UI_DIVIDER}"
|
||
|
||
local include_audio="yes"
|
||
if [[ -n "$audio_bdf" ]]; then
|
||
read -p "是否同时直通显卡音频功能(${audio_bdf})?(yes/no) [yes]: " include_audio
|
||
include_audio="${include_audio:-yes}"
|
||
else
|
||
include_audio="no"
|
||
fi
|
||
|
||
if qm_has_hostpci_bdf "$vmid" "$gpu_bdf"; then
|
||
display_error "该 GPU 已存在于 VM 的 hostpci 配置中" "无需重复添加。"
|
||
return 1
|
||
fi
|
||
|
||
local idx0
|
||
idx0="$(qm_find_free_hostpci_index "$vmid" 2>/dev/null)" || {
|
||
display_error "未找到可用 hostpci 插槽" "请先释放 VM 的 hostpci0-hostpci15。"
|
||
return 1
|
||
}
|
||
|
||
local hostpci0_value="${gpu_bdf}"
|
||
if qm_is_q35_machine "$vmid"; then
|
||
hostpci0_value="${hostpci0_value},pcie=1,x-vga=1"
|
||
else
|
||
hostpci0_value="${hostpci0_value},x-vga=1"
|
||
fi
|
||
|
||
local conf_path
|
||
conf_path="$(get_qm_conf_path "$vmid")"
|
||
if [[ -f "$conf_path" ]]; then
|
||
backup_file "$conf_path" >/dev/null 2>&1 || true
|
||
fi
|
||
|
||
if ! confirm_action "为 VM $vmid 添加 GPU 直通(hostpci${idx0} = ${hostpci0_value})"; then
|
||
return 0
|
||
fi
|
||
|
||
if ! qm set "$vmid" "-hostpci${idx0}" "$hostpci0_value" >/dev/null 2>&1; then
|
||
display_error "qm set 执行失败" "请检查 VM 是否锁定,或查看 /var/log/pve-tools.log。"
|
||
return 1
|
||
fi
|
||
|
||
if [[ "$include_audio" == "yes" && -n "$audio_bdf" ]]; then
|
||
local idx1
|
||
idx1="$(qm_find_free_hostpci_index "$vmid" 2>/dev/null)" || {
|
||
display_error "显卡已添加,但未找到可用 hostpci 插槽添加音频功能" "请手工添加 $audio_bdf。"
|
||
return 1
|
||
}
|
||
|
||
local hostpci1_value="${audio_bdf}"
|
||
if qm_is_q35_machine "$vmid"; then
|
||
hostpci1_value="${hostpci1_value},pcie=1"
|
||
fi
|
||
|
||
if ! qm set "$vmid" "-hostpci${idx1}" "$hostpci1_value" >/dev/null 2>&1; then
|
||
log_warn "音频功能直通写入失败(GPU 已写入)"
|
||
else
|
||
log_success "音频功能已写入: hostpci${idx1} = $hostpci1_value"
|
||
fi
|
||
fi
|
||
|
||
local ignore_msrs="no"
|
||
read -p "是否写入 KVM ignore_msrs(Windows/NVIDIA 常见告警缓解)(yes/no)[no]: " ignore_msrs
|
||
ignore_msrs="${ignore_msrs:-no}"
|
||
if [[ "$ignore_msrs" == "yes" || "$ignore_msrs" == "YES" ]]; then
|
||
if confirm_action "写入 /etc/modprobe.d/kvm.conf 的 ignore_msrs 配置并要求重启?"; then
|
||
local kvm_content
|
||
kvm_content="options kvm ignore_msrs=1 report_ignored_msrs=0"
|
||
apply_block "/etc/modprobe.d/kvm.conf" "NVIDIA_IGNORE_MSRS" "$kvm_content"
|
||
log_success "已写入 KVM ignore_msrs 配置"
|
||
fi
|
||
fi
|
||
|
||
if [[ -n "$ids_csv" ]]; then
|
||
local set_vfio="no"
|
||
read -p "是否写入 VFIO ids 绑定配置(用于将设备绑定到 vfio-pci)(yes/no)[no]: " set_vfio
|
||
set_vfio="${set_vfio:-no}"
|
||
if [[ "$set_vfio" == "yes" || "$set_vfio" == "YES" ]]; then
|
||
nvidia_try_write_vfio_ids_conf "$ids_csv" || true
|
||
fi
|
||
fi
|
||
|
||
display_success "$(nvidia_t OK_DONE)" "如 VM 正在运行中,请重启 VM;如写入了 VFIO/kvm 配置,请按提示重启宿主机。"
|
||
return 0
|
||
}
|
||
|
||
nvidia_vgpu_list_types() {
|
||
if [[ ! -d /sys/class/mdev_bus ]]; then
|
||
return 1
|
||
fi
|
||
find /sys/class/mdev_bus -maxdepth 4 -type d -name mdev_supported_types 2>/dev/null | while read -r d; do
|
||
find "$d" -maxdepth 1 -mindepth 1 -type d 2>/dev/null
|
||
done
|
||
}
|
||
|
||
nvidia_vgpu_show_license() {
|
||
local conf="/etc/nvidia/gridd.conf"
|
||
if [[ -f "$conf" ]]; then
|
||
echo -e "${CYAN}gridd.conf:${NC} $conf"
|
||
grep -E '^(ServerAddress|ServerPort|FeatureType|EnableUI)=' "$conf" 2>/dev/null | sed 's/^/ /'
|
||
fi
|
||
if command -v systemctl >/dev/null 2>&1; then
|
||
systemctl is-enabled nvidia-gridd >/dev/null 2>&1 && echo -e "${CYAN}nvidia-gridd:${NC} enabled" || true
|
||
systemctl is-active nvidia-gridd >/dev/null 2>&1 && echo -e "${CYAN}nvidia-gridd:${NC} active" || true
|
||
fi
|
||
if command -v nvidia-smi >/dev/null 2>&1; then
|
||
nvidia-smi -q 2>/dev/null | grep -Ei 'License|vGPU' | head -n 30 | sed 's/^/ /' || true
|
||
fi
|
||
}
|
||
|
||
nvidia_vgpu_update_license() {
|
||
local conf="/etc/nvidia/gridd.conf"
|
||
if [[ ! -f "$conf" ]]; then
|
||
display_error "未找到 gridd.conf" "请先安装 NVIDIA vGPU 驱动/组件后再配置许可证。"
|
||
return 1
|
||
fi
|
||
|
||
local addr port
|
||
read -p "许可证服务器地址(例: 1.2.3.4 或 lic.example.com): " addr
|
||
read -p "许可证服务器端口 [7070]: " port
|
||
port="${port:-7070}"
|
||
|
||
if [[ -z "$addr" ]]; then
|
||
display_error "地址不能为空"
|
||
return 1
|
||
fi
|
||
if [[ ! "$port" =~ ^[0-9]+$ || "$port" -lt 1 || "$port" -gt 65535 ]]; then
|
||
display_error "端口不合法: $port"
|
||
return 1
|
||
fi
|
||
|
||
if ! confirm_action "更新 vGPU 许可证服务器配置并重启 nvidia-gridd?"; then
|
||
return 0
|
||
fi
|
||
|
||
backup_file "$conf" >/dev/null 2>&1 || true
|
||
if grep -q '^ServerAddress=' "$conf"; then
|
||
sed -i "s/^ServerAddress=.*/ServerAddress=${addr}/" "$conf"
|
||
else
|
||
echo "ServerAddress=${addr}" >> "$conf"
|
||
fi
|
||
|
||
if grep -q '^ServerPort=' "$conf"; then
|
||
sed -i "s/^ServerPort=.*/ServerPort=${port}/" "$conf"
|
||
else
|
||
echo "ServerPort=${port}" >> "$conf"
|
||
fi
|
||
|
||
if command -v systemctl >/dev/null 2>&1; then
|
||
systemctl restart nvidia-gridd >/dev/null 2>&1 || true
|
||
fi
|
||
display_success "许可证配置已更新"
|
||
return 0
|
||
}
|
||
|
||
nvidia_vgpu_assign_to_vm() {
|
||
log_step "$(nvidia_t OPT_VGPU)"
|
||
|
||
if ! iommu_is_enabled; then
|
||
display_error "$(nvidia_t ERR_IOMMU)" "$(nvidia_t TIP_ENABLE_IOMMU)"
|
||
return 1
|
||
fi
|
||
|
||
if [[ ! -d /sys/class/mdev_bus ]]; then
|
||
display_error "未检测到 mdev 支持" "请确认内核与硬件支持 mediated device,并且已加载相关驱动。"
|
||
return 1
|
||
fi
|
||
|
||
local vmid
|
||
vmid="$(nvidia_select_vmid)"
|
||
local rc=$?
|
||
if [[ "$rc" -eq 2 ]]; then
|
||
return 0
|
||
fi
|
||
if [[ -z "$vmid" ]]; then
|
||
return 1
|
||
fi
|
||
|
||
local gpu_bdf
|
||
gpu_bdf="$(nvidia_select_gpu_bdf)"
|
||
rc=$?
|
||
if [[ "$rc" -eq 2 ]]; then
|
||
return 0
|
||
fi
|
||
if [[ -z "$gpu_bdf" ]]; then
|
||
return 1
|
||
fi
|
||
|
||
local base_sysfs="/sys/bus/pci/devices/${gpu_bdf}/mdev_supported_types"
|
||
if [[ ! -d "$base_sysfs" ]]; then
|
||
display_error "该 GPU 未提供 mdev_supported_types" "该卡可能不支持 vGPU/mdev,或驱动未正确加载。"
|
||
return 1
|
||
fi
|
||
|
||
local types
|
||
types="$(find "$base_sysfs" -maxdepth 1 -mindepth 1 -type d 2>/dev/null)"
|
||
if [[ -z "$types" ]]; then
|
||
display_error "未发现可用 vGPU 类型" "请确认 vGPU 驱动已安装,并且该设备支持 vGPU。"
|
||
return 1
|
||
fi
|
||
|
||
echo -e "${CYAN}可用 vGPU 类型:${NC}"
|
||
echo "$types" | awk -v base="$base_sysfs" '{
|
||
type=$0;
|
||
n=split(type,a,"/");
|
||
id=a[n];
|
||
name_file=type"/name";
|
||
avail_file=type"/available_instances";
|
||
name="";
|
||
avail="";
|
||
if ((getline l < name_file) > 0) name=l;
|
||
close(name_file);
|
||
if ((getline k < avail_file) > 0) avail=k;
|
||
close(avail_file);
|
||
printf " [%d] %s | %s | available=%s\n", NR, id, name, avail
|
||
}'
|
||
echo -e "${UI_DIVIDER}"
|
||
|
||
local pick
|
||
read -p "$(nvidia_t INPUT_PICK) (0 返回): " pick
|
||
pick="${pick:-0}"
|
||
if [[ "$pick" == "0" ]]; then
|
||
return 0
|
||
fi
|
||
if [[ ! "$pick" =~ ^[0-9]+$ ]]; then
|
||
display_error "序号必须是数字"
|
||
return 1
|
||
fi
|
||
|
||
local type_path
|
||
type_path="$(echo "$types" | awk -v n="$pick" 'NR==n{print $0}')"
|
||
if [[ -z "$type_path" ]]; then
|
||
display_error "无效选择"
|
||
return 1
|
||
fi
|
||
|
||
local avail
|
||
avail="$(cat "${type_path}/available_instances" 2>/dev/null || echo 0)"
|
||
if [[ ! "$avail" =~ ^[0-9]+$ || "$avail" -le 0 ]]; then
|
||
display_error "该类型无可用实例" "请释放已有 vGPU 实例,或选择其他类型。"
|
||
return 1
|
||
fi
|
||
|
||
local uuid
|
||
uuid="$(cat /proc/sys/kernel/random/uuid 2>/dev/null || true)"
|
||
if [[ -z "$uuid" ]]; then
|
||
display_error "无法生成 UUID"
|
||
return 1
|
||
fi
|
||
|
||
if ! confirm_action "创建 vGPU 实例并分配给 VM $vmid?"; then
|
||
return 0
|
||
fi
|
||
|
||
if ! echo "$uuid" > "${type_path}/create" 2>/dev/null; then
|
||
display_error "vGPU 实例创建失败" "请检查驱动/权限,并确认该类型可用。"
|
||
return 1
|
||
fi
|
||
|
||
local idx
|
||
idx="$(qm_find_free_hostpci_index "$vmid" 2>/dev/null)" || {
|
||
display_error "已创建 vGPU 实例,但未找到可用 hostpci 插槽" "请手工将 mdev=$uuid 添加到 VM。"
|
||
return 1
|
||
}
|
||
|
||
local value="${gpu_bdf},mdev=${uuid}"
|
||
if qm_is_q35_machine "$vmid"; then
|
||
value="${value},pcie=1"
|
||
fi
|
||
|
||
local conf_path
|
||
conf_path="$(get_qm_conf_path "$vmid")"
|
||
if [[ -f "$conf_path" ]]; then
|
||
backup_file "$conf_path" >/dev/null 2>&1 || true
|
||
fi
|
||
|
||
if ! qm set "$vmid" "-hostpci${idx}" "$value" >/dev/null 2>&1; then
|
||
display_error "qm set 写入失败" "请手工添加 hostpci${idx}: ${value}"
|
||
return 1
|
||
fi
|
||
|
||
display_success "$(nvidia_t OK_DONE)" "已创建并绑定 mdev=${uuid},如 VM 运行中请重启 VM。"
|
||
return 0
|
||
}
|
||
|
||
nvidia_vgpu_menu() {
|
||
while true; do
|
||
clear
|
||
show_menu_header "$(nvidia_t OPT_VGPU)"
|
||
show_menu_option "1" "vGPU 类型选择与分配"
|
||
show_menu_option "2" "vGPU 许可证状态"
|
||
show_menu_option "3" "更新 vGPU 许可证配置"
|
||
show_menu_option "0" "$(nvidia_t OPT_BACK)"
|
||
show_menu_footer
|
||
read -p "$(nvidia_t INPUT_CHOICE) [0-3]: " choice
|
||
case "$choice" in
|
||
1) nvidia_vgpu_assign_to_vm ;;
|
||
2) clear; show_menu_header "$(nvidia_t OPT_VGPU)"; nvidia_vgpu_show_license ;;
|
||
3) nvidia_vgpu_update_license ;;
|
||
0) return ;;
|
||
*) log_error "无效选择" ;;
|
||
esac
|
||
pause_function
|
||
done
|
||
}
|
||
|
||
nvidia_driver_info() {
|
||
clear
|
||
show_menu_header "$(nvidia_t OPT_DRV_INFO)"
|
||
|
||
local open_loaded="no"
|
||
local prop_loaded="no"
|
||
if lsmod 2>/dev/null | grep -q '^nouveau'; then
|
||
open_loaded="yes"
|
||
fi
|
||
if lsmod 2>/dev/null | grep -q '^nvidia'; then
|
||
prop_loaded="yes"
|
||
fi
|
||
|
||
echo -e "${CYAN}驱动状态:${NC}"
|
||
echo " nouveau 已加载: $open_loaded"
|
||
echo " nvidia 已加载: $prop_loaded"
|
||
echo -e "${UI_DIVIDER}"
|
||
|
||
if command -v nvidia-smi >/dev/null 2>&1; then
|
||
echo -e "${CYAN}nvidia-smi:${NC}"
|
||
nvidia-smi 2>/dev/null | sed 's/^/ /' || true
|
||
echo -e "${UI_DIVIDER}"
|
||
echo -e "${CYAN}GPU 指标(CSV):${NC}"
|
||
nvidia-smi --query-gpu=index,name,driver_version,temperature.gpu,utilization.gpu,power.draw,power.limit,memory.used,memory.total --format=csv,noheader,nounits 2>/dev/null | sed 's/^/ /' || true
|
||
else
|
||
display_error "未找到 nvidia-smi" "如需查看驱动信息,请先安装 NVIDIA 驱动或确认 PATH。"
|
||
fi
|
||
}
|
||
|
||
nvidia_driver_export_report() {
|
||
local ts
|
||
ts="$(date +%Y%m%d_%H%M%S)"
|
||
local out="/var/log/pve-tools-nvidia-report-${ts}.txt"
|
||
{
|
||
echo "time: $(date)"
|
||
echo "pveversion: $(pveversion 2>/dev/null || true)"
|
||
echo "kernel: $(uname -r)"
|
||
echo
|
||
echo "lspci (nvidia):"
|
||
lspci -Dnn 2>/dev/null | grep -i nvidia || true
|
||
echo
|
||
echo "lsmod (nvidia/nouveau):"
|
||
lsmod 2>/dev/null | grep -E '^(nvidia|nouveau)\b' || true
|
||
echo
|
||
if command -v nvidia-smi >/dev/null 2>&1; then
|
||
echo "nvidia-smi:"
|
||
nvidia-smi 2>/dev/null || true
|
||
echo
|
||
echo "nvidia-smi -q (head):"
|
||
nvidia-smi -q 2>/dev/null | head -n 200 || true
|
||
fi
|
||
} > "$out" 2>/dev/null || {
|
||
display_error "导出失败" "请检查 /var/log 写入权限与磁盘空间。"
|
||
return 1
|
||
}
|
||
log_success "已导出: $out"
|
||
return 0
|
||
}
|
||
|
||
nvidia_driver_info_menu() {
|
||
while true; do
|
||
clear
|
||
show_menu_header "$(nvidia_t OPT_DRV_INFO)"
|
||
show_menu_option "1" "查看驱动与监控面板"
|
||
show_menu_option "2" "导出驱动诊断报告"
|
||
show_menu_option "0" "$(nvidia_t OPT_BACK)"
|
||
show_menu_footer
|
||
read -p "$(nvidia_t INPUT_CHOICE) [0-2]: " choice
|
||
case "$choice" in
|
||
1) nvidia_driver_info ;;
|
||
2) nvidia_driver_export_report ;;
|
||
0) return ;;
|
||
*) log_error "无效选择" ;;
|
||
esac
|
||
pause_function
|
||
done
|
||
}
|
||
|
||
nvidia_apt_has_pkg() {
|
||
local pkg="$1"
|
||
apt-cache show "$pkg" >/dev/null 2>&1
|
||
}
|
||
|
||
nvidia_driver_switch_to_proprietary() {
|
||
echo -e "${YELLOW}$(nvidia_t WARN_HIGH_RISK)${NC}"
|
||
if ! confirm_action "安装并启用官方 NVIDIA 驱动(闭源)?"; then
|
||
return 0
|
||
fi
|
||
|
||
log_step "更新软件包列表..."
|
||
apt-get update -y >/dev/null 2>&1 || true
|
||
|
||
if nvidia_apt_has_pkg "nvidia-driver"; then
|
||
log_step "安装 nvidia-driver..."
|
||
apt-get install -y nvidia-driver
|
||
else
|
||
display_error "未找到可用的 nvidia-driver 软件包" "请检查软件源,或使用 NVIDIA 官方安装方式。"
|
||
return 1
|
||
fi
|
||
|
||
if confirm_action "安装完成,是否现在重启宿主机?"; then
|
||
reboot
|
||
fi
|
||
return 0
|
||
}
|
||
|
||
nvidia_driver_switch_to_open() {
|
||
echo -e "${YELLOW}$(nvidia_t WARN_HIGH_RISK)${NC}"
|
||
if ! confirm_action "卸载 NVIDIA 驱动并切回开源驱动(nouveau)?"; then
|
||
return 0
|
||
fi
|
||
|
||
log_step "卸载 NVIDIA 驱动..."
|
||
apt-get purge -y 'nvidia-*' || true
|
||
apt-get autoremove -y || true
|
||
|
||
if confirm_action "是否更新 initramfs(推荐)?"; then
|
||
update-initramfs -u || true
|
||
fi
|
||
|
||
if confirm_action "操作完成,是否现在重启宿主机?"; then
|
||
reboot
|
||
fi
|
||
return 0
|
||
}
|
||
|
||
nvidia_restore_latest_backup_file() {
|
||
local target="$1"
|
||
local backup_dir="/var/backups/pve-tools"
|
||
local base
|
||
base="$(basename "$target")"
|
||
|
||
if [[ ! -d "$backup_dir" ]]; then
|
||
return 1
|
||
fi
|
||
|
||
local latest
|
||
latest="$(ls -1t "${backup_dir}/${base}."*.bak 2>/dev/null | head -n 1)"
|
||
if [[ -z "$latest" ]]; then
|
||
return 1
|
||
fi
|
||
|
||
backup_file "$target" >/dev/null 2>&1 || true
|
||
if cp -a "$latest" "$target" >/dev/null 2>&1; then
|
||
log_success "已回滚: $target"
|
||
log_info "使用备份: $latest"
|
||
return 0
|
||
fi
|
||
return 1
|
||
}
|
||
|
||
nvidia_driver_rollback() {
|
||
echo -e "${YELLOW}$(nvidia_t WARN_HIGH_RISK)${NC}"
|
||
if ! confirm_action "回滚最近一次驱动相关配置备份?"; then
|
||
return 0
|
||
fi
|
||
|
||
local files=(
|
||
"/etc/modprobe.d/pve-blacklist.conf"
|
||
"/etc/modprobe.d/kvm.conf"
|
||
"/etc/modprobe.d/pve-tools-nvidia-vfio.conf"
|
||
"/etc/modprobe.d/vfio.conf"
|
||
"/etc/default/grub"
|
||
"/etc/nvidia/gridd.conf"
|
||
)
|
||
|
||
local ok=0
|
||
local f
|
||
for f in "${files[@]}"; do
|
||
if nvidia_restore_latest_backup_file "$f"; then
|
||
ok=$((ok+1))
|
||
fi
|
||
done
|
||
|
||
if [[ "$ok" -le 0 ]]; then
|
||
display_error "未找到可用备份" "请确认之前确实产生过备份(/var/backups/pve-tools),或手工回滚配置。"
|
||
return 1
|
||
fi
|
||
|
||
display_success "回滚完成" "建议执行 update-initramfs -u,并按需重启宿主机。"
|
||
return 0
|
||
}
|
||
|
||
nvidia_driver_switch_menu() {
|
||
while true; do
|
||
clear
|
||
show_menu_header "$(nvidia_t OPT_DRV_SWITCH)"
|
||
echo -e "${YELLOW}$(nvidia_t WARN_HIGH_RISK)${NC}"
|
||
echo -e "${UI_DIVIDER}"
|
||
show_menu_option "1" "切换到闭源驱动(官方 NVIDIA)"
|
||
show_menu_option "2" "切换到开源驱动(nouveau)"
|
||
show_menu_option "3" "回滚最近一次备份"
|
||
show_menu_option "0" "$(nvidia_t OPT_BACK)"
|
||
show_menu_footer
|
||
read -p "$(nvidia_t INPUT_CHOICE) [0-3]: " choice
|
||
case "$choice" in
|
||
1) nvidia_driver_switch_to_proprietary ;;
|
||
2) nvidia_driver_switch_to_open ;;
|
||
3) nvidia_driver_rollback ;;
|
||
0) return ;;
|
||
*) log_error "无效选择" ;;
|
||
esac
|
||
pause_function
|
||
done
|
||
}
|
||
|
||
nvidia_gpu_management_menu() {
|
||
while true; do
|
||
clear
|
||
show_menu_header "$(nvidia_t MENU_TITLE)"
|
||
echo -e "${CYAN}$(nvidia_t MENU_DESC)${NC}"
|
||
echo -e "${UI_DIVIDER}"
|
||
show_menu_option "1" "$(nvidia_t OPT_PT)"
|
||
show_menu_option "2" "$(nvidia_t OPT_VGPU)"
|
||
show_menu_option "3" "$(nvidia_t OPT_DRV_INFO)"
|
||
show_menu_option "4" "$(nvidia_t OPT_DRV_SWITCH)"
|
||
show_menu_option "0" "$(nvidia_t OPT_BACK)"
|
||
show_menu_footer
|
||
read -p "$(nvidia_t INPUT_CHOICE) [0-4]: " choice
|
||
case "$choice" in
|
||
1) nvidia_gpu_passthrough_vm ;;
|
||
2) nvidia_vgpu_menu ;;
|
||
3) nvidia_driver_info_menu ;;
|
||
4) nvidia_driver_switch_menu ;;
|
||
0) return ;;
|
||
*) log_error "无效选择" ;;
|
||
esac
|
||
pause_function
|
||
done
|
||
}
|
||
|
||
# 主程序
|
||
main() {
|
||
check_root
|
||
ensure_legal_acceptance
|
||
check_debug_mode "$@"
|
||
check_pve_version
|
||
|
||
# 检查更新
|
||
check_update
|
||
|
||
# 选择镜像源
|
||
select_mirror
|
||
|
||
while true; do
|
||
|
||
show_menu
|
||
read -n 2 choice
|
||
echo
|
||
echo
|
||
|
||
case $choice in
|
||
1)
|
||
menu_optimization
|
||
;;
|
||
2)
|
||
menu_sources_updates
|
||
;;
|
||
3)
|
||
menu_boot_kernel
|
||
;;
|
||
4)
|
||
menu_gpu_passthrough
|
||
;;
|
||
5)
|
||
menu_vm_container
|
||
;;
|
||
6)
|
||
menu_storage_disk
|
||
;;
|
||
7)
|
||
menu_tools_about
|
||
;;
|
||
0)
|
||
echo "感谢使用,谢谢喵"
|
||
echo "再见!"
|
||
exit 0
|
||
;;
|
||
*)
|
||
log_error "哎呀,这个选项不存在呢"
|
||
log_warn "请输入 0-7 之间的数字"
|
||
;;
|
||
esac
|
||
|
||
echo
|
||
pause_function
|
||
done
|
||
}
|
||
|
||
# 运行主程序
|
||
main "$@"
|