#!/bin/bash # SPDX-License-Identifier: GPL-3.0-only # Copyright (C) 2026 Ciriu Networks setup_colors() { # NO_COLOR 遵循 no-color.org 规范:只要设置了该变量(即使为空值)即禁用颜色 if [[ -t 1 && -z "${NO_COLOR+x}" ]]; 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='' PINK='' 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 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}" } # 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 } confirm_high_risk_action() { local action_desc="$1" local risk_desc="$2" local impact_desc="$3" local backup_desc="$4" local confirm_word="${5:-CONFIRM}" echo -e "${RED}${UI_DIVIDER}${NC}" echo -e "${RED}高风险数据操作警告${NC}" echo -e "${YELLOW}操作:${NC} $action_desc" echo -e "${YELLOW}风险:${NC} $risk_desc" echo -e "${YELLOW}影响:${NC} $impact_desc" echo -e "${YELLOW}建议:${NC} $backup_desc" echo -e "${RED}请输入确认词 ${confirm_word} 继续,其他任意输入将取消。${NC}" echo -e "${RED}${UI_DIVIDER}${NC}" local confirm read -p "确认词: " -r confirm if [[ "$confirm" == "$confirm_word" ]]; then return 0 fi log_warn "未通过高风险确认,操作已取消。" return 1 } vm_show_data_risk_banner() { # 每会话仅完整展示一次,避免每次重绘菜单都整屏刷同一段风险横幅 if [[ "${VM_RISK_BANNER_SHOWN:-0}" -eq 1 ]]; then return 0 fi VM_RISK_BANNER_SHOWN=1 echo -e "${RED}${UI_DIVIDER}${NC}" echo -e "${RED}以下操作将直接改写 VM 配置、磁盘、快照、克隆、恢复或迁移状态。${NC}" echo -e "${RED}本软件会执行您要求它做的事情,不会替您判断对错。${NC}" echo -e "${RED}您已被告知风险,请确保备份就绪、窗口明确、命令理解到位。${NC}" echo -e "${RED}如果心存疑虑,请停下来——数据无价。${NC}" echo -e "${RED}恢复参考: https://pve.u3u.icu/advanced/data-recovery-after-mistake${NC}" echo -e "${RED}${UI_DIVIDER}${NC}" } LEGAL_VERSION="1.1" LEGAL_EFFECTIVE_DATE="2026-04-05" 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 "继续使用本脚本即表示您同意以下条款:" echo -e " ${CYAN}ULA${NC}(最终用户许可与使用协议): https://pve.u3u.icu/ula" echo -e " ${CYAN}TOS${NC}(服务条款): https://pve.u3u.icu/tos" echo echo -e "${RED}本软件会做您明确告诉它要做的事情,无论那件事情多么荒谬或具有破坏性。${NC}" echo -e "${RED}您已被告知所有风险,并已独立决定使用本软件。${NC}" echo -e "${RED}从此刻起,您与您的数据之间唯一的屏障就是您自己的谨慎与备份策略。${NC}" echo echo -e "${YELLOW}最后提醒:如果您仍然心存疑虑,请不要使用本软件。${NC}" echo -e "${YELLOW}世界上有许多带有商业支持、附带责任保险的成熟 PVE 管理工具,您可以考虑购买它们。${NC}" echo echo -e "${YELLOW} 撤回同意: 删除 ${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_effective_date=${LEGAL_EFFECTIVE_DATE}" "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 result_var="${2:-}" local backup_dir="/var/backups/pve-tools" if [[ -z "$file_path" ]]; then log_error "backup_file: 缺少文件路径参数" return 1 fi if [[ ! -f "$file_path" ]]; then log_warn "文件不存在,跳过备份: $file_path" return 1 fi mkdir -p "$backup_dir" >/dev/null 2>&1 || { log_error "无法创建备份目录: $backup_dir" return 1 } local filename timestamp backup_path filename="$(basename "$file_path")" timestamp="$(date +%Y%m%d_%H%M%S)" backup_path="${backup_dir}/${filename}.${timestamp}.bak" if cp -a "$file_path" "$backup_path"; then [[ -n "$result_var" ]] && printf -v "$result_var" '%s' "$backup_path" log_success "文件已备份: $backup_path" return 0 fi log_error "备份失败: $file_path" return 1 } pve_tools_download_url() { local url="$1" local timeout="${2:-15}" if [[ -z "$url" ]]; then return 1 fi if command -v curl >/dev/null 2>&1; then curl -fsSL --connect-timeout "$timeout" --max-time "$timeout" "$url" 2>/dev/null elif command -v wget >/dev/null 2>&1; then wget -q -T "$timeout" -O - "$url" 2>/dev/null else return 1 fi } pve_tools_download_file() { local url="$1" local output_file="$2" local timeout="${3:-60}" if [[ -z "$url" || -z "$output_file" ]]; then return 1 fi if command -v curl >/dev/null 2>&1; then curl -fsSL --connect-timeout 15 --max-time "$timeout" -o "$output_file" "$url" 2>/dev/null elif command -v wget >/dev/null 2>&1; then wget -q -T "$timeout" -O "$output_file" "$url" 2>/dev/null else return 1 fi } pve_tools_choose_update_urls() { local prefer_mirror=0 local version_url="$VERSION_FILE_URL" local update_url="$UPDATE_FILE_URL" local script_url="$PVE_TOOLS_SCRIPT_URL" if [[ -n "$USER_COUNTRY_CODE" ]]; then prefer_mirror=$USE_MIRROR_FOR_UPDATE elif detect_network_region >/dev/null 2>&1; then prefer_mirror=$USE_MIRROR_FOR_UPDATE fi if [[ "$prefer_mirror" -eq 1 ]]; then version_url="${GITHUB_MIRROR_PREFIX}${VERSION_FILE_URL}" update_url="${GITHUB_MIRROR_PREFIX}${UPDATE_FILE_URL}" script_url="${GITHUB_MIRROR_PREFIX}${PVE_TOOLS_SCRIPT_URL}" fi printf '%s|%s|%s|%s\n' "$prefer_mirror" "$version_url" "$update_url" "$script_url" } pve_tools_version_gt() { local newer="$1" local current="$2" [[ -n "$newer" && -n "$current" ]] || return 1 # Strip pre-release suffixes (-beta, -alpha, -rc) so sort -V compares only # the numeric version; a pre-release is never newer than its base release. local newer_clean="${newer%%-*}" local current_clean="${current%%-*}" [[ "$(printf '%s\n' "$newer_clean" "$current_clean" | sort -V | tail -n1)" == "$newer_clean" && "$newer_clean" != "$current_clean" ]] } # 写入配置块(带标记) # 用法: apply_block 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 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_CMDLINE_LINUX_DEFAULT 是否已含指定参数(按 key 精确匹配,不受注释/其他行干扰) # 用法: grub_has_param "intel_iommu=on" 或 grub_has_param "intel_iommu" grub_has_param() { local param_key="${1%%=*}" local current_line current_params current_line=$(grep '^GRUB_CMDLINE_LINUX_DEFAULT=' /etc/default/grub 2>/dev/null) || return 1 current_params=$(echo "$current_line" | sed 's/^GRUB_CMDLINE_LINUX_DEFAULT="\(.*\)"$/\1/') local -a items=() read -r -a items <<< "$current_params" local item for item in "${items[@]}"; do [[ "${item%%=*}" == "$param_key" ]] && return 0 done return 1 } # 添加 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('=' 前部分), # 避免旧正则 \b$key[^ ]* 在 key 含 '.' 等元字符时的前缀误伤/误删 local param_key="${param%%=*}" local -a items=() kept=() read -r -a items <<< "$current_params" local item for item in "${items[@]}"; do if [[ "${item%%=*}" != "$param_key" ]]; then kept+=("$item") fi done kept+=("$param") local new_params="${kept[*]}" # 写回配置文件 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 "intel_iommu" 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/') # 数组化精确删除:按 key 匹配(兼容传入 "key" 与 "key=value" 两种形式) local param_key="${param%%=*}" local -a items=() kept=() read -r -a items <<< "$current_params" local item for item in "${items[@]}"; do if [[ "${item%%=*}" == "$param_key" ]]; then continue fi kept+=("$item") done local new_params="${kept[*]}" # 写回配置文件 sed -i "s|^GRUB_CMDLINE_LINUX_DEFAULT=.*|GRUB_CMDLINE_LINUX_DEFAULT=\"$new_params\"|" /etc/default/grub log_success "GRUB 参数已删除: $param" } # ============ GRUB 参数幂等管理函数结束 ============ # 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 } pause_function() { echo -n "按任意键继续... " read -n 1 -s input if [[ -n ${input} ]]; then echo -e "\b " fi } #--------------开启硬件直通---------------- # 开启硬件直通 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 } # 镜像源选择函数